@sema-agent/core 7.8.0 → 7.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/agents/subagent.d.ts +1 -1
  3. package/dist/core/ask-origin.d.ts +16 -1
  4. package/dist/core/ask-origin.js +5 -1
  5. package/dist/core/checkpoint-store.d.ts +36 -35
  6. package/dist/core/effective-path-target.d.ts +43 -0
  7. package/dist/core/effective-path-target.js +56 -0
  8. package/dist/core/engine-notice.d.ts +8 -0
  9. package/dist/core/fs-write-gate-policy.js +2 -1
  10. package/dist/core/gate-lanes.d.ts +1 -0
  11. package/dist/core/gate-lanes.js +60 -28
  12. package/dist/core/gate-outcome.d.ts +8 -4
  13. package/dist/core/gate-outcome.js +2 -1
  14. package/dist/core/governance-codes.d.ts +1 -1
  15. package/dist/core/governance-codes.js +2 -0
  16. package/dist/core/hooks.d.ts +66 -32
  17. package/dist/core/mcp-injection-drop.d.ts +74 -0
  18. package/dist/core/mcp-injection-drop.js +27 -0
  19. package/dist/core/permission-rule-consent.d.ts +29 -36
  20. package/dist/core/permission-rule-consent.js +110 -60
  21. package/dist/core/permission-rule-model.d.ts +311 -96
  22. package/dist/core/permission-rule-model.js +309 -50
  23. package/dist/core/permission-rule-org.d.ts +19 -12
  24. package/dist/core/permission-rule-org.js +13 -16
  25. package/dist/core/permission-rule-provider.d.ts +10 -6
  26. package/dist/core/permission-rule-provider.js +12 -8
  27. package/dist/core/permission-rule-session.d.ts +7 -6
  28. package/dist/core/permission-rule-session.js +29 -10
  29. package/dist/core/permission-rule-store.d.ts +40 -18
  30. package/dist/core/permission-rule-store.js +68 -42
  31. package/dist/core/permission-rule-sync.d.ts +9 -3
  32. package/dist/core/permission-rule-sync.js +29 -25
  33. package/dist/core/permission-rule-syntax.d.ts +30 -0
  34. package/dist/core/permission-rule-syntax.js +44 -0
  35. package/dist/core/permission-rules.d.ts +56 -33
  36. package/dist/core/permission-rules.js +88 -55
  37. package/dist/core/persisted-rule-arms.d.ts +61 -0
  38. package/dist/core/persisted-rule-arms.js +50 -0
  39. package/dist/core/runner/active-skill-scope.js +2 -1
  40. package/dist/core/runner/contracts.d.ts +21 -2
  41. package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
  42. package/dist/core/runner/permission-rule-lanes.js +25 -21
  43. package/dist/core/runner/prepare-gate-stations.js +1 -1
  44. package/dist/core/runner/prepare-safety-scan.js +8 -2
  45. package/dist/core/runner/prepare-task.js +1 -1
  46. package/dist/core/runner/resume-admission.d.ts +53 -0
  47. package/dist/core/runner/resume-admission.js +83 -0
  48. package/dist/core/runner/resume-apply.d.ts +50 -0
  49. package/dist/core/runner/resume-apply.js +184 -0
  50. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  51. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  52. package/dist/core/runner/resume-claim.d.ts +32 -0
  53. package/dist/core/runner/resume-claim.js +27 -0
  54. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  55. package/dist/core/runner/resume-internals-and-config.js +50 -0
  56. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  57. package/dist/core/runner/resume-policy-outcome.js +127 -0
  58. package/dist/core/runner/resume-preflight.d.ts +40 -0
  59. package/dist/core/runner/resume-preflight.js +122 -0
  60. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  61. package/dist/core/runner/resume-review-outcome.js +88 -0
  62. package/dist/core/runner/run-harness-handlers.js +1 -1
  63. package/dist/core/runner/runtask.d.ts +20 -0
  64. package/dist/core/runner/runtask.js +85 -734
  65. package/dist/core/runner/session-rule-policy.js +2 -1
  66. package/dist/core/runner/tool-end-body.d.ts +12 -5
  67. package/dist/core/runner/tool-end-body.js +5 -5
  68. package/dist/core/runner/tool-face-overlay.js +22 -3
  69. package/dist/core/runner/tool-output-projection.d.ts +9 -6
  70. package/dist/core/runner/tool-output-projection.js +1 -18
  71. package/dist/core/runner/turn-attachments.d.ts +2 -2
  72. package/dist/core/sensitive-path-policy.js +5 -3
  73. package/dist/core/shell-lexer.d.ts +47 -0
  74. package/dist/core/shell-lexer.js +478 -0
  75. package/dist/core/shell-scan.d.ts +60 -0
  76. package/dist/core/shell-scan.js +183 -0
  77. package/dist/core/shell-wrapper-table.d.ts +297 -0
  78. package/dist/core/shell-wrapper-table.js +58 -0
  79. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  80. package/dist/core/tool-catalog-entries.js +6 -6
  81. package/dist/core/tool-face.d.ts +80 -4
  82. package/dist/core/tool-face.js +10 -0
  83. package/dist/core/tool-policy.d.ts +1 -6
  84. package/dist/core/tool-registry.d.ts +8 -11
  85. package/dist/core/tool-registry.js +5 -2
  86. package/dist/core/tool-roster.d.ts +20 -2
  87. package/dist/core/tool-roster.js +36 -8
  88. package/dist/index.d.ts +7 -4
  89. package/dist/index.js +6 -3
  90. package/dist/stores/file/checkpoint-store.js +1 -1
  91. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  92. package/dist/stores/file/permission-rule-store.js +64 -16
  93. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  94. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  95. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  96. package/dist/tools/fs/fs-search-tools.js +17 -9
  97. package/dist/tools/fs/index.js +1 -1
  98. package/dist/tools/fs/notebook.d.ts +1 -1
  99. package/dist/tools/fs/safety.d.ts +1 -1
  100. package/dist/tools/fs/search.d.ts +0 -8
  101. package/dist/tools/fs/search.js +0 -23
  102. package/package.json +1 -1
  103. package/test/export-surface.snapshot.json +99 -11
