approval-md 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +98 -13
  3. package/cli.js +66 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +19 -0
  8. package/dist/src/channels/contract.js +105 -1
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.js +6 -1
  17. package/dist/src/channels/tagging.js.map +1 -1
  18. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  19. package/dist/src/channels/telegram-webhook.js +536 -0
  20. package/dist/src/channels/telegram-webhook.js.map +1 -0
  21. package/dist/src/channels/telegram.d.ts +271 -1
  22. package/dist/src/channels/telegram.js +403 -45
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/cli/amend.js +350 -4
  25. package/dist/src/cli/amend.js.map +1 -1
  26. package/dist/src/cli/attest.js +43 -6
  27. package/dist/src/cli/attest.js.map +1 -1
  28. package/dist/src/cli/channel-relay.d.ts +108 -0
  29. package/dist/src/cli/channel-relay.js +334 -0
  30. package/dist/src/cli/channel-relay.js.map +1 -0
  31. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  32. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  33. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  34. package/dist/src/cli/channel-telegram.d.ts +272 -26
  35. package/dist/src/cli/channel-telegram.js +478 -41
  36. package/dist/src/cli/channel-telegram.js.map +1 -1
  37. package/dist/src/cli/channel.d.ts +1 -1
  38. package/dist/src/cli/channel.js +5 -1
  39. package/dist/src/cli/channel.js.map +1 -1
  40. package/dist/src/cli/daemon.js +37 -5
  41. package/dist/src/cli/daemon.js.map +1 -1
  42. package/dist/src/cli/doctor.d.ts +63 -0
  43. package/dist/src/cli/doctor.js +856 -111
  44. package/dist/src/cli/doctor.js.map +1 -1
  45. package/dist/src/cli/execute.d.ts +17 -1
  46. package/dist/src/cli/execute.js +341 -20
  47. package/dist/src/cli/execute.js.map +1 -1
  48. package/dist/src/cli/exit-codes.d.ts +12 -0
  49. package/dist/src/cli/exit-codes.js +13 -0
  50. package/dist/src/cli/exit-codes.js.map +1 -1
  51. package/dist/src/cli/gate.d.ts +2 -0
  52. package/dist/src/cli/gate.js +233 -2
  53. package/dist/src/cli/gate.js.map +1 -1
  54. package/dist/src/cli/help.d.ts +9 -1
  55. package/dist/src/cli/help.js +246 -38
  56. package/dist/src/cli/help.js.map +1 -1
  57. package/dist/src/cli/hook.d.ts +371 -3
  58. package/dist/src/cli/hook.js +1926 -44
  59. package/dist/src/cli/hook.js.map +1 -1
  60. package/dist/src/cli/init.js +16 -7
  61. package/dist/src/cli/init.js.map +1 -1
  62. package/dist/src/cli/instructions.js +24 -3
  63. package/dist/src/cli/instructions.js.map +1 -1
  64. package/dist/src/cli/log-anchor.d.ts +55 -0
  65. package/dist/src/cli/log-anchor.js +199 -60
  66. package/dist/src/cli/log-anchor.js.map +1 -1
  67. package/dist/src/cli/main.d.ts +0 -12
  68. package/dist/src/cli/main.js +51 -0
  69. package/dist/src/cli/main.js.map +1 -1
  70. package/dist/src/cli/muse.d.ts +2 -0
  71. package/dist/src/cli/muse.js +62 -0
  72. package/dist/src/cli/muse.js.map +1 -0
  73. package/dist/src/cli/payload.js +79 -1
  74. package/dist/src/cli/payload.js.map +1 -1
  75. package/dist/src/cli/scaffold.js +6 -0
  76. package/dist/src/cli/scaffold.js.map +1 -1
  77. package/dist/src/cli/serve.d.ts +53 -0
  78. package/dist/src/cli/serve.js +270 -0
  79. package/dist/src/cli/serve.js.map +1 -0
  80. package/dist/src/cli/up.js +21 -2
  81. package/dist/src/cli/up.js.map +1 -1
  82. package/dist/src/cli/verb-registry.d.ts +2 -0
  83. package/dist/src/cli/verb-registry.js +337 -39
  84. package/dist/src/cli/verb-registry.js.map +1 -1
  85. package/dist/src/cli/wordmark.d.ts +2 -2
  86. package/dist/src/cli/wordmark.js +2 -2
  87. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  88. package/dist/src/core/attest-bootstrap.js +135 -0
  89. package/dist/src/core/attest-bootstrap.js.map +1 -0
  90. package/dist/src/core/attest.d.ts +20 -3
  91. package/dist/src/core/attest.js +25 -0
  92. package/dist/src/core/attest.js.map +1 -1
  93. package/dist/src/core/audit.d.ts +55 -3
  94. package/dist/src/core/audit.js +52 -5
  95. package/dist/src/core/audit.js.map +1 -1
  96. package/dist/src/core/channel-lease.d.ts +322 -0
  97. package/dist/src/core/channel-lease.js +663 -0
  98. package/dist/src/core/channel-lease.js.map +1 -0
  99. package/dist/src/core/command-class.d.ts +51 -0
  100. package/dist/src/core/command-class.js +760 -8
  101. package/dist/src/core/command-class.js.map +1 -1
  102. package/dist/src/core/daemon-host.d.ts +178 -0
  103. package/dist/src/core/daemon-host.js +228 -0
  104. package/dist/src/core/daemon-host.js.map +1 -0
  105. package/dist/src/core/daemon-identity.d.ts +238 -0
  106. package/dist/src/core/daemon-identity.js +252 -0
  107. package/dist/src/core/daemon-identity.js.map +1 -0
  108. package/dist/src/core/gate.d.ts +306 -5
  109. package/dist/src/core/gate.js +772 -24
  110. package/dist/src/core/gate.js.map +1 -1
  111. package/dist/src/core/harness-version.d.ts +128 -4
  112. package/dist/src/core/harness-version.js +179 -12
  113. package/dist/src/core/harness-version.js.map +1 -1
  114. package/dist/src/core/harness-wait.d.ts +82 -3
  115. package/dist/src/core/harness-wait.js +89 -3
  116. package/dist/src/core/harness-wait.js.map +1 -1
  117. package/dist/src/core/log-subscribe.d.ts +21 -0
  118. package/dist/src/core/log-subscribe.js +16 -6
  119. package/dist/src/core/log-subscribe.js.map +1 -1
  120. package/dist/src/core/log-write-layer.d.ts +55 -0
  121. package/dist/src/core/log-write-layer.js +91 -0
  122. package/dist/src/core/log-write-layer.js.map +1 -0
  123. package/dist/src/core/log.d.ts +50 -21
  124. package/dist/src/core/log.js +197 -19
  125. package/dist/src/core/log.js.map +1 -1
  126. package/dist/src/core/payload-census.d.ts +51 -0
  127. package/dist/src/core/payload-census.js +125 -2
  128. package/dist/src/core/payload-census.js.map +1 -1
  129. package/dist/src/core/payload-store.d.ts +31 -0
  130. package/dist/src/core/payload-store.js +113 -8
  131. package/dist/src/core/payload-store.js.map +1 -1
  132. package/dist/src/core/payload.d.ts +18 -3
  133. package/dist/src/core/payload.js +20 -4
  134. package/dist/src/core/payload.js.map +1 -1
  135. package/dist/src/core/policy-diff.d.ts +10 -0
  136. package/dist/src/core/policy-diff.js +10 -2
  137. package/dist/src/core/policy-diff.js.map +1 -1
  138. package/dist/src/core/policy-explain.d.ts +7 -1
  139. package/dist/src/core/policy-explain.js +16 -1
  140. package/dist/src/core/policy-explain.js.map +1 -1
  141. package/dist/src/core/policy-load.d.ts +24 -0
  142. package/dist/src/core/policy-load.js.map +1 -1
  143. package/dist/src/core/policy-match.d.ts +38 -0
  144. package/dist/src/core/policy-match.js +35 -0
  145. package/dist/src/core/policy-match.js.map +1 -1
  146. package/dist/src/core/policy-proposal.d.ts +23 -0
  147. package/dist/src/core/policy-proposal.js +8 -2
  148. package/dist/src/core/policy-proposal.js.map +1 -1
  149. package/dist/src/core/read-scope.d.ts +10 -0
  150. package/dist/src/core/read-scope.js +4 -0
  151. package/dist/src/core/read-scope.js.map +1 -1
  152. package/dist/src/core/run-payload.d.ts +160 -0
  153. package/dist/src/core/run-payload.js +304 -0
  154. package/dist/src/core/run-payload.js.map +1 -0
  155. package/dist/src/core/sender-identity.d.ts +35 -10
  156. package/dist/src/core/sender-identity.js +48 -11
  157. package/dist/src/core/sender-identity.js.map +1 -1
  158. package/dist/src/core/state.d.ts +52 -1
  159. package/dist/src/core/state.js +52 -6
  160. package/dist/src/core/state.js.map +1 -1
  161. package/dist/src/core/telegram-config.d.ts +79 -0
  162. package/dist/src/core/telegram-config.js +108 -0
  163. package/dist/src/core/telegram-config.js.map +1 -1
  164. package/dist/src/core/verify.d.ts +23 -1
  165. package/dist/src/core/verify.js +20 -2
  166. package/dist/src/core/verify.js.map +1 -1
  167. package/dist/src/daemon/audit.d.ts +80 -5
  168. package/dist/src/daemon/audit.js +126 -6
  169. package/dist/src/daemon/audit.js.map +1 -1
  170. package/dist/src/daemon/daemon.d.ts +174 -2
  171. package/dist/src/daemon/daemon.js +298 -7
  172. package/dist/src/daemon/daemon.js.map +1 -1
  173. package/dist/src/daemon/git-evidence.d.ts +2 -2
  174. package/dist/src/daemon/git-evidence.js +1 -1
  175. package/dist/src/daemon/projection.d.ts +13 -2
  176. package/dist/src/daemon/projection.js +39 -6
  177. package/dist/src/daemon/projection.js.map +1 -1
  178. package/dist/src/mcp/server.d.ts +39 -3
  179. package/dist/src/mcp/server.js +22 -3
  180. package/dist/src/mcp/server.js.map +1 -1
  181. package/dist/src/muse/server.d.ts +16 -0
  182. package/dist/src/muse/server.js +238 -0
  183. package/dist/src/muse/server.js.map +1 -0
  184. package/dist/src/serve/archive.d.ts +158 -0
  185. package/dist/src/serve/archive.js +354 -0
  186. package/dist/src/serve/archive.js.map +1 -0
  187. package/dist/src/serve/arguments.d.ts +93 -0
  188. package/dist/src/serve/arguments.js +228 -0
  189. package/dist/src/serve/arguments.js.map +1 -0
  190. package/dist/src/serve/credentials.d.ts +91 -0
  191. package/dist/src/serve/credentials.js +143 -0
  192. package/dist/src/serve/credentials.js.map +1 -0
  193. package/dist/src/serve/follow.d.ts +86 -0
  194. package/dist/src/serve/follow.js +101 -0
  195. package/dist/src/serve/follow.js.map +1 -0
  196. package/dist/src/serve/hook-thread.d.ts +134 -0
  197. package/dist/src/serve/hook-thread.js +333 -0
  198. package/dist/src/serve/hook-thread.js.map +1 -0
  199. package/dist/src/serve/hook-worker.d.ts +134 -0
  200. package/dist/src/serve/hook-worker.js +173 -0
  201. package/dist/src/serve/hook-worker.js.map +1 -0
  202. package/dist/src/serve/server.d.ts +412 -0
  203. package/dist/src/serve/server.js +1251 -0
  204. package/dist/src/serve/server.js.map +1 -0
  205. package/docs/README-extended.md +766 -0
  206. package/docs/cli-reference.md +1633 -43
  207. package/package.json +4 -2
  208. package/schema/event.schema.json +59 -2
  209. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  210. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  211. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  212. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  213. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  214. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  215. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  216. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  217. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  218. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  219. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  220. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  221. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  222. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  223. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  224. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  225. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  226. package/schema/policy.schema.json +50 -6
