Skip to main content
Glama
danielnguyen

personal-agents-bridge

by danielnguyen

Personal Agents Bridge

A single-owner demonstration of ChatGPT → MCP → Agents API → self-hosted Codex → sandboxed Git worktree → independent review → exact-tree draft PR publication.

ChatGPT invokes eight MCP tools through Secure MCP Tunnel. By default the bridge creates Agents API sessions and connects a local Codex executor. For an allowlisted repository, implementation happens in an isolated Git worktree backed by a task-private Git store. Command networking is disabled; writes are confined to the task worktree and task scratch. Implementation agents cannot push or merge.

A separate session independently reviews bounded controller evidence. Only after a completed PASS can publish_task freeze the task and publish the exact reviewed Git tree through the trusted controller. The human retains the merge decision.

This is a public source demonstration, not a multi-user production service or an official OpenAI project. Repository sandboxes permit broad reads; use a dedicated host/account without unrelated secrets. See SECURITY.md and architecture for the actual guarantees and limits. Publishing this source does not make its private MCP deployment publicly accessible.

Opt-in Local Codex controller slice

start_task accepts optional execution_backend: omitted or agents_api retains the existing default; local_codex explicitly opts into subscription-authenticated execution for an allowlisted repository_id only. Selection is immutable and part of request deduplication. Omitted/explicit default selections keep the legacy fingerprint representation, including retries of pre-integration tasks.

The Controller remains the sole authority. Both paths share registration checks, pinned base commits, task-private Git stores/worktrees, contracts, baseline capture, SQLite task records, request IDs, ownership, audit and expiration. Local tasks receive a local-policy TASK.md and the same contract inline, not an assertion of the Agents API repository sandbox. They never create Agents API sessions. The API client is constructed lazily; local-only controller operation does not require an inference OPENAI_API_KEY. Existing tunnel authorization credentials are still independently required and unchanged.

This slice supports start → get → native human response → same turn → independent validation/review → qualified exact-tree draft publication → cleanup. get_task reports persisted execution_backend and implementer.local observations: phase/execution state, thread/turn IDs, model, submission-attempt/acknowledgement flags, terminal observation, bounded diagnostics and human-attention requirements. Codex IDs are not Agents API session IDs. Latest bounded output is labeled model; command counts are not certified command/test evidence. A completed turn is not a test PASS.

Synchronous lifecycle callbacks save identity before the next inference step. Submission intent is recorded before dispatch; absence of an acknowledgement is not proof of delivery or non-delivery. After restart unfinished local records require attention, with known IDs retained and no automatic replay. Completed records are retained. A process surviving a controller crash is not automatically reattached or killed: unknown ownership/termination remains a cleanup blocker.

Bounded review packaging and explicit pre-execution retry

Local contracts may optionally select reference_files, for example ["browser_trust_inspector.py", "README.md"], separately from allowed_files. The Controller captures only those unchanged UTF-8 regular files from the pinned baseline Git tree: at most four files and 32 KiB total. Paths must be repository-relative, unique, outside protected paths and disjoint from edit scope. Missing, non-text, symlinked, modified, oversized or secret-redacted required references fail closed. Admission checks baseline availability before execution. Review and publication checks bind baseline commit/tree, blob IDs, content SHA-256, lengths and modes and compare the candidate reference files against the baseline again. Context is untrusted source data, not reviewer instructions, runtime proof or independent validation. Existing contracts select no files implicitly; Agents API contracts do not support this optional field. The whole packet still has the same 128 KiB bound.

Native notification retention keeps the existing 24,576-byte/64-record budgets, but optional command output now retains at most 256 characters per event instead of 2,048. Missing, malformed, redacted, truncated and upstream-unverified output remain explicit; output-dependent requirements may still FAIL. Required command, identity, lifecycle, exit and file-operation fields are not reduced. Genuine essential-evidence overflow still increments omitted_events and blocks SCOPE. Previously omitted events cannot be reconstructed and historical FAILs are unchanged.

Local evidence remains limited to 131,072 UTF-8 JSON bytes. When a full packet overflows, the Controller may encode repeated complete-file patch bodies as pab.exact-diff-references.v1 references into the existing baseline/current maps. Literal fragments retain all patch headers, modes, paths and unsupported/binary markers. File hashes and the original patch hash/length accompany the references; construction must reconstruct the exact retained patch before accepting the packet. The reviewer contract explains the encoding, and Controller findings validation rejects invalid encodings. No native execution, independent validation, provenance, scope or Git evidence is removed. This is lossless for the already sanitized packet, not a claim that redacted source or notification history has become complete. Unmatched hunks stay literal; still-oversized packets fail EVIDENCE_SIZE_LIMIT. Agents API packaging is unchanged.

An owner may explicitly request review_task again with a new request ID after an EVIDENCE_SIZE_LIMIT failure, but only for a still-live, uncleaned local task whose stored flags prove no reviewer instance or submission occurred. Prior validation must have completed with confirmed termination and an unchanged original; no reviewer/validation job or owned process may remain. The failed operation, reviewer, validation attempt and packet diagnostic are retained. The candidate Git state must match exactly, and independent validation runs anew before a fresh reviewer starts. Same-ID retries never rerun anything, including after restart. Persisted stop intent from graceful shutdown is not reversed by retry. Uncertain submissions, started reviewers, other failures, expiration and cleanup do not gain retry authority. Authorization still uses the existing single-owner MCP boundary, not a new human identity claim. Snapshot/change-and-restore races remain the existing bounded-model limitation; no immutable freeze or broader recovery mechanism is introduced.

Recovering an already cleaned engineering task

The retained Browser Trust Inspector task cannot be revived by this correction. Keep its workspace, contract, validation results and reviewed tree untouched. Do not edit SQLite, reverse cleanup, extend its deadline or reuse its request IDs. Repository tasks reject initial_files; there is no import-for-review API.

