@stigmer/runner 3.12.4 → 3.12.6

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 (180) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent.js +19 -6
  3. package/dist/activities/call-agent.js.map +1 -1
  4. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  5. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  6. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  7. package/dist/activities/execute-cursor/error-classifier.d.ts +9 -0
  8. package/dist/activities/execute-cursor/error-classifier.js +30 -1
  9. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  10. package/dist/activities/execute-cursor/index.d.ts +10 -1
  11. package/dist/activities/execute-cursor/index.js +62 -36
  12. package/dist/activities/execute-cursor/index.js.map +1 -1
  13. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  14. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  15. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  16. package/dist/activities/execute-cursor/service-tier.d.ts +38 -29
  17. package/dist/activities/execute-cursor/service-tier.js +92 -63
  18. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  19. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  20. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  21. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  22. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/usage-accumulator.d.ts +16 -2
  24. package/dist/activities/execute-cursor/usage-accumulator.js +12 -2
  25. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  27. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  28. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  30. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  31. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/index.js +57 -5
  33. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  34. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  35. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  37. package/dist/activities/execute-deep-agent/setup.js +17 -16
  38. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  40. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/middleware/path-normalization.d.ts +49 -49
  45. package/dist/middleware/path-normalization.js +73 -84
  46. package/dist/middleware/path-normalization.js.map +1 -1
  47. package/dist/middleware/types.d.ts +3 -3
  48. package/dist/runner-manager.d.ts +1 -1
  49. package/dist/runner-manager.js +22 -15
  50. package/dist/runner-manager.js.map +1 -1
  51. package/dist/runner.js +11 -0
  52. package/dist/runner.js.map +1 -1
  53. package/dist/shared/args-preview.d.ts +8 -0
  54. package/dist/shared/args-preview.js +14 -3
  55. package/dist/shared/args-preview.js.map +1 -1
  56. package/dist/shared/artifact-storage.d.ts +11 -1
  57. package/dist/shared/artifact-storage.js +10 -1
  58. package/dist/shared/artifact-storage.js.map +1 -1
  59. package/dist/shared/caller-identity.d.ts +10 -7
  60. package/dist/shared/caller-identity.js +10 -7
  61. package/dist/shared/caller-identity.js.map +1 -1
  62. package/dist/shared/declared-preferences.d.ts +47 -0
  63. package/dist/shared/declared-preferences.js +64 -0
  64. package/dist/shared/declared-preferences.js.map +1 -0
  65. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  66. package/dist/shared/mcp-enabled-tools.js +2 -3
  67. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  68. package/dist/shared/mcp-resolver.d.ts +20 -15
  69. package/dist/shared/mcp-resolver.js +11 -12
  70. package/dist/shared/mcp-resolver.js.map +1 -1
  71. package/dist/shared/placeholder-resolver.d.ts +9 -2
  72. package/dist/shared/placeholder-resolver.js +9 -2
  73. package/dist/shared/placeholder-resolver.js.map +1 -1
  74. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  75. package/dist/shared/plan-mode-permissions.js +27 -55
  76. package/dist/shared/plan-mode-permissions.js.map +1 -1
  77. package/dist/shared/skill-mount.d.ts +89 -0
  78. package/dist/shared/skill-mount.js +142 -0
  79. package/dist/shared/skill-mount.js.map +1 -0
  80. package/dist/shared/skill-writer.d.ts +28 -26
  81. package/dist/shared/skill-writer.js +79 -102
  82. package/dist/shared/skill-writer.js.map +1 -1
  83. package/dist/shared/thinking-mode.d.ts +35 -0
  84. package/dist/shared/thinking-mode.js +43 -0
  85. package/dist/shared/thinking-mode.js.map +1 -0
  86. package/dist/shared/worker-shutdown.d.ts +67 -0
  87. package/dist/shared/worker-shutdown.js +79 -0
  88. package/dist/shared/worker-shutdown.js.map +1 -0
  89. package/dist/shared/workspace/types.d.ts +3 -2
  90. package/dist/workflow-engine/loader.js +82 -6
  91. package/dist/workflow-engine/loader.js.map +1 -1
  92. package/dist/workflow-engine/tasks/human-input.d.ts +2 -1
  93. package/dist/workflow-engine/tasks/human-input.js +31 -3
  94. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  95. package/dist/workflow-engine/types.d.ts +17 -3
  96. package/dist/workflow-engine/types.js.map +1 -1
  97. package/dist/workflows/human-input-orchestrator.d.ts +4 -0
  98. package/dist/workflows/human-input-orchestrator.js +13 -0
  99. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  100. package/package.json +15 -11
  101. package/src/activities/call-agent.ts +20 -6
  102. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  103. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  104. package/src/activities/execute-cursor/__tests__/error-classifier-billing.test.ts +67 -0
  105. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +67 -7
  106. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  107. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +33 -1
  108. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  109. package/src/activities/execute-cursor/error-classifier.ts +42 -1
  110. package/src/activities/execute-cursor/index.ts +78 -41
  111. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  112. package/src/activities/execute-cursor/service-tier.ts +94 -63
  113. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  114. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  115. package/src/activities/execute-cursor/usage-accumulator.ts +11 -1
  116. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  117. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  118. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  119. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  120. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  121. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  122. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  123. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  124. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  125. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  126. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  127. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  128. package/src/activities/execute-deep-agent/index.ts +57 -5
  129. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  130. package/src/activities/execute-deep-agent/setup.ts +21 -19
  131. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  132. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  133. package/src/index.ts +1 -1
  134. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  135. package/src/middleware/path-normalization.ts +78 -90
  136. package/src/middleware/types.ts +3 -3
  137. package/src/runner-manager.ts +26 -16
  138. package/src/runner.ts +14 -0
  139. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  140. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  141. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  142. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  143. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  144. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  145. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  146. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  147. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  148. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  149. package/src/shared/args-preview.ts +17 -3
  150. package/src/shared/artifact-storage.ts +20 -1
  151. package/src/shared/caller-identity.ts +10 -7
  152. package/src/shared/declared-preferences.ts +84 -0
  153. package/src/shared/mcp-enabled-tools.ts +2 -3
  154. package/src/shared/mcp-resolver.ts +20 -20
  155. package/src/shared/placeholder-resolver.ts +9 -2
  156. package/src/shared/plan-mode-permissions.ts +27 -58
  157. package/src/shared/skill-mount.ts +179 -0
  158. package/src/shared/skill-writer.ts +96 -130
  159. package/src/shared/thinking-mode.ts +52 -0
  160. package/src/shared/worker-shutdown.ts +99 -0
  161. package/src/shared/workspace/types.ts +3 -2
  162. package/src/workflow-engine/__tests__/loader.test.ts +95 -0
  163. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +156 -0
  164. package/src/workflow-engine/loader.ts +97 -6
  165. package/src/workflow-engine/tasks/human-input.ts +41 -6
  166. package/src/workflow-engine/types.ts +18 -3
  167. package/src/workflows/human-input-orchestrator.ts +20 -2
  168. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  169. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  170. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  171. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  172. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  173. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  174. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  175. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  176. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  177. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  178. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  179. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  180. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -25,8 +25,25 @@