@@ -2,17 +2,21 @@
2
2
  * design/179 §5/§8 / design/389 — the file backend of the permission-rule store's DURABLE partition (one
3
3
  * bucket file per owner), plus the file home of the ORG partition's durable state (`org.json`).
4
4
  *
5
- * ## Schema 3 and the one-time migration
5
+ * ## Schema 4 and the one-time migration
6
6
  *
7
7
  * Pre-389 files (schema 1/2) kept the org last-known-good snapshot as an `org` block INSIDE each
8
- * principal's bucket file — deployment-wide policy stored per person. Schema 3 drops that block; the org
9
- * state lives in one `org.json` beside the buckets. `FileDurableRulePartitionProvider` migrates every
10
- * legacy bucket file ONCE at construction (under the writer lock: the migration is a write, and a live
11
- * older writer over the same directory is refused rather than raced), folding each file's `org` block
12
- * into `org.json` by the anti-rollback rule (the highest high-water wins; an equal revision with
13
- * different content keeps what is already installed and discloses). After that the reader accepts
14
- * schema 3 ONLY a 1/2/4 file is refused whole, loudly (zero rules + disclosure). There is no second
15
- * read path for the old shapes: the migrator is the only code that reads them, and it runs once.
8
+ * principal's bucket file — deployment-wide policy stored per person. Schema 3 dropped that block; the org
9
+ * state lives in one `org.json` beside the buckets. Schema 4 adds the `behavior` member to every rule row,
10
+ * tombstone and quarantine row (the rule identity is the (behavior, text, scope) triple): a schema-3 row has
11
+ * none, and every row a schema-3 engine wrote was an allow, so the migration stamps `"allow"` on each — the
12
+ * ONE reading of the pre-field row (`readRowBehavior`), applied once, here, never at a read face.
13
+ * `FileDurableRulePartitionProvider` migrates every legacy bucket file (1, 2 or 3) ONCE at construction
14
+ * (under the writer lock: the migration is a write, and a live older writer over the same directory is
15
+ * refused rather than raced), folding a 1/2 file's `org` block into `org.json` by the anti-rollback rule
16
+ * (the highest high-water wins; an equal revision with different content keeps what is already installed
17
+ * and discloses). After that the reader accepts schema 4 ONLY — a 1/2/3/5 file is refused whole, loudly
18
+ * (zero rules + disclosure). There is no second read path for the old shapes: the migrator is the only
19
+ * code that reads them, and it runs once.
16
20
  *
