melchizedek-agents 1.0.3 → 1.2.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 +29 -2
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +524 -0
- package/DOCUMENTATION.md +178 -43
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +6 -2
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +7 -7
- package/config/agents/examples/critic.yaml +24 -24
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +37 -35
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +859 -13
- package/config/agents/syndicateSchema.yaml +122 -73
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +64 -62
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +24 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +64 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +43 -5
- package/dist/lib/a2a/oauthSetup.d.ts +106 -0
- package/dist/lib/a2a/oauthSetup.js +259 -0
- package/dist/lib/agentDialect.d.ts +76 -0
- package/dist/lib/agentDialect.js +151 -0
- package/dist/lib/compile.d.ts +42 -6
- package/dist/lib/compile.js +97 -45
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +20 -1
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +110 -1
- package/dist/lib/doctor.js +205 -9
- package/dist/lib/loadSyndicate.d.ts +54 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +25 -1
- package/dist/lib/models/capabilities.js +79 -1
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +2 -1
- package/dist/lib/models/gateway.js +4 -2
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/onboarding.d.ts +134 -0
- package/dist/lib/onboarding.js +539 -0
- package/dist/lib/runtime/approvals.d.ts +8 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
- package/dist/lib/runtime/native/agentLoop.js +107 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
- package/dist/lib/runtime/native/checkpoint.js +359 -0
- package/dist/lib/runtime/native/delegate.d.ts +222 -18
- package/dist/lib/runtime/native/delegate.js +390 -39
- package/dist/lib/runtime/native/interrupts.d.ts +184 -2
- package/dist/lib/runtime/native/interrupts.js +391 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
- package/dist/lib/runtime/syndicateTurn.js +353 -68
- package/dist/lib/runtime/turnControl.d.ts +56 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +12 -2
- package/dist/lib/storage/postgres/index.js +8 -3
- package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
- package/dist/lib/storage/postgres/taskQueue.js +29 -5
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +316 -1
- package/dist/lib/syndicateSchema.js +203 -29
- package/dist/lib/tools/auth.d.ts +20 -6
- package/dist/lib/tools/auth.js +21 -5
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/credentialUses.d.ts +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +73 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +242 -0
- package/dist/lib/tools/oauthTools.js +447 -0
- package/dist/lib/tools/openapiTools.d.ts +11 -6
- package/dist/lib/tools/openapiTools.js +88 -38
- package/dist/lib/tools/taskTools.d.ts +61 -6
- package/dist/lib/tools/taskTools.js +145 -21
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +14 -7
- package/dist/lib/workflow.js +14 -7
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +131 -1
- package/dist/scripts/assistant_worker.d.ts +12 -0
- package/dist/scripts/assistant_worker.js +81 -22
- package/dist/scripts/doctor.d.ts +7 -2
- package/dist/scripts/doctor.js +9 -4
- package/dist/scripts/setup.d.ts +23 -0
- package/dist/scripts/setup.js +154 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/yaml_codemod.d.ts +62 -0
- package/dist/scripts/yaml_codemod.js +652 -0
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +7 -4
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- package/skills/melchizedek-models/SKILL.md +11 -4
- package/skills/melchizedek-onboard/SKILL.md +49 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialHosts.ts — the operator's credential host allowlist:
|
|
3
|
+
* which hosts the value of each static credential variable may be sent to
|
|
4
|
+
* (ADR 0122).
|
|
5
|
+
*
|
|
6
|
+
* WHY this file exists:
|
|
7
|
+
* A syndicate YAML (possibly registry-stored) names a credential variable
|
|
8
|
+
* and, beside it, the host its value goes to: an OpenAPI `auth` sends
|
|
9
|
+
* `bearer_env` or `api_key.env` to the spec's server (or `base_url`), and
|
|
10
|
+
* an `oauth2` block sends `client_secret_env` to its `token_url`.
|
|
11
|
+
* credentialEnvProblem (lib/tools/credentialEnv.ts) keeps the framework's
|
|
12
|
+
* own secrets out of reach, and the OAuth host allowlist (ADR 0114) binds
|
|
13
|
+
* a provider's tokens, but nothing bound a static secret to its API: a
|
|
14
|
+
* YAML that names TRACKER_TOKEN could aim it at a host of its own. The
|
|
15
|
+
* binding is the operator's, never the YAML's: an environment variable or
|
|
16
|
+
* `createA2AApp({ credentialHosts })`.
|
|
17
|
+
*
|
|
18
|
+
* THE RULE (ADR 0122):
|
|
19
|
+
* - With no allowlist configured, nothing changes: a credential goes to
|
|
20
|
+
* the host the YAML names, as before. The doctor and the server binary
|
|
21
|
+
* name every credential variable a served YAML sends, as unbound.
|
|
22
|
+
* - With an allowlist configured, it is the whole list: every credential
|
|
23
|
+
* variable a YAML sends must be on it, and every host its value goes to
|
|
24
|
+
* must be one of that variable's hosts.
|
|
25
|
+
* - The check runs when a syndicate is loaded to be served, when its tools
|
|
26
|
+
* are compiled, and again before each call sends the value.
|
|
27
|
+
*
|
|
28
|
+
* WHAT IT COVERS: `bearer_env`, `api_key.env` and `client_secret_env` (both
|
|
29
|
+
* grants: the client-credentials token request, the consent step's code
|
|
30
|
+
* exchange and its refresh). Not `client_id_env`: a client id is not a
|
|
31
|
+
* secret. Not MCP_BEARER_TOKENS: it maps each token to its host already,
|
|
32
|
+
* and the operator writes both.
|
|
33
|
+
*
|
|
34
|
+
* THE FORMAT: `MELCHIZEDEK_CREDENTIAL_HOSTS="TRACKER_TOKEN=api.tracker.example.com;WEATHER_KEY=api.weather.example.com,*.weather.example.com"`.
|
|
35
|
+
* Entries are separated by `;` (or a newline), a variable from its hosts by
|
|
36
|
+
* `=`, hosts by `,`. A host is what the OAuth host allowlist takes
|
|
37
|
+
* (lib/tools/oauthHosts.ts): a hostname, `*.example.com`, IPv4 or
|
|
38
|
+
* `[IPv6]`, with no scheme, port or path.
|
|
39
|
+
*
|
|
40
|
+
* Nothing here reads or prints a credential's value: variable names and
|
|
41
|
+
* hosts only. No regular expression here can backtrack: the separators are
|
|
42
|
+
* single characters, and a variable name is one bounded character class.
|
|
43
|
+
*/
|
|
44
|
+
/** The environment variable the allowlist is read from when no option sets it. */
|
|
45
|
+
export declare const CREDENTIAL_HOSTS_ENV = "MELCHIZEDEK_CREDENTIAL_HOSTS";
|
|
46
|
+
/** Credential variable name → the hosts its value may be sent to. */
|
|
47
|
+
export type CredentialHostAllowlist = Readonly<Record<string, readonly string[]>>;
|
|
48
|
+
/**
|
|
49
|
+
* The allowlist, checked and normalized (hosts lowercased, duplicates
|
|
50
|
+
* dropped). Throws, naming the variable and the host, on anything malformed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkCredentialHosts(allowlist: Record<string, readonly string[]>, source?: string): Record<string, string[]>;
|
|
53
|
+
/** The allowlist from its text form (MELCHIZEDEK_CREDENTIAL_HOSTS). Throws on a malformed entry. */
|
|
54
|
+
export declare function parseCredentialHosts(text: string): Record<string, string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Sets this process's allowlist (createA2AApp's `credentialHosts` does),
|
|
57
|
+
* which wins over the environment variable. `undefined` returns to the
|
|
58
|
+
* variable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function setCredentialHosts(allowlist: Record<string, readonly string[]> | undefined): void;
|
|
61
|
+
/**
|
|
62
|
+
* The allowlist in force: the one set by `setCredentialHosts`, else the
|
|
63
|
+
* environment variable's, else undefined (not configured). A malformed
|
|
64
|
+
* variable throws, so the first check refuses rather than allowing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function credentialHosts(env?: NodeJS.ProcessEnv): CredentialHostAllowlist | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Why the value of credential variable `name` may not be sent to `url`, or
|
|
69
|
+
* null. `role` names the URL in the message (`server`, `token_url`).
|
|
70
|
+
* `allowlist` defaults to the one in force; `null` means none is
|
|
71
|
+
* configured, which allows (the behaviour before ADR 0122).
|
|
72
|
+
*/
|
|
73
|
+
export declare function credentialHostProblem(name: string, url: string, role?: string, allowlist?: CredentialHostAllowlist | null | undefined): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* The call-time check: why the value of `name` may not be sent to
|
|
76
|
+
* `destination` now, or null. A malformed allowlist refuses.
|
|
77
|
+
*/
|
|
78
|
+
export declare function credentialCallProblem(name: string, destination: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Marks an OAuth client config (lib/tools/oauthConsent.ts) with the
|
|
81
|
+
* variable its client secret came from, so the consent step's code exchange
|
|
82
|
+
* and the refresh hook can check the token endpoint at call time. Set by
|
|
83
|
+
* oauthClientsFor (lib/tools/oauthTools.ts); a client built by hand carries
|
|
84
|
+
* none and is not checked.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CLIENT_SECRET_ENV: unique symbol;
|
|
87
|
+
/** The variable a client config's secret came from, when oauthClientsFor marked it. */
|
|
88
|
+
export declare function clientSecretEnvOf(client: object): string | undefined;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialHosts.ts — the operator's credential host allowlist:
|
|
3
|
+
* which hosts the value of each static credential variable may be sent to
|
|
4
|
+
* (ADR 0122).
|
|
5
|
+
*
|
|
6
|
+
* WHY this file exists:
|
|
7
|
+
* A syndicate YAML (possibly registry-stored) names a credential variable
|
|
8
|
+
* and, beside it, the host its value goes to: an OpenAPI `auth` sends
|
|
9
|
+
* `bearer_env` or `api_key.env` to the spec's server (or `base_url`), and
|
|
10
|
+
* an `oauth2` block sends `client_secret_env` to its `token_url`.
|
|
11
|
+
* credentialEnvProblem (lib/tools/credentialEnv.ts) keeps the framework's
|
|
12
|
+
* own secrets out of reach, and the OAuth host allowlist (ADR 0114) binds
|
|
13
|
+
* a provider's tokens, but nothing bound a static secret to its API: a
|
|
14
|
+
* YAML that names TRACKER_TOKEN could aim it at a host of its own. The
|
|
15
|
+
* binding is the operator's, never the YAML's: an environment variable or
|
|
16
|
+
* `createA2AApp({ credentialHosts })`.
|
|
17
|
+
*
|
|
18
|
+
* THE RULE (ADR 0122):
|
|
19
|
+
* - With no allowlist configured, nothing changes: a credential goes to
|
|
20
|
+
* the host the YAML names, as before. The doctor and the server binary
|
|
21
|
+
* name every credential variable a served YAML sends, as unbound.
|
|
22
|
+
* - With an allowlist configured, it is the whole list: every credential
|
|
23
|
+
* variable a YAML sends must be on it, and every host its value goes to
|
|
24
|
+
* must be one of that variable's hosts.
|
|
25
|
+
* - The check runs when a syndicate is loaded to be served, when its tools
|
|
26
|
+
* are compiled, and again before each call sends the value.
|
|
27
|
+
*
|
|
28
|
+
* WHAT IT COVERS: `bearer_env`, `api_key.env` and `client_secret_env` (both
|
|
29
|
+
* grants: the client-credentials token request, the consent step's code
|
|
30
|
+
* exchange and its refresh). Not `client_id_env`: a client id is not a
|
|
31
|
+
* secret. Not MCP_BEARER_TOKENS: it maps each token to its host already,
|
|
32
|
+
* and the operator writes both.
|
|
33
|
+
*
|
|
34
|
+
* THE FORMAT: `MELCHIZEDEK_CREDENTIAL_HOSTS="TRACKER_TOKEN=api.tracker.example.com;WEATHER_KEY=api.weather.example.com,*.weather.example.com"`.
|
|
35
|
+
* Entries are separated by `;` (or a newline), a variable from its hosts by
|
|
36
|
+
* `=`, hosts by `,`. A host is what the OAuth host allowlist takes
|
|
37
|
+
* (lib/tools/oauthHosts.ts): a hostname, `*.example.com`, IPv4 or
|
|
38
|
+
* `[IPv6]`, with no scheme, port or path.
|
|
39
|
+
*
|
|
40
|
+
* Nothing here reads or prints a credential's value: variable names and
|
|
41
|
+
* hosts only. No regular expression here can backtrack: the separators are
|
|
42
|
+
* single characters, and a variable name is one bounded character class.
|
|
43
|
+
*/
|
|
44
|
+
import { hostAllowed, hostPatternProblem } from './oauthHosts.js';
|
|
45
|
+
/** The environment variable the allowlist is read from when no option sets it. */
|
|
46
|
+
export const CREDENTIAL_HOSTS_ENV = 'MELCHIZEDEK_CREDENTIAL_HOSTS';
|
|
47
|
+
/** A credential variable's name, as a YAML may write it (lib/syndicateSchema.ts), bounded. */
|
|
48
|
+
const ENV_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
49
|
+
/**
|
|
50
|
+
* The allowlist, checked and normalized (hosts lowercased, duplicates
|
|
51
|
+
* dropped). Throws, naming the variable and the host, on anything malformed.
|
|
52
|
+
*/
|
|
53
|
+
export function checkCredentialHosts(allowlist, source = 'credentialHosts') {
|
|
54
|
+
if (!allowlist || typeof allowlist !== 'object' || Array.isArray(allowlist))
|
|
55
|
+
throw new Error(`${source} must map each credential variable to its hosts.`);
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [name, hosts] of Object.entries(allowlist)) {
|
|
58
|
+
if (!ENV_NAME.test(name))
|
|
59
|
+
throw new Error(`${source}: "${name.slice(0, 64)}" is not an environment variable name (A–Z, 0–9, _).`);
|
|
60
|
+
if (!Array.isArray(hosts) || hosts.length === 0)
|
|
61
|
+
throw new Error(`${source}: ${name} needs at least one host.`);
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const raw of hosts) {
|
|
64
|
+
const host = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
65
|
+
const problem = host ? hostPatternProblem(host) : 'is empty';
|
|
66
|
+
if (problem)
|
|
67
|
+
throw new Error(`${source}: ${name}: host "${String(raw).slice(0, 80)}" ${problem}.`);
|
|
68
|
+
seen.add(host);
|
|
69
|
+
}
|
|
70
|
+
out[name] = [...seen];
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** The allowlist from its text form (MELCHIZEDEK_CREDENTIAL_HOSTS). Throws on a malformed entry. */
|
|
75
|
+
export function parseCredentialHosts(text) {
|
|
76
|
+
const parsed = {};
|
|
77
|
+
for (const entry of text.split(/[;\n]/)) {
|
|
78
|
+
const trimmed = entry.trim();
|
|
79
|
+
if (!trimmed)
|
|
80
|
+
continue;
|
|
81
|
+
const eq = trimmed.indexOf('=');
|
|
82
|
+
if (eq <= 0)
|
|
83
|
+
throw new Error(`${CREDENTIAL_HOSTS_ENV}: each entry is VARIABLE=host,host (got "${trimmed.slice(0, 80)}").`);
|
|
84
|
+
const name = trimmed.slice(0, eq).trim();
|
|
85
|
+
if (Object.hasOwn(parsed, name))
|
|
86
|
+
throw new Error(`${CREDENTIAL_HOSTS_ENV}: ${name.slice(0, 64)} is listed twice.`);
|
|
87
|
+
parsed[name] = trimmed.slice(eq + 1).split(',').map((h) => h.trim()).filter((h) => h !== '');
|
|
88
|
+
}
|
|
89
|
+
return checkCredentialHosts(parsed, CREDENTIAL_HOSTS_ENV);
|
|
90
|
+
}
|
|
91
|
+
let configured;
|
|
92
|
+
let envCache;
|
|
93
|
+
/**
|
|
94
|
+
* Sets this process's allowlist (createA2AApp's `credentialHosts` does),
|
|
95
|
+
* which wins over the environment variable. `undefined` returns to the
|
|
96
|
+
* variable.
|
|
97
|
+
*/
|
|
98
|
+
export function setCredentialHosts(allowlist) {
|
|
99
|
+
configured = allowlist === undefined ? undefined : checkCredentialHosts(allowlist);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The allowlist in force: the one set by `setCredentialHosts`, else the
|
|
103
|
+
* environment variable's, else undefined (not configured). A malformed
|
|
104
|
+
* variable throws, so the first check refuses rather than allowing.
|
|
105
|
+
*/
|
|
106
|
+
export function credentialHosts(env = process.env) {
|
|
107
|
+
if (configured)
|
|
108
|
+
return configured;
|
|
109
|
+
const raw = env[CREDENTIAL_HOSTS_ENV];
|
|
110
|
+
if (raw === undefined || raw.trim() === '')
|
|
111
|
+
return undefined;
|
|
112
|
+
if (envCache?.raw !== raw)
|
|
113
|
+
envCache = { raw, value: parseCredentialHosts(raw) };
|
|
114
|
+
return envCache.value;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Why the value of credential variable `name` may not be sent to `url`, or
|
|
118
|
+
* null. `role` names the URL in the message (`server`, `token_url`).
|
|
119
|
+
* `allowlist` defaults to the one in force; `null` means none is
|
|
120
|
+
* configured, which allows (the behaviour before ADR 0122).
|
|
121
|
+
*/
|
|
122
|
+
export function credentialHostProblem(name, url, role = 'server', allowlist = credentialHosts()) {
|
|
123
|
+
const list = allowlist ?? undefined;
|
|
124
|
+
if (!list)
|
|
125
|
+
return null;
|
|
126
|
+
if (!Object.hasOwn(list, name)) {
|
|
127
|
+
return `${name} is not on the operator's credential host allowlist (${CREDENTIAL_HOSTS_ENV} or createA2AApp's credentialHosts), so it is sent nowhere`;
|
|
128
|
+
}
|
|
129
|
+
let hostname;
|
|
130
|
+
try {
|
|
131
|
+
hostname = new URL(url).hostname;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return `${role} '${url.slice(0, 200)}' is not a URL`;
|
|
135
|
+
}
|
|
136
|
+
const hosts = list[name];
|
|
137
|
+
if (hostAllowed(hosts, hostname))
|
|
138
|
+
return null;
|
|
139
|
+
return `${name} may be sent only to ${hosts.join(', ')}; ${hostname} (${role}) is not one of them`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The call-time check: why the value of `name` may not be sent to
|
|
143
|
+
* `destination` now, or null. A malformed allowlist refuses.
|
|
144
|
+
*/
|
|
145
|
+
export function credentialCallProblem(name, destination) {
|
|
146
|
+
try {
|
|
147
|
+
return credentialHostProblem(name, destination, 'server', credentialHosts() ?? null);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return `the credential host allowlist (${CREDENTIAL_HOSTS_ENV}) is malformed`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Marks an OAuth client config (lib/tools/oauthConsent.ts) with the
|
|
155
|
+
* variable its client secret came from, so the consent step's code exchange
|
|
156
|
+
* and the refresh hook can check the token endpoint at call time. Set by
|
|
157
|
+
* oauthClientsFor (lib/tools/oauthTools.ts); a client built by hand carries
|
|
158
|
+
* none and is not checked.
|
|
159
|
+
*/
|
|
160
|
+
export const CLIENT_SECRET_ENV = Symbol.for('melchizedek.clientSecretEnv');
|
|
161
|
+
/** The variable a client config's secret came from, when oauthClientsFor marked it. */
|
|
162
|
+
export function clientSecretEnvOf(client) {
|
|
163
|
+
const name = client[CLIENT_SECRET_ENV];
|
|
164
|
+
return typeof name === 'string' ? name : undefined;
|
|
165
|
+
}
|
|
@@ -24,10 +24,17 @@
|
|
|
24
24
|
* row: a provider's own error is reported by kind, not by message,
|
|
25
25
|
* since a provider may echo the token it refused.
|
|
26
26
|
*/
|
|
27
|
-
import { SpanStatusCode, trace } from '@opentelemetry/api';
|
|
27
|
+
import { INVALID_SPAN_CONTEXT, SpanStatusCode, trace } from '@opentelemetry/api';
|
|
28
|
+
import { turnUntraced } from '../runtime/turnControl.js';
|
|
28
29
|
import { scopeHashOf } from '../observability/audit.js';
|
|
29
30
|
import { PROVIDER_NAME, ToolCredentialError } from './auth.js';
|
|
30
31
|
const tracer = () => trace.getTracer('melchizedek.credentials');
|
|
32
|
+
/** `credential.refresh` as an active span; in a turn that opted out of tracing, a non-recording one. */
|
|
33
|
+
function withRefreshSpan(provider, fn) {
|
|
34
|
+
if (turnUntraced())
|
|
35
|
+
return fn(trace.wrapSpanContext(INVALID_SPAN_CONTEXT));
|
|
36
|
+
return tracer().startActiveSpan('credential.refresh', { attributes: { 'credential.provider': provider } }, fn);
|
|
37
|
+
}
|
|
31
38
|
/** The AAD a field is sealed under: its app, user, provider and field, NUL-separated. */
|
|
32
39
|
export function credentialContext(key, field) {
|
|
33
40
|
return `${key.appName}\0${key.userId}\0${key.provider}\0${field}`;
|
|
@@ -107,7 +114,7 @@ export function credentialStore(options) {
|
|
|
107
114
|
throw new ToolCredentialError('expired', key.provider);
|
|
108
115
|
}
|
|
109
116
|
const refreshToken = await open(row.refreshTokenEnc, key, 'refresh');
|
|
110
|
-
return
|
|
117
|
+
return withRefreshSpan(key.provider, async (span) => {
|
|
111
118
|
try {
|
|
112
119
|
let issued;
|
|
113
120
|
try {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialUses.ts — every static credential variable a syndicate
|
|
3
|
+
* YAML sends somewhere, and where (ADR 0122).
|
|
4
|
+
*
|
|
5
|
+
* A YAML sends a static credential in three places: an OpenAPI entry's
|
|
6
|
+
* `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
|
|
7
|
+
* `base_url`), and an `oauth2` block's `client_secret_env` (to its
|
|
8
|
+
* `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
|
|
9
|
+
* operator's credential host allowlist (lib/tools/credentialHosts.ts) is
|
|
10
|
+
* checked against them when a served syndicate loads
|
|
11
|
+
* (`syndicateCredentialHostProblems`), and the doctor and the server binary
|
|
12
|
+
* name each variable no allowlist binds (`unboundCredentialEnvs`).
|
|
13
|
+
*
|
|
14
|
+
* Names and hosts only: no value is read here.
|
|
15
|
+
*/
|
|
16
|
+
import type { CredentialHostAllowlist } from './credentialHosts.ts';
|
|
17
|
+
/** One credential variable a YAML sends, and to what. */
|
|
18
|
+
export interface CredentialUse {
|
|
19
|
+
/** The agent that holds the tools ("Parent › Child" inside a nested file). */
|
|
20
|
+
agent: string;
|
|
21
|
+
/** `openapi <spec>` or `mcp <url>`. */
|
|
22
|
+
tools: string;
|
|
23
|
+
/** The variable's name. */
|
|
24
|
+
env: string;
|
|
25
|
+
/** Where the YAML names it. */
|
|
26
|
+
role: 'bearer_env' | 'api_key' | 'client_secret_env';
|
|
27
|
+
/**
|
|
28
|
+
* The URLs its value goes to: the token endpoint for a client secret,
|
|
29
|
+
* `base_url` for an OpenAPI entry that sets one. Absent for an OpenAPI
|
|
30
|
+
* entry whose server is in its spec (read only when checked, by
|
|
31
|
+
* `syndicateCredentialHostProblems`).
|
|
32
|
+
*/
|
|
33
|
+
destinations?: string[];
|
|
34
|
+
/** The OpenAPI entry, for its spec's servers. */
|
|
35
|
+
openapi?: OpenApiEntryLike;
|
|
36
|
+
}
|
|
37
|
+
/** The parts of an `oauth2` block read here. */
|
|
38
|
+
interface OAuth2Like {
|
|
39
|
+
token_url: string;
|
|
40
|
+
client_secret_env?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The parts of an `openapi:` entry read here. */
|
|
43
|
+
interface OpenApiEntryLike {
|
|
44
|
+
spec?: string;
|
|
45
|
+
base_url?: string;
|
|
46
|
+
auth?: {
|
|
47
|
+
bearer_env?: string;
|
|
48
|
+
api_key?: {
|
|
49
|
+
env?: string;
|
|
50
|
+
};
|
|
51
|
+
oauth2?: OAuth2Like;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
interface AgentLike {
|
|
55
|
+
name?: string;
|
|
56
|
+
openapi?: OpenApiEntryLike[];
|
|
57
|
+
mcp_server_url?: string;
|
|
58
|
+
mcp_auth?: {
|
|
59
|
+
oauth2?: OAuth2Like;
|
|
60
|
+
};
|
|
61
|
+
yaml_reference?: string;
|
|
62
|
+
}
|
|
63
|
+
/** The parts of a syndicate config read here. */
|
|
64
|
+
export interface SyndicateLike {
|
|
65
|
+
orchestrator?: AgentLike;
|
|
66
|
+
subagents?: AgentLike[];
|
|
67
|
+
}
|
|
68
|
+
/** The credential variables one agent's tools send. */
|
|
69
|
+
export declare function agentCredentialUses(agent: AgentLike, prefix?: string): CredentialUse[];
|
|
70
|
+
/**
|
|
71
|
+
* Every credential variable these syndicates send, orchestrators and
|
|
72
|
+
* subagents, following a nested `yaml_reference:` through `load` when given
|
|
73
|
+
* (once per file; agent names then read "Parent › Child").
|
|
74
|
+
*/
|
|
75
|
+
export declare function syndicateCredentialUses(configs: readonly SyndicateLike[], load?: (ref: string) => SyndicateLike): CredentialUse[];
|
|
76
|
+
/**
|
|
77
|
+
* Why these syndicates may not be served here, one line per refusal
|
|
78
|
+
* (ADR 0122): each credential variable they send, against the operator's
|
|
79
|
+
* credential host allowlist. With none configured, nothing is refused. The
|
|
80
|
+
* A2A server checks every syndicate it loads to serve; the compile checks
|
|
81
|
+
* again, and each call once more.
|
|
82
|
+
*/
|
|
83
|
+
export declare function syndicateCredentialHostProblems(configs: readonly SyndicateLike[], options?: {
|
|
84
|
+
load?: (ref: string) => SyndicateLike;
|
|
85
|
+
allowlist?: CredentialHostAllowlist | null;
|
|
86
|
+
}): string[];
|
|
87
|
+
/**
|
|
88
|
+
* The credential variables these uses send that no allowlist binds, sorted
|
|
89
|
+
* and once each: every one of them when none is configured. Names only.
|
|
90
|
+
*/
|
|
91
|
+
export declare function unboundCredentialEnvs(uses: readonly CredentialUse[], allowlist: CredentialHostAllowlist | null | undefined): string[];
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialUses.ts — every static credential variable a syndicate
|
|
3
|
+
* YAML sends somewhere, and where (ADR 0122).
|
|
4
|
+
*
|
|
5
|
+
* A YAML sends a static credential in three places: an OpenAPI entry's
|
|
6
|
+
* `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
|
|
7
|
+
* `base_url`), and an `oauth2` block's `client_secret_env` (to its
|
|
8
|
+
* `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
|
|
9
|
+
* operator's credential host allowlist (lib/tools/credentialHosts.ts) is
|
|
10
|
+
* checked against them when a served syndicate loads
|
|
11
|
+
* (`syndicateCredentialHostProblems`), and the doctor and the server binary
|
|
12
|
+
* name each variable no allowlist binds (`unboundCredentialEnvs`).
|
|
13
|
+
*
|
|
14
|
+
* Names and hosts only: no value is read here.
|
|
15
|
+
*/
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { credentialHostProblem, credentialHosts } from './credentialHosts.js';
|
|
18
|
+
import { openApiServers } from './openapiTools.js';
|
|
19
|
+
/** The credential variables one agent's tools send. */
|
|
20
|
+
export function agentCredentialUses(agent, prefix = '') {
|
|
21
|
+
const name = `${prefix}${agent.name ?? ''}`;
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const entry of agent.openapi ?? []) {
|
|
24
|
+
const tools = `openapi ${entry?.spec ?? ''}`;
|
|
25
|
+
const auth = entry?.auth;
|
|
26
|
+
if (!auth)
|
|
27
|
+
continue;
|
|
28
|
+
const servers = entry.base_url ? { destinations: [entry.base_url] } : {};
|
|
29
|
+
if (auth.bearer_env)
|
|
30
|
+
out.push({ agent: name, tools, env: auth.bearer_env, role: 'bearer_env', ...servers, openapi: entry });
|
|
31
|
+
if (auth.api_key?.env)
|
|
32
|
+
out.push({ agent: name, tools, env: auth.api_key.env, role: 'api_key', ...servers, openapi: entry });
|
|
33
|
+
if (auth.oauth2?.client_secret_env)
|
|
34
|
+
out.push({ agent: name, tools, env: auth.oauth2.client_secret_env, role: 'client_secret_env', destinations: [auth.oauth2.token_url] });
|
|
35
|
+
}
|
|
36
|
+
const mcp = agent.mcp_auth?.oauth2;
|
|
37
|
+
if (mcp?.client_secret_env)
|
|
38
|
+
out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, env: mcp.client_secret_env, role: 'client_secret_env', destinations: [mcp.token_url] });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every credential variable these syndicates send, orchestrators and
|
|
43
|
+
* subagents, following a nested `yaml_reference:` through `load` when given
|
|
44
|
+
* (once per file; agent names then read "Parent › Child").
|
|
45
|
+
*/
|
|
46
|
+
export function syndicateCredentialUses(configs, load) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const visit = (config, prefix) => {
|
|
50
|
+
const agents = [config.orchestrator, ...(config.subagents ?? [])].filter((a) => !!a);
|
|
51
|
+
for (const agent of agents) {
|
|
52
|
+
if (agent.yaml_reference) {
|
|
53
|
+
if (!load || seen.has(agent.yaml_reference))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(agent.yaml_reference);
|
|
56
|
+
visit(load(agent.yaml_reference), `${prefix}${agent.name ?? agent.yaml_reference} › `);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
out.push(...agentCredentialUses(agent, prefix));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
for (const config of configs)
|
|
63
|
+
visit(config, '');
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** The URLs a use's value goes to; an OpenAPI spec that cannot be read gives none (its compile fails on its own). */
|
|
67
|
+
function destinationsOf(use) {
|
|
68
|
+
if (use.destinations)
|
|
69
|
+
return use.destinations;
|
|
70
|
+
if (!use.openapi?.spec)
|
|
71
|
+
return [];
|
|
72
|
+
try {
|
|
73
|
+
return openApiServers(use.openapi, resolve(process.cwd()));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Why these syndicates may not be served here, one line per refusal
|
|
81
|
+
* (ADR 0122): each credential variable they send, against the operator's
|
|
82
|
+
* credential host allowlist. With none configured, nothing is refused. The
|
|
83
|
+
* A2A server checks every syndicate it loads to serve; the compile checks
|
|
84
|
+
* again, and each call once more.
|
|
85
|
+
*/
|
|
86
|
+
export function syndicateCredentialHostProblems(configs, options = {}) {
|
|
87
|
+
const allowlist = options.allowlist === undefined ? credentialHosts() ?? null : options.allowlist;
|
|
88
|
+
if (!allowlist)
|
|
89
|
+
return [];
|
|
90
|
+
const out = new Set();
|
|
91
|
+
for (const use of syndicateCredentialUses(configs, options.load)) {
|
|
92
|
+
const role = use.role === 'client_secret_env' ? 'token_url' : 'server';
|
|
93
|
+
const where = `${use.agent} · ${use.tools} · ${use.role}`;
|
|
94
|
+
if (!Object.hasOwn(allowlist, use.env)) {
|
|
95
|
+
out.add(`${where}: ${credentialHostProblem(use.env, '', role, allowlist)}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const url of destinationsOf(use)) {
|
|
99
|
+
const problem = credentialHostProblem(use.env, url, role, allowlist);
|
|
100
|
+
if (problem)
|
|
101
|
+
out.add(`${where}: ${problem}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...out];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The credential variables these uses send that no allowlist binds, sorted
|
|
108
|
+
* and once each: every one of them when none is configured. Names only.
|
|
109
|
+
*/
|
|
110
|
+
export function unboundCredentialEnvs(uses, allowlist) {
|
|
111
|
+
return [...new Set(uses.map((u) => u.env))].filter((name) => !allowlist || !Object.hasOwn(allowlist, name)).sort();
|
|
112
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDeclaration } from '../models/contract.ts';
|
|
2
2
|
import type { RedirectPolicy } from '../net/redirects.ts';
|
|
3
|
+
import type { OAuth2AuthConfig } from './oauthTools.ts';
|
|
3
4
|
import type { Tool } from './tool.ts';
|
|
4
5
|
export declare function assertSafeMcpUrl(raw: string, env?: NodeJS.ProcessEnv): Promise<URL>;
|
|
5
6
|
export declare function mcpAuthHeaders(url: URL, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -41,11 +42,33 @@ export declare function mcpToolParameters(inputSchema: {
|
|
|
41
42
|
properties?: Record<string, unknown>;
|
|
42
43
|
required?: string[];
|
|
43
44
|
} | undefined): ToolDeclaration['parameters'];
|
|
45
|
+
/** How an agent reaches its MCP server: which tools it may use, and the OAuth grant the server takes. */
|
|
46
|
+
export interface McpToolOptions {
|
|
47
|
+
/** `mcp_tools`: the names this agent may use. Required with an authorization_code grant. */
|
|
48
|
+
tools?: string[];
|
|
49
|
+
/** `mcp_auth.oauth2` (ADR 0112). Without it, MCP_BEARER_TOKENS applies. */
|
|
50
|
+
oauth2?: OAuth2AuthConfig;
|
|
51
|
+
}
|
|
52
|
+
/** Per-user connections at most per authorization-code server; the least recently used is closed first. */
|
|
53
|
+
export declare const MAX_MCP_USER_CONNECTIONS = 64;
|
|
44
54
|
/**
|
|
45
55
|
* The tools a remote MCP server offers, as own Tools, or none when the
|
|
46
56
|
* server cannot be reached or refuses (logged, never thrown). The
|
|
47
57
|
* connection stays open for the tools' calls; closeMcpConnections ends it.
|
|
58
|
+
*
|
|
59
|
+
* With `oauth2` (ADR 0112):
|
|
60
|
+
* - client_credentials: one connection, every request carrying the
|
|
61
|
+
* server's own token for the provider (lib/tools/oauthTools.ts).
|
|
62
|
+
* - authorization_code: the token is each user's, so there is no
|
|
63
|
+
* connection at compile time. Each name in `tools` becomes a Tool whose
|
|
64
|
+
* call reads the run's user's token (`ctx.accessToken`, which asks for
|
|
65
|
+
* consent when the user has not granted it) and runs on that user's own
|
|
66
|
+
* connection. The parameters are the server's, learned from the first
|
|
67
|
+
* listing any user's connection makes; until then a call connects and
|
|
68
|
+
* asks the model to call again with them, rather than call blind.
|
|
69
|
+
* A missing variable or a refused URL fails the compile (thrown), naming
|
|
70
|
+
* the variable, never a value.
|
|
48
71
|
*/
|
|
49
|
-
export declare function loadMcpTools(mcpServerUrl: string): Promise<Tool[]>;
|
|
72
|
+
export declare function loadMcpTools(mcpServerUrl: string, options?: McpToolOptions): Promise<Tool[]>;
|
|
50
73
|
/** The server's tools, as the compiler lists them on an agent: loadMcpTools' own Tools. */
|
|
51
|
-
export declare function createMcpTools(mcpServerUrl: string): Promise<Tool[]>;
|
|
74
|
+
export declare function createMcpTools(mcpServerUrl: string, options?: McpToolOptions): Promise<Tool[]>;
|