25
25
  * records its before-bytes at authorization time instead (`captureDeleteBefore`,
26
26
  * issue #303). A delete via shell (`rm`) stays on the approval gate as always.
27
27
  *
28
+ * VIRTUAL ROOT — THE ONE PATH DIALECT (issue #754)
29
+ * ------------------------------------------------
30
+ * Every backend here is constructed with `virtualMode: true`: a leading "/"
31
+ * denotes the WORKSPACE ROOT, traversal (`..`, `~`) is rejected, and every
32
+ * resolution stays inside `rootDir` by construction. This is the same dialect
33
+ * the rest of the harness already speaks — the system prompt's path-resolution
34
+ * directive, `resolveWorkspacePath(..., virtualRoot=true)` in the CAS observer
35
+ * and turn-boundary capture, `InlinePublisher.normalizePath`, and the approval
36
+ * gate's capturability checks. Before this flag, deepagents' legacy default
37
+ * passed absolute paths through to the REAL filesystem: a `write_file("/tmp/x")`
38
+ * escaped the session workspace onto the host AND escaped review entirely
39
+ * (auto-approved as capture-mode flow, but the observer/boundary/publisher all
40
+ * looked inside the workspace and found nothing). Read-side, the same legacy
41
+ * pass-through was why plan mode needed rule-based read fencing at all. Do not
42
+ * remove this flag: workspace confinement is structural, not policy.
43
+ *
28
44
  * @since File-Change HITL Redesign (Phase 3 — CAS deep-agent wiring); sub-agent
29
- * gitignored capture parity (Session 26, DD-19); shell restore (issue #248)
45
+ * gitignored capture parity (Session 26, DD-19); shell restore (issue #248);
46
+ * virtual-root confinement (issue #754)
30
47
  */
31
48
 
32
49
  import { FilesystemBackend, LocalShellBackend } from "deepagents";
@@ -111,12 +128,12 @@ export async function createCasCaptureBackend(
111
128
 
112
129
  if (shellEnv !== undefined) {
113
130
  const backend = new CasCaptureShellBackend(
114
- { rootDir, env: shellEnv },
131
+ { rootDir, virtualMode: true, env: shellEnv },
115
132
  { observer },
116
133
  );
117
134
  await backend.initialize();
118
135
  return backend;
119
136
  }
120
137
 
121
- return new CasCaptureFilesystemBackend({ rootDir }, { observer });
138
+ return new CasCaptureFilesystemBackend({ rootDir, virtualMode: true }, { observer });
122
139
  }
@@ -78,10 +78,33 @@ import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stam
78
78
  import { deriveTurnCommandProvenance } from "./command-provenance.js";
79
79
  import { describeExecutionError } from "../../shared/model-error.js";
80
80
  import { inferProvider, type LlmProvider } from "../../shared/llm-proxy.js";
81
+ import { getShutdownSignalForQueue } from "../../shared/worker-shutdown.js";
81
82
 
82
83
  /** The harness id stamped on the deep-agent's file-review ledger events. */
83
84
  const DEEP_AGENT_HARNESS_ID = "deep-agent";
84
85
 
86
+ /**
87
+ * The worker-shutdown terminal status (#776). A shutdown is not a pause: the
88
+ * execution will not resume on this worker, so persisting PAUSED would strand
89
+ * the user with a lie. The copy is byte-identical to the Cursor harness's
90
+ * worker-shutdown branch so every downstream status.error consumer (the Go and
91
+ * Java workflow fallbacks, the cloud channel decision table) keys on ONE shape.
92
+ */
93
+ function buildWorkerShutdownStatus(): AgentExecutionStatus {
94
+ return create(AgentExecutionStatusSchema, {
95
+ phase: ExecutionPhase.EXECUTION_FAILED,
96
+ error: "Execution interrupted: runner worker was shut down. Retry or resume.",
97
+ completedAt: utcTimestamp(),
98
+ messages: [
99
+ create(AgentMessageSchema, {
100
+ type: MessageType.MESSAGE_SYSTEM,
101
+ content: "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
102
+ timestamp: utcTimestamp(),
103
+ }),
104
+ ],
105
+ });
106
+ }
107
+
85
108
  /**
86
109
  * Best-effort provider inference for error-message wording. inferProvider
87
110
  * throws on unrecognized names; an error path must never throw over a label.
@@ -115,6 +138,11 @@ export function createDeepAgentActivities(config: Config) {
115
138
  ): Promise<unknown> => {
116
139
  const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
117
140
  activityStarted();
141
+ // Worker-shutdown classification channel (#776): the pause paths below
142
+ // consult this signal to distinguish "my worker is draining" (SIGTERM,
143
+ // desktop quit) from "the orchestrator cancelled me" (a real user
144
+ // pause). See shared/worker-shutdown.ts for the ownership contract.
145
+ const shutdownSignal = getShutdownSignalForQueue(Context.current().info.taskQueue);
118
146
  let setup: SetupResult | null = null;
119
147
  // Exclusive turn lock on the workspace working tree — held across the
120
148
  // entire tree-mutating window (decision reconcile, agent writes,
@@ -567,6 +595,13 @@ export function createDeepAgentActivities(config: Config) {
567
595
 
568
596
  if (result.terminalStatus) {
569
597
  if (initialStatus.phase === ExecutionPhase.EXECUTION_PAUSED) {
598
+ // The stream loop marks PAUSED for ANY platform cancellation; a
599
+ // worker shutdown is not a pause and must not persist as one (#776).
600
+ if (shutdownSignal?.aborted) {
601
+ console.log(`[ExecuteDeepAgent] Cancelled (worker shutdown) for execution ${executionId}: events=${result.eventsProcessed}`);
602
+ await persistStatus(client, executionId, buildWorkerShutdownStatus(), { offload: statusOffload }).catch(() => {});
603
+ throw new CancelledFailure("Activity cancelled (worker shutdown, not user pause)");
604
+ }
570
605
  await persistStatus(client, executionId, initialStatus, { offload: statusOffload });
571
606
  console.log(`[ExecuteDeepAgent] Paused for execution ${executionId}: events=${result.eventsProcessed}`);
572
607
  throw new CancelledFailure("Activity paused by orchestrator");
@@ -617,15 +652,20 @@ export function createDeepAgentActivities(config: Config) {
617
652
  policyEngineVersion: intr.policySource ? POLICY_ENGINE_VERSION : "",
618
653
  });
619
654
 
620
- // Capture a sanitized args preview while the graph is paused, so the
621
- // approval UI renders the proposed change before the tool runs. Args
622
- // are correlated from the AI-message tool call in graph state (the
623
- // single source of truth).
624
- const { argsPreview } = captureApprovalArtifacts({
655
+ // Capture a sanitized args preview AND the redacted args object
656
+ // while the graph is paused, so the approval UI renders the
657
+ // proposed change before the tool runs and the row header can
658
+ // extract its filename-first path from `args` like every other
659
+ // row (issue #754: a placeholder without args rendered a
660
+ // pathless "Write" header). Args are correlated from the
661
+ // AI-message tool call in graph state (the single source of
662
+ // truth).
663
+ const { argsPreview, args } = captureApprovalArtifacts({
625
664
  toolCallId: intr.toolCallId,
626
665
  messages: aiMessages,
627
666
  });
628
667
  if (argsPreview) toolCall.argsPreview = argsPreview;
668
+ if (args) toolCall.args = args as typeof toolCall.args;
629
669
 
630
670
  aiMsg.toolCalls.push(toolCall);
631
671
  }
@@ -749,6 +789,13 @@ export function createDeepAgentActivities(config: Config) {
749
789
 
750
790
  } catch (err: unknown) {
751
791
  if (err instanceof CancelledFailure) {
792
+ // Worker shutdown is infrastructure failure, not pause — the caught
793
+ // CancelledFailure is itself the interruption evidence (#776).
794
+ if (shutdownSignal?.aborted) {
795
+ console.log(`[ExecuteDeepAgent] Cancelled (worker shutdown) for execution ${executionId}`);
796
+ await persistStatus(client, executionId, buildWorkerShutdownStatus()).catch(() => {});
797
+ throw err;
798
+ }
752
799
  console.log(`[ExecuteDeepAgent] Cancelled (pause) for execution ${executionId}`);
753
800
  const pausedStatus = create(AgentExecutionStatusSchema, {
754
801
  phase: ExecutionPhase.EXECUTION_PAUSED,
@@ -758,6 +805,11 @@ export function createDeepAgentActivities(config: Config) {
758
805
  }
759
806
 
760
807
  if (Context.current().cancellationSignal.aborted) {
808
+ if (shutdownSignal?.aborted) {
809
+ console.log(`[ExecuteDeepAgent] Error during worker-shutdown cancellation for ${executionId}: ${err}`);
810
+ await persistStatus(client, executionId, buildWorkerShutdownStatus()).catch(() => {});
811
+ throw new CancelledFailure("Activity cancelled (worker shutdown, not user pause)");
812
+ }
761
813
  console.log(`[ExecuteDeepAgent] Error during cancellation for ${executionId}, treating as pause: ${err}`);
762
814
  const pausedStatus = create(AgentExecutionStatusSchema, {
763
815
  phase: ExecutionPhase.EXECUTION_PAUSED,
@@ -17,6 +17,10 @@ import {
17
17
  type SenderIdentity,
18
18
  } from "../../shared/sender-identity.js";
19
19
  import { formatSessionContextText } from "../../shared/session-context.js";
20
+ import {
21
+ formatDeclaredPreferencesText,
22
+ type DeclaredPreferencesContent,
23
+ } from "../../shared/declared-preferences.js";
20
24
  import {
21
25
  visionDisclosureLines,
22
26
  type NotViewableEntry,
@@ -166,6 +170,14 @@ export interface PromptBuilderInput {
166
170
  * context is standing session state, like skills.
167
171
  */
168
172
  sessionContext?: string;
173
+ /**
174
+ * Platform-declared standing preferences (stigmer/stigmer#293): the org's
175
+ * and user's standing context, server-snapshotted onto the execution
176
+ * spec's `declared_preferences` at create. Injected on EVERY turn like
177
+ * the bridge — the native system prompt is rebuilt per invocation, so an
178
+ * edited preference reaches the very next turn.
179
+ */
180
+ declaredPreferences?: DeclaredPreferencesContent;
169
181
  }
170
182
 
171
183
  // The prompt renders the injector's own result type — a local structural twin
@@ -229,6 +241,16 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
229
241
  formatSenderIdentityText(input.senderIdentity);
230
242
  }
231
243
 
244
+ // Platform-declared standing facts precede embedder-supplied context
245
+ // (DD-002 D3): both are standing background, but the declared preferences
246
+ // are platform-authored while session context is the embedder's overlay —
247
+ // the more specific overlay reads later and naturally refines.
248
+ if (input.declaredPreferences) {
249
+ prompt +=
250
+ "\n\n## Declared preferences\n\n" +
251
+ formatDeclaredPreferencesText(input.declaredPreferences);
252
+ }
253
+
232
254
  // Standing facts about the user (session context) come before the
233
255
  // carried conversation (bridge): the bridge may refer back to them.
234
256
  if (input.sessionContext) {
@@ -32,6 +32,7 @@ import {
32
32
  resolveCallerIdentity,
33
33
  } from "../../shared/caller-identity.js";
34
34
  import { readSessionContext } from "../../shared/session-context.js";
35
+ import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
35
36
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
36
37
  import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
37
38
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
@@ -88,12 +89,9 @@ import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
88
89
  import {
89
90
  mergeSkillRefs,
90
91
  fetchSkillsByRefs,
91
- writeSkills,
92
- computeSkillPaths,
93
- checkSkillIntegrity,
92
+ mountSkills,
94
93
  generatePromptSection,
95
94
  generateAlsoAvailableSection,
96
- fetchSkillArtifacts,
97
95
  } from "../../shared/skill-writer.js";
98
96
  import { filterSkills, SKILL_COUNT_THRESHOLD } from "../../shared/skill-relevance.js";
99
97
  import { injectAttachments } from "./attachment-injector.js";
@@ -478,8 +476,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
478
476
  const skills = await fetchSkillsByRefs(client, skillRefs);
479
477
 
480
478
  if (skills.length > 0) {
481
- const artifacts = await fetchSkillArtifacts(client, skills);
482
- const { paths: skillPaths } = await writeSkills(skills, workspaceBackend, artifacts);
479
+ // platformDir is an invariant of this path: provisionWorkspace below
480
+ // threads ensurePlatformDir into every backend it constructs.
481
+ const { paths: skillPaths } = await mountSkills(
482
+ client, skills, workspaceBackend.platformDir!,
483
+ );
483
484
 
484
485
  const userMessage = execution.spec!.message || "";
485
486
  const skillNames = skills.map(s => s.spec?.name || s.metadata?.slug || "unknown");
@@ -573,6 +574,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
573
574
  contextBridge: readContextBridge(session.spec!.metadata),
574
575
  senderIdentity: readSenderIdentity(session.spec!.metadata),
575
576
  sessionContext: readSessionContext(session.spec!.metadata),
577
+ declaredPreferences: readDeclaredPreferences(
578
+ execution.spec!.declaredPreferences,
579
+ ),
576
580
  });
577
581
 
578
582
  // Step 9: Construct the LLM model. Resolution to the provider API id
@@ -606,16 +610,13 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
606
610
  const execConfig = execution.spec!.executionConfig;
607
611
  const isPlanMode = execConfig?.interactionMode === InteractionMode.PLAN;
608
612
  const shellEnv = isPlanMode ? undefined : buildShellEnv(envResult.mergedEnvVars);
609
- // Plan mode's filesystem permission rules, hoisted once: the parent graph,
610
- // every sub-agent graph, AND the path-normalization middleware (issue #429)
611
- // all derive from this single value, so a rule-bearing graph can never miss
612
- // the shim that keeps prompt-compliant relative paths from dying in rule
613
- // validation (`path must be absolute`). Built from the same rootDir the
614
- // backends and the shim resolve against, so the workspace read boundary
615
- // (issue #528) and the paths it must admit agree by construction.
616
- const planModePermissions = isPlanMode
617
- ? buildPlanModePermissions(workspaceBackend.rootDir)
618
- : undefined;
613
+ // Plan mode's filesystem permission rules, hoisted once: the parent graph
614
+ // and every sub-agent graph carry this single value. The rules are the
615
+ // write-deny half of plan mode only the read boundary is structural
616
+ // (virtual-rooted backends, issue #754), and the path-normalization shim
617
+ // is no longer tied to the rules: every native graph installs it (see
618
+ // buildMiddlewareStack below) so the whole harness speaks one dialect.
619
+ const planModePermissions = isPlanMode ? buildPlanModePermissions() : undefined;
619
620
 
620
621
  const toolServerMap = new Map<string, string>();
621
622
  if (mcpConnection) {
@@ -729,9 +730,10 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
729
730
  } : null,
730
731
  otelSpans: { toolServerMap },
731
732
  approvalGate: approvalGateConfig,
732
- pathNormalization: planModePermissions
733
- ? { rootDir: workspaceBackend.rootDir }
734
- : null,
733
+ // Every graph, every mode (issue #754): the dialect-repair seam that
734
+ // keeps model-supplied paths canonical for the virtual-rooted backend
735
+ // and every downstream consumer (gate, capture, spans).
736
+ pathNormalization: { rootDir: workspaceBackend.rootDir },
735
737
  });
736
738
  timing.mark("build_middleware");
737
739
 
@@ -42,8 +42,7 @@ import { SubAgentGate } from "../../shared/subagent-gate.js";
42
42
  import { isModelRegistered } from "../../shared/model-registry.js";
43
43
  import {
44
44
  fetchSkillsByRefs,
45
- fetchSkillArtifacts,
46
- writeSkills,
45
+ mountSkills,
47
46
  generatePromptSection,
48
47
  } from "../../shared/skill-writer.js";
49
48
 
@@ -493,6 +492,10 @@ export function resolveSubagentSkillPrompt(
493
492
  * (approval-gated by the sub-agent's own middleware). Absent (plan mode),
494
493
  * the backend is filesystem-only — read-only by construction, matching the
495
494
  * parent.
495
+ *
496
+ * Every variant is virtual-rooted (`virtualMode: true`) like the parent's —
497
+ * see the cas-capture-backend.ts header (issue #754): workspace confinement
498
+ * is structural on every graph, sub-agents included.
496
499
  */
497
500
  async function buildSubagentBackend(opts: {
498
501
  readonly workspaceRootDir: string;
@@ -508,11 +511,12 @@ async function buildSubagentBackend(opts: {
508
511
  }
509
512
 
510
513
  if (opts.shellEnv === undefined) {
511
- return new FilesystemBackend({ rootDir: opts.workspaceRootDir });
514
+ return new FilesystemBackend({ rootDir: opts.workspaceRootDir, virtualMode: true });
512
515
  }
513
516
 
514
517
  const shellBackend = new LocalShellBackend({
515
518
  rootDir: opts.workspaceRootDir,
519
+ virtualMode: true,
516
520
  env: opts.shellEnv,
517
521
  });
518
522
  await shellBackend.initialize();
@@ -554,17 +558,14 @@ export async function compileSubagents(
554
558
  // Structural coupling (DD-19): a sub-agent gate flows gitignored writes into
555
559
  // CAS iff a CAS observer backs that sub-agent's filesystem backend. Deriving
556
560
  // both from the same `casObserver` makes "unobserved unreviewable bytes"
557
- // impossible by construction. Same idiom for path normalization
558
- // (issue #429): derived from the same `permissions` baked into the graph
559
- // below, so a rule-bearing graph always carries the shim that keeps
560
- // prompt-compliant relative paths from dying in rule validation.
561
+ // impossible by construction. Path normalization is unconditional
562
+ // (issue #754): every graph speaks the virtual dialect, so every graph
563
+ // carries the repair seam matching the parent's composition.
561
564
  const middleware = buildSubAgentMiddleware({
562
565
  costCap: opts.costCap,
563
566
  approvalGate: opts.approvalGate,
564
567
  captureIgnored: !!opts.casObserver,
565
- ...(opts.permissions?.length
566
- ? { pathNormalization: { rootDir: opts.workspaceRootDir } }
567
- : {}),
568
+ pathNormalization: { rootDir: opts.workspaceRootDir },
568
569
  });
569
570
 
570
571
  const modelName = spec.model ?? opts.parentModelName;
@@ -679,8 +680,14 @@ export async function transformAndCompileSubagents(
679
680
  const skills = await fetchSkillsByRefs(skillClient, refs);
680
681
 
681
682
  if (skills.length > 0) {
682
- const artifacts = await fetchSkillArtifacts(skillClient, skills);
683
- const { paths: skillPaths } = await writeSkills(skills, workspaceBackend, artifacts);
683
+ // Runs after the parent's setup step 7b mounted its skills, so any
684
+ // skill shared by parent and sub-agent is a cache hit here — the
685
+ // hash-keyed marker turns the old double download into a no-op.
686
+ // platformDir is an invariant: provisionWorkspace threads
687
+ // ensurePlatformDir into every backend it constructs.
688
+ const { paths: skillPaths } = await mountSkills(
689
+ skillClient, skills, workspaceBackend.platformDir!,
690
+ );
684
691
 
685
692
  for (const skill of skills) {
686
693
  const slug = (skill as { metadata?: { slug?: string } }).metadata?.slug;
@@ -75,9 +75,9 @@ export interface SubAgentMiddlewareOptions {
75
75
  */
76
76
  readonly captureIgnored?: boolean;
77
77
  /**
78
- * Workspace-relative path normalization (issue #429). Present iff this
79
- * sub-agent's graph carries filesystem permission rules the caller
80
- * derives it from the same `permissions` value it bakes into the graph.
78
+ * Virtual-dialect path normalization (issues #429/#754). Present on every
79
+ * sub-agent graph the backends are virtual-rooted, so every graph
80
+ * carries the repair seam, matching the parent composition.
81
81
  */
82
82
  readonly pathNormalization?: PathNormalizationConfig;
83
83
  }
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * import { createStigmerRunner } from '@stigmer/runner';
15
15
  *
16
16
  * const runner = await createStigmerRunner({
17
- * taskQueue: 'agent_execution_runner',
17
+ * taskQueue: 'stigmer_runner',
18
18
  * temporalAddress: 'localhost:7233',
19
19
  * stigmerEndpoint: 'http://localhost:7234',
20
20
  * });
@@ -35,36 +35,47 @@ async function argsSeenByHandler(
35
35
  }
36
36
 
37
37
  describe("normalizeWorkspacePathArg", () => {
38
- it("maps a workspace-relative path to its rootDir-joined absolute form", () => {
39
- expect(normalizeWorkspacePathArg("src/main.py", ROOT)).toBe(`${ROOT}/src/main.py`);
38
+ it("maps a workspace-relative path to its virtual-absolute form", () => {
39
+ expect(normalizeWorkspacePathArg("src/main.py", ROOT)).toBe("/src/main.py");
40
40
  });
41
41
 
42
- it("strips a leading ./ before joining", () => {
43
- expect(normalizeWorkspacePathArg("./notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
42
+ it("strips a leading ./ before rooting", () => {
43
+ expect(normalizeWorkspacePathArg("./notes.md", ROOT)).toBe("/notes.md");
44
44
  });
45
45
 
46
46
  it("handles multi-workspace entry-relative paths", () => {
47
47
  expect(normalizeWorkspacePathArg("entry-1/src/main.py", ROOT)).toBe(
48
- `${ROOT}/entry-1/src/main.py`,
48
+ "/entry-1/src/main.py",
49
49
  );
50
50
  });
51
51
 
52
52
  it("resolves interior .. segments that stay inside the root", () => {
53
- expect(normalizeWorkspacePathArg("src/../notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
53
+ expect(normalizeWorkspacePathArg("src/../notes.md", ROOT)).toBe("/notes.md");
54
+ // Same repair on an already-virtual path: the virtual resolver rejects
55
+ // `..` outright, so the safe interior collapse happens at this seam.
56
+ expect(normalizeWorkspacePathArg("/src/../notes.md", ROOT)).toBe("/notes.md");
54
57
  });
55
58
 
56
- it("leaves absolute paths untouched", () => {
59
+ it("maps a REAL-absolute in-root path to its virtual form (legacy-dialect compatibility)", () => {
60
+ expect(normalizeWorkspacePathArg(`${ROOT}/src/main.py`, ROOT)).toBe("/src/main.py");
61
+ // The root itself is the virtual root.
62
+ expect(normalizeWorkspacePathArg(ROOT, ROOT)).toBe("/");
63
+ });
64
+
65
+ it("leaves canonical virtual-absolute paths untouched", () => {
66
+ // In the virtual dialect these are already canonical names — "/etc/hosts"
67
+ // means the WORKSPACE's etc/hosts, not the host file.
57
68
  expect(normalizeWorkspacePathArg("/etc/hosts", ROOT)).toBeUndefined();
58
- expect(normalizeWorkspacePathArg(`${ROOT}/src/main.py`, ROOT)).toBeUndefined();
59
69
  expect(normalizeWorkspacePathArg("/", ROOT)).toBeUndefined();
60
70
  });
61
71
 
62
72
  it("refuses to rewrite relatives that escape the workspace root", () => {
63
- // A naive join would resolve `..` away and hand upstream validation a
64
- // clean out-of-root absolute path an out-of-root read that today's
65
- // validation refuses. The guard keeps the refusal.
73
+ // posix.normalize would silently swallow a leading ".." on an absolute
74
+ // path rewriting the call onto a DIFFERENT in-root file. The escape is
75
+ // detected in relative form and left raw for the upstream refusal.
66
76
  expect(normalizeWorkspacePathArg("../sibling/secret.txt", ROOT)).toBeUndefined();
67
77
  expect(normalizeWorkspacePathArg("src/../../escape.txt", ROOT)).toBeUndefined();
78
+ expect(normalizeWorkspacePathArg("/src/../../escape.txt", ROOT)).toBeUndefined();
68
79
  });
69
80
 
70
81
  it("leaves ~-carrying paths untouched (upstream refuses them either way)", () => {
@@ -84,7 +95,7 @@ describe("createPathNormalizationMiddleware", () => {
84
95
  file_path: "src/main.py",
85
96
  content: "x",
86
97
  });
87
- expect(args.file_path).toBe(`${ROOT}/src/main.py`);
98
+ expect(args.file_path).toBe("/src/main.py");
88
99
  // Sibling args ride along untouched.
89
100
  expect(args.content).toBe("x");
90
101
  }
@@ -93,27 +104,26 @@ describe("createPathNormalizationMiddleware", () => {
93
104
  it("rewrites the base path on ls/glob/grep", async () => {
94
105
  for (const tool of ["ls", "glob", "grep"]) {
95
106
  const args = await argsSeenByHandler(tool, { path: "src", pattern: "**/*.py" });
96
- expect(args.path).toBe(`${ROOT}/src`);
107
+ expect(args.path).toBe("/src");
97
108
  // The pattern is never a path — byte-untouched.
98
109
  expect(args.pattern).toBe("**/*.py");
99
110
  }
100
111
  });
101
112
 
102
- it("passes absolute paths through byte-untouched", async () => {
113
+ it("passes canonical virtual paths through byte-untouched", async () => {
103
114
  const args = await argsSeenByHandler("read_file", { file_path: "/etc/hosts" });
104
115
  expect(args.file_path).toBe("/etc/hosts");
105
116
  });
106
117
 
107
- it("supplies the workspace root when the base path is omitted on ls/glob/grep", async () => {
108
- // Deliberate reversal of the original #429 pin ("leaves an omitted base
109
- // path alone"): the tools' schema default is "/" the OS ROOT under the
110
- // legacy backend applied inside the tool AFTER this seam, so under the
111
- // #528 workspace read boundary an untouched omission would deny the bare
112
- // first listing. The middleware fills the omission with the root it
113
- // already knows.
118
+ it("leaves an omitted base path alone — the tools' '/' default IS the workspace root now", async () => {
119
+ // Deliberate reversal of the #528 injection: under the legacy backend the
120
+ // schema default "/" meant the OS ROOT, so the middleware had to fill the
121
+ // omission. Under the virtual root (issue #754) the default already means
122
+ // the workspace root filling it would just duplicate the tool's own
123
+ // behavior.
114
124
  for (const tool of ["ls", "glob", "grep"]) {
115
125
  const args = await argsSeenByHandler(tool, { pattern: "TOKEN" });
116
- expect(args.path).toBe(ROOT);
126
+ expect(args.path).toBeUndefined();
117
127
  expect(args.pattern).toBe("TOKEN");
118
128
  }
119
129
  });
@@ -125,16 +135,6 @@ describe("createPathNormalizationMiddleware", () => {
125
135
  expect(args.file_path).toBeUndefined();
126
136
  });
127
137
 
128
- it("leaves an explicit '/' untouched on the dir tools — the rules answer it honestly", async () => {
129
- // The model asked for the OS root; under plan mode's read boundary the
130
- // honest answer is the rules' denial, not silently substituted workspace
131
- // contents (the #429 never-convert-a-refusal-into-an-allowance doctrine).
132
- for (const tool of ["ls", "glob", "grep"]) {
133
- const args = await argsSeenByHandler(tool, { path: "/" });
134
- expect(args.path).toBe("/");
135
- }
136
- });
137
-
138
138
  it("never touches tools outside the built-in filesystem set", async () => {
139
139
  // An MCP tool could plausibly carry a same-named arg with different
140
140
  // semantics; only the six built-in names are rewritten.