approval-md 0.4.1 → 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 +8 -0
- package/dist/src/cli/attest.js +2 -1
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit.js +3 -2
- package/dist/src/cli/audit.js.map +1 -1
- package/dist/src/cli/channel-telegram.js +2 -1
- 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.js +4 -3
- 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/hook.d.ts +7 -0
- package/dist/src/cli/hook.js +252 -30
- 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.js +2 -1
- package/dist/src/cli/log-unlock.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/vault.js +2 -1
- package/dist/src/cli/vault.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 +15 -0
- package/dist/src/core/audit.js +19 -3
- 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/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 +26 -42
- package/dist/src/core/gate.js +84 -3
- 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/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.js +3 -2
- package/dist/src/core/policy-proposal.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/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/docs/README-extended.md +13 -0
- package/docs/cli-reference.md +64 -3
- package/package.json +2 -2
- package/schema/event.schema.json +34 -2
- package/schema/fixtures/event/invalid/audit-reviewed-model-actor.json +20 -0
- package/schema/fixtures/event/invalid/audit-reviewed-verdict-source-model.json +20 -0
- package/schema/fixtures/event/invalid/execution-started-harness-tool-space.json +18 -0
- 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 +126 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy-declared tool-name mapping (APRV-499, amended SPEC.md §5.2 proposed).
|
|
3
|
+
*
|
|
4
|
+
* A harness hook classifies a tool call through the adapter's own tables: its
|
|
5
|
+
* shell tool, its file and read tools, its pass-through tools, and (Hermes) a
|
|
6
|
+
* hard-coded rule table over the harness's side-effecting tools. A call none of
|
|
7
|
+
* those claims used to be answered "not a gated tool" and allowed with no
|
|
8
|
+
* record, which put every MCP tool (`mcp__<server>__<tool>` on Claude Code)
|
|
9
|
+
* outside the gate and made a new app a core release.
|
|
10
|
+
*
|
|
11
|
+
* This module is the policy's half of the answer, and it is pure: no I/O, no
|
|
12
|
+
* clock. Two keys:
|
|
13
|
+
*
|
|
14
|
+
* - `tools`: an ORDERED list of `{match, class}`. `match` is a glob over the
|
|
15
|
+
* whole tool name whose only wildcard is `*` (any run of characters,
|
|
16
|
+
* including none); `class` is a concrete class the policy declares. The first
|
|
17
|
+
* entry that matches decides, and later entries are not read.
|
|
18
|
+
* - `defaults.unmapped_tool`: `record` or `ask`, for a call no entry matches.
|
|
19
|
+
* Such a call is judged under {@link UNMAPPED_TOOL_CLASS}, whose default
|
|
20
|
+
* autonomy the key supplies (see `core/policy-match.ts`'s `fromDefaults`).
|
|
21
|
+
* Absent, the call stays outside the gate exactly as before.
|
|
22
|
+
*
|
|
23
|
+
* The adapter's own tables keep precedence over every entry, and that ordering
|
|
24
|
+
* lives in the hook, not here: those tables read the call's ARGUMENTS
|
|
25
|
+
* (`cronjob_manage list` is a read, `create` is not), while an entry reads only
|
|
26
|
+
* the NAME, so letting an entry override them would let a name-only line
|
|
27
|
+
* loosen a call the runtime classified from what it does.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The class a harness tool call is judged under when no `tools` entry claims it
|
|
31
|
+
* and the policy declares `defaults.unmapped_tool` (APRV-499).
|
|
32
|
+
*
|
|
33
|
+
* Fixed rather than derived from the tool name, so one `classes` line prices
|
|
34
|
+
* every unmapped call and a policy cannot be surprised by a class it never
|
|
35
|
+
* wrote. The tool's NAME rides on the record instead (`harness_tool` on the
|
|
36
|
+
* `execution.started` payload, and the whole call in a gated request's
|
|
37
|
+
* payload).
|
|
38
|
+
*/
|
|
39
|
+
export const UNMAPPED_TOOL_CLASS = "harness.tool.unmapped";
|
|
40
|
+
/**
|
|
41
|
+
* Can this tool name be recorded? (APRV-499, refutation S4.)
|
|
42
|
+
*
|
|
43
|
+
* The shape the event schema admits for `harness_tool` on an
|
|
44
|
+
* `execution.started` (letters, digits, `_`, `.`, `:`, `-`, at most 256),
|
|
45
|
+
* which is also every literal a `tools` match may spell. The hook asks this
|
|
46
|
+
* BEFORE it classifies a call under a mapped or unmapped class, so a name
|
|
47
|
+
* outside it is refused by name rather than at the write boundary as an opaque
|
|
48
|
+
* `append-failed`. Model APIs already cap tool names well inside this set, so
|
|
49
|
+
* the refusal is a corner; what it buys is a message that says what is wrong.
|
|
50
|
+
*/
|
|
51
|
+
export function isRecordableToolName(name) {
|
|
52
|
+
return /^[A-Za-z0-9_.:-]{1,256}$/u.test(name);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Does the glob `pattern` match the whole of `name`?
|
|
56
|
+
*
|
|
57
|
+
* `*` matches any run of characters, including none; every other character is
|
|
58
|
+
* literal and compared case-sensitively. Linear in the name for a fixed
|
|
59
|
+
* pattern: the literal runs between wildcards are found leftmost-first, which
|
|
60
|
+
* is exact for a language whose only wildcard is `*` (taking an earlier
|
|
61
|
+
* occurrence of a run never rules out a match a later one would allow).
|
|
62
|
+
*/
|
|
63
|
+
export function matchesToolPattern(pattern, name) {
|
|
64
|
+
const parts = pattern.split("*");
|
|
65
|
+
if (parts.length === 1)
|
|
66
|
+
return pattern === name;
|
|
67
|
+
const first = parts[0] ?? "";
|
|
68
|
+
const last = parts[parts.length - 1] ?? "";
|
|
69
|
+
if (name.length < first.length + last.length)
|
|
70
|
+
return false;
|
|
71
|
+
if (!name.startsWith(first) || !name.endsWith(last))
|
|
72
|
+
return false;
|
|
73
|
+
let position = first.length;
|
|
74
|
+
const end = name.length - last.length;
|
|
75
|
+
for (let index = 1; index < parts.length - 1; index += 1) {
|
|
76
|
+
const part = parts[index] ?? "";
|
|
77
|
+
if (part === "")
|
|
78
|
+
continue;
|
|
79
|
+
const at = name.indexOf(part, position);
|
|
80
|
+
if (at === -1 || at + part.length > end)
|
|
81
|
+
return false;
|
|
82
|
+
position = at + part.length;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The policy's verdict for `toolName`: the first matching entry, else the
|
|
88
|
+
* unmapped-tool default, else not gated.
|
|
89
|
+
*/
|
|
90
|
+
export function toolMapVerdict(policy, toolName) {
|
|
91
|
+
const entries = policy.tools ?? [];
|
|
92
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
93
|
+
const entry = entries[index];
|
|
94
|
+
if (entry === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
if (matchesToolPattern(entry.match, toolName)) {
|
|
97
|
+
return { kind: "mapped", cls: entry.class, match: entry.match, index };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const mode = policy.defaults?.unmapped_tool;
|
|
101
|
+
if (mode === "record" || mode === "ask") {
|
|
102
|
+
return { kind: "unmapped", cls: UNMAPPED_TOOL_CLASS, mode };
|
|
103
|
+
}
|
|
104
|
+
return { kind: "not-gated" };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Is `cls` declared by `classes`: an exact key, or under a trailing family key
|
|
108
|
+
* `<prefix>.*` whose literal prefix it extends?
|
|
109
|
+
*
|
|
110
|
+
* The family half is the reading `agent_may_request` gives a family (APRV-445):
|
|
111
|
+
* interior wildcards and the bare `*` are not families, because a pattern that
|
|
112
|
+
* could match across branches would declare classes nobody looked at.
|
|
113
|
+
*/
|
|
114
|
+
export function classDeclared(classes, cls) {
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(classes, cls))
|
|
116
|
+
return true;
|
|
117
|
+
const segments = cls.split(".");
|
|
118
|
+
for (let length = segments.length - 1; length >= 1; length -= 1) {
|
|
119
|
+
const family = `${segments.slice(0, length).join(".")}.*`;
|
|
120
|
+
if (Object.prototype.hasOwnProperty.call(classes, family))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The load-time checks on `tools` that no JSON Schema can state, because each
|
|
127
|
+
* relates an entry to another part of the file (APRV-499).
|
|
128
|
+
*
|
|
129
|
+
* Any error fails the WHOLE policy closed (`schema-invalid`), exactly as a
|
|
130
|
+
* malformed glob does at the schema: a mapping its author believes is in force
|
|
131
|
+
* and the runtime does not apply is the failure this project exists to
|
|
132
|
+
* prevent, and the strict answer to it is every class `manual`.
|
|
133
|
+
*
|
|
134
|
+
* - An entry's `class` must be declared by `classes`. A class nothing prices
|
|
135
|
+
* would resolve by `defaults.autonomy`, which reads in the log as a decision
|
|
136
|
+
* the operator made about this app when they made none.
|
|
137
|
+
* - An entry may not name {@link UNMAPPED_TOOL_CLASS}: that class asserts that
|
|
138
|
+
* no entry claimed the call, and a record saying so about a call an entry did
|
|
139
|
+
* claim would be false.
|
|
140
|
+
* - Two entries may not share a `match`. The second can never decide, so its
|
|
141
|
+
* author would be reading a line that is not in force.
|
|
142
|
+
*/
|
|
143
|
+
export function toolMapErrors(policy) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
const entries = policy.tools ?? [];
|
|
146
|
+
const classes = policy.classes ?? {};
|
|
147
|
+
const seen = new Map();
|
|
148
|
+
entries.forEach((entry, index) => {
|
|
149
|
+
const at = `/tools/${String(index)}`;
|
|
150
|
+
const earlier = seen.get(entry.match);
|
|
151
|
+
if (earlier !== undefined) {
|
|
152
|
+
errors.push({
|
|
153
|
+
path: `${at}/match`,
|
|
154
|
+
keyword: "tool-match-duplicate",
|
|
155
|
+
message: `match ${JSON.stringify(entry.match)} repeats entry ${String(earlier)}; the first matching entry decides, so this one could never be read`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
seen.set(entry.match, index);
|
|
160
|
+
}
|
|
161
|
+
if (entry.class === UNMAPPED_TOOL_CLASS) {
|
|
162
|
+
errors.push({
|
|
163
|
+
path: `${at}/class`,
|
|
164
|
+
keyword: "tool-class-reserved",
|
|
165
|
+
message: `${UNMAPPED_TOOL_CLASS} names the calls no tools entry claims, so an entry may not map to it; price unmapped calls with defaults.unmapped_tool or a classes line for ${UNMAPPED_TOOL_CLASS}`,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!classDeclared(classes, entry.class)) {
|
|
170
|
+
errors.push({
|
|
171
|
+
path: `${at}/class`,
|
|
172
|
+
keyword: "tool-class-undeclared",
|
|
173
|
+
message: `class ${JSON.stringify(entry.class)} is not declared by classes (as an exact key or under a trailing <prefix>.* family key); declare it so the mapping resolves under a line the policy wrote rather than defaults.autonomy`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return errors;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=tool-map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-map.js","sourceRoot":"","sources":["../../../src/core/tool-map.ts"],"names":[],"mappings":"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"}
|
|
@@ -71,9 +71,9 @@ import type { LogHead } from "../core/log.js";
|
|
|
71
71
|
* because a module that resolves its own package root differs between the source
|
|
72
72
|
* tree and the build output for no benefit at all.
|
|
73
73
|
*/
|
|
74
|
-
export declare const APPROVALD_VERSION = "0.4.
|
|
74
|
+
export declare const APPROVALD_VERSION = "0.4.2";
|
|
75
75
|
/** `user.name` on every commit this module makes. */
|
|
76
|
-
export declare const GIT_EVIDENCE_AUTHOR_NAME = "approvald 0.4.
|
|
76
|
+
export declare const GIT_EVIDENCE_AUTHOR_NAME = "approvald 0.4.2";
|
|
77
77
|
/** `user.email` on every commit: fixed, and deliberately undeliverable. */
|
|
78
78
|
export declare const GIT_EVIDENCE_AUTHOR_EMAIL = "approvald@noreply.approval.md";
|
|
79
79
|
/**
|
|
@@ -75,7 +75,7 @@ import { PAYLOAD_STORE_DIRNAME } from "../core/payload-store.js";
|
|
|
75
75
|
* because a module that resolves its own package root differs between the source
|
|
76
76
|
* tree and the build output for no benefit at all.
|
|
77
77
|
*/
|
|
78
|
-
export const APPROVALD_VERSION = "0.4.
|
|
78
|
+
export const APPROVALD_VERSION = "0.4.2";
|
|
79
79
|
/** `user.name` on every commit this module makes. */
|
|
80
80
|
export const GIT_EVIDENCE_AUTHOR_NAME = `approvald ${APPROVALD_VERSION}`;
|
|
81
81
|
/** `user.email` on every commit: fixed, and deliberately undeliverable. */
|
package/docs/README-extended.md
CHANGED
|
@@ -292,6 +292,7 @@ The closed [policy schema](../schema/policy.schema.json) and [SPEC section 5](..
|
|
|
292
292
|
| `channels` | Telegram and web configuration, plus supported prompt-layout choices. |
|
|
293
293
|
| `vault` | The name of the environment variable holding the vault passphrase. |
|
|
294
294
|
| `payload_retention` | Optional pruning policy for eligible stored payloads; absence means retain them. |
|
|
295
|
+
| `delegation` | The judge's bounds, reserved in 0.4.2: loads only when off and changes nothing. |
|
|
295
296
|
|
|
296
297
|
Keys ending in `_env` name environment variables, not secret values. Prompt customization cannot hide the fields required for a decision or remove the canonical payload from the authority path.
|
|
297
298
|
|
|
@@ -312,12 +313,16 @@ semantics: SPEC.md section 5.
|
|
|
312
313
|
| `defaults.approval_ttl` | How long a pending request stays actionable. Duration string, `24h`. No default; the scaffolded policy writes one (§5.1). |
|
|
313
314
|
| `defaults.token_delivery` | How a minted token reaches the process that will spend it. `manual` (the default, and what an absent key means): printed once on the granting surface and carried by a human. `sealed`: sealed to a per-request X25519 key so `approval wait` can hand it back, which addresses the token and never authorizes it (§10.4, APRV-105). |
|
|
314
315
|
| `defaults.on_expiry` | What happens when the TTL lapses. `reject` is the only value, and absent means `reject` (§5.1). |
|
|
316
|
+
| `defaults.unmapped_tool` | What a harness hook does with a tool call that neither the adapter's own tables nor any `tools` entry claims. `record`: judged under `harness.tool.unmapped`, which resolves `autonomous` unless a `classes` rule says otherwise, so the call runs and its `execution.started` names the tool (such record-only starts are not charged to `daily_actions` and do not accrue to the loop floor). `ask`: the same class resolves `manual`, so a human decides. Absent means the call is not a gate question and leaves no record, as before the key existed (§5.2, APRV-499, proposed). |
|
|
315
317
|
| `payload_retention` | How long payload bytes are kept after their action is terminal. Absent means nothing is ever pruned (§5.2). |
|
|
316
318
|
| `protected_paths` | Repo-relative files and directory prefixes whose edit is classified `policy.edit`. A bare string is the whole entry. Additive only, no globs, and absent means the built-in protected set alone (§5.2, APRV-107). |
|
|
317
319
|
| `protected_paths[].path` | The path half of the object form: the same grammar as the bare string, an exact file (`SPEC.md`) or a directory prefix (`design/`) (§5.2, APRV-266). |
|
|
318
320
|
| `protected_paths[].class` | The class half: one lowercase segment under `policy.edit`. Four reserved names, `policy.edit.spec`, `policy.edit.harness`, `policy.edit.ci` and `policy.edit.design`, plus any word an author mints beside them. Nothing outside `policy.edit` may be named, and a route below the `policy.edit` line is refused `protected-route-floor`. No default: an entry that wants a sub-class states it (§5.2, APRV-266). |
|
|
319
321
|
| `read_scope` | Directories an agent's reads may stay inside. A read resolving outside every root is `read.file.out_of_scope`. Additive only: the gate root (this policy's own directory), the session scratchpad and the system temp root are in scope whatever this says, and absent means those alone (§5.2, APRV-347). |
|
|
320
322
|
| `read_scope.roots` | The extra directories. Absolute, or relative to the gate root. No globs and no negation; each is resolved on disk before it is compared, so a symlink cannot smuggle a read out of one. Absent means no widening (§5.2, APRV-347). |
|
|
323
|
+
| `tools` | Ordered `{match, class}` entries naming the class a harness's own tool calls are judged under, for the calls the adapter's built-in tables do not claim (every MCP tool among them). The first matching entry decides; the adapter's shell, file, read and pass-through tools and Hermes's rule table keep precedence. Absent or empty maps nothing (§5.2, APRV-499, proposed). |
|
|
324
|
+
| `tools[].match` | A glob over the whole tool name, case-sensitive, `*` the only wildcard (any run, including none); otherwise letters, digits, `_`, `.`, `:` and `-`, at most 256. Two `*` in a row, or two entries with one `match`, fail the policy closed. Required (§5.2, APRV-499). |
|
|
325
|
+
| `tools[].class` | The concrete class the call is judged under. Must be declared by `classes`, exactly or under a trailing `<prefix>.*` family, and may not be `harness.tool.unmapped`; either fault fails the policy closed. Required (§5.2, APRV-499). |
|
|
321
326
|
| `approvers.<name>.channels` | The channels one approver can decide on. At least one: an approver reachable nowhere can never grant. No default (§5.1). |
|
|
322
327
|
| `approvers.<name>.senders` | The transport account ids this person decides from, per channel: `senders.telegram: "12345678"`, the numeric `callback_query.from.id` and never a `@handle`, which is mutable and would transfer an identity. Optional and additive; absent everywhere means every decision is recorded against the identity the deciding process was launched with, exactly as before the key existed. Declaring the first entry for a channel turns enforcement on for that channel: a sender it authenticates and this block does not name is refused `sender-unmapped` and nothing is recorded. Two approvers claiming one id refuses the whole policy (`sender-ambiguous`), so every class resolves `manual`. Only channels whose transport authenticates a sender may appear, which today is `telegram` alone (§5.2, §10.3, APRV-324). |
|
|
323
328
|
| `classes.<pattern>.autonomy` | Required on every class rule, so it has no default. Six levels, strictest first: `human-only`, `manual`, `supervised-live`, `supervised-retro`, `autonomous`, and `supervised`, which is the pre-split spelling and the DEPRECATED alias of `supervised-retro`: it still parses, `approval doctor`'s `autonomy-alias` row names every rule that writes it, and a future schema version removes it (§5.2, APRV-127, APRV-185, APRV-335). |
|
|
@@ -335,6 +340,14 @@ semantics: SPEC.md section 5.
|
|
|
335
340
|
| `audit.sampling_secret_env` | Name of the variable holding the operator's HMAC sampling secret. Unnamed means sampling is off and says so (§5.2, §11). |
|
|
336
341
|
| `audit.skew_tolerance` | How far a gate-typed event's timestamp may step back before verification reports an anomaly. Report-only; default 2 seconds (§8). |
|
|
337
342
|
| `audit.checkpoint_keys` | Public halves of the Ed25519 keys permitted to sign a `log.checkpoint`, base64 DER SPKI. The private halves live in the vault and never in this file. A list, so a retired key stays listed: a checkpoint signed by a key the list does not carry is refused. Absent, empty or unreadable means verification skips the checkpoint check with a reason and never reports it as a pass (§9, APRV-220). |
|
|
343
|
+
| `delegation` | The judge's bounds (a pinned model that may later review samples, advise on cards and decide manual cards in named classes). RESERVED in 0.4.2: the block is validated and loads only in its off form (`model: null`, every list empty, `max_autonomy: manual`, `daily_cap: 0`, `advice: false`; `{}` is the same), which changes nothing. Any other value fails the policy closed with `delegation-not-supported`, so every class resolves `manual`. Absent means no judge, exactly as before the key existed. Cores before 0.4.2 refuse the key outright (§5.2 proposed, APRV-500; `docs/cli-reference.md`, policy check). |
|
|
344
|
+
| `delegation.model` | The judge identity, `model:<name>@<major>.<minor>.<patch>`, or `null`. Reserved: non-null is `delegation-not-supported` (APRV-500). |
|
|
345
|
+
| `delegation.classes` | Exact keys of `classes` the judge may act on: no wildcard, not `human-only`, not `autonomous`. Reserved: non-empty is `delegation-not-supported` (APRV-500). |
|
|
346
|
+
| `delegation.max_autonomy` | `manual` (default), `supervised-live` or `supervised-retro`: a pin, so every delegated class must declare an autonomy at least as strict. Reserved: anything but `manual` is `delegation-not-supported` (APRV-500). |
|
|
347
|
+
| `delegation.daily_cap` | Judge grants per rolling day, 0 to 1000; above 0 requires `escalate_on` to hold `irreversible` and `unknown_class`. Reserved: anything but 0 is `delegation-not-supported` (APRV-500). |
|
|
348
|
+
| `delegation.escalate_on` | Judge outcomes sent to a human: `deny`, `low_confidence`, `irreversible`, `unknown_class`. Reserved: non-empty is `delegation-not-supported` (APRV-500). |
|
|
349
|
+
| `delegation.advice` | Whether the judge advises on manual cards. Reserved: `true` is `delegation-not-supported` (APRV-500). |
|
|
350
|
+
| `delegation.reviewers` | Extra review identities: `human:<approver id>` or `model:<name>@<version>` equal to `model`. The only place a `model:` identity parses; every verb still requires `human:`. Reserved: non-empty is `delegation-not-supported` (APRV-500). |
|
|
338
351
|
| `daemons` | The daemon instance ids permitted to append to this log, for the case where one party runs the daemon for another. Optional and additive; ABSENT means no restriction, exactly as before the key existed, and an empty list admits no daemon at all. Every record a daemon appends carries its id (`daemon` on the record), which is `APPROVAL_DAEMON_ID` from that process's launch environment or the derived `daemon-<instance id>` that `approval status` and `approval doctor` print. An id this list does not name is refused at the write boundary (`daemon-not-allowed`) with nothing written; an id it DOES name gains nothing else, since the id is self-reported and no verdict, budget, floor, draw or token reads it. Read from the attested policy only (§5.2, APRV-383, `design/hosted-daemon-identity.md`). |
|
|
339
352
|
| `audit.checkpoint_every` | How long the log may go without a human-signed checkpoint before verification says one is due, and before the listener puts one `CHECKPOINT DUE` prompt on the approver's channel (`approval setup checkpoint` mints the key). Report-only at every layer: a due checkpoint is a warning and never a refusal. Absent means the cadence is off and nothing is ever reported as due (§9, APRV-220, APRV-257). |
|
|
340
353
|
| `daemon.read_proof` | Which prefix proof a long-lived reader runs before reusing a cached prefix: `full` (the default, re-hash the whole prefix on every read) or `incremental` (hash only the appended bytes, re-proving in full on a cadence). One-shot processes, the Claude Code hook and `approval log verify` prove in full regardless (§5.2, APRV-217). |
|
package/docs/cli-reference.md
CHANGED
|
@@ -730,7 +730,8 @@ channel; policy-authorized execution is never represented as a human grant.
|
|
|
730
730
|
"provenance":"rule"|"default"|"inherited"|"fail-closed"|"floor",
|
|
731
731
|
"manualBecause":null|"matched-rule"|"irreversibility-floor"|"load-failure",
|
|
732
732
|
"loadFailure":null|{"code":"file-missing"|"no-block"|"multiple-blocks"|
|
|
733
|
-
"yaml-error"|"schema-invalid"|"protected-route-floor"
|
|
733
|
+
"yaml-error"|"schema-invalid"|"protected-route-floor"|
|
|
734
|
+
"sender-ambiguous"|"delegation-not-supported",
|
|
734
735
|
"message":"..."},
|
|
735
736
|
"matched":null|{"pattern":"vcs.push.main","rule":{"autonomy":"supervised"}},
|
|
736
737
|
"overridden":null|{"pattern":"read.web"|null,"autonomy":"autonomous"},
|
|
@@ -774,6 +775,62 @@ protected surface without removing a path from any list. The policy does not
|
|
|
774
775
|
load, so every class answers `manual` with `manualBecause: "load-failure"`, and
|
|
775
776
|
the message names the offending entry.
|
|
776
777
|
|
|
778
|
+
### Delegation block, reserved in 0.4.2; no behaviour (APRV-500)
|
|
779
|
+
|
|
780
|
+
A policy may carry one top-level `delegation` block. It is where the judge
|
|
781
|
+
will live: a pinned model, `model:<name>@<major>.<minor>.<patch>`, that may
|
|
782
|
+
later review supervised samples, advise on manual cards, and decide manual
|
|
783
|
+
cards in named classes within a daily cap. None of that exists in this core.
|
|
784
|
+
The block is parsed, validated, and loaded only in its off form:
|
|
785
|
+
|
|
786
|
+
```yaml
|
|
787
|
+
delegation:
|
|
788
|
+
model: null # no judge; every other key is inert
|
|
789
|
+
classes: [] # exact keys of classes, never a wildcard
|
|
790
|
+
max_autonomy: manual # manual | supervised-live | supervised-retro
|
|
791
|
+
daily_cap: 0 # 0 to 1000 judge grants per rolling day
|
|
792
|
+
escalate_on: [] # deny, low_confidence, irreversible, unknown_class
|
|
793
|
+
advice: false
|
|
794
|
+
reviewers: [] # human:<approver id> or model:<name>@<version>
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
An empty mapping (`delegation: {}`) and any subset of these keys at these
|
|
798
|
+
values is the same off form. It changes no resolution, no request and no
|
|
799
|
+
record, and `policy check` adds one line to the trace saying the block is
|
|
800
|
+
declared and off.
|
|
801
|
+
|
|
802
|
+
Any other value fails the load with `loadFailure.code`
|
|
803
|
+
`delegation-not-supported`, and the message names every key that is not off.
|
|
804
|
+
The policy then answers `manual` for every class, like any policy that does
|
|
805
|
+
not load. Accepting `daily_cap: 10` and doing nothing would leave the author
|
|
806
|
+
believing a setting is in force that nobody enforces, so this core refuses it.
|
|
807
|
+
|
|
808
|
+
The block's relationships to the rest of the file are checked first, and a
|
|
809
|
+
fault there is `schema-invalid` with its own keyword in `errors`:
|
|
810
|
+
|
|
811
|
+
| keyword | rule |
|
|
812
|
+
| --- | --- |
|
|
813
|
+
| `delegation-class-undeclared` | each `classes` entry is an exact key of `classes`; a class reached only through a `<prefix>.*` family is refused |
|
|
814
|
+
| `delegation-class-level` | no delegated class declares `human-only` or `autonomous` |
|
|
815
|
+
| `delegation-max-autonomy-pin` | every delegated class declares an autonomy at least as strict as `max_autonomy`. It is a pin: loosening a delegated row without rewriting this block fails the load |
|
|
816
|
+
| `delegation-escalation-floor` | `daily_cap` above 0 requires `escalate_on` to contain `irreversible` and `unknown_class` |
|
|
817
|
+
| `delegation-reviewer-model` | a `model:` reviewer equals `model` exactly |
|
|
818
|
+
| `delegation-reviewer-unknown` | a `human:` reviewer names a key of `approvers` |
|
|
819
|
+
| `delegation-model-required` | `advice: true`, a cap above 0 or a `model:` reviewer needs `model` |
|
|
820
|
+
|
|
821
|
+
The `model:` identity is reserved with the block. `delegation.reviewers` is
|
|
822
|
+
the only place it parses. Grant, reject, revoke, attest, `audit review` and
|
|
823
|
+
every channel decision still require `human:` and refuse a `model:` actor
|
|
824
|
+
with `actor-not-human`. The event schema admits no `model:` actor and no
|
|
825
|
+
`verdict_source: model`, which is registered as reserved and never written.
|
|
826
|
+
|
|
827
|
+
`policy diff` (and the `policy amend` ceremony) shows the block's keys as
|
|
828
|
+
`delegation.<key>` paths and, for an edit that turns one on, the
|
|
829
|
+
`delegation-not-supported` failure of the new side. Older cores (0.4.1 and
|
|
830
|
+
earlier) do not know the key and refuse even the off form as
|
|
831
|
+
`schema-invalid`, so a template carries the block only once every daemon that
|
|
832
|
+
reads it runs 0.4.2 or later.
|
|
833
|
+
|
|
777
834
|
Human output: the `decisionPath` lines, then a final line `-> <autonomy>`
|
|
778
835
|
carrying "(fail-closed: `<code>`)" or "(floor applied over `<pattern>`:
|
|
779
836
|
`<autonomy>`)" when either applies. stderr stays empty on a successful answer.
|
|
@@ -2068,7 +2125,9 @@ to decide whether to fix itself, stop retrying, or ask a human.
|
|
|
2068
2125
|
attestation (detail: `not-attested` | `hash-mismatch` | `unreadable`). Run
|
|
2069
2126
|
`approval policy attest`.
|
|
2070
2127
|
- `envelope-invalid` — the envelope failed `envelope.schema.json`, or the task
|
|
2071
|
-
file has no frontmatter or no `approval:` key.
|
|
2128
|
+
file has no frontmatter or no `approval:` key. Also: an envelope, a proposal
|
|
2129
|
+
or a token-minting `request` naming the class `harness.tool.unmapped`, which
|
|
2130
|
+
is reserved to the harness hook (APRV-499).
|
|
2072
2131
|
- `task-file-unreadable` — the task file could not be read (exit 4).
|
|
2073
2132
|
- `task-already-registered` — this task id already has a `task.registered` record.
|
|
2074
2133
|
- `envelope-missing` — the file carries no `approval:` envelope AND the log holds
|
|
@@ -2237,7 +2296,9 @@ Frozen public API in the same sense the gate's codes are.
|
|
|
2237
2296
|
is wrong and nothing is recoverable: the grant is complete, and it authorized
|
|
2238
2297
|
a process that runs the command itself rather than through `approval run`.
|
|
2239
2298
|
Distinct from `token-mismatch`, which would send an agent hunting for a token
|
|
2240
|
-
that deliberately never existed.
|
|
2299
|
+
that deliberately never existed. Also returned by `run` and `consume` for any
|
|
2300
|
+
grant on the class `harness.tool.unmapped`, which only the harness hook may
|
|
2301
|
+
spend (APRV-499).
|
|
2241
2302
|
- `class-human-only` — the class the grant authorizes resolves to `human-only`,
|
|
2242
2303
|
so the token may not be spent and `approval token` reports it unspendable
|
|
2243
2304
|
rather than live. The gate refuses the request that would mint such a token,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "approval-md",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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": "9f796b03f184f139d4cccde09cea4a098951631d"
|
|
63
63
|
}
|
package/schema/event.schema.json
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"actor": {
|
|
71
71
|
"type": "string",
|
|
72
72
|
"pattern": "^(human|agent|system):.+",
|
|
73
|
-
"description": "SPEC.md §8: who caused the event, as `<kind>:<id>` (the example record uses `human:carter`). Three kinds: `human:` for a person acting through a channel or the CLI, `agent:` for an agent-originated event, and `system:` for events the daemon originates with no external prompt (for example `approval.expired`, which fires on TTL lapse rather than on any actor's action). Approval decisions are restricted to `human:` — see the per-type rules below."
|
|
73
|
+
"description": "SPEC.md §8: who caused the event, as `<kind>:<id>` (the example record uses `human:carter`). Three kinds: `human:` for a person acting through a channel or the CLI, `agent:` for an agent-originated event, and `system:` for events the daemon originates with no external prompt (for example `approval.expired`, which fires on TTL lapse rather than on any actor's action). Approval decisions are restricted to `human:` — see the per-type rules below. A fourth kind, `model:<name>@<version>` (the judge, a pinned model reviewer), is RESERVED by APRV-500 (amended SPEC.md §5.2 proposed) and deliberately absent from this pattern: no record written by this release carries it, so a `model:` actor is refused here at the write boundary until a later core admits it on named events."
|
|
74
74
|
},
|
|
75
75
|
"alg": {
|
|
76
76
|
"const": "sha256/jcs",
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"verdict_source": {
|
|
275
275
|
"type": "string",
|
|
276
276
|
"const": "explicit",
|
|
277
|
-
"description": "SPEC.md §5.2 (amended, PR #614 refutation F5): how the verdict was given. `explicit` is the only value: since APRV-482 a verdict is said by the reviewer (`--ok`/`--deny`, a card's OK or second Deny) and never defaulted. A review written before then may carry `verdict: \"ok\"` that the runtime filled in for a grade-only tap or a bare terminal review, and nothing else in the record tells the two apart, so this field is the discriminator a follower reads: present means explicit, absent (on an old record) means the verdict may have been defaulted. REQUIRED on new records."
|
|
277
|
+
"description": "SPEC.md §5.2 (amended, PR #614 refutation F5): how the verdict was given. `explicit` is the only value: since APRV-482 a verdict is said by the reviewer (`--ok`/`--deny`, a card's OK or second Deny) and never defaulted. A review written before then may carry `verdict: \"ok\"` that the runtime filled in for a grade-only tap or a bare terminal review, and nothing else in the record tells the two apart, so this field is the discriminator a follower reads: present means explicit, absent (on an old record) means the verdict may have been defaulted. REQUIRED on new records. A second value, `model` (a review given by the judge, `model:<name>@<version>`), is RESERVED by APRV-500 (amended SPEC.md §10.3 proposed): registered so the word means nothing else, never written by this release, and refused here by the `const`. A follower that meets it may ignore it, and then never counts a model review as a human approval."
|
|
278
278
|
},
|
|
279
279
|
"payload_hash": {
|
|
280
280
|
"type": "string",
|
|
@@ -1007,6 +1007,38 @@
|
|
|
1007
1007
|
}
|
|
1008
1008
|
}
|
|
1009
1009
|
},
|
|
1010
|
+
{
|
|
1011
|
+
"$comment": "SPEC.md §8 (APRV-499, proposed): a harness `execution.started` whose class came from the policy's tool mapping (a `tools` entry, or `harness.tool.unmapped` under `defaults.unmapped_tool`) carries `harness_tool`, the harness tool NAME the call was made under, because several tools share one such class and the class alone cannot say which ran. The name only: the call's arguments are bound by `payload_hash` and are never written to the log. OPTIONAL and additive: every record written before the field existed still validates and still verifies, and a start on any other path records none. Only the SHAPE is constrained here: the characters a `tools` match may name (letters, digits, `_`, `.`, `:`, `-`), at most 256 of them, so a name outside that set refuses the append rather than writing bytes no mapping could have named.",
|
|
1012
|
+
"if": {
|
|
1013
|
+
"type": "object",
|
|
1014
|
+
"properties": {
|
|
1015
|
+
"event": {
|
|
1016
|
+
"const": "execution.started"
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
"required": [
|
|
1020
|
+
"event"
|
|
1021
|
+
]
|
|
1022
|
+
},
|
|
1023
|
+
"then": {
|
|
1024
|
+
"type": "object",
|
|
1025
|
+
"properties": {
|
|
1026
|
+
"payload": {
|
|
1027
|
+
"type": "object",
|
|
1028
|
+
"properties": {
|
|
1029
|
+
"harness_tool": {
|
|
1030
|
+
"type": "string",
|
|
1031
|
+
"minLength": 1,
|
|
1032
|
+
"maxLength": 256,
|
|
1033
|
+
"pattern": "^[A-Za-z0-9_.:-]+$",
|
|
1034
|
+
"description": "SPEC.md §8 (APRV-499, proposed): the harness tool name a policy-mapped or unmapped harness execution was made under.",
|
|
1035
|
+
"examples": ["mcp__contextsling__publish"]
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1010
1042
|
{
|
|
1011
1043
|
"$comment": "SPEC.md §10.3 (amended, APRV-38): a decision collected as part of a batch gesture carries the batch's channel delivery id as a first-class payload field. Optional — a unit decision has no batch — and constrained only when present, so the field can never arrive as an empty string or a number that audit would have to guess at.",
|
|
1012
1044
|
"if": {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"seq": 16,
|
|
3
|
+
"ts": "2026-08-04T09:26:02Z",
|
|
4
|
+
"event": "audit.reviewed",
|
|
5
|
+
"task": "task-049",
|
|
6
|
+
"action_key": "task-049:archive:2026-08-04",
|
|
7
|
+
"actor": "model:judge@0.3.0",
|
|
8
|
+
"channel": "cli",
|
|
9
|
+
"payload": {
|
|
10
|
+
"subject_seq": 15,
|
|
11
|
+
"subject_event": "audit.sampled",
|
|
12
|
+
"reviewed": true,
|
|
13
|
+
"sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
|
|
14
|
+
"verdict": "ok",
|
|
15
|
+
"verdict_source": "explicit"
|
|
16
|
+
},
|
|
17
|
+
"alg": "sha256/jcs",
|
|
18
|
+
"prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
|
|
19
|
+
"hash": "6752e6617b2b810c1cc0cf09c51678c78891ff6fdfd8bb9399aecbdfca00dd5b"
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"seq": 16,
|
|
3
|
+
"ts": "2026-08-04T09:26:02Z",
|
|
4
|
+
"event": "audit.reviewed",
|
|
5
|
+
"task": "task-049",
|
|
6
|
+
"action_key": "task-049:archive:2026-08-04",
|
|
7
|
+
"actor": "human:carter",
|
|
8
|
+
"channel": "cli",
|
|
9
|
+
"payload": {
|
|
10
|
+
"subject_seq": 15,
|
|
11
|
+
"subject_event": "audit.sampled",
|
|
12
|
+
"reviewed": true,
|
|
13
|
+
"sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
|
|
14
|
+
"verdict": "ok",
|
|
15
|
+
"verdict_source": "model"
|
|
16
|
+
},
|
|
17
|
+
"alg": "sha256/jcs",
|
|
18
|
+
"prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
|
|
19
|
+
"hash": "6752e6617b2b810c1cc0cf09c51678c78891ff6fdfd8bb9399aecbdfca00dd5b"
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"seq": 12,
|
|
3
|
+
"ts": "2026-10-05T22:50:00Z",
|
|
4
|
+
"event": "execution.started",
|
|
5
|
+
"task": "hook:sess-1:tu-4",
|
|
6
|
+
"action_key": "hook:sess-1:tu-4:harness.tool.unmapped",
|
|
7
|
+
"actor": "agent:claude-code",
|
|
8
|
+
"payload": {
|
|
9
|
+
"class": "harness.tool.unmapped",
|
|
10
|
+
"est_cost_usd": "0",
|
|
11
|
+
"execution": "harness",
|
|
12
|
+
"payload_hash": "3c987d69fe833bbea2b588ea7e7bc0848bbfb90703b298729a3d2d6d45346011",
|
|
13
|
+
"harness_tool": "mcp tool\nwith a line break"
|
|
14
|
+
},
|
|
15
|
+
"alg": "sha256/jcs",
|
|
16
|
+
"prev": "d973375d6aeec5e6cd2de3a0f7dc8f5bc1d9be77225f9af0f851e7fc519a07c3",
|
|
17
|
+
"hash": "4b7e0c2d9a8f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6d5c4b3a291807f6e5d4"
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"seq": 12,
|
|
3
|
+
"ts": "2026-10-05T22:50:00Z",
|
|
4
|
+
"event": "execution.started",
|
|
5
|
+
"task": "hook:sess-1:tu-4",
|
|
6
|
+
"action_key": "hook:sess-1:tu-4:harness.tool.unmapped",
|
|
7
|
+
"actor": "agent:claude-code",
|
|
8
|
+
"payload": {
|
|
9
|
+
"class": "harness.tool.unmapped",
|
|
10
|
+
"est_cost_usd": "0",
|
|
11
|
+
"execution": "harness",
|
|
12
|
+
"payload_hash": "3c987d69fe833bbea2b588ea7e7bc0848bbfb90703b298729a3d2d6d45346011",
|
|
13
|
+
"policy_sha256": "5f1c0de9a3b7e2d4c6f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e",
|
|
14
|
+
"harness_tool": "mcp__contextsling__publish"
|
|
15
|
+
},
|
|
16
|
+
"alg": "sha256/jcs",
|
|
17
|
+
"prev": "d973375d6aeec5e6cd2de3a0f7dc8f5bc1d9be77225f9af0f851e7fc519a07c3",
|
|
18
|
+
"hash": "4b7e0c2d9a8f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6d5c4b3a291807f6e5d4"
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1",
|
|
3
|
+
"defaults": {
|
|
4
|
+
"autonomy": "manual"
|
|
5
|
+
},
|
|
6
|
+
"delegation": {
|
|
7
|
+
"model": null,
|
|
8
|
+
"classes": [
|
|
9
|
+
"digest.share",
|
|
10
|
+
"digest.share"
|
|
11
|
+
],
|
|
12
|
+
"max_autonomy": "manual",
|
|
13
|
+
"daily_cap": 0,
|
|
14
|
+
"escalate_on": [],
|
|
15
|
+
"advice": false,
|
|
16
|
+
"reviewers": []
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1",
|
|
3
|
+
"defaults": {
|
|
4
|
+
"autonomy": "manual"
|
|
5
|
+
},
|
|
6
|
+
"delegation": {
|
|
7
|
+
"model": null,
|
|
8
|
+
"classes": [
|
|
9
|
+
"intent.*"
|
|
10
|
+
],
|
|
11
|
+
"max_autonomy": "manual",
|
|
12
|
+
"daily_cap": 0,
|
|
13
|
+
"escalate_on": [],
|
|
14
|
+
"advice": false,
|
|
15
|
+
"reviewers": []
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1",
|
|
3
|
+
"defaults": {
|
|
4
|
+
"autonomy": "manual"
|
|
5
|
+
},
|
|
6
|
+
"delegation": {
|
|
7
|
+
"model": null,
|
|
8
|
+
"classes": [],
|
|
9
|
+
"max_autonomy": "manual",
|
|
10
|
+
"daily_cap": 0,
|
|
11
|
+
"escalate_on": [
|
|
12
|
+
"deny",
|
|
13
|
+
"deny"
|
|
14
|
+
],
|
|
15
|
+
"advice": false,
|
|
16
|
+
"reviewers": []
|
|
17
|
+
}
|
|
18
|
+
}
|