After independent review, merge and separately authorized deployment of this fix, the smallest existing-interface recovery is a new explicitly authorized local task against the same registered repository and original pinned base_ref. Its bounded contract must authorize read-only access to the exact retained source files and reproduction of only the original allowed files in its new worktree (including any required deletions), preserve the original requirements/invariants and exact validation commands, and prohibit reimplementation or edits to the old task. Owner authorization must cover that read access and the new implementer turn; do not expose the old database, prompts, credentials, .git, TASK.md or unrelated task state as seed material. If the original obligations cannot truthfully apply to reproduction, stop for an explicit contract decision rather than silently weakening them. No promise is made that an arbitrary contract can recover this way.

Require the new candidate tree to equal the retained reviewed tree 9497d776d724de9347ec5bfc5118cce507575a96 before authorizing independent review. Then use review_task with a new ID for fresh Controller validation and a new independent reviewer. Old test results remain historical evidence only. Tree equality does not transfer implementation history or a PASS verdict. Any mismatch or expired new task stops the recovery; do not repair hashes or auto-retry. Publication, if a genuine qualified PASS results, needs separate authorization. No recovery execution or production mutation is part of this correction PR.

Durable native human requests

Command/file approvals and item/tool/requestUserInput requests remain pending in the original live turn. The synchronous lifecycle callback saves each request in the existing SQLite task record before exposing it. Raw JSON-RPC request IDs and accepted response contents remain private. MCP exposes an opaque request_ref, task/thread/turn IDs, request method, bounded description, expected response type, available decisions or exact question IDs/options, status and delivery diagnostics. Model prose never creates an authoritative pending request.

get_task returns waiting_for_approval or waiting_for_clarification, with human_attention_required: true and implementer.pending_human_requests. Obtain the human's explicit response, then call the existing continue_task:

{
  "task_id": "task_<returned UUID>",
  "request_id": "unique_human_response_operation",
  "human_response": {
    "request_ref": "<returned request UUID>",
    "thread_id": "<returned thread ID>",
    "turn_id": "<returned turn ID>",
    "decision": "accept"
  }
}

Approvals require accept, decline or cancel (also constrained by native available decisions); prose such as "yes" is not a decision. For clarification, replace decision with "answers":{"<exact question ID>":{"answers":["human answer"]}}. Every question must be answered, with no extra IDs. Advertised options are required unless the question allows free text. Do not send instruction for local replies; Agents API continuation still uses its existing instruction input unchanged.

The Controller records the response, original native identity and operation fingerprint atomically before releasing the callback. The adapter sends exactly one JSON-RPC response to that request, not another turn/start or thread. Identical operation retries return current state without redelivery; conflicting input or a new operation answering an already handled request fails. Responses cannot change task ownership or thread/turn binding. Waiting never extends the task deadline.

Delivery states distinguish durable preparation, attempted write, unconfirmed write, and server clearing. SQLite commit and pipe delivery are not atomic. A crash or transport/storage failure between them leaves uncertain delivery, never an automatic resend. Pipe writes are not remote acknowledgements. As documented by Codex app-server, serverRequest/resolved can mean answered or cleared; a clear before a human response invalidates the request, not an implicit approval. A server-side clear can also race a response already being written; no exactly-once remote-consumption guarantee is claimed. Only a real terminal observation completes execution.

Restart retains request identity and marks pending/intermediate delivery for attention; persisted JSON cannot recreate a native callback. Cleanup, expiration and shutdown invalidate requests and stop execution without waiting for human answers. Unsupported request types, secret-input questions, unrepresentable questions/options and overflow fail closed. Approvals with incomplete/redacted descriptions cannot be accepted (decline/cancel remain available). There are at most 32 requests per turn, eight questions per request and 20 options per question.

Post-completion/new-turn continuation and restart reattachment remain unsupported. Local review_task supports the bounded independent analysis described below. Local publish_task now requires the qualified independent PASS and exact-tree gates below. Human routing alone cannot authorize publication.

Pinned Codex 0.157.1 supports the item/tool/requestUserInput protocol request, but its blocking request_user_input tool is available in Plan mode, not Default mode unless default_mode_request_user_input is enabled. That feature is under development and disabled by default. PAB does not select Plan mode or enable that feature; its pinned feature subset does not attest every other effective feature. Therefore the protocol's existence is not proof that the default PAB turn can generate a live callback. See the pinned mode gate and feature default. The separate async-question tool emits agent-message items, not this JSON-RPC callback. PAB does not substitute those messages for native authorization. The retained acceptance had no native pending request; it cannot establish whether the model attempted an unavailable tool or used async messaging. Qualifying real human routing requires separately authorized mode/capability work and live exact-callback/turn continuity evidence. Native command/file approvals remain supported when actually requested; the harmless acceptance command did not trigger one. No capabilities are enabled by this correction.

Local evidence and independent analysis

The Controller synchronously retains bounded item/started and item/completed command/file-change notifications in task.local_execution_evidence in its existing SQLite store. Source task/thread/turn/item identity, command, cwd, status, exit code, output, missing fields, redaction/truncation and rejected/omitted event counts remain explicit. File-change claims are separate from controller-observed file bytes. Model prose, reasoning, human answers and raw notification logs are excluded. Final adapter summaries cannot reconstruct missing notification history.

