- Attach up to eight supported files directly to an initial or later turn. OAO copies their original bytes into the agent sandbox. See Send files to an agent for media types and limits.
- Put a small, trusted text excerpt directly in
initialMessageor a later turn. The complete message must be no more than 100,000 JavaScript characters. - Publish narrow
callertools such asread_repository_fileandsearch_repository, then run those tools in your own server process against one allowlisted repository root.
Create a server-side API key
Open API keys in the console, select Create API key, enter a descriptive name, and grant only the scopes the integration needs. Save the secret from the one-time acknowledgement dialog before closing it. For automated provisioning, create the integration key through the API using an already-authenticated human or service principal withproject:admin:
oao_... secret once. Copy it into
the integration’s secret store immediately. OAO stores only a keyed hash and
cannot reveal the plaintext again. See Authentication
for browser-cookie authentication, tenant authorization, and key handling.
Grant the smallest set of scopes for the process you are running:
The complete SDK example uses every scope except
run:cancel. An operator can
create the agent separately and remove agent:write from the long-lived worker
key afterward.
Store the key only in the server environment:
OAO_API_URL is the API origin, without /v1, when using @oao/sdk-js.
External HTTP examples below add /v1 themselves.
Every mutation also needs an Idempotency-Key. Persist a key before the first
attempt, and reuse that same key and identical JSON body when the outcome is
uncertain. A replay returns the original public response. Reusing the key for a
different body returns 409 idempotency_conflict.
Pattern 1: attach a small trusted text file
Read the selected file on your server and send canonical base64. The API stores the original bytes durably with the run, verifies them by SHA-256 at dispatch, copies them into the sandbox, and exposes only safe metadata in transcript reads. It does not inject the file content into the model prompt:Pattern 2: inline a small trusted text excerpt
Read and review the text on your server before including it. Reject binary data and secrets, add a clear path label, and check the final assembled message rather than only the file length:Pattern 3: run allowlisted repository tools
The following private-workspace example creates one agent, creates its first session atomically withinitialMessage, handles resumable SSE, services two
caller-owned tools, and optionally submits one later turn after the previous
run settles.
Add the private SDK to an OAO workspace package that owns the integration:
private: true; this install form is for packages in this
monorepo. Save the following as src/codebase-agent.ts in that package and run
it with the package’s TypeScript runner.
Why the file boundary is strict
The example applies all of these checks before returning content:- Resolves one configured repository root to a canonical absolute path.
- Rejects absolute paths, NUL bytes, traversal outside the root, denied directories, secret-like filenames, key/certificate extensions, and unknown text extensions.
- Rejects every symbolic-link component and verifies the canonical result is still under the root.
- Opens only regular files, caps each read at 256 KiB, rejects NUL-containing or invalid UTF-8 data, and applies a conservative secret-content check.
- Treats search as literal text, caps files inspected, matches returned, and excerpt length, and skips unreadable or disallowed files.
- Returns generic failure text instead of exception messages, absolute paths, file metadata, or stack traces.
Claim leases, fences, and results
A caller tool starts atcaller_pending. The worker claims it for a lease from
1,000 through 300,000 milliseconds and receives a positive-integer string
fence. Renew and release operations must use the current fence. The example
renews a 60-second lease every 20 seconds and stops the heartbeat before result
submission.
Submit the fence with exactly one safe, versioned result envelope:
value must match the published outputSchema. A safe failure uses
status: "failure" with one of the current public failure codes and a generic
message. Never put credentials, raw tool payloads, absolute host paths, stack
traces, or raw model reasoning in safeResult.
Use one idempotency key per logical claim, renewal, release, or result. Retry an
uncertain request with the same key and body. A tool result is immutable: the
same result key and body returns outcome: "replayed"; a different key or body
conflicts. If a lease expires or its fence becomes stale, do not submit anyway.
Let the work be reclaimed and re-executed under the new fence.
Make production tool processing crash-safe
Persist one operation record pertoolCallId in your own durable store. At a
minimum it needs phase, claimIdempotencyKey, the claimed fence, the exact
result envelope (or its canonical bytes and hash), and
resultIdempotencyKey. Update this record before each network request:
- Generate and persist the claim key and claim body, then call
claim. - Persist the returned fence before executing the handler.
- For an effectful handler, pass a stable downstream idempotency key derived from the tool-call identity; a lease fence prevents stale OAO commits but cannot undo an external effect that already started.
- Persist the final safe-result body and result key before submitting it.
- Mark the operation complete only after OAO accepts or replays that exact result.
Resume the event stream durably
GET /v1/projects/{projectId}/events is project-wide. Each SSE frame’s id is
an opaque cursor backed by committed PostgreSQL position. Persist it only after
your application has applied the event, then reconnect with:
reconnect: true. It waits
reconnectDelayMs (1 second by default) between connections, or an SSE retry
value if the server sends one. The server closes a normal stream about every 25
seconds; that is not data loss. It also writes : comment lines when the
stream opens and after 10 seconds of silence, so proxies keep the connection
alive. Parsers must skip those lines. PostgreSQL is authoritative, while
LISTEN/NOTIFY only wakes readers.
Expect duplicate events after a crash between applying an event and saving its
cursor. Make your projection idempotent by event id or by
(aggregateType, aggregateId, aggregateSequence). Re-fetch the run or session
when an event kind is unknown. The example always re-reads the target run and
lists durable tool calls, so an SSE frame is a notification rather than the
sole copy of state.
On SIGINT or SIGTERM, abort the stream and any outstanding HTTP request,
stop lease renewal, finish the state-file rename, and exit. On restart, load
the saved cursor and any persisted mutation key before making another request.
If a process dies while holding a tool claim, its lease expires and another
worker can claim the durable work.
External repository: use HTTP directly
Because@oao/sdk-js is not currently published, a server outside the OAO
monorepo should use the HTTP and SSE contracts. This complete Node.js 22
example uses the inline-text pattern, creates a session, watches its first run,
and sends one optional follow-up only after settlement. Use it with an agent
that has no caller-owned tools.
Keep the same repository executor, result envelope, lease heartbeat, fence, and
idempotency rules. Do not weaken the boundary because the worker lives in a
different repository.
Failures and recovery
Run failures are durable terminal outcomes, not transport exceptions. Read the
run, session transcript, safe timeline, and public request ID before deciding
whether to submit a new turn.
POST /runs/{runId}/resume exists for a settled
run and creates another run in its session, but it does not continue an
unsettled execution or bypass the one-active-run invariant.
Authorized session views include provider thinking and tool arguments/results.
Public events and application logs still omit that content, credentials,
authorization headers, and provider reasoning signatures.

