@stigmer/runner 3.5.3 → 3.6.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/discover-mcp-server.js +9 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -0
- package/dist/activities/execute-cursor/index.js +49 -9
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -0
- package/dist/activities/execute-cursor/prompt-builder.js +9 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +6 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +45 -9
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +26 -0
- package/dist/client/stigmer-client.js +37 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +89 -0
- package/dist/shared/caller-identity.js +124 -0
- package/dist/shared/caller-identity.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +85 -0
- package/dist/shared/channel-attachment.js +203 -0
- package/dist/shared/channel-attachment.js.map +1 -0
- package/dist/shared/datastore-attachment.d.ts +2 -25
- package/dist/shared/datastore-attachment.js +1 -28
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +51 -0
- package/dist/shared/synthesized-attachment.js +45 -0
- package/dist/shared/synthesized-attachment.js.map +1 -0
- package/package.json +2 -2
- package/src/__test-utils__/mock-client.ts +4 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +49 -0
- package/src/activities/discover-mcp-server.ts +13 -1
- package/src/activities/execute-cursor/index.ts +72 -13
- package/src/activities/execute-cursor/prompt-builder.ts +19 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +10 -0
- package/src/activities/execute-deep-agent/setup.ts +66 -10
- package/src/client/stigmer-client.ts +46 -0
- package/src/shared/__tests__/caller-identity.test.ts +159 -0
- package/src/shared/__tests__/channel-attachment.test.ts +276 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +4 -4
- package/src/shared/caller-identity.ts +161 -0
- package/src/shared/channel-attachment.ts +237 -0
- package/src/shared/datastore-attachment.ts +2 -54
- package/src/shared/synthesized-attachment.ts +77 -0
|
@@ -49,6 +49,10 @@ import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "..
|
|
|
49
49
|
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
50
50
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
51
51
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
52
|
+
import {
|
|
53
|
+
injectCallerIdentityEnv,
|
|
54
|
+
resolveCallerIdentity,
|
|
55
|
+
} from "../../shared/caller-identity.js";
|
|
52
56
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
53
57
|
import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
54
58
|
import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
|
|
@@ -60,10 +64,12 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
|
|
|
60
64
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
61
65
|
import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
62
66
|
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
67
|
+
import { synthesizeDatastoreAttachment } from "../../shared/datastore-attachment.js";
|
|
63
68
|
import {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from "../../shared/
|
|
69
|
+
discoverChannelMessaging,
|
|
70
|
+
synthesizeChannelAttachment,
|
|
71
|
+
} from "../../shared/channel-attachment.js";
|
|
72
|
+
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
67
73
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
68
74
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
69
75
|
import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
|
|
@@ -590,11 +596,23 @@ async function executeCursorInner(
|
|
|
590
596
|
}
|
|
591
597
|
}
|
|
592
598
|
|
|
593
|
-
// Phase 4: Resolve MCP servers with approval policies
|
|
599
|
+
// Phase 4: Resolve MCP servers with approval policies.
|
|
600
|
+
// The MCP-bound env map (and ONLY it — never the agent process env)
|
|
601
|
+
// carries the reserved caller-identity keys, so a server that declares
|
|
602
|
+
// them in spec.env can template the platform-verified caller into its
|
|
603
|
+
// headers. filterEnvToDeclaredKeys keeps every other server blind.
|
|
594
604
|
await reportSetupProgress(client, executionId, "Resolving MCP servers");
|
|
595
605
|
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
606
|
+
const mcpEnvVars = injectCallerIdentityEnv(
|
|
607
|
+
envVars,
|
|
608
|
+
resolveCallerIdentity(
|
|
609
|
+
blueprint.sessionSpec.metadata,
|
|
610
|
+
session.status?.audit?.specAudit?.createdBy,
|
|
611
|
+
),
|
|
612
|
+
sessionId,
|
|
613
|
+
);
|
|
596
614
|
let mcpResolution = await resolveMcpServers(
|
|
597
|
-
client, blueprint.mergedMcpServerUsages,
|
|
615
|
+
client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
|
|
598
616
|
);
|
|
599
617
|
setupTiming.mark("resolve_mcp_servers");
|
|
600
618
|
|
|
@@ -602,11 +620,24 @@ async function executeCursorInner(
|
|
|
602
620
|
heartbeatPhase = "resolving_mcp_servers";
|
|
603
621
|
const sessionOrg = session.metadata?.org ?? "";
|
|
604
622
|
mcpResolution = await backfillMcpServersIfNeeded(
|
|
605
|
-
client, mcpResolution, blueprint.mergedMcpServerUsages,
|
|
623
|
+
client, mcpResolution, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
|
|
606
624
|
transportPosture, heartbeat, secretKeys,
|
|
607
625
|
);
|
|
608
626
|
setupTiming.mark("backfill_mcp");
|
|
609
627
|
|
|
628
|
+
// The synthesized attachments' credential story (DD-006 D4): the
|
|
629
|
+
// exchanged token authenticates the discovery reads per-call (a
|
|
630
|
+
// desktop runner's ambient embedded_runner credential is refused by
|
|
631
|
+
// the messaging reach; undefined lets a cloud sandbox runner's
|
|
632
|
+
// ambient session-scoped token or OSS's no-auth apply). The
|
|
633
|
+
// attachment header falls back to the ambient credential where no
|
|
634
|
+
// exchange happens.
|
|
635
|
+
const exchangedRunnerToken =
|
|
636
|
+
await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
|
|
637
|
+
const attachmentCredential = exchangedRunnerToken
|
|
638
|
+
?? config.stigmerTokenRef?.current
|
|
639
|
+
?? config.stigmerToken;
|
|
640
|
+
|
|
610
641
|
// Phase 4a2: Synthesize the datastore records attachment (T05).
|
|
611
642
|
// Deliberately AFTER resolve + backfill: the attachment has no
|
|
612
643
|
// McpServerUsage and reports discovered capabilities, so the
|
|
@@ -614,18 +645,38 @@ async function executeCursorInner(
|
|
|
614
645
|
// delete_record (which on channels would be silently skipped).
|
|
615
646
|
// Empty approval maps keep it approval-free by construction.
|
|
616
647
|
if (blueprint.datastoreUsages.length > 0) {
|
|
617
|
-
const scopedCredential =
|
|
618
|
-
(await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
|
|
619
|
-
?? config.stigmerTokenRef?.current
|
|
620
|
-
?? config.stigmerToken;
|
|
621
648
|
const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
|
|
622
649
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
623
|
-
credential:
|
|
650
|
+
credential: attachmentCredential,
|
|
624
651
|
backendEndpoint: config.stigmerBackendEndpoint,
|
|
625
652
|
});
|
|
626
653
|
if (attachment) {
|
|
627
|
-
const resolvedServers =
|
|
628
|
-
mcpResolution.resolvedServers, attachment,
|
|
654
|
+
const resolvedServers = injectSynthesizedAttachment(
|
|
655
|
+
mcpResolution.resolvedServers, attachment, "datastore records",
|
|
656
|
+
);
|
|
657
|
+
mcpResolution = {
|
|
658
|
+
resolvedServers,
|
|
659
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Phase 4a3: Synthesize the channel messaging attachment (DD-006
|
|
665
|
+
// D7/D8), the records attachment's twin. The discovery read is the
|
|
666
|
+
// attachment decision — the control plane runs the SAME candidate
|
|
667
|
+
// computation the send authorization uses — and every failure mode
|
|
668
|
+
// (no channel, OSS, registry down, pre-3a control plane) degrades
|
|
669
|
+
// to honest absence: no tool, no section, execution unharmed.
|
|
670
|
+
const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
|
|
671
|
+
if (channelMessaging.length > 0) {
|
|
672
|
+
const attachment = synthesizeChannelAttachment(channelMessaging, {
|
|
673
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
674
|
+
credential: attachmentCredential,
|
|
675
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
676
|
+
});
|
|
677
|
+
if (attachment) {
|
|
678
|
+
const resolvedServers = injectSynthesizedAttachment(
|
|
679
|
+
mcpResolution.resolvedServers, attachment, "channel messaging",
|
|
629
680
|
);
|
|
630
681
|
mcpResolution = {
|
|
631
682
|
resolvedServers,
|
|
@@ -992,6 +1043,7 @@ async function executeCursorInner(
|
|
|
992
1043
|
userMessage: spec.message,
|
|
993
1044
|
skills: skillMetadata,
|
|
994
1045
|
datastoreUsages: blueprint.datastoreUsages,
|
|
1046
|
+
channelMessaging,
|
|
995
1047
|
subAgents: blueprint.subAgents,
|
|
996
1048
|
workspaceDirs: blueprint.workspaceDirs,
|
|
997
1049
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -1605,6 +1657,7 @@ async function executeCursorInner(
|
|
|
1605
1657
|
userMessage: spec.message,
|
|
1606
1658
|
skills: skillMetadata,
|
|
1607
1659
|
datastoreUsages: blueprint.datastoreUsages,
|
|
1660
|
+
channelMessaging,
|
|
1608
1661
|
subAgents: blueprint.subAgents,
|
|
1609
1662
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1610
1663
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -2169,6 +2222,11 @@ export interface BuildPromptInput {
|
|
|
2169
2222
|
skills: import("./prompt-builder.js").SkillMetadata[];
|
|
2170
2223
|
/** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
|
|
2171
2224
|
datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
|
|
2225
|
+
/**
|
|
2226
|
+
* Serving proactive channels + their templates (the DD-006 D2
|
|
2227
|
+
* discovery read) — the `<available_channel_templates>` section.
|
|
2228
|
+
*/
|
|
2229
|
+
channelMessaging?: import("../../shared/channel-attachment.js").ChannelMessagingInfo[];
|
|
2172
2230
|
subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
|
|
2173
2231
|
workspaceDirs: string[];
|
|
2174
2232
|
workspaceFileRefs: string[];
|
|
@@ -2276,6 +2334,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2276
2334
|
userMessage,
|
|
2277
2335
|
skills,
|
|
2278
2336
|
datastoreUsages: input.datastoreUsages ?? [],
|
|
2337
|
+
channelMessaging: input.channelMessaging ?? [],
|
|
2279
2338
|
subAgents,
|
|
2280
2339
|
workspaceDirs,
|
|
2281
2340
|
workspaceFileRefs,
|
|
@@ -22,6 +22,10 @@ import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentex
|
|
|
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
24
|
import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
|
|
25
|
+
import {
|
|
26
|
+
formatChannelTemplatesSection,
|
|
27
|
+
type ChannelMessagingInfo,
|
|
28
|
+
} from "../../shared/channel-attachment.js";
|
|
25
29
|
import {
|
|
26
30
|
formatSenderIdentityText,
|
|
27
31
|
type SenderIdentity,
|
|
@@ -61,6 +65,12 @@ export interface EnhancedPromptOptions {
|
|
|
61
65
|
* pointing the model at the synthesized record tools.
|
|
62
66
|
*/
|
|
63
67
|
datastoreUsages?: DatastoreUsage[];
|
|
68
|
+
/**
|
|
69
|
+
* Serving proactive channels + their approved templates — rendered as
|
|
70
|
+
* the `<available_channel_templates>` section (proactive-messaging
|
|
71
|
+
* DD-003 D5) beside the synthesized send_channel_message tool.
|
|
72
|
+
*/
|
|
73
|
+
channelMessaging?: ChannelMessagingInfo[];
|
|
64
74
|
subAgents: SubAgent[];
|
|
65
75
|
workspaceDirs: string[];
|
|
66
76
|
workspaceFileRefs: string[];
|
|
@@ -133,6 +143,15 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
133
143
|
sections.push(formatDatastoresSection(options.datastoreUsages));
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
if (options.channelMessaging !== undefined && options.channelMessaging.length > 0) {
|
|
147
|
+
// "" when nothing is sendable — the tool alone still serves text
|
|
148
|
+
// sends inside a 24-hour window (DD-006 D6).
|
|
149
|
+
const channelSection = formatChannelTemplatesSection(options.channelMessaging);
|
|
150
|
+
if (channelSection !== "") {
|
|
151
|
+
sections.push(channelSection);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
if (options.subAgents.length > 0) {
|
|
137
156
|
sections.push(formatSubAgentsSection(options.subAgents));
|
|
138
157
|
}
|
|
@@ -98,6 +98,12 @@ export interface PromptBuilderInput {
|
|
|
98
98
|
* formatDatastoresSection); empty when the agent uses no datastores.
|
|
99
99
|
*/
|
|
100
100
|
datastoresPromptSection?: string;
|
|
101
|
+
/**
|
|
102
|
+
* The `<available_channel_templates>` section
|
|
103
|
+
* (shared/channel-attachment.ts formatChannelTemplatesSection); absent
|
|
104
|
+
* when the agent serves no proactive channel or nothing is sendable.
|
|
105
|
+
*/
|
|
106
|
+
channelTemplatesPromptSection?: string;
|
|
101
107
|
workspaceFileRefs: string[];
|
|
102
108
|
workspaceRoot: string;
|
|
103
109
|
injectedFiles: InjectedFile[];
|
|
@@ -174,6 +180,10 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
174
180
|
prompt += "\n\n" + input.datastoresPromptSection;
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
if (input.channelTemplatesPromptSection) {
|
|
184
|
+
prompt += "\n\n" + input.channelTemplatesPromptSection;
|
|
185
|
+
}
|
|
186
|
+
|
|
177
187
|
if (input.workspaceFileRefs.length > 0) {
|
|
178
188
|
const refSection = buildReferencedFilesSection(
|
|
179
189
|
input.workspaceFileRefs,
|
|
@@ -26,6 +26,10 @@ import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js
|
|
|
26
26
|
import { createCheckpointer } from "../../shared/checkpointer/factory.js";
|
|
27
27
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
28
28
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
29
|
+
import {
|
|
30
|
+
injectCallerIdentityEnv,
|
|
31
|
+
resolveCallerIdentity,
|
|
32
|
+
} from "../../shared/caller-identity.js";
|
|
29
33
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
30
34
|
import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
|
|
31
35
|
import { resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
@@ -33,9 +37,14 @@ import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js"
|
|
|
33
37
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
34
38
|
import {
|
|
35
39
|
formatDatastoresSection,
|
|
36
|
-
injectDatastoreAttachment,
|
|
37
40
|
synthesizeDatastoreAttachment,
|
|
38
41
|
} from "../../shared/datastore-attachment.js";
|
|
42
|
+
import {
|
|
43
|
+
discoverChannelMessaging,
|
|
44
|
+
formatChannelTemplatesSection,
|
|
45
|
+
synthesizeChannelAttachment,
|
|
46
|
+
} from "../../shared/channel-attachment.js";
|
|
47
|
+
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
39
48
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
40
49
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
41
50
|
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
@@ -334,12 +343,41 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
334
343
|
];
|
|
335
344
|
const datastoreUsages = agent.spec!.datastoreUsages || [];
|
|
336
345
|
|
|
346
|
+
// The synthesized attachments' credential story (DD-006 D4): the
|
|
347
|
+
// exchanged token authenticates the discovery reads per-call (the
|
|
348
|
+
// messaging reach refuses a desktop runner's ambient embedded_runner
|
|
349
|
+
// credential; undefined lets the ambient credential apply). The
|
|
350
|
+
// attachment header falls back to the ambient credential.
|
|
351
|
+
const exchangedRunnerToken =
|
|
352
|
+
await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
|
|
353
|
+
const attachmentCredential = exchangedRunnerToken
|
|
354
|
+
?? config.stigmerTokenRef?.current
|
|
355
|
+
?? config.stigmerToken;
|
|
356
|
+
|
|
357
|
+
// The channel discovery read runs BEFORE the MCP gate below: an
|
|
358
|
+
// agent whose ONLY tool source is a proactive channel would
|
|
359
|
+
// otherwise never enter MCP resolution and never connect the
|
|
360
|
+
// attachment (DD-006 D7). Every failure mode degrades to an empty
|
|
361
|
+
// answer — no tool, no section, execution unharmed.
|
|
362
|
+
const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
|
|
363
|
+
|
|
337
364
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
338
|
-
if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
|
|
365
|
+
if (mcpServerUsages.length > 0 || datastoreUsages.length > 0 || channelMessaging.length > 0) {
|
|
339
366
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
340
367
|
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
368
|
+
// The MCP-bound env map (and ONLY it) carries the reserved
|
|
369
|
+
// caller-identity keys — mirror of the Cursor harness's Phase 4
|
|
370
|
+
// injection; filterEnvToDeclaredKeys keeps undeclared servers blind.
|
|
371
|
+
const mcpEnvVars = injectCallerIdentityEnv(
|
|
372
|
+
envResult.mergedEnvVars,
|
|
373
|
+
resolveCallerIdentity(
|
|
374
|
+
session.spec!.metadata,
|
|
375
|
+
session.status?.audit?.specAudit?.createdBy,
|
|
376
|
+
),
|
|
377
|
+
sessionId,
|
|
378
|
+
);
|
|
341
379
|
resolvedMcpServers = await resolveMcpServers(
|
|
342
|
-
client, mcpServerUsages,
|
|
380
|
+
client, mcpServerUsages, mcpEnvVars, transportPosture,
|
|
343
381
|
);
|
|
344
382
|
timing.mark("resolve_mcp_servers");
|
|
345
383
|
|
|
@@ -348,7 +386,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
348
386
|
client,
|
|
349
387
|
resolvedMcpServers.resolvedServers,
|
|
350
388
|
mcpServerUsages,
|
|
351
|
-
|
|
389
|
+
mcpEnvVars,
|
|
352
390
|
sessionOrg,
|
|
353
391
|
transportPosture,
|
|
354
392
|
undefined,
|
|
@@ -360,17 +398,30 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
360
398
|
// approval maps keep it approval-free by construction (see
|
|
361
399
|
// shared/datastore-attachment.ts).
|
|
362
400
|
if (datastoreUsages.length > 0) {
|
|
363
|
-
const scopedCredential =
|
|
364
|
-
(await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
|
|
365
|
-
?? config.stigmerTokenRef?.current
|
|
366
|
-
?? config.stigmerToken;
|
|
367
401
|
const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
|
|
368
402
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
369
|
-
credential:
|
|
403
|
+
credential: attachmentCredential,
|
|
404
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
405
|
+
});
|
|
406
|
+
if (attachment) {
|
|
407
|
+
backfilledServers = injectSynthesizedAttachment(
|
|
408
|
+
backfilledServers, attachment, "datastore records",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The channel messaging attachment (DD-006 D7/D8) — the records
|
|
414
|
+
// attachment's twin, injected under the same after-backfill rule.
|
|
415
|
+
if (channelMessaging.length > 0) {
|
|
416
|
+
const attachment = synthesizeChannelAttachment(channelMessaging, {
|
|
417
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
418
|
+
credential: attachmentCredential,
|
|
370
419
|
backendEndpoint: config.stigmerBackendEndpoint,
|
|
371
420
|
});
|
|
372
421
|
if (attachment) {
|
|
373
|
-
backfilledServers =
|
|
422
|
+
backfilledServers = injectSynthesizedAttachment(
|
|
423
|
+
backfilledServers, attachment, "channel messaging",
|
|
424
|
+
);
|
|
374
425
|
}
|
|
375
426
|
}
|
|
376
427
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
@@ -445,6 +496,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
445
496
|
datastoresPromptSection: datastoreUsages.length > 0
|
|
446
497
|
? formatDatastoresSection(datastoreUsages)
|
|
447
498
|
: undefined,
|
|
499
|
+
// "" (nothing sendable) threads as undefined: the tool alone still
|
|
500
|
+
// serves text sends inside a 24-hour window (DD-006 D6).
|
|
501
|
+
channelTemplatesPromptSection: channelMessaging.length > 0
|
|
502
|
+
? formatChannelTemplatesSection(channelMessaging) || undefined
|
|
503
|
+
: undefined,
|
|
448
504
|
workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
|
|
449
505
|
workspaceRoot: workspaceBackend.rootDir,
|
|
450
506
|
injectedFiles,
|
|
@@ -50,6 +50,8 @@ import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/ap
|
|
|
50
50
|
import { WorkflowInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/query_pb";
|
|
51
51
|
import type { WorkflowInstance } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
52
52
|
import { PlatformQueryController, GetRunnerScopedTokenInputSchema } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
53
|
+
import { ChannelMessageQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_query_pb";
|
|
54
|
+
import type { ChannelTemplate, MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
53
55
|
import { isEmbeddedRunnerToken } from "./token-claims.js";
|
|
54
56
|
import { assertCreateRequirements, assertReferenceRequirements } from "./server-contracts.js";
|
|
55
57
|
|
|
@@ -156,6 +158,7 @@ export class StigmerClient {
|
|
|
156
158
|
private readonly workflowQuery: Client<typeof WorkflowQueryController>;
|
|
157
159
|
private readonly workflowInstanceQuery: Client<typeof WorkflowInstanceQueryController>;
|
|
158
160
|
private readonly platformQuery: Client<typeof PlatformQueryController>;
|
|
161
|
+
private readonly channelMessageQuery: Client<typeof ChannelMessageQueryController>;
|
|
159
162
|
|
|
160
163
|
private readonly tokenRef: TokenRef | null;
|
|
161
164
|
private readonly runnerTokenRef: TokenRef | null;
|
|
@@ -239,6 +242,7 @@ export class StigmerClient {
|
|
|
239
242
|
this.workflowQuery = createClient(WorkflowQueryController, this.transport);
|
|
240
243
|
this.workflowInstanceQuery = createClient(WorkflowInstanceQueryController, this.transport);
|
|
241
244
|
this.platformQuery = createClient(PlatformQueryController, this.transport);
|
|
245
|
+
this.channelMessageQuery = createClient(ChannelMessageQueryController, this.transport);
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
/**
|
|
@@ -380,6 +384,48 @@ export class StigmerClient {
|
|
|
380
384
|
}
|
|
381
385
|
}
|
|
382
386
|
|
|
387
|
+
/**
|
|
388
|
+
* The agent's serving proactive-messaging channels, as data
|
|
389
|
+
* (proactive-messaging DD-006 D2) — the runner's tool-attachment
|
|
390
|
+
* decision. An empty list is the everyday answer (most agents have no
|
|
391
|
+
* proactive channel).
|
|
392
|
+
*
|
|
393
|
+
* When a scoped runner token is supplied, the read authenticates with
|
|
394
|
+
* it per-call (the {@link getExecutionContextByExecutionId} precedent):
|
|
395
|
+
* the messaging reach refuses the desktop runner's ambient
|
|
396
|
+
* embedded_runner credential outright, and one desktop runner process
|
|
397
|
+
* serves many sessions concurrently, so the credential cannot live in
|
|
398
|
+
* a shared ref. A cloud sandbox runner's ambient credential is already
|
|
399
|
+
* the session-scoped token; OSS sends nothing and answers empty.
|
|
400
|
+
*/
|
|
401
|
+
async listMessagingChannels(scopedToken?: string): Promise<MessagingChannel[]> {
|
|
402
|
+
const res = await this.channelMessageQuery.listMessagingChannels(
|
|
403
|
+
{},
|
|
404
|
+
scopedToken
|
|
405
|
+
? { headers: { authorization: `Bearer ${scopedToken}` } }
|
|
406
|
+
: undefined,
|
|
407
|
+
);
|
|
408
|
+
return res.entries;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The channel's provider template registry, approved entries only —
|
|
413
|
+
* the `<available_channel_templates>` prompt section's source
|
|
414
|
+
* (proactive-messaging DD-003 D5). Entries carry the DD-006 D1
|
|
415
|
+
* sendability verdict (`unsupportedReason`, empty means sendable);
|
|
416
|
+
* the section formatter filters on it. Credential rules as
|
|
417
|
+
* {@link listMessagingChannels}.
|
|
418
|
+
*/
|
|
419
|
+
async listChannelTemplates(channel: string, scopedToken?: string): Promise<ChannelTemplate[]> {
|
|
420
|
+
const res = await this.channelMessageQuery.listTemplates(
|
|
421
|
+
{ channel, approvedOnly: true },
|
|
422
|
+
scopedToken
|
|
423
|
+
? { headers: { authorization: `Bearer ${scopedToken}` } }
|
|
424
|
+
: undefined,
|
|
425
|
+
);
|
|
426
|
+
return res.entries;
|
|
427
|
+
}
|
|
428
|
+
|
|
383
429
|
async getSession(sessionId: string): Promise<Session> {
|
|
384
430
|
return this.sessionQuery.get({ value: sessionId });
|
|
385
431
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the caller identity: the reserved env keys, the
|
|
3
|
+
* precedence chain, the authoritative injection, and the discovery
|
|
4
|
+
* sentinel that keeps identity-templating servers discoverable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
ANONYMOUS_KIND,
|
|
10
|
+
CALLER_IDENTITY_KIND_ENV_KEY,
|
|
11
|
+
CALLER_IDENTITY_VALUE_ENV_KEY,
|
|
12
|
+
SESSION_ID_ENV_KEY,
|
|
13
|
+
STIGMER_USER_KIND,
|
|
14
|
+
anonymousCallerIdentity,
|
|
15
|
+
injectAnonymousCallerIdentityForDiscovery,
|
|
16
|
+
injectCallerIdentityEnv,
|
|
17
|
+
resolveCallerIdentity,
|
|
18
|
+
} from "../caller-identity.js";
|
|
19
|
+
import {
|
|
20
|
+
SENDER_IDENTITY_METADATA_KEY,
|
|
21
|
+
SENDER_KIND_METADATA_KEY,
|
|
22
|
+
} from "../sender-identity.js";
|
|
23
|
+
import { resolveHeaders } from "../placeholder-resolver.js";
|
|
24
|
+
|
|
25
|
+
describe("reserved env keys", () => {
|
|
26
|
+
it("are pinned verbatim — MCP server specs template these names (contract guard)", () => {
|
|
27
|
+
// These names appear in user-authored McpServer YAML (spec.env
|
|
28
|
+
// declarations and ${...} header templates). Renaming them breaks
|
|
29
|
+
// every deployed server that consumes caller identity.
|
|
30
|
+
expect(CALLER_IDENTITY_KIND_ENV_KEY).toBe("STIGMER_CALLER_IDENTITY_KIND");
|
|
31
|
+
expect(CALLER_IDENTITY_VALUE_ENV_KEY).toBe("STIGMER_CALLER_IDENTITY_VALUE");
|
|
32
|
+
expect(SESSION_ID_ENV_KEY).toBe("STIGMER_SESSION_ID");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("resolveCallerIdentity precedence", () => {
|
|
37
|
+
const channelMetadata = {
|
|
38
|
+
[SENDER_IDENTITY_METADATA_KEY]: "919800000001",
|
|
39
|
+
[SENDER_KIND_METADATA_KEY]: "whatsapp_phone",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it("channel sender wins, kind passed through VERBATIM from the broker", () => {
|
|
43
|
+
expect(
|
|
44
|
+
resolveCallerIdentity(channelMetadata, { id: "idacc_1", email: "owner@example.com" }),
|
|
45
|
+
).toEqual({ kind: "whatsapp_phone", value: "919800000001" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("falls to the session creator (stigmer_user) when no channel sender exists", () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveCallerIdentity({}, { id: "idacc_1", email: "owner@example.com" }),
|
|
51
|
+
).toEqual({ kind: STIGMER_USER_KIND, value: "owner@example.com" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("prefers the creator's email over the historically-mixed id field", () => {
|
|
55
|
+
// The audit actor's `id` is sometimes an identity-account id and
|
|
56
|
+
// sometimes an email (per the proto's own @internal note) — email is
|
|
57
|
+
// what humans bind against, so it wins when present.
|
|
58
|
+
expect(resolveCallerIdentity(undefined, { id: "idacc_1", email: "a@b.c" }).value).toBe("a@b.c");
|
|
59
|
+
expect(resolveCallerIdentity(undefined, { id: "idacc_1" }).value).toBe("idacc_1");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("falls to anonymous when neither source exists (OSS sparse audit, no metadata)", () => {
|
|
63
|
+
expect(resolveCallerIdentity(undefined, undefined)).toEqual({
|
|
64
|
+
kind: ANONYMOUS_KIND,
|
|
65
|
+
value: "",
|
|
66
|
+
});
|
|
67
|
+
expect(resolveCallerIdentity({}, { id: " ", email: "" })).toEqual({
|
|
68
|
+
kind: ANONYMOUS_KIND,
|
|
69
|
+
value: "",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a half-present channel identity (value without kind) is NOT a channel sender", () => {
|
|
74
|
+
// readSenderIdentity requires value AND kind; a half-stamped session
|
|
75
|
+
// falls through to the creator rather than fabricating a kind.
|
|
76
|
+
expect(
|
|
77
|
+
resolveCallerIdentity(
|
|
78
|
+
{ [SENDER_IDENTITY_METADATA_KEY]: "919800000001" },
|
|
79
|
+
{ email: "owner@example.com" },
|
|
80
|
+
),
|
|
81
|
+
).toEqual({ kind: STIGMER_USER_KIND, value: "owner@example.com" });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("injectCallerIdentityEnv", () => {
|
|
86
|
+
const identity = { kind: "whatsapp_phone", value: "919800000001" };
|
|
87
|
+
|
|
88
|
+
it("returns a NEW map with the reserved keys set — input never mutated", () => {
|
|
89
|
+
const input = { GRIST_API_KEY: "secret" };
|
|
90
|
+
const result = injectCallerIdentityEnv(input, identity, "sess_1");
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
GRIST_API_KEY: "secret",
|
|
94
|
+
[CALLER_IDENTITY_KIND_ENV_KEY]: "whatsapp_phone",
|
|
95
|
+
[CALLER_IDENTITY_VALUE_ENV_KEY]: "919800000001",
|
|
96
|
+
[SESSION_ID_ENV_KEY]: "sess_1",
|
|
97
|
+
});
|
|
98
|
+
expect(input).toEqual({ GRIST_API_KEY: "secret" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("platform values are authoritative — a user env var cannot impersonate a caller", () => {
|
|
102
|
+
const result = injectCallerIdentityEnv(
|
|
103
|
+
{ [CALLER_IDENTITY_VALUE_ENV_KEY]: "999999999999" },
|
|
104
|
+
identity,
|
|
105
|
+
"sess_1",
|
|
106
|
+
);
|
|
107
|
+
expect(result[CALLER_IDENTITY_VALUE_ENV_KEY]).toBe("919800000001");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("the reserved keys can never hit the unresolved-placeholder silent-skip path", () => {
|
|
111
|
+
// At runtime PlaceholderResolutionError is caught and the server
|
|
112
|
+
// silently dropped from the execution — always-present injection makes
|
|
113
|
+
// that unreachable for these keys, whatever the identity resolved to.
|
|
114
|
+
const env = injectCallerIdentityEnv({}, anonymousCallerIdentity(), "");
|
|
115
|
+
const headers = resolveHeaders(
|
|
116
|
+
{
|
|
117
|
+
"X-Stigmer-Caller-Kind": `\${${CALLER_IDENTITY_KIND_ENV_KEY}}`,
|
|
118
|
+
"X-Stigmer-Caller-Value": `\${${CALLER_IDENTITY_VALUE_ENV_KEY}}`,
|
|
119
|
+
},
|
|
120
|
+
env,
|
|
121
|
+
);
|
|
122
|
+
expect(headers).toEqual({
|
|
123
|
+
"X-Stigmer-Caller-Kind": ANONYMOUS_KIND,
|
|
124
|
+
"X-Stigmer-Caller-Value": "",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("injectAnonymousCallerIdentityForDiscovery", () => {
|
|
130
|
+
it("resolves declared identity placeholders with the anonymous sentinel", () => {
|
|
131
|
+
// Discovery has no session: without the sentinel, a server templating
|
|
132
|
+
// ${STIGMER_CALLER_IDENTITY_VALUE} would throw
|
|
133
|
+
// PlaceholderResolutionError and never get its tools classified.
|
|
134
|
+
const declared = new Set([
|
|
135
|
+
"GRIST_API_KEY",
|
|
136
|
+
CALLER_IDENTITY_KIND_ENV_KEY,
|
|
137
|
+
CALLER_IDENTITY_VALUE_ENV_KEY,
|
|
138
|
+
]);
|
|
139
|
+
const env = injectAnonymousCallerIdentityForDiscovery(declared, {
|
|
140
|
+
GRIST_API_KEY: "secret",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(
|
|
144
|
+
resolveHeaders(
|
|
145
|
+
{ "X-Stigmer-Caller-Kind": `\${${CALLER_IDENTITY_KIND_ENV_KEY}}` },
|
|
146
|
+
env,
|
|
147
|
+
),
|
|
148
|
+
).toEqual({ "X-Stigmer-Caller-Kind": ANONYMOUS_KIND });
|
|
149
|
+
expect(env[CALLER_IDENTITY_VALUE_ENV_KEY]).toBe("");
|
|
150
|
+
expect(env.GRIST_API_KEY).toBe("secret");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("is declaration-gated: servers that never declared the keys get an untouched map", () => {
|
|
154
|
+
const input = { OTHER: "x" };
|
|
155
|
+
const result = injectAnonymousCallerIdentityForDiscovery(new Set(["OTHER"]), input);
|
|
156
|
+
expect(result).toBe(input);
|
|
157
|
+
expect(result).not.toHaveProperty(CALLER_IDENTITY_KIND_ENV_KEY);
|
|
158
|
+
});
|
|
159
|
+
});
|