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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The confined Codex session runner (APRV-325.3).
|
|
3
|
+
*
|
|
4
|
+
* APRV-325.2 made the canonical workspace writable through one door. That is
|
|
5
|
+
* worth nothing on its own: a door beside an open window is decoration. This
|
|
6
|
+
* module is the window being closed — the shell a Codex session runs gets a
|
|
7
|
+
* DISPOSABLE workspace of its own, no write authority over the canonical one,
|
|
8
|
+
* no write authority over the gate, no ambient credentials, no egress, and no
|
|
9
|
+
* way to ask for any of it back.
|
|
10
|
+
*
|
|
11
|
+
* ## Five removals, and where each is enforced
|
|
12
|
+
*
|
|
13
|
+
* | what the shell does not get | enforced by |
|
|
14
|
+
* |---|---|
|
|
15
|
+
* | canonical workspace writes | Seatbelt `(deny file-write*)` with an allow-list that names only the disposable workspace |
|
|
16
|
+
* | gate writes (log, policy, vault, keys) | the same deny; the gate home is not on the allow-list |
|
|
17
|
+
* | reads of anything but the two workspaces | Seatbelt `(deny file-read*)` with an allow-list of two roots (APRV-347's read jail) |
|
|
18
|
+
* | ambient credentials | `core/child-env.ts`, then {@link CONFINED_ENV_ALLOW} |
|
|
19
|
+
* | credential material on disk | Seatbelt `denyRead` over the vault, the env map and the sealing keys, emitted after the jail's allows so it is the last word |
|
|
20
|
+
* | external egress | Seatbelt `(deny network-outbound)`, loopback included |
|
|
21
|
+
* | mutable executor code | the broker, the CLI and the pinned Node live under the root-owned install root, which is on neither allow-list |
|
|
22
|
+
*
|
|
23
|
+
* ## No opt-out, and no raw fallback
|
|
24
|
+
*
|
|
25
|
+
* `approval run` has `--no-sandbox`, because an operator holding a human's grant
|
|
26
|
+
* may deliberately reach the world. A confined session has no such flag and no
|
|
27
|
+
* such path: {@link planConfinedSession} REFUSES when the host cannot apply a
|
|
28
|
+
* profile, where `core/sandbox.ts`'s ordinary posture table would record
|
|
29
|
+
* `unsupported` and proceed. That difference is the whole point — an
|
|
30
|
+
* unsupported host that ran the shell anyway would be a session advertised as
|
|
31
|
+
* confined and not confined, which is worse than no session at all.
|
|
32
|
+
* `APPROVAL_SANDBOX_FORCE_UNAVAILABLE` and `APPROVAL_SANDBOX_REQUIRED` still
|
|
33
|
+
* only tighten, so neither is a way in.
|
|
34
|
+
*
|
|
35
|
+
* ## What it does not claim
|
|
36
|
+
*
|
|
37
|
+
* This confines a CHILD PROCESS on macOS. It is not isolation and it is not a
|
|
38
|
+
* claim about the Codex desktop application, which runs outside anything this
|
|
39
|
+
* runtime spawns; `docs/codex-boundary-probe.md` holds what was measured there.
|
|
40
|
+
* Inbound sockets are not denied (`core/sandbox.ts`'s stated limit), and a
|
|
41
|
+
* confined child can still write inside its own disposable workspace, which is
|
|
42
|
+
* the point of giving it one.
|
|
43
|
+
*/
|
|
44
|
+
import { type DetectOptions, type SandboxMechanism } from "../core/sandbox.js";
|
|
45
|
+
import type { BrokerInstallation } from "./broker.js";
|
|
46
|
+
export declare const CONFINED_SESSION_VERSION: "approval.codex.confined-session.v1";
|
|
47
|
+
/**
|
|
48
|
+
* The ONLY environment variables a confined session passes through.
|
|
49
|
+
*
|
|
50
|
+
* An ALLOW-list, and for the reason the write rules are one. `core/child-env.ts`
|
|
51
|
+
* strips a named family (`APPROVAL_`, `TELEGRAM_`, `VAULT_`, `AGENTMAIL_`) and
|
|
52
|
+
* that is right for `approval run`, which runs a command a human approved and
|
|
53
|
+
* whose environment is the operator's own. It is not enough here: a Codex
|
|
54
|
+
* session's host carries `OPENAI_API_KEY`, `GITHUB_TOKEN`, `AWS_SECRET_ACCESS_KEY`,
|
|
55
|
+
* an `SSH_AUTH_SOCK` and whatever else the operator's shell exports, and none of
|
|
56
|
+
* those is under a prefix this runtime knows. A deny-list would have to keep up
|
|
57
|
+
* with every provider anyone ever adds; this names the handful a shell needs to
|
|
58
|
+
* function, so a credential nobody thought of is absent rather than forgotten.
|
|
59
|
+
*
|
|
60
|
+
* `core/child-env.ts` still runs FIRST, so its credential-bearing count is
|
|
61
|
+
* reported on the same terms as everywhere else in the runtime. This list then
|
|
62
|
+
* narrows what survives; it can only ever remove more.
|
|
63
|
+
*
|
|
64
|
+
* This is a control over NAMES and therefore best-effort by construction: a
|
|
65
|
+
* credential exported under a name on this list still passes. The load-bearing
|
|
66
|
+
* control beside it is that egress is denied, so a secret that does reach the
|
|
67
|
+
* child has nowhere to go.
|
|
68
|
+
*/
|
|
69
|
+
export declare const CONFINED_ENV_ALLOW: readonly string[];
|
|
70
|
+
/** How long a confined command may run before it is killed. */
|
|
71
|
+
export declare const DEFAULT_CONFINED_TIMEOUT_MS = 120000;
|
|
72
|
+
/**
|
|
73
|
+
* Why a confined session could not be prepared or run. Frozen, distinct, and
|
|
74
|
+
* pinned by `tests/codex-confine.test.ts` (SPEC.md §11.1 invariant 6).
|
|
75
|
+
*/
|
|
76
|
+
export declare const CONFINE_REFUSAL_CODES: readonly [
|
|
77
|
+
/** This host has no sandbox mechanism in this build. A confined session refuses. */
|
|
78
|
+
"sandbox-unsupported",
|
|
79
|
+
/** The mechanism exists and did not work. */
|
|
80
|
+
"sandbox-unavailable",
|
|
81
|
+
/** The disposable workspace could not be created. */
|
|
82
|
+
"workspace-unavailable",
|
|
83
|
+
/** The command could not be resolved, so it was never wrapped. */
|
|
84
|
+
"command-unresolvable",
|
|
85
|
+
/** The command was killed at the deadline. No canonical effect is possible. */
|
|
86
|
+
"timeout"];
|
|
87
|
+
export type ConfineRefusalCode = (typeof CONFINE_REFUSAL_CODES)[number];
|
|
88
|
+
export interface ConfineRefusal {
|
|
89
|
+
ok: false;
|
|
90
|
+
code: ConfineRefusalCode;
|
|
91
|
+
message: string;
|
|
92
|
+
}
|
|
93
|
+
export interface ConfinedSession {
|
|
94
|
+
version: typeof CONFINED_SESSION_VERSION;
|
|
95
|
+
/** The scratch tree the shell may write. Removed by {@link ConfinedSession.dispose}. */
|
|
96
|
+
workspace: string;
|
|
97
|
+
/** The canonical workspace: readable, never writable from inside. */
|
|
98
|
+
canonical: string;
|
|
99
|
+
mechanism: SandboxMechanism;
|
|
100
|
+
/** Absolute subtrees the profile allows writes to. */
|
|
101
|
+
writeAllow: readonly string[];
|
|
102
|
+
/**
|
|
103
|
+
* Absolute directories the profile allows reads of (APRV-347's read jail),
|
|
104
|
+
* with everything else denied.
|
|
105
|
+
*
|
|
106
|
+
* Two roots, and no more: the disposable workspace, and the canonical
|
|
107
|
+
* workspace. A session has to READ the tree it is reasoning about, which is
|
|
108
|
+
* why the canonical workspace is here and why write confinement rather than
|
|
109
|
+
* read denial is what stops it changing one. Everything the operator's home
|
|
110
|
+
* holds beside it — other repositories, other credentials, `~/.ssh`, the gate
|
|
111
|
+
* home — is outside the jail and unreadable, which is the half `denyRead`
|
|
112
|
+
* alone could never cover, because a deny-list has to have heard of a path to
|
|
113
|
+
* deny it.
|
|
114
|
+
*
|
|
115
|
+
* `core/sandbox.ts` adds the fixed runtime set and the executable's own
|
|
116
|
+
* install prefix itself, so nothing here has to name a toolchain.
|
|
117
|
+
*/
|
|
118
|
+
readAllow: readonly string[];
|
|
119
|
+
/** Absolute paths the profile denies reads of, inside the jail or out. */
|
|
120
|
+
denyRead: readonly string[];
|
|
121
|
+
/** The child's environment: {@link CONFINED_ENV_ALLOW} and the session's own. */
|
|
122
|
+
env: Record<string, string>;
|
|
123
|
+
/**
|
|
124
|
+
* How many credential-bearing variables `core/child-env.ts` withheld. A
|
|
125
|
+
* COUNT, never a name: a name is half of a credential, and SPEC.md §11.1's
|
|
126
|
+
* raw-secrets invariant is not satisfied by leaking the other half slowly.
|
|
127
|
+
*/
|
|
128
|
+
envStripped: number;
|
|
129
|
+
/** How many source variables did not survive the allow-list, in total. */
|
|
130
|
+
envWithheld: number;
|
|
131
|
+
/** Remove the disposable workspace. Idempotent, never throws. */
|
|
132
|
+
dispose: () => void;
|
|
133
|
+
}
|
|
134
|
+
export interface ConfineOptions {
|
|
135
|
+
/** Forwarded to `core/sandbox.ts`'s probe. Tests inject a platform here. */
|
|
136
|
+
detect?: DetectOptions;
|
|
137
|
+
/** The environment to derive the child's from. Defaults to this process's. */
|
|
138
|
+
source?: NodeJS.ProcessEnv;
|
|
139
|
+
/** Where the disposable workspace is created. Defaults to `os.tmpdir()`. */
|
|
140
|
+
scratchRoot?: string;
|
|
141
|
+
}
|
|
142
|
+
export type ConfinedSessionResult = {
|
|
143
|
+
ok: true;
|
|
144
|
+
session: ConfinedSession;
|
|
145
|
+
} | ConfineRefusal;
|
|
146
|
+
/**
|
|
147
|
+
* Prepare one confined session, or refuse.
|
|
148
|
+
*
|
|
149
|
+
* The refusal on an unsupported host is deliberate and is the one place this
|
|
150
|
+
* module is STRICTER than `core/sandbox.ts`'s posture table. See the header.
|
|
151
|
+
*/
|
|
152
|
+
export declare function planConfinedSession(installation: BrokerInstallation, options?: ConfineOptions): ConfinedSessionResult;
|
|
153
|
+
export interface ConfinedRun {
|
|
154
|
+
ok: true;
|
|
155
|
+
/** The child's own exit code, or `128 + signal` when a signal killed it. */
|
|
156
|
+
exitCode: number;
|
|
157
|
+
stdout: string;
|
|
158
|
+
stderr: string;
|
|
159
|
+
/** True when the deadline killed it. The exit code is then the signal's. */
|
|
160
|
+
timedOut: boolean;
|
|
161
|
+
}
|
|
162
|
+
export type ConfinedRunResult = ConfinedRun | ConfineRefusal;
|
|
163
|
+
export interface ConfinedRunOptions {
|
|
164
|
+
timeoutMs?: number;
|
|
165
|
+
/** Working directory for the child. Defaults to the disposable workspace. */
|
|
166
|
+
cwd?: string;
|
|
167
|
+
maxBuffer?: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Run one command inside a prepared session.
|
|
171
|
+
*
|
|
172
|
+
* The command is resolved BEFORE it is wrapped, for `core/sandbox.ts`'s reason:
|
|
173
|
+
* an `execvp` failure inside `sandbox-exec` exits 71, and 71 recorded as the
|
|
174
|
+
* child's exit code is a lie about a command that never ran. Here it is a
|
|
175
|
+
* refusal instead of an unwrapped spawn, because an unwrapped spawn is exactly
|
|
176
|
+
* the raw fallback this session must not have.
|
|
177
|
+
*/
|
|
178
|
+
export declare function runConfined(session: ConfinedSession, command: string, args: readonly string[], options?: ConfinedRunOptions): ConfinedRunResult;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The confined Codex session runner (APRV-325.3).
|
|
3
|
+
*
|
|
4
|
+
* APRV-325.2 made the canonical workspace writable through one door. That is
|
|
5
|
+
* worth nothing on its own: a door beside an open window is decoration. This
|
|
6
|
+
* module is the window being closed — the shell a Codex session runs gets a
|
|
7
|
+
* DISPOSABLE workspace of its own, no write authority over the canonical one,
|
|
8
|
+
* no write authority over the gate, no ambient credentials, no egress, and no
|
|
9
|
+
* way to ask for any of it back.
|
|
10
|
+
*
|
|
11
|
+
* ## Five removals, and where each is enforced
|
|
12
|
+
*
|
|
13
|
+
* | what the shell does not get | enforced by |
|
|
14
|
+
* |---|---|
|
|
15
|
+
* | canonical workspace writes | Seatbelt `(deny file-write*)` with an allow-list that names only the disposable workspace |
|
|
16
|
+
* | gate writes (log, policy, vault, keys) | the same deny; the gate home is not on the allow-list |
|
|
17
|
+
* | reads of anything but the two workspaces | Seatbelt `(deny file-read*)` with an allow-list of two roots (APRV-347's read jail) |
|
|
18
|
+
* | ambient credentials | `core/child-env.ts`, then {@link CONFINED_ENV_ALLOW} |
|
|
19
|
+
* | credential material on disk | Seatbelt `denyRead` over the vault, the env map and the sealing keys, emitted after the jail's allows so it is the last word |
|
|
20
|
+
* | external egress | Seatbelt `(deny network-outbound)`, loopback included |
|
|
21
|
+
* | mutable executor code | the broker, the CLI and the pinned Node live under the root-owned install root, which is on neither allow-list |
|
|
22
|
+
*
|
|
23
|
+
* ## No opt-out, and no raw fallback
|
|
24
|
+
*
|
|
25
|
+
* `approval run` has `--no-sandbox`, because an operator holding a human's grant
|
|
26
|
+
* may deliberately reach the world. A confined session has no such flag and no
|
|
27
|
+
* such path: {@link planConfinedSession} REFUSES when the host cannot apply a
|
|
28
|
+
* profile, where `core/sandbox.ts`'s ordinary posture table would record
|
|
29
|
+
* `unsupported` and proceed. That difference is the whole point — an
|
|
30
|
+
* unsupported host that ran the shell anyway would be a session advertised as
|
|
31
|
+
* confined and not confined, which is worse than no session at all.
|
|
32
|
+
* `APPROVAL_SANDBOX_FORCE_UNAVAILABLE` and `APPROVAL_SANDBOX_REQUIRED` still
|
|
33
|
+
* only tighten, so neither is a way in.
|
|
34
|
+
*
|
|
35
|
+
* ## What it does not claim
|
|
36
|
+
*
|
|
37
|
+
* This confines a CHILD PROCESS on macOS. It is not isolation and it is not a
|
|
38
|
+
* claim about the Codex desktop application, which runs outside anything this
|
|
39
|
+
* runtime spawns; `docs/codex-boundary-probe.md` holds what was measured there.
|
|
40
|
+
* Inbound sockets are not denied (`core/sandbox.ts`'s stated limit), and a
|
|
41
|
+
* confined child can still write inside its own disposable workspace, which is
|
|
42
|
+
* the point of giving it one.
|
|
43
|
+
*/
|
|
44
|
+
import { spawnSync } from "node:child_process";
|
|
45
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
46
|
+
import { tmpdir } from "node:os";
|
|
47
|
+
import { join } from "node:path";
|
|
48
|
+
import { childEnvironment } from "../core/child-env.js";
|
|
49
|
+
import { credentialPathsFor, detectSandbox, resolveExecutable, wrapForSandbox, } from "../core/sandbox.js";
|
|
50
|
+
export const CONFINED_SESSION_VERSION = "approval.codex.confined-session.v1";
|
|
51
|
+
/**
|
|
52
|
+
* The ONLY environment variables a confined session passes through.
|
|
53
|
+
*
|
|
54
|
+
* An ALLOW-list, and for the reason the write rules are one. `core/child-env.ts`
|
|
55
|
+
* strips a named family (`APPROVAL_`, `TELEGRAM_`, `VAULT_`, `AGENTMAIL_`) and
|
|
56
|
+
* that is right for `approval run`, which runs a command a human approved and
|
|
57
|
+
* whose environment is the operator's own. It is not enough here: a Codex
|
|
58
|
+
* session's host carries `OPENAI_API_KEY`, `GITHUB_TOKEN`, `AWS_SECRET_ACCESS_KEY`,
|
|
59
|
+
* an `SSH_AUTH_SOCK` and whatever else the operator's shell exports, and none of
|
|
60
|
+
* those is under a prefix this runtime knows. A deny-list would have to keep up
|
|
61
|
+
* with every provider anyone ever adds; this names the handful a shell needs to
|
|
62
|
+
* function, so a credential nobody thought of is absent rather than forgotten.
|
|
63
|
+
*
|
|
64
|
+
* `core/child-env.ts` still runs FIRST, so its credential-bearing count is
|
|
65
|
+
* reported on the same terms as everywhere else in the runtime. This list then
|
|
66
|
+
* narrows what survives; it can only ever remove more.
|
|
67
|
+
*
|
|
68
|
+
* This is a control over NAMES and therefore best-effort by construction: a
|
|
69
|
+
* credential exported under a name on this list still passes. The load-bearing
|
|
70
|
+
* control beside it is that egress is denied, so a secret that does reach the
|
|
71
|
+
* child has nowhere to go.
|
|
72
|
+
*/
|
|
73
|
+
export const CONFINED_ENV_ALLOW = [
|
|
74
|
+
"PATH",
|
|
75
|
+
"HOME",
|
|
76
|
+
"SHELL",
|
|
77
|
+
"USER",
|
|
78
|
+
"LOGNAME",
|
|
79
|
+
"LANG",
|
|
80
|
+
"LC_ALL",
|
|
81
|
+
"LC_CTYPE",
|
|
82
|
+
"TERM",
|
|
83
|
+
"LINES",
|
|
84
|
+
"COLUMNS",
|
|
85
|
+
];
|
|
86
|
+
/** How long a confined command may run before it is killed. */
|
|
87
|
+
export const DEFAULT_CONFINED_TIMEOUT_MS = 120_000;
|
|
88
|
+
/**
|
|
89
|
+
* Why a confined session could not be prepared or run. Frozen, distinct, and
|
|
90
|
+
* pinned by `tests/codex-confine.test.ts` (SPEC.md §11.1 invariant 6).
|
|
91
|
+
*/
|
|
92
|
+
export const CONFINE_REFUSAL_CODES = [
|
|
93
|
+
/** This host has no sandbox mechanism in this build. A confined session refuses. */
|
|
94
|
+
"sandbox-unsupported",
|
|
95
|
+
/** The mechanism exists and did not work. */
|
|
96
|
+
"sandbox-unavailable",
|
|
97
|
+
/** The disposable workspace could not be created. */
|
|
98
|
+
"workspace-unavailable",
|
|
99
|
+
/** The command could not be resolved, so it was never wrapped. */
|
|
100
|
+
"command-unresolvable",
|
|
101
|
+
/** The command was killed at the deadline. No canonical effect is possible. */
|
|
102
|
+
"timeout",
|
|
103
|
+
];
|
|
104
|
+
function refuse(code, message) {
|
|
105
|
+
return { ok: false, code, message };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Prepare one confined session, or refuse.
|
|
109
|
+
*
|
|
110
|
+
* The refusal on an unsupported host is deliberate and is the one place this
|
|
111
|
+
* module is STRICTER than `core/sandbox.ts`'s posture table. See the header.
|
|
112
|
+
*/
|
|
113
|
+
export function planConfinedSession(installation, options = {}) {
|
|
114
|
+
const detection = detectSandbox(options.detect ?? {});
|
|
115
|
+
if (!detection.supported) {
|
|
116
|
+
return refuse("sandbox-unsupported", `a confined Codex session needs a sandbox mechanism and this build has none for this host: ${detection.reason}. Unlike \`approval run\`, this verb does not proceed and record \`unsupported\`: a session advertised as confined and not confined is worse than no session`);
|
|
117
|
+
}
|
|
118
|
+
if (!detection.available || detection.mechanism === null) {
|
|
119
|
+
return refuse("sandbox-unavailable", `the sandbox mechanism is present and did not work: ${detection.reason}. There is no opt-out and no raw fallback`);
|
|
120
|
+
}
|
|
121
|
+
let workspace;
|
|
122
|
+
try {
|
|
123
|
+
const root = options.scratchRoot === undefined ? tmpdir() : options.scratchRoot;
|
|
124
|
+
workspace = realpathSync(mkdtempSync(join(root, "approval-codex-session-")));
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
return refuse("workspace-unavailable", `the disposable session workspace could not be created: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
128
|
+
}
|
|
129
|
+
const source = options.source ?? process.env;
|
|
130
|
+
const { env: filtered, stripped } = childEnvironment({ source });
|
|
131
|
+
// Then the allow-list, which can only ever remove more. See CONFINED_ENV_ALLOW.
|
|
132
|
+
const env = {};
|
|
133
|
+
for (const name of CONFINED_ENV_ALLOW) {
|
|
134
|
+
const value = filtered[name];
|
|
135
|
+
if (value !== undefined)
|
|
136
|
+
env[name] = value;
|
|
137
|
+
}
|
|
138
|
+
const withheld = Object.values(source).filter((value) => value !== undefined).length -
|
|
139
|
+
Object.keys(env).length;
|
|
140
|
+
// The child's own view of "somewhere to put temporary files" is inside the
|
|
141
|
+
// room. Left alone it would point at a directory the profile denies, and a
|
|
142
|
+
// shell whose TMPDIR is unwritable fails in ways that look like the sandbox
|
|
143
|
+
// being broken rather than doing its job.
|
|
144
|
+
env["TMPDIR"] = workspace;
|
|
145
|
+
env["APPROVAL_CODEX_SESSION_WORKSPACE"] = workspace;
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
session: {
|
|
149
|
+
version: CONFINED_SESSION_VERSION,
|
|
150
|
+
workspace,
|
|
151
|
+
canonical: installation.root,
|
|
152
|
+
mechanism: detection.mechanism,
|
|
153
|
+
// The ONLY writable subtree. The canonical workspace, the gate home, the
|
|
154
|
+
// install root and the rest of the filesystem are absent, which is what
|
|
155
|
+
// makes this an allow-list rather than a wish.
|
|
156
|
+
writeAllow: [workspace],
|
|
157
|
+
readAllow: [workspace, installation.root],
|
|
158
|
+
denyRead: credentialPathsFor(installation.logPath),
|
|
159
|
+
env,
|
|
160
|
+
envStripped: stripped,
|
|
161
|
+
envWithheld: Math.max(0, withheld),
|
|
162
|
+
dispose: () => {
|
|
163
|
+
try {
|
|
164
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// A scratch directory that outlives the session is visible to a person
|
|
168
|
+
// and costs nothing; failing to remove it must not fail the session.
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Run one command inside a prepared session.
|
|
176
|
+
*
|
|
177
|
+
* The command is resolved BEFORE it is wrapped, for `core/sandbox.ts`'s reason:
|
|
178
|
+
* an `execvp` failure inside `sandbox-exec` exits 71, and 71 recorded as the
|
|
179
|
+
* child's exit code is a lie about a command that never ran. Here it is a
|
|
180
|
+
* refusal instead of an unwrapped spawn, because an unwrapped spawn is exactly
|
|
181
|
+
* the raw fallback this session must not have.
|
|
182
|
+
*/
|
|
183
|
+
export function runConfined(session, command, args, options = {}) {
|
|
184
|
+
const resolved = resolveExecutable(command, session.env);
|
|
185
|
+
if (resolved === null) {
|
|
186
|
+
return refuse("command-unresolvable", `${JSON.stringify(command)} could not be resolved on the session's PATH. It was NOT spawned unwrapped: a confined session has no raw fallback`);
|
|
187
|
+
}
|
|
188
|
+
const wrapped = wrapForSandbox(session.mechanism, resolved, args, {
|
|
189
|
+
loopback: false,
|
|
190
|
+
denyRead: session.denyRead,
|
|
191
|
+
writeAllow: session.writeAllow,
|
|
192
|
+
allowRead: session.readAllow,
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
const result = spawnSync(wrapped.command, wrapped.args, {
|
|
196
|
+
cwd: options.cwd ?? session.workspace,
|
|
197
|
+
env: session.env,
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
timeout: options.timeoutMs ?? DEFAULT_CONFINED_TIMEOUT_MS,
|
|
200
|
+
maxBuffer: options.maxBuffer ?? 4 * 1024 * 1024,
|
|
201
|
+
});
|
|
202
|
+
const timedOut = result.signal !== null && result.error !== undefined;
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
exitCode: exitCodeOf(result.status, result.signal),
|
|
206
|
+
stdout: result.stdout ?? "",
|
|
207
|
+
stderr: result.stderr ?? "",
|
|
208
|
+
timedOut,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
// The profile file, not the workspace: the session owns that and disposes
|
|
213
|
+
// of it when it ends, so several commands share one room.
|
|
214
|
+
try {
|
|
215
|
+
rmSync(wrapped.cleanup, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Same reasoning as the workspace: visible, and never a reason to fail.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** The shell's convention, so a signal death is distinguishable from an exit. */
|
|
223
|
+
function exitCodeOf(status, signal) {
|
|
224
|
+
if (status !== null)
|
|
225
|
+
return status;
|
|
226
|
+
if (signal === null)
|
|
227
|
+
return 1;
|
|
228
|
+
const numbers = { SIGTERM: 15, SIGKILL: 9, SIGINT: 2, SIGHUP: 1 };
|
|
229
|
+
return 128 + (numbers[signal] ?? 0);
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/codex/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strict Codex broker server (APRV-325.2): one tool, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT `src/mcp/server.ts` with a flag. That server
|
|
5
|
+
* publishes the whole agent verb catalog — `run` spawns argv on the host,
|
|
6
|
+
* `adapter <name>` spends vault credentials, `journal write` writes a local
|
|
7
|
+
* file — and its tool list is derived from the verb registry, so a verb added
|
|
8
|
+
* tomorrow appears on it. A constrained Codex session is the case where that
|
|
9
|
+
* derivation is exactly wrong: the session must reach ONE door, the door must
|
|
10
|
+
* be the same door next month, and the way to guarantee that is a server whose
|
|
11
|
+
* catalog is a literal of length one.
|
|
12
|
+
*
|
|
13
|
+
* Everything else follows from the same reasoning:
|
|
14
|
+
*
|
|
15
|
+
* - **The catalog is a literal.** {@link BROKER_TOOLS} is checked at CALL time
|
|
16
|
+
* as well as at list time, so a client that crafted the name itself is
|
|
17
|
+
* refused by the same check that filtered the list (the defence in depth
|
|
18
|
+
* `mcp-guest-restricted` keeps on the broad server).
|
|
19
|
+
* - **The input schema has no authority in it.** `operations` and
|
|
20
|
+
* `expected_policy_sha256`, `additionalProperties: false`, and the broker
|
|
21
|
+
* refuses an unknown key a second time. There is no `--as`, no path, no
|
|
22
|
+
* class, no token and no sandbox flag to remove, because none was ever
|
|
23
|
+
* published.
|
|
24
|
+
* - **Identity and every path come from the manifest**, which lives under a
|
|
25
|
+
* root-owned install root. The operator who installed it chose them; this
|
|
26
|
+
* process cannot be argued into different ones.
|
|
27
|
+
* - **A refusal is a RESULT, not a JSON-RPC error**, for the reason the broad
|
|
28
|
+
* server gives: the call was well formed and the runtime's answer was no,
|
|
29
|
+
* which the caller has to be able to read as data and branch on.
|
|
30
|
+
*/
|
|
31
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
32
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
33
|
+
import { type Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
34
|
+
import { type BrokerInstallation, type BrokerOptions } from "./broker.js";
|
|
35
|
+
export declare const BROKER_TOOL_NAME: "codex_workspace_apply";
|
|
36
|
+
/** The whole published contract of this server. */
|
|
37
|
+
export declare const BROKER_TOOL: Tool;
|
|
38
|
+
export interface BrokerServerOptions {
|
|
39
|
+
installation: BrokerInstallation;
|
|
40
|
+
/** Forwarded to the broker. `tokens` is deliberately absent: see below. */
|
|
41
|
+
broker?: Omit<BrokerOptions, "tokens">;
|
|
42
|
+
}
|
|
43
|
+
export declare const BROKER_INSTRUCTIONS = "This server is the ONLY way a change reaches the canonical workspace in this session. It publishes exactly one tool, `codex_workspace_apply`, and no other name is callable whether or not something advertised it. You cannot name the acting identity, the workspace root, the policy file, the log, an action class, a reversibility, a grant token or a sandbox posture: all of those belong to the installation an operator owns, and a call that mentions one is refused rather than quietly overridden. Read the policy, hash its exact bytes, and send that digest as `expected_policy_sha256`; if it has changed since, you are told so instead of being evaluated against something you did not read. Every proposal is all-or-nothing: the operations are staged, journaled and applied under a workspace lock, and the outcome you are given is what reading the workspace back proved, not what this process believes it did. When the policy sends a class to a human, the refusal names the action keys awaiting a decision; call again with the same bytes once they have decided, and the same call will apply. Shell, network and credentials are not here and are not coming: this door is for workspace changes.";
|
|
44
|
+
/**
|
|
45
|
+
* Build the strict server. Nothing is connected until `Server.connect`.
|
|
46
|
+
*
|
|
47
|
+
* `tokens` is omitted from what a caller of this constructor may forward for
|
|
48
|
+
* the same reason `--as` is absent from the published schema: a raw grant token
|
|
49
|
+
* is spend material, and a server process holding one for every class would be
|
|
50
|
+
* a server that executes a human's decision without the human. The manual path
|
|
51
|
+
* works through sealed delivery instead — the human grants, the token is sealed
|
|
52
|
+
* to the key the request minted, and `core/execute.ts` opens it at the start.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createCodexBrokerServer(options: BrokerServerOptions): Server;
|
|
55
|
+
/** Build the strict server and connect it. Resolves once connected. */
|
|
56
|
+
export declare function serveCodexBroker(options: BrokerServerOptions, transport: Transport): Promise<Server>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strict Codex broker server (APRV-325.2): one tool, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT `src/mcp/server.ts` with a flag. That server
|
|
5
|
+
* publishes the whole agent verb catalog — `run` spawns argv on the host,
|
|
6
|
+
* `adapter <name>` spends vault credentials, `journal write` writes a local
|
|
7
|
+
* file — and its tool list is derived from the verb registry, so a verb added
|
|
8
|
+
* tomorrow appears on it. A constrained Codex session is the case where that
|
|
9
|
+
* derivation is exactly wrong: the session must reach ONE door, the door must
|
|
10
|
+
* be the same door next month, and the way to guarantee that is a server whose
|
|
11
|
+
* catalog is a literal of length one.
|
|
12
|
+
*
|
|
13
|
+
* Everything else follows from the same reasoning:
|
|
14
|
+
*
|
|
15
|
+
* - **The catalog is a literal.** {@link BROKER_TOOLS} is checked at CALL time
|
|
16
|
+
* as well as at list time, so a client that crafted the name itself is
|
|
17
|
+
* refused by the same check that filtered the list (the defence in depth
|
|
18
|
+
* `mcp-guest-restricted` keeps on the broad server).
|
|
19
|
+
* - **The input schema has no authority in it.** `operations` and
|
|
20
|
+
* `expected_policy_sha256`, `additionalProperties: false`, and the broker
|
|
21
|
+
* refuses an unknown key a second time. There is no `--as`, no path, no
|
|
22
|
+
* class, no token and no sandbox flag to remove, because none was ever
|
|
23
|
+
* published.
|
|
24
|
+
* - **Identity and every path come from the manifest**, which lives under a
|
|
25
|
+
* root-owned install root. The operator who installed it chose them; this
|
|
26
|
+
* process cannot be argued into different ones.
|
|
27
|
+
* - **A refusal is a RESULT, not a JSON-RPC error**, for the reason the broad
|
|
28
|
+
* server gives: the call was well formed and the runtime's answer was no,
|
|
29
|
+
* which the caller has to be able to read as data and branch on.
|
|
30
|
+
*/
|
|
31
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
32
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
33
|
+
import { SPEC_VERSION } from "../core/version.js";
|
|
34
|
+
import { applyWorkspaceChange, BROKER_TOOLS, } from "./broker.js";
|
|
35
|
+
export const BROKER_TOOL_NAME = "codex_workspace_apply";
|
|
36
|
+
/** The whole published contract of this server. */
|
|
37
|
+
export const BROKER_TOOL = {
|
|
38
|
+
name: BROKER_TOOL_NAME,
|
|
39
|
+
title: "apply a bounded workspace change through the approval gate",
|
|
40
|
+
description: "Apply a bounded, typed change to the canonical workspace through approval.md's gate. Every operation is create, replace, delete or move on a relative path inside the workspace; `replace`, `delete` and `move` state the SHA-256 of the bytes they expect to find and are refused if the workspace has moved on. `expected_policy_sha256` is the digest of the policy you read, and a proposal built against a different one is refused rather than silently re-evaluated. The acting identity, the workspace root, the policy, the log and every class are the installation's and cannot be named here. A class the policy resolves to a human decision is refused with `approval-required` and the action keys a person must grant; ask again once they have. Nothing partially applies: either the whole proposal took, or it did not, or the answer is honestly unknown and a person owns it.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
required: ["operations", "expected_policy_sha256"],
|
|
45
|
+
properties: {
|
|
46
|
+
operations: {
|
|
47
|
+
type: "array",
|
|
48
|
+
minItems: 1,
|
|
49
|
+
maxItems: 64,
|
|
50
|
+
description: "The bounded closed change language. `{kind:\"create\", path, after_base64}`, `{kind:\"replace\", path, expected_before_sha256, after_base64}`, `{kind:\"delete\", path, expected_before_sha256}`, `{kind:\"move\", from, to, expected_before_sha256}`. Paths are relative NFC POSIX paths inside the workspace; images are canonical base64, at most 1 MiB each and 8 MiB combined.",
|
|
51
|
+
items: { type: "object" },
|
|
52
|
+
},
|
|
53
|
+
expected_policy_sha256: {
|
|
54
|
+
type: "string",
|
|
55
|
+
pattern: "^[0-9a-f]{64}$",
|
|
56
|
+
description: "SHA-256 of the exact APPROVAL.md bytes this proposal was built against. A mismatch refuses `attestation-drift`.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export const BROKER_INSTRUCTIONS = "This server is the ONLY way a change reaches the canonical workspace in this session. It publishes exactly one tool, `codex_workspace_apply`, and no other name is callable whether or not something advertised it. You cannot name the acting identity, the workspace root, the policy file, the log, an action class, a reversibility, a grant token or a sandbox posture: all of those belong to the installation an operator owns, and a call that mentions one is refused rather than quietly overridden. Read the policy, hash its exact bytes, and send that digest as `expected_policy_sha256`; if it has changed since, you are told so instead of being evaluated against something you did not read. Every proposal is all-or-nothing: the operations are staged, journaled and applied under a workspace lock, and the outcome you are given is what reading the workspace back proved, not what this process believes it did. When the policy sends a class to a human, the refusal names the action keys awaiting a decision; call again with the same bytes once they have decided, and the same call will apply. Shell, network and credentials are not here and are not coming: this door is for workspace changes.";
|
|
62
|
+
/**
|
|
63
|
+
* Build the strict server. Nothing is connected until `Server.connect`.
|
|
64
|
+
*
|
|
65
|
+
* `tokens` is omitted from what a caller of this constructor may forward for
|
|
66
|
+
* the same reason `--as` is absent from the published schema: a raw grant token
|
|
67
|
+
* is spend material, and a server process holding one for every class would be
|
|
68
|
+
* a server that executes a human's decision without the human. The manual path
|
|
69
|
+
* works through sealed delivery instead — the human grants, the token is sealed
|
|
70
|
+
* to the key the request minted, and `core/execute.ts` opens it at the start.
|
|
71
|
+
*/
|
|
72
|
+
export function createCodexBrokerServer(options) {
|
|
73
|
+
const server = new Server({ name: "approval-md-codex-broker", version: SPEC_VERSION }, { capabilities: { tools: {} }, instructions: BROKER_INSTRUCTIONS });
|
|
74
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [BROKER_TOOL] }));
|
|
75
|
+
server.setRequestHandler(CallToolRequestSchema, (request) => {
|
|
76
|
+
const name = request.params.name;
|
|
77
|
+
if (!BROKER_TOOLS.has(name)) {
|
|
78
|
+
throw new McpError(ErrorCode.InvalidParams, `unknown tool ${JSON.stringify(name)}: this server publishes exactly ${[...BROKER_TOOLS].join(", ")}. Shell, network, credential and gate-authority verbs are not withheld here, they are absent`);
|
|
79
|
+
}
|
|
80
|
+
const result = applyWorkspaceChange(name, options.installation, request.params.arguments, options.broker ?? {});
|
|
81
|
+
const payload = result.ok
|
|
82
|
+
? { ...result }
|
|
83
|
+
: { error: { code: result.code, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }), ...(result.pending === undefined ? {} : { pending: result.pending }), ...(result.state === undefined ? {} : { state: result.state }) } };
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
86
|
+
structuredContent: payload,
|
|
87
|
+
...(result.ok ? {} : { isError: true }),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return server;
|
|
91
|
+
}
|
|
92
|
+
/** Build the strict server and connect it. Resolves once connected. */
|
|
93
|
+
export async function serveCodexBroker(options, transport) {
|
|
94
|
+
const server = createCodexBrokerServer(options);
|
|
95
|
+
await server.connect(transport);
|
|
96
|
+
return server;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/codex/serve.ts"],"names":[],"mappings":"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"}
|