17
21
  * ## Why this store builds its own symlink refusal
18
22
  *
@@ -55,23 +59,23 @@
55
59
  * and is not: it detects corruption, and it does not detect an editor, because an editor recomputes it.
56
60
  * For this backend the trust is host = user, exactly as for the settings file it mirrors.
57
61
  */
58
- import type { PersistedAllowRule, RuleTombstone } from "../../core/permission-rule-model.js";
59
- import type { DurableRulePartition, DurableRulePartitionProvider, PermissionRuleWriter, QuarantinedRuleAdd, RuleOwner, RuleSyncFrontier, StoredAllowRules, WritableDurableRulePartition } from "../../core/permission-rule-store.js";
62
+ import type { PersistedRule, RuleTombstone } from "../../core/permission-rule-model.js";
63
+ import type { DurableRulePartition, DurableRulePartitionProvider, PermissionRuleWriter, QuarantinedRuleAdd, RuleOwner, RuleSyncFrontier, StoredRules, WritableDurableRulePartition } from "../../core/permission-rule-store.js";
60
64
  import { PERMISSION_RULE_WRITER } from "../../core/permission-rule-store.js";
61
65
  import type { OrgRuleStatePersistence, PersistedOrgRuleState } from "../../core/permission-rule-org.js";
62
66
  import type { StoreDurability, StoreFidelity } from "../../core/checkpoint-store.js";
63
67
  /**
64
68
  * The on-disk shape of one bucket. `schemaVersion` is the only field a future reader may rely on before
65
- * validating. Schema 3 (design/389): the five core fields plus the OPTIONAL `sync` block (observation
66
- * vector + round timestamp) and `quarantined` area (rows moved out of the live view, bytes preserved).
67
- * The org state is NOT here — see {@link OrgStateFile}.
69
+ * validating. Schema 4: the five core fields plus the OPTIONAL `sync` block (observation vector + round
70
+ * timestamp) and `quarantined` area (rows moved out of the live view, bytes preserved); every rule,
71
+ * tombstone and quarantine row carries its `behavior`. The org state is NOT here — see {@link OrgStateFile}.
68
72
  */
