melchizedek-agents 1.3.1 → 1.5.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 (64) hide show
  1. package/.env.example +15 -0
  2. package/CHANGELOG.md +134 -0
  3. package/DOCUMENTATION.md +61 -5
  4. package/ONBOARDING.md +3 -3
  5. package/README.md +1 -0
  6. package/config/agents/examples/connectors.yaml +4 -1
  7. package/config/agents/examples/event_assistant.yaml +82 -0
  8. package/config/agents/syndicate.schema.json +80 -6
  9. package/db/hardening.sql +38 -12
  10. package/dist/lib/a2a/app.js +2 -1
  11. package/dist/lib/chatgpt/state.js +4 -4
  12. package/dist/lib/compile.js +15 -2
  13. package/dist/lib/compileNative.js +1 -0
  14. package/dist/lib/index.d.ts +2 -0
  15. package/dist/lib/index.js +2 -0
  16. package/dist/lib/loadSyndicate.js +4 -0
  17. package/dist/lib/memory/providers.js +2 -1
  18. package/dist/lib/models/claudeAdapter.js +2 -1
  19. package/dist/lib/models/credentialSource.d.ts +28 -0
  20. package/dist/lib/models/credentialSource.js +49 -0
  21. package/dist/lib/models/endpoints.js +2 -1
  22. package/dist/lib/models/gateway.js +2 -1
  23. package/dist/lib/models/gatewayAdapter.js +2 -1
  24. package/dist/lib/models/geminiAdapter.js +2 -1
  25. package/dist/lib/models/gptAdapter.js +2 -1
  26. package/dist/lib/models/grokAdapter.js +2 -1
  27. package/dist/lib/models/kimiAdapter.js +2 -1
  28. package/dist/lib/models/providerMap.js +3 -2
  29. package/dist/lib/observability/embeddings.js +2 -1
  30. package/dist/lib/observability/lineage.js +11 -3
  31. package/dist/lib/observability/supabaseSpanExporter.d.ts +19 -1
  32. package/dist/lib/observability/supabaseSpanExporter.js +37 -1
  33. package/dist/lib/observability/tracer.d.ts +9 -0
  34. package/dist/lib/observability/tracer.js +45 -0
  35. package/dist/lib/paths.d.ts +31 -0
  36. package/dist/lib/paths.js +76 -0
  37. package/dist/lib/runtime/credentials.d.ts +21 -0
  38. package/dist/lib/runtime/credentials.js +32 -0
  39. package/dist/lib/runtime/native/delegate.d.ts +2 -0
  40. package/dist/lib/runtime/native/delegate.js +9 -2
  41. package/dist/lib/runtime/native/interrupts.d.ts +33 -3
  42. package/dist/lib/runtime/native/interrupts.js +99 -9
  43. package/dist/lib/runtime/syndicateTurn.js +41 -11
  44. package/dist/lib/storage/postgres/index.d.ts +25 -0
  45. package/dist/lib/storage/postgres/index.js +10 -1
  46. package/dist/lib/syndicateSchema.d.ts +26 -4
  47. package/dist/lib/syndicateSchema.js +63 -1
  48. package/dist/lib/tools/generateImageTool.js +6 -6
  49. package/dist/lib/tools/inspectImageTool.d.ts +2 -0
  50. package/dist/lib/tools/inspectImageTool.js +17 -10
  51. package/dist/lib/tools/mcpToolFactory.d.ts +34 -1
  52. package/dist/lib/tools/mcpToolFactory.js +200 -27
  53. package/dist/lib/tools/mcpTransports.d.ts +4 -2
  54. package/dist/lib/tools/mcpTransports.js +4 -2
  55. package/dist/lib/tools/taskTools.js +3 -3
  56. package/dist/lib/tools/xApiSearchTool.js +2 -1
  57. package/dist/lib/workflow/agentNode.d.ts +19 -3
  58. package/dist/lib/workflow/agentNode.js +106 -13
  59. package/dist/lib/workflow/resume.d.ts +8 -7
  60. package/dist/lib/workflow/resume.js +17 -10
  61. package/dist/lib/workflow/turn.d.ts +12 -2
  62. package/dist/lib/workflow/turn.js +22 -12
  63. package/dist/scripts/a2a_server.js +5 -0
  64. package/package.json +11 -4
package/db/hardening.sql CHANGED
@@ -30,6 +30,27 @@
30
30
  -- The A2A server checks at boot whether this file has been applied and
31
31
  -- prints a prominent warning if not (see lib/persistence/supabaseProvider.ts).
32
32
 
