personal-agents-bridge
by danielnguyen
README.md
# 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](SECURITY.md)
and [architecture](docs/ARCHITECTURE.md) 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`:
```json
{
"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](https://developers.openai.com/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](https://github.com/openai/codex/blob/36650394c5b38c2990ccf2a3457165ca3e9d9726/codex-rs/tools/src/tool_config.rs#L17)
and [feature default](https://github.com/openai/codex/blob/36650394c5b38c2990ccf2a3457165ca3e9d9726/codex-rs/features/src/lib.rs#L1605).
The separate [async-question tool](https://github.com/openai/codex/blob/36650394c5b38c2990ccf2a3457165ca3e9d9726/codex-rs/core/src/tools/handlers/request_user_input_async.rs)
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](https://github.com/openai/codex/blob/36650394c5b38c2990ccf2a3457165ca3e9d9726/codex-rs/app-server-protocol/src/protocol/item_builders.rs#L116)
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:
```bash
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](https://developers.openai.com/codex/app-server),
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):
```bash
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:
```bash
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.
## 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.
```bash
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](.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](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):
```bash
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:
```json
{"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](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](https://github.com/coreutils/coreutils/blob/v8.32/src/timeout.c#L450-L462));
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:
```bash
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](docs/TOOLS.md) 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](docs/LOCAL_CODEX_FEASIBILITY.md) records
isolated offline experiments, migration blockers, and operator validation steps.
It does not change the production execution backend or authorize migration.
See [VERIFICATION.md](VERIFICATION.md) for local coverage and limitations. Live
GitHub publication requires a separate, explicitly authorized acceptance test.
Licensed under the [Apache License 2.0](LICENSE). Third-party dependencies retain
their own licenses.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues