@stigmer/runner 3.2.3 → 3.3.0
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/blueprint-resolver.d.ts +9 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +2 -0
- package/dist/activities/execute-cursor/index.js +28 -1
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
- package/dist/activities/execute-cursor/prompt-builder.js +4 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +24 -2
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/runner-manager.js +3 -0
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +79 -0
- package/dist/shared/datastore-attachment.js +129 -0
- package/dist/shared/datastore-attachment.js.map +1 -0
- package/package.json +2 -2
- 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/execute-cursor/__tests__/build-prompt.test.ts +25 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
- package/src/activities/execute-cursor/index.ts +37 -1
- package/src/activities/execute-cursor/prompt-builder.ts +12 -1
- 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__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
- package/src/activities/execute-deep-agent/setup.ts +30 -2
- package/src/config.ts +11 -0
- package/src/runner-manager.ts +3 -0
- package/src/runner.ts +3 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
- package/src/shared/datastore-attachment.ts +166 -0
|
@@ -55,7 +55,11 @@ import { DeltaEnricher } from "./delta-enricher.js";
|
|
|
55
55
|
import { TodoTracker } from "./todo-tracker.js";
|
|
56
56
|
import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
57
57
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
58
|
-
import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
58
|
+
import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
59
|
+
import {
|
|
60
|
+
injectDatastoreAttachment,
|
|
61
|
+
synthesizeDatastoreAttachment,
|
|
62
|
+
} from "../../shared/datastore-attachment.js";
|
|
59
63
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
60
64
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
61
65
|
import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
|
|
@@ -583,6 +587,33 @@ async function executeCursorInner(
|
|
|
583
587
|
client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
|
|
584
588
|
heartbeat, secretKeys,
|
|
585
589
|
);
|
|
590
|
+
|
|
591
|
+
// Phase 4a2: Synthesize the datastore records attachment (T05).
|
|
592
|
+
// Deliberately AFTER resolve + backfill: the attachment has no
|
|
593
|
+
// McpServerUsage and reports discovered capabilities, so the
|
|
594
|
+
// backfill's destructiveHint tightener can never force-gate
|
|
595
|
+
// delete_record (which on channels would be silently skipped).
|
|
596
|
+
// Empty approval maps keep it approval-free by construction.
|
|
597
|
+
if (blueprint.datastoreUsages.length > 0) {
|
|
598
|
+
const scopedCredential =
|
|
599
|
+
(await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
|
|
600
|
+
?? config.stigmerTokenRef?.current
|
|
601
|
+
?? config.stigmerToken;
|
|
602
|
+
const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
|
|
603
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
604
|
+
credential: scopedCredential,
|
|
605
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
606
|
+
});
|
|
607
|
+
if (attachment) {
|
|
608
|
+
const resolvedServers = injectDatastoreAttachment(
|
|
609
|
+
mcpResolution.resolvedServers, attachment,
|
|
610
|
+
);
|
|
611
|
+
mcpResolution = {
|
|
612
|
+
resolvedServers,
|
|
613
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
586
617
|
const mcpConfig = mcpResolution.cursorConfig;
|
|
587
618
|
|
|
588
619
|
// Phase 4b: Merge approval policies from all layers.
|
|
@@ -920,6 +951,7 @@ async function executeCursorInner(
|
|
|
920
951
|
instructions: blueprint.instructions,
|
|
921
952
|
userMessage: spec.message,
|
|
922
953
|
skills: skillMetadata,
|
|
954
|
+
datastoreUsages: blueprint.datastoreUsages,
|
|
923
955
|
subAgents: blueprint.subAgents,
|
|
924
956
|
workspaceDirs: blueprint.workspaceDirs,
|
|
925
957
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -1500,6 +1532,7 @@ async function executeCursorInner(
|
|
|
1500
1532
|
instructions: blueprint.instructions,
|
|
1501
1533
|
userMessage: spec.message,
|
|
1502
1534
|
skills: skillMetadata,
|
|
1535
|
+
datastoreUsages: blueprint.datastoreUsages,
|
|
1503
1536
|
subAgents: blueprint.subAgents,
|
|
1504
1537
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1505
1538
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -2056,6 +2089,8 @@ export interface BuildPromptInput {
|
|
|
2056
2089
|
instructions: string;
|
|
2057
2090
|
userMessage: string;
|
|
2058
2091
|
skills: import("./prompt-builder.js").SkillMetadata[];
|
|
2092
|
+
/** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
|
|
2093
|
+
datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
|
|
2059
2094
|
subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
|
|
2060
2095
|
workspaceDirs: string[];
|
|
2061
2096
|
workspaceFileRefs: string[];
|
|
@@ -2155,6 +2190,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2155
2190
|
instructions,
|
|
2156
2191
|
userMessage,
|
|
2157
2192
|
skills,
|
|
2193
|
+
datastoreUsages: input.datastoreUsages ?? [],
|
|
2158
2194
|
subAgents,
|
|
2159
2195
|
workspaceDirs,
|
|
2160
2196
|
workspaceFileRefs,
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
|
-
import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
20
|
+
import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
21
21
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
22
22
|
import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
23
23
|
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
24
|
+
import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
|
|
24
25
|
import {
|
|
25
26
|
formatSenderIdentityText,
|
|
26
27
|
type SenderIdentity,
|
|
@@ -53,6 +54,12 @@ export interface EnhancedPromptOptions {
|
|
|
53
54
|
instructions: string;
|
|
54
55
|
userMessage: string;
|
|
55
56
|
skills: SkillMetadata[];
|
|
57
|
+
/**
|
|
58
|
+
* Datastores attached via `datastore_usages` — rendered as the
|
|
59
|
+
* `<available_datastores>` section (DD-005 SD-5, skills precedent)
|
|
60
|
+
* pointing the model at the synthesized record tools.
|
|
61
|
+
*/
|
|
62
|
+
datastoreUsages?: DatastoreUsage[];
|
|
56
63
|
subAgents: SubAgent[];
|
|
57
64
|
workspaceDirs: string[];
|
|
58
65
|
workspaceFileRefs: string[];
|
|
@@ -113,6 +120,10 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
113
120
|
sections.push(formatSkillsSection(options.skills));
|
|
114
121
|
}
|
|
115
122
|
|
|
123
|
+
if (options.datastoreUsages !== undefined && options.datastoreUsages.length > 0) {
|
|
124
|
+
sections.push(formatDatastoresSection(options.datastoreUsages));
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
if (options.subAgents.length > 0) {
|
|
117
128
|
sections.push(formatSubAgentsSection(options.subAgents));
|
|
118
129
|
}
|
|
@@ -239,6 +239,7 @@ const httpConfig: Config = {
|
|
|
239
239
|
temporalAddress: "localhost:7233",
|
|
240
240
|
temporalNamespace: "default",
|
|
241
241
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
242
|
+
mcpBridgeEndpoint: null,
|
|
242
243
|
stigmerToken: null,
|
|
243
244
|
cursorApiKey: "",
|
|
244
245
|
workspaceRootDir: "/tmp/test",
|
|
@@ -284,6 +284,7 @@ const baseConfig: Config = {
|
|
|
284
284
|
temporalAddress: "localhost:7233",
|
|
285
285
|
temporalNamespace: "default",
|
|
286
286
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
287
|
+
mcpBridgeEndpoint: null,
|
|
287
288
|
stigmerToken: null,
|
|
288
289
|
cursorApiKey: "",
|
|
289
290
|
workspaceRootDir: "/tmp/test",
|
|
@@ -238,6 +238,7 @@ const baseConfig: Config = {
|
|
|
238
238
|
temporalAddress: "localhost:7233",
|
|
239
239
|
temporalNamespace: "default",
|
|
240
240
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
241
|
+
mcpBridgeEndpoint: null,
|
|
241
242
|
stigmerToken: null,
|
|
242
243
|
cursorApiKey: "",
|
|
243
244
|
workspaceRootDir: "/tmp/test",
|
|
@@ -35,6 +35,7 @@ describe("ExecuteDeepAgent activity", () => {
|
|
|
35
35
|
temporalAddress: "localhost:7233",
|
|
36
36
|
temporalNamespace: "default",
|
|
37
37
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
38
|
+
mcpBridgeEndpoint: null,
|
|
38
39
|
stigmerToken: null,
|
|
39
40
|
cursorApiKey: "",
|
|
40
41
|
workspaceRootDir: "/tmp/test",
|
|
@@ -244,6 +244,7 @@ const memoryConfig: Config = {
|
|
|
244
244
|
temporalAddress: "localhost:7233",
|
|
245
245
|
temporalNamespace: "default",
|
|
246
246
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
247
|
+
mcpBridgeEndpoint: null,
|
|
247
248
|
stigmerToken: null,
|
|
248
249
|
cursorApiKey: "",
|
|
249
250
|
workspaceRootDir: "/tmp/test",
|
|
@@ -92,6 +92,11 @@ export interface PromptBuilderInput {
|
|
|
92
92
|
provisionResults: ProvisionResult[];
|
|
93
93
|
containerRoot: string;
|
|
94
94
|
skillsPromptSection: string;
|
|
95
|
+
/**
|
|
96
|
+
* The `<available_datastores>` section (shared/datastore-attachment.ts
|
|
97
|
+
* formatDatastoresSection); empty when the agent uses no datastores.
|
|
98
|
+
*/
|
|
99
|
+
datastoresPromptSection?: string;
|
|
95
100
|
workspaceFileRefs: string[];
|
|
96
101
|
workspaceRoot: string;
|
|
97
102
|
injectedFiles: InjectedFile[];
|
|
@@ -156,6 +161,10 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
156
161
|
prompt += input.skillsPromptSection;
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
if (input.datastoresPromptSection) {
|
|
165
|
+
prompt += "\n\n" + input.datastoresPromptSection;
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
if (input.workspaceFileRefs.length > 0) {
|
|
160
169
|
const refSection = buildReferencedFilesSection(
|
|
161
170
|
input.workspaceFileRefs,
|
|
@@ -28,6 +28,11 @@ import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
|
28
28
|
import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
|
|
29
29
|
import { resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
30
30
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
31
|
+
import {
|
|
32
|
+
formatDatastoresSection,
|
|
33
|
+
injectDatastoreAttachment,
|
|
34
|
+
synthesizeDatastoreAttachment,
|
|
35
|
+
} from "../../shared/datastore-attachment.js";
|
|
31
36
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
32
37
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
33
38
|
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
@@ -314,16 +319,17 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
314
319
|
...(agent.spec!.mcpServerUsages || []),
|
|
315
320
|
...(session.spec!.mcpServerUsages || []),
|
|
316
321
|
];
|
|
322
|
+
const datastoreUsages = agent.spec!.datastoreUsages || [];
|
|
317
323
|
|
|
318
324
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
319
|
-
if (mcpServerUsages.length > 0) {
|
|
325
|
+
if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
|
|
320
326
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
321
327
|
resolvedMcpServers = await resolveMcpServers(
|
|
322
328
|
client, mcpServerUsages, envResult.mergedEnvVars,
|
|
323
329
|
);
|
|
324
330
|
|
|
325
331
|
const sessionOrg = session.metadata?.org ?? "";
|
|
326
|
-
|
|
332
|
+
let backfilledServers = await backfillMcpServersIfNeeded(
|
|
327
333
|
client,
|
|
328
334
|
resolvedMcpServers.resolvedServers,
|
|
329
335
|
mcpServerUsages,
|
|
@@ -332,6 +338,25 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
332
338
|
undefined,
|
|
333
339
|
envResult.secretKeys,
|
|
334
340
|
);
|
|
341
|
+
|
|
342
|
+
// The datastore records attachment (T05) — injected AFTER resolve +
|
|
343
|
+
// backfill so the destructiveHint tightener can never gate it; empty
|
|
344
|
+
// approval maps keep it approval-free by construction (see
|
|
345
|
+
// shared/datastore-attachment.ts).
|
|
346
|
+
if (datastoreUsages.length > 0) {
|
|
347
|
+
const scopedCredential =
|
|
348
|
+
(await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
|
|
349
|
+
?? config.stigmerTokenRef?.current
|
|
350
|
+
?? config.stigmerToken;
|
|
351
|
+
const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
|
|
352
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
353
|
+
credential: scopedCredential,
|
|
354
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
355
|
+
});
|
|
356
|
+
if (attachment) {
|
|
357
|
+
backfilledServers = injectDatastoreAttachment(backfilledServers, attachment);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
335
360
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
336
361
|
|
|
337
362
|
mcpConnection = await connectMcpServers(
|
|
@@ -398,6 +423,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
398
423
|
provisionResults,
|
|
399
424
|
containerRoot: workspaceBackend.rootDir,
|
|
400
425
|
skillsPromptSection,
|
|
426
|
+
datastoresPromptSection: datastoreUsages.length > 0
|
|
427
|
+
? formatDatastoresSection(datastoreUsages)
|
|
428
|
+
: undefined,
|
|
401
429
|
workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
|
|
402
430
|
workspaceRoot: workspaceBackend.rootDir,
|
|
403
431
|
injectedFiles,
|
package/src/config.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface Config {
|
|
|
60
60
|
readonly temporalNamespace: string;
|
|
61
61
|
readonly stigmerBackendEndpoint: string;
|
|
62
62
|
readonly stigmerToken: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* The MCP bridge endpoint for the runner-synthesized datastore
|
|
65
|
+
* records attachment (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
|
|
66
|
+
* https://mcp.stigmer.ai). Null selects the OSS/local shape: a
|
|
67
|
+
* spawned `stigmer mcp-server` stdio child against the local backend.
|
|
68
|
+
* See shared/datastore-attachment.ts.
|
|
69
|
+
*/
|
|
70
|
+
readonly mcpBridgeEndpoint: string | null;
|
|
63
71
|
/**
|
|
64
72
|
* Bearer credential for the Cursor SDK's Connect RPC transport.
|
|
65
73
|
*
|
|
@@ -153,6 +161,8 @@ export function loadConfig(): Config {
|
|
|
153
161
|
? requireEnv("STIGMER_TOKEN")
|
|
154
162
|
: (process.env.STIGMER_TOKEN ?? null);
|
|
155
163
|
|
|
164
|
+
const mcpBridgeEndpoint = process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null;
|
|
165
|
+
|
|
156
166
|
// In proxy mode, pass STIGMER_TOKEN as the SDK's API key. The SDK exchanges
|
|
157
167
|
// it (via REST proxy → Tomcat → Cursor) for an access token. The HTTP/2
|
|
158
168
|
// interceptor injects x-stigmer-auth for BiDi proxy authentication while
|
|
@@ -202,6 +212,7 @@ export function loadConfig(): Config {
|
|
|
202
212
|
temporalNamespace,
|
|
203
213
|
stigmerBackendEndpoint,
|
|
204
214
|
stigmerToken,
|
|
215
|
+
mcpBridgeEndpoint,
|
|
205
216
|
cursorApiKey,
|
|
206
217
|
workspaceRootDir,
|
|
207
218
|
mode,
|
package/src/runner-manager.ts
CHANGED
|
@@ -583,6 +583,9 @@ export function mapManagerOptionsToConfig(
|
|
|
583
583
|
temporalNamespace: options.temporalNamespace ?? "default",
|
|
584
584
|
stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
|
|
585
585
|
stigmerToken: options.stigmerToken ?? null,
|
|
586
|
+
// Env-only like the env-loaded path (loadConfig): the bridge endpoint
|
|
587
|
+
// is deployment topology, not per-session state.
|
|
588
|
+
mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
|
|
586
589
|
stigmerTokenRef: tokenRef,
|
|
587
590
|
stigmerRunnerTokenRef: runnerTokenRef,
|
|
588
591
|
cursorApiKey: proxyActive
|
package/src/runner.ts
CHANGED
|
@@ -268,6 +268,9 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
|
|
|
268
268
|
temporalNamespace: options.temporalNamespace ?? "default",
|
|
269
269
|
stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
|
|
270
270
|
stigmerToken: options.stigmerToken ?? null,
|
|
271
|
+
// Env-only like the env-loaded path (loadConfig): the bridge endpoint
|
|
272
|
+
// is deployment topology, not per-session state.
|
|
273
|
+
mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
|
|
271
274
|
cursorApiKey: proxyActive
|
|
272
275
|
? (options.cursorApiKey ?? "proxy-managed")
|
|
273
276
|
: (options.cursorApiKey ?? ""),
|
|
@@ -420,6 +420,7 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
420
420
|
temporalAddress: "localhost:7233",
|
|
421
421
|
temporalNamespace: "default",
|
|
422
422
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
423
|
+
mcpBridgeEndpoint: null,
|
|
423
424
|
cursorApiKey: "",
|
|
424
425
|
workspaceRootDir: "/tmp",
|
|
425
426
|
maxConcurrentActivities: 5,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The synthesized datastore records attachment (T05): connection
|
|
2
|
+
// shapes, the approval-free-by-construction property, and the
|
|
3
|
+
// structural immunity to the connect backfill's destructiveHint
|
|
4
|
+
// tightener — the two hazards that would otherwise silently skip
|
|
5
|
+
// record writes on channels (UNATTENDED mode).
|
|
6
|
+
|
|
7
|
+
import { create } from "@bufbuild/protobuf";
|
|
8
|
+
import { DatastoreUsageSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
9
|
+
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js";
|
|
13
|
+
import { needsBackfill } from "../connect-backfill.js";
|
|
14
|
+
import {
|
|
15
|
+
DATASTORE_ATTACHMENT_SLUG,
|
|
16
|
+
formatDatastoresSection,
|
|
17
|
+
injectDatastoreAttachment,
|
|
18
|
+
synthesizeDatastoreAttachment,
|
|
19
|
+
} from "../datastore-attachment.js";
|
|
20
|
+
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
21
|
+
|
|
22
|
+
function usage(slug: string) {
|
|
23
|
+
return create(DatastoreUsageSchema, {
|
|
24
|
+
datastoreRef: create(ApiResourceReferenceSchema, { slug }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NO_LEASES: ActiveLeases = {
|
|
29
|
+
global: false,
|
|
30
|
+
categories: new Set(),
|
|
31
|
+
servers: new Set(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("synthesizeDatastoreAttachment", () => {
|
|
35
|
+
it("returns undefined when the agent uses no datastores", () => {
|
|
36
|
+
expect(
|
|
37
|
+
synthesizeDatastoreAttachment([], {
|
|
38
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
39
|
+
credential: "tok",
|
|
40
|
+
backendEndpoint: "http://localhost:7234",
|
|
41
|
+
}),
|
|
42
|
+
).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("builds the HTTP shape against the bridge /records route with the injected credential", () => {
|
|
46
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
47
|
+
bridgeEndpoint: "https://mcp.stigmer.ai/",
|
|
48
|
+
credential: "sandbox-token",
|
|
49
|
+
backendEndpoint: "http://localhost:7234",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(attachment).toMatchObject({
|
|
53
|
+
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
54
|
+
connectionType: "http",
|
|
55
|
+
url: "https://mcp.stigmer.ai/records",
|
|
56
|
+
headers: { Authorization: "Bearer sandbox-token" },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("omits the Authorization header without a credential", () => {
|
|
61
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
62
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
63
|
+
credential: null,
|
|
64
|
+
backendEndpoint: "http://localhost:7234",
|
|
65
|
+
});
|
|
66
|
+
expect(attachment?.headers).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("builds the OSS stdio shape: the CLI-embedded bridge with the records roster", () => {
|
|
70
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
71
|
+
bridgeEndpoint: null,
|
|
72
|
+
credential: null,
|
|
73
|
+
backendEndpoint: "http://localhost:7234",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(attachment).toMatchObject({
|
|
77
|
+
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
78
|
+
connectionType: "stdio",
|
|
79
|
+
command: "stigmer",
|
|
80
|
+
args: ["mcp-server"],
|
|
81
|
+
env: {
|
|
82
|
+
STIGMER_MCP_ROSTER: "records",
|
|
83
|
+
STIGMER_SERVER_ADDRESS: "localhost:7234",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is approval-free by construction: zero entries in the merged approval map", () => {
|
|
89
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
90
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
91
|
+
credential: "tok",
|
|
92
|
+
backendEndpoint: "http://localhost:7234",
|
|
93
|
+
})!;
|
|
94
|
+
|
|
95
|
+
// Absence from the map means auto-approved for EVERY consumer (the
|
|
96
|
+
// Cursor hook, the deep-agent gate) — DD-001 SD-3's structural bypass.
|
|
97
|
+
const merged = mergeApprovalPolicies([attachment], [], NO_LEASES);
|
|
98
|
+
expect(merged.size).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
|
|
102
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
103
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
104
|
+
credential: "tok",
|
|
105
|
+
backendEndpoint: "http://localhost:7234",
|
|
106
|
+
})!;
|
|
107
|
+
|
|
108
|
+
// If this ever became true, the backfill's connect + classification
|
|
109
|
+
// would force-gate delete_record (it carries destructiveHint), and on
|
|
110
|
+
// channels a gated tool is silently auto-skipped.
|
|
111
|
+
expect(attachment.discoveredCapabilitiesEmpty).toBe(false);
|
|
112
|
+
expect(needsBackfill(attachment)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("injectDatastoreAttachment", () => {
|
|
117
|
+
const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
|
|
118
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
119
|
+
credential: "tok",
|
|
120
|
+
backendEndpoint: "http://localhost:7234",
|
|
121
|
+
})!;
|
|
122
|
+
|
|
123
|
+
it("appends to the resolved servers", () => {
|
|
124
|
+
const other: ResolvedMcpServer = {
|
|
125
|
+
slug: "github",
|
|
126
|
+
connectionType: "http",
|
|
127
|
+
url: "https://example.com",
|
|
128
|
+
toolApprovals: [],
|
|
129
|
+
pinnedToolApprovals: [],
|
|
130
|
+
discoveredCapabilitiesEmpty: false,
|
|
131
|
+
};
|
|
132
|
+
const result = injectDatastoreAttachment([other], attachment);
|
|
133
|
+
expect(result.map((s) => s.slug)).toEqual(["github", DATASTORE_ATTACHMENT_SLUG]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("replaces a user server shadowing the reserved slug, loudly", () => {
|
|
137
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
138
|
+
const impostor: ResolvedMcpServer = {
|
|
139
|
+
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
140
|
+
connectionType: "http",
|
|
141
|
+
url: "https://evil.example.com",
|
|
142
|
+
toolApprovals: [],
|
|
143
|
+
pinnedToolApprovals: [],
|
|
144
|
+
discoveredCapabilitiesEmpty: false,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = injectDatastoreAttachment([impostor], attachment);
|
|
148
|
+
|
|
149
|
+
expect(result).toHaveLength(1);
|
|
150
|
+
expect(result[0].url).toBe("https://mcp.stigmer.ai/records");
|
|
151
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("reserved"));
|
|
152
|
+
warnSpy.mockRestore();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("formatDatastoresSection", () => {
|
|
157
|
+
it("names the attached datastores and points at describe_datastore first", () => {
|
|
158
|
+
const section = formatDatastoresSection([usage("clinic"), usage("inventory")]);
|
|
159
|
+
expect(section).toContain("<available_datastores>");
|
|
160
|
+
expect(section).toContain("- clinic");
|
|
161
|
+
expect(section).toContain("- inventory");
|
|
162
|
+
expect(section).toContain("describe_datastore");
|
|
163
|
+
expect(section).toContain("</available_datastores>");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner-synthesized datastore records attachment (T05, DD-001 SD-2).
|
|
3
|
+
*
|
|
4
|
+
* When an agent declares `datastore_usages`, the runner synthesizes ONE
|
|
5
|
+
* MCP attachment serving the five record tools (DD-005) — the agent
|
|
6
|
+
* builder writes a usage line and gets tools; no McpServer resource, no
|
|
7
|
+
* Environment, no credential in any manifest. Harness-agnostic like
|
|
8
|
+
* mcp-resolver.ts: both ExecuteCursor and ExecuteDeepAgent inject the
|
|
9
|
+
* synthesized entry through their existing ResolvedMcpServer paths.
|
|
10
|
+
*
|
|
11
|
+
* Two connection shapes, one roster (the bridge's records-only roster,
|
|
12
|
+
* T05 R1):
|
|
13
|
+
* - Bridge endpoint configured (cloud): Streamable HTTP against the
|
|
14
|
+
* bridge's /records route, with the execution's own session-scoped
|
|
15
|
+
* credential as the Bearer token — the server's reach chain (DD-006
|
|
16
|
+
* Path 1) authorizes from that token; the bridge stays a
|
|
17
|
+
* non-validating passthrough.
|
|
18
|
+
* - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
|
|
19
|
+
* stdio child with STIGMER_MCP_ROSTER=records against the local
|
|
20
|
+
* backend (zero new distribution — the CLI already embeds the
|
|
21
|
+
* bridge). No credential: the local backend is unauthenticated and
|
|
22
|
+
* OSS callers resolve as the local principal (T05 R2).
|
|
23
|
+
*
|
|
24
|
+
* Approval-free by construction (DD-001 SD-3): empty toolApprovals +
|
|
25
|
+
* pinnedToolApprovals mean mergeApprovalPolicies emits no entries for
|
|
26
|
+
* this server — zero classifier involvement. `discoveredCapabilitiesEmpty`
|
|
27
|
+
* is false and the attachment has no McpServerUsage, so the connect
|
|
28
|
+
* backfill (whose destructiveHint tightener would force-gate
|
|
29
|
+
* delete_record — silently skipped on channels under UNATTENDED mode)
|
|
30
|
+
* is structurally unable to touch it. Callers must still inject AFTER
|
|
31
|
+
* resolve + backfill; both harness call sites do.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { DatastoreUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
35
|
+
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The synthesized attachment's slug. Reserved: a user McpServer with
|
|
39
|
+
* this slug is shadowed by the synthesized attachment (the record tools
|
|
40
|
+
* must exist whenever `datastore_usages` says so), with a warning.
|
|
41
|
+
*/
|
|
42
|
+
export const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
|
|
43
|
+
|
|
44
|
+
/** The bridge route serving the records-only roster (mcp-server T05 R1). */
|
|
45
|
+
export const RECORDS_ROUTE = "/records";
|
|
46
|
+
|
|
47
|
+
export interface DatastoreAttachmentOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The bridge's HTTP endpoint (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
|
|
50
|
+
* https://mcp.stigmer.ai). Null selects the OSS stdio shape.
|
|
51
|
+
*/
|
|
52
|
+
bridgeEndpoint: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The execution's session-scoped credential (the sandbox token a
|
|
55
|
+
* cloud runner holds, or the desktop runner's exchanged scoped
|
|
56
|
+
* token). Null attaches no Authorization header (OSS/local).
|
|
57
|
+
*/
|
|
58
|
+
credential: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The stigmer backend endpoint the stdio child dials
|
|
61
|
+
* (config.stigmerBackendEndpoint). Only used for the OSS shape.
|
|
62
|
+
*/
|
|
63
|
+
backendEndpoint: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Synthesize the records attachment for an agent's datastore usages.
|
|
68
|
+
* Returns undefined when the agent uses no datastores — the attachment
|
|
69
|
+
* exists exactly when the usage edge does.
|
|
70
|
+
*/
|
|
71
|
+
export function synthesizeDatastoreAttachment(
|
|
72
|
+
datastoreUsages: DatastoreUsage[],
|
|
73
|
+
options: DatastoreAttachmentOptions,
|
|
74
|
+
): ResolvedMcpServer | undefined {
|
|
75
|
+
if (datastoreUsages.length === 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Approval-free by construction + backfill-proof: see file header.
|
|
80
|
+
const base = {
|
|
81
|
+
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
82
|
+
toolApprovals: [],
|
|
83
|
+
pinnedToolApprovals: [],
|
|
84
|
+
discoveredCapabilitiesEmpty: false,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
connectionType: "http",
|
|
91
|
+
url: options.bridgeEndpoint.replace(/\/+$/, "") + RECORDS_ROUTE,
|
|
92
|
+
headers: options.credential !== null && options.credential !== ""
|
|
93
|
+
? { Authorization: `Bearer ${options.credential}` }
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...base,
|
|
100
|
+
connectionType: "stdio",
|
|
101
|
+
command: "stigmer",
|
|
102
|
+
args: ["mcp-server"],
|
|
103
|
+
env: {
|
|
104
|
+
STIGMER_MCP_ROSTER: "records",
|
|
105
|
+
STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Inject the synthesized attachment into a resolved server list —
|
|
112
|
+
* AFTER resolve + backfill (see file header). A user server shadowing
|
|
113
|
+
* the reserved slug is replaced, loudly.
|
|
114
|
+
*/
|
|
115
|
+
export function injectDatastoreAttachment(
|
|
116
|
+
resolvedServers: ResolvedMcpServer[],
|
|
117
|
+
attachment: ResolvedMcpServer,
|
|
118
|
+
): ResolvedMcpServer[] {
|
|
119
|
+
const shadowed = resolvedServers.some((s) => s.slug === attachment.slug);
|
|
120
|
+
if (shadowed) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`MCP server slug "${attachment.slug}" is reserved for the datastore ` +
|
|
123
|
+
"records attachment; the user-defined server is replaced.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
...resolvedServers.filter((s) => s.slug !== attachment.slug),
|
|
128
|
+
attachment,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The `<available_datastores>` prompt section (DD-005 SD-5, the
|
|
134
|
+
* skills-section precedent): names the attached datastores and points
|
|
135
|
+
* the model at describe_datastore first. Shared by both harnesses so
|
|
136
|
+
* the section text cannot drift (the sender-identity precedent).
|
|
137
|
+
*/
|
|
138
|
+
export function formatDatastoresSection(datastoreUsages: DatastoreUsage[]): string {
|
|
139
|
+
const slugs = datastoreUsages
|
|
140
|
+
.map((u) => u.datastoreRef?.slug)
|
|
141
|
+
.filter((s): s is string => !!s);
|
|
142
|
+
const entries = slugs.map((slug) => `- ${slug}`);
|
|
143
|
+
return [
|
|
144
|
+
"<available_datastores>",
|
|
145
|
+
"You have access to the following datastores through the record tools",
|
|
146
|
+
"(describe_datastore, find_records, insert_record, update_record, delete_record).",
|
|
147
|
+
"Before your first operation against a datastore, call describe_datastore to",
|
|
148
|
+
"learn its collections, field encodings, and which operations you are allowed",
|
|
149
|
+
"to perform.",
|
|
150
|
+
"",
|
|
151
|
+
...entries,
|
|
152
|
+
"</available_datastores>",
|
|
153
|
+
].join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The gRPC dial target (host:port) for a backend endpoint URL — the
|
|
158
|
+
* shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
|
|
159
|
+
*/
|
|
160
|
+
function grpcTarget(endpoint: string): string {
|
|
161
|
+
try {
|
|
162
|
+
return new URL(endpoint).host;
|
|
163
|
+
} catch {
|
|
164
|
+
return endpoint;
|
|
165
|
+
}
|
|
166
|
+
}
|