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,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a daemon process learns its own id, and which ids its policy admits
|
|
3
|
+
* (APRV-383).
|
|
4
|
+
*
|
|
5
|
+
* The other half of `core/daemon-identity.ts`, which holds the grammar, the
|
|
6
|
+
* process state and the one question the write boundary asks. This half does the
|
|
7
|
+
* reading: the launch environment, the instance the log belongs to, and the
|
|
8
|
+
* `daemons` list in an attested policy. It is deliberately NOT imported by
|
|
9
|
+
* `core/log.ts` — see that module's header for the import cycle this separation
|
|
10
|
+
* avoids and for why the seam is the right one anyway.
|
|
11
|
+
*
|
|
12
|
+
* ## The id
|
|
13
|
+
*
|
|
14
|
+
* Two sources, in this order:
|
|
15
|
+
*
|
|
16
|
+
* 1. `APPROVAL_DAEMON_ID` in the launch environment. An operator hosting several
|
|
17
|
+
* tenants names their processes, and a name a person chose is the one a person
|
|
18
|
+
* can recognise on a card, in a log and in an allowlist. It is in the
|
|
19
|
+
* environment rather than in a file for the reason every other launch fact is
|
|
20
|
+
* (SPEC.md §11.1 invariant 7): the process's operator establishes it, and no
|
|
21
|
+
* verb reads it out of the working tree.
|
|
22
|
+
* 2. Otherwise DERIVED from the instance: `daemon-` plus the eight hex digits
|
|
23
|
+
* `core/instance.ts` already computes from the absolute path of the instance's
|
|
24
|
+
* `.approval` directory, which `approval doctor` prints as its `keychain-scope`
|
|
25
|
+
* suffix and `.approval/env` carries in the open.
|
|
26
|
+
*
|
|
27
|
+
* The derived form is what makes AC1's "stable across restarts on the same
|
|
28
|
+
* machine and keystore" true without storing anything: the input is a directory
|
|
29
|
+
* path, so a restart, a rebuild of the process and a second daemon started in the
|
|
30
|
+
* same checkout all derive one id, while a different checkout on the same machine
|
|
31
|
+
* derives a different one. It is the same identity the keystore items are scoped
|
|
32
|
+
* by, which is the point — an operator reading `daemon-3f2a9c11` in a record and
|
|
33
|
+
* `approval-tg-token-3f2a9c11` in `.approval/env` is looking at one gate.
|
|
34
|
+
*
|
|
35
|
+
* A random id written to a file was rejected for the reason `core/instance.ts`
|
|
36
|
+
* rejects it: a file an operator can copy is an identity that travels, and an id
|
|
37
|
+
* that changed on every restart would make the `daemons` allowlist unwritable.
|
|
38
|
+
*
|
|
39
|
+
* ## The allowlist
|
|
40
|
+
*
|
|
41
|
+
* Read from the ATTESTED policy and from nothing else, which is invariant 1 doing
|
|
42
|
+
* its ordinary work: the bytes a human signed are the only bytes a refusal may be
|
|
43
|
+
* derived from, and a policy file edited since is inoperative. The daemon resolves
|
|
44
|
+
* it against its own verified read and its own attestation check, never against a
|
|
45
|
+
* claim from a caller.
|
|
46
|
+
*/
|
|
47
|
+
import { checkAttestation } from "./attest.js";
|
|
48
|
+
import { DAEMON_ID_ENV, declareDaemonIdentity, isDaemonId, setDaemonAllowlist, } from "./daemon-identity.js";
|
|
49
|
+
import { instanceIdFor } from "./instance.js";
|
|
50
|
+
import { loadPolicy } from "./policy-load.js";
|
|
51
|
+
import { readVerifiedRecords } from "./state.js";
|
|
52
|
+
/** The prefix a derived id wears, so the string says what kind of id it is. */
|
|
53
|
+
export const DERIVED_DAEMON_ID_PREFIX = "daemon-";
|
|
54
|
+
/** The id this instance derives when the launch environment declares none. */
|
|
55
|
+
export function derivedDaemonId(logPath) {
|
|
56
|
+
return `${DERIVED_DAEMON_ID_PREFIX}${instanceIdFor(logPath)}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the id, preferring an explicit declaration and otherwise deriving one.
|
|
60
|
+
*
|
|
61
|
+
* A declared id that fails the grammar is a REFUSAL rather than a fall back to
|
|
62
|
+
* the derived form: the operator meant the value they set, and a runtime that
|
|
63
|
+
* quietly substituted another would write records under a name nobody chose and
|
|
64
|
+
* check them against an allowlist nobody wrote for it. A blank or
|
|
65
|
+
* whitespace-only value is treated as unset, because an empty variable is how a
|
|
66
|
+
* shell spells "I did not set this".
|
|
67
|
+
*/
|
|
68
|
+
export function resolveDaemonId(logPath, env = process.env) {
|
|
69
|
+
const raw = env[DAEMON_ID_ENV];
|
|
70
|
+
if (raw === undefined || raw.trim().length === 0) {
|
|
71
|
+
return { ok: true, id: derivedDaemonId(logPath), source: "derived" };
|
|
72
|
+
}
|
|
73
|
+
if (!isDaemonId(raw)) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
code: "daemon-id-invalid",
|
|
77
|
+
declared: raw,
|
|
78
|
+
message: `${DAEMON_ID_ENV}=${JSON.stringify(raw)} is not a usable daemon id: lowercase letters, digits, \`.\`, \`-\` and \`_\`, starting with a letter or digit, at most 64 characters. Unset it to derive ${derivedDaemonId(logPath)} from this instance`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, id: raw, source: "environment" };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Declare this process's identity into `core/daemon-identity.ts`, and hand back
|
|
85
|
+
* what was resolved.
|
|
86
|
+
*
|
|
87
|
+
* Called by the daemon runtime at construction, beside `markDaemonProcess()`. An
|
|
88
|
+
* UNUSABLE declared id is still declared — as an identity with no id — because a
|
|
89
|
+
* daemon that cannot name itself must be refused at the write boundary rather
|
|
90
|
+
* than left unmarked and writing: the CLI verbs refuse to start on the same
|
|
91
|
+
* resolution, and this is what closes the gap for a `Daemon` constructed
|
|
92
|
+
* directly.
|
|
93
|
+
*/
|
|
94
|
+
export function declareDaemonIdentityFor(logPath, env = process.env) {
|
|
95
|
+
const resolved = resolveDaemonId(logPath, env);
|
|
96
|
+
declareResolvedDaemonIdentity(resolved);
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Declare an identity that was already resolved by {@link resolveDaemonId}
|
|
101
|
+
* (APRV-448).
|
|
102
|
+
*
|
|
103
|
+
* The same declaration {@link declareDaemonIdentityFor} makes, for a process
|
|
104
|
+
* that resolved its id once at startup and must declare it again somewhere the
|
|
105
|
+
* environment and the log path are not at hand: `approval serve`'s hook worker
|
|
106
|
+
* threads, which hold their own copy of this module's state, and the Telegram
|
|
107
|
+
* webhook listener, whose preparation resolved the id before anything ran. One
|
|
108
|
+
* resolution, declared in every thread that appends, is what makes the id on a
|
|
109
|
+
* hook's records the id on a verb's.
|
|
110
|
+
*
|
|
111
|
+
* An unusable declared id is declared as an identity with no id, for the reason
|
|
112
|
+
* {@link declareDaemonIdentityFor} gives: refused at the write boundary rather
|
|
113
|
+
* than left unmarked and writing.
|
|
114
|
+
*/
|
|
115
|
+
export function declareResolvedDaemonIdentity(resolved) {
|
|
116
|
+
if (resolved.ok) {
|
|
117
|
+
declareDaemonIdentity({ id: resolved.id, source: resolved.source });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
declareDaemonIdentity({ id: null, declared: resolved.declared, source: null });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The `daemons` list a LOADED policy declares, or `null` for no restriction.
|
|
125
|
+
*
|
|
126
|
+
* `null` for a policy that does not load at all, which looks like a widening and
|
|
127
|
+
* is not: this function answers "what does this document say", and every caller
|
|
128
|
+
* that could refuse on the answer requires an ATTESTED policy first
|
|
129
|
+
* ({@link resolveDaemonAllowlist}). A policy that fails to load restricts
|
|
130
|
+
* nothing here and makes every class `manual` everywhere else, which is where the
|
|
131
|
+
* fail-closed behaviour of an unreadable policy lives.
|
|
132
|
+
*/
|
|
133
|
+
export function daemonAllowlistOf(load) {
|
|
134
|
+
if (!load.ok)
|
|
135
|
+
return null;
|
|
136
|
+
const declared = load.policy.daemons;
|
|
137
|
+
return declared === undefined ? null : declared;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The allowlist in force, from the attested policy and the verified log.
|
|
141
|
+
*
|
|
142
|
+
* `records` are the caller's own VERIFIED records (invariant 1) and `load` its
|
|
143
|
+
* own policy load; both are passed in rather than read here so that the daemon's
|
|
144
|
+
* tick does one verified read and one policy load, and so that this function is
|
|
145
|
+
* pure and exhaustively testable.
|
|
146
|
+
*
|
|
147
|
+
* A failure is reported and is NOT a restriction of its own: the caller leaves
|
|
148
|
+
* whatever was last resolved in force (`core/daemon-identity.ts`'s
|
|
149
|
+
* `setDaemonAllowlist` is simply not called), so a policy that becomes unreadable
|
|
150
|
+
* cannot be a way out of the list it carried a moment ago, and a deployment whose
|
|
151
|
+
* policy was never attested is unrestricted exactly as it was before this key
|
|
152
|
+
* existed.
|
|
153
|
+
*/
|
|
154
|
+
export function resolveDaemonAllowlist(records, load) {
|
|
155
|
+
if (!load.ok) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
reason: "policy-unloadable",
|
|
159
|
+
detail: `the policy could not be loaded (${load.code}), so no \`daemons\` list could be read from it`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const attestation = checkAttestation([...records], load.source.path);
|
|
163
|
+
if (attestation.status !== "attested") {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: "policy-not-attested",
|
|
167
|
+
detail: `the policy at ${load.source.path} is not attested (${attestation.status}), so its \`daemons\` list is not a list a human signed`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, allowed: daemonAllowlistOf(load) };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the allowlist and put it in force, or leave the previous one standing.
|
|
174
|
+
*
|
|
175
|
+
* The one call the daemon's tick makes. It returns the resolution so the caller
|
|
176
|
+
* can say what happened, and it never widens: on a failure nothing is set.
|
|
177
|
+
*/
|
|
178
|
+
export function refreshDaemonAllowlist(records, load) {
|
|
179
|
+
const resolution = resolveDaemonAllowlist(records, load);
|
|
180
|
+
if (resolution.ok)
|
|
181
|
+
setDaemonAllowlist(resolution.allowed);
|
|
182
|
+
return resolution;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The stricter of two allowlists, which never admits an id either of them
|
|
186
|
+
* refuses (APRV-448 review): `null` (no restriction) yields to a list, and two
|
|
187
|
+
* lists intersect.
|
|
188
|
+
*
|
|
189
|
+
* A thread that has never made a successful resolution of its own (a hook
|
|
190
|
+
* worker spawned after the policy lost its attestation) starts from the
|
|
191
|
+
* restriction its listener holds, combined this way with whatever it held
|
|
192
|
+
* before, so a fresh thread is never a way out of the list in force.
|
|
193
|
+
*/
|
|
194
|
+
export function narrowerAllowlist(a, b) {
|
|
195
|
+
if (a === null)
|
|
196
|
+
return b === null ? null : [...b];
|
|
197
|
+
if (b === null)
|
|
198
|
+
return [...a];
|
|
199
|
+
return a.filter((id) => b.includes(id));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* {@link refreshDaemonAllowlist} for a process that has no verified read of its
|
|
203
|
+
* own in hand (APRV-448): read the store's log through the verified path, load
|
|
204
|
+
* the policy from `where`, and refresh.
|
|
205
|
+
*
|
|
206
|
+
* `approval serve` calls it before every verb call and every hook call, inside
|
|
207
|
+
* the store lock, and the Telegram webhook listener calls it at startup and on
|
|
208
|
+
* every dispatch cycle, which is that process's tick. The read is the cached
|
|
209
|
+
* verified read every other caller in the process shares, so it costs a walk of
|
|
210
|
+
* what was appended since, never a second walk of the chain.
|
|
211
|
+
*
|
|
212
|
+
* A log that does not verify resolves nothing and sets nothing, exactly as an
|
|
213
|
+
* unattested policy does: the restriction last resolved stays in force, and the
|
|
214
|
+
* append that follows meets the same unverifiable log on its own path and is
|
|
215
|
+
* refused there in its own words.
|
|
216
|
+
*/
|
|
217
|
+
export function refreshDaemonAllowlistFrom(logPath, where) {
|
|
218
|
+
const read = readVerifiedRecords(logPath);
|
|
219
|
+
if (!read.ok) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
reason: "log-unverified",
|
|
223
|
+
detail: `the log at ${logPath} did not verify (${read.code}), so no attestation could be checked against it`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return refreshDaemonAllowlist(read.records, loadPolicy(where));
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=daemon-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-host.js","sourceRoot":"","sources":["../../../src/core/daemon-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH daemon wrote this record, and the check the write boundary runs on it
|
|
3
|
+
* (APRV-383).
|
|
4
|
+
*
|
|
5
|
+
* ## The fact this module is about
|
|
6
|
+
*
|
|
7
|
+
* `core/daemon-actor.ts` answers "is this process the daemon". Every record the
|
|
8
|
+
* daemon appends carried that and nothing more: a generic `system:daemon` actor,
|
|
9
|
+
* one string, identical on every machine that ever ran one. That is enough while
|
|
10
|
+
* a gate's daemon is the operator's own process in the operator's own checkout,
|
|
11
|
+
* and it stops being enough the moment one party HOSTS a daemon for another.
|
|
12
|
+
* Three properties of that arrangement already hold: authority comes from the
|
|
13
|
+
* tenant's attested policy and never from the host (§5.2, APRV-324 and 356),
|
|
14
|
+
* credentials come from the launch environment so one process holds one tenant's
|
|
15
|
+
* token, and a decision arrives only through the tenant's own sender mapping so
|
|
16
|
+
* the host cannot mint one. What was missing is the smallest of the four and the
|
|
17
|
+
* one a tenant reads first: opening their own log and seeing which daemon acted
|
|
18
|
+
* on their behalf.
|
|
19
|
+
*
|
|
20
|
+
* So a daemon-written record carries a `daemon` field naming the instance that
|
|
21
|
+
* wrote it, and an attested policy MAY list the ids permitted to write
|
|
22
|
+
* (`daemons`, read by `core/daemon-host.ts`). An id the list does not name is
|
|
23
|
+
* refused at the write boundary.
|
|
24
|
+
*
|
|
25
|
+
* ## What the id is NOT, and this is the important half
|
|
26
|
+
*
|
|
27
|
+
* It is a SELF-REPORTED field, so SPEC.md §11.1 invariant 4 governs it
|
|
28
|
+
* completely: it may raise scrutiny and may never lower it. Concretely, in this
|
|
29
|
+
* runtime:
|
|
30
|
+
*
|
|
31
|
+
* - a listed id gains NOTHING. No verdict, no autonomy, no budget, no floor, no
|
|
32
|
+
* sampling draw, no token and no TTL reads this field or the list. Being named
|
|
33
|
+
* in `daemons` buys a daemon exactly the ability it had before the key existed;
|
|
34
|
+
* - an unlisted id is REFUSED, which is the only direction the field moves
|
|
35
|
+
* anything, and it moves it strictly towards writing less;
|
|
36
|
+
* - the id never enters a payload hash and never enters a token. It is a
|
|
37
|
+
* top-level record field, so the record's own chain hash covers it the way it
|
|
38
|
+
* covers `actor` (a record's provenance has to be part of what the chain
|
|
39
|
+
* protects, or it could be edited without breaking the chain), and no bound
|
|
40
|
+
* payload, no `payload_hash` and no `token_sha256` is computed over it.
|
|
41
|
+
*
|
|
42
|
+
* And it is ATTRIBUTION rather than authorization. A hostile host process holds
|
|
43
|
+
* the tenant's log handle and can call anything in this module the daemon calls,
|
|
44
|
+
* exactly as it can call {@link markDaemonProcess}; `core/daemon-actor.ts` states
|
|
45
|
+
* that boundary and this module inherits it word for word. What the allowlist
|
|
46
|
+
* catches is the likelier failure and the one nothing caught before: a daemon
|
|
47
|
+
* started against the wrong tenant's log, a second daemon nobody meant to leave
|
|
48
|
+
* running, a container rebuilt with a new id, each of which used to write records
|
|
49
|
+
* indistinguishable from the intended daemon's.
|
|
50
|
+
*
|
|
51
|
+
* ## Why the state is module state and not an argument
|
|
52
|
+
*
|
|
53
|
+
* For the reason `core/daemon-actor.ts` gives for taking no arguments: a field a
|
|
54
|
+
* caller passes is a field a caller chooses, and an `appendEvent` option naming
|
|
55
|
+
* the writing daemon would let any process in this runtime claim to be one. The
|
|
56
|
+
* daemon runtime declares its own identity once, at construction, through
|
|
57
|
+
* `core/daemon-host.ts`, and `core/log.ts` READS it. `EventInput` deliberately
|
|
58
|
+
* has no `daemon` member, and `buildRecord` composes a record field by field, so
|
|
59
|
+
* a caller that puts the property on its input object anyway is ignored rather
|
|
60
|
+
* than obeyed.
|
|
61
|
+
*
|
|
62
|
+
* ## Which processes declare, and why the stamp does not ask for the mark
|
|
63
|
+
*
|
|
64
|
+
* Three processes append on a gate's behalf without being a session, and since
|
|
65
|
+
* APRV-448 all three declare the same identity through the same two calls in
|
|
66
|
+
* `core/daemon-host.ts` (`declareDaemonIdentityFor` or
|
|
67
|
+
* `declareResolvedDaemonIdentity`, then `refreshDaemonAllowlist*`): the daemon
|
|
68
|
+
* loop (`daemon/daemon.ts`, at construction), `approval serve` (its verbs on the
|
|
69
|
+
* listener's thread and its hook calls on their worker threads), and `approval
|
|
70
|
+
* channel telegram webhook`. In a co-located deployment, where a facade, a
|
|
71
|
+
* daemon and a channel run in one process per tenant, a tenant reading their
|
|
72
|
+
* own log therefore sees one id on the sweeps and on the actions.
|
|
73
|
+
*
|
|
74
|
+
* The stamp is keyed on the DECLARATION, not on `core/daemon-actor.ts`'s mark.
|
|
75
|
+
* The two answer different questions: the mark says "this process is the daemon
|
|
76
|
+
* loop" and is what routes an advance under the autonomous
|
|
77
|
+
* `log.advance.daemon` class (APRV-382), while the declaration says "records
|
|
78
|
+
* this process writes name this instance". `approval serve` dispatches verbs an
|
|
79
|
+
* agent asked for, so marking it the daemon to get the stamp would hand its
|
|
80
|
+
* process the daemon's advance route, a widening nothing here needs. Declaring
|
|
81
|
+
* without marking stamps and restricts, and widens nothing.
|
|
82
|
+
*
|
|
83
|
+
* ## Why this module imports almost nothing
|
|
84
|
+
*
|
|
85
|
+
* `core/log.ts` imports it, and `core/log.ts` is the bottom of this codebase's
|
|
86
|
+
* import graph: `core/instance.ts` (where the derived id comes from) reaches
|
|
87
|
+
* `core/attest.ts` through `core/env-file.ts`, and `core/attest.ts` spreads
|
|
88
|
+
* `APPEND_ERROR_CODES` at module scope, so importing the
|
|
89
|
+
* derivation here would close a cycle whose evaluation order throws before any
|
|
90
|
+
* test ran. The split is also the right seam on its own terms: what the write
|
|
91
|
+
* boundary needs is a grammar, a state and one question, and everything that
|
|
92
|
+
* reads a policy or a filesystem to ANSWER that question lives in
|
|
93
|
+
* `core/daemon-host.ts`.
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* The environment variable a launch may declare the daemon's id in.
|
|
97
|
+
*
|
|
98
|
+
* Beside `APPROVAL_HUMAN` and the credential variables, and in the launch
|
|
99
|
+
* environment for the same reason they are: SPEC.md §11.1 invariant 7 keeps this
|
|
100
|
+
* runtime from reading configuration out of the working tree, and a hosted
|
|
101
|
+
* daemon's id is configuration its OPERATOR sets, never something the tenant's
|
|
102
|
+
* repository tells the host about itself.
|
|
103
|
+
*/
|
|
104
|
+
export declare const DAEMON_ID_ENV = "APPROVAL_DAEMON_ID";
|
|
105
|
+
/** The longest id this runtime writes or admits. */
|
|
106
|
+
export declare const DAEMON_ID_MAX_LENGTH = 64;
|
|
107
|
+
/** Is `value` a well-formed daemon id? */
|
|
108
|
+
export declare function isDaemonId(value: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* The two ways an append can be refused for the identity of the daemon making
|
|
111
|
+
* it, spread into `APPEND_ERROR_CODES` by `core/log.ts`.
|
|
112
|
+
*
|
|
113
|
+
* Two codes rather than one, because the repairs have nothing in common and
|
|
114
|
+
* SPEC.md §11.1 invariant 6 is about a caller being able to act on the answer:
|
|
115
|
+
* one is a line to add to an attested policy, the other is a variable to fix in
|
|
116
|
+
* the launch environment of a process that should not have started.
|
|
117
|
+
*
|
|
118
|
+
* Declared here rather than in `core/log.ts`'s literal list so that the codes and
|
|
119
|
+
* the logic that emits them cannot drift apart; the union there spreads this one,
|
|
120
|
+
* exactly as `ATTEST_ERROR_CODES` spreads it.
|
|
121
|
+
*/
|
|
122
|
+
export declare const DAEMON_APPEND_REFUSAL_CODES: readonly [
|
|
123
|
+
/**
|
|
124
|
+
* The daemon declared an id through {@link DAEMON_ID_ENV} that is not a
|
|
125
|
+
* well-formed id at all, so no record it wrote could be attributed to anything.
|
|
126
|
+
*
|
|
127
|
+
* Refused rather than silently falling back to the derived id, which is the
|
|
128
|
+
* fail-closed direction twice over: an operator who set the variable meant the
|
|
129
|
+
* value they set, and a runtime that quietly used a different one would write
|
|
130
|
+
* records under a name nobody chose, against an allowlist nobody wrote for it.
|
|
131
|
+
*/
|
|
132
|
+
"daemon-id-invalid",
|
|
133
|
+
/**
|
|
134
|
+
* The attested policy lists the daemon ids that may write to this log, and this
|
|
135
|
+
* daemon's id is not among them.
|
|
136
|
+
*
|
|
137
|
+
* An ABSENT list is no restriction, not an empty one: a policy that never
|
|
138
|
+
* declares the key behaves exactly as every policy written before it existed.
|
|
139
|
+
*/
|
|
140
|
+
"daemon-not-allowed"];
|
|
141
|
+
export type DaemonAppendRefusalCode = (typeof DAEMON_APPEND_REFUSAL_CODES)[number];
|
|
142
|
+
/** Where this process's id came from. */
|
|
143
|
+
export type DaemonIdSource = "environment" | "derived";
|
|
144
|
+
/**
|
|
145
|
+
* What a daemon process has declared about itself, or `null` in a process that
|
|
146
|
+
* has declared nothing.
|
|
147
|
+
*
|
|
148
|
+
* `id` is `null` exactly when {@link DAEMON_ID_ENV} held something unusable: the
|
|
149
|
+
* state is still declared, because a daemon that cannot name itself must be
|
|
150
|
+
* REFUSED rather than left unmarked and writing.
|
|
151
|
+
*/
|
|
152
|
+
export interface DaemonIdentity {
|
|
153
|
+
/** The resolved id, or `null` when the declared one is unusable. */
|
|
154
|
+
id: string | null;
|
|
155
|
+
/** The raw value {@link DAEMON_ID_ENV} held, when it held one. */
|
|
156
|
+
declared: string | null;
|
|
157
|
+
/** Where {@link id} came from; `null` when there is no id. */
|
|
158
|
+
source: DaemonIdSource | null;
|
|
159
|
+
/**
|
|
160
|
+
* The ids the attested policy admits, or `null` for no restriction.
|
|
161
|
+
*
|
|
162
|
+
* `null` is the state a process starts in and the state an attested policy
|
|
163
|
+
* declaring no `daemons` key resolves to. The difference between them matters
|
|
164
|
+
* to nothing here, deliberately: neither is a restriction, and a runtime that
|
|
165
|
+
* refused because it had not looked yet would refuse every deployment that
|
|
166
|
+
* never adopts the key.
|
|
167
|
+
*/
|
|
168
|
+
allowed: readonly string[] | null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Declare the id this process's records carry. Called through
|
|
172
|
+
* `core/daemon-host.ts` by the processes the header names (the daemon loop,
|
|
173
|
+
* `approval serve` and its hook threads, the Telegram webhook listener), and by
|
|
174
|
+
* nothing else.
|
|
175
|
+
*
|
|
176
|
+
* Takes the RESOLVED values rather than resolving them, because resolution reads
|
|
177
|
+
* the environment and the filesystem and this module is the one `core/log.ts`
|
|
178
|
+
* imports (see the header). Idempotent, and a second call replaces the first: a
|
|
179
|
+
* runtime that re-resolves its identity is reporting a fact about itself, and the
|
|
180
|
+
* newest reading is the one in force.
|
|
181
|
+
*/
|
|
182
|
+
export declare function declareDaemonIdentity(state: {
|
|
183
|
+
id: string | null;
|
|
184
|
+
declared?: string | null;
|
|
185
|
+
source: DaemonIdSource | null;
|
|
186
|
+
}): void;
|
|
187
|
+
/**
|
|
188
|
+
* Record which daemon ids this log's ATTESTED policy admits.
|
|
189
|
+
*
|
|
190
|
+
* Called only after a successful resolution: the daemon loaded its policy, read
|
|
191
|
+
* the verified log, and the attestation matched (`core/daemon-host.ts`). A
|
|
192
|
+
* resolution that FAILED calls nothing, so the restriction last resolved stays in
|
|
193
|
+
* force rather than lapsing — a policy that becomes unreadable must not be a way
|
|
194
|
+
* to escape the list it carried a moment ago.
|
|
195
|
+
*
|
|
196
|
+
* `null` means the attested policy declares none, which is no restriction.
|
|
197
|
+
*/
|
|
198
|
+
export declare function setDaemonAllowlist(allowed: readonly string[] | null): void;
|
|
199
|
+
/** Forget the declaration. For tests, and for a runtime that stops being one. */
|
|
200
|
+
export declare function clearDaemonIdentity(): void;
|
|
201
|
+
/** What this process has declared about itself, or `null`. */
|
|
202
|
+
export declare function daemonIdentity(): DaemonIdentity | null;
|
|
203
|
+
/** What {@link daemonStampForAppend} answers. */
|
|
204
|
+
export type DaemonStamp =
|
|
205
|
+
/** Not a declaring daemon process: the record is written as it always was. */
|
|
206
|
+
{
|
|
207
|
+
kind: "absent";
|
|
208
|
+
}
|
|
209
|
+
/** Stamp this id onto the record. */
|
|
210
|
+
| {
|
|
211
|
+
kind: "stamp";
|
|
212
|
+
id: string;
|
|
213
|
+
}
|
|
214
|
+
/** Refuse the append; nothing is written. */
|
|
215
|
+
| {
|
|
216
|
+
kind: "refuse";
|
|
217
|
+
code: DaemonAppendRefusalCode;
|
|
218
|
+
message: string;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* The write boundary's question, answered from this process's own state
|
|
222
|
+
* (APRV-383).
|
|
223
|
+
*
|
|
224
|
+
* Both halves of the answer are here rather than in two functions, because they
|
|
225
|
+
* are one decision made in one order: a process that cannot name itself is
|
|
226
|
+
* refused before anything asks whether its name is allowed.
|
|
227
|
+
*
|
|
228
|
+
* A process that declared NO identity gets `absent`, whether or not it marked
|
|
229
|
+
* itself the daemon. That is every session, every hook process and the
|
|
230
|
+
* pre-APRV-383 daemon, and the records they write are the records they always
|
|
231
|
+
* wrote; turning an undeclared mark into a dead log would make an unrelated
|
|
232
|
+
* module's `markDaemonProcess` call a stop-the-world bug. The answer is keyed on
|
|
233
|
+
* the declaration alone (APRV-448, see the header): the daemon loop marks and
|
|
234
|
+
* declares together, while `approval serve` and the webhook listener declare
|
|
235
|
+
* without marking, because the mark carries the daemon's advance route and the
|
|
236
|
+
* declaration carries nothing but a name and a restriction.
|
|
237
|
+
*/
|
|
238
|
+
export declare function daemonStampForAppend(): DaemonStamp;
|