33
+ -- ── The API roles ────────────────────────────────────────────────────────
34
+ -- anon and authenticated are Supabase's PostgREST roles. Plain Postgres and
35
+ -- embedded Postgres (PGlite) have no REST API and normally no such roles;
36
+ -- there, the PUBLIC revokes and RLS below are the whole lockdown, and the
37
+ -- revokes by role name are skipped rather than failing. These two helpers
38
+ -- live in pg_temp: they exist for this session only and leave nothing behind.
39
+ CREATE OR REPLACE FUNCTION pg_temp.melchizedek_api_roles() RETURNS text
40
+ LANGUAGE sql AS $$
41
+ SELECT string_agg(quote_ident(rolname), ', ' ORDER BY rolname)
42
+ FROM pg_roles WHERE rolname IN ('anon', 'authenticated');
43
+ $$;
44
+
45
+ CREATE OR REPLACE FUNCTION pg_temp.melchizedek_revoke_api(target text) RETURNS void
46
+ LANGUAGE plpgsql AS $$
47
+ DECLARE roles text := pg_temp.melchizedek_api_roles();
48
+ BEGIN
49
+ IF roles IS NOT NULL THEN
50
+ EXECUTE format('REVOKE ALL ON %s FROM %s', target, roles);
51
+ END IF;
52
+ END $$;
53
+
33
54
  -- ── Tier 1: lock the public API paths ────────────────────────────────────
34
55
 
35
56
  -- Enable RLS. With RLS on and NO policies defined, anon/authenticated get
@@ -40,8 +61,11 @@ ALTER TABLE adk_sessions ENABLE ROW LEVEL SECURITY;
40
61
  -- Belt and suspenders: also revoke the default table privileges from the
41
62
  -- API roles, so even a future accidentally-created permissive policy
42
63
  -- cannot re-expose the tables.
43
- REVOKE ALL ON adk_memory_facts FROM anon, authenticated;
44
- REVOKE ALL ON adk_sessions FROM anon, authenticated;
64
+ DO $$
65
+ BEGIN
66
+ PERFORM pg_temp.melchizedek_revoke_api('adk_memory_facts');
67
+ PERFORM pg_temp.melchizedek_revoke_api('adk_sessions');
68
+ END $$;
45
69
 
46
70
  -- Optional observability ledger (db/telemetry.sql). Guarded: the tables only
47
71
  -- exist when the operator opted into TELEMETRY_SUPABASE. adk_turns holds
@@ -54,14 +78,14 @@ BEGIN
54
78
  'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage', 'melchizedek_audit'] LOOP
55
79
  IF to_regclass('public.' || t) IS NOT NULL THEN
56
80
  EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
57
- EXECUTE format('REVOKE ALL ON %I FROM anon, authenticated', t);
81
+ PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
58
82
  END IF;
59
83
  END LOOP;
60
84
  -- The production view and the retention function read/delete those
61
85
  -- tables on behalf of the caller; neither belongs on the public API.
62
86
  FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly'] LOOP
63
87
  IF to_regclass('public.' || t) IS NOT NULL THEN
64
- EXECUTE format('REVOKE ALL ON %I FROM anon, authenticated', t);
88
+ PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
65
89
  END IF;
66
90
  END LOOP;
67
91
  -- Function privileges for these are handled in the FUNCTIONS block below.
@@ -73,7 +97,7 @@ DO $$
73
97
  BEGIN
74
98
  IF to_regclass('public.melchizedek_schema_version') IS NOT NULL THEN
75
99
  EXECUTE 'ALTER TABLE melchizedek_schema_version ENABLE ROW LEVEL SECURITY';
76
- EXECUTE 'REVOKE ALL ON melchizedek_schema_version FROM anon, authenticated';
100
+ PERFORM pg_temp.melchizedek_revoke_api('melchizedek_schema_version');
77
101
  END IF;
78
102
  END $$;
79
103
 
@@ -84,29 +108,29 @@ DO $$
84
108
  BEGIN
85
109
  IF to_regclass('public.adk_agent_registry') IS NOT NULL THEN
86
110
  EXECUTE 'ALTER TABLE adk_agent_registry ENABLE ROW LEVEL SECURITY';
87
- EXECUTE 'REVOKE ALL ON adk_agent_registry FROM anon, authenticated';
111
+ PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry');
88
112
  END IF;
89
113
  -- The task tools' lists and job queue (migration 0009).
90
114
  IF to_regclass('public.melchizedek_tasks') IS NOT NULL THEN
91
115
  EXECUTE 'ALTER TABLE melchizedek_tasks ENABLE ROW LEVEL SECURITY';
92
116
  EXECUTE 'ALTER TABLE melchizedek_task_owners ENABLE ROW LEVEL SECURITY';
93
- EXECUTE 'REVOKE ALL ON melchizedek_tasks, melchizedek_task_owners FROM anon, authenticated';
117
+ PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tasks, melchizedek_task_owners');
94
118
  END IF;
95
119
  -- The memory processed marker (migration 0007): identifiers only.
96
120
  IF to_regclass('public.melchizedek_memory_ingest') IS NOT NULL THEN
97
121
  EXECUTE 'ALTER TABLE melchizedek_memory_ingest ENABLE ROW LEVEL SECURITY';
98
- EXECUTE 'REVOKE ALL ON melchizedek_memory_ingest FROM anon, authenticated';
122
+ PERFORM pg_temp.melchizedek_revoke_api('melchizedek_memory_ingest');
99
123
  END IF;