Native aggregate output now retains distinct absent, null, empty, present and malformed availability. The pinned output projection can encode an empty aggregate as null. This explains why missing output need not mean failed execution, but PAB never converts absent/null into verified empty stdout or stderr. Optional output gaps stay in the notification ledger and the review result's scope_qualification.command_output_limitations, not fatal adapter diagnostics. Start/completion, identity, command/cwd, status and exit evidence remain mandatory; malformed/conflicting records and unrelated runtime uncertainty still block qualification. Truncation/redaction remains explicit. The reviewer must FAIL an output-dependent obligation without sufficient evidence; controller-owned independent execution remains mandatory for required tests. This neither generates PASS findings nor weakens exact-tree publication gates. Old acceptance records and FAIL reviews are not rewritten or automatically requalified; missing availability provenance in old command records fails closed.

The internal Controller.buildPacket(task) path can prepare a pab.local-codex-review.v1 packet after the local job is finished and the owned app-server is confirmed closed. The existing review_task uses this internal path; no new MCP operation is added. It reuses pinned-baseline/worktree validation, contract checks, controller before/after repository snapshots, current file hashes, unauthorized file detection and Git state checks. Agents API packet behavior is unchanged; native notifications never populate Agents API or file-RPC evidence structures.

Limits: at most 64 command/file records together and 24 KiB of record JSON; command text is bounded to 2048 characters and optional output to 256 characters, cwd to 1024, and file-change records to 16 entries with 1024-character paths/diffs. Overflow is counted, not silently treated as complete history. Packets share the existing 128 KiB UTF-8 limit and reject overflow without dropping required sections. Sensitive text uses existing controller redaction; evidence remains private operational data. Bounded output length/SHA-256 comparisons of UTF-8 JSON strings distinguish consistent start/completion prefixes from conflicts beyond the retained text. Truncation still does not imply independently verified output completeness. Reference reads pin Linux descriptors without following symlinks or opening special files for content, and recheck path identities. Reference Git checks authenticate raw commit, root/intermediate tree and blob bytes, not only the final blob. These checks do not establish a filesystem freeze or exhaustive historical compliance.

Required tests correlate only exact command strings at the approved cwd, not substrings or inferred shell wrappers. References to native records are not independently verified validation, even with exit code zero. Unmatched tests, incomplete starts/completions, missing output/exit codes, truncation and uncertain or interrupted turns remain visible. Old tasks without a ledger report unavailable history. Upstream output completeness and exhaustive command/file coverage remain unverified. Notifications cannot prove absence of prohibited operations.

Snapshots are sampled state, not a syscall/network audit or immutable freeze. Existing Git diff enumeration can omit ignored writes; the normal-checkout snapshot includes ignored files. App-server closure does not attest descendant termination, and concurrent external changes remain a possible capture race despite before/after Git checks. Evidence capture alone produces no independent PASS or publication authorization; validation, independent review and controller publication gates follow.

Bounded A/B acceptance at c11a3caa2abbb94380eb71579f07a5e4056bba97 observed four ChatGPT-authenticated turns, correct fixture bytes, independently passing validation and a separate terminal reviewer. The Controller overrode model PASS with SCOPE_EXECUTION_UNCERTAIN because a completed zero-exit write had unavailable output. That actual FAIL remains unchanged. Native test-command correlation was unmatched (shell wrapper); no native file-change event was retained. Human routing remains UNVERIFIED. This correction is offline-tested only: fresh qualified SCOPE acceptance and separately authorized exact-tree publication remain outstanding. No billing attribution guarantee or deployment authorization follows. No live turns are needed for tests:

node --test test/publication.test.mjs test/local-validation.test.mjs test/local-reviewer.test.mjs test/local-evidence.test.mjs test/local-codex-backend.test.mjs test/local-controller-integration.test.mjs
npm test
git diff --check

Independent local reviewer

After a confirmed completed implementation, review_task independently executes the persisted contract's required tests as described below, then freezes a bounded packet into a separate reviewer directory and starts a fresh LocalCodexBackend instance and thread. No implementer thread is resumed, no conversation history is supplied, and the implementer must already be closed. Reviewer mode requests native read-only sandboxing and command networking disabled at thread/turn creation, retains subscription authentication and unique-default model checks, and rejects writable scopes, existing thread history and resumption. Native reviewer human requests are not approved or routed in this slice; they interrupt analysis. These use the existing app-server protocol, not another isolation framework. Tests verify requested/returned policies using mocks, not independent live kernel enforcement.

The reviewer is instructed to read only evidence.json, not execute implementation tests, install dependencies, access unrelated contexts or use network services. Native read-only mode is not a narrow filesystem read boundary. Evidence-only read scope is behavioral; no OS-enforced read isolation from other host files or Codex history is claimed. Read-only instructions are not themselves enforcement.

The Controller owns the packet hash, reviewed Git tree/state, separate reviewer model/thread/turn identity, submission/acknowledgement and terminal observations. It verifies packet file type/mode/hash, task identity/contract, implementation tree and normal-checkout state before and after review. get_task rechecks integrity before exposing a retained result while the workspace exists. Mutations invalidate the result. Closing the owned server and checking sampled state is not proof of escaped-descendant termination or an immutable freeze against external writers.

The distinct local instruction contract requires all requirement/invariant IDs plus SCOPE and TEST_EVIDENCE. Strict JSON, exact IDs, nonempty bounded evidence, consistent overall verdict, 16 KiB output and 2000-character per-finding limits are validated; malformed/incomplete output yields needs_attention, not PASS. Findings remain model analysis, not independently certified truth. Controller overrides are explicit:

  • Required test commands cannot obtain TEST_EVIDENCE PASS from native matches, completion/exit zero or reviewer assertions. Only complete controller-owned validation, bound to the exact reviewed tree/state, can remove that failure floor.

  • SCOPE uses pab.local-scope.bounded.v1: validated registration/task identity and pinned baseline, unchanged normal checkout and protected TASK.md/.codex, unchanged Git index/refs/config/operation state, approved final file scope, and authenticated, acknowledged, terminal and closed local execution. Missing or contradictory provenance, unresolved safeguard uncertainty, rejected/omitted notifications and observed prohibited-operation indications fail qualification. Native command text is screened conservatively for Git mutation indications; this is not a universal shell parser or historical audit. The reviewer must assess every retained command/file record and each additional contract obligation.

  • Missing optional diagnostics do not invent additional tests or violations.

