@sema-agent/core 7.4.0 → 7.5.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 (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. package/test/export-surface.snapshot.json +76 -28
@@ -0,0 +1,76 @@
1
+ import { formatLocalDate } from "../../prompts/default.js";
2
+ import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
3
+ import { renderAgentListingDelta } from "./turn-attachments.js";
4
+ export function prepareListings(input) {
5
+ const { spec, tools, onceLedger, sessionId, toolFaceSnapshot, activeTools, resume, skillsListing, listingRideRef, renderWithDate, envFacts, tzValid, userTz } = input;
6
+ if (spec.agents !== undefined && spec.agents.length > 0) {
7
+ const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
8
+ for (const def of spec.agents) {
9
+ const unknownAllow = (def.allowTools ?? []).filter((n) => n !== "*" && !known.has(n));
10
+ if (unknownAllow.length > 0) {
11
+ try {
12
+ onceLedger.onError(new Error(`TaskSpec.agents: agent "${def.name}" allows tool(s) ${unknownAllow.join(", ")} not present in this task's assembled roster — likely a typo (the entry would be item-filtered at spawn; the agent stays usable). Advisory only: the delegation pool can differ from this roster, so a tool mounted only on the delegation tool or produced by per-spawn extraTools makes this spurious.`), { phase: "config", sessionId });
13
+ }
14
+ catch {
15
+ }
16
+ }
17
+ for (const n of def.denyTools ?? []) {
18
+ if (n === "*")
19
+ continue;
20
+ if (!known.has(n)) {
21
+ const e = new Error(`TaskSpec.agents: agent "${def.name}" declares tool "${n}" in its denied tools, but no such tool exists in this deployment — fix the agent's tools list or mount the tool.`);
22
+ e.code = "config.agent.unknown_tool";
23
+ throw e;
24
+ }
25
+ }
26
+ }
27
+ }
28
+ const rosterBearingSpec = (spec.tools ?? []).find((t) => t.agentListing !== undefined && t.agentListing.length > 0);
29
+ const rosterFaceDeferred = rosterBearingSpec !== undefined &&
30
+ !(toolFaceSnapshot.exclude?.includes(rosterBearingSpec.name) ?? false) &&
31
+ (toolFaceSnapshot.defer?.includes(rosterBearingSpec.name) ?? false) &&
32
+ !activeTools.has(rosterBearingSpec.name);
33
+ const agentListingSpec = rosterBearingSpec !== undefined &&
34
+ !rosterFaceDeferred &&
35
+ !(toolFaceSnapshot.exclude?.includes(rosterBearingSpec.name) ?? false)
36
+ ? rosterBearingSpec
37
+ : undefined;
38
+ const agentListing = agentListingSpec
39
+ ? {
40
+ entries: agentListingSpec.agentListing,
41
+ toolName: agentListingSpec.name,
42
+ seedAnnounced: resume !== undefined || spec.sessionId !== undefined,
43
+ ...(agentListingSpec.agentModels !== undefined ? { models: agentListingSpec.agentModels } : {}),
44
+ }
45
+ :
46
+ !rosterFaceDeferred && ((resume?.seed.announcedListings?.agents?.length ?? 0) > 0 || spec.sessionId !== undefined)
47
+ ?
48
+ {
49
+ entries: [],
50
+ toolName: (spec.tools ?? []).find((t) => t.agentListing !== undefined)?.name ?? "Agent",
51
+ seedAnnounced: true,
52
+ }
53
+ : undefined;
54
+ if (spec.attachments?.agentListing === false && agentListing !== undefined && agentListing.entries.length > 0) {
55
+ onceLedger.onError(new Error(`attachments.agentListing is explicitly false but the "${agentListing.toolName}" delegation tool mounts a ${agentListing.entries.length}-type roster — the model will never see the agent-type listing its tool description points to.`), { phase: "config", sessionId });
56
+ }
57
+ if (spec.attachments?.skillsListing === false && skillsListing !== undefined && skillsListing.entries.length > 0) {
58
+ onceLedger.onError(new Error(`attachments.skillsListing is explicitly false but ${skillsListing.entries.length} skill(s) are mounted — the model will never see the skills listing the ${SKILL_TOOL_NAME} tool description points to.`), { phase: "config", sessionId });
59
+ }
60
+ const announcedListingsRef = onceLedger.arms();
61
+ if (rosterFaceDeferred && rosterBearingSpec !== undefined) {
62
+ const rideEntries = rosterBearingSpec.agentListing;
63
+ const rideModels = rosterBearingSpec.agentModels;
64
+ listingRideRef.current = (newly) => {
65
+ if (!newly.includes(rosterBearingSpec.name))
66
+ return undefined;
67
+ if ((announcedListingsRef.agents?.length ?? 0) > 0)
68
+ return undefined;
69
+ return renderAgentListingDelta({ announcedAgentTypes: undefined }, rideEntries, rosterBearingSpec.name, rideModels);
70
+ };
71
+ }
72
+ const dateChange = renderWithDate
73
+ ? { legDate: envFacts.date, today: () => formatLocalDate(new Date(), tzValid ? userTz : undefined) }
74
+ : undefined;
75
+ return { agentListing, announcedListingsRef, dateChange };
76
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * design/390 §1.2 M8 — prepareTask's LSP phase, verbatim from the driver: the manager fold (spec > deps), the
3
+ * LSP tool mount over the per-task env with its git-ignore filter, the diagnostics lane behind its four
4
+ * preconditions (a manager whose sessions feed a registry, write-capable hands, not opted out), the run-scoped
5
+ * registry key, and the edit nudge (raw path → the SAME resolution the tool leg uses → registry reset +
6
+ * session didChange). No manager ⇒ nothing mounts and both lane seats are undefined. The interface is the
7
+ * dependency list the segment had implicitly (design/238 R-1).
8
+ *
9
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between the
10
+ * fork-governance note before it and the memory trio's occupancy read after it (design/390 §1.5 S1: a stretch
11
+ * with no await is extracted as a sync function — an `async` wrapper would open a microtask window).
12
+ * Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
13
+ */
14
+ import type { AgentTool, ExecutionEnv } from "../../internal/harness.js";
15
+ import type { LspDiagnosticsRegistry } from "../lsp-diagnostics.js";
16
+ import type { RunnerDeps, TaskSpec } from "../types.js";
17
+ import type { Prepared } from "./contracts.js";
18
+ export interface PrepareLspInput {
19
+ /** borrowed-readonly — the REBOUND spec. Read: `lspManager` (the per-task manager, outranks the deployment's),
20
+ * `lspDiagnostics` (the lane's opt-out), `handsReadOnly` (a read-only run has no lane). Never mutated. */
21
+ spec: Pick<TaskSpec, "lspManager" | "lspDiagnostics" | "handsReadOnly">;
22
+ /** borrowed-readonly — the deployment's manager seat (read only when the spec carries none). */
23
+ deps: Pick<RunnerDeps, "lspManager">;
24
+ /** borrowed-readonly — the run's resolved env: the tool's in-env server and git-ignore filter, and the
25
+ * session lookup the nudge performs. */
26
+ executionEnv: ExecutionEnv;
27
+ /** borrowed-readonly — the SETTLED task root: the tool's git-ignore root, and the nudge's resolution base when
28
+ * no cwd is tracked. */
29
+ taskRootFinal: string;
30
+ /** borrowed-readonly — whether a real fs env is present (the lane's hands precondition). */
31
+ handsEnabled: boolean;
32
+ /** borrowed-readonly — the acquired session id: the run's key into the deployment-shared registry. */
33
+ sessionId: string;
34
+ /** borrowed-readonly — the tracked-cwd cell the hands band writes; the nudge READS `current` per call as its
35
+ * resolution base (identity is the contract — the band's `cd` handling writes this exact cell). */
36
+ handsCwdRef: Prepared["cwdRef"];
37
+ /** borrowed-mutable — the run's shared roster (identity is the contract). Writer here: ONE push (the LSP tool)
38
+ * when a manager exists. */
39
+ tools: AgentTool[];
40
+ /** borrowed-mutable — the env-hand membership set the read-posture observer projects; writer here: `add("LSP")`
41
+ * on the mount. */
42
+ envHandToolNames: Set<string>;
43
+ }
44
+ export interface PrepareLspResult {
45
+ /** borrowed-readonly — the manager's diagnostics registry when the lane is live, else undefined (the
46
+ * registry is a DEPLOYMENT object shared by every task; this run reads and resets it under its own key). */
47
+ lspDiagnostics: LspDiagnosticsRegistry | undefined;
48
+ /** owned — the edit nudge (raw path in), or undefined when the lane is not live. */
49
+ nudgeLspOnEdit: ((rawPath: string) => void) | undefined;
50
+ /** borrowed-readonly — the run's registry key (= the session id): a session hosts one run at a time and the
51
+ * run loop releases the key at the terminal. */
52
+ lspRunIdent: string;
53
+ }
54
+ /** The M8 phase body — prepareTask's LSP stretch, verbatim (see the module header). */
55
+ export declare function prepareLsp(input: PrepareLspInput): PrepareLspResult;
@@ -0,0 +1,27 @@
1
+ import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
2
+ import { pathToUri } from "../lsp-protocol.js";
3
+ export function prepareLsp(input) {
4
+ const { spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, tools, envHandToolNames } = input;
5
+ const lspManager = spec.lspManager ?? deps.lspManager;
6
+ if (lspManager) {
7
+ const lspRoot = taskRootFinal;
8
+ tools.push(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }));
9
+ envHandToolNames.add("LSP");
10
+ }
11
+ const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
12
+ ? lspManager.diagnostics
13
+ : undefined;
14
+ const lspRunIdent = sessionId;
15
+ const nudgeLspOnEdit = lspDiagnostics
16
+ ? (rawPath) => {
17
+ const baseDir = handsCwdRef?.current ?? taskRootFinal;
18
+ const filePath = resolveLspPath(rawPath, baseDir);
19
+ lspDiagnostics.fileEdited(lspRunIdent, pathToUri(filePath));
20
+ void lspManager
21
+ .sessionFor(filePath, undefined, executionEnv)
22
+ .then((session) => session?.notifyFileChanged?.(filePath))
23
+ .catch(() => undefined);
24
+ }
25
+ : undefined;
26
+ return { lspDiagnostics, nudgeLspOnEdit, lspRunIdent };
27
+ }
@@ -1,6 +1,6 @@
1
1
  import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
