Skip to content

Migrating to your own repository ​

The production example runs a complete read → approve → edit → test workflow against a disposable Git fixture, and its smoke verifies approval, denial, recovery after the Worker is SIGKILLed, SSE reconnection, and cancellation. This page explains what to change when you move that workflow onto your repository, your sign-in system, and your retention policy.

Read the production section of Golden Paths first, and confirm pnpm verify:production-example passes locally.

Understand the example's security model first ​

The example keeps its capabilities deliberately narrow. That is the point of the layer, not a demo shortcut: every tool in RepositoryTools.mjs constrains model input to argv or stdin, while the programs themselves are fixed strings.

MechanismWhat the example doesWhy keep it
Path allowlistRepoRead accepts only src/greeting.sh and test/greeting.test.shThe model cannot name a path outside the list, and symlinks are rejected explicitly
Pre-write checkRepoWrite requires expected_content to match the file byte for bytePrevents overwriting someone else's commit — an optimistic lock
Trusted testsRepoRunTests compares the test file with the expected content before running itOtherwise the model could edit tests to make them pass
No networkThe Docker container starts with mode: 'none'Repository code cannot reach the network

The usual migration mistake is relaxing these rules to make the example "work with my repository". Map them onto your real constraints first, then widen.

What the SDK owns, and what you own ​

The production starter copies the example modules into your project, so it helps to know where the boundary sits: the recovery semantics come from the SDK, and the copied code only encodes your repository policy. Do not re-implement the guarantees the SDK already provides.

ConcernOwnerNotes
Route queue, Session lease, fencing tokenSDKAgentServer + AgentWorker + Runtime Store
Post-crash recovery plan (model outcome, tool outcomes, pending approvals)SDKDurableSessionRecoveryCoordinator; the example only supplies the policy in RepositoryRecovery.mjs
Workspace checkpoints and restoreSDKDockerExecutionHost checkpoint / restore / reclaim
Crash boundary and recovery statePostgreSQLThe launcher caches none of it: route state, fencing token, and the committed checkpoint are read from the store
Submission and terminal-event reconciliationYour projectRepositoryState.mjs + RepositoryReconcile.mjs are your own queue tables and catch-up policy. The example records acceptance before enqueueing, rebuilds a lost acceptance record from the durable journal projection (never from the transcript projection, which may be missing that input entirely), and republishes a recorded terminal result only after the route settles for the same request and attempt. A journal longer than one replay budget falls back to its tail: an accepted-but-unenqueued request is always the last durable event, so long Sessions recover too
Idempotent terminal publicationSDKappendEvent(..., { idempotencyKey }) stores one event when a Worker and a reconciler publish the same terminal result concurrently, with no read-then-append scan
Durable journal projectionSDKprojectDurableSession reconstructs an accepted request from the journal, the authority for accepted input
Tool allowlists and approval policyYour projectRepositoryTools.mjs and the approval records in RepositoryState.mjs
Smoke assertionsYour projectsmoke.mjs asserts against the example fixture; rewrite it for your repository

RepositoryDemoProvider.mjs only serves the smoke run's deterministic model output and can be deleted once you use a real model.

Step 1: Point it at your repository ​

The example copies a fixture into a temporary Git repository at startup. Replace that block in run.mjs:

js
// Before: copy a disposable fixture
const repositoryPath = join(temporaryRoot, 'repository');
await cp(join(root, 'fixture'), repositoryPath, { recursive: true });
await execFileAsync('git', ['init', '--quiet', repositoryPath]);

// After: clone a real repository; never run the Agent on a working copy
const repositoryPath = join(temporaryRoot, 'repository');
await execFileAsync('git', ['clone', '--quiet', '--depth', '1',
  process.env.AGENT_REPOSITORY_URL, repositoryPath]);

Keep in mind:

  • Always run the Agent on a clone or worktree, never on a developer's working copy. Checkpoint recovery replaces file content, so a working copy would lose local changes.
  • Use a deploy key or short-lived credential for private repositories. That credential is only for git clone; keep it out of the container environment.
  • To pin a revision, run git -C <dir> checkout <sha> after cloning and record that SHA, so you can tell later exactly what the Agent changed.

