melchizedek-agents 0.18.0 → 0.19.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 +12 -0
- package/CHANGELOG.md +825 -0
- package/DOCUMENTATION.md +217 -59
- package/README.md +42 -1
- package/config/agents/examples/harness.yaml +3 -1
- package/config/agents/examples/model_zoo.yaml +10 -9
- package/config/agents/syndicateSchema.yaml +50 -16
- package/config/agents/templates/account_memory.yaml +2 -6
- package/config/agents/templates/case_desk.yaml +3 -9
- package/config/agents/templates/draft_review.yaml +3 -9
- package/config/agents/templates/intake_extractor.yaml +1 -3
- package/config/agents/templates/research_brief.yaml +3 -9
- package/config/agents/templates/research_desk.yaml +3 -9
- package/config/agents/templates/review_panel.yaml +4 -12
- package/config/agents/templates/support_triage.yaml +4 -12
- package/config/agents/templates/systems_operator.yaml +2 -6
- package/db/hardening.sql +7 -1
- package/db/migrations/0013_tool_credentials.sql +260 -0
- package/dist/lib/a2a/app.d.ts +70 -6
- package/dist/lib/a2a/app.js +107 -2
- package/dist/lib/a2a/executor.d.ts +20 -3
- package/dist/lib/a2a/executor.js +66 -7
- package/dist/lib/a2a/remoteAgent.d.ts +24 -9
- package/dist/lib/a2a/remoteAgent.js +31 -14
- package/dist/lib/compile.d.ts +121 -30
- package/dist/lib/compile.js +137 -124
- package/dist/lib/compileAdk.d.ts +29 -0
- package/dist/lib/compileAdk.js +132 -0
- package/dist/lib/compileNative.d.ts +85 -0
- package/dist/lib/compileNative.js +222 -0
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.js +6 -5
- package/dist/lib/doctor.js +1 -0
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/loadSyndicate.d.ts +18 -0
- package/dist/lib/memory/erase.d.ts +5 -3
- package/dist/lib/memory/erase.js +5 -2
- package/dist/lib/memory/namespace.d.ts +8 -1
- package/dist/lib/memory/namespace.js +12 -6
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
- package/dist/lib/memory/supabaseMemoryService.js +42 -2
- package/dist/lib/model.d.ts +47 -0
- package/dist/lib/model.js +37 -0
- package/dist/lib/models/adapterResolver.d.ts +113 -0
- package/dist/lib/models/adapterResolver.js +136 -0
- package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
- package/dist/lib/models/adkGeminiAdapter.js +367 -0
- package/dist/lib/models/adkShim.d.ts +111 -0
- package/dist/lib/models/adkShim.js +136 -0
- package/dist/lib/models/capabilities.d.ts +3 -0
- package/dist/lib/models/capabilities.js +20 -13
- package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
- package/dist/lib/models/chatCompletionsAdapter.js +854 -0
- package/dist/lib/models/circuitBreaker.d.ts +42 -0
- package/dist/lib/models/circuitBreaker.js +70 -0
- package/dist/lib/models/claudeAdapter.d.ts +103 -0
- package/dist/lib/models/claudeAdapter.js +669 -0
- package/dist/lib/models/claudeLlm.d.ts +39 -62
- package/dist/lib/models/claudeLlm.js +44 -349
- package/dist/lib/models/claudeModels.d.ts +127 -0
- package/dist/lib/models/claudeModels.js +237 -0
- package/dist/lib/models/contract.d.ts +395 -0
- package/dist/lib/models/contract.js +39 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/errorResponse.d.ts +50 -0
- package/dist/lib/models/errorResponse.js +48 -0
- package/dist/lib/models/fallback.d.ts +17 -10
- package/dist/lib/models/fallback.js +53 -42
- package/dist/lib/models/fallbackAdapter.d.ts +55 -0
- package/dist/lib/models/fallbackAdapter.js +62 -0
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.d.ts +51 -0
- package/dist/lib/models/gatewayAdapter.js +81 -0
- package/dist/lib/models/gatewayLlm.d.ts +12 -33
- package/dist/lib/models/gatewayLlm.js +14 -75
- package/dist/lib/models/geminiAdapter.d.ts +123 -0
- package/dist/lib/models/geminiAdapter.js +831 -0
- package/dist/lib/models/geminiState.d.ts +25 -0
- package/dist/lib/models/geminiState.js +25 -0
- package/dist/lib/models/genaiMapping.d.ts +263 -0
- package/dist/lib/models/genaiMapping.js +862 -0
- package/dist/lib/models/gptAdapter.d.ts +240 -0
- package/dist/lib/models/gptAdapter.js +804 -0
- package/dist/lib/models/gptLlm.d.ts +63 -123
- package/dist/lib/models/gptLlm.js +88 -602
- package/dist/lib/models/grokAdapter.d.ts +86 -0
- package/dist/lib/models/grokAdapter.js +142 -0
- package/dist/lib/models/grokLlm.d.ts +22 -45
- package/dist/lib/models/grokLlm.js +22 -67
- package/dist/lib/models/kimiAdapter.d.ts +157 -0
- package/dist/lib/models/kimiAdapter.js +203 -0
- package/dist/lib/models/kimiLlm.d.ts +15 -107
- package/dist/lib/models/kimiLlm.js +17 -153
- package/dist/lib/models/ollamaAdapter.d.ts +125 -0
- package/dist/lib/models/ollamaAdapter.js +155 -0
- package/dist/lib/models/ollamaLlm.d.ts +11 -107
- package/dist/lib/models/ollamaLlm.js +13 -151
- package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
- package/dist/lib/models/openAiCompatibleLlm.js +109 -671
- package/dist/lib/models/providerState.d.ts +63 -0
- package/dist/lib/models/providerState.js +61 -0
- package/dist/lib/models/reasoning.d.ts +26 -0
- package/dist/lib/models/reasoning.js +59 -0
- package/dist/lib/models/registry.d.ts +58 -35
- package/dist/lib/models/registry.js +89 -118
- package/dist/lib/models/retry.d.ts +2 -1
- package/dist/lib/models/retry.js +3 -2
- package/dist/lib/models/schemaNormalize.d.ts +98 -0
- package/dist/lib/models/schemaNormalize.js +321 -1
- package/dist/lib/models/tracedGemini.d.ts +41 -0
- package/dist/lib/models/tracedGemini.js +98 -0
- package/dist/lib/models/urls.d.ts +11 -0
- package/dist/lib/models/urls.js +16 -0
- package/dist/lib/net/redirects.d.ts +21 -2
- package/dist/lib/net/redirects.js +11 -34
- package/dist/lib/observability/audit.d.ts +5 -1
- package/dist/lib/observability/lineage.d.ts +14 -0
- package/dist/lib/observability/lineage.js +33 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
- package/dist/lib/observability/supabaseSpanExporter.js +17 -9
- package/dist/lib/observability/tracer.d.ts +26 -8
- package/dist/lib/observability/tracer.js +43 -18
- package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
- package/dist/lib/runtime/adkMemoryBridge.js +68 -0
- package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
- package/dist/lib/runtime/adkSessionBridge.js +176 -0
- package/dist/lib/runtime/credentials.d.ts +59 -0
- package/dist/lib/runtime/credentials.js +62 -0
- package/dist/lib/runtime/events.d.ts +229 -0
- package/dist/lib/runtime/events.js +274 -0
- package/dist/lib/runtime/logging.d.ts +32 -0
- package/dist/lib/runtime/logging.js +55 -0
- package/dist/lib/runtime/memoryService.d.ts +72 -0
- package/dist/lib/runtime/memoryService.js +25 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +203 -0
- package/dist/lib/runtime/native/agentLoop.js +687 -0
- package/dist/lib/runtime/native/compaction.d.ts +103 -0
- package/dist/lib/runtime/native/compaction.js +191 -0
- package/dist/lib/runtime/native/delegate.d.ts +98 -0
- package/dist/lib/runtime/native/delegate.js +173 -0
- package/dist/lib/runtime/native/history.d.ts +83 -0
- package/dist/lib/runtime/native/history.js +312 -0
- package/dist/lib/runtime/native/interrupts.d.ts +126 -0
- package/dist/lib/runtime/native/interrupts.js +291 -0
- package/dist/lib/runtime/native/request.d.ts +215 -0
- package/dist/lib/runtime/native/request.js +585 -0
- package/dist/lib/runtime/native/selfCorrection.d.ts +106 -0
- package/dist/lib/runtime/native/selfCorrection.js +253 -0
- package/dist/lib/runtime/native/step.d.ts +148 -0
- package/dist/lib/runtime/native/step.js +239 -0
- package/dist/lib/runtime/native/taskMode.d.ts +64 -0
- package/dist/lib/runtime/native/taskMode.js +126 -0
- package/dist/lib/runtime/native/telemetry.d.ts +75 -0
- package/dist/lib/runtime/native/telemetry.js +299 -0
- package/dist/lib/runtime/native/tempState.d.ts +31 -0
- package/dist/lib/runtime/native/tempState.js +56 -0
- package/dist/lib/runtime/nativeTurn.d.ts +86 -0
- package/dist/lib/runtime/nativeTurn.js +107 -0
- package/dist/lib/runtime/questions.d.ts +29 -7
- package/dist/lib/runtime/questions.js +47 -32
- package/dist/lib/runtime/runtimeFlag.d.ts +30 -0
- package/dist/lib/runtime/runtimeFlag.js +50 -0
- package/dist/lib/runtime/sessions.d.ts +166 -0
- package/dist/lib/runtime/sessions.js +201 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +41 -5
- package/dist/lib/runtime/syndicateTurn.js +202 -32
- package/dist/lib/session/supabaseSessionService.d.ts +64 -17
- package/dist/lib/session/supabaseSessionService.js +148 -105
- package/dist/lib/session/transcript.d.ts +68 -22
- package/dist/lib/session/transcript.js +102 -28
- package/dist/lib/skills.js +33 -7
- package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
- package/dist/lib/storage/postgres/credentialStore.js +82 -0
- package/dist/lib/storage/postgres/index.d.ts +12 -0
- package/dist/lib/storage/postgres/index.js +6 -1
- package/dist/lib/storage/postgres/sessionService.d.ts +43 -15
- package/dist/lib/storage/postgres/sessionService.js +128 -66
- package/dist/lib/syndicateSchema.d.ts +47 -0
- package/dist/lib/syndicateSchema.js +72 -3
- package/dist/lib/toolRegistry.d.ts +15 -2
- package/dist/lib/toolRegistry.js +33 -17
- package/dist/lib/tools/adkTool.d.ts +81 -0
- package/dist/lib/tools/adkTool.js +268 -0
- package/dist/lib/tools/auth.d.ts +118 -0
- package/dist/lib/tools/auth.js +89 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +7 -6
- package/dist/lib/tools/collectionsSearchTool.js +11 -26
- package/dist/lib/tools/credentialCipher.d.ts +58 -0
- package/dist/lib/tools/credentialCipher.js +116 -0
- package/dist/lib/tools/credentialStore.d.ts +77 -0
- package/dist/lib/tools/credentialStore.js +246 -0
- package/dist/lib/tools/examples.d.ts +43 -0
- package/dist/lib/tools/examples.js +62 -0
- package/dist/lib/tools/generateImageTool.d.ts +15 -6
- package/dist/lib/tools/generateImageTool.js +19 -29
- package/dist/lib/tools/inspectImageTool.d.ts +9 -6
- package/dist/lib/tools/inspectImageTool.js +13 -17
- package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
- package/dist/lib/tools/mcpToolFactory.js +46 -47
- package/dist/lib/tools/memoryTools.d.ts +61 -0
- package/dist/lib/tools/memoryTools.js +119 -0
- package/dist/lib/tools/nativeTools.d.ts +38 -0
- package/dist/lib/tools/nativeTools.js +43 -0
- package/dist/lib/tools/oauthConsent.d.ts +181 -0
- package/dist/lib/tools/oauthConsent.js +330 -0
- package/dist/lib/tools/openapi/call.d.ts +108 -0
- package/dist/lib/tools/openapi/call.js +295 -0
- package/dist/lib/tools/openapi/parse.d.ts +147 -0
- package/dist/lib/tools/openapi/parse.js +570 -0
- package/dist/lib/tools/openapiTools.d.ts +25 -17
- package/dist/lib/tools/openapiTools.js +124 -121
- package/dist/lib/tools/scienceTools.d.ts +14 -14
- package/dist/lib/tools/skillToolset.d.ts +58 -87
- package/dist/lib/tools/skillToolset.js +61 -142
- package/dist/lib/tools/skills/env.d.ts +58 -0
- package/dist/lib/tools/skills/env.js +165 -0
- package/dist/lib/tools/skills/executor.d.ts +114 -0
- package/dist/lib/tools/skills/executor.js +331 -0
- package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
- package/dist/lib/tools/skills/frontmatter.js +215 -0
- package/dist/lib/tools/skills/loader.d.ts +91 -0
- package/dist/lib/tools/skills/loader.js +257 -0
- package/dist/lib/tools/skills/tools.d.ts +187 -0
- package/dist/lib/tools/skills/tools.js +386 -0
- package/dist/lib/tools/taskTools.d.ts +15 -15
- package/dist/lib/tools/tool.d.ts +354 -0
- package/dist/lib/tools/tool.js +304 -0
- package/dist/lib/tools/toolContract.d.ts +89 -33
- package/dist/lib/tools/toolContract.js +104 -82
- package/dist/lib/tools/urlContextTool.d.ts +3 -0
- package/dist/lib/tools/urlContextTool.js +5 -1
- package/dist/lib/tools/webExtractTool.d.ts +2 -2
- package/dist/lib/tools/webExtractTool.js +2 -1
- package/dist/lib/tools/webSearchTool.d.ts +6 -9
- package/dist/lib/tools/webSearchTool.js +14 -59
- package/dist/lib/tools/wikiTools.d.ts +19 -19
- package/dist/lib/tools/wikiTools.js +13 -9
- package/dist/lib/tools/xApiSearchTool.d.ts +2 -2
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/tools/xSearchTool.d.ts +7 -9
- package/dist/lib/tools/xSearchTool.js +11 -62
- package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
- package/dist/lib/tools/xaiSearchParams.js +126 -0
- package/dist/lib/wiki/agentRun.d.ts +12 -0
- package/dist/lib/wiki/agentRun.js +72 -34
- package/dist/lib/wiki/entities.d.ts +12 -0
- package/dist/lib/wiki/entities.js +45 -3
- package/dist/lib/workflow/agentNode.d.ts +136 -0
- package/dist/lib/workflow/agentNode.js +274 -0
- package/dist/lib/workflow/graph.d.ts +177 -0
- package/dist/lib/workflow/graph.js +458 -0
- package/dist/lib/workflow/nodeEvents.d.ts +50 -0
- package/dist/lib/workflow/nodeEvents.js +101 -0
- package/dist/lib/workflow/pause.d.ts +122 -0
- package/dist/lib/workflow/pause.js +180 -0
- package/dist/lib/workflow/resume.d.ts +165 -0
- package/dist/lib/workflow/resume.js +440 -0
- package/dist/lib/workflow/route.d.ts +58 -0
- package/dist/lib/workflow/route.js +69 -0
- package/dist/lib/workflow/scheduler.d.ts +415 -0
- package/dist/lib/workflow/scheduler.js +743 -0
- package/dist/lib/workflow/toolNode.d.ts +127 -0
- package/dist/lib/workflow/toolNode.js +236 -0
- package/dist/lib/workflow/turn.d.ts +118 -0
- package/dist/lib/workflow/turn.js +216 -0
- package/dist/lib/workflowConfig.d.ts +3 -2
- package/dist/lib/workflowConfig.js +3 -12
- package/dist/scripts/a2a_server.js +4 -3
- package/dist/scripts/assistant_worker.d.ts +2 -1
- package/dist/scripts/assistant_worker.js +9 -4
- package/dist/scripts/syndicate_chat.js +19 -10
- package/package.json +4 -1
- package/skills/briefs/melchizedek-author.md +3 -2
- package/skills/briefs/melchizedek-models.md +13 -9
- package/skills/briefs/melchizedek.md +3 -3
- package/skills/melchizedek/SKILL.md +2 -1
- package/skills/melchizedek-author/SKILL.md +2 -1
- package/skills/melchizedek-author/assets/minimal.yaml +2 -0
- package/skills/melchizedek-models/SKILL.md +23 -6
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0013_tool_credentials.sql — third-party OAuth tokens held for tools (ADR 0072)
|
|
3
|
+
-- ============================================================================
|
|
4
|
+
-- A tool that acts for an end user against a third-party API (a calendar, a
|
|
5
|
+
-- repository host, a CRM) needs that user's delegated access, not the
|
|
6
|
+
-- server's own key. melchizedek_tool_credentials holds one token set per
|
|
7
|
+
-- app, end user and provider:
|
|
8
|
+
--
|
|
9
|
+
-- app_name the app the run pins (the root syndicate's memory namespace)
|
|
10
|
+
-- user_id the A2A server's scope key, as sessions and tasks store it
|
|
11
|
+
-- provider the provider's name as the tool asks for it ('github')
|
|
12
|
+
-- scopes what the user granted
|
|
13
|
+
-- access_token_enc, refresh_token_enc
|
|
14
|
+
-- ciphertext only, in the engine's envelope ('mzc1.<key id>.…',
|
|
15
|
+
-- AES-256-GCM by default, lib/tools/credentialCipher.ts). The
|
|
16
|
+
-- CHECK refuses anything else, so a plaintext token cannot be
|
|
17
|
+
-- written by mistake. The key never reaches the database.
|
|
18
|
+
-- key_id which key sealed the row, so a wrong key fails closed by name
|
|
19
|
+
-- expires_at when the access token stops working (NULL: not stated)
|
|
20
|
+
-- version bumped by every write; a refresh writes only over the version
|
|
21
|
+
-- it read, so two instances refreshing at once keep one result
|
|
22
|
+
--
|
|
23
|
+
-- The server's role reads and writes it; no API role can (RLS on, grants
|
|
24
|
+
-- revoked, as db/hardening.sql repeats). melchizedek_erase_scope is replaced
|
|
25
|
+
-- with the 0011 function plus a `credentials` store, so one erase of a user
|
|
26
|
+
-- also removes their tokens (ADR 0020 item 7). Idempotent.
|
|
27
|
+
-- ============================================================================
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS melchizedek_tool_credentials (
|
|
30
|
+
app_name TEXT NOT NULL,
|
|
31
|
+
user_id TEXT NOT NULL,
|
|
32
|
+
provider TEXT NOT NULL,
|
|
33
|
+
scopes TEXT[] NOT NULL DEFAULT '{}',
|
|
34
|
+
access_token_enc TEXT NOT NULL CHECK (access_token_enc ~ '^mzc1[.]'),
|
|
35
|
+
refresh_token_enc TEXT CHECK (refresh_token_enc IS NULL OR refresh_token_enc ~ '^mzc1[.]'),
|
|
36
|
+
key_id TEXT NOT NULL,
|
|
37
|
+
expires_at TIMESTAMPTZ,
|
|
38
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
39
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
40
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
41
|
+
refreshed_at TIMESTAMPTZ,
|
|
42
|
+
PRIMARY KEY (app_name, user_id, provider)
|
|
43
|
+
);
|
|
44
|
+
-- Erase finds a user's rows across apps.
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_melchizedek_tool_credentials_user
|
|
46
|
+
ON melchizedek_tool_credentials (user_id);
|
|
47
|
+
|
|
48
|
+
ALTER TABLE melchizedek_tool_credentials ENABLE ROW LEVEL SECURITY;
|
|
49
|
+
DO $$
|
|
50
|
+
BEGIN
|
|
51
|
+
REVOKE ALL ON melchizedek_tool_credentials FROM PUBLIC;
|
|
52
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
53
|
+
REVOKE ALL ON melchizedek_tool_credentials FROM anon;
|
|
54
|
+
END IF;
|
|
55
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
56
|
+
REVOKE ALL ON melchizedek_tool_credentials FROM authenticated;
|
|
57
|
+
END IF;
|
|
58
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
59
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON melchizedek_tool_credentials TO service_role;
|
|
60
|
+
END IF;
|
|
61
|
+
END $$;
|
|
62
|
+
|
|
63
|
+
-- One erase covers the credentials too: the 0011 function, plus the
|
|
64
|
+
-- `credentials` store at the end.
|
|
65
|
+
CREATE OR REPLACE FUNCTION melchizedek_erase_scope(
|
|
66
|
+
p_scope_key text,
|
|
67
|
+
p_namespace text DEFAULT NULL,
|
|
68
|
+
p_include_nested boolean DEFAULT false
|
|
69
|
+
) RETURNS TABLE(store text, deleted bigint)
|
|
70
|
+
LANGUAGE plpgsql
|
|
71
|
+
SECURITY INVOKER
|
|
72
|
+
SET search_path = public
|
|
73
|
+
AS $$
|
|
74
|
+
DECLARE
|
|
75
|
+
-- LIKE pattern for scopes nested beneath this one, with the scope's own
|
|
76
|
+
-- wildcard characters escaped.
|
|
77
|
+
nested_like text := replace(replace(replace(p_scope_key, '\', '\\'), '%', '\%'), '_', '\_') || '/%';
|
|
78
|
+
contexts text[];
|
|
79
|
+
kept text[];
|
|
80
|
+
traces text[];
|
|
81
|
+
n bigint;
|
|
82
|
+
BEGIN
|
|
83
|
+
IF p_scope_key IS NULL OR btrim(p_scope_key) = '' THEN
|
|
84
|
+
RAISE EXCEPTION 'melchizedek_erase_scope: scope_key is required';
|
|
85
|
+
END IF;
|
|
86
|
+
|
|
87
|
+
-- The conversations (context ids) being erased. With a namespace, they are
|
|
88
|
+
-- the namespace's own session rows; without one, every row of the scope.
|
|
89
|
+
-- adk_sessions.id is '<app_name>:<user_id>:<context id>'.
|
|
90
|
+
SELECT coalesce(array_agg(DISTINCT substr(s.id, length(s.app_name) + length(s.user_id) + 3)), '{}')
|
|
91
|
+
INTO contexts
|
|
92
|
+
FROM adk_sessions s
|
|
93
|
+
WHERE (s.user_id = p_scope_key OR (p_include_nested AND s.user_id LIKE nested_like))
|
|
94
|
+
AND (p_namespace IS NULL OR s.app_name = p_namespace);
|
|
95
|
+
|
|
96
|
+
-- With a namespace: the conversations still live in another namespace,
|
|
97
|
+
-- which are the only ones a namespace erase keeps. Sub-agent rows share
|
|
98
|
+
-- their conversation's id, so excluding `contexts` keeps them with it.
|
|
99
|
+
SELECT coalesce(array_agg(DISTINCT substr(s.id, length(s.app_name) + length(s.user_id) + 3)), '{}')
|
|
100
|
+
INTO kept
|
|
101
|
+
FROM adk_sessions s
|
|
102
|
+
WHERE p_namespace IS NOT NULL
|
|
103
|
+
AND (s.user_id = p_scope_key OR (p_include_nested AND s.user_id LIKE nested_like))
|
|
104
|
+
AND s.app_name <> p_namespace
|
|
105
|
+
AND NOT (substr(s.id, length(s.app_name) + length(s.user_id) + 3) = ANY(contexts));
|
|
106
|
+
|
|
107
|
+
-- Ledger traces of the turns being erased, collected before anything is deleted.
|
|
108
|
+
IF to_regclass('public.adk_turns') IS NOT NULL THEN
|
|
109
|
+
EXECUTE $q$
|
|
110
|
+
SELECT coalesce(array_agg(DISTINCT trace_id), '{}') FROM adk_turns
|
|
111
|
+
WHERE (user_id = $1 OR ($2 AND user_id LIKE $3))
|
|
112
|
+
AND ($4 IS NULL OR session_id IS NULL OR NOT (session_id = ANY($5)))
|
|
113
|
+
$q$ INTO traces USING p_scope_key, p_include_nested, nested_like, p_namespace, kept;
|
|
114
|
+
ELSE
|
|
115
|
+
traces := '{}';
|
|
116
|
+
END IF;
|
|
117
|
+
|
|
118
|
+
-- Memory facts. user_key is '<namespace>/<scope>' and a namespace never
|
|
119
|
+
-- contains '/', so the scope is exactly what follows the first slash.
|
|
120
|
+
DELETE FROM adk_memory_facts f
|
|
121
|
+
WHERE (p_namespace IS NULL OR split_part(f.user_key, '/', 1) = p_namespace)
|
|
122
|
+
AND (substr(f.user_key, strpos(f.user_key, '/') + 1) = p_scope_key
|
|
123
|
+
OR (p_include_nested AND substr(f.user_key, strpos(f.user_key, '/') + 1) LIKE nested_like));
|
|
124
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
125
|
+
store := 'memory_facts'; deleted := n; RETURN NEXT;
|
|
126
|
+
|
|
127
|
+
-- Sessions: every row of the scope in those conversations, whatever app
|
|
128
|
+
-- name ADK gave it (subagents run under their own agent name).
|
|
129
|
+
DELETE FROM adk_sessions s
|
|
130
|
+
WHERE (s.user_id = p_scope_key OR (p_include_nested AND s.user_id LIKE nested_like))
|
|
131
|
+
AND substr(s.id, length(s.app_name) + length(s.user_id) + 3) = ANY(contexts);
|
|
132
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
133
|
+
store := 'sessions'; deleted := n; RETURN NEXT;
|
|
134
|
+
|
|
135
|
+
-- The ledger, when installed.
|
|
136
|
+
IF to_regclass('public.adk_turns') IS NOT NULL THEN
|
|
137
|
+
EXECUTE $q$
|
|
138
|
+
DELETE FROM adk_turns
|
|
139
|
+
WHERE (user_id = $1 OR ($2 AND user_id LIKE $3))
|
|
140
|
+
AND ($4 IS NULL OR session_id IS NULL OR NOT (session_id = ANY($5)))
|
|
141
|
+
$q$ USING p_scope_key, p_include_nested, nested_like, p_namespace, kept;
|
|
142
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
143
|
+
ELSE
|
|
144
|
+
n := 0;
|
|
145
|
+
END IF;
|
|
146
|
+
store := 'turns'; deleted := n; RETURN NEXT;
|
|
147
|
+
|
|
148
|
+
IF to_regclass('public.adk_telemetry') IS NOT NULL THEN
|
|
149
|
+
EXECUTE 'DELETE FROM adk_telemetry WHERE trace_id = ANY($1)' USING traces;
|
|
150
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
151
|
+
ELSE
|
|
152
|
+
n := 0;
|
|
153
|
+
END IF;
|
|
154
|
+
store := 'spans'; deleted := n; RETURN NEXT;
|
|
155
|
+
|
|
156
|
+
IF to_regclass('public.adk_payloads') IS NOT NULL THEN
|
|
157
|
+
EXECUTE 'DELETE FROM adk_payloads WHERE trace_id = ANY($1)' USING traces;
|
|
158
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
159
|
+
ELSE
|
|
160
|
+
n := 0;
|
|
161
|
+
END IF;
|
|
162
|
+
store := 'payloads'; deleted := n; RETURN NEXT;
|
|
163
|
+
|
|
164
|
+
IF to_regclass('public.adk_verdicts') IS NOT NULL THEN
|
|
165
|
+
EXECUTE 'DELETE FROM adk_verdicts WHERE trace_id = ANY($1)' USING traces;
|
|
166
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
167
|
+
ELSE
|
|
168
|
+
n := 0;
|
|
169
|
+
END IF;
|
|
170
|
+
store := 'verdicts'; deleted := n; RETURN NEXT;
|
|
171
|
+
|
|
172
|
+
IF EXISTS (
|
|
173
|
+
SELECT 1 FROM information_schema.columns
|
|
174
|
+
WHERE table_schema = 'public' AND table_name = 'adk_labels' AND column_name = 'trace_id'
|
|
175
|
+
) THEN
|
|
176
|
+
EXECUTE 'DELETE FROM adk_labels WHERE trace_id = ANY($1)' USING traces;
|
|
177
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
178
|
+
ELSE
|
|
179
|
+
n := 0;
|
|
180
|
+
END IF;
|
|
181
|
+
store := 'labels'; deleted := n; RETURN NEXT;
|
|
182
|
+
|
|
183
|
+
IF to_regclass('public.adk_a2a_tasks') IS NOT NULL THEN
|
|
184
|
+
EXECUTE $q$
|
|
185
|
+
DELETE FROM adk_a2a_tasks
|
|
186
|
+
WHERE (owner = $1 OR ($2 AND owner LIKE $3))
|
|
187
|
+
AND ($4 IS NULL OR context_id IS NULL OR NOT (context_id = ANY($5)))
|
|
188
|
+
$q$ USING p_scope_key, p_include_nested, nested_like, p_namespace, kept;
|
|
189
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
190
|
+
ELSE
|
|
191
|
+
n := 0;
|
|
192
|
+
END IF;
|
|
193
|
+
store := 'tasks'; deleted := n; RETURN NEXT;
|
|
194
|
+
|
|
195
|
+
-- Memory ingestion markers (migration 0007): how far each conversation was
|
|
196
|
+
-- distilled. They carry the scope's key and its conversation ids.
|
|
197
|
+
IF to_regclass('public.melchizedek_memory_ingest') IS NOT NULL THEN
|
|
198
|
+
EXECUTE $q$
|
|
199
|
+
DELETE FROM melchizedek_memory_ingest m
|
|
200
|
+
WHERE ($1 IS NULL OR split_part(m.user_key, '/', 1) = $1)
|
|
201
|
+
AND (substr(m.user_key, strpos(m.user_key, '/') + 1) = $2
|
|
202
|
+
OR ($3 AND substr(m.user_key, strpos(m.user_key, '/') + 1) LIKE $4))
|
|
203
|
+
$q$ USING p_namespace, p_scope_key, p_include_nested, nested_like;
|
|
204
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
205
|
+
ELSE
|
|
206
|
+
n := 0;
|
|
207
|
+
END IF;
|
|
208
|
+
store := 'memory_markers'; deleted := n; RETURN NEXT;
|
|
209
|
+
|
|
210
|
+
-- The task tools' list and job queue (migration 0009), per caller across
|
|
211
|
+
-- every syndicate: erased with the whole scope, kept by a namespace erase.
|
|
212
|
+
n := 0;
|
|
213
|
+
IF p_namespace IS NULL AND to_regclass('public.melchizedek_tasks') IS NOT NULL THEN
|
|
214
|
+
EXECUTE $q$
|
|
215
|
+
DELETE FROM melchizedek_tasks WHERE owner = $1 OR ($2 AND owner LIKE $3)
|
|
216
|
+
$q$ USING p_scope_key, p_include_nested, nested_like;
|
|
217
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
218
|
+
EXECUTE $q$
|
|
219
|
+
DELETE FROM melchizedek_task_owners WHERE owner = $1 OR ($2 AND owner LIKE $3)
|
|
220
|
+
$q$ USING p_scope_key, p_include_nested, nested_like;
|
|
221
|
+
END IF;
|
|
222
|
+
store := 'task_tools'; deleted := n; RETURN NEXT;
|
|
223
|
+
|
|
224
|
+
-- Third-party credentials held for the scope's tools (this migration).
|
|
225
|
+
-- They are filed per app (the memory namespace the run pins), so a
|
|
226
|
+
-- namespace erase removes that app's and keeps the others; an erase
|
|
227
|
+
-- without one removes every app's.
|
|
228
|
+
IF to_regclass('public.melchizedek_tool_credentials') IS NOT NULL THEN
|
|
229
|
+
EXECUTE $q$
|
|
230
|
+
DELETE FROM melchizedek_tool_credentials c
|
|
231
|
+
WHERE ($1 IS NULL OR c.app_name = $1)
|
|
232
|
+
AND (c.user_id = $2 OR ($3 AND c.user_id LIKE $4))
|
|
233
|
+
$q$ USING p_namespace, p_scope_key, p_include_nested, nested_like;
|
|
234
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
235
|
+
ELSE
|
|
236
|
+
n := 0;
|
|
237
|
+
END IF;
|
|
238
|
+
store := 'credentials'; deleted := n; RETURN NEXT;
|
|
239
|
+
END;
|
|
240
|
+
$$;
|
|
241
|
+
|
|
242
|
+
-- No API role may execute it (db/hardening.sql repeats this for every
|
|
243
|
+
-- melchizedek function); the server's role gets it back.
|
|
244
|
+
DO $$
|
|
245
|
+
BEGIN
|
|
246
|
+
REVOKE ALL ON FUNCTION melchizedek_erase_scope(text, text, boolean) FROM PUBLIC;
|
|
247
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
248
|
+
REVOKE ALL ON FUNCTION melchizedek_erase_scope(text, text, boolean) FROM anon;
|
|
249
|
+
END IF;
|
|
250
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
251
|
+
REVOKE ALL ON FUNCTION melchizedek_erase_scope(text, text, boolean) FROM authenticated;
|
|
252
|
+
END IF;
|
|
253
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
254
|
+
GRANT EXECUTE ON FUNCTION melchizedek_erase_scope(text, text, boolean) TO service_role;
|
|
255
|
+
END IF;
|
|
256
|
+
END $$;
|
|
257
|
+
|
|
258
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
259
|
+
VALUES (13, '0013_tool_credentials')
|
|
260
|
+
ON CONFLICT (version) DO NOTHING;
|
package/dist/lib/a2a/app.d.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* Middleware order, and why:
|
|
9
9
|
* 1. /healthz, /readyz — unauthenticated, so a load balancer or
|
|
10
10
|
* Kubernetes probe needs no secret. They reveal nothing but liveness.
|
|
11
|
+
* 1b. The OAuth consent callback, when `toolCredentials.consent` is set
|
|
12
|
+
* (ADR 0085): a provider redirects the person's browser to it, so it
|
|
13
|
+
* cannot carry the bearer. Its own rate limit; the single-use state
|
|
14
|
+
* nonce admits it.
|
|
11
15
|
* 2. Failed-auth limiter — counts only 401s per IP, so the shared secret
|
|
12
16
|
* cannot be guessed online at whatever rate the host allows.
|
|
13
17
|
* 3. Bearer check (when a secret is configured), constant-time.
|
|
@@ -22,12 +26,19 @@
|
|
|
22
26
|
* the handler cache, the rate-limiter counters. Sessions and memory are
|
|
23
27
|
* durable when Supabase is configured. Run one replica, or put sticky
|
|
24
28
|
* routing in front, until the task store is durable.
|
|
29
|
+
*
|
|
30
|
+
* Stores (ADR 0080): `storage` takes a session store and a memory service
|
|
31
|
+
* with either face, the engine's or ADK's. In-process sessions are the
|
|
32
|
+
* engine's InProcessSessionService. A `resolveModel` may still return an ADK
|
|
33
|
+
* model instance: its type is CompileOptions'.
|
|
25
34
|
*/
|
|
26
|
-
import type { Express, Request } from 'express';
|
|
35
|
+
import type { Express, Request, RequestHandler } from 'express';
|
|
27
36
|
import type { Store } from 'express-rate-limit';
|
|
28
37
|
import type { AgentCard } from '@a2a-js/sdk';
|
|
29
38
|
import type { TaskStore } from '@a2a-js/sdk/server';
|
|
30
|
-
import type {
|
|
39
|
+
import type { CompileOptions } from '../compile.ts';
|
|
40
|
+
import type { EitherSessionService } from '../runtime/adkSessionBridge.ts';
|
|
41
|
+
import type { EitherMemoryService } from '../runtime/adkMemoryBridge.ts';
|
|
31
42
|
import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
|
|
32
43
|
import type { ProviderEndpoint, ProviderId } from '../models/registry.ts';
|
|
33
44
|
import type { TurnLock } from './turnLock.ts';
|
|
@@ -41,8 +52,27 @@ import type { A2AContext } from './executor.ts';
|
|
|
41
52
|
import type { IdentityScheme } from './identity.ts';
|
|
42
53
|
import type { Policy } from './policy.ts';
|
|
43
54
|
import type { TaskRecord } from '../observability/metrics.ts';
|
|
55
|
+
import type { OAuthConsent, ToolCredentials } from '../tools/oauthConsent.ts';
|
|
44
56
|
/** Agent ids address a config (file or "registry:<id>"): a safe charset only. */
|
|
45
57
|
export declare function isValidAgentId(agentId: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The consent callback (ADR 0085): the provider's redirect, carrying `code`
|
|
60
|
+
* and `state` (or `error`). It never reads a redirect URI from the request,
|
|
61
|
+
* never logs the query, and answers a page that carries no value. The
|
|
62
|
+
* response is never cached and sends no referrer, since its URL holds the
|
|
63
|
+
* code.
|
|
64
|
+
*/
|
|
65
|
+
export declare function consentCallback(consent: Pick<OAuthConsent, 'complete'>, opts?: {
|
|
66
|
+
resolveRequest?: A2AAppOptions['resolveRequest'];
|
|
67
|
+
/** The server's bearer secret, when it has one: the authenticator is consulted only behind it. */
|
|
68
|
+
serverSecret?: string;
|
|
69
|
+
/** Refuse a callback whose request the authenticator does not accept. Default true (ADR 0085). */
|
|
70
|
+
requireCallerIdentity?: boolean;
|
|
71
|
+
log?: (m: string) => void;
|
|
72
|
+
warn?: (m: string) => void;
|
|
73
|
+
}): RequestHandler;
|
|
74
|
+
/** What a model resolver returns, as the compiler takes it. */
|
|
75
|
+
type ResolvedModel = ReturnType<NonNullable<CompileOptions['resolveModel']>>;
|
|
46
76
|
export interface A2AAppOptions {
|
|
47
77
|
/** The syndicate served at /a2a/*: a YAML name or "registry:<id>". */
|
|
48
78
|
defaultSyndicate: string;
|
|
@@ -58,8 +88,10 @@ export interface A2AAppOptions {
|
|
|
58
88
|
* agent id (default: in-process).
|
|
59
89
|
*/
|
|
60
90
|
storage?: {
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
/** The engine's SessionService or ADK's BaseSessionService (ADR 0080). */
|
|
92
|
+
sessionService: EitherSessionService;
|
|
93
|
+
/** The engine's MemoryService or ADK's BaseMemoryService. */
|
|
94
|
+
memoryService?: EitherMemoryService;
|
|
63
95
|
taskStore?: (agentId: string) => TaskStore;
|
|
64
96
|
/** Erase everything stored for a scope (DELETE /memory). Without it the route answers 501. */
|
|
65
97
|
erase?: (scopeKey: string, options: {
|
|
@@ -155,6 +187,35 @@ export interface A2AAppOptions {
|
|
|
155
187
|
};
|
|
156
188
|
/** Adopter routes, mounted after authentication and before the A2A routes. */
|
|
157
189
|
routes?: (app: Express) => void;
|
|
190
|
+
/**
|
|
191
|
+
* Tool credentials for every turn (ADR 0072): the sealed per-user store,
|
|
192
|
+
* and the OAuth consent step (ADR 0085, lib/tools/oauthConsent.ts). With
|
|
193
|
+
* `consent`, a call whose provider the user has not granted ends its task
|
|
194
|
+
* input-required with a `consent_request` data part, and the server mounts
|
|
195
|
+
* the consent callback (GET at the path of the consent's configured
|
|
196
|
+
* redirect URI): it completes the authorization-code flow with PKCE and
|
|
197
|
+
* stores the grant. The callback is reached by the person's browser, so it
|
|
198
|
+
* sits before the bearer check; it has its own rate limit, and when the
|
|
199
|
+
* request carries a credential the authenticator accepts, the caller must
|
|
200
|
+
* be the user the flow is for.
|
|
201
|
+
*/
|
|
202
|
+
toolCredentials?: ToolCredentials & {
|
|
203
|
+
/** Callback requests per window per client IP. Default 30 per 15 minutes. */
|
|
204
|
+
callbackLimit?: {
|
|
205
|
+
windowMs: number;
|
|
206
|
+
max: number;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Refuse a callback whose request the authenticator does not accept.
|
|
210
|
+
* Default true: the browser that completes the grant must carry the
|
|
211
|
+
* flow's user's identity (a session cookie or a gateway header that
|
|
212
|
+
* resolveRequest reads), so a forwarded authorization link cannot link
|
|
213
|
+
* someone else's account (ADR 0085). false lets the state nonce alone
|
|
214
|
+
* bind the flow to its user, for a deployment whose browsers carry no
|
|
215
|
+
* identity and that accepts that risk.
|
|
216
|
+
*/
|
|
217
|
+
requireCallerIdentity?: boolean;
|
|
218
|
+
};
|
|
158
219
|
/** Refuse to start when Supabase hardening is missing (public deployments). */
|
|
159
220
|
requireHardenedDb?: boolean;
|
|
160
221
|
/** Wall-clock budget per task, ms. 0 = none. */
|
|
@@ -206,9 +267,11 @@ export interface A2AAppOptions {
|
|
|
206
267
|
/**
|
|
207
268
|
* Model resolution per request. Default: lib/models/registry.ts with the
|
|
208
269
|
* caller's X-API-Key scoped to its X-Provider. Override to route through
|
|
209
|
-
* your own gateway or credential store.
|
|
270
|
+
* your own gateway or credential store. Returns what
|
|
271
|
+
* CompileOptions.resolveModel returns: a model id, or a model instance
|
|
272
|
+
* (an ADK BaseLlm while the ADK runtime ships).
|
|
210
273
|
*/
|
|
211
|
-
resolveModel?: (modelName: string | undefined, ctx: A2AContext) =>
|
|
274
|
+
resolveModel?: (modelName: string | undefined, ctx: A2AContext) => ResolvedModel;
|
|
212
275
|
/** Bindings applied at every config load (e.g. current_date). */
|
|
213
276
|
bindings?: () => Record<string, string>;
|
|
214
277
|
log?: (message: string) => void;
|
|
@@ -278,3 +341,4 @@ export declare function compileAgentCard(config: SyndicateYamlConfig, opts: {
|
|
|
278
341
|
*/
|
|
279
342
|
export declare function memoryAppName(cfg: SyndicateYamlConfig): string;
|
|
280
343
|
export declare function createA2AApp(options: A2AAppOptions): Promise<A2AApp>;
|
|
344
|
+
export {};
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* Middleware order, and why:
|
|
9
9
|
* 1. /healthz, /readyz — unauthenticated, so a load balancer or
|
|
10
10
|
* Kubernetes probe needs no secret. They reveal nothing but liveness.
|
|
11
|
+
* 1b. The OAuth consent callback, when `toolCredentials.consent` is set
|
|
12
|
+
* (ADR 0085): a provider redirects the person's browser to it, so it
|
|
13
|
+
* cannot carry the bearer. Its own rate limit; the single-use state
|
|
14
|
+
* nonce admits it.
|
|
11
15
|
* 2. Failed-auth limiter — counts only 401s per IP, so the shared secret
|
|
12
16
|
* cannot be guessed online at whatever rate the host allows.
|
|
13
17
|
* 3. Bearer check (when a secret is configured), constant-time.
|
|
@@ -22,6 +26,11 @@
|
|
|
22
26
|
* the handler cache, the rate-limiter counters. Sessions and memory are
|
|
23
27
|
* durable when Supabase is configured. Run one replica, or put sticky
|
|
24
28
|
* routing in front, until the task store is durable.
|
|
29
|
+
*
|
|
30
|
+
* Stores (ADR 0080): `storage` takes a session store and a memory service
|
|
31
|
+
* with either face, the engine's or ADK's. In-process sessions are the
|
|
32
|
+
* engine's InProcessSessionService. A `resolveModel` may still return an ADK
|
|
33
|
+
* model instance: its type is CompileOptions'.
|
|
25
34
|
*/
|
|
26
35
|
import express from 'express';
|
|
27
36
|
import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
|
|
@@ -30,7 +39,7 @@ import { AGENT_CARD_PATH } from '@a2a-js/sdk';
|
|
|
30
39
|
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
|
|
31
40
|
import { duplicateInterfacesForLegacy } from '@a2a-js/sdk/compat/v0_3';
|
|
32
41
|
import { agentCardHandler, jsonRpcHandler, restHandler } from '@a2a-js/sdk/server/express';
|
|
33
|
-
import {
|
|
42
|
+
import { InProcessSessionService } from '../runtime/sessions.js';
|
|
34
43
|
import { loadSyndicate, loadSyndicateFromRegistry } from '../loadSyndicate.js';
|
|
35
44
|
import { providerForModel, resolveModel } from '../models/registry.js';
|
|
36
45
|
import { createSupabaseServices, hasSupabaseCredentials } from '../persistence/supabaseProvider.js';
|
|
@@ -46,6 +55,7 @@ import { memoryCrossesProviders, memoryDestinations } from '../memory/providers.
|
|
|
46
55
|
import { A2A_APP_NAME, SyndicateExecutor, TaskLimiter, deriveUserId, requestContextStorage, } from './executor.js';
|
|
47
56
|
import { HEADER_VALUE_PATTERN, SCOPE_KEY_PATTERN } from './identity.js';
|
|
48
57
|
import { createMetrics } from '../observability/metrics.js';
|
|
58
|
+
import { ConsentError } from '../tools/oauthConsent.js';
|
|
49
59
|
const SURFACE_HEADERS = [
|
|
50
60
|
['x-surface', 'name'],
|
|
51
61
|
['x-surface-guild', 'guild'],
|
|
@@ -56,6 +66,78 @@ const SURFACE_HEADERS = [
|
|
|
56
66
|
export function isValidAgentId(agentId) {
|
|
57
67
|
return /^[A-Za-z0-9_.:-]+$/.test(agentId) && !agentId.includes('..');
|
|
58
68
|
}
|
|
69
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
70
|
+
/** True when the request's bearer is `secret` (constant-time). */
|
|
71
|
+
function bearerMatches(req, secret) {
|
|
72
|
+
const header = req.headers.authorization;
|
|
73
|
+
if (!header || !header.startsWith('Bearer '))
|
|
74
|
+
return false;
|
|
75
|
+
const given = createHash('sha256').update(header.substring(7)).digest();
|
|
76
|
+
return timingSafeEqual(given, createHash('sha256').update(secret).digest());
|
|
77
|
+
}
|
|
78
|
+
/** The page the person's browser lands on: what happened, and nothing else (no code, state or token). */
|
|
79
|
+
function consentPage(title, body) {
|
|
80
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(title)}</title></head>`
|
|
81
|
+
+ `<body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5"><h1 style="font-size:1.25rem">${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></body></html>`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The consent callback (ADR 0085): the provider's redirect, carrying `code`
|
|
85
|
+
* and `state` (or `error`). It never reads a redirect URI from the request,
|
|
86
|
+
* never logs the query, and answers a page that carries no value. The
|
|
87
|
+
* response is never cached and sends no referrer, since its URL holds the
|
|
88
|
+
* code.
|
|
89
|
+
*/
|
|
90
|
+
export function consentCallback(consent, opts = {}) {
|
|
91
|
+
return async (req, res) => {
|
|
92
|
+
res.set({
|
|
93
|
+
'Cache-Control': 'no-store',
|
|
94
|
+
Pragma: 'no-cache',
|
|
95
|
+
'Referrer-Policy': 'no-referrer',
|
|
96
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
|
|
97
|
+
'X-Content-Type-Options': 'nosniff',
|
|
98
|
+
'X-Frame-Options': 'DENY',
|
|
99
|
+
});
|
|
100
|
+
// When the browser carries a credential the authenticator accepts, the caller must be the flow's user.
|
|
101
|
+
// Behind a server secret the authenticator is asked only once the secret matched, as on every other
|
|
102
|
+
// route: a trusted-header authenticator believes whoever reached it.
|
|
103
|
+
let callerUserId;
|
|
104
|
+
const gated = opts.serverSecret === undefined || bearerMatches(req, opts.serverSecret);
|
|
105
|
+
if (opts.resolveRequest && gated) {
|
|
106
|
+
try {
|
|
107
|
+
callerUserId = (await opts.resolveRequest(req))?.scopeKey;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
callerUserId = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (opts.requireCallerIdentity !== false && callerUserId === undefined) {
|
|
114
|
+
opts.warn?.('Consent callback refused: no authenticated caller.');
|
|
115
|
+
res.status(401).type('html').send(consentPage('Authorization not completed', 'Sign in, then open the authorization link again.'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const done = await consent.complete({
|
|
120
|
+
state: req.query.state,
|
|
121
|
+
code: req.query.code,
|
|
122
|
+
error: req.query.error,
|
|
123
|
+
...(callerUserId !== undefined ? { callerUserId } : {}),
|
|
124
|
+
});
|
|
125
|
+
opts.log?.(`✓ Consent callback: ${done.provider} granted (scope ${scopeHashOf(done.userId)})`);
|
|
126
|
+
res.status(200).type('html').send(consentPage('Authorization complete', 'You can close this window and send a message in your conversation to continue.'));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof ConsentError) {
|
|
130
|
+
opts.warn?.(`Consent callback refused: ${err.code}${err.provider ? ` (${err.provider})` : ''}`);
|
|
131
|
+
const status = err.code === 'exchange_failed' ? 502 : err.code === 'store_failed' ? 500 : err.code === 'wrong_user' ? 403 : 400;
|
|
132
|
+
res.status(status).type('html').send(consentPage('Authorization not completed', err.message));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Nothing from an unexpected error reaches the page or the log: it may carry a value.
|
|
136
|
+
opts.warn?.('Consent callback failed unexpectedly.');
|
|
137
|
+
res.status(500).type('html').send(consentPage('Authorization not completed', 'The authorization could not be completed. Ask the agent again for a new link.'));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
59
141
|
const legacyCompat = { enabled: true };
|
|
60
142
|
/**
|
|
61
143
|
* The authenticated caller of the request being handled, for adopter routes
|
|
@@ -290,7 +372,7 @@ export async function createA2AApp(options) {
|
|
|
290
372
|
* even when Supabase is configured (the docs promise "nothing persists"),
|
|
291
373
|
* and only `long-term` syndicates get the memory service.
|
|
292
374
|
*/
|
|
293
|
-
const internalSessions = new
|
|
375
|
+
const internalSessions = new InProcessSessionService();
|
|
294
376
|
// Long-term memory sends transcripts to its extraction and embedding
|
|
295
377
|
// providers; say so once per syndicate when they are not the agents' own.
|
|
296
378
|
const memoryFlowWarned = new Set();
|
|
@@ -386,6 +468,7 @@ export async function createA2AApp(options) {
|
|
|
386
468
|
streamText: options.streamText,
|
|
387
469
|
turnLock,
|
|
388
470
|
turnLockWaitMs: options.turnLockWaitMs,
|
|
471
|
+
...(options.toolCredentials ? { toolCredentials: { store: options.toolCredentials.store, ...(options.toolCredentials.consent ? { consent: options.toolCredentials.consent } : {}) } } : {}),
|
|
389
472
|
limiter,
|
|
390
473
|
agentId,
|
|
391
474
|
policy: options.policy,
|
|
@@ -499,6 +582,28 @@ export async function createA2AApp(options) {
|
|
|
499
582
|
}
|
|
500
583
|
res.json({ status: 'ready', sessions: sessionBackend, inFlight: limiter.inFlight });
|
|
501
584
|
});
|
|
585
|
+
// ── 1b. The OAuth consent callback (ADR 0085) ─────────────────────────────
|
|
586
|
+
// The provider redirects the person's browser here, so it cannot carry the
|
|
587
|
+
// A2A bearer: it sits before the bearer check, behind its own rate limit.
|
|
588
|
+
// The state nonce (single-use, expiring, bound to the user, the session
|
|
589
|
+
// and the paused call) is what admits it.
|
|
590
|
+
const consent = options.toolCredentials?.consent;
|
|
591
|
+
if (consent) {
|
|
592
|
+
const callbackLimit = options.toolCredentials?.callbackLimit ?? { windowMs: 15 * 60 * 1000, max: 30 };
|
|
593
|
+
app.get(new URL(consent.redirectUri).pathname, rateLimit({
|
|
594
|
+
windowMs: callbackLimit.windowMs,
|
|
595
|
+
max: callbackLimit.max,
|
|
596
|
+
standardHeaders: true,
|
|
597
|
+
legacyHeaders: false,
|
|
598
|
+
message: { error: 'Too many authorization attempts; try again later.' },
|
|
599
|
+
}), consentCallback(consent, {
|
|
600
|
+
resolveRequest: options.resolveRequest,
|
|
601
|
+
...(options.serverSecret ? { serverSecret: options.serverSecret } : {}),
|
|
602
|
+
requireCallerIdentity: options.toolCredentials?.requireCallerIdentity !== false,
|
|
603
|
+
log,
|
|
604
|
+
warn,
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
502
607
|
// ── 2–3. Authentication ───────────────────────────────────────────────────
|
|
503
608
|
const failWindow = options.authFailureLimit ?? { windowMs: 15 * 60 * 1000, max: 30 };
|
|
504
609
|
app.use(rateLimit({
|
|
@@ -8,15 +8,23 @@
|
|
|
8
8
|
* cancellation, the per-server concurrency cap, and memory ingestion after
|
|
9
9
|
* the reply. Every turn semantic — dispatch, delegation, relay fallback,
|
|
10
10
|
* guards, the step cap — is the runtime's.
|
|
11
|
+
*
|
|
12
|
+
* Its stores are the engine's (ADR 0080): it takes a session store and a
|
|
13
|
+
* memory service with either face, the engine's or ADK's, reads a session
|
|
14
|
+
* through the engine's SessionService, and hands the turn runner the faces
|
|
15
|
+
* its fixed signature names through the bridges
|
|
16
|
+
* (lib/runtime/adkSessionBridge.ts, lib/runtime/adkMemoryBridge.ts).
|
|
11
17
|
*/
|
|
12
18
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
19
|
import type { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
|
14
|
-
import type { BaseMemoryService, BaseSessionService } from '@google/adk';
|
|
15
20
|
import type { CompileOptions } from '../compile.ts';
|
|
16
21
|
import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
|
|
17
22
|
import type { TurnLock } from './turnLock.ts';
|
|
23
|
+
import type { EitherSessionService } from '../runtime/adkSessionBridge.ts';
|
|
24
|
+
import type { EitherMemoryService } from '../runtime/adkMemoryBridge.ts';
|
|
18
25
|
import type { PendingApproval } from '../runtime/approvals.ts';
|
|
19
26
|
import type { MessagePart, SyndicateTurnResult } from '../runtime/syndicateTurn.ts';
|
|
27
|
+
import type { ToolCredentials } from '../tools/oauthConsent.ts';
|
|
20
28
|
import type { TaskRecord } from '../observability/metrics.ts';
|
|
21
29
|
import type { AuditSink } from '../observability/audit.ts';
|
|
22
30
|
import type { Policy } from './policy.ts';
|
|
@@ -94,8 +102,10 @@ export declare function a2aPartsToMessage(rawParts: unknown[]): {
|
|
|
94
102
|
};
|
|
95
103
|
export interface ExecutorOptions {
|
|
96
104
|
config: SyndicateYamlConfig;
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
/** The conversation store: the engine's SessionService or ADK's BaseSessionService. */
|
|
106
|
+
sessionService: EitherSessionService;
|
|
107
|
+
/** Long-term memory: the engine's MemoryService or ADK's BaseMemoryService. */
|
|
108
|
+
memoryService?: EitherMemoryService;
|
|
99
109
|
/** Builds the per-request model resolver from the caller's context (BYOK). */
|
|
100
110
|
compileFor: (ctx: A2AContext) => CompileOptions;
|
|
101
111
|
/** Wall-clock budget per task in ms; 0 or undefined = none. */
|
|
@@ -119,6 +129,8 @@ export interface ExecutorOptions {
|
|
|
119
129
|
turnLock?: TurnLock;
|
|
120
130
|
/** How long a second turn on a busy conversation waits, ms. Default 30 s. */
|
|
121
131
|
turnLockWaitMs?: number;
|
|
132
|
+
/** Tool credentials and the consent step for every turn (ADR 0072, ADR 0085). */
|
|
133
|
+
toolCredentials?: ToolCredentials;
|
|
122
134
|
log: (message: string) => void;
|
|
123
135
|
warn: (message: string) => void;
|
|
124
136
|
}
|
|
@@ -188,6 +200,11 @@ export declare function answerStream(eventBus: ExecutionEventBus, taskId: string
|
|
|
188
200
|
export declare class SyndicateExecutor implements AgentExecutor {
|
|
189
201
|
private readonly opts;
|
|
190
202
|
private configHash;
|
|
203
|
+
/** The store as the engine reads it. */
|
|
204
|
+
private readonly sessions;
|
|
205
|
+
/** The faces the turn runner's fixed signature names (ADR 0045 item 4). */
|
|
206
|
+
private readonly turnSessions;
|
|
207
|
+
private readonly turnMemory;
|
|
191
208
|
constructor(opts: ExecutorOptions);
|
|
192
209
|
/** Provenance stamp for every turn this executor serves (lineage.ts). */
|
|
193
210
|
configHashFor(): string;
|