@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.
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,276 @@
1
+ /**
2
+ * The channel messaging attachment (proactive-messaging DD-006 D7/D8):
3
+ * discovery with the never-throw failure posture, both connection
4
+ * shapes, the structural approval-freedom the datastore attachment
5
+ * pinned before it, and the prompt section's filter/order/cap rules
6
+ * (DD-006 D6). The cross-repo pinned strings (slug, route, roster) are
7
+ * guarded here and in the mcp-server integration test — the
8
+ * TOOL_CALL_LIMIT precedent.
9
+ */
10
+
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { Code, ConnectError } from "@connectrpc/connect";
13
+ import type {
14
+ ChannelTemplate,
15
+ MessagingChannel,
16
+ } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
17
+
18
+ import { mockStigmerClient } from "../../__test-utils__/mock-client.js";
19
+ import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js";
20
+ import { needsBackfill } from "../connect-backfill.js";
21
+ import {
22
+ CHANNEL_ATTACHMENT_SLUG,
23
+ CHANNELS_ROUTE,
24
+ TEMPLATE_SECTION_CAP,
25
+ discoverChannelMessaging,
26
+ formatChannelTemplatesSection,
27
+ synthesizeChannelAttachment,
28
+ type ChannelMessagingInfo,
29
+ } from "../channel-attachment.js";
30
+ import type { ResolvedMcpServer } from "../mcp-resolver.js";
31
+
32
+ function channel(slug: string): MessagingChannel {
33
+ return { channel: slug, provider: "whatsapp" } as MessagingChannel;
34
+ }
35
+
36
+ function template(overrides: Partial<ChannelTemplate>): ChannelTemplate {
37
+ return {
38
+ name: "fee_reminder",
39
+ language: "en",
40
+ category: "UTILITY",
41
+ status: "APPROVED",
42
+ parameterFormat: "POSITIONAL",
43
+ parameterNames: ["1", "2"],
44
+ bodyText: "Hi {{1}}, your fee of {{2}} is due.",
45
+ headerFormat: "",
46
+ rejectionReason: "",
47
+ unsupportedReason: "",
48
+ ...overrides,
49
+ } as ChannelTemplate;
50
+ }
51
+
52
+ function info(slug: string, templates: ChannelTemplate[]): ChannelMessagingInfo {
53
+ return { channel: channel(slug), templates };
54
+ }
55
+
56
+ const noLeases: ActiveLeases = {
57
+ global: false,
58
+ categories: new Set(),
59
+ servers: new Set(),
60
+ };
61
+
62
+ describe("discoverChannelMessaging (the DD-006 D4 failure posture)", () => {
63
+ it("returns channels with their templates, threading the scoped credential", async () => {
64
+ const client = mockStigmerClient({
65
+ listMessagingChannels: vi.fn().mockResolvedValue([channel("isc-whatsapp")]),
66
+ listChannelTemplates: vi.fn().mockResolvedValue([template({})]),
67
+ });
68
+
69
+ const result = await discoverChannelMessaging(client, "scoped-tok");
70
+
71
+ expect(result).toHaveLength(1);
72
+ expect(result[0].channel.channel).toBe("isc-whatsapp");
73
+ expect(result[0].templates).toHaveLength(1);
74
+ expect(client.listMessagingChannels).toHaveBeenCalledWith("scoped-tok");
75
+ expect(client.listChannelTemplates).toHaveBeenCalledWith("isc-whatsapp", "scoped-tok");
76
+ });
77
+
78
+ it("answers empty for the everyday no-channel agent without fetching templates", async () => {
79
+ const client = mockStigmerClient();
80
+
81
+ expect(await discoverChannelMessaging(client, undefined)).toEqual([]);
82
+ expect(client.listChannelTemplates).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it.each([
86
+ ["UNIMPLEMENTED (a control plane predating 3a)", Code.Unimplemented],
87
+ ["FAILED_PRECONDITION (the OSS posture on older servers)", Code.FailedPrecondition],
88
+ ["PERMISSION_DENIED (an unexpected reach refusal)", Code.PermissionDenied],
89
+ ])("degrades %s to honest absence, never a throw", async (_label, code) => {
90
+ const client = mockStigmerClient({
91
+ listMessagingChannels: vi.fn().mockRejectedValue(new ConnectError("nope", code)),
92
+ });
93
+ const quiet = vi.spyOn(console, "debug").mockImplementation(() => {});
94
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
95
+
96
+ expect(await discoverChannelMessaging(client, undefined)).toEqual([]);
97
+
98
+ quiet.mockRestore();
99
+ warn.mockRestore();
100
+ });
101
+
102
+ it("a template-read failure degrades PER CHANNEL — the tool survives, the section entry does not", async () => {
103
+ const client = mockStigmerClient({
104
+ listMessagingChannels: vi.fn().mockResolvedValue([channel("wa-a"), channel("wa-b")]),
105
+ listChannelTemplates: vi.fn()
106
+ .mockImplementation(async (slug: string) => {
107
+ if (slug === "wa-a") throw new ConnectError("registry down", Code.Unavailable);
108
+ return [template({})];
109
+ }),
110
+ });
111
+ const quiet = vi.spyOn(console, "debug").mockImplementation(() => {});
112
+
113
+ const result = await discoverChannelMessaging(client, undefined);
114
+
115
+ expect(result.map((r) => [r.channel.channel, r.templates.length])).toEqual([
116
+ ["wa-a", 0],
117
+ ["wa-b", 1],
118
+ ]);
119
+ quiet.mockRestore();
120
+ });
121
+ });
122
+
123
+ describe("synthesizeChannelAttachment", () => {
124
+ const options = {
125
+ bridgeEndpoint: "https://mcp.stigmer.ai/",
126
+ credential: "sandbox-token",
127
+ backendEndpoint: "http://localhost:7234",
128
+ };
129
+
130
+ it("returns undefined when the agent serves no proactive channel", () => {
131
+ expect(synthesizeChannelAttachment([], options)).toBeUndefined();
132
+ });
133
+
134
+ it("builds the HTTP shape against the bridge /channels route with the credential", () => {
135
+ const attachment = synthesizeChannelAttachment([info("isc-whatsapp", [])], options);
136
+ expect(attachment).toMatchObject({
137
+ slug: CHANNEL_ATTACHMENT_SLUG,
138
+ connectionType: "http",
139
+ url: "https://mcp.stigmer.ai/channels",
140
+ headers: { Authorization: "Bearer sandbox-token" },
141
+ });
142
+ });
143
+
144
+ it("builds the OSS stdio shape: the CLI-embedded bridge with the channels roster", () => {
145
+ const attachment = synthesizeChannelAttachment([info("isc-whatsapp", [])], {
146
+ bridgeEndpoint: null,
147
+ credential: null,
148
+ backendEndpoint: "http://localhost:7234",
149
+ });
150
+ expect(attachment).toMatchObject({
151
+ connectionType: "stdio",
152
+ command: "stigmer",
153
+ args: ["mcp-server"],
154
+ env: {
155
+ STIGMER_MCP_ROSTER: "channels",
156
+ STIGMER_SERVER_ADDRESS: "localhost:7234",
157
+ },
158
+ });
159
+ });
160
+
161
+ it("pins the cross-repo strings the mcp-server side guards too", () => {
162
+ expect(CHANNEL_ATTACHMENT_SLUG).toBe("stigmer-channels");
163
+ expect(CHANNELS_ROUTE).toBe("/channels");
164
+ });
165
+
166
+ it("is approval-free by construction: zero entries in the merged approval map", () => {
167
+ const attachment = synthesizeChannelAttachment([info("isc-whatsapp", [])], options)!;
168
+ // Forced, not convenient (DD-002 D6): both calling surfaces run
169
+ // UNATTENDED mode, where a gated tool resolves as skip-and-adapt —
170
+ // a gated send tool means reminders never send.
171
+ const merged = mergeApprovalPolicies(
172
+ [attachment as ResolvedMcpServer], [], noLeases,
173
+ );
174
+ expect(merged.size).toBe(0);
175
+ });
176
+
177
+ it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
178
+ const attachment = synthesizeChannelAttachment([info("isc-whatsapp", [])], options)!;
179
+ expect(attachment.discoveredCapabilitiesEmpty).toBe(false);
180
+ expect(needsBackfill(attachment)).toBe(false);
181
+ });
182
+ });
183
+
184
+ describe("formatChannelTemplatesSection (DD-006 D6)", () => {
185
+ it("renders sendable templates with body text, parameters, and the image-header requirement", () => {
186
+ const section = formatChannelTemplatesSection([
187
+ info("isc-whatsapp", [
188
+ template({}),
189
+ template({
190
+ name: "invoice_qr",
191
+ parameterFormat: "NAMED",
192
+ parameterNames: ["member_name", "amount"],
193
+ bodyText: "Hello {{member_name}}, pay {{amount}}.",
194
+ headerFormat: "IMAGE",
195
+ }),
196
+ ]),
197
+ ]);
198
+
199
+ expect(section).toContain("<available_channel_templates>");
200
+ expect(section).toContain("channel: isc-whatsapp (whatsapp)");
201
+ expect(section).toContain("- fee_reminder (en) [UTILITY], parameters: 1, 2");
202
+ expect(section).toContain('"Hi {{1}}, your fee of {{2}} is due."');
203
+ expect(section).toContain(
204
+ "- invoice_qr (en) [UTILITY], parameters: member_name, amount"
205
+ + " (requires header_image_link: a public HTTPS image URL)",
206
+ );
207
+ expect(section).toContain("send_channel_message");
208
+ expect(section).toContain("</available_channel_templates>");
209
+ });
210
+
211
+ it("filters unsendable templates out entirely — the console is the diagnosis surface", () => {
212
+ const section = formatChannelTemplatesSection([
213
+ info("isc-whatsapp", [
214
+ template({}),
215
+ template({
216
+ name: "dynamic_promo",
217
+ unsupportedReason: "this template has a dynamic-URL button, which this platform"
218
+ + " version cannot supply parameters for",
219
+ }),
220
+ ]),
221
+ ]);
222
+
223
+ expect(section).toContain("fee_reminder");
224
+ expect(section).not.toContain("dynamic_promo");
225
+ });
226
+
227
+ it("returns \"\" when nothing is sendable — the tool alone still serves text sends", () => {
228
+ expect(formatChannelTemplatesSection([info("isc-whatsapp", [])])).toBe("");
229
+ expect(formatChannelTemplatesSection([
230
+ info("isc-whatsapp", [template({ unsupportedReason: "unsupported" })]),
231
+ ])).toBe("");
232
+ });
233
+
234
+ it("orders deterministically by (name, language) — never registry order", () => {
235
+ const section = formatChannelTemplatesSection([
236
+ info("isc-whatsapp", [
237
+ template({ name: "b_second", language: "en" }),
238
+ template({ name: "a_first", language: "hi" }),
239
+ template({ name: "a_first", language: "en" }),
240
+ ]),
241
+ ]);
242
+
243
+ const first = section.indexOf("a_first (en)");
244
+ const second = section.indexOf("a_first (hi)");
245
+ const third = section.indexOf("b_second (en)");
246
+ expect(first).toBeGreaterThan(-1);
247
+ expect(second).toBeGreaterThan(first);
248
+ expect(third).toBeGreaterThan(second);
249
+ });
250
+
251
+ it("caps the section and names how many were withheld", () => {
252
+ const many = Array.from({ length: TEMPLATE_SECTION_CAP + 5 }, (_, i) =>
253
+ template({ name: `t_${String(i).padStart(3, "0")}` }));
254
+
255
+ const section = formatChannelTemplatesSection([info("isc-whatsapp", many)]);
256
+
257
+ expect(section).toContain(`t_${String(TEMPLATE_SECTION_CAP - 1).padStart(3, "0")}`);
258
+ expect(section).not.toContain(`t_${String(TEMPLATE_SECTION_CAP).padStart(3, "0")}`);
259
+ expect(section).toContain("(5 more approved templates not shown)");
260
+ });
261
+
262
+ it("the cap spans channels; a later channel with no budget left is omitted whole", () => {
263
+ const fill = Array.from({ length: TEMPLATE_SECTION_CAP }, (_, i) =>
264
+ template({ name: `t_${String(i).padStart(3, "0")}` }));
265
+
266
+ const section = formatChannelTemplatesSection([
267
+ info("wa-a", fill),
268
+ info("wa-b", [template({ name: "starved" })]),
269
+ ]);
270
+
271
+ expect(section).toContain("channel: wa-a (whatsapp)");
272
+ expect(section).not.toContain("channel: wa-b (whatsapp)");
273
+ expect(section).not.toContain("starved");
274
+ expect(section).toContain("(1 more approved template not shown)");
275
+ });
276
+ });
@@ -14,9 +14,9 @@ import { needsBackfill } from "../connect-backfill.js";
14
14
  import {
15
15
  DATASTORE_ATTACHMENT_SLUG,
16
16
  formatDatastoresSection,
17
- injectDatastoreAttachment,
18
17
  synthesizeDatastoreAttachment,
19
18
  } from "../datastore-attachment.js";
