@stigmer/runner 3.12.7 → 3.12.8

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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The runner-synthesized memory capture attachment (DD-005 D1).
3
+ *
4
+ * When the execution's recall snapshot says memory is on
5
+ * (`spec.recalled_memories.enabled` — the ONE server-owned field that
6
+ * serves both the recall and capture sides, stamped by the create
7
+ * pipeline's compose step), the runner synthesizes ONE MCP attachment
8
+ * serving the `remember` tool. No discovery RPC exists on this path:
9
+ * unlike the channel attachment's registry read, the enablement answer
10
+ * is already on the spec — a free, synchronous read (the
11
+ * conversation-attachment session-label precedent). The enabled bit with
12
+ * ZERO facts is a meaningful state: memory is on, nothing stored yet —
13
+ * the tool is offered so the first fact can be proposed.
14
+ *
15
+ * Two connection shapes, one roster (the channels pattern):
16
+ * - Bridge endpoint configured (cloud): Streamable HTTP against the
17
+ * bridge's /memory route with the execution's own session-scoped
18
+ * credential as the Bearer token, plus the capture context as
19
+ * per-request headers.
20
+ * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
21
+ * stdio child with STIGMER_MCP_ROSTER=memory, plus the capture
22
+ * context as STIGMER_MEMORY_* env.
23
+ *
24
+ * The capture context (org + agent/session/execution ids) is
25
+ * attribution, never authorization (the Stage 3 provenance decision,
26
+ * owner-ratified 2026-08-22): the cloud create handler accepts it only
27
+ * from a session-sandbox credential and overrides session/org with the
28
+ * token's own claims; the OSS server stores it under the local
29
+ * single-user trust model. The subject is never threaded — the server
30
+ * derives it from the credential (DD-005 D2).
31
+ *
32
+ * Approval-free by construction (the synthesized-attachment contract):
33
+ * the tool only ever creates a PROPOSAL the user must confirm through
34
+ * the control plane, so gating the propose call would stack a second
35
+ * consent gate in front of the real one (DD-005 D3: consent is the
36
+ * confirm RPC, not tool approval). Callers inject AFTER resolve +
37
+ * backfill.
38
+ *
39
+ * Failure posture: the attachment is synthesized from values already in
40
+ * hand, so the only failure mode is the create RPC refusing at call
41
+ * time — which the mcp-server's memory error mapper relays honestly.
42
+ * When the snapshot is absent or disabled: no tool, honest absence.
43
+ */
44
+ import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
45
+ import type { ResolvedMcpServer } from "./mcp-resolver.js";
46
+ import { type SynthesizedAttachmentOptions } from "./synthesized-attachment.js";
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
+ * Runner-internal (the resolved-server name and shadow key — the
51
+ * mcp-server never sees it); pinned by this module's test. The ROUTE
52
+ * and the context keys below are cross-repo strings, pinned on both
53
+ * sides (the TOOL_CALL_LIMIT precedent).
54
+ */
55
+ export declare const MEMORY_ATTACHMENT_SLUG = "stigmer-memory";
56
+ /** The bridge route serving the memory-only roster (mcp-server twin: MEMORY_ROUTE). */
57
+ export declare const MEMORY_ROUTE = "/memory";
58
+ /**
59
+ * The capture-context carriers, mirrored byte-for-byte by the
60
+ * mcp-server's memory domain (domains/memory/context.ts): headers on
61
+ * the bridge's per-request path, env on the stdio child — the same
62
+ * per-request-then-startup split the credential itself uses.
63
+ */
64
+ export declare const MEMORY_ORG_HEADER = "x-stigmer-memory-org";
65
+ export declare const MEMORY_AGENT_ID_HEADER = "x-stigmer-memory-agent-id";
66
+ export declare const MEMORY_SESSION_ID_HEADER = "x-stigmer-memory-session-id";
67
+ export declare const MEMORY_EXECUTION_ID_HEADER = "x-stigmer-memory-execution-id";
68
+ export declare const MEMORY_ORG_ENV = "STIGMER_MEMORY_ORG";
69
+ export declare const MEMORY_AGENT_ID_ENV = "STIGMER_MEMORY_AGENT_ID";
70
+ export declare const MEMORY_SESSION_ID_ENV = "STIGMER_MEMORY_SESSION_ID";
71
+ export declare const MEMORY_EXECUTION_ID_ENV = "STIGMER_MEMORY_EXECUTION_ID";
72
+ /**
73
+ * Where a proposed memory comes from — threaded to the mcp-server so
74
+ * the create request carries org addressing and provenance. Empty
75
+ * fields are omitted from the carrier (best-effort attribution; the
76
+ * server treats absent as empty).
77
+ */
78
+ export interface MemoryCaptureContext {
79
+ readonly org: string;
80
+ readonly agentId: string;
81
+ readonly sessionId: string;
82
+ readonly agentExecutionId: string;
83
+ }
84
+ /**
85
+ * Reports whether the execution's recall snapshot offers the remember
86
+ * tool (DD-005 D1: the snapshot's enabled bit IS the runner's injection
87
+ * signal — one server-owned field, no parallel flag, no discovery
88
+ * round-trip). Exposed for the harnesses' MCP gates, which must open
89
+ * MCP resolution for a memory-only agent.
90
+ */
91
+ export declare function memoryCaptureEnabled(recalled: RecalledMemories | undefined): boolean;
92
+ /**
93
+ * Synthesize the memory capture attachment. Returns undefined when the
94
+ * recall snapshot is absent or disabled — the attachment exists exactly
95
+ * when the server-composed snapshot says memory is on.
96
+ */
97
+ export declare function synthesizeMemoryAttachment(recalled: RecalledMemories | undefined, context: MemoryCaptureContext, options: SynthesizedAttachmentOptions): ResolvedMcpServer | undefined;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The runner-synthesized memory capture attachment (DD-005 D1).
3
+ *
4
+ * When the execution's recall snapshot says memory is on
5
+ * (`spec.recalled_memories.enabled` — the ONE server-owned field that
6
+ * serves both the recall and capture sides, stamped by the create
7
+ * pipeline's compose step), the runner synthesizes ONE MCP attachment
8
+ * serving the `remember` tool. No discovery RPC exists on this path:
9
+ * unlike the channel attachment's registry read, the enablement answer
10
+ * is already on the spec — a free, synchronous read (the
11
+ * conversation-attachment session-label precedent). The enabled bit with
12
+ * ZERO facts is a meaningful state: memory is on, nothing stored yet —
13
+ * the tool is offered so the first fact can be proposed.
14
+ *
15
+ * Two connection shapes, one roster (the channels pattern):
16
+ * - Bridge endpoint configured (cloud): Streamable HTTP against the
17
+ * bridge's /memory route with the execution's own session-scoped
18
+ * credential as the Bearer token, plus the capture context as
19
+ * per-request headers.
20
+ * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
21
+ * stdio child with STIGMER_MCP_ROSTER=memory, plus the capture
22
+ * context as STIGMER_MEMORY_* env.
23
+ *
24
+ * The capture context (org + agent/session/execution ids) is
25
+ * attribution, never authorization (the Stage 3 provenance decision,
26
+ * owner-ratified 2026-08-22): the cloud create handler accepts it only
27
+ * from a session-sandbox credential and overrides session/org with the
28
+ * token's own claims; the OSS server stores it under the local
29
+ * single-user trust model. The subject is never threaded — the server
30
+ * derives it from the credential (DD-005 D2).
31
+ *
32
+ * Approval-free by construction (the synthesized-attachment contract):
33
+ * the tool only ever creates a PROPOSAL the user must confirm through
34
+ * the control plane, so gating the propose call would stack a second
35
+ * consent gate in front of the real one (DD-005 D3: consent is the
36
+ * confirm RPC, not tool approval). Callers inject AFTER resolve +
37
+ * backfill.
38
+ *
39
+ * Failure posture: the attachment is synthesized from values already in
40
+ * hand, so the only failure mode is the create RPC refusing at call
41
+ * time — which the mcp-server's memory error mapper relays honestly.
42
+ * When the snapshot is absent or disabled: no tool, honest absence.
43
+ */
44
+ import { grpcTarget } from "./synthesized-attachment.js";
45
+ /**
46
+ * The synthesized attachment's slug. Reserved: a user McpServer with
47
+ * this slug is shadowed by the synthesized attachment, with a warning.
48
+ * Runner-internal (the resolved-server name and shadow key — the
49
+ * mcp-server never sees it); pinned by this module's test. The ROUTE
50
+ * and the context keys below are cross-repo strings, pinned on both
51
+ * sides (the TOOL_CALL_LIMIT precedent).
52
+ */
53
+ export const MEMORY_ATTACHMENT_SLUG = "stigmer-memory";
54
+ /** The bridge route serving the memory-only roster (mcp-server twin: MEMORY_ROUTE). */
55
+ export const MEMORY_ROUTE = "/memory";
56
+ /**
57
+ * The capture-context carriers, mirrored byte-for-byte by the
58
+ * mcp-server's memory domain (domains/memory/context.ts): headers on
59
+ * the bridge's per-request path, env on the stdio child — the same
60
+ * per-request-then-startup split the credential itself uses.
61
+ */
62
+ export const MEMORY_ORG_HEADER = "x-stigmer-memory-org";
63
+ export const MEMORY_AGENT_ID_HEADER = "x-stigmer-memory-agent-id";
64
+ export const MEMORY_SESSION_ID_HEADER = "x-stigmer-memory-session-id";
65
+ export const MEMORY_EXECUTION_ID_HEADER = "x-stigmer-memory-execution-id";
66
+ export const MEMORY_ORG_ENV = "STIGMER_MEMORY_ORG";
67
+ export const MEMORY_AGENT_ID_ENV = "STIGMER_MEMORY_AGENT_ID";
68
+ export const MEMORY_SESSION_ID_ENV = "STIGMER_MEMORY_SESSION_ID";
69
+ export const MEMORY_EXECUTION_ID_ENV = "STIGMER_MEMORY_EXECUTION_ID";
70
+ /**
71
+ * Reports whether the execution's recall snapshot offers the remember
72
+ * tool (DD-005 D1: the snapshot's enabled bit IS the runner's injection
73
+ * signal — one server-owned field, no parallel flag, no discovery
74
+ * round-trip). Exposed for the harnesses' MCP gates, which must open
75
+ * MCP resolution for a memory-only agent.
76
+ */
77
+ export function memoryCaptureEnabled(recalled) {
78
+ return recalled?.enabled === true;
79
+ }
80
+ /**
81
+ * Synthesize the memory capture attachment. Returns undefined when the
82
+ * recall snapshot is absent or disabled — the attachment exists exactly
83
+ * when the server-composed snapshot says memory is on.
84
+ */
85
+ export function synthesizeMemoryAttachment(recalled, context, options) {
86
+ if (!memoryCaptureEnabled(recalled)) {
87
+ return undefined;
88
+ }
89
+ // Approval-free by construction + backfill-proof: see file header.
90
+ const base = {
91
+ slug: MEMORY_ATTACHMENT_SLUG,
92
+ toolApprovals: [],
93
+ pinnedToolApprovals: [],
94
+ toolApprovalOverrides: [],
95
+ discoveredCapabilitiesEmpty: false,
96
+ };
97
+ if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
98
+ return {
99
+ ...base,
100
+ connectionType: "http",
101
+ url: options.bridgeEndpoint.replace(/\/+$/, "") + MEMORY_ROUTE,
102
+ headers: {
103
+ ...(options.credential !== null && options.credential !== ""
104
+ ? { Authorization: `Bearer ${options.credential}` }
105
+ : undefined),
106
+ ...nonEmptyEntries([
107
+ [MEMORY_ORG_HEADER, context.org],
108
+ [MEMORY_AGENT_ID_HEADER, context.agentId],
109
+ [MEMORY_SESSION_ID_HEADER, context.sessionId],
110
+ [MEMORY_EXECUTION_ID_HEADER, context.agentExecutionId],
111
+ ]),
112
+ },
113
+ };
114
+ }
115
+ return {
116
+ ...base,
117
+ connectionType: "stdio",
118
+ command: "stigmer",
119
+ args: ["mcp-server"],
120
+ env: {
121
+ STIGMER_MCP_ROSTER: "memory",
122
+ STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
123
+ ...nonEmptyEntries([
124
+ [MEMORY_ORG_ENV, context.org],
125
+ [MEMORY_AGENT_ID_ENV, context.agentId],
126
+ [MEMORY_SESSION_ID_ENV, context.sessionId],
127
+ [MEMORY_EXECUTION_ID_ENV, context.agentExecutionId],
128
+ ]),
129
+ },
130
+ };
131
+ }
132
+ /** The non-empty context fields as carrier entries (absent means empty). */
133
+ function nonEmptyEntries(pairs) {
134
+ return Object.fromEntries(pairs.filter(([, value]) => value !== ""));
135
+ }
136
+ //# sourceMappingURL=memory-attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-attachment.js","sourceRoot":"","sources":["../../src/shared/memory-attachment.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@
12
12
  * this table and that fixture in lockstep.
13
13
  */
14
14
  import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
15
+ import { MEMORY_ATTACHMENT_SLUG } from "./memory-attachment.js";
15
16
  // Bare tool name -> ToolKind, covering both harness naming conventions. A name
16
17
  // found here is a built-in and wins over a non-empty mcp_server_slug (an MCP
17
18
  // server is not expected to shadow a built-in name; matching the legacy resolver).
@@ -68,6 +69,14 @@ const TOOL_NAME_TO_KIND = new Map([
68
69
  * back to a name lookup, so this is never worse than no classification).
69
70
  */
70
71
  export function classifyTool(name, mcpServerSlug) {
72
+ // The first-party remember tool (DD-005), slug-scoped on purpose: it is
73
+ // served by the synthesized memory attachment, so only that reserved
74
+ // slug earns the MEMORY kind (and its consent-chip rendering) — a
75
+ // third-party MCP server's coincidental `remember` stays a plain MCP
76
+ // tool, and a bare `remember` with no slug stays unclassified.
77
+ if (name === "remember" && mcpServerSlug === MEMORY_ATTACHMENT_SLUG) {
78
+ return ToolKind.MEMORY;
79
+ }
71
80
  const builtin = TOOL_NAME_TO_KIND.get(name);
72
81
  if (builtin !== undefined) {
73
82
  return builtin;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-kind.js","sourceRoot":"","sources":["../../src/shared/tool-kind.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-kind.js","sourceRoot":"","sources":["../../src/shared/tool-kind.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/runner",
3
- "version": "3.12.7",
3
+ "version": "3.12.8",
4
4
  "description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -94,7 +94,7 @@
94
94
  "@opentelemetry/sdk-metrics": "^2.0.0",
95
95
  "@opentelemetry/sdk-trace-base": "^2.0.0",
96
96
  "@opentelemetry/sdk-trace-node": "^2.0.0",
97
- "@stigmer/protos": "3.12.7",
97
+ "@stigmer/protos": "3.12.8",
98
98
  "@temporalio/activity": "1.16.2",
99
99
  "@temporalio/common": "1.16.2",
100
100
  "@temporalio/interceptors-opentelemetry": "1.16.2",
@@ -84,6 +84,7 @@ import {
84
84
  readChannelConversationId,
85
85
  synthesizeConversationAttachment,
86
86
  } from "../../shared/conversation-attachment.js";
87
+ import { synthesizeMemoryAttachment } from "../../shared/memory-attachment.js";
87
88
  import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
88
89
  import { mergeApprovalPolicies } from "./approval-policy.js";
89
90
  import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
@@ -717,6 +718,33 @@ async function executeCursorInner(
717
718
  resolvedMcpServers, conversationAttachment, "conversation participation",
718
719
  );
719
720
  }
721
+
722
+ // Phase 4a5: Synthesize the memory capture attachment (DD-005 D1) —
723
+ // the fourth sibling. The recall snapshot's enabled bit IS the
724
+ // attachment decision (server-stamped at execution create; a free
725
+ // local read, like the conversation attachment's session label). The
726
+ // capture context is attribution the server verifies or trusts per
727
+ // edition (Stage 3 provenance decision); the subject is never
728
+ // threaded — it derives from the credential.
729
+ const memoryAttachment = synthesizeMemoryAttachment(
730
+ execution.spec?.recalledMemories,
731
+ {
732
+ org: session.metadata?.org ?? "",
733
+ agentId: blueprint.agent.metadata?.id ?? "",
734
+ sessionId,
735
+ agentExecutionId: executionId,
736
+ },
737
+ {
738
+ bridgeEndpoint: config.mcpBridgeEndpoint,
739
+ credential: attachmentCredential,
740
+ backendEndpoint: config.stigmerBackendEndpoint,
741
+ },
742
+ );
743
+ if (memoryAttachment) {
744
+ resolvedMcpServers = injectSynthesizedAttachment(
745
+ resolvedMcpServers, memoryAttachment, "memory capture",
746
+ );
747
+ }
720
748
  // The one projection point: every mutation above is now visible in the
721
749
  // Cursor SDK config by construction (no per-mutation rebuild to forget).
722
750
  const mcpConfig = toCursorMcpConfig(resolvedMcpServers);
@@ -14,6 +14,7 @@ const nothing = {
14
14
  mcpServerUsageCount: 0,
15
15
  channelMessagingCount: 0,
16
16
  conversationChannelId: undefined,
17
+ memoryCaptureEnabled: false,
17
18
  };
18
19
 
19
20
  describe("shouldConnectMcp", () => {
@@ -34,4 +35,10 @@ describe("shouldConnectMcp", () => {
34
35
  // channel — the escalation tool is the ONLY source.
35
36
  expect(shouldConnectMcp({ ...nothing, conversationChannelId: "agch_1" })).toBe(true);
36
37
  });
38
+
39
+ it("enters on memory capture alone (the memory attachment)", () => {
40
+ // A tool-less agent whose user has memory on: the remember tool is
41
+ // the ONLY source, and skipping the block would silently drop it.
42
+ expect(shouldConnectMcp({ ...nothing, memoryCaptureEnabled: true })).toBe(true);
43
+ });
37
44
  });
@@ -22,6 +22,8 @@ export interface McpToolSources {
22
22
  /** The serving channel id when this session IS a live channel
23
23
  * conversation (the conversation attachment, DD-008). */
24
24
  readonly conversationChannelId: string | undefined;
25
+ /** The recall snapshot's enabled bit (the memory attachment, DD-005 D1). */
26
+ readonly memoryCaptureEnabled: boolean;
25
27
  }
26
28
 
27
29
  /** True when any tool source demands MCP resolution and connect. */
@@ -29,6 +31,7 @@ export function shouldConnectMcp(sources: McpToolSources): boolean {
29
31
  return (
30
32
  sources.mcpServerUsageCount > 0 ||
31
33
  sources.channelMessagingCount > 0 ||
32
- sources.conversationChannelId !== undefined
34
+ sources.conversationChannelId !== undefined ||
35
+ sources.memoryCaptureEnabled
33
36
  );
34
37
  }
@@ -47,6 +47,10 @@ import {
47
47
  readChannelConversationId,
48
48
  synthesizeConversationAttachment,
49
49
  } from "../../shared/conversation-attachment.js";
50
+ import {
51
+ memoryCaptureEnabled,
52
+ synthesizeMemoryAttachment,
53
+ } from "../../shared/memory-attachment.js";
50
54
  import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
51
55
  import { shouldConnectMcp } from "./mcp-gate.js";
52
56
  import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
@@ -390,11 +394,17 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
390
394
  // free, synchronous read, so no hoisted discovery needed.
391
395
  const conversationChannelId = readChannelConversationId(session.metadata?.labels);
392
396
 
397
+ // The memory-attachment decision (DD-005 D1): the recall snapshot's
398
+ // enabled bit, stamped server-side at execution create — like the
399
+ // conversation attachment, a free, synchronous spec read.
400
+ const captureMemories = memoryCaptureEnabled(execution.spec?.recalledMemories);
401
+
393
402
  let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
394
403
  if (shouldConnectMcp({
395
404
  mcpServerUsageCount: mcpServerUsages.length,
396
405
  channelMessagingCount: channelMessaging.length,
397
406
  conversationChannelId,
407
+ memoryCaptureEnabled: captureMemories,
398
408
  })) {
399
409
  await reportSetupProgress(client, executionId, "Connecting tools…");
400
410
  const transportPosture = resolveMcpTransportPosture(config.mode);
@@ -456,6 +466,32 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
456
466
  backfilledServers, conversationAttachment, "conversation participation",
457
467
  );
458
468
  }
469
+
470
+ // The memory capture attachment (DD-005 D1) — same after-backfill
471
+ // rule. The capture context is attribution the server verifies or
472
+ // trusts per edition (Stage 3 provenance decision); the subject is
473
+ // never threaded — it derives from the credential.
474
+ if (captureMemories) {
475
+ const memoryAttachment = synthesizeMemoryAttachment(
476
+ execution.spec?.recalledMemories,
477
+ {
478
+ org: session.metadata?.org ?? "",
479
+ agentId: agent.metadata?.id ?? "",
480
+ sessionId,
481
+ agentExecutionId: executionId,
482
+ },
483
+ {
484
+ bridgeEndpoint: config.mcpBridgeEndpoint,
485
+ credential: attachmentCredential,
486
+ backendEndpoint: config.stigmerBackendEndpoint,
487
+ },
488
+ );
489
+ if (memoryAttachment) {
490
+ backfilledServers = injectSynthesizedAttachment(
491
+ backfilledServers, memoryAttachment, "memory capture",
492
+ );
493
+ }
494
+ }
459
495
  resolvedMcpServers = { resolvedServers: backfilledServers };
460
496
  timing.mark("backfill_mcp");
461
497
 
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The memory capture attachment (DD-005 D1): the recall-snapshot gate
3
+ * (enabled bit, zero-facts included), both connection shapes with the
4
+ * capture-context carriers, the structural approval-freedom of
5
+ * synthesized attachments, and the cross-repo string pins (route +
6
+ * context keys, guarded here and in the mcp-server memory integration
7
+ * test — the TOOL_CALL_LIMIT precedent). The slug and roster are
8
+ * runner-internal and guarded here alone.
9
+ */
10
+
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { describe, expect, it } from "vitest";
13
+ import { RecalledMemoriesSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
14
+
15
+ import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js";
16
+ import { needsBackfill } from "../connect-backfill.js";
17
+ import {
18
+ MEMORY_AGENT_ID_ENV,
19
+ MEMORY_AGENT_ID_HEADER,
20
+ MEMORY_ATTACHMENT_SLUG,
21
+ MEMORY_EXECUTION_ID_ENV,
22
+ MEMORY_EXECUTION_ID_HEADER,
23
+ MEMORY_ORG_ENV,
24
+ MEMORY_ORG_HEADER,
25
+ MEMORY_ROUTE,
26
+ MEMORY_SESSION_ID_ENV,
27
+ MEMORY_SESSION_ID_HEADER,
28
+ memoryCaptureEnabled,
29
+ synthesizeMemoryAttachment,
30
+ type MemoryCaptureContext,
31
+ } from "../memory-attachment.js";
32
+ import type { ResolvedMcpServer } from "../mcp-resolver.js";
33
+
34
+ const context: MemoryCaptureContext = {
35
+ org: "acme",
36
+ agentId: "agt_1",
37
+ sessionId: "ses_1",
38
+ agentExecutionId: "aex_1",
39
+ };
40
+
41
+ const cloudOptions = {
42
+ bridgeEndpoint: "https://mcp.stigmer.ai/",
43
+ credential: "sandbox-token",
44
+ backendEndpoint: "http://localhost:7234",
45
+ };
46
+
47
+ const ossOptions = {
48
+ bridgeEndpoint: null,
49
+ credential: null,
50
+ backendEndpoint: "http://localhost:7234",
51
+ };
52
+
53
+ const noLeases: ActiveLeases = {
54
+ global: false,
55
+ categories: new Set(),
56
+ servers: new Set(),
57
+ };
58
+
59
+ describe("memoryCaptureEnabled (the DD-005 D1 injection signal)", () => {
60
+ it("is the snapshot's enabled bit — absent and disabled read false", () => {
61
+ expect(memoryCaptureEnabled(undefined)).toBe(false);
62
+ expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: false }))).toBe(false);
63
+ expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: true }))).toBe(true);
64
+ });
65
+
66
+ it("enabled with ZERO facts still offers the tool — memory on, nothing stored yet", () => {
67
+ const snapshot = create(RecalledMemoriesSchema, { enabled: true, facts: [] });
68
+ expect(memoryCaptureEnabled(snapshot)).toBe(true);
69
+ expect(synthesizeMemoryAttachment(snapshot, context, cloudOptions)).toBeDefined();
70
+ });
71
+ });
72
+
73
+ describe("synthesizeMemoryAttachment", () => {
74
+ const enabled = create(RecalledMemoriesSchema, { enabled: true });
75
+
76
+ it("returns undefined when the snapshot is absent or disabled — honest absence", () => {
77
+ expect(synthesizeMemoryAttachment(undefined, context, cloudOptions)).toBeUndefined();
78
+ expect(
79
+ synthesizeMemoryAttachment(
80
+ create(RecalledMemoriesSchema, { enabled: false }),
81
+ context,
82
+ cloudOptions,
83
+ ),
84
+ ).toBeUndefined();
85
+ });
86
+
87
+ it("builds the HTTP shape against the bridge /memory route with credential + context headers", () => {
88
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions);
89
+ expect(attachment).toMatchObject({
90
+ slug: MEMORY_ATTACHMENT_SLUG,
91
+ connectionType: "http",
92
+ url: "https://mcp.stigmer.ai/memory",
93
+ headers: {
94
+ Authorization: "Bearer sandbox-token",
95
+ [MEMORY_ORG_HEADER]: "acme",
96
+ [MEMORY_AGENT_ID_HEADER]: "agt_1",
97
+ [MEMORY_SESSION_ID_HEADER]: "ses_1",
98
+ [MEMORY_EXECUTION_ID_HEADER]: "aex_1",
99
+ },
100
+ });
101
+ });
102
+
103
+ it("builds the OSS stdio shape: the CLI-embedded bridge with the memory roster + context env", () => {
104
+ const attachment = synthesizeMemoryAttachment(enabled, context, ossOptions);
105
+ expect(attachment).toMatchObject({
106
+ connectionType: "stdio",
107
+ command: "stigmer",
108
+ args: ["mcp-server"],
109
+ env: {
110
+ STIGMER_MCP_ROSTER: "memory",
111
+ STIGMER_SERVER_ADDRESS: "localhost:7234",
112
+ [MEMORY_ORG_ENV]: "acme",
113
+ [MEMORY_AGENT_ID_ENV]: "agt_1",
114
+ [MEMORY_SESSION_ID_ENV]: "ses_1",
115
+ [MEMORY_EXECUTION_ID_ENV]: "aex_1",
116
+ },
117
+ });
118
+ });
119
+
120
+ it("omits empty context fields from the carrier — best-effort attribution, never blank entries", () => {
121
+ const partial: MemoryCaptureContext = {
122
+ org: "acme",
123
+ agentId: "",
124
+ sessionId: "ses_1",
125
+ agentExecutionId: "",
126
+ };
127
+
128
+ const http = synthesizeMemoryAttachment(enabled, partial, cloudOptions);
129
+ expect(Object.keys(http?.headers ?? {}).sort()).toEqual([
130
+ "Authorization",
131
+ MEMORY_ORG_HEADER,
132
+ MEMORY_SESSION_ID_HEADER,
133
+ ].sort());
134
+
135
+ const stdio = synthesizeMemoryAttachment(enabled, partial, ossOptions);
136
+ expect(stdio?.env).not.toHaveProperty(MEMORY_AGENT_ID_ENV);
137
+ expect(stdio?.env).not.toHaveProperty(MEMORY_EXECUTION_ID_ENV);
138
+ });
139
+
140
+ it("pins the cross-repo strings the mcp-server side guards too", () => {
141
+ expect(MEMORY_ATTACHMENT_SLUG).toBe("stigmer-memory");
142
+ expect(MEMORY_ROUTE).toBe("/memory");
143
+ expect(MEMORY_ORG_HEADER).toBe("x-stigmer-memory-org");
144
+ expect(MEMORY_AGENT_ID_HEADER).toBe("x-stigmer-memory-agent-id");
145
+ expect(MEMORY_SESSION_ID_HEADER).toBe("x-stigmer-memory-session-id");
146
+ expect(MEMORY_EXECUTION_ID_HEADER).toBe("x-stigmer-memory-execution-id");
147
+ expect(MEMORY_ORG_ENV).toBe("STIGMER_MEMORY_ORG");
148
+ expect(MEMORY_AGENT_ID_ENV).toBe("STIGMER_MEMORY_AGENT_ID");
149
+ expect(MEMORY_SESSION_ID_ENV).toBe("STIGMER_MEMORY_SESSION_ID");
150
+ expect(MEMORY_EXECUTION_ID_ENV).toBe("STIGMER_MEMORY_EXECUTION_ID");
151
+ });
152
+
153
+ it("is approval-free by construction: zero entries in the merged approval map", () => {
154
+ // Consent is the confirm RPC, not tool approval (DD-005 D3): the
155
+ // tool only creates a proposal, so gating it would stack a second
156
+ // consent gate in front of the real one.
157
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
158
+ const merged = mergeApprovalPolicies([attachment as ResolvedMcpServer], noLeases);
159
+ expect(merged.size).toBe(0);
160
+ });
161
+
162
+ it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
163
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
164
+ expect(attachment.discoveredCapabilitiesEmpty).toBe(false);
165
+ expect(needsBackfill(attachment)).toBe(false);
166
+ });
167
+ });