approval-md 0.4.0 → 0.4.2
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 +11 -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/attest.js +2 -1
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +1 -1
- package/dist/src/cli/audit-card.js +40 -1
- package/dist/src/cli/audit-card.js.map +1 -1
- package/dist/src/cli/audit.d.ts +7 -5
- package/dist/src/cli/audit.js +25 -12
- 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 +172 -16
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel-web.js +2 -1
- package/dist/src/cli/channel-web.js.map +1 -1
- package/dist/src/cli/channel.js +2 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/doctor.js +1 -1
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +12 -0
- package/dist/src/cli/execute.js +50 -4
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/gate.js +2 -1
- package/dist/src/cli/gate.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 +121 -3
- package/dist/src/cli/hook.js +719 -79
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/log-checkpoint.js +2 -1
- package/dist/src/cli/log-checkpoint.js.map +1 -1
- package/dist/src/cli/log-unlock.d.ts +23 -0
- package/dist/src/cli/log-unlock.js +110 -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/setup-common.js +2 -1
- package/dist/src/cli/setup-common.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/vault.js +2 -1
- package/dist/src/cli/vault.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/attest-bootstrap.js +2 -1
- package/dist/src/core/attest-bootstrap.js.map +1 -1
- package/dist/src/core/attest.js +5 -4
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +113 -9
- package/dist/src/core/audit.js +382 -18
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/budgets.d.ts +12 -0
- package/dist/src/core/budgets.js +54 -1
- package/dist/src/core/budgets.js.map +1 -1
- package/dist/src/core/checkpoint.js +3 -2
- package/dist/src/core/checkpoint.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/decision-refusal.js +2 -1
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/delegation.d.ts +109 -0
- package/dist/src/core/delegation.js +244 -0
- package/dist/src/core/delegation.js.map +1 -0
- package/dist/src/core/execute.js +12 -3
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate-window.js +4 -3
- package/dist/src/core/gate-window.js.map +1 -1
- package/dist/src/core/gate.d.ts +84 -43
- package/dist/src/core/gate.js +114 -6
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.js +2 -1
- package/dist/src/core/gesture-refusal.js.map +1 -1
- package/dist/src/core/identity.d.ts +105 -0
- package/dist/src/core/identity.js +122 -0
- package/dist/src/core/identity.js.map +1 -0
- 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/loop.d.ts +11 -0
- package/dist/src/core/loop.js +63 -4
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/policy-diff.js +20 -1
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.js +18 -2
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +38 -1
- package/dist/src/core/policy-load.js +34 -2
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.js +30 -5
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +17 -0
- package/dist/src/core/policy-proposal.js +32 -16
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +186 -1
- package/dist/src/core/prompt-layout.js +345 -3
- package/dist/src/core/prompt-layout.js.map +1 -1
- package/dist/src/core/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/token.js +11 -0
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/tool-map.d.ts +125 -0
- package/dist/src/core/tool-map.js +179 -0
- package/dist/src/core/tool-map.js.map +1 -0
- 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 +15 -0
- package/docs/cli-reference.md +571 -28
- package/package.json +2 -2
- package/schema/event.schema.json +387 -29
- 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-model-actor.json +20 -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-reviewed-verdict-source-model.json +20 -0
- package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
- package/schema/fixtures/event/invalid/execution-started-harness-tool-space.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
- package/schema/fixtures/event/valid/audit-sampled.json +2 -1
- package/schema/fixtures/event/valid/daemon-identity.json +2 -1
- package/schema/fixtures/event/valid/execution-started-harness-tool.json +19 -0
- package/schema/fixtures/policy/invalid/delegation-advice-string.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-class-duplicate.json +18 -0
- package/schema/fixtures/policy/invalid/delegation-class-wildcard.json +17 -0
- package/schema/fixtures/policy/invalid/delegation-daily-cap-fractional.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-daily-cap-negative.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-daily-cap-too-high.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-escalate-duplicate.json +18 -0
- package/schema/fixtures/policy/invalid/delegation-escalate-unknown.json +17 -0
- package/schema/fixtures/policy/invalid/delegation-max-autonomy-alias.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-max-autonomy-autonomous.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-model-no-version.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-model-provider-form.json +15 -0
- package/schema/fixtures/policy/invalid/delegation-null.json +7 -0
- package/schema/fixtures/policy/invalid/delegation-reviewer-agent.json +17 -0
- package/schema/fixtures/policy/invalid/delegation-reviewer-bare-id.json +17 -0
- package/schema/fixtures/policy/invalid/delegation-unknown-key.json +16 -0
- package/schema/fixtures/policy/invalid/tools-class-wildcard.json +9 -0
- package/schema/fixtures/policy/invalid/tools-match-double-star.json +9 -0
- package/schema/fixtures/policy/invalid/unmapped-tool-unknown.json +7 -0
- package/schema/fixtures/policy/valid/delegation-empty.json +7 -0
- package/schema/fixtures/policy/valid/delegation-grammar-full.json +41 -0
- package/schema/fixtures/policy/valid/delegation-off.json +15 -0
- package/schema/fixtures/policy/valid/tools-mapping.json +17 -0
- package/schema/policy.schema.json +178 -0
package/dist/src/core/gate.d.ts
CHANGED
|
@@ -119,6 +119,7 @@ import { type ClockOptions } from "./clock.js";
|
|
|
119
119
|
import { type HarnessProvenance } from "./harness-version.js";
|
|
120
120
|
import { type AppendError, type AppendOptions, type EventRecord, type LogHead } from "./log.js";
|
|
121
121
|
import { type UsdInput } from "./money.js";
|
|
122
|
+
import type { PromptRendering } from "./prompt-layout.js";
|
|
122
123
|
import { type Autonomy, type PolicyLoadResult } from "./policy-load.js";
|
|
123
124
|
import { type Resolution } from "./policy-match.js";
|
|
124
125
|
import { type DrawAsker, type DrawRefusalReason, type LiveDrawRecord } from "./live-draw.js";
|
|
@@ -134,6 +135,30 @@ import { type ValidationError } from "./validate.js";
|
|
|
134
135
|
export { requestState, type Decision, type DeclaredAction, type ExecutionFacts, type RequestDerivation, type RequestState, WITHDRAW_REASONS, isWithdrawReason, type WithdrawReason, } from "./state.js";
|
|
135
136
|
/** Actor stamped on runtime-originated expiry events (SPEC.md §8 `system:`). */
|
|
136
137
|
export declare const EXPIRY_ACTOR = "system:gate";
|
|
138
|
+
/**
|
|
139
|
+
* Does an `approvers` list name this actor (APRV-137, amended SPEC.md §5.2)?
|
|
140
|
+
*
|
|
141
|
+
* The spelling a valid policy uses is the bare id (`alice`), which is what
|
|
142
|
+
* `policy.schema.json` admits and what the keys of the top-level `approvers`
|
|
143
|
+
* map are: its `identifier` pattern is lowercase alphanumerics with `_` and
|
|
144
|
+
* `-`, so a `human:` prefix inside a roster is a schema violation and never
|
|
145
|
+
* reaches here. The whole actor string is compared as well, which can only ever
|
|
146
|
+
* match a loader more permissive than the shipped schema; it widens nothing for
|
|
147
|
+
* a valid policy, because `alice` and `human:alice` are the same person under
|
|
148
|
+
* either comparison.
|
|
149
|
+
*
|
|
150
|
+
* Comparison is exact and case-sensitive. An identity that matched under
|
|
151
|
+
* folding would let `human:Alice` and `human:alice` be one approver on one host
|
|
152
|
+
* and two on another, and a roster is a list of people rather than a pattern
|
|
153
|
+
* language. An empty list names nobody and therefore matches nobody;
|
|
154
|
+
* `approvers` carries `minItems: 1`, so a valid policy cannot produce one, and
|
|
155
|
+
* that branch stays a fail-closed backstop rather than a reachable path.
|
|
156
|
+
*
|
|
157
|
+
* Exported for `core/audit.ts` (APRV-483), whose review check is this one
|
|
158
|
+
* comparison applied to a reviewer, so a grant and a review can never disagree
|
|
159
|
+
* about who is on a roster.
|
|
160
|
+
*/
|
|
161
|
+
export declare function namesApprover(approvers: readonly string[], actor: string): boolean;
|
|
137
162
|
/**
|
|
138
163
|
* The closed set of gate refusal codes. Agents branch on these, so the union is
|
|
139
164
|
* frozen public API in the same sense the exit codes are: adding a code is a
|
|
@@ -567,7 +592,12 @@ export interface GateRefusal {
|
|
|
567
592
|
limits?: IntakeVerdict[];
|
|
568
593
|
/** Schema errors, when `code` is `envelope-invalid`. */
|
|
569
594
|
errors?: ValidationError[];
|
|
570
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* The underlying append error, when `code` is `append-failed`; and, on a
|
|
597
|
+
* `budget-exceeded` from {@link startHarnessExecution}, the error that kept
|
|
598
|
+
* its `budget.exceeded` record out (APRV-478). Never present beside a
|
|
599
|
+
* `record`.
|
|
600
|
+
*/
|
|
571
601
|
append?: AppendError;
|
|
572
602
|
/**
|
|
573
603
|
* The two policy hashes actually compared, when `code` is `policy-drift`
|
|
@@ -686,6 +716,22 @@ type ReadOutcome = {
|
|
|
686
716
|
* underneath it.
|
|
687
717
|
*/
|
|
688
718
|
export declare function readGateRecords(logPath: string, schemaDir?: string): ReadOutcome;
|
|
719
|
+
/**
|
|
720
|
+
* The policy file the gate will hash for attestation.
|
|
721
|
+
*
|
|
722
|
+
* `file` wins; otherwise discovery walks `POLICY_FILENAMES` in `dir` exactly as
|
|
723
|
+
* `loadPolicy` does, so the attested file and the enforced file are the same
|
|
724
|
+
* file. When neither exists the first candidate is returned anyway, so
|
|
725
|
+
* `checkAttestation` reports `unreadable` and the gate refuses — a missing
|
|
726
|
+
* policy is never a pass.
|
|
727
|
+
*/
|
|
728
|
+
/**
|
|
729
|
+
* The policy file a gate operation reads: `policy.file`, or the first policy
|
|
730
|
+
* filename found in `policy.dir` (default: the working directory). Exported for
|
|
731
|
+
* `core/audit.ts` (APRV-483), whose roster check must read the same file a
|
|
732
|
+
* grant would and hold it to the same attestation.
|
|
733
|
+
*/
|
|
734
|
+
export declare function policyPathOf(options: GateOptions): string;
|
|
689
735
|
/**
|
|
690
736
|
* The policy as a gate operation would load it: one read through the seam of
|
|
691
737
|
* {@link GateOptions.policy}, parsed, failing closed (APRV-324).
|
|
@@ -780,6 +826,13 @@ export interface RegisterOptions extends GateOptions {
|
|
|
780
826
|
harness?: HarnessProvenance;
|
|
781
827
|
/** Set by `propose` alone: the `propose:` task namespace is its (APRV-445). */
|
|
782
828
|
proposal?: true;
|
|
829
|
+
/**
|
|
830
|
+
* Set by the harness hook alone (APRV-499, claude-edge ruling): the one
|
|
831
|
+
* caller that may declare {@link UNMAPPED_TOOL_CLASS}. A CALL option for the
|
|
832
|
+
* reason `harness` is one: an envelope is authored by the party under
|
|
833
|
+
* oversight, and no CLI flag, task file or HTTP route sets this.
|
|
834
|
+
*/
|
|
835
|
+
toolMapHook?: true;
|
|
783
836
|
}
|
|
784
837
|
/**
|
|
785
838
|
* The `task.registered` record for `task`, or the `not-registered` refusal.
|
|
@@ -1124,6 +1177,17 @@ export interface DecideOptions extends GateOptions {
|
|
|
1124
1177
|
* silently mixed in with policy-attested ones.
|
|
1125
1178
|
*/
|
|
1126
1179
|
senderSource?: "policy";
|
|
1180
|
+
/**
|
|
1181
|
+
* Which prompt layout the approver decided on (APRV-489), recorded as
|
|
1182
|
+
* `payload.rendering` on `approval.granted` / `approval.rejected` only.
|
|
1183
|
+
*
|
|
1184
|
+
* Set by a decision surface that drew a non-default layout, or that was asked
|
|
1185
|
+
* for one and drew the technical card instead (then `fallback` names why).
|
|
1186
|
+
* Absent under a technical policy, so those records are byte-identical to
|
|
1187
|
+
* every earlier build's. A record of the screen and never an input: no check
|
|
1188
|
+
* here or anywhere reads it back.
|
|
1189
|
+
*/
|
|
1190
|
+
rendering?: PromptRendering;
|
|
1127
1191
|
}
|
|
1128
1192
|
export type DecideResult = {
|
|
1129
1193
|
ok: true;
|
|
@@ -1318,6 +1382,16 @@ export interface ConsumeHarnessOptions extends GateOptions {
|
|
|
1318
1382
|
* else, absence included, records `carried`.
|
|
1319
1383
|
*/
|
|
1320
1384
|
spendingTask?: string;
|
|
1385
|
+
/**
|
|
1386
|
+
* The harness tool NAME of the call doing the spending, when its class came
|
|
1387
|
+
* from the policy's tool mapping (APRV-499, refutation S3): written to the
|
|
1388
|
+
* start as `harness_tool`, exactly as the policy-authorized start writes it,
|
|
1389
|
+
* so every start a mapped or unmapped harness call produces names its tool.
|
|
1390
|
+
* It authorizes nothing: the spend is bound by `presentedPayloadHash`, whose
|
|
1391
|
+
* payload already carries the name, so a name that disagreed with the
|
|
1392
|
+
* approved bytes would have been refused `payload-mismatch` first.
|
|
1393
|
+
*/
|
|
1394
|
+
harnessTool?: string;
|
|
1321
1395
|
}
|
|
1322
1396
|
/**
|
|
1323
1397
|
* How the authorization reached the process that spent it (APRV-200).
|
|
@@ -1452,53 +1526,20 @@ export interface HarnessStartInput {
|
|
|
1452
1526
|
payload_hash?: string;
|
|
1453
1527
|
/** Canonical decimal USD string (APRV-121); a JSON number is read as the historical form. */
|
|
1454
1528
|
est_cost_usd?: UsdInput;
|
|
1529
|
+
/**
|
|
1530
|
+
* The harness tool NAME the start records, when the class came from the
|
|
1531
|
+
* policy's tool mapping rather than from what the call does (APRV-499):
|
|
1532
|
+
* a `tools` entry, or `harness.tool.unmapped`. Several tools share one
|
|
1533
|
+
* such class, so the class alone cannot say which ran. The name and never the
|
|
1534
|
+
* arguments: those are bound by `payload_hash` and stay out of the log.
|
|
1535
|
+
* Shape-checked by the event schema at the write boundary.
|
|
1536
|
+
*/
|
|
1537
|
+
harness_tool?: string;
|
|
1455
1538
|
}
|
|
1456
1539
|
export type HarnessStartResult = {
|
|
1457
1540
|
ok: true;
|
|
1458
1541
|
record: EventRecord;
|
|
1459
1542
|
} | GateRefusal;
|
|
1460
|
-
/**
|
|
1461
|
-
* Charge and record a harness execution that no human was asked about
|
|
1462
|
-
* (APRV-141).
|
|
1463
|
-
*
|
|
1464
|
-
* ## The blind spot this closes
|
|
1465
|
-
*
|
|
1466
|
-
* `core/budgets.ts` computes consumption from `approval.granted` and
|
|
1467
|
-
* `execution.started`, and `core/audit.ts` draws its retrospective sample from
|
|
1468
|
-
* `execution.started` alone. The harness hook wrote neither for a supervised or
|
|
1469
|
-
* autonomous verdict — the comment said, correctly, that writing one per agent
|
|
1470
|
-
* action fills the log — so under Claude Code the majority of real activity
|
|
1471
|
-
* consumed no budget, `daily_actions` included, and was invisible to the
|
|
1472
|
-
* overseer that exists to read a sample of it. A budget that the busiest
|
|
1473
|
-
* execution path does not charge is not a budget, and the decision recorded on
|
|
1474
|
-
* APRV-141 is that the log volume is the lesser cost.
|
|
1475
|
-
*
|
|
1476
|
-
* ## Why this record and not a new event type
|
|
1477
|
-
*
|
|
1478
|
-
* It is the same `execution.started` {@link consumeHarnessGrant} appends, with
|
|
1479
|
-
* the same `execution: "harness"` marker saying why no `execution.completed` or
|
|
1480
|
-
* `execution.failed` will ever follow: the harness runs the command and this
|
|
1481
|
-
* runtime never observes an exit status. Reusing the shape means budgets and
|
|
1482
|
-
* audit count these without learning a second vocabulary, and the gate's
|
|
1483
|
-
* existing single-use rule (a key with an `execution.started` is
|
|
1484
|
-
* `already-executed`) applies unchanged. What differs is only the authorization
|
|
1485
|
-
* being recorded: there, a human's grant; here, the policy itself.
|
|
1486
|
-
*
|
|
1487
|
-
* ## What it refuses
|
|
1488
|
-
*
|
|
1489
|
-
* The same two facts the hook's own guard checks and `core/execute.ts` checks
|
|
1490
|
-
* before an unattended start — attestation and loop-escalation — re-checked at
|
|
1491
|
-
* the write boundary against the records this append is authorized by, plus the
|
|
1492
|
-
* budget verdict this record is the charge for. A class that resolves `manual`
|
|
1493
|
-
* is refused outright: a manual action is authorized by a grant and spent
|
|
1494
|
-
* through {@link consumeHarnessGrant} or a token, and admitting one here would
|
|
1495
|
-
* be a second, unapproved spender.
|
|
1496
|
-
*
|
|
1497
|
-
* Since APRV-146 the content binding is refused here too. `payload-hash-required`
|
|
1498
|
-
* says the caller named no bytes, and it is a refusal rather than an omitted
|
|
1499
|
-
* field because a start event with no `payload_hash` is a record that says
|
|
1500
|
-
* something ran without saying what — the state APRV-140 closed everywhere else.
|
|
1501
|
-
*/
|
|
1502
1543
|
export declare function startHarnessExecution(logPath: string, input: HarnessStartInput, actor: string, options?: GateOptions): HarnessStartResult;
|
|
1503
1544
|
/**
|
|
1504
1545
|
* Which untrusted reporter asserted a harness outcome. CLOSED, and extended only
|
package/dist/src/core/gate.js
CHANGED
|
@@ -116,6 +116,7 @@ import { basename, join } from "node:path";
|
|
|
116
116
|
import { isPrincipalActor } from "./actor.js";
|
|
117
117
|
import { ATTESTATION_REFUSAL, attestationRefusal, checkAttestationOfBytes, isPolicySha256, POLICY_HASH_FIELD, unreadablePolicyStatus, } from "./attest.js";
|
|
118
118
|
import { evaluateBudgetsWithTask } from "./budgets.js";
|
|
119
|
+
import { toolMapVerdict, UNMAPPED_TOOL_CLASS } from "./tool-map.js";
|
|
119
120
|
import { evaluateIntakeLimits, intakeRefusalOf, } from "./intake-limits.js";
|
|
120
121
|
import { tick } from "./clock.js";
|
|
121
122
|
import { readTaskFile } from "./frontmatter.js";
|
|
@@ -135,6 +136,7 @@ import { payloadOf, readVerifiedRecords, requestState, } from "./state.js";
|
|
|
135
136
|
import { mintToken, tokenHash, TOKEN_HASH_FIELD } from "./token.js";
|
|
136
137
|
import { validate } from "./validate.js";
|
|
137
138
|
import { displayHashOf, DISPLAY_HASH_FIELD } from "./wysiwys.js";
|
|
139
|
+
import { HUMAN_ACTOR_RULE, reservedKindNote } from "./identity.js";
|
|
138
140
|
/**
|
|
139
141
|
* The approval-state derivation moved to `core/state.ts` in APRV-20 (finding
|
|
140
142
|
* S4: `gate.ts` and `token.ts` imported each other). It is re-exported here, its
|
|
@@ -145,7 +147,7 @@ export { requestState, WITHDRAW_REASONS, isWithdrawReason, } from "./state.js";
|
|
|
145
147
|
/** Actor stamped on runtime-originated expiry events (SPEC.md §8 `system:`). */
|
|
146
148
|
export const EXPIRY_ACTOR = "system:gate";
|
|
147
149
|
/** Actors permitted to decide. Human-only, in code (SPEC.md §10.1). */
|
|
148
|
-
const HUMAN_ACTOR =
|
|
150
|
+
const HUMAN_ACTOR = HUMAN_ACTOR_RULE;
|
|
149
151
|
/**
|
|
150
152
|
* Does an `approvers` list name this actor (APRV-137, amended SPEC.md §5.2)?
|
|
151
153
|
*
|
|
@@ -164,8 +166,12 @@ const HUMAN_ACTOR = /^human:.+/u;
|
|
|
164
166
|
* language. An empty list names nobody and therefore matches nobody;
|
|
165
167
|
* `approvers` carries `minItems: 1`, so a valid policy cannot produce one, and
|
|
166
168
|
* that branch stays a fail-closed backstop rather than a reachable path.
|
|
169
|
+
*
|
|
170
|
+
* Exported for `core/audit.ts` (APRV-483), whose review check is this one
|
|
171
|
+
* comparison applied to a reviewer, so a grant and a review can never disagree
|
|
172
|
+
* about who is on a roster.
|
|
167
173
|
*/
|
|
168
|
-
function namesApprover(approvers, actor) {
|
|
174
|
+
export function namesApprover(approvers, actor) {
|
|
169
175
|
const bare = actor.startsWith("human:") ? actor.slice("human:".length) : actor;
|
|
170
176
|
return approvers.some((name) => name === actor || name === bare);
|
|
171
177
|
}
|
|
@@ -618,7 +624,13 @@ export function readGateRecords(logPath, schemaDir) {
|
|
|
618
624
|
* `checkAttestation` reports `unreadable` and the gate refuses — a missing
|
|
619
625
|
* policy is never a pass.
|
|
620
626
|
*/
|
|
621
|
-
|
|
627
|
+
/**
|
|
628
|
+
* The policy file a gate operation reads: `policy.file`, or the first policy
|
|
629
|
+
* filename found in `policy.dir` (default: the working directory). Exported for
|
|
630
|
+
* `core/audit.ts` (APRV-483), whose roster check must read the same file a
|
|
631
|
+
* grant would and hold it to the same attestation.
|
|
632
|
+
*/
|
|
633
|
+
export function policyPathOf(options) {
|
|
622
634
|
const policy = options.policy ?? {};
|
|
623
635
|
if (policy.file !== undefined)
|
|
624
636
|
return policy.file;
|
|
@@ -985,6 +997,19 @@ function attemptRegister(logPath, source, actor, options) {
|
|
|
985
997
|
if (!validation.ok) {
|
|
986
998
|
return refuse("envelope-invalid", `the envelope failed schema validation; nothing was appended`, { errors: validation.errors });
|
|
987
999
|
}
|
|
1000
|
+
// APRV-499 (claude-edge ruling): `harness.tool.unmapped` is reserved to the
|
|
1001
|
+
// harness hook, the way `harness.launch.*` is reserved to a rule. It names
|
|
1002
|
+
// "a tool call no `tools` entry claimed", a fact only the hook establishes,
|
|
1003
|
+
// and under `unmapped_tool: record` it resolves autonomous, so a task
|
|
1004
|
+
// envelope, `approval propose` or an HTTP registration declaring it would be
|
|
1005
|
+
// borrowing that autonomy for an action nobody classified. Refused before
|
|
1006
|
+
// the log is read; nothing is appended.
|
|
1007
|
+
if (options.toolMapHook !== true) {
|
|
1008
|
+
const reserved = actionsOf(resolved.envelope).find((action) => action.class === UNMAPPED_TOOL_CLASS);
|
|
1009
|
+
if (reserved !== undefined) {
|
|
1010
|
+
return refuse("envelope-invalid", `action ${JSON.stringify(reserved.idempotency_key)} declares class ${UNMAPPED_TOOL_CLASS}, which is reserved to the harness hook: it names a tool call that no \`tools\` entry of the attested policy claimed, and only the hook observes that. Declare the action under the class that describes what it does. Nothing was appended.`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
988
1013
|
const read = readGateRecords(logPath);
|
|
989
1014
|
if (!read.ok)
|
|
990
1015
|
return read;
|
|
@@ -1418,6 +1443,17 @@ function attemptRequest(logPath, input, actor, options) {
|
|
|
1418
1443
|
if (!isPrincipalActor(actor)) {
|
|
1419
1444
|
return refuse("actor-invalid", `request requires a human: or agent: actor, got ${JSON.stringify(actor)}`);
|
|
1420
1445
|
}
|
|
1446
|
+
// APRV-499 fix round 2 (recheck SF2), the request-time half of the
|
|
1447
|
+
// registration refusal above: `harness.tool.unmapped` is asked about only by
|
|
1448
|
+
// the harness hook, and always as a harness request (`execution: "harness"`,
|
|
1449
|
+
// which mints no token). A token-minting request on the class, which is what
|
|
1450
|
+
// `approval request` makes after the hook's own question was withdrawn, would
|
|
1451
|
+
// put a credential in front of a human that only `approval consume` could
|
|
1452
|
+
// spend, and that path refuses it (`harness-executed`). Refused before the
|
|
1453
|
+
// log is read; nothing is appended.
|
|
1454
|
+
if (input.cls === UNMAPPED_TOOL_CLASS && input.execution !== "harness") {
|
|
1455
|
+
return refuse("envelope-invalid", `action ${JSON.stringify(input.actionKey)} is declared under ${UNMAPPED_TOOL_CLASS}, which is reserved to the harness hook: only the hook asks about it, as a harness request that mints no token, and nothing else may request it. Nothing was appended.`);
|
|
1456
|
+
}
|
|
1421
1457
|
const read = readGateRecords(logPath);
|
|
1422
1458
|
if (!read.ok)
|
|
1423
1459
|
return read;
|
|
@@ -1944,7 +1980,7 @@ export function decide(logPath, actionKey, decision, actor, options = {}) {
|
|
|
1944
1980
|
function attemptDecide(logPath, actionKey, decision, actor, options) {
|
|
1945
1981
|
const ts = tick(options);
|
|
1946
1982
|
if (!HUMAN_ACTOR.test(actor)) {
|
|
1947
|
-
return refuse("actor-not-human", `${decision} is a human-only verb; the actor must match human:<id>, got ${JSON.stringify(actor)}`);
|
|
1983
|
+
return refuse("actor-not-human", `${decision} is a human-only verb; the actor must match human:<id>, got ${JSON.stringify(actor)}${reservedKindNote(actor)}`);
|
|
1948
1984
|
}
|
|
1949
1985
|
// APRV-239, beside the actor check and before anything is read: whether a
|
|
1950
1986
|
// grade came with words is a property of these arguments alone, so it is
|
|
@@ -2123,6 +2159,16 @@ function attemptDecide(logPath, actionKey, decision, actor, options) {
|
|
|
2123
2159
|
if (options.senderSource !== undefined)
|
|
2124
2160
|
payload["sender_source"] = options.senderSource;
|
|
2125
2161
|
}
|
|
2162
|
+
// APRV-489. Grant and reject only, the two answers a prompt collects, and only
|
|
2163
|
+
// when the surface drew (or was asked for) a non-default layout. Copied in a
|
|
2164
|
+
// fixed shape so nothing else a caller hangs on the object reaches the log.
|
|
2165
|
+
if (decision !== "revoke" && options.rendering !== undefined) {
|
|
2166
|
+
payload["rendering"] = {
|
|
2167
|
+
style: options.rendering.style,
|
|
2168
|
+
...(options.rendering.fallback === undefined ? {} : { fallback: options.rendering.fallback }),
|
|
2169
|
+
...(options.rendering.say_does_ignored === true ? { say_does_ignored: true } : {}),
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2126
2172
|
if (decision === "grant") {
|
|
2127
2173
|
const cls = derivation.declared.class ?? "";
|
|
2128
2174
|
const resolution = resolve(load, cls, derivation.declared.reversible === null ? {} : { reversible: derivation.declared.reversible });
|
|
@@ -2873,6 +2919,9 @@ function attemptHarnessConsume(logPath, actionKey, actor, options) {
|
|
|
2873
2919
|
}
|
|
2874
2920
|
if (derivation.decisionSeq !== null)
|
|
2875
2921
|
payload["grant_seq"] = derivation.decisionSeq;
|
|
2922
|
+
// APRV-499 (refutation S3): which tool a mapped or unmapped spend was.
|
|
2923
|
+
if (options.harnessTool !== undefined)
|
|
2924
|
+
payload["harness_tool"] = options.harnessTool;
|
|
2876
2925
|
const appended = append(logPath, {
|
|
2877
2926
|
ts,
|
|
2878
2927
|
event: "execution.started",
|
|
@@ -2931,6 +2980,40 @@ function attemptHarnessConsume(logPath, actionKey, actor, options) {
|
|
|
2931
2980
|
* field because a start event with no `payload_hash` is a record that says
|
|
2932
2981
|
* something ran without saying what — the state APRV-140 closed everywhere else.
|
|
2933
2982
|
*/
|
|
2983
|
+
/**
|
|
2984
|
+
* Is the class this harness start names the one the ATTESTED policy's tool
|
|
2985
|
+
* mapping gives its tool? (APRV-499, fix round 1.)
|
|
2986
|
+
*
|
|
2987
|
+
* The hook classifies an unclaimed tool call from a policy read of its own,
|
|
2988
|
+
* and this function attests the bytes it reads itself, so between the two a
|
|
2989
|
+
* file swap could have the hook classify under one mapping while the start is
|
|
2990
|
+
* recorded under another. For every class that is a belt to braces (the
|
|
2991
|
+
* resolution here is the attested one), but the class `harness.tool.unmapped`
|
|
2992
|
+
* also decides whether `daily_actions` and the loop floor count the start
|
|
2993
|
+
* (ruling H1), so the name and the class are checked against the attested
|
|
2994
|
+
* mapping here, where the append happens: a start naming `harness_tool` must
|
|
2995
|
+
* carry exactly the class that mapping gives that name, and a start of
|
|
2996
|
+
* `harness.tool.unmapped` written without a name is an ordinary declared
|
|
2997
|
+
* action, budgeted and counted like any other.
|
|
2998
|
+
*/
|
|
2999
|
+
function toolMapStartRefusal(load, input) {
|
|
3000
|
+
if (input.harness_tool === undefined) {
|
|
3001
|
+
// APRV-499 (claude-edge ruling): only the hook's record of a tool it names
|
|
3002
|
+
// may use the reserved class.
|
|
3003
|
+
if (input.cls !== UNMAPPED_TOOL_CLASS)
|
|
3004
|
+
return null;
|
|
3005
|
+
return refuse("not-granted", `class ${UNMAPPED_TOOL_CLASS} is reserved to the harness hook's record of a tool call it names (harness_tool), and the start of ${input.actionKey} names none; nothing authorizes it here and nothing was appended.`);
|
|
3006
|
+
}
|
|
3007
|
+
const verdict = load.ok ? toolMapVerdict(load.policy, input.harness_tool) : null;
|
|
3008
|
+
const expected = verdict === null || verdict.kind === "not-gated"
|
|
3009
|
+
? null
|
|
3010
|
+
: verdict.kind === "mapped"
|
|
3011
|
+
? verdict.cls
|
|
3012
|
+
: UNMAPPED_TOOL_CLASS;
|
|
3013
|
+
if (expected === input.cls)
|
|
3014
|
+
return null;
|
|
3015
|
+
return refuse(ATTESTATION_REFUSAL, `the start of ${input.actionKey} names harness tool ${JSON.stringify(input.harness_tool)} under class ${input.cls}, and the attested policy's tool mapping ${expected === null ? "does not gate that tool at all" : `gives it class ${expected}`}; the call was classified against policy bytes other than the attested ones, so nothing was appended. Retry once the policy file is the attested one (\`approval policy attest\` after an intended edit).`);
|
|
3016
|
+
}
|
|
2934
3017
|
export function startHarnessExecution(logPath, input, actor, options = {}) {
|
|
2935
3018
|
// APRV-150, and the writer the incident was reported against. This is the
|
|
2936
3019
|
// busiest append in the system — one per class per gated tool call, most of
|
|
@@ -3021,8 +3104,24 @@ function attemptHarnessStart(logPath, input, actor, options) {
|
|
|
3021
3104
|
if (refusal !== null)
|
|
3022
3105
|
return refusal;
|
|
3023
3106
|
}
|
|
3107
|
+
// APRV-499 fix round 1: a start that names a harness tool is re-judged
|
|
3108
|
+
// against the ATTESTED mapping, inside the attempt that appends. Before the
|
|
3109
|
+
// budget verdict, which writes.
|
|
3110
|
+
const mapped = toolMapStartRefusal(load, input);
|
|
3111
|
+
if (mapped !== null)
|
|
3112
|
+
return mapped;
|
|
3024
3113
|
const cost = costOf(input.est_cost_usd);
|
|
3025
|
-
const budget = evaluateBudgetsWithTask(read.records, budgetScopeOf(load, resolution), {
|
|
3114
|
+
const budget = evaluateBudgetsWithTask(read.records, budgetScopeOf(load, resolution), {
|
|
3115
|
+
class: input.cls,
|
|
3116
|
+
est_cost_usd: cost,
|
|
3117
|
+
// Ruling H1 (APRV-499): every start this function writes is authorized by
|
|
3118
|
+
// the policy alone, so one of `harness.tool.unmapped` naming its tool
|
|
3119
|
+
// (checked against the attested mapping above) is a record of
|
|
3120
|
+
// unclassified tool use and is not charged to `daily_actions`.
|
|
3121
|
+
...(input.cls === UNMAPPED_TOOL_CLASS && input.harness_tool !== undefined
|
|
3122
|
+
? { recordOnly: true }
|
|
3123
|
+
: {}),
|
|
3124
|
+
}, ts, input.task);
|
|
3026
3125
|
if (!budget.pass) {
|
|
3027
3126
|
const failed = budget.verdicts.filter((verdict) => !verdict.pass);
|
|
3028
3127
|
const logged = append(logPath, {
|
|
@@ -3043,7 +3142,14 @@ function attemptHarnessStart(logPath, input, actor, options) {
|
|
|
3043
3142
|
.join(", ")}`;
|
|
3044
3143
|
return logged.ok
|
|
3045
3144
|
? refuse("budget-exceeded", message, { verdicts: failed, record: logged.record })
|
|
3046
|
-
: refuse("budget-exceeded", `${message}; the budget.exceeded event could not be appended: ${logged.message}`,
|
|
3145
|
+
: refuse("budget-exceeded", `${message}; the budget.exceeded event could not be appended: ${logged.message}`,
|
|
3146
|
+
// APRV-478: the writer's own error rides along, so a caller that
|
|
3147
|
+
// waits for the log's lock on its event loop (the harness hook's
|
|
3148
|
+
// `spendUnderLock`) can see that the evidence lost a lock race,
|
|
3149
|
+
// re-run the whole attempt and give the record the same chance to
|
|
3150
|
+
// land that a synchronous lock wait gave it. The verdict is the
|
|
3151
|
+
// budget refusal either way.
|
|
3152
|
+
logged.append === undefined ? { verdicts: failed } : { verdicts: failed, append: logged.append });
|
|
3047
3153
|
}
|
|
3048
3154
|
const payload = {
|
|
3049
3155
|
// The budgets contract: class and est_cost_usd on every start event.
|
|
@@ -3059,6 +3165,8 @@ function attemptHarnessStart(logPath, input, actor, options) {
|
|
|
3059
3165
|
// Without it a reader of a policy-authorized start had to reconstruct the
|
|
3060
3166
|
// rules in force from the latest `policy.updated` before this seq.
|
|
3061
3167
|
...harnessStartPolicyStamp(attested.sha256),
|
|
3168
|
+
// APRV-499: which tool a mapped or unmapped start was, by name only.
|
|
3169
|
+
...(input.harness_tool === undefined ? {} : { harness_tool: input.harness_tool }),
|
|
3062
3170
|
};
|
|
3063
3171
|
const appended = append(logPath, {
|
|
3064
3172
|
ts,
|