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,252 @@
|
|
|
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 const DAEMON_ID_ENV = "APPROVAL_DAEMON_ID";
|
|
105
|
+
/** The longest id this runtime writes or admits. */
|
|
106
|
+
export const DAEMON_ID_MAX_LENGTH = 64;
|
|
107
|
+
/**
|
|
108
|
+
* The shape an id takes: lowercase alphanumerics, dots, dashes and underscores,
|
|
109
|
+
* starting with an alphanumeric.
|
|
110
|
+
*
|
|
111
|
+
* Narrow on purpose, and for the reason `payload.harness_version` is narrow
|
|
112
|
+
* (APRV-227): the value is a string from a launch environment being written into
|
|
113
|
+
* an append-only log, so a banner, a path, a newline or a shell fragment must not
|
|
114
|
+
* be able to arrive through it. It is also a value an operator COMPARES by eye
|
|
115
|
+
* against `approval doctor` and against the `daemons` list in their policy, and
|
|
116
|
+
* two ids differing only in case are two ids nobody can tell apart on a phone.
|
|
117
|
+
*/
|
|
118
|
+
const DAEMON_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
119
|
+
/** Is `value` a well-formed daemon id? */
|
|
120
|
+
export function isDaemonId(value) {
|
|
121
|
+
return value.length <= DAEMON_ID_MAX_LENGTH && DAEMON_ID_PATTERN.test(value);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The two ways an append can be refused for the identity of the daemon making
|
|
125
|
+
* it, spread into `APPEND_ERROR_CODES` by `core/log.ts`.
|
|
126
|
+
*
|
|
127
|
+
* Two codes rather than one, because the repairs have nothing in common and
|
|
128
|
+
* SPEC.md §11.1 invariant 6 is about a caller being able to act on the answer:
|
|
129
|
+
* one is a line to add to an attested policy, the other is a variable to fix in
|
|
130
|
+
* the launch environment of a process that should not have started.
|
|
131
|
+
*
|
|
132
|
+
* Declared here rather than in `core/log.ts`'s literal list so that the codes and
|
|
133
|
+
* the logic that emits them cannot drift apart; the union there spreads this one,
|
|
134
|
+
* exactly as `ATTEST_ERROR_CODES` spreads it.
|
|
135
|
+
*/
|
|
136
|
+
export const DAEMON_APPEND_REFUSAL_CODES = [
|
|
137
|
+
/**
|
|
138
|
+
* The daemon declared an id through {@link DAEMON_ID_ENV} that is not a
|
|
139
|
+
* well-formed id at all, so no record it wrote could be attributed to anything.
|
|
140
|
+
*
|
|
141
|
+
* Refused rather than silently falling back to the derived id, which is the
|
|
142
|
+
* fail-closed direction twice over: an operator who set the variable meant the
|
|
143
|
+
* value they set, and a runtime that quietly used a different one would write
|
|
144
|
+
* records under a name nobody chose, against an allowlist nobody wrote for it.
|
|
145
|
+
*/
|
|
146
|
+
"daemon-id-invalid",
|
|
147
|
+
/**
|
|
148
|
+
* The attested policy lists the daemon ids that may write to this log, and this
|
|
149
|
+
* daemon's id is not among them.
|
|
150
|
+
*
|
|
151
|
+
* An ABSENT list is no restriction, not an empty one: a policy that never
|
|
152
|
+
* declares the key behaves exactly as every policy written before it existed.
|
|
153
|
+
*/
|
|
154
|
+
"daemon-not-allowed",
|
|
155
|
+
];
|
|
156
|
+
/** The declaring process's own state. Never set on another process's behalf. */
|
|
157
|
+
let identity = null;
|
|
158
|
+
/**
|
|
159
|
+
* The pid that made the declaration, for the reason `core/daemon-actor.ts` keeps
|
|
160
|
+
* one: the stamp answers only while it is still this process's own. Kept beside
|
|
161
|
+
* {@link identity} rather than inside it, so the reported state is unchanged.
|
|
162
|
+
*/
|
|
163
|
+
let declaredPid = null;
|
|
164
|
+
/**
|
|
165
|
+
* Declare the id this process's records carry. Called through
|
|
166
|
+
* `core/daemon-host.ts` by the processes the header names (the daemon loop,
|
|
167
|
+
* `approval serve` and its hook threads, the Telegram webhook listener), and by
|
|
168
|
+
* nothing else.
|
|
169
|
+
*
|
|
170
|
+
* Takes the RESOLVED values rather than resolving them, because resolution reads
|
|
171
|
+
* the environment and the filesystem and this module is the one `core/log.ts`
|
|
172
|
+
* imports (see the header). Idempotent, and a second call replaces the first: a
|
|
173
|
+
* runtime that re-resolves its identity is reporting a fact about itself, and the
|
|
174
|
+
* newest reading is the one in force.
|
|
175
|
+
*/
|
|
176
|
+
export function declareDaemonIdentity(state) {
|
|
177
|
+
identity = {
|
|
178
|
+
id: state.id,
|
|
179
|
+
declared: state.declared ?? null,
|
|
180
|
+
source: state.source,
|
|
181
|
+
// A fresh declaration carries no allowlist. The daemon resolves one from its
|
|
182
|
+
// attested policy and sets it separately, so an identity re-declared mid-run
|
|
183
|
+
// cannot silently drop a restriction that was in force.
|
|
184
|
+
allowed: identity === null ? null : identity.allowed,
|
|
185
|
+
};
|
|
186
|
+
declaredPid = process.pid;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Record which daemon ids this log's ATTESTED policy admits.
|
|
190
|
+
*
|
|
191
|
+
* Called only after a successful resolution: the daemon loaded its policy, read
|
|
192
|
+
* the verified log, and the attestation matched (`core/daemon-host.ts`). A
|
|
193
|
+
* resolution that FAILED calls nothing, so the restriction last resolved stays in
|
|
194
|
+
* force rather than lapsing — a policy that becomes unreadable must not be a way
|
|
195
|
+
* to escape the list it carried a moment ago.
|
|
196
|
+
*
|
|
197
|
+
* `null` means the attested policy declares none, which is no restriction.
|
|
198
|
+
*/
|
|
199
|
+
export function setDaemonAllowlist(allowed) {
|
|
200
|
+
if (identity === null)
|
|
201
|
+
return;
|
|
202
|
+
identity = { ...identity, allowed: allowed === null ? null : [...allowed] };
|
|
203
|
+
}
|
|
204
|
+
/** Forget the declaration. For tests, and for a runtime that stops being one. */
|
|
205
|
+
export function clearDaemonIdentity() {
|
|
206
|
+
identity = null;
|
|
207
|
+
declaredPid = null;
|
|
208
|
+
}
|
|
209
|
+
/** What this process has declared about itself, or `null`. */
|
|
210
|
+
export function daemonIdentity() {
|
|
211
|
+
return identity;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The write boundary's question, answered from this process's own state
|
|
215
|
+
* (APRV-383).
|
|
216
|
+
*
|
|
217
|
+
* Both halves of the answer are here rather than in two functions, because they
|
|
218
|
+
* are one decision made in one order: a process that cannot name itself is
|
|
219
|
+
* refused before anything asks whether its name is allowed.
|
|
220
|
+
*
|
|
221
|
+
* A process that declared NO identity gets `absent`, whether or not it marked
|
|
222
|
+
* itself the daemon. That is every session, every hook process and the
|
|
223
|
+
* pre-APRV-383 daemon, and the records they write are the records they always
|
|
224
|
+
* wrote; turning an undeclared mark into a dead log would make an unrelated
|
|
225
|
+
* module's `markDaemonProcess` call a stop-the-world bug. The answer is keyed on
|
|
226
|
+
* the declaration alone (APRV-448, see the header): the daemon loop marks and
|
|
227
|
+
* declares together, while `approval serve` and the webhook listener declare
|
|
228
|
+
* without marking, because the mark carries the daemon's advance route and the
|
|
229
|
+
* declaration carries nothing but a name and a restriction.
|
|
230
|
+
*/
|
|
231
|
+
export function daemonStampForAppend() {
|
|
232
|
+
const state = identity;
|
|
233
|
+
if (state === null || declaredPid !== process.pid)
|
|
234
|
+
return { kind: "absent" };
|
|
235
|
+
if (state.id === null) {
|
|
236
|
+
return {
|
|
237
|
+
kind: "refuse",
|
|
238
|
+
code: "daemon-id-invalid",
|
|
239
|
+
message: `this daemon declared ${DAEMON_ID_ENV}=${JSON.stringify(state.declared ?? "")}, which is not a usable daemon id (lowercase letters, digits, \`.\`, \`-\` and \`_\`, starting with a letter or digit, at most ${String(DAEMON_ID_MAX_LENGTH)} characters), so no record it wrote could name which daemon wrote it. Nothing was written: fix ${DAEMON_ID_ENV} in this process's launch environment, or unset it and let the id derive from this instance`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const allowed = state.allowed;
|
|
243
|
+
if (allowed !== null && !allowed.includes(state.id)) {
|
|
244
|
+
return {
|
|
245
|
+
kind: "refuse",
|
|
246
|
+
code: "daemon-not-allowed",
|
|
247
|
+
message: `this daemon's id is ${state.id} and the attested policy's \`daemons\` list admits ${allowed.length === 0 ? "no daemon at all" : allowed.join(", ")}, so it may not write to this log. Nothing was written: add ${state.id} to \`daemons\` and re-attest the policy, or run the daemon whose id the list already names`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return { kind: "stamp", id: state.id };
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=daemon-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-identity.js","sourceRoot":"","sources":["../../../src/core/daemon-identity.ts"],"names":[],"mappings":"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"}
|
package/dist/src/core/gate.d.ts
CHANGED
|
@@ -37,9 +37,12 @@
|
|
|
37
37
|
*
|
|
38
38
|
* ## Lazy expiry — the named requirement
|
|
39
39
|
*
|
|
40
|
-
* A request expires when `ts > requestTs +
|
|
41
|
-
* not** an `approval.expired` event exists.
|
|
42
|
-
*
|
|
40
|
+
* A request expires when `ts > requestTs + effective window`, **whether or
|
|
41
|
+
* not** an `approval.expired` event exists. The effective window is
|
|
42
|
+
* `defaults.approval_ttl`, narrowed since APRV-423 by whatever harness ceiling
|
|
43
|
+
* the requesting hook declared on `payload.harness_cap_ms` (a self-reported
|
|
44
|
+
* bound that can only shorten; see `core/harness-wait.ts`). Nothing may depend
|
|
45
|
+
* on a daemon having run: if the expiry sweep is asleep, a late grant must still be refused.
|
|
43
46
|
* {@link requestState} therefore computes expiry two ways — from the event, and
|
|
44
47
|
* lazily from the arithmetic — and treats them as equivalent.
|
|
45
48
|
*
|
|
@@ -497,7 +500,51 @@ export declare const GATE_REFUSAL_CODES: readonly [
|
|
|
497
500
|
* without an address would spend a human's decision on an authorization
|
|
498
501
|
* nothing can ever open. Nothing is appended; the next attempt asks again.
|
|
499
502
|
*/
|
|
500
|
-
"token-delivery-unavailable"
|
|
503
|
+
"token-delivery-unavailable",
|
|
504
|
+
/**
|
|
505
|
+
* `approval propose` named a class the policy has not opened to agents
|
|
506
|
+
* (APRV-445, amended SPEC.md §5.2): the class is not an exact key of
|
|
507
|
+
* `policy.classes`, or neither that key nor a declared `<prefix>.*` family of
|
|
508
|
+
* it sets `agent_may_request: true`. Nothing is registered and nothing is
|
|
509
|
+
* appended. The repair is the operator's, a policy edit and a re-attestation;
|
|
510
|
+
* no retry by the agent changes it.
|
|
511
|
+
*/
|
|
512
|
+
"class-not-agent-requestable",
|
|
513
|
+
/**
|
|
514
|
+
* `approval propose` / `approval start` carried a payload larger than
|
|
515
|
+
* {@link PROPOSE_PAYLOAD_MAX_BYTES} (APRV-445). Refused before the bytes are
|
|
516
|
+
* parsed or hashed; nothing is stored and nothing is appended.
|
|
517
|
+
*/
|
|
518
|
+
"payload-too-large",
|
|
519
|
+
/**
|
|
520
|
+
* `approval request` (or a task FILE's registration) named a `propose:` task
|
|
521
|
+
* (APRV-445 refutation, S1). A proposal is requested only through `propose`,
|
|
522
|
+
* which re-checks the operator's opening of its class; a plain request on the
|
|
523
|
+
* same task would skip that check, and a file-registered `propose:` id would
|
|
524
|
+
* borrow the exclusions proposals get from the harness hook.
|
|
525
|
+
*/
|
|
526
|
+
"task-is-proposal",
|
|
527
|
+
/**
|
|
528
|
+
* `approval start` named a task `propose` did not register (APRV-445, L9).
|
|
529
|
+
* `start` records executions of proposals and of nothing else; every other
|
|
530
|
+
* action executes through `approval run`, an adapter, or the hook.
|
|
531
|
+
*/
|
|
532
|
+
"task-not-proposal",
|
|
533
|
+
/**
|
|
534
|
+
* `approval propose --key` does not begin with `<class>:` (APRV-445, S4). The
|
|
535
|
+
* key names the class it was proposed under, so a key cannot be read back
|
|
536
|
+
* later as belonging to a different class than its record says.
|
|
537
|
+
*/
|
|
538
|
+
"key-class-mismatch",
|
|
539
|
+
/**
|
|
540
|
+
* `approval propose` found the log moving under it on every one of its
|
|
541
|
+
* bounded attempts (APRV-445 recheck 3): each re-read showed an answer the
|
|
542
|
+
* next append was refused for. Exit 1, nothing more was appended, and the
|
|
543
|
+
* proposal's standing is whatever the log now says; read it with `wait
|
|
544
|
+
* --timeout 0` or call again. Distinct from `already-decided`, which would
|
|
545
|
+
* tell a caller a proposal is answered when it may stand granted or pending.
|
|
546
|
+
*/
|
|
547
|
+
"contended"];
|
|
501
548
|
export type GateRefusalCode = (typeof GATE_REFUSAL_CODES)[number];
|
|
502
549
|
/** Every gate failure is one of these. Nothing here throws. */
|
|
503
550
|
export interface GateRefusal {
|
|
@@ -731,7 +778,24 @@ export declare function register(logPath: string, source: RegisterSource, actor:
|
|
|
731
778
|
*/
|
|
732
779
|
export interface RegisterOptions extends GateOptions {
|
|
733
780
|
harness?: HarnessProvenance;
|
|
781
|
+
/** Set by `propose` alone: the `propose:` task namespace is its (APRV-445). */
|
|
782
|
+
proposal?: true;
|
|
734
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* The `task.registered` record for `task`, or the `not-registered` refusal.
|
|
786
|
+
*
|
|
787
|
+
* The one raise site for SPEC.md §11.2's `not-registered` ("no task.registered
|
|
788
|
+
* record exists for the task id"). `request` reaches it through
|
|
789
|
+
* {@link registeredAction}; `approval wait` calls it directly (APRV-428), so a
|
|
790
|
+
* wait on a task the log has never seen is refused in the same words rather
|
|
791
|
+
* than answered as a vacuous grant. The latest registration wins, as it always
|
|
792
|
+
* has for `registeredAction`; the register path refuses a second one, so a log
|
|
793
|
+
* carrying two was written by something else.
|
|
794
|
+
*/
|
|
795
|
+
export declare function taskRegistration(records: EventRecord[], task: string): {
|
|
796
|
+
ok: true;
|
|
797
|
+
record: EventRecord;
|
|
798
|
+
} | GateRefusal;
|
|
735
799
|
/**
|
|
736
800
|
* The declared action for `(task, actionKey)`, as registered in the log.
|
|
737
801
|
*
|
|
@@ -797,6 +861,25 @@ export interface RequestInput {
|
|
|
797
861
|
* policy's `defaults.approval_ttl` remains the only deadline with authority.
|
|
798
862
|
*/
|
|
799
863
|
wait_until?: string;
|
|
864
|
+
/**
|
|
865
|
+
* The ceiling the requesting harness imposes on the hook process that is
|
|
866
|
+
* asking, in milliseconds (APRV-423).
|
|
867
|
+
*
|
|
868
|
+
* Recorded as `payload.harness_cap_ms` and read back by `core/state.ts`,
|
|
869
|
+
* which judges the request against the SHORTER of this minus a fixed margin
|
|
870
|
+
* and the policy's own `defaults.approval_ttl`. It is the answer to APRV-410:
|
|
871
|
+
* a harness that kills the hook while the question is still on a phone turns
|
|
872
|
+
* the tap that follows into a grant on a call nobody holds, so the question
|
|
873
|
+
* is made to end first and both records say the same thing.
|
|
874
|
+
*
|
|
875
|
+
* Claimed, and structurally unable to reduce scrutiny (SPEC.md §11.1
|
|
876
|
+
* invariant 4): the runtime takes a MINIMUM, so an overstated cap changes
|
|
877
|
+
* nothing and an understated one only shortens the claimant's own window.
|
|
878
|
+
* The write boundary constrains it to a positive integer on a request that
|
|
879
|
+
* also declares `execution: "harness"` — a cap on a token-minting request
|
|
880
|
+
* would be a question with a shorter life than the authorization it mints.
|
|
881
|
+
*/
|
|
882
|
+
harnessCapMs?: number;
|
|
800
883
|
/**
|
|
801
884
|
* The caller has established that loop safety floors this action to `manual`
|
|
802
885
|
* for this invocation (APRV-145, amended SPEC.md §10.2).
|
|
@@ -842,6 +925,15 @@ export interface RequestInput {
|
|
|
842
925
|
* spend a human's decision on an authorization that can never execute.
|
|
843
926
|
*/
|
|
844
927
|
delivery?: "self";
|
|
928
|
+
/**
|
|
929
|
+
* This request was opened by `approval propose`, whose caller named the class
|
|
930
|
+
* itself (APRV-445). When set, intake re-checks `agentRequestability` against
|
|
931
|
+
* the policy bytes THIS attempt read, inside the retried read-check-append
|
|
932
|
+
* cycle, so a re-attestation between `propose`'s own check and the append
|
|
933
|
+
* cannot admit a class the policy no longer opens to agents (SPEC.md §11.1
|
|
934
|
+
* invariant 5). It only ever adds a refusal.
|
|
935
|
+
*/
|
|
936
|
+
agentProposal?: true;
|
|
845
937
|
}
|
|
846
938
|
/**
|
|
847
939
|
* Why a `supervised-live` action was, or was not, sent to the human gate
|
|
@@ -1090,6 +1182,15 @@ export type DecideResult = {
|
|
|
1090
1182
|
*/
|
|
1091
1183
|
export declare function decide(logPath: string, actionKey: string, decision: Decision, actor: string, options?: DecideOptions): DecideResult;
|
|
1092
1184
|
export interface WithdrawOptions extends GateOptions {
|
|
1185
|
+
/**
|
|
1186
|
+
* Withdraw only a pending request that is VOID at the moment of the append
|
|
1187
|
+
* (APRV-445 recheck, SF1): pinned to a policy hash that is no longer the
|
|
1188
|
+
* attested one. Judged inside the retried read-check-append cycle, so a
|
|
1189
|
+
* caller acting on a stale reading cannot retract a question a concurrent
|
|
1190
|
+
* caller has just re-asked under the policy in force. A live, non-void
|
|
1191
|
+
* request refuses `duplicate-request`.
|
|
1192
|
+
*/
|
|
1193
|
+
onlyIfVoid?: true;
|
|
1093
1194
|
/** Why the requester is retracting. Defaults to `cancelled`. */
|
|
1094
1195
|
reason?: WithdrawReason;
|
|
1095
1196
|
/** The requester's free-text elaboration, recorded in the payload. */
|
|
@@ -1172,7 +1273,19 @@ export interface HarnessCarry {
|
|
|
1172
1273
|
/** `seq` of the `approval.granted`, on `granted` only. */
|
|
1173
1274
|
decisionSeq: number | null;
|
|
1174
1275
|
}
|
|
1175
|
-
export declare function findHarnessCarry(records: EventRecord[], payloadHash: string, cls: string, ts: string, ttlMs: number | null
|
|
1276
|
+
export declare function findHarnessCarry(records: EventRecord[], payloadHash: string, cls: string, ts: string, ttlMs: number | null,
|
|
1277
|
+
/**
|
|
1278
|
+
* How long a question of this caller's outlives the wait that opened it
|
|
1279
|
+
* (APRV-410, `core/harness-wait.ts`'s `abandonedAfterMs`), or `null` for a
|
|
1280
|
+
* caller that declares no such window.
|
|
1281
|
+
*
|
|
1282
|
+
* Only a GRANTED candidate is bounded by it. A pending one is the question
|
|
1283
|
+
* this invocation is about to adopt, and the withdrawal of an abandoned
|
|
1284
|
+
* pending question is the requester's own business, not this function's:
|
|
1285
|
+
* `cli/hook.ts` sweeps those through `withdraw`, which is requester-only by
|
|
1286
|
+
* design (APRV-106 rule 1).
|
|
1287
|
+
*/
|
|
1288
|
+
abandonAfterMs?: number | null): HarnessCarry | null;
|
|
1176
1289
|
/**
|
|
1177
1290
|
* `GateOptions` plus the one thing a harness spend states about itself
|
|
1178
1291
|
* (APRV-146).
|
|
@@ -1447,3 +1560,191 @@ export type ExpireResult = {
|
|
|
1447
1560
|
* as `rejected`.
|
|
1448
1561
|
*/
|
|
1449
1562
|
export declare function expire(logPath: string, actionKey: string, options?: GateOptions): ExpireResult;
|
|
1563
|
+
/**
|
|
1564
|
+
* The largest payload `approval propose` and `approval start` accept, in UTF-8
|
|
1565
|
+
* bytes of the JSON text as the caller sent it: 256 KiB.
|
|
1566
|
+
*
|
|
1567
|
+
* Well under `approval serve`'s 1 MiB body cap, so the refusal a caller meets is
|
|
1568
|
+
* this named one rather than a transport's. A proposal is a sentence or a
|
|
1569
|
+
* paragraph a human will read on a phone; a payload this size is not one.
|
|
1570
|
+
*/
|
|
1571
|
+
export declare const PROPOSE_PAYLOAD_MAX_BYTES: number;
|
|
1572
|
+
/** The longest `--key` `propose` accepts, in UTF-8 bytes (APRV-445, L6). */
|
|
1573
|
+
export declare const PROPOSE_KEY_MAX_BYTES = 1024;
|
|
1574
|
+
/** The longest `--summary` `propose` accepts, in UTF-8 bytes (APRV-445, L6). */
|
|
1575
|
+
export declare const PROPOSE_SUMMARY_MAX_BYTES = 4096;
|
|
1576
|
+
/** `origin.app` on every envelope `propose` registers. */
|
|
1577
|
+
export declare const PROPOSE_ORIGIN_APP = "approval-propose";
|
|
1578
|
+
/** The task-id prefix `propose` derives under. */
|
|
1579
|
+
export declare const PROPOSE_TASK_PREFIX = "propose:";
|
|
1580
|
+
/**
|
|
1581
|
+
* Was this task registered by `propose` (APRV-445)?
|
|
1582
|
+
*
|
|
1583
|
+
* A proposal's request carries `execution: "harness"` like a hook's, so the two
|
|
1584
|
+
* hook-side readers of that marker (the carry and the abandoned-question sweep)
|
|
1585
|
+
* ask this first: a proposal waits days for a human, is spent only by `start`,
|
|
1586
|
+
* and is withdrawn only by its requester's own call.
|
|
1587
|
+
*/
|
|
1588
|
+
export declare function isProposalTask(task: unknown): boolean;
|
|
1589
|
+
/**
|
|
1590
|
+
* The task id `propose` registers under, from who is asking, the class and the
|
|
1591
|
+
* key: `propose:` and the first 32 hex digits of the SHA-256 over the RFC 8785
|
|
1592
|
+
* canonical JSON of `[actor, class, key]`.
|
|
1593
|
+
*
|
|
1594
|
+
* Deterministic, so a retry lands on the task its first attempt registered and
|
|
1595
|
+
* is recognised as a retry. The ACTOR is the tenant half of the derivation: one
|
|
1596
|
+
* `approval serve` process serves one store as one fixed `agent:<id>`, so within
|
|
1597
|
+
* a store the actor is who the tenant's agent is, and two actors proposing the
|
|
1598
|
+
* same key derive two different tasks, the second of which `register` refuses
|
|
1599
|
+
* because the key is already declared under the first (`task-already-registered`).
|
|
1600
|
+
* The class is in it so that the same key proposed under a second class is a
|
|
1601
|
+
* different task rather than a silent re-declaration.
|
|
1602
|
+
*/
|
|
1603
|
+
export declare function proposedTaskId(actor: string, cls: string, actionKey: string): string;
|
|
1604
|
+
/** What an agent proposes. The payload is a parsed JSON object. */
|
|
1605
|
+
export interface ProposeInput {
|
|
1606
|
+
cls: string;
|
|
1607
|
+
actionKey: string;
|
|
1608
|
+
summary: string;
|
|
1609
|
+
payload: unknown;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* How a proposal stands after the call.
|
|
1613
|
+
*
|
|
1614
|
+
* `decision` is what the caller branches on: `requested` means a human is (or
|
|
1615
|
+
* was) asked and `state` says where that stands; `autonomous` and `supervised`
|
|
1616
|
+
* mean the policy itself authorizes the action, nothing was put in front of a
|
|
1617
|
+
* human, and the caller proceeds and records the execution with `approval start`.
|
|
1618
|
+
*
|
|
1619
|
+
* `idempotent` is true exactly when this call appended nothing: a retry of a
|
|
1620
|
+
* proposal whose registration (and, on the manual path, whose request) already
|
|
1621
|
+
* stands under the same class, key and payload hash.
|
|
1622
|
+
*/
|
|
1623
|
+
export type ProposeResult = {
|
|
1624
|
+
ok: true;
|
|
1625
|
+
task: string;
|
|
1626
|
+
actionKey: string;
|
|
1627
|
+
cls: string;
|
|
1628
|
+
payloadHash: string;
|
|
1629
|
+
decision: "requested" | "autonomous" | "supervised";
|
|
1630
|
+
/**
|
|
1631
|
+
* `null` off the manual path, where there is no request to be in a state;
|
|
1632
|
+
* `executed` once `start` recorded the execution (APRV-445, L7).
|
|
1633
|
+
*/
|
|
1634
|
+
state: RequestState | "executed" | null;
|
|
1635
|
+
seq: number | null;
|
|
1636
|
+
idempotent: boolean;
|
|
1637
|
+
} | GateRefusal;
|
|
1638
|
+
/**
|
|
1639
|
+
* `approval propose` (APRV-445, amended SPEC.md §5.2): register and request one
|
|
1640
|
+
* action in one call, for a requester that holds the payload inline and no file
|
|
1641
|
+
* on the gate's machine.
|
|
1642
|
+
*
|
|
1643
|
+
* ## Why this verb may name its class when `request` may not
|
|
1644
|
+
*
|
|
1645
|
+
* `approval request` reads the class from the log's `task.registered` record so
|
|
1646
|
+
* that an agent cannot call a `financial.spend` a `read.web`. Here the agent is
|
|
1647
|
+
* also the registrant, so that rule alone would prove nothing, and the bound is
|
|
1648
|
+
* the operator's instead: the class must be an EXACT key of `policy.classes`, and
|
|
1649
|
+
* that key or a declared `<prefix>.*` family of it must say
|
|
1650
|
+
* `agent_may_request: true`. A class reached only through a wildcard is refused,
|
|
1651
|
+
* so the set of classes an agent can propose in is a set of lines the operator
|
|
1652
|
+
* wrote and attested by name. What the agent chooses among them is still its own
|
|
1653
|
+
* statement (SPEC.md §11.1 invariant 4 is why the choice is bounded at all), and
|
|
1654
|
+
* an operator opening two classes of different autonomy to agents is trusting
|
|
1655
|
+
* the agent to pick the right one; the record names the class it picked.
|
|
1656
|
+
*
|
|
1657
|
+
* ## Order
|
|
1658
|
+
*
|
|
1659
|
+
* Attestation, then the agent-requestability gate and the class's own autonomy
|
|
1660
|
+
* (`human-only`, an unruled `harness.launch.*`), all BEFORE anything is
|
|
1661
|
+
* registered, so a refused class leaves no registration behind. Then the
|
|
1662
|
+
* registration, then {@link request} with `agentProposal: true`, which repeats
|
|
1663
|
+
* the gate against the bytes its own attempt reads. `execution: "harness"` on the
|
|
1664
|
+
* request: nothing ever presents a token for a proposal, the requester proceeds
|
|
1665
|
+
* on the grant itself and records that with `approval start`, so a minted token
|
|
1666
|
+
* would be a live credential with no spender.
|
|
1667
|
+
*
|
|
1668
|
+
* ## Idempotency
|
|
1669
|
+
*
|
|
1670
|
+
* The task id is {@link proposedTaskId}. A second call with the same actor,
|
|
1671
|
+
* class, key and payload hash finds its own registration and appends nothing
|
|
1672
|
+
* while the request is live or decided, answering with the request's state and
|
|
1673
|
+
* `idempotent: true`. A withdrawn or expired request (a re-attestation voids
|
|
1674
|
+
* pending requests as `policy-drift`) is asked again under the same registration.
|
|
1675
|
+
* The same key with DIFFERENT bytes refuses: `duplicate-request` while a request
|
|
1676
|
+
* is live, `payload-mismatch` otherwise, because a key is bound to the bytes it
|
|
1677
|
+
* was first registered with and new bytes need a new key.
|
|
1678
|
+
*/
|
|
1679
|
+
export declare function propose(logPath: string, input: ProposeInput, actor: string, options?: GateOptions, attempt?: number, appendedEarlier?: boolean): ProposeResult;
|
|
1680
|
+
export type StartProposedResult = {
|
|
1681
|
+
ok: true;
|
|
1682
|
+
task: string;
|
|
1683
|
+
actionKey: string;
|
|
1684
|
+
cls: string;
|
|
1685
|
+
/** `grant` when a human's harness grant was spent; `policy` when the policy authorized it. */
|
|
1686
|
+
authorization: "grant" | "policy";
|
|
1687
|
+
record: EventRecord;
|
|
1688
|
+
} | GateRefusal;
|
|
1689
|
+
/**
|
|
1690
|
+
* `approval start <task> --action <key> --payload-json <json>` (APRV-445): record
|
|
1691
|
+
* the `execution.started` for an action its requester is about to carry out
|
|
1692
|
+
* itself, outside any harness hook.
|
|
1693
|
+
*
|
|
1694
|
+
* Until this verb an agent holding only the serve agent credential could ask and
|
|
1695
|
+
* wait, and could not say that it then acted: `approval run` spawns argv on the
|
|
1696
|
+
* gate's machine, and the harness hook writes starts only for tool calls it
|
|
1697
|
+
* classified. A proposal the policy authorizes outright (`autonomous`,
|
|
1698
|
+
* `supervised`) therefore left no record at all, and one a human granted left a
|
|
1699
|
+
* grant nobody spent, so a second publication of the same key was indistinguishable
|
|
1700
|
+
* from the first.
|
|
1701
|
+
*
|
|
1702
|
+
* It adds no authority. It writes through the two existing spenders and through
|
|
1703
|
+
* nothing else: {@link consumeHarnessGrant} when the key carries a request (it
|
|
1704
|
+
* refuses unless that request is granted, unexpired, harness-executed and
|
|
1705
|
+
* unspent, under the policy it was asked under), and {@link startHarnessExecution}
|
|
1706
|
+
* when it carries none (it refuses a `manual` or `human-only` class, an escalated
|
|
1707
|
+
* loop and a spent key, and charges the budgets). Both pin the single-use rule:
|
|
1708
|
+
* a second start of one key is `already-executed`.
|
|
1709
|
+
*
|
|
1710
|
+
* Two checks of its own, both before either spender runs. REQUESTER-ONLY: the
|
|
1711
|
+
* actor must be the one that registered the task (`not-requester` otherwise), as
|
|
1712
|
+
* `withdraw` is, so an agent cannot spend or exhaust a key someone else declared.
|
|
1713
|
+
* And the bytes: the caller presents the payload it is about to act on, and its
|
|
1714
|
+
* hash must equal the registered `payload_hash` (`payload-mismatch` otherwise),
|
|
1715
|
+
* so the thing done is the thing that was approved, which the grant path checks
|
|
1716
|
+
* again against the grant's own binding.
|
|
1717
|
+
*/
|
|
1718
|
+
export declare function startProposed(logPath: string, task: string, actionKey: string, payload: unknown, actor: string, options?: GateOptions): StartProposedResult;
|
|
1719
|
+
/**
|
|
1720
|
+
* A request's state as a waiter must read it (APRV-445 refutation, B1).
|
|
1721
|
+
*
|
|
1722
|
+
* `requestState` answers what the log RECORDED. A waiter needs what the record
|
|
1723
|
+
* still AUTHORIZES, and the two part in three ways the gate already enforces
|
|
1724
|
+
* at the spend and the decision but no reader was told about:
|
|
1725
|
+
*
|
|
1726
|
+
* - a GRANT whose request has lapsed its window is `expired`: there is no
|
|
1727
|
+
* separate grant TTL (`grantLapsed`), so a tap at hour 71 of a 72 h window
|
|
1728
|
+
* authorizes nothing at hour 73;
|
|
1729
|
+
* - a PENDING request pinned to a policy hash that is no longer the attested
|
|
1730
|
+
* one is `void` for every task, because the decision refuses `policy-drift`;
|
|
1731
|
+
* a GRANT is `void` on the same condition only where its spend enforces the
|
|
1732
|
+
* same refusal: a harness grant (`execution: "harness"`), which includes
|
|
1733
|
+
* every proposal. A token grant spent through `approval run` stays `granted`;
|
|
1734
|
+
* - a request the runtime withdrew for `policy-drift` (APRV-235) is `void` too,
|
|
1735
|
+
* for the same reason, rather than an ordinary requester `withdrawn`;
|
|
1736
|
+
* - a key whose execution started is `executed`.
|
|
1737
|
+
*
|
|
1738
|
+
* `void` and `expired` both mean "ask again": the question as asked can no
|
|
1739
|
+
* longer be answered usefully. Everything else is `requestState` verbatim. An
|
|
1740
|
+
* unattested policy (`attestedSha256` null) voids nothing, because that state is
|
|
1741
|
+
* the human's to repair and may end in the same bytes.
|
|
1742
|
+
*/
|
|
1743
|
+
export type RequestStanding = RequestState | "void" | "executed";
|
|
1744
|
+
export declare function requestStanding(records: EventRecord[], actionKey: string, ts: string, ttlMs: number | null, attestedSha256: string | null): RequestStanding;
|
|
1745
|
+
/**
|
|
1746
|
+
* The SHA-256 the live policy bytes are attested under, or `null` when they are
|
|
1747
|
+
* not attested (unread, unattested, or changed since). One read of the file
|
|
1748
|
+
* through the same seam every gate operation uses.
|
|
1749
|
+
*/
|
|
1750
|
+
export declare function attestedPolicySha256(records: EventRecord[], options?: GateOptions): string | null;
|