@@ -73,6 +73,29 @@ function object(properties, required) {
73
73
  /** An object whose keys are not enumerated here (a log record, a policy rule). */
74
74
  const OPEN_OBJECT = { type: "object" };
75
75
  const arrayOf = (items) => ({ type: "array", items });
76
+ /**
77
+ * One refusal family in `approval status` (APRV-376): how many records the log
78
+ * carries, and the newest few of them, newest first.
79
+ *
80
+ * `code` is the surface's own refusal code, copied verbatim from the record, and
81
+ * it is spelled here as a plain string rather than an enum on purpose. The two
82
+ * closed vocabularies live at the write boundary (`schema/event.schema.json`) and
83
+ * in `core/decision-refusal.ts` and `core/gesture-refusal.ts`; a third copy in a
84
+ * REPORT schema would refuse to describe a log it was older than, which is the
85
+ * wrong failure direction for a field that authorizes nothing.
86
+ *
87
+ * `sender` is the account the gesture arrived from, when the record carries one,
88
+ * in the form the record carries it: `hashed` marks APRV-370's keyed digest, and
89
+ * it is present only when the record says so.
90
+ */
91
+ const REFUSAL_FAMILY = object({
92
+ count: INTEGER,
93
+ recent: arrayOf(object({
94
+ seq: INTEGER,
95
+ code: STRING,
96
+ sender: object({ channel: STRING, id: STRING, hashed: BOOLEAN }, ["channel", "id"]),
97
+ }, ["seq", "code"])),
98
+ }, ["count", "recent"]);
76
99
  /**
77
100
  * Positional arguments, named in order. `items: false` closes the tail: an
78
101
  * unexpected argument is a usage error in the CLI and is one here too.
@@ -91,18 +114,44 @@ function positionals(names, requiredCount) {
91
114
  };
92
115
  }
93
116
  const NO_POSITIONALS = { type: "array", maxItems: 0 };
94
- /** The argv after `--`, for the two verbs that take one. */
117
+ /** The argv after `--`, for the verbs that take one. */
95
118
  const TRAILING = {
96
119
  type: "array",
97
120
  items: STRING,
98
121
  minItems: 1,
99
122
  description: "everything after the first `--`, passed through untouched",
100
123
  };
101
- /** Flags are `string | boolean` (see `args.ts`); nothing else can be parsed. */
124
+ /**
125
+ * Path-typed flags, by the input schema they belong to (APRV-421).
126
+ *
127
+ * A flag declared `"path"` PUBLISHES as an ordinary `{"type":"string"}` — the
128
+ * wire contract is unchanged and no consumer sees a new keyword — while the
129
+ * registry retains the fact that its value NAMES A FILE. `approval serve`
130
+ * reads that through {@link pathFlagsOf} to confine such a value to the store
131
+ * the server was started for, so a flag added to this file tomorrow is
132
+ * confined on the day it appears rather than on the day somebody remembers to
133
+ * add it to a list kept somewhere else.
134
+ *
135
+ * A WeakMap keyed by the schema object, rather than a field on {@link VerbSpec},
136
+ * because `input()` returns that schema and all hundred-odd call sites would
137
+ * otherwise have to change to carry a second value.
138
+ */
139
+ const PATH_FLAGS_BY_INPUT = new WeakMap();
140
+ /** The flags of `spec` whose value names a filesystem path. */
141
+ export function pathFlagsOf(spec) {
142
+ return PATH_FLAGS_BY_INPUT.get(spec.input) ?? [];
143
+ }
144
+ /**
145
+ * Flags are `string | boolean` (see `args.ts`); nothing else can be parsed.
146
+ * `path` is a `string` the registry knows more about — see above.
147
+ */
102
148
  function input(parts) {
103
149
  const flagProperties = {};
150
+ const pathFlags = [];
104
151
  for (const [flag, kind] of Object.entries(parts.flags)) {
105
- flagProperties[flag] = kind === "string" ? STRING : BOOLEAN;
152
+ if (kind === "path")
153
+ pathFlags.push(flag);
154
+ flagProperties[flag] = kind === "boolean" ? BOOLEAN : STRING;
106
155
  }
107
156
  const properties = {
108
157
  positionals: parts.positionals ?? NO_POSITIONALS,
@@ -115,18 +164,20 @@ function input(parts) {
115
164
  };
116
165
  if (parts.trailing !== undefined)
117
166
  properties["trailing"] = parts.trailing;
118
- return {
167
+ const schema = {
119
168
  type: "object",
120
169
  properties,
121
170
  required: [],
122
171
  additionalProperties: false,
123
172
  };
173
+ PATH_FLAGS_BY_INPUT.set(schema, pathFlags);
174
+ return schema;
124
175
  }
125
176
  /** Flags every command accepts. */
126
177
  const HELP_FLAGS = { "--help": "boolean", "-h": "boolean" };
127
178
  const JSON_FLAG = { "--json": "boolean" };
128
- const LOG_FLAG = { "--log": "string" };
129
- const POLICY_FLAGS = { "--policy": "string", "--dir": "string" };
179
+ const LOG_FLAG = { "--log": "path" };
180
+ const POLICY_FLAGS = { "--policy": "path", "--dir": "path" };
130
181
  const AS_FLAG = { "--as": "string" };
131
182
  /**
132
183
  * The shared failure shape. Usage and I/O failures print
@@ -253,6 +304,8 @@ const POLICY_RESOLUTION_OUTPUT = object({
253
304
  winner: BOOLEAN,
254
305
  tieBreak: STRING,
255
306
  }, ["pattern", "specificity", "autonomy", "winner", "tieBreak"])),
307
+ // APRV-445: may an agent open a request in this class with `propose`.
308
+ agentRequest: object({ allowed: BOOLEAN, explicit: BOOLEAN, pattern: nullable(STRING), humanOnly: BOOLEAN }, ["allowed", "explicit", "pattern", "humanOnly"]),
256
309
  decisionPath: arrayOf(STRING),
257
310
  }, [
258
311
  "class",
@@ -266,6 +319,7 @@ const POLICY_RESOLUTION_OUTPUT = object({
266
319
  "irreversibility",
267
320
  "irreversiblePatterns",
268
321
  "candidates",
322
+ "agentRequest",
269
323
  "decisionPath",
270
324
  ]);
271
325
  // ---------------------------------------------------------------------------
@@ -289,7 +343,7 @@ const VERBS = [
289
343
  purpose: "Scaffold a working directory: APPROVAL.md (SPEC.md §5.1's canonical policy, to be read and edited), the empty .approval/log/ directory, .approval/QUEUE.md in its empty state, and the .gitignore lines for the index, the vault, the environment source map and the atomic-write temp files. It appends nothing, attests nothing, and overwrites nothing; a re-run writes nothing and reports what already exists.",
290
344
  human_only: true,
291
345
  human_only_note: "It writes the policy file a human must then read and attest, in a directory a human chose. Nothing it writes is operative, but a scaffold an agent could drop into a tree is a policy file nobody decided to have.",
292
- input: input({ flags: { "--dir": "string", ...JSON_FLAG, ...HELP_FLAGS } }),
346
+ input: input({ flags: { "--dir": "path", ...JSON_FLAG, ...HELP_FLAGS } }),
293
347
  output: object({
294
348
  ok: { const: true },
295
349
  dir: STRING,
@@ -305,7 +359,7 @@ const VERBS = [
305
359
  purpose: "Interactive solo setup: asks for a human id, terminal or Telegram, and five manual class families; writes a new policy and identity configuration, shows the exact bytes, requires typed understood, then attests. It classifies policy.core, refuses non-terminal input and --json, and never overwrites an existing policy.",
306
360
  human_only: true,
307
361
  human_only_note: "This command authors and attests APPROVAL.md and declares APPROVAL_HUMAN. It is reserved to a person at a terminal and is never exposed through MCP.",
308
- input: input({ flags: { "--dir": "string", "--api-base": "string", ...HELP_FLAGS } }),
362
+ input: input({ flags: { "--dir": "path", "--api-base": "string", ...HELP_FLAGS } }),
309
363
  output: null,
310
364
  error: ERROR_SCHEMA,
311
365
  exit_codes: BASE_EXIT_CODES,
@@ -323,6 +377,9 @@ const VERBS = [
323
377
  records: nullable(INTEGER),
324
378
  head: HEAD,
325
379
  intactThroughSeq: INTEGER,
380
+ tear: { enum: ["nul-filled", "partial-line"] },
381
+ tornBytes: INTEGER,
382
+ intactBytes: INTEGER,
326
383
  firstBadSeq: nullable(INTEGER),
327
384
  reason: STRING,
328
385
  message: STRING,
@@ -533,13 +590,14 @@ const VERBS = [
533
590
  {
534
591
  name: "policy",
535
592
  subcommand: "attest",
536
- purpose: "Record a human's sign-off on the policy file's exact bytes, as one policy.updated event carrying their SHA-256. Gate operations refuse while the live file is unattested or has changed since the last attestation, so an edited policy is inoperative until a human re-attests it. With --organ <path> it attests one of the gate's ORGANS instead — the harness files that install the hook — as one gate.organ.attested event no gate operation reads: those paths are human-only, so no grant for a hand edit to one can exist and this record is the only evidence the protected-path guard can accept (APRV-272). With --path <path> it signs off an ordinary PROTECTED path (policy.edit or a policy.edit.* sub-class) as one gate.path.signed_off event, which is what resolves SPEC.md's `(Amended APRV-n, pending sign-off.)` suffix: whole-file evidence that a human read those bytes, weaker than the hunk a grant binds, and read by the protected-path guard only after its grant search has failed (APRV-338). All three routes are human-only, refuse an agent actor with a machine-readable code, and compute their digest in the runtime.",
593
+ purpose: "Record a human's sign-off on the policy file's exact bytes, as one policy.updated event carrying their SHA-256. Gate operations refuse while the live file is unattested or has changed since the last attestation, so an edited policy is inoperative until a human re-attests it. With --organ <path> it attests one of the gate's ORGANS instead — the harness files that install the hook — as one gate.organ.attested event no gate operation reads: those paths are human-only, so no grant for a hand edit to one can exist and this record is the only evidence the protected-path guard can accept (APRV-272). With --path <path> it signs off an ordinary PROTECTED path (policy.edit or a policy.edit.* sub-class) as one gate.path.signed_off event, which is what resolves SPEC.md's `(Amended APRV-n, pending sign-off.)` suffix: whole-file evidence that a human read those bytes, weaker than the hunk a grant binds, and read by the protected-path guard only after its grant search has failed (APRV-338). All three routes are human-only, refuse an agent actor with a machine-readable code, and compute their digest in the runtime. With --bootstrap it attests a store's FIRST policy only, reading the verified log first: a re-run over the same bytes refuses policy-already-attested and changed bytes refuse policy-amendment-required, both at exit 1 with nothing appended, which is the operator's provisioning step for a tenant with no shell (APRV-449).",
537
594
  human_only: true,
538
595
  input: input({
539
596
  flags: {
540
597
  ...POLICY_FLAGS,
541
598
  "--organ": "string",
542
599
  "--path": "string",
600
+ "--bootstrap": "boolean",
543
601
  ...AS_FLAG,
544
602
  ...LOG_FLAG,
545
603
  ...JSON_FLAG,
@@ -668,7 +726,7 @@ const VERBS = [
668
726
  flags: {
669
727
  "--action": "string",
670
728
  ...AS_FLAG,
671
- "--payload": "string",
729
+ "--payload": "path",
672
730
  ...POLICY_FLAGS,
673
731
  ...LOG_FLAG,
674
732
  ...JSON_FLAG,
@@ -688,6 +746,66 @@ const VERBS = [
688
746
  error: ERROR_SCHEMA,
689
747
  exit_codes: BASE_EXIT_CODES,
690
748
  },
749
+ {
750
+ name: "propose",
751
+ purpose: "Register and request ONE action in one call, with the payload inline as a JSON object (--payload-json, at most 262144 bytes) rather than a file, for a requester with no file on the gate's machine. Unlike request, the caller names the class, and that is bounded by the operator: the class must be an EXACT key of the policy's classes (a wildcard match does not count) and that key or a declared <prefix>.* family must set agent_may_request: true, else class-not-agent-requestable and nothing is written. --key MUST begin with <class>: (key-class-mismatch otherwise); consumers read the class from the record. --key is at most 1024 bytes and --summary at most 4096; lone surrogates and non-finite numbers are usage errors. The task id is derived from the actor, class and key (propose:<32 hex>), so a retry with the same class, key and bytes appends nothing and answers idempotent:true while the request is pending, usably granted, refused or executed (state executed); a request that can no longer be answered usefully (withdrawn, expired, a grant whose window lapsed, or one voided by a re-attested policy) is asked again by the same call. The same key with different bytes refuses duplicate-request while a request is live and payload-mismatch otherwise. decision requested: a human is asked, poll with wait <task> --timeout 0. decision autonomous or supervised: nothing is asked, act and then record it with start. The summary is stored in the log in cleartext; the payload is stored in the payload store, never in the log.",
752
+ human_only: false,
753
+ input: input({
754
+ flags: {
755
+ "--class": "string",
756
+ "--key": "string",
757
+ "--summary": "string",
758
+ "--payload-json": "string",
759
+ ...AS_FLAG,
760
+ ...POLICY_FLAGS,
761
+ ...LOG_FLAG,
762
+ ...JSON_FLAG,
763
+ ...HELP_FLAGS,
764
+ },
765
+ }),
766
+ output: object({
767
+ ok: { const: true },
768
+ task: STRING,
769
+ action_key: STRING,
770
+ class: STRING,
771
+ payload_hash: SHA256,
772
+ decision: { enum: ["requested", "autonomous", "supervised"] },
773
+ state: nullable({
774
+ enum: ["requested", "granted", "rejected", "revoked", "expired", "withdrawn", "executed"],
775
+ }),
776
+ seq: nullable(INTEGER),
777
+ idempotent: BOOLEAN,
778
+ }, ["ok", "task", "action_key", "class", "payload_hash", "decision", "state", "seq", "idempotent"]),
779
+ error: ERROR_SCHEMA,
780
+ exit_codes: BASE_EXIT_CODES,
781
+ },
782
+ {
783
+ name: "start",
784
+ purpose: "Record execution.started for a proposed action you are about to carry out yourself, once per key. Proposals only (task-not-proposal otherwise). REQUESTER-ONLY: the actor must be the one that registered the task. --payload-json is the bytes you are about to act on and must hash to the registered payload_hash. A key whose request is pending or granted-and-unspent spends its harness grant (refused unless granted, unexpired, unspent and under the policy it was asked under); a key with no such request (none, or one withdrawn, expired, rejected or revoked) is recorded as authorized by the policy (refused for a manual or human-only class). It grants nothing: it writes only what the grant or the policy already authorized, and a second start of the same key is already-executed.",
785
+ human_only: false,
786
+ input: input({
787
+ positionals: positionals([{ name: "task", description: "the task id propose returned" }], 1),
788
+ flags: {
789
+ "--action": "string",
790
+ "--payload-json": "string",
791
+ ...AS_FLAG,
792
+ ...POLICY_FLAGS,
793
+ ...LOG_FLAG,
794
+ ...JSON_FLAG,
795
+ ...HELP_FLAGS,
796
+ },
797
+ }),
798
+ output: object({
799
+ ok: { const: true },
800
+ task: STRING,
801
+ action_key: STRING,
802
+ class: STRING,
803
+ authorization: { enum: ["grant", "policy"] },
804
+ seq: INTEGER,
805
+ }, ["ok", "task", "action_key", "class", "authorization", "seq"]),
806
+ error: ERROR_SCHEMA,
807
+ exit_codes: BASE_EXIT_CODES,
808
+ },
691
809
  {
692
810
  name: "grant",
693
811
  purpose: "Record a human approval of a pending request and mint the single-use execution token for its action. The raw token is PRINTED ONCE here and stored nowhere: the log holds only its SHA-256, so nothing can recover it afterwards. Budgets are re-evaluated at grant time, because the moment that matters for a commitment is the moment the human commits.",
@@ -1163,7 +1281,7 @@ const VERBS = [
1163
1281
  },
1164
1282
  {
1165
1283
  name: "wait",
1166
- purpose: "Block until every approval.requested of a task has a decision, or the timeout elapses. THE EXIT CODE IS THE DECISION: 0 granted, 1 rejected, revoked or withdrawn, 3 expired, 6 timeout. It writes nothing by default, not even the expiry it may derive; --withdraw-on-timeout is the one exception, appending approval.withdrawn for the requests this actor opened so a question nobody can answer to does not sit in a human's queue. Only the manual path produces requests to wait for, so a task with none returns immediately at exit 0. Under policy token_delivery: sealed, a granted action's --json entry also carries the raw execution token, opened from the grant's ciphertext with the private key this machine kept when it opened the request; that removes the terminal paste and works across machines. Recovering a minted token is not minting one: it still exists only because a human granted it, still binds to the payload bytes, and is still single-use.",
1284
+ purpose: "Block until every approval.requested of a task has a decision, or the timeout elapses. THE EXIT CODE IS THE DECISION: 0 granted (or nothing-to-wait-for, which `status` names and which is no grant), 1 rejected, revoked, withdrawn or not-registered, 3 expired (a grant whose request window lapsed included), 6 timeout, 7 void (a re-attest voided a pending request or a harness grant: ask again). It writes nothing by default, not even the expiry it may derive; --withdraw-on-timeout is the one exception, appending approval.withdrawn for the requests this actor opened so a question nobody can answer to does not sit in a human's queue. Only the manual path produces requests to wait for, so a registered task with none, or whose every grant an execution has already spent, returns immediately at exit 0 with status nothing-to-wait-for, NEVER granted; a task the log has never registered is refused not-registered at exit 1 (APRV-428). --timeout 0 reads the current state once and never sleeps (an undecided request answers timeout, exit 6, at once; it refuses --withdraw-on-timeout): use it to poll through approval serve, which runs every call through one queue that a sleeping wait would hold. Under policy token_delivery: sealed, a granted action's --json entry also carries the raw execution token, opened from the grant's ciphertext with the private key this machine kept when it opened the request; that removes the terminal paste and works across machines. Recovering a minted token is not minting one: it still exists only because a human granted it, still binds to the payload bytes, and is still single-use.",
1167
1285
  human_only: false,
1168
1286
  input: input({
1169
1287
  positionals: positionals([{ name: "task", description: "the task id" }], 1),
@@ -1181,7 +1299,18 @@ const VERBS = [
1181
1299
  output: object({
1182
1300
  ok: BOOLEAN,
1183
1301
  task: STRING,
1184
- status: { enum: ["granted", "rejected", "withdrawn", "expired", "timeout"] },
1302
+ status: {
1303
+ enum: [
1304
+ "granted",
1305
+ "executed",
1306
+ "rejected",
1307
+ "withdrawn",
1308
+ "void",
1309
+ "expired",
1310
+ "nothing-to-wait-for",
1311
+ "timeout",
1312
+ ],
1313
+ },
1185
1314
  actions: arrayOf(object({
1186
1315
  action_key: STRING,
1187
1316
  state: STRING,
@@ -1195,10 +1324,13 @@ const VERBS = [
1195
1324
  }, ["ok", "task", "status", "actions"]),
1196
1325
  error: ERROR_SCHEMA,
1197
1326
  exit_codes: [
1198
- { code: 0, meaning: "granted (a task with no requests is granted vacuously)" },
1327
+ {
1328
+ code: 0,
1329
+ meaning: "granted; or executed on the policy's own authority after its request ended; or nothing-to-wait-for, a registered task with no requests or whose every grant is already spent. `status` says which, and only `granted` is a grant",
1330
+ },
1199
1331
  {
1200
1332
  code: 1,
1201
- meaning: "NOT AUTHORIZED and terminal — a human said no (rejected/revoked), or the requester withdrew the request; or the log is corrupt. `status` says which",
1333
+ meaning: "NOT AUTHORIZED and terminal — a human said no (rejected/revoked), or the requester withdrew the request (`status` says which); or the task has no task.registered record (refusal `not-registered`); or the log is corrupt",
1202
1334
  },
1203
1335
  USAGE,
1204
1336
  { code: 3, meaning: "EXPIRED — the TTL lapsed before a decision landed; or a torn tail" },
@@ -1207,6 +1339,10 @@ const VERBS = [
1207
1339
  code: 6,
1208
1340
  meaning: "TIMEOUT — the wait elapsed with request(s) undecided. Nothing was appended, they are still live, and waiting again is legitimate",
1209
1341
  },
1342
+ {
1343
+ code: 7,
1344
+ meaning: "VOID — the policy was re-attested since the request was routed, and the answer can never be used: a PENDING request (any task; no decision may land on it) or a HARNESS grant (execution: harness, every proposal included; its spend refuses policy-drift). A token grant for approval run is not void and reads granted. Ask again: for a proposal, the same propose call re-files",
1345
+ },
1210
1346
  ],
1211
1347
  },
1212
1348
  {
@@ -1224,7 +1360,20 @@ const VERBS = [
1224
1360
  requested_ts: STRING,
1225
1361
  seq: INTEGER,
1226
1362
  ttl_remaining_ms: nullable(INTEGER),
1227
- }, ["action_key", "task", "class", "est_cost_usd", "requested_ts", "seq", "ttl_remaining_ms"])),
1363
+ // APRV-423: the whole window `ttl_remaining_ms` counts down from,
1364
+ // which is the policy's TTL narrowed by the requesting hook's
1365
+ // harness cap; null only when nothing bounds the request.
1366
+ ttl_ms: nullable(INTEGER),
1367
+ }, [
1368
+ "action_key",
1369
+ "task",
1370
+ "class",
1371
+ "est_cost_usd",
1372
+ "requested_ts",
1373
+ "seq",
1374
+ "ttl_remaining_ms",
1375
+ "ttl_ms",
1376
+ ])),
1228
1377
  }, ["ok", "pending"]),
1229
1378
  error: ERROR_SCHEMA,
1230
1379
  exit_codes: [OK, USAGE, TORN, IO],
@@ -1312,13 +1461,15 @@ const VERBS = [
1312
1461
  },
1313
1462
  {
1314
1463
  name: "status",
1315
- purpose: "System HEALTH, from the log: attestation state, the latest chain verdict, dangling executions, indeterminate executions, budget headroom from a zero-cost probe, loop escalations, and the payload store's size. Exit 1 when any of those needs attention. `dangling` is executions the runtime meant to watch and did not, and never harness executions, which are terminal by design and gain no outcome; `indeterminate` is side effects that were attempted and whose fate nobody has established, and it appears only when there are some. This is what an operator must fix; `queue` is what a human must answer, and neither carries the other's content. Writes nothing.",
1464
+ purpose: "System HEALTH, from the log: attestation state, the latest chain verdict, dangling executions, indeterminate executions, budget headroom from a zero-cost probe, loop escalations, and the payload store's size. Exit 1 when any of those needs attention. `dangling` is executions the runtime meant to watch and did not, and never harness executions, which are terminal by design and gain no outcome; `indeterminate` is side effects that were attempted and whose fate nobody has established, and it appears only when there are some. `refusals` counts the decisions and the gestures a decision surface would not take, with the newest few of each, and it appears only when the log carries one: it is informational, like `coverage`, because a refusal is the gate having worked. This is what an operator must fix; `queue` is what a human must answer, and neither carries the other's content. Writes nothing.",
1316
1465
  human_only: false,
1317
1466
  input: input({ flags: { ...POLICY_FLAGS, ...LOG_FLAG, ...JSON_FLAG, ...HELP_FLAGS } }),
1318
1467
  output: object({
1319
1468
  ok: { const: true },
1320
1469
  healthy: BOOLEAN,
1321
- attestation: object({ state: STRING, seq: nullable(INTEGER) }, ["state", "seq"]),
1470
+ // APRV-449: `attested_by` is the actor of the attesting record at `seq`,
1471
+ // read from the verified log, `null` when there is none. Always present.
1472
+ attestation: object({ state: STRING, seq: nullable(INTEGER), attested_by: nullable(STRING) }, ["state", "seq", "attested_by"]),
1322
1473
  verification: object({ status: STRING, records: nullable(INTEGER) }, ["status", "records"]),
1323
1474
  dangling: arrayOf(OPEN_OBJECT),
1324
1475
  indeterminate: arrayOf(OPEN_OBJECT),
@@ -1346,10 +1497,34 @@ const VERBS = [
1346
1497
  reconciliation: arrayOf(OPEN_OBJECT),
1347
1498
  payload_store: OPEN_OBJECT,
1348
1499
  anomalies: arrayOf(OPEN_OBJECT),
1500
+ // APRV-376: the two refusal families, counted, with the newest few of
1501
+ // each. INFORMATIONAL and outside `healthy` and the exit code, exactly
1502
+ // as `anomalies` and `coverage` are: a refusal is the gate having
1503
+ // worked. Present only when the log carries one of either family, and
1504
+ // each family present only when it has one, so a repository where
1505
+ // nothing has been refused emits the object it always emitted. Neither
1506
+ // is reported anywhere else short of `approval log tail`.
1507
+ refusals: object({
1508
+ decision: REFUSAL_FAMILY,
1509
+ gesture: REFUSAL_FAMILY,
1510
+ }, []),
1349
1511
  // APRV-214: present only while a window stands, so a repository with
1350
1512
  // none emits the object it always emitted. It counts toward `healthy`,
1351
1513
  // which is why it is reported here rather than only by `gate status`.
1352
1514
  gate_window: OPEN_OBJECT,
1515
+ // APRV-383: the daemon identity this instance resolves — the id a daemon
1516
+ // started HERE would write onto every record it appends, where the id
1517
+ // came from, and the `daemons` allowlist in force for it. ALWAYS present,
1518
+ // like `coverage`, so every consumer sees the same three keys on every
1519
+ // log. INFORMATIONAL and outside `healthy` and the exit code: nothing
1520
+ // here is a fact about this log's integrity. `id: null` is an
1521
+ // `APPROVAL_DAEMON_ID` that is not an id; `allowed: null` is no
1522
+ // restriction, which is not the same fact as an empty list.
1523
+ daemon: object({
1524
+ id: nullable(STRING),
1525
+ source: nullable(STRING),
1526
+ allowed: { anyOf: [{ type: "array", items: STRING }, { type: "null" }] },
1527
+ }, ["id", "source", "allowed"]),
1353
1528
  }, [
1354
1529
  "ok",
1355
1530
  "healthy",
@@ -1362,6 +1537,7 @@ const VERBS = [
1362
1537
  "coverage",
1363
1538
  "reconciliation",
1364
1539
  "payload_store",
1540
+ "daemon",
1365
1541
  ]),
1366
1542
  error: ERROR_SCHEMA,
1367
1543
  exit_codes: [OK, { code: 1, meaning: "something needs attention" }, USAGE, IO],
@@ -1377,7 +1553,7 @@ const VERBS = [
1377
1553
  "--since": "string",
1378
1554
  "--until": "string",
1379
1555
  "--source": "string",
1380
- "--vault": "string",
1556
+ "--vault": "path",
1381
1557
  ...POLICY_FLAGS,
1382
1558
  ...LOG_FLAG,
1383
1559
  ...JSON_FLAG,
@@ -1426,9 +1602,9 @@ const VERBS = [
1426
1602
  flags: {
1427
1603
  ...LOG_FLAG,
1428
1604
  ...POLICY_FLAGS,
1429
- "--tasks": "string",
1605
+ "--tasks": "path",
1430
1606
  "--api-base": "string",
1431
- "--root": "string",
1607
+ "--root": "path",
1432
1608
  ...JSON_FLAG,
1433
1609
  ...HELP_FLAGS,
1434
1610
  },
@@ -1460,7 +1636,7 @@ const VERBS = [
1460
1636
  flags: {
1461
1637
  ...LOG_FLAG,
1462
1638
  "--policy-dir": "string",
1463
- "--policy": "string",
1639
+ "--policy": "path",
1464
1640
  "--payload-dir": "string",
1465
1641
  ...AS_FLAG,
1466
1642
  "--interactive": "boolean",
@@ -1539,6 +1715,28 @@ const VERBS = [
1539
1715
  error: ERROR_SCHEMA,
1540
1716
  exit_codes: [OK, { code: 1, meaning: "a credential variable is unset" }, USAGE],
1541
1717
  },
1718
+ {
1719
+ name: "channel",
1720
+ subcommand: "relay",
1721
+ purpose: "Serve POST /relay/gesture on loopback for an operator's control plane (APRV-455): propose, attest or decline a policy by sha256, or grant or reject a request by action key, attributed to the EdgeOS account the attested policy maps. Authenticated by APPROVAL_RELAY_SECRET from the launch environment; a post without it is refused and never logged. It holds no human identity: an unmapped account is refused sender-unmapped. The raw execution token of a grant never leaves the process.",
1722
+ human_only: true,
1723
+ human_only_note: "It records human decisions and attestations on the word of whoever holds a secret its launcher chose, so starting it is the operator's ceremony: no wrapper publishes it, neither serve credential reaches it, and the hook classifier holds the command line `policy.core` (human-only in the reference policy). The attribution it records is exactly as strong as control over who launches it.",
1724
+ input: input({
1725
+ flags: {
1726
+ "--listen": "string",
1727
+ "--port": "string",
1728
+ "--allow-non-loopback": "boolean",
1729
+ "--proposer": "string",
1730
+ ...POLICY_FLAGS,
1731
+ ...LOG_FLAG,
1732
+ ...JSON_FLAG,
1733
+ ...HELP_FLAGS,
1734
+ },
1735
+ }),
1736
+ output: null,
1737
+ error: ERROR_SCHEMA,
1738
+ exit_codes: BASE_EXIT_CODES,
1739
+ },
1542
1740
  {
1543
1741
  name: "daemon",
1544
1742
  subcommand: "run",
@@ -1548,8 +1746,8 @@ const VERBS = [
1548
1746
  input: input({
1549
1747
  flags: {
1550
1748
  ...LOG_FLAG,
1551
- "--tasks": "string",
1552
- "--out": "string",
1749
+ "--tasks": "path",
1750
+ "--out": "path",
1553
1751
  ...POLICY_FLAGS,
1554
1752
  "--interval": "string",
1555
1753
  "--debounce": "string",
@@ -1575,8 +1773,8 @@ const VERBS = [
1575
1773
  input: input({
1576
1774
  flags: {
1577
1775
  ...LOG_FLAG,
1578
- "--tasks": "string",
1579
- "--out": "string",
1776
+ "--tasks": "path",
1777
+ "--out": "path",
1580
1778
  ...POLICY_FLAGS,
1581
1779
  "--interval": "string",
1582
1780
  "--debounce": "string",
@@ -1616,6 +1814,33 @@ const VERBS = [
1616
1814
  error: ERROR_SCHEMA,
1617
1815
  exit_codes: READ_ONLY_EXIT_CODES,
1618
1816
  },
1817
+ {
1818
+ name: "payload",
1819
+ subcommand: "run",
1820
+ purpose: "Print the payload `approval run` will recompute for a command before it spawns it: the argv, the cwd, and — since APRV-401 — the size and SHA-256 of the script that argv names, so a grant over a script binds the SCRIPT'S BYTES and not its path. A script is a known interpreter (the shells, node, python3 and kin) followed by a path operand, or a path at argv[0] the kernel reads a shebang from. With --hash it prints the payload_hash instead, the value a task file's declaration carries. An argv naming no script carries no `script` key and hashes exactly as it did before the rule existed. An inline program is already in the argv; the interpreter itself, a launcher this runtime does not name, whatever the script itself reads or runs, and the window between this hash and the spawn are all outside the binding. Reads that one file and nothing else: no log, no policy, no network, no token, and it executes nothing.",
1821
+ human_only: false,
1822
+ human_only_note: "Agent-facing by construction: it describes a command an agent is about to ask permission for, before any approval exists, and produces no authority of any kind. The bytes it prints are checked at intake and recomputed at execution, so a wrong answer here refuses rather than widens.",
1823
+ input: input({
1824
+ positionals: NO_POSITIONALS,
1825
+ flags: {
1826
+ // Path-typed, and it matters: the value is the directory every relative
1827
+ // argv word is resolved against, so a caller who could name any
1828
+ // directory could ask this verb to digest any file under it.
1829
+ "--cwd": "path",
1830
+ "--hash": "boolean",
1831
+ ...JSON_FLAG,
1832
+ ...HELP_FLAGS,
1833
+ },
1834
+ trailing: TRAILING,
1835
+ }),
1836
+ // The canonical payload itself on stdout, as `agentmail-draft` does it: the
1837
+ // bytes ARE the result, and an envelope around them would be a second thing
1838
+ // to strip before hashing. `--hash --json` prints {ok, hash}, which is the
1839
+ // one shape here that is a RESULT rather than the payload.
1840
+ output: null,
1841
+ error: ERROR_SCHEMA,
1842
+ exit_codes: READ_ONLY_EXIT_CODES,
1843
+ },
1619
1844
  {
1620
1845
  name: "payload",
1621
1846
  subcommand: "agentmail-draft",
@@ -1667,7 +1892,7 @@ const VERBS = [
1667
1892
  "--message": "string",
1668
1893
  "--task": "string",
1669
1894
  "--session": "string",
1670
- "--journal": "string",
1895
+ "--journal": "path",
1671
1896
  ...AS_FLAG,
1672
1897
  ...JSON_FLAG,
1673
1898
  ...HELP_FLAGS,
@@ -1743,7 +1968,7 @@ const VERBS = [
1743
1968
  flags: {
1744
1969
  "--limit": "string",
1745
1970
  "--since": "string",
1746
- "--journal": "string",
1971
+ "--journal": "path",
1747
1972
  ...JSON_FLAG,
1748
1973
  ...HELP_FLAGS,
1749
1974
  },
@@ -1919,7 +2144,7 @@ const VERBS = [
1919
2144
  "--logs": "string",
1920
2145
  "--env-file": "string",
1921
2146
  "--exec": "string",
1922
- "--out": "string",
2147
+ "--out": "path",
1923
2148
  "--uninstall": "boolean",
1924
2149
  ...AS_FLAG,
1925
2150
  ...LOG_FLAG,
@@ -1940,7 +2165,7 @@ const VERBS = [
1940
2165
  positionals: positionals([{ name: "name", description: "the credential name" }], 1),
1941
2166
  flags: {
1942
2167
  "--value-env": "string",
1943
- "--vault": "string",
2168
+ "--vault": "path",
1944
2169
  ...LOG_FLAG,
1945
2170
  ...POLICY_FLAGS,
1946
2171
  ...AS_FLAG,
@@ -1960,7 +2185,7 @@ const VERBS = [
1960
2185
  human_only_note: "Names are not values, but the name set is a map of the machine's reach, and all three vault verbs resolve identity exactly as `policy attest` does. Keeping the noun human-only is what stops an agent's tooling from touching the credential store in passing.",
1961
2186
  input: input({
1962
2187
  flags: {
1963
- "--vault": "string",
2188
+ "--vault": "path",
1964
2189
  ...LOG_FLAG,
1965
2190
  ...POLICY_FLAGS,
1966
2191
  ...AS_FLAG,
@@ -1986,7 +2211,7 @@ const VERBS = [
1986
2211
  input: input({
1987
2212
  positionals: positionals([{ name: "name", description: "the credential name" }], 1),
1988
2213
  flags: {
1989
- "--vault": "string",
2214
+ "--vault": "path",
1990
2215
  ...LOG_FLAG,
1991
2216
  ...POLICY_FLAGS,
1992
2217
  ...AS_FLAG,
@@ -2013,9 +2238,9 @@ const VERBS = [
2013
2238
  positionals: positionals([{ name: "action-key", description: "the action's idempotency_key" }], 1),
2014
2239
  flags: {
2015
2240
  "--token": "string",
2016
- "--payload": "string",
2241
+ "--payload": "path",
2017
2242
  ...AS_FLAG,
2018
- "--vault": "string",
2243
+ "--vault": "path",
2019
2244
  ...POLICY_FLAGS,
2020
2245
  ...LOG_FLAG,
2021
2246
  "--timeout": "string",
@@ -2070,9 +2295,9 @@ const VERBS = [
2070
2295
  positionals: positionals([{ name: "action-key", description: "the action's idempotency_key" }], 1),
2071
2296
  flags: {
2072
2297
  "--token": "string",
2073
- "--payload": "string",
2298
+ "--payload": "path",
2074
2299
  ...AS_FLAG,
2075
- "--vault": "string",
2300
+ "--vault": "path",
2076
2301
  ...POLICY_FLAGS,
2077
2302
  ...LOG_FLAG,
2078
2303
  "--timeout": "string",
@@ -2126,7 +2351,7 @@ const VERBS = [
2126
2351
  input: input({
2127
2352
  positionals: positionals([{ name: "action-key", description: "the action's idempotency_key" }], 1),
2128
2353
  flags: {
2129
- "--token": "string", "--payload": "string", ...AS_FLAG, "--vault": "string",
2354
+ "--token": "string", "--payload": "path", ...AS_FLAG, "--vault": "path",
2130
2355
  ...POLICY_FLAGS, ...LOG_FLAG, "--timeout": "string", ...JSON_FLAG, ...HELP_FLAGS,
2131
2356
  },
2132
2357
  }),
@@ -2150,6 +2375,7 @@ const VERBS = [
2150
2375
  ...AS_FLAG,
2151
2376
  "--timeout": "string",
2152
2377
  "--interval": "string",
2378
+ "--harness-cap": "string",
2153
2379
  ...POLICY_FLAGS,
2154
2380
  ...LOG_FLAG,
2155
2381
  ...HELP_FLAGS,
@@ -2173,6 +2399,7 @@ const VERBS = [
2173
2399
  ...AS_FLAG,
2174
2400
  "--timeout": "string",
2175
2401
  "--interval": "string",
2402
+ "--harness-cap": "string",
2176
2403
  ...POLICY_FLAGS,
2177
2404
  ...LOG_FLAG,
2178
2405
  ...HELP_FLAGS,
@@ -2197,6 +2424,7 @@ const VERBS = [
2197
2424
  "--timeout": "string",
2198
2425
  "--interval": "string",
2199
2426
  "--retry-grace": "string",
2427
+ "--harness-cap": "string",
2200
2428
  ...POLICY_FLAGS,
2201
2429
  ...LOG_FLAG,
2202
2430
  ...HELP_FLAGS,
@@ -2221,6 +2449,7 @@ const VERBS = [
2221
2449
  "--timeout": "string",
2222
2450
  "--interval": "string",
2223
2451
  "--retry-grace": "string",
2452
+ "--harness-cap": "string",
2224
2453
  ...POLICY_FLAGS,
2225
2454
  ...LOG_FLAG,
2226
2455
  ...HELP_FLAGS,
@@ -2245,6 +2474,7 @@ const VERBS = [
2245
2474
  "--timeout": "string",
2246
2475
  "--interval": "string",
2247
2476
  "--retry-grace": "string",
2477
+ "--harness-cap": "string",
2248
2478
  ...POLICY_FLAGS,
2249
2479
  ...LOG_FLAG,
2250
2480
  ...HELP_FLAGS,
@@ -2257,6 +2487,31 @@ const VERBS = [
2257
2487
  { code: 2, meaning: "the hook is misconfigured or a PostToolUse outcome was not recorded; the diagnostic is on stderr" },
2258
2488
  ],
2259
2489
  },
2490
+ {
2491
+ name: "hook",
2492
+ subcommand: "hermes",
2493
+ purpose: "Read one Hermes Agent pre_tool_call or post_tool_call event on stdin (snake_case, and the event names are its own rather than Claude Code's), classify its terminal command or its write_file and patch paths and its read paths, resolve the class against APPROVAL.md, and answer allow or deny — waiting on a real decision when the class is manual. THE VERDICT IS NEVER 'ask'. It answers in ONE dialect and nothing else, Hermes's own {action, message} object: a deny is that object AT EXIT 2, which Hermes documents as an unconditional block whose message it reads from the stdout directive first (so body and code agree by the harness's own rule, and a refusal whose body never made it out still blocks); an allow is {} at exit 0, because Hermes has no allow directive and no directive is the allow, so the allow's reason goes to stderr. The Claude-compatible {decision, reason} form is never emitted beside it, because Muse established that a refusal hedged across dialects is read as a broken hook rather than as a stronger no. execute_code is refused outright (hook-hermes-execute-code-unbound): the call carries a program and no path, no argv and no working directory, so no class can be resolved and no payload can bind what it would do. A terminal call that names no absolute workdir, and a write_file, patch, read_file or search_files call whose path is relative or absent, are refused too (hook-unsupported-execution-context, APRV-415): the live probe found that Hermes resolves both against a per-session recorded directory that a cd moves and that no field of the event carries, so the verdict would bind bytes in a directory the hook cannot see, and the refusal names the retry — an absolute path — so the session repairs itself. A per-entry fail_closed makes a hook crash, timeout or unparseable output BLOCK, which the same probe OBSERVED on Hermes main at 118984d7 in three trials of three, making this the first harness since Claude Code whose hook is a gate rather than a backstop; its default is false and v0.21.3 (build 2026.9.14) ignores the key silently, so docs/hermes-hook.md states the version floor before it states anything else and approval doctor's harness-version row reports a build below it.",
2494
+ human_only: false,
2495
+ human_only_note: "The agent harness surface, so agent-facing by construction: Hermes invokes it around the agent's own tool calls. It proposes and waits; it never records a decision.",
2496
+ input: input({
2497
+ flags: {
2498
+ ...AS_FLAG,
2499
+ "--timeout": "string",
2500
+ "--interval": "string",
2501
+ "--retry-grace": "string",
2502
+ "--harness-cap": "string",
2503
+ ...POLICY_FLAGS,
2504
+ ...LOG_FLAG,
2505
+ ...HELP_FLAGS,
2506
+ },
2507
+ }),
2508
+ output: null,
2509
+ error: ERROR_SCHEMA,
2510
+ exit_codes: [
2511
+ { code: 0, meaning: "an allow ({} on stdout, the reason on stderr), and every post_tool_call report, which exits 0 whatever it found because Hermes reads exit 2 as a block and the call has already run" },
2512
+ { code: 2, meaning: "a deny: the {action:'block', message} object on stdout is the message and the exit code is the unconditional block under it; also a usage error before any event was read, which prints no verdict" },
2513
+ ],
2514
+ },
2260
2515
  {
2261
2516
  name: "hook",
2262
2517
  subcommand: "classify",
@@ -2278,7 +2533,7 @@ const VERBS = [
2278
2533
  human_only: false,
2279
2534
  input: input({
2280
2535
  positionals: positionals([{ name: "file", description: "the markdown file to parse" }], 1),
2281
- flags: { "--out": "string", ...JSON_FLAG, ...HELP_FLAGS },
2536
+ flags: { "--out": "path", ...JSON_FLAG, ...HELP_FLAGS },
2282
2537
  }),
2283
2538
  output: object({
2284
2539
  ok: { const: true },
@@ -2459,6 +2714,49 @@ const VERBS = [
2459
2714
  { code: 4, meaning: "the transport did not close cleanly" },
2460
2715
  ],
2461
2716
  },
2717
+ {
2718
+ name: "serve",
2719
+ purpose: "Serve the verbs of this registry over HTTP, in the foreground, for a harness in a sandbox that has no local log and no local policy. The published surface is `mcp serve`'s exactly — the registry filtered by human_only false, `--as` deleted from every schema, the launch identity appended last, no grant — plus the three things the MCP transport withheld for transport reasons: `hook/<harness>` taking the harness envelope as the request body and answering the bytes the stdin form prints, `log/follow` paged by an exclusive (seq, hash) cursor, and `export`, the store as an archive carrying the policy, the log, the payload bytes behind every payload_hash and the projections, and never the keys, the environment source map or the vault. TWO bearer credentials come from the launch environment and authorization is PER VERB: the agent one opens the hook and an allowlist of what a harness under oversight needs in order to ask and to act on a grant, the tenant one opens everything else, so the agent never reads the log it is judged by and a verb added to this registry is the tenant's until somebody widens the list. Binds loopback unless an explicit flag widens it; it terminates no TLS.",
2720
+ human_only: true,
2721
+ human_only_note: "An OPERATOR process, for `mcp serve`'s reason and one more. It is long-lived, launched by a person, and holds the agent identity every call is recorded under; on top of that it holds BOTH credentials, and the party that decides which door an agent gets is not the agent.",
2722
+ input: input({
2723
+ flags: {
2724
+ ...AS_FLAG,
2725
+ "--port": "string",
2726
+ "--listen": "string",
2727
+ "--allow-non-loopback": "boolean",
2728
+ "--hook-timeout": "string",
2729
+ "--hook-harness-cap": "string",
2730
+ "--hook-threads": "string",
2731
+ "--hook-queue": "string",
2732
+ ...POLICY_FLAGS,
2733
+ ...LOG_FLAG,
2734
+ ...JSON_FLAG,
2735
+ ...HELP_FLAGS,
2736
+ },
2737
+ }),
2738
+ output: null,
2739
+ error: ERROR_SCHEMA,
2740
+ exit_codes: [
2741
+ { code: 0, meaning: "the server was interrupted and closed cleanly" },
2742
+ USAGE,
2743
+ { code: 4, meaning: "the listener could not bind, or did not close cleanly" },
2744
+ ],
2745
+ },
2746
+ {
2747
+ name: "muse",
2748
+ purpose: "Start the single-tenant local Muse consumer prototype. It exposes bounded register and request proposals plus a verified pending inbox, status and canonical request detail, with distinct read and proposal launch credentials. It has no human decision, token, execution, export or generic verb route; the configured Telegram channel remains the human decision path.",
2749
+ human_only: true,
2750
+ human_only_note: "An operator launches this credential-holding listener and pins its tenant and store before accepting calls.",
2751
+ input: input({ flags: { "--dir": "path", "--port": "string", ...HELP_FLAGS } }),
2752
+ output: null,
2753
+ error: ERROR_SCHEMA,
2754
+ exit_codes: [
2755
+ { code: 0, meaning: "the local listener was interrupted and closed cleanly" },
2756
+ USAGE,
2757
+ { code: 4, meaning: "the local listener could not bind" },
2758
+ ],
2759
+ },
2462
2760
  {
2463
2761
  name: "reindex",
2464
2762
  purpose: "Rebuild the SQLite index projection from the log. The database is a cache and the log is the truth: the index is rebuilt from scratch at a temporary path and renamed into place. A corrupt log is refused outright and a torn tail is refused without --force. The log is never written to.",
@@ -2466,7 +2764,7 @@ const VERBS = [
2466
2764
  input: input({
2467
2765
  flags: {
2468
2766
  ...LOG_FLAG,
2469
- "--index": "string",
2767
+ "--index": "path",
2470
2768
  "--force": "boolean",
2471
2769
  ...JSON_FLAG,
2472
2770
  ...HELP_FLAGS,
@@ -2481,7 +2779,7 @@ const VERBS = [
2481
2779
  purpose: "Regenerate .approval/QUEUE.md, the read-only markdown queue projection of SPEC.md §9.1, whole, from the verified log. This is the screenshot and never the truth: editing the file authorizes nothing and the next render overwrites it. Writes exactly one file, atomically; a log that does not verify refuses and writes nothing.",
2482
2780
  human_only: false,
2483
2781
  input: input({
2484
- flags: { ...LOG_FLAG, "--out": "string", ...POLICY_FLAGS, ...JSON_FLAG, ...HELP_FLAGS },
2782
+ flags: { ...LOG_FLAG, "--out": "path", ...POLICY_FLAGS, ...JSON_FLAG, ...HELP_FLAGS },
2485
2783
  }),
2486
2784
  output: object({
2487
2785
  ok: { const: true },