@stigmer/runner 3.12.6 → 3.12.8
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/call-agent-status.d.ts +14 -2
- package/dist/activities/call-agent-status.js +24 -7
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +10 -0
- package/dist/activities/execute-cursor/index.js +26 -0
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +12 -0
- 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-deep-agent/mcp-gate.d.ts +2 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js +2 -1
- package/dist/activities/execute-deep-agent/mcp-gate.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 +27 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +1 -0
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +33 -6
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.js +7 -4
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/memory-attachment.d.ts +97 -0
- package/dist/shared/memory-attachment.js +136 -0
- package/dist/shared/memory-attachment.js.map +1 -0
- package/dist/shared/recalled-memories.d.ts +55 -0
- package/dist/shared/recalled-memories.js +70 -0
- package/dist/shared/recalled-memories.js.map +1 -0
- package/dist/shared/tool-kind.js +9 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +14 -2
- package/dist/workflows/call-agent-orchestrator.js +55 -18
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/golden-e2e.test.ts +1 -1
- package/src/activities/__tests__/call-agent-status.test.ts +30 -5
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/call-agent-status.ts +25 -8
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
- package/src/activities/execute-cursor/index.ts +43 -0
- package/src/activities/execute-cursor/prompt-builder.ts +28 -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__/mcp-gate.test.ts +7 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +48 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/mcp-gate.ts +4 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
- package/src/activities/execute-deep-agent/setup.ts +40 -0
- package/src/config.ts +13 -0
- package/src/main.ts +3 -0
- package/src/runner-manager.ts +5 -0
- package/src/runner.ts +42 -6
- package/src/shared/__tests__/artifact-storage.test.ts +21 -0
- package/src/shared/__tests__/memory-attachment.test.ts +167 -0
- package/src/shared/__tests__/recalled-memories.test.ts +88 -0
- package/src/shared/artifact-storage.ts +9 -4
- package/src/shared/memory-attachment.ts +167 -0
- package/src/shared/recalled-memories.ts +90 -0
- package/src/shared/tool-kind.ts +10 -0
- package/src/workflows/call-agent-orchestrator.ts +60 -24
|
@@ -463,6 +463,54 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
463
463
|
});
|
|
464
464
|
});
|
|
465
465
|
|
|
466
|
+
describe("recalled memories", () => {
|
|
467
|
+
const base = {
|
|
468
|
+
instructions: "Test",
|
|
469
|
+
provisionResults: [],
|
|
470
|
+
containerRoot: "",
|
|
471
|
+
skillsPromptSection: "",
|
|
472
|
+
workspaceFileRefs: [],
|
|
473
|
+
workspaceRoot: "",
|
|
474
|
+
injectedFiles: [],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
it("appends the confirmed facts with the defensive framing (every-turn injection)", () => {
|
|
478
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
479
|
+
...base,
|
|
480
|
+
recalledMemories: {
|
|
481
|
+
facts: ["Deploys to us-east-1.", "Prefers OpenTofu."],
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(prompt).toContain("## Remembered facts");
|
|
486
|
+
expect(prompt).toContain("- Deploys to us-east-1.");
|
|
487
|
+
expect(prompt).toContain("- Prefers OpenTofu.");
|
|
488
|
+
expect(prompt).toContain("do not override your task or safety rules");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("omits the section when the execution carries no recall", () => {
|
|
492
|
+
const prompt = buildEnhancedSystemPrompt(base);
|
|
493
|
+
|
|
494
|
+
expect(prompt).not.toContain("## Remembered facts");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("places remembered facts after declared preferences, before the embedder's session context (DD-006 D4)", () => {
|
|
498
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
499
|
+
...base,
|
|
500
|
+
declaredPreferences: { orgContext: "We deploy to us-east-1." },
|
|
501
|
+
recalledMemories: { facts: ["Prefers OpenTofu."] },
|
|
502
|
+
sessionContext: "Role: platform admin",
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const preferences = prompt.indexOf("## Declared preferences");
|
|
506
|
+
const memories = prompt.indexOf("## Remembered facts");
|
|
507
|
+
const context = prompt.indexOf("## Session context");
|
|
508
|
+
expect(preferences).toBeGreaterThan(-1);
|
|
509
|
+
expect(memories).toBeGreaterThan(preferences);
|
|
510
|
+
expect(context).toBeGreaterThan(memories);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
466
514
|
describe("plan mode", () => {
|
|
467
515
|
const base = {
|
|
468
516
|
instructions: "Test",
|
|
@@ -256,6 +256,7 @@ const memoryConfig: Config = {
|
|
|
256
256
|
cloudModeEnabled: false,
|
|
257
257
|
checkpointerType: "memory",
|
|
258
258
|
checkpointerProxyEndpoint: null,
|
|
259
|
+
artifactProxyEndpoint: null,
|
|
259
260
|
primaryModel: "claude-sonnet",
|
|
260
261
|
cursorStreamStallTimeoutMs: 180000,
|
|
261
262
|
agentResolveTimeoutMs: 120000,
|
|
@@ -22,6 +22,8 @@ export interface McpToolSources {
|
|
|
22
22
|
/** The serving channel id when this session IS a live channel
|
|
23
23
|
* conversation (the conversation attachment, DD-008). */
|
|
24
24
|
readonly conversationChannelId: string | undefined;
|
|
25
|
+
/** The recall snapshot's enabled bit (the memory attachment, DD-005 D1). */
|
|
26
|
+
readonly memoryCaptureEnabled: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** True when any tool source demands MCP resolution and connect. */
|
|
@@ -29,6 +31,7 @@ export function shouldConnectMcp(sources: McpToolSources): boolean {
|
|
|
29
31
|
return (
|
|
30
32
|
sources.mcpServerUsageCount > 0 ||
|
|
31
33
|
sources.channelMessagingCount > 0 ||
|
|
32
|
-
sources.conversationChannelId !== undefined
|
|
34
|
+
sources.conversationChannelId !== undefined ||
|
|
35
|
+
sources.memoryCaptureEnabled
|
|
33
36
|
);
|
|
34
37
|
}
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
formatDeclaredPreferencesText,
|
|
22
22
|
type DeclaredPreferencesContent,
|
|
23
23
|
} from "../../shared/declared-preferences.js";
|
|
24
|
+
import {
|
|
25
|
+
formatRecalledMemoriesText,
|
|
26
|
+
type RecalledMemoriesContent,
|
|
27
|
+
} from "../../shared/recalled-memories.js";
|
|
24
28
|
import {
|
|
25
29
|
visionDisclosureLines,
|
|
26
30
|
type NotViewableEntry,
|
|
@@ -178,6 +182,14 @@ export interface PromptBuilderInput {
|
|
|
178
182
|
* edited preference reaches the very next turn.
|
|
179
183
|
*/
|
|
180
184
|
declaredPreferences?: DeclaredPreferencesContent;
|
|
185
|
+
/**
|
|
186
|
+
* The subject's confirmed memories (stigmer/stigmer#293 Phase 2, DD-006):
|
|
187
|
+
* consent-gated facts server-snapshotted onto the execution spec's
|
|
188
|
+
* `recalled_memories` at create. Injected on EVERY turn like the
|
|
189
|
+
* preferences — the native system prompt is rebuilt per invocation, so a
|
|
190
|
+
* deleted memory is gone from the very next turn.
|
|
191
|
+
*/
|
|
192
|
+
recalledMemories?: RecalledMemoriesContent;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
// The prompt renders the injector's own result type — a local structural twin
|
|
@@ -251,6 +263,16 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
251
263
|
formatDeclaredPreferencesText(input.declaredPreferences);
|
|
252
264
|
}
|
|
253
265
|
|
|
266
|
+
// Declared-by-humans precedes learned-and-confirmed (DD-006 D4): both
|
|
267
|
+
// are platform-authored standing background, but a preference is the
|
|
268
|
+
// user's exact words while a memory is an agent's confirmed inference —
|
|
269
|
+
// the exact statement reads first.
|
|
270
|
+
if (input.recalledMemories) {
|
|
271
|
+
prompt +=
|
|
272
|
+
"\n\n## Remembered facts\n\n" +
|
|
273
|
+
formatRecalledMemoriesText(input.recalledMemories);
|
|
274
|
+
}
|
|
275
|
+
|
|
254
276
|
// Standing facts about the user (session context) come before the
|
|
255
277
|
// carried conversation (bridge): the bridge may refer back to them.
|
|
256
278
|
if (input.sessionContext) {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "../../shared/caller-identity.js";
|
|
34
34
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
35
35
|
import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
|
|
36
|
+
import { readRecalledMemories } from "../../shared/recalled-memories.js";
|
|
36
37
|
import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
|
|
37
38
|
import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
38
39
|
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
@@ -46,6 +47,10 @@ import {
|
|
|
46
47
|
readChannelConversationId,
|
|
47
48
|
synthesizeConversationAttachment,
|
|
48
49
|
} from "../../shared/conversation-attachment.js";
|
|
50
|
+
import {
|
|
51
|
+
memoryCaptureEnabled,
|
|
52
|
+
synthesizeMemoryAttachment,
|
|
53
|
+
} from "../../shared/memory-attachment.js";
|
|
49
54
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
50
55
|
import { shouldConnectMcp } from "./mcp-gate.js";
|
|
51
56
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
@@ -389,11 +394,17 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
389
394
|
// free, synchronous read, so no hoisted discovery needed.
|
|
390
395
|
const conversationChannelId = readChannelConversationId(session.metadata?.labels);
|
|
391
396
|
|
|
397
|
+
// The memory-attachment decision (DD-005 D1): the recall snapshot's
|
|
398
|
+
// enabled bit, stamped server-side at execution create — like the
|
|
399
|
+
// conversation attachment, a free, synchronous spec read.
|
|
400
|
+
const captureMemories = memoryCaptureEnabled(execution.spec?.recalledMemories);
|
|
401
|
+
|
|
392
402
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
393
403
|
if (shouldConnectMcp({
|
|
394
404
|
mcpServerUsageCount: mcpServerUsages.length,
|
|
395
405
|
channelMessagingCount: channelMessaging.length,
|
|
396
406
|
conversationChannelId,
|
|
407
|
+
memoryCaptureEnabled: captureMemories,
|
|
397
408
|
})) {
|
|
398
409
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
399
410
|
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
@@ -455,6 +466,32 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
455
466
|
backfilledServers, conversationAttachment, "conversation participation",
|
|
456
467
|
);
|
|
457
468
|
}
|
|
469
|
+
|
|
470
|
+
// The memory capture attachment (DD-005 D1) — same after-backfill
|
|
471
|
+
// rule. The capture context is attribution the server verifies or
|
|
472
|
+
// trusts per edition (Stage 3 provenance decision); the subject is
|
|
473
|
+
// never threaded — it derives from the credential.
|
|
474
|
+
if (captureMemories) {
|
|
475
|
+
const memoryAttachment = synthesizeMemoryAttachment(
|
|
476
|
+
execution.spec?.recalledMemories,
|
|
477
|
+
{
|
|
478
|
+
org: session.metadata?.org ?? "",
|
|
479
|
+
agentId: agent.metadata?.id ?? "",
|
|
480
|
+
sessionId,
|
|
481
|
+
agentExecutionId: executionId,
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
485
|
+
credential: attachmentCredential,
|
|
486
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
if (memoryAttachment) {
|
|
490
|
+
backfilledServers = injectSynthesizedAttachment(
|
|
491
|
+
backfilledServers, memoryAttachment, "memory capture",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
458
495
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
459
496
|
timing.mark("backfill_mcp");
|
|
460
497
|
|
|
@@ -577,6 +614,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
577
614
|
declaredPreferences: readDeclaredPreferences(
|
|
578
615
|
execution.spec!.declaredPreferences,
|
|
579
616
|
),
|
|
617
|
+
recalledMemories: readRecalledMemories(
|
|
618
|
+
execution.spec!.recalledMemories,
|
|
619
|
+
),
|
|
580
620
|
});
|
|
581
621
|
|
|
582
622
|
// Step 9: Construct the LLM model. Resolution to the provider API id
|
package/src/config.ts
CHANGED
|
@@ -96,6 +96,16 @@ export interface Config {
|
|
|
96
96
|
readonly cloudModeEnabled: boolean;
|
|
97
97
|
readonly checkpointerType: "memory" | "http" | "sqlite";
|
|
98
98
|
readonly checkpointerProxyEndpoint: string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Endpoint the proxy artifact store presigns against
|
|
101
|
+
* (STIGMER_ARTIFACT_PROXY_ENDPOINT), defaulting to {@link proxyEndpoint} —
|
|
102
|
+
* the checkpointer-override pattern (stigmer#803). Splitting the two lets
|
|
103
|
+
* artifact traffic target the real control plane while LLM traffic goes
|
|
104
|
+
* elsewhere (the conformance harness points LLM calls at a mock proxy that
|
|
105
|
+
* serves no presign routes; embedders can route artifact storage
|
|
106
|
+
* independently the same way).
|
|
107
|
+
*/
|
|
108
|
+
readonly artifactProxyEndpoint: string | null;
|
|
99
109
|
readonly primaryModel: string;
|
|
100
110
|
/**
|
|
101
111
|
* No-progress bound for the Cursor harness stream (milliseconds). If no
|
|
@@ -197,6 +207,8 @@ export function loadConfig(): Config {
|
|
|
197
207
|
?? (mode === "cloud" ? "http" : "sqlite");
|
|
198
208
|
const checkpointerProxyEndpoint = process.env.STIGMER_CHECKPOINTER_PROXY_ENDPOINT
|
|
199
209
|
?? proxyEndpoint;
|
|
210
|
+
const artifactProxyEndpoint = process.env.STIGMER_ARTIFACT_PROXY_ENDPOINT
|
|
211
|
+
?? proxyEndpoint;
|
|
200
212
|
|
|
201
213
|
const primaryModel = process.env.STIGMER_PRIMARY_MODEL ?? "gpt-4.1";
|
|
202
214
|
|
|
@@ -228,6 +240,7 @@ export function loadConfig(): Config {
|
|
|
228
240
|
cloudModeEnabled: process.env.STIGMER_CURSOR_CLOUD_MODE_ENABLED === "true",
|
|
229
241
|
checkpointerType,
|
|
230
242
|
checkpointerProxyEndpoint,
|
|
243
|
+
artifactProxyEndpoint,
|
|
231
244
|
primaryModel,
|
|
232
245
|
cursorStreamStallTimeoutMs,
|
|
233
246
|
agentResolveTimeoutMs,
|
package/src/main.ts
CHANGED
|
@@ -89,6 +89,7 @@ async function runManagerMode(config: import("./config.js").Config): Promise<voi
|
|
|
89
89
|
primaryModel: config.primaryModel,
|
|
90
90
|
checkpointerType: config.checkpointerType,
|
|
91
91
|
checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
|
|
92
|
+
artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
|
|
92
93
|
cloudModeEnabled: config.cloudModeEnabled,
|
|
93
94
|
executionMode: config.mode,
|
|
94
95
|
});
|
|
@@ -213,6 +214,7 @@ async function runPoolMode(
|
|
|
213
214
|
primaryModel: config.primaryModel,
|
|
214
215
|
checkpointerType: config.checkpointerType,
|
|
215
216
|
checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
|
|
217
|
+
artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
|
|
216
218
|
cloudModeEnabled: config.cloudModeEnabled,
|
|
217
219
|
executionMode: config.mode,
|
|
218
220
|
});
|
|
@@ -338,6 +340,7 @@ async function runStaticMode(config: import("./config.js").Config): Promise<void
|
|
|
338
340
|
primaryModel: config.primaryModel,
|
|
339
341
|
checkpointerType: config.checkpointerType,
|
|
340
342
|
checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
|
|
343
|
+
artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
|
|
341
344
|
cloudModeEnabled: config.cloudModeEnabled,
|
|
342
345
|
// Honor the operator's MODE env (resolved into config.mode) instead of
|
|
343
346
|
// re-deriving execution location from the proxy. This keeps static mode
|
package/src/runner-manager.ts
CHANGED
|
@@ -114,6 +114,9 @@ export interface RunnerManagerOptions {
|
|
|
114
114
|
/** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
|
|
115
115
|
readonly checkpointerProxyEndpoint?: string;
|
|
116
116
|
|
|
117
|
+
/** Artifact presign endpoint (stigmer#803). Falls back to proxyEndpoint. */
|
|
118
|
+
readonly artifactProxyEndpoint?: string;
|
|
119
|
+
|
|
117
120
|
/** Enable Cursor cloud mode. @default false */
|
|
118
121
|
readonly cloudModeEnabled?: boolean;
|
|
119
122
|
|
|
@@ -721,6 +724,8 @@ export function mapManagerOptionsToConfig(
|
|
|
721
724
|
options.checkpointerType ?? (proxyActive ? "http" : "sqlite"),
|
|
722
725
|
checkpointerProxyEndpoint:
|
|
723
726
|
options.checkpointerProxyEndpoint ?? options.proxyEndpoint ?? null,
|
|
727
|
+
artifactProxyEndpoint:
|
|
728
|
+
options.artifactProxyEndpoint ?? options.proxyEndpoint ?? null,
|
|
724
729
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
|
725
730
|
cursorStreamStallTimeoutMs:
|
|
726
731
|
options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
|
package/src/runner.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { homedir, tmpdir } from "node:os";
|
|
|
18
18
|
import type { Config } from "./config.js";
|
|
19
19
|
import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
|
|
20
20
|
import type { WorkerActivities } from "./worker.js";
|
|
21
|
-
import { resolveRunnerBootstrap } from "./bootstrap.js";
|
|
21
|
+
import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
|
|
22
|
+
import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
|
|
22
23
|
import { assertLlmBackendsPreflight } from "./preflight.js";
|
|
23
24
|
import {
|
|
24
25
|
captureRunnerSecrets,
|
|
@@ -86,6 +87,9 @@ export interface StigmerRunnerOptions {
|
|
|
86
87
|
/** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
|
|
87
88
|
readonly checkpointerProxyEndpoint?: string;
|
|
88
89
|
|
|
90
|
+
/** Artifact presign endpoint (stigmer#803). Falls back to proxyEndpoint. */
|
|
91
|
+
readonly artifactProxyEndpoint?: string;
|
|
92
|
+
|
|
89
93
|
/** Enable Cursor cloud mode for workspace-less execution. @default false */
|
|
90
94
|
readonly cloudModeEnabled?: boolean;
|
|
91
95
|
|
|
@@ -253,11 +257,6 @@ export async function createStigmerRunner(
|
|
|
253
257
|
// Only the worker connection consumes these coordinates — no activity dials
|
|
254
258
|
// Temporal directly (emit-event, the last one, now routes signals through
|
|
255
259
|
// the server's SendSignal lane; see oss#517).
|
|
256
|
-
//
|
|
257
|
-
// The static runner brings its own already-proxy-valid token (harness/CLI),
|
|
258
|
-
// so it does not consume the minted runner token from the bootstrap response;
|
|
259
|
-
// that proxy-credential lifecycle lives in createStigmerRunnerManager (the
|
|
260
|
-
// long-lived desktop host that needs it). Only the coordinates are used here.
|
|
261
260
|
const coordinates = await resolveRunnerBootstrap({
|
|
262
261
|
explicitAddress: options.temporalAddress,
|
|
263
262
|
explicitNamespace: options.temporalNamespace,
|
|
@@ -269,11 +268,44 @@ export async function createStigmerRunner(
|
|
|
269
268
|
// activity clients read the ref per request instead of pinning the boot
|
|
270
269
|
// token for the pod's whole life.
|
|
271
270
|
const tokenRef = { current: baseConfig.stigmerToken };
|
|
271
|
+
|
|
272
|
+
// Adopt the bootstrap-minted embedded_runner credential for gRPC runner-class
|
|
273
|
+
// calls (stigmer-cloud#507). The static path historically discarded it ("the
|
|
274
|
+
// static token is already proxy-valid") — true for the PROXY lane, but the
|
|
275
|
+
// ExecutionContext decrypt lane is gated on runner-class token_type
|
|
276
|
+
// (stigmer-cloud#152/#155): a user-token static runner (conformance harness,
|
|
277
|
+
// CLI daemon with a cloud token) had its scoped-token exchange refused and
|
|
278
|
+
// silently read REDACTED secret values. The coordinator owns the mint's TTL
|
|
279
|
+
// (same module the desktop manager uses — one refresh implementation, not
|
|
280
|
+
// two); its only sink here is the gRPC runner-credential ref, because the
|
|
281
|
+
// static host's proxy token is provided by the host and stays untouched.
|
|
282
|
+
// Servers that mint nothing (explicit-address boots, tokenless OSS, cloud
|
|
283
|
+
// sandboxes with baked credentials) leave the ref null — byte-identical
|
|
284
|
+
// behavior to before.
|
|
285
|
+
const runnerTokenRef: { current: string | null } = { current: null };
|
|
286
|
+
const runnerTokenCoordinator = createRunnerTokenCoordinator({
|
|
287
|
+
applyProxyToken: (token) => {
|
|
288
|
+
runnerTokenRef.current = token;
|
|
289
|
+
},
|
|
290
|
+
reMint: () =>
|
|
291
|
+
refreshRunnerAccessToken({
|
|
292
|
+
token: tokenRef.current,
|
|
293
|
+
stigmerEndpoint: baseConfig.stigmerBackendEndpoint,
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
if (coordinates.runnerAccessToken) {
|
|
297
|
+
runnerTokenCoordinator.adoptMintedToken(
|
|
298
|
+
coordinates.runnerAccessToken,
|
|
299
|
+
coordinates.runnerAccessTokenExpiresInSeconds,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
272
303
|
const config: Config = {
|
|
273
304
|
...baseConfig,
|
|
274
305
|
temporalAddress: coordinates.temporalAddress,
|
|
275
306
|
temporalNamespace: coordinates.temporalNamespace,
|
|
276
307
|
stigmerTokenRef: tokenRef,
|
|
308
|
+
stigmerRunnerTokenRef: runnerTokenRef,
|
|
277
309
|
};
|
|
278
310
|
markBoot("bootstrap_resolved");
|
|
279
311
|
|
|
@@ -359,6 +391,7 @@ export async function createStigmerRunner(
|
|
|
359
391
|
},
|
|
360
392
|
shutdown() {
|
|
361
393
|
tokenRenewal?.stop();
|
|
394
|
+
runnerTokenCoordinator.stop();
|
|
362
395
|
// Classification first, drain second: an in-flight activity cancelled
|
|
363
396
|
// by the drain must observe the signal already aborted (see
|
|
364
397
|
// shared/worker-shutdown.ts for the ownership contract).
|
|
@@ -418,6 +451,9 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
|
|
|
418
451
|
checkpointerProxyEndpoint: options.checkpointerProxyEndpoint
|
|
419
452
|
?? options.proxyEndpoint
|
|
420
453
|
?? null,
|
|
454
|
+
artifactProxyEndpoint: options.artifactProxyEndpoint
|
|
455
|
+
?? options.proxyEndpoint
|
|
456
|
+
?? null,
|
|
421
457
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
|
422
458
|
cursorStreamStallTimeoutMs:
|
|
423
459
|
options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
|
|
@@ -654,6 +654,7 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
654
654
|
runnerId: null,
|
|
655
655
|
checkpointerType: "memory" as const,
|
|
656
656
|
checkpointerProxyEndpoint: null,
|
|
657
|
+
artifactProxyEndpoint: null,
|
|
657
658
|
primaryModel: "gpt-4.1",
|
|
658
659
|
cursorStreamStallTimeoutMs: 180000,
|
|
659
660
|
agentResolveTimeoutMs: 120000,
|
|
@@ -672,10 +673,14 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
672
673
|
});
|
|
673
674
|
|
|
674
675
|
it("defaults to proxy in cloud mode", () => {
|
|
676
|
+
// loadConfig derives artifactProxyEndpoint from proxyEndpoint when the
|
|
677
|
+
// STIGMER_ARTIFACT_PROXY_ENDPOINT override is unset; Config literals here
|
|
678
|
+
// mirror that invariant.
|
|
675
679
|
const cfg = loadArtifactStorageConfig({
|
|
676
680
|
...baseConfig,
|
|
677
681
|
mode: "cloud",
|
|
678
682
|
proxyEndpoint: "https://proxy.example.com",
|
|
683
|
+
artifactProxyEndpoint: "https://proxy.example.com",
|
|
679
684
|
stigmerToken: "tok",
|
|
680
685
|
});
|
|
681
686
|
expect(cfg.type).toBe("proxy");
|
|
@@ -689,6 +694,7 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
689
694
|
...baseConfig,
|
|
690
695
|
mode: "local",
|
|
691
696
|
proxyEndpoint: "https://localhost:9090",
|
|
697
|
+
artifactProxyEndpoint: "https://localhost:9090",
|
|
692
698
|
stigmerToken: "tok",
|
|
693
699
|
});
|
|
694
700
|
expect(cfg.type).toBe("proxy");
|
|
@@ -696,6 +702,21 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
696
702
|
expect(cfg.proxyAuthToken).toBe("tok");
|
|
697
703
|
});
|
|
698
704
|
|
|
705
|
+
it("presigns against the artifact override when split from the LLM proxy endpoint (stigmer#803)", () => {
|
|
706
|
+
// The conformance harness points LLM traffic at a mock proxy that serves
|
|
707
|
+
// no presign routes; the artifact override routes storage at the real
|
|
708
|
+
// control plane independently (the checkpointer-override pattern).
|
|
709
|
+
process.env.ARTIFACT_STORAGE_TYPE = "proxy";
|
|
710
|
+
const cfg = loadArtifactStorageConfig({
|
|
711
|
+
...baseConfig,
|
|
712
|
+
proxyEndpoint: "https://mock-llm.example.com",
|
|
713
|
+
artifactProxyEndpoint: "https://service.example.com",
|
|
714
|
+
stigmerToken: "tok",
|
|
715
|
+
});
|
|
716
|
+
expect(cfg.type).toBe("proxy");
|
|
717
|
+
expect(cfg.proxyEndpoint).toBe("https://service.example.com");
|
|
718
|
+
});
|
|
719
|
+
|
|
699
720
|
it("honors ARTIFACT_STORAGE_TYPE=none even when a proxy endpoint is configured", () => {
|
|
700
721
|
// "none" is a deliberate operator/e2e posture — it must beat the
|
|
701
722
|
// storage-follows-transport default exactly like the other overrides.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory capture attachment (DD-005 D1): the recall-snapshot gate
|
|
3
|
+
* (enabled bit, zero-facts included), both connection shapes with the
|
|
4
|
+
* capture-context carriers, the structural approval-freedom of
|
|
5
|
+
* synthesized attachments, and the cross-repo string pins (route +
|
|
6
|
+
* context keys, guarded here and in the mcp-server memory integration
|
|
7
|
+
* test — the TOOL_CALL_LIMIT precedent). The slug and roster are
|
|
8
|
+
* runner-internal and guarded here alone.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { create } from "@bufbuild/protobuf";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { RecalledMemoriesSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
14
|
+
|
|
15
|
+
import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js";
|
|
16
|
+
import { needsBackfill } from "../connect-backfill.js";
|
|
17
|
+
import {
|
|
18
|
+
MEMORY_AGENT_ID_ENV,
|
|
19
|
+
MEMORY_AGENT_ID_HEADER,
|
|
20
|
+
MEMORY_ATTACHMENT_SLUG,
|
|
21
|
+
MEMORY_EXECUTION_ID_ENV,
|
|
22
|
+
MEMORY_EXECUTION_ID_HEADER,
|
|
23
|
+
MEMORY_ORG_ENV,
|
|
24
|
+
MEMORY_ORG_HEADER,
|
|
25
|
+
MEMORY_ROUTE,
|
|
26
|
+
MEMORY_SESSION_ID_ENV,
|
|
27
|
+
MEMORY_SESSION_ID_HEADER,
|
|
28
|
+
memoryCaptureEnabled,
|
|
29
|
+
synthesizeMemoryAttachment,
|
|
30
|
+
type MemoryCaptureContext,
|
|
31
|
+
} from "../memory-attachment.js";
|
|
32
|
+
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
33
|
+
|
|
34
|
+
const context: MemoryCaptureContext = {
|
|
35
|
+
org: "acme",
|
|
36
|
+
agentId: "agt_1",
|
|
37
|
+
sessionId: "ses_1",
|
|
38
|
+
agentExecutionId: "aex_1",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const cloudOptions = {
|
|
42
|
+
bridgeEndpoint: "https://mcp.stigmer.ai/",
|
|
43
|
+
credential: "sandbox-token",
|
|
44
|
+
backendEndpoint: "http://localhost:7234",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ossOptions = {
|
|
48
|
+
bridgeEndpoint: null,
|
|
49
|
+
credential: null,
|
|
50
|
+
backendEndpoint: "http://localhost:7234",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const noLeases: ActiveLeases = {
|
|
54
|
+
global: false,
|
|
55
|
+
categories: new Set(),
|
|
56
|
+
servers: new Set(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe("memoryCaptureEnabled (the DD-005 D1 injection signal)", () => {
|
|
60
|
+
it("is the snapshot's enabled bit — absent and disabled read false", () => {
|
|
61
|
+
expect(memoryCaptureEnabled(undefined)).toBe(false);
|
|
62
|
+
expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: false }))).toBe(false);
|
|
63
|
+
expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: true }))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("enabled with ZERO facts still offers the tool — memory on, nothing stored yet", () => {
|
|
67
|
+
const snapshot = create(RecalledMemoriesSchema, { enabled: true, facts: [] });
|
|
68
|
+
expect(memoryCaptureEnabled(snapshot)).toBe(true);
|
|
69
|
+
expect(synthesizeMemoryAttachment(snapshot, context, cloudOptions)).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("synthesizeMemoryAttachment", () => {
|
|
74
|
+
const enabled = create(RecalledMemoriesSchema, { enabled: true });
|
|
75
|
+
|
|
76
|
+
it("returns undefined when the snapshot is absent or disabled — honest absence", () => {
|
|
77
|
+
expect(synthesizeMemoryAttachment(undefined, context, cloudOptions)).toBeUndefined();
|
|
78
|
+
expect(
|
|
79
|
+
synthesizeMemoryAttachment(
|
|
80
|
+
create(RecalledMemoriesSchema, { enabled: false }),
|
|
81
|
+
context,
|
|
82
|
+
cloudOptions,
|
|
83
|
+
),
|
|
84
|
+
).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("builds the HTTP shape against the bridge /memory route with credential + context headers", () => {
|
|
88
|
+
const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions);
|
|
89
|
+
expect(attachment).toMatchObject({
|
|
90
|
+
slug: MEMORY_ATTACHMENT_SLUG,
|
|
91
|
+
connectionType: "http",
|
|
92
|
+
url: "https://mcp.stigmer.ai/memory",
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: "Bearer sandbox-token",
|
|
95
|
+
[MEMORY_ORG_HEADER]: "acme",
|
|
96
|
+
[MEMORY_AGENT_ID_HEADER]: "agt_1",
|
|
97
|
+
[MEMORY_SESSION_ID_HEADER]: "ses_1",
|
|
98
|
+
[MEMORY_EXECUTION_ID_HEADER]: "aex_1",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("builds the OSS stdio shape: the CLI-embedded bridge with the memory roster + context env", () => {
|
|
104
|
+
const attachment = synthesizeMemoryAttachment(enabled, context, ossOptions);
|
|
105
|
+
expect(attachment).toMatchObject({
|
|
106
|
+
connectionType: "stdio",
|
|
107
|
+
command: "stigmer",
|
|
108
|
+
args: ["mcp-server"],
|
|
109
|
+
env: {
|
|
110
|
+
STIGMER_MCP_ROSTER: "memory",
|
|
111
|
+
STIGMER_SERVER_ADDRESS: "localhost:7234",
|
|
112
|
+
[MEMORY_ORG_ENV]: "acme",
|
|
113
|
+
[MEMORY_AGENT_ID_ENV]: "agt_1",
|
|
114
|
+
[MEMORY_SESSION_ID_ENV]: "ses_1",
|
|
115
|
+
[MEMORY_EXECUTION_ID_ENV]: "aex_1",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("omits empty context fields from the carrier — best-effort attribution, never blank entries", () => {
|
|
121
|
+
const partial: MemoryCaptureContext = {
|
|
122
|
+
org: "acme",
|
|
123
|
+
agentId: "",
|
|
124
|
+
sessionId: "ses_1",
|
|
125
|
+
agentExecutionId: "",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const http = synthesizeMemoryAttachment(enabled, partial, cloudOptions);
|
|
129
|
+
expect(Object.keys(http?.headers ?? {}).sort()).toEqual([
|
|
130
|
+
"Authorization",
|
|
131
|
+
MEMORY_ORG_HEADER,
|
|
132
|
+
MEMORY_SESSION_ID_HEADER,
|
|
133
|
+
].sort());
|
|
134
|
+
|
|
135
|
+
const stdio = synthesizeMemoryAttachment(enabled, partial, ossOptions);
|
|
136
|
+
expect(stdio?.env).not.toHaveProperty(MEMORY_AGENT_ID_ENV);
|
|
137
|
+
expect(stdio?.env).not.toHaveProperty(MEMORY_EXECUTION_ID_ENV);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("pins the cross-repo strings the mcp-server side guards too", () => {
|
|
141
|
+
expect(MEMORY_ATTACHMENT_SLUG).toBe("stigmer-memory");
|
|
142
|
+
expect(MEMORY_ROUTE).toBe("/memory");
|
|
143
|
+
expect(MEMORY_ORG_HEADER).toBe("x-stigmer-memory-org");
|
|
144
|
+
expect(MEMORY_AGENT_ID_HEADER).toBe("x-stigmer-memory-agent-id");
|
|
145
|
+
expect(MEMORY_SESSION_ID_HEADER).toBe("x-stigmer-memory-session-id");
|
|
146
|
+
expect(MEMORY_EXECUTION_ID_HEADER).toBe("x-stigmer-memory-execution-id");
|
|
147
|
+
expect(MEMORY_ORG_ENV).toBe("STIGMER_MEMORY_ORG");
|
|
148
|
+
expect(MEMORY_AGENT_ID_ENV).toBe("STIGMER_MEMORY_AGENT_ID");
|
|
149
|
+
expect(MEMORY_SESSION_ID_ENV).toBe("STIGMER_MEMORY_SESSION_ID");
|
|
150
|
+
expect(MEMORY_EXECUTION_ID_ENV).toBe("STIGMER_MEMORY_EXECUTION_ID");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("is approval-free by construction: zero entries in the merged approval map", () => {
|
|
154
|
+
// Consent is the confirm RPC, not tool approval (DD-005 D3): the
|
|
155
|
+
// tool only creates a proposal, so gating it would stack a second
|
|
156
|
+
// consent gate in front of the real one.
|
|
157
|
+
const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
|
|
158
|
+
const merged = mergeApprovalPolicies([attachment as ResolvedMcpServer], noLeases);
|
|
159
|
+
expect(merged.size).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
|
|
163
|
+
const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
|
|
164
|
+
expect(attachment.discoveredCapabilitiesEmpty).toBe(false);
|
|
165
|
+
expect(needsBackfill(attachment)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|