@sema-agent/core 7.49.5 → 7.50.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 (200) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/subagent-steps.d.ts +6 -14
  4. package/dist/agents/subagent-steps.js +3 -17
  5. package/dist/brain/stream-shared.d.ts +1 -1
  6. package/dist/core/allow-entry.d.ts +1 -1
  7. package/dist/core/allow-entry.js +1 -1
  8. package/dist/core/ask-origin.d.ts +15 -11
  9. package/dist/core/ask-origin.js +6 -14
  10. package/dist/core/auto-mode-prompt.js +6 -2
  11. package/dist/core/checkpoint-store.d.ts +6 -4
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/context-edit.d.ts +2 -2
  14. package/dist/core/decision-receipt.d.ts +2 -2
  15. package/dist/core/decision-receipt.js +30 -11
  16. package/dist/core/delegation-frames.d.ts +66 -18
  17. package/dist/core/delegation-frames.js +22 -0
  18. package/dist/core/engine-notice.d.ts +7 -5
  19. package/dist/core/frame-settlement.d.ts +6 -1
  20. package/dist/core/gate-fold.js +34 -19
  21. package/dist/core/gate-lanes.js +15 -13
  22. package/dist/core/gate-outcome.d.ts +2 -2
  23. package/dist/core/governance-codes.js +2 -0
  24. package/dist/core/hooks.d.ts +28 -20
  25. package/dist/core/human-input-projection.js +2 -0
  26. package/dist/core/lsp-diagnostics.d.ts +3 -0
  27. package/dist/core/lsp-diagnostics.js +6 -0
  28. package/dist/core/lsp-session.d.ts +39 -17
  29. package/dist/core/lsp-session.js +118 -58
  30. package/dist/core/lsp.d.ts +83 -3
  31. package/dist/core/lsp.js +146 -16
  32. package/dist/core/mcp.js +2 -1
  33. package/dist/core/media-byte-cap.d.ts +14 -0
  34. package/dist/core/media-byte-cap.js +15 -3
  35. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  36. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  37. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  38. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  39. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  40. package/dist/core/memory-engine/consolidation.js +1 -0
  41. package/dist/core/memory-engine/distiller.d.ts +8 -2
  42. package/dist/core/memory-engine/distiller.js +8 -6
  43. package/dist/core/memory-engine/engine.d.ts +37 -4
  44. package/dist/core/memory-engine/engine.js +127 -38
  45. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  46. package/dist/core/memory-engine/file-backend.js +44 -16
  47. package/dist/core/memory-engine/index.d.ts +2 -2
  48. package/dist/core/memory-engine/index.js +1 -1
  49. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  50. package/dist/core/memory-engine/landing-admission.js +68 -0
  51. package/dist/core/memory-engine/types.d.ts +23 -0
  52. package/dist/core/mode-policy.d.ts +3 -2
  53. package/dist/core/mode-policy.js +2 -0
  54. package/dist/core/permission-rule-model.d.ts +19 -13
  55. package/dist/core/permission-rule-model.js +7 -4
  56. package/dist/core/permission-rule-provider.d.ts +11 -0
  57. package/dist/core/permission-rules.d.ts +1 -1
  58. package/dist/core/permission-rules.js +5 -2
  59. package/dist/core/persisted-rule-arms.d.ts +15 -7
  60. package/dist/core/persisted-rule-arms.js +13 -6
  61. package/dist/core/reminder-mint.d.ts +5 -0
  62. package/dist/core/reminder-mint.js +1 -0
  63. package/dist/core/retired-keys.d.ts +8 -0
  64. package/dist/core/retired-keys.js +4 -0
  65. package/dist/core/rule-lane-reads.d.ts +4 -4
  66. package/dist/core/rule-lane-reads.js +10 -1
  67. package/dist/core/runner/construction-doors.d.ts +10 -4
  68. package/dist/core/runner/construction-doors.js +25 -2
  69. package/dist/core/runner/contracts.d.ts +20 -3
  70. package/dist/core/runner/env-capability-census.js +1 -1
  71. package/dist/core/runner/gate-exit.js +2 -1
  72. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  73. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  74. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  75. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  76. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  77. package/dist/core/runner/permission-rule-lanes.js +13 -43
  78. package/dist/core/runner/person-path-rows.d.ts +244 -0
  79. package/dist/core/runner/person-path-rows.js +387 -0
  80. package/dist/core/runner/policy-throw.d.ts +34 -0
  81. package/dist/core/runner/policy-throw.js +25 -0
  82. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  83. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  84. package/dist/core/runner/prepare-artifact.js +2 -8
  85. package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
  86. package/dist/core/runner/prepare-ask-lane.js +14 -16
  87. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  88. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  89. package/dist/core/runner/prepare-context-lane.js +2 -2
  90. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  91. package/dist/core/runner/prepare-gate-stations.js +1 -1
  92. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  93. package/dist/core/runner/prepare-hands-readface.js +57 -43
  94. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  95. package/dist/core/runner/prepare-lsp.js +11 -11
  96. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  97. package/dist/core/runner/prepare-policy-chain.js +11 -10
  98. package/dist/core/runner/prepare-run-refs.js +1 -1
  99. package/dist/core/runner/prepare-task.js +24 -16
  100. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  101. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  102. package/dist/core/runner/read-root-grant.d.ts +1 -25
  103. package/dist/core/runner/read-root-grant.js +1 -17
  104. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  105. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  106. package/dist/core/runner/run-brain-sinks.js +2 -4
  107. package/dist/core/runner/run-harness-handlers.js +5 -4
  108. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  109. package/dist/core/runner/run-identity-wiring.js +10 -1
  110. package/dist/core/runner/run-leg.d.ts +1 -1
  111. package/dist/core/runner/run-leg.js +22 -9
  112. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  113. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  114. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  115. package/dist/core/runner/run-terminal-adoption.js +1 -1
  116. package/dist/core/runner/run-turn-boundary.js +2 -2
  117. package/dist/core/runner/runtask.js +8 -8
  118. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  119. package/dist/core/runner/session-file-state-replay.js +8 -0
  120. package/dist/core/runner/stream-steer-verb.js +3 -0
  121. package/dist/core/runner/tool-end-body.d.ts +5 -4
  122. package/dist/core/runner/tool-end-body.js +4 -4
  123. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  124. package/dist/core/runner/tool-output-projection.js +66 -47
  125. package/dist/core/runner-deps.d.ts +6 -3
  126. package/dist/core/shared-memory/types.d.ts +2 -2
  127. package/dist/core/shell-read-silence.d.ts +23 -0
  128. package/dist/core/shell-read-silence.js +18 -0
  129. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  130. package/dist/core/synthetic-read-verdict.js +89 -18
  131. package/dist/core/task-event.d.ts +113 -25
  132. package/dist/core/task-spec.d.ts +44 -0
  133. package/dist/core/task-tool-shape.d.ts +1 -1
  134. package/dist/core/tool-catalog-entries.js +16 -2
  135. package/dist/core/tool-policy.d.ts +25 -15
  136. package/dist/core/tool-policy.js +9 -13
  137. package/dist/core/trace.d.ts +3 -2
  138. package/dist/core/types.d.ts +3 -3
  139. package/dist/core/types.js +1 -1
  140. package/dist/core/untrusted-text.d.ts +2 -2
  141. package/dist/core/user-attachment.d.ts +164 -0
  142. package/dist/core/user-attachment.js +437 -0
  143. package/dist/engine/compaction/compaction.js +10 -3
  144. package/dist/engine/compaction/utils.js +6 -0
  145. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  146. package/dist/engine/execution-env/node-execution-env.js +125 -0
  147. package/dist/engine/harness/agent-harness.d.ts +12 -2
  148. package/dist/engine/harness/agent-harness.js +15 -3
  149. package/dist/engine/harness/types.d.ts +21 -0
  150. package/dist/engine/llm/content-block-render.d.ts +15 -0
  151. package/dist/engine/llm/content-block-render.js +10 -0
  152. package/dist/engine/llm/index.d.ts +1 -0
  153. package/dist/engine/llm/index.js +1 -0
  154. package/dist/engine/llm/types.d.ts +80 -4
  155. package/dist/engine/llm/types.js +43 -1
  156. package/dist/engine/session/import-validate.js +2 -0
  157. package/dist/index.d.ts +6 -4
  158. package/dist/index.js +2 -1
  159. package/dist/internal/harness-types.d.ts +1 -1
  160. package/dist/internal/llm.d.ts +2 -2
  161. package/dist/internal/llm.js +1 -1
  162. package/dist/orchestration/workflow.d.ts +4 -5
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompts/default.d.ts +1 -1
  165. package/dist/tools/artifact/artifact-tool.js +1 -1
  166. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  167. package/dist/tools/fs/dir-entries.d.ts +4 -10
  168. package/dist/tools/fs/dir-entries.js +4 -11
  169. package/dist/tools/fs/file-read-check.js +3 -3
  170. package/dist/tools/fs/fs-pdf.js +2 -4
  171. package/dist/tools/fs/fs-read.d.ts +47 -4
  172. package/dist/tools/fs/fs-read.js +63 -19
  173. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  174. package/dist/tools/fs/fs-search-tools.js +7 -2
  175. package/dist/tools/fs/fs-write.d.ts +1 -1
  176. package/dist/tools/fs/fs-write.js +7 -6
  177. package/dist/tools/fs/index.d.ts +7 -1
  178. package/dist/tools/fs/index.js +56 -34
  179. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  180. package/dist/tools/fs/read-deny-home.js +32 -22
  181. package/dist/tools/fs/read-deny.d.ts +14 -0
  182. package/dist/tools/fs/read-deny.js +8 -0
  183. package/dist/tools/fs/repo-map.d.ts +3 -2
  184. package/dist/tools/fs/repo-map.js +11 -3
  185. package/dist/tools/fs/safety.d.ts +41 -2
  186. package/dist/tools/fs/safety.js +12 -3
  187. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  188. package/dist/tools/fs/shell-read-check.js +44 -24
  189. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  190. package/dist/tools/fs/shell-read-face.d.ts +15 -0
  191. package/dist/tools/fs/shell-read-face.js +5 -0
  192. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  193. package/dist/tools/fs/shell-read-judge.js +1 -1
  194. package/dist/tools/loop-tick.d.ts +2 -2
  195. package/package.json +3 -1
  196. package/test/export-surface.snapshot.json +39 -1
  197. package/wire-changes/core@7.50.0.json +1699 -0
  198. package/wire-changes/core@7.50.1.json +65 -0
  199. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  200. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -11,6 +11,7 @@ import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "
