@sema-agent/core 7.7.0 → 7.9.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 (186) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/send-message-tool.js +2 -1
  6. package/dist/agents/subagent-steps.js +5 -4
  7. package/dist/agents/subagent.d.ts +1 -1
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/core/ask-origin.d.ts +12 -1
  10. package/dist/core/ask-origin.js +5 -1
  11. package/dist/core/ask-question.js +2 -2
  12. package/dist/core/checkpoint-store.d.ts +64 -24
  13. package/dist/core/context-edit.d.ts +0 -22
  14. package/dist/core/context-edit.js +2 -11
  15. package/dist/core/engine-notice.d.ts +6 -0
  16. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  17. package/dist/core/fs-write-gate-policy.js +7 -7
  18. package/dist/core/gate-fold.js +2 -2
  19. package/dist/core/gate-lanes.js +53 -26
  20. package/dist/core/gate-outcome.d.ts +8 -4
  21. package/dist/core/gate-outcome.js +2 -1
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +6 -0
  24. package/dist/core/hooks.d.ts +64 -33
  25. package/dist/core/hooks.js +1 -1
  26. package/dist/core/lsp.js +2 -2
  27. package/dist/core/mcp-server-spec.d.ts +17 -0
  28. package/dist/core/mcp.js +3 -1
  29. package/dist/core/memory-engine/tools.js +4 -12
  30. package/dist/core/permission-rule-consent.d.ts +29 -36
  31. package/dist/core/permission-rule-consent.js +110 -60
  32. package/dist/core/permission-rule-model.d.ts +254 -55
  33. package/dist/core/permission-rule-model.js +323 -43
  34. package/dist/core/permission-rule-org.d.ts +9 -8
  35. package/dist/core/permission-rule-org.js +6 -15
  36. package/dist/core/permission-rule-provider.d.ts +10 -6
  37. package/dist/core/permission-rule-provider.js +12 -8
  38. package/dist/core/permission-rule-session.d.ts +7 -6
  39. package/dist/core/permission-rule-session.js +29 -10
  40. package/dist/core/permission-rule-store.d.ts +40 -18
  41. package/dist/core/permission-rule-store.js +68 -42
  42. package/dist/core/permission-rule-sync.d.ts +9 -3
  43. package/dist/core/permission-rule-sync.js +29 -25
  44. package/dist/core/permission-rule-syntax.d.ts +30 -0
  45. package/dist/core/permission-rule-syntax.js +44 -0
  46. package/dist/core/permission-rules.d.ts +85 -33
  47. package/dist/core/permission-rules.js +104 -88
  48. package/dist/core/persisted-rule-arms.d.ts +56 -0
  49. package/dist/core/persisted-rule-arms.js +48 -0
  50. package/dist/core/present-plan-tool.js +3 -4
  51. package/dist/core/runner/active-skill-scope.js +7 -4
  52. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  53. package/dist/core/runner/contracts.d.ts +46 -3
  54. package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
  55. package/dist/core/runner/permission-rule-lanes.js +27 -21
  56. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  58. package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
  59. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  60. package/dist/core/runner/prepare-defer-classify.js +10 -10
  61. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  62. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  63. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  64. package/dist/core/runner/prepare-gate-stations.js +4 -2
  65. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  66. package/dist/core/runner/prepare-hands-readface.js +3 -2
  67. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  70. package/dist/core/runner/prepare-lsp.js +3 -2
  71. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  72. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  73. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  74. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  75. package/dist/core/runner/prepare-park-ask.js +4 -1
  76. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  77. package/dist/core/runner/prepare-project-context.js +7 -5
  78. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  79. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  80. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  81. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  82. package/dist/core/runner/prepare-question-face.js +5 -3
  83. package/dist/core/runner/prepare-run-refs.d.ts +16 -0
  84. package/dist/core/runner/prepare-run-refs.js +6 -0
  85. package/dist/core/runner/prepare-safety-scan.js +48 -2
  86. package/dist/core/runner/prepare-task.js +37 -19
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  89. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  90. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  91. package/dist/core/runner/resume-admission.d.ts +53 -0
  92. package/dist/core/runner/resume-admission.js +83 -0
  93. package/dist/core/runner/resume-apply.d.ts +50 -0
  94. package/dist/core/runner/resume-apply.js +184 -0
  95. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  96. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  97. package/dist/core/runner/resume-claim.d.ts +32 -0
  98. package/dist/core/runner/resume-claim.js +27 -0
  99. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  100. package/dist/core/runner/resume-internals-and-config.js +50 -0
  101. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  102. package/dist/core/runner/resume-policy-outcome.js +127 -0
  103. package/dist/core/runner/resume-preflight.d.ts +40 -0
  104. package/dist/core/runner/resume-preflight.js +122 -0
  105. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  106. package/dist/core/runner/resume-review-outcome.js +88 -0
  107. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  108. package/dist/core/runner/run-harness-handlers.js +345 -0
  109. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  110. package/dist/core/runner/run-turn-boundary.js +694 -0
  111. package/dist/core/runner/runtask.d.ts +20 -0
  112. package/dist/core/runner/runtask.js +129 -1774
  113. package/dist/core/runner/session-rule-policy.js +9 -4
  114. package/dist/core/runner/synthetic-tools.js +5 -8
  115. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  116. package/dist/core/runner/tool-disclosure.js +9 -5
  117. package/dist/core/runner/tool-end-body.d.ts +12 -5
  118. package/dist/core/runner/tool-end-body.js +8 -7
  119. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  120. package/dist/core/runner/tool-face-overlay.js +125 -0
  121. package/dist/core/runner/tool-output-projection.d.ts +14 -1
  122. package/dist/core/runner/tool-output-projection.js +24 -18
  123. package/dist/core/runner/turn-attachments.d.ts +2 -2
  124. package/dist/core/sensitive-path-policy.d.ts +3 -0
  125. package/dist/core/sensitive-path-policy.js +6 -5
  126. package/dist/core/shared-memory/tools.js +3 -6
  127. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  128. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  129. package/dist/core/task-event.d.ts +10 -0
  130. package/dist/core/task-registry.js +4 -5
  131. package/dist/core/task-spec.d.ts +8 -0
  132. package/dist/core/tool-catalog-entries.d.ts +48 -0
  133. package/dist/core/tool-catalog-entries.js +174 -0
  134. package/dist/core/tool-conformance.d.ts +22 -0
  135. package/dist/core/tool-conformance.js +37 -0
  136. package/dist/core/tool-face.d.ts +171 -0
  137. package/dist/core/tool-face.js +25 -0
  138. package/dist/core/tool-policy.d.ts +24 -0
  139. package/dist/core/tool-policy.js +4 -2
  140. package/dist/core/tool-registry.d.ts +114 -0
  141. package/dist/core/tool-registry.js +114 -0
  142. package/dist/core/tool-result-budget.d.ts +0 -3
  143. package/dist/core/tool-result-budget.js +4 -3
  144. package/dist/core/tool-result-store.d.ts +0 -18
  145. package/dist/core/tool-result-store.js +2 -18
  146. package/dist/core/tool-roster.d.ts +459 -0
  147. package/dist/core/tool-roster.js +507 -0
  148. package/dist/core/tool-spec.d.ts +54 -3
  149. package/dist/core/tools.js +7 -0
  150. package/dist/core/wiring-manifest.d.ts +13 -0
  151. package/dist/core/wiring-manifest.js +2 -1
  152. package/dist/core/write-protect.d.ts +2 -1
  153. package/dist/core/write-protect.js +5 -4
  154. package/dist/engine/compaction/utils.d.ts +6 -0
  155. package/dist/engine/compaction/utils.js +19 -19
  156. package/dist/engine/loop/types.d.ts +6 -0
  157. package/dist/index.d.ts +17 -6
  158. package/dist/index.js +17 -6
  159. package/dist/orchestration/goal.js +2 -1
  160. package/dist/orchestration/run-spec.js +3 -5
  161. package/dist/orchestration/run-workflow-tool.js +2 -2
  162. package/dist/prompt-assembly/event-registry.js +3 -3
  163. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  164. package/dist/stores/file/checkpoint-store.js +1 -1
  165. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  166. package/dist/stores/file/permission-rule-store.js +64 -16
  167. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  168. package/dist/tools/fs/fs-bash.d.ts +1 -1
  169. package/dist/tools/fs/fs-bash.js +7 -11
  170. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  171. package/dist/tools/fs/fs-read.js +2 -2
  172. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  173. package/dist/tools/fs/fs-search-tools.js +3 -16
  174. package/dist/tools/fs/fs-write.js +4 -6
  175. package/dist/tools/fs/notebook.d.ts +1 -1
  176. package/dist/tools/fs/repo-map.js +2 -2
  177. package/dist/tools/fs/safety.d.ts +3 -2
  178. package/dist/tools/fs/safety.js +6 -5
  179. package/dist/tools/monitor.js +2 -2
  180. package/dist/tools/scheduler-tools.js +5 -8
  181. package/dist/tools/task-list.js +5 -8
  182. package/dist/tools/todo.js +2 -2
  183. package/dist/tools/web.js +3 -6
  184. package/dist/tools/worktree.js +3 -4
  185. package/package.json +4 -1
  186. package/test/export-surface.snapshot.json +189 -17
