@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.
- 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 +9 -0
- package/dist/activities/execute-cursor/index.js +32 -1
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +16 -1
- package/dist/activities/execute-cursor/prompt-builder.js +13 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +13 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +11 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +26 -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/dist/shared/session-context.d.ts +35 -0
- package/dist/shared/session-context.js +52 -0
- package/dist/shared/session-context.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 +74 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
- package/src/activities/execute-cursor/index.ts +48 -1
- package/src/activities/execute-cursor/prompt-builder.ts +31 -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__/prompt-builder.test.ts +45 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +26 -0
- package/src/activities/execute-deep-agent/setup.ts +32 -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/__tests__/session-context.test.ts +67 -0
- package/src/shared/datastore-attachment.ts +166 -0
- 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
|
-
|
|
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,
|
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,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
|
+
}
|