19
+ import { injectSynthesizedAttachment } from "../synthesized-attachment.js";
20
20
  import type { ResolvedMcpServer } from "../mcp-resolver.js";
21
21
 
22
22
  function usage(slug: string) {
@@ -113,7 +113,7 @@ describe("synthesizeDatastoreAttachment", () => {
113
113
  });
114
114
  });
115
115
 
116
- describe("injectDatastoreAttachment", () => {
116
+ describe("injectSynthesizedAttachment (the shared injection path)", () => {
117
117
  const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
118
118
  bridgeEndpoint: "https://mcp.stigmer.ai",
119
119
  credential: "tok",
@@ -129,7 +129,7 @@ describe("injectDatastoreAttachment", () => {
129
129
  pinnedToolApprovals: [],
130
130
  discoveredCapabilitiesEmpty: false,
131
131
  };
132
- const result = injectDatastoreAttachment([other], attachment);
132
+ const result = injectSynthesizedAttachment([other], attachment, "datastore records");
133
133
  expect(result.map((s) => s.slug)).toEqual(["github", DATASTORE_ATTACHMENT_SLUG]);
134
134
  });
135
135
 
@@ -144,7 +144,7 @@ describe("injectDatastoreAttachment", () => {
144
144
  discoveredCapabilitiesEmpty: false,
145
145
  };
146
146
 
147
- const result = injectDatastoreAttachment([impostor], attachment);
147
+ const result = injectSynthesizedAttachment([impostor], attachment, "datastore records");
148
148
 
149
149
  expect(result).toHaveLength(1);
150
150
  expect(result[0].url).toBe("https://mcp.stigmer.ai/records");
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The caller identity for MCP server configs — reserved platform env keys
3
+ * that carry the platform-verified "who is calling" into user-defined MCP
4
+ * server headers/args, without ever passing through the model.
5
+ *
6
+ * The identity is a (kind, value) pair with fixed precedence:
7
+ * 1. The channel sender (Meta/Slack-verified, stamped into
8
+ * `SessionSpec.metadata` by the cloud broker — sender-identity.ts is
9
+ * the reader).
10
+ * 2. The session creator (`stigmer_user`) from the Session resource's
11
+ * audit actor — console/CLI sessions have no channel sender, but the
12
+ * platform knows exactly who created the session.
13
+ * 3. The anonymous sentinel — discovery (no session exists) and sessions
14
+ * with no readable creator. Consumers must treat anonymous as a
15
+ * first-class caller: answer tools/list, refuse tool calls.
16
+ *
17
+ * Injection is opt-in by construction: the values enter the env map used
18
+ * for MCP placeholder resolution, and `filterEnvToDeclaredKeys` already
19
+ * restricts every server to the keys it declared in `spec.env`. A server
20
+ * that never declares the reserved keys never receives identity.
21
+ * Declarations MUST be `optional: true` — execution creation validates
22
+ * declared-env completeness in both editions, and these keys have no value
23
+ * until the runner injects them.
24
+ *
25
+ * Trust model: the resulting header is RUNNER-asserted, not signed. It
26
+ * closes the prompt-injection hole (the model cannot influence the value),
27
+ * but the receiving server must pair it with a shared secret and be
28
+ * operated by someone who trusts the runner's network path.
29
+ */
30
+
31
+ import { readSenderIdentity } from "./sender-identity.js";
32
+
33
+ /** Reserved env key: the identity's kind token. */
34
+ export const CALLER_IDENTITY_KIND_ENV_KEY = "STIGMER_CALLER_IDENTITY_KIND";
35
+
36
+ /** Reserved env key: the identity's value. */
37
+ export const CALLER_IDENTITY_VALUE_ENV_KEY = "STIGMER_CALLER_IDENTITY_VALUE";
38
+
39
+ /** Reserved env key: the session the identity was resolved for. */
40
+ export const SESSION_ID_ENV_KEY = "STIGMER_SESSION_ID";
41
+
42
+ /**
43
+ * Kind token for a platform user (console/CLI session creator). Channel
44
+ * kinds (`whatsapp_phone`, `slack_user_id`, ...) pass through VERBATIM
45
+ * from the cloud broker's metadata — this module never rewrites them.
46
+ */
47
+ export const STIGMER_USER_KIND = "stigmer_user";
48
+
49
+ /**
50
+ * Kind token for the anonymous caller. Deliberately a real token rather
51
+ * than an absent key: every declared placeholder must resolve in every
52
+ * resolution context, or discovery fails with PlaceholderResolutionError
53
+ * before the server's tools are ever classified.
54
+ */
55
+ export const ANONYMOUS_KIND = "anonymous";
56
+
57
+ /** The resolved caller identity. */
58
+ export interface CallerIdentity {
59
+ kind: string;
60
+ value: string;
61
+ }
62
+
63
+ /** The audit actor shape read from `status.audit.spec_audit.created_by`. */
64
+ export interface SessionCreatorActor {
65
+ id?: string;
66
+ email?: string;
67
+ }
68
+
69
+ /** The identity injected when no session context exists (discovery). */
70
+ export function anonymousCallerIdentity(): CallerIdentity {
71
+ return { kind: ANONYMOUS_KIND, value: "" };
72
+ }
73
+
74
+ /**
75
+ * Resolve the caller identity for a session: channel sender first, then
76
+ * the session creator, then anonymous.
77
+ *
78
+ * The creator value prefers email over id: bindings are maintained by
79
+ * humans, and the audit actor's `id` field is historically mixed
80
+ * (identity-account id vs email — see the proto's own @internal note).
81
+ * Binding matchers should compare emails case-insensitively.
82
+ */
83
+ export function resolveCallerIdentity(
84
+ sessionMetadata: Record<string, string> | undefined,
85
+ creator?: SessionCreatorActor,
86
+ ): CallerIdentity {
87
+ const sender = readSenderIdentity(sessionMetadata);
88
+ if (sender) {
89
+ return { kind: sender.kind, value: sender.value };
90
+ }
91
+
92
+ const email = creator?.email?.trim();
93
+ const id = creator?.id?.trim();
94
+ const value = email || id;
95
+ if (value) {
96
+ return { kind: STIGMER_USER_KIND, value };
97
+ }
98
+
99
+ return anonymousCallerIdentity();
100
+ }
101
+
102
+ /**
103
+ * Return a NEW env map with the reserved caller-identity keys set —
104
+ * platform values are authoritative over same-named user entries (the
105
+ * injectPlatformEnv precedent: a user env var must never be able to
106
+ * impersonate a caller).
107
+ *
108
+ * Call this on the env map handed to MCP resolution ONLY — never on the
109
+ * map that reaches agent subprocess environments. Per-server opt-in is
110
+ * enforced downstream by filterEnvToDeclaredKeys.
111
+ */
112
+ export function injectCallerIdentityEnv(
113
+ envVars: Record<string, string>,
114
+ identity: CallerIdentity,
115
+ sessionId: string,
116
+ ): Record<string, string> {
117
+ const reserved: Record<string, string> = {
118
+ [CALLER_IDENTITY_KIND_ENV_KEY]: identity.kind,
119
+ [CALLER_IDENTITY_VALUE_ENV_KEY]: identity.value,
120
+ [SESSION_ID_ENV_KEY]: sessionId,
121
+ };
122
+
123
+ for (const [key, value] of Object.entries(reserved)) {
124
+ if (key in envVars && envVars[key] !== value) {
125
+ console.info(
126
+ `Platform env var '${key}' overrides value from ExecutionContext ` +
127
+ `(caller-identity vars are authoritative)`,
128
+ );
129
+ }
130
+ }
131
+
132
+ return { ...envVars, ...reserved };
133
+ }
134
+
135
+ /**
136
+ * Discovery-context injection: the connect workflow resolves the same
137
+ * header templates with no session, so every declared reserved key gets
138
+ * the anonymous sentinel — otherwise a caller-identity-templating server
139
+ * can never be discovered. Gated on the server's declared keys, matching
140
+ * injectPlatformEnv's contract in the discovery activity.
141
+ */
142
+ export function injectAnonymousCallerIdentityForDiscovery(
143
+ declaredEnvKeys: ReadonlySet<string>,
144
+ envVars: Record<string, string>,
145
+ ): Record<string, string> {
146
+ const anonymous = anonymousCallerIdentity();
147
+ const sentinels: Record<string, string> = {
148
+ [CALLER_IDENTITY_KIND_ENV_KEY]: anonymous.kind,
149
+ [CALLER_IDENTITY_VALUE_ENV_KEY]: anonymous.value,
150
+ [SESSION_ID_ENV_KEY]: "",
151
+ };
152
+
153
+ let result: Record<string, string> | undefined;
154
+ for (const [key, value] of Object.entries(sentinels)) {
155
+ if (!declaredEnvKeys.has(key)) continue;
156
+ if (!result) result = { ...envVars };
157
+ result[key] = value;
158
+ }
159
+
160
+ return result ?? envVars;
161
+ }