approval-md 0.3.0 → 0.4.1
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 +83 -914
- package/SPEC.md +101 -13
- package/cli.js +134 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +72 -0
- package/dist/src/channels/contract.js +161 -1
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +25 -0
- package/dist/src/channels/tagging.js +63 -6
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-minimal.d.ts +251 -0
- package/dist/src/channels/telegram-minimal.js +618 -0
- package/dist/src/channels/telegram-minimal.js.map +1 -0
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +725 -20
- package/dist/src/channels/telegram.js +1332 -123
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +358 -5
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.js +43 -6
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +1 -1
- package/dist/src/cli/audit-card.js +40 -1
- package/dist/src/cli/audit-card.js.map +1 -1
- package/dist/src/cli/audit.d.ts +7 -5
- package/dist/src/cli/audit.js +22 -10
- package/dist/src/cli/audit.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
- package/dist/src/cli/channel-telegram-webhook.js +716 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +648 -56
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +1 -1
- package/dist/src/cli/channel.js +5 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/daemon.js +37 -5
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +856 -111
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +29 -1
- package/dist/src/cli/execute.js +387 -21
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +10 -1
- package/dist/src/cli/help.js +283 -54
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +485 -6
- package/dist/src/cli/hook.js +2370 -70
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/log-unlock.d.ts +23 -0
- package/dist/src/cli/log-unlock.js +109 -0
- package/dist/src/cli/log-unlock.js.map +1 -0
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +72 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- package/dist/src/cli/scaffold.js +6 -0
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/up.js +26 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +338 -39
- 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/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +20 -3
- package/dist/src/core/attest.js +25 -0
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +153 -12
- package/dist/src/core/audit.js +415 -20
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -0
- package/dist/src/core/command-class.d.ts +51 -0
- package/dist/src/core/command-class.js +766 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/gate.d.ts +364 -6
- package/dist/src/core/gate.js +802 -27
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +179 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/log-lock.d.ts +435 -0
- package/dist/src/core/log-lock.js +1093 -0
- package/dist/src/core/log-lock.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +130 -22
- package/dist/src/core/log.js +473 -50
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +7 -1
- package/dist/src/core/policy-explain.js +16 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +24 -0
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +38 -0
- package/dist/src/core/policy-match.js +35 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +40 -0
- package/dist/src/core/policy-proposal.js +37 -16
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +186 -1
- package/dist/src/core/prompt-layout.js +345 -3
- package/dist/src/core/prompt-layout.js.map +1 -1
- package/dist/src/core/read-scope.d.ts +10 -0
- package/dist/src/core/read-scope.js +4 -0
- package/dist/src/core/read-scope.js.map +1 -1
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sender-identity.d.ts +59 -11
- package/dist/src/core/sender-identity.js +79 -11
- package/dist/src/core/sender-identity.js.map +1 -1
- package/dist/src/core/state.d.ts +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/validate.d.ts +20 -6
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +298 -7
- 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/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +22 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +166 -0
- package/dist/src/serve/archive.js +363 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +768 -0
- package/docs/cli-reference.md +2139 -67
- package/package.json +4 -2
- package/schema/event.schema.json +413 -30
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
- package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
- package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
- package/schema/fixtures/event/valid/audit-sampled.json +2 -1
- package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +16 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
- package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
- package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
- package/schema/policy.schema.json +102 -6
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thread a `POST /hook/<harness>` call runs on (APRV-427).
|
|
3
|
+
*
|
|
4
|
+
* A harness hook is synchronous from its first line to its verdict, and its
|
|
5
|
+
* wait sleeps with `Atomics.wait`. Run on the listener's main thread, one hook
|
|
6
|
+
* waiting on a human froze the event loop for the whole of that wait: no socket
|
|
7
|
+
* was read, so the tenant's `GET /status` and `POST /verb/queue` sat unanswered
|
|
8
|
+
* until the question was decided, and N gated calls from one sandbox queued
|
|
9
|
+
* behind each other. Scoping a lock cannot fix a thread that does not run, so
|
|
10
|
+
* the call runs here instead and the main thread stays free.
|
|
11
|
+
*
|
|
12
|
+
* This file runs the hook and nothing else. The verdict is `commandHook`'s, with
|
|
13
|
+
* the request body as the stdin it would have read, exactly as before; what is
|
|
14
|
+
* added is the {@link HookWaitSeam}, which is how this thread hands the store
|
|
15
|
+
* lock back to the listener while the hook polls and asks for it again before
|
|
16
|
+
* the hook appends.
|
|
17
|
+
*
|
|
18
|
+
* ## The handshake
|
|
19
|
+
*
|
|
20
|
+
* The lock lives on the main thread (`serve/server.ts`), because every other
|
|
21
|
+
* writer in the process takes it there. This thread holds it BY PROXY: the
|
|
22
|
+
* main thread acquires it on this thread's behalf and flips a shared flag.
|
|
23
|
+
*
|
|
24
|
+
* - A job arrives with the flag at 0. This thread first reads the verified log
|
|
25
|
+
* ONCE, outside the lock, to warm its own read cache (below), then posts
|
|
26
|
+
* `resume` and blocks on the flag before it runs a line of the hook, so
|
|
27
|
+
* intake, the sweep, register and request all happen under the lock.
|
|
28
|
+
* - `enterWait` clears the flag FIRST and then posts `wait`: the main thread
|
|
29
|
+
* releases the lock, and a later `leaveWait` cannot be satisfied by the grant
|
|
30
|
+
* that has just ended.
|
|
31
|
+
* - `leaveWait` posts `resume` and blocks on the flag until the main thread has
|
|
32
|
+
* re-acquired the lock and set it to 1.
|
|
33
|
+
* - `done` carries the exit code and both streams, and ends the last section.
|
|
34
|
+
*
|
|
35
|
+
* A blocked `Atomics.wait` here costs this thread and nothing else, which is
|
|
36
|
+
* the point of the thread.
|
|
37
|
+
*
|
|
38
|
+
* ## The warm read, outside the lock (APRV-427 review)
|
|
39
|
+
*
|
|
40
|
+
* A new thread's cache is empty, and its first verified read walks the whole
|
|
41
|
+
* chain: 633 ms on a mature tenant log. Taken inside the lock, five threads
|
|
42
|
+
* arriving together serialised five cold walks and held the tenant's `status`
|
|
43
|
+
* behind all of them. So the cold walk happens here, before the lock is asked
|
|
44
|
+
* for, and in parallel with every other thread's. The hook's own reads under
|
|
45
|
+
* the lock are then warm: a hash over the prefix this thread already proved,
|
|
46
|
+
* plus a walk of whatever was appended since. Nothing is decided from the warm
|
|
47
|
+
* read itself, and nothing is appended on the strength of it; every append
|
|
48
|
+
* inside the lock is still compare-and-append against a head read inside the
|
|
49
|
+
* lock (SPEC.md §11.1 invariant 5).
|
|
50
|
+
*/
|
|
51
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
52
|
+
import { commandHook } from "../cli/hook.js";
|
|
53
|
+
import { declareResolvedDaemonIdentity, narrowerAllowlist, refreshDaemonAllowlistFrom, } from "../core/daemon-host.js";
|
|
54
|
+
import { daemonIdentity, setDaemonAllowlist } from "../core/daemon-identity.js";
|
|
55
|
+
import { harnessCapFitsMargin } from "../core/harness-wait.js";
|
|
56
|
+
import { processReadCache, readVerifiedRecords, useVerifiedSnapshots } from "../core/state.js";
|
|
57
|
+
const port = parentPort;
|
|
58
|
+
if (port === null) {
|
|
59
|
+
throw new Error("serve/hook-worker.ts runs as a worker thread and was loaded on the main thread");
|
|
60
|
+
}
|
|
61
|
+
// This thread is an `approval hook` process in every way that matters to a
|
|
62
|
+
// read, so it makes the same opt-in the hook makes on its gated path (APRV-188):
|
|
63
|
+
// a read may resume behind the daemon's published snapshot, admitted only
|
|
64
|
+
// against a hash this thread computes itself. Set once, before the warm read,
|
|
65
|
+
// so the warm read and the hook's own reads are one cache under one proof.
|
|
66
|
+
useVerifiedSnapshots(true);
|
|
67
|
+
// APRV-448. This thread appends on the gate's behalf exactly as the listener's
|
|
68
|
+
// own thread does, so it declares the same identity before it runs a line of
|
|
69
|
+
// any hook. Declared once: the resolution is the listener's, fixed at startup.
|
|
70
|
+
const writer = workerData;
|
|
71
|
+
declareResolvedDaemonIdentity(writer.daemon);
|
|
72
|
+
port.on("message", (job) => {
|
|
73
|
+
const flag = new Int32Array(job.flag);
|
|
74
|
+
const post = (message) => port.postMessage(message);
|
|
75
|
+
// Cold reads (cache misses) made inside a locked section, counted section
|
|
76
|
+
// by section: `mark` is taken when the lock arrives and settled when it goes.
|
|
77
|
+
let coldReadsInLock = 0;
|
|
78
|
+
let mark = 0;
|
|
79
|
+
const settle = () => {
|
|
80
|
+
coldReadsInLock += processReadCache.stats.misses - mark;
|
|
81
|
+
};
|
|
82
|
+
/** Block until the main thread has taken the store lock for this thread. */
|
|
83
|
+
const held = () => {
|
|
84
|
+
while (Atomics.load(flag, 0) !== 1)
|
|
85
|
+
Atomics.wait(flag, 0, 0);
|
|
86
|
+
mark = processReadCache.stats.misses;
|
|
87
|
+
};
|
|
88
|
+
const seam = {
|
|
89
|
+
enterWait: () => {
|
|
90
|
+
settle();
|
|
91
|
+
Atomics.store(flag, 0, 0);
|
|
92
|
+
post({ type: "wait" });
|
|
93
|
+
},
|
|
94
|
+
leaveWait: () => {
|
|
95
|
+
post({ type: "resume" });
|
|
96
|
+
held();
|
|
97
|
+
// Every append after a wait (the execution after a grant, an expiry) is
|
|
98
|
+
// judged against the list in force NOW, not the one read before a wait
|
|
99
|
+
// that may have lasted minutes (APRV-448 review).
|
|
100
|
+
refreshDaemonAllowlistFrom(job.logPath, writer.policy);
|
|
101
|
+
},
|
|
102
|
+
cancelled: () => Atomics.load(flag, 1) === 1,
|
|
103
|
+
};
|
|
104
|
+
const gone = () => Atomics.load(flag, 1) === 1;
|
|
105
|
+
const out = [];
|
|
106
|
+
const err = [];
|
|
107
|
+
try {
|
|
108
|
+
// Outside the lock, and its answer is thrown away: its only effect is the
|
|
109
|
+
// proved prefix this thread's cache now holds. A read that fails here is
|
|
110
|
+
// the hook's to meet and report, inside the lock, in its own words.
|
|
111
|
+
//
|
|
112
|
+
// A caller that went away before the hook began is owed nothing and has
|
|
113
|
+
// asked nothing yet (APRV-427 review), so the call ends without running a
|
|
114
|
+
// line of the hook: looked at before the warm read, after it, and once
|
|
115
|
+
// more when the lock arrives, because the wait for the lock is where a
|
|
116
|
+
// queue of busy sections makes a client give up. Past this point the
|
|
117
|
+
// hook's own poll is what notices.
|
|
118
|
+
if (gone()) {
|
|
119
|
+
post({ type: "cancelled" });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
readVerifiedRecords(job.logPath);
|
|
123
|
+
if (gone()) {
|
|
124
|
+
post({ type: "cancelled" });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
post({ type: "resume" });
|
|
128
|
+
held();
|
|
129
|
+
if (gone()) {
|
|
130
|
+
settle();
|
|
131
|
+
post({ type: "cancelled" });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// The `daemons` allowlist in force for this call (APRV-448), resolved from
|
|
135
|
+
// the attested policy and this thread's verified read, inside the lock and
|
|
136
|
+
// before the hook appends anything: the listener's thread does the same
|
|
137
|
+
// before every verb call. A resolution that fails leaves the previous one
|
|
138
|
+
// standing, as the daemon's tick does; and the standing one is never wider
|
|
139
|
+
// than the listener's, so a thread spawned after the policy lost its
|
|
140
|
+
// attestation does not start unrestricted (APRV-448 review).
|
|
141
|
+
setDaemonAllowlist(narrowerAllowlist(daemonIdentity()?.allowed ?? null, job.allowed));
|
|
142
|
+
refreshDaemonAllowlistFrom(job.logPath, writer.policy);
|
|
143
|
+
// The budget, looked at once more now the lock is here (APRV-427 review):
|
|
144
|
+
// a call that had room when it arrived and spent it waiting in line or for
|
|
145
|
+
// the lock would open a question the harness kills its asker before
|
|
146
|
+
// anyone can answer. Refused as saturation instead, appending nothing.
|
|
147
|
+
//
|
|
148
|
+
// Measured ONCE, and the same number is what the hook judges by, waits by
|
|
149
|
+
// and records: a remainder re-measured inside the hook a few milliseconds
|
|
150
|
+
// later could fall under the margin after this check had admitted it, and
|
|
151
|
+
// turn an admitted call into `hook-harness-cap-too-short`.
|
|
152
|
+
const { arrivedAt, capMs } = job.budget;
|
|
153
|
+
if (capMs !== null) {
|
|
154
|
+
const remainingMs = Math.max(0, capMs - Math.max(0, Date.now() - arrivedAt));
|
|
155
|
+
if (harnessCapFitsMargin(capMs) && !harnessCapFitsMargin(remainingMs)) {
|
|
156
|
+
settle();
|
|
157
|
+
post({ type: "saturated", remainingMs });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
seam.remainingCapMs = remainingMs;
|
|
161
|
+
}
|
|
162
|
+
const code = commandHook(job.argv, { out: (text) => out.push(text), err: (text) => err.push(text) }, job.cwd, () => job.body, seam);
|
|
163
|
+
settle();
|
|
164
|
+
post({ type: "done", code, stdout: out.join(""), stderr: err.join(""), coldReadsInLock });
|
|
165
|
+
}
|
|
166
|
+
catch (cause) {
|
|
167
|
+
// `commandHook` already turns a throw inside the harness path into an
|
|
168
|
+
// ordinary deny, so reaching this is a failure of the thread itself. It is
|
|
169
|
+
// reported as one, and the listener answers it as a refusal: never an allow.
|
|
170
|
+
post({ type: "failed", message: cause instanceof Error ? cause.message : String(cause) });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
//# sourceMappingURL=hook-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-worker.js","sourceRoot":"","sources":["../../../src/serve/hook-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approval serve` — the agent-facing surface over HTTP, for a harness that
|
|
3
|
+
* has no local log and no local policy (APRV-421).
|
|
4
|
+
*
|
|
5
|
+
* A tenant whose harness runs in a sandbox (Agent Village's Hermes tenants,
|
|
6
|
+
* the Meta Muse connector packet) has nowhere for `approval hook <harness>` to
|
|
7
|
+
* write and nothing for it to read. The hosted daemon runs elsewhere, one
|
|
8
|
+
* process per tenant (design/hosted-daemon-identity.md); what was missing was a
|
|
9
|
+
* way to reach it. This is that door, and it is a DOOR rather than a second
|
|
10
|
+
* gate.
|
|
11
|
+
*
|
|
12
|
+
* ## Transport only, and the file says so by importing rather than repeating
|
|
13
|
+
*
|
|
14
|
+
* Every verdict, every verb and every batch of records here comes from the
|
|
15
|
+
* function the CLI dispatches to:
|
|
16
|
+
*
|
|
17
|
+
* - the verb surface is `mcp/server.ts`'s — {@link publishedVerbs},
|
|
18
|
+
* {@link toolDefinitions}, {@link buildArgv} and {@link invokeVerb}, so the
|
|
19
|
+
* catalog MATCHES `approval mcp serve` because it IS `approval mcp serve`'s,
|
|
20
|
+
* `--as` is absent from every published schema for the same one reason, and
|
|
21
|
+
* the launch identity is appended last by the same one line;
|
|
22
|
+
* - a hook call is `commandHook` with the request body as its stdin, so the
|
|
23
|
+
* bytes on the response are the bytes the stdin form prints, for every
|
|
24
|
+
* adapter in `HARNESS_ADAPTERS`, with no dialect knowledge in this file;
|
|
25
|
+
* - a follow page is `core/log-subscribe.ts`'s verified subscription, drained
|
|
26
|
+
* once (`serve/follow.ts`).
|
|
27
|
+
*
|
|
28
|
+
* Nothing here classifies a command, resolves a policy, mints a token or
|
|
29
|
+
* verifies a chain. Two implementations of the gate sequence would be two
|
|
30
|
+
* gates, and the second would be the one nobody reviewed.
|
|
31
|
+
*
|
|
32
|
+
* ## Two credentials, two parties
|
|
33
|
+
*
|
|
34
|
+
* `serve/credentials.ts` holds the reasoning. The short form: the agent
|
|
35
|
+
* credential opens the verbs and the hook, the tenant credential opens the
|
|
36
|
+
* log, the export and status, and the agent credential never reads the log it
|
|
37
|
+
* is judged by.
|
|
38
|
+
*
|
|
39
|
+
* ## Which daemon wrote it (APRV-448)
|
|
40
|
+
*
|
|
41
|
+
* Every record this process appends carries the `daemon` field, with the id
|
|
42
|
+
* `approval status` reports for the same store: `APPROVAL_DAEMON_ID` from the
|
|
43
|
+
* launch environment, or the id derived from the store's instance, resolved
|
|
44
|
+
* ONCE at startup by `core/daemon-host.ts`'s `resolveDaemonId`, the function
|
|
45
|
+
* the daemon loop resolves its own id with. The listener's thread declares it
|
|
46
|
+
* before it binds, and every hook thread declares the same resolution before
|
|
47
|
+
* its first job (`serve/hook-worker.ts`), so a verb's records and a hook's
|
|
48
|
+
* carry one id. No request names any part of it.
|
|
49
|
+
*
|
|
50
|
+
* The attested policy's `daemons` allowlist is enforced at the write boundary
|
|
51
|
+
* exactly as it is for the daemon: refreshed from the verified log inside the
|
|
52
|
+
* store lock before every verb call and every hook call, and an id the list
|
|
53
|
+
* does not admit is refused `daemon-not-allowed` with nothing written. This
|
|
54
|
+
* process does NOT mark itself the daemon (`core/daemon-actor.ts`): that mark
|
|
55
|
+
* routes an advance under the daemon's own autonomous class, and a process that
|
|
56
|
+
* dispatches the verbs an agent asks for must never hold it.
|
|
57
|
+
*
|
|
58
|
+
* ## What it does not do
|
|
59
|
+
*
|
|
60
|
+
* - **No TLS.** There is no certificate handling in this process and there is
|
|
61
|
+
* not going to be: the supported deployment is a loopback bind behind a
|
|
62
|
+
* proxy the operator owns, and a non-loopback bind requires an explicit flag
|
|
63
|
+
* and prints a banner (`cli/serve.ts`).
|
|
64
|
+
* - **No record of its own.** This server appends nothing on its own account.
|
|
65
|
+
* Every event in the log under it was written by a verb a caller asked for,
|
|
66
|
+
* under the identity the operator fixed at launch.
|
|
67
|
+
* - **No state a restart loses.** The only things held between requests are
|
|
68
|
+
* the store lock and a few warm hook threads, which are properties of the
|
|
69
|
+
* process rather than of any caller. Follow cursors belong to the caller, so
|
|
70
|
+
* a host that sleeps and wakes serves the same next page it would have served
|
|
71
|
+
* before.
|
|
72
|
+
*
|
|
73
|
+
* ## What is serialised, and what is not (APRV-427)
|
|
74
|
+
*
|
|
75
|
+
* One lock per store ({@link storeLock}). It covers every stretch of work in
|
|
76
|
+
* this process that may append to the store or must read it as one snapshot:
|
|
77
|
+
* a verb call, a follow page, the export, and a hook call's MUTATION sections
|
|
78
|
+
* (everything up to its poll loop, and everything after it). It does not cover
|
|
79
|
+
* a hook call's WAIT: the call runs on a worker thread (`serve/hook-thread.ts`)
|
|
80
|
+
* and hands the lock back for as long as it polls, so one question waiting on
|
|
81
|
+
* a human no longer holds the tenant's `status`, `queue` and follow, or the
|
|
82
|
+
* next hook call's own question, until the human answers. The catalog takes no
|
|
83
|
+
* lock at all. Across processes the guarantee is unchanged and is not this
|
|
84
|
+
* lock's: every append goes through `core/log.ts`'s lockfile and
|
|
85
|
+
* compare-and-append.
|
|
86
|
+
* - **No `.approval/env`.** SPEC.md §11.1 invariant 7, inherited verbatim from
|
|
87
|
+
* the MCP server: the environment a gate operation runs under is the one the
|
|
88
|
+
* host launched this process with.
|
|
89
|
+
*/
|
|
90
|
+
import { type HookThreadStats, type StoreLock } from "./hook-thread.js";
|
|
91
|
+
/**
|
|
92
|
+
* Identity is resolved by the MCP server's own function, re-exported here so
|
|
93
|
+
* that `cli/serve.ts` reaches it through ITS server exactly as `cli/mcp.ts`
|
|
94
|
+
* reaches it through the MCP one.
|
|
95
|
+
*
|
|
96
|
+
* Two transports fixing an identity by two rules would be two answers to
|
|
97
|
+
* "which identities may this process act as", and the answer has to be one:
|
|
98
|
+
* `agent:<id>` and nothing else, refused at startup, never supplied by a
|
|
99
|
+
* caller.
|
|
100
|
+
*/
|
|
101
|
+
export { resolveAgentActor, type IdentityCheck } from "../mcp/server.js";
|
|
102
|
+
import { type ServeCredentials, type ServeScope } from "./credentials.js";
|
|
103
|
+
/** Largest request body accepted. A tool call and a hook envelope are small. */
|
|
104
|
+
export declare const MAX_BODY_BYTES: number;
|
|
105
|
+
/**
|
|
106
|
+
* How much of an oversized body this server will read and throw away before
|
|
107
|
+
* dropping the socket.
|
|
108
|
+
*
|
|
109
|
+
* An oversized request is refused at {@link MAX_BODY_BYTES}, but the refusal
|
|
110
|
+
* cannot be WRITTEN until the client stops writing, or the response closes the
|
|
111
|
+
* socket under it and the caller sees a transport error instead of a verdict.
|
|
112
|
+
* So the remainder is discarded as it arrives, up to this much, and beyond it
|
|
113
|
+
* the connection is simply dropped: a caller streaming without end is not a
|
|
114
|
+
* caller owed a polite answer.
|
|
115
|
+
*/
|
|
116
|
+
export declare const DRAIN_LIMIT_BYTES: number;
|
|
117
|
+
/**
|
|
118
|
+
* How much of a captured stream travels back in a response body.
|
|
119
|
+
*
|
|
120
|
+
* Both streams are bounded, and both carry a flag saying whether they were
|
|
121
|
+
* clipped. Generous enough that a verdict is never clipped in practice, and
|
|
122
|
+
* present so that a verb which decided to print a megabyte cannot make a
|
|
123
|
+
* response unsendable. Clipping happens at a CODEPOINT boundary
|
|
124
|
+
* ({@link clip}), so what comes back is always valid text.
|
|
125
|
+
*/
|
|
126
|
+
export declare const MAX_STREAM_BYTES: number;
|
|
127
|
+
/**
|
|
128
|
+
* The exit code every server-authored refusal carries.
|
|
129
|
+
*
|
|
130
|
+
* NON-ZERO, always, and this is the finding that made the whole response
|
|
131
|
+
* contract change. A refusal used to travel as an HTTP status with the exit
|
|
132
|
+
* code in a header that refusals did not set, so a client following the
|
|
133
|
+
* documented rule — write stdout, exit `x-approval-exit-code` — read a missing
|
|
134
|
+
* header as zero, and zero is ALLOW on Claude Code, Cursor, Codex and Muse. A
|
|
135
|
+
* 413 on an oversized hook envelope was therefore an allow. Nothing on this
|
|
136
|
+
* surface may be able to turn a refusal into a permission, so the exit code is
|
|
137
|
+
* in the body, it is always present, and on a refusal it is always this.
|
|
138
|
+
*/
|
|
139
|
+
export declare const REFUSAL_EXIT_CODE = 2;
|
|
140
|
+
/**
|
|
141
|
+
* The verbs the AGENT credential may call. An ALLOWLIST, and the direction is
|
|
142
|
+
* the whole of it.
|
|
143
|
+
*
|
|
144
|
+
* The catalog is the registry's and does not change (`mcp/server.ts` owns it,
|
|
145
|
+
* and this surface publishes exactly what `approval mcp serve` publishes).
|
|
146
|
+
* This is authorization, which is a different question asked one layer later,
|
|
147
|
+
* and it is asked per verb.
|
|
148
|
+
*
|
|
149
|
+
* ## Why a list of what a harness MAY do
|
|
150
|
+
*
|
|
151
|
+
* The threat model this transport exists for is a sandboxed harness on another
|
|
152
|
+
* machine, not an operator at their own laptop. `approval mcp serve` is run BY
|
|
153
|
+
* the operator, on the operator's box, over a pipe the operator handed it; its
|
|
154
|
+
* whole catalog is reachable there because the party at the other end is the
|
|
155
|
+
* party who started it. Here the party at the other end is the party under
|
|
156
|
+
* oversight, and the operative rule is that it never reads the log it is
|
|
157
|
+
* judged by, nor spends the host's credentials, nor runs argv on the host.
|
|
158
|
+
*
|
|
159
|
+
* A denylist would have to name `log tail`, `log export`, `queue`, `coverage`,
|
|
160
|
+
* `doctor`, every `adapter <name>`, `run`, `sandbox`, `token`, `reindex`,
|
|
161
|
+
* `render` and every verb that lands next. This list names what a harness
|
|
162
|
+
* under oversight needs in order to ASK and to ACT ON a grant, so a verb added
|
|
163
|
+
* to the registry tomorrow is TENANT-scoped until somebody decides otherwise.
|
|
164
|
+
* Fail closed, per SPEC.md §11, which is the same reasoning `GUEST_VERBS`
|
|
165
|
+
* gives for being positive.
|
|
166
|
+
*
|
|
167
|
+
* What is on it, and why each:
|
|
168
|
+
*
|
|
169
|
+
* - `instructions`, `hook_classify` — read the guide, and find out what the
|
|
170
|
+
* classifier makes of a command. Neither reads the log or the policy file;
|
|
171
|
+
* both tell an agent what it is about to be judged for before it asks.
|
|
172
|
+
* - `request`, `wait`, `withdraw` — ask, wait for the answer, and retract your
|
|
173
|
+
* own question. This is the gate sequence, and it is the reason the agent
|
|
174
|
+
* credential exists.
|
|
175
|
+
* - `propose`, `start` (APRV-445) — the same sequence for an action the agent
|
|
176
|
+
* declares itself, with its payload as a flag value: register-and-request in
|
|
177
|
+
* a class the operator declared by name and opened with `agent_may_request`,
|
|
178
|
+
* and record the execution the grant or the policy authorized. Neither takes
|
|
179
|
+
* a path, and `start` is requester-only.
|
|
180
|
+
* That is the whole list. It is seven verbs, and the shortness is the point: a
|
|
181
|
+
* harness under oversight asks and is answered, and everything else about the
|
|
182
|
+
* gate belongs to the party the gate is for.
|
|
183
|
+
*
|
|
184
|
+
* ## What came OFF this list in review, and why
|
|
185
|
+
*
|
|
186
|
+
* - `register` takes a task-file POSITIONAL, which is a host path. The hook
|
|
187
|
+
* endpoint is a harness's way to register: it synthesises the envelope from
|
|
188
|
+
* the tool call, so nothing is lost and a host path stops being something a
|
|
189
|
+
* remote caller names.
|
|
190
|
+
* - `log_verify` and `gate_status` hand back the log's length, its head hash
|
|
191
|
+
* and the live state of the gate. "Not a record" is not the same as "not
|
|
192
|
+
* about the log": a head hash moving is a decision being recorded, and an
|
|
193
|
+
* open window is the human's current posture toward this very agent.
|
|
194
|
+
* - `policy_check` and `policy_test` answer what the policy does with a class,
|
|
195
|
+
* and `policy_check` names the absolute store path while doing it.
|
|
196
|
+
* - **Both payload builders**, under the rule that a builder is the agent's
|
|
197
|
+
* only if it builds from the request's own arguments and never reaches a
|
|
198
|
+
* host resource. `payload_hash` takes a FILE, and on this transport that
|
|
199
|
+
* file is on the daemon's machine while the caller is not: every path it
|
|
200
|
+
* could name is one the host put there, so answering would be a hash oracle
|
|
201
|
+
* that also files the bytes into the payload store.
|
|
202
|
+
* `payload_agentmail-draft` takes two ids rather than a path, but it reads
|
|
203
|
+
* the draft over HTTPS with `AGENTMAIL_API_KEY` FROM THIS SERVER'S OWN
|
|
204
|
+
* ENVIRONMENT, so a remote caller would be spending the host's credential on
|
|
205
|
+
* ids it chose — the same fault wearing a network instead of a filesystem.
|
|
206
|
+
* The narrow reading was taken on purpose: publishing a builder that should
|
|
207
|
+
* have been withheld is a capability leak, and withholding one that should
|
|
208
|
+
* have been published is a line of configuration.
|
|
209
|
+
*
|
|
210
|
+
* Everything else is the tenant's, including the ones easiest to wave through:
|
|
211
|
+
* `log_tail` and `log_export` return RECORDS, `queue` returns the tenant's
|
|
212
|
+
* pending decisions, `run` and `sandbox` spawn argv on the daemon's machine,
|
|
213
|
+
* every `adapter_<name>` spends vault credentials, and `status` is the
|
|
214
|
+
* tenant's view of the oversight the agent is under. All of them stay
|
|
215
|
+
* PUBLISHED, so the surface is honest about what exists, and all of them
|
|
216
|
+
* answer the tenant credential only.
|
|
217
|
+
*/
|
|
218
|
+
export declare const AGENT_VERBS: ReadonlySet<string>;
|
|
219
|
+
/**
|
|
220
|
+
* The frozen refusal vocabulary this server authors itself (SPEC.md §11.1
|
|
221
|
+
* invariant 6).
|
|
222
|
+
*
|
|
223
|
+
* A refusal a VERB produced keeps the verb's own code, unchanged, because it
|
|
224
|
+
* is the CLI's refusal arriving over a different transport; so does a refusal
|
|
225
|
+
* `buildArgv` produced, `mcp-` prefix and all, because that function is the
|
|
226
|
+
* one both transports call and renaming its codes here would make one surface
|
|
227
|
+
* lie about the other. What is listed below is only what this file decides.
|
|
228
|
+
*/
|
|
229
|
+
export declare const SERVE_REFUSAL_CODES: readonly ["serve-unauthorized", "serve-agent-forbidden", "serve-tenant-forbidden", "serve-unknown-path", "serve-malformed-url", "serve-unknown-verb", "serve-unknown-harness", "serve-method-not-allowed", "serve-body-too-large", "serve-body-unreadable", "serve-invalid-cursor", "serve-path-pinned", "serve-path-outside-store", "serve-positional-flag", "serve-flag-not-permitted", "serve-export-symlink", "serve-export-hardlink", "serve-export-failed", "serve-hook-failed", "serve-hook-saturated"];
|
|
230
|
+
export type ServeRefusalCode = (typeof SERVE_REFUSAL_CODES)[number];
|
|
231
|
+
export interface ServeOptions {
|
|
232
|
+
/** `agent:<id>`, already validated. Every verb call is recorded under it. */
|
|
233
|
+
actor: string;
|
|
234
|
+
/** The store root: the working directory every relative path resolves against. */
|
|
235
|
+
cwd: string;
|
|
236
|
+
/** `--log`, when the operator pinned one. */
|
|
237
|
+
log?: string;
|
|
238
|
+
/** `--policy`, when the operator pinned one. */
|
|
239
|
+
policy?: string;
|
|
240
|
+
/** The two bearer credentials, already resolved from the environment. */
|
|
241
|
+
credentials: ServeCredentials;
|
|
242
|
+
/**
|
|
243
|
+
* The launch environment the daemon id is resolved from (APRV-448):
|
|
244
|
+
* `APPROVAL_DAEMON_ID` when it holds one, otherwise the id derived from the
|
|
245
|
+
* store's instance. Defaults to `process.env`. Nothing else in this server
|
|
246
|
+
* reads it; the verbs read the environment the process was launched with, as
|
|
247
|
+
* before. An unusable declared id makes {@link serveApproval} reject before
|
|
248
|
+
* it binds, which the CLI has already refused in its own words.
|
|
249
|
+
*/
|
|
250
|
+
env?: NodeJS.ProcessEnv;
|
|
251
|
+
/** Interface to bind. The CLI owns the widening decision; this defaults to loopback. */
|
|
252
|
+
host?: string;
|
|
253
|
+
/** TCP port. `0` asks the kernel for an ephemeral one, which is what tests use. */
|
|
254
|
+
port: number;
|
|
255
|
+
/**
|
|
256
|
+
* A unix-domain socket to listen on INSTEAD of `host`/`port` (APRV-445). An
|
|
257
|
+
* absolute path whose directory the serving uid owns; see
|
|
258
|
+
* {@link checkUnixSocketTarget} and {@link UNIX_SOCKET_MODE}.
|
|
259
|
+
*/
|
|
260
|
+
socketPath?: string;
|
|
261
|
+
/** `--timeout` pinned on every hook call, when the operator chose one. */
|
|
262
|
+
hookTimeout?: string;
|
|
263
|
+
/**
|
|
264
|
+
* `--harness-cap` pinned on every hook call, when the operator stated one
|
|
265
|
+
* (APRV-423).
|
|
266
|
+
*
|
|
267
|
+
* Independent of {@link ServeOptions.hookTimeout} and composed with it: that
|
|
268
|
+
* one bounds how long a call WAITS, this one states the ceiling the harness
|
|
269
|
+
* on the far side of the call imposes on its own hook process. The request
|
|
270
|
+
* this server opens is then judged by the shorter of the policy's TTL and
|
|
271
|
+
* that ceiling minus a margin, so the expiry is recorded while the tenant's
|
|
272
|
+
* harness is still listening.
|
|
273
|
+
*/
|
|
274
|
+
hookHarnessCap?: string;
|
|
275
|
+
/**
|
|
276
|
+
* Hook calls that may run at once, each on its own thread (APRV-427 review).
|
|
277
|
+
* Defaults to {@link DEFAULT_HOOK_THREADS}. See `serve/hook-thread.ts`.
|
|
278
|
+
*/
|
|
279
|
+
hookThreads?: number;
|
|
280
|
+
/**
|
|
281
|
+
* Hook calls that may wait for a thread before one is refused
|
|
282
|
+
* `serve-hook-saturated`. Defaults to {@link DEFAULT_HOOK_QUEUE}.
|
|
283
|
+
*/
|
|
284
|
+
hookQueue?: number;
|
|
285
|
+
/** Request lines. The CLI passes stderr; stdout is never written to. */
|
|
286
|
+
notice?: (text: string) => void;
|
|
287
|
+
}
|
|
288
|
+
export interface ServeHandle {
|
|
289
|
+
readonly host: string;
|
|
290
|
+
readonly port: number;
|
|
291
|
+
/** The unix socket this listener is bound to, or `null` for a TCP bind. */
|
|
292
|
+
readonly socketPath: string | null;
|
|
293
|
+
/** The id every record this process appends carries (APRV-448). */
|
|
294
|
+
readonly daemonId: string;
|
|
295
|
+
/** How many requests this listener has answered. Diagnostics and tests. */
|
|
296
|
+
requests(): number;
|
|
297
|
+
/** The hook thread pool's state now. Diagnostics and tests. */
|
|
298
|
+
hookThreads(): HookThreadStats;
|
|
299
|
+
close(): Promise<void>;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* One captured stream, bounded and clipped at a CODEPOINT boundary.
|
|
303
|
+
*
|
|
304
|
+
* A naive byte slice can cut a multi-byte sequence in half and produce a
|
|
305
|
+
* string that is not valid text, which for a JSON body means either a
|
|
306
|
+
* replacement character where a verdict had a word or, worse, a body a strict
|
|
307
|
+
* client refuses to parse. `StringDecoder` emits only the complete codepoints
|
|
308
|
+
* in what it was given and keeps the incomplete tail to itself, which is
|
|
309
|
+
* exactly the clip wanted here.
|
|
310
|
+
*/
|
|
311
|
+
export declare function clip(text: string): {
|
|
312
|
+
text: string;
|
|
313
|
+
truncated: boolean;
|
|
314
|
+
};
|
|
315
|
+
export interface CatalogEntry {
|
|
316
|
+
name: string;
|
|
317
|
+
title: string;
|
|
318
|
+
description: string;
|
|
319
|
+
inputSchema: unknown;
|
|
320
|
+
/** Which credential opens this verb. Annotation only; the check is below. */
|
|
321
|
+
scope: ServeScope;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The published surface: `approval mcp serve`'s tool list, with each entry's
|
|
325
|
+
* scope stated.
|
|
326
|
+
*
|
|
327
|
+
* Derived, not restated. A verb added to the registry tomorrow appears here
|
|
328
|
+
* the same day it appears as an MCP tool, with `--as` already deleted from its
|
|
329
|
+
* schema by the same function, and `grant` is absent for the same reason it is
|
|
330
|
+
* absent there — the registry marks it `human_only` and no transport publishes
|
|
331
|
+
* a human's authority. Its SCOPE is tenant until somebody puts it on
|
|
332
|
+
* {@link AGENT_VERBS}, which is the fail-closed half of the same derivation.
|
|
333
|
+
*/
|
|
334
|
+
export declare function serveCatalog(): CatalogEntry[];
|
|
335
|
+
/**
|
|
336
|
+
* The flags every hook call is made with.
|
|
337
|
+
*
|
|
338
|
+
* Exported so a test can hand the SAME argv to `commandHook` directly and
|
|
339
|
+
* compare the bytes. That comparison is the acceptance criterion, and it is
|
|
340
|
+
* only honest if both sides are the same invocation of the same verb: a test
|
|
341
|
+
* that built its own flag list would be comparing two spellings.
|
|
342
|
+
*
|
|
343
|
+
* `--as` is appended LAST, as everywhere else on this surface, so the identity
|
|
344
|
+
* the operator fixed when they started the process wins over anything that
|
|
345
|
+
* arrived by another route (`parseFlags` keeps the last occurrence).
|
|
346
|
+
*/
|
|
347
|
+
export declare function hookArgv(options: Pick<ServeOptions, "actor" | "cwd" | "log" | "policy" | "hookTimeout" | "hookHarnessCap">): string[];
|
|
348
|
+
type Route = {
|
|
349
|
+
kind: "catalog";
|
|
350
|
+
} | {
|
|
351
|
+
kind: "verb";
|
|
352
|
+
name: string;
|
|
353
|
+
} | {
|
|
354
|
+
kind: "hook";
|
|
355
|
+
harness: string;
|
|
356
|
+
} | {
|
|
357
|
+
kind: "follow";
|
|
358
|
+
} | {
|
|
359
|
+
kind: "export";
|
|
360
|
+
} | {
|
|
361
|
+
kind: "status";
|
|
362
|
+
};
|
|
363
|
+
/** The route a path names, or null. Method is checked by the handler. */
|
|
364
|
+
export declare function routeOf(pathname: string): Route | null;
|
|
365
|
+
/**
|
|
366
|
+
* Which credential opens a route.
|
|
367
|
+
*
|
|
368
|
+
* `null` means either one does, which is true of the catalog alone: it is the
|
|
369
|
+
* published contract, it reads no log and it appends nothing, and both parties
|
|
370
|
+
* have a reason to ask what this server is.
|
|
371
|
+
*/
|
|
372
|
+
export declare function scopeOf(route: Route): ServeScope | null;
|
|
373
|
+
/**
|
|
374
|
+
* Start the listener. Resolves once it is bound and its real port is known;
|
|
375
|
+
* rejects when the bind fails, so the CLI can report it and exit.
|
|
376
|
+
*/
|
|
377
|
+
export declare function serveApproval(options: ServeOptions): Promise<ServeHandle>;
|
|
378
|
+
/**
|
|
379
|
+
* The mode a unix socket is given once bound: everyone the DIRECTORY admits.
|
|
380
|
+
*
|
|
381
|
+
* A hosted tenant runs the agent's sandbox as a different uid from this server,
|
|
382
|
+
* on the same machine, and the socket is how the two meet without a TCP port
|
|
383
|
+
* anybody on the host could dial. Who may connect is therefore decided by the
|
|
384
|
+
* directory the socket sits in, which the operator owns and sets (`0710` with
|
|
385
|
+
* the sandbox's group, an ACL, a bind mount), and the socket itself does not
|
|
386
|
+
* second-guess it.
|
|
387
|
+
*/
|
|
388
|
+
export declare const UNIX_SOCKET_MODE = 438;
|
|
389
|
+
/**
|
|
390
|
+
* Refuse a socket path this process should not bind (APRV-445).
|
|
391
|
+
*
|
|
392
|
+
* Absolute, inside an existing directory OWNED BY THE SERVING UID (the directory
|
|
393
|
+
* is the access control, so a directory somebody else owns is somebody else's
|
|
394
|
+
* access control), and not an existing file of any kind other than a socket: a
|
|
395
|
+
* stale socket is replaced, and anything else at that path is a file this
|
|
396
|
+
* server would destroy by binding over it. Synchronous and side-effect free, so
|
|
397
|
+
* the CLI can refuse before any listener exists.
|
|
398
|
+
*/
|
|
399
|
+
export declare function checkUnixSocketTarget(socketPath: string): {
|
|
400
|
+
ok: true;
|
|
401
|
+
} | {
|
|
402
|
+
ok: false;
|
|
403
|
+
message: string;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* The lock for the store whose log is `logPath` (resolved against `root`).
|
|
407
|
+
*
|
|
408
|
+
* Exported so a test can hold the SAME lock the listener takes and watch what
|
|
409
|
+
* waits on it: that is how the suite shows a hook call's appends are inside
|
|
410
|
+
* the lock and its wait is outside it, rather than inferring it from timing.
|
|
411
|
+
*/
|
|
412
|
+
export declare function storeLock(logPath: string, root: string): StoreLock;
|