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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload retention pruning — the enforcement half of amended SPEC.md §5.2
|
|
3
|
+
* (APRV-38's vocabulary, APRV-41's daemon).
|
|
4
|
+
*
|
|
5
|
+
* > "A payload is prunable once the action it is bound to has been in a terminal
|
|
6
|
+
* > state (`executed`, `rejected`, `expired`, `revoked`) for longer than the
|
|
7
|
+
* > duration. A payload whose action is not terminal is never prunable, at any
|
|
8
|
+
* > age […]. Orphaned payloads (bytes with no recorded binding) are prunable
|
|
9
|
+
* > regardless of the key. […] Pruning is performed by the daemon and by nothing
|
|
10
|
+
* > else, and each removal appends a `payload.pruned` event, so a log states what
|
|
11
|
+
* > its store no longer holds."
|
|
12
|
+
*
|
|
13
|
+
* ## Write-ahead, always in that order
|
|
14
|
+
*
|
|
15
|
+
* For every file this module removes, the `payload.pruned` event lands in the
|
|
16
|
+
* log **first** and the `unlink` follows. The ordering is the whole design: a
|
|
17
|
+
* crash between the two leaves a file on disk that the log already says is gone,
|
|
18
|
+
* which the next tick completes by unlinking it and appending nothing further
|
|
19
|
+
* ({@link PrunePlan.completions}). The opposite order would leave the other
|
|
20
|
+
* failure — bytes deleted with no record of the deletion — which is the one
|
|
21
|
+
* outcome a store holding the material evidence of what a human approved cannot
|
|
22
|
+
* afford. Deleting evidence is acceptable; deleting it silently is not.
|
|
23
|
+
*
|
|
24
|
+
* Idempotence needs no remembered state: a hash that already carries a
|
|
25
|
+
* `payload.pruned` event is never given a second one, and the plan is re-derived
|
|
26
|
+
* from the verified log on every tick.
|
|
27
|
+
*
|
|
28
|
+
* ## Terminal time comes from the log, never from the filesystem
|
|
29
|
+
*
|
|
30
|
+
* The clock that decides whether retention has elapsed reads the timestamp of the
|
|
31
|
+
* event that made the action terminal (`execution.completed`, `approval.rejected`,
|
|
32
|
+
* `approval.revoked`, `approval.expired`). File mtimes are not evidence: they are
|
|
33
|
+
* rewritten by a copy, a checkout, a backup restore, or an `rsync`, and pruning on
|
|
34
|
+
* them would let a routine filesystem operation decide when approval evidence
|
|
35
|
+
* disappears.
|
|
36
|
+
*
|
|
37
|
+
* A **lazily** expired request (the TTL has arithmetically lapsed but no
|
|
38
|
+
* `approval.expired` record exists) is deliberately not terminal here. The
|
|
39
|
+
* daemon's own TTL sweep appends that event on the same tick; the payload becomes
|
|
40
|
+
* prunable once the log says so, and retention is then measured from the recorded
|
|
41
|
+
* moment rather than from one this module computed for itself.
|
|
42
|
+
*
|
|
43
|
+
* `execution.failed` is likewise not terminal: a failed action may be retried
|
|
44
|
+
* against the very bytes in question, and loop escalation (SPEC.md §10.2) exists
|
|
45
|
+
* precisely because failures recur.
|
|
46
|
+
*
|
|
47
|
+
* ## The orphan rule, resolved conservatively (flagged for review)
|
|
48
|
+
*
|
|
49
|
+
* §5.2 says orphaned payloads are "prunable regardless of the key", which reads
|
|
50
|
+
* as though orphans could be swept even with `payload_retention` absent. APRV-41's
|
|
51
|
+
* acceptance criteria say the absent key means no pruning at all. This module
|
|
52
|
+
* takes the strictest reading that satisfies both:
|
|
53
|
+
*
|
|
54
|
+
* - **`payload_retention` absent: the pruning subsystem does not run.** Nothing is
|
|
55
|
+
* deleted, orphan or not, and no `payload.pruned` is appended. Retention is an
|
|
56
|
+
* operator's explicit choice to forget, and an operator who never made it never
|
|
57
|
+
* asked this runtime to delete anything.
|
|
58
|
+
* - **`payload_retention` present: orphans are prunable at any age**, which is
|
|
59
|
+
* what "regardless of the key" then means — the duration governs bound payloads
|
|
60
|
+
* and does not gate residue nothing ever bound.
|
|
61
|
+
*
|
|
62
|
+
* An orphan is a file whose hash appears in **no** log record other than a
|
|
63
|
+
* `payload.pruned` (head-moved residue: the gate stored the bytes, its append was
|
|
64
|
+
* refused, and no request ever declared them). A hash mentioned by any other
|
|
65
|
+
* record is bound, and a binding this module cannot attribute to an action key is
|
|
66
|
+
* treated as live rather than as an orphan — fail closed in both directions.
|
|
67
|
+
*
|
|
68
|
+
* ## Nothing else here decides anything
|
|
69
|
+
*
|
|
70
|
+
* Which hashes the log already binds, and which it already says are pruned, is
|
|
71
|
+
* `core/payload-census.ts`'s — the same computation the reporting surfaces read,
|
|
72
|
+
* so what a reader is shown and what this module would delete can never drift
|
|
73
|
+
* apart. Approval state per action is `core/state.ts`'s `requestState`; the append is
|
|
74
|
+
* `core/log.ts`'s `appendEvent` with `expectedHead` (compare-and-append, SPEC.md
|
|
75
|
+
* §11.1 invariant 5); the unlink is `core/payload-store.ts`'s. The timestamp on
|
|
76
|
+
* every `payload.pruned` is the runtime's, read from the injected clock at the
|
|
77
|
+
* write boundary, and the actor is `system:daemon` — a party under oversight must
|
|
78
|
+
* never be able to author either.
|
|
79
|
+
*/
|
|
80
|
+
import { type Clock } from "../core/clock.js";
|
|
81
|
+
import { type EventRecord } from "../core/log.js";
|
|
82
|
+
/**
|
|
83
|
+
* SPEC.md §8 and `event.schema.json`: `payload.pruned` carries a `system:` actor.
|
|
84
|
+
* Declared here rather than imported from `daemon.ts` so the pruner has no
|
|
85
|
+
* dependency on the loop that calls it.
|
|
86
|
+
*/
|
|
87
|
+
export declare const PRUNE_ACTOR = "system:daemon";
|
|
88
|
+
/** The terminal states of amended SPEC.md §5.2, and nothing beyond them. */
|
|
89
|
+
export type TerminalState = "executed" | "rejected" | "revoked" | "expired";
|
|
90
|
+
/** Why a payload is prunable. Mirrors `payload.pruned`'s `reason`. */
|
|
91
|
+
export type PruneReason = "payload_retention" | "orphaned";
|
|
92
|
+
/** One file the plan says may go, and the evidence for saying so. */
|
|
93
|
+
export interface PruneCandidate {
|
|
94
|
+
hash: string;
|
|
95
|
+
reason: PruneReason;
|
|
96
|
+
/** The action whose terminal state released the bytes; `null` for an orphan. */
|
|
97
|
+
actionKey: string | null;
|
|
98
|
+
task: string | null;
|
|
99
|
+
terminalState: TerminalState | null;
|
|
100
|
+
/** The `ts` of the event that made it terminal; retention is measured from it. */
|
|
101
|
+
terminalTs: string | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* What one tick would do.
|
|
105
|
+
*
|
|
106
|
+
* `completions` are hashes whose `payload.pruned` is already in the log while the
|
|
107
|
+
* file is still on disk — a crash between the append and the unlink, or a store
|
|
108
|
+
* that was restored from a backup taken before the prune. They are unlinked and
|
|
109
|
+
* **no second event is appended**: the log already states the fact.
|
|
110
|
+
*/
|
|
111
|
+
export interface PrunePlan {
|
|
112
|
+
candidates: PruneCandidate[];
|
|
113
|
+
completions: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* What may be pruned right now. Pure: no I/O, no clock, no policy loading.
|
|
117
|
+
*
|
|
118
|
+
* `retentionMs` is `null` when the policy declares no `payload_retention` — or
|
|
119
|
+
* when the policy could not be loaded at all, which fails closed to the same
|
|
120
|
+
* answer. Either way the plan is empty: the subsystem does not run.
|
|
121
|
+
*
|
|
122
|
+
* `nowIso` is the evaluation moment, injected. A pruner that read the clock
|
|
123
|
+
* itself could not be replayed, and retention arithmetic that cannot be replayed
|
|
124
|
+
* cannot be audited.
|
|
125
|
+
*/
|
|
126
|
+
export declare function planPrune(records: EventRecord[], presentHashes: string[], nowIso: string, retentionMs: number | null): PrunePlan;
|
|
127
|
+
/** Why a prune pass complained. Reported to the caller; never thrown. */
|
|
128
|
+
export type PruneWarningCode =
|
|
129
|
+
/** The verified read refused, so nothing was planned this pass. */
|
|
130
|
+
"log-unreadable"
|
|
131
|
+
/** A `payload.pruned` append was refused; the file stays, untouched. */
|
|
132
|
+
| "append-refused"
|
|
133
|
+
/** The event landed but the file could not be removed. The next tick retries. */
|
|
134
|
+
| "unlink-failed";
|
|
135
|
+
export interface PruneWarning {
|
|
136
|
+
code: PruneWarningCode;
|
|
137
|
+
message: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* One prune that completed: the event landed AND the bytes went (APRV-57).
|
|
141
|
+
*
|
|
142
|
+
* Reported separately from {@link PruneReport.appended} because only these carry
|
|
143
|
+
* a `seq` and only these are finished. A candidate whose append landed and whose
|
|
144
|
+
* unlink FAILED appears in `appended` and in `warnings`, never here: the store
|
|
145
|
+
* still holds the bytes, and a success line for it would say otherwise.
|
|
146
|
+
*
|
|
147
|
+
* Crash-window completions ({@link PrunePlan.completions}) are absent for the
|
|
148
|
+
* opposite reason: they append nothing, so there is no record for a `seq` to
|
|
149
|
+
* name, and the log said the file was gone on some earlier tick already.
|
|
150
|
+
*/
|
|
151
|
+
export interface PrunedRecord {
|
|
152
|
+
candidate: PruneCandidate;
|
|
153
|
+
/** `seq` of the appended `payload.pruned` record. */
|
|
154
|
+
seq: number;
|
|
155
|
+
}
|
|
156
|
+
export interface PruneReport {
|
|
157
|
+
/** `payload_retention` in milliseconds, or `null` when the subsystem is off. */
|
|
158
|
+
retentionMs: number | null;
|
|
159
|
+
/** Hashes for which a `payload.pruned` was appended this pass. */
|
|
160
|
+
appended: PruneCandidate[];
|
|
161
|
+
/** Appended-and-unlinked prunes, with the seq of each event (APRV-57). */
|
|
162
|
+
pruned: PrunedRecord[];
|
|
163
|
+
/** Hashes whose file was removed (appended-then-unlinked, plus completions). */
|
|
164
|
+
removed: string[];
|
|
165
|
+
/** Crash-window files finished without a second event. */
|
|
166
|
+
completed: string[];
|
|
167
|
+
warnings: PruneWarning[];
|
|
168
|
+
}
|
|
169
|
+
export interface PruneOptions {
|
|
170
|
+
/** The append-only log. Re-read before every append. */
|
|
171
|
+
logPath: string;
|
|
172
|
+
/** The payload store. Defaults to the store beside `logPath`. */
|
|
173
|
+
storeDir?: string;
|
|
174
|
+
/** Policy location, with `loadPolicy`'s semantics. */
|
|
175
|
+
policy: {
|
|
176
|
+
dir?: string;
|
|
177
|
+
file?: string;
|
|
178
|
+
};
|
|
179
|
+
schemaDir?: string;
|
|
180
|
+
/** The write-boundary clock (amended SPEC.md §8). Tests inject; production does not. */
|
|
181
|
+
clock?: Clock;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read `payload_retention` from the policy in force right now.
|
|
185
|
+
*
|
|
186
|
+
* Fails closed to `null` exactly as the daemon's TTL read does: a policy that
|
|
187
|
+
* cannot be loaded declares no retention, so nothing is pruned on its behalf. An
|
|
188
|
+
* unparseable duration is likewise `null` — the schema's duration pattern makes
|
|
189
|
+
* that unreachable through a validated policy, and a retention rule this module
|
|
190
|
+
* cannot read is not one it may guess at.
|
|
191
|
+
*/
|
|
192
|
+
export declare function retentionMsOf(policy: {
|
|
193
|
+
dir?: string;
|
|
194
|
+
file?: string;
|
|
195
|
+
}, schemaDir?: string): number | null;
|
|
196
|
+
/**
|
|
197
|
+
* One pruning pass: plan, append, unlink, repeat.
|
|
198
|
+
*
|
|
199
|
+
* The log is re-read before each append rather than once for the pass, because
|
|
200
|
+
* each append moves the head the next one is compared against and because a CLI
|
|
201
|
+
* verb may have appended in between. `expectedHead` therefore always names the
|
|
202
|
+
* head the decision was made from; a `head-moved` refusal drops the candidate and
|
|
203
|
+
* the next tick re-derives it.
|
|
204
|
+
*
|
|
205
|
+
* Nothing is retried in place, and no state survives the call.
|
|
206
|
+
*/
|
|
207
|
+
export declare function prunePayloads(options: PruneOptions): PruneReport;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approval mcp serve --http` — the §10.5 wrapper over the MCP streamable-HTTP
|
|
3
|
+
* transport, one session per connection (APRV-174).
|
|
4
|
+
*
|
|
5
|
+
* The stdio server of `./server.ts` serves ONE client through the pipe the
|
|
6
|
+
* operator handed it: one process, one connection, one identity. This module
|
|
7
|
+
* changes exactly one thing about that picture, the transport, and it changes it
|
|
8
|
+
* so that several clients can be served at once. Everything else is the same
|
|
9
|
+
* code: the tool list is still {@link createApprovalMcpServer}'s, a tool call
|
|
10
|
+
* still builds an argv and calls the function the CLI dispatches to, and a
|
|
11
|
+
* refusal is still the CLI's refusal.
|
|
12
|
+
*
|
|
13
|
+
* ## One `Server` + one transport per session
|
|
14
|
+
*
|
|
15
|
+
* The SDK's `StreamableHTTPServerTransport` is stateful per session: it mints an
|
|
16
|
+
* `mcp-session-id` at `initialize`, stamps it on every response, and expects the
|
|
17
|
+
* client to send it back. This module owns one `node:http` listener and a map
|
|
18
|
+
* from that id to the `{ Server, transport, actor }` triple it belongs to. An
|
|
19
|
+
* initialize POST with no session header opens a triple; every later request is
|
|
20
|
+
* routed by the header; the triple is dropped when its transport closes (a
|
|
21
|
+
* DELETE, or the socket going away).
|
|
22
|
+
*
|
|
23
|
+
* ## Identity is minted BEFORE the transport exists, per session
|
|
24
|
+
*
|
|
25
|
+
* `cli/mcp.ts` settles identity before it constructs the stdio transport,
|
|
26
|
+
* because "a server that connected first and refused later would be a server a
|
|
27
|
+
* client had already spoken to". That property is preserved verbatim here, once
|
|
28
|
+
* per session: {@link mintSessionActor} runs before the `Server`/transport pair
|
|
29
|
+
* is built, and its result is the `actor` that pair closes over for its whole
|
|
30
|
+
* life.
|
|
31
|
+
*
|
|
32
|
+
* **Nothing a client sends reaches that actor.** There is no code path from a
|
|
33
|
+
* header, a URL, an `initialize` payload's `clientInfo`, or a tool argument into
|
|
34
|
+
* the identity a session runs as. `clientInfo.name` is a label a client chose
|
|
35
|
+
* for itself, and SPEC.md §11 says a self-reported field never reduces scrutiny;
|
|
36
|
+
* an identity a caller could name would be an identity a caller could escalate,
|
|
37
|
+
* so the server names it:
|
|
38
|
+
*
|
|
39
|
+
* - plain `--http`: every session runs as the operator's own `--as` /
|
|
40
|
+
* `APPROVAL_AGENT` actor, which is the stdio behavior with more connections;
|
|
41
|
+
* - `--guest`: every session mints its own `agent:guest-<6 hex>`, so the log,
|
|
42
|
+
* the budgets and the refusals see one stranger per connection rather than one
|
|
43
|
+
* crowd. That is the whole reason this scheme exists — an actor a limit can be
|
|
44
|
+
* keyed on.
|
|
45
|
+
*
|
|
46
|
+
* ## Loopback, caps, and no authentication whatsoever
|
|
47
|
+
*
|
|
48
|
+
* This listener authenticates nobody, exactly like `channels/web.ts`. It binds
|
|
49
|
+
* `127.0.0.1` unless the operator writes another host out in full, and the CLI
|
|
50
|
+
* prints a loud banner when they do; the deployment this was built for
|
|
51
|
+
* (SPEC.md §10.5, the crowd demo) is a tunnel in front of a loopback bind, where
|
|
52
|
+
* the tunnel is the thing that faces the network.
|
|
53
|
+
*
|
|
54
|
+
* Two caps bound what a stranger can spend: {@link MAX_CONCURRENT_SESSIONS} live
|
|
55
|
+
* at once and {@link MAX_LIFETIME_SESSIONS} over the process's life. Over either
|
|
56
|
+
* one, an initialize is refused with a plain HTTP 503 naming the reason, before
|
|
57
|
+
* a session exists.
|
|
58
|
+
*/
|
|
59
|
+
import { type ServerPaths } from "./server.js";
|
|
60
|
+
/** Live sessions this listener will hold at once. */
|
|
61
|
+
export declare const MAX_CONCURRENT_SESSIONS = 20;
|
|
62
|
+
/** Sessions this listener will open over the life of the process. */
|
|
63
|
+
export declare const MAX_LIFETIME_SESSIONS = 200;
|
|
64
|
+
/** Largest request body accepted, in bytes. A tool call is small. */
|
|
65
|
+
export declare const MAX_BODY_BYTES: number;
|
|
66
|
+
/** The prefix every guest session's actor carries. */
|
|
67
|
+
export declare const GUEST_ACTOR_PREFIX = "agent:guest-";
|
|
68
|
+
/**
|
|
69
|
+
* Mint one session's actor.
|
|
70
|
+
*
|
|
71
|
+
* `used` is the set of actors this listener has already handed out, for the life
|
|
72
|
+
* of the process rather than of the live map: two sessions that shared an actor
|
|
73
|
+
* would share a budget and a refusal history, which is the one thing this scheme
|
|
74
|
+
* exists to keep apart. Six hex digits is short enough to read out loud in a
|
|
75
|
+
* demo, so the collision check is not decoration.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mintSessionActor(used: ReadonlySet<string>): string;
|
|
78
|
+
export interface HttpServeOptions extends ServerPaths {
|
|
79
|
+
/**
|
|
80
|
+
* The actor every session runs as, or `null` under {@link guest}, where each
|
|
81
|
+
* session mints its own. Already validated by `resolveAgentActor`.
|
|
82
|
+
*/
|
|
83
|
+
actor: string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Guest mode: per-session identity (APRV-174), plus the narrowed tool list,
|
|
86
|
+
* the `mcp-guest-restricted` call-time refusal and the `wait` clamp that
|
|
87
|
+
* `./server.ts` applies (APRV-175).
|
|
88
|
+
*/
|
|
89
|
+
guest: boolean;
|
|
90
|
+
/** Interface to bind. Defaults to `127.0.0.1`; the CLI owns the widening decision. */
|
|
91
|
+
host?: string;
|
|
92
|
+
/** TCP port. `0` asks the kernel for an ephemeral one, which is what tests use. */
|
|
93
|
+
port: number;
|
|
94
|
+
/** Session lifecycle lines. The CLI passes stderr; stdout is never written to. */
|
|
95
|
+
notice?: (text: string) => void;
|
|
96
|
+
}
|
|
97
|
+
export interface McpHttpServer {
|
|
98
|
+
/** The interface actually bound. */
|
|
99
|
+
readonly host: string;
|
|
100
|
+
/** The port actually bound, resolved after an ephemeral request. */
|
|
101
|
+
readonly port: number;
|
|
102
|
+
/** Actors of the live sessions, in open order. Diagnostics and tests. */
|
|
103
|
+
sessionActors(): string[];
|
|
104
|
+
/** How many sessions this listener has opened, ever. */
|
|
105
|
+
lifetimeSessions(): number;
|
|
106
|
+
/** Close every session and the listener. Idempotent. */
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Start the HTTP MCP listener. Resolves once it is bound and its real port is
|
|
111
|
+
* known; rejects when the bind fails, so the CLI can report it and exit.
|
|
112
|
+
*/
|
|
113
|
+
export declare function serveApprovalMcpHttp(options: HttpServeOptions): Promise<McpHttpServer>;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP wrapper of SPEC.md §10.5 — the same verbs as tools, over the CLI's
|
|
3
|
+
* own code paths (APRV-87).
|
|
4
|
+
*
|
|
5
|
+
* §10.5 asks for "a thin MCP server exposing the same verbs as tools … for
|
|
6
|
+
* clients where MCP is more ergonomic than shelling out", and says plainly that
|
|
7
|
+
* "it shares the CLI's code paths". Both halves of that sentence are load-
|
|
8
|
+
* bearing here, and the second one is the whole design: **there is no second
|
|
9
|
+
* implementation of any verb in this file.** A tool call builds an argv, hands
|
|
10
|
+
* it to the function `src/cli/main.ts` dispatches to, captures the `--json`
|
|
11
|
+
* object the CLI would have printed, and returns it. A refusal is the refusal
|
|
12
|
+
* the CLI prints, with the same machine-readable `error.code`.
|
|
13
|
+
*
|
|
14
|
+
* ## The scoping decision: agent-facing only
|
|
15
|
+
*
|
|
16
|
+
* SPEC.md §11 names the agent the **untrusted policy** and the human the
|
|
17
|
+
* **trusted, expensive overseer**. An MCP client is an agent's harness. Offering
|
|
18
|
+
* `grant` to it hands the untrusted policy the overseer's pen, and no amount of
|
|
19
|
+
* care inside the tool implementation would undo that. So the tool list is
|
|
20
|
+
* exactly {@link VERB_REGISTRY} filtered by `human_only === false`, minus the
|
|
21
|
+
* transport exclusions in {@link EXCLUDED_VERBS}, and the registry's `human_only`
|
|
22
|
+
* marker — not a list kept here — is what decides. `verb-registry.ts` says it in
|
|
23
|
+
* its own header: the marker "exists so a wrapper does not offer an agent a door
|
|
24
|
+
* the runtime will only slam".
|
|
25
|
+
*
|
|
26
|
+
* The runtime would slam it anyway (the CLI layer refuses, core refuses again,
|
|
27
|
+
* and the event schema refuses a third time). Publishing the door would still be
|
|
28
|
+
* wrong: a tool list is a statement about what this surface is for.
|
|
29
|
+
*
|
|
30
|
+
* ## Identity is the server's, and a tool call cannot change it
|
|
31
|
+
*
|
|
32
|
+
* The server runs AS one agent identity, fixed when the operator starts it
|
|
33
|
+
* (`--as agent:<id>`, or `APPROVAL_AGENT`). `human:` and `system:` are refused at
|
|
34
|
+
* startup. Every tool whose verb accepts `--as` gets the server's identity
|
|
35
|
+
* appended LAST, after anything the caller supplied, and `--as` is removed from
|
|
36
|
+
* every published input schema so that a client sending it is refused by the
|
|
37
|
+
* schema rather than quietly ignored. Two mechanisms for one rule, because the
|
|
38
|
+
* rule is the reason this server is safe to run at all.
|
|
39
|
+
*
|
|
40
|
+
* ## Guest mode narrows, and never widens (APRV-175)
|
|
41
|
+
*
|
|
42
|
+
* A server built with `guest: true` publishes {@link GUEST_VERBS} INTERSECTED
|
|
43
|
+
* with the list above, refuses anything else at call time with
|
|
44
|
+
* `mcp-guest-restricted` even when a client crafted the name itself, clamps
|
|
45
|
+
* `wait` to {@link GUEST_WAIT_TIMEOUT_MS}, and says all of that in
|
|
46
|
+
* {@link GUEST_INSTRUCTIONS}. Two properties are worth stating plainly: the
|
|
47
|
+
* intersection means guest mode can only ever take tools away, so no name the
|
|
48
|
+
* full server withholds becomes reachable by turning it on; and the refusal
|
|
49
|
+
* lives at CALL time, so `tools/list` describes the boundary rather than being
|
|
50
|
+
* it. The human-only arm is unchanged and is checked first, because "no session
|
|
51
|
+
* on any transport may do this" outranks "this session may not".
|
|
52
|
+
*
|
|
53
|
+
* ## What this server does NOT do
|
|
54
|
+
*
|
|
55
|
+
* - It reads no `.approval/env` (SPEC.md §11.1 invariant 7). The environment a
|
|
56
|
+
* gate operation runs under is the one the operator launched this process
|
|
57
|
+
* with, exactly as for any other `approval` invocation.
|
|
58
|
+
* - It maps nothing onto the MCP tasks/elicitation extension. §10.5 says the
|
|
59
|
+
* tasks extension MAY be mapped onto `awaiting` "when client support
|
|
60
|
+
* stabilizes"; that is post-v1, and until then `wait` blocks and answers.
|
|
61
|
+
* - It appends nothing of its own. `tools/list` touches no file.
|
|
62
|
+
*/
|
|
63
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
64
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
65
|
+
import { type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
66
|
+
import { type JsonSchema, type VerbSpec } from "../cli/verb-registry.js";
|
|
67
|
+
/**
|
|
68
|
+
* Agent-facing verbs that are still not tools, each for a reason that is about
|
|
69
|
+
* the TRANSPORT or the verb's role rather than about authority. Anything
|
|
70
|
+
* excluded for authority reasons is `human_only` in the registry instead, which
|
|
71
|
+
* is where that judgment belongs.
|
|
72
|
+
*/
|
|
73
|
+
export declare const EXCLUDED_VERBS: ReadonlyMap<string, string>;
|
|
74
|
+
/**
|
|
75
|
+
* The verbs a GUEST session may call (APRV-175), as registry labels.
|
|
76
|
+
*
|
|
77
|
+
* A POSITIVE allowlist, and the direction is the whole point. Guest mode exists
|
|
78
|
+
* so strangers can drive a gate over the network without executing anything on
|
|
79
|
+
* the host, and the two verbs that make that dangerous are not the only ones:
|
|
80
|
+
* `run` spawns argv on the server machine, an `adapter <name>` spends vault
|
|
81
|
+
* credentials, `token` hands out spend material, `journal write` writes a local
|
|
82
|
+
* file the operator reads. A deny list would have to name each of those and
|
|
83
|
+
* every one that lands next; this list names what a guest MAY do, so a verb
|
|
84
|
+
* added tomorrow is absent until someone decides otherwise. Fail closed, per
|
|
85
|
+
* SPEC.md §11.
|
|
86
|
+
*
|
|
87
|
+
* The list is the demo's shape: read the guide (`instructions`), declare and
|
|
88
|
+
* ask (`register`, `request`), watch (`wait`, `status`, `queue`), and inspect
|
|
89
|
+
* (`log verify`, `policy check`, `policy test`). Nothing here executes a side
|
|
90
|
+
* effect, and nothing here is a human's authority — those are `human_only` in
|
|
91
|
+
* the registry and were never published on any transport.
|
|
92
|
+
*
|
|
93
|
+
* This narrows and never widens: {@link publishedVerbs} intersects it with the
|
|
94
|
+
* ordinary filter, so a name the full server does not publish cannot become a
|
|
95
|
+
* tool by turning guest mode on.
|
|
96
|
+
*/
|
|
97
|
+
export declare const GUEST_VERBS: ReadonlySet<string>;
|
|
98
|
+
/** Guest `wait` never blocks the shared queue for longer than this. */
|
|
99
|
+
export declare const GUEST_WAIT_TIMEOUT_MS = 5000;
|
|
100
|
+
/** `<name>_<subcommand words>` — `log_verify`, `channel_telegram_health`. */
|
|
101
|
+
export declare function toolName(spec: VerbSpec): string;
|
|
102
|
+
/**
|
|
103
|
+
* The verbs this server publishes, in registry order.
|
|
104
|
+
*
|
|
105
|
+
* Under `guest`, the ordinary filter is INTERSECTED with {@link GUEST_VERBS}:
|
|
106
|
+
* guest mode subtracts and can never add.
|
|
107
|
+
*/
|
|
108
|
+
export declare function publishedVerbs(guest?: boolean): VerbSpec[];
|
|
109
|
+
/**
|
|
110
|
+
* The input schema for one tool: the registry's input schema, with the single
|
|
111
|
+
* documented deletion of `--as`.
|
|
112
|
+
*
|
|
113
|
+
* Nothing else is translated, reshaped or regenerated. The registry's schemas
|
|
114
|
+
* are already `{type:"object", properties:{positionals, flags, trailing?}}` with
|
|
115
|
+
* `additionalProperties:false` throughout, which is exactly what MCP wants a
|
|
116
|
+
* tool `inputSchema` to be, so the SDK's low-level {@link Server} is used rather
|
|
117
|
+
* than `McpServer`: `McpServer.registerTool` takes Zod, and translating JSON
|
|
118
|
+
* Schema into Zod so the SDK can translate it back would put a lossy round trip
|
|
119
|
+
* between the one source and the published contract.
|
|
120
|
+
*
|
|
121
|
+
* `additionalProperties:false` is also what refuses `{"as":"human:carter"}`:
|
|
122
|
+
* there is no such property, at any level, on any published tool.
|
|
123
|
+
*/
|
|
124
|
+
export declare function toolInputSchema(spec: VerbSpec): JsonSchema;
|
|
125
|
+
/** The `tools/list` answer, derived entirely from the registry. */
|
|
126
|
+
export declare function toolDefinitions(guest?: boolean): Tool[];
|
|
127
|
+
/** The environment variable an operator may use instead of `--as`. */
|
|
128
|
+
export declare const AGENT_ACTOR_ENV = "APPROVAL_AGENT";
|
|
129
|
+
export type IdentityCheck = {
|
|
130
|
+
ok: true;
|
|
131
|
+
actor: string;
|
|
132
|
+
} | {
|
|
133
|
+
ok: false;
|
|
134
|
+
message: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Resolve and validate the identity this server acts as.
|
|
138
|
+
*
|
|
139
|
+
* A `human:` value is refused rather than accepted-and-downgraded: an operator
|
|
140
|
+
* who typed one meant something, and the something they meant is not available
|
|
141
|
+
* here. `system:` is refused for the same reason `expire` takes no identity —
|
|
142
|
+
* the runtime's own actor is not a thing a caller supplies.
|
|
143
|
+
*/
|
|
144
|
+
export declare function resolveAgentActor(flag: string | null, env?: NodeJS.ProcessEnv): IdentityCheck;
|
|
145
|
+
/** Pinned paths the operator chose when launching the server. */
|
|
146
|
+
export interface ServerPaths {
|
|
147
|
+
/** Working directory every relative path resolves against. */
|
|
148
|
+
cwd: string;
|
|
149
|
+
/** `--log`, when the operator pinned one. */
|
|
150
|
+
log?: string;
|
|
151
|
+
/** `--policy`, when the operator pinned one. */
|
|
152
|
+
policy?: string;
|
|
153
|
+
}
|
|
154
|
+
export interface ServerOptions extends ServerPaths {
|
|
155
|
+
/** `agent:<id>`; already validated by {@link resolveAgentActor}. */
|
|
156
|
+
actor: string;
|
|
157
|
+
/**
|
|
158
|
+
* The invoke queue this server runs its tool calls through (APRV-174).
|
|
159
|
+
*
|
|
160
|
+
* Omitted on stdio, where one process serves one client and
|
|
161
|
+
* {@link createApprovalMcpServer} builds its own. The HTTP transport passes
|
|
162
|
+
* ONE queue shared by every session, because the reason the queue exists
|
|
163
|
+
* (`wait` blocks the event loop with `Atomics.wait`, `run` uses `spawnSync`)
|
|
164
|
+
* is a property of the process, not of the connection.
|
|
165
|
+
*/
|
|
166
|
+
serialize?: <T>(work: () => Promise<T>) => Promise<T>;
|
|
167
|
+
/**
|
|
168
|
+
* Guest mode (APRV-175): the tool list narrows to {@link GUEST_VERBS}, a call
|
|
169
|
+
* to anything else is refused `mcp-guest-restricted` at CALL time, and `wait`
|
|
170
|
+
* is clamped to {@link GUEST_WAIT_TIMEOUT_MS}.
|
|
171
|
+
*/
|
|
172
|
+
guest?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The `--timeout` a guest's `wait` actually runs with (APRV-175).
|
|
176
|
+
*
|
|
177
|
+
* A clamp rather than an override: a caller asking for less than the ceiling
|
|
178
|
+
* gets what they asked for, and anything larger, unparseable or absent becomes
|
|
179
|
+
* {@link GUEST_WAIT_TIMEOUT_MS}. It is appended LAST, the same mechanism that
|
|
180
|
+
* pins `--as`, so the caller's own value loses without being rejected.
|
|
181
|
+
*
|
|
182
|
+
* The ceiling is not politeness. `wait` blocks the event loop (`Atomics.wait`)
|
|
183
|
+
* and every HTTP session shares one invoke queue, so an unbounded `wait` is one
|
|
184
|
+
* stranger stalling every other session and the listener with them.
|
|
185
|
+
*/
|
|
186
|
+
export declare function guestWaitTimeout(rawFlags: unknown): string;
|
|
187
|
+
export type ArgvBuild = {
|
|
188
|
+
ok: true;
|
|
189
|
+
argv: string[];
|
|
190
|
+
} | {
|
|
191
|
+
ok: false;
|
|
192
|
+
code: string;
|
|
193
|
+
message: string;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Map one tool call's arguments onto the argv the CLI would have been given.
|
|
197
|
+
*
|
|
198
|
+
* Validation here is deliberate rather than delegated: the low-level MCP
|
|
199
|
+
* {@link Server} does not validate `arguments` against a tool's `inputSchema`,
|
|
200
|
+
* and "the schema said additionalProperties:false" has to be enforced by
|
|
201
|
+
* something. So an unknown top-level key, an unknown flag, or a non-string
|
|
202
|
+
* argument is refused, which is what makes `{"as":"human:carter"}` a refusal
|
|
203
|
+
* instead of a silently dropped field.
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildArgv(spec: VerbSpec, rawArgs: unknown, options: ServerOptions): ArgvBuild;
|
|
206
|
+
/** What one verb invocation produced. */
|
|
207
|
+
interface Invocation {
|
|
208
|
+
code: number;
|
|
209
|
+
stdout: string;
|
|
210
|
+
stderr: string;
|
|
211
|
+
child?: {
|
|
212
|
+
stdout: string;
|
|
213
|
+
stderr: string;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** The `_meta` key prefix this server stamps its out-of-band facts under. */
|
|
217
|
+
export declare const META_PREFIX = "approval.md/";
|
|
218
|
+
/**
|
|
219
|
+
* Shape one invocation into a tool result.
|
|
220
|
+
*
|
|
221
|
+
* A gate refusal is a RESULT, never a thrown JSON-RPC error: the command was
|
|
222
|
+
* well-formed and the runtime's answer was no, which is a fact the caller must
|
|
223
|
+
* be able to read as data. It comes back as `isError: true` carrying the same
|
|
224
|
+
* `{"error":{"code","message"}}` object the CLI prints, so a client branches on
|
|
225
|
+
* `error.code` exactly as it would on the CLI's stderr.
|
|
226
|
+
*
|
|
227
|
+
* A non-zero exit that carries a SUCCESS-shaped object is not an error: `wait`
|
|
228
|
+
* exits 1 on a rejection and 6 on a timeout, `status` exits 1 when something
|
|
229
|
+
* needs attention, `log verify` exits 3 on a torn tail. Those are answers. The
|
|
230
|
+
* exit code always travels in `_meta` so nothing is lost either way.
|
|
231
|
+
*/
|
|
232
|
+
export declare function toolResult(result: Invocation): CallToolResult;
|
|
233
|
+
/**
|
|
234
|
+
* Serialize everything.
|
|
235
|
+
*
|
|
236
|
+
* Two reasons, and only the first is about this process. In-process, several
|
|
237
|
+
* verbs are synchronous and blocking (`wait` sleeps with `Atomics.wait`, `run`
|
|
238
|
+
* uses `spawnSync`), so overlapping them buys nothing and interleaves their
|
|
239
|
+
* captured output. Across processes, an append still goes through
|
|
240
|
+
* `core/log.ts`'s lockfile and compare-and-append, so a CLI running beside this
|
|
241
|
+
* server is safe whatever this queue does — the queue is politeness, the
|
|
242
|
+
* lockfile is the guarantee.
|
|
243
|
+
*
|
|
244
|
+
* APRV-174: over HTTP one process serves many sessions, and the first reason
|
|
245
|
+
* above is about the process. `ServerOptions.serialize` therefore lets the HTTP
|
|
246
|
+
* listener hand every session the SAME queue; stdio passes nothing and gets a
|
|
247
|
+
* fresh one, exactly as before.
|
|
248
|
+
*/
|
|
249
|
+
export declare function serializer(): <T>(work: () => Promise<T>) => Promise<T>;
|
|
250
|
+
/**
|
|
251
|
+
* What a GUEST session is told at connect time (APRV-175).
|
|
252
|
+
*
|
|
253
|
+
* Distinct from the full text because the situation is distinct, and the two
|
|
254
|
+
* facts a guest most needs are the two a full client never needs: `wait` comes
|
|
255
|
+
* back fast, and a granted request executes NOWHERE. Saying the second one
|
|
256
|
+
* plainly matters more than it looks. A stranger who saw their action granted
|
|
257
|
+
* and assumed an email went out has learned the wrong thing about this system;
|
|
258
|
+
* what they are driving is the approval flow itself.
|
|
259
|
+
*/
|
|
260
|
+
export declare const GUEST_INSTRUCTIONS = "approval.md gates agent actions that touch the world, and you are connected to it as a GUEST. The tools here are a narrow slice of the agent surface: read the guide (`instructions`), declare an action in a task envelope and register it (`register`), ask for it (`request`), then watch (`wait`, `status`, `queue`) and inspect (`log_verify`, `policy_check`, `policy_test`). Calling anything else is refused `mcp-guest-restricted` whether or not it is listed, because everything else executes on, or spends the credentials of, the machine hosting this gate. Two things to expect. `wait` returns FAST: a guest's timeout is clamped to five seconds server-side, so treat it as a poll and call `status` again rather than asking for a longer one. And NOTHING YOU ARE GRANTED EXECUTES ANYWHERE: there is no `run` here, no adapter, and no side effect at the end of the flow. What you are driving is the approval flow itself \u2014 a real request, a real human decision, a real hash-chained record of both. Your identity is this session's alone (`agent:guest-<id>`, chosen by the server), and everything you do is recorded under it.";
|
|
261
|
+
/** Build the MCP server. Nothing is connected until {@link Server.connect}. */
|
|
262
|
+
export declare function createApprovalMcpServer(options: ServerOptions): Server;
|
|
263
|
+
/** Build the server and connect it to `transport`. Resolves once connected. */
|
|
264
|
+
export declare function serveApprovalMcp(options: ServerOptions, transport: Transport): Promise<Server>;
|
|
265
|
+
export {};
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `grant` to it hands the untrusted policy the overseer's pen, and no amount of
|
|
19
19
|
* care inside the tool implementation would undo that. So the tool list is
|
|
20
20
|
* exactly {@link VERB_REGISTRY} filtered by `human_only === false`, minus the
|
|
21
|
-
*
|
|
21
|
+
* transport exclusions in {@link EXCLUDED_VERBS}, and the registry's `human_only`
|
|
22
22
|
* marker — not a list kept here — is what decides. `verb-registry.ts` says it in
|
|
23
23
|
* its own header: the marker "exists so a wrapper does not offer an agent a door
|
|
24
24
|
* the runtime will only slam".
|
|
@@ -85,6 +85,10 @@ export const EXCLUDED_VERBS = new Map([
|
|
|
85
85
|
"consume",
|
|
86
86
|
"internal plumbing: its own purpose says so. `run` wraps it and is published instead, so a client that reached for `consume` would be spending a token outside the verb that records the outcome.",
|
|
87
87
|
],
|
|
88
|
+
[
|
|
89
|
+
"log follow",
|
|
90
|
+
"it is an unbounded foreground stream. MCP tool calls share a finite request queue and return one finite result, so publishing this verb would occupy that queue indefinitely; MCP clients can poll log verify or use the CLI stream as a separate process.",
|
|
91
|
+
],
|
|
88
92
|
[
|
|
89
93
|
"hook claude-code",
|
|
90
94
|
"it reads one PreToolUse event from STDIN and its registry input schema has nowhere to put that event. On a stdio server, stdin is the JSON-RPC stream: a tool that read it would eat the protocol. It is also the wrong shape for MCP — a harness that can call tools calls `request` and `wait` directly.",
|
|
@@ -93,6 +97,10 @@ export const EXCLUDED_VERBS = new Map([
|
|
|
93
97
|
"hook cursor",
|
|
94
98
|
"it reads one Cursor preToolUse event from STDIN and its registry input schema has nowhere to put that event. On a stdio server, stdin is the JSON-RPC stream: a tool that read it would eat the protocol. It is also the wrong shape for MCP — a harness that can call tools calls `request` and `wait` directly.",
|
|
95
99
|
],
|
|
100
|
+
[
|
|
101
|
+
"hook codex",
|
|
102
|
+
"it reads one Codex PreToolUse or PostToolUse event from STDIN and its registry input schema has nowhere to put that event. On a stdio server, stdin is the JSON-RPC stream: a tool that read it would eat the protocol. It is also the wrong shape for MCP — a harness that can call tools calls `request` and `wait` directly.",
|
|
103
|
+
],
|
|
96
104
|
]);
|
|
97
105
|
/**
|
|
98
106
|
* The verbs a GUEST session may call (APRV-175), as registry labels.
|