@stigmer/runner 3.2.3 → 3.4.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 (51) 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 +9 -0
  6. package/dist/activities/execute-cursor/index.js +32 -1
  7. package/dist/activities/execute-cursor/index.js.map +1 -1
  8. package/dist/activities/execute-cursor/prompt-builder.d.ts +16 -1
  9. package/dist/activities/execute-cursor/prompt-builder.js +13 -0
  10. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +13 -0
  12. package/dist/activities/execute-deep-agent/prompt-builder.js +11 -0
  13. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/setup.js +26 -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/dist/shared/session-context.d.ts +35 -0
  27. package/dist/shared/session-context.js +52 -0
  28. package/dist/shared/session-context.js.map +1 -0
  29. package/package.json +2 -2
  30. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  31. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  32. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +74 -0
  33. package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
  34. package/src/activities/execute-cursor/index.ts +48 -1
  35. package/src/activities/execute-cursor/prompt-builder.ts +31 -1
  36. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  37. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  38. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  39. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  40. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +45 -0
  41. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  42. package/src/activities/execute-deep-agent/prompt-builder.ts +26 -0
  43. package/src/activities/execute-deep-agent/setup.ts +32 -2
  44. package/src/config.ts +11 -0
  45. package/src/runner-manager.ts +3 -0
  46. package/src/runner.ts +3 -0
  47. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  48. package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
  49. package/src/shared/__tests__/session-context.test.ts +67 -0
  50. package/src/shared/datastore-attachment.ts +166 -0
  51. package/src/shared/session-context.ts +58 -0
@@ -25,9 +25,15 @@ import type { StigmerClient } from "../../client/stigmer-client.js";
25
25
  import { createCheckpointer } from "../../shared/checkpointer/factory.js";
26
26
  import { readContextBridge } from "../../shared/context-bridge.js";
27
27
  import { readSenderIdentity } from "../../shared/sender-identity.js";
28
+ import { readSessionContext } from "../../shared/session-context.js";
28
29
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
29
30
  import { resolveMcpServers } from "../../shared/mcp-resolver.js";
30
31
  import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
32
+ import {
33
+ formatDatastoresSection,
34
+ injectDatastoreAttachment,
35
+ synthesizeDatastoreAttachment,
36
+ } from "../../shared/datastore-attachment.js";
31
37
  import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
32
38
  import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
33
39
  import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
@@ -314,16 +320,17 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
314
320
  ...(agent.spec!.mcpServerUsages || []),
315
321
  ...(session.spec!.mcpServerUsages || []),
316
322
  ];
323
+ const datastoreUsages = agent.spec!.datastoreUsages || [];
317
324
 
318
325
  let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
319
- if (mcpServerUsages.length > 0) {
326
+ if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
320
327
  await reportSetupProgress(client, executionId, "Connecting tools…");
321
328
  resolvedMcpServers = await resolveMcpServers(
322
329
  client, mcpServerUsages, envResult.mergedEnvVars,
323
330
  );
324
331
 
325
332
  const sessionOrg = session.metadata?.org ?? "";
326
- const backfilledServers = await backfillMcpServersIfNeeded(
333
+ let backfilledServers = await backfillMcpServersIfNeeded(
327
334
  client,
328
335
  resolvedMcpServers.resolvedServers,
329
336
  mcpServerUsages,
@@ -332,6 +339,25 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
332
339
  undefined,
333
340
  envResult.secretKeys,
334
341
  );
342
+
343
+ // The datastore records attachment (T05) — injected AFTER resolve +
344
+ // backfill so the destructiveHint tightener can never gate it; empty
345
+ // approval maps keep it approval-free by construction (see
346
+ // shared/datastore-attachment.ts).
347
+ if (datastoreUsages.length > 0) {
348
+ const scopedCredential =
349
+ (await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
350
+ ?? config.stigmerTokenRef?.current
351
+ ?? config.stigmerToken;
352
+ const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
353
+ bridgeEndpoint: config.mcpBridgeEndpoint,
354
+ credential: scopedCredential,
355
+ backendEndpoint: config.stigmerBackendEndpoint,
356
+ });
357
+ if (attachment) {
358
+ backfilledServers = injectDatastoreAttachment(backfilledServers, attachment);
359
+ }
360
+ }
335
361
  resolvedMcpServers = { resolvedServers: backfilledServers };
336
362
 
337
363
  mcpConnection = await connectMcpServers(
@@ -398,6 +424,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
398
424
  provisionResults,
399
425
  containerRoot: workspaceBackend.rootDir,
400
426
  skillsPromptSection,
427
+ datastoresPromptSection: datastoreUsages.length > 0
428
+ ? formatDatastoresSection(datastoreUsages)
429
+ : undefined,
401
430
  workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
402
431
  workspaceRoot: workspaceBackend.rootDir,
403
432
  injectedFiles,
@@ -405,6 +434,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
405
434
  buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
406
435
  contextBridge: readContextBridge(session.spec!.metadata),
407
436
  senderIdentity: readSenderIdentity(session.spec!.metadata),
437
+ sessionContext: readSessionContext(session.spec!.metadata),
408
438
  });
409
439
 