2
  import { type CaptureEntitlementInput, type MemoryCapturePosture } from "./memory-capture-optout.js";
3
- import type { Prepared } from "./prepare-task.js";
3
+ import type { Prepared } from "./contracts.js";
4
4
  export interface PrepareMemoryInput {
5
5
  spec: TaskSpec;
6
6
  deps: RunnerDeps;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * design/390 §1.2 M0 (the offload half of the prelude) — prepareTask's nested-usage accumulator, the
3
+ * large-tool-result offload store resolution and the THREE offload wrappers every later mount site
4
+ * applies (caller tools, first-party hand tools, remote MCP/A2A peers), verbatim from the driver.
5
+ *
6
+ * SYNCHRONOUS by construction: the segment held no await in the driver, and a phase that yields where
7
+ * the driver did not would open a microtask window between two steps that used to be adjacent (the
8
+ * design/238 T1 posture). Nothing here touches the store — the wrappers close over it and only the
9
+ * wrapped tools' `execute` reach it, at run time.
10
+ *
11
+ * What stays in the driver: the trust-axis choice of `offloadScope` (`spec.principal ?? "default"`)
12
+ * and its disclosure, and the reserved-name door that FOLLOWS these wrappers (it awaits a bounded
13
+ * session forget, so it belongs with the driver's other refusal legs).
14
+ */
15
+ import { type ToolResultStore } from "../tool-result-store.js";
16
+ import type { AgentTool } from "../../internal/harness.js";
17
+ import type { NestedUsage, NestedUsageAccum, RunnerDeps, TaskSpec } from "../types.js";
18
+ import type { PrepareResume } from "./contracts.js";
19
+ export interface PrepareOffloadWrappersInput {
20
+ /** borrowed-readonly — the REBOUND spec (the config-doors result); read for the per-task coarse
21
+ * offload knob only. */
22
+ spec: Pick<TaskSpec, "toolResultThresholdChars">;
23
+ /** borrowed-readonly — the deployment's coarse offload knob and its injected store (used AS-IS when
24
+ * present; a Runner-shared instance is viewed per task, see the body). */
25
+ deps: Pick<RunnerDeps, "toolResultThresholdChars" | "toolResultStore">;
26
+ /** borrowed-readonly — the resume leg, read for the checkpoint seed's nested-usage snapshot so
27
+ * delegated cost spent before a suspend is not lost; `undefined` on a fresh run. */
28
+ resume: Pick<PrepareResume, "seed"> | undefined;
29
+ /** borrowed-readonly — the run's TRUST identity (`spec.principal ?? "default"`, minted by the
30
+ * driver): the namespace a Runner-shared store is scoped under. Never the registry domain. */
31
+ offloadScope: string;
32
+ /** borrowed-readonly — the acquired session id, the offload wrapper's ref-minting coordinate. */
33
+ sessionId: string;
34
+ }
35
+ export interface PrepareOffloadWrappersResult {
36
+ /** borrowed-mutable — the run's nested sub-run usage accumulator (`Prepared.nestedStats`).
37
+ * Mutation owners after this call: `reportUsage` below (the only writer — every delegation tool
38
+ * reports through it via the tool ctx); the driver reads it into `Prepared`. */
39
+ nestedStats: NestedUsageAccum;
40
+ /** borrowed-readonly — the one writer of `nestedStats`; the driver threads it onto the tool ctx. */
41
+ reportUsage: (u: NestedUsage) => void;
42
+ /** borrowed-readonly — the store the wrappers close over, or `undefined` when offload is disabled
43
+ * (threshold 0 / non-finite). The driver reads it for the reserved-name door, the ReadToolResult
44
+ * mount, the durable-suspend gate and the compaction lane. Not a resource: nothing to release. */
45
+ offloadStore: ToolResultStore | undefined;
46
+ /** borrowed-mutable — the LIVE "currently callable tools" accessor cell the offload preview's
47
+ * pageback hint reads. Mutation owners after this call: the deferred-disclosure branch of the
48
+ * driver sets `.current` once the deferred/active sets exist; nothing else writes it. */
49
+ offloadReachableToolsRef: {
50
+ current?: () => ReadonlySet<string>;
51
+ };
52
+ /** borrowed-readonly — wrap a CALLER tool (per-tool `offload` / `offloadThresholdChars` honored). */
53
+ maybeOffload: (tool: AgentTool, perTool?: {
54
+ offload?: boolean;
55
+ offloadThresholdChars?: number;
56
+ }) => AgentTool;
57
+ /** borrowed-readonly — wrap a FIRST-PARTY mount (the per-tool persistence table; Read/Write exempt). */
58
+ firstPartyOffload: (tool: AgentTool) => AgentTool;
59
+ /** borrowed-readonly — wrap a REMOTE-PEER mount (MCP `_meta` size declaration, else 50K). */
60
+ remoteToolOffload: (tool: AgentTool) => AgentTool;
61
+ }
62
+ export declare function prepareOffloadWrappers(input: PrepareOffloadWrappersInput): PrepareOffloadWrappersResult;
@@ -0,0 +1,45 @@
1
+ import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, withToolResultOffload, } from "../tool-result-store.js";
2
+ export function prepareOffloadWrappers(input) {
3
+ const { spec, deps, resume, offloadScope, sessionId } = input;
4
+ const nestedStats = { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false };
5
+ if (resume) {
6
+ nestedStats.tokens = resume.seed.nestedStats.tokens;
7
+ nestedStats.turns = resume.seed.nestedStats.turns;
8
+ nestedStats.tasks = resume.seed.nestedStats.tasks;
9
+ nestedStats.costMicroUsd = resume.seed.nestedStats.costMicroUsd;
10
+ nestedStats.anyUnpriced = resume.seed.nestedStats.anyUnpriced ?? true;
11
+ }
12
+ const reportUsage = (u) => {
13
+ nestedStats.tokens += u.tokens;
14
+ nestedStats.turns += u.turns;
15
+ nestedStats.tasks += u.tasks;
16
+ nestedStats.costMicroUsd += u.costMicroUsd ?? 0;
17
+ if (u.costMicroUsd === undefined)
18
+ nestedStats.anyUnpriced = true;
19
+ };
20
+ const explicitGlobalThreshold = spec.toolResultThresholdChars ?? deps.toolResultThresholdChars;
21
+ const offloadThreshold = explicitGlobalThreshold ?? DEFAULT_TOOL_RESULT_THRESHOLD_CHARS;
22
+ const offloadEnabled = Number.isFinite(offloadThreshold) && offloadThreshold > 0;
23
+ const rawOffloadStore = offloadEnabled ? (deps.toolResultStore ?? new InMemoryToolResultStore()) : undefined;
24
+ const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
25
+ ? new ScopedToolResultStore(rawOffloadStore, offloadScope)
26
+ : rawOffloadStore;
27
+ const offloadReachableToolsRef = {};
28
+ const maybeOffload = (tool, perTool) => {
29
+ if (!offloadStore || perTool?.offload === false)
30
+ return tool;
31
+ return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId, () => offloadReachableToolsRef.current?.());
32
+ };
33
+ const firstPartyOffload = (tool) => {
34
+ const policy = firstPartyOffloadPolicy(tool.name);
35
+ if (policy.offload === false)
36
+ return maybeOffload(tool, policy);
37
+ return maybeOffload(tool, explicitGlobalThreshold === undefined ? policy : undefined);
38
+ };
39
+ const remoteToolOffload = (tool) => {
40
+ if (explicitGlobalThreshold !== undefined)
41
+ return maybeOffload(tool);
42
+ return maybeOffload(tool, { offloadThresholdChars: tool.mcpMaxResultSizeChars ?? 50_000 });
43
+ };
44
+ return { nestedStats, reportUsage, offloadStore, offloadReachableToolsRef, maybeOffload, firstPartyOffload, remoteToolOffload };
45
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * design/389 — prepare's permission-rule phase: the TWO gate lanes the unified store feeds.
3
+ *
4
+ * `prepareTask` used to assemble the personal lane inline (a store read + a hand-spliced session
5
+ * overlay + two admission arms) and the org lane from a second seam. Both now read the ONE store query —
6
+ * `PermissionRuleStore.effective({ sessionId })` — and this module is where that read is taken and how it
7
+ * is shared between the two lanes of one adjudicated call.
8
+ *
9
+ * ## One read per adjudicated call
10
+ *
11
+ * The gate consults the org lane (`orgRules.adjudicate`) on every decision and the personal lane
12
+ * (`persistedRules.admits`) only on a surviving ask, in that order. The org lane's read is therefore
13
+ * HANDED to the personal lane by `toolCallId` (taken once, then forgotten), so a governed deployment pays
14
+ * one store query — one org-source fetch — per call, exactly as it did with two seams. An entry the
15
+ * personal lane never claims (an allow, a deny) is evicted first-in-first-out past a small bound; an
16
+ * evicted or never-handed entry simply costs the personal lane its own read. The handoff is a cost
17
+ * device, not a semantic one: both lanes judge one snapshot, which is the honest shape for one decision.
18
+ *
19
+ * ## Where the lanes do NOT go
20
+ *
21
+ * Neither is a `ToolPolicy`, and neither joins `policyLayers`: inside the fold an allow is the lowest
22
+ * position (a permanent no-op) and a layer's mere presence flips `hasEffectAwareGate`, which would
23
+ * silence the ungated-write warning for deployments that wired no policy at all.
24
+ */
25
+ import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
26
+ import type { OrgGateVerdict, PersistedRuleAnswer, PersistedRuleHit } from "../hooks.js";
27
+ import type { PersistedAllowRule } from "../permission-rule-model.js";
28
+ import { type OrgRuleResolution } from "../permission-rule-org.js";
29
+ import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
30
+ /** The one SHELL tool the persisted-rule lane speaks for. */
31
+ export declare const PERSISTED_RULE_TOOL = "Bash";
32
+ /** design/382 §2.5 — the READ tool the directory-rule family speaks for (its one other admission
33
+ * point, the cd-segment notch, lives inside the model's own conjunction/coverage arms). */
34
+ export declare const DIRECTORY_RULE_TOOL = "Read";
35
+ /**
36
+ * design/252 G-2 — the evidence an INHERITED-lane ask carries.
37
+ *
38
+ * The three inherited mint sites present an ANCESTOR policy's own decision, resolved at the ancestor's
39
+ * approver, from inside the policy fold — upstream of the gate's org layer and its rule lane, neither of
40
+ * which has spoken for this call at that point. So the record is stamped rather than omitted (every ask
41
+ * the engine mints carries one, and a consumer never has to tell a missing field from a field that means
42
+ * something), and each half names the accurate absence for THIS deployment: a partition that is not
43
+ * configured at all reads `"not_wired"`, and one that exists but has not adjudicated this ask reads
44
+ * `"not_adjudicated"`. Reporting the second for a deployment that has neither would assert a governance
45
+ * layer that does not exist.
46
+ *
47
+ * FROZEN: one record is handed to every inherited ask on this leg, and `resolveAsk` shallow-spreads the
48
+ * request — so an `onAsk` consumer holding `req.ruleEvidence` would otherwise be able to edit what the
49
+ * next ask on this leg reports.
50
+ */
51
+ export declare function inheritedAskRuleEvidence(provider: PermissionRuleStoreProvider | undefined): AskRuleEvidence;
52
+ /**
53
+ * design/252 G-2 — the snapshot REVISION an org answer was read off, screened before it becomes
54
+ * evidence.
55
+ *
56
+ * #123 loud-bad-value: the org partition's own install and serving gates screen a snapshot's revision,
57
+ * so a healthy in-tree partition cannot reach here with junk; a FOREIGN store (the interface is public)
58
+ * can. A non-finite value is refused rather than carried — an evidence field is worthless if `NaN` can
59
+ * wear the same shape as revision 41 — and the defect is announced through `onDefect` instead of quietly
60
+ * becoming "this deployment reported no revision".
61
+ *
62
+ * The ADJUDICATION is untouched either way: the caller's verdict is the whole decision, and a
63
+ * defective ATTRIBUTION must never change what a governance layer decided.
64
+ */
65
+ export declare function orgRevisionEvidenceOf(resolution: OrgRuleResolution, onDefect: (message: string) => void): {
66
+ revision?: number;
67
+ };
68
+ /**
69
+ * design/252 G-2 / design/375 — project the admitting COVERAGE SET onto the gate's rule-lane answer:
70
+ * each member's canonical text plus its own add DOTS. A single rule admitting the whole command is a
71
+ * one-element set; a compound admitted by the per-segment conjunction is the set of covering rules in
72
+ * segment order (the model's own reporting-order contract).
73
+ *
74
+ * `adds` on a listed rule are the LIVE adds — the store's read already dropped tombstoned dots — so
75
+ * this is the set that actually admitted the call, which is the identity a later audit reconciles a
76
+ * shadowed ask against. The sanitized display text deliberately is not one. Copied rather than
77
+ * aliased: the evidence must not change under a store that reuses its row objects.
78
+ */
79
+ export declare function persistedRuleHitOf(admitting: readonly PersistedAllowRule[] | undefined): PersistedRuleHit | undefined;
80
+ /**
81
+ * design/382 §2.5-1 — the persisted-rule lane's READ arm: the call's target path in the SAME lexical
82
+ * identity the rule family is defined over — absolute as spelled, relative resolved against the LIVE
83
+ * tracked working directory (the very base the Read tool itself resolves against; task root when no
84
+ * tracker moved), then normalized lexically, zero IO. One admission predicate
85
+ * ({@link directoryRuleAdmits}), the one eligibility predicate for scope/liveness, first table-order hit
86
+ * reported. Everything this lane does NOT do is structural: it clears ASKS only — the read fences
87
+ * (workspace containment, the sensitive-read deny face) judge inside the tool exactly as they would with
88
+ * no rule anywhere, and an unresolvable path is simply no admission (fail toward asking).
89
+ */
90
+ export declare function directoryRuleLaneAnswer(table: readonly PersistedAllowRule[], args: unknown, ctx: {
91
+ root: string | undefined;
92
+ sessionId: string | undefined;
93
+ liveCwd: string | undefined;
94
+ }): PersistedRuleHit | undefined;
95
+ /** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so
96
+ * a deployment without one keeps a decision path byte-identical to a build without the feature. */
97
+ export interface PermissionRuleLanes {
98
+ personal: {
99
+ admits: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
100
+ } | undefined;
101
+ org: {
102
+ adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
103
+ } | undefined;
104
+ }
105
+ /**
106
+ * Build the gate's two permission-rule lanes over the unified store, resolved once per task.
107
+ *
108
+ * design/182 §4.5 (F-011) — the LOCAL-OWNER declaration is judged here, at prepare. It is a declaration,
109
+ * so one that cannot be honored is refused LOUDLY rather than degrading into "this deployment's standing
110
+ * approvals silently stopped applying" (the §7.4 org bit's rule, applied to the availability direction:
111
+ * the failure is deterministic and belongs at wiring time).
112
+ */
113
+ export declare function createPermissionRuleLanes(cfg: {
114
+ provider: PermissionRuleStoreProvider | undefined;
115
+ localOwnerDeclared: boolean;
116
+ principal: string | undefined;
117
+ sessionId: string;
118
+ /** The isolation-aware task root — the same value the file fence, the prompt cwd and the session rules
119
+ * use, so a project-scoped rule agrees with every other place that says "this project". */
120
+ root: string;
121
+ /** The LIVE tracked working directory (adversarial-review P1): the base a relative cd resolves against
122
+ * after an earlier observable `cd`; read per call. */
123
+ liveCwd: () => string | undefined;
124
+ /** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */
125
+ questionToolMounted: boolean;
126
+ questionToolName: string;
127
+ onRevisionDefect: (message: string) => void;
128
+ /** Sink for the store's disclosure lines (a session partition that could not be read, rows it served
129
+ * that were dropped) and for a durable read failure — each read's lines are delivered once, whichever
130
+ * lane took the read. */
131
+ onDisclosure: (message: string) => void;
132
+ }): PermissionRuleLanes;
@@ -0,0 +1,140 @@
1
+ import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf } from "../permission-rule-model.js";
2
+ import { orgRuleVerdictFor } from "../permission-rule-org.js";
3
+ export const PERSISTED_RULE_TOOL = "Bash";
4
+ export const DIRECTORY_RULE_TOOL = "Read";
5
+ const HANDOFF_BOUND = 256;
6
+ export function inheritedAskRuleEvidence(provider) {
7
+ const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
8
+ const personal = provider?.partitions.durable === true ? "not_adjudicated" : "not_wired";
9
+ return Object.freeze({ orgRevisionAbsent: org, orgRuleAbsent: org, personalRuleDotsAbsent: personal });
10
+ }
11
+ export function orgRevisionEvidenceOf(resolution, onDefect) {
12
+ const reported = resolution.revision;
13
+ if (reported === undefined)
14
+ return {};
15
+ if (typeof reported === "number" && Number.isFinite(reported))
16
+ return { revision: reported };
17
+ onDefect(`the org rule partition reported revision ${JSON.stringify(reported)} — a snapshot revision is a finite number; ` +
18
+ `the adjudication stands, but the ask carries no revision evidence for this call`);
19
+ return {};
20
+ }
21
+ export function persistedRuleHitOf(admitting) {
22
+ return admitting === undefined
23
+ ? undefined
24
+ : { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
25
+ }
26
+ export function directoryRuleLaneAnswer(table, args, ctx) {
27
+ const filePath = args?.file_path;
28
+ if (typeof filePath !== "string" || filePath === "")
29
+ return undefined;
30
+ const spelled = filePath.startsWith("/") ? filePath : `${(ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "")}/${filePath}`;
31
+ const target = lexicalNormalAbsolutePathOf(spelled);
32
+ if (target === undefined)
33
+ return undefined;
34
+ const hit = table.find((r) => eligiblePersisted(r, { tool: DIRECTORY_RULE_TOOL, cwd: ctx.root, sessionId: ctx.sessionId }) && directoryRuleAdmits(r, target));
35
+ return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
36
+ }
37
+ export function createPermissionRuleLanes(cfg) {
38
+ const provider = cfg.provider;
39
+ if (cfg.localOwnerDeclared) {
40
+ if (provider === undefined || !provider.partitions.durable) {
41
+ throw new Error("RunnerDeps.localOwnerRules is declared but no durable permission-rule partition is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
42
+ }
43
+ if (provider.forLocalOwner === undefined) {
44
+ throw new Error("RunnerDeps.localOwnerRules is declared but the wired permission-rule provider implements no forLocalOwner() face — a provider without a local-owner bucket cannot honor the declaration; refusing rather than silently resolving zero rules");
45
+ }
46
+ }
47
+ if (provider === undefined)
48
+ return { personal: undefined, org: undefined };
49
+ const anonymous = cfg.principal === undefined || cfg.principal === "";
50
+ const store = anonymous && cfg.localOwnerDeclared
51
+ ?
52
+ provider.forLocalOwner()
53
+ : provider.forPrincipal(anonymous ? undefined : cfg.principal);
54
+ const handed = new Map();
55
+ const read = () => store.effective({ sessionId: cfg.sessionId }).then((view) => {
56
+ for (const line of view.disclosures)
57
+ cfg.onDisclosure(line);
58
+ return view;
59
+ });
60
+ const hand = (toolCallId) => {
61
+ const p = read();
62
+ handed.delete(toolCallId);
63
+ handed.set(toolCallId, p);
64
+ if (handed.size > HANDOFF_BOUND) {
65
+ const oldest = handed.keys().next().value;
66
+ if (oldest !== undefined)
67
+ handed.delete(oldest);
68
+ }
69
+ p.catch(() => undefined);
70
+ return p;
71
+ };
72
+ const claim = (toolCallId) => {
73
+ const p = handed.get(toolCallId);
74
+ if (p === undefined)
75
+ return read();
76
+ handed.delete(toolCallId);
77
+ return p;
78
+ };
79
+ const org = provider.partitions.org
80
+ ? {
81
+ adjudicate: async (req) => {
82
+ if (req.toolName === cfg.questionToolName && cfg.questionToolMounted)
83
+ return { status: "available" };
84
+ let resolution;
85
+ try {
86
+ resolution = (await hand(req.toolCallId)).org;
87
+ }
88
+ catch (err) {
89
+ return { status: "unavailable", disclosures: [`the permission-rule store threw while resolving the org partition: ${err instanceof Error ? err.message : String(err)}`] };
90
+ }
91
+ if (resolution.status === "unavailable" || resolution.status === "ungoverned") {
92
+ return { status: "unavailable", disclosures: resolution.status === "ungoverned" ? ["the store reports no org partition while the provider declares one — unreadable governance, never an empty one"] : resolution.disclosures };
93
+ }
94
+ const revisionCell = orgRevisionEvidenceOf(resolution, cfg.onRevisionDefect);
95
+ const command = req.args?.command;
96
+ if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
97
+ return { status: "available", ...revisionCell };
98
+ const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
99
+ return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
100
+ },
101
+ }
102
+ : undefined;
103
+ const personal = provider.partitions.durable
104
+ ? {
105
+ admits: async (req) => {
106
+ if (anonymous && !cfg.localOwnerDeclared)
107
+ return undefined;
108
+ if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
109
+ return undefined;
110
+ let view;
111
+ try {
112
+ view = await claim(req.toolCallId);
113
+ }
114
+ catch (err) {
115
+ cfg.onDisclosure(err instanceof Error ? err.message : String(err));
116
+ return { unreadable: true };
117
+ }
118
+ if (view.unreadable !== undefined) {
119
+ cfg.onDisclosure(view.unreadable);
120
+ return { unreadable: true };
121
+ }
122
+ const table = view.rules;
123
+ const liveCwd = cfg.liveCwd();
124
+ if (req.toolName === DIRECTORY_RULE_TOOL) {
125
+ return directoryRuleLaneAnswer(table, req.args, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
126
+ }
127
+ const command = req.args?.command;
128
+ if (typeof command !== "string")
129
+ return undefined;
130
+ const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
131
+ const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
132
+ if (admitting !== undefined)
133
+ return persistedRuleHitOf(admitting);
134
+ const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
135
+ return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
136
+ },
137
+ }
138
+ : undefined;
139
+ return { personal, org };
140
+ }