@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.
Files changed (45) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +9 -1
  3. package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
  4. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  5. package/dist/activities/execute-cursor/index.d.ts +2 -0
  6. package/dist/activities/execute-cursor/index.js +28 -1
  7. package/dist/activities/execute-cursor/index.js.map +1 -1
  8. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
  9. package/dist/activities/execute-cursor/prompt-builder.js +4 -0
  10. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
  12. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  13. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/setup.js +24 -2
  15. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  16. package/dist/config.d.ts +8 -0
  17. package/dist/config.js +2 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/runner-manager.js +3 -0
  20. package/dist/runner-manager.js.map +1 -1
  21. package/dist/runner.js +3 -0
  22. package/dist/runner.js.map +1 -1
  23. package/dist/shared/datastore-attachment.d.ts +79 -0
  24. package/dist/shared/datastore-attachment.js +129 -0
  25. package/dist/shared/datastore-attachment.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  28. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  29. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +25 -0
  30. package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
  31. package/src/activities/execute-cursor/index.ts +37 -1
  32. package/src/activities/execute-cursor/prompt-builder.ts +12 -1
  33. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  34. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  35. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  36. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  37. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  38. package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
  39. package/src/activities/execute-deep-agent/setup.ts +30 -2
  40. package/src/config.ts +11 -0
  41. package/src/runner-manager.ts +3 -0
  42. package/src/runner.ts +3 -0
  43. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  44. package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
  45. 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
- const backfilledServers = await backfillMcpServersIfNeeded(
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,
@@ -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
+ }