69
73
  interface RuleFile {
70
- schemaVersion: 3;
74
+ schemaVersion: 4;
71
75
  actor: string;
72
76
  counter: number;
73
77
  rev: number;
74
- rules: PersistedAllowRule[];
78
+ rules: PersistedRule[];
75
79
  tombstones: RuleTombstone[];
76
80
  sync?: {
77
81
  observedVector?: RuleSyncFrontier;
@@ -88,10 +92,10 @@ interface OrgStateFile {
88
92
  }
89
93
  /** The org partition's durable home in a rule directory. */
90
94
  export declare const ORG_STATE_FILE = "org.json";
91
- /** Written once the directory is known to hold only schema-3 buckets. Its presence ends the legacy
92
- * scan for good: a schema 1/2 bucket that appears AFTERWARDS (an older engine writing after the
95
+ /** Written once the directory is known to hold only schema-4 buckets. Its presence ends the legacy
96
+ * scan for good: a schema 1/2/3 bucket that appears AFTERWARDS (an older engine writing after the
93
97
  * cutover) is refused by the reader on every later construction too, never re-admitted by a restart. */
94
- export declare const SCHEMA3_MARKER_FILE = ".rule-schema-3";
98
+ export declare const SCHEMA_MARKER_FILE = ".rule-schema-4";
95
99
  /** The anti-rollback contract's DESIGNED refusal (a lower high-water mark, or different content under
96
100
  * the same revision) — distinguished by class from every other install failure, because the two are
97
101
  * handled in opposite directions by the migration: a refusal means the block is superseded and may be
@@ -148,7 +152,7 @@ declare class FileDurableRulePartition implements WritableDurableRulePartition {
148
152
  */
149
153
  private screenDurableRows;
150
154
  private write;
151
- list(): Promise<StoredAllowRules>;
155
+ list(): Promise<StoredRules>;
152
156
  /** The quarantine area (design/182 §5.2/§8.3): introspection only — never part of `list()`. */
153
157
  quarantined(): Promise<QuarantinedRuleAdd[]>;
154
158
  /**
@@ -209,7 +213,8 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
209
213
  dataRoot?: string;
210
214
  });
211
215
  /**
212
- * Fold every legacy bucket file in `dir` into schema 3 and its `org` block into `org.json`. A no-op on a
216
+ * Fold every legacy bucket file in `dir` into schema 4 (stamping `behavior: "allow"` on every pre-field
217
+ * row — the one pre-field reading) and a 1/2 file's `org` block into `org.json`. A no-op on a
213
218
  * directory with no legacy file (zero writes, the lock untouched — the second construction over a
214
219
  * migrated directory is byte-silent). A legacy file whose checksum or shape does not hold is NOT
215
220
  * migrated: its bytes stay where they are for a person to look at, and the reader keeps refusing it
@@ -220,10 +225,10 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
220
225
  * shape — the old engine's buckets are refused whole until they are migrated.
221
226
  */
222
227
  private migrateLegacyBuckets;
223
- /** Publish the schema-3 completion marker (idempotent, atomic, no lock: its content never changes and
228
+ /** Publish the schema-4 completion marker (idempotent, atomic, no lock: its content never changes and
224
229
  * a concurrent writer of the same bytes is harmless). A failure to write it is disclosed, not thrown —
225
230
  * the next construction simply scans again, which is the safe direction. */
226
- private writeSchema3Marker;
231
+ private writeSchemaMarker;
227
232
  /** The org partition's durable home in this directory (design/182 §7.4 contract; `org.json`). Shares
228
233
  * this provider's writer lock — an install is a write. */
229
234
  orgStatePersistence(): OrgRuleStatePersistence;
@@ -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
@@ -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.
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { ReadDenyMatcher } from "./read-deny.js";
3
3
  import type { ReadFace } from "./read-face.js";
4
+ import type { CwdRef } from "./fs-shared.js";
4
5
  /**
5
6
  * The Grep card's details assembly, PURE over the engine text ({mode, offset} from the request) —
6
7
  * exported so the text→structured mapping is pinnable with synthetic texts (the byte-truncation and
@@ -10,5 +11,5 @@ import type { ReadFace } from "./read-face.js";
10
11
  * this seam the honesty boundary.
11
12
  */
12
13
  export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
13
- export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
14
- export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
14
+ export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace, cwdRef?: CwdRef): AgentTool;
15
+ export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace, cwdRef?: CwdRef): AgentTool;
@@ -1,8 +1,10 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { resolveKey, violationText, violationDetails } from "./safety.js";
4
- import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
4
+ import { runGrepDetailed, runGlobDetailed, invalidGlobTokens } from "./search.js";
5
5
  import { toolFace } from "../../core/tool-catalog-entries.js";
