@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
@@ -4,13 +4,13 @@
4
4
  *
5
5
  * This is NOT a unit test of our code (there is no production vertex code
6
6
  * yet). It pins the exact cross-package behavior production will rely on:
7
- * the REAL `ChatAnthropic` (@langchain/anthropic, bundling @anthropic-ai/sdk
8
- * 0.95.x) driving the REAL `AnthropicVertex` client (@anthropic-ai/vertex-sdk,
9
- * bundling its own nested @anthropic-ai/sdk >=0.115). If a future bump of
7
+ * the REAL `ChatAnthropic` (@langchain/anthropic) driving the REAL
8
+ * `AnthropicVertex` client (@anthropic-ai/vertex-sdk), both resolving the
9
+ * single override-pinned @anthropic-ai/sdk copy. If a future bump of
10
10
  * either side changes request shaping, streaming event handling, tool-call
11
11
  * assembly, or usage accounting across this seam, this suite fails in CI
12
- * instead of production. See scripts/check-langchain-deps.sh for why two
13
- * @anthropic-ai/sdk copies coexist and when they collapse to one.
12
+ * instead of production. See scripts/check-langchain-deps.sh for the
13
+ * override rationale and the single-copy invariant it guards.
14
14
  *
15
15
  * Determinism: zero credentials, zero network. Google auth is bypassed by
16
16
  * injecting a fake `authClient` (the SDK's supported constructor option —
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ classifyTurnInterruption,
4
+ getShutdownSignalForQueue,
5
+ registerWorkerShutdownSignal,
6
+ signalWorkerShutdown,
7
+ unregisterWorkerShutdownSignal,
8
+ } from "../worker-shutdown.js";
9
+
10
+ describe("worker-shutdown signal registry", () => {
11
+ it("registers a live signal an activity can resolve by queue name", () => {
12
+ const controller = registerWorkerShutdownSignal("session:reg-1");
13
+ const signal = getShutdownSignalForQueue("session:reg-1");
14
+ expect(signal).toBe(controller.signal);
15
+ expect(signal?.aborted).toBe(false);
16
+ unregisterWorkerShutdownSignal("session:reg-1");
17
+ });
18
+
19
+ it("aborting via signalWorkerShutdown marks the queue's signal (idempotent, unknown queues are no-ops)", () => {
20
+ registerWorkerShutdownSignal("session:abort-1");
21
+ signalWorkerShutdown("session:abort-1");
22
+ signalWorkerShutdown("session:abort-1");
23
+ expect(getShutdownSignalForQueue("session:abort-1")?.aborted).toBe(true);
24
+ expect(() => signalWorkerShutdown("session:never-registered")).not.toThrow();
25
+ unregisterWorkerShutdownSignal("session:abort-1");
26
+ });
27
+
28
+ it("re-registering a reused queue replaces an aborted signal with a fresh one", () => {
29
+ // A re-created worker on a reused queue (desktop re-opens a session) must
30
+ // not observe the previous worker's aborted signal as its own shutdown.
31
+ const first = registerWorkerShutdownSignal("session:reuse-1");
32
+ first.abort();
33
+ const second = registerWorkerShutdownSignal("session:reuse-1");
34
+ expect(getShutdownSignalForQueue("session:reuse-1")).toBe(second.signal);
35
+ expect(getShutdownSignalForQueue("session:reuse-1")?.aborted).toBe(false);
36
+ unregisterWorkerShutdownSignal("session:reuse-1");
37
+ });
38
+
39
+ it("unregistering removes the signal", () => {
40
+ registerWorkerShutdownSignal("session:gone-1");
41
+ unregisterWorkerShutdownSignal("session:gone-1");
42
+ expect(getShutdownSignalForQueue("session:gone-1")).toBeUndefined();
43
+ });
44
+ });
45
+
46
+ describe("classifyTurnInterruption", () => {
47
+ const none = {
48
+ heartbeatCancelled: false,
49
+ heartbeatWorkerShutdown: false,
50
+ cancellationSignalAborted: false,
51
+ shutdownSignalAborted: false,
52
+ };
53
+
54
+ it("an uninterrupted turn is none", () => {
55
+ expect(classifyTurnInterruption(none)).toBe("none");
56
+ });
57
+
58
+ it("GRACE-WINDOW GUARD (#776): an aborted shutdown signal alone stays none — a run that completed inside the drain grace window must not be failed", () => {
59
+ expect(
60
+ classifyTurnInterruption({ ...none, shutdownSignalAborted: true }),
61
+ ).toBe("none");
62
+ });
63
+
64
+ it("heartbeat cancellation without a shutdown signal is the orchestrator's pause", () => {
65
+ expect(
66
+ classifyTurnInterruption({ ...none, heartbeatCancelled: true }),
67
+ ).toBe("pause");
68
+ });
69
+
70
+ it("heartbeat cancellation WITH the shutdown signal aborted is a worker shutdown, not a pause (the #776 misclassification)", () => {
71
+ expect(
72
+ classifyTurnInterruption({
73
+ ...none,
74
+ heartbeatCancelled: true,
75
+ shutdownSignalAborted: true,
76
+ }),
77
+ ).toBe("worker-shutdown");
78
+ });
79
+
80
+ it("the heartbeat's own workerShutdown flag classifies directly", () => {
81
+ expect(
82
+ classifyTurnInterruption({ ...none, heartbeatWorkerShutdown: true }),
83
+ ).toBe("worker-shutdown");
84
+ });
85
+
86
+ it("a delivered cancellation with the shutdown signal aborted is a worker shutdown (signal races the heartbeat tick)", () => {
87
+ expect(
88
+ classifyTurnInterruption({
89
+ ...none,
90
+ cancellationSignalAborted: true,
91
+ shutdownSignalAborted: true,
92
+ }),
93
+ ).toBe("worker-shutdown");
94
+ });
95
+
96
+ it("a delivered cancellation alone (heartbeat timeout / infra cancel) is neither shutdown nor pause", () => {
97
+ expect(
98
+ classifyTurnInterruption({ ...none, cancellationSignalAborted: true }),
99
+ ).toBe("none");
100
+ });
101
+ });
@@ -34,11 +34,25 @@ export const MAX_ARGS_PREVIEW_LENGTH = 500;
34
34
  * Cursor gate path must instead use {@link buildElidedArgsPreview}, which keeps
35
35
  * the JSON valid and preserves the salient identity fields.
36
36
  */
37
- export function sanitizeArgsPreview(args: Record<string, unknown>): string {
38
- const sanitized: Record<string, unknown> = {};
37
+ /**
38
+ * Redact secret-keyed values (see {@link SENSITIVE_ARG_KEYS}), preserving every
39
+ * other entry verbatim. The shared first step of both preview builders, and the
40
+ * shape stamped as `args` on interrupt-placeholder tool calls (issue #754's
41
+ * header fix): full enough for the UI's path/primary-arg extraction, never
42
+ * carrying a secret value.
43
+ */
44
+ export function redactSensitiveArgs(
45
+ args: Record<string, unknown>,
46
+ ): Record<string, unknown> {
47
+ const redacted: Record<string, unknown> = {};
39
48
  for (const [key, value] of Object.entries(args)) {
40
- sanitized[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
49
+ redacted[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
41
50
  }
51
+ return redacted;
52
+ }
53
+
54
+ export function sanitizeArgsPreview(args: Record<string, unknown>): string {
55
+ const sanitized = redactSensitiveArgs(args);
42
56
  try {
43
57
  const json = JSON.stringify(sanitized);
44
58
  return json.length > MAX_ARGS_PREVIEW_LENGTH
@@ -56,7 +56,17 @@ export interface ArtifactStorage {
56
56
  exists(key: string): Promise<boolean>;
57
57
  }
58
58
 
59
- export type ArtifactStorageType = "local" | "proxy";
59
+ /**
60
+ * "none" is the explicit no-store posture: capture degrades to the deny-gate
61
+ * (file writes gated pre-execution, DD-22/DD-26) and offload is disabled —
62
+ * the same first-class absent-store state a proxy misconfig or unwritable
63
+ * local path resolves to, but chosen deliberately (ARTIFACT_STORAGE_TYPE=none)
64
+ * instead of reached by failure. The e2e file-gate stack boots with it, and
65
+ * an operator can use it to force gate-everything behavior. It is only valid
66
+ * where an absent store is: claimcheck (fail-hard by contract) refuses it at
67
+ * construction.
68
+ */
69
+ export type ArtifactStorageType = "local" | "proxy" | "none";
60
70
 
61
71
  // ── Local Backend ────────────────────────────────────────────────────
62
72
 
@@ -371,6 +381,7 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
371
381
  const type: ArtifactStorageType =
372
382
  envType === "proxy" ? "proxy" :
373
383
  envType === "local" ? "local" :
384
+ envType === "none" ? "none" :
374
385
  config.proxyEndpoint ? "proxy" : "local";
375
386
 
376
387
  return {
@@ -387,6 +398,14 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
387
398
  }
388
399
 
389
400
  export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStorage {
401
+ if (cfg.type === "none") {
402
+ // Deliberate no-store posture. Throwing keeps both consumers honest:
403
+ // resolveUsableArtifactStorage catches and degrades to the deny-gate
404
+ // (the intent of "none"), while claimcheck's direct fail-hard call
405
+ // surfaces the config contradiction at boot instead of silently
406
+ // dropping Temporal payloads.
407
+ throw new Error("ARTIFACT_STORAGE_TYPE=none: artifact storage deliberately disabled");
408
+ }
390
409
  if (cfg.type === "proxy") {
391
410
  if (!cfg.proxyEndpoint) {
392
411
  throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");
@@ -58,15 +58,18 @@ export const ANONYMOUS_KIND = "anonymous";
58
58
 
59
59
  /**
60
60
  * Audit-actor id that backends stamp when NO caller identity exists —
61
- * the OSS server writes it on every create (no local auth), and the
62
- * cloud's AuditActorBuilder falls back to it for caller-less internal
63
- * writes. It names "nobody in particular": unrelated sessions from
64
- * unrelated people all carry it, so presenting it as a caller identity
65
- * would make the one string a grantable value that silently covers ALL
66
- * such traffic in an MCP server's binding sheet. A creator matching this
61
+ * the OSS server writes it on every create unless the deployment
62
+ * configured an operator identity (STIGMER_OPERATOR_EMAIL,
63
+ * stigmer/stigmer#400; a configured install stamps a real actor whose
64
+ * email resolves below like any other), and the cloud's
65
+ * AuditActorBuilder falls back to it for caller-less internal writes.
66
+ * It names "nobody in particular": unrelated sessions from unrelated
67
+ * people all carry it, so presenting it as a caller identity would make
68
+ * the one string a grantable value that silently covers ALL such
69
+ * traffic in an MCP server's binding sheet. A creator matching this
67
70
  * sentinel (and carrying no email) is therefore unresolvable and falls
68
71
  * to anonymous — the deny-by-default the docs guide already promises
69
- * for self-hosted backends.
72
+ * for unconfigured self-hosted backends.
70
73
  */
71
74
  export const SYSTEM_CREATOR_SENTINEL = "system";
72
75
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Declared preferences (stigmer/stigmer#293, DD-002): standing free-text
3
+ * preferences the organization and the calling user declared once — "keep
4
+ * answers terse", "we deploy to us-east-1" — that every eligible execution
5
+ * receives without the user re-typing them.
6
+ *
7
+ * The server composes the CONTENT at execution create: the create pipeline
8
+ * snapshots `Organization.spec.preferences.standing_context` and (cloud, for
9
+ * first-party human callers only) the caller
10
+ * `IdentityAccount.spec.preferences.standing_context` verbatim onto the
11
+ * execution spec's `declared_preferences` field. This module owns the
12
+ * PRESENTATION — the preamble and the per-scope attribution — so the framing
13
+ * cannot drift between harnesses.
14
+ *
15
+ * Like conversation-catchup (its typed-field sibling) there is no metadata
16
+ * key to mirror-guard: the value rides a TYPED proto field, so codegen
17
+ * enforces the cross-repo contract. Degradation is safe by construction: an
18
+ * absent or blank field renders nothing, and a runner predating this module
19
+ * simply ignores it — the agent runs without preferences, exactly the
20
+ * pre-#293 behavior, never worse.
21
+ */
22
+
23
+ import type { DeclaredPreferences } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
24
+
25
+ /**
26
+ * How the preferences are introduced to the model, shared by both harnesses
27
+ * so the behavioral contract cannot drift between them. Same posture as the
28
+ * session-context channel: calibration material, never authority — the task
29
+ * always outranks a standing preference.
30
+ */
31
+ const DECLARED_PREFERENCES_PREAMBLE =
32
+ "Standing preferences declared by the organization and/or the user you " +
33
+ "are assisting. Treat them as background you already know: use them to " +
34
+ "calibrate depth, defaults, and tone. Do not repeat them back, quote " +
35
+ "them, or mention that you received them. They are context, not " +
36
+ "instructions that override your task.";
37
+
38
+ /**
39
+ * The non-blank preference scopes of an execution, in presentation order.
40
+ * At least one scope is set (the read function returns undefined otherwise).
41
+ */
42
+ export interface DeclaredPreferencesContent {
43
+ /** The organization's standing context, trimmed; absent when blank. */
44
+ orgContext?: string;
45
+ /** The calling user's standing context, trimmed; absent when blank. */
46
+ userContext?: string;
47
+ }
48
+
49
+ /**
50
+ * Read the declared preferences from an execution spec's
51
+ * `declared_preferences`. Returns undefined when the field is absent or both
52
+ * scopes are blank — the caller renders no section. Blank-is-absent applies
53
+ * PER SCOPE: an org-only snapshot renders only the org subsection.
54
+ */
55
+ export function readDeclaredPreferences(
56
+ preferences: DeclaredPreferences | undefined,
57
+ ): DeclaredPreferencesContent | undefined {
58
+ const orgContext = preferences?.orgContext?.trim() || undefined;
59
+ const userContext = preferences?.userContext?.trim() || undefined;
60
+ if (!orgContext && !userContext) {
61
+ return undefined;
62
+ }
63
+ return { orgContext, userContext };
64
+ }
65
+
66
+ /**
67
+ * The framed preferences body (preamble + labeled per-scope subsections),
68
+ * ready for section wrapping. Attribution is explicit — the model must know
69
+ * WHO declared what. Organization first, user last: the user's refinement
70
+ * reads last and so naturally wins where the two conflict, without this
71
+ * module encoding any override rule.
72
+ */
73
+ export function formatDeclaredPreferencesText(
74
+ content: DeclaredPreferencesContent,
75
+ ): string {
76
+ const sections: string[] = [DECLARED_PREFERENCES_PREAMBLE];
77
+ if (content.orgContext) {
78
+ sections.push(`Declared by the organization:\n${content.orgContext}`);
79
+ }
80
+ if (content.userContext) {
81
+ sections.push(`Declared by the user:\n${content.userContext}`);
82
+ }
83
+ return sections.join("\n\n");
84
+ }
@@ -7,9 +7,8 @@
7
7
  * - An empty usage list falls back to the server's default_enabled_tools.
8
8
  * - Both empty means NO restriction — every discovered tool is available.
9
9
  *
10
- * Both harnesses import THIS module so they can never drift on these
11
- * semantics (the mcp-transport-guard precedent for logic shared across the
12
- * duplicated resolvers — see the header of shared/mcp-resolver.ts):
10
+ * Resolution is shared (shared/mcp-resolver.ts threads the effective list
11
+ * onto every ResolvedMcpServer), but ENFORCEMENT is per-harness:
13
12
  * - The deep-agent harness filters the discovered toolset before it reaches
14
13
  * the model (shared/mcp-manager.ts connectMcpServers).
15
14
  * - The Cursor harness cannot hide tools (the Cursor SDK's McpServerConfig
@@ -2,13 +2,17 @@
2
2
  * Resolves Stigmer McpServerUsage references into an intermediate
3
3
  * ResolvedMcpServer format.
4
4
  *
5
- * Consumers: the deep-agent harness (execute-deep-agent/setup.ts), the
6
- * connect backfill (shared/connect-backfill.ts), and the discovery
7
- * activity (activities/discover-mcp-server.ts, via mcpServerToResolved).
8
- * NOTE: the Cursor harness does NOT use this module — it has its own
9
- * near-duplicate resolver at activities/execute-cursor/mcp-resolver.ts.
10
- * A behavioral change here (like the transport guard) must be mirrored
11
- * there until the two are consolidated.
5
+ * THE single resolver for both harnesses (oss#387 retired the Cursor
6
+ * harness's near-duplicate): the deep-agent harness
7
+ * (execute-deep-agent/setup.ts), the Cursor harness
8
+ * (execute-cursor/index.ts), the connect backfill
9
+ * (shared/connect-backfill.ts), and the discovery activity
10
+ * (activities/discover-mcp-server.ts, via mcpServerToResolved) all consume
11
+ * it. Each harness maps the result into its SDK format at the last hop:
12
+ * toMcpClientConfig (shared/mcp-manager.ts) for LangChain,
13
+ * toCursorMcpConfig (execute-cursor/cursor-mcp-config.ts) for the Cursor
14
+ * SDK — so a behavioral change here (like the transport guard) lands in
15
+ * both execution paths by construction.
12
16
  */
13
17
 
14
18
  import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
@@ -51,8 +55,11 @@ export interface ResolvedMcpServer {
51
55
  * enabled_tools, falling back to the server's default_enabled_tools when
52
56
  * the usage's list is empty (see shared/mcp-enabled-tools.ts). Absent when
53
57
  * unrestricted — which keeps synthesized attachment servers (built without
54
- * a usage) and the discovery path unfiltered by construction. Enforced at
55
- * connect time by connectMcpServers (shared/mcp-manager.ts).
58
+ * a usage) and the discovery path unfiltered by construction. Enforcement
59
+ * is per-harness: the deep-agent path filters at connect time
60
+ * (connectMcpServers, shared/mcp-manager.ts); the Cursor SDK config cannot
61
+ * hide tools, so that harness denies non-enabled calls in the HITL hook
62
+ * (see hook-script.ts).
56
63
  */
57
64
  enabledTools?: string[];
58
65
  /**
@@ -61,11 +68,10 @@ export interface ResolvedMcpServer {
61
68
  * override to its own server — a flat cross-server list is how an
62
69
  * override once leaked onto (or silently un-gated) a same-named tool on
63
70
  * another server. Deliberately REQUIRED, not optional: empty means "no
64
- * overrides", and every construction site — including the near-duplicate
65
- * Cursor resolver and the synthesized attachments, which have no usage
66
- * and therefore no layer 3 must say so explicitly, so a forgotten
67
- * mirror cannot compile. Consumed by mergeApprovalPolicies
68
- * (shared/approval-policy.ts).
71
+ * overrides", and every construction site — including the synthesized
72
+ * attachments, which have no usage and therefore no layer 3 must say so
73
+ * explicitly, so a forgotten site cannot compile. Consumed by
74
+ * mergeApprovalPolicies (shared/approval-policy.ts).
69
75
  */
70
76
  toolApprovalOverrides: ToolApprovalOverride[];
71
77
  }
@@ -202,12 +208,6 @@ export function mcpServerToResolved(
202
208
  }
203
209
  }
204
210
 
205
- export function extractMcpServerSlugs(usages: McpServerUsage[]): string[] {
206
- return usages
207
- .map((u) => u.mcpServerRef?.slug)
208
- .filter((s): s is string => !!s);
209
- }
210
-
211
211
  /**
212
212
  * Merge MCP server usages from agent (base) and session (overlay).
213
213
  *
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Strict placeholder resolver for ${VAR_NAME} syntax in MCP server configs.
3
3
  *
4
+ * Port of the Python agent-runner's PlaceholderResolver (strict mode only).
4
5
  * Always raises on unresolved placeholders — sending literal ${VAR} as an
5
- * HTTP header value produces cryptic auth failures from the remote server.
6
+ * HTTP header value (e.g. Authorization: Bearer ${API_KEY}) produces
7
+ * cryptic auth failures from the remote server.
6
8
  */
7
9
 
8
10
  const PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
@@ -41,6 +43,9 @@ export function resolvePlaceholders(
41
43
 
42
44
  /**
43
45
  * Resolve placeholders in all values of a headers map.
46
+ *
47
+ * @throws PlaceholderResolutionError if any header value contains an
48
+ * unresolvable placeholder.
44
49
  */
45
50
  export function resolveHeaders(
46
51
  headers: Record<string, string>,
@@ -55,7 +60,9 @@ export function resolveHeaders(
55
60
 
56
61
  /**
57
62
  * Filter env vars to only keys declared in the MCP server's spec.env.
58
- * Prevents secret over-sharing by restricting the environment to
63
+ *
64
+ * Mirrors the agent-runner's _filter_env_to_declared_keys: prevents
65
+ * secret over-sharing by restricting the subprocess/HTTP environment to
59
66
  * explicitly declared variables.
60
67
  */
61
68
  export function filterEnvToDeclaredKeys(
@@ -2,26 +2,25 @@
2
2
  * The Plan-mode filesystem permission rules — the enforcement twin of
3
3
  * `plan-mode-prompt.ts` (which carries the instruction half of the contract).
4
4
  *
5
- * Plan mode is contained BY CONSTRUCTION on the native harness: these rules
6
- * deny every filesystem write everywhere AND scope reads to the session
7
- * workspace (issue #528 owner ruling: the workspace is plan mode's read
8
- * boundary on cloud and desktop runners alike). Without the read boundary,
9
- * model-provided absolute paths reached anywhere the process account could
10
- * read the pod filesystem (including /proc/self/environ) on cloud runners,
11
- * the user's whole home directory on desktop while plan mode still carries
12
- * exfiltration-capable tools (web_fetch, MCP). Rules are first-match-wins
13
- * with a permissive default (deepagents' decidePathAccess), so order is
14
- * load-bearing: the workspace read-allow must precede the read-deny.
5
+ * Plan mode's policy is: reads anywhere in the workspace, writes nowhere.
6
+ * Only the write half needs a RULE. The read boundary that issue #528 built
7
+ * out of rules (workspace read-allow + read-deny, glob-escaped real root) is
8
+ * now STRUCTURAL: every native-harness backend is virtual-rooted
9
+ * (`virtualMode: true` see cas-capture-backend.ts, issue #754), so every
10
+ * path a tool can express resolves inside the workspace root and traversal
11
+ * is rejected at resolution. A rule cannot widen that, and no workspace-root
12
+ * glob is needed to narrow it — which also retires the whole
13
+ * special-characters-in-the-root hazard (`escapeGlobLiteral` and its
14
+ * end-to-end matcher suite) that the real-path rules carried.
15
15
  *
16
- * The workspace-root pattern is matched as a STRING against the raw tool-call
17
- * path (micromatch, dot:true), before the backend touches disk. That is
18
- * exactly why the legitimate out-of-workspace reads keep working: skills,
19
- * attachments, and the approved plan live in the platform dir but are
20
- * addressed through the `{workspace}/.stigmer` symlink (see
21
- * shared/workspace/stigmer-link.ts), so their path strings are in-root even
22
- * though the bytes are not. The same holds for multi-workspace local entries
23
- * (`{workspace}/{name}` symlinks). A realpath-based boundary would break
24
- * both; do not "harden" this into one.
16
+ * Paths in these rules are VIRTUAL: "/" is the workspace root, matched by
17
+ * micromatch against the raw tool-call path (dot:true) after the
18
+ * path-normalization middleware has canonicalized it (relative "/"-rooted;
19
+ * see middleware/path-normalization.ts). The legitimate platform-dir reads
20
+ * (skills, attachments, the approved plan) keep working exactly as before:
21
+ * they are addressed through the `{workspace}/.stigmer` symlink
22
+ * (shared/workspace/stigmer-link.ts), an in-root path string in either
23
+ * dialect.
25
24
  *
26
25
  * Applied in execute-deep-agent/setup.ts to the parent graph AND threaded
27
26
  * into every compiled sub-agent graph: deepagents' parent-permission
@@ -30,13 +29,6 @@
30
29
  * (issue #255). Kept as its own side-effect-free module so tests can pin the
31
30
  * production rules without dragging in setup.ts's import graph.
32
31
  *
33
- * Rules travel with a companion: every graph that carries them also installs
34
- * the path-normalization middleware (middleware/path-normalization.ts,
35
- * issues #429/#528), because deepagents' rule validation refuses
36
- * workspace-relative paths outright and its `ls`/`glob`/`grep` schema default
37
- * of "/" would deny the bare first listing. Both are derived from the same
38
- * expression at each composition site so they cannot drift apart.
39
- *
40
32
  * Invariant: never combine these rules with a shell-capable (sandbox)
41
33
  * backend — deepagents rejects that pairing at graph construction (see the
42
34
  * cas-capture-backend.ts header). Plan mode guarantees it by construction:
@@ -44,41 +36,18 @@
44
36
  * for shell capability on both the parent and sub-agent backends.
45
37
  */
46
38
 
47
- import { resolve } from "node:path";
48
39
  import type { FilesystemPermission } from "deepagents";
49
40
 
50
41
  /**
51
- * Backslash-escape every character micromatch/picomatch treats as glob
52
- * syntax, so the result matches the input literally. micromatch exports no
53
- * escape API of its own, and this is correctness, not caution: an unescaped
54
- * `(` in a desktop project path would make the workspace read-allow rule
55
- * silently never match — bricking every plan-mode read for that workspace.
56
- * Semantics are pinned end-to-end through deepagents' own matcher by the
57
- * special-character workspace suite in plan-mode-path-normalization.test.ts.
58
- */
59
- export function escapeGlobLiteral(literal: string): string {
60
- return literal.replace(/[\\*?()[\]{}!+@]/g, "\\$&");
61
- }
62
-
63
- /**
64
- * Build the plan-mode rule set for a graph whose filesystem backend is
65
- * rooted at `workspaceRootDir`. The three rules read as the policy:
66
- * reads allowed in the workspace, reads denied everywhere else, writes
67
- * denied everywhere.
42
+ * Build the plan-mode rule set. One rule IS the policy: writes denied
43
+ * everywhere ("/**" is every virtual path; deepagents evaluates
44
+ * first-match-wins with a permissive default, so reads stay allowed
45
+ * workspace-confined structurally, not by rule).
68
46
  *
69
- * `{root}/**` matches the root itself as well as its subtree (verified
70
- * against the installed micromatch), so one allow pattern suffices. The
71
- * root is `path.resolve`d first because enforcement canonicalizes incoming
72
- * paths (collapsed slashes, no trailing separator) before matching — a
73
- * trailing slash in the pattern would silently match nothing.
47
+ * Deliberately parameterless: the pre-#754 signature took the workspace root
48
+ * to build a real-path read fence; resurrecting a root-derived rule would
49
+ * silently mismatch the virtual dialect the enforcement now sees.
74
50
  */
75
- export function buildPlanModePermissions(
76
- workspaceRootDir: string,
77
- ): FilesystemPermission[] {
78
- const canonicalRoot = resolve(workspaceRootDir);
79
- return [
80
- { operations: ["read"], paths: [`${escapeGlobLiteral(canonicalRoot)}/**`] },
81
- { operations: ["read"], paths: ["/**"], mode: "deny" },
82
- { operations: ["write"], paths: ["/**"], mode: "deny" },
83
- ];
51
+ export function buildPlanModePermissions(): FilesystemPermission[] {
52
+ return [{ operations: ["write"], paths: ["/**"], mode: "deny" }];
84
53
  }