Step 2: Rewrite the tool allowlists ​

All three fixed programs in RepositoryTools.mjs need to match your repository:

  1. The case branches in READ_FILE: list the paths the Agent may read. Reading an entire repository is rarely realistic; start with the files it actually needs, such as build scripts, target sources, and configuration.
  2. The first check in WRITE_FILE: replace test "$1" = src/greeting.sh with your writable path set. Allow source files only and keep test files and CI configuration out of it.
  3. RUN_TESTS: replace sh test/greeting.test.sh with your real test command and keep the content comparison that precedes it.

Once the allowlist grows, read it from one place instead of scattering it through shell strings:

js
const allowedWrites = new Set(['packages/api/src/handler.ts', 'packages/api/src/schema.ts']);
const allowedReads = new Set([...allowedWrites, 'package.json', 'pnpm-lock.yaml']);

Update the tool descriptions too, or the model will keep guessing at the example's paths:

js
defineTool({
  name: 'RepoWrite',
  description: 'Replace an allowed source file after approval.',
  // ...
});

Step 3: Connect your own sign-in ​

The example authenticates with a single shared token:

js
authenticate(request) {
  if (request.headers.get('authorization') !== 'Bearer local-demo') return null;
  return { tenantId, subject: 'browser-user', scopes: ['session:admin'] };
}

The principal returned by authenticate decides three things: tenantId isolates storage and events, subject participates in approval isolation, and scopes authorize commands. Keep that shape when you plug in real authentication:

js
authenticate(request) {
  const claims = await verifySessionCookie(request.headers.get('cookie'));
  if (!claims) return null;
  return {
    tenantId: claims.organizationId,
    subject: claims.userId,
    scopes: scopesForRole(claims.role),
  };
}

Boundaries worth knowing:

  • Do not hand session:admin to ordinary users. It satisfies every scope. Split it by role into session:create, session:read, session:write, and permission:resolve (session.fork needs both session:read and session:create).
  • Approvals are isolated by tenant, Session, subject, and permissionRequestId. If a different person clicks Approve in the same Session, the request does not resolve. That is intentional.
  • Cross-tenant access always returns SESSION_NOT_FOUND instead of disclosing whether the Session exists, so do not debug it as a missing Session.

Step 4: Decide retention ​

The example deletes its temporary database and checkpoints on exit. Production is the opposite: decide what you keep and for how long.

DataExample behaviorProduction guidance
Routes, events, approvals, transcriptsPostgreSQL, removed with the containerA dedicated instance with backups; partition event tables by time
Workspace checkpointsLocal checkpointDirectoryShared storage or controlled object storage, or cross-host recovery fails
The Agent's Git changesLeft in the temporary repositoryHave the Agent push a branch or emit a patch for review before merge
Docker workspace volumesRemoved with the containerDo not retain: cheap to rebuild and prone to leaving sensitive content behind

The PostgreSQL schema version is 3 and initialize() migrates under a global advisory lock. Read the schema section of Runtime Store before upgrading the SDK.

Local checkpoints only restore on the same host. Multi-replica deployments need a shared ExecutionHost implementation or controlled checkpoint upload; treating a local checkpoint ID as a distributed source of truth fails as soon as recovery lands on another machine.

Step 5: From smoke to real use ​

smoke.mjs currently asserts the fixture's fixed output (Tests passed (exit 0).). That will fail against a real repository, which is useful: it tells you the acceptance criteria changed. Suggested path:

  1. Keep the smoke's flow assertions (approval happens, recovery after the Worker is killed, SSE reconnection, cancellation works) and replace the content assertion with your own verdict, such as "the named test file goes from red to green".
  2. Run --smoke first with the deterministic provider to validate the wiring, then set OPENAI_API_KEY to switch to a real model.
  3. Be explicit about one limitation before launch: the example runs a single API process and provides neither API failover nor exactly-once arbitrary tool execution. When a test run is interrupted with an unknown outcome, stop and reconcile instead of retrying automatically.

Migration checklist ​

Released under the MIT License.