@@ -1,17 +1,18 @@
1
1
  import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
+ import { isRuleBehavior, readRowBehavior } from "../../core/permission-rule-model.js";
4
5
  import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, } from "../../core/permission-rule-store.js";
5
6
  import { canonicalize } from "../../core/canonical-json.js";
6
7
  import { BootLock } from "./fs-atomic.js";
7
8
  import { assertAdoptionBootGate } from "./adoption/marker.js";
8
9
  function isRuleFile(v) {
9
10
  const p = v;
10
- return typeof p === "object" && p !== null && p.schemaVersion === 3 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined));
11
+ return typeof p === "object" && p !== null && p.schemaVersion === 4 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined));
11
12
  }
12
- function isLegacyRuleFile(v) {
13
+ function isOlderRuleFile(v) {
13
14
  const p = v;
14
- return typeof p === "object" && p !== null && (p.schemaVersion === 1 || p.schemaVersion === 2) && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number";
15
+ return typeof p === "object" && p !== null && (p.schemaVersion === 1 || p.schemaVersion === 2 || p.schemaVersion === 3) && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number";
15
16
  }
16
17
  function isOrgStateFile(v) {
17
18
  const p = v;
@@ -24,7 +25,7 @@ function jsonForm(v) {
24
25
  return JSON.parse(JSON.stringify(v));
25
26
  }
26
27
  export const ORG_STATE_FILE = "org.json";
27
- export const SCHEMA3_MARKER_FILE = ".rule-schema-3";
28
+ export const SCHEMA_MARKER_FILE = ".rule-schema-4";
28
29
  export class OrgRevisionRefusal extends Error {
29
30
  }
30
31
  const LOCAL_OWNER_FILE = "local-owner.json";
@@ -158,8 +159,8 @@ class FileDurableRulePartition {
158
159
  }
159
160
  if (!isRuleFile(parsed)) {
160
161
  const sv = schemaVersionWord(parsed);
161
- const why = sv === 1 || sv === 2
162
- ? `${this.file} is a schema ${String(sv)} bucket that was not migrated to schema 3 (written by an older engine after this directory was migrated?); refusing the whole file and loading zero rules`
162
+ const why = sv === 1 || sv === 2 || sv === 3
163
+ ? `${this.file} is a schema ${String(sv)} bucket that was not migrated to schema 4 (written by an older engine after this directory was migrated?); refusing the whole file and loading zero rules`
163
164
  : `${this.file} does not carry a readable rule-file shape; refusing the whole file and loading zero rules`;
164
165
  this.disclose(why);
165
166
  return { unreadable: why };
@@ -170,6 +171,19 @@ class FileDurableRulePartition {
170
171
  this.disclose(why);
171
172
  return { unreadable: why };
172
173
  }
174
+ const rowKinds = [
175
+ ["rule", parsed.rules],
176
+ ["tombstone", parsed.tombstones],
177
+ ["quarantined", parsed.quarantined ?? []],
178
+ ];
179
+ for (const [kind, rows] of rowKinds) {
180
+ const bad = rows.findIndex((r) => !isRuleBehavior(r?.behavior));
181
+ if (bad !== -1) {
182
+ const why = `${this.file} carries a ${kind} row (#${bad}) whose behavior word is outside deny/ask/allow — a schema 4 row must name its behavior; refusing the whole file and loading zero rules`;
183
+ this.disclose(why);
184
+ return { unreadable: why };
185
+ }
186
+ }
173
187
  return { file: parsed };
174
188
  }
175
189
  disclose(message) {
@@ -218,7 +232,7 @@ class FileDurableRulePartition {
218
232
  throw new Error(`refusing to write the permission-rule store while it cannot be read: ${r.unreadable}`);
219
233
  }
220
234
  if ("absent" in r) {
221
- return { schemaVersion: 3, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] };
235
+ return { schemaVersion: 4, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] };
222
236
  }
223
237
  return {
224
238
  schemaVersion: r.file.schemaVersion,
@@ -324,7 +338,7 @@ export class FileDurableRulePartitionProvider {
324
338
  migrateLegacyBuckets() {
325
339
  if (!existsSync(this.dir))
326
340
  return;
327
- if (existsSync(join(this.dir, SCHEMA3_MARKER_FILE)))
341
+ if (existsSync(join(this.dir, SCHEMA_MARKER_FILE)))
328
342
  return;
329
343
  try {
330
344
  assertSafeDir(this.dir);
@@ -359,7 +373,7 @@ export class FileDurableRulePartitionProvider {
359
373
  legacy.push({ name, file: read });
360
374
  }
361
375
  if (legacy.length === 0) {
362
- this.writeSchema3Marker();
376
+ this.writeSchemaMarker();
363
377
  return;
364
378
  }
365
379
  const orgHome = this.fileOrgPersistence();
@@ -380,12 +394,18 @@ export class FileDurableRulePartitionProvider {
380
394
  }
381
395
  }
382
396
  }
383
- const next = { ...rest, schemaVersion: 3 };
397
+ const stamped = stampRowBehaviors(rest);
398
+ if (stamped === undefined) {
399
+ complete = false;
400
+ this.onError?.(`bucket ${name} left untouched: a row carries a behavior word outside deny/ask/allow, which no engine version wrote; the file is not migrated`);
401
+ continue;
402
+ }
403
+ const next = { ...stamped, schemaVersion: 4 };
384
404
  atomicPublish(this.dir, join(this.dir, name), JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
385
- this.onError?.(`migrated ${name} to rule-file schema 3${org !== undefined ? ` (its org block moved to ${ORG_STATE_FILE})` : ""}`);
405
+ this.onError?.(`migrated ${name} to rule-file schema 4${org !== undefined ? ` (its org block moved to ${ORG_STATE_FILE})` : ""}`);
386
406
  }
387
407
  if (complete)
388
- this.writeSchema3Marker();
408
+ this.writeSchemaMarker();
389
409
  }
390
410
  finally {
391
411
  if (!held) {
@@ -394,12 +414,12 @@ export class FileDurableRulePartitionProvider {
394
414
  }
395
415
  }
396
416
  }
397
- writeSchema3Marker() {
417
+ writeSchemaMarker() {
398
418
  try {
399
- atomicPublish(this.dir, join(this.dir, SCHEMA3_MARKER_FILE), JSON.stringify({ schemaVersion: 3 }));
419
+ atomicPublish(this.dir, join(this.dir, SCHEMA_MARKER_FILE), JSON.stringify({ schemaVersion: 4 }));
400
420
  }
401
421
  catch (err) {
402
- this.onError?.(`could not write ${SCHEMA3_MARKER_FILE}: ${err.message}; the legacy scan will run again next construction`);
422
+ this.onError?.(`could not write ${SCHEMA_MARKER_FILE}: ${err.message}; the legacy scan will run again next construction`);
403
423
  }
404
424
  }
405
425
  orgStatePersistence() {
@@ -449,6 +469,34 @@ export class FileDurableRulePartitionProvider {
449
469
  this.lock = undefined;
450
470
  }
451
471
  }
472
+ function stampRowBehaviors(body) {
473
+ const rules = [];
474
+ for (const r of body.rules) {
475
+ const behavior = readRowBehavior(r.behavior);
476
+ if (behavior === undefined)
477
+ return undefined;
478
+ rules.push({ ...r, behavior });
479
+ }
480
+ const tombstones = [];
481
+ for (const t of body.tombstones) {
482
+ const behavior = readRowBehavior(t.behavior);
483
+ if (behavior === undefined)
484
+ return undefined;
485
+ tombstones.push({ ...t, behavior });
486
+ }
487
+ let quarantined;
488
+ if (body.quarantined !== undefined) {
489
+ quarantined = [];
490
+ for (const q of body.quarantined) {
491
+ const behavior = readRowBehavior(q.behavior);
492
+ if (behavior === undefined)
493
+ return undefined;
494
+ quarantined.push({ ...q, behavior });
495
+ }
496
+ }
497
+ const { rules: _r, tombstones: _t, quarantined: _q, ...rest } = body;
498
+ return { ...rest, rules, tombstones, ...(quarantined !== undefined ? { quarantined } : {}) };
499
+ }
452
500
  function readLegacyBucket(file) {
453
501
  let raw;
454
502
  try {
@@ -467,7 +515,7 @@ function readLegacyBucket(file) {
467
515
  catch {
468
516
  return undefined;
469
517
  }
470
- if (!isLegacyRuleFile(parsed))
518
+ if (!isOlderRuleFile(parsed))
471
519
  return undefined;
472
520
  const { checksum, ...body } = parsed;
473
521
  return checksum === checksumOf(body) ? parsed : undefined;
@@ -80,7 +80,7 @@ export interface LeadingCommandNameOptions {
80
80
  * name instead of refusing it.
81
81
  *
82
82
  * OPT-IN, and used by ONE family of callers: the persisted permission-RULE lane
83
- * ({@link import("../../core/permission-rule-model.js").parseAllowRuleText} and its matcher), where
83
+ * ({@link import("../../core/permission-rule-model.js").parseRuleText} and its matcher), where
84
84
  * both sides of the comparison are literal text a person read on an approval card. The refusal this
85
85
  * option lifts exists for the argv[0]-NAME filters (the read-only allowlist, the coarse
86
86
  * command-name policy, the skill specifier): those compare a bare token against a name set, and a
@@ -62,7 +62,7 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
62
62
  * the read-only allowlist face (`core.bash_readonly@1`). Consumed by prepare-task's
63
63
  * fullShellReachable roster assertion (its first load-bearing consumer).
64
64
  */
65
- export declare const FULL_SHELL_CONTRACT_ID = "core.bash@1";
65
+ export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
66
66
  /** The CC-verbatim exit-1 interpretation for `command`, or undefined when exit 1 means a real error.
67
67
  * Conservative parse: last `;`/`&&`/`||`/newline statement → last `|` pipeline segment → leading
68
68
  * command name (env-assignments skipped, path prefix stripped); `git grep`/`git diff` special-cased
@@ -2,7 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { defaultTaskRegistry } from "../../core/task-registry.js";
4
4
  import { accountDroppedBytes, spoolDropNote } from "../../core/task-registry-shared.js";
5
- import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
5
+ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
6
6
  import { hasBackgroundShell } from "../../core/background-shell.js";
7
7
  import { delimitUntrusted } from "../../core/untrusted-text.js";
8
8
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
@@ -12,6 +12,7 @@ import { ghRateLimitHint } from "./gh-rate-limit.js";
12
12
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
13
13
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
14
14
  import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
15
+ import { toolFace } from "../../core/tool-catalog-entries.js";
15
16
  const RECURSIVE_CAUSE_MAX_PATHS = 3;
16
17
  const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
17
18
  function operandFamily(paths) {
@@ -58,7 +59,7 @@ export function bashReversibilityProbe(allow, boundary) {
58
59
  : { reversible: false, ...outOfRootGate() };
59
60
  };
60
61
  }
61
- export const FULL_SHELL_CONTRACT_ID = "core.bash@1";
62
+ export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
62
63
  const EXIT1_INTERPRETATION = {
63
64
  grep: "No matches found",
64
65
  rg: "No matches found",
@@ -537,7 +538,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
537
538
  const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
538
539
  return defineTool({
539
540
  name: "Bash",
540
- contract: { contractId: FULL_SHELL_CONTRACT_ID, implementationRevision: "1" },
541
+ ...toolFace("bash"),
541
542
  description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained, bgSessionScoped, bgEnvIsolatedOwned),
542
543
  parameters: Type.Object({
543
544
  command: Type.String({ description: "The command to execute" }),
@@ -559,8 +560,6 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
559
560
  description: "Set to true to run this command in the background.",
560
561
  })),
561
562
  }),
562
- effect: "write",
563
- contentOrigin: "execution",
564
563
  isConcurrencySafe: (args) => {
565
564
  const cmd = args?.command;
566
565
  if (typeof cmd !== "string")
@@ -991,7 +990,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
991
990
  const canonicalBoundary = createCanonicalReadBoundary(env, readRoots);
992
991
  return defineTool({
993
992
  name: "Bash",
994
- contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
993
+ ...toolFace("bash-readonly"),
995
994
  description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
996
995
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
997
996
  "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
@@ -1002,7 +1001,6 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1002
1001
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
1003
1002
  timeout: Type.Optional(Type.Number({ description: bashTimeoutParamDescription(timeoutCaps, true) })),
1004
1003
  }),
1005
- effect: "read",
1006
1004
  execute: async (args, ctx) => {
1007
1005
  const { command, timeout } = args;
1008
1006
  const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
@@ -1035,10 +1033,9 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1035
1033
  export function createEnvTaskOutputTool(env) {
1036
1034
  return defineTool({
1037
1035
  name: TASK_OUTPUT_TOOL_NAME,
1038
- contract: TASK_OUTPUT_CONTRACT,
1036
+ ...toolFace("task-output-hands"),
1039
1037
  description: composeTaskOutputDescription(ENV_DIRECT_TASK_TOOL_CAPS),
1040
1038
  parameters: composeTaskOutputParams(ENV_DIRECT_TASK_TOOL_CAPS),
1041
- effect: "read",
1042
1039
  execute: async (args) => {
1043
1040
  const { task_id, filter } = args;
1044
1041
  const id = resolveTaskIdArg(task_id);
@@ -1073,10 +1070,9 @@ export function createEnvTaskOutputTool(env) {
1073
1070
  export function createEnvTaskStopTool(env, registry = defaultTaskRegistry) {
1074
1071
  return defineTool({
1075
1072
  name: TASK_STOP_TOOL_NAME,
1076
- contract: TASK_STOP_CONTRACT,
1073
+ ...toolFace("task-stop-hands"),
1077
1074
  description: composeTaskStopDescription(ENV_DIRECT_TASK_TOOL_CAPS),
1078
1075
  parameters: TASK_STOP_PARAMS,
1079
- effect: "write",
1080
1076
  execute: async (args) => {
1081
1077
  const { task_id, shell_id } = args;
1082
1078
  const id = resolveTaskIdArg(task_id, shell_id);
@@ -29,7 +29,7 @@ type ReadPdfReturn = string | {
29
29
  * downsampler seam then applies the same pipeline as the Read image branch). poppler absent → HONEST
30
30
  * degradation with the working alternative, never a fake success.
31
31
  * Wire discipline: the base64 payload rides `details.file` only while ≤48K chars (same OMIT rule as the
32
- * image variant — a bigger card would be dropped whole by the structuredFrom 4×cap and blind the shell).
32
+ * image variant — a bigger card would be dropped whole by the card projector's 4×cap and blind the shell).
33
33
  * structured-card audit item B (2026-08-05): that budget management assumed `"document"` was on `CC_DETAIL_TYPES`, which
34
34
  * it was not — every `document` card, sized or not, was dropped at the gate, making the 48K rule dead
35
35
  * code and the pages/fallback-level disclosure invisible to a host. The word is registered now.
@@ -9,6 +9,7 @@ import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_
9
9
  import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
10
10
  import { openSystemReminder } from "../../core/reminder-mint.js";
11
11
  import { discloseReminderShaped } from "../../core/reminder-disclosure.js";
12
+ import { toolFace } from "../../core/tool-catalog-entries.js";
12
13
  export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
13
14
  const cyberReminder = readCyberReminder === false ? "" : readCyberReminderText(reminderMark);
14
15
  const reminderDisclosureWindows = new Map();
@@ -23,7 +24,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
23
24
  : "- `file_path` may be relative (resolved against the tracked working directory) or absolute (within the configured roots).\n";
24
25
  return defineTool({
25
26
  name: "Read",
26
- contract: { contractId: "core.read@1", implementationRevision: "1" },
27
+ ...toolFace("read"),
27
28
  description: "Reads a file from the local filesystem.\n" +
28
29
  "\n" +
29
30
  pathBoundLine +
@@ -55,7 +56,6 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
55
56
  limit: Type.Optional(Type.Integer({ minimum: 1, description: "The number of lines to read. Only provide if the file is too large to read at once. (Default: the whole file, bounded by the output-token cap.)" })),
56
57
  pages: Type.Optional(Type.String({ description: `Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.` })),
57
58
  }),
58
- effect: "read",
59
59
  execute: async (args, ctx) => {
60
60
  const { offset, limit, pages } = args;
61
61
  const path = fileArgPath(args);
@@ -1,5 +1,4 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { ToolEffect } from "../../core/types.js";
3
2
  import type { ReadDenyMatcher } from "./read-deny.js";
4
3
  import type { ReadFace } from "./read-face.js";
5
4
  /**
@@ -13,9 +12,3 @@ import type { ReadFace } from "./read-face.js";
13
12
  export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
14
13
  export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
15
14
  export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
16
- /** Static side-effect class of every hand tool, by name (design/44 §3). Used by prepare-task to (a) feed
17
- * wake/resume reconciliation and (b) drive the verifier read-only boundary. Every mutating hand tool is
18
- * `write` (RB-264 ⑥W1 folded `Write` back in — see below); `bash` is `write` (a command can do anything);
19
- * `bash_readonly` is `read` (so it survives the verifier boundary — but still goes through the policy
20
- * gate, council #7: effect:read is a redo-safety class, never a policy circumvention). */
21
- export declare const HAND_TOOL_EFFECTS: Readonly<Record<string, ToolEffect>>;
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { resolveKey, violationText, violationDetails } from "./safety.js";
4
4
  import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
5
+ import { toolFace } from "../../core/tool-catalog-entries.js";
5
6
  export function grepDetailFields(text, mode, offset, structuredRows) {
6
7
  const structuredIdentityComplete = structuredRows !== undefined && structuredRows.every((r) => r.path !== undefined || r.text === "--");
7
8
  const structuredPaths = structuredIdentityComplete && structuredRows !== undefined ? [...new Set(structuredRows.filter((r) => r.path !== undefined).map((r) => r.path))] : undefined;
@@ -74,7 +75,7 @@ export function grepDetailFields(text, mode, offset, structuredRows) {
74
75
  export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
75
76
  return defineTool({
76
77
  name: "Grep",
77
- contract: { contractId: "core.grep@1", implementationRevision: "1" },
78
+ ...toolFace("grep"),
78
79
  description: "Content search built on ripgrep. Prefer this over `grep`/`rg` via Bash — results integrate with the permission UI and file links.\n" +
79
80
  "\n" +
80
81
  '- Full regex syntax (e.g. "log.*Error", "function\\s+\\w+"). Ripgrep, not grep — escape literal braces (`interface\\{\\}`).\n' +
@@ -107,7 +108,6 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
107
108
  })),
108
109
  offset: Type.Optional(Type.Number({ description: "Skip the first N output lines (pagination partner of head_limit)." })),
109
110
  }),
110
- effect: "read",
111
111
  execute: async (args, ctx) => {
112
112
  const retired = args;
113
113
  for (const [name, repair] of [
@@ -175,7 +175,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
175
175
  export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
176
176
  return defineTool({
177
177
  name: "Glob",
178
- contract: { contractId: "core.glob@1", implementationRevision: "1" },
178
+ ...toolFace("glob"),
179
179
  description: 'Fast file pattern matching. Supports glob patterns like "**/*.js" or "src/**/*.ts". Returns matching file paths sorted by modification time.\n' +
180
180
  "\n" +
181
181
  "- On environments that do not report modification times, results fall back to alphabetical order\n" +
@@ -194,7 +194,6 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
194
194
  })),
195
195
  max_results: Type.Optional(Type.Number({ description: "Cap results (default 500)." })),
196
196
  }),
197
- effect: "read",
198
197
  execute: async (args, ctx) => {
199
198
  let { pattern, path } = args;
200
199
  const { max_results } = args;
@@ -222,15 +221,3 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
222
221
  },
223
222
  });
224
223
  }
225
- export const HAND_TOOL_EFFECTS = {
226
- Read: "read",
227
- Edit: "write",
228
- Write: "write",
229
- NotebookEdit: "write",
230
- Grep: "read",
231
- Glob: "read",
232
- RepoMap: "read",
233
- Bash: "write",
234
- TaskOutput: "read",
235
- TaskStop: "write",
236
- };
@@ -4,6 +4,7 @@ import { defineTool, errorResult } from "../../core/tools.js";
4
4
  import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
+ import { toolFace } from "../../core/tool-catalog-entries.js";
7
8
  async function envFinalWrite(env, key, content, signal, opts) {
8
9
  const fireEdited = () => {
9
10
  if (opts?.edited?.hook === undefined)
@@ -88,7 +89,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
88
89
  export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
89
90
  return defineTool({
90
91
  name: "Edit",
91
- contract: { contractId: "core.edit@1", implementationRevision: "1" },
92
+ ...toolFace("edit"),
92
93
  description: "Performs exact string replacement in a file.\n" +
93
94
  "\n" +
94
95
  "- You must Read the file in this conversation before editing, or the call will fail.\n" +
@@ -101,7 +102,6 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
101
102
  new_string: Type.Optional(Type.String({ description: "The text to replace it with (must be different from old_string)" })),
102
103
  replace_all: Type.Optional(Type.Boolean({ description: "Replace all occurrences of old_string (default false)" })),
103
104
  }),
104
- effect: "write",
105
105
  validateInput: async (args, ctx) => {
106
106
  const a = args;
107
107
  if (Array.isArray(a.edits) && a.edits.length > 0)
@@ -327,7 +327,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
327
327
  export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
328
328
  return defineTool({
329
329
  name: "Write",
330
- contract: { contractId: "core.write@1", implementationRevision: "1" },
330
+ ...toolFace("write"),
331
331
  description: "Writes a file to the local filesystem, overwriting if one exists.\n" +
332
332
  "\n" +
333
333
  "When to use: creating a new file, or fully replacing one you've already Read. Overwriting an existing " +
@@ -337,7 +337,6 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
337
337
  ...FILE_PATH_PARAMS,
338
338
  content: Type.String({ description: "The content to write to the file" }),
339
339
  }),
340
- effect: "write",
341
340
  validateInput: async (args, ctx) => {
342
341
  const path = fileArgPath(args);
343
342
  if (path === undefined)
@@ -427,7 +426,7 @@ const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is requ
427
426
  export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
428
427
  return defineTool({
429
428
  name: "NotebookEdit",
430
- contract: { contractId: "core.notebook_edit@1", implementationRevision: "1" },
429
+ ...toolFace("notebook-edit"),
431
430
  description: "Replaces, inserts, or deletes a single cell in a Jupyter notebook (.ipynb file).\n" +
432
431
  "\n" +
433
432
  "Usage:\n" +
@@ -449,7 +448,6 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
449
448
  description: "The type of edit to make (replace, insert, delete). Defaults to replace.",
450
449
  })),
451
450
  }),
452
- effect: "write",
453
451
  validateInput: async (args, ctx) => {
454
452
  const a = args;
455
453
  const mode = a.edit_mode ?? "replace";
@@ -92,5 +92,5 @@ export interface NotebookRender {
92
92
  export declare function renderNotebookCells(cells: readonly NotebookCell[], imageBase64Budget: number): NotebookRender;
93
93
  /** Wire-card projection of a cell: base64 replaced by a length marker so the `details` card never
94
94
  * carries raw image bytes (they ride the model-facing content blocks; an oversized card would be
95
- * omitted whole by the structuredFrom 4×cap and blind the shell). */
95
+ * omitted whole by the card projector's 4×cap and blind the shell). */
96
96
  export declare function stripNotebookImageData(cell: NotebookCell): NotebookCell;
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { resolveKey, violationText, violationDetails } from "./safety.js";
4
4
  import { buildIgnore, denyWithheldNote, walk, walkIsPartial } from "./search.js";
5
+ import { toolFace } from "../../core/tool-catalog-entries.js";
5
6
  const DEFAULT_MAX_CHARS = 16_000;
6
7
  const DEFAULT_MAX_FILES = 400;
7
8
  const FILE_SCAN_MAX_BYTES = 256 * 1024;
@@ -82,7 +83,7 @@ function rankEntries(a, b) {
82
83
  export function createRepoMapTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
83
84
  return defineTool({
84
85
  name: "RepoMap",
85
- contract: { contractId: "core.repo_map@1", implementationRevision: "1" },
86
+ ...toolFace("repo-map"),
86
87
  description: "Get a compact map of the repository: each source file with its top-level symbols (functions, " +
87
88
  "classes, types). Use this FIRST to orient in an unfamiliar codebase before grepping — it shows " +
88
89
  "what exists and where, so you can jump to the relevant files. Token-budgeted and honest about " +
@@ -92,7 +93,6 @@ export function createRepoMapTool(env, rootCanonical, additionalRoots, readDeny,
92
93
  max_chars: Type.Optional(Type.Number({ description: `Token budget for the rendered map in chars (default ${DEFAULT_MAX_CHARS}; a very small value yields only a truncation note).` })),
93
94
  max_files: Type.Optional(Type.Number({ description: `Max files to scan (default ${DEFAULT_MAX_FILES}).` })),
94
95
  }),
95
- effect: "read",
96
96
  execute: async (args, ctx) => {
97
97
  const a = args;
98
98
  const signal = ctx.signal;
@@ -1,4 +1,5 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
+ import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-registry.js";
2
3
  /**
3
4
  * batch-B (CC 2.1.187 parity): Read/Edit/Write take the file path as `file_path`. Read the target the same way
4
5
  * EVERYWHERE the path is consumed from a tool-call's args — not just the tool body but ALSO every gate that
@@ -28,7 +29,7 @@ export declare function fileArgPath(args: unknown): string | undefined;
28
29
  * this module stays dependency-free of the alias table — "NotebookEdit" is the canonical CC name and
29
30
  * old→new normalization is identity for it).
30
31
  */
31
- export declare function writeTargetPath(canonicalName: string, args: unknown): string | undefined;
32
+ export declare function writeTargetPath(call: ToolCallLike, pt?: ReturnType<typeof protectivePathTargetOf>): string | undefined;
32
33
  /**
33
34
  * The "hand" file tools (design/44) — safety invariants ported from CC's `FileEditTool` (design/43 Rule
34
35
  * 7): without these a weak model blind-edits / overwrites files (P0 safety, not polish). This module is
@@ -320,7 +321,7 @@ export declare function violationText(toolName: string, v: FsViolation): string;
320
321
  * every renderer of a {@link resolveKey} violation — `undefined` keeps the pre-existing detail-less
321
322
  * shape for all other violation codes, so nothing else changes byte-wise.
322
323
  *
323
- * RB-397-c (回溯复查批): `type` is the WIRE discriminator — `structuredFrom`'s `CC_DETAIL_TYPES` gate
324
+ * RB-397-c (回溯复查批): `type` is the WIRE discriminator — the leg's card projector (`createStructuredProjector`, design/388 L4)
324
325
  * (tool-output-projection.ts) keys on `details.type`, so without it the card never reached
325
326
  * `tool_end.structured` and the "count from fields" promise above held for in-process readers only.
326
327
  * `code` stays as-is for the existing RB-371 consumers — the new key is strictly additive.
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
3
+ import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
3
4
  export function fileArgPath(args) {
4
5
  const a = args;
5
6
  const fp = a?.file_path;
@@ -8,12 +9,12 @@ export function fileArgPath(args) {
8
9
  const p = a?.path;
9
10
  return typeof p === "string" && p.length > 0 ? p : undefined;
10
11
  }
11
- export function writeTargetPath(canonicalName, args) {
12
- const np = args?.notebook_path;
12
+ export function writeTargetPath(call, pt = protectivePathTargetOf(call)) {
13
+ if (pt !== undefined)
14
+ return pathTargetValue(call.args, pt);
15
+ const np = call.args?.notebook_path;
13
16
  const notebook = typeof np === "string" && np.length > 0 ? np : undefined;
14
- if (canonicalName === "NotebookEdit")
15
- return notebook ?? fileArgPath(args);
16
- return fileArgPath(args) ?? notebook;
17
+ return fileArgPath(call.args) ?? notebook;
17
18
  }
18
19
  export function sha256(content) {
19
20
  return createHash("sha256").update(content, "utf8").digest("hex");
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasBackgroundShell } from "../core/background-shell.js";
4
4
  import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
5
+ import { toolFace } from "../core/tool-catalog-entries.js";
5
6
  const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
6
7
  "\n" +
7
8
  "- Each line the command writes to stdout becomes a notification event delivered to you between turns; " +
@@ -42,7 +43,7 @@ function envBackgroundWallSec(caps, requestedSec) {
42
43
  export function createMonitorTool(env, opts) {
43
44
  return defineTool({
44
45
  name: "Monitor",
45
- contract: { contractId: "core.monitor@1", implementationRevision: "1" },
46
+ ...toolFace("monitor"),
46
47
  description: MONITOR_DESCRIPTION,
47
48
  parameters: Type.Object({
48
49
  command: Type.String({
@@ -58,7 +59,6 @@ export function createMonitorTool(env, opts) {
58
59
  description: "Set to true for a session-resident monitor with no watch timeout of its own (default false). It keeps watching until the execution environment's background time budget expires, until you stop it with TaskStop, or until the session ends.",
59
60
  })),
60
61
  }),
61
- effect: "write",
62
62
  execute: async (args, ctx) => {
63
63
  const { command, description, timeout_ms, persistent } = args;
64
64
  if (!hasBackgroundShell(env)) {
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasScheduler, isValidCronExpr, } from "../core/scheduler.js";
4
+ import { toolFace } from "../core/tool-catalog-entries.js";
4
5
  export const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
5
6
  export const AUTONOMOUS_LOOP_SENTINEL = "<<autonomous-loop>>";
6
7
  export const AUTONOMOUS_LOOP_DYNAMIC_SENTINEL = "<<autonomous-loop-dynamic>>";
@@ -199,7 +200,7 @@ Every user who asks for "9am" gets \`0 9\`, and every user who asks for "hourly"
199
200
  Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.`;
200
201
  const cronCreate = defineTool({
201
202
  name: "CronCreate",
202
- contract: { contractId: "core.cron_create@1", implementationRevision: "1" },
203
+ ...toolFace("cron-create"),
203
204
  description: cronCreateDescription,
204
205
  parameters: Type.Object({
205
206
  prompt: Type.String({ description: "Self-contained instructions for the future task (it runs unattended)." }),
@@ -222,7 +223,6 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
222
223
  })),
223
224
  label: Type.Optional(Type.String({ description: "Short label (also a dedup key with the schedule)." })),
224
225
  }),
225
- effect: "write",
226
226
  execute: async (args) => serializedCronCreateOp(async () => {
227
227
  const a = args;
228
228
  if (a.schedule !== undefined && a.cron !== undefined) {
@@ -305,10 +305,9 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
305
305
  };
306
306
  const cronCancel = defineTool({
307
307
  name: "CronDelete",
308
- contract: { contractId: "core.cron_delete@1", implementationRevision: "1" },
308
+ ...toolFace("cron-delete"),
309
309
  description: "Cancel a cron job previously scheduled with CronCreate. Idempotent; ids also appear in CronList.",
310
310
  parameters: Type.Object({ id: Type.String({ description: "Job ID returned by CronCreate." }) }),
311
- effect: "idempotent",
312
311
  execute: async (args) => {
313
312
  const { id } = args;
314
313
  const taskId = id;
@@ -329,10 +328,9 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
329
328
  });
330
329
  const cronList = defineTool({
331
330
  name: "CronList",
332
- contract: { contractId: "core.cron_list@1", implementationRevision: "1" },
331
+ ...toolFace("cron-list"),
333
332
  description: "List all cron jobs scheduled via CronCreate (id, when, label).",
334
333
  parameters: Type.Object({}),
335
- effect: "read",
336
334
  execute: async () => {
337
335
  const rows = [];
338
336
  const seen = new Set();
@@ -409,7 +407,7 @@ MOUNT NOTE: this host has no resident wakeup leg, so ${SCHEDULE_WAKEUP_TOOL_NAME
409
407
  MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup scheduled here is PERSISTENT — contrary to the paragraph above, it can outlive this session instead of ending with it. End the loop explicitly with \`stop: true\` when the work is done; do not rely on the session ending to cancel it.`;
410
408
  const scheduleWakeup = defineTool({
411
409
  name: SCHEDULE_WAKEUP_TOOL_NAME,
412
- contract: { contractId: "core.schedule_wakeup@1", implementationRevision: "1" },
410
+ ...toolFace("schedule-wakeup"),
413
411
  description: `${SCHEDULE_WAKEUP_PROMPT}${wakeupMountNote}`,
414
412
  parameters: Type.Object({
415
413
  delaySeconds: Type.Optional(Type.Number({
@@ -428,7 +426,6 @@ MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup
428
426
  description: "true = nothing changed (you checked and there is nothing to report). false = something happened worth keeping (edited a file, posted a message, advanced state, surfaced a finding). Consecutive noop:true ticks are collapsed in the user's terminal view and tracked as a streak. Required unless `stop` is true.",
429
427
  })),
430
428
  }),
431
- effect: "write",
432
429
  execute: async (args) => serializedWakeupOp(async () => {
433
430
  const a = args;
434
431
  if (a.stop === true) {