11
11
  import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, anchorStoreRoot, effectiveControlRootOf, ensureDirExists, LEDGER_FILE, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, writeAllSync, STORE_JOURNAL_FILE, STORE_TXN_LOCK_DIR, TXN_LOCK_STALE_MS, acquireStoreTxnLock, assertStoreTxnLockOwnership, tryStoreTxnLock, } from "./layout.js";
12
12
  import { screenInboundEntries } from "./data-plane.js";
13
13
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
14
+ import { admittingLandings, cutPerLanding, LandingsNeedAdmission, requireLandingsAdmitted } from "./landing-admission.js";
14
15
  import { thrownText } from "../thrown-value.js";
15
16
  export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
16
17
  export function canonicalJsonStringify(value) {
@@ -526,6 +527,7 @@ export class FileMemoryEngineBackend {
526
527
  unboundRowsKnown = false;
527
528
  adoptionNoticeKeys = new Set();
528
529
  transfersAppendFault;
530
+ landingFault;
529
531
  recoveryCommitProbe;
530
532
  chainHealProbe;
531
533
  exportFaceProbe;
@@ -2111,17 +2113,20 @@ export class FileMemoryEngineBackend {
2111
2113
  return scored.slice(0, limit);
2112
2114
  }
2113
2115
  async applyPatches(patches, opts) {
2114
- const lock = await this.acquireTxnLock();
2115
- try {
2116
- return await this.applyPatchesLocked(patches, lock.token, opts?.anchoredRoot);
2117
- }
2118
- catch (err) {
2119
- throw withPermissionRemedy(err, this.directoryRoot);
2120
- }
2121
- finally {
2122
- lock.release();
2123
- }
2116
+ return admittingLandings(opts?.admit, async (admitted) => {
2117
+ const lock = await this.acquireTxnLock();
2118
+ try {
2119
+ return await this.applyPatchesLocked(patches, lock.token, opts?.anchoredRoot, admitted, opts?.confirm);
2120
+ }
2121
+ catch (err) {
2122
+ throw err instanceof LandingsNeedAdmission ? err : withPermissionRemedy(err, this.directoryRoot);
2123
+ }
2124
+ finally {
2125
+ lock.release();
2126
+ }
2127
+ });
2124
2128
  }
2129
+ admitsLandings = true;
2125
2130
  txnLockDir() {
2126
2131
  return join(this.controlPlaneRoot, STORE_TXN_LOCK_DIR);
2127
2132
  }
@@ -2131,7 +2136,7 @@ export class FileMemoryEngineBackend {
2131
2136
  assertTxnLockOwnership(token) {
2132
2137
  assertStoreTxnLockOwnership(this.controlPlaneRoot, token);
2133
2138
  }
2134
- async applyPatchesLocked(patches, lockToken, anchoredRoot) {
2139
+ async applyPatchesLocked(patches, lockToken, anchoredRoot, admitted, confirm) {
2135
2140
  this.assertTxnLockOwnership(lockToken);
2136
2141
  if (anchoredRoot !== undefined)
2137
2142
  this.assertRootAnchored(anchoredRoot);
@@ -2146,9 +2151,12 @@ export class FileMemoryEngineBackend {
2146
2151
  const plannedTargets = new Set();
2147
2152
  const nonDeleteIds = new Set();
2148
2153
  const plannedDeletes = new Map();
2154
+ const landings = [];
2155
+ const shares = [];
2149
2156
  this.batchScan = new Map();
2150
2157
  try {
2151
2158
  for (const patch of patches) {
2159
+ const landingsBefore = landings.length;
2152
2160
  try {
2153
2161
  if (patch.op !== "delete") {
2154
2162
  if (patch.entry !== undefined && patch.entry.id !== patch.id) {
@@ -2161,11 +2169,13 @@ export class FileMemoryEngineBackend {
2161
2169
  }
2162
2170
  nonDeleteIds.add(patch.id);
2163
2171
  }
2164
- this.planOne(patch, report, ops, rows, plannedTargets, plannedDeletes, transfers);
2172
+ this.planOne(patch, report, ops, rows, plannedTargets, plannedDeletes, transfers, landings);
2165
2173
  }
2166
2174
  catch (err) {
2167
2175
  report.conflicts.push({ op: patch.op, id: patch.id, reason: `io error: ${thrownText(err)}` });
2168
2176
  }
2177
+ if (admitted !== undefined)
2178
+ shares.push({ opsEnd: ops.length, evidenceEnd: transfers.length, ...(landings.length > landingsBefore ? { landed: cloneRows(rows) } : {}) });
2169
2179
  }
2170
2180
  }
2171
2181
  finally {
@@ -2174,6 +2184,15 @@ export class FileMemoryEngineBackend {
2174
2184
  if (ops.length === 0) {
2175
2185
  return report;
2176
2186
  }
2187
+ if (admitted !== undefined)
2188
+ requireLandingsAdmitted(admitted, landings);
2189
+ for (const cut of admitted === undefined ? [{ ops, evidence: transfers, account: rows }] : cutPerLanding(ops, transfers, shares, rows)) {
2190
+ confirm?.();
2191
+ this.commitCut(cut, lockToken, anchoredRoot);
2192
+ }
2193
+ return report;
2194
+ }
2195
+ commitCut({ ops, evidence: transfers, account: rows }, lockToken, anchoredRoot) {
2177
2196
  const txn = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2178
2197
  const stagedFiles = [];
2179
2198
  const refuseUnstaging = (batch, at) => {
@@ -2229,6 +2248,7 @@ export class FileMemoryEngineBackend {
2229
2248
  this.refuseEscapingTargets([op], "applyPatches (after the commit point; the journal is kept)");
2230
2249
  if (op.kind === "write" && op.staged) {
2231
2250
  renameSync(op.staged, op.target);
2251
+ this.landingFault?.(op.target);
2232
2252
  }
2233
2253
  else if (op.kind === "delete") {
2234
2254
  rmSync(op.target, { force: true });
@@ -2244,7 +2264,6 @@ export class FileMemoryEngineBackend {
2244
2264
  this.saveLedger();
2245
2265
  this.appendTransfers(coherentTransfers);
2246
2266
  this.closeJournalIfOurs(txn);
2247
- return report;
2248
2267
  }
2249
2268
  closeJournalIfOurs(txn) {
2250
2269
  const jp = join(this.controlPlaneRoot, STORE_JOURNAL_FILE);
@@ -2265,7 +2284,7 @@ export class FileMemoryEngineBackend {
2265
2284
  }
2266
2285
  rmSync(jp, { force: true });
2267
2286
  }
2268
- planOne(patch, report, ops, rows, plannedTargets, plannedDeletes, transfers) {
2287
+ planOne(patch, report, ops, rows, plannedTargets, plannedDeletes, transfers, landings) {
2269
2288
  if (!isValidEntryId(patch.id)) {
2270
2289
  report.conflicts.push({ op: patch.op, id: patch.id, reason: `invalid_id_shape (id ${JSON.stringify(patch.id)} fails the frontmatter id contract — the read side would drop it, leaving an applied-but-invisible entry)` });
2271
2290
  return;
@@ -2336,6 +2355,7 @@ export class FileMemoryEngineBackend {
2336
2355
  }
2337
2356
  if (existingAnywhere !== undefined && existingAnywhere.entry.slug !== entry.slug) {
2338
2357
  ops.push({ kind: "delete", target: existingAnywhere.path });
2358
+ landings.push({ id: entry.id, scope: existingAnywhere.entry.scope, slug: existingAnywhere.entry.slug });
2339
2359
  }
2340
2360
  const dir = this.scopeDir(entry.scope);
2341
2361
  let slug = entry.slug;
@@ -2356,6 +2376,7 @@ export class FileMemoryEngineBackend {
2356
2376
  plannedTargets.add(target);
2357
2377
  ops.push({ kind: "write", target, content });
2358
2378
  ops.push({ kind: "shadow-write", target: entry.id, content });
2379
+ landings.push({ id: entry.id, scope: entry.scope, slug });
2359
2380
  const now = this.now();
2360
2381
  bindRowTracked(rows, entry.id, { rev: computeEntryRev(entry), scope: entry.scope, slug, at: now }, transfers, "applyPatches-move");
2361
2382
  const priorBinding = plannedDeletes.get(entry.id);
@@ -2413,17 +2434,21 @@ export class FileMemoryEngineBackend {
2413
2434
  const shadowText = this.readCommittedShadow(patch.id);
2414
2435
  if (shadowText !== undefined && shadowText !== serializeEntryFile(found.entry)) {
2415
2436
  ops.push({ kind: "write", target: found.path, content: shadowText });
2437
+ landings.push({ id: patch.id, scope: found.entry.scope, slug: found.entry.slug });
2416
2438
  }
2417
2439
  return;
2418
2440
  }
2419
2441
  if (patch.op === "delete") {
2420
2442
  ops.push({ kind: "delete", target: found.path });
2421
2443
  ops.push({ kind: "shadow-delete", target: patch.id });
2444
+ landings.push({ id: patch.id, scope: found.entry.scope, slug: found.entry.slug });
2422
2445
  const deletedBinding = rowsBinding(rows, patch.id);
2423
2446
  if (deletedBinding !== undefined) {
2424
2447
  const derived = this.bindingAbsPath(deletedBinding);
2425
- if (derived !== found.path)
2448
+ if (derived !== found.path) {
2426
2449
  ops.push({ kind: "delete", target: derived });
2450
+ landings.push({ id: patch.id, scope: deletedBinding.scope, slug: deletedBinding.slug });
2451
+ }
2427
2452
  }
2428
2453
  transfers.push({
2429
2454
  ev: randomUUID(),
@@ -2446,11 +2471,14 @@ export class FileMemoryEngineBackend {
2446
2471
  const dir = this.scopeDir(entry.scope);
2447
2472
  const target = join(dir, `${entry.slug}.md`);
2448
2473
  const content = serializeEntryFile(entry);
2449
- if (target !== found.path)
2474
+ if (target !== found.path) {
2450
2475
  ops.push({ kind: "delete", target: found.path });
2476
+ landings.push({ id: entry.id, scope: found.entry.scope, slug: found.entry.slug });
2477
+ }
2451
2478
  plannedTargets.add(target);
2452
2479
  ops.push({ kind: "write", target, content });
2453
2480
  ops.push({ kind: "shadow-write", target: entry.id, content });
2481
+ landings.push({ id: entry.id, scope: entry.scope, slug: entry.slug });
2454
2482
  bindRowTracked(rows, entry.id, { rev: computeEntryRev(entry), scope: entry.scope, slug: entry.slug, at: this.now() }, transfers, "applyPatches-move");
2455
2483
  report.applied.push({ op: "update", id: entry.id, slug: entry.slug });
2456
2484
  }
@@ -10,9 +10,9 @@ export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, com
10
10
  export { consolidationExposedFrontmatter } from "./frontmatter.js";
11
11
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
12
12
  export { deriveShadowedSet } from "./shadowing.js";
13
- export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
13
+ export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type ConsolidationWriteGate, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
14
14
  export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
15
- export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
15
+ export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_WRITE_REFUSED_CODE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationWriteJudge, type ConsolidationWriteTarget, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
16
16
  export type { OriginClearanceRow, OriginClearanceEvent, OriginClearanceShadow } from "./origin-clearance.js";
17
17
  export { MEMORY_ORIGIN_CAUSES, writableRootOf } from "./types.js";
18
18
  export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryScopeEnumeration, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
@@ -12,7 +12,7 @@ export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
12
12
  export { deriveShadowedSet } from "./shadowing.js";
13
13
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, CONSOLIDATION_RUN_STOP_REASONS, } from "./consolidation.js";
14
14
  export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
15
- export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, } from "./consolidation-driver.js";
15
+ export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_WRITE_REFUSED_CODE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, } from "./consolidation-driver.js";
16
16
  export { MEMORY_ORIGIN_CAUSES, writableRootOf } from "./types.js";
17
17
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
18
18
  export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, isOrgScopeKey, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, } from "./scope-contract.js";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The landing-admission protocol: a caller that holds authority over WHERE notes may land (the consolidation run's write
3
+ * judge) and a backend that names the landings (it alone knows the final spelling — a collision suffix is its decision, taken
4
+ * under its transaction lock against the files as they are at that moment).
5
+ *
6
+ * A LANDING is every entry file a patch writes or removes: an add's final spelling, an update's target, the file a delete
7
+ * removes, and the old file a rename leaves. The backend plans a batch under its lock, finds every landing, and — before it
8
+ * stages anything — leaves the lock to put the ones the caller has not yet been told of to `admit`; it then plans again and
9
+ * writes only when every landing is one `admit` resolved. A spelling that moved in between (a rival writer took the name) is
10
+ * asked about again; it is never renamed after it was judged. The judgment is awaited outside the lock: it may wait on a rule store.
11
+ *
12
+ * An admitted batch is asked whole and committed one landing at a time ({@link cutPerLanding}): a backend's commit record is
13
+ * replayed by its crash recovery without anyone to ask, so it never carries a landing behind another one. A crash between two
14
+ * landings leaves the earlier landed and the later uncommitted, to be asked again by whoever applies it next; recovery only
15
+ * ever completes the one landing whose commit had begun.
16
+ */
17
+ import type { MemoryBackend, NotePatch, PatchReport } from "./types.js";
18
+ /** One landing: the entry's id and the scope directory and slug of the file it writes or removes. */
19
+ export interface PatchLanding {
20
+ readonly id: string;
21
+ readonly scope: string;
22
+ readonly slug: string;
23
+ }
24
+ /** Resolves ⇒ the landings may be written; rejects ⇒ nothing is written and the rejection propagates unchanged. */
25
+ export type AdmitLandings = (landings: readonly PatchLanding[]) => Promise<void>;
26
+ /** Rounds of "plan, ask, plan again" a batch gets before the backend gives up on a name that keeps moving. */
27
+ export declare const MAX_LANDING_ADMISSION_ROUNDS = 4;
28
+ /** Thrown from inside the lock, before anything is staged: these landings have not been admitted yet. */
29
+ export declare class LandingsNeedAdmission extends Error {
30
+ readonly landings: readonly PatchLanding[];
31
+ constructor(landings: readonly PatchLanding[]);
32
+ }
33
+ /**
34
+ * Inside the lock, once the batch is planned with its `landings`: throw {@link LandingsNeedAdmission} naming the ones not in
35
+ * `admitted`. Nothing is on disk yet, so the throw is a clean exit.
36
+ */
37
+ export declare function requireLandingsAdmitted(admitted: ReadonlySet<string>, landings: readonly PatchLanding[]): void;
38
+ /** One planned patch's share of an admitted batch: where its operations and evidence end in the batch's lists, and — iff it has
39
+ * a landing — the account as it stands after it. */
40
+ export interface PatchShare<R> {
41
+ readonly opsEnd: number;
42
+ readonly evidenceEnd: number;
43
+ readonly landed?: R;
44
+ }
45
+ /** One transaction of an admitted batch: the operations and evidence it commits and the account it leaves. */
46
+ export interface LandingCut<O, E, R> {
47
+ ops: O[];
48
+ evidence: E[];
49
+ account: R;
50
+ }
51
+ /**
52
+ * The commit cuts of an admitted batch, in order: a cut closes at each patch that has a landing, so no cut carries two patches'
53
+ * landings (a rename's two files are one patch's, committed together; a patch with operations but no landing — a conflict's restore
54
+ * of committed bytes — rides with the next). A cut leaves the account as it stood after its landing; the last cut leaves
55
+ * `finalAccount`, the batch's (whatever the patches after the last landing changed rides with it).
56
+ */
57
+ export declare function cutPerLanding<O, E, R>(ops: readonly O[], evidence: readonly E[], shares: readonly PatchShare<R>[], finalAccount: R): Array<LandingCut<O, E, R>>;
58
+ /** A backend that cannot admit is handed the landings its caller admitted one call each, so none of its commits carries a landing
59
+ * behind another whatever its own commit unit is; `before` (the caller's holder check) runs ahead of each call. */
60
+ export declare function applyEachAlone(backend: Pick<MemoryBackend, "applyPatches">, patches: readonly NotePatch[], before: () => void): Promise<PatchReport>;
61
+ /**
62
+ * The backend's side of the loop. `run(admitted)` plans and applies one locked attempt (it throws
63
+ * {@link LandingsNeedAdmission} when the plan names a spelling not in `admitted`); with no `admit` it runs once, unconditioned.
64
+ */
65
+ export declare function admittingLandings<T>(admit: AdmitLandings | undefined, run: (admitted: ReadonlySet<string> | undefined) => Promise<T>): Promise<T>;
@@ -0,0 +1,68 @@
1
+ export const MAX_LANDING_ADMISSION_ROUNDS = 4;
2
+ export class LandingsNeedAdmission extends Error {
3
+ landings;
4
+ constructor(landings) {
5
+ super("memory landings need admission");
6
+ this.landings = landings;
7
+ }
8
+ }
9
+ const landingKey = (l) => `${l.scope}\u0000${l.slug}`;
10
+ export function requireLandingsAdmitted(admitted, landings) {
11
+ const pending = landings.filter((l) => !admitted.has(landingKey(l)));
12
+ if (pending.length > 0)
13
+ throw new LandingsNeedAdmission(pending);
14
+ }
15
+ export function cutPerLanding(ops, evidence, shares, finalAccount) {
16
+ const cuts = [];
17
+ let open;
18
+ let opsAt = 0;
19
+ let evidenceAt = 0;
20
+ for (const s of shares) {
21
+ if (s.opsEnd > opsAt || s.evidenceEnd > evidenceAt) {
22
+ if (open === undefined)
23
+ cuts.push((open = { ops: [], evidence: [], account: finalAccount }));
24
+ open.ops.push(...ops.slice(opsAt, s.opsEnd));
25
+ open.evidence.push(...evidence.slice(evidenceAt, s.evidenceEnd));
26
+ opsAt = s.opsEnd;
27
+ evidenceAt = s.evidenceEnd;
28
+ }
29
+ if (s.landed !== undefined && open !== undefined) {
30
+ open.account = s.landed;
31
+ open = undefined;
32
+ }
33
+ }
34
+ const last = cuts[cuts.length - 1];
35
+ if (last !== undefined)
36
+ last.account = finalAccount;
37
+ return cuts;
38
+ }
39
+ export async function applyEachAlone(backend, patches, before) {
40
+ const report = { applied: [], conflicts: [] };
41
+ for (const p of patches) {
42
+ before();
43
+ const r = await backend.applyPatches([p]);
44
+ report.applied.push(...r.applied);
45
+ report.conflicts.push(...r.conflicts);
46
+ }
47
+ return report;
48
+ }
49
+ export async function admittingLandings(admit, run) {
50
+ if (admit === undefined)
51
+ return run(undefined);
52
+ const admitted = new Set();
53
+ for (let round = 0;; round++) {
54
+ try {
55
+ return await run(admitted);
56
+ }
57
+ catch (err) {
58
+ if (!(err instanceof LandingsNeedAdmission))
59
+ throw err;
60
+ if (round >= MAX_LANDING_ADMISSION_ROUNDS) {
61
+ throw Object.assign(new Error(`memory landings kept moving: ${err.landings.length} spelling(s) were still unadmitted after ${MAX_LANDING_ADMISSION_ROUNDS} rounds, nothing was written`), { code: "memory.landing_unstable" });
62
+ }
63
+ await admit(err.landings);
64
+ for (const l of err.landings)
65
+ admitted.add(landingKey(l));
66
+ }
67
+ }
68
+ }
@@ -379,10 +379,33 @@ export interface MemoryBackend {
379
379
  * the whole batch with `memory.target_outside_store` when the anchor is another root (`detail.recorded`
380
380
  * = the anchor, `detail.now` = `anchoredRoot`): a session mounted before a re-anchor commits nothing
381
381
  * into the re-anchored store. A backend without a directory ignores it.
382
+ *
383
+ * `opts.admit` is the caller's authority over WHERE the batch lands, asked at the spelling each ADD will finally take (after
384
+ * the backend's own collision suffix). A backend that declares {@link admitsLandings} resolves every add's final landing
385
+ * `{ id, scope, slug }`, calls `admit` with ALL the ones it has not yet been told of BEFORE it writes any of them (never while
386
+ * it holds its own transaction lock: the caller's judgment may wait on a store), and writes only spellings `admit` has
387
+ * resolved: a spelling that moves after `admit` returned (a rival writer took the name) is asked about again before anything is
388
+ * written, never renamed after the judgment. A rejection from `admit` writes nothing and propagates unchanged. Asked whole, an
389
+ * admitted batch commits one landing at a time: whatever the backend's crash recovery replays without asking (its commit
390
+ * record) never carries a landing behind another one, so a crash between two landings leaves the later one unwritten until
391
+ * its next apply asks again. A backend that does not declare it is handed neither `admit` nor `confirm`: the caller asks about
392
+ * the spellings it froze, all of them, just before the batch, and hands it the writes one call each, its own check before each.
393
+ * `opts.confirm` is the caller's last word, called synchronously under the transaction lock before each commit of the batch
394
+ * (once every spelling is admitted, before anything of that commit is staged), with or without `admit`; a throw aborts what is
395
+ * not yet committed and propagates unchanged.
382
396
  */
383
397
  applyPatches(patches: readonly NotePatch[], opts?: {
384
398
  readonly anchoredRoot?: string;
399
+ readonly admit?: (landings: readonly {
400
+ readonly id: string;
401
+ readonly scope: string;
402
+ readonly slug: string;
403
+ }[]) => Promise<void>;
404
+ readonly confirm?: () => void;
385
405
  }): Promise<PatchReport>;
406
+ /** `true` ⇔ {@link applyPatches} honors `opts.admit` and `opts.confirm` (the File backend does). Absent ⇒ the caller admits the frozen
407
+ * spellings itself and sends the writes one call each. */
408
+ readonly admitsLandings?: true;
386
409
  /** design/84 Seam B cursor pair, migrated verbatim onto the entry-id plane (S2 rewires consolidation). */
387
410
  getConsolidationCursor(scope: string): Promise<string | undefined>;
388
411
  setConsolidationCursor(scope: string, cursor: string): Promise<void>;
@@ -165,7 +165,8 @@ export declare function refusePermissionModeDoors(spec: Pick<TaskSpec, "permissi
165
165
  * The doors a RESUMED leg's prepare refuses at, read as one screen for the resume ladder to run BEFORE the claim: the mode
166
166
  * doors on the re-supplied config ({@link refusePermissionModeDoors}), its output-style seat (`resolveOutputStyleFrame`), its
167
167
  * approval-reach declaration (`screenApprovalReach`), the skills it invokes by name (`screenUserInvokedSkills` — the part no
168
- * roster changes; a name the leg's roster reads as nothing is prepare's) and the mode words the checkpoint row recorded (`recordedModeSeed`) — prepare's own functions, so the refusal carries the very
168
+ * roster changes; a name the leg's roster reads as nothing is prepare's), its attachments (`screenUserAttachments` — a resume
169
+ * sends no new human message, so any entry is refused) and the mode words the checkpoint row recorded (`recordedModeSeed`) — prepare's own functions, so the refusal carries the very
169
170
  * sentence and code prepare would throw. It leaves by the resume's ONE refusal channel, `CheckpointError`
170
171
  * `checkpoint.config_refused` (`message` = prepare's sentence, `detail.code` = prepare's code, `detail.field` = the input that
171
172
  * carries it: `"taskConfig"` or `"inheritedGate"`), where prepare itself would end the run `failed` after the claim — a
@@ -174,7 +175,7 @@ export declare function refusePermissionModeDoors(spec: Pick<TaskSpec, "permissi
174
175
  * corrected resume. A throw that is not one of prepare's five codes (a caller's accessor failing) is not a refusal and leaves
175
176
  * as it was thrown.
176
177
  */
177
- export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle" | "approvalReach" | "skills" | "userInvokedSkills">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
178
+ export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle" | "approvalReach" | "skills" | "userInvokedSkills" | "userAttachments">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
178
179
  /** The three-member answer every mode question gets — the manifest member, the steer verb's return value and the
179
180
  * fold notice's detail share it. `requested` is absent when the caller declared nothing (a root inherits `default`,
180
181
  * a child inherits its parent). */
@@ -8,6 +8,7 @@ import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
8
8
  import { createSafeNotifier } from "./safe-notify.js";
9
9
  import { assertNoRetiredKeys, RETIRED_TASK_SPEC_KEYS } from "./retired-keys.js";
10
10
  import { resolveOutputStyleFrame } from "./output-style.js";
11
+ import { screenUserAttachments } from "./user-attachment.js";
11
12
  import { CheckpointError, isCheckpointConfigRefusalCode, recordedModeSeed } from "./checkpoint-store.js";
12
13
  import { screenApprovalReach } from "./wiring-manifest.js";
13
14
  import { screenUserInvokedSkills } from "./skill-grant.js";
@@ -116,6 +117,7 @@ export function refuseResumeDoors(config, recordedGate) {
116
117
  resolveOutputStyleFrame(config.outputStyle);
117
118
  screenApprovalReach(config.approvalReach);
118
119
  screenUserInvokedSkills(config);
120
+ screenUserAttachments(config.userAttachments, { resumeLeg: true });
119
121
  });
120
122
  screenAs("inheritedGate", () => void recordedModeSeed(recordedGate));
121
123
  }
@@ -75,7 +75,7 @@
75
75
  import { type DenyFace } from "../tools/fs/read-deny.js";
76
76
  import type { AssertAllKeysHandled } from "./ask-origin.js";
77
77
  import { type ToolCallLike } from "./tool-registry.js";
78
- import { type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
78
+ import { type PathDenialEvidence, type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
79
79
  import { type TargetViews } from "./path-rule-binding.js";
80
80
  /**
81
81
  * The closed set of rule BEHAVIORS — what a matching rule says about the call. The upstream vocabulary
@@ -141,11 +141,12 @@ export declare const READ_RULE_TOOL = "Read";
141
141
  * roots, the write tools — judged on the path that call reads, searches or writes. A path rule on any other tool keeps
142
142
  * its tool's name as its reach (`Write(<path>)` is judged on the Write tool, a stricter reading the reference does not make).
143
143
  *
144
- * The two columns are enforced where each behavior can be: a deny row is exported to the engine's deny tables
145
- * (`ToolPolicy.pathRules` bound where the policy is installed, the person's rows at prepare), which refuse inside the tool, descend into a search's
146
- * results and reach the shell's file commands; an ask is a question asked at the gate before the call runs, so it is
147
- * judged there, on the call's own path target only — it does not descend (a search rooted above the asked path is not
148
- * asked, and its results are not filtered).
144
+ * Both tightening columns are judged at the gate, on the call's own path target, before the call runs — by the rule
145
+ * policy's lane and by the persisted lane alike, one column read for deny and for ask. A deny row is exported to the
146
+ * engine's deny tables as well (`ToolPolicy.pathRules` bound where the policy is installed, the person's rows at
147
+ * prepare), which do what the gate cannot: refuse inside the tool at execution time, descend into a search's results,
148
+ * reach the shell's file commands' operands, and keep a refused target out of the out-of-root question. An ask does not
149
+ * descend (a search rooted above the asked path is not asked, and its results are not filtered).
149
150
  *
150
151
  * The ALLOW column is the reference's too: an allow by path is read on these two tools only — a `Read(<path>)` allow speaks
151
152
  * for the read face, an `Edit(<path>)` allow for the write face and the read face (an edit allowed is a read allowed) — and
@@ -156,9 +157,9 @@ export declare const PATH_RULE_FACES: Readonly<Record<string, Readonly<Partial<R
156
157
  /** The face a call's protective path target is on: `read` for a read slot, `write` for every other access; none without
157
158
  * one — the face a {@link PATH_RULE_FACES} rule is judged against, in a rule policy and in the persisted rule lane alike. */
158
159
  export declare function callPathFaceOf(call: ToolCallLike): DenyFace | undefined;
159
- /** Does some {@link PATH_RULE_FACES} ask column hold `face` — may a face-keyed ask row speak for a call on it, whatever
160
- * the call's tool is called (a caller's tool declaring a read path target is on the read face too)? */
161
- export declare function faceAskSpeaksFor(face: DenyFace | undefined): boolean;
160
+ /** Does some {@link PATH_RULE_FACES} tightening column (deny or ask) hold `face` — may a face-keyed row speak for a call on
161
+ * it, whatever the call's tool is called (a caller's tool declaring a read path target is on the read face too)? */
162
+ export declare function faceRowsSpeakFor(face: DenyFace | undefined): boolean;
162
163
  /**
163
164
  * The tools whose rule content NAMES THE VALUE of one parameter instead of a command or a path — the reference's `Skill(<name>)`
164
165
  * form, one row per tool: `param` is the argument the rule's content is read against, `params` every scalar argument the tool
@@ -949,6 +950,9 @@ export interface PersistedRuleVerdict {
949
950
  * the lexer's reason. The lane projects it as an UNREADABLE answer carrying that reason (it fails closed)
950
951
  * — never as a matched ask rule. */
951
952
  readonly unreadable?: string;
953
+ /** A path DENY's coordinates: the view of the target the reaching row met and the face the call is on. Minted here,
954
+ * with the verdict; the gate's exits mint the refusal's card from it and judge nothing a second time. */
955
+ readonly denial?: PathDenialEvidence;
952
956
  }
953
957
  /**
954
958
  * The ONE adjudication over a MIXED persisted-rule table for a shell call: walk the behaviors in
@@ -991,10 +995,12 @@ export declare function adjudicatePersistedRules(rules: readonly PersistedRule[]
991
995
  *
992
996
  * A row is anchored by `anchorsOf` (the person's rows: a project row at its scope root, the rest at the chain's top leg).
993
997
  *
994
- * `call.face` is the face the call's protective target is on (`read` / `write`): an ASK row speaks for the call when it
995
- * is the call's tool's row, or when {@link PATH_RULE_FACES} puts that face in the row's ask column (a `Read(<path>)` ask
996
- * row asks for a Grep / Glob searching under the path). A deny row's reach beyond its tool is not judged here: it is the
997
- * run's deny tables, taken at prepare, which refuse inside the tool and descend into a search's results.
998
+ * `call.face` is the face the call's protective target is on (`read` / `write`): a tightening row (deny or ask) speaks for
999
+ * the call when it is the call's tool's row, or when {@link PATH_RULE_FACES} puts that face in the row's column of THAT
1000
+ * behavior (a `Read(<path>)` deny row refuses, a `Read(<path>)` ask row asks for, a Grep / Glob / RepoMap / caller's reader
1001
+ * searching or reading under the path) — the one sentence the rule policy's lane speaks too. Both directions judge the call's
1002
+ * own target here; the run's deny tables are a different job (a search's results, a shell command's operands, the
1003
+ * execution-time re-judgment).
998
1004
  */
999
1005
  export declare function adjudicatePersistedPathRules(rules: readonly PersistedRule[], call: {
1000
1006
  tool: string;
@@ -38,8 +38,8 @@ export function callPathFaceOf(call) {
38
38
  const target = protectivePathTargetOf(call);
39
39
  return target === undefined ? undefined : target.access === "read" ? "read" : "write";
40
40
  }
41
- export function faceAskSpeaksFor(face) {
42
- return face !== undefined && Object.values(PATH_RULE_FACES).some((columns) => columns.ask?.includes(face) === true);
41
+ export function faceRowsSpeakFor(face) {
42
+ return face !== undefined && Object.values(PATH_RULE_FACES).some((columns) => columns.deny?.includes(face) === true || columns.ask?.includes(face) === true);
43
43
  }
44
44
  export const NAME_RULE_FACES = Object.freeze({
45
45
  Skill: Object.freeze({ param: "skill", params: Object.freeze(["skill", "args"]) }),
@@ -1000,7 +1000,7 @@ export function adjudicatePersistedRules(rules, call) {
1000
1000
  }
1001
1001
  export function adjudicatePersistedPathRules(rules, call, views, anchorsOf, leads) {
1002
1002
  const speaks = (rule, behavior) => eligiblePersisted(rule, call, behavior) ||
1003
- (behavior === "ask" && call.face !== undefined && PATH_RULE_FACES[rule.tool]?.ask?.includes(call.face) === true && eligiblePersisted(rule, { ...call, tool: rule.tool }, behavior));
1003
+ (call.face !== undefined && PATH_RULE_FACES[rule.tool]?.[behavior]?.includes(call.face) === true && eligiblePersisted(rule, { ...call, tool: rule.tool }, behavior));
1004
1004
  const bind = (rows) => bindPathRules(rows.map((rule) => ({ parsed: rule, ruleText: rule.rule, rule })), (s) => anchorsOf(s.rule));
1005
1005
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
1006
1006
  if (behavior === "allow") {
@@ -1021,7 +1021,10 @@ export function adjudicatePersistedPathRules(rules, call, views, anchorsOf, lead
1021
1021
  const hit = judgeRuleRows(rowsWhereTheyLead(bind(rows), leads), views.tighten, "tighten");
1022
1022
  if (hit === undefined)
1023
1023
  continue;
1024
- return hit.unreadable !== undefined ? { behavior: "ask", rules: [hit.row.spec.rule], unreadable: hit.unreadable } : { behavior, rules: [hit.row.spec.rule] };
1024
+ const rule = hit.row.spec.rule;
1025
+ if (hit.unreadable !== undefined)
1026
+ return { behavior: "ask", rules: [rule], unreadable: hit.unreadable };
1027
+ return { behavior, rules: [rule], ...(behavior === "deny" && hit.view !== undefined && call.face !== undefined ? { denial: { rule: rule.rule, view: hit.view, face: call.face } } : {}) };
1025
1028
  }
1026
1029
  return undefined;
1027
1030
  }
@@ -61,6 +61,17 @@ export interface RemovedPermissionRule {
61
61
  source: "user" | "project";
62
62
  status: "removed";
63
63
  }
64
+ /** One read of the person's store as every consumer of it reads it, the failure law applied (a run's one reader of it,
65
+ * `person-path-rows.ts`, takes it; the rule lanes and the read station read it). */
66
+ export interface PersonRowsRead {
67
+ /** The rows this read stands on: each partition it read, as read; each it could not read, the deny and ask rows of the
68
+ * run's last successful read of that partition (none before one). This session's rows first. */
69
+ readonly rules: readonly EffectivePermissionRule[];
70
+ /** The org partition's resolution off this read. Absent when the whole read failed — {@link unreadable} says why. */
71
+ readonly org?: OrgRuleResolution;
72
+ /** Present iff a personal partition (or the whole read) could not be read: the failure's line. */
73
+ readonly unreadable?: string;
74
+ }
64
75
  /** THE answer of the store's one read query. */
65
76
  export interface EffectivePermissionRules {
66
77
  /** Live personal rows, in reporting order: this session's rows first (the narrowest standing yes), then
@@ -57,7 +57,7 @@
57
57
  * 匹配,不是 shell AST / 前缀 lane —— `Bash(command:git push*)` 匹配不了 `cd /x && git push`,
58
58
  * 「以为挡住了」比没挡更危险。`primaryFieldGeneric:"allow-lexical"` 显式解锁。
59
59
  */
60
- import type { NamedToolPolicy } from "./tool-policy.js";
60
+ import { type NamedToolPolicy } from "./tool-policy.js";
61
61
  import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
62
62
  import { type PathRuleBases } from "./permission-rule-model.js";
63
63
  export { parsePermissionRule, type ParsedPermissionRule };
@@ -1,3 +1,4 @@
1
+ import { attest } from "./tool-policy.js";
1
2
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
3
  import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
3
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
@@ -348,7 +349,7 @@ export function createPermissionRulePolicy(rules, opts) {
348
349
  const hit = judgeRuleRows(rows, views, "tighten");
349
350
  const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
350
351
  if (hit !== undefined && hit.unreadable === undefined)
351
- return { rule: rule };
352
+ return { rule: rule, ...(hit.view !== undefined ? { view: hit.view } : {}) };
352
353
  if (hit !== undefined)
353
354
  return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
354
355
  }
@@ -416,7 +417,9 @@ export function createPermissionRulePolicy(rules, opts) {
416
417
  const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
417
418
  const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
418
419
  if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
419
- return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
420
+ const denied = { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
421
+ const face = contentDeny.view !== undefined ? callPathFaceOf(req) : undefined;
422
+ return face !== undefined ? attest(denied, req, { pathDenial: { rule: contentDeny.rule.ruleText, view: contentDeny.view, face } }) : denied;
420
423
  }
421
424
  if (entry) {
422
425
  const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
@@ -11,14 +11,15 @@
11
11
  * the read closes over the gate input, and the allow arm is a conjunction over the gate's own state
12
12
  * (mandates, the org latch, the marked-call seat) rather than a function of the answer alone.
13
13
  */
14
- import type { PermissionResult } from "./tool-policy.js";
14
+ import { type PermissionResult, type ToolCallRequest } from "./tool-policy.js";
15
15
  import type { PersistedRuleAnswer, PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
16
16
  /** The lane's one read, normalized: at most one of `hit` / `unreadable` / `coverage` (the gate's reader
17
17
  * produces it; the arms below read it). */
18
18
  export interface PersistedRuleRead {
19
19
  readonly hit?: PersistedRuleHit;
20
20
  /** The lane could not answer: with `reason`, the CALL could not be read against the person's deny/ask rows;
21
- * without, the store could not be read — its rows take no part in the decision (the record says so). */
21
+ * without, a partition of the store (or all of it) could not be read — a deny / ask row inside it may forbid this call,
22
+ * so no allow beside it is a yes (the lane has already folded that: an allow, a coverage or nothing reads as this). */
22
23
  readonly unreadable?: true;
23
24
  /** The lexer's reason when the CALL could not be read against the person's deny/ask rows. */
24
25
  readonly reason?: string;
@@ -42,7 +43,9 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
42
43
  * · DENY — the call is refused outright, with `decisionReason: "persisted_rule"` (the same word the
43
44
  * lane's allow carries: one authority, one word) and the rule text disclosed through the display
44
45
  * transform; the STATION attributes the refusal (`tightened: "deny"` ⇒ it writes the pass's refusing
45
- * layer — the attribution is a station's word and lives in the station file).
46
+ * layer — the attribution is a station's word and lives in the station file). A path row's hit reports where
47
+ * it met the call; with `call` given and the face the call's own, the deny carries that as its attested `pathDenial`, so every exit that
48
+ * reads the deny (the gate's exit, the approved-edit re-check, a resumed leg's belt) mints the same card from the same fact.
46
49
  * · ASK — on an allow, a fresh ask is minted carrying `matchedAskRule` (the marker an explicit `ask`
47
50
  * permission rule stamps, so the ask's origin derives as `ask_rule`: the classifier never answers
48
51
  * it, the persisted ALLOW arm never clears it, and the rule-offer face reports `shadowed`); on an
@@ -55,11 +58,16 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
55
58
  * an allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
56
59
  * derives the origin `rule_store_unavailable` from its own record of the read), an ask acquires the
57
60
  * bit, a deny stays a deny.
58
- * A store that could not be READ (`unreadable` without a `reason`) contributes no row: the decision is returned
59
- * untouched, as for no hit — the lane announces the skipped store, and the rows apply again once a read succeeds.
61
+ * · UNREADABLE STORE — a partition of the store (or all of it) could not be READ (`unreadable` without a `reason`): a deny
62
+ * or ask row inside it may forbid this call and nobody can tell, so it fails CLOSED the same way, to the question
63
+ * only a person answers — an allow becomes an ask carrying `requiresRealApproval`, an ask acquires the bit, a deny stays
64
+ * a deny. Every call the lane judges is in this state until a read succeeds again (the lane announces the skipped store,
65
+ * once per run, and reads again on the next call). The two forms differ in WHICH fact the gate records (the call form
66
+ * is the class `rule_unreadable`; the store form `governance_unavailable` — both bypass-immune) and in the
67
+ * resumed-call belt, which vetoes only the call form: a person already answered that call's question.
60
68
  * An allow hit (or no hit) returns the decision untouched: the allow arm is the station's.
61
69
  */
62
- export declare function applyPersistedTightening(decision: PermissionResult, read: PersistedRuleRead | undefined): {
70
+ export declare function applyPersistedTightening(decision: PermissionResult, read: PersistedRuleRead | undefined, call?: ToolCallRequest): {
63
71
  decision: PermissionResult;
64
- tightened?: "deny" | "ask" | "unreadable";
72
+ tightened?: "deny" | "ask" | "unreadable" | "store_unreadable";
65
73
  };