approval-md 0.4.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/SPEC.md +3 -0
- package/cli.js +72 -4
- package/dist/src/channels/contract.d.ts +53 -0
- package/dist/src/channels/contract.js +56 -0
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/tagging.d.ts +25 -0
- package/dist/src/channels/tagging.js +57 -5
- 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.d.ts +454 -19
- package/dist/src/channels/telegram.js +931 -80
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +8 -1
- package/dist/src/cli/amend.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-telegram-webhook.d.ts +2 -0
- package/dist/src/cli/channel-telegram-webhook.js +7 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +75 -2
- package/dist/src/cli/channel-telegram.js +170 -15
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/execute.d.ts +12 -0
- package/dist/src/cli/execute.js +46 -1
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +1 -0
- package/dist/src/cli/help.js +39 -18
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +114 -3
- package/dist/src/cli/hook.js +468 -50
- package/dist/src/cli/hook.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.js +23 -6
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/up.js +5 -0
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/verb-registry.js +1 -0
- 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/audit.d.ts +98 -9
- package/dist/src/core/audit.js +363 -15
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/command-class.js +6 -0
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/gate.d.ts +58 -1
- package/dist/src/core/gate.js +30 -3
- package/dist/src/core/gate.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.d.ts +82 -3
- package/dist/src/core/log.js +285 -40
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +17 -0
- package/dist/src/core/policy-proposal.js +29 -14
- 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/sender-identity.d.ts +24 -1
- package/dist/src/core/sender-identity.js +31 -0
- package/dist/src/core/sender-identity.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/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/serve/archive.d.ts +8 -0
- package/dist/src/serve/archive.js +11 -2
- package/dist/src/serve/archive.js.map +1 -1
- package/docs/README-extended.md +2 -0
- package/docs/cli-reference.md +507 -25
- package/package.json +2 -2
- package/schema/event.schema.json +354 -28
- 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/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.json +2 -1
- package/schema/policy.schema.json +52 -0
package/docs/cli-reference.md
CHANGED
|
@@ -311,6 +311,64 @@ that is due to a refusal of anything: it is a warning on `log verify`, a
|
|
|
311
311
|
doctor`'s `checkpoint` row. A gate that held up an action for want of a tap is a
|
|
312
312
|
gate whose operator turns the check off.
|
|
313
313
|
|
|
314
|
+
## log unlock
|
|
315
|
+
|
|
316
|
+
Takes over `events.jsonl.lock` from a writer that died holding it, when no
|
|
317
|
+
writer could do that itself, and records it.
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
approval log unlock --pid <n|none> --as human:<id> [--log <path>] [--json]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Why a person.** Every writer that finds the lock held waits out its whole
|
|
324
|
+
lock timeout (two seconds) and then judges the holder once. It takes the lock
|
|
325
|
+
back by itself only when it can prove the holder gone: the holder ran in this
|
|
326
|
+
writer's own pid namespace and boot, and `kill(pid, 0)` answers ESRCH (on Linux
|
|
327
|
+
also with `/proc/<pid>` absent or a zombie, or the pid now names a process that
|
|
328
|
+
started later), or the lockfile names no holder, is ten minutes old, and sat
|
|
329
|
+
unchanged through the writer's whole wait (a try-once caller never takes it). Any
|
|
330
|
+
other holder (another container, another boot, a pid answering EPERM, a `/proc`
|
|
331
|
+
it cannot read) is never taken automatically, because a wrong reclaim forks the
|
|
332
|
+
chain. The writer's `lock-timeout` refusal then names the pid and this command.
|
|
333
|
+
|
|
334
|
+
**What it does.** It reads the lockfile strictly, refuses a `--pid` that is not
|
|
335
|
+
the one the record names (`none` for a lockfile with no holder record), refuses
|
|
336
|
+
a holder it sees running (Linux: the same start time in this namespace;
|
|
337
|
+
elsewhere: the pid on this host in this boot), and refuses beside a `log sync`
|
|
338
|
+
snapshot. It then takes the lock exactly as a writer's reclaim does (an
|
|
339
|
+
exclusive `link(2)` to `<lock>.stale.<pid>.<created ms>`, then a `rename(2)` of
|
|
340
|
+
its own lockfile over the lock's path) and appends `audit.lock_reclaimed` as the
|
|
341
|
+
first record under it, with your `human:` actor, reason `operator-cleared` (your
|
|
342
|
+
word, which the log never presents as a writer's proof), the strictly parsed
|
|
343
|
+
holder and the lock's age. A holder `created` the log cannot vouch for (more
|
|
344
|
+
than a day before the log's last record, or more than five minutes ahead of
|
|
345
|
+
this clock) is recorded as `null`, with no age. A stale name a dead reclaimer or
|
|
346
|
+
anyone else left is cleared first, and a dead reclaimer's own
|
|
347
|
+
`<lock>.take.<pid>.*` file with it. Anything at the lock's path that is not a
|
|
348
|
+
lockfile (a FIFO, a link, a malformed file) is not taken: the refusal says to
|
|
349
|
+
`rm -v` it.
|
|
350
|
+
|
|
351
|
+
It also refuses, touching nothing:
|
|
352
|
+
|
|
353
|
+
- while a reclaim is in flight: a writer writes its own
|
|
354
|
+
`<lock>.take.<pid>.<nonce>` before it claims, and a running one is named, so
|
|
355
|
+
a reclaimer that is stopped (SIGSTOP, a debugger) is never unlocked around;
|
|
356
|
+
- when the record could not be appended: a daemon whose id the attested
|
|
357
|
+
policy refuses, or a torn tail (repair the tail first);
|
|
358
|
+
- when the claim is refused EPERM or EACCES (Linux `fs.protected_hardlinks`:
|
|
359
|
+
the lockfile is another user's): the refusal names the owner's uid; run it as
|
|
360
|
+
that user or root, or `rm -v` the lockfile once no writer runs;
|
|
361
|
+
- when a directory sits at the stale name: the refusal names `rm -r` for it.
|
|
362
|
+
|
|
363
|
+
**Run it only when no writer is running.** The verb asserts, on your word, that
|
|
364
|
+
the holder is gone. Human-only: the actor comes from `--as` or `APPROVAL_HUMAN`,
|
|
365
|
+
the schema refuses an `agent:` actor on the record, and `core/command-class.ts`
|
|
366
|
+
classifies `approval log unlock` (and `node <cli> log unlock`) `policy.core`, so the harness hook denies an agent
|
|
367
|
+
that tries it that way. Other spellings (`npx approval log unlock`, `npm exec`,
|
|
368
|
+
a wrapper script) classify as workspace writes, and `--as` is the caller's to
|
|
369
|
+
set, so the classification is a speed bump rather than the boundary (APRV-491).
|
|
370
|
+
Exit 0 when it unlocked or there was no lock; 4 when it refused.
|
|
371
|
+
|
|
314
372
|
## log follow
|
|
315
373
|
|
|
316
374
|
`approval log follow --from <seq> --json` is the channel-independent decision
|
|
@@ -3538,20 +3596,113 @@ carries in the open.
|
|
|
3538
3596
|
|
|
3539
3597
|
## audit review
|
|
3540
3598
|
|
|
3599
|
+
**The reviewer must be on the class's roster (APRV-483).** Where the policy rule
|
|
3600
|
+
that resolves the sampled action's class names `approvers`, the reviewer must be
|
|
3601
|
+
one of them, compared exactly as a grant's approver is (`namesApprover` in
|
|
3602
|
+
`core/gate.ts`): a bare id on the roster matches `human:<id>`. Anyone else is
|
|
3603
|
+
refused `actor-not-approver` (exit 1) and nothing is appended. This binds every
|
|
3604
|
+
surface, because the check is in `reviewSample` itself: `--as human:<id>` at a
|
|
3605
|
+
terminal and a sender-mapped tap on a Telegram card are held to the same list.
|
|
3606
|
+
A rule that names no `approvers` restricts nobody, exactly as for grants. The
|
|
3607
|
+
class comes from the action's registration (a sample naming no class is refused
|
|
3608
|
+
`actor-not-approver`, never read as "no roster"), and the roster is read from
|
|
3609
|
+
the policy file a grant would read, only when its bytes are the latest
|
|
3610
|
+
attestation's: an unattested, edited, unreadable or `--policy`-substituted file
|
|
3611
|
+
is refused `policy-not-attested` (exit 1), because a roster read from a file
|
|
3612
|
+
nobody attested is one the reviewer could have chosen. Attested bytes that do not
|
|
3613
|
+
load (a schema-invalid rule, a YAML error) are refused `policy-invalid` (exit 1):
|
|
3614
|
+
`policy attest` hashes bytes without parsing them, and the fail-closed reading
|
|
3615
|
+
of a broken policy names no roster, which for a review would mean anyone. Nothing
|
|
3616
|
+
resolves from a broken file, so an on-roster reviewer is refused too; the repair
|
|
3617
|
+
is a corrected policy, attested.
|
|
3618
|
+
|
|
3619
|
+
**The roster is the one in force when the action ran (PR #614 refutation F2,
|
|
3620
|
+
recheck NF-3).** Every `audit.sampled` the runtime writes pins the attested
|
|
3621
|
+
policy the sampled execution ran under, as `payload.policy_sha256`: the
|
|
3622
|
+
`sha256` named by the latest `policy.updated` attestation before the
|
|
3623
|
+
`execution.started` record, which is the policy the gate checked when it
|
|
3624
|
+
recorded the start (a harness start carries the same hash as its own
|
|
3625
|
+
`policy_sha256`). A policy attested between the run and the sweep therefore
|
|
3626
|
+
changes neither who may review the sample nor whether anyone can. Only an
|
|
3627
|
+
execution older than every attestation in the log pins the latest attestation
|
|
3628
|
+
at sampling instead. A review of that sample reads the
|
|
3629
|
+
roster only from policy bytes that hash to the pin, and only when an
|
|
3630
|
+
attestation before the sample names that hash. The bytes come from the payload
|
|
3631
|
+
store beside the log, where every attestation since APRV-356 keeps the text it
|
|
3632
|
+
attested (PR #614 fix round 3), so editing and re-attesting the policy, a
|
|
3633
|
+
settings save from the app included, leaves older open samples reviewable. The
|
|
3634
|
+
policy file on disk is the fallback, used only when its bytes hash to the pin
|
|
3635
|
+
(a chain last attested before APRV-356 stored none). Bytes in neither place are
|
|
3636
|
+
refused `policy-not-attested`, and the message names the hash it needs. A later
|
|
3637
|
+
re-attestation that renames or drops the class's rule therefore cannot leave
|
|
3638
|
+
the sample with no roster, and one that names a different roster cannot open
|
|
3639
|
+
it to a reviewer the action's policy did not name. A class that matches no rule in the pinned policy
|
|
3640
|
+
(it reached supervised through `defaults.autonomy`) is refused
|
|
3641
|
+
`actor-not-approver` with that reason, because the defaults carry no roster
|
|
3642
|
+
and for a review "no roster" would mean anyone. A sample written before samples
|
|
3643
|
+
pinned a policy has no `policy_sha256` and keeps the latest-attestation reading
|
|
3644
|
+
above.
|
|
3645
|
+
|
|
3646
|
+
Exactly one of `--ok` and `--deny` is required (APRV-482). A bare review, or one
|
|
3647
|
+
carrying only `--reaction`, is refused `verdict-required` (exit 1) and writes
|
|
3648
|
+
nothing: under supervised-retro the review counts as the approval, and an
|
|
3649
|
+
approval is something the reviewer says, never a default. Both flags together
|
|
3650
|
+
is a usage error (exit 2). The verdict is judged after the roster (PR #614
|
|
3651
|
+
refutation N6): a reviewer off the roster is told `actor-not-approver` whether
|
|
3652
|
+
or not they gave a verdict, and a grade with no verdict is not judged for its
|
|
3653
|
+
note until the verdict comes.
|
|
3654
|
+
|
|
3541
3655
|
`--note` is optional — unlike `execution resolve`, this event records only that a
|
|
3542
3656
|
person looked, and the runtime is not relying on the note for a fact it does not
|
|
3543
3657
|
otherwise have. Human-only: a runtime that could mark its own samples reviewed
|
|
3544
3658
|
would be a supervision backlog that empties itself.
|
|
3545
3659
|
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3660
|
+
Since APRV-483 a review needs the attested policy, because its roster check
|
|
3661
|
+
reads the class's `approvers` from it (below). Before that no attestation was
|
|
3662
|
+
required, for the reason `execution resolve` states: review recorded an
|
|
3663
|
+
observation and exercised no policy authority. Under supervised-retro the
|
|
3664
|
+
review is the approval, and the trade is stated plainly: a policy edited and not
|
|
3665
|
+
yet re-attested now holds the review backlog open until a human re-attests.
|
|
3666
|
+
|
|
3667
|
+
What it appends is `audit.reviewed`, naming the sample's action key and task; its
|
|
3668
|
+
payload is listed field by field under [the review record](#the-review-record),
|
|
3669
|
+
which is the one place those names are defined. An action key with several open
|
|
3670
|
+
samples refuses `ambiguous-subject`.
|
|
3671
|
+
|
|
3672
|
+
### The review record
|
|
3673
|
+
|
|
3674
|
+
The field names a follower reads (the data pipeline's follower and the dbt
|
|
3675
|
+
models built on it read reviews as approvals under supervised-retro). This
|
|
3676
|
+
table is the single definition; other docs link here rather than restating it.
|
|
3677
|
+
Every `audit.reviewed` written since APRV-481 carries the three required fields,
|
|
3678
|
+
and since PR #614's F5 fix a fourth, `verdict_source`; the write boundary refuses
|
|
3679
|
+
one that does not. The verdict alone cannot say whether the reviewer gave it:
|
|
3680
|
+
before APRV-482 the runtime wrote `ok` for a grade-only tap or a bare
|
|
3681
|
+
`audit review`, and such a record may carry all three APRV-481 fields, so
|
|
3682
|
+
`verdict_source` is the field that separates the two. A review written before then
|
|
3683
|
+
may lack them and still verifies (the read boundary is the schema's
|
|
3684
|
+
`audit_reviewed_record_historical`), so a follower treats their absence on an
|
|
3685
|
+
old record as "not recorded", never as a default.
|
|
3686
|
+
|
|
3687
|
+
| Field | Where | Required | Meaning |
|
|
3688
|
+
| --- | --- | --- | --- |
|
|
3689
|
+
| `event` | record | yes | `audit.reviewed`. |
|
|
3690
|
+
| `seq`, `ts`, `hash` | record | yes | The review's own chain position and runtime-assigned time. |
|
|
3691
|
+
| `actor` | record | yes | `human:<id>`, the reviewer. Since APRV-483 a person on the class's `approvers` roster wherever the rule names one. |
|
|
3692
|
+
| `action_key`, `task` | record | when the sample named them | The reviewed action and its task. |
|
|
3693
|
+
| `channel` | record | no | The surface, when the writer recorded one. |
|
|
3694
|
+
| `payload.subject_seq` | payload | yes (APRV-481) | The `seq` of the `audit.sampled` record this review answers. Not the execution's seq. |
|
|
3695
|
+
| `payload.sampled_subject_hash` | payload | yes (APRV-481) | The `hash` of the `execution.started` record the sample named: the join key from a review to the execution it judged. |
|
|
3696
|
+
| `payload.verdict` | payload | yes (APRV-481) | `ok` or `denied`. The enforcement field. Explicit only on records that carry `verdict_source`. |
|
|
3697
|
+
| `payload.verdict_source` | payload | yes (PR #614 F5) | Always `explicit`: the reviewer said the verdict (`--ok`/`--deny`, a card's OK or second Deny). The discriminator for a follower: a review WITHOUT it predates APRV-482 or was written by an older build, and its `ok` may have been defaulted for a grade-only tap or a bare terminal review. Count only reviews carrying it as explicit approvals. |
|
|
3698
|
+
| `payload.payload_hash` | payload | no | The SHA-256/JCS binding of the bytes the execution ran, present ONLY when the surface showed those bytes whole before the verdict (a Telegram card's `bytes` view). Absent when the reviewer saw a hash, nothing, or a terminal. Checked against the execution's binding before it is written. |
|
|
3699
|
+
| `payload.reaction` | payload | no | `disliked`, `indifferent`, `liked` or `loved`. Guidance, never enforcement; absent is absent. |
|
|
3700
|
+
| `payload.note` | payload | with `loved`/`disliked` | The reviewer's words, verbatim. |
|
|
3701
|
+
| `payload.sender`, `payload.sender_source` | payload | no | The authenticated account the review arrived from, when a transport authenticated one. |
|
|
3702
|
+
| `payload.subject_event`, `payload.reviewed` | payload | no | Constants (`audit.sampled`, `true`) kept for older readers. |
|
|
3703
|
+
|
|
3704
|
+
A denial is followed by a runtime-authored `reconciliation.required` whose
|
|
3705
|
+
`payload.review_seq` is the review's `seq`.
|
|
3555
3706
|
|
|
3556
3707
|
**`--json`** (one object on stdout):
|
|
3557
3708
|
|
|
@@ -3920,6 +4071,224 @@ turn on was already on the `ChannelRequest`, and `--json`, `approval queue` and
|
|
|
3920
4071
|
the web page carried it all along; rendering stays a pure function of
|
|
3921
4072
|
(request, layout).
|
|
3922
4073
|
|
|
4074
|
+
### The minimal card: `channels.telegram.prompt.style` (APRV-489)
|
|
4075
|
+
|
|
4076
|
+
The technical card is three messages written for an engineer. A resident
|
|
4077
|
+
approving on a phone needs to know what their agent wants to do, the exact
|
|
4078
|
+
words it will act on, how long they have, and where the buttons are.
|
|
4079
|
+
`style: minimal` sends that as ONE message, with the whole technical card
|
|
4080
|
+
collapsed inside it:
|
|
4081
|
+
|
|
4082
|
+
```yaml
|
|
4083
|
+
channels:
|
|
4084
|
+
telegram:
|
|
4085
|
+
prompt:
|
|
4086
|
+
always: [ttl_remaining_ms]
|
|
4087
|
+
style: minimal # technical (the default) | minimal
|
|
4088
|
+
say:
|
|
4089
|
+
digest.share:
|
|
4090
|
+
does: "share a note about you with other people"
|
|
4091
|
+
quote: { scope: "Shared with", expires_at: "Until", text: "Note", digest_id: "Reference" }
|
|
4092
|
+
note: none
|
|
4093
|
+
```
|
|
4094
|
+
|
|
4095
|
+
**For the approver, in plain words.** The bold first line says what your agent
|
|
4096
|
+
wants to do. The box under it holds the agent's own words, copied exactly from
|
|
4097
|
+
what will be posted, shared or run; approval.md writes nothing inside the box
|
|
4098
|
+
except the labels. If a line under the box starts with ⚠ or says "Not shown
|
|
4099
|
+
here", the box is not the whole story: open "Full details" before deciding. A
|
|
4100
|
+
line marked "not checked" is a description (by an AI model or by your agent)
|
|
4101
|
+
that nobody verified. The next line says how long you have; if you do nothing,
|
|
4102
|
+
nothing happens. Tap "Full details" to see everything the runtime checked,
|
|
4103
|
+
including the exact text this approval is bound to. Approve says yes; Deny says
|
|
4104
|
+
no. For anything that deletes, sends files or money, or talks to outside
|
|
4105
|
+
services, open "Full details" every time.
|
|
4106
|
+
|
|
4107
|
+
**The exact layout** (HTML; `[ ]` are the inline buttons):
|
|
4108
|
+
|
|
4109
|
+
```
|
|
4110
|
+
<b>Your agent wants to <phrase></b>
|
|
4111
|
+
<blockquote><b>Label:</b> quoted value one payload value per line, verbatim
|
|
4112
|
+
…</blockquote>
|
|
4113
|
+
⚠ This runs at least 3 commands. Only the beginning is shown above: … a command, only when cut
|
|
4114
|
+
⚠ There is more than fits here: open Full details before deciding. when any other value was cut
|
|
4115
|
+
Not shown here: <key>, <key>. Open Full details before deciding. when the say entry leaves a field off (~)
|
|
4116
|
+
<i>AI summary (not checked):</i> … only when a model sentence is attached
|
|
4117
|
+
<i>Your agent says (not checked):</i> … only when the class's say entry has note: summary
|
|
4118
|
+
<i>Your agent estimates the cost (not checked):</i> $0.00 only when the estimate is above zero
|
|
4119
|
+
Open for about 3 days. If you don't answer, your agent will not do this.
|
|
4120
|
+
<blockquote expandable><b>Full details (tap to open)</b>
|
|
4121
|
+
… the technical card's three messages, in order, character for character …</blockquote>
|
|
4122
|
+
[✅ Approve] [✋ Deny]
|
|
4123
|
+
```
|
|
4124
|
+
|
|
4125
|
+
A tool call the agent is blocked in says "Your agent is waiting: about 4
|
|
4126
|
+
minutes left. If you don't answer, it will not do this." instead. The time is
|
|
4127
|
+
the gate's own window for the request (the policy's TTL, narrowed by a
|
|
4128
|
+
harness hook's cap), from the verified log, as it stood when the card was
|
|
4129
|
+
drawn; the line is not refreshed afterwards. A request with no time left says
|
|
4130
|
+
"Time is up: this request has closed, and an answer now will not count."
|
|
4131
|
+
|
|
4132
|
+
**What is computed, what is quoted, what is described.**
|
|
4133
|
+
|
|
4134
|
+
- *Computed by the runtime:* the first line, chosen by the exact class the LOG
|
|
4135
|
+
records: core's own phrase for a class core emits (`network.call`,
|
|
4136
|
+
`read.web`, `browser.exec`, `cron.manage`, `process.write`, `skill.manage`,
|
|
4137
|
+
`agent.delegate`, `message.send`, `files.delete.scratch`); else, for a
|
|
4138
|
+
payload the runtime reads itself, its structural kind ("run a command",
|
|
4139
|
+
"change a file", "send an email", followed by `(type: <class>)`), whatever
|
|
4140
|
+
the class's `say` entry says; else, for an opaque payload only, the attested
|
|
4141
|
+
`say.<class>.does`. The notices under the box; the deadline line; everything
|
|
4142
|
+
in "Full details"; the buttons.
|
|
4143
|
+
- *Quoted from the bound payload:* the box, and only payload values. Each line
|
|
4144
|
+
is one payload value, verbatim (marked as below, and visibly cut when long),
|
|
4145
|
+
under a label (the operator's attested label, or the runtime's fixed label
|
|
4146
|
+
for a field of a shape it knows), or alone when it is the only quotation.
|
|
4147
|
+
Nothing computed, abbreviated, joined or paraphrased is ever inside the box;
|
|
4148
|
+
a list of addresses is one line per address. Each value sits inside the
|
|
4149
|
+
runtime's own Unicode isolate (U+2068 … U+2069), so right-to-left letters in
|
|
4150
|
+
it cannot reorder its label or the lines around it. The cut mark is drawn
|
|
4151
|
+
inside the value's isolate, so a cut right-to-left value shows it at its left
|
|
4152
|
+
edge, still visible; the notice under the box says the same thing outside
|
|
4153
|
+
any isolate. The operator's `does` phrase in the headline is not isolated.
|
|
4154
|
+
The bytes were hash-checked against the request's
|
|
4155
|
+
`payload_hash` before the channel saw them, so they are what will be acted
|
|
4156
|
+
on; their content was written by the agent. A command is shown up to 160
|
|
4157
|
+
characters (under `Command:` when its `cwd` is quoted as `In folder:`); a
|
|
4158
|
+
longer one is shown from its start and cut, and only then does a computed
|
|
4159
|
+
line under the box say so ("⚠ This runs at least 3 commands. Only the
|
|
4160
|
+
beginning is shown above", the classifier's count as a lower bound, since it
|
|
4161
|
+
does not see commands started inside other commands such as a
|
|
4162
|
+
here-document piped into `sh`, `$( … )`, `bash -c` or `eval`; "⚠ Only the
|
|
4163
|
+
beginning of this command is shown above" when it counts one; "⚠ More than
|
|
4164
|
+
one command may be here, and only the beginning is shown above" when it
|
|
4165
|
+
cannot count). A command shown whole gets no line: the
|
|
4166
|
+
card makes no claim about how many commands it is. The line never names or
|
|
4167
|
+
paraphrases a command the box does not show; the
|
|
4168
|
+
classifier's outline is in "Full details" only, because it leaves out flags
|
|
4169
|
+
and their values, which is where a deletion target or an uploaded file
|
|
4170
|
+
lives. A file change shows the file, the change and every other field under
|
|
4171
|
+
its own key name (`tool:`, `replace_all: true`). An email shows From, To, Cc,
|
|
4172
|
+
Bcc, Subject, Format and Message. An opaque payload shows the fields its
|
|
4173
|
+
`say.<class>.quote` map labels.
|
|
4174
|
+
- *Never a clean-looking partial:* when a command is cut, its computed count
|
|
4175
|
+
line says so; when any other value is cut, "⚠ There is more than fits here:
|
|
4176
|
+
open Full details before deciding." follows the box; when the declaration leaves fields off (`~`),
|
|
4177
|
+
"Not shown here: <their key names>. Open Full details before deciding." does.
|
|
4178
|
+
Both are outside the box, so quoted text can neither produce nor suppress
|
|
4179
|
+
them.
|
|
4180
|
+
- *Described, and labelled "not checked":* the AI summary and the agent's own
|
|
4181
|
+
summary and estimate. They never stand alone and never come first: they sit
|
|
4182
|
+
below the box.
|
|
4183
|
+
|
|
4184
|
+
**Quoted text is hostile input.** Every quoted value is one line (a line break
|
|
4185
|
+
is drawn ` ⏎ `). Control, format, bidirectional and every default-ignorable
|
|
4186
|
+
character (variation selectors, joiners, tag characters, U+034F, the Khmer and
|
|
4187
|
+
Mongolian invisibles), the Hangul fillers and the braille blank, and combining
|
|
4188
|
+
marks beyond two on one character are drawn as `«U+202E»`; so are `«` and `⏎`
|
|
4189
|
+
in the value, which makes the marking injective. One exception keeps emoji
|
|
4190
|
+
readable: a grapheme cluster that is WHOLLY a recognised emoji sequence
|
|
4191
|
+
(Unicode's RGI set, `\p{RGI_Emoji}` with the regular-expression `v` flag: ❤️,
|
|
4192
|
+
👨👩👧, 1️⃣, 👍🏽) is drawn as itself. Every other presentation selector or joiner
|
|
4193
|
+
is marked, so a U+FE0F after a face that is already an emoji, or a U+200D
|
|
4194
|
+
between two pictographs that form no emoji, cannot carry hidden bits. On a
|
|
4195
|
+
runtime without that property nothing is exempt. Node 20, this package's
|
|
4196
|
+
floor, ships V8 11.3, which has it. A value longer than 280 characters is
|
|
4197
|
+
cut between grapheme clusters with `…(cut; see full details)`, and everything is
|
|
4198
|
+
HTML-escaped. A value cannot start a line of its own, close the box, open or
|
|
4199
|
+
close the collapsed block, or be bold.
|
|
4200
|
+
|
|
4201
|
+
Injective is not "never looks the same". Look-alike letters from other scripts
|
|
4202
|
+
(a Cyrillic а for a Latin a), a lone emoji with and without its presentation
|
|
4203
|
+
selector, and runs of ordinary spaces are drawn as themselves, so two different
|
|
4204
|
+
values can still look alike. Telegram clients also turn URLs, `@names`,
|
|
4205
|
+
`#tags` and `/commands` in the box and in "Full details" into links; a link's
|
|
4206
|
+
text is always its own target (markup in a value is escaped, so a value cannot
|
|
4207
|
+
make link text say something else), but a look-alike domain stays look-alike.
|
|
4208
|
+
|
|
4209
|
+
**When the technical card is sent instead.** The minimal card is never guessed
|
|
4210
|
+
at or drawn partially. These requests get today's technical card, the decision
|
|
4211
|
+
record says why (`payload.rendering.fallback`, below), and the listener says
|
|
4212
|
+
so on stderr:
|
|
4213
|
+
|
|
4214
|
+
- an attestation prompt, a `policy.*` or `log.*` class, or a protected path;
|
|
4215
|
+
- a truncated payload, or none;
|
|
4216
|
+
- any abnormal health fact (a budget over its ceiling, a policy not attested,
|
|
4217
|
+
an autonomy that is not `manual`), even when the row layout hides its row;
|
|
4218
|
+
- an opaque payload whose class has no `say` entry, carrying a key the entry's
|
|
4219
|
+
`quote` map does not name, or that is not an object;
|
|
4220
|
+
- a request delivered as part of a batch: a digest, its members, the collapsed
|
|
4221
|
+
stale-request summary, and a batch whose digest did not fit or has one member;
|
|
4222
|
+
- a card over 3800 characters (Telegram's limit is 4096, and a relay's
|
|
4223
|
+
`Agent: <name>` line needs room): the canonical rendering is never cut to fit;
|
|
4224
|
+
- a policy file that does not match its attestation (`policy-unattested`);
|
|
4225
|
+
- a minimal card the Bot API or a relay refuses (any 4xx but 429): the
|
|
4226
|
+
technical card is sent once instead (`send-refused`); a 429, a 5xx or a
|
|
4227
|
+
network failure is retried by the next cycle as before.
|
|
4228
|
+
|
|
4229
|
+
When the edit that settles a minimal card is refused (it replaces the only
|
|
4230
|
+
message holding the request), a short settle text with the outcome, the
|
|
4231
|
+
request's key and who decided when is sent instead, so a decided card never
|
|
4232
|
+
looks undecided.
|
|
4233
|
+
|
|
4234
|
+
Review cards ("REVIEW — THIS ALREADY RAN"), their note prompt and checkpoint
|
|
4235
|
+
prompts are the same under both styles in this release.
|
|
4236
|
+
|
|
4237
|
+
**`say`.** Keyed by EXACT class name: a pattern such as `files.*` is refused at
|
|
4238
|
+
load (`prompt-say-wildcard`), because one friendly phrase must never stand for
|
|
4239
|
+
several classes. For a class core phrases itself (the list above) core's phrase
|
|
4240
|
+
always wins: a `say` entry may set `quote` and `note` for it but not `does`
|
|
4241
|
+
(`prompt-say-builtin`). The same holds for a class the command classifier
|
|
4242
|
+
emits (`files.delete.out_of_scope`, `vcs.push.main`, `harness.launch.*`, …),
|
|
4243
|
+
whose payload is a command or a file change the runtime phrases itself
|
|
4244
|
+
(`prompt-say-kind`). For any other class `does` is a phrase for OPAQUE
|
|
4245
|
+
payloads only: when a request of that class carries a command, a file change
|
|
4246
|
+
or an email, the runtime's kind phrase is drawn instead and the record notes
|
|
4247
|
+
`say_does_ignored` (below). For those classes `does` is required and is a verb
|
|
4248
|
+
phrase of at most 120 characters that completes "Your agent wants to …": it
|
|
4249
|
+
starts with a letter and has no line break, `.` `!` `?` `…` `:` `;`, markup or
|
|
4250
|
+
`⚠` (`prompt-say-does`), so operator text cannot read as a second sentence or
|
|
4251
|
+
as one of the runtime's notices. `quote` maps every top-level key an opaque
|
|
4252
|
+
payload may carry to the label it is quoted under (letters, digits and spaces,
|
|
4253
|
+
at most 24; `""` only when it is the only key shown) or to `~` for a key
|
|
4254
|
+
deliberately left off, and must show at least one key (`prompt-say-label`,
|
|
4255
|
+
`prompt-say-shape`); `note` is `summary` (the default) or `none`. A `say` entry is read only under `style: minimal`. A class
|
|
4256
|
+
core phrases itself still needs a `say` entry with a `quote` map when its
|
|
4257
|
+
payload is opaque (a Hermes tool payload, `{tool, input}`, is). A `say` entry
|
|
4258
|
+
for a class no rule names loads: it applies if that class is ever requested.
|
|
4259
|
+
|
|
4260
|
+
**Other channels.** `style` and `say` are validated wherever they appear and an
|
|
4261
|
+
unknown value fails the policy closed (`prompt-style-unknown`,
|
|
4262
|
+
`prompt-say-shape`, `prompt-say-wildcard` on an untyped channel; the schema's
|
|
4263
|
+
own `enum`, `pattern` and shape errors on `telegram`, `web` and `cli`). Only
|
|
4264
|
+
Telegram draws a minimal card; the CLI and web channels IGNORE the keys and keep
|
|
4265
|
+
their full prompt, which shows more than was asked for rather than less.
|
|
4266
|
+
|
|
4267
|
+
**What the record says.** A decision taken on a card drawn under a minimal
|
|
4268
|
+
policy carries `payload.rendering` on its `approval.granted` or
|
|
4269
|
+
`approval.rejected`: `{"style": "minimal"}`, or `{"style": "technical",
|
|
4270
|
+
"fallback": "<reason>"}` when the technical card was sent instead. A minimal
|
|
4271
|
+
card whose class's `say` entry set a `does` phrase the runtime did not use
|
|
4272
|
+
(the payload was a command, a file change or an email, so the runtime drew its
|
|
4273
|
+
own phrase) records `{"style": "minimal", "say_does_ignored": true}`. Under a
|
|
4274
|
+
technical policy the key is absent and the record is unchanged. Nothing reads
|
|
4275
|
+
it back; it is a statement about the screen the approver answered on. It is an
|
|
4276
|
+
OPEN field: `schema/event.schema.json` does not constrain it in this release,
|
|
4277
|
+
so a reader with a closed schema for `approval.granted` must admit it.
|
|
4278
|
+
|
|
4279
|
+
**Changing it.** It is a policy key, so changing it is an attestation, and a
|
|
4280
|
+
decision on a request routed under the old policy is refused `policy-drift`, as
|
|
4281
|
+
for any policy change. The Telegram listener resolves `style` and `say` for EACH
|
|
4282
|
+
card it draws, and only from attested bytes: it reads the policy file once,
|
|
4283
|
+
requires those bytes to hash to the latest attestation in the verified log
|
|
4284
|
+
(the digest the gate decides under), and takes the settings from the same
|
|
4285
|
+
bytes. A re-attested setting therefore reaches the next card without a
|
|
4286
|
+
restart, and an edit to the file that nobody attested changes nothing: the card
|
|
4287
|
+
is technical, and when the attested policy asks for minimal the record says
|
|
4288
|
+
`fallback: "policy-unattested"`. An older core refuses the keys at the schema, and the
|
|
4289
|
+
whole policy then fails closed to all-`manual`: roll the core out before the
|
|
4290
|
+
policy that uses them.
|
|
4291
|
+
|
|
3923
4292
|
## channel cli
|
|
3924
4293
|
|
|
3925
4294
|
**The rendering convention (SPEC.md §9).** Every displayed field carries a
|
|
@@ -4236,6 +4605,34 @@ place in the walkthrough and a duplicate message, never a pending request nobody
|
|
|
4236
4605
|
is shown; an approval that depended on a channel's memory would not be an
|
|
4237
4606
|
acceptable trade.
|
|
4238
4607
|
|
|
4608
|
+
**Earlier copies, and `--no-stale-copy`** (APRV-196, APRV-456). Every single-request decision
|
|
4609
|
+
button carries `<verb>:<nonce>:<action ref>` (a digest's "all" buttons carry no reference and never had a fallback), where the nonce is issued by this
|
|
4610
|
+
process and the reference is the first 16 hex of the action key's SHA-256. On
|
|
4611
|
+
the direct-bot shape (no `--api-base`, or one naming `https://api.telegram.org`)
|
|
4612
|
+
a tap whose nonce this process is not holding, such as a button on a copy sent
|
|
4613
|
+
before a restart, falls back to its reference: when this process holds that
|
|
4614
|
+
action open on a newer card, the tap decides it there, and the gate judges it
|
|
4615
|
+
exactly as it judges any other tap. That fallback is safe because only the bot
|
|
4616
|
+
token can put a button in front of the approver.
|
|
4617
|
+
|
|
4618
|
+
The fallback is **off** when `--api-base` names anything other than the Bot API
|
|
4619
|
+
(a relay, or a self-hosted Bot API server, which this rule treats as a relay)
|
|
4620
|
+
and when `--no-stale-copy` is passed. Then a decision tap whose nonce this
|
|
4621
|
+
process is not holding is refused `nonce-not-issued` (a member of the channel
|
|
4622
|
+
decision refusal union, beside `sender-unmapped`): it never reaches the gate,
|
|
4623
|
+
the approver's toast says this listener did not send that button, and the live
|
|
4624
|
+
card stays armed. When the tap's reference names a request this process holds
|
|
4625
|
+
open, one `audit.decision_refused` records the attempt under that request's key,
|
|
4626
|
+
naming the sender the policy resolves; a reference that names nothing open here,
|
|
4627
|
+
or a replay of the same button within this process, records nothing (the memory of refused buttons is per process and bounded, so a redelivery after a restart can record once more). The restart banner says that
|
|
4628
|
+
earlier copies no longer decide. The reason is the relay: anyone holding its
|
|
4629
|
+
token can send the approver a card with forged text and a real pending action's
|
|
4630
|
+
reference, and on the fallback the approver's genuine tap would pass the chat
|
|
4631
|
+
check and the sender mapping. The flag is launch configuration only: no policy
|
|
4632
|
+
key and nothing in an update can set it or clear it. `approval up` and
|
|
4633
|
+
[`channel telegram webhook`](#channel-telegram-webhook) take the same flag and
|
|
4634
|
+
apply the same rule.
|
|
4635
|
+
|
|
4239
4636
|
**The execution token is printed on this terminal's stdout and is never sent to
|
|
4240
4637
|
Telegram.** A chat transcript is stored on someone else's servers, backed up to
|
|
4241
4638
|
phones, and readable by anyone later added to the chat — it is not a credential
|
|
@@ -4279,16 +4676,61 @@ Each `audit.sampled` with no later `audit.reviewed` now arrives as a **review
|
|
|
4279
4676
|
card**.
|
|
4280
4677
|
|
|
4281
4678
|
A card is not a prompt and says so. Its headline is `REVIEW — THIS ALREADY RAN`,
|
|
4282
|
-
it carries no
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4679
|
+
it carries no approve button, and it accepts no token: the action has happened,
|
|
4680
|
+
and a card offering an approve would present a settled fact as a live
|
|
4681
|
+
authorization. What it carries is the same rows a prompt would show for the same
|
|
4682
|
+
action — class, the command breakdown, task, the agent's claimed summary, the
|
|
4683
|
+
model gloss where one is attached — plus two the card adds: `ran at` (the
|
|
4684
|
+
`execution.started` the sample named) and `verdict` (that the runtime allowed
|
|
4685
|
+
this without asking, which autonomy said so, the rate it was drawn at, and how
|
|
4686
|
+
the log says it ended). Everything computed is derived from the verified log,
|
|
4687
|
+
the payload store and the classifier; the claimed rows sit under the same "NOT
|
|
4688
|
+
verified by the runtime" heading a prompt gives them.
|
|
4689
|
+
|
|
4690
|
+
**The card shows the payload that ran (APRV-480).** Under supervised-retro the
|
|
4691
|
+
review is the individual approval nobody gave before the action executed, so the
|
|
4692
|
+
reviewer reads the published bytes rather than a key and the agent's own summary
|
|
4693
|
+
of what it did. Between the computed and the claimed rows the card carries one
|
|
4694
|
+
of three payload regions, and its heading says which:
|
|
4695
|
+
|
|
4696
|
+
| Heading | What the reviewer sees | When |
|
|
4697
|
+
| --- | --- | --- |
|
|
4698
|
+
| `PAYLOAD — the bytes that ran, shown whole; this review covers them` | the canonical rendering of the bytes, whole, in a `<pre>` block | the runtime holds bytes that hash to the execution's binding and they fit one card |
|
|
4699
|
+
| `PAYLOAD — NOT SHOWN, hash only; this review does not cover the bytes` | the binding's `sha256`, and why the bytes are absent | nobody holds the bytes, the bytes held do not hash to the binding, or they are longer than this card's payload budget (at most 2000 escaped characters, less when the card's rows leave less room) |
|
|
4700
|
+
| `PAYLOAD — none recorded; the execution bound to no payload hash` | nothing to show and nothing to name | the execution and its registration recorded no `payload_hash` |
|
|
4701
|
+
|
|
4702
|
+
The bytes are shown whole or not at all: a card is one message edited in place,
|
|
4703
|
+
so it cannot spill a payload over several messages the way a request prompt
|
|
4704
|
+
does, and a review over half the bytes would claim more than the reviewer read.
|
|
4705
|
+
Only the first case lets the review record carry a `payload_hash` (see
|
|
4706
|
+
[the review record](#the-review-record)). A terminal review (`approval audit
|
|
4707
|
+
review`) shows no payload and so never records one.
|
|
4708
|
+
|
|
4709
|
+
**The whole card fits one message (PR #614 refutation F4).** The agent's summary
|
|
4710
|
+
is cut at 400 characters and every other row at 300, each with a marker saying
|
|
4711
|
+
how many characters are not shown (the registration record keeps the text
|
|
4712
|
+
whole); a card whose rows are pathologically long cuts them further, in steps,
|
|
4713
|
+
until it fits. A notice shows at most 600 characters, the rest on the
|
|
4714
|
+
listener's stderr. The payload budget is what the card's own rows leave under
|
|
4715
|
+
Telegram's 4096-character limit after the longest heading and the largest
|
|
4716
|
+
notice a tap can add, capped at 2000, and it is computed from the card alone,
|
|
4717
|
+
so the card a reviewer saw and the view a tap records cannot disagree.
|
|
4718
|
+
|
|
4719
|
+
**A card that fails to send (PR #614 recheck NF-2).** When Telegram refuses the
|
|
4720
|
+
card itself (HTTP 400 `message is too long`, `can't parse entities`,
|
|
4721
|
+
`message text is empty` or `text must be non-empty`, `reply markup is too long`,
|
|
4722
|
+
`BUTTON_DATA_INVALID`, `ENTITIES_TOO_LONG`), resending it can only fail again:
|
|
4723
|
+
the listener prints a coded `approval: telegram review-offer-failed:` line
|
|
4724
|
+
naming `approval audit review <seq> --ok` (or `--deny`), leaves that sample for
|
|
4725
|
+
a terminal review, and offers the next one. Every other failure (a 429, a
|
|
4726
|
+
timeout, a 5xx, a network error) prints `approval: telegram review-offer-retry:`
|
|
4727
|
+
and pauses review cards for Telegram's `retry_after` or the backoff, whichever
|
|
4728
|
+
is longer (one minute, doubling per attempt; never more than an hour, whatever
|
|
4729
|
+
`retry_after` says), then offers the same sample again.
|
|
4730
|
+
After five failed attempts the sample is left for a terminal review with the
|
|
4731
|
+
`review-offer-failed` line. Either way the sample stays open in
|
|
4732
|
+
`approval audit list` and QUEUE.md, the queue behind it no longer waits, and a
|
|
4733
|
+
restarted listener offers it afresh.
|
|
4292
4734
|
|
|
4293
4735
|
Six buttons, bare emoji and no words (APRV-302), in two rows: the verdict on the
|
|
4294
4736
|
first (✅ OK, 🛑 Deny) and the grade on the second, worst to best (👎 disliked,
|
|
@@ -4296,12 +4738,39 @@ first (✅ OK, 🛑 Deny) and the grade on the second, worst to best (👎 disli
|
|
|
4296
4738
|
|
|
4297
4739
|
| Tap | What is recorded |
|
|
4298
4740
|
| --- | --- |
|
|
4299
|
-
| ✅ | `audit.reviewed` with verdict `ok
|
|
4300
|
-
| a reaction |
|
|
4301
|
-
|
|
|
4302
|
-
| 🛑
|
|
4741
|
+
| ✅ | `audit.reviewed` with verdict `ok`, and the held grade if there is one. |
|
|
4742
|
+
| a reaction, nothing armed | **Nothing.** Refused `verdict-required` (APRV-482): a grade is not a verdict. The card holds the grade and says `GRADE … HELD` in its heading. |
|
|
4743
|
+
| ✅ after a reaction | verdict `ok` with the held grade (a `loved` or `disliked` asks for words first). |
|
|
4744
|
+
| 🛑 once | **Nothing.** The tap reaches the runtime with no verdict, and the card arms (it says `DENY ARMED` on itself) only when the answer is `verdict-required`, which the runtime gives only to a mapped sender on the class's roster. Any other refusal is shown on the card and arms nothing. |
|
|
4745
|
+
| 🛑 twice | verdict `denied` with the held grade if any, and the reconciliation obligation it opens is named on the reply. |
|
|
4303
4746
|
| a reaction with deny armed | verdict `denied` with that grade. |
|
|
4304
4747
|
|
|
4748
|
+
**A held grade and an armed Deny belong to the account that tapped them (PR #614
|
|
4749
|
+
refutation F3).** The card remembers the transport's sender id with each. A tap
|
|
4750
|
+
that would finish another account's half-finished review records nothing and is
|
|
4751
|
+
refused on the card with one line: any ✅ or 🛑 while another account holds the
|
|
4752
|
+
grade or armed Deny, and any reaction while another account armed Deny. A
|
|
4753
|
+
reaction tapped with nothing armed only replaces the held grade, and its tapper
|
|
4754
|
+
then holds it. A record therefore never carries a grade or a denial its reviewer
|
|
4755
|
+
did not tap. Only an account that may review can leave either behind (PR #614
|
|
4756
|
+
recheck NF-1): a grade is held, and a Deny arms, only after the runtime has
|
|
4757
|
+
passed the sender and the roster and answered `verdict-required`, so an
|
|
4758
|
+
unmapped or off-roster account in the approver chat cannot block the
|
|
4759
|
+
approvers' taps.
|
|
4760
|
+
|
|
4761
|
+
**A verdict is an explicit act (APRV-482).** Under supervised-retro a review
|
|
4762
|
+
counts as the individual approval nobody gave before the action ran, so nothing
|
|
4763
|
+
records `ok` on a reviewer's behalf. The explicit forms per surface:
|
|
4764
|
+
|
|
4765
|
+
| Surface | `ok` | `denied` | Refused `verdict-required` |
|
|
4766
|
+
| --- | --- | --- | --- |
|
|
4767
|
+
| Telegram card | ✅ (alone, or after a grade) | 🛑 twice, or 🛑 then a grade | a grade tapped with nothing armed |
|
|
4768
|
+
| `approval audit review` | `--ok` | `--deny` | neither flag, with or without `--reaction`; both flags is a usage error (exit 2) |
|
|
4769
|
+
|
|
4770
|
+
The refusal on the card is the runtime's own: the tap reaches `reviewSample`
|
|
4771
|
+
with no verdict, and the code and its message come back exactly as a terminal
|
|
4772
|
+
sees them.
|
|
4773
|
+
|
|
4305
4774
|
The card does not print this table under itself. It used to, and the paragraph
|
|
4306
4775
|
of rules pushed the rows a review is actually about off the first screen for a
|
|
4307
4776
|
reader who had read them on the card before. The two things a tap could get
|
|
@@ -4334,7 +4803,10 @@ reviewer has to be able to say which half they meant.
|
|
|
4334
4803
|
|
|
4335
4804
|
Every append goes through the same `reviewSample` that `approval audit review`
|
|
4336
4805
|
calls, recorded against the human identity this listener was configured with
|
|
4337
|
-
(`--as` / `APPROVAL_HUMAN`), never anything the callback carried.
|
|
4806
|
+
(`--as` / `APPROVAL_HUMAN`), never anything the callback carried. That identity,
|
|
4807
|
+
or the person a sender mapping resolves a tap to, must be on the class's
|
|
4808
|
+
`approvers` roster where the rule names one, or the card shows
|
|
4809
|
+
`actor-not-approver` and keeps its buttons (APRV-483). So `approval
|
|
4338
4810
|
feedback` shows a reaction given on a card exactly as one given at a terminal:
|
|
4339
4811
|
same record, same `human:<id>`, same everything.
|
|
4340
4812
|
|
|
@@ -4459,7 +4931,10 @@ a gate.
|
|
|
4459
4931
|
|
|
4460
4932
|
**Only the arrival changes, and that is the whole claim.** The pending queue is
|
|
4461
4933
|
re-derived from the verified log every cycle by the same `dispatchPending`. The
|
|
4462
|
-
handlers are the same
|
|
4934
|
+
handlers are the same function objects `listen` registers (since APRV-456 that
|
|
4935
|
+
includes the recorder for a `nonce-not-issued` refusal, and `--no-stale-copy`
|
|
4936
|
+
or a relayed `--api-base` turns the earlier-copy fallback off here exactly as it
|
|
4937
|
+
does there). A tap becomes a
|
|
4463
4938
|
decision through the same `handleUpdate`, the same `callback_query.from.id`
|
|
4464
4939
|
reading, the same `approvers.<id>.senders` resolution, the same
|
|
4465
4940
|
`recordChannelDecision` and the same annotate-after-decision edit. There is no
|
|
@@ -6163,6 +6638,13 @@ A 409 that still happens at runtime — another machine, or a poller started
|
|
|
6163
6638
|
outside this runtime — is reported once, with the instances the registry knows
|
|
6164
6639
|
about, and its repeats are counted rather than reprinted.
|
|
6165
6640
|
|
|
6641
|
+
**A relayed Telegram channel refuses buttons it is not holding** (APRV-456). An
|
|
6642
|
+
`--api-base` that is not `https://api.telegram.org`, or `--no-stale-copy`, turns
|
|
6643
|
+
off the earlier-copy fallback, and a decision tap whose nonce this process is
|
|
6644
|
+
not holding is refused `nonce-not-issued` instead of reaching the gate. The rule
|
|
6645
|
+
and its reason are under [`channel telegram listen`](#channel-telegram-listen)
|
|
6646
|
+
("Earlier copies, and `--no-stale-copy`").
|
|
6647
|
+
|
|
6166
6648
|
**The ambient runtime: the daemon loop and every configured channel in one
|
|
6167
6649
|
supervised foreground process.** `approval daemon run --with-channels` is the
|
|
6168
6650
|
same verb spelled from the other side, and it reaches the same function before
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "approval-md",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Human approval for agent actions. CLI runtime for the approval.md convention (pre-release).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"better-sqlite3": "12.11.1",
|
|
60
60
|
"yaml": "2.9.0"
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "5f9b9c3da2b0b220b344590d6d5319d27edf3b61"
|
|
63
63
|
}
|