6
+ import { pathTargetBaseOf, splitAbsoluteGlobPattern } from "../../core/effective-path-target.js";
7
+ import { pathTargetValue } from "../../core/tool-registry.js";
6
8
  export function grepDetailFields(text, mode, offset, structuredRows) {
7
9
  const structuredIdentityComplete = structuredRows !== undefined && structuredRows.every((r) => r.path !== undefined || r.text === "--");
8
10
  const structuredPaths = structuredIdentityComplete && structuredRows !== undefined ? [...new Set(structuredRows.filter((r) => r.path !== undefined).map((r) => r.path))] : undefined;
@@ -72,7 +74,8 @@ export function grepDetailFields(text, mode, offset, structuredRows) {
72
74
  }
73
75
  return detailFields;
74
76
  }
75
- export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
77
+ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef) {
78
+ const declaredTarget = toolFace("grep").pathTarget;
76
79
  return defineTool({
77
80
  name: "Grep",
78
81
  ...toolFace("grep"),
@@ -136,9 +139,11 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
136
139
  `Close the bracket/brace, or drop \`glob\` and filter with \`path\`/\`type\` instead.`);
137
140
  }
138
141
  }
139
- let scoped = a.path;
140
- if (a.path !== undefined) {
141
- const r = await resolveKey(env, rootCanonical, a.path, ctx.signal, rootCanonical, additionalRoots, undefined, readDeny, readFace);
142
+ const baseCwd = pathTargetBaseOf(declaredTarget, { root: rootCanonical, cwd: cwdRef?.current });
143
+ const scopeSpelling = declaredTarget === undefined ? undefined : pathTargetValue(a, declaredTarget);
144
+ let scoped = scopeSpelling;
145
+ if (scopeSpelling !== undefined) {
146
+ const r = await resolveKey(env, rootCanonical, scopeSpelling, ctx.signal, baseCwd, additionalRoots, undefined, readDeny, readFace);
142
147
  if (!r.ok)
143
148
  return errorResult(violationText("Grep", r.violation), violationDetails(r.violation));
144
149
  scoped = r.key;
@@ -146,7 +151,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
146
151
  if (!info.ok) {
147
152
  const ex = await env.exists(scoped, ctx.signal);
148
153
  if (!(ex.ok && !ex.value)) {
149
- return errorResult(`Error (Grep): cannot determine the type of "${a.path.slice(0, 300)}" (stat failed: ${info.error.message.slice(0, 200)}); refusing to search it — if it is a FIFO/socket/device, reading it would hang. Retry if this was transient.`);
154
+ return errorResult(`Error (Grep): cannot determine the type of "${scopeSpelling.slice(0, 300)}" (stat failed: ${info.error.message.slice(0, 200)}); refusing to search it — if it is a FIFO/socket/device, reading it would hang. Retry if this was transient.`);
150
155
  }
151
156
  }
152
157
  }
@@ -172,7 +177,8 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
172
177
  },
173
178
  });
174
179
  }
175
- export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
180
+ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef) {
181
+ const declaredTarget = toolFace("glob").pathTarget;
176
182
  return defineTool({
177
183
  name: "Glob",
178
184
  ...toolFace("glob"),
@@ -195,7 +201,8 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
195
201
  max_results: Type.Optional(Type.Number({ description: "Cap results (default 500)." })),
196
202
  }),
197
203
  execute: async (args, ctx) => {
198
- let { pattern, path } = args;
204
+ let pattern = args.pattern;
205
+ let path = declaredTarget === undefined ? undefined : pathTargetValue(args, declaredTarget);
199
206
  const { max_results } = args;
200
207
  if (path === undefined) {
201
208
  const split = splitAbsoluteGlobPattern(pattern);
@@ -204,9 +211,10 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
204
211
  pattern = split.relativePattern;
205
212
  }
206
213
  }
214
+ const baseCwd = pathTargetBaseOf(declaredTarget, { root: rootCanonical, cwd: cwdRef?.current });
207
215
  let scoped = path;
208
216
  if (path !== undefined) {
209
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, rootCanonical, additionalRoots, undefined, readDeny, readFace);
217
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, baseCwd, additionalRoots, undefined, readDeny, readFace);
210
218
  if (!r.ok)