SCOPE may PASS under the accepted single-owner, trusted-repository dedicated-VM model, not as proof of exhaustive historical compliance or Agents API sandbox equivalence. Minimum gates only downgrade findings, never generate PASS or override the reviewer's FAIL on a requirement/invariant. Coverage limitations are retained in every review result, including PASS. A legitimate FAIL remains useful. The persisted review_result includes validated findings, controller overrides, model-proposed overall result, packet hash, bounded qualification and reviewed-tree/thread/turn binding, using pab.local-review.v2. Invalid, cancelled, incomplete, unacknowledged or uncertain execution cannot produce a review result.

Request retries never submit twice. There is one local reviewer attempt per task in this slice; a failed/uncertain attempt is not automatically replaced. Restart preserves completed findings and marks unfinished execution/validation for attention, without replay. There is no atomic transaction spanning app-server dispatch and SQLite persistence; missing acknowledgements remain uncertain. Cleanup, expiration and shutdown cancel/close both owned roles before any workspace deletion. Local post-completion implementation continuation remains unsupported.

Qualified local publication

publish_task uses the existing controller-owned publication.mjs path, not a second publisher. Both local roles must be completed, terminal, acknowledged and closed, with distinct implementer/reviewer threads and no active owned processes, unresolved human requests or unknown execution diagnostics. The three adapter coverage warnings (non-exhaustive upstream output, untrusted model prose, and no independent escaped-descendant attestation) remain explicit accepted limitations, not claims of exhaustive enforcement.

The Controller rechecks strict findings and their persisted digest, packet hash, reviewed tree/state, task/registration/normal-checkout identity, protected paths, and exact-tree independent validation including confirmed termination. Empty test lists still require a completed validation lifecycle. Legacy tasks lacking the new pre-execution protected snapshot or review binding cannot qualify retrospectively.

The shared publisher freezes implementation/review changes, uses only the fixed task branch, builds a commit from the reviewed tree, verifies tree identity, refuses default-branch or conflicting-ref updates, and creates only draft PRs. Retry input fingerprints and uncertain push/PR reconciliation remain unchanged; missing remote acknowledgements do not authorize duplicate PR creation. Local thread/turn identities are recorded without fabricating Agents API session IDs. Required tests must pass independently; a completed validation attempt or native exit zero is insufficient.

Snapshot checks cannot exclude external change-and-restore races, ignored artifacts, broad host reads or escaped descendants. A persisted findings digest detects changed records, not a compromised Controller/database owner. Existing immutable Git tree publication preserves exact reviewed bytes; it is not universal execution confinement. No live publication acceptance or deployment authorization is implied by these tests.

Controller-owned local validation

review_task owns one durable validation attempt before reviewer execution. No new MCP operation is added. The implementer must be terminal with its app-server closed. The Controller checks the registered worktree, contract and reviewedGitState(); it executes only exact test_commands from persisted contract.json, using /bin/sh -c. Every command starts from a fresh disposable copy of that Git tree's blob bytes and executable modes. No checkout filters, Git metadata, TASK.md, ignored files, external dependencies or prior test artifacts are copied. Symlinks, submodules, invalid paths, more than 2000 files or 16 MiB make capture unavailable. Dependencies must already be usable without installation; unavailable tools fail.

local-validation.mjs uses the same supported native codex sandbox permission table/profile mechanism as the repository sandbox, without its Agents API/RPC workers. CLI-pinned permissions allow writes only to the disposable candidate and scratch, protect .git/.codex, and disable command networking. HOME/CODEX_HOME are disposable; the child environment is an explicit allowlist with system PATH, no inherited API keys, tokens, credential helpers or agent sockets. There is no model execution or authentication. Host filesystem reads remain broad, as in the existing repository policy: this is not credential-read isolation. Use only trusted repositories/contracts on the dedicated host without unrelated secrets.

The task's SQLite local_validation record is exposed under get_task.reviewer.independent_validation and copied into the local packet's independent_validation section with version pab.controller-local-validation.v1. It retains the operation/task/attempt correlation, exact candidate tree and sampled Git state, per-command attempt and cwd identity, start intent/observations, completion, exit code/signal, timeouts, termination uncertainty, and redacted stdout/stderr. Output is capped at 2048 bytes per stream per command; truncation or redaction cannot qualify as verified passing evidence. At most ten 1000-character commands run, with a maximum 60-second process deadline each, also bounded by task expiration. Existing packet-size rejection remains in force; no evidence is silently discarded.

Results are synchronously saved before packet construction. Candidate and normal checkout snapshots are compared after execution; mismatch blocks reviewer startup. Absent or different-tree validation cannot satisfy TEST_EVIDENCE. Aggregate completed means the validation attempt finished, not that its tests passed. Only all required commands with observed start/completion, zero exit, complete output, unchanged original and confirmed termination can qualify. Test design and requirement sufficiency remain review questions; native notifications/model prose never supply independent results. SCOPE is qualified separately under the bounded contract above; independent test success cannot override a scope or requirement FAIL.

Cancellation kills the owned process group before waiting; missing close or live group members leave termination unconfirmed and block workspace deletion. This does not prove termination of deliberately escaped descendants. Test copies are retained for inspection until explicit task-workspace deletion. SQLite intent and OS spawn/exit cannot be atomic: a crash in between is uncertain, never replayed. Restart retains identity/results without killing guessed PIDs or reattaching; unconfirmed termination requires operator reconciliation. The review operation and one-attempt-per-task policy deduplicate validation as well as reviewer execution. Before/after snapshots cannot exclude external change-and-restore races; no new filesystem freeze, historical audit or security infrastructure is claimed.