100
124
  -- Third-party tokens held for tools (migration 0013): ciphertext only,
101
125
  -- still nothing an API role should read.
102
126
  IF to_regclass('public.melchizedek_tool_credentials') IS NOT NULL THEN
103
127
  EXECUTE 'ALTER TABLE melchizedek_tool_credentials ENABLE ROW LEVEL SECURITY';
104
- EXECUTE 'REVOKE ALL ON melchizedek_tool_credentials FROM anon, authenticated';
128
+ PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tool_credentials');
105
129
  END IF;
106
130
  -- Its history (migration 0005): every definition an id has held.
107
131
  IF to_regclass('public.adk_agent_registry_versions') IS NOT NULL THEN
108
132
  EXECUTE 'ALTER TABLE adk_agent_registry_versions ENABLE ROW LEVEL SECURITY';
109
- EXECUTE 'REVOKE ALL ON adk_agent_registry_versions FROM anon, authenticated';
133
+ PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry_versions');
110
134
  END IF;
111
135
  END $$;
112
136
 
@@ -141,7 +165,8 @@ BEGIN
141
165
  'melchizedek_memory_dimensions', 'melchizedek_prune_memory_ingest',
142
166
  'melchizedek_prune_memory', 'melchizedek_prune_audit')
143
167
  LOOP
144
- EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', fn);
168
+ EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC', fn);
169
+ PERFORM pg_temp.melchizedek_revoke_api(format('FUNCTION %s', fn));
145
170
  IF has_service_role THEN
146
171
  EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);
147
172
  END IF;
@@ -170,9 +195,10 @@ AS $$
170
195
  'melchizedek_audit', 'melchizedek_tool_credentials');
171
196
  $$;
172
197
 
173
- REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC, anon, authenticated;
198
+ REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC;
174
199
  DO $$
175
200
  BEGIN
201
+ PERFORM pg_temp.melchizedek_revoke_api('FUNCTION melchizedek_rls_status()');
176
202
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
177
203
  GRANT EXECUTE ON FUNCTION melchizedek_rls_status() TO service_role;
178
204
  END IF;
@@ -68,6 +68,7 @@ import { setOAuthHosts } from '../tools/oauthHosts.js';
68
68
  import { setCredentialHosts } from '../tools/credentialHosts.js';
69
69
  import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
70
70
  import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