211
219
  return errorResult(violationText("Glob", r.violation), violationDetails(r.violation));
212
220
  scoped = r.key;
@@ -52,7 +52,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
52
52
  if (!readOnly) {
53
53
  tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited));
54
54
  }
55
- tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
55
+ tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
56
56
  if (includeShell) {
57
57
  tools.push(readOnly
58
58
  ? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
@@ -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;
@@ -321,7 +321,7 @@ export declare function violationText(toolName: string, v: FsViolation): string;
321
321
  * every renderer of a {@link resolveKey} violation — `undefined` keeps the pre-existing detail-less
322
322
  * shape for all other violation codes, so nothing else changes byte-wise.
323
323
  *
324
- * 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)
325
325
  * (tool-output-projection.ts) keys on `details.type`, so without it the card never reached
326
326
  * `tool_end.structured` and the "count from fields" promise above held for in-process readers only.
327
327
  * `code` stays as-is for the existing RB-371 consumers — the new key is strictly additive.
@@ -51,14 +51,6 @@ export interface GrepParams {
51
51
  * (`"*.ts,*.tsx"` → two globs), while a brace token (`*.{ts,tsx}`) is kept whole (its commas are
52
52
  * alternation syntax, not separators). CC-verbatim splitting rules. */
53
53
  export declare function splitGlobParam(glob: string): string[];
54
- /** 批④ #8 (CC 2.1.198 RWp, bundle :332987): decompose an ABSOLUTE glob pattern into a base directory +
55
- * relative pattern — cut at the last separator before the first glob metachar (`* ? [ {`); a pattern
56
- * with no metachar splits dirname/basename. Returns undefined for a relative pattern (caller keeps it
57
- * as-is) or when no usable base can be derived. */
58
- export declare function splitAbsoluteGlobPattern(pattern: string): {
59
- baseDir: string;
60
- relativePattern: string;
61
- } | undefined;
62
54
  /**
63
55
  * POSIX-safe single-quote so an arbitrary model-supplied string can't break out of the shell command that
64
56
  * runs ripgrep. Wraps in single quotes (inside which the shell expands nothing — no `$()`, backtick, `;`, or
@@ -71,29 +71,6 @@ export function splitGlobParam(glob) {
71
71
  }
72
72
  return out;
73
73
  }
74
- export function splitAbsoluteGlobPattern(pattern) {
75
- if (!isAbsolutePathForm(pattern))
76
- return undefined;
77
- const m = /[*?[{]/.exec(pattern);
78
- if (!m) {
79
- const i = Math.max(pattern.lastIndexOf("/"), pattern.lastIndexOf("\\"));
80
- if (i <= 0)
81
- return undefined;
82
- return { baseDir: pattern.slice(0, i), relativePattern: pattern.slice(i + 1) };
83
- }
84
- const prefix = pattern.slice(0, m.index);
85
- const cut = Math.max(prefix.lastIndexOf("/"), prefix.lastIndexOf("\\"));
86
- if (cut === -1)
87
- return undefined;
88
- let baseDir = pattern.slice(0, cut);
89
- if (baseDir === "" && cut === 0)
90
- baseDir = "/";
91
- if (/^[A-Za-z]:$/.test(baseDir))
92
- baseDir = baseDir + pattern[cut];
93
- if (!baseDir)
94
- return undefined;
95
- return { baseDir, relativePattern: pattern.slice(cut + 1) };
96
- }
97
74
  export function shellQuote(s) {
98
75
  return `'${s.replace(/'/g, `'\\''`)}'`;
99
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.8.0",
3
+ "version": "7.9.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",