Cleanup initiates cancellation before waiting on a local job, closes the owned app-server, and bounds each wait. Shutdown does the same. Files remain unless delete_workspace=true; deletion reuses the task worktree removal mechanism and is blocked when execution/closure is unresolved. Task/audit records survive. server_closed reports the owned server only; escaped-descendant termination is not attested. No app-server event is promoted to Agents API command/file-RPC evidence.

Local execution adapter

local-codex-backend.mjs remains a reusable adapter for trusted repositories on the owner's dedicated VM. Agents API remains the default backend; no deployment is included. It uses the installed codex app-server over stdio with no added dependencies. Codex Runner's SDK pattern is simpler for batch execution, but its adapter uses never approvals; app-server supplies the bidirectional native approvals needed here. No containers, namespace launchers or managed-policy framework are involved.

LocalCodexBackend({cwd, codexPath, codexHome, onApproval, onQuestion, onProgress, onLifecycle, reviewOnly}) requires an absolute, caller-approved workspace. connect() checks the existing ChatGPT login, subscription plan and effective OpenAI provider configuration without logging in, copying credentials or setting forced_login_method. API-key/custom provider routes and enabled configured MCP servers fail preflight. The child receives an explicit environment allowlist, not API keys/tokens/proxy overrides. Apps, plugins, hooks, web search and subagents are disabled. Existing authentication/config files are not edited by this module; Codex itself manages its ordinary state/token refresh.

Before each execution, model/list must return exactly one visible default across all pages. That model is recorded in result.model and explicitly sent to thread creation/resume and turn execution; the configured model is not a fallback. Resume first checks the stored thread model matches the selected default, and the thread response must confirm the same model. Missing/ambiguous catalogs, model mismatches and unavailable models fail without trying another model. This selects the catalog default, not proof of account entitlement or successful inference.

run({prompt, allowedFiles, threadId?, signal?}) starts a thread or resumes the explicit ID, reapplies workspace-write/on-request/user-review policy, disables command networking and returns completed, failed, interrupted or uncertain. File scopes containing TASK.md, .codex or .git path components are rejected before any task RPC, along with absolute paths and traversal. This validates the requested scope; it does not add a model-process filesystem enforcement boundary. Persist the returned thread ID in the caller; a new backend instance can resume it using the same Codex home. One turn per instance is allowed. There is no automatic retry of uncertain submissions. cancel() requests interruption; await the run() result for terminal confirmation. close() terminates the owned server process group, best-effort; escaped descendants and restart reconciliation are not independently attested. Always close in finally. close() returns whether the owned server's exit was observed (or no server was started), not a guarantee about escaped descendants.

onApproval(request) must obtain a human decision and return accept, decline or cancel; absent handlers decline. Session-wide grants are not accepted. onQuestion(request) returns {questionId:{answers:["answer"]}}. Unsupported permission/clarification requests interrupt rather than silently grant access. onProgress(event) is a synchronous observer of active-turn notifications. Requests cleared by Codex or interrupted turns cannot receive late approvals. onLifecycle(event) is synchronous: model selection, thread acknowledgement, pre-dispatch submission intent, turn acknowledgement, terminal observation and human requests. It exposes no account credentials; human request payloads remain private and must be bounded/redacted by the receiver. Throwing or returning a promise fails closed rather than allowing unsaved identity to advance. The Controller sets deferHumanRequests: true: supported callbacks remain in the adapter until respondToRequest({requestId, threadId, turnId, response}), cancellation or terminal completion. This synchronous method accepts only the original active native callback identity. Lifecycle events include the native ID, response dispatch and server clearing. Standalone onApproval/onQuestion behavior remains available when deferral is disabled; no pending controller callback promises are introduced.

Accepted limitation: native approvals do not intercept every in-sandbox action. File scope, asking before dependency changes/destruction/secret reads/network or LAN/Tailscale access, and prohibiting Git publication are explicit behavioral instructions, not universal pre-execution enforcement. Workspace-write is not PAB's existing per-path repository sandbox. Use only trusted repositories; publication must go through the Controller's qualified review gates, never the executor. Do not assert security equivalence.

Results retain command text, cwd, status, exit code, bounded output and native item provenance. Missing start/output/exit/completion and local truncation remain explicit; upstream evidence completeness is unverified. completed means the turn completed, not that tests passed. Model messages are separately labeled, never trusted test evidence. Keep results/events private: they can contain repository contents and paths. Authentication checks and token usage do not independently establish billing attribution.

Offline validation (no model turns):

node --test test/local-codex-backend.test.mjs test/local-controller-integration.test.mjs
node --test test/controller.test.mjs
npm test
git diff --check

The initial PR #5 controller slice recorded 61 focused, 17 controller, and 207 full offline tests, with 0 failures and 0 skipped tests in each run; git diff --check also passes. Validation uses injected adapters, never real model turns. Offline tests do not establish live authentication, billing attribution or controller live acceptance.

Operator-only fixture acceptance

The operator reports successful live fixture acceptance after the model-selection fix:

  • Model: gpt-6-astra.

  • Turn completed and terminal observed.

  • Fixture contents verified.

  • One validation command with successful execution evidence.

  • No reported failure codes.

These are operator-reported results, not a new live run by the coding agent. Subscription billing attribution remains independently unverified. This single fixture does not verify live resume, cancellation or security equivalence with PAB. Earlier attempts encountered CODEX_DISCONNECTED in the coding-agent environment and an operator-reported unavailable configured model (gpt-6.1-sol); the subsequent operator acceptance above establishes success for the bounded fixture, not those other environments or capabilities. No additional live turn is run for this closeout.