71
+ import { credentialValue } from '../models/credentialSource.js';
71
72
  const SURFACE_HEADERS = [
72
73
  ['x-surface', 'name'],
73
74
  ['x-surface-guild', 'guild'],
@@ -335,7 +336,7 @@ export async function createA2AApp(options) {
335
336
  // Embeddings and fact extraction use the SERVER's Gemini key: memory is
336
337
  // operator infrastructure, like tools.
337
338
  const services = await createSupabaseServices({
338
- apiKey: process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY || '',
339
+ apiKey: credentialValue('GOOGLE_GENAI_API_KEY') || credentialValue('GEMINI_API_KEY') || '',
339
340
  withMemory: true,
340
341
  extractor: options.memory?.extractor,
341
342
  embedder: options.memory?.embedder,
@@ -21,8 +21,9 @@
21
21
  * times and the file's path.
22
22
  */
23
23
  import { existsSync, readFileSync, statSync } from 'node:fs';
24
- import { homedir } from 'node:os';
25
24
  import { dirname, join, resolve } from 'node:path';
25
+ import { chatGptSignInFile } from '../paths.js';
26
+ import { credentialEnv } from '../models/credentialSource.js';
26
27
  /** Where the credential file lives, when not the default (a path; never a value). */
27
28
  export const CHATGPT_SIGNIN_FILE_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE';
28
29
  /** `off` ignores a stored sign-in (a served process on a machine that also signed in). */
@@ -31,8 +32,7 @@ export const CHATGPT_SIGNIN_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN';
31
32
  export const PLAN_SCOPE = 'chatgpt.tokens.use.direct';
32
33
  /** The credential file: MELCHIZEDEK_CHATGPT_SIGNIN_FILE, else ~/.melchizedek/chatgpt-signin.json. */
33
34
  export function signInFile(env = process.env) {
34
- const set = env[CHATGPT_SIGNIN_FILE_ENV]?.trim();
35
- return set ? resolve(set) : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
35
+ return chatGptSignInFile(env);
36
36
  }
37
37
  /** The git work tree a path sits inside, if any (an ancestor holding `.git`). */
38
38
  export function insideGitWorkTree(file) {
@@ -100,7 +100,7 @@ export function chatGptSignInStatus(env = process.env) {
100
100
  * OPENAI_PLATFORM other than direct, no OPENAI_BASE_URL proxy, which must
101
101
  * never receive the token).
102
102
  */
103
- export function chatGptSignInRoutesOpenAi(env = process.env) {
103
+ export function chatGptSignInRoutesOpenAi(env = credentialEnv()) {
104
104
  if (env.OPENAI_API_KEY?.trim())
105
105
  return false;
106
106
  const platform = env.OPENAI_PLATFORM?.trim().toLowerCase();
@@ -42,7 +42,7 @@ import { REASONING_OLDER_SPELLING } from './syndicateSchema.js';
42
42
  import { reasoningConfig } from './models/reasoning.js';
43
43
  import { instructionFor, toEngineAgent } from './agentDialect.js';
44
44
  import { resolveTools as resolveNamedTools } from './toolRegistry.js';
45
- import { createMcpTools } from './tools/mcpToolFactory.js';
45
+ import { createMcpTools, loadStdioMcpTools } from './tools/mcpToolFactory.js';
46
46
  import { requireApproval, toolOf } from './tools/tool.js';
47
47
  import { examplesInstructionTool } from './tools/examples.js';
48
48
  import { capabilitySummary, describeCapabilities } from './models/capabilities.js';
@@ -145,6 +145,17 @@ export function examplesTool(examples) {
145
145
  const tool = examplesInstructionTool(examples);
146
146
  return tool ? [tool] : [];
147
147
  }
148
+ /** A stdio server's tools (ADR 0132), narrowed to the names the entry lists. */
149
+ async function stdioOfferedBy(server, opts) {
150
+ const label = `${server.name} (stdio: ${server.command})`;
151
+ opts.log?.(`Loading MCP tools: ${label}`);
152
+ const offered = await loadStdioMcpTools(server.name, { command: server.command, args: server.args, env: server.env, pass_env: server.pass_env, cwd: server.cwd });
153
+ const allowed = new Set(server.tools);
154
+ const missing = server.tools.filter((n) => !offered.some((t) => t.name === n));
155
+ if (missing.length)
156
+ opts.log?.(`MCP ${label} does not offer ${missing.map((n) => `'${n}'`).join(', ')}`);
157
+ return offered.filter((t) => allowed.has(t.name));
158
+ }
148
159
  async function resolveAgentTools(agent, opts) {
149
160
  const tools = [...resolveNamedTools(agent.tools, opts.onUnknownTool), ...examplesTool(agent.examples)];
150
161
  // OpenAPI operations become tools here, so require_approval can name them.
@@ -185,7 +196,9 @@ async function resolveAgentTools(agent, opts) {
185
196
  // mcp_servers (ADR 0124): the loader refused a declared name twice; a
186
197
  // server that answers with a name already taken is refused here too, so
187
198
  // no server's tool can stand in for another's or the agent's own.
188
- const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
199
+ const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => server.command !== undefined
200
+ ? stdioOfferedBy(server, opts)
201
+ : offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
189
202
  for (const [i, offered] of servers.entries()) {
190
203
  const server = agent.mcp_servers[i];
191
204
  for (const mcpTool of offered) {
@@ -215,6 +215,7 @@ export function workflowSubagentOf(workflow) {
215
215
  ...(run.memory ? { memory: run.memory } : {}),
216
216
  ...(run.log ? { log: run.log } : {}),
217
217
  ...(run.credentials ? { credentials: run.credentials } : {}),
218
+ ...(run.consent ? { consent: run.consent } : {}),
218
219
  }),
219
220
  };
220
221
  }
@@ -57,3 +57,5 @@ export type { MemoryStore } from './memory/store.ts';
57
57
  export { validateSyndicateConfig, syndicateJsonSchema, SyndicateValidationError } from './syndicateSchema.ts';
58
58
  export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logProviderStatuses, resolveModel, describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.ts';
59
59
  export { loadEnv } from './loadEnv.ts';
60
+ export { setCredentialSource, credentialValue } from './models/credentialSource.ts';
61
+ export type { CredentialSource } from './models/credentialSource.ts';
package/dist/lib/index.js CHANGED
@@ -48,3 +48,5 @@ export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logP
48
48
  // subpath until 0.16.0.
49
49
  describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.js';
50
50
  export { loadEnv } from './loadEnv.js';
51
+ // Where credentials are read, by environment variable name (ADR 0133).
52
+ export { setCredentialSource, credentialValue } from './models/credentialSource.js';
@@ -261,6 +261,10 @@ function anchorSpecPaths(config, dir) {
261
261
  for (const entry of agent?.openapi ?? [])
262
262
  if (!path.isAbsolute(entry.spec))
263
263
  entry.spec = path.resolve(dir, entry.spec);
264
+ // A stdio server's working directory, like a spec, is the YAML's (ADR 0132).
265
+ for (const entry of agent?.mcp_servers ?? [])
266
+ if (entry.cwd && !path.isAbsolute(entry.cwd))
267
+ entry.cwd = path.resolve(dir, entry.cwd);
264
268
  };
265
269
  anchor(config.orchestrator);
266
270
  for (const sub of config.subagents ?? [])
@@ -37,6 +37,7 @@ import { providerForModel } from '../models/providerMap.js';
37
37
  import { resolveModel } from '../models/registry.js';
38
38
  import { startEngineSpan, traceLlmGeneration } from '../observability/tracer.js';
39
39
  import { trimTrailingSlashes } from '../models/urls.js';
40
+ import { credentialEnv } from '../models/credentialSource.js';
40
41
  /**
41
42
  * Extraction on any model id. The key passed here reaches only a model of
42
43
  * the matching provider (resolveModel's BYOK rule); other providers read
@@ -194,7 +195,7 @@ export const DEFAULT_EMBEDDING_MODELS = {
194
195
  * already passes for memory; it is used only when the configured extraction
195
196
  * model or embedder is Gemini.
196
197
  */
197
- export function memoryProvidersFromEnv(env = process.env, geminiKey) {
198
+ export function memoryProvidersFromEnv(env = credentialEnv(), geminiKey) {
198
199
  const extractionModel = env.MEMORY_EXTRACTION_MODEL?.trim() || MEMORY_EXTRACTION_MODEL;
199
200
  const extractor = modelExtractor({
200
201
  model: extractionModel,
@@ -47,6 +47,7 @@ import { currentTurnStart, providerStateOf, withProviderState } from './provider
47
47
  import { errorDecision, errorText } from './errorResponse.js';
48
48
  import { adaptiveThinkingFor, claudeGeneration, claudeReasoningOf, claudeUrlImagesOn, THINKING_BINDING_BETA } from './claudeModels.js';
49
49
  import { isAboveHigh } from './reasoning.js';
50
+ import { credentialValue } from './credentialSource.js';
50
51
  /** The provider id this adapter reports and writes its state under (lib/models/providerMap.ts). */
51
52
  export const ANTHROPIC_PROVIDER = 'anthropic';
52
53
  /** The providerState kind this adapter writes: the signed blocks that preceded a part. */
@@ -217,7 +218,7 @@ export class ClaudeAdapter {
217
218
  yield setupFailure('ENDPOINT_MISCONFIGURED', messageOf(err));
218
219
  return;
219
220
  }
220
- const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey || process.env.ANTHROPIC_API_KEY);
221
+ const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey || credentialValue('ANTHROPIC_API_KEY'));
221
222
  if ('error' in spec) {
222
223
  yield setupFailure(endpoint.platform === 'direct' ? 'MISSING_API_KEY' : 'ENDPOINT_MISCONFIGURED', spec.error);
223
224
  return;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * lib/models/credentialSource.ts — where the engine reads a credential by
3
+ * its environment variable name (ADR 0133).
4
+ *
5
+ * By default every provider key, gateway key and embedding key is read from
6
+ * `process.env`. An embedding application that holds keys elsewhere (an OS
7
+ * keychain, a secret manager) sets a source once:
8
+ *
9
+ * setCredentialSource((name) => keychain.get(name));
10
+ *
11
+ * and every read below asks the source first, by the same names the
12
+ * environment uses (ANTHROPIC_API_KEY, MODEL_GATEWAY_API_KEY, …), at the
13
+ * moment of use: nothing is cached, so a key the user removes stops working
14
+ * on the next call. A source that returns undefined for a name falls back to
15
+ * the environment. A dependency-free leaf, so providerMap.ts stays one.
16
+ */
17
+ /** Returns the credential held under an environment variable name, or undefined. Must not throw. */
18
+ export type CredentialSource = (name: string) => string | undefined;
19
+ /** Sets (or, with undefined, clears) the process's credential source. */
20
+ export declare function setCredentialSource(next: CredentialSource | undefined): void;
21
+ /** The credential named `name`: the source's, else `env`'s. A source that throws counts as holding nothing. */
22
+ export declare function credentialValue(name: string, env?: NodeJS.ProcessEnv): string | undefined;
23
+ /**
24
+ * `env` as the engine's env-reading helpers see it: a read by name asks the
25
+ * source first. For functions that take an `env` parameter; enumeration and
26
+ * spreading see only `env`'s own variables.
27
+ */
28
+ export declare function credentialEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * lib/models/credentialSource.ts — where the engine reads a credential by
3
+ * its environment variable name (ADR 0133).
4
+ *
5
+ * By default every provider key, gateway key and embedding key is read from
6
+ * `process.env`. An embedding application that holds keys elsewhere (an OS
7
+ * keychain, a secret manager) sets a source once:
8
+ *
9
+ * setCredentialSource((name) => keychain.get(name));
10
+ *
11
+ * and every read below asks the source first, by the same names the
12
+ * environment uses (ANTHROPIC_API_KEY, MODEL_GATEWAY_API_KEY, …), at the
13
+ * moment of use: nothing is cached, so a key the user removes stops working
14
+ * on the next call. A source that returns undefined for a name falls back to
15
+ * the environment. A dependency-free leaf, so providerMap.ts stays one.
16
+ */
17
+ let source;
18
+ /** Sets (or, with undefined, clears) the process's credential source. */
19
+ export function setCredentialSource(next) {
20
+ source = next;
21
+ }
22
+ /** The credential named `name`: the source's, else `env`'s. A source that throws counts as holding nothing. */
23
+ export function credentialValue(name, env = process.env) {
24
+ if (source) {
25
+ try {
26
+ const held = source(name);
27
+ if (held !== undefined && held !== '')
28
+ return held;
29
+ }
30
+ catch {
31
+ /* a broken source must not take the engine down; the environment still answers */
32
+ }
33
+ }
34
+ return env[name];
35
+ }
36
+ /**
37
+ * `env` as the engine's env-reading helpers see it: a read by name asks the
38
+ * source first. For functions that take an `env` parameter; enumeration and
39
+ * spreading see only `env`'s own variables.
40
+ */
41
+ export function credentialEnv(env = process.env) {
42
+ if (!source)
43
+ return env;
44
+ return new Proxy(env, {
45
+ get(target, key, receiver) {
46
+ return typeof key === 'string' ? credentialValue(key, target) : Reflect.get(target, key, receiver);
47
+ },
48
+ });
49
+ }
@@ -31,6 +31,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
31
31
  };
32
32
  import { PROVIDERS, providerKeyPresent } from './providerMap.js';
33
33
  import { trimTrailingSlashes } from './urls.js';
34
+ import { credentialEnv } from './credentialSource.js';
34
35
  export const PLATFORMS_FOR = {
35
36
  gemini: ['direct', 'vertex'],
36
37
  anthropic: ['direct', 'bedrock', 'vertex'],
@@ -81,7 +82,7 @@ export function platformFromEnv(provider, env = process.env) {
81
82
  return raw;
82
83
  }
83
84
  /** A provider's endpoint as the environment configures it. */
84
- export function endpointFromEnv(provider, env = process.env) {
85
+ export function endpointFromEnv(provider, env = credentialEnv()) {
85
86
  const platform = platformFromEnv(provider, env);
86
87
  const mapEnv = MODEL_MAP_ENV[provider];
87
88
  const models = mapEnv ? parseModelMap(env[mapEnv], mapEnv) : undefined;
@@ -41,6 +41,7 @@ import { chatGptSignInRoutesOpenAi } from '../chatgpt/state.js';
41
41
  import { providerReady } from './endpoints.js';
42
42
  import { PROVIDERS, providerForModel } from './providerMap.js';
43
43
  import { trimTrailingSlashes } from './urls.js';
44
+ import { credentialValue } from './credentialSource.js';
44
45
  export const GATEWAY_ENV = 'MODEL_GATEWAY';
45
46
  export const GATEWAY_KEY_ENV = 'MODEL_GATEWAY_API_KEY';
46
47
  export const GATEWAY_BASE_URL_ENV = 'MODEL_GATEWAY_BASE_URL';
@@ -78,7 +79,7 @@ export function gatewayConfig() {
78
79
  return {
79
80
  gateway,
80
81
  baseUrl: trimTrailingSlashes(override || gateway.baseUrl),
81
- keyPresent: !!process.env[GATEWAY_KEY_ENV],
82
+ keyPresent: !!credentialValue(GATEWAY_KEY_ENV),
82
83
  };
83
84
  }
84
85
  /** A human-readable misconfiguration, or undefined when the gateway is
@@ -31,6 +31,7 @@
31
31
  import { ChatCompletionsAdapter } from './chatCompletionsAdapter.js';
32
32
  import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAY_MODEL_MAP_ENV, gatewayConfig, gatewayWireModel } from './gateway.js';
33
33
  import { providerForModel } from './providerMap.js';
34
+ import { credentialValue } from './credentialSource.js';
34
35
  export class GatewayAdapter extends ChatCompletionsAdapter {
35
36
  /** Attribution stays with the upstream provider named by the YAML id. */
36
37
  provider;
@@ -47,7 +48,7 @@ export class GatewayAdapter extends ChatCompletionsAdapter {
47
48
  return `${this.#cfg?.baseUrl ?? ''}/chat/completions`;
48
49
  }
49
50
  headers() {
50
- return { Authorization: `Bearer ${process.env[GATEWAY_KEY_ENV] ?? ''}` };
51
+ return { Authorization: `Bearer ${credentialValue(GATEWAY_KEY_ENV) ?? ''}` };
51
52
  }
52
53
  wireModelName(model) {
53
54
  return this.#cfg ? gatewayWireModel(model, this.#cfg.gateway) : model;
@@ -52,6 +52,7 @@ import { currentTurnStart, providerStateOf } from './providerState.js';
52
52
  import { classifyError, errorStatus, retryUntilFirstYield } from './retry.js';
53
53
  import { isAboveHigh, reasoningConfig } from './reasoning.js';
54
54
  import { setLlmSpanAttribute } from '../observability/tracer.js';
55
+ import { credentialValue } from './credentialSource.js';
55
56
  /**
56
57
  * The provider id this adapter reports and writes its state under, and the
57
58
  * providerState kind for a Gemini thought signature (ADR 0046). Defined once
@@ -846,7 +847,7 @@ function nonEmpty(value) {
846
847
  }
847
848
  /** The Gemini API key from the environment, in ADK's order. */
848
849
  function envApiKey() {
849
- return nonEmpty(process.env.GOOGLE_GENAI_API_KEY) ?? nonEmpty(process.env.GOOGLE_API_KEY) ?? nonEmpty(process.env.GEMINI_API_KEY);
850
+ return nonEmpty(credentialValue('GOOGLE_GENAI_API_KEY')) ?? nonEmpty(credentialValue('GOOGLE_API_KEY')) ?? nonEmpty(credentialValue('GEMINI_API_KEY'));
850
851
  }
851
852
  function messageOf(err) {
852
853
  return err instanceof Error ? err.message : String(err);
@@ -48,6 +48,7 @@ import { currentTurnStart, providerStateOf } from './providerState.js';
48
48
  import { effortWord } from './reasoning.js';
49
49
  import { toContractJsonSchema } from './schemaNormalize.js';
50
50
  import { setLlmSpanAttribute } from '../observability/tracer.js';
51
+ import { credentialValue } from './credentialSource.js';
51
52
  /** The providerState kind the Responses adapters write: the reasoning output items that preceded a part (ADR 0046, ADR 0050). */
52
53
  export const REASONING_STATE_KIND = 'reasoning_items';
53
54
  /** The `include` value that returns reasoning items with their encrypted content. */
@@ -410,7 +411,7 @@ export class GptAdapter {
410
411
  return undefined;
411
412
  }
412
413
  apiKeyFromEnv() {
413
- return process.env.OPENAI_API_KEY;
414
+ return credentialValue('OPENAI_API_KEY');
414
415
  }
415
416
  missingKeyMessage() {
416
417
  return 'OPENAI_API_KEY is not set in environment.';
@@ -29,6 +29,7 @@ import { GptAdapter } from './gptAdapter.js';
29
29
  import { effortWord } from './reasoning.js';
30
30
  import { collectionIdsFromEnv, collectionsMaxResultsFromEnv, xaiWebSearchParamsFromEnv, xSearchParamsFromEnv, } from '../tools/xaiSearchParams.js';
31
31
  import { setLlmSpanAttribute } from '../observability/tracer.js';
32
+ import { credentialValue } from './credentialSource.js';
32
33
  export const XAI_BASE_URL = 'https://api.x.ai/v1';
33
34
  /**
34
35
  * The ids that take a reasoning effort and return encrypted reasoning:
@@ -60,7 +61,7 @@ export class GrokAdapter extends GptAdapter {
60
61
  return XAI_BASE_URL;
61
62
  }
62
63
  apiKeyFromEnv() {
63
- return process.env.XAI_API_KEY;
64
+ return credentialValue('XAI_API_KEY');
64
65
  }
65
66
  missingKeyMessage() {
66
67
  return 'XAI_API_KEY is not set in environment.';
@@ -120,6 +120,7 @@
120
120
  import { DEFAULT_KIMI_REASONING_EFFORT } from '../config.js';
121
121
  import { ChatCompletionsAdapter, reasonsNotAtAll } from './chatCompletionsAdapter.js';
122
122
  import { trimTrailingSlashes } from './urls.js';
123
+ import { credentialValue } from './credentialSource.js';
123
124
  export const MOONSHOT_BASE_URL = 'https://api.moonshot.ai/v1';
124
125
  /** The flagship takes `reasoning_effort`; the K2 generation takes a `thinking` switch. */
125
126
  export function isKimiK3(model) {
@@ -153,7 +154,7 @@ export class KimiAdapter extends ChatCompletionsAdapter {
153
154
  return `${this.#baseUrl}/chat/completions`;
154
155
  }
155
156
  #key() {
156
- return this.#apiKey ?? process.env.MOONSHOT_API_KEY;
157
+ return this.#apiKey ?? credentialValue('MOONSHOT_API_KEY');
157
158
  }
158
159
  headers() {
159
160
  return { Authorization: `Bearer ${this.#key() ?? ''}` };
@@ -13,6 +13,7 @@
13
13
  * grok-* → xai ollama/<model> → ollama (local, keyless)
14
14
  * kimi-* → moonshot everything else → gemini (the default)
15
15
  */
16
+ import { credentialValue } from './credentialSource.js';
16
17
  export const PROVIDERS = {
17
18
  gemini: { keyEnv: 'GOOGLE_GENAI_API_KEY', label: 'Google Gemini' },
18
19
  anthropic: { keyEnv: 'ANTHROPIC_API_KEY', label: 'Anthropic Claude' },
@@ -43,9 +44,9 @@ export function providerKeyPresent(provider) {
43
44
  return true;
44
45
  // Gemini historically accepts either env name.
45
46
  if (provider === 'gemini') {
46
- return realValue(process.env.GOOGLE_GENAI_API_KEY) || realValue(process.env.GEMINI_API_KEY);
47
+ return realValue(credentialValue('GOOGLE_GENAI_API_KEY')) || realValue(credentialValue('GEMINI_API_KEY'));
47
48
  }
48
- return realValue(process.env[keyEnv]);
49
+ return realValue(credentialValue(keyEnv));
49
50
  }
50
51
  /** Set, and not a `.env.example` placeholder (`your_..._here`, `<...>`).
51
52
  * Inlined rather than imported: this module stays a dependency-free leaf. */
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL } from '../config.js';
12
12
  import { memoryProvidersFromEnv } from '../memory/providers.js';
13
+ import { credentialValue } from '../models/credentialSource.js';
13
14
  const MAX_CHARS = 8_000;
14
15
  /**
15
16
  * Embeds each text with the SAME embedder long-term memory uses, so the two
@@ -20,7 +21,7 @@ const MAX_CHARS = 8_000;
20
21
  export async function embedTexts(texts, options = {}) {
21
22
  let embedder = options.embedder;
22
23
  if (!embedder) {
23
- const apiKey = options.apiKey ?? process.env.GOOGLE_GENAI_API_KEY ?? process.env.GEMINI_API_KEY;
24
+ const apiKey = options.apiKey ?? credentialValue('GOOGLE_GENAI_API_KEY') ?? credentialValue('GEMINI_API_KEY');
24
25
  const provider = (process.env.MEMORY_EMBEDDING_PROVIDER?.trim() || 'gemini').toLowerCase();
25
26
  if (provider === 'gemini' && !apiKey)
26
27
  throw new Error('GOOGLE_GENAI_API_KEY is required for Gemini embeddings');
@@ -26,6 +26,7 @@ import { createHash } from 'node:crypto';
26
26
  import { execSync } from 'node:child_process';
27
27
  import { existsSync, readFileSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
+ import { enginePackageRoot } from '../paths.js';
29
30
  /** Schema version stamped on every ledger row (adk_turns / adk_payloads). */
30
31
  export const TELEMETRY_SCHEMA_VERSION = 2;
31
32
  // ── Span lineage: the names spans are given ──────────────────────────────────
@@ -105,8 +106,10 @@ export function engineVersion() {
105
106
  return cachedEngineVersion;
106
107
  let pkgVersion = '0.0.0';
107
108
  try {
108
- const pkgPath = join(process.cwd(), 'package.json');
109
- if (existsSync(pkgPath)) {
109
+ // The engine's own package.json, not the embedding application's.
110
+ const root = enginePackageRoot();
111
+ const pkgPath = root ? join(root, 'package.json') : '';
112
+ if (pkgPath && existsSync(pkgPath)) {
110
113
  pkgVersion = String(JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? pkgVersion);
111
114
  }
112
115
  }
@@ -116,7 +119,12 @@ export function engineVersion() {
116
119
  let commit = process.env.SOURCE_VERSION ?? process.env.HEROKU_SLUG_COMMIT ?? process.env.GIT_COMMIT ?? '';
117
120
  if (!commit) {
118
121
  try {
119
- commit = execSync('git rev-parse --short HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
122
+ // Only from the engine's own checkout: an installed package inside an
123
+ // application's repository would otherwise report the application's commit.
124
+ const root = enginePackageRoot();
125
+ if (!root || !existsSync(join(root, '.git')))
126
+ throw new Error('not a checkout');
127
+ commit = execSync('git rev-parse --short HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] })
120
128
  .toString()
121
129
  .trim();
122
130
  }
@@ -174,7 +174,8 @@ export interface ExportStats {
174
174
  deadLettered: number;
175
175
  payloadsDropped: number;
176
176
  }
177
- interface SupabaseLikeClient {
177
+ /** Where the exporter writes: supabase-js, or `postgresLedgerClient` on a pg pool. */
178
+ export interface LedgerClient {
178
179
  from(table: string): {
179
180
  insert(rows: unknown[]): PromiseLike<{
180
181
  error: {
@@ -183,6 +184,23 @@ interface SupabaseLikeClient {
183
184
  }>;
184
185
  };
185
186
  }
187
+ type SupabaseLikeClient = LedgerClient;
188
+ /** The tables the exporter writes; nothing else is ever named in its SQL. */
189
+ export declare const LEDGER_TABLES: readonly ['adk_telemetry', 'adk_turns', 'adk_payloads'];
190
+ /** The subset of a pg Pool the ledger client needs. */
191
+ export interface LedgerPool {
192
+ query(text: string, values?: unknown[]): Promise<unknown>;
193
+ }
194
+ /**
195
+ * The ledger over a direct Postgres connection (ADR 0131): the same rows the
196
+ * Supabase sink writes, through one parameterised statement per batch. The
197
+ * rows go in as one JSON array and `jsonb_populate_recordset` gives each
198
+ * value its column's type, so JSON, arrays and timestamps need no mapping
199
+ * here. Only the columns the rows carry are named, so the rest keep their
200
+ * defaults. The table must be one of LEDGER_TABLES and every column name
201
+ * comes from the row builders above, never from span content.
202
+ */
203
+ export declare function postgresLedgerClient(pool: LedgerPool): LedgerClient;
186
204
  export interface ExporterOptions {
187
205
  /** Injected client (tests); default: lazily created supabase-js client. */
188
206
  client?: SupabaseLikeClient;