@stigmer/runner 3.12.7 → 3.12.9

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 (79) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/fetch-interceptor.js +29 -16
  3. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.d.ts +14 -0
  5. package/dist/activities/execute-cursor/index.js +76 -5
  6. package/dist/activities/execute-cursor/index.js.map +1 -1
  7. package/dist/activities/execute-cursor/message-translator.d.ts +15 -9
  8. package/dist/activities/execute-cursor/message-translator.js +15 -9
  9. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  10. package/dist/activities/execute-deep-agent/index.js +9 -0
  11. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  12. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +2 -0
  13. package/dist/activities/execute-deep-agent/mcp-gate.js +2 -1
  14. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  15. package/dist/activities/execute-deep-agent/setup.d.ts +10 -1
  16. package/dist/activities/execute-deep-agent/setup.js +45 -3
  17. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  18. package/dist/payload-codecs.d.ts +1 -1
  19. package/dist/payload-codecs.js +6 -4
  20. package/dist/payload-codecs.js.map +1 -1
  21. package/dist/shared/memory-attachment.d.ts +97 -0
  22. package/dist/shared/memory-attachment.js +136 -0
  23. package/dist/shared/memory-attachment.js.map +1 -0
  24. package/dist/shared/memory-retrieval.d.ts +126 -0
  25. package/dist/shared/memory-retrieval.js +293 -0
  26. package/dist/shared/memory-retrieval.js.map +1 -0
  27. package/dist/shared/runner-credential-keys.js +2 -1
  28. package/dist/shared/runner-credential-keys.js.map +1 -1
  29. package/dist/shared/tool-kind.js +9 -0
  30. package/dist/shared/tool-kind.js.map +1 -1
  31. package/package.json +3 -2
  32. package/src/__tests__/history-encryption-e2e.test.ts +1 -1
  33. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -1
  34. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +49 -15
  35. package/src/activities/execute-cursor/fetch-interceptor.ts +37 -16
  36. package/src/activities/execute-cursor/index.ts +86 -5
  37. package/src/activities/execute-cursor/message-translator.ts +15 -9
  38. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +7 -0
  39. package/src/activities/execute-deep-agent/index.ts +10 -0
  40. package/src/activities/execute-deep-agent/mcp-gate.ts +4 -1
  41. package/src/activities/execute-deep-agent/setup.ts +70 -6
  42. package/src/payload-codecs.ts +12 -9
  43. package/src/shared/__tests__/memory-attachment.test.ts +167 -0
  44. package/src/shared/__tests__/memory-retrieval.test.ts +310 -0
  45. package/src/shared/memory-attachment.ts +167 -0
  46. package/src/shared/memory-retrieval.ts +383 -0
  47. package/src/shared/runner-credential-keys.ts +2 -1
  48. package/src/shared/tool-kind.ts +10 -0
  49. package/dist/claimcheck/compressor.d.ts +0 -2
  50. package/dist/claimcheck/compressor.js +0 -8
  51. package/dist/claimcheck/compressor.js.map +0 -1
  52. package/dist/claimcheck/config.d.ts +0 -7
  53. package/dist/claimcheck/config.js +0 -10
  54. package/dist/claimcheck/config.js.map +0 -1
  55. package/dist/claimcheck/index.d.ts +0 -3
  56. package/dist/claimcheck/index.js +0 -4
  57. package/dist/claimcheck/index.js.map +0 -1
  58. package/dist/claimcheck/payload-codec.d.ts +0 -23
  59. package/dist/claimcheck/payload-codec.js +0 -105
  60. package/dist/claimcheck/payload-codec.js.map +0 -1
  61. package/dist/encryption/config.d.ts +0 -64
  62. package/dist/encryption/config.js +0 -113
  63. package/dist/encryption/config.js.map +0 -1
  64. package/dist/encryption/index.d.ts +0 -3
  65. package/dist/encryption/index.js +0 -3
  66. package/dist/encryption/index.js.map +0 -1
  67. package/dist/encryption/payload-codec.d.ts +0 -41
  68. package/dist/encryption/payload-codec.js +0 -130
  69. package/dist/encryption/payload-codec.js.map +0 -1
  70. package/src/__tests__/claimcheck-codec.test.ts +0 -256
  71. package/src/__tests__/encryption-codec.test.ts +0 -287
  72. package/src/__tests__/fixtures/encrypted-payload-fixture.json +0 -15
  73. package/src/claimcheck/compressor.ts +0 -9
  74. package/src/claimcheck/config.ts +0 -20
  75. package/src/claimcheck/index.ts +0 -3
  76. package/src/claimcheck/payload-codec.ts +0 -139
  77. package/src/encryption/config.ts +0 -161
  78. package/src/encryption/index.ts +0 -3
  79. package/src/encryption/payload-codec.ts +0 -152
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Semantic selection of recalled memories (stigmer/stigmer#293 Phase 3a,
3
+ * DD-008): when a subject's confirmed-fact set outgrows what wholesale
4
+ * injection should carry, select the most relevant facts for THIS
5
+ * execution instead of injecting everything.
6
+ *
7
+ * Sibling of recalled-memories.ts (which owns presentation) — this module
8
+ * owns SELECTION: which subset of the server-composed candidate set
9
+ * (`spec.recalled_memories`, the auditable snapshot both editions' compose
10
+ * steps stamp) actually rides the prompt. The compose steps never change;
11
+ * selection is a runner concern because the runner owns prompt assembly,
12
+ * provider credentials, and the metered proxy lane (DD-008 D1 — the
13
+ * titling-convergence doctrine: control-plane-adjacent LLM work runs once,
14
+ * in the shared runner, for both editions).
15
+ *
16
+ * Mechanism (DD-008 D2/D3): embed-on-read, no stored vectors anywhere.
17
+ * Selection activates ONLY above RETRIEVAL_K candidates; below that,
18
+ * top-k degenerates to wholesale, so no embeddings call is made and the
19
+ * shipped Phase 2 path runs untouched. When active: ONE batched
20
+ * embeddings call (query + all candidates), in-process cosine ranking,
21
+ * top-k by relevance, presented in snapshot order (relevance order would
22
+ * carry no information the model needs and would churn the prompt prefix).
23
+ * The query is the execution's `spec.message` — the current turn.
24
+ *
25
+ * The audit contract (DD-008 D5): the selection outcome is recorded in a
26
+ * runner-owned `RecalledMemoriesReport` on the execution status (the
27
+ * streaming_usage posture — one writer, written at prompt build). A report
28
+ * is returned whenever facts are injected, wholesale or selected; when
29
+ * nothing is injected (recall absent/disabled/empty) there is no report —
30
+ * absent report = wholesale, true by construction, so pre-3a executions
31
+ * read identically.
32
+ *
33
+ * Written-once across re-invocations: the same execution's prompt is
34
+ * rebuilt on approval resume (native) and fresh-agent recovery (cursor).
35
+ * Re-running selection there could pick a DIFFERENT subset mid-execution —
36
+ * so when the loaded execution already carries a report, this module
37
+ * REPLAYS it (selected ids resolved against the snapshot, or wholesale
38
+ * for a selection_active=false report) instead of re-embedding. Selection
39
+ * is computed at most once per execution, by construction.
40
+ *
41
+ * Failure posture: selection is an optimization. ANY failure — no
42
+ * embedder, HTTP error, timeout, malformed response — degrades to
43
+ * wholesale injection with a selection_active=false report, never a
44
+ * failed or degraded execution (DD-008 D3). Deployments with no
45
+ * embeddings-capable credential (Anthropic-only, Cursor-only OSS) run
46
+ * Phase 2 behavior unchanged, forever.
47
+ */
48
+
49
+ import { create } from "@bufbuild/protobuf";
50
+ import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
51
+ import {
52
+ RecalledMemoriesReportSchema,
53
+ type RecalledMemoriesReport,
54
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
55
+ import type { RecalledMemoriesContent } from "./recalled-memories.js";
56
+ import { resolveProxyBaseUrl, buildProxyHeaders } from "./llm-proxy.js";
57
+ import { checkDirectCredentials } from "./llm-backend.js";
58
+ import { getRunnerSecret } from "./runner-credential-store.js";
59
+
60
+ /**
61
+ * Selection activates only when the candidate set EXCEEDS this many facts;
62
+ * at or below it, top-k degenerates to wholesale and no embeddings call is
63
+ * made. 20 is comfortably above the dozens-scale where wholesale is fine
64
+ * (activation is rare) and comfortably below the 100-record cap (activation
65
+ * is meaningful): at 500-char facts, 20 facts ≈ 10KB of prompt. One
66
+ * constant, one place, deliberately not adaptive or per-org configurable
67
+ * in v1 (DD-008 D3).
68
+ */
69
+ export const RETRIEVAL_K = 20;
70
+
71
+ /**
72
+ * The v1 embedder (DD-008 D4). OpenAI-only: resolved through the Stigmer
73
+ * proxy when one is configured (cloud — platform key, metered), else the
74
+ * operator's direct OpenAI key (OSS). NOTE: llm-proxy's `inferProvider`
75
+ * does not know the `text-*` prefix — this module never infers; the
76
+ * provider is fixed alongside the model.
77
+ */
78
+ export const EMBEDDING_MODEL = "text-embedding-3-small";
79
+
80
+ /**
81
+ * Upper bound on the query text sent to the embedder. Facts are write-time
82
+ * capped at 500 chars, but the query is `spec.message` — unbounded. A giant
83
+ * pasted message would blow the embedder's per-input token limit (8192 for
84
+ * text-embedding-3-small) and 400 the WHOLE batched call, silently forcing
85
+ * wholesale on exactly the executions where selection matters. 20K chars
86
+ * sits safely under the limit at worst-case chars-per-token; selection
87
+ * intent is dominated by the message head, and a truncated query beats no
88
+ * selection.
89
+ */
90
+ export const QUERY_MAX_CHARS = 20_000;
91
+
92
+ /**
93
+ * Bound on the single embeddings round trip. Generous relative to the
94
+ * observed 200–400ms typical latency: this exists to keep a hung
95
+ * connection from stalling prompt build, not to race the provider.
96
+ */
97
+ const EMBED_TIMEOUT_MS = 15_000;
98
+
99
+ /** The OpenAI SDK-default base, used only in direct (unproxied) mode. */
100
+ const DIRECT_OPENAI_BASE_URL = "https://api.openai.com/v1";
101
+
102
+ /**
103
+ * One batched embeddings call: one vector per input, in input order.
104
+ * The seam unit tests inject through, and the boundary a stored-vector
105
+ * optimization would slot behind if caps ever grow (DD-008 D2).
106
+ */
107
+ export type EmbedFn = (inputs: readonly string[]) => Promise<number[][]>;
108
+
109
+ export interface MemoryRetrievalOptions {
110
+ /** Null/undefined when the deployment has no proxy (direct mode). */
111
+ readonly proxyEndpoint: string | null | undefined;
112
+ /** Bearer for proxy mode; unused in direct mode. */
113
+ readonly stigmerToken: string | null | undefined;
114
+ /** Scopes the proxied call for FGA authorization and billing attribution. */
115
+ readonly executionId: string;
116
+ /**
117
+ * The report a PREVIOUS invocation of this same execution recorded, if
118
+ * any (`execution.status.recalled_memories_report`). Presence replays
119
+ * the recorded outcome instead of re-selecting — the written-once rule.
120
+ */
121
+ readonly priorReport?: RecalledMemoriesReport;
122
+ /** Test seam; defaults to the proxy/direct embedder resolution. */
123
+ readonly embed?: EmbedFn;
124
+ }
125
+
126
+ export interface MemorySelectionResult {
127
+ /**
128
+ * The facts to inject, in snapshot order — undefined when recall is
129
+ * absent, disabled, or empty (render nothing, exactly the
130
+ * readRecalledMemories contract).
131
+ */
132
+ readonly content: RecalledMemoriesContent | undefined;
133
+ /**
134
+ * The injection outcome to stamp on the execution status. Undefined
135
+ * exactly when `content` is undefined: no injection, no report.
136
+ */
137
+ readonly report: RecalledMemoriesReport | undefined;
138
+ }
139
+
140
+ /** A candidate fact: the snapshot entry with its audit identity intact. */
141
+ interface Candidate {
142
+ readonly memoryId: string;
143
+ readonly content: string;
144
+ }
145
+
146
+ /**
147
+ * Select the facts to inject for one execution.
148
+ *
149
+ * This is the ONE entry point both harnesses call at prompt build, replacing
150
+ * their direct `readRecalledMemories` reads on the injection path (the read
151
+ * function remains the presentation-side authority; this module reads the
152
+ * proto itself because selection needs `memory_id`, which the render
153
+ * boundary deliberately strips).
154
+ *
155
+ * Never throws: every failure path returns wholesale.
156
+ */
157
+ export async function selectRecalledFacts(
158
+ recalled: RecalledMemories | undefined,
159
+ queryText: string,
160
+ options: MemoryRetrievalOptions,
161
+ ): Promise<MemorySelectionResult> {
162
+ const candidates = readCandidates(recalled);
163
+ if (candidates.length === 0) {
164
+ return { content: undefined, report: undefined };
165
+ }
166
+
167
+ // Written-once: a prior invocation of this execution already decided.
168
+ if (options.priorReport !== undefined) {
169
+ const replayed = replayReport(candidates, options.priorReport);
170
+ if (replayed !== undefined) {
171
+ return replayed;
172
+ }
173
+ // Unreplayable (recorded ids missing from the immutable snapshot —
174
+ // structurally impossible, defended anyway): fall through and select
175
+ // fresh rather than inject nothing.
176
+ log(
177
+ `prior report for execution ${options.executionId} did not resolve ` +
178
+ `against the snapshot; re-selecting`,
179
+ );
180
+ }
181
+
182
+ if (candidates.length <= RETRIEVAL_K) {
183
+ return wholesale(candidates);
184
+ }
185
+
186
+ const embed = options.embed ?? resolveEmbedder(options);
187
+ if (embed === undefined) {
188
+ // No embeddings-capable credential: the recorded no-embedder posture
189
+ // (DD-008 D4) — Phase 2 behavior, honestly reported.
190
+ return wholesale(candidates);
191
+ }
192
+
193
+ try {
194
+ const query = queryText.slice(0, QUERY_MAX_CHARS);
195
+ const vectors = await embed([query, ...candidates.map((c) => c.content)]);
196
+ if (vectors.length !== candidates.length + 1) {
197
+ throw new Error(
198
+ `embedder returned ${vectors.length} vectors for ` +
199
+ `${candidates.length + 1} inputs`,
200
+ );
201
+ }
202
+ const [queryVector, ...factVectors] = vectors;
203
+ const selected = topKBySimilarity(queryVector, factVectors, RETRIEVAL_K);
204
+ return {
205
+ content: { facts: selected.map((i) => candidates[i].content) },
206
+ report: create(RecalledMemoriesReportSchema, {
207
+ selectionActive: true,
208
+ injectedMemoryIds: selected.map((i) => candidates[i].memoryId),
209
+ embeddingModel: EMBEDDING_MODEL,
210
+ }),
211
+ };
212
+ } catch (err) {
213
+ log(
214
+ `selection failed for execution ${options.executionId}, degrading to ` +
215
+ `wholesale: ${err instanceof Error ? err.message : String(err)}`,
216
+ );
217
+ return wholesale(candidates);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * The snapshot's renderable candidates, in snapshot order. Same semantics
223
+ * as readRecalledMemories (disabled → none; blank contents dropped
224
+ * defensively) but keeping `memory_id` — the report's audit link.
225
+ */
226
+ function readCandidates(recalled: RecalledMemories | undefined): Candidate[] {
227
+ if (!recalled?.enabled) {
228
+ return [];
229
+ }
230
+ return (recalled.facts ?? [])
231
+ .map((fact) => ({
232
+ memoryId: fact.memoryId ?? "",
233
+ content: fact.content?.trim() ?? "",
234
+ }))
235
+ .filter((c) => c.content !== "");
236
+ }
237
+
238
+ /**
239
+ * Replay a previously recorded outcome so a re-invocation injects exactly
240
+ * what the first invocation did. A selection_active=false report replays
241
+ * as wholesale (including the case where a transient embed failure was
242
+ * recorded — retrying could select a subset and diverge the prompt).
243
+ * Returns undefined when a recorded id no longer resolves.
244
+ */
245
+ function replayReport(
246
+ candidates: Candidate[],
247
+ prior: RecalledMemoriesReport,
248
+ ): MemorySelectionResult | undefined {
249
+ if (!prior.selectionActive) {
250
+ return wholesale(candidates);
251
+ }
252
+ const byId = new Map(candidates.map((c) => [c.memoryId, c.content]));
253
+ const facts: string[] = [];
254
+ for (const id of prior.injectedMemoryIds) {
255
+ const content = byId.get(id);
256
+ if (content === undefined) {
257
+ return undefined;
258
+ }
259
+ facts.push(content);
260
+ }
261
+ if (facts.length === 0) {
262
+ return undefined;
263
+ }
264
+ return { content: { facts }, report: prior };
265
+ }
266
+
267
+ /** Wholesale injection of the full candidate set, honestly reported. */
268
+ function wholesale(candidates: Candidate[]): MemorySelectionResult {
269
+ return {
270
+ content: { facts: candidates.map((c) => c.content) },
271
+ report: create(RecalledMemoriesReportSchema, { selectionActive: false }),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * The credential lanes, in the platform's standing precedence (the
277
+ * titling-lane idiom): proxy when configured — the runner authenticates
278
+ * with its Stigmer token and the proxy owns the provider key — else the
279
+ * operator's direct OpenAI key, else no embedder.
280
+ */
281
+ function resolveEmbedder(options: MemoryRetrievalOptions): EmbedFn | undefined {
282
+ if (options.proxyEndpoint) {
283
+ return fetchEmbedder(
284
+ resolveProxyBaseUrl(options.proxyEndpoint, "openai"),
285
+ buildProxyHeaders(options.stigmerToken ?? "", {
286
+ executionId: options.executionId,
287
+ }),
288
+ );
289
+ }
290
+ if (checkDirectCredentials("openai") === null) {
291
+ return fetchEmbedder(DIRECT_OPENAI_BASE_URL, {
292
+ Authorization: `Bearer ${getRunnerSecret("OPENAI_API_KEY") ?? ""}`,
293
+ });
294
+ }
295
+ return undefined;
296
+ }
297
+
298
+ /**
299
+ * The real embedder: one POST {base}/embeddings. A plain typed fetch, not
300
+ * LangChain's OpenAIEmbeddings — the SDK wrapper discards the response
301
+ * `usage` block and adds nothing over a single request. Proxy-side
302
+ * metering reads the JSON usage from the relayed body.
303
+ */
304
+ function fetchEmbedder(baseUrl: string, headers: Record<string, string>): EmbedFn {
305
+ return async (inputs) => {
306
+ const response = await fetch(`${baseUrl}/embeddings`, {
307
+ method: "POST",
308
+ headers: { ...headers, "Content-Type": "application/json" },
309
+ body: JSON.stringify({ model: EMBEDDING_MODEL, input: inputs }),
310
+ signal: AbortSignal.timeout(EMBED_TIMEOUT_MS),
311
+ });
312
+ if (!response.ok) {
313
+ const body = await response.text().catch(() => "");
314
+ throw new Error(
315
+ `embeddings request failed: HTTP ${response.status} ${body.slice(0, 300)}`,
316
+ );
317
+ }
318
+ const parsed = (await response.json()) as {
319
+ data?: Array<{ index?: number; embedding?: number[] }>;
320
+ };
321
+ if (!Array.isArray(parsed.data)) {
322
+ throw new Error("embeddings response carries no data array");
323
+ }
324
+ // Place by the response's own index field — the API documents input
325
+ // order, but the contract names the index as authoritative.
326
+ const vectors: number[][] = new Array(inputs.length);
327
+ for (const item of parsed.data) {
328
+ const index = item.index ?? -1;
329
+ if (index < 0 || index >= inputs.length || !Array.isArray(item.embedding)) {
330
+ throw new Error("embeddings response entry is malformed");
331
+ }
332
+ vectors[index] = item.embedding;
333
+ }
334
+ if (vectors.some((v) => v === undefined)) {
335
+ throw new Error("embeddings response is missing entries");
336
+ }
337
+ return vectors;
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Indices of the top-k facts by cosine similarity to the query, returned
343
+ * ASCENDING — i.e. re-sorted to snapshot order (DD-008 D3: selection is by
344
+ * relevance, presentation stays oldest-first). Ties break toward the lower
345
+ * snapshot index, so equal scores never reorder across invocations.
346
+ */
347
+ function topKBySimilarity(
348
+ queryVector: number[],
349
+ factVectors: number[][],
350
+ k: number,
351
+ ): number[] {
352
+ return factVectors
353
+ .map((vector, index) => ({ index, score: cosineSimilarity(queryVector, vector) }))
354
+ .sort((a, b) => b.score - a.score || a.index - b.index)
355
+ .slice(0, k)
356
+ .map((entry) => entry.index)
357
+ .sort((a, b) => a - b);
358
+ }
359
+
360
+ /**
361
+ * Plain cosine. OpenAI embeddings arrive unit-normalized (a dot product
362
+ * would suffice today), but normalizing here keeps ranking correct under
363
+ * any future embedder without a silent provider assumption.
364
+ */
365
+ function cosineSimilarity(a: number[], b: number[]): number {
366
+ let dot = 0;
367
+ let normA = 0;
368
+ let normB = 0;
369
+ const length = Math.min(a.length, b.length);
370
+ for (let i = 0; i < length; i++) {
371
+ dot += a[i] * b[i];
372
+ normA += a[i] * a[i];
373
+ normB += b[i] * b[i];
374
+ }
375
+ if (normA === 0 || normB === 0) {
376
+ return 0;
377
+ }
378
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
379
+ }
380
+
381
+ function log(msg: string): void {
382
+ console.warn(`[memory-retrieval] ${msg}`);
383
+ }
@@ -63,7 +63,8 @@ export const RUNNER_CREDENTIAL_ENV_KEYS: readonly string[] = [
63
63
  * rotation bookkeeping, not secrets.
64
64
  */
65
65
  export const RUNNER_ENCRYPTION_ENV_KEYS: readonly string[] = [
66
- // Temporal payload-encryption keys (encryption/config.ts). An agent that
66
+ // Temporal payload-encryption keys (@stigmer/temporal-codecs' encryption
67
+ // config, read through the injected getRunnerSecret). An agent that
67
68
  // reads these could decrypt the runner's Temporal history payloads.
68
69
  "STIGMER_PAYLOAD_ENCRYPTION_KEY",
69
70
  "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY",
@@ -14,6 +14,8 @@
14
14
 
15
15
  import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
16
16
 
17
+ import { MEMORY_ATTACHMENT_SLUG } from "./memory-attachment.js";
18
+
17
19
  // Bare tool name -> ToolKind, covering both harness naming conventions. A name
18
20
  // found here is a built-in and wins over a non-empty mcp_server_slug (an MCP
19
21
  // server is not expected to shadow a built-in name; matching the legacy resolver).
@@ -81,6 +83,14 @@ const TOOL_NAME_TO_KIND: ReadonlyMap<string, ToolKind> = new Map([
81
83
  * back to a name lookup, so this is never worse than no classification).
82
84
  */
83
85
  export function classifyTool(name: string, mcpServerSlug?: string): ToolKind {
86
+ // The first-party remember tool (DD-005), slug-scoped on purpose: it is
87
+ // served by the synthesized memory attachment, so only that reserved
88
+ // slug earns the MEMORY kind (and its consent-chip rendering) — a
89
+ // third-party MCP server's coincidental `remember` stays a plain MCP
90
+ // tool, and a bare `remember` with no slug stays unclassified.
91
+ if (name === "remember" && mcpServerSlug === MEMORY_ATTACHMENT_SLUG) {
92
+ return ToolKind.MEMORY;
93
+ }
84
94
  const builtin = TOOL_NAME_TO_KIND.get(name);
85
95
  if (builtin !== undefined) {
86
96
  return builtin;
@@ -1,2 +0,0 @@
1
- export declare function compress(data: Buffer): Buffer<ArrayBuffer>;
2
- export declare function decompress(data: Buffer): Buffer<ArrayBuffer>;
@@ -1,8 +0,0 @@
1
- import { gzipSync, gunzipSync } from "node:zlib";
2
- export function compress(data) {
3
- return gzipSync(data);
4
- }
5
- export function decompress(data) {
6
- return gunzipSync(data);
7
- }
8
- //# sourceMappingURL=compressor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compressor.js","sourceRoot":"","sources":["../../src/claimcheck/compressor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEjD,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,QAAQ,CAAC,IAAI,CAAwB,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,UAAU,CAAC,IAAI,CAAwB,CAAC;AACjD,CAAC"}
@@ -1,7 +0,0 @@
1
- export interface ClaimcheckConfig {
2
- readonly enabled: boolean;
3
- readonly thresholdBytes: number;
4
- readonly compressionEnabled: boolean;
5
- readonly keyPrefix: string;
6
- }
7
- export declare function loadClaimcheckConfig(): ClaimcheckConfig;
@@ -1,10 +0,0 @@
1
- const DEFAULT_THRESHOLD_BYTES = 128 * 1024; // 128KB
2
- export function loadClaimcheckConfig() {
3
- return {
4
- enabled: process.env.CLAIMCHECK_ENABLED === "true",
5
- thresholdBytes: parseInt(process.env.CLAIMCHECK_THRESHOLD_BYTES ?? String(DEFAULT_THRESHOLD_BYTES), 10),
6
- compressionEnabled: process.env.CLAIMCHECK_COMPRESSION_ENABLED !== "false",
7
- keyPrefix: process.env.CLAIMCHECK_KEY_PREFIX ?? "claimcheck/",
8
- };
9
- }
10
- //# sourceMappingURL=config.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/claimcheck/config.ts"],"names":[],"mappings":"AAOA,MAAM,uBAAuB,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,QAAQ;AAEpD,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,MAAM;QAClD,cAAc,EAAE,QAAQ,CACtB,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,MAAM,CAAC,uBAAuB,CAAC,EACzE,EAAE,CACH;QACD,kBAAkB,EAAE,OAAO,CAAC,GAAG,CAAC,8BAA8B,KAAK,OAAO;QAC1E,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,aAAa;KAC9D,CAAC;AACJ,CAAC"}
@@ -1,3 +0,0 @@
1
- export { ClaimcheckPayloadCodec } from "./payload-codec.js";
2
- export { loadClaimcheckConfig, type ClaimcheckConfig } from "./config.js";
3
- export { compress, decompress } from "./compressor.js";
@@ -1,4 +0,0 @@
1
- export { ClaimcheckPayloadCodec } from "./payload-codec.js";
2
- export { loadClaimcheckConfig } from "./config.js";
3
- export { compress, decompress } from "./compressor.js";
4
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/claimcheck/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAyB,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1,23 +0,0 @@
1
- /**
2
- * Temporal PayloadCodec that transparently offloads large payloads to
3
- * external storage (ArtifactStorage). Payloads below the threshold pass
4
- * through unchanged. Payloads at or above the threshold are compressed
5
- * (optional), uploaded, and replaced with a small reference marker.
6
- *
7
- * On decode, markers are detected, the original payload is downloaded
8
- * and decompressed, and the original bytes are restored — transparent
9
- * to workflow/activity code.
10
- */
11
- import type { Payload, PayloadCodec } from "@temporalio/common";
12
- import type { ArtifactStorage } from "../shared/artifact-storage.js";
13
- import type { ClaimcheckConfig } from "./config.js";
14
- export declare class ClaimcheckPayloadCodec implements PayloadCodec {
15
- private readonly storage;
16
- private readonly config;
17
- constructor(storage: ArtifactStorage, config: ClaimcheckConfig);
18
- encode(payloads: Payload[]): Promise<Payload[]>;
19
- decode(payloads: Payload[]): Promise<Payload[]>;
20
- private encodePayload;
21
- private decodePayload;
22
- private isClaimcheckPayload;
23
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * Temporal PayloadCodec that transparently offloads large payloads to
3
- * external storage (ArtifactStorage). Payloads below the threshold pass
4
- * through unchanged. Payloads at or above the threshold are compressed
5
- * (optional), uploaded, and replaced with a small reference marker.
6
- *
7
- * On decode, markers are detected, the original payload is downloaded
8
- * and decompressed, and the original bytes are restored — transparent
9
- * to workflow/activity code.
10
- */
11
- import { randomUUID } from "node:crypto";
12
- import { compress, decompress } from "./compressor.js";
13
- const MARKER_METADATA_KEY = "encoding";
14
- const MARKER_ENCODING_VALUE = "binary/claimcheck";
15
- export class ClaimcheckPayloadCodec {
16
- storage;
17
- config;
18
- constructor(storage, config) {
19
- this.storage = storage;
20
- this.config = config;
21
- }
22
- async encode(payloads) {
23
- return Promise.all(payloads.map((p) => this.encodePayload(p)));
24
- }
25
- async decode(payloads) {
26
- return Promise.all(payloads.map((p) => this.decodePayload(p)));
27
- }
28
- async encodePayload(payload) {
29
- const data = payload.data;
30
- if (!data || data.length < this.config.thresholdBytes) {
31
- return payload;
32
- }
33
- const originalBuf = Buffer.from(data);
34
- let uploadBuf = originalBuf;
35
- let compressed = false;
36
- if (this.config.compressionEnabled) {
37
- const compressedBuf = compress(originalBuf);
38
- if (compressedBuf.length < originalBuf.length) {
39
- uploadBuf = compressedBuf;
40
- compressed = true;
41
- }
42
- }
43
- const key = `${this.config.keyPrefix}${randomUUID()}`;
44
- await this.storage.upload(key, uploadBuf, "application/octet-stream");
45
- const marker = {
46
- key,
47
- size: data.length,
48
- compressed,
49
- metadata: serializeMetadata(payload.metadata),
50
- };
51
- return {
52
- metadata: {
53
- [MARKER_METADATA_KEY]: Buffer.from(MARKER_ENCODING_VALUE),
54
- },
55
- data: Buffer.from(JSON.stringify(marker)),
56
- };
57
- }
58
- async decodePayload(payload) {
59
- if (!this.isClaimcheckPayload(payload)) {
60
- return payload;
61
- }
62
- const marker = JSON.parse(Buffer.from(payload.data).toString("utf-8"));
63
- let rawBuf;
64
- try {
65
- rawBuf = await this.storage.download(marker.key);
66
- }
67
- catch (err) {
68
- // Preserve the claimcheck-scoped error contract; the shared download error
69
- // already carries the HTTP status (proxy) or the miss (local) as the cause.
70
- const cause = err instanceof Error ? err.message : String(err);
71
- throw new Error(`Claimcheck retrieve failed for key ${marker.key}: ${cause}`);
72
- }
73
- const dataBuf = marker.compressed ? decompress(rawBuf) : rawBuf;
74
- return {
75
- metadata: marker.metadata
76
- ? deserializeMetadata(marker.metadata)
77
- : payload.metadata,
78
- data: dataBuf,
79
- };
80
- }
81
- isClaimcheckPayload(payload) {
82
- const encoding = payload.metadata?.[MARKER_METADATA_KEY];
83
- if (!encoding)
84
- return false;
85
- return Buffer.from(encoding).toString("utf-8") === MARKER_ENCODING_VALUE;
86
- }
87
- }
88
- function serializeMetadata(metadata) {
89
- if (!metadata)
90
- return undefined;
91
- const out = {};
92
- for (const [key, value] of Object.entries(metadata)) {
93
- if (value)
94
- out[key] = Buffer.from(value).toString("base64");
95
- }
96
- return out;
97
- }
98
- function deserializeMetadata(metadata) {
99
- const out = {};
100
- for (const [key, value] of Object.entries(metadata)) {
101
- out[key] = Buffer.from(value, "base64");
102
- }
103
- return out;
104
- }
105
- //# sourceMappingURL=payload-codec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"payload-codec.js","sourceRoot":"","sources":["../../src/claimcheck/payload-codec.ts"],"names":[],"mappings":"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"}
@@ -1,64 +0,0 @@
1
- /**
2
- * Payload-encryption configuration (stigmer-cloud#227, stigmer#398).
3
- *
4
- * Two key sources, strict precedence:
5
- *
6
- * 1. Environment (STIGMER_PAYLOAD_ENCRYPTION_KEY(_ID)) — the operator's
7
- * explicit choice: self-hosted deployments sharing one key with their
8
- * server, and cloud sandboxes injected with the platform key. When the
9
- * env key is set, bootstrap-delivered material is ignored entirely.
10
- * 2. Bootstrap-delivered — server-managed per-identity keys handed to
11
- * desktop-class runners by getRunnerBootstrapConfig. Held in memory
12
- * only; persistence lives server-side, which is what makes Temporal
13
- * replay work across runner restarts (every boot re-fetches the SAME
14
- * key).
15
- *
16
- * Encryption is enabled iff a key is present from either source — the same
17
- * enabled-iff-configured pattern as the claim-check codec. A malformed key
18
- * fails the boot rather than silently running plaintext: an operator who set
19
- * the key (or a server that minted one) intended history to be encrypted.
20
- *
21
- * Key rotation: payloads carry the id of the key that encrypted them.
22
- * During a rotation window the previous key stays readable via the
23
- * secondary pair while new payloads are written under the primary key.
24
- * Workers capture keys at construction, so a rotated bootstrap key lands
25
- * on the next runner boot — there is no live re-key.
26
- */
27
- export interface EncryptionKey {
28
- readonly keyId: string;
29
- /** 32-byte AES-256 key. */
30
- readonly key: Buffer;
31
- }
32
- export interface PayloadEncryptionConfig {
33
- /** Key used to encrypt outgoing payloads (and decrypt its own). */
34
- readonly primary: EncryptionKey;
35
- /** Decrypt-only key accepted during rotation windows. */
36
- readonly secondary?: EncryptionKey;
37
- }
38
- /**
39
- * Server-managed key material delivered by getRunnerBootstrapConfig.
40
- * Structurally mirrors {@link BootstrapPayloadEncryptionKeys} in
41
- * stigmer-client.ts — declared here so this leaf module stays free of
42
- * client imports.
43
- */
44
- export interface BootstrapKeyMaterial {
45
- readonly key: string;
46
- readonly keyId?: string;
47
- readonly secondaryKey?: string;
48
- readonly secondaryKeyId?: string;
49
- }
50
- /**
51
- * Returns the encryption config, or undefined when encryption is not
52
- * configured (the codec is then simply not installed).
53
- *
54
- * Source precedence: the env key wins outright; bootstrap-delivered
55
- * material applies only when no env key is set (see the module doc).
56
- *
57
- * @throws when a key is present but malformed, or a key id is missing —
58
- * key misconfiguration must stop the boot, not degrade to plaintext.
59
- * This applies equally to bootstrap material: a server that hands out
60
- * a bad key or omits its id has broken the protocol contract, and
61
- * running plaintext against a server that manages keys would silently
62
- * defeat the feature.
63
- */
64
- export declare function loadPayloadEncryptionConfig(bootstrap?: BootstrapKeyMaterial): PayloadEncryptionConfig | undefined;