For future operator-authorized reproduction only, from this checkout in the owner's ordinary shell, the following creates a fresh disposable workspace and may consume Codex subscription allowance. It neither logs in nor runs PAB's paid live tests:

node --input-type=module <<'JS'
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
import { LocalCodexBackend } from './local-codex-backend.mjs';
const cwd = await mkdtemp('/tmp/pab-local-acceptance-');
const backend = new LocalCodexBackend({ cwd, timeoutMs: 120000, onApproval: async () => 'decline' });
try {
  console.log({ workspace: cwd, route: await backend.connect() });
  const command = `/usr/bin/python3 -B -c "from pathlib import Path; assert Path('answer.txt').read_text() == '42\\n'; print('PAB_LOCAL_OK')"`;
  const result = await backend.run({ allowedFiles: ['answer.txt'], prompt:
    `Use apply_patch to create answer.txt containing exactly 42 and a newline. Run exactly one command: ${command}. No other commands, unrelated reads, network, dependencies or Git. Report the actual result.` });
  await writeFile(`${cwd}/result.private.json`, JSON.stringify(result, null, 2), { mode: 0o600 });
  const fixtureMatches = await readFile(`${cwd}/answer.txt`, 'utf8').then(value => value === '42\n', () => false);
  const evidence = result.commands;
  const passed = result.status === 'completed' && result.terminalObserved && fixtureMatches && evidence.length === 1 &&
    evidence[0].exitCode === 0 && evidence[0].status === 'completed' && !evidence[0].missing.length &&
    !evidence[0].locallyTruncated && evidence[0].output?.includes('PAB_LOCAL_OK');
  console.log({ status: result.status, passed, fixtureMatches, commandCount: evidence.length });
  if (!passed) process.exitCode = 1;
} finally { await backend.close(); }
JS

Inspect the private command evidence as well as the fixture; do not publish raw logs or rerun an uncertain turn automatically. The reported live success is limited to the bounded fixture acceptance above. This acceptance was for the standalone adapter, not this controller integration. PR #6 adds offline-tested durable human-response routing to the same live turn; its live human-routing acceptance remains unverified. Uncertain delivery is not replayed. Local validation, bounded PASS qualification and exact-tree publication integration have offline coverage only; end-to-end live qualification remains outstanding. Existing Agents API gates and deployment authorization are unchanged.

Related MCP server: sark

Setup

Requires Linux, Node.js 24+, npm, Python 3, Git, flock, and standalone Codex with sandbox -P, exec-server, exec-server forward, and its sandbox runtime. The host must permit the required Linux namespaces and Landlock operations. Publication also requires /usr/bin/gh and controller-accessible GitHub authentication. Dependency versions are pinned in package-lock.json; Codex and the tunnel binary are external prerequisites. Protocol/kernel compatibility must be tested on your host.

npm ci --ignore-scripts
npm test

Tests use fake Agents API sessions, real local kernel sandboxes and local Git remotes. They need namespace creation and local listeners; missing enforcement fails tests rather than silently skipping them. They do not publish to GitHub. If Node reports a missing libatomic.so.1, install your OS compatibility package. run.sh also supports an operator-provided library under the private state directory.

Review .env.example for variable names; the application does not load dotenv files automatically. Supply secrets via the process environment or a private secret manager. Never put credentials in MCP arguments or committed files. Follow TUNNEL.md for the private tunnel, owner confirmation and self-hosted execution setup. Start the production bridge with ./run.sh only under the configured tunnel; its singleton lock prevents two controllers using one store.

Register a separate, explicitly approved repository locally (still supported):

node register-repository.mjs example /absolute/path/to/approved-repository

The command pins canonical path and filesystem/Git identity in an owner-only local registry. It rejects this bridge's source repository. Never register the bridge as its own implementation target. MCP callers use only repository_id: "example". No repository registrations or account configuration are shipped with this source.

Remote registration of owned GitHub repositories

After one-time deployment and trusted GitHub authentication, eligible registrations require no SSH session or per-repository host approval. Ask ChatGPT to discover registrations with list_registered_repositories, explain the requested registration and obtain your consent, then call:

{"repository":"danielnguyen/browser-trust-inspector"}

on register_github_repository. The result contains status, repository_id (github_<numeric GitHub repository ID>), numeric GitHub repository/owner IDs, canonical full name, visibility and a confirmed baseline commit. Separately authorize start_task with that returned ID, a complete contract, a unique request_id, and execution_backend: "local_codex". Registration never starts a task or changes the default Agents API backend. Discovery is read-only and returns no local paths; legacy local registrations may lack numeric GitHub metadata.

The Controller, not ChatGPT or an implementation agent, enforces authorization: GitHub's authenticated /user must be a user account whose numeric ID matches the repository owner's numeric ID and whose login matches the API repository identity. Accessible owned private repositories are supported. Organizations, collaborator-only access, missing access/identity and the PAB source repository are rejected. The source denylist pins this distribution's GitHub repository ID 1388046916 as well as its canonical name, so renaming that repository does not evade self-protection. Forked deployments must retain equivalent protection for their own source identity. The single-owner tunnel gate applies to both tools; conversational consent is not independently authenticated human identity. Every accepted caller has the owner's registration authority within this policy.

Repositories are provisioned beneath the fixed, private ~/.local/share/personal-agents-bridge/repositories, never a caller-selected path. Only generated HTTPS GitHub URLs are used. No URL, path, credential, Git option or shell command is accepted through MCP. The Controller obtains GitHub credentials through its existing gh login/environment; no credential value or helper setting is copied into a repository or task. Hooks/templates and global/system Git config are disabled during provisioning. Filters have no configured executable drivers; submodules and symlink entries are rejected rather than followed. No dependencies or repository code are executed. This is not a new confidentiality boundary: the existing broad-read limitations in SECURITY.md still apply.

