approval-md 0.1.0 → 0.2.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 +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The egress sandbox (APRV-193): the room an allowed command runs in.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is for
|
|
5
|
+
*
|
|
6
|
+
* Arbitrary code execution is capability-complete. The gate classifies a
|
|
7
|
+
* command by its TEXT, and `npm test` runs whatever the agent wrote a minute
|
|
8
|
+
* ago, so for laundered exec the text has stopped describing the effect. No
|
|
9
|
+
* classifier over shell strings can close that, and `core/child-env.ts`
|
|
10
|
+
* (APRV-205) closed only half of it: the child no longer inherits the session's
|
|
11
|
+
* credentials, and it still inherits the session's network.
|
|
12
|
+
*
|
|
13
|
+
* This module removes the network. It does not try to predict what the code
|
|
14
|
+
* will do; it takes away what the code would need. Where APRV-205 starves the
|
|
15
|
+
* child of secrets, this starves it of a place to send them.
|
|
16
|
+
*
|
|
17
|
+
* ## The shape, and why it is this shape
|
|
18
|
+
*
|
|
19
|
+
* A NETWORK-ONLY sandbox, with one filesystem exception. The filesystem is left
|
|
20
|
+
* alone because the gate's own IPC is a FILE: `src/daemon/` opens no socket, it
|
|
21
|
+
* polls `.approval/log/events.jsonl`. So denying egress costs the gate nothing
|
|
22
|
+
* and needs no plumbing to stay reachable, and the "one door" other agent
|
|
23
|
+
* sandboxes build an egress proxy for already exists as an append to a file.
|
|
24
|
+
* That is the single largest simplification available here, and it is why the
|
|
25
|
+
* default profile denies the network, denies reads of the credential material
|
|
26
|
+
* beside the log, and touches nothing else.
|
|
27
|
+
*
|
|
28
|
+
* Loopback is denied WITH the rest. There is no gate socket to except: the
|
|
29
|
+
* daemon is a file reader. The one thing loopback costs is a test suite that
|
|
30
|
+
* starts its own localhost server, which is what {@link EgressAllowance.loopback}
|
|
31
|
+
* is for, and that allowance is an operator's decision recorded where it is
|
|
32
|
+
* used (`docs/sandboxed-exec.md` has the survey).
|
|
33
|
+
*
|
|
34
|
+
* ## The mechanism, and what is verified rather than assumed
|
|
35
|
+
*
|
|
36
|
+
* macOS only in this build: `/usr/bin/sandbox-exec` with a Sandbox Profile
|
|
37
|
+
* Language (SBPL) profile. It ships with every macOS, needs no privilege and no
|
|
38
|
+
* daemon, and every macOS agent sandbox in the field uses it. Linux
|
|
39
|
+
* (`bwrap --unshare-net`, `unshare --net`) is a stated follow-up rather than a
|
|
40
|
+
* silent omission: {@link detectSandbox} reports `supported: false` there and
|
|
41
|
+
* says so, and `docs/sandboxed-exec.md` carries the gap.
|
|
42
|
+
*
|
|
43
|
+
* Three Seatbelt behaviours are measured, not assumed. Each of them, got wrong,
|
|
44
|
+
* ships a profile that silently protects nothing, which is the worst artifact
|
|
45
|
+
* this design could produce:
|
|
46
|
+
*
|
|
47
|
+
* 1. `network-outbound` covers AF_UNIX connects as well as AF_INET. A bare
|
|
48
|
+
* `(deny network-outbound)` kills local IPC — DNS's mDNSResponder socket
|
|
49
|
+
* included — and the process dies before it can do anything. The unix
|
|
50
|
+
* exception is mandatory.
|
|
51
|
+
* 2. `subpath` and `literal` filters match the kernel's RESOLVED path. On macOS
|
|
52
|
+
* `/tmp` is a symlink to `/private/tmp`, so a profile naming `/tmp/x` denies
|
|
53
|
+
* NOTHING. Every path is realpath-resolved before it is written into a
|
|
54
|
+
* profile, and `tests/sandbox.test.ts` pins the unresolved spelling as the
|
|
55
|
+
* no-op it is.
|
|
56
|
+
* 3. `sandbox-exec` execs through `execvp`, so a PATH lookup still happens
|
|
57
|
+
* inside the wrapper — but a lookup that FAILS exits 71 (EX_OSERR), which
|
|
58
|
+
* would be recorded as the child's own exit code. So {@link resolveExecutable}
|
|
59
|
+
* does the lookup first and a command that cannot be found is never wrapped:
|
|
60
|
+
* it reaches `spawnSync` unwrapped and fails as ENOENT exactly as it does
|
|
61
|
+
* today.
|
|
62
|
+
*
|
|
63
|
+
* ## What it does not claim
|
|
64
|
+
*
|
|
65
|
+
* A sandboxed child can still WRITE files, including files a later unsandboxed
|
|
66
|
+
* process will run. That is the same laundering one step removed, and it is why
|
|
67
|
+
* `APPROVAL_HOOK_REQUIRE_SANDBOX` (which makes the hook insist that every
|
|
68
|
+
* code-running command wear the room) matters more than any single wrapper. It
|
|
69
|
+
* would be closed properly by running the whole SESSION under the profile,
|
|
70
|
+
* which this build cannot do: an agent harness needs the model API, and denying
|
|
71
|
+
* egress denies exactly that. A session-wide sandbox needs an allowlist
|
|
72
|
+
* reaching one host, which Seatbelt cannot express by hostname and which the
|
|
73
|
+
* prior art solves with a local proxy.
|
|
74
|
+
*
|
|
75
|
+
* Inbound sockets are not denied, so a child could in principle bind a port and
|
|
76
|
+
* wait to be contacted; that needs a peer that can reach this machine and is
|
|
77
|
+
* recorded here as a stated limit rather than an oversight. And the sandbox is
|
|
78
|
+
* not isolation: it is one capability removed from a process that otherwise has
|
|
79
|
+
* the ordinary powers of the session.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* The mechanisms this build knows. One, today.
|
|
83
|
+
*
|
|
84
|
+
* A union rather than a string so the Linux follow-up (APRV-193g) is an
|
|
85
|
+
* addition the compiler finds every site of, rather than a value that quietly
|
|
86
|
+
* flows through code written for Seatbelt.
|
|
87
|
+
*/
|
|
88
|
+
export declare const SANDBOX_MECHANISMS: readonly ["sandbox-exec"];
|
|
89
|
+
export type SandboxMechanism = (typeof SANDBOX_MECHANISMS)[number];
|
|
90
|
+
/**
|
|
91
|
+
* What an execution's record says about the room it ran in.
|
|
92
|
+
*
|
|
93
|
+
* Written to `execution.started` as `sandbox`, and it is a fact about the
|
|
94
|
+
* runtime's own behaviour rather than anything a caller asserted: three of the
|
|
95
|
+
* four values are computed here, and the fourth (`opted-out`) is a flag the
|
|
96
|
+
* operator typed, which is exactly why it is RECORDED — an opt-out nobody can
|
|
97
|
+
* see afterwards is an opt-out that costs nothing to take.
|
|
98
|
+
*/
|
|
99
|
+
export declare const SANDBOX_STATES: readonly [
|
|
100
|
+
/** The child ran with outbound network denied. */
|
|
101
|
+
"egress-denied",
|
|
102
|
+
/** `--no-sandbox` was passed. The child ran with the session's own network. */
|
|
103
|
+
"opted-out",
|
|
104
|
+
/**
|
|
105
|
+
* The manual path: a human granted these exact bytes and the grant is the
|
|
106
|
+
* authority to reach the world. `approval run` on a token is the one door,
|
|
107
|
+
* and a door that denied egress would not be a door.
|
|
108
|
+
*/
|
|
109
|
+
"granted-egress",
|
|
110
|
+
/**
|
|
111
|
+
* This platform has no mechanism in this build (anything but macOS today).
|
|
112
|
+
* Recorded on every execution so an auditor can see exactly which runs were
|
|
113
|
+
* unprotected, rather than inferring it from the absence of a field.
|
|
114
|
+
*/
|
|
115
|
+
"unsupported"];
|
|
116
|
+
export type SandboxState = (typeof SANDBOX_STATES)[number];
|
|
117
|
+
/** What the machine can do, probed rather than inferred. */
|
|
118
|
+
export interface SandboxDetection {
|
|
119
|
+
/** Can a command be run egress-denied here, right now? */
|
|
120
|
+
readonly available: boolean;
|
|
121
|
+
/** The mechanism that would be used, or `null`. */
|
|
122
|
+
readonly mechanism: SandboxMechanism | null;
|
|
123
|
+
/**
|
|
124
|
+
* Does this build have a mechanism for this platform AT ALL?
|
|
125
|
+
*
|
|
126
|
+
* The distinction from {@link available} is load-bearing and it is the one
|
|
127
|
+
* place this module is deliberately not maximally strict. `supported: false`
|
|
128
|
+
* means "nothing was ever written for this platform", which is a known gap
|
|
129
|
+
* with a follow-up task and a docs row. `supported: true, available: false`
|
|
130
|
+
* means "the mechanism that should be here did not work", which is a broken
|
|
131
|
+
* promise, and the callers treat the two differently: the first proceeds and
|
|
132
|
+
* records `unsupported`, the second refuses to run the command at all.
|
|
133
|
+
*/
|
|
134
|
+
readonly supported: boolean;
|
|
135
|
+
/** Can loopback be carved back in? (Seatbelt yes; a Linux netns cannot.) */
|
|
136
|
+
readonly loopback: boolean;
|
|
137
|
+
/** Why, when {@link available} is false. Empty otherwise. */
|
|
138
|
+
readonly reason: string;
|
|
139
|
+
}
|
|
140
|
+
/** What the profile lets through. Everything absent here is denied. */
|
|
141
|
+
export interface EgressAllowance {
|
|
142
|
+
/**
|
|
143
|
+
* Allow connections to `localhost:*`.
|
|
144
|
+
*
|
|
145
|
+
* Off by default. The gate needs nothing: its IPC is a file. This exists for
|
|
146
|
+
* the one legitimate case the survey found, a test suite that starts its own
|
|
147
|
+
* server, and it is a real widening — a loopback port is a port, and anything
|
|
148
|
+
* listening on one is reachable from inside.
|
|
149
|
+
*/
|
|
150
|
+
readonly loopback: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Absolute paths whose CONTENTS the child may not read. Resolved before they
|
|
153
|
+
* reach the profile. Directories deny their whole subtree.
|
|
154
|
+
*/
|
|
155
|
+
readonly denyRead: readonly string[];
|
|
156
|
+
}
|
|
157
|
+
/** The default: nothing allowed, nothing denied beyond the network. */
|
|
158
|
+
export declare const DENY_ALL_EGRESS: EgressAllowance;
|
|
159
|
+
/**
|
|
160
|
+
* The files beside a log that hold credential material, for the profile's
|
|
161
|
+
* `denyRead`.
|
|
162
|
+
*
|
|
163
|
+
* Named individually rather than by denying the whole approval home. The home
|
|
164
|
+
* also holds the log, the payload store and the queue, and a child that could
|
|
165
|
+
* not read the log could not read the decision that authorized it. What is
|
|
166
|
+
* denied is what a laundering script would want: the vault's ciphertext, the
|
|
167
|
+
* environment source map that says where the secrets come from, and the
|
|
168
|
+
* per-request private keys that seal tokens.
|
|
169
|
+
*
|
|
170
|
+
* This is defence in depth over material that is already encrypted or already
|
|
171
|
+
* useless without the passphrase — the load-bearing custody control is that
|
|
172
|
+
* the passphrase is not in the child's environment at all (APRV-205). It is
|
|
173
|
+
* worth having on the one platform where it costs nothing.
|
|
174
|
+
*/
|
|
175
|
+
export declare function credentialPathsFor(logPath: string): string[];
|
|
176
|
+
/**
|
|
177
|
+
* Forces the unavailable branch. A STRICTNESS increase in every caller — the
|
|
178
|
+
* run path refuses and the session launcher refuses — so a forged value cannot
|
|
179
|
+
* widen anything, which is what keeps it clear of SPEC.md §11.1 invariant 4.
|
|
180
|
+
*/
|
|
181
|
+
export declare const FORCE_UNAVAILABLE_ENV = "APPROVAL_SANDBOX_FORCE_UNAVAILABLE";
|
|
182
|
+
/**
|
|
183
|
+
* Treat an unsupported platform as a broken promise rather than a known gap:
|
|
184
|
+
* with this set, a machine with no mechanism refuses instead of proceeding.
|
|
185
|
+
* The same one-way property holds — it can only ever refuse more.
|
|
186
|
+
*/
|
|
187
|
+
export declare const REQUIRE_SANDBOX_ENV = "APPROVAL_SANDBOX_REQUIRED";
|
|
188
|
+
export interface DetectOptions {
|
|
189
|
+
readonly platform?: NodeJS.Platform;
|
|
190
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* What this machine can do. Cached for the default call, because every verb
|
|
194
|
+
* that asks would otherwise spawn a probe per invocation, and the answer cannot
|
|
195
|
+
* change inside one process.
|
|
196
|
+
*/
|
|
197
|
+
export declare function detectSandbox(options?: DetectOptions): SandboxDetection;
|
|
198
|
+
/** Testing seam: forget the cached probe. Never called by the runtime. */
|
|
199
|
+
export declare function resetSandboxDetection(): void;
|
|
200
|
+
/**
|
|
201
|
+
* A path as the KERNEL will see it.
|
|
202
|
+
*
|
|
203
|
+
* A path that does not exist yet cannot be resolved, so its directory is
|
|
204
|
+
* resolved and the name re-joined. A profile naming an unresolved path denies
|
|
205
|
+
* nothing at all and reports no error, which is the failure this function
|
|
206
|
+
* exists to prevent.
|
|
207
|
+
*/
|
|
208
|
+
export declare function resolveForProfile(path: string): string;
|
|
209
|
+
/**
|
|
210
|
+
* The SBPL profile.
|
|
211
|
+
*
|
|
212
|
+
* `(allow default)` first, then the denial: SBPL takes the LAST matching rule,
|
|
213
|
+
* so exceptions are written after the deny. This is deliberately a deny-LIST
|
|
214
|
+
* and not the `(deny default)` posture a true isolation sandbox takes. The
|
|
215
|
+
* property being enforced is egress, and a deny-default profile spends itself
|
|
216
|
+
* re-allowing dyld, `/dev/urandom`, the process's own binary and every
|
|
217
|
+
* temporary directory, which is a different task with a different failure mode
|
|
218
|
+
* (and a much larger chance of breaking ordinary development, which is how a
|
|
219
|
+
* control gets switched off).
|
|
220
|
+
*/
|
|
221
|
+
export declare function seatbeltProfile(allowance: EgressAllowance): string;
|
|
222
|
+
/** The argv to spawn, and the temporary directory the caller must remove. */
|
|
223
|
+
export interface WrappedSpawn {
|
|
224
|
+
readonly command: string;
|
|
225
|
+
readonly args: string[];
|
|
226
|
+
/** A directory holding the profile file; remove it once the child is gone. */
|
|
227
|
+
readonly cleanup: string;
|
|
228
|
+
/** What the caller records. */
|
|
229
|
+
readonly mechanism: SandboxMechanism;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve a command the way `execvp` would, and say when it cannot be found.
|
|
233
|
+
*
|
|
234
|
+
* Done HERE rather than left to the wrapper for the reason in the header: an
|
|
235
|
+
* execvp failure inside `sandbox-exec` exits 71, and 71 recorded as the child's
|
|
236
|
+
* exit code is a lie about a command that never ran. A command that does not
|
|
237
|
+
* resolve is returned as `null`, and the caller spawns it unwrapped so the
|
|
238
|
+
* ENOENT surfaces exactly as it does with no sandbox in the picture.
|
|
239
|
+
*/
|
|
240
|
+
export declare function resolveExecutable(command: string, env?: NodeJS.ProcessEnv): string | null;
|
|
241
|
+
/**
|
|
242
|
+
* Build the wrapped spawn for `argv`.
|
|
243
|
+
*
|
|
244
|
+
* Writes the profile to a fresh 0600 file under a private directory: the
|
|
245
|
+
* profile names the paths whose reads are denied, which is not secret, but a
|
|
246
|
+
* world-writable profile would be a profile another process could rewrite
|
|
247
|
+
* between this write and the exec.
|
|
248
|
+
*/
|
|
249
|
+
export declare function wrapForSandbox(mechanism: SandboxMechanism, command: string, args: readonly string[], allowance?: EgressAllowance): WrappedSpawn;
|
|
250
|
+
export interface PostureInput {
|
|
251
|
+
/** `--no-sandbox` was passed. */
|
|
252
|
+
readonly optedOut: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* A human's grant over these exact bytes is in the caller's hand (the manual
|
|
255
|
+
* path presented a token). The grant IS the authority to reach the world.
|
|
256
|
+
*
|
|
257
|
+
* Not a self-report that widens anything: a token that does not verify runs
|
|
258
|
+
* no command at all, so the loosening is reachable only by holding something
|
|
259
|
+
* a human minted. SPEC.md §11.1 invariant 4 is about a declaration the
|
|
260
|
+
* executing party authors; this is a secret it cannot author.
|
|
261
|
+
*/
|
|
262
|
+
readonly granted: boolean;
|
|
263
|
+
/** The machine's capability. */
|
|
264
|
+
readonly detection: SandboxDetection;
|
|
265
|
+
/** `APPROVAL_SANDBOX_REQUIRED=1` promotes an unsupported platform to a refusal. */
|
|
266
|
+
readonly requireSupported?: boolean;
|
|
267
|
+
}
|
|
268
|
+
export type SandboxPosture = {
|
|
269
|
+
readonly kind: "apply";
|
|
270
|
+
readonly state: "egress-denied";
|
|
271
|
+
readonly mechanism: SandboxMechanism;
|
|
272
|
+
} | {
|
|
273
|
+
readonly kind: "skip";
|
|
274
|
+
readonly state: Exclude<SandboxState, "egress-denied">;
|
|
275
|
+
} | {
|
|
276
|
+
readonly kind: "refuse";
|
|
277
|
+
readonly reason: string;
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* What to do about the sandbox for one execution.
|
|
281
|
+
*
|
|
282
|
+
* Total, pure, and exhaustively testable: it reads a detection and two booleans
|
|
283
|
+
* and returns one of three answers. The ORDER of the branches is the policy —
|
|
284
|
+
* a broken mechanism refuses before an opt-out is considered, so `--no-sandbox`
|
|
285
|
+
* on a machine whose sandbox is broken is still a refusal rather than a way to
|
|
286
|
+
* turn the noise off.
|
|
287
|
+
*/
|
|
288
|
+
export declare function sandboxPosture(input: PostureInput): SandboxPosture;
|
|
289
|
+
/** Is `APPROVAL_SANDBOX_REQUIRED` set on this environment? */
|
|
290
|
+
export declare function sandboxRequired(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealed token delivery (amended SPEC.md §6.3, §10.4, §11.1 — APRV-105).
|
|
3
|
+
*
|
|
4
|
+
* The problem this closes is a handover, not an authorization. A token is minted
|
|
5
|
+
* at exactly one site — the grant path of `core/gate.ts` — and spent at another,
|
|
6
|
+
* `approval run`. The two are different processes, and until now the only
|
|
7
|
+
* transport between them was a human with a clipboard: `approval grant` (or the
|
|
8
|
+
* Telegram listener) printed the raw token on ITS stdout, and `approval wait`,
|
|
9
|
+
* which reads only the verified log, had nothing to return because the log
|
|
10
|
+
* carries hashes. On one machine that is a paste. Across two — the listener on a
|
|
11
|
+
* laptop, the agent elsewhere, the log synced through git — it is a paste that
|
|
12
|
+
* cannot happen.
|
|
13
|
+
*
|
|
14
|
+
* So the requester publishes an ADDRESS. `approval request` mints an ephemeral
|
|
15
|
+
* X25519 keypair, keeps the private half in a 0600 file beside the log, and puts
|
|
16
|
+
* the public half on `approval.requested` as `token_recipient_key`. On grant the
|
|
17
|
+
* mint site seals the raw token to that key and records the ciphertext beside
|
|
18
|
+
* the digest as `token_sealed`. `approval wait` opens it with the local private
|
|
19
|
+
* key and hands the token back to the process that asked for it.
|
|
20
|
+
*
|
|
21
|
+
* ## The keypair ADDRESSES; it does not AUTHORIZE
|
|
22
|
+
*
|
|
23
|
+
* The obvious objection is that the agent creates both halves of the key, so how
|
|
24
|
+
* does this control the agent? It does not, and it does not need to. The four
|
|
25
|
+
* properties that make an action human-gated are untouched:
|
|
26
|
+
*
|
|
27
|
+
* - a token exists only after a human grant (one mint site);
|
|
28
|
+
* - it binds to the exact payload bytes;
|
|
29
|
+
* - it is single-use, and consumption is an `execution.started` in the log;
|
|
30
|
+
* - credentials answer only to tokens.
|
|
31
|
+
*
|
|
32
|
+
* A keypair cannot mint a token, forge a grant, alter the bound payload, or
|
|
33
|
+
* reuse a spent one. What changes is who can READ a minted token: the requesting
|
|
34
|
+
* process instead of the human's clipboard, which is where the human was
|
|
35
|
+
* carrying it anyway. A malicious agent that substitutes a key substitutes it
|
|
36
|
+
* only in its OWN request, which the approver sees and approves as that agent's
|
|
37
|
+
* request; identity spoofing is exactly as it was in SPEC.md §11, where the
|
|
38
|
+
* trust boundary is the machine.
|
|
39
|
+
*
|
|
40
|
+
* What is lost, stated plainly: the marginal ability to approve on the phone and
|
|
41
|
+
* then withhold delivery.
|
|
42
|
+
*
|
|
43
|
+
* ## Residual risk, stated plainly
|
|
44
|
+
*
|
|
45
|
+
* Someone who steals the private-key file AND reads the log can recover an
|
|
46
|
+
* unspent token inside its TTL. That window equals the terminal-paste window
|
|
47
|
+
* today, and it closes at consume or at TTL, whichever comes first — this module
|
|
48
|
+
* unlinks the key file at both. The ciphertext lives in a public, permanent log,
|
|
49
|
+
* so the seal must be sound and the token short-lived; it already is.
|
|
50
|
+
*
|
|
51
|
+
* ## The construction
|
|
52
|
+
*
|
|
53
|
+
* X25519 ECDH to a fresh ephemeral sender key, HKDF-SHA-256 to a 32-byte AEAD
|
|
54
|
+
* key, AES-256-GCM with a fresh 12-byte nonce. All from `node:crypto`; no
|
|
55
|
+
* dependency is added. The sender's ephemeral public key travels with the
|
|
56
|
+
* ciphertext, so the recipient needs nothing but its own private key and the
|
|
57
|
+
* record. The HKDF `info` binds the ciphertext to this scheme and to the action
|
|
58
|
+
* it was minted for, so a seal lifted from one grant cannot be replayed as
|
|
59
|
+
* another's even by someone holding both key files.
|
|
60
|
+
*/
|
|
61
|
+
/** The payload key carrying the requester's public key on `approval.requested`. */
|
|
62
|
+
export declare const RECIPIENT_KEY_FIELD = "token_recipient_key";
|
|
63
|
+
/** The payload key carrying the sealed token on `approval.granted`. */
|
|
64
|
+
export declare const SEALED_TOKEN_FIELD = "token_sealed";
|
|
65
|
+
/**
|
|
66
|
+
* The payload key on `approval.requested` saying the requester will consume its
|
|
67
|
+
* own grant in its own process (APRV-211), so the raw token is delivered ONLY
|
|
68
|
+
* through the seal and is never returned to the granting surface.
|
|
69
|
+
*
|
|
70
|
+
* A fact about delivery, recorded where every other delivery fact is recorded.
|
|
71
|
+
* It authorizes nothing and relaxes nothing: the token is still minted only by
|
|
72
|
+
* a human's grant, still bound to the payload bytes, still single-use. What it
|
|
73
|
+
* removes is a reader — a terminal that was being handed a live credential it
|
|
74
|
+
* had no use for.
|
|
75
|
+
*/
|
|
76
|
+
export declare const SELF_DELIVERY_FIELD = "token_delivery_self";
|
|
77
|
+
/** `.approval/keys/`, the sibling of the log directory the private keys live in. */
|
|
78
|
+
export declare const KEY_STORE_DIRNAME = "keys";
|
|
79
|
+
/** The scheme identifier recorded inside every seal. One value in v0.1. */
|
|
80
|
+
export declare const SEAL_ALG = "x25519-hkdf-sha256/aes-256-gcm";
|
|
81
|
+
/** The key store for a log path, by the same rule the payload store uses. */
|
|
82
|
+
export declare function keyStoreDirFor(logPath: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The file one action key's private key lives in.
|
|
85
|
+
*
|
|
86
|
+
* The action key is used verbatim wherever it is already a safe file name, and
|
|
87
|
+
* percent-encoded elsewhere. Percent-encoding is injective, so two distinct
|
|
88
|
+
* action keys can never name one file — which matters more than readability,
|
|
89
|
+
* since a collision would hand one action's token to another. The common case
|
|
90
|
+
* (`task-042:chaser:2026-08-04`) survives unchanged, so the directory listing
|
|
91
|
+
* still reads as the action keys it holds.
|
|
92
|
+
*/
|
|
93
|
+
export declare function keyPath(keyDir: string, actionKey: string): string;
|
|
94
|
+
/** A freshly minted recipient keypair: the public half travels, the private stays. */
|
|
95
|
+
export interface RecipientKeypair {
|
|
96
|
+
/** Base64 DER SPKI. What rides on `approval.requested`. */
|
|
97
|
+
publicKey: string;
|
|
98
|
+
/** Base64 DER PKCS#8. What is written 0600 and never leaves the machine. */
|
|
99
|
+
privateKey: string;
|
|
100
|
+
}
|
|
101
|
+
/** Mint an ephemeral X25519 keypair for one request. */
|
|
102
|
+
export declare function mintRecipientKeypair(): RecipientKeypair;
|
|
103
|
+
/** Is this a value that could be a recipient public key? Shape only. */
|
|
104
|
+
export declare function isRecipientKey(value: unknown): value is string;
|
|
105
|
+
export type KeyWriteResult = {
|
|
106
|
+
ok: true;
|
|
107
|
+
path: string;
|
|
108
|
+
} | {
|
|
109
|
+
ok: false;
|
|
110
|
+
message: string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Write one private key, atomically and 0600.
|
|
114
|
+
*
|
|
115
|
+
* The mode is set by `openSync`'s mode argument on a file created `wx`, so the
|
|
116
|
+
* key never exists at a wider mode even for an instant — a `chmod` after the
|
|
117
|
+
* fact would leave a window in which any process on the machine could read it.
|
|
118
|
+
* The rename is atomic, so a reader either sees the whole key or no file.
|
|
119
|
+
*/
|
|
120
|
+
export declare function writePrivateKey(keyDir: string, actionKey: string, privateKey: string): KeyWriteResult;
|
|
121
|
+
/** The private key for an action, or `null` when this machine holds none. */
|
|
122
|
+
export declare function readPrivateKey(keyDir: string, actionKey: string): string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Remove one action's private key. Idempotent and never throws.
|
|
125
|
+
*
|
|
126
|
+
* Called at every death of the authorization it addresses: consumption, expiry,
|
|
127
|
+
* revocation. A key that outlived its grant would be a decryption capability for
|
|
128
|
+
* a ciphertext that is in the log forever, kept for no reason at all.
|
|
129
|
+
*/
|
|
130
|
+
export declare function forgetPrivateKey(keyDir: string, actionKey: string): boolean;
|
|
131
|
+
/** What `approval.granted` carries beside `token_sha256`. */
|
|
132
|
+
export interface SealedToken {
|
|
133
|
+
/** The scheme. One value in v0.1; present so a reader never has to guess. */
|
|
134
|
+
alg: string;
|
|
135
|
+
/** The sender's ephemeral X25519 public key, base64 DER SPKI. */
|
|
136
|
+
epk: string;
|
|
137
|
+
/** The AES-GCM nonce, base64. Fresh for every seal. */
|
|
138
|
+
nonce: string;
|
|
139
|
+
/** Ciphertext, base64. */
|
|
140
|
+
ct: string;
|
|
141
|
+
/** The GCM authentication tag, base64. */
|
|
142
|
+
tag: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Seal a raw token to a recipient public key.
|
|
146
|
+
*
|
|
147
|
+
* Returns `null` for a recipient key that cannot be parsed. A grant is not
|
|
148
|
+
* refused over an unusable recipient key: the authorization is the human's
|
|
149
|
+
* decision and stands, the digest still binds it, and the raw token is still
|
|
150
|
+
* printed once on the granting surface. What fails is the convenience, and a
|
|
151
|
+
* convenience must never be able to void a human's yes.
|
|
152
|
+
*/
|
|
153
|
+
export declare function sealToken(token: string, recipientPublicKey: string, actionKey: string): SealedToken | null;
|
|
154
|
+
/** Read a `token_sealed` payload value as a {@link SealedToken}, or `null`. */
|
|
155
|
+
export declare function asSealedToken(value: unknown): SealedToken | null;
|
|
156
|
+
/**
|
|
157
|
+
* Open a sealed token with a recipient private key.
|
|
158
|
+
*
|
|
159
|
+
* Returns `null` for anything that does not authenticate: a wrong key, a
|
|
160
|
+
* tampered ciphertext, a seal minted for a different action key. There is one
|
|
161
|
+
* failure, deliberately: distinguishing "wrong key" from "wrong ciphertext"
|
|
162
|
+
* would be an oracle, and the caller's response to either is identical — fall
|
|
163
|
+
* back to the raw token the granting surface printed.
|
|
164
|
+
*/
|
|
165
|
+
export declare function openSealedToken(sealed: SealedToken, recipientPrivateKey: string, actionKey: string): string | null;
|