@stigmer/runner 3.5.2 → 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.
Files changed (45) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/discover-mcp-server.js +9 -1
  3. package/dist/activities/discover-mcp-server.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.d.ts +5 -0
  5. package/dist/activities/execute-cursor/index.js +49 -9
  6. package/dist/activities/execute-cursor/index.js.map +1 -1
  7. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -0
  8. package/dist/activities/execute-cursor/prompt-builder.js +9 -0
  9. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  10. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +6 -0
  11. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  12. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  13. package/dist/activities/execute-deep-agent/setup.js +45 -9
  14. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  15. package/dist/client/stigmer-client.d.ts +26 -0
  16. package/dist/client/stigmer-client.js +37 -0
  17. package/dist/client/stigmer-client.js.map +1 -1
  18. package/dist/shared/caller-identity.d.ts +89 -0
  19. package/dist/shared/caller-identity.js +124 -0
  20. package/dist/shared/caller-identity.js.map +1 -0
  21. package/dist/shared/channel-attachment.d.ts +85 -0
  22. package/dist/shared/channel-attachment.js +203 -0
  23. package/dist/shared/channel-attachment.js.map +1 -0
  24. package/dist/shared/datastore-attachment.d.ts +2 -25
  25. package/dist/shared/datastore-attachment.js +1 -28
  26. package/dist/shared/datastore-attachment.js.map +1 -1
  27. package/dist/shared/synthesized-attachment.d.ts +51 -0
  28. package/dist/shared/synthesized-attachment.js +45 -0
  29. package/dist/shared/synthesized-attachment.js.map +1 -0
  30. package/package.json +2 -2
  31. package/src/__test-utils__/mock-client.ts +4 -0
  32. package/src/activities/__tests__/discover-mcp-server.test.ts +49 -0
  33. package/src/activities/discover-mcp-server.ts +13 -1
  34. package/src/activities/execute-cursor/index.ts +72 -13
  35. package/src/activities/execute-cursor/prompt-builder.ts +19 -0
  36. package/src/activities/execute-deep-agent/prompt-builder.ts +10 -0
  37. package/src/activities/execute-deep-agent/setup.ts +66 -10
  38. package/src/client/stigmer-client.ts +46 -0
  39. package/src/shared/__tests__/caller-identity.test.ts +159 -0
  40. package/src/shared/__tests__/channel-attachment.test.ts +276 -0
  41. package/src/shared/__tests__/datastore-attachment.test.ts +4 -4
  42. package/src/shared/caller-identity.ts +161 -0
  43. package/src/shared/channel-attachment.ts +237 -0
  44. package/src/shared/datastore-attachment.ts +2 -54
  45. package/src/shared/synthesized-attachment.ts +77 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The runner-synthesized channel messaging attachment (proactive-messaging
3
+ * DD-006 D7/D8) — the datastore records attachment's structural twin.
4
+ *
5
+ * When the control plane says an agent serves at least one
6
+ * proactive-messaging channel (the `listMessagingChannels` discovery
7
+ * read, DD-006 D2 — the SAME candidate computation the send
8
+ * authorization runs, so attachment and authority cannot disagree), the
9
+ * runner synthesizes ONE MCP attachment serving `send_channel_message`,
10
+ * and injects the `<available_channel_templates>` prompt section so the
11
+ * model composes template sends in context without spending a tool
12
+ * round (DD-003 D5).
13
+ *
14
+ * Two connection shapes, one roster (the records pattern):
15
+ * - Bridge endpoint configured (cloud): Streamable HTTP against the
16
+ * bridge's /channels route with the execution's own session-scoped
17
+ * credential as the Bearer token.
18
+ * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
19
+ * stdio child with STIGMER_MCP_ROSTER=channels. In practice OSS
20
+ * answers the discovery read with an empty list (DD-006 D3), so
21
+ * this shape only serves local deployments that grow a messaging
22
+ * runtime later — it exists for symmetry with the deployment
23
+ * topology, not for a live OSS path today.
24
+ *
25
+ * Approval-free by construction, and FORCED, not convenient (DD-002
26
+ * D6): both calling surfaces run APPROVAL_MODE_UNATTENDED, where a
27
+ * gated tool resolves as skip-and-adapt — a gated send tool would mean
28
+ * reminders never send. Empty approval maps + no McpServerUsage keep
29
+ * the connect backfill structurally unable to gate it (see
30
+ * synthesized-attachment.ts). Callers inject AFTER resolve + backfill.
31
+ *
32
+ * Failure posture (DD-006 D4): every discovery failure — OSS's empty
33
+ * answer, a registry outage, a control plane predating the RPC
34
+ * (UNIMPLEMENTED), a reach refusal — degrades to honest absence: no
35
+ * tool, no section, execution unharmed.
36
+ */
37
+
38
+ import { Code, ConnectError } from "@connectrpc/connect";
39
+ import type {
40
+ ChannelTemplate,
41
+ MessagingChannel,
42
+ } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
43
+ import type { StigmerClient } from "../client/stigmer-client.js";
44
+ import type { ResolvedMcpServer } from "./mcp-resolver.js";
45
+ import { grpcTarget, type SynthesizedAttachmentOptions } from "./synthesized-attachment.js";
46
+
47
+ /**
48
+ * The synthesized attachment's slug. Reserved: a user McpServer with
49
+ * this slug is shadowed by the synthesized attachment, with a warning.
50
+ * Pinned cross-repo by the mcp-server integration test (the
51
+ * TOOL_CALL_LIMIT precedent).
52
+ */
53
+ export const CHANNEL_ATTACHMENT_SLUG = "stigmer-channels";
54
+
55
+ /** The bridge route serving the channels-only roster (mcp-server DD-006 D8). */
56
+ export const CHANNELS_ROUTE = "/channels";
57
+
58
+ /**
59
+ * The most templates the prompt section carries (DD-006 D6): Meta
60
+ * allows hundreds per WABA, and an unbounded section would tax every
61
+ * run's context. Deterministic (name, language) order plus a withheld
62
+ * count keep the agent's behavior independent of registry ordering.
63
+ */
64
+ export const TEMPLATE_SECTION_CAP = 30;
65
+
66
+ /** One channel plus its sendable approved templates, ready to format. */
67
+ export interface ChannelMessagingInfo {
68
+ channel: MessagingChannel;
69
+ templates: ChannelTemplate[];
70
+ }
71
+
72
+ /**
73
+ * The discovery read plus the per-channel template fetch, with the
74
+ * DD-006 D4 failure posture applied: this function NEVER throws — any
75
+ * failure returns an empty list (no tool, no section), because a
76
+ * messaging hiccup must not fail an execution that may not even want
77
+ * to send anything.
78
+ */
79
+ export async function discoverChannelMessaging(
80
+ client: StigmerClient,
81
+ scopedCredential: string | undefined,
82
+ ): Promise<ChannelMessagingInfo[]> {
83
+ let channels: MessagingChannel[];
84
+ try {
85
+ channels = await client.listMessagingChannels(scopedCredential);
86
+ } catch (err) {
87
+ logDiscoveryFailure("listMessagingChannels", err);
88
+ return [];
89
+ }
90
+ if (channels.length === 0) {
91
+ return [];
92
+ }
93
+
94
+ // Template reads degrade PER CHANNEL: a registry outage on one
95
+ // channel must not strip the section for another, and never the tool
96
+ // (a channel with unreadable templates can still send text inside a
97
+ // 24-hour window).
98
+ return Promise.all(channels.map(async (channel) => {
99
+ try {
100
+ return {
101
+ channel,
102
+ templates: await client.listChannelTemplates(channel.channel, scopedCredential),
103
+ };
104
+ } catch (err) {
105
+ logDiscoveryFailure(`listTemplates(${channel.channel})`, err);
106
+ return { channel, templates: [] };
107
+ }
108
+ }));
109
+ }
110
+
111
+ /**
112
+ * Synthesize the channel messaging attachment. Returns undefined when
113
+ * the agent serves no proactive channel — the attachment exists exactly
114
+ * when the discovery read says so.
115
+ */
116
+ export function synthesizeChannelAttachment(
117
+ channels: ChannelMessagingInfo[],
118
+ options: SynthesizedAttachmentOptions,
119
+ ): ResolvedMcpServer | undefined {
120
+ if (channels.length === 0) {
121
+ return undefined;
122
+ }
123
+
124
+ // Approval-free by construction + backfill-proof: see file header.
125
+ const base = {
126
+ slug: CHANNEL_ATTACHMENT_SLUG,
127
+ toolApprovals: [],
128
+ pinnedToolApprovals: [],
129
+ discoveredCapabilitiesEmpty: false,
130
+ };
131
+
132
+ if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
133
+ return {
134
+ ...base,
135
+ connectionType: "http",
136
+ url: options.bridgeEndpoint.replace(/\/+$/, "") + CHANNELS_ROUTE,
137
+ headers: options.credential !== null && options.credential !== ""
138
+ ? { Authorization: `Bearer ${options.credential}` }
139
+ : undefined,
140
+ };
141
+ }
142
+
143
+ return {
144
+ ...base,
145
+ connectionType: "stdio",
146
+ command: "stigmer",
147
+ args: ["mcp-server"],
148
+ env: {
149
+ STIGMER_MCP_ROSTER: "channels",
150
+ STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * The `<available_channel_templates>` prompt section (DD-003 D5):
157
+ * approved AND sendable templates with their full body text, so the
158
+ * model fills positional placeholders beside the values it composes.
159
+ * Unsendable entries are filtered, not annotated (DD-006 D6 — the
160
+ * console panel is the diagnosis surface, the prompt is a composition
161
+ * aid). Returns "" when nothing survives the filter — the tool alone
162
+ * still serves text sends inside a 24-hour window.
163
+ */
164
+ export function formatChannelTemplatesSection(channels: ChannelMessagingInfo[]): string {
165
+ let withheld = 0;
166
+ let budget = TEMPLATE_SECTION_CAP;
167
+
168
+ const channelBlocks: string[] = [];
169
+ for (const { channel, templates } of channels) {
170
+ // Sendable-only (unsupportedReason empty), deterministic order —
171
+ // agent behavior must never depend on registry ordering (DD-006 D6).
172
+ const sendable = templates
173
+ .filter((t) => t.unsupportedReason === "")
174
+ .sort((a, b) => a.name.localeCompare(b.name) || a.language.localeCompare(b.language));
175
+
176
+ const kept = sendable.slice(0, Math.max(budget, 0));
177
+ withheld += sendable.length - kept.length;
178
+ budget -= kept.length;
179
+ if (kept.length === 0) {
180
+ continue;
181
+ }
182
+
183
+ const lines = kept.map((t) => {
184
+ const parameters = t.parameterNames.length > 0
185
+ ? `, parameters: ${t.parameterNames.join(", ")}`
186
+ : "";
187
+ const header = t.headerFormat === "IMAGE"
188
+ ? " (requires header_image_link: a public HTTPS image URL)"
189
+ : "";
190
+ return [
191
+ ` - ${t.name} (${t.language}) [${t.category}]${parameters}${header}`,
192
+ ` "${t.bodyText}"`,
193
+ ].join("\n");
194
+ });
195
+ channelBlocks.push([`channel: ${channel.channel} (${channel.provider})`, ...lines].join("\n"));
196
+ }
197
+
198
+ if (channelBlocks.length === 0) {
199
+ return "";
200
+ }
201
+
202
+ const footer = withheld > 0
203
+ ? [`(${withheld} more approved template${withheld === 1 ? "" : "s"} not shown)`]
204
+ : [];
205
+ return [
206
+ "<available_channel_templates>",
207
+ "You can send business-initiated messages on the channels below with the",
208
+ "send_channel_message tool. Outside a 24-hour customer-service window the",
209
+ "provider only accepts a pre-approved template, so prefer a template. Fill",
210
+ "every placeholder from the conversation; never invent a value.",
211
+ "",
212
+ ...channelBlocks,
213
+ ...footer,
214
+ "</available_channel_templates>",
215
+ ].join("\n");
216
+ }
217
+
218
+ /**
219
+ * Expected absences log quietly; anything else warns so an operator can
220
+ * diagnose a mis-provisioned credential without failing the execution.
221
+ * UNIMPLEMENTED is a control plane predating the discovery RPC — the
222
+ * DD-006 D4 deploy-order self-healing case.
223
+ */
224
+ function logDiscoveryFailure(what: string, err: unknown): void {
225
+ const ce = ConnectError.from(err);
226
+ const expected =
227
+ ce.code === Code.Unimplemented ||
228
+ ce.code === Code.FailedPrecondition ||
229
+ ce.code === Code.Unavailable;
230
+ if (expected) {
231
+ console.debug(`[channel-attachment] ${what} degraded to honest absence: ${ce.message}`);
232
+ } else {
233
+ console.warn(
234
+ `[channel-attachment] ${what} failed unexpectedly (no tool, no section): ${ce.message}`,
235
+ );
236
+ }
237
+ }
@@ -33,6 +33,7 @@
33
33
 
34
34
  import type { DatastoreUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
35
35
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
36
+ import { grpcTarget, type SynthesizedAttachmentOptions } from "./synthesized-attachment.js";
36
37
 
37
38
  /**
38
39
  * The synthesized attachment's slug. Reserved: a user McpServer with
@@ -44,25 +45,6 @@ export const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
44
45
  /** The bridge route serving the records-only roster (mcp-server T05 R1). */
45
46
  export const RECORDS_ROUTE = "/records";
46
47
 
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
48
  /**
67
49
  * Synthesize the records attachment for an agent's datastore usages.
68
50
  * Returns undefined when the agent uses no datastores — the attachment
@@ -70,7 +52,7 @@ export interface DatastoreAttachmentOptions {
70
52
  */
71
53
  export function synthesizeDatastoreAttachment(
72
54
  datastoreUsages: DatastoreUsage[],
73
- options: DatastoreAttachmentOptions,
55
+ options: SynthesizedAttachmentOptions,
74
56
  ): ResolvedMcpServer | undefined {
75
57
  if (datastoreUsages.length === 0) {
76
58
  return undefined;
@@ -107,28 +89,6 @@ export function synthesizeDatastoreAttachment(
107
89
  };
108
90
  }
109
91
 
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
92
  /**
133
93
  * The `<available_datastores>` prompt section (DD-005 SD-5, the
134
94
  * skills-section precedent): names the attached datastores and points
@@ -152,15 +112,3 @@ export function formatDatastoresSection(datastoreUsages: DatastoreUsage[]): stri
152
112
  "</available_datastores>",
153
113
  ].join("\n");
154
114
  }
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,77 @@
1
+ /**
2
+ * Shared mechanics of runner-synthesized MCP attachments — the pieces
3
+ * the datastore records attachment (T05) and the channel messaging
4
+ * attachment (proactive-messaging DD-006 D8) have in common, extracted
5
+ * when the second attachment arrived.
6
+ *
7
+ * A synthesized attachment is a first-party MCP server entry the runner
8
+ * builds itself (no McpServer resource, no Environment, no credential in
9
+ * any manifest) on a RESERVED slug. Approval-freedom is structural, not
10
+ * configured: empty toolApprovals + pinnedToolApprovals mean
11
+ * mergeApprovalPolicies emits no entries, and discoveredCapabilitiesEmpty
12
+ * false + no McpServerUsage keep the connect backfill's destructiveHint
13
+ * tightener structurally unable to touch it. Callers must still inject
14
+ * AFTER resolve + backfill; every harness call site does.
15
+ */
16
+
17
+ import type { ResolvedMcpServer } from "./mcp-resolver.js";
18
+
19
+ /**
20
+ * Connection options for a synthesized attachment — one shape for every
21
+ * attachment because the deployment topology, not the domain, decides
22
+ * the connection.
23
+ */
24
+ export interface SynthesizedAttachmentOptions {
25
+ /**
26
+ * The bridge's HTTP endpoint (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
27
+ * https://mcp.stigmer.ai). Null selects the OSS/local stdio shape.
28
+ */
29
+ bridgeEndpoint: string | null;
30
+ /**
31
+ * The execution's session-scoped credential (the sandbox token a
32
+ * cloud runner holds, or the desktop runner's exchanged scoped
33
+ * token). Null attaches no Authorization header (OSS/local).
34
+ */
35
+ credential: string | null;
36
+ /**
37
+ * The stigmer backend endpoint the stdio child dials
38
+ * (config.stigmerBackendEndpoint). Only used for the OSS shape.
39
+ */
40
+ backendEndpoint: string;
41
+ }
42
+
43
+ /**
44
+ * Inject a synthesized attachment into a resolved server list — AFTER
45
+ * resolve + backfill (see the module header). A user server shadowing
46
+ * the reserved slug is replaced, loudly; `label` names the attachment
47
+ * in that warning (e.g. "datastore records").
48
+ */
49
+ export function injectSynthesizedAttachment(
50
+ resolvedServers: ResolvedMcpServer[],
51
+ attachment: ResolvedMcpServer,
52
+ label: string,
53
+ ): ResolvedMcpServer[] {
54
+ const shadowed = resolvedServers.some((s) => s.slug === attachment.slug);
55
+ if (shadowed) {
56
+ console.warn(
57
+ `MCP server slug "${attachment.slug}" is reserved for the ${label} ` +
58
+ "attachment; the user-defined server is replaced.",
59
+ );
60
+ }
61
+ return [
62
+ ...resolvedServers.filter((s) => s.slug !== attachment.slug),
63
+ attachment,
64
+ ];
65
+ }
66
+
67
+ /**
68
+ * The gRPC dial target (host:port) for a backend endpoint URL — the
69
+ * shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
70
+ */
71
+ export function grpcTarget(endpoint: string): string {
72
+ try {
73
+ return new URL(endpoint).host;
74
+ } catch {
75
+ return endpoint;
76
+ }
77
+ }