Provisioning fetches the default branch history into a staged checkout, verifies the API-pinned commit, checks the tree before checkout, and rechecks GitHub ownership and identity afterward. The existing version-1 registry retains its canonical path/directory/Git identities, with a separate github_repositories binding map. Task resolution verifies the origin and registered baseline; publication also checks GitHub numeric identity to reject repository replacement or transfer. All existing worktree, validation, review and exact-tree publication gates still apply.

Managed GitHub checkouts are verified with full strict Git object/connectivity checking, a restricted storage/configuration layout, an index-to-tree comparison, and an independent filesystem walk comparing every file's raw Git blob hash, size and mode against the pinned tree. Missing/extra files, corrupt objects, alternate object stores, symlinks and unexpected metadata fail closed, even when HEAD or Git status looks correct. Retry, discovery and task resolution recheck this state; none repairs it. Every selected base_ref also passes the same supported-tree admission before task-worktree creation, including historical commits. This closes a historical symlink policy bypass; it is not evidence of a demonstrated sandbox escape. Legacy host registrations retain their existing tree policy. Supported GitHub HTTPS/SCP/SSH origins are compared by complete normalized owner/repository identity, not prefixes.

Identical requests revalidate and return already_registered; they do not refresh the baseline or alter an existing checkout. Rename, transfer, origin/identity changes and local ID/path collisions fail closed. An existing host-registered GitHub origin is discovered by its original logical ID, not silently duplicated. Empty, archived, disabled, oversized or unsupported-content repositories are rejected. API-reported repository size and checked-out file bytes are each capped at 128 MiB; at most 20,000 entries are inspected, with bounded Git/API output. The registry remains capped at 64 KiB, with at most 100 discovery entries. These are admission and evidence limits, not a filesystem quota on downloaded history. API commands have 15-second limits. Registration Git subprocesses (including fetch and integrity checks) have 120-second limits. They run asynchronously under existing coreutils /usr/bin/timeout, without detached children or unref'ed jobs, using a fixed umask and bounded output (128 KiB API, 256 KiB Git). Output is never returned as an error. Healthy subprocesses are event-driven: no periodic process-table scans. A bounded Node deadline supplements the coreutils watchdog. Cancellation, deadline expiry, or output overflow immediately signal the group and, if still running, its leader. The launch deliberately does not use --foreground: GNU timeout establishes its own process group before forking the command (coreutils 8.32 source); a real-subprocess regression checks the command and timeout leader's observed PGIDs. The Controller begins settlement on exit, not only on pipe close, so surviving descendants holding inherited output descriptors cannot hide behind an open pipe. An absent group (kill(-pgid, 0) returning ESRCH) needs no /proc scan. Otherwise, Linux /proc distinguishes live members from zombies only during settlement; remaining live members are killed and cannot yield successful completion. Verification retries are serialized at 25ms intervals and bounded by a two-second settlement deadline. Success requires pipe closure, exit zero, and no live group members. Uncertain termination retains the existing registry lock/audit failure semantics; timers, abort listeners and verification retries stop at settlement, including on verification failure. This verifies the owned process group, not a universal claim about descendants that deliberately escape it. Repository code is never executed. Registration/discovery share only their own lock; unrelated task cleanup and timers remain responsive. Shutdown cancels registration, stops tasks without first waiting for registration, then drains registration and its audit before closing SQLite. No network retry is automatic. Unexpected termination remains fail-closed.

Supervisor regressions use real disposable subprocesses, including inherited pipes, background descendants, shutdown, cancellation, validation deadlines and injected verification failures. Reproduce the offline performance workload separately with:

node --test test/registration-process.test.mjs test/github-registration.test.mjs
node test/registration-process-benchmark.mjs /tmp/pab-monitor-measurements.json

The benchmark creates a new result file (refuses overwrite), runs three alternating control/monitored samples, and measures parent CPU, 40 sequential git --version calls, two CPU-only Python validation processes, and one/three idle sleepers. It does not register repositories, invoke Codex, or contact GitHub. Measurements on Node 26.5.1 / GNU timeout 8.32 before/after the event-driven correction (medians):

Workload

Before: control / monitored

After: control / monitored

One idle child, parent CPU (% of one core)

0.063 / 53.454

0.059 / 0.062

Forty Git calls (wall ms)

40.70 / 1034.49

31.21 / 58.83

Two concurrent validations (wall ms)

2813.21 / 4259.83

2790.60 / 2782.57

Three idle children, parent CPU (% of one core)

0.126 / 98.960

0.076 / 0.085

These are host measurements, not CI timing thresholds or universal performance guarantees. The control launches directly; monitored execution additionally retains the timeout process, fixed umask and termination checks. No safeguards were disabled to obtain these results. Deliberate process-group escape and unkillable kernel waits remain outside confirmed termination; uncertainty never becomes registration success.

Failure recovery and deployment

Both registration writers share an exclusive repositories.json.lock directory. Controller requests serialize; another process receives REPOSITORY_REGISTRY_BUSY. The publication order is: verify objects/tree/index/files, synchronize every staged file (including Git objects and metadata) and directory bottom-up, reverify, rename the checkout, synchronize its parent, reverify the published checkout, then write and synchronize an exclusive private registry temporary file, atomically rename it, and synchronize the registry parent. New provisioning-root ancestors have their parent entries synchronized too. A final resolver check precedes success. Existing-entry retries also synchronize and reverify before success. Discovery is read-only. Synchronization failures fail closed; pre-registry failures leave no trusted entry. If registry publication was attempted but its acknowledgement/verification fails, the outcome is explicitly uncertain, not success. An entry already atomically visible at that boundary must pass complete validation before any subsequent use.

