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.
- package/.env.example +15 -0
- package/CHANGELOG.md +134 -0
- package/DOCUMENTATION.md +61 -5
- package/ONBOARDING.md +3 -3
- package/README.md +1 -0
- package/config/agents/examples/connectors.yaml +4 -1
- package/config/agents/examples/event_assistant.yaml +82 -0
- package/config/agents/syndicate.schema.json +80 -6
- package/db/hardening.sql +38 -12
- package/dist/lib/a2a/app.js +2 -1
- package/dist/lib/chatgpt/state.js +4 -4
- package/dist/lib/compile.js +15 -2
- package/dist/lib/compileNative.js +1 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.js +4 -0
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/models/claudeAdapter.js +2 -1
- package/dist/lib/models/credentialSource.d.ts +28 -0
- package/dist/lib/models/credentialSource.js +49 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.js +2 -1
- package/dist/lib/models/geminiAdapter.js +2 -1
- package/dist/lib/models/gptAdapter.js +2 -1
- package/dist/lib/models/grokAdapter.js +2 -1
- package/dist/lib/models/kimiAdapter.js +2 -1
- package/dist/lib/models/providerMap.js +3 -2
- package/dist/lib/observability/embeddings.js +2 -1
- package/dist/lib/observability/lineage.js +11 -3
- package/dist/lib/observability/supabaseSpanExporter.d.ts +19 -1
- package/dist/lib/observability/supabaseSpanExporter.js +37 -1
- package/dist/lib/observability/tracer.d.ts +9 -0
- package/dist/lib/observability/tracer.js +45 -0
- package/dist/lib/paths.d.ts +31 -0
- package/dist/lib/paths.js +76 -0
- package/dist/lib/runtime/credentials.d.ts +21 -0
- package/dist/lib/runtime/credentials.js +32 -0
- package/dist/lib/runtime/native/delegate.d.ts +2 -0
- package/dist/lib/runtime/native/delegate.js +9 -2
- package/dist/lib/runtime/native/interrupts.d.ts +33 -3
- package/dist/lib/runtime/native/interrupts.js +99 -9
- package/dist/lib/runtime/syndicateTurn.js +41 -11
- package/dist/lib/storage/postgres/index.d.ts +25 -0
- package/dist/lib/storage/postgres/index.js +10 -1
- package/dist/lib/syndicateSchema.d.ts +26 -4
- package/dist/lib/syndicateSchema.js +63 -1
- package/dist/lib/tools/generateImageTool.js +6 -6
- package/dist/lib/tools/inspectImageTool.d.ts +2 -0
- package/dist/lib/tools/inspectImageTool.js +17 -10
- package/dist/lib/tools/mcpToolFactory.d.ts +34 -1
- package/dist/lib/tools/mcpToolFactory.js +200 -27
- package/dist/lib/tools/mcpTransports.d.ts +4 -2
- package/dist/lib/tools/mcpTransports.js +4 -2
- package/dist/lib/tools/taskTools.js +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/workflow/agentNode.d.ts +19 -3
- package/dist/lib/workflow/agentNode.js +106 -13
- package/dist/lib/workflow/resume.d.ts +8 -7
- package/dist/lib/workflow/resume.js +17 -10
- package/dist/lib/workflow/turn.d.ts +12 -2
- package/dist/lib/workflow/turn.js +22 -12
- package/dist/scripts/a2a_server.js +5 -0
- 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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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 =
|
|
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();
|
package/dist/lib/compile.js
CHANGED
|
@@ -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) =>
|
|
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
|
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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 =
|
|
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 ||
|
|
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 =
|
|
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: !!
|
|
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 ${
|
|
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(
|
|
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
|
|
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
|
|
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 ??
|
|
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(
|
|
47
|
+
return realValue(credentialValue('GOOGLE_GENAI_API_KEY')) || realValue(credentialValue('GEMINI_API_KEY'));
|
|
47
48
|
}
|
|
48
|
-
return realValue(
|
|
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 ??
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|