approval-md 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approval codex bridge`: approval.md as the client of Codex's app-server
|
|
3
|
+
* protocol (APRV-361, adopting APRV-349's recommendation).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* The native Codex hook refuses every shell call. A `Bash` pre-event carries
|
|
8
|
+
* `tool_input` keys exactly `["command"]`, so the adapter cannot bind the
|
|
9
|
+
* directory the command will run in, and it answers
|
|
10
|
+
* `hook-unsupported-execution-context` rather than approve bytes whose meaning
|
|
11
|
+
* it does not know (APRV-310, APRV-311). The app-server protocol is a different
|
|
12
|
+
* shape: Codex stops before it acts, asks its client, and waits, and the
|
|
13
|
+
* question it asks carries `cwd` on the same frame as the command, minted by
|
|
14
|
+
* the harness runtime rather than reported by the model.
|
|
15
|
+
*
|
|
16
|
+
* `docs/codex-app-server-bridge.md` is the evidence and the recommendation.
|
|
17
|
+
* Read the recommendation before changing anything here: this is an ADVISORY
|
|
18
|
+
* checkpoint for everyday Codex sessions this runtime starts, and it is not a
|
|
19
|
+
* boundary. The auto-reviewer can resolve a question before this client sees
|
|
20
|
+
* it, and the approval policy and sandbox posture decide how many questions
|
|
21
|
+
* exist at all. Both are governed by the harness's own configuration, which
|
|
22
|
+
* this project does not attest. The claim this verb supports is "this client
|
|
23
|
+
* decided every question this app-server child asked in this session", and
|
|
24
|
+
* nothing wider. See the custody section below for which word in that sentence
|
|
25
|
+
* is load-bearing.
|
|
26
|
+
*
|
|
27
|
+
* ## Custody: the server is this process's own child (APRV-365)
|
|
28
|
+
*
|
|
29
|
+
* A pending approval request is replayed to whatever connects NEXT, so "who
|
|
30
|
+
* may connect" is a real question about any app-server, and it is the question
|
|
31
|
+
* the follow-up list filed. For this verb it is answered by construction: the
|
|
32
|
+
* server is started here, by {@link driveSession}, as a child process over
|
|
33
|
+
* stdio pipes. There is no socket, nothing binds a path, and no other process
|
|
34
|
+
* has a file descriptor to speak on. The custody rule is the operating
|
|
35
|
+
* system's rather than this runtime's, which is the strongest kind available
|
|
36
|
+
* and the only kind this project would not have to attest.
|
|
37
|
+
*
|
|
38
|
+
* Two consequences worth stating rather than leaving to be inferred. Within
|
|
39
|
+
* one run there is no replay hazard at all: a question this client is asked
|
|
40
|
+
* cannot reach another client, because there is no other client. And the claim
|
|
41
|
+
* is scoped to THIS CHILD and THIS SESSION: a Codex started outside this
|
|
42
|
+
* arrangement is a different process with a different connection, and nothing
|
|
43
|
+
* here observes it, exactly as `docs/codex-activation.md` says of a Codex
|
|
44
|
+
* started outside the confined session.
|
|
45
|
+
*
|
|
46
|
+
* What this verb deliberately does NOT do is inspect the server command for a
|
|
47
|
+
* shape that would attach to something already running instead of starting a
|
|
48
|
+
* child. That would be a guess at another program's command line, which this
|
|
49
|
+
* repository has no record of, and a check written against a guessed shape
|
|
50
|
+
* finds nothing while reporting that it looked (the trap APRV-379 names and
|
|
51
|
+
* APRV-364's item reader is careful about). The property is stated and true;
|
|
52
|
+
* an operator who passes `-- <something that attaches>` after the separator has
|
|
53
|
+
* left the arrangement this section describes, and the report's claim is then
|
|
54
|
+
* about a session this verb did not establish.
|
|
55
|
+
*
|
|
56
|
+
* ## It reuses the hook's flow; it does not fork it
|
|
57
|
+
*
|
|
58
|
+
* Every decision is `cli/hook.ts`'s {@link decideHarnessCall}: the same
|
|
59
|
+
* classifier over `{command, cwd}`, the same human-only refusal, the same
|
|
60
|
+
* unruled `harness.launch.*` refusal, the same sandbox requirement, the same
|
|
61
|
+
* loop floor and unattended guard, and the same register, request and wait
|
|
62
|
+
* against the verified view. What changes is only where the answer goes: a
|
|
63
|
+
* JSON-RPC reply on the connection instead of a decision object on stdout.
|
|
64
|
+
*
|
|
65
|
+
* The request is translated into the hook's own `HookInput` — tool `Bash`,
|
|
66
|
+
* `tool_input.command` the string the server sent, `cwd` the directory the
|
|
67
|
+
* server named — and nothing else is invented. Both fields come from the
|
|
68
|
+
* server, which is what makes them usable: a `cwd` the model reported would be
|
|
69
|
+
* a self-reported field reducing scrutiny (SPEC §11.1 invariant 4).
|
|
70
|
+
*
|
|
71
|
+
* ## The deadline is the policy's, not a harness ceiling
|
|
72
|
+
*
|
|
73
|
+
* Every hook adapter answers inside a ceiling its harness sets, and the retry
|
|
74
|
+
* grace and adopt-on-retry machinery exist so a denial-by-deadline is
|
|
75
|
+
* recoverable. This transport has no timeout at all (`docs/codex-app-server-bridge.md`,
|
|
76
|
+
* question 2), so the wait here defaults to the policy's `approval_ttl`: a
|
|
77
|
+
* human who answers in eleven minutes is answering rather than arriving too
|
|
78
|
+
* late. `--wait` overrides it; nothing shortens the request's own TTL.
|
|
79
|
+
*
|
|
80
|
+
* ## What it answers, and what it never answers
|
|
81
|
+
*
|
|
82
|
+
* `accept` and `decline` only, in the vocabulary the request advertised through
|
|
83
|
+
* `availableDecisions`, and never `acceptForSession`, `cancel` or `abort`.
|
|
84
|
+
* `acceptForSession` converts one decision into standing authority for a whole
|
|
85
|
+
* session, which is a grant shape this project does not have. `cancel` and
|
|
86
|
+
* `abort` mean "stop the turn", which is a different act from "no to this
|
|
87
|
+
* action", and sending one would record an interruption as a denial.
|
|
88
|
+
*
|
|
89
|
+
* That rule is carried by the TYPE since APRV-367, not by a reviewer's memory:
|
|
90
|
+
* every reply word is a {@link BridgeDecisionWord}, whose eight inhabitants are
|
|
91
|
+
* the four spellings of yes and the four of no, and {@link encodeDecision} is
|
|
92
|
+
* the one place a decision becomes bytes and re-asks the question at runtime. A
|
|
93
|
+
* word outside the vocabulary is unconstructible, and were one to arrive anyway
|
|
94
|
+
* the reply becomes a decline, since the only safe substitute for a word you
|
|
95
|
+
* cannot name is no.
|
|
96
|
+
*
|
|
97
|
+
* ## The two file-change APIs, and the correlation (APRV-363, APRV-379)
|
|
98
|
+
*
|
|
99
|
+
* The LEGACY `applyPatchApproval` carries its `fileChanges` map on the request
|
|
100
|
+
* itself, so there is nothing to correlate and nothing to re-render. Since
|
|
101
|
+
* APRV-363 it goes through the same `decideHarnessCall` the exec half uses,
|
|
102
|
+
* classified by the paths it names and bound with the change as it arrived plus
|
|
103
|
+
* its digest. A request carrying a map and no directory is refused exactly as
|
|
104
|
+
* an exec request with no `cwd` is: a relative path resolves somewhere, and a
|
|
105
|
+
* directory this client guessed would be a guess the grant is bound to.
|
|
106
|
+
*
|
|
107
|
+
* An `item/fileChange/requestApproval` on the ITEM-BASED API carries no
|
|
108
|
+
* content: `threadId`, `turnId`, `itemId`, `startedAtMs`, `reason` and
|
|
109
|
+
* `grantRoot`, and nothing else. The bytes arrived EARLIER, on the
|
|
110
|
+
* `item/started` notification for that item, whose `item.changes` is an array
|
|
111
|
+
* of `{path, kind, diff}` (observed on 0.155.0; the frame is in
|
|
112
|
+
* `docs/codex-app-server-bridge.md`, question 1). Since APRV-379 this verb
|
|
113
|
+
* keeps every item the thread announces, by item id, and answers the request
|
|
114
|
+
* against the frame that id names. What reaches the classifier is the change
|
|
115
|
+
* set the server sent, in the shape it sent it: the paths take their classes
|
|
116
|
+
* and the payload binds the changes verbatim with a digest over them as
|
|
117
|
+
* received. Nothing is re-rendered into an `apply_patch` envelope and nothing
|
|
118
|
+
* parses `diff`.
|
|
119
|
+
*
|
|
120
|
+
* The correlation is where an approval could authorize bytes nobody classified,
|
|
121
|
+
* so every way the two frames could fail to be about one change is a refusal:
|
|
122
|
+
* an id no `item/started` announced, an id naming an item that is not a
|
|
123
|
+
* `fileChange`, a frame with no readable change set, and a request naming a
|
|
124
|
+
* thread or a turn the frame does not, are all `bridge-file-change-unbound`.
|
|
125
|
+
* An item whose `item/completed` arrived BEFORE the question is
|
|
126
|
+
* `bridge-file-change-already-completed`: a change that finished before it was
|
|
127
|
+
* asked about is not a change this client is in a position to decide.
|
|
128
|
+
*
|
|
129
|
+
* A server request this verb does not recognise is declined too, on the same
|
|
130
|
+
* rule: a question nobody classified is not a question to answer yes to.
|
|
131
|
+
*
|
|
132
|
+
* ## The approval policy is pinned, and the pin is checked (APRV-366)
|
|
133
|
+
*
|
|
134
|
+
* The thread starts with `approvalPolicy: "untrusted"`, which is `UnlessTrusted`
|
|
135
|
+
* on the wire and the only variant under which every command and every patch
|
|
136
|
+
* asks. Under `on-request` or `never` an unknown fraction of the session never
|
|
137
|
+
* reaches this client, and "this client decided every question it was asked"
|
|
138
|
+
* would still be true while meaning nothing. There is no flag.
|
|
139
|
+
*
|
|
140
|
+
* What the verb can prove about it depends on the server. A `thread/start` the
|
|
141
|
+
* server refuses stops the run, carrying its error verbatim, which is where a
|
|
142
|
+
* refusal of the value itself lands. A server that reports an effective policy
|
|
143
|
+
* of its own, on `thread/start`'s result or on a thread notification, stops the
|
|
144
|
+
* run when that policy is not the pinned one. A server that reports nothing is
|
|
145
|
+
* run against, and the report then claims only what happened: the pin was
|
|
146
|
+
* requested and no frame confirmed it.
|
|
147
|
+
*
|
|
148
|
+
* ## A probe turn runs first, and it can stop the session (APRV-364)
|
|
149
|
+
*
|
|
150
|
+
* Codex carries a server-side auto-reviewer that can resolve an approval with a
|
|
151
|
+
* model call before this client is asked, and tells the client afterwards
|
|
152
|
+
* through `item/autoApprovalReview` notifications. Whether it runs is a setting
|
|
153
|
+
* in the harness's own configuration, which this project does not attest, and
|
|
154
|
+
* there is no frame in the observed vocabulary where the server reports it. So
|
|
155
|
+
* it cannot be READ, and the only way to establish anything is to watch what
|
|
156
|
+
* happens to one command.
|
|
157
|
+
*
|
|
158
|
+
* Every start therefore runs a PREFLIGHT turn asking for one harmless command
|
|
159
|
+
* ({@link PROBE_COMMAND}) before the operator's own turn, with no flag to skip
|
|
160
|
+
* it. Three outcomes, told apart by the command-item notifications:
|
|
161
|
+
*
|
|
162
|
+
* - an approval request for it reaches this client: the run continues, and the
|
|
163
|
+
* report records the pin as confirmed by OBSERVATION;
|
|
164
|
+
* - a command ran and no request arrived: `bridge-approval-policy-mismatch`,
|
|
165
|
+
* because a policy under which one command did not ask is not `untrusted`
|
|
166
|
+
* whatever the server says about itself;
|
|
167
|
+
* - no command ran at all: `bridge-preflight-void`, carrying the turn's frames
|
|
168
|
+
* verbatim. It is never retried and never reported as a pass.
|
|
169
|
+
*
|
|
170
|
+
* An `item/autoApprovalReview` notification in either turn is
|
|
171
|
+
* `bridge-auto-reviewer-active` and ends the run, after one
|
|
172
|
+
* `audit.question_preempted` is appended for it (APRV-378): the moment
|
|
173
|
+
* something other than this gate answered a question this gate exists to ask is
|
|
174
|
+
* the moment this project most wants in the log.
|
|
175
|
+
*
|
|
176
|
+
* THE PROBE'S OWN REQUEST NEVER REACHES THE GATE. It is declined immediately,
|
|
177
|
+
* as an observation. Routing it through `decideHarnessCall` would register an
|
|
178
|
+
* action and could put `true` on a human's phone at every bridge start, and a
|
|
179
|
+
* preflight that spends a person's attention is not a harmless one.
|
|
180
|
+
*
|
|
181
|
+
* WHAT A PASS MEANS, exactly: one question reached this client unanswered by
|
|
182
|
+
* anything else. It is not a proof that the auto-reviewer is off for every
|
|
183
|
+
* question, and nothing here says that it is.
|
|
184
|
+
*
|
|
185
|
+
* ## One at a time, on purpose
|
|
186
|
+
*
|
|
187
|
+
* The gate's wait is synchronous, so while one question is being decided this
|
|
188
|
+
* process is not reading frames. That is the fail-closed direction: frames
|
|
189
|
+
* queue and are answered in arrival order, and a second request cannot be
|
|
190
|
+
* answered from a decision made about the first. The observed protocol asks one
|
|
191
|
+
* question at a time (the turn does not move past an unanswered one).
|
|
192
|
+
*
|
|
193
|
+
* ## Limitations stated rather than implied
|
|
194
|
+
*
|
|
195
|
+
* An OPEN GATE WINDOW is not honoured here. The hook's bypass prints a hook
|
|
196
|
+
* verdict and appends a record shaped for the hook; wiring it into this
|
|
197
|
+
* transport is more surface than this task carries, and ignoring it is the
|
|
198
|
+
* strict direction — a window widens authority, and this verb simply does not
|
|
199
|
+
* widen. An operator who opens a window and expects this verb to fall through
|
|
200
|
+
* will find it still asking.
|
|
201
|
+
*/
|
|
202
|
+
import { hookScope, type HarnessVerdict } from "./hook.js";
|
|
203
|
+
import type { Streams } from "./main.js";
|
|
204
|
+
/**
|
|
205
|
+
* The approval policy this verb starts a thread under (`untrusted` on the
|
|
206
|
+
* wire).
|
|
207
|
+
*
|
|
208
|
+
* `UnlessTrusted` is the only variant under which every command asks
|
|
209
|
+
* (`docs/codex-app-server-bridge.md`, question 5), and `unless-trusted` is
|
|
210
|
+
* REFUSED by the server: the accepted spelling is `untrusted`, established by
|
|
211
|
+
* the 2026-09-18 probe. There is no flag: a session gating an unknown fraction
|
|
212
|
+
* of itself is the thing this pin exists to prevent, and an operator who could
|
|
213
|
+
* pass `on-request` would have exactly that session (APRV-366).
|
|
214
|
+
*/
|
|
215
|
+
export declare const APPROVAL_POLICY = "untrusted";
|
|
216
|
+
/** The sandbox posture the thread starts under. */
|
|
217
|
+
export declare const SANDBOX = "read-only";
|
|
218
|
+
/**
|
|
219
|
+
* The decision words this verb will send, most literal first.
|
|
220
|
+
*
|
|
221
|
+
* `accept`/`decline` are the item-based API's spelling and `approved`/`denied`
|
|
222
|
+
* the legacy one. The session-wide and amendment-carrying variants are absent
|
|
223
|
+
* from the accept list, and `cancel`/`abort` from the decline list, for the
|
|
224
|
+
* reasons in this module's header. A value this runtime does not name is never
|
|
225
|
+
* sent, however loudly the server advertises it.
|
|
226
|
+
*/
|
|
227
|
+
export declare const ACCEPT_WORDS: readonly ["accept", "approved", "approve", "allow"];
|
|
228
|
+
export declare const DECLINE_WORDS: readonly ["decline", "denied", "deny", "reject"];
|
|
229
|
+
/** One of the four spellings of yes this verb will send. */
|
|
230
|
+
export type BridgeAcceptWord = (typeof ACCEPT_WORDS)[number];
|
|
231
|
+
/** One of the four spellings of no. */
|
|
232
|
+
export type BridgeDeclineWord = (typeof DECLINE_WORDS)[number];
|
|
233
|
+
/**
|
|
234
|
+
* Everything this verb can put in a `decision` field, as a type (APRV-367).
|
|
235
|
+
*
|
|
236
|
+
* The list is the whole vocabulary: eight spellings of two words. A value
|
|
237
|
+
* outside it is a TYPE ERROR at every point the reply is built, which is the
|
|
238
|
+
* half of the rule a reviewer cannot forget to check, and it is refused again
|
|
239
|
+
* at runtime by {@link encodeDecision}, which is the half that survives a
|
|
240
|
+
* caller with an `any` in it.
|
|
241
|
+
*/
|
|
242
|
+
export type BridgeDecisionWord = BridgeAcceptWord | BridgeDeclineWord;
|
|
243
|
+
/** The two things this verb ever means, whatever the server calls them. */
|
|
244
|
+
export type BridgeOutcome = "accept" | "decline";
|
|
245
|
+
/** Is this one of the eight words? The runtime face of {@link BridgeDecisionWord}. */
|
|
246
|
+
export declare function isBridgeDecisionWord(value: unknown): value is BridgeDecisionWord;
|
|
247
|
+
/**
|
|
248
|
+
* The reply payload for one word: the ONE place a decision becomes bytes.
|
|
249
|
+
*
|
|
250
|
+
* `null` for anything this runtime does not name. The types make such a value
|
|
251
|
+
* unconstructible, so this is the defence against the code changing out from
|
|
252
|
+
* under the types rather than against any input a server can send: no
|
|
253
|
+
* `availableDecisions` list can reach it, because {@link chooseDecision} only
|
|
254
|
+
* ever returns a member.
|
|
255
|
+
*
|
|
256
|
+
* The caller answers `null` by sending a DECLINE, and that is the whole
|
|
257
|
+
* reasoning: the only safe substitute for a word you cannot name is no. It gets
|
|
258
|
+
* no refusal code of its own, because a code in a closed union that no input
|
|
259
|
+
* can produce is a string a second implementation cannot exercise and would
|
|
260
|
+
* have to take on trust.
|
|
261
|
+
*/
|
|
262
|
+
export declare function encodeDecision(word: string): {
|
|
263
|
+
decision: BridgeDecisionWord;
|
|
264
|
+
} | null;
|
|
265
|
+
/**
|
|
266
|
+
* Every refusal this verb can answer with that is NOT a gate verdict, closed
|
|
267
|
+
* and machine-readable (SPEC §11.1 invariant 7).
|
|
268
|
+
*
|
|
269
|
+
* A gate verdict carries the gate's own code (`hook-class-human-only`,
|
|
270
|
+
* `hook-rejected`, `hook-timeout`, and the rest); these are the refusals the
|
|
271
|
+
* bridge reaches on its own, before or instead of asking.
|
|
272
|
+
*/
|
|
273
|
+
export declare const BRIDGE_REFUSAL_CODES: readonly [
|
|
274
|
+
/** A file-change request whose content this verb cannot produce (APRV-363). */
|
|
275
|
+
"bridge-file-change-unbound",
|
|
276
|
+
/** A server request this verb has no reading for. */
|
|
277
|
+
"bridge-unknown-request",
|
|
278
|
+
/**
|
|
279
|
+
* A file-change request whose item had already COMPLETED when it arrived
|
|
280
|
+
* (APRV-379).
|
|
281
|
+
*
|
|
282
|
+
* Distinct from `bridge-file-change-unbound`, which says the content could
|
|
283
|
+
* not be produced. Here the content was produced: the `item/started` frame is
|
|
284
|
+
* held, the item id correlates, and the change set is right there. What is
|
|
285
|
+
* wrong is the ORDER. `item/completed` for that item arrived before the
|
|
286
|
+
* question about it did, and a change that finished before it was asked about
|
|
287
|
+
* is not a change this client is in a position to decide. Answering yes would
|
|
288
|
+
* put a grant in the log for an effect that had already happened, and
|
|
289
|
+
* answering the ordinary no would tell an operator to go and stop something
|
|
290
|
+
* that is over.
|
|
291
|
+
*
|
|
292
|
+
* The repairs differ, which is why the codes do: an unbound change is a
|
|
293
|
+
* correlation that did not happen and points at this client or at a protocol
|
|
294
|
+
* that changed shape, and this one points at a session whose approval policy
|
|
295
|
+
* is not the one it was pinned to, or at a server that reordered its frames.
|
|
296
|
+
*/
|
|
297
|
+
"bridge-file-change-already-completed",
|
|
298
|
+
/**
|
|
299
|
+
* An exec request whose command string names no argv this client can bind
|
|
300
|
+
* (APRV-362).
|
|
301
|
+
*
|
|
302
|
+
* Distinct from `bridge-request-unbound`, which says a field is MISSING. This
|
|
303
|
+
* one says the field arrived and could not be read as the rendering of an
|
|
304
|
+
* argv: an unterminated quote, a bare double quote, a trailing backslash, or
|
|
305
|
+
* whitespace no join produces. The repairs differ, which is why the codes do:
|
|
306
|
+
* a missing `cwd` is a server that changed shape, and this is a command
|
|
307
|
+
* string that did not come from joining the words that will run.
|
|
308
|
+
*/
|
|
309
|
+
"bridge-command-unbound",
|
|
310
|
+
/** An exec request carrying no command string, or no cwd. */
|
|
311
|
+
"bridge-request-unbound"];
|
|
312
|
+
export type BridgeRefusalCode = (typeof BRIDGE_REFUSAL_CODES)[number];
|
|
313
|
+
/**
|
|
314
|
+
* Every way this verb STOPS a session instead of answering a question
|
|
315
|
+
* (APRV-366), closed and machine-readable.
|
|
316
|
+
*
|
|
317
|
+
* A separate array from {@link BRIDGE_REFUSAL_CODES}, and deliberately not a
|
|
318
|
+
* member of it. Those are answers: one approval request declined, the turn
|
|
319
|
+
* carrying on. These end the run before or instead of a turn, because the
|
|
320
|
+
* session could not be established as the kind of session this verb is willing
|
|
321
|
+
* to sit in front of. The conformance union `bridge_refusal_codes` is
|
|
322
|
+
* documented as "every way the bridge can decline an app-server approval
|
|
323
|
+
* request", so a stop code inside it would describe a different boundary, which
|
|
324
|
+
* is the reasoning that kept these out of `hook_deny_codes` too.
|
|
325
|
+
*
|
|
326
|
+
* The process exit for both is {@link EXIT_IO}, as it is for every other
|
|
327
|
+
* protocol stop here: the exit codes are frozen public API and a session that
|
|
328
|
+
* could not be started is not a new number. The code below is the distinct part
|
|
329
|
+
* a caller branches on.
|
|
330
|
+
*/
|
|
331
|
+
export declare const BRIDGE_STOP_CODES: readonly [
|
|
332
|
+
/**
|
|
333
|
+
* The server refused `thread/start`, so no thread exists and the approval
|
|
334
|
+
* policy this verb requires was never established. The server's own error is
|
|
335
|
+
* carried verbatim in the detail, which is where a refusal of the policy
|
|
336
|
+
* VALUE shows up (the 2026-09-18 probe's `unknown variant \`unless-trusted\`,
|
|
337
|
+
* expected one of \`untrusted\`, \`on-request\`, \`granular\`, \`never\``).
|
|
338
|
+
*/
|
|
339
|
+
"bridge-thread-start-refused",
|
|
340
|
+
/**
|
|
341
|
+
* The server started a thread and reported an effective approval policy that
|
|
342
|
+
* is not {@link APPROVAL_POLICY}. Under any other variant an unknown fraction
|
|
343
|
+
* of the session never produces a question at all, so "this client decided
|
|
344
|
+
* every question it was asked" would be true and would mean nothing.
|
|
345
|
+
*/
|
|
346
|
+
"bridge-approval-policy-mismatch",
|
|
347
|
+
/**
|
|
348
|
+
* An `item/autoApprovalReview` notification arrived, in the preflight turn or
|
|
349
|
+
* in the real one (APRV-364).
|
|
350
|
+
*
|
|
351
|
+
* Codex carries a server-side auto-reviewer that can resolve an approval with
|
|
352
|
+
* a model call BEFORE the client path runs, and tells the client afterwards
|
|
353
|
+
* through these notifications (`docs/codex-app-server-bridge.md`, question
|
|
354
|
+
* 3). A session with a reviewer in front of the gate is a session whose
|
|
355
|
+
* silence means nothing: the questions this client was not asked are
|
|
356
|
+
* indistinguishable from questions nobody wanted to ask. So the run stops
|
|
357
|
+
* rather than gating whatever is left over.
|
|
358
|
+
*
|
|
359
|
+
* It leaves a RECORD since APRV-378: one `audit.question_preempted`,
|
|
360
|
+
* appended through the real append path before the stop, naming the source,
|
|
361
|
+
* the question as Codex identified it, and the verdict the reviewer reached
|
|
362
|
+
* where the notification stated one. The write is best-effort and the stop
|
|
363
|
+
* does not depend on it; a failure to append is reported on stderr beside
|
|
364
|
+
* the stop rather than swallowed.
|
|
365
|
+
*/
|
|
366
|
+
"bridge-auto-reviewer-active",
|
|
367
|
+
/**
|
|
368
|
+
* The preflight turn ran no command at all, so the probe established nothing
|
|
369
|
+
* (APRV-364).
|
|
370
|
+
*
|
|
371
|
+
* The preflight is a prompt, and a model is free to answer a prompt in prose.
|
|
372
|
+
* When that happens no approval request arrives AND no command executes, and
|
|
373
|
+
* the fact AC1 wants — that a command reached this client as a question —
|
|
374
|
+
* was not observed. Reporting it as a pass would be reporting a verdict
|
|
375
|
+
* nobody established, which is the APRV-359 lesson; reporting it as the
|
|
376
|
+
* policy mismatch would blame a healthy session for a model's choice of
|
|
377
|
+
* words. So it is its own code, the report carries the turn's frames
|
|
378
|
+
* verbatim, and nothing is retried: an operator runs the verb again.
|
|
379
|
+
*/
|
|
380
|
+
"bridge-preflight-void"];
|
|
381
|
+
export type BridgeStopCode = (typeof BRIDGE_STOP_CODES)[number];
|
|
382
|
+
/**
|
|
383
|
+
* Where the report's claim about the approval policy COMES FROM (APRV-364).
|
|
384
|
+
*
|
|
385
|
+
* APRV-366 wrote this as a boolean, and a boolean could say only that some
|
|
386
|
+
* frame echoed the pin back. The preflight probe establishes the same thing a
|
|
387
|
+
* different way, by watching what happens to one command, and the two are not
|
|
388
|
+
* the same strength of evidence: an echo is the server describing itself, and
|
|
389
|
+
* an observation is a thing that happened. A reader who is told `true` cannot
|
|
390
|
+
* tell them apart, so the field names its source instead.
|
|
391
|
+
*
|
|
392
|
+
* - `unconfirmed` — nothing has confirmed the pin. Where every run starts, and
|
|
393
|
+
* where a run that stopped before the probe finished stays.
|
|
394
|
+
* - `reported` — a server frame named the pinned policy as the effective one.
|
|
395
|
+
* The observed 0.155.0 server names none, so this is rare in practice.
|
|
396
|
+
* - `observed` — a probe command produced an approval request that reached
|
|
397
|
+
* this client. THE HONESTY LINE, and it is narrow on purpose: it proves that
|
|
398
|
+
* ONE question reached this client unanswered by anything else. It is not a
|
|
399
|
+
* proof that the auto-reviewer is off, and no code or document here may say
|
|
400
|
+
* that it is.
|
|
401
|
+
*/
|
|
402
|
+
export declare const BRIDGE_PIN_SOURCES: readonly ["unconfirmed", "reported", "observed"];
|
|
403
|
+
export type BridgePinSource = (typeof BRIDGE_PIN_SOURCES)[number];
|
|
404
|
+
/**
|
|
405
|
+
* The thread this verb started, as the report records it (APRV-366).
|
|
406
|
+
*
|
|
407
|
+
* `requested` is what went on the wire, `effective` is what the server said
|
|
408
|
+
* about it, and `confirmed` is the difference between the two: a server that
|
|
409
|
+
* echoes the policy back proves the pin, and one that says nothing leaves this
|
|
410
|
+
* client able to claim only that it asked. That distinction is recorded rather
|
|
411
|
+
* than smoothed over, because a report that said "untrusted" for both cases
|
|
412
|
+
* would be asserting something no frame carried.
|
|
413
|
+
*
|
|
414
|
+
* `confirmed` widened from a boolean to a {@link BridgePinSource} in APRV-364,
|
|
415
|
+
* when the probe gave it a second and stronger way to be true.
|
|
416
|
+
*/
|
|
417
|
+
export interface BridgeThreadRecord {
|
|
418
|
+
id: string | null;
|
|
419
|
+
cwd: string;
|
|
420
|
+
requested: {
|
|
421
|
+
approvalPolicy: string;
|
|
422
|
+
sandbox: string;
|
|
423
|
+
};
|
|
424
|
+
effective: {
|
|
425
|
+
approvalPolicy: string | null;
|
|
426
|
+
};
|
|
427
|
+
confirmed: BridgePinSource;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The one command the preflight turn asks for (APRV-364).
|
|
431
|
+
*
|
|
432
|
+
* Chosen for having no effect: it writes nothing, reads nothing, prints
|
|
433
|
+
* nothing, and exits zero. The point of the probe is the QUESTION it raises,
|
|
434
|
+
* and a probe whose command mattered would be a probe an operator had to think
|
|
435
|
+
* about before running.
|
|
436
|
+
*/
|
|
437
|
+
export declare const PROBE_COMMAND = "true";
|
|
438
|
+
/**
|
|
439
|
+
* The preflight prompt, written to leave a model as little room as a prompt can
|
|
440
|
+
* (APRV-364).
|
|
441
|
+
*
|
|
442
|
+
* It cannot leave none, which is why {@link BRIDGE_STOP_CODES} carries
|
|
443
|
+
* `bridge-preflight-void`: a model that answers in prose has run no command,
|
|
444
|
+
* and that outcome is reported rather than guessed at.
|
|
445
|
+
*/
|
|
446
|
+
export declare const PROBE_PROMPT: string;
|
|
447
|
+
/** What the preflight turn established, once it ended. */
|
|
448
|
+
export declare const BRIDGE_PROBE_OUTCOMES: readonly ["pending", "asked", "executed", "void"];
|
|
449
|
+
export type BridgeProbeOutcome = (typeof BRIDGE_PROBE_OUTCOMES)[number];
|
|
450
|
+
/**
|
|
451
|
+
* The preflight turn, as the report records it (APRV-364).
|
|
452
|
+
*
|
|
453
|
+
* `outcome` is the whole of what the probe established:
|
|
454
|
+
*
|
|
455
|
+
* - `asked` — an approval request for the probe arrived, so one question
|
|
456
|
+
* reached this client. The session continues.
|
|
457
|
+
* - `executed` — a command ran and no request arrived. A policy under which
|
|
458
|
+
* one command did not ask is not `untrusted`, whatever the server said about
|
|
459
|
+
* itself, so the run stops under `bridge-approval-policy-mismatch`.
|
|
460
|
+
* - `void` — no command ran at all, so nothing was established. The run stops
|
|
461
|
+
* under `bridge-preflight-void` and `frames` carries the turn verbatim.
|
|
462
|
+
* - `pending` — the turn has not ended. Only ever seen in a report that
|
|
463
|
+
* stopped for some other reason first.
|
|
464
|
+
*/
|
|
465
|
+
export interface BridgePreflightRecord {
|
|
466
|
+
turnId: string | null;
|
|
467
|
+
/** The command the prompt named: {@link PROBE_COMMAND}. */
|
|
468
|
+
command: string;
|
|
469
|
+
outcome: BridgeProbeOutcome;
|
|
470
|
+
/**
|
|
471
|
+
* The word sent on the probe's own approval request, when one arrived.
|
|
472
|
+
*
|
|
473
|
+
* Always a decline. The probe is an observation, and a probe this client
|
|
474
|
+
* approved would be a probe that ran.
|
|
475
|
+
*/
|
|
476
|
+
decision: BridgeDecisionWord | null;
|
|
477
|
+
/**
|
|
478
|
+
* Every frame the preflight turn produced, verbatim, present ONLY on the
|
|
479
|
+
* void stop.
|
|
480
|
+
*
|
|
481
|
+
* On a void there is nothing else to show: the stop says a fact could not be
|
|
482
|
+
* established, and the frames are the whole of the evidence for why. On any
|
|
483
|
+
* other outcome they are noise, and a report that always carried them would
|
|
484
|
+
* bury the line that matters.
|
|
485
|
+
*/
|
|
486
|
+
frames?: unknown[];
|
|
487
|
+
/** The turn's own error, when `turn/failed` ended it. */
|
|
488
|
+
error?: unknown;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The effective approval policy a server frame reports, or `null` when it
|
|
492
|
+
* reports none.
|
|
493
|
+
*
|
|
494
|
+
* The locations are a documented short list rather than a generic walk: this
|
|
495
|
+
* value can STOP a session, so it is read from places whose meaning is known,
|
|
496
|
+
* and a stray `approvalPolicy` nested inside some unrelated structure must not
|
|
497
|
+
* be able to end a run. The observed 0.155.0 server echoes none of them, which
|
|
498
|
+
* is why an absent value is not itself a stop.
|
|
499
|
+
*/
|
|
500
|
+
export declare function effectiveApprovalPolicy(value: unknown): string | null;
|
|
501
|
+
/** One answered question, for the report and for the tests. */
|
|
502
|
+
export interface BridgeAnswer {
|
|
503
|
+
method: string;
|
|
504
|
+
/** The server's own id for the request, echoed on the reply. */
|
|
505
|
+
id: unknown;
|
|
506
|
+
/** `accept` or `decline`, as this verb decided it. */
|
|
507
|
+
outcome: BridgeOutcome;
|
|
508
|
+
/**
|
|
509
|
+
* The word actually sent: one of the eight this runtime names, chosen to
|
|
510
|
+
* match what the request advertised (APRV-367). Its TYPE is the vocabulary,
|
|
511
|
+
* so a row saying `acceptForSession` cannot be constructed here.
|
|
512
|
+
*/
|
|
513
|
+
decision: BridgeDecisionWord;
|
|
514
|
+
/** Where that word came from: the request's own list, or this verb's fallback. */
|
|
515
|
+
decisionSource: "advertised" | "fallback";
|
|
516
|
+
/** The gate's code, or a {@link BRIDGE_REFUSAL_CODES} entry. `null` on an accept. */
|
|
517
|
+
code: string | null;
|
|
518
|
+
/** The gate's reason or the refusal's detail, for the operator. */
|
|
519
|
+
detail: string;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The decision words a request says are legal, if it says.
|
|
523
|
+
*
|
|
524
|
+
* Walked generically rather than read from one key path, so a renamed field of
|
|
525
|
+
* the same shape still answers. This is the server describing its own
|
|
526
|
+
* vocabulary, which is the one thing a client should never pin.
|
|
527
|
+
*/
|
|
528
|
+
export declare function advertisedDecisions(params: unknown): string[];
|
|
529
|
+
/**
|
|
530
|
+
* The word to send for this outcome, and where it came from (AC4).
|
|
531
|
+
*
|
|
532
|
+
* An advertised word wins, matched case-insensitively and NEVER by prefix, so a
|
|
533
|
+
* server that offers `acceptWithExecpolicyAmendment` is not read as offering
|
|
534
|
+
* `accept`: an amendment carries terms nobody approved. A request advertising
|
|
535
|
+
* nothing gets this verb's own first word, and the report says `fallback` so
|
|
536
|
+
* the choice is visible rather than assumed.
|
|
537
|
+
*
|
|
538
|
+
* What it returns is this runtime's own spelling of the matched word rather
|
|
539
|
+
* than the server's (APRV-367). That is the point of the type: a
|
|
540
|
+
* {@link BridgeDecisionWord} has eight inhabitants, all of them named here, so
|
|
541
|
+
* no path through this function can produce a word this project did not choose
|
|
542
|
+
* to be able to send. The two spellings differ only in letter case, since the
|
|
543
|
+
* match is case-insensitive equality with one of the eight.
|
|
544
|
+
*/
|
|
545
|
+
export declare function chooseDecision(params: unknown, outcome: BridgeOutcome): {
|
|
546
|
+
decision: BridgeDecisionWord;
|
|
547
|
+
decisionSource: "advertised" | "fallback";
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* The exec request's command, as BOTH the words that will run and the string
|
|
551
|
+
* that renders them (APRV-362).
|
|
552
|
+
*
|
|
553
|
+
* ## Which API, and why the string has to be un-joined
|
|
554
|
+
*
|
|
555
|
+
* The two live shapes differ in the one way that matters. The legacy
|
|
556
|
+
* `execCommandApproval` sends `command` as an argv array, which is what the
|
|
557
|
+
* kernel receives. The item-based `item/commandExecution/requestApproval` sends
|
|
558
|
+
* it as a single string, produced by `shlex_join` over that same argv
|
|
559
|
+
* (`docs/codex-app-server-bridge.md`, question 1). This verb drives the
|
|
560
|
+
* ITEM-BASED API, so the string is what it must consume, and the decision the
|
|
561
|
+
* task asked for is settled by which API the bridge speaks rather than by
|
|
562
|
+
* preference: the legacy array is still read, because a request in that shape
|
|
563
|
+
* is a request this client can answer, but it is not the path in use.
|
|
564
|
+
*
|
|
565
|
+
* Consuming the string means un-joining it. A classifier handed the rendering
|
|
566
|
+
* and never the words is classifying its own re-parse, and the gap between the
|
|
567
|
+
* two is where an approval could authorize words nobody read. So both are
|
|
568
|
+
* produced here, both reach the registered payload, and a reader can see the
|
|
569
|
+
* one against the other instead of being asked to trust that they agree.
|
|
570
|
+
*
|
|
571
|
+
* ## What this refuses, and what it deliberately does not
|
|
572
|
+
*
|
|
573
|
+
* A string that is not readable as a join — an unterminated quote, a bare
|
|
574
|
+
* double quote, a trailing backslash, or separation no join emits — is refused
|
|
575
|
+
* with `bridge-command-unbound`. So is an argv this runtime cannot render and
|
|
576
|
+
* read back unchanged, which is unreachable for a correct {@link shlexJoin} and
|
|
577
|
+
* checked anyway, because the cost is one pass over a short string and the
|
|
578
|
+
* failure it guards against is binding words nobody will run.
|
|
579
|
+
*
|
|
580
|
+
* It does NOT demand that {@link shlexJoin} reproduce the received bytes. That
|
|
581
|
+
* would pin the counterpart's quoting predicate, which this repository has no
|
|
582
|
+
* record of: the probe captured one command string and it is consistent with
|
|
583
|
+
* every candidate. A join written for shell safety quotes more than this one
|
|
584
|
+
* does, so demanding byte equality would refuse ordinary traffic on a guess.
|
|
585
|
+
* What is demanded instead is the part that is checkable without knowing which
|
|
586
|
+
* characters the counterpart chose to quote, and the rest is recorded.
|
|
587
|
+
*
|
|
588
|
+
* `proposedExecpolicyAmendment` is deliberately not read, though the task names
|
|
589
|
+
* it as a candidate second source. The 2026-09-18 observation records that the
|
|
590
|
+
* field was PRESENT and records nothing about its shape, and a comparison
|
|
591
|
+
* written against a guessed shape silently matches nothing, which is worse than
|
|
592
|
+
* the check it pretends to be. It becomes usable once a probe captures it.
|
|
593
|
+
*/
|
|
594
|
+
export type BoundCommand = {
|
|
595
|
+
ok: true;
|
|
596
|
+
command: string;
|
|
597
|
+
argv: string[];
|
|
598
|
+
source: "rendering" | "argv";
|
|
599
|
+
} | {
|
|
600
|
+
ok: false;
|
|
601
|
+
reason: string;
|
|
602
|
+
};
|
|
603
|
+
export declare function bindCommand(params: unknown): BoundCommand | null;
|
|
604
|
+
/**
|
|
605
|
+
* The turn a frame belongs to, where it names one (APRV-364).
|
|
606
|
+
*
|
|
607
|
+
* The preflight and the real turn are told apart by this value, and a frame
|
|
608
|
+
* that names no turn is decided by which turn is running instead. Both
|
|
609
|
+
* spellings are read because the protocol has used both casings elsewhere and
|
|
610
|
+
* neither reading can widen anything: a turn id is only ever used to decide
|
|
611
|
+
* which of two phases a frame belongs to.
|
|
612
|
+
*/
|
|
613
|
+
export declare function turnIdOf(params: unknown): string | null;
|
|
614
|
+
/**
|
|
615
|
+
* Is this method one of Codex's auto-approval-review notifications (APRV-364)?
|
|
616
|
+
*
|
|
617
|
+
* The recorded names are `item/autoApprovalReview/started` and
|
|
618
|
+
* `item/autoApprovalReview/completed`
|
|
619
|
+
* (`docs/codex-app-server-bridge.md`, question 3). The match is on the
|
|
620
|
+
* SUBSTRING rather than on those two exact names, case-folded, because a
|
|
621
|
+
* reviewer notification this runtime failed to recognise would be a session
|
|
622
|
+
* that ran with a reviewer in front of the gate: over-matching costs a stop
|
|
623
|
+
* that an operator can read and re-run, and under-matching costs the whole
|
|
624
|
+
* point of the check.
|
|
625
|
+
*/
|
|
626
|
+
export declare function isAutoReviewNotification(method: string): boolean;
|
|
627
|
+
/**
|
|
628
|
+
* The verdict an auto-review notification states, or `null` (APRV-378).
|
|
629
|
+
*
|
|
630
|
+
* Read from a short list of named places rather than by a generic walk, for the
|
|
631
|
+
* reason {@link effectiveApprovalPolicy} is: this value goes into the log as
|
|
632
|
+
* another party's decision, and a string picked up from some unrelated
|
|
633
|
+
* structure would be this runtime putting words in their mouth. `null` is
|
|
634
|
+
* recorded as an ABSENT verdict, never as a default one.
|
|
635
|
+
*/
|
|
636
|
+
export declare function autoReviewVerdict(params: unknown): string | null;
|
|
637
|
+
/**
|
|
638
|
+
* Does this notification say a COMMAND was executed (APRV-364)?
|
|
639
|
+
*
|
|
640
|
+
* The one reader in this file written against a shape nobody recorded in full.
|
|
641
|
+
* The 2026-09-18 vocabulary carries `item/started` and `item/completed`, and it
|
|
642
|
+
* does not record the item object they carry, so this looks for an item whose
|
|
643
|
+
* type reads as a command execution, or, failing that, for an item carrying a
|
|
644
|
+
* `command` string.
|
|
645
|
+
*
|
|
646
|
+
* That is a guess, and the reason it is an acceptable one is the direction it
|
|
647
|
+
* fails in. This value only ever chooses BETWEEN TWO STOPS: a preflight turn
|
|
648
|
+
* where a command ran without asking stops under
|
|
649
|
+
* `bridge-approval-policy-mismatch`, and one where nothing ran stops under
|
|
650
|
+
* `bridge-preflight-void`. A guess that misses turns the first into the
|
|
651
|
+
* second; it can never turn either into a pass, because a pass needs an
|
|
652
|
+
* approval request to have ARRIVED, which is a frame this client was handed
|
|
653
|
+
* rather than one it went looking for. The void report carries the frames
|
|
654
|
+
* verbatim, which is also how the real item shape gets recorded here at last.
|
|
655
|
+
*/
|
|
656
|
+
export declare function namesCommandExecution(params: unknown): boolean;
|
|
657
|
+
/** What the verb was asked to do, once the flags are read. */
|
|
658
|
+
interface BridgePlan {
|
|
659
|
+
logPath: string;
|
|
660
|
+
root: string;
|
|
661
|
+
options: ReturnType<typeof hookScope>["options"];
|
|
662
|
+
actor: string;
|
|
663
|
+
workspace: string;
|
|
664
|
+
prompt: string;
|
|
665
|
+
waitMs: number;
|
|
666
|
+
intervalMs: number;
|
|
667
|
+
serverCommand: string;
|
|
668
|
+
serverArgs: string[];
|
|
669
|
+
json: boolean;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Decide one exec approval request through the gate (AC2, AC3).
|
|
673
|
+
*
|
|
674
|
+
* Exported for the tests, which drive it without a server so the DECISION can
|
|
675
|
+
* be asserted apart from the transport.
|
|
676
|
+
*/
|
|
677
|
+
export declare function decideExecRequest(streams: Streams, plan: BridgePlan, params: unknown): {
|
|
678
|
+
verdict: HarnessVerdict;
|
|
679
|
+
threadId: string | null;
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* One item this thread's server has told this client about (APRV-379).
|
|
683
|
+
*
|
|
684
|
+
* Every `item/started` is recorded, whatever its type, and not only the
|
|
685
|
+
* file-change ones. That is what lets "an id this client never saw" be told
|
|
686
|
+
* from "an id that names a `userMessage`": the first is a client that missed a
|
|
687
|
+
* frame and the second is a server request that points at the wrong thing, and
|
|
688
|
+
* an operator reading one refusal should not have to guess which happened.
|
|
689
|
+
*/
|
|
690
|
+
export interface RecordedItem {
|
|
691
|
+
id: string;
|
|
692
|
+
/** `item.type` as the server spelled it, or `null` where it named none. */
|
|
693
|
+
type: string | null;
|
|
694
|
+
/** The change set VERBATIM, for a `fileChange` item that carried one. */
|
|
695
|
+
changes: readonly unknown[] | null;
|
|
696
|
+
threadId: string | null;
|
|
697
|
+
turnId: string | null;
|
|
698
|
+
/** Has `item/completed` for this item already arrived? */
|
|
699
|
+
completed: boolean;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Every item this thread has announced, by item id.
|
|
703
|
+
*
|
|
704
|
+
* Kept for the LIFE OF THE THREAD rather than cleared at each turn's end,
|
|
705
|
+
* because the frame that carries the content and the request that asks about it
|
|
706
|
+
* are two frames and nothing in the protocol promises they share a turn. It
|
|
707
|
+
* grows with the number of items a session produces, which is the session's own
|
|
708
|
+
* size; a bridge that dropped entries to stay small would be a bridge that
|
|
709
|
+
* refuses a change it was told about, and the refusal would look exactly like a
|
|
710
|
+
* protocol it had not caught up with.
|
|
711
|
+
*/
|
|
712
|
+
export type ItemIndex = Map<string, RecordedItem>;
|
|
713
|
+
/**
|
|
714
|
+
* The change set an `item/started` frame carries for a `fileChange` item, or
|
|
715
|
+
* `null` (APRV-379).
|
|
716
|
+
*
|
|
717
|
+
* The observed shape is an ARRAY of `{path, kind, diff}` under `item.changes`
|
|
718
|
+
* (`docs/codex-app-server-bridge.md`, question 1). It is read as an array and
|
|
719
|
+
* carried whole; nothing here looks inside an entry, because the classifier
|
|
720
|
+
* reads the paths and the payload binds the bytes, and a second reader of the
|
|
721
|
+
* same material in this module would be a second account of one change.
|
|
722
|
+
*/
|
|
723
|
+
export declare function itemChanges(item: Record<string, unknown>): readonly unknown[] | null;
|
|
724
|
+
/**
|
|
725
|
+
* Record what an `item/started` or `item/completed` notification says
|
|
726
|
+
* (APRV-379).
|
|
727
|
+
*
|
|
728
|
+
* `item/started` writes the entry, `item/completed` marks it completed and
|
|
729
|
+
* leaves the recorded content ALONE. Refreshing the change set from the
|
|
730
|
+
* completion frame would let a server hand this client one change set, be asked
|
|
731
|
+
* about it, and have a different one in the record afterwards; the frame this
|
|
732
|
+
* client decides against is the one it was holding when the question arrived.
|
|
733
|
+
*/
|
|
734
|
+
export declare function recordItemFrame(index: ItemIndex, method: string, params: unknown): void;
|
|
735
|
+
/** What the correlation produced, or why it produced nothing. */
|
|
736
|
+
export type Correlation = {
|
|
737
|
+
ok: true;
|
|
738
|
+
item: RecordedItem;
|
|
739
|
+
changes: readonly unknown[];
|
|
740
|
+
} | {
|
|
741
|
+
ok: false;
|
|
742
|
+
code: BridgeRefusalCode;
|
|
743
|
+
detail: string;
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
746
|
+
* Find the `item/started` frame an item-based file-change request refers to
|
|
747
|
+
* (APRV-379).
|
|
748
|
+
*
|
|
749
|
+
* THE WHOLE RISK OF THIS TASK LIVES HERE. The request names an identifier, the
|
|
750
|
+
* bytes arrived on another frame, and a correlation that matched the wrong item
|
|
751
|
+
* would let a grant authorize bytes nobody classified. So every way the two
|
|
752
|
+
* frames could fail to be about the same change is a refusal, and none of them
|
|
753
|
+
* is resolved in favour of going ahead:
|
|
754
|
+
*
|
|
755
|
+
* - no `item/started` for that id was ever seen: `bridge-file-change-unbound`,
|
|
756
|
+
* which is the refusal this verb has answered since APRV-361;
|
|
757
|
+
* - the id names an item that is not a `fileChange`: same code, its own detail.
|
|
758
|
+
* An id that points at a `userMessage` is a request this client has no
|
|
759
|
+
* content for, however much content that item has;
|
|
760
|
+
* - the item carried no readable change set: same code. A `fileChange` frame
|
|
761
|
+
* with nothing in `changes` is an identifier again;
|
|
762
|
+
* - the request names a THREAD or a TURN the frame does not: same code.
|
|
763
|
+
* Item ids are server-minted and observed unique, so this should never fire,
|
|
764
|
+
* and that is exactly why it is checked rather than assumed. Two frames that
|
|
765
|
+
* disagree about which conversation they belong to are not established to be
|
|
766
|
+
* about one change, and "should never happen" is the reasoning that lets a
|
|
767
|
+
* wrong match through. A frame or a request that names NEITHER field is not
|
|
768
|
+
* held to it: absence is not disagreement, and the observed frames carry
|
|
769
|
+
* both;
|
|
770
|
+
* - the item already COMPLETED: `bridge-file-change-already-completed`, for the
|
|
771
|
+
* reasons that code carries.
|
|
772
|
+
*/
|
|
773
|
+
export declare function correlateFileChange(index: ItemIndex, params: unknown): Correlation;
|
|
774
|
+
/**
|
|
775
|
+
* The change map a file-change request carries INLINE, or `null` (APRV-363).
|
|
776
|
+
*
|
|
777
|
+
* The LEGACY `applyPatchApproval` carries `fileChanges`, a map of path to
|
|
778
|
+
* change, on the request itself. There is nothing to correlate and nothing
|
|
779
|
+
* arrives on another frame, so it is the one file-change shape this verb can
|
|
780
|
+
* bind: the bytes it decides about are the bytes it was sent.
|
|
781
|
+
*/
|
|
782
|
+
export declare function inlineFileChanges(params: unknown): Record<string, unknown> | null;
|
|
783
|
+
/**
|
|
784
|
+
* Decide one file-change request, whichever API it arrived on (APRV-363,
|
|
785
|
+
* APRV-379).
|
|
786
|
+
*
|
|
787
|
+
* Through the SAME path an exec request takes: the hook's `decideHarnessCall`,
|
|
788
|
+
* so the human-only refusal, the loop floor, the unattended guard, the
|
|
789
|
+
* registration and the wait are one implementation. What differs is the tool
|
|
790
|
+
* name and the bound material, and the description of both is `cli/hook.ts`'s,
|
|
791
|
+
* never this module's.
|
|
792
|
+
*
|
|
793
|
+
* TWO SOURCES FOR THE CHANGE SET, one decision path. The LEGACY
|
|
794
|
+
* `applyPatchApproval` carries it inline, so it is read off the request. The
|
|
795
|
+
* ITEM-BASED `item/fileChange/requestApproval` carries an identifier, so it is
|
|
796
|
+
* correlated to the `item/started` frame this client recorded, by
|
|
797
|
+
* {@link correlateFileChange}, which refuses rather than guessing. Either way
|
|
798
|
+
* what reaches the describer is the change set the server sent, in the shape it
|
|
799
|
+
* sent it.
|
|
800
|
+
*
|
|
801
|
+
* THE DIRECTORY, and the two halves differ here for a recorded reason. The
|
|
802
|
+
* legacy request carries one (`cwd`, or `grantRoot` for the same purpose) and
|
|
803
|
+
* is refused without it, exactly as APRV-363 left it. The item-based request
|
|
804
|
+
* carries neither: `grantRoot` was `null` in both captures and there is no
|
|
805
|
+
* `cwd` on that shape at all (`docs/codex-app-server-bridge.md`, question 1).
|
|
806
|
+
* So it falls back to the workspace THIS CLIENT named on `thread/start`, which
|
|
807
|
+
* is this client's own binding rather than a guess or a server claim, and the
|
|
808
|
+
* fallback widens nothing: the observed change paths are absolute, the
|
|
809
|
+
* describer resolves every path against that directory, and one landing
|
|
810
|
+
* outside it is refused `hook-io` whichever way it was spelled.
|
|
811
|
+
*/
|
|
812
|
+
export declare function decideFileChangeRequest(streams: Streams, plan: BridgePlan, params: unknown, items: ItemIndex): {
|
|
813
|
+
verdict: HarnessVerdict;
|
|
814
|
+
threadId: string | null;
|
|
815
|
+
};
|
|
816
|
+
export declare function runCodexBridge(argv: string[], streams: Streams, cwd: string): Promise<number>;
|
|
817
|
+
/** The help text, printed by `approval codex bridge --help`. */
|
|
818
|
+
export declare const CODEX_BRIDGE_HELP: string;
|
|
819
|
+
export {};
|