This is ordered filesystem durability, conditional on Linux/local filesystem fsync and atomic-rename semantics, durable existing state roots, and storage honoring those operations. It is not a universal crash/power-loss atomicity claim or protection against a malicious trusted OS/operator. The multi-directory operation is not a single transaction; inspection may be necessary after interruption. Tree inspection is bounded at 100,000 storage entries and 512 MiB combined checkout/Git storage, not a download quota. Content-transforming checkouts that differ from raw Git blobs are intentionally not admitted.

A crash may leave the lock, a .stage-github_<id> directory, a registry temporary file, or an unregistered github_<id> checkout. Any staging failure retains its stage for inspection; retries do not reuse it. Unconfirmed process termination retains the registry lock and prevents reporting a clean Controller shutdown. A registry write failure after checkout publication retains that unregistered destination. No startup replay, automatic lock stealing, orphan adoption, destructive replacement or hash repair is performed. A lost acknowledgement after a successful registry replacement can return the existing entry only after complete identity/content validation and synchronization. Corruption is never automatically repaired or adopted. REGISTRATION_INCOMPLETE_REQUIRES_INSPECTION, REGISTRATION_DESTINATION_EXISTS, or a persistent REPOSITORY_REGISTRY_BUSY require a trusted operator to stop further registration, establish that the original Controller/fetch/helper processes have exited, inspect registry identities and private artifacts, and remove only confirmed unregistered leftovers/abandoned locks before a new explicit request. Do not delete registered repositories. Ordinary successful registrations need no host intervention; uncertain filesystem/process failures deliberately do.

Controller invocation audit records retain the validated bounded requested GitHub name (null for invalid input), Controller role, and sanitized success, denial, failure or uncertain outcome. They contain no raw subprocess output, local paths or credentials. The initial durable record is uncertain until completion, so a crash cannot fabricate success; no audit record triggers replay. Errors after an attempted registry publication are REGISTRATION_PUBLICATION_UNCERTAIN. MCP authentication or schema rejections before Controller invocation are not registration executions.

For deployment, independently review and merge first, stage a clean release with locked dependencies, preserve/backup the existing registry and state, ensure the managed runtime has /usr/bin/timeout and its user has an authenticated GitHub user account with repository read access and storage available under the fixed provisioning root, and switch the existing launch profile only during an owner-authorized idle restart. No credential or registry migration is required. Use this release's local registration CLI, not an older writer that lacks the shared lock. The managed tunnel does not forward GH_TOKEN/GITHUB_TOKEN; its existing private gh login is the supported route. Do not copy credentials, acceptance state or fixtures into a release/task checkout.

Post-deployment acceptance requires separate authorization: discover registrations; register a small, owner-owned disposable public/private GitHub fixture; verify numeric identity and baseline, repeat once and confirm no second checkout; verify organization and collaborator-only denial; then separately authorize a local task with a minimal contract and confirm isolated worktree execution. Validation, review and publication remain separate approvals. Offline mocks do not establish live credential, transport, or production acceptance. No live registration is part of implementation validation.

Workflow

Tool

Purpose

list_registered_repositories

Discover bounded logical IDs and GitHub identity without host paths.

register_github_repository

Controller-register an eligible GitHub user-owned repository; never starts execution.

start_task

Start a contract in a disposable workspace or allowlisted repository worktree.

get_task

Inspect bounded progress, clarification, review and publication state.

continue_task

Answer clarification or explicitly request further implementation.

review_task

Freeze evidence and request independent review in a separate session.

publish_task

Freeze a completed PASS-reviewed repository task and create a draft PR.

cleanup_task

Stop execution/delete remote sessions; optionally remove the worktree.

Tool schemas and contracts describe scope, costs, retries, review and publication for the default Agents API path. Local execution is the limited opt-in path described above; local publication requires the bounded qualification gates. Agents API execution/review uses paid API sessions. Publication accepts only task_id, title, optional body, and draft:true (the default), never arbitrary paths, commands or branches. It verifies unchanged state and commit-tree equality, then pushes only its task branch without force. Deletes, renames, modes, symlinks and binary files retain their Git semantics. Submodules are unsupported. PRs target the registered GitHub repository's default branch; the bridge never merges them.

Agents API tasks' commands use codex sandbox -P bridge_task -C <worktree> -- <command> with a trusted pinned profile. File-write RPCs have equivalent sandbox enforcement. The outer exec-server transport retains the network access required by the Agents API. Startup probes and controller attestation must succeed before implementation. Tasks without repository_id retain the legacy disposable-workspace isolation; they do not have the repository command-network guarantee and cannot use publication.

Private runtime data

Default state is under ~/.local/state/personal-agents-bridge/; disposable task workspaces and evidence are under /var/tmp/personal-agents-bridge/. These are portable defaults, not paths to a registered repository. /var/tmp may be cleared by the OS. Preserve required artifacts before cleanup or host replacement.

Task expiry cleans remote resources after 15 minutes while retaining files. Explicit workspace deletion verifies absence; bounded audit/task records survive in controller state. Logs, evidence, databases, registry, tunnel configuration and credentials are private operational data and excluded by .gitignore if copied into this checkout. Ignore rules do not sanitize already-tracked files or history.

npm run test:live is a separate, paid manual integration check. It creates real sessions and must not run alongside the production controller. It is not part of npm test and was not used for public-source preparation.

Status and license

The local Codex feasibility assessment records isolated offline experiments, migration blockers, and operator validation steps. It does not change the production execution backend or authorize migration.

See VERIFICATION.md for local coverage and limitations. Live GitHub publication requires a separate, explicitly authorized acceptance test. Licensed under the Apache License 2.0. Third-party dependencies retain their own licenses.

Related MCP Connectors

Related MCP Servers