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
package/dist/src/cli/execute.js
CHANGED
|
@@ -52,7 +52,7 @@ import { spawnSync } from "node:child_process";
|
|
|
52
52
|
import { readdirSync, rmSync } from "node:fs";
|
|
53
53
|
import { constants as osConstants } from "node:os";
|
|
54
54
|
import { dirname, isAbsolute, resolve as resolvePathSegments } from "node:path";
|
|
55
|
-
import { HUMAN_ACTOR_ENV, checkAttestation, resolveHumanActor } from "../core/attest.js";
|
|
55
|
+
import { HUMAN_ACTOR_ENV, attesterAt, checkAttestation, resolveHumanActor, } from "../core/attest.js";
|
|
56
56
|
import { RESOLVE_DANGLING_COMMAND, proveDanglingAdvances, } from "../core/advance-cycle.js";
|
|
57
57
|
import { openObligations } from "../core/audit.js";
|
|
58
58
|
import { evaluateBudgets } from "../core/budgets.js";
|
|
@@ -66,16 +66,19 @@ import { harnessLoopEscalation, harnessOutcomeCoverage, loopClearance } from "..
|
|
|
66
66
|
import { isPayloadHash, runPayloadHash } from "../core/payload.js";
|
|
67
67
|
import { payloadStoreCensus } from "../core/payload-census.js";
|
|
68
68
|
import { payloadStoreDirFor } from "../core/payload-store.js";
|
|
69
|
-
import {
|
|
69
|
+
import { boundScript, describeBoundScript } from "../core/run-payload.js";
|
|
70
|
+
import { attestedPolicySha256, requestStanding, taskRegistration, withdraw, } from "../core/gate.js";
|
|
70
71
|
import { openGateWindow } from "../core/gate-window.js";
|
|
71
72
|
import { keyStoreDirFor } from "../core/seal.js";
|
|
72
73
|
import { readVerifiedRecords, requestState } from "../core/state.js";
|
|
73
74
|
import { deliveredToken } from "../core/token.js";
|
|
74
75
|
import { passphraseEnvFor } from "../core/vault.js";
|
|
76
|
+
import { resolveDaemonAllowlist, resolveDaemonId } from "../core/daemon-host.js";
|
|
77
|
+
import { DAEMON_ID_ENV } from "../core/daemon-identity.js";
|
|
75
78
|
import { loadPolicy, parseDuration, POLICY_FILENAMES } from "../core/policy-load.js";
|
|
76
79
|
import { verify } from "../core/verify.js";
|
|
77
80
|
import { boolFlag, parseFlags, stringFlag } from "./args.js";
|
|
78
|
-
import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
|
|
81
|
+
import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_VOID, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
|
|
79
82
|
import { repoRoot } from "./git-scope.js";
|
|
80
83
|
import { resolveReadRoots } from "./hook.js";
|
|
81
84
|
import { publishedState } from "./log-advance.js";
|
|
@@ -170,6 +173,10 @@ function emitRefusal(streams, json, refusal) {
|
|
|
170
173
|
error["seq"] = refusal.seq;
|
|
171
174
|
if (refusal.record !== undefined)
|
|
172
175
|
error["event_seq"] = refusal.record.seq;
|
|
176
|
+
// APRV-448: the write-boundary code under `append-failed`, as `approval
|
|
177
|
+
// gate` and the gate verbs report it.
|
|
178
|
+
if (refusal.append !== undefined)
|
|
179
|
+
error["append"] = refusal.append.code;
|
|
173
180
|
streams.err(`${JSON.stringify({ ok: false, error })}\n`);
|
|
174
181
|
}
|
|
175
182
|
else {
|
|
@@ -371,12 +378,23 @@ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
|
|
|
371
378
|
if (hashFlag !== null && !isPayloadHash(hashFlag)) {
|
|
372
379
|
return usageError(streams, json, `--payload-hash expects 64 lowercase hex characters (SHA-256 over the RFC 8785 canonical serialization of the payload), got ${JSON.stringify(hashFlag)}`, RUN_HELP);
|
|
373
380
|
}
|
|
381
|
+
// APRV-401 put the SCRIPT inside that computation: an interpreter followed by
|
|
382
|
+
// a path operand, or a path at argv[0], carries that file's digest, so a
|
|
383
|
+
// script rewritten between the declaration and this moment hashes differently
|
|
384
|
+
// and is refused here rather than executed under a grant nobody gave it. The
|
|
385
|
+
// refusal names what was bound, because "the hash differs" leaves an agent to
|
|
386
|
+
// guess whether the argv moved or the file under it did, and those have
|
|
387
|
+
// different repairs.
|
|
374
388
|
const payloadHash = runPayloadHash(childArgv, cwd);
|
|
375
389
|
if (hashFlag !== null && hashFlag !== payloadHash) {
|
|
390
|
+
const script = boundScript(childArgv, cwd);
|
|
391
|
+
const binding = script === null
|
|
392
|
+
? `The argv names no readable script, so the binding is the argv and the cwd alone (amended SPEC.md §6.2, APRV-401).`
|
|
393
|
+
: `The binding covers the script's bytes as well (amended SPEC.md §6.2, APRV-401): ${describeBoundScript(script)}, so a script edited since the declaration changes this value.`;
|
|
376
394
|
return emitRefusal(streams, json, {
|
|
377
395
|
ok: false,
|
|
378
396
|
code: "payload-mismatch",
|
|
379
|
-
message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. \`approval run\` recomputes the binding from the argv
|
|
397
|
+
message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. ${binding} \`approval run\` recomputes the binding from the argv, the cwd and those bytes and never accepts a caller's substitute for it (amended SPEC.md §10.4, APRV-140); the flag states what you believe you are running, and this is a refusal to run something else. Nothing was appended.`,
|
|
380
398
|
});
|
|
381
399
|
}
|
|
382
400
|
// APRV-205. The child's environment is built BEFORE `execution.started`,
|
|
@@ -486,6 +504,8 @@ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
|
|
|
486
504
|
// ===========================================================================
|
|
487
505
|
// approval wait
|
|
488
506
|
// ===========================================================================
|
|
507
|
+
/** `wait --timeout 0` (and `0s`, `0ms`, …): read once, never sleep (APRV-445). */
|
|
508
|
+
const ZERO_DURATION = /^0(?:ms|s|m|h|d|w)?$/u;
|
|
489
509
|
/** Synchronous sleep with no dependency and no busy-spin. */
|
|
490
510
|
function sleepSync(ms) {
|
|
491
511
|
if (ms <= 0)
|
|
@@ -499,10 +519,28 @@ function sleepSync(ms) {
|
|
|
499
519
|
function stateRole(state) {
|
|
500
520
|
if (state === "granted")
|
|
501
521
|
return "ok";
|
|
502
|
-
if (state === "expired" ||
|
|
522
|
+
if (state === "expired" ||
|
|
523
|
+
state === "requested" ||
|
|
524
|
+
state === "withdrawn" ||
|
|
525
|
+
state === NOTHING_TO_WAIT_FOR) {
|
|
503
526
|
return "warn";
|
|
527
|
+
}
|
|
504
528
|
return "fail";
|
|
505
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* `approval wait`'s status for a REGISTERED task that holds nothing a wait could
|
|
532
|
+
* resolve (APRV-428): no `approval.requested` at all (no action was declared,
|
|
533
|
+
* none reached the manual path, or none has been requested yet), or every
|
|
534
|
+
* granted action has already started executing, so its grant is spent.
|
|
535
|
+
*
|
|
536
|
+
* Exit 0, because the wait contract has always returned at once and at 0 for a
|
|
537
|
+
* task with no requests, and the supervised path tells an agent its
|
|
538
|
+
* `proceed: true` needs no wait at all. What changes is the WORD: before
|
|
539
|
+
* APRV-428 this was `granted`, and a caller that branched on `status` saw a
|
|
540
|
+
* grant nobody made. Nothing is minted either way, since `approval run` still
|
|
541
|
+
* needs a token on the manual path.
|
|
542
|
+
*/
|
|
543
|
+
const NOTHING_TO_WAIT_FOR = "nothing-to-wait-for";
|
|
506
544
|
/**
|
|
507
545
|
* `approval wait`'s human answer: the verdict, then one aligned row per action.
|
|
508
546
|
*
|
|
@@ -510,7 +548,11 @@ function stateRole(state) {
|
|
|
510
548
|
* (`plainLeft`) and the state beside it carries the colour instead.
|
|
511
549
|
*/
|
|
512
550
|
export function renderWaitHuman(task, status, actions, st = style()) {
|
|
513
|
-
const glyph = status === "granted"
|
|
551
|
+
const glyph = status === "granted"
|
|
552
|
+
? "ok"
|
|
553
|
+
: status === "expired" || status === "withdrawn" || status === NOTHING_TO_WAIT_FOR
|
|
554
|
+
? "skip"
|
|
555
|
+
: "fail";
|
|
514
556
|
const head = `${st.glyph(glyph)} ${st.key(task)} ${st.paint(stateRole(status), status)}`;
|
|
515
557
|
if (actions.length === 0)
|
|
516
558
|
return `${head}\n`;
|
|
@@ -521,6 +563,25 @@ export function renderWaitHuman(task, status, actions, st = style()) {
|
|
|
521
563
|
}));
|
|
522
564
|
return `${head}\n${st.table(rows, { indent: 2 })}\n`;
|
|
523
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* `approval wait`'s refusal of a task the log has never registered (APRV-428).
|
|
568
|
+
*
|
|
569
|
+
* The frozen `{ok:false,error:{code,message}}` shape on stderr, like every other
|
|
570
|
+
* refusal this verb prints, and exit 1: the same code `approval request` exits
|
|
571
|
+
* for the same `not-registered`, and the meaning wait's exit 1 already carries
|
|
572
|
+
* (not authorized, and waiting again will not change that). A separate writer
|
|
573
|
+
* because the verb's other refusals are the execute union's log-read codes,
|
|
574
|
+
* and `not-registered` is a member of the gate union only.
|
|
575
|
+
*/
|
|
576
|
+
function emitWaitRefusal(streams, json, refusal) {
|
|
577
|
+
if (json) {
|
|
578
|
+
streams.err(`${JSON.stringify({ ok: false, error: { code: refusal.code, message: refusal.message } })}\n`);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
streams.err(`${renderRefusal(style({ json }), refusal.code, refusal.message)}\n`);
|
|
582
|
+
}
|
|
583
|
+
return EXIT_INTEGRITY;
|
|
584
|
+
}
|
|
524
585
|
/** Every action key of `task` that ever carried an `approval.requested`. */
|
|
525
586
|
function requestedKeysOf(records, task) {
|
|
526
587
|
const keys = [];
|
|
@@ -560,9 +621,18 @@ export function commandWait(argv, streams, cwd) {
|
|
|
560
621
|
if (timeoutText === null) {
|
|
561
622
|
return usageError(streams, json, "missing --timeout <duration>", WAIT_HELP);
|
|
562
623
|
}
|
|
563
|
-
|
|
624
|
+
// APRV-445. `--timeout 0` is a STATUS READ: one verified read of the log, the
|
|
625
|
+
// current state of every request of the task, and no sleep at all. The
|
|
626
|
+
// duration grammar has no zero (a zero TTL or window in a policy would be a
|
|
627
|
+
// control that never runs), so the spelling is admitted here and only here.
|
|
628
|
+
// It is what a poller on `approval serve` uses: that server runs every verb
|
|
629
|
+
// and every hook call through one queue, and a `wait` that sleeps holds the
|
|
630
|
+
// whole tenant's hook traffic for as long as it sleeps. The result set and
|
|
631
|
+
// the exit codes are unchanged; an undecided request answers `timeout`
|
|
632
|
+
// (exit 6) at once, as a wait that ran out would.
|
|
633
|
+
const timeoutMs = ZERO_DURATION.test(timeoutText) ? 0 : parseDuration(timeoutText);
|
|
564
634
|
if (timeoutMs === null) {
|
|
565
|
-
return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
|
|
635
|
+
return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, or 0 to read the current state without waiting, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
|
|
566
636
|
}
|
|
567
637
|
const intervalText = stringFlag(flags, "--interval");
|
|
568
638
|
const intervalMs = intervalText === null ? DEFAULT_WAIT_INTERVAL_MS : parseDuration(intervalText);
|
|
@@ -576,6 +646,12 @@ export function commandWait(argv, streams, cwd) {
|
|
|
576
646
|
// after a nine-minute wait, and failing then, would leave exactly the stale
|
|
577
647
|
// request the flag exists to prevent.
|
|
578
648
|
const withdrawOnTimeout = boolFlag(flags, "--withdraw-on-timeout");
|
|
649
|
+
// APRV-445 (L1). A zero wait is a status read, and a status read that
|
|
650
|
+
// withdraws whatever is still pending would retract every question a poller
|
|
651
|
+
// looked at, on the first look.
|
|
652
|
+
if (withdrawOnTimeout && timeoutMs === 0) {
|
|
653
|
+
return usageError(streams, json, "--withdraw-on-timeout cannot be combined with --timeout 0: a zero wait reads the current state, and withdrawing on it would retract every pending request the moment it is polled", WAIT_HELP);
|
|
654
|
+
}
|
|
579
655
|
const asFlag = stringFlag(flags, "--as");
|
|
580
656
|
const withdrawActor = asFlag ?? resolveHumanActor();
|
|
581
657
|
if (withdrawOnTimeout && (withdrawActor === null || !PRINCIPAL_ACTOR.test(withdrawActor))) {
|
|
@@ -592,33 +668,72 @@ export function commandWait(argv, streams, cwd) {
|
|
|
592
668
|
message: read.message,
|
|
593
669
|
});
|
|
594
670
|
}
|
|
671
|
+
// APRV-428. A task the log has never registered is refused, not answered.
|
|
672
|
+
// Before this check the loop below found no requests and reported the task
|
|
673
|
+
// `granted` with no actions: a typo read as a grant, and a shim polling
|
|
674
|
+
// until granted would have proceeded on a request it never opened. Checked
|
|
675
|
+
// on every pass rather than once, because it costs one walk of records
|
|
676
|
+
// already in hand and a registration cannot disappear from an append-only
|
|
677
|
+
// log, so the answer only ever moves from refusal to not.
|
|
678
|
+
const registered = taskRegistration(read.records, task);
|
|
679
|
+
if (!registered.ok)
|
|
680
|
+
return emitWaitRefusal(streams, json, registered);
|
|
595
681
|
const ts = now();
|
|
596
682
|
const actions = [];
|
|
597
683
|
let pending = false;
|
|
684
|
+
// APRV-445 refutation (B1): the state a waiter acts on is what the record
|
|
685
|
+
// still AUTHORIZES. A grant whose window lapsed reads `expired`, and a
|
|
686
|
+
// pending request, or a harness grant (every proposal), pinned to a
|
|
687
|
+
// superseded policy reads `void` (a token grant `run` still spends stays
|
|
688
|
+
// `granted`), so
|
|
689
|
+
// a poller asks again instead of being told `granted` about an answer the
|
|
690
|
+
// spend will refuse.
|
|
691
|
+
const attestedSha = attestedPolicySha256(read.records, { policy: policyLocation(flags, cwd) });
|
|
692
|
+
// A granted action whose grant nothing has spent yet: the one thing a
|
|
693
|
+
// `granted` status can honestly promise (APRV-428). Judged on the
|
|
694
|
+
// standing, so a lapsed or voided grant never counts as one.
|
|
695
|
+
let unspentGrant = false;
|
|
598
696
|
for (const key of requestedKeysOf(read.records, task)) {
|
|
599
697
|
const derivation = requestState(read.records, key, ts, ttlMs);
|
|
698
|
+
const standing = requestStanding(read.records, key, ts, ttlMs, attestedSha);
|
|
699
|
+
// An executed key reads as its recorded decision when a grant authorized
|
|
700
|
+
// it (the long-standing `granted`), and as `executed` when it ran on the
|
|
701
|
+
// policy's own authority after its request ended (APRV-445 recheck L-a):
|
|
702
|
+
// a policy-path `start` after a withdrawal is not a `withdrawn` action.
|
|
703
|
+
const state = standing === "executed"
|
|
704
|
+
? derivation.state === "granted"
|
|
705
|
+
? "granted"
|
|
706
|
+
: "executed"
|
|
707
|
+
: standing;
|
|
600
708
|
// APRV-105. The token, when this machine can open it: the grant sealed it
|
|
601
709
|
// to the ephemeral public key this action's request published, and the
|
|
602
710
|
// private half is in the key store beside the log. Attached only to a
|
|
603
711
|
// GRANTED action, and only in `--json` below — a `null` on every other
|
|
604
712
|
// state would be a field consumers have to ignore, and a token on a
|
|
605
713
|
// rejected action would be a value with nothing behind it.
|
|
606
|
-
const token = json &&
|
|
714
|
+
const token = json && state === "granted"
|
|
607
715
|
? deliveredToken(read.records, key, keyStoreDirFor(logPath))
|
|
608
716
|
: null;
|
|
609
717
|
actions.push({
|
|
610
718
|
action_key: key,
|
|
611
|
-
state
|
|
719
|
+
state,
|
|
612
720
|
seq: derivation.decisionSeq ?? derivation.requestSeq,
|
|
613
721
|
...(token === null ? {} : { token }),
|
|
614
722
|
});
|
|
615
|
-
if (
|
|
723
|
+
if (state === "requested")
|
|
616
724
|
pending = true;
|
|
725
|
+
if (state === "granted" &&
|
|
726
|
+
derivation.state === "granted" &&
|
|
727
|
+
derivation.execution.started === null) {
|
|
728
|
+
unspentGrant = true;
|
|
729
|
+
}
|
|
617
730
|
}
|
|
618
731
|
if (!pending) {
|
|
619
732
|
// Precedence, documented in --help: a human's "no" outranks a lapse, and
|
|
620
|
-
// both outrank "everything was granted". A task with no
|
|
621
|
-
//
|
|
733
|
+
// both outrank "everything was granted". A registered task with no
|
|
734
|
+
// requests at all, or whose every grant has already been spent by an
|
|
735
|
+
// execution, has nothing to wait for and says so (APRV-428); it was
|
|
736
|
+
// "granted vacuously" before, which is a grant nobody made.
|
|
622
737
|
//
|
|
623
738
|
// APRV-106 puts `withdrawn` between the two, and REUSES exit 1 rather
|
|
624
739
|
// than adding a code. The table in `cli/exit-codes.ts` is frozen public
|
|
@@ -630,15 +745,31 @@ export function commandWait(argv, streams, cwd) {
|
|
|
630
745
|
// the JSON and printed beside the action in the human render.
|
|
631
746
|
const rejected = actions.some((action) => action.state === "rejected" || action.state === "revoked");
|
|
632
747
|
const withdrawn = actions.some((action) => action.state === "withdrawn");
|
|
748
|
+
const voided = actions.some((action) => action.state === "void");
|
|
633
749
|
const expired = actions.some((action) => action.state === "expired");
|
|
750
|
+
// APRV-445: `void` sits below a human's no and a requester's withdrawal
|
|
751
|
+
// (both final) and above a lapse, because its repair is the most
|
|
752
|
+
// specific: the same question, asked again under the policy in force.
|
|
634
753
|
const status = rejected
|
|
635
754
|
? "rejected"
|
|
636
755
|
: withdrawn
|
|
637
756
|
? "withdrawn"
|
|
757
|
+
: voided
|
|
758
|
+
? "void"
|
|
759
|
+
: expired
|
|
760
|
+
? "expired"
|
|
761
|
+
: unspentGrant
|
|
762
|
+
? "granted"
|
|
763
|
+
: actions.length > 0 && actions.every((action) => action.state === "executed")
|
|
764
|
+
? "executed"
|
|
765
|
+
: NOTHING_TO_WAIT_FOR;
|
|
766
|
+
const code = rejected || withdrawn
|
|
767
|
+
? EXIT_INTEGRITY
|
|
768
|
+
: voided
|
|
769
|
+
? EXIT_VOID
|
|
638
770
|
: expired
|
|
639
|
-
?
|
|
640
|
-
:
|
|
641
|
-
const code = rejected || withdrawn ? EXIT_INTEGRITY : expired ? EXIT_TORN_TAIL : EXIT_OK;
|
|
771
|
+
? EXIT_TORN_TAIL
|
|
772
|
+
: EXIT_OK;
|
|
642
773
|
if (json)
|
|
643
774
|
emitJson(streams, { ok: true, task, status, actions });
|
|
644
775
|
else
|
|
@@ -707,11 +838,18 @@ function pendingRequests(records, ts, ttlMs) {
|
|
|
707
838
|
const derivation = requestState(records, key, ts, ttlMs);
|
|
708
839
|
if (derivation.state !== "requested")
|
|
709
840
|
continue;
|
|
841
|
+
// APRV-423 (second review pass): the window is the derivation's, never a
|
|
842
|
+
// second copy of the arithmetic here. `requestState` already took the
|
|
843
|
+
// minimum of the policy's TTL and the declared harness cap less its margin;
|
|
844
|
+
// reading `ttlMs` again would show a capped request the policy's hour while
|
|
845
|
+
// the gate refuses it after four minutes, and would show a capped request
|
|
846
|
+
// under a policy with no TTL as "no TTL" when it has one.
|
|
847
|
+
const windowMs = derivation.effectiveTtlMs;
|
|
710
848
|
const requestedAt = Date.parse(derivation.requestTs ?? "");
|
|
711
849
|
const asked = Date.parse(ts);
|
|
712
|
-
const remaining =
|
|
850
|
+
const remaining = windowMs === null || Number.isNaN(requestedAt) || Number.isNaN(asked)
|
|
713
851
|
? null
|
|
714
|
-
: requestedAt +
|
|
852
|
+
: requestedAt + windowMs - asked;
|
|
715
853
|
entries.push({
|
|
716
854
|
action_key: key,
|
|
717
855
|
task: derivation.task,
|
|
@@ -720,6 +858,7 @@ function pendingRequests(records, ts, ttlMs) {
|
|
|
720
858
|
requested_ts: derivation.requestTs,
|
|
721
859
|
seq: derivation.requestSeq,
|
|
722
860
|
ttl_remaining_ms: remaining,
|
|
861
|
+
ttl_ms: windowMs,
|
|
723
862
|
});
|
|
724
863
|
}
|
|
725
864
|
return entries.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
@@ -773,7 +912,13 @@ export function renderQueueHuman(pending, ttlMs, st = style()) {
|
|
|
773
912
|
entry.class ?? "-",
|
|
774
913
|
`$${String(entry.est_cost_usd ?? 0)}`,
|
|
775
914
|
entry.requested_ts ?? "-",
|
|
776
|
-
|
|
915
|
+
// The fraction is of the entry's OWN window (APRV-423): a capped request's
|
|
916
|
+
// four minutes, or the policy's TTL for one nothing narrowed. The `ttlMs`
|
|
917
|
+
// parameter stays as the fallback for an entry that carries no window.
|
|
918
|
+
{
|
|
919
|
+
text: ttlText(entry.ttl_remaining_ms),
|
|
920
|
+
role: ttlRole(entry.ttl_remaining_ms, entry.ttl_ms ?? ttlMs),
|
|
921
|
+
},
|
|
777
922
|
]);
|
|
778
923
|
return `${table(st, rows, {
|
|
779
924
|
header: ["action", "task", "class", "cost", "requested", "ttl"],
|
|
@@ -950,6 +1095,101 @@ function gitCoverageSummary(records, flags, cwd) {
|
|
|
950
1095
|
covered: report.covered,
|
|
951
1096
|
};
|
|
952
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* The two refusal records `status` counts, and the key each is reported under
|
|
1100
|
+
* (APRV-376).
|
|
1101
|
+
*
|
|
1102
|
+
* The event NAMES are matched here rather than imported from
|
|
1103
|
+
* `core/decision-refusal.ts` and `core/gesture-refusal.ts`, and that is
|
|
1104
|
+
* deliberate: both modules are writers on the append path, and the whole safety
|
|
1105
|
+
* argument for those records is that nothing reads them from anywhere that
|
|
1106
|
+
* decides. A report that filters on a string pulls in no writer, and the two
|
|
1107
|
+
* suites that pin the module graph stay true without an exemption for this file.
|
|
1108
|
+
*/
|
|
1109
|
+
const REFUSAL_FAMILIES = {
|
|
1110
|
+
decision: "audit.decision_refused",
|
|
1111
|
+
gesture: "audit.gesture_refused",
|
|
1112
|
+
};
|
|
1113
|
+
/**
|
|
1114
|
+
* How many of the newest refusals each family lists.
|
|
1115
|
+
*
|
|
1116
|
+
* Small and fixed on purpose. The COUNT answers "is this happening", and the
|
|
1117
|
+
* point of the listing beside it is to hand an operator a seq to paste into
|
|
1118
|
+
* `approval log tail` and an account to recognize. A row that grew with the log
|
|
1119
|
+
* would push the rest of the report off a terminal for a fact that is
|
|
1120
|
+
* informational, and the whole history is `approval log export` away.
|
|
1121
|
+
*/
|
|
1122
|
+
const RECENT_REFUSALS = 5;
|
|
1123
|
+
/** One record, read the way `status` reports it: verbatim, and nothing derived. */
|
|
1124
|
+
function refusalEntry(record) {
|
|
1125
|
+
const payload = record.payload ?? {};
|
|
1126
|
+
// The event schema requires `code` on both types, so the fallback is for a
|
|
1127
|
+
// reader of a log written by something else: a report that threw on a record
|
|
1128
|
+
// it could not read would take the whole health report down with it.
|
|
1129
|
+
const code = typeof payload["code"] === "string" ? payload["code"] : "(no code)";
|
|
1130
|
+
const sender = payload["sender"];
|
|
1131
|
+
if (sender === null || typeof sender !== "object")
|
|
1132
|
+
return { seq: record.seq, code };
|
|
1133
|
+
const observed = sender;
|
|
1134
|
+
const channel = observed["channel"];
|
|
1135
|
+
const id = observed["id"];
|
|
1136
|
+
if (typeof channel !== "string" || typeof id !== "string")
|
|
1137
|
+
return { seq: record.seq, code };
|
|
1138
|
+
return {
|
|
1139
|
+
seq: record.seq,
|
|
1140
|
+
code,
|
|
1141
|
+
sender: {
|
|
1142
|
+
channel,
|
|
1143
|
+
id,
|
|
1144
|
+
// APRV-370's marker, carried only when the record carries it: a keyed
|
|
1145
|
+
// digest and a raw account id are different things to go looking for.
|
|
1146
|
+
...(observed["hashed"] === true ? { hashed: true } : {}),
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* The refused decisions and refused gestures in this log (APRV-376).
|
|
1152
|
+
*
|
|
1153
|
+
* Both families were visible only through `approval log tail` and `approval log
|
|
1154
|
+
* export` until this row existed. The fact an operator needs is small and the
|
|
1155
|
+
* log is the wrong place to read it from: told that taps from an account they
|
|
1156
|
+
* did not map are being refused, they want to know how many and from which
|
|
1157
|
+
* account, and the chain is a poor answer to a question that size.
|
|
1158
|
+
*
|
|
1159
|
+
* INFORMATIONAL, on the terms `harness outcomes` and `git coverage` set: it
|
|
1160
|
+
* moves neither `healthy` nor the exit code. A refusal is the gate having
|
|
1161
|
+
* worked, and a report that went red because the gate refused a stranger would
|
|
1162
|
+
* teach an operator to stop reading it. It reads only verified records (SPEC.md
|
|
1163
|
+
* §11.1 invariant 1) because its caller passes only those, and it reads them
|
|
1164
|
+
* only to report: nothing here authorizes, settles, charges or samples
|
|
1165
|
+
* anything.
|
|
1166
|
+
*
|
|
1167
|
+
* A family with no records is ABSENT rather than zero, and the object is empty
|
|
1168
|
+
* when both are, so a log with no refusals emits the object it always emitted.
|
|
1169
|
+
* A zero here would be a row an operator scrolls past on every healthy
|
|
1170
|
+
* repository, which is how the rows that matter stop being read.
|
|
1171
|
+
*/
|
|
1172
|
+
function refusalSummaries(records) {
|
|
1173
|
+
const summaries = {};
|
|
1174
|
+
for (const family of Object.keys(REFUSAL_FAMILIES)) {
|
|
1175
|
+
const seen = records.filter((record) => record.event === REFUSAL_FAMILIES[family]);
|
|
1176
|
+
if (seen.length === 0)
|
|
1177
|
+
continue;
|
|
1178
|
+
summaries[family] = {
|
|
1179
|
+
count: seen.length,
|
|
1180
|
+
// Newest first: the reason to read this row at all is what just happened.
|
|
1181
|
+
recent: seen.slice(-RECENT_REFUSALS).reverse().map(refusalEntry),
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
return summaries;
|
|
1185
|
+
}
|
|
1186
|
+
/** How the human rendering spells one entry's account. */
|
|
1187
|
+
function refusalSenderText(entry) {
|
|
1188
|
+
if (entry.sender === undefined)
|
|
1189
|
+
return "";
|
|
1190
|
+
const keyed = entry.sender.hashed === true ? " (keyed)" : "";
|
|
1191
|
+
return ` ${entry.sender.channel}:${entry.sender.id}${keyed}`;
|
|
1192
|
+
}
|
|
953
1193
|
export function commandStatus(argv, streams, cwd) {
|
|
954
1194
|
const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS, "--verbose": "boolean" }, STATUS_HELP, streams, cwd);
|
|
955
1195
|
if (outcome.kind === "handled")
|
|
@@ -977,6 +1217,13 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
977
1217
|
// anything from one.
|
|
978
1218
|
const records = read.ok ? read.records : [];
|
|
979
1219
|
const attestation = checkAttestation(records, policyPathFor(flags, cwd));
|
|
1220
|
+
// APRV-449: who made the attestation the state names, read from the verified
|
|
1221
|
+
// record at that seq, never from the file. On `hash-mismatch` it is the
|
|
1222
|
+
// attester of the bytes the log last vouched for, which is the policy a
|
|
1223
|
+
// re-attestation would replace.
|
|
1224
|
+
const attestedBy = attestation.status === "attested" || attestation.status === "hash-mismatch"
|
|
1225
|
+
? attesterAt(records, attestation.seq)
|
|
1226
|
+
: null;
|
|
980
1227
|
// APRV-120. `dangling` is now the `open` custody state only: a harness
|
|
981
1228
|
// execution is terminal by design and never gains an outcome, so listing it
|
|
982
1229
|
// as debris trained operators to ignore this list (the reference repository's
|
|
@@ -1070,6 +1317,28 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1070
1317
|
status: verification.status,
|
|
1071
1318
|
records: verification.status === "corrupt" ? null : verification.records,
|
|
1072
1319
|
};
|
|
1320
|
+
// APRV-383. The daemon identity this instance resolves, and the allowlist in
|
|
1321
|
+
// force for it. `status` is not the daemon, so what it reports is the id a
|
|
1322
|
+
// daemon started HERE would write: that is the fact an operator wiring up a
|
|
1323
|
+
// hosted deployment needs, and the one a tenant needs to look up a `daemon`
|
|
1324
|
+
// field they have just read in a record.
|
|
1325
|
+
//
|
|
1326
|
+
// The allowlist comes from `resolveDaemonAllowlist`, so it is reported only
|
|
1327
|
+
// when the policy loads AND is attested — the same bytes the write boundary
|
|
1328
|
+
// would refuse on. An unattested policy reports `null`, no restriction, which
|
|
1329
|
+
// is exactly what the write boundary would do with it.
|
|
1330
|
+
const daemonId = resolveDaemonId(logPath);
|
|
1331
|
+
const daemonAllowlist = resolveDaemonAllowlist(records, loadPolicy(policyLocation(flags, cwd)));
|
|
1332
|
+
const daemonReport = {
|
|
1333
|
+
id: daemonId.ok ? daemonId.id : null,
|
|
1334
|
+
source: daemonId.ok ? daemonId.source : null,
|
|
1335
|
+
allowed: daemonAllowlist.ok ? daemonAllowlist.allowed : null,
|
|
1336
|
+
};
|
|
1337
|
+
// APRV-376, and informational for the reason stated where it is computed: the
|
|
1338
|
+
// two refusal families, counted, with the newest few of each. Neither the
|
|
1339
|
+
// health line below nor the exit code reads it.
|
|
1340
|
+
const refusals = refusalSummaries(records);
|
|
1341
|
+
const refusalFamilies = Object.keys(refusals);
|
|
1073
1342
|
// APRV-40. Timestamp anomalies (SPEC.md §8) are informational and deliberately
|
|
1074
1343
|
// outside `healthy`: they are a judgment, not an integrity verdict. `verify`
|
|
1075
1344
|
// already declined to refuse on them, and `status` does not get to overrule it
|
|
@@ -1106,6 +1375,9 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1106
1375
|
seq: attestation.status === "attested" || attestation.status === "hash-mismatch"
|
|
1107
1376
|
? attestation.seq
|
|
1108
1377
|
: null,
|
|
1378
|
+
// APRV-449, additive and always present: the identity the attesting
|
|
1379
|
+
// record declared, `null` when there is no attestation to name.
|
|
1380
|
+
attested_by: attestedBy,
|
|
1109
1381
|
},
|
|
1110
1382
|
verification: verificationSummary,
|
|
1111
1383
|
dangling,
|
|
@@ -1127,7 +1399,16 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1127
1399
|
},
|
|
1128
1400
|
reconciliation: obligations,
|
|
1129
1401
|
payload_store: payloadStore,
|
|
1402
|
+
// APRV-383. Always present, for the reason `coverage` is: three keys every
|
|
1403
|
+
// consumer sees on every log, whether or not a daemon has ever run here and
|
|
1404
|
+
// whether or not the policy restricts one. `allowed: null` is no restriction
|
|
1405
|
+
// and is not the same fact as an empty list, which admits nobody.
|
|
1406
|
+
daemon: daemonReport,
|
|
1130
1407
|
...(anomalies.length === 0 ? {} : { anomalies }),
|
|
1408
|
+
// APRV-376. Present only when the log carries a refusal of either family,
|
|
1409
|
+
// and each family present only when that family has one, so a repository
|
|
1410
|
+
// where nothing has been refused emits the object it always emitted.
|
|
1411
|
+
...(refusalFamilies.length === 0 ? {} : { refusals }),
|
|
1131
1412
|
// Present only while a window stands, exactly as `anomalies` and
|
|
1132
1413
|
// `indeterminate` are: a repository with no window emits the object it
|
|
1133
1414
|
// has always emitted, byte for byte.
|
|
@@ -1159,7 +1440,7 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1159
1440
|
// a bug report, so APRV-102 took the `muted` dressing off it. The state
|
|
1160
1441
|
// beside it still carries the colour.
|
|
1161
1442
|
right: `${st.paint(attestation.status === "attested" ? "ok" : "warn", attestation.status)}${attestation.status === "attested" || attestation.status === "hash-mismatch"
|
|
1162
|
-
? ` (seq ${attestation.seq})`
|
|
1443
|
+
? ` (seq ${attestation.seq}${attestedBy === null ? "" : `, by ${attestedBy}`})`
|
|
1163
1444
|
: ""}`,
|
|
1164
1445
|
},
|
|
1165
1446
|
{
|
|
@@ -1254,6 +1535,26 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1254
1535
|
? `${String(coverage.covered)} of ${String(coverage.observed)} effects carry evidence`
|
|
1255
1536
|
: (coverage.reason ?? "unavailable")),
|
|
1256
1537
|
},
|
|
1538
|
+
// APRV-376. Its own INFORMATIONAL row, and printed only when the log
|
|
1539
|
+
// carries a refusal of either family: a repository where nothing has been
|
|
1540
|
+
// refused reads exactly as it always did, and a row saying "none" on every
|
|
1541
|
+
// healthy repository is a row an operator learns to skip. The counts say
|
|
1542
|
+
// whether this is happening; the lines under them hand over a seq to paste
|
|
1543
|
+
// into `approval log tail` and an account to recognize.
|
|
1544
|
+
...(refusalFamilies.length === 0
|
|
1545
|
+
? []
|
|
1546
|
+
: [
|
|
1547
|
+
{
|
|
1548
|
+
left: "refusals",
|
|
1549
|
+
right: st.muted(`${refusalFamilies
|
|
1550
|
+
.map((family) => `${String(refusals[family]?.count ?? 0)} ${family}`)
|
|
1551
|
+
.join(", ")} (reported; health unaffected)`),
|
|
1552
|
+
under: refusalFamilies.flatMap((family) => (refusals[family]?.recent ?? []).map((entry) =>
|
|
1553
|
+
// The family is padded to the longer of the two names, so the
|
|
1554
|
+
// seqs line up under each other when both are listed.
|
|
1555
|
+
`${family.padEnd("decision".length)} seq ${String(entry.seq)} ${entry.code}${refusalSenderText(entry)}`)),
|
|
1556
|
+
},
|
|
1557
|
+
]),
|
|
1257
1558
|
{
|
|
1258
1559
|
// APRV-214. Its own row and not a footnote: while this says OPEN, the
|
|
1259
1560
|
// policy is deciding nothing for the harness, and the person reading
|
|
@@ -1282,6 +1583,26 @@ export function commandStatus(argv, streams, cwd) {
|
|
|
1282
1583
|
under: obligations.map((item) => `seq ${item.seq} ${item.action_key} ${item.class} ${item.obligation} — close with \`approval audit reconcile ${item.seq}\``),
|
|
1283
1584
|
}),
|
|
1284
1585
|
},
|
|
1586
|
+
{
|
|
1587
|
+
// APRV-383. The id a daemon started against THIS log writes onto every
|
|
1588
|
+
// record it appends, and whether the attested policy admits it. Printed
|
|
1589
|
+
// by a verb that is not the daemon on purpose: an operator setting up a
|
|
1590
|
+
// hosted deployment needs the id before anything is running, and a tenant
|
|
1591
|
+
// reading a record's `daemon` field needs somewhere to look it up.
|
|
1592
|
+
// Informational: it moves neither `healthy` nor the exit code, because
|
|
1593
|
+
// nothing here is a fact about the log's integrity.
|
|
1594
|
+
left: "daemon identity",
|
|
1595
|
+
right: daemonReport.id === null
|
|
1596
|
+
? st.fail(`UNUSABLE (${DAEMON_ID_ENV} is set to something that is not an id)`)
|
|
1597
|
+
: `${daemonReport.id} ${st.muted(`(${daemonReport.source})`)}`,
|
|
1598
|
+
under: daemonReport.allowed === null
|
|
1599
|
+
? [st.muted("the policy lists no allowed daemon ids, so any daemon may write")]
|
|
1600
|
+
: [
|
|
1601
|
+
daemonReport.id !== null && daemonReport.allowed.includes(daemonReport.id)
|
|
1602
|
+
? st.muted(`allowed by the policy's daemons list (${daemonReport.allowed.join(", ")})`)
|
|
1603
|
+
: st.warn(`NOT in the policy's daemons list (${daemonReport.allowed.length === 0 ? "empty" : daemonReport.allowed.join(", ")}) — a daemon started here would be refused every append`),
|
|
1604
|
+
],
|
|
1605
|
+
},
|
|
1285
1606
|
{ left: "log", right: relPath(logPath, cwd) },
|
|
1286
1607
|
];
|
|
1287
1608
|
streams.out(`${st.table(rows)}\n`);
|