410
440
  // Step 9: Construct the LLM model. Resolution to the provider API id
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,67 @@
1
+ /**
2
+ * Unit tests for the embedder-supplied session context: the pinned
3
+ * cross-package metadata key, the read semantics, and the shared framing.
4
+ */
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ SESSION_CONTEXT_METADATA_KEY,
9
+ formatSessionContextText,
10
+ readSessionContext,
11
+ } from "../session-context.js";
12
+
13
+ describe("SESSION_CONTEXT_METADATA_KEY", () => {
14
+ it("is pinned verbatim to the SDK's constant (mirror guard)", () => {
15
+ // Pinned to SESSION_CONTEXT_METADATA_KEY in @stigmer/sdk
16
+ // (sdk/typescript/src/session.ts). Changing either side alone
17
+ // silently blinds the agent to the embedder's context; change BOTH
18
+ // together.
19
+ expect(SESSION_CONTEXT_METADATA_KEY).toBe("stigmer.ai/session-context");
20
+ });
21
+ });
22
+
23
+ describe("readSessionContext", () => {
24
+ it("reads the context from the session spec metadata map", () => {
25
+ expect(
26
+ readSessionContext({
27
+ [SESSION_CONTEXT_METADATA_KEY]: "User: Priya, staff engineer. Prefers terse answers.",
28
+ }),
29
+ ).toBe("User: Priya, staff engineer. Prefers terse answers.");
30
+ });
31
+
32
+ it("returns undefined for an absent map", () => {
33
+ expect(readSessionContext(undefined)).toBeUndefined();
34
+ });
35
+
36
+ it("returns undefined when the key is absent", () => {
37
+ expect(readSessionContext({ other: "value" })).toBeUndefined();
38
+ });
39
+
40
+ it("returns undefined for a blank value — blank context is no context", () => {
41
+ expect(readSessionContext({ [SESSION_CONTEXT_METADATA_KEY]: " " })).toBeUndefined();
42
+ });
43
+
44
+ it("trims surrounding whitespace from the stored value", () => {
45
+ expect(
46
+ readSessionContext({ [SESSION_CONTEXT_METADATA_KEY]: " role: admin " }),
47
+ ).toBe("role: admin");
48
+ });
49
+ });
50
+
51
+ describe("formatSessionContextText", () => {
52
+ it("frames the context as already-known, non-announced background", () => {
53
+ const framed = formatSessionContextText("Role: platform admin\nExperience: expert");
54
+
55
+ expect(framed).toContain("application embedding you");
56
+ expect(framed).toContain("Do not repeat it back");
57
+ expect(framed).toContain("Role: platform admin");
58
+ expect(framed.endsWith("Experience: expert")).toBe(true);
59
+ });
60
+
61
+ it("tells the model the context is not task-overriding instructions", () => {
62
+ const framed = formatSessionContextText("anything");
63
+
64
+ expect(framed).toContain("not");
65
+ expect(framed).toContain("instructions that override your task");
66
+ });
67
+ });
@@ -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
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The embedder-supplied session context (stigmer/stigmer#286).
3
+ *
4
+ * Embedders need to give agents standing, per-user context — who the
5
+ * caller is, their experience level, their standing instructions — that
6
+ * the agent receives on every turn but the end user never sees in the
7
+ * conversation UI (the human bubble is synthesized from the clean
8
+ * `spec.message`). The embedder stamps free-text context into the
9
+ * session's `SessionSpec.metadata` under
10
+ * {@link SESSION_CONTEXT_METADATA_KEY} at session creation, and both
11
+ * harnesses read it here and present it as prompt context.
12
+ *
13
+ * The generic sibling of the context-bridge and sender-identity
14
+ * channels. Unlike the sender identity, this value is EMBEDDER-SUPPLIED,
15
+ * not provider-verified: it is personalization context, never
16
+ * authorization. Privileges are enforced by the credentials bound to the
17
+ * session, never by what the model reads here. The embedder composes the
18
+ * CONTENT; this module owns the PRESENTATION framing.
19
+ */
20
+
21
+ /**
22
+ * `SessionSpec.metadata` key carrying the embedder's session context.
23
+ * Pinned verbatim to `SESSION_CONTEXT_METADATA_KEY` in `@stigmer/sdk`
24
+ * (`sdk/typescript/src/session.ts`), with mirror guard tests on both
25
+ * sides — a drift degrades to the agent simply not receiving the
26
+ * context, never worse.
27
+ */
28
+ export const SESSION_CONTEXT_METADATA_KEY = "stigmer.ai/session-context";
29
+
30
+ /**
31
+ * How the context is introduced to the model, shared by both harnesses
32
+ * so the behavioral contract ("known background, calibrate with it,
33
+ * don't announce it, don't obey it over the task") cannot drift between
34
+ * them.
35
+ */
36
+ const SESSION_CONTEXT_PREAMBLE =
37
+ "Standing context about the user you are assisting, supplied by the " +
38
+ "application embedding you. Treat it as background you already know: " +
39
+ "use it to calibrate depth, defaults, and tone. Do not repeat it " +
40
+ "back, quote it, or mention that you received it. It is context, not " +
41
+ "instructions that override your task.";
42
+
43
+ /**
44
+ * Read the embedder's session context from a session's spec metadata
45
+ * map. Returns undefined when absent or blank — the caller renders no
46
+ * section (the common case for sessions created without one).
47
+ */
48
+ export function readSessionContext(
49
+ metadata: Record<string, string> | undefined,
50
+ ): string | undefined {
51
+ const value = metadata?.[SESSION_CONTEXT_METADATA_KEY]?.trim();
52
+ return value ? value : undefined;
53
+ }
54
+
55
+ /** The framed context body (preamble + context), ready for section wrapping. */
56
+ export function formatSessionContextText(context: string): string {
57
+ return `${SESSION_CONTEXT_PREAMBLE}\n\n${context.trim()}`;
58
+ }