@stigmer/runner 3.12.4 → 3.12.5
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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
- package/dist/activities/execute-cursor/index.d.ts +10 -1
- package/dist/activities/execute-cursor/index.js +47 -29
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
- package/dist/activities/execute-cursor/skill-resolver.js +5 -108
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +57 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +17 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/path-normalization.d.ts +49 -49
- package/dist/middleware/path-normalization.js +73 -84
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/middleware/types.d.ts +3 -3
- package/dist/runner-manager.d.ts +1 -1
- package/dist/runner-manager.js +22 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/args-preview.d.ts +8 -0
- package/dist/shared/args-preview.js +14 -3
- package/dist/shared/args-preview.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -1
- package/dist/shared/artifact-storage.js +10 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/declared-preferences.d.ts +47 -0
- package/dist/shared/declared-preferences.js +64 -0
- package/dist/shared/declared-preferences.js.map +1 -0
- package/dist/shared/mcp-enabled-tools.d.ts +2 -3
- package/dist/shared/mcp-enabled-tools.js +2 -3
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +20 -15
- package/dist/shared/mcp-resolver.js +11 -12
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/placeholder-resolver.d.ts +9 -2
- package/dist/shared/placeholder-resolver.js +9 -2
- package/dist/shared/placeholder-resolver.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -46
- package/dist/shared/plan-mode-permissions.js +27 -55
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/skill-mount.d.ts +89 -0
- package/dist/shared/skill-mount.js +142 -0
- package/dist/shared/skill-mount.js.map +1 -0
- package/dist/shared/skill-writer.d.ts +28 -26
- package/dist/shared/skill-writer.js +79 -102
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/worker-shutdown.d.ts +67 -0
- package/dist/shared/worker-shutdown.js +79 -0
- package/dist/shared/worker-shutdown.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +3 -2
- package/dist/workflow-engine/loader.js +38 -1
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +23 -2
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/package.json +15 -11
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
- package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
- package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
- package/src/activities/execute-cursor/index.ts +63 -35
- package/src/activities/execute-cursor/prompt-builder.ts +29 -2
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
- package/src/activities/execute-cursor/skill-resolver.ts +10 -137
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
- package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
- package/src/activities/execute-deep-agent/index.ts +57 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
- package/src/activities/execute-deep-agent/setup.ts +21 -19
- package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
- package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
- package/src/index.ts +1 -1
- package/src/middleware/__tests__/path-normalization.test.ts +32 -32
- package/src/middleware/path-normalization.ts +78 -90
- package/src/middleware/types.ts +3 -3
- package/src/runner-manager.ts +26 -16
- package/src/runner.ts +14 -0
- package/src/shared/__tests__/artifact-storage.test.ts +28 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
- package/src/shared/__tests__/declared-preferences.test.ts +109 -0
- package/src/shared/__tests__/foundry-seam.test.ts +5 -5
- package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
- package/src/shared/__tests__/skill-mount.test.ts +238 -0
- package/src/shared/__tests__/skill-writer.test.ts +151 -128
- package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
- package/src/shared/__tests__/vertex-seam.test.ts +5 -5
- package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
- package/src/shared/args-preview.ts +17 -3
- package/src/shared/artifact-storage.ts +20 -1
- package/src/shared/declared-preferences.ts +84 -0
- package/src/shared/mcp-enabled-tools.ts +2 -3
- package/src/shared/mcp-resolver.ts +20 -20
- package/src/shared/placeholder-resolver.ts +9 -2
- package/src/shared/plan-mode-permissions.ts +27 -58
- package/src/shared/skill-mount.ts +179 -0
- package/src/shared/skill-writer.ts +96 -130
- package/src/shared/worker-shutdown.ts +99 -0
- package/src/shared/workspace/types.ts +3 -2
- package/src/workflow-engine/__tests__/loader.test.ts +51 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
- package/src/workflow-engine/loader.ts +46 -1
- package/src/workflow-engine/tasks/human-input.ts +33 -5
- package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
- package/dist/activities/execute-cursor/connect-backfill.js +0 -27
- package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
- package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
- package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
- package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
- package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
- package/src/activities/execute-cursor/connect-backfill.ts +0 -52
- package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
- package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor harness's MCP output adapter: maps the shared resolver's
|
|
3
|
+
* {@link ResolvedMcpServer} servers into the Cursor SDK's mcpServers config
|
|
4
|
+
* for Agent.create(), plus the harness's env pre-flight check.
|
|
5
|
+
*
|
|
6
|
+
* Resolution itself lives in shared/mcp-resolver.ts (oss#387 consolidated the
|
|
7
|
+
* near-duplicate resolver this file used to carry) — this module is the
|
|
8
|
+
* symmetric twin of the deep-agent harness's toMcpClientConfig in
|
|
9
|
+
* shared/mcp-manager.ts: both consume the identical intermediate, only the
|
|
10
|
+
* final SDK serialization differs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
14
|
+
import type { ResolvedMcpServer } from "../../shared/mcp-resolver.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Cursor SDK MCP server config shape (matches @cursor/sdk McpServerConfig).
|
|
18
|
+
* Defined here to avoid tight coupling to the SDK's internal types.
|
|
19
|
+
*/
|
|
20
|
+
export type CursorMcpServerConfig =
|
|
21
|
+
| {
|
|
22
|
+
type?: "stdio";
|
|
23
|
+
command: string;
|
|
24
|
+
args?: string[];
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
type?: "http" | "sse";
|
|
30
|
+
url: string;
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform resolved MCP servers into the Cursor SDK's mcpServers config
|
|
36
|
+
* for Agent.create().
|
|
37
|
+
*
|
|
38
|
+
* Call it ONCE, after the last mutation of the resolved-server list (backfill,
|
|
39
|
+
* synthesized-attachment injection): the config is a projection of that list,
|
|
40
|
+
* and building it early just creates a stale copy someone must remember to
|
|
41
|
+
* rebuild. The Cursor SDK config cannot hide tools, so enabledTools is NOT
|
|
42
|
+
* expressed here — the HITL hook enforces it (see ResolvedMcpServer.enabledTools).
|
|
43
|
+
*/
|
|
44
|
+
export function toCursorMcpConfig(
|
|
45
|
+
servers: ResolvedMcpServer[],
|
|
46
|
+
): Record<string, CursorMcpServerConfig> {
|
|
47
|
+
const result: Record<string, CursorMcpServerConfig> = {};
|
|
48
|
+
|
|
49
|
+
for (const server of servers) {
|
|
50
|
+
if (server.connectionType === "stdio") {
|
|
51
|
+
if (!server.command) continue;
|
|
52
|
+
result[server.slug] = {
|
|
53
|
+
type: "stdio",
|
|
54
|
+
command: server.command,
|
|
55
|
+
args: server.args,
|
|
56
|
+
env: server.env,
|
|
57
|
+
cwd: server.cwd,
|
|
58
|
+
};
|
|
59
|
+
} else {
|
|
60
|
+
if (!server.url) continue;
|
|
61
|
+
result[server.slug] = {
|
|
62
|
+
type: server.connectionType,
|
|
63
|
+
url: server.url,
|
|
64
|
+
headers: server.headers,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate that resolved MCP servers have their required env vars populated.
|
|
74
|
+
* Returns a list of warnings for servers with empty/missing env.
|
|
75
|
+
* Used as a pre-flight check before agent.send() to surface config issues early.
|
|
76
|
+
*/
|
|
77
|
+
export function validateMcpServerEnv(
|
|
78
|
+
servers: ResolvedMcpServer[],
|
|
79
|
+
usages: McpServerUsage[],
|
|
80
|
+
): string[] {
|
|
81
|
+
const warnings: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const usage of usages) {
|
|
84
|
+
const slug = usage.mcpServerRef?.slug;
|
|
85
|
+
if (!slug) continue;
|
|
86
|
+
|
|
87
|
+
const resolved = servers.find((s) => s.slug === slug);
|
|
88
|
+
if (!resolved) {
|
|
89
|
+
warnings.push(`MCP server '${slug}': failed to resolve (server may not exist or is inaccessible)`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (resolved.connectionType === "stdio" && resolved.env) {
|
|
94
|
+
const emptyKeys = Object.entries(resolved.env)
|
|
95
|
+
.filter(([, v]) => !v)
|
|
96
|
+
.map(([k]) => k);
|
|
97
|
+
if (emptyKeys.length > 0) {
|
|
98
|
+
warnings.push(
|
|
99
|
+
`MCP server '${slug}': env vars [${emptyKeys.join(", ")}] are empty — ` +
|
|
100
|
+
`server subprocess will likely fail to connect`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return warnings;
|
|
107
|
+
}
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
resolveCallerIdentity,
|
|
57
57
|
} from "../../shared/caller-identity.js";
|
|
58
58
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
59
|
+
import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
|
|
59
60
|
import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
60
61
|
import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
|
|
61
62
|
import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
@@ -71,7 +72,8 @@ import { DeltaEnricher } from "./delta-enricher.js";
|
|
|
71
72
|
import { TodoTracker } from "./todo-tracker.js";
|
|
72
73
|
import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
73
74
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
74
|
-
import { resolveMcpServers
|
|
75
|
+
import { resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
76
|
+
import { toCursorMcpConfig, validateMcpServerEnv } from "./cursor-mcp-config.js";
|
|
75
77
|
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
76
78
|
import {
|
|
77
79
|
discoverChannelMessaging,
|
|
@@ -85,7 +87,7 @@ import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment
|
|
|
85
87
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
86
88
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
87
89
|
import { enabledToolsBySlug } from "../../shared/mcp-enabled-tools.js";
|
|
88
|
-
import { backfillMcpServersIfNeeded } from "
|
|
90
|
+
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
89
91
|
import { resolveExecutionEnv } from "./env-resolver.js";
|
|
90
92
|
import { resolveBlueprint } from "./blueprint-resolver.js";
|
|
91
93
|
import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
@@ -147,7 +149,7 @@ import type { ClassifiedError } from "./error-classifier.js";
|
|
|
147
149
|
import { createAgent, createCloudAgent } from "./session-lifecycle.js";
|
|
148
150
|
import { setMaxListeners } from "node:events";
|
|
149
151
|
import { startHeartbeat } from "../../shared/heartbeat.js";
|
|
150
|
-
import { getShutdownSignalForQueue } from "../../
|
|
152
|
+
import { classifyTurnInterruption, getShutdownSignalForQueue } from "../../shared/worker-shutdown.js";
|
|
151
153
|
|
|
152
154
|
/**
|
|
153
155
|
* Creates the activity functions bound to the runner config.
|
|
@@ -627,16 +629,19 @@ async function executeCursorInner(
|
|
|
627
629
|
),
|
|
628
630
|
sessionId,
|
|
629
631
|
);
|
|
630
|
-
|
|
632
|
+
// The resolved-server list mutates through backfill and attachment
|
|
633
|
+
// injection below; the Cursor SDK config is projected from it exactly
|
|
634
|
+
// once, after the last mutation (see toCursorMcpConfig).
|
|
635
|
+
let resolvedMcpServers = (await resolveMcpServers(
|
|
631
636
|
client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
|
|
632
|
-
);
|
|
637
|
+
)).resolvedServers;
|
|
633
638
|
setupTiming.mark("resolve_mcp_servers");
|
|
634
639
|
|
|
635
640
|
// Phase 4a: Connect backfill for undiscovered MCP servers
|
|
636
641
|
heartbeatPhase = "resolving_mcp_servers";
|
|
637
642
|
const sessionOrg = session.metadata?.org ?? "";
|
|
638
|
-
|
|
639
|
-
client,
|
|
643
|
+
resolvedMcpServers = await backfillMcpServersIfNeeded(
|
|
644
|
+
client, resolvedMcpServers, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
|
|
640
645
|
transportPosture, heartbeat, secretKeys,
|
|
641
646
|
);
|
|
642
647
|
setupTiming.mark("backfill_mcp");
|
|
@@ -684,13 +689,9 @@ async function executeCursorInner(
|
|
|
684
689
|
backendEndpoint: config.stigmerBackendEndpoint,
|
|
685
690
|
});
|
|
686
691
|
if (attachment) {
|
|
687
|
-
|
|
688
|
-
|
|
692
|
+
resolvedMcpServers = injectSynthesizedAttachment(
|
|
693
|
+
resolvedMcpServers, attachment, "channel messaging",
|
|
689
694
|
);
|
|
690
|
-
mcpResolution = {
|
|
691
|
-
resolvedServers,
|
|
692
|
-
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
693
|
-
};
|
|
694
695
|
}
|
|
695
696
|
}
|
|
696
697
|
|
|
@@ -710,15 +711,13 @@ async function executeCursorInner(
|
|
|
710
711
|
},
|
|
711
712
|
);
|
|
712
713
|
if (conversationAttachment) {
|
|
713
|
-
|
|
714
|
-
|
|
714
|
+
resolvedMcpServers = injectSynthesizedAttachment(
|
|
715
|
+
resolvedMcpServers, conversationAttachment, "conversation participation",
|
|
715
716
|
);
|
|
716
|
-
mcpResolution = {
|
|
717
|
-
resolvedServers,
|
|
718
|
-
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
719
|
-
};
|
|
720
717
|
}
|
|
721
|
-
|
|
718
|
+
// The one projection point: every mutation above is now visible in the
|
|
719
|
+
// Cursor SDK config by construction (no per-mutation rebuild to forget).
|
|
720
|
+
const mcpConfig = toCursorMcpConfig(resolvedMcpServers);
|
|
722
721
|
|
|
723
722
|
// Phase 4b: Merge approval policies from all layers.
|
|
724
723
|
//
|
|
@@ -734,16 +733,15 @@ async function executeCursorInner(
|
|
|
734
733
|
// see ResolvedMcpServer.toolApprovalOverrides (issue #349) — so there
|
|
735
734
|
// is no separate override input to pass here.
|
|
736
735
|
const mergedPolicies = mergeApprovalPolicies(
|
|
737
|
-
|
|
736
|
+
resolvedMcpServers,
|
|
738
737
|
leases,
|
|
739
738
|
);
|
|
740
739
|
heartbeat();
|
|
741
740
|
|
|
742
741
|
// Phase 4c: Validate MCP server env health (diagnostic, non-blocking)
|
|
743
742
|
const mcpWarnings = validateMcpServerEnv(
|
|
744
|
-
|
|
743
|
+
resolvedMcpServers,
|
|
745
744
|
blueprint.mergedMcpServerUsages,
|
|
746
|
-
envVars,
|
|
747
745
|
);
|
|
748
746
|
if (mcpWarnings.length > 0) {
|
|
749
747
|
console.warn(
|
|
@@ -910,7 +908,7 @@ async function executeCursorInner(
|
|
|
910
908
|
// servers' allow-lists, enforced by the hook's "disabled" arm ahead of
|
|
911
909
|
// every approval bypass. The Cursor SDK config cannot hide a server's
|
|
912
910
|
// tools, so this deny-at-call is the harness's enforcement.
|
|
913
|
-
enabledToolsBySlug(
|
|
911
|
+
enabledToolsBySlug(resolvedMcpServers),
|
|
914
912
|
);
|
|
915
913
|
const hitlGate = await installHitlGate({
|
|
916
914
|
workspaceRoot: primaryWorkspaceDir,
|
|
@@ -1175,6 +1173,7 @@ async function executeCursorInner(
|
|
|
1175
1173
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1176
1174
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1177
1175
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1176
|
+
declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
|
|
1178
1177
|
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1179
1178
|
// The turn's recorded transcript, seeded from the persisted execution
|
|
1180
1179
|
// on a reinvocation (Phase 3). Consumed only by the HITL-recovery
|
|
@@ -1401,13 +1400,21 @@ async function executeCursorInner(
|
|
|
1401
1400
|
// classifies a shutdown from the shutdown signal directly (in
|
|
1402
1401
|
// resolvePreBoundaryTerminal). The heartbeat timer may set `cancelled` before
|
|
1403
1402
|
// the AbortSignal microtask propagates; the direct signal check catches that.
|
|
1404
|
-
|
|
1405
|
-
|
|
1403
|
+
// The decision table (including #776's grace-window guard: an aborted
|
|
1404
|
+
// shutdown signal with NO interruption evidence stays "none") lives in
|
|
1405
|
+
// classifyTurnInterruption — shared/worker-shutdown.ts.
|
|
1406
|
+
const interruption = classifyTurnInterruption({
|
|
1407
|
+
heartbeatCancelled: periodicHeartbeat.cancelled,
|
|
1408
|
+
heartbeatWorkerShutdown: periodicHeartbeat.workerShutdown,
|
|
1409
|
+
cancellationSignalAborted: Context.current().cancellationSignal.aborted,
|
|
1410
|
+
shutdownSignalAborted: shutdownSignal?.aborted ?? false,
|
|
1411
|
+
});
|
|
1412
|
+
if (interruption === "worker-shutdown") {
|
|
1406
1413
|
turnState.pauseDetected = false;
|
|
1407
|
-
} else if (
|
|
1414
|
+
} else if (interruption === "pause") {
|
|
1408
1415
|
turnState.pauseDetected = true;
|
|
1409
1416
|
}
|
|
1410
|
-
workerShutdownDetected =
|
|
1417
|
+
workerShutdownDetected = interruption === "worker-shutdown";
|
|
1411
1418
|
|
|
1412
1419
|
// Post-stream finalize, shared by the primary turn and both recovery retries:
|
|
1413
1420
|
// finalize the transcript + streaming flags, mark any in-flight sub-agent
|
|
@@ -1505,10 +1512,16 @@ async function executeCursorInner(
|
|
|
1505
1512
|
return { kind: "return" };
|
|
1506
1513
|
}
|
|
1507
1514
|
|
|
1508
|
-
// Worker shutdown: the runner
|
|
1515
|
+
// Worker shutdown: the runner/manager aborted the shutdown signal. NOT a
|
|
1509
1516
|
// user pause. Checked via the shutdown signal directly so a retry (whose
|
|
1510
|
-
// periodic heartbeat is already stopped) still classifies it correctly
|
|
1511
|
-
|
|
1517
|
+
// periodic heartbeat is already stopped) still classifies it correctly —
|
|
1518
|
+
// but only alongside a delivered cancellation: a retry that completed
|
|
1519
|
+
// normally inside the drain grace window must stay a completion (#776's
|
|
1520
|
+
// grace-window guard, mirroring the primary's `interrupted` gate).
|
|
1521
|
+
if (
|
|
1522
|
+
workerShutdownDetected ||
|
|
1523
|
+
((shutdownSignal?.aborted ?? false) && Context.current().cancellationSignal.aborted)
|
|
1524
|
+
) {
|
|
1512
1525
|
status.phase = ExecutionPhase.EXECUTION_FAILED;
|
|
1513
1526
|
status.error = "Execution interrupted: runner worker was shut down. Retry or resume.";
|
|
1514
1527
|
status.completedAt = utcTimestamp();
|
|
@@ -1865,6 +1878,7 @@ async function executeCursorInner(
|
|
|
1865
1878
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1866
1879
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1867
1880
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1881
|
+
declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
|
|
1868
1882
|
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1869
1883
|
// Composed fresh (not reused from Phase 10): the failed primary
|
|
1870
1884
|
// stream may have appended partial work onto status.messages,
|
|
@@ -2150,9 +2164,12 @@ async function executeCursorInner(
|
|
|
2150
2164
|
periodicHeartbeat?.stop();
|
|
2151
2165
|
|
|
2152
2166
|
if (err instanceof CancelledFailure) {
|
|
2153
|
-
//
|
|
2154
|
-
//
|
|
2155
|
-
|
|
2167
|
+
// Worker shutdown is infrastructure failure, not pause. The direct
|
|
2168
|
+
// signal check covers a CancelledFailure thrown BEFORE the post-stream
|
|
2169
|
+
// classification ran (workerShutdownDetected still false); no extra
|
|
2170
|
+
// interruption-evidence gate is needed here — the caught
|
|
2171
|
+
// CancelledFailure IS the evidence (#776).
|
|
2172
|
+
if (workerShutdownDetected || (shutdownSignal?.aborted ?? false)) {
|
|
2156
2173
|
console.log(`ExecuteCursor cancelled (worker shutdown) for execution ${executionId}`);
|
|
2157
2174
|
status.phase = ExecutionPhase.EXECUTION_FAILED;
|
|
2158
2175
|
status.error = "Execution interrupted: runner worker was shut down. Retry or resume.";
|
|
@@ -2451,10 +2468,19 @@ export interface BuildPromptInput {
|
|
|
2451
2468
|
* turn.
|
|
2452
2469
|
*/
|
|
2453
2470
|
sessionContext?: string;
|
|
2471
|
+
/**
|
|
2472
|
+
* Platform-declared standing preferences from the execution spec's
|
|
2473
|
+
* `declared_preferences` (stigmer/stigmer#293). Like the bridge, only
|
|
2474
|
+
* the enhanced-prompt path consumes it — deliberately frozen per Cursor
|
|
2475
|
+
* session (DD-002 D3): the first turn delivers it into the agent's own
|
|
2476
|
+
* conversation store, and repeating it on resumed turns would bloat the
|
|
2477
|
+
* store with identical content.
|
|
2478
|
+
*/
|
|
2479
|
+
declaredPreferences?: import("../../shared/declared-preferences.js").DeclaredPreferencesContent;
|
|
2454
2480
|
/**
|
|
2455
2481
|
* Conversation catchup from the execution spec's `conversation_catchup`
|
|
2456
2482
|
* (cloud DD-006): what happened on the channel conversation that the
|
|
2457
|
-
* agent has not seen. PER-TURN, so unlike the
|
|
2483
|
+
* agent has not seen. PER-TURN, so unlike the standing values
|
|
2458
2484
|
* above it rides BOTH prompt paths — the enhanced prompt and a resumed
|
|
2459
2485
|
* turn's prefix (the `interaction_mode` shape). Handback lands
|
|
2460
2486
|
* mid-session on a resumed agent: the resumed path is the one that
|
|
@@ -2589,6 +2615,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2589
2615
|
contextBridge: input.contextBridge,
|
|
2590
2616
|
senderIdentity: input.senderIdentity,
|
|
2591
2617
|
sessionContext: input.sessionContext,
|
|
2618
|
+
declaredPreferences: input.declaredPreferences,
|
|
2592
2619
|
conversationCatchup,
|
|
2593
2620
|
},
|
|
2594
2621
|
{
|
|
@@ -2655,6 +2682,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2655
2682
|
contextBridge: input.contextBridge,
|
|
2656
2683
|
senderIdentity: input.senderIdentity,
|
|
2657
2684
|
sessionContext: input.sessionContext,
|
|
2685
|
+
declaredPreferences: input.declaredPreferences,
|
|
2658
2686
|
conversationCatchup,
|
|
2659
2687
|
});
|
|
2660
2688
|
}
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
type SenderIdentity,
|
|
32
32
|
} from "../../shared/sender-identity.js";
|
|
33
33
|
import { formatSessionContextText } from "../../shared/session-context.js";
|
|
34
|
+
import {
|
|
35
|
+
formatDeclaredPreferencesText,
|
|
36
|
+
type DeclaredPreferencesContent,
|
|
37
|
+
} from "../../shared/declared-preferences.js";
|
|
34
38
|
import {
|
|
35
39
|
visionDisclosureLines,
|
|
36
40
|
type NotViewableEntry,
|
|
@@ -147,11 +151,20 @@ export interface EnhancedPromptOptions {
|
|
|
147
151
|
* store — the context is constant for the session's lifetime.
|
|
148
152
|
*/
|
|
149
153
|
sessionContext?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Platform-declared standing preferences (stigmer/stigmer#293): the org's
|
|
156
|
+
* and user's standing context, server-snapshotted onto the execution
|
|
157
|
+
* spec's `declared_preferences` at create. Like the bridge, it lands in
|
|
158
|
+
* the first message and persists in the cursor agent's own conversation
|
|
159
|
+
* store — deliberately frozen per Cursor session (DD-002 D3): repeating
|
|
160
|
+
* it every resumed turn would bloat the store with identical content.
|
|
161
|
+
*/
|
|
162
|
+
declaredPreferences?: DeclaredPreferencesContent;
|
|
150
163
|
/**
|
|
151
164
|
* Conversation catchup (cloud DD-006): what happened on the channel
|
|
152
165
|
* conversation that the agent has not seen, read from the execution
|
|
153
|
-
* spec's `conversation_catchup`. PER-TURN, unlike the
|
|
154
|
-
*
|
|
166
|
+
* spec's `conversation_catchup`. PER-TURN, unlike the standing siblings
|
|
167
|
+
* above: it rides BOTH prompt paths — this enhanced prompt and
|
|
155
168
|
* a resumed turn's prefix (the `interaction_mode` shape) — because
|
|
156
169
|
* handback lands mid-session on a resumed agent, the exact case the
|
|
157
170
|
* metadata lane cannot reach.
|
|
@@ -232,6 +245,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
232
245
|
sections.push(formatSenderIdentitySection(options.senderIdentity));
|
|
233
246
|
}
|
|
234
247
|
|
|
248
|
+
// Platform-declared standing facts precede embedder-supplied context
|
|
249
|
+
// (DD-002 D3): both are standing background, but the declared preferences
|
|
250
|
+
// are platform-authored while session context is the embedder's overlay —
|
|
251
|
+
// the more specific overlay reads later and naturally refines.
|
|
252
|
+
if (options.declaredPreferences) {
|
|
253
|
+
sections.push(formatDeclaredPreferencesSection(options.declaredPreferences));
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
// Standing facts about the user (session context) come before the
|
|
236
257
|
// carried conversation (bridge): the bridge may refer back to them.
|
|
237
258
|
if (options.sessionContext) {
|
|
@@ -445,6 +466,12 @@ export function formatSenderIdentitySection(identity: SenderIdentity): string {
|
|
|
445
466
|
return `<conversation_sender>\n${formatSenderIdentityText(identity)}\n</conversation_sender>`;
|
|
446
467
|
}
|
|
447
468
|
|
|
469
|
+
export function formatDeclaredPreferencesSection(
|
|
470
|
+
preferences: DeclaredPreferencesContent,
|
|
471
|
+
): string {
|
|
472
|
+
return `<declared_preferences>\n${formatDeclaredPreferencesText(preferences)}\n</declared_preferences>`;
|
|
473
|
+
}
|
|
474
|
+
|
|
448
475
|
export function formatSessionContextSection(context: string): string {
|
|
449
476
|
return `<session_context>\n${formatSessionContextText(context)}\n</session_context>`;
|
|
450
477
|
}
|
|
@@ -47,7 +47,7 @@ import type {
|
|
|
47
47
|
ModelParameterValue,
|
|
48
48
|
} from "@cursor/sdk";
|
|
49
49
|
import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
|
|
50
|
-
import type { CursorMcpServerConfig } from "./mcp-
|
|
50
|
+
import type { CursorMcpServerConfig } from "./cursor-mcp-config.js";
|
|
51
51
|
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
// Constants
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Resolves skill resources and writes them to the platform-managed directory.
|
|
3
3
|
*
|
|
4
4
|
* - Fetches skills via gRPC (by reference)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - Mounts each skill via the shared skill-mount mechanics (SKILL.md +
|
|
6
|
+
* extracted artifact, hash-keyed cache — see shared/skill-mount.ts)
|
|
7
7
|
* - Uses a platform-managed directory outside the workspace
|
|
8
8
|
* - Ensures the workspace `.stigmer` symlink (see stigmer-link.ts)
|
|
9
9
|
* - Returns metadata for prompt injection
|
|
@@ -17,38 +17,20 @@
|
|
|
17
17
|
* transfer.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { mkdir
|
|
21
|
-
import { join
|
|
22
|
-
import { ConnectError, Code } from "@connectrpc/connect";
|
|
20
|
+
import { mkdir } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
23
22
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
24
|
-
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
25
23
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
26
24
|
import type { SkillMetadata } from "./prompt-builder.js";
|
|
27
25
|
import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
28
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
SKILLS_SUBDIR,
|
|
28
|
+
mountIsFresh,
|
|
29
|
+
downloadArtifact,
|
|
30
|
+
writeSkillMount,
|
|
31
|
+
} from "../../shared/skill-mount.js";
|
|
29
32
|
import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
|
|
30
33
|
|
|
31
|
-
const SKILLS_SUBDIR = "skills";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Marker recording what a skill's mount directory currently holds. Written
|
|
35
|
-
* LAST, after every file of the mount landed — a crash mid-write leaves no
|
|
36
|
-
* marker, so the next execution remounts instead of trusting a partial tree.
|
|
37
|
-
*/
|
|
38
|
-
const MOUNT_MARKER_FILE = ".stigmer-mount.json";
|
|
39
|
-
|
|
40
|
-
interface MountMarker {
|
|
41
|
-
/** Content-addressed version hash (`Skill.status.version_hash`) of the mounted content. */
|
|
42
|
-
versionHash: string;
|
|
43
|
-
/**
|
|
44
|
-
* Whether the artifact's files are part of the mount. `false` when the
|
|
45
|
-
* skill has no artifact OR when the download failed and the mount fell
|
|
46
|
-
* back to SKILL.md only — the latter makes the next execution retry the
|
|
47
|
-
* download rather than cache the degraded mount.
|
|
48
|
-
*/
|
|
49
|
-
artifactMounted: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
34
|
export interface SkillResolverOptions {
|
|
53
35
|
sessionId: string;
|
|
54
36
|
primaryWorkspaceDir: string;
|
|
@@ -145,112 +127,3 @@ export async function resolveSkills(
|
|
|
145
127
|
|
|
146
128
|
return results;
|
|
147
129
|
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Whether the mount at `skillDir` already holds this version's content.
|
|
151
|
-
*
|
|
152
|
-
* Fresh means: the marker's hash matches AND the mount isn't a degraded
|
|
153
|
-
* SKILL.md-only fallback when the skill does carry an artifact. Any read or
|
|
154
|
-
* parse failure counts as stale — the remount is the safe default.
|
|
155
|
-
*/
|
|
156
|
-
async function mountIsFresh(skillDir: string, versionHash: string, wantsArtifact: boolean): Promise<boolean> {
|
|
157
|
-
try {
|
|
158
|
-
const raw = await readFile(join(skillDir, MOUNT_MARKER_FILE), "utf-8");
|
|
159
|
-
const marker = JSON.parse(raw) as Partial<MountMarker>;
|
|
160
|
-
return marker.versionHash === versionHash && (marker.artifactMounted === true || !wantsArtifact);
|
|
161
|
-
} catch {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Download a skill artifact's ZIP bytes, transfer lane first (#675).
|
|
168
|
-
*
|
|
169
|
-
* The URL lane (getArtifactDownloadUrl → HTTP GET) carries any valid skill
|
|
170
|
-
* size; the unary getArtifact response is capped by the server's 10MB gRPC
|
|
171
|
-
* message limit. Servers that predate the lane (and cloud until its sibling
|
|
172
|
-
* lands) answer the mint with UNIMPLEMENTED — those fall back to the unary
|
|
173
|
-
* path, which behaves exactly as before for ≤10MB artifacts.
|
|
174
|
-
*
|
|
175
|
-
* Runs only on a mount-cache miss (#672's hash-keyed marker above) — a hit
|
|
176
|
-
* skips the transfer entirely, whichever lane would have carried it.
|
|
177
|
-
*
|
|
178
|
-
* Exported for tests.
|
|
179
|
-
*/
|
|
180
|
-
export async function downloadArtifact(
|
|
181
|
-
client: StigmerClient,
|
|
182
|
-
artifactStorageKey: string,
|
|
183
|
-
): Promise<Uint8Array | undefined> {
|
|
184
|
-
let minted;
|
|
185
|
-
try {
|
|
186
|
-
minted = await client.getSkillArtifactDownloadUrl(artifactStorageKey);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
if (err instanceof ConnectError && err.code === Code.Unimplemented) {
|
|
189
|
-
const resp = await client.getSkillArtifact(artifactStorageKey);
|
|
190
|
-
return resp.artifact && resp.artifact.length > 0 ? resp.artifact : undefined;
|
|
191
|
-
}
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const resp = await fetch(minted.url);
|
|
196
|
-
if (!resp.ok) {
|
|
197
|
-
throw new Error(`artifact fetch failed: HTTP ${resp.status} from ${minted.url}`);
|
|
198
|
-
}
|
|
199
|
-
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
200
|
-
if (minted.sizeBytes > 0n && BigInt(bytes.length) !== minted.sizeBytes) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
`artifact fetch truncated: got ${bytes.length} bytes, expected ${minted.sizeBytes}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
return bytes.length > 0 ? bytes : undefined;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* (Re)write a skill's mount directory from scratch.
|
|
210
|
-
*
|
|
211
|
-
* The directory is removed first so files deleted between versions don't
|
|
212
|
-
* linger in the mount, then SKILL.md and the artifact files are written, and
|
|
213
|
-
* the marker is stamped LAST (see MOUNT_MARKER_FILE for the crash-safety
|
|
214
|
-
* contract). SKILL.md always comes from `spec.skillMd` — the server's
|
|
215
|
-
* authoritative copy — never from the zip; both the zip's SKILL.md and any
|
|
216
|
-
* stray marker-named entry are excluded from extraction so the mount's
|
|
217
|
-
* ownership of those two files is unconditional.
|
|
218
|
-
*/
|
|
219
|
-
async function writeSkillMount(
|
|
220
|
-
skill: Skill,
|
|
221
|
-
skillDir: string,
|
|
222
|
-
artifactBytes: Uint8Array | undefined,
|
|
223
|
-
): Promise<void> {
|
|
224
|
-
await rm(skillDir, { recursive: true, force: true });
|
|
225
|
-
await mkdir(skillDir, { recursive: true });
|
|
226
|
-
|
|
227
|
-
await writeFile(join(skillDir, "SKILL.md"), skill.spec!.skillMd, "utf-8");
|
|
228
|
-
|
|
229
|
-
const artifactMounted = artifactBytes !== undefined && artifactBytes.length > 0;
|
|
230
|
-
if (artifactMounted) {
|
|
231
|
-
const entries = await extractZipFileEntries(artifactBytes, { exclude: ["SKILL.md", MOUNT_MARKER_FILE] });
|
|
232
|
-
for (const entry of entries) {
|
|
233
|
-
const filePath = join(skillDir, entry.path);
|
|
234
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
235
|
-
await writeFile(filePath, entry.content);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const versionHash = skill.status?.versionHash ?? "";
|
|
240
|
-
if (versionHash !== "") {
|
|
241
|
-
const marker: MountMarker = { versionHash, artifactMounted };
|
|
242
|
-
await writeFile(join(skillDir, MOUNT_MARKER_FILE), JSON.stringify(marker), "utf-8");
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Clean up platform-managed skill directory for a session.
|
|
248
|
-
*/
|
|
249
|
-
export async function cleanupSkills(sessionId: string): Promise<void> {
|
|
250
|
-
const platformDir = getPlatformDir(sessionId);
|
|
251
|
-
try {
|
|
252
|
-
await rm(platformDir, { recursive: true, force: true });
|
|
253
|
-
} catch {
|
|
254
|
-
// best-effort
|
|
255
|
-
}
|
|
256
|
-
}
|
|
@@ -81,4 +81,19 @@ describe("captureApprovalArtifacts", () => {
|
|
|
81
81
|
const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
|
|
82
82
|
expect(argsPreview).toContain("needle");
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
it("returns the redacted args object for stamping the placeholder row (issue #754)", () => {
|
|
86
|
+
// The interrupt-placeholder ToolCall must carry `args`, not just the
|
|
87
|
+
// preview string: the row header extracts its filename-first path from
|
|
88
|
+
// args, so a placeholder without them rendered a pathless "Write" gate.
|
|
89
|
+
// Redaction posture matches the preview: secret keys never surface.
|
|
90
|
+
const messages = [
|
|
91
|
+
aiMessage([
|
|
92
|
+
{ id: "call-1", name: "write_file", args: { file_path: "a.txt", content: "hi", token: "sk-secret" } },
|
|
93
|
+
]),
|
|
94
|
+
];
|
|
95
|
+
const { args } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
|
|
96
|
+
|
|
97
|
+
expect(args).toEqual({ file_path: "a.txt", content: "hi", token: "[REDACTED]" });
|
|
98
|
+
});
|
|
84
99
|
});
|