approval-md 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -914
- package/SPEC.md +101 -13
- package/cli.js +134 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +72 -0
- package/dist/src/channels/contract.js +161 -1
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +25 -0
- package/dist/src/channels/tagging.js +63 -6
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-minimal.d.ts +251 -0
- package/dist/src/channels/telegram-minimal.js +618 -0
- package/dist/src/channels/telegram-minimal.js.map +1 -0
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +725 -20
- package/dist/src/channels/telegram.js +1332 -123
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +358 -5
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.js +43 -6
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +1 -1
- package/dist/src/cli/audit-card.js +40 -1
- package/dist/src/cli/audit-card.js.map +1 -1
- package/dist/src/cli/audit.d.ts +7 -5
- package/dist/src/cli/audit.js +22 -10
- package/dist/src/cli/audit.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
- package/dist/src/cli/channel-telegram-webhook.js +716 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +648 -56
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +1 -1
- package/dist/src/cli/channel.js +5 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/daemon.js +37 -5
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +856 -111
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +29 -1
- package/dist/src/cli/execute.js +387 -21
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +10 -1
- package/dist/src/cli/help.js +283 -54
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +485 -6
- package/dist/src/cli/hook.js +2370 -70
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/log-unlock.d.ts +23 -0
- package/dist/src/cli/log-unlock.js +109 -0
- package/dist/src/cli/log-unlock.js.map +1 -0
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +72 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- package/dist/src/cli/scaffold.js +6 -0
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/up.js +26 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +338 -39
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +20 -3
- package/dist/src/core/attest.js +25 -0
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +153 -12
- package/dist/src/core/audit.js +415 -20
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -0
- package/dist/src/core/command-class.d.ts +51 -0
- package/dist/src/core/command-class.js +766 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/gate.d.ts +364 -6
- package/dist/src/core/gate.js +802 -27
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +179 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/log-lock.d.ts +435 -0
- package/dist/src/core/log-lock.js +1093 -0
- package/dist/src/core/log-lock.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +130 -22
- package/dist/src/core/log.js +473 -50
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +7 -1
- package/dist/src/core/policy-explain.js +16 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +24 -0
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +38 -0
- package/dist/src/core/policy-match.js +35 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +40 -0
- package/dist/src/core/policy-proposal.js +37 -16
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +186 -1
- package/dist/src/core/prompt-layout.js +345 -3
- package/dist/src/core/prompt-layout.js.map +1 -1
- package/dist/src/core/read-scope.d.ts +10 -0
- package/dist/src/core/read-scope.js +4 -0
- package/dist/src/core/read-scope.js.map +1 -1
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sender-identity.d.ts +59 -11
- package/dist/src/core/sender-identity.js +79 -11
- package/dist/src/core/sender-identity.js.map +1 -1
- package/dist/src/core/state.d.ts +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/validate.d.ts +20 -6
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +298 -7
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +22 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +166 -0
- package/dist/src/serve/archive.js +363 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +768 -0
- package/docs/cli-reference.md +2139 -67
- package/package.json +4 -2
- package/schema/event.schema.json +413 -30
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
- package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
- package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
- package/schema/fixtures/event/valid/audit-sampled.json +2 -1
- package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +16 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
- package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
- package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
- package/schema/policy.schema.json +102 -6
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The holder of `<log>.lock`, and the one way a lock left by a writer that died
|
|
3
|
+
* holding it is taken back (APRV-479).
|
|
4
|
+
*
|
|
5
|
+
* ## The defect this exists for
|
|
6
|
+
*
|
|
7
|
+
* `core/log.ts` serializes every append with a lockfile created `wx` and never
|
|
8
|
+
* stolen, because silently breaking somebody else's lock is how two writers
|
|
9
|
+
* come to share a `seq`. A writer that dies while it holds the lock never
|
|
10
|
+
* removes it: a harness hook killed by SIGTERM with no listener registered (the
|
|
11
|
+
* default disposition), any verb killed by SIGKILL, a Hermes gateway restart in
|
|
12
|
+
* the middle of an append. Every later writer, the daemon included, then waits
|
|
13
|
+
* its two seconds and refuses `lock-timeout`, and the tenant's gate stays
|
|
14
|
+
* wedged until a human removes the file.
|
|
15
|
+
*
|
|
16
|
+
* ## The rule every decision here follows
|
|
17
|
+
*
|
|
18
|
+
* A wrong reclaim (two writers under one lock, a forked chain) is far worse than
|
|
19
|
+
* a missed one (a wedge a human clears with one command). So a holder is LIVE
|
|
20
|
+
* unless this process can prove it gone, and a lock is taken back only by a
|
|
21
|
+
* writer that has already waited out its whole lock timeout.
|
|
22
|
+
*
|
|
23
|
+
* ## What this module does
|
|
24
|
+
*
|
|
25
|
+
* 1. **The lockfile names its holder.** {@link createLockFile} writes one JSON
|
|
26
|
+
* line into the file it creates: pid, host, boot, on Linux the pid and time
|
|
27
|
+
* namespaces and the process start time, when the lock was taken, which kind
|
|
28
|
+
* of holder (`append` | `hold`), and a nonce. An older writer's lockfile is
|
|
29
|
+
* empty.
|
|
30
|
+
* 2. **Only a regular file that parses strictly is judged.** Every read opens
|
|
31
|
+
* without blocking and without following links, and reads only a regular
|
|
32
|
+
* file of at most 4 KiB. Anything else at the lock's path (a FIFO, a link, a
|
|
33
|
+
* directory, an oversized or malformed file, a record of another version) is
|
|
34
|
+
* not this writer's to judge: it is LIVE.
|
|
35
|
+
* 3. **The judgement ({@link judgeHolder}).** Gone only when the holder is in
|
|
36
|
+
* this process's own pid namespace and boot, and provably not running:
|
|
37
|
+
* - Linux: `/proc` is this namespace's (`/proc/self` names this pid), the
|
|
38
|
+
* record's boot id and pid-namespace inode equal this process's, and then
|
|
39
|
+
* `kill(pid, 0)` answers ESRCH with `/proc/<pid>` absent or a zombie, or
|
|
40
|
+
* `/proc/<pid>/stat` names a process whose start time (read in the same
|
|
41
|
+
* time namespace) is not the holder's;
|
|
42
|
+
* - elsewhere (macOS): the same hostname, boot readings within
|
|
43
|
+
* {@link BOOT_SLACK_S} seconds, and `kill(pid, 0)` answering ESRCH.
|
|
44
|
+
*
|
|
45
|
+
* Everything else is LIVE: EPERM, another namespace or boot or host, a
|
|
46
|
+
* `/proc` entry that cannot be read.
|
|
47
|
+
* 4. **A lockfile with no holder record** (empty: an older version's, or a
|
|
48
|
+
* writer killed between the create and the write) is gone only once its
|
|
49
|
+
* mtime is {@link LEGACY_LOCK_RECLAIM_AGE_MS} old AND this writer watched
|
|
50
|
+
* that very file (inode, mtime, size) unchanged through its own wait.
|
|
51
|
+
* 5. **The reclaim ({@link reclaimStaleLock}) is two atomic steps, and nothing
|
|
52
|
+
* else is written.** The taker's own complete lockfile is written first,
|
|
53
|
+
* under `<lock>.take.<pid>.<nonce>`, so whoever finds a claim beside the
|
|
54
|
+
* lock also finds its taker and can judge it (`approval log unlock` refuses
|
|
55
|
+
* while one is seen running). Then the claim: `link(2)` of the lock's path to a name
|
|
56
|
+
* derived from the judged record, `<lock>.stale.<pid>.<created ms>`, which
|
|
57
|
+
* fails EEXIST for every other reclaimer of the same lockfile, and which is
|
|
58
|
+
* then read back to prove it names the very file that was judged (inode,
|
|
59
|
+
* mtime, bytes). Then the take: this writer's own lockfile is `rename(2)`d
|
|
60
|
+
* over the lock's path, once the claim and the lock's path are read again
|
|
61
|
+
* and are still the judged file. The lock's
|
|
62
|
+
* path is never empty, so no ordinary `wx` create slips in, and between the
|
|
63
|
+
* claim and the take nothing in the protocol can change it: the holder is
|
|
64
|
+
* dead, every other writer's create fails EEXIST, and every other
|
|
65
|
+
* reclaimer's claim fails EEXIST. (A bare `rename` of the lockfile aside
|
|
66
|
+
* cannot be made safe: a reclaimer that judged the dead lock and stalled
|
|
67
|
+
* would later move whatever live lock had replaced it.)
|
|
68
|
+
* 6. **The reclaim is in the log first.** The reclaimer appends
|
|
69
|
+
* `audit.lock_reclaimed` as the first record under the lock it took
|
|
70
|
+
* (`core/log.ts`), carrying the strictly parsed pid, op and `created` and
|
|
71
|
+
* nothing else from the file, and only then unlinks the stale name.
|
|
72
|
+
* 7. **A termination signal does not leave a lock behind.**
|
|
73
|
+
* {@link guardTerminationWhileLocked} is installed before a lockfile is
|
|
74
|
+
* created, and before a reclaim's claim, and held until the lock is removed: for SIGTERM, SIGINT and SIGHUP with
|
|
75
|
+
* no listener, a listener that re-raises the signal with its default
|
|
76
|
+
* disposition once the lock is released, so the process still dies of it.
|
|
77
|
+
* SIGKILL cannot be caught, which is what 1 to 6 are for.
|
|
78
|
+
*
|
|
79
|
+
* Never reclaimed automatically, whatever the holder: a lock beside a
|
|
80
|
+
* `log sync` snapshot (a sync killed part way may have left the working log at
|
|
81
|
+
* its committed bytes), and a lock beside a log that does not exist. A lock from
|
|
82
|
+
* another container or boot is never reclaimed automatically either; the
|
|
83
|
+
* refusal names the pid and the human verb, `approval log unlock --pid <n>`
|
|
84
|
+
* ({@link takeLockForUnlock}).
|
|
85
|
+
*/
|
|
86
|
+
/** The holder record's format version. A reader treats any other as not its to judge. */
|
|
87
|
+
export declare const LOCK_HOLDER_VERSION = 1;
|
|
88
|
+
/**
|
|
89
|
+
* How old an empty lockfile (no holder record) must be before it is reclaimed.
|
|
90
|
+
* An append holds the lock for milliseconds, and the longest holder in this
|
|
91
|
+
* runtime (`approval log sync`) for seconds.
|
|
92
|
+
*/
|
|
93
|
+
export declare const LEGACY_LOCK_RECLAIM_AGE_MS: number;
|
|
94
|
+
/**
|
|
95
|
+
* How long a writer must have watched an empty lockfile unchanged (the same
|
|
96
|
+
* inode, mtime and size since the first refusal of its wait) before it may take
|
|
97
|
+
* it as `legacy-aged`. A writer of this version fills its record within
|
|
98
|
+
* microseconds of its create, so an empty file that stays empty across a retry
|
|
99
|
+
* interval is not one; an mtime alone can read old through a stepped clock or
|
|
100
|
+
* a file server's lagging one (APRV-479 R3-8).
|
|
101
|
+
*/
|
|
102
|
+
export declare const LEGACY_LOCK_WATCH_MS = 20;
|
|
103
|
+
/**
|
|
104
|
+
* Off Linux, how far apart two boot readings (`now - uptime`, in seconds) may
|
|
105
|
+
* be and still count as one boot. A wall-clock step moves the reading, so a
|
|
106
|
+
* larger gap is "another boot, or a stepped clock": live, never gone.
|
|
107
|
+
*/
|
|
108
|
+
export declare const BOOT_SLACK_S = 60;
|
|
109
|
+
/**
|
|
110
|
+
* `created` exactly as `Date#toISOString` writes it AND a real instant: the
|
|
111
|
+
* string must survive a round trip, which `2026-02-30T00:00:00.000Z` (a shape
|
|
112
|
+
* match that V8 rolls over to March 2nd) does not.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isCanonicalTimestamp(value: string): boolean;
|
|
115
|
+
/** `append`: one record. `hold`: a whole operation under `withAppendLock`. */
|
|
116
|
+
export type LockOp = "append" | "hold";
|
|
117
|
+
/** Who a process is, as far as a liveness judgement can use it. */
|
|
118
|
+
export interface HolderIdentity {
|
|
119
|
+
pid: number;
|
|
120
|
+
host: string;
|
|
121
|
+
/** Linux: `/proc/sys/kernel/random/boot_id`. Elsewhere: `~<boot epoch seconds>`. */
|
|
122
|
+
boot: string;
|
|
123
|
+
/** Linux only: `/proc/self/ns/pid`. */
|
|
124
|
+
pidns?: string;
|
|
125
|
+
/** Linux only: field 22 of `/proc/self/stat` (clock ticks after boot, in this time namespace). */
|
|
126
|
+
start?: string;
|
|
127
|
+
/** Linux only, where the kernel has time namespaces: `/proc/self/ns/time`. */
|
|
128
|
+
timens?: string;
|
|
129
|
+
}
|
|
130
|
+
/** What a lockfile written by this version holds, one JSON line. */
|
|
131
|
+
export interface LockHolder extends HolderIdentity {
|
|
132
|
+
v: typeof LOCK_HOLDER_VERSION;
|
|
133
|
+
/** When the lock was taken, as `Date#toISOString` writes it. */
|
|
134
|
+
created: string;
|
|
135
|
+
op: LockOp;
|
|
136
|
+
/** Random, so two lockfiles are never byte-identical. */
|
|
137
|
+
nonce: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Why a lock was taken over: the `audit.lock_reclaimed` payload's closed
|
|
141
|
+
* `reason` set. `holder-dead` and `legacy-aged` are a writer's proof;
|
|
142
|
+
* `operator-cleared` is a person's assertion (`approval log unlock`), never a
|
|
143
|
+
* proof, and only a `human:` actor carries it.
|
|
144
|
+
*/
|
|
145
|
+
export type LockReclaimReason = "holder-dead" | "legacy-aged" | "operator-cleared";
|
|
146
|
+
/**
|
|
147
|
+
* The judgement on one holder record. `running` (on a live verdict): this
|
|
148
|
+
* process saw the holder itself running (Linux: its start time; elsewhere: its
|
|
149
|
+
* pid, on this host, in this boot). A live verdict without it is a holder this
|
|
150
|
+
* process cannot check, which a human may clear with `approval log unlock`.
|
|
151
|
+
*/
|
|
152
|
+
export type HolderVerdict = {
|
|
153
|
+
state: "gone";
|
|
154
|
+
why: string;
|
|
155
|
+
} | {
|
|
156
|
+
state: "live";
|
|
157
|
+
why: string;
|
|
158
|
+
running: boolean;
|
|
159
|
+
};
|
|
160
|
+
/** This process, as a holder record describes it. */
|
|
161
|
+
export interface SelfIdentity {
|
|
162
|
+
pid: number;
|
|
163
|
+
host: string;
|
|
164
|
+
boot: string | undefined;
|
|
165
|
+
pidns: string | undefined;
|
|
166
|
+
start: string | undefined;
|
|
167
|
+
timens?: string | undefined;
|
|
168
|
+
linux: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Linux: whether `/proc` is this process's own pid namespace's procfs
|
|
171
|
+
* (`/proc/self` names `process.pid`). `false` means no pid read from it can be
|
|
172
|
+
* trusted, and every holder is live. Absent is taken as `true` (tests).
|
|
173
|
+
*/
|
|
174
|
+
procIsOwn?: boolean;
|
|
175
|
+
}
|
|
176
|
+
/** What `/proc/<pid>/stat` said. */
|
|
177
|
+
export type ProcStatRead = {
|
|
178
|
+
kind: "stat";
|
|
179
|
+
state: string;
|
|
180
|
+
start: string;
|
|
181
|
+
}
|
|
182
|
+
/** ENOENT or ESRCH: no entry this process can see. */
|
|
183
|
+
| {
|
|
184
|
+
kind: "absent";
|
|
185
|
+
}
|
|
186
|
+
/** Anything else (EACCES, EPERM, an unparseable line): nothing can be concluded. */
|
|
187
|
+
| {
|
|
188
|
+
kind: "unreadable";
|
|
189
|
+
why: string;
|
|
190
|
+
};
|
|
191
|
+
/** `kill(pid, 0)`: `ok` (exists), `ESRCH` (no such process), `EPERM` (exists, another user's), `other`. */
|
|
192
|
+
export type SignalZero = "ok" | "ESRCH" | "EPERM" | "other";
|
|
193
|
+
/** What a liveness judgement may ask of the operating system. Injected by tests. */
|
|
194
|
+
export interface LivenessProbe {
|
|
195
|
+
/** Linux: `/proc/<pid>/stat`, as read by this process. */
|
|
196
|
+
linuxStat(pid: number): ProcStatRead;
|
|
197
|
+
signalZero(pid: number): SignalZero;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parse `/proc/<pid>/stat`: the command name is parenthesized and may contain
|
|
201
|
+
* spaces and parentheses, so the fields are counted from the LAST `)`. After it,
|
|
202
|
+
* index 0 is field 3 (state) and index 19 is field 22 (starttime).
|
|
203
|
+
*/
|
|
204
|
+
export declare function parseProcStat(text: string): {
|
|
205
|
+
state: string;
|
|
206
|
+
start: string;
|
|
207
|
+
} | null;
|
|
208
|
+
/** The real probe: `/proc` and `kill(pid, 0)`. */
|
|
209
|
+
export declare const NODE_LIVENESS_PROBE: LivenessProbe;
|
|
210
|
+
/**
|
|
211
|
+
* Is `/proc` this process's own pid namespace's procfs? `/proc/self` resolves
|
|
212
|
+
* to the reader's pid as THAT procfs numbers it, so it names `process.pid` only
|
|
213
|
+
* when the procfs belongs to the namespace `process.pid` was issued in, or when
|
|
214
|
+
* the pid happens to be the same number in both (APRV-479 R3-9). So the
|
|
215
|
+
* `NSpid:` line of `/proc/self/status` must also hold exactly one field, this
|
|
216
|
+
* pid: it lists the pid in every namespace from the procfs's own down to the
|
|
217
|
+
* reader's, and one field means they are the same namespace. A kernel without
|
|
218
|
+
* the line (before 4.1) proves nothing, and every holder is then live.
|
|
219
|
+
*/
|
|
220
|
+
export declare function procIsOwnNamespace(read?: (path: string) => string, pid?: number, status?: (path: string) => string): boolean;
|
|
221
|
+
/** This process's identity, read once: none of it changes while the process lives. */
|
|
222
|
+
export declare function selfIdentity(): SelfIdentity;
|
|
223
|
+
/** The record this process writes into a lockfile it creates now. */
|
|
224
|
+
export declare function holderRecord(op: LockOp, now?: Date): LockHolder;
|
|
225
|
+
/**
|
|
226
|
+
* What a lockfile's bytes say: a strictly parsed v1 record, `legacy` (empty or
|
|
227
|
+
* whitespace only: no holder record at all), or `unknown` (anything else: a
|
|
228
|
+
* malformed record, another version, text that is not JSON), which is never
|
|
229
|
+
* judged.
|
|
230
|
+
*/
|
|
231
|
+
export declare function parseHolder(bytes: Buffer | string): {
|
|
232
|
+
kind: "v1";
|
|
233
|
+
holder: LockHolder;
|
|
234
|
+
} | {
|
|
235
|
+
kind: "legacy";
|
|
236
|
+
} | {
|
|
237
|
+
kind: "unknown";
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Is the process that wrote `holder` provably gone? Pure: the identity of the
|
|
241
|
+
* reader and the operating system's answers are parameters. "Live" means "not
|
|
242
|
+
* provably gone". The holder's boot and pid namespace are established before
|
|
243
|
+
* any pid is looked up, because a pid means nothing outside the namespace it
|
|
244
|
+
* was issued in.
|
|
245
|
+
*/
|
|
246
|
+
export declare function judgeHolder(holder: HolderIdentity, self: SelfIdentity, probe: LivenessProbe): HolderVerdict;
|
|
247
|
+
/**
|
|
248
|
+
* Replace the liveness judgement (tests only: the pin that a live holder's
|
|
249
|
+
* lock survives only because of the check). `null` restores the real one.
|
|
250
|
+
*/
|
|
251
|
+
export declare function setLockLivenessForTests(judge: ((holder: HolderIdentity) => HolderVerdict) | null): void;
|
|
252
|
+
/** Where a test may act inside the lock's protocol. Nothing in `src/` sets one. */
|
|
253
|
+
export type LockStep = "after-open" | "judged" | "claimed" | "before-take";
|
|
254
|
+
/** Run `seam` at each named step (tests only: a stall, a signal, a planted file). `null` removes it. */
|
|
255
|
+
export declare function setLockSeamForTests(seam: ((step: LockStep) => void) | null): void;
|
|
256
|
+
/** The lock this process created or took: enough to release only that one. */
|
|
257
|
+
export interface OwnLock {
|
|
258
|
+
path: string;
|
|
259
|
+
ino: bigint;
|
|
260
|
+
bytes: Buffer;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create `path` with `wx` and write this process's holder record into it.
|
|
264
|
+
* Throws what `openSync` throws, so `EEXIST` means another writer holds it. A
|
|
265
|
+
* record that cannot be written leaves an empty lockfile, which is still a lock.
|
|
266
|
+
* The caller installs the signal guard BEFORE calling this.
|
|
267
|
+
*/
|
|
268
|
+
export declare function createLockFile(path: string, op: LockOp): OwnLock;
|
|
269
|
+
/**
|
|
270
|
+
* Remove the lock this process holds, and only it: anything else at the path
|
|
271
|
+
* (another inode, other bytes) is left where it is.
|
|
272
|
+
*/
|
|
273
|
+
export declare function releaseLockFile(own: OwnLock): void;
|
|
274
|
+
/** What an `audit.lock_reclaimed` record says, and the stale name to unlink once it is in the log. */
|
|
275
|
+
export interface ReclaimNote {
|
|
276
|
+
/** The lockfile's base name. */
|
|
277
|
+
lockfile: string;
|
|
278
|
+
reason: LockReclaimReason;
|
|
279
|
+
/**
|
|
280
|
+
* From a v1 record only: the strictly parsed pid, op and `created`. `created`
|
|
281
|
+
* is `null` when it is not plausible ({@link plausibleCreated}).
|
|
282
|
+
*/
|
|
283
|
+
holder?: {
|
|
284
|
+
pid: number;
|
|
285
|
+
op: LockOp;
|
|
286
|
+
created: string | null;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* How long the lock had been held, from `created` (or an empty lockfile's
|
|
290
|
+
* mtime), by this process's clock. Absent exactly when `created` is `null`.
|
|
291
|
+
*/
|
|
292
|
+
age_ms?: number;
|
|
293
|
+
/** The claim name, removed after the record lands. Never recorded. */
|
|
294
|
+
stale: string;
|
|
295
|
+
/** One line for a person. Never recorded. */
|
|
296
|
+
why: string;
|
|
297
|
+
}
|
|
298
|
+
/** How far before the log's last record a holder's `created` may lie and still be recorded. */
|
|
299
|
+
export declare const CREATED_BEFORE_LAST_RECORD_MS: number;
|
|
300
|
+
/** How far past this process's clock a holder's `created` may lie and still be recorded. */
|
|
301
|
+
export declare const CREATED_AFTER_NOW_MS: number;
|
|
302
|
+
/**
|
|
303
|
+
* The record of a reclaim carries a holder's `created` (and the age derived
|
|
304
|
+
* from it) only when it is plausible: no earlier than the log's last record's
|
|
305
|
+
* `ts` minus {@link CREATED_BEFORE_LAST_RECORD_MS}, and no later than now plus
|
|
306
|
+
* {@link CREATED_AFTER_NOW_MS}. Anything else is a value the file chose (any
|
|
307
|
+
* canonical instant parses, year 0000 and 9999 included), so the record says
|
|
308
|
+
* `created: null` and carries no age. With no last record to measure from, no
|
|
309
|
+
* `created` is plausible. (APRV-479 R3-7, the stricter rule.)
|
|
310
|
+
*/
|
|
311
|
+
export declare function plausibleCreated(note: ReclaimNote, lastTs: number | null, now: number): ReclaimNote;
|
|
312
|
+
/** What the writer knows about the log before it claims: whether it could record a reclaim at all, and its last record's time. */
|
|
313
|
+
export type RecordContext = {
|
|
314
|
+
ok: true;
|
|
315
|
+
lastTs: number | null;
|
|
316
|
+
} | {
|
|
317
|
+
ok: false;
|
|
318
|
+
why: string;
|
|
319
|
+
};
|
|
320
|
+
/** What {@link reclaimStaleLock} found and did. */
|
|
321
|
+
export type ReclaimOutcome =
|
|
322
|
+
/** This process now holds the lock and must record `note` first. */
|
|
323
|
+
{
|
|
324
|
+
kind: "taken";
|
|
325
|
+
own: OwnLock;
|
|
326
|
+
releaseGuard: () => void;
|
|
327
|
+
note: ReclaimNote;
|
|
328
|
+
}
|
|
329
|
+
/** The lock moved, or another writer is reclaiming it: wait again. */
|
|
330
|
+
| {
|
|
331
|
+
kind: "retry";
|
|
332
|
+
why: string;
|
|
333
|
+
}
|
|
334
|
+
/** The lock stays where it is; `why` names its holder and the human command. */
|
|
335
|
+
| {
|
|
336
|
+
kind: "kept";
|
|
337
|
+
why: string;
|
|
338
|
+
};
|
|
339
|
+
/** The human verb, as a refusal names it. */
|
|
340
|
+
export declare function unlockCommand(pid: number | null): string;
|
|
341
|
+
/** A lockfile as a writer saw it at the first refusal of its wait (R3-8). */
|
|
342
|
+
export interface LockObservation {
|
|
343
|
+
ino: bigint;
|
|
344
|
+
mtimeNs: bigint;
|
|
345
|
+
size: bigint;
|
|
346
|
+
/** When it was seen, by this process's clock. */
|
|
347
|
+
at: number;
|
|
348
|
+
}
|
|
349
|
+
/** What is at `<logPath>.lock` now, without opening it: a regular file's identity, or `null`. */
|
|
350
|
+
export declare function observeLock(logPath: string, now?: number): LockObservation | null;
|
|
351
|
+
/** Options for {@link reclaimStaleLock}. */
|
|
352
|
+
export interface ReclaimOptions {
|
|
353
|
+
now?: number;
|
|
354
|
+
/** The writer's own write-boundary check of the record it would append: a reclaim it would refuse is not made. */
|
|
355
|
+
recordValid?: (note: ReclaimNote) => boolean;
|
|
356
|
+
/**
|
|
357
|
+
* The lockfile as this writer saw it at the first refusal of this wait
|
|
358
|
+
* ({@link observeLock}). An empty lockfile is taken only when it is that
|
|
359
|
+
* same file, watched for at least {@link LEGACY_LOCK_WATCH_MS}.
|
|
360
|
+
*/
|
|
361
|
+
watched?: LockObservation | null;
|
|
362
|
+
/**
|
|
363
|
+
* Can this writer append at all right now (its daemon stamp, the log's
|
|
364
|
+
* tail), and when was the log's last record? Asked before the claim, so a
|
|
365
|
+
* reclaim whose record would be refused is never made; the last record's
|
|
366
|
+
* time bounds the `created` the record may carry ({@link plausibleCreated}).
|
|
367
|
+
* Absent: appendable, with no last record to measure from.
|
|
368
|
+
*/
|
|
369
|
+
recordContext?: () => RecordContext;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Judge `<logPath>.lock` once and, when its holder is provably gone, take it:
|
|
373
|
+
* claim, prove the claim, take. Called by a writer only after its whole lock
|
|
374
|
+
* timeout has passed. Nothing is written but the stale name (the claim) and
|
|
375
|
+
* this writer's own lockfile.
|
|
376
|
+
*/
|
|
377
|
+
export declare function reclaimStaleLock(logPath: string, op: LockOp, options?: ReclaimOptions): ReclaimOutcome;
|
|
378
|
+
/** The stale name of a reclaim whose record is now in the log. */
|
|
379
|
+
export declare function finishReclaim(note: ReclaimNote): void;
|
|
380
|
+
/** What {@link takeLockForUnlock} found and did. */
|
|
381
|
+
export type UnlockOutcome = {
|
|
382
|
+
kind: "taken";
|
|
383
|
+
own: OwnLock;
|
|
384
|
+
releaseGuard: () => void;
|
|
385
|
+
note: ReclaimNote;
|
|
386
|
+
} | {
|
|
387
|
+
kind: "none";
|
|
388
|
+
} | {
|
|
389
|
+
kind: "refused";
|
|
390
|
+
why: string;
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* `approval log unlock --pid <n|none>`: a human's take of a lock no writer
|
|
394
|
+
* reclaims (another container, another boot, a pid that cannot be checked, a
|
|
395
|
+
* claim whose reclaimer died, a planted stale name). The human asserts that no
|
|
396
|
+
* writer is running; this refuses only what it can see is wrong: a `--pid`
|
|
397
|
+
* that is not the record's, a holder it sees running, a lock beside a sync
|
|
398
|
+
* snapshot, and anything at the lock's path that is not a lockfile (which `rm`
|
|
399
|
+
* removes). It takes the lock with the same claim and take as a writer's
|
|
400
|
+
* reclaim, so the record of it (reason `operator-cleared`) is the first under
|
|
401
|
+
* the lock it takes.
|
|
402
|
+
*/
|
|
403
|
+
export declare function takeLockForUnlock(logPath: string, expected: number | null, lastTs?: number | null, now?: number): UnlockOutcome;
|
|
404
|
+
/**
|
|
405
|
+
* Hold SIGTERM, SIGINT and SIGHUP from just before a lockfile is created until
|
|
406
|
+
* just after it is removed, for each signal nobody listens for (the default
|
|
407
|
+
* disposition would kill the process with the lockfile in place). Returns the
|
|
408
|
+
* release.
|
|
409
|
+
*
|
|
410
|
+
* Node shows a caught signal to JavaScript only when the event loop turns, and
|
|
411
|
+
* removing the last listener before that turn drops it. So after the release:
|
|
412
|
+
* a verb that returns or awaits dies of the signal at its next turn;
|
|
413
|
+
* `approval run` awaits {@link yieldToTerminationSignals} before it spawns, so
|
|
414
|
+
* a stop request during its append ends it first; and a synchronous wait calls
|
|
415
|
+
* {@link settleTerminationGuards} before it blocks, so the wait runs under the
|
|
416
|
+
* default disposition, at the price of losing a signal that landed in the
|
|
417
|
+
* append's own milliseconds. A create that fails EEXIST releases and settles at
|
|
418
|
+
* once, so a lock wait runs unguarded (a signal there kills at once, holding
|
|
419
|
+
* nothing).
|
|
420
|
+
*
|
|
421
|
+
* Windows has no such signals to hold; there this is a no-op.
|
|
422
|
+
*/
|
|
423
|
+
export declare function guardTerminationWhileLocked(): () => void;
|
|
424
|
+
/**
|
|
425
|
+
* Synchronous code about to block (an `Atomics.wait` poll, a `spawnSync`) calls
|
|
426
|
+
* this first: when no lock is held, the guard is removed now, so the block is
|
|
427
|
+
* not run with termination signals held. No-op while a lock is held.
|
|
428
|
+
*/
|
|
429
|
+
export declare function settleTerminationGuards(): void;
|
|
430
|
+
/**
|
|
431
|
+
* Turn the event loop past a poll phase, so a termination signal a released
|
|
432
|
+
* lock's guard caught is dispatched now and the process dies of it here,
|
|
433
|
+
* before the caller starts whatever it was about to start.
|
|
434
|
+
*/
|
|
435
|
+
export declare function yieldToTerminationSignals(): Promise<void>;
|