approval-md 0.3.0 → 0.4.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 +83 -914
- package/SPEC.md +98 -13
- package/cli.js +66 -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 +19 -0
- package/dist/src/channels/contract.js +105 -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.js +6 -1
- package/dist/src/channels/tagging.js.map +1 -1
- 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 +271 -1
- package/dist/src/channels/telegram.js +403 -45
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +350 -4
- 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/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 +273 -0
- package/dist/src/cli/channel-telegram-webhook.js +709 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +272 -26
- package/dist/src/cli/channel-telegram.js +478 -41
- 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 +17 -1
- package/dist/src/cli/execute.js +341 -20
- 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 +9 -1
- package/dist/src/cli/help.js +246 -38
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +371 -3
- package/dist/src/cli/hook.js +1926 -44
- 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/main.d.ts +0 -12
- package/dist/src/cli/main.js +51 -0
- 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 +21 -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 +337 -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 +55 -3
- package/dist/src/core/audit.js +52 -5
- 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 +760 -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 +306 -5
- package/dist/src/core/gate.js +772 -24
- 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-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 +50 -21
- package/dist/src/core/log.js +197 -19
- 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 +23 -0
- package/dist/src/core/policy-proposal.js +8 -2
- package/dist/src/core/policy-proposal.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 +35 -10
- package/dist/src/core/sender-identity.js +48 -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/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 +158 -0
- package/dist/src/serve/archive.js +354 -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 +766 -0
- package/docs/cli-reference.md +1633 -43
- package/package.json +4 -2
- package/schema/event.schema.json +59 -2
- 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/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +15 -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 +50 -6
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store as an archive (APRV-421): a POSIX ustar tar, gzipped, written here
|
|
3
|
+
* rather than by a library.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a writer and not a dependency
|
|
6
|
+
*
|
|
7
|
+
* The repository rule is minimal dependencies, justified per addition, and the
|
|
8
|
+
* justification for a tar library would have to survive one question: what does
|
|
9
|
+
* it do that ninety lines here do not? A ustar header is a fixed 512-byte
|
|
10
|
+
* record with octal fields and a checksum, `node:zlib` already ships the
|
|
11
|
+
* compression, and the archive this verb produces contains a handful of files
|
|
12
|
+
* whose paths are short and known. A dependency would add a supply-chain
|
|
13
|
+
* surface to a process that hands a tenant their own evidence, in exchange for
|
|
14
|
+
* code this file can state completely.
|
|
15
|
+
*
|
|
16
|
+
* ## The contents are a POSITIVE allowlist
|
|
17
|
+
*
|
|
18
|
+
* {@link EXPORTED_PATHS} names what goes in. Everything else is out by not
|
|
19
|
+
* being named, which is the fail-closed direction SPEC.md §11 asks for
|
|
20
|
+
* everywhere else: `.approval/keys`, `.approval/env`, `.approval/daemon` and
|
|
21
|
+
* any `vault.enc` are excluded because nothing admits them, not because a
|
|
22
|
+
* denylist remembered them. A file added to the store next month is absent
|
|
23
|
+
* from this archive until somebody decides it belongs, and that decision is a
|
|
24
|
+
* diff here.
|
|
25
|
+
*
|
|
26
|
+
* `.approval/payloads/` IS on the list, and it is the entry the reasoning
|
|
27
|
+
* turns on. The log records a `payload_hash` for every action a human was
|
|
28
|
+
* shown, and the bytes those hashes name live in that directory. An archive
|
|
29
|
+
* carrying the hashes without the bytes would hand a tenant a chain of
|
|
30
|
+
* references to evidence they no longer hold: a receipt for an exit rather
|
|
31
|
+
* than an exit. Payload bytes are the tenant's own and are not credential
|
|
32
|
+
* material — the vault, the keys and the environment source map are, and
|
|
33
|
+
* those are the things excluded below.
|
|
34
|
+
*
|
|
35
|
+
* ## What the reader is for
|
|
36
|
+
*
|
|
37
|
+
* {@link readTarEntries} exists so that a test, and a conformance reader after
|
|
38
|
+
* it, can assert what an archive CONTAINS rather than trusting the writer that
|
|
39
|
+
* produced it. A test that checked the writer against itself would pass on the
|
|
40
|
+
* day the writer put the vault in.
|
|
41
|
+
*/
|
|
42
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from "node:fs";
|
|
43
|
+
import { join, posix, relative, sep } from "node:path";
|
|
44
|
+
import { gzipSync } from "node:zlib";
|
|
45
|
+
/** One tar block. The format's unit, and every offset below is inside one. */
|
|
46
|
+
const BLOCK = 512;
|
|
47
|
+
/**
|
|
48
|
+
* What an export carries, relative to the store root, in archive order.
|
|
49
|
+
*
|
|
50
|
+
* A trailing `/` means "this directory, recursively". Everything else is one
|
|
51
|
+
* file, included when it exists and silently absent when it does not: a store
|
|
52
|
+
* whose index has never been built is an ordinary store, not a broken one.
|
|
53
|
+
*/
|
|
54
|
+
export const EXPORTED_PATHS = [
|
|
55
|
+
"APPROVAL.md",
|
|
56
|
+
".approval/log/",
|
|
57
|
+
".approval/payloads/",
|
|
58
|
+
".approval/QUEUE.md",
|
|
59
|
+
".approval/index.sqlite",
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Paths an export MUST NOT carry, checked as well as excluded.
|
|
63
|
+
*
|
|
64
|
+
* The allowlist above is what decides; this list is what a test asserts
|
|
65
|
+
* against, so the property "no credential material left the store" is written
|
|
66
|
+
* down as a property and not merely implied by the absence of an entry.
|
|
67
|
+
*/
|
|
68
|
+
export const EXCLUDED_PREFIXES = [
|
|
69
|
+
".approval/keys",
|
|
70
|
+
".approval/env",
|
|
71
|
+
".approval/daemon",
|
|
72
|
+
];
|
|
73
|
+
/** Any file with this name is credential material, wherever it sits. */
|
|
74
|
+
export const EXCLUDED_BASENAMES = ["vault.enc"];
|
|
75
|
+
function octal(value, width) {
|
|
76
|
+
// `width - 1` digits and a NUL, which is what every reader accepts and what
|
|
77
|
+
// GNU tar itself writes for these fields.
|
|
78
|
+
return value.toString(8).padStart(width - 1, "0").slice(-(width - 1)) + "\0";
|
|
79
|
+
}
|
|
80
|
+
function writeString(block, offset, width, value) {
|
|
81
|
+
block.write(value.slice(0, width), offset, width, "utf8");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Split a path into ustar's `prefix` and `name` fields.
|
|
85
|
+
*
|
|
86
|
+
* Refused rather than truncated when it does not fit: a truncated path in an
|
|
87
|
+
* archive is a file restored to the wrong place, which is worse than an export
|
|
88
|
+
* that says plainly it cannot represent this store.
|
|
89
|
+
*/
|
|
90
|
+
export function splitTarName(path) {
|
|
91
|
+
if (Buffer.byteLength(path) <= 100)
|
|
92
|
+
return { name: path, prefix: "" };
|
|
93
|
+
const cut = path.lastIndexOf("/", path.length - 1);
|
|
94
|
+
for (let index = cut; index > 0; index -= 1) {
|
|
95
|
+
if (path[index] !== "/")
|
|
96
|
+
continue;
|
|
97
|
+
const prefix = path.slice(0, index);
|
|
98
|
+
const name = path.slice(index + 1);
|
|
99
|
+
if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) {
|
|
100
|
+
return { name, prefix };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`${path} cannot be written into a ustar archive: the name exceeds the format's 100/155 byte fields`);
|
|
104
|
+
}
|
|
105
|
+
/** One 512-byte ustar header, checksum included. */
|
|
106
|
+
function header(entry) {
|
|
107
|
+
const block = Buffer.alloc(BLOCK, 0);
|
|
108
|
+
const { name, prefix } = splitTarName(entry.path);
|
|
109
|
+
writeString(block, 0, 100, name);
|
|
110
|
+
writeString(block, 100, 8, octal(0o644, 8));
|
|
111
|
+
writeString(block, 108, 8, octal(0, 8));
|
|
112
|
+
writeString(block, 116, 8, octal(0, 8));
|
|
113
|
+
writeString(block, 124, 12, octal(entry.data.length, 12));
|
|
114
|
+
writeString(block, 136, 12, octal(Math.floor(entry.mtime), 12));
|
|
115
|
+
// The checksum field is computed as though it held eight spaces.
|
|
116
|
+
block.write(" ", 148, 8, "utf8");
|
|
117
|
+
block.write("0", 156, 1, "utf8"); // typeflag: a regular file
|
|
118
|
+
block.write("ustar\0", 257, 6, "utf8");
|
|
119
|
+
block.write("00", 263, 2, "utf8");
|
|
120
|
+
writeString(block, 345, 155, prefix);
|
|
121
|
+
let sum = 0;
|
|
122
|
+
for (const byte of block)
|
|
123
|
+
sum += byte;
|
|
124
|
+
// Six octal digits, a NUL and a space: the spelling every reader accepts.
|
|
125
|
+
block.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "utf8");
|
|
126
|
+
return block;
|
|
127
|
+
}
|
|
128
|
+
function padding(size) {
|
|
129
|
+
const remainder = size % BLOCK;
|
|
130
|
+
return remainder === 0 ? Buffer.alloc(0) : Buffer.alloc(BLOCK - remainder, 0);
|
|
131
|
+
}
|
|
132
|
+
/** Write `entries` as an uncompressed ustar archive. */
|
|
133
|
+
export function writeTar(entries) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
parts.push(header(entry), entry.data, padding(entry.data.length));
|
|
137
|
+
}
|
|
138
|
+
// Two zero blocks end the archive; a reader that stops at the first one is
|
|
139
|
+
// still correct, and one that requires both is satisfied.
|
|
140
|
+
parts.push(Buffer.alloc(BLOCK * 2, 0));
|
|
141
|
+
return Buffer.concat(parts);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read an uncompressed ustar archive back.
|
|
145
|
+
*
|
|
146
|
+
* Deliberately strict about the checksum: the point of this function is to be
|
|
147
|
+
* an independent reader of what {@link writeTar} produced, and a reader that
|
|
148
|
+
* ignored the field the writer computes would not be independent of it.
|
|
149
|
+
*/
|
|
150
|
+
export function readTarEntries(archive) {
|
|
151
|
+
const entries = [];
|
|
152
|
+
let offset = 0;
|
|
153
|
+
while (offset + BLOCK <= archive.length) {
|
|
154
|
+
const block = archive.subarray(offset, offset + BLOCK);
|
|
155
|
+
if (block.every((byte) => byte === 0))
|
|
156
|
+
break;
|
|
157
|
+
const declared = Number.parseInt(block.subarray(148, 156).toString("utf8").trim(), 8);
|
|
158
|
+
let sum = 0;
|
|
159
|
+
for (const [index, byte] of block.entries()) {
|
|
160
|
+
sum += index >= 148 && index < 156 ? 0x20 : byte;
|
|
161
|
+
}
|
|
162
|
+
if (sum !== declared) {
|
|
163
|
+
throw new Error(`tar header checksum mismatch at offset ${String(offset)}`);
|
|
164
|
+
}
|
|
165
|
+
const trim = (buffer) => {
|
|
166
|
+
const end = buffer.indexOf(0);
|
|
167
|
+
return buffer.subarray(0, end === -1 ? buffer.length : end).toString("utf8");
|
|
168
|
+
};
|
|
169
|
+
const name = trim(block.subarray(0, 100));
|
|
170
|
+
const prefix = trim(block.subarray(345, 500));
|
|
171
|
+
const size = Number.parseInt(trim(block.subarray(124, 136)) || "0", 8);
|
|
172
|
+
const mtime = Number.parseInt(trim(block.subarray(136, 148)) || "0", 8);
|
|
173
|
+
offset += BLOCK;
|
|
174
|
+
entries.push({
|
|
175
|
+
path: prefix.length === 0 ? name : `${prefix}/${name}`,
|
|
176
|
+
data: Buffer.from(archive.subarray(offset, offset + size)),
|
|
177
|
+
mtime,
|
|
178
|
+
});
|
|
179
|
+
offset += size + padding(size).length;
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Is this store-relative path one an export may never carry?
|
|
185
|
+
*
|
|
186
|
+
* `excludedLock` is the ONE lockfile this server itself creates, passed in as
|
|
187
|
+
* an exact path rather than matched as a class. An earlier pass excluded
|
|
188
|
+
* `*.lock` by suffix, which quietly deleted a tenant's own
|
|
189
|
+
* `.approval/payloads/x.lock` from their archive: the tenant's bytes are the
|
|
190
|
+
* tenant's, and a name this runtime happens to use for its own bookkeeping is
|
|
191
|
+
* not a reason to drop somebody else's file. Only the path derived from the
|
|
192
|
+
* log is dropped, and only because the export holds that lock while it walks,
|
|
193
|
+
* so the file exists for exactly the span of the copy and means nothing but
|
|
194
|
+
* "somebody was reading when this was made".
|
|
195
|
+
*/
|
|
196
|
+
export function isExcludedPath(path, excludedLock = null) {
|
|
197
|
+
const base = path.split("/").at(-1) ?? path;
|
|
198
|
+
if (EXCLUDED_BASENAMES.includes(base))
|
|
199
|
+
return true;
|
|
200
|
+
if (excludedLock !== null && path === excludedLock)
|
|
201
|
+
return true;
|
|
202
|
+
return EXCLUDED_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A symlink the export walk reached.
|
|
206
|
+
*
|
|
207
|
+
* Thrown rather than returned because it must abort the WHOLE archive from
|
|
208
|
+
* wherever it is found, and an error is the one control flow that cannot be
|
|
209
|
+
* accidentally ignored by a caller that forgot to check a return value.
|
|
210
|
+
*/
|
|
211
|
+
export class ExportSymlinkError extends Error {
|
|
212
|
+
/** The store-relative path of the link itself, never its target. */
|
|
213
|
+
path;
|
|
214
|
+
constructor(path) {
|
|
215
|
+
super(`${path} is a symbolic link. An export copies the tenant's own store and never what a link points at: a link under the store can name credential material inside it (\`.approval/keys\`, \`.approval/env\`, the vault) or any file on the host, and a copy that followed one would put those bytes in the tenant's archive. The whole export is refused rather than the link skipped, because an archive silently missing a file is an archive nobody can tell from a complete one. Replace the link with the file, or move it outside the store.`);
|
|
216
|
+
this.name = "ExportSymlinkError";
|
|
217
|
+
this.path = path;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A HARD link, which a name-based exclusion cannot see at all (APRV-421,
|
|
222
|
+
* second review pass).
|
|
223
|
+
*
|
|
224
|
+
* `ln .approval/keys/sender.key .approval/log/note.jsonl` produces a second
|
|
225
|
+
* name for the same inode. There is no link to refuse to follow and no target
|
|
226
|
+
* to notice: the excluded path and the admitted path ARE one file, and the
|
|
227
|
+
* admitted name is a perfectly ordinary regular file by every test a walker
|
|
228
|
+
* can apply to it except one — its link count.
|
|
229
|
+
*
|
|
230
|
+
* So `nlink > 1` on a regular file under the store is refused, fail-closed and
|
|
231
|
+
* whole, exactly as a symlink is. The false positive is a tenant who
|
|
232
|
+
* deliberately hard-linked something into their own store, which is rare, is
|
|
233
|
+
* visible in the refusal, and is repaired by copying the file.
|
|
234
|
+
*/
|
|
235
|
+
export class ExportHardLinkError extends Error {
|
|
236
|
+
/** The store-relative path of the link, which is also the file. */
|
|
237
|
+
path;
|
|
238
|
+
/** How many names this inode has. */
|
|
239
|
+
links;
|
|
240
|
+
constructor(path, links) {
|
|
241
|
+
super(`${path} has ${String(links)} names (it is a hard link). A second name for one inode cannot be told from an ordinary file by its path, so an excluded file — a key under \`.approval/keys\`, the environment source map, the vault — can be given an admitted name and copied into the archive with nothing to notice. The whole export is refused rather than the file skipped, for the reason a symbolic link refuses it: an archive silently missing a file is one nobody can tell from a complete one. Replace the link with a copy.`);
|
|
242
|
+
this.name = "ExportHardLinkError";
|
|
243
|
+
this.path = path;
|
|
244
|
+
this.links = links;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function walk(root, relativeDir, into, lock) {
|
|
248
|
+
let names;
|
|
249
|
+
try {
|
|
250
|
+
names = readdirSync(join(root, relativeDir)).sort();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const name of names) {
|
|
256
|
+
const relativePath = posix.join(relativeDir, name);
|
|
257
|
+
collect(root, relativePath, into, lock);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function collect(root, relativePath, into, lock) {
|
|
261
|
+
// Checked on every path rather than only on the roots: the allowlist decides
|
|
262
|
+
// what is offered and this decides what is accepted, and a nested `vault.enc`
|
|
263
|
+
// is refused by the second even when the first admitted the directory above
|
|
264
|
+
// it.
|
|
265
|
+
if (isExcludedPath(relativePath, lock))
|
|
266
|
+
return;
|
|
267
|
+
const absolute = join(root, relativePath);
|
|
268
|
+
let stats;
|
|
269
|
+
try {
|
|
270
|
+
// LSTAT, never stat (APRV-421). `stat` follows the link, so the exclusion
|
|
271
|
+
// check above would be reading one name while the copy below read another
|
|
272
|
+
// file entirely: `ln -s .approval/keys/sender.key .approval/log/note.jsonl`
|
|
273
|
+
// passes every name-based check ever written and hands over the key.
|
|
274
|
+
stats = lstatSync(absolute);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (stats.isSymbolicLink())
|
|
280
|
+
throw new ExportSymlinkError(relativePath);
|
|
281
|
+
if (stats.isDirectory()) {
|
|
282
|
+
walk(root, relativePath, into, lock);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Not a regular file: a fifo, a socket or a device. Skipped rather than
|
|
286
|
+
// refused, because none of them is a way to name another file's bytes, and
|
|
287
|
+
// a store that happens to hold one is not a store that is lying about what
|
|
288
|
+
// it contains.
|
|
289
|
+
if (!stats.isFile())
|
|
290
|
+
return;
|
|
291
|
+
// OPEN, then ask the DESCRIPTOR what it is (APRV-421, second review pass).
|
|
292
|
+
//
|
|
293
|
+
// `lstat` above and a `readFileSync(path)` below would be two resolutions of
|
|
294
|
+
// one name with a window between them, and the classic move is to replace a
|
|
295
|
+
// checked regular file with a symlink to somebody's key inside that window.
|
|
296
|
+
// `O_NOFOLLOW` refuses to open a symlink at all, and `fstat` describes THE
|
|
297
|
+
// FILE THAT WAS OPENED rather than whatever the name points at now — so the
|
|
298
|
+
// regular-file and link-count checks below are about the bytes actually read.
|
|
299
|
+
const fd = openSync(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
300
|
+
try {
|
|
301
|
+
const opened = fstatSync(fd);
|
|
302
|
+
if (!opened.isFile())
|
|
303
|
+
return;
|
|
304
|
+
if (opened.nlink > 1)
|
|
305
|
+
throw new ExportHardLinkError(relativePath, opened.nlink);
|
|
306
|
+
into.push({
|
|
307
|
+
path: relativePath,
|
|
308
|
+
data: readFileSync(fd),
|
|
309
|
+
mtime: Math.floor(opened.mtimeMs / 1000),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
try {
|
|
314
|
+
closeSync(fd);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// `readFileSync(fd)` closes it on some paths; a double close is not an
|
|
318
|
+
// error worth propagating out of an export.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Build the export archive for the store rooted at `root`.
|
|
324
|
+
*
|
|
325
|
+
* Reads only; writes nothing anywhere, appends no record, and touches no
|
|
326
|
+
* credential. A path outside the allowlist cannot be reached from here, because
|
|
327
|
+
* the walk starts at the allowlist and nowhere else, and no path is followed
|
|
328
|
+
* through a link.
|
|
329
|
+
*
|
|
330
|
+
* Throws {@link ExportSymlinkError} when the walk meets a symbolic link. The
|
|
331
|
+
* caller turns that into a refusal naming the link's own relative path.
|
|
332
|
+
*/
|
|
333
|
+
export function buildStoreArchive(root, logPath) {
|
|
334
|
+
// The one file this server's own append lock creates, as an exact path.
|
|
335
|
+
const lock = storeRelative(root, `${logPath}.lock`);
|
|
336
|
+
const entries = [];
|
|
337
|
+
for (const candidate of EXPORTED_PATHS) {
|
|
338
|
+
const relativePath = candidate.endsWith("/") ? candidate.slice(0, -1) : candidate;
|
|
339
|
+
collect(root, relativePath, entries, lock);
|
|
340
|
+
}
|
|
341
|
+
// Sorted, so two exports of one unchanged store are the same archive: a
|
|
342
|
+
// tenant comparing two downloads is comparing the store and not the order a
|
|
343
|
+
// directory happened to be read in.
|
|
344
|
+
entries.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
345
|
+
return {
|
|
346
|
+
bytes: gzipSync(writeTar(entries), { level: 6 }),
|
|
347
|
+
paths: entries.map((entry) => entry.path),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** `relative()` in POSIX spelling, for callers that hold absolute paths. */
|
|
351
|
+
export function storeRelative(root, path) {
|
|
352
|
+
return relative(root, path).split(sep).join("/");
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../../../src/serve/archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a caller on this transport may say: nothing about the filesystem
|
|
3
|
+
* outside the store, and nothing shaped like a flag where a value belongs
|
|
4
|
+
* (APRV-421, review findings 3 and 1).
|
|
5
|
+
*
|
|
6
|
+
* ## The two holes this closes
|
|
7
|
+
*
|
|
8
|
+
* **A published path flag is a filesystem oracle.** `approval mcp serve`
|
|
9
|
+
* publishes `--log`, `--dir` and `--policy` on the verbs that accept them and
|
|
10
|
+
* injects the operator's pins only where the operator passed any. On a pipe
|
|
11
|
+
* the operator handed the server that is harmless: the party at the other end
|
|
12
|
+
* is the party who started it. Over HTTP the party at the other end is a
|
|
13
|
+
* sandboxed harness on another machine, and the same flag answers questions
|
|
14
|
+
* about files it cannot see.
|
|
15
|
+
*
|
|
16
|
+
* **A positional is a flag if it starts with a dash.** `buildArgv` emits
|
|
17
|
+
* positionals FIRST and verbatim, and `parseFlags` reads any token beginning
|
|
18
|
+
* with `-` as a flag. So a caller refused `{"flags":{"--payload":"…"}}` could
|
|
19
|
+
* write `{"positionals":["--payload","/tmp/secret","t1"]}` and reach the same
|
|
20
|
+
* flag through the front door — including the `--payload=/tmp/secret`
|
|
21
|
+
* spelling, which `parseFlags` splits on `=`. Positionals are VALUES. One that
|
|
22
|
+
* looks like a flag is refused rather than escaped, because escaping is a
|
|
23
|
+
* thing to get subtly wrong once and lose.
|
|
24
|
+
*
|
|
25
|
+
* `trailing` needs no such rule and deliberately does not get one. `buildArgv`
|
|
26
|
+
* always emits `--` before it, and `parseFlags` STOPS at `--`, pushing the
|
|
27
|
+
* remainder into positionals without reading a single token as a flag. A
|
|
28
|
+
* blanket refusal of dash-leading trailing elements would instead break the
|
|
29
|
+
* one agent verb that needs them: `hook classify -- git push -f` classifies a
|
|
30
|
+
* command line, and command lines have flags. The invariant that makes this
|
|
31
|
+
* safe is pinned by a test rather than assumed here.
|
|
32
|
+
*
|
|
33
|
+
* ## The rule, by scope
|
|
34
|
+
*
|
|
35
|
+
* **Both scopes.** No positional may begin with `-`. `--dir`, `--log` and
|
|
36
|
+
* `--policy` are appended to every call from the launch configuration and are
|
|
37
|
+
* refused from a caller even when the value is correct: one process serves one
|
|
38
|
+
* store, and a caller restating it is a caller that believes it can choose.
|
|
39
|
+
*
|
|
40
|
+
* **Agent scope.** Every path-typed flag is refused OUTRIGHT rather than
|
|
41
|
+
* confined: a sandboxed harness has no files on the daemon's machine, so a
|
|
42
|
+
* path it names is either useless or somebody else's. Payload bytes reach the
|
|
43
|
+
* gate for such a harness through the hook route, which builds the envelope
|
|
44
|
+
* from the tool call itself. On top of that, only the flags named in
|
|
45
|
+
* {@link AGENT_FLAGS} are accepted at all, per verb, so a flag added to one of
|
|
46
|
+
* the seven agent verbs tomorrow is refused until somebody decides otherwise.
|
|
47
|
+
*
|
|
48
|
+
* **Tenant scope.** Every path-typed flag is CONFINED to the store, and which
|
|
49
|
+
* flags those are comes from the registry ({@link pathFlagsOf}) rather than
|
|
50
|
+
* from a list kept here, so a flag declared `"path"` tomorrow is confined on
|
|
51
|
+
* the day it appears. Positionals that name files are confined the same way.
|
|
52
|
+
*/
|
|
53
|
+
import { type VerbSpec } from "../cli/verb-registry.js";
|
|
54
|
+
import type { ServeScope } from "./credentials.js";
|
|
55
|
+
/**
|
|
56
|
+
* The three the server pins itself, refused from any caller in any scope.
|
|
57
|
+
*
|
|
58
|
+
* They ARE the store. Everything else this module decides is about values
|
|
59
|
+
* inside it.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PINNED_FLAGS: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* What each AGENT verb may be given, flag by flag.
|
|
64
|
+
*
|
|
65
|
+
* A positive list per verb, and short on purpose. The agent surface is seven
|
|
66
|
+
* verbs; between them they need an action key, a decision deadline, a note,
|
|
67
|
+
* a proposal's class, key, summary and inline payload, and `--json`. Everything else on those verbs is either the store (pinned), the
|
|
68
|
+
* identity (`--as`, refused by the argv builder both transports share), a path
|
|
69
|
+
* (refused below), or something nobody has decided a sandboxed harness should
|
|
70
|
+
* have.
|
|
71
|
+
*
|
|
72
|
+
* `--help` is absent deliberately: a help page over a transport whose client
|
|
73
|
+
* is a program is an odd thing to serve, and `instructions` is the verb for
|
|
74
|
+
* that.
|
|
75
|
+
*/
|
|
76
|
+
export declare const AGENT_FLAGS: ReadonlyMap<string, ReadonlySet<string>>;
|
|
77
|
+
export type ArgumentCheck = {
|
|
78
|
+
ok: true;
|
|
79
|
+
} | {
|
|
80
|
+
ok: false;
|
|
81
|
+
code: string;
|
|
82
|
+
message: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Check one verb call's arguments before an argv is built from them.
|
|
86
|
+
*
|
|
87
|
+
* Runs for EVERY scope. The tenant credential is the operator's and is trusted
|
|
88
|
+
* with far more than the agent's, and that is still not a reason to let one
|
|
89
|
+
* process serve two stores: the pins are the launch configuration's, and a
|
|
90
|
+
* second store reached through an argument would be a second gate with no
|
|
91
|
+
* attestation, no daemon id, and no record of which one answered.
|
|
92
|
+
*/
|
|
93
|
+
export declare function checkVerbArguments(spec: VerbSpec, rawArgs: unknown, storeRoot: string, cwd: string, scope: ServeScope): ArgumentCheck;
|