melchizedek-agents 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.env.example +19 -2
  2. package/CHANGELOG.md +333 -0
  3. package/DOCUMENTATION.md +99 -39
  4. package/README.md +1 -0
  5. package/config/agents/README.md +2 -2
  6. package/config/agents/examples/ares.yaml +6 -10
  7. package/config/agents/examples/assistant.yaml +6 -6
  8. package/config/agents/examples/augustin.yaml +9 -15
  9. package/config/agents/examples/cartographers.yaml +6 -6
  10. package/config/agents/examples/council.yaml +6 -6
  11. package/config/agents/examples/critic.yaml +16 -16
  12. package/config/agents/examples/harness.yaml +4 -4
  13. package/config/agents/examples/image_production.yaml +33 -32
  14. package/config/agents/examples/librarian.yaml +4 -4
  15. package/config/agents/examples/patient_advocate.yaml +6 -10
  16. package/config/agents/examples/pipeline.yaml +20 -19
  17. package/config/agents/examples/research.yaml +36 -43
  18. package/config/agents/examples/scribe.yaml +31 -32
  19. package/config/agents/examples/scriptorium.yaml +6 -6
  20. package/config/agents/examples/structured_critic.yaml +82 -0
  21. package/config/agents/examples/style_council.yaml +9 -11
  22. package/config/agents/examples/syndicate.yaml +3 -5
  23. package/config/agents/examples/tutor.yaml +2 -2
  24. package/config/agents/examples/weather.yaml +2 -2
  25. package/config/agents/syndicate.schema.json +1979 -0
  26. package/config/agents/syndicateSchema.yaml +109 -72
  27. package/config/agents/templates/README.md +1 -1
  28. package/config/agents/templates/account_memory.yaml +4 -4
  29. package/config/agents/templates/case_desk.yaml +6 -6
  30. package/config/agents/templates/conversational.yaml +2 -2
  31. package/config/agents/templates/draft_review.yaml +6 -6
  32. package/config/agents/templates/intake_extractor.yaml +61 -60
  33. package/config/agents/templates/research_brief.yaml +6 -6
  34. package/config/agents/templates/research_desk.yaml +6 -6
  35. package/config/agents/templates/review_panel.yaml +8 -8
  36. package/config/agents/templates/support_triage.yaml +30 -29
  37. package/config/agents/templates/systems_operator.yaml +21 -5
  38. package/db/migrations/0014_durable_runs.sql +31 -0
  39. package/dist/lib/a2a/app.d.ts +15 -0
  40. package/dist/lib/a2a/app.js +53 -5
  41. package/dist/lib/a2a/executor.d.ts +7 -1
  42. package/dist/lib/a2a/executor.js +34 -4
  43. package/dist/lib/a2a/oauthSetup.d.ts +90 -0
  44. package/dist/lib/a2a/oauthSetup.js +205 -0
  45. package/dist/lib/agentDialect.d.ts +66 -0
  46. package/dist/lib/agentDialect.js +134 -0
  47. package/dist/lib/compile.d.ts +10 -5
  48. package/dist/lib/compile.js +69 -37
  49. package/dist/lib/compileNative.js +2 -0
  50. package/dist/lib/dispatch.d.ts +10 -5
  51. package/dist/lib/dispatch.js +10 -5
  52. package/dist/lib/doctor.d.ts +34 -1
  53. package/dist/lib/doctor.js +47 -3
  54. package/dist/lib/loadSyndicate.d.ts +45 -2
  55. package/dist/lib/loadSyndicate.js +22 -2
  56. package/dist/lib/memory/namespace.d.ts +6 -5
  57. package/dist/lib/memory/namespace.js +6 -5
  58. package/dist/lib/memory/providers.js +3 -4
  59. package/dist/lib/models/capabilities.d.ts +16 -1
  60. package/dist/lib/models/capabilities.js +55 -1
  61. package/dist/lib/observability/tracer.d.ts +8 -1
  62. package/dist/lib/observability/tracer.js +33 -5
  63. package/dist/lib/runtime/approvals.d.ts +6 -0
  64. package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
  65. package/dist/lib/runtime/native/agentLoop.js +105 -11
  66. package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
  67. package/dist/lib/runtime/native/checkpoint.js +365 -0
  68. package/dist/lib/runtime/native/delegate.d.ts +141 -18
  69. package/dist/lib/runtime/native/delegate.js +235 -39
  70. package/dist/lib/runtime/native/interrupts.d.ts +101 -2
  71. package/dist/lib/runtime/native/interrupts.js +201 -1
  72. package/dist/lib/runtime/native/request.d.ts +13 -3
  73. package/dist/lib/runtime/native/request.js +28 -4
  74. package/dist/lib/runtime/native/telemetry.js +10 -15
  75. package/dist/lib/runtime/questions.d.ts +7 -4
  76. package/dist/lib/runtime/sessions.d.ts +1 -1
  77. package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
  78. package/dist/lib/runtime/syndicateTurn.js +74 -22
  79. package/dist/lib/runtime/turnControl.d.ts +15 -0
  80. package/dist/lib/runtime/turnControl.js +10 -0
  81. package/dist/lib/storage/postgres/index.d.ts +6 -1
  82. package/dist/lib/storage/postgres/index.js +7 -2
  83. package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
  84. package/dist/lib/storage/postgres/taskQueue.js +20 -3
  85. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  86. package/dist/lib/storage/postgres/taskStore.js +74 -8
  87. package/dist/lib/syndicateSchema.d.ts +278 -0
  88. package/dist/lib/syndicateSchema.js +182 -15
  89. package/dist/lib/tools/auth.d.ts +7 -5
  90. package/dist/lib/tools/auth.js +12 -4
  91. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  92. package/dist/lib/tools/credentialEnv.js +52 -0
  93. package/dist/lib/tools/credentialStore.js +9 -2
  94. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  95. package/dist/lib/tools/mcpToolFactory.js +179 -21
  96. package/dist/lib/tools/oauthHosts.d.ts +71 -0
  97. package/dist/lib/tools/oauthHosts.js +154 -0
  98. package/dist/lib/tools/oauthTools.d.ts +233 -0
  99. package/dist/lib/tools/oauthTools.js +421 -0
  100. package/dist/lib/tools/openapiTools.d.ts +4 -6
  101. package/dist/lib/tools/openapiTools.js +40 -36
  102. package/dist/lib/tools/taskTools.d.ts +30 -5
  103. package/dist/lib/tools/taskTools.js +74 -10
  104. package/dist/lib/workflow.d.ts +8 -4
  105. package/dist/lib/workflow.js +8 -4
  106. package/dist/lib/workflowConfig.d.ts +6 -0
  107. package/dist/scripts/a2a_server.d.ts +36 -0
  108. package/dist/scripts/a2a_server.js +122 -1
  109. package/dist/scripts/assistant_worker.d.ts +10 -0
  110. package/dist/scripts/assistant_worker.js +69 -19
  111. package/dist/scripts/doctor.d.ts +4 -1
  112. package/dist/scripts/doctor.js +5 -2
  113. package/dist/scripts/yaml_codemod.d.ts +52 -0
  114. package/dist/scripts/yaml_codemod.js +570 -0
  115. package/package.json +11 -7
  116. package/skills/briefs/melchizedek-author.md +7 -5
  117. package/skills/briefs/melchizedek-models.md +4 -3
  118. package/skills/briefs/melchizedek.md +1 -0
  119. package/skills/melchizedek/SKILL.md +1 -0
  120. package/skills/melchizedek-author/SKILL.md +7 -4
  121. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  122. package/skills/melchizedek-models/SKILL.md +6 -3
@@ -0,0 +1,233 @@
1
+ /**
2
+ * lib/tools/oauthTools.ts — `auth: { oauth2 }` from a syndicate YAML: an
3
+ * OpenAPI entry or an MCP server whose calls carry an OAuth access token
4
+ * (WS6-3c, ADR 0112).
5
+ *
6
+ * WHY this file exists:
7
+ * The credential store (ADR 0072) holds each user's tokens and the consent
8
+ * step (ADR 0085) puts them there, but nothing said which tools send which
9
+ * provider's token. The YAML says it, next to the tool it protects:
10
+ *
11
+ * openapi:
12
+ * - spec: specs/tracker.yaml
13
+ * auth:
14
+ * oauth2:
15
+ * provider: tracker
16
+ * grant: authorization_code
17
+ * authorization_url: https://tracker.example.com/oauth/authorize
18
+ * token_url: https://tracker.example.com/oauth/token
19
+ * client_id_env: TRACKER_CLIENT_ID
20
+ * client_secret_env: TRACKER_CLIENT_SECRET
21
+ * scopes: [issues:read]
22
+ *
23
+ * and `mcp_auth: { oauth2: { ... } }` beside an agent's `mcp_server_url`.
24
+ *
25
+ * THE TWO GRANTS:
26
+ * - authorization_code: the token is the run's own user's, read through
27
+ * `ctx.accessToken(provider)` at each call. A user who has not granted it
28
+ * is asked (the consent pause, when the run has a consent step whose
29
+ * clients include the provider: `oauthClientsFor` builds them from the
30
+ * same YAML, and `oauthRefreshProviders` the store's refresh hooks).
31
+ * The tool never chooses whose token.
32
+ * - client_credentials: the server's own token for the provider, from its
33
+ * token endpoint with the client id and secret, held in this process's
34
+ * memory until shortly before it expires, never in the credential store
35
+ * (no user owns it).
36
+ *
37
+ * SECRETS: the YAML names environment variables, never values; the variable
38
+ * names pass credentialEnvProblem (lib/tools/credentialEnv.ts), so a YAML
39
+ * cannot send a framework secret to a token endpoint. No token, client
40
+ * secret or provider error text reaches a result, an error, a log line or
41
+ * a span: a failure is reported by kind (ToolCredentialError).
42
+ *
43
+ * WHERE A TOKEN MAY GO: a token is sent only over https, or http to a
44
+ * loopback host (local development), to the server the YAML names, and
45
+ * only to a host the operator's allowlist binds to the provider
46
+ * (lib/tools/oauthHosts.ts, ADR 0114): with none configured, an
47
+ * authorization_code grant is refused. The allowlist is checked when a
48
+ * served syndicate loads (`syndicateOAuthHostProblems`), when the tools
49
+ * compile (the server, the token endpoint and the authorization
50
+ * endpoint), and again before each call sends a token
51
+ * (`oauthCallProblem`). The client-credentials token endpoint passes the
52
+ * same SSRF guard as the server it serves (ALLOW_PRIVATE_OPENAPI or
53
+ * ALLOW_PRIVATE_MCP), and no redirect from it is followed.
54
+ */
55
+ import type { OAuthProvider } from './auth.ts';
56
+ import type { OAuthHostAllowlist } from './oauthHosts.ts';
57
+ import type { OAuthClientConfig } from './oauthConsent.ts';
58
+ import type { ToolContext } from './tool.ts';
59
+ /** The `oauth2` block of an `auth` (OpenAPI) or an `mcp_auth` (MCP). */
60
+ export interface OAuth2AuthConfig {
61
+ /** The credential store's name for the provider (lowercase). */
62
+ provider: string;
63
+ grant: 'authorization_code' | 'client_credentials';
64
+ /** The provider's authorization endpoint (authorization_code only). */
65
+ authorization_url?: string;
66
+ /** The provider's token endpoint. */
67
+ token_url: string;
68
+ /** The client id, written out (it is not a secret) … */
69
+ client_id?: string;
70
+ /** … or the environment variable holding it. */
71
+ client_id_env?: string;
72
+ /** The environment variable holding the client secret. Required for client_credentials. */
73
+ client_secret_env?: string;
74
+ scopes?: string[];
75
+ /** Extra authorization parameters (authorization_code only), e.g. `access_type: offline`. */
76
+ authorization_params?: Record<string, string>;
77
+ }
78
+ /** An agent's `mcp_auth:` beside its `mcp_server_url`. */
79
+ export interface McpAuthConfig {
80
+ oauth2: OAuth2AuthConfig;
81
+ }
82
+ /** Where a declared grant sits: for the doctor, and for the error messages. */
83
+ export interface OAuthGrantUse {
84
+ /** The agent that holds the tools. */
85
+ agent: string;
86
+ /** `openapi <spec>` or `mcp <url>`. */
87
+ tools: string;
88
+ oauth2: OAuth2AuthConfig;
89
+ }
90
+ /** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server. */
91
+ export declare function agentOAuthGrants(agent: {
92
+ name?: string;
93
+ openapi?: Array<{
94
+ spec?: string;
95
+ auth?: {
96
+ oauth2?: OAuth2AuthConfig;
97
+ };
98
+ }>;
99
+ mcp_server_url?: string;
100
+ mcp_auth?: {
101
+ oauth2?: OAuth2AuthConfig;
102
+ };
103
+ }, prefix?: string): OAuthGrantUse[];
104
+ /** The environment variables a grant reads, by role. */
105
+ export declare function oauthEnvNames(oauth2: OAuth2AuthConfig): string[];
106
+ /** Why a token may not be sent to `raw`, or null: https, or http to a loopback host. */
107
+ export declare function tokenTransportProblem(raw: string): string | null;
108
+ /**
109
+ * Why this grant may not be declared for these servers, one line per
110
+ * refusal: each server the tools call, the token endpoint and the
111
+ * authorization endpoint must be hosts the operator's allowlist binds to the
112
+ * provider (lib/tools/oauthHosts.ts). With no allowlist, an
113
+ * authorization_code grant is refused and a client_credentials one allowed.
114
+ * `allowlist` defaults to the one in force; `null` means none is configured.
115
+ */
116
+ export declare function oauthGrantHostProblems(oauth2: OAuth2AuthConfig, servers: readonly string[], allowlist?: OAuthHostAllowlist | null): string[];
117
+ /**
118
+ * Why a call may not send this grant's token (or secret) to `destination`
119
+ * now, or null: the transport rule and the allowlist in force, checked again
120
+ * at each call. The compile checked the same; this holds if a request is
121
+ * aimed elsewhere, or the allowlist changed. A malformed allowlist refuses.
122
+ */
123
+ export declare function oauthCallProblem(oauth2: Pick<OAuth2AuthConfig, 'provider' | 'grant'>, destination: string): string | null;
124
+ export interface ClientCredentialsOptions {
125
+ provider: string;
126
+ tokenUrl: string;
127
+ clientId: string;
128
+ clientSecret: string;
129
+ scopes?: string[];
130
+ /** Private and loopback token endpoints are allowed (ALLOW_PRIVATE_OPENAPI / ALLOW_PRIVATE_MCP). */
131
+ allowPrivate?: boolean;
132
+ /** A token this close to its expiry is fetched again. Default 60 s. */
133
+ refreshSkewMs?: number;
134
+ /** The token request's time limit, ms. Default 10 s. */
135
+ timeoutMs?: number;
136
+ /** For tests. */
137
+ fetch?: typeof fetch;
138
+ now?: () => number;
139
+ }
140
+ /**
141
+ * The server's own token for a provider (RFC 6749 4.4), fetched when first
142
+ * needed and again shortly before it expires, one request at a time. A
143
+ * failure throws ToolCredentialError('grant_failed'), never the provider's
144
+ * text, which may echo what it refused.
145
+ */
146
+ export declare function clientCredentialsGrant(options: ClientCredentialsOptions): {
147
+ token(signal?: AbortSignal): Promise<string>;
148
+ };
149
+ /**
150
+ * What a tool calls for the token of one call, naming where that call goes.
151
+ * Throws ToolCredentialError, whose message names no value: `host_refused`
152
+ * when the destination is not one of the provider's hosts.
153
+ */
154
+ export type OAuthTokenSource = (ctx: ToolContext | undefined, destination: string) => Promise<string>;
155
+ /**
156
+ * The token source a declared `oauth2` block gives its tools. Reads the
157
+ * client-credentials variables now, so a variable that is not set (or that
158
+ * the allowlist refuses) fails the compile, naming the variable only, and
159
+ * so does a host the operator's OAuth host allowlist does not bind to the
160
+ * provider (ADR 0114). `where` prefixes those messages; `server` is the URL
161
+ * (or URLs) the token is sent to.
162
+ */
163
+ export declare function oauthTokenSource(oauth2: OAuth2AuthConfig, where: string, server: string | readonly string[], options?: {
164
+ allowPrivate?: boolean;
165
+ env?: NodeJS.ProcessEnv;
166
+ fetch?: typeof fetch;
167
+ }): OAuthTokenSource;
168
+ /** The parts of a syndicate config `oauthClientsFor` reads. */
169
+ interface AgentLike {
170
+ name?: string;
171
+ openapi?: Array<{
172
+ spec?: string;
173
+ auth?: {
174
+ oauth2?: OAuth2AuthConfig;
175
+ };
176
+ }>;
177
+ mcp_server_url?: string;
178
+ mcp_auth?: {
179
+ oauth2?: OAuth2AuthConfig;
180
+ };
181
+ yaml_reference?: string;
182
+ }
183
+ interface SyndicateLike {
184
+ orchestrator?: AgentLike;
185
+ subagents?: AgentLike[];
186
+ }
187
+ /**
188
+ * The OAuth clients the consent step needs for these syndicates' declared
189
+ * authorization-code grants, by provider: pass them as
190
+ * `oauthConsent({ providers })` (lib/tools/oauthConsent.ts). Client ids and
191
+ * secrets are read from the variables the YAML names. Two declarations of
192
+ * one provider must agree on its endpoints, client and scopes, or this
193
+ * throws: a provider's grant is one grant, whichever tool asks for it.
194
+ * `load` reads a nested `yaml_reference:` file, when given.
195
+ */
196
+ export declare function oauthClientsFor(configs: readonly SyndicateLike[], options?: {
197
+ env?: NodeJS.ProcessEnv;
198
+ load?: (ref: string) => SyndicateLike;
199
+ allowlist?: OAuthHostAllowlist | null;
200
+ }): Record<string, OAuthClientConfig>;
201
+ /**
202
+ * Every OAuth grant these syndicates declare, orchestrators and subagents,
203
+ * following a nested `yaml_reference:` through `load` when given (once per
204
+ * file; agent names then read "Parent › Child").
205
+ */
206
+ export declare function syndicateOAuthGrants(configs: readonly SyndicateLike[], load?: (ref: string) => SyndicateLike): OAuthGrantUse[];
207
+ /**
208
+ * Why these syndicates may not be served here, one line per refusal
209
+ * (ADR 0114): each declared grant's MCP server, token endpoint and
210
+ * authorization endpoint against the operator's OAuth host allowlist (an
211
+ * OpenAPI entry's servers are in its spec, so its compile checks those).
212
+ * The A2A server checks every syndicate it loads to serve; the compile
213
+ * checks again, and each call once more.
214
+ */
215
+ export declare function syndicateOAuthHostProblems(configs: readonly SyndicateLike[], options?: {
216
+ load?: (ref: string) => SyndicateLike;
217
+ allowlist?: OAuthHostAllowlist | null;
218
+ }): string[];
219
+ /**
220
+ * The credential store's refresh hooks for the same providers
221
+ * (`credentialStore({ providers })`, lib/tools/credentialStore.ts): an
222
+ * expired user token is renewed at the provider's token endpoint with its
223
+ * refresh token (RFC 6749 6) instead of asking the person again. Built from
224
+ * `oauthClientsFor`'s clients; `allowPrivate` lets a private or loopback
225
+ * token endpoint through the guard (development).
226
+ */
227
+ export declare function oauthRefreshProviders(clients: Record<string, OAuthClientConfig>, options?: {
228
+ allowPrivate?: boolean;
229
+ fetch?: typeof fetch;
230
+ timeoutMs?: number;
231
+ now?: () => number;
232
+ }): Record<string, OAuthProvider>;
233
+ export {};
@@ -0,0 +1,421 @@
1
+ /**
2
+ * lib/tools/oauthTools.ts — `auth: { oauth2 }` from a syndicate YAML: an
3
+ * OpenAPI entry or an MCP server whose calls carry an OAuth access token
4
+ * (WS6-3c, ADR 0112).
5
+ *
6
+ * WHY this file exists:
7
+ * The credential store (ADR 0072) holds each user's tokens and the consent
8
+ * step (ADR 0085) puts them there, but nothing said which tools send which
9
+ * provider's token. The YAML says it, next to the tool it protects:
10
+ *
11
+ * openapi:
12
+ * - spec: specs/tracker.yaml
13
+ * auth:
14
+ * oauth2:
15
+ * provider: tracker
16
+ * grant: authorization_code
17
+ * authorization_url: https://tracker.example.com/oauth/authorize
18
+ * token_url: https://tracker.example.com/oauth/token
19
+ * client_id_env: TRACKER_CLIENT_ID
20
+ * client_secret_env: TRACKER_CLIENT_SECRET
21
+ * scopes: [issues:read]
22
+ *
23
+ * and `mcp_auth: { oauth2: { ... } }` beside an agent's `mcp_server_url`.
24
+ *
25
+ * THE TWO GRANTS:
26
+ * - authorization_code: the token is the run's own user's, read through
27
+ * `ctx.accessToken(provider)` at each call. A user who has not granted it
28
+ * is asked (the consent pause, when the run has a consent step whose
29
+ * clients include the provider: `oauthClientsFor` builds them from the
30
+ * same YAML, and `oauthRefreshProviders` the store's refresh hooks).
31
+ * The tool never chooses whose token.
32
+ * - client_credentials: the server's own token for the provider, from its
33
+ * token endpoint with the client id and secret, held in this process's
34
+ * memory until shortly before it expires, never in the credential store
35
+ * (no user owns it).
36
+ *
37
+ * SECRETS: the YAML names environment variables, never values; the variable
38
+ * names pass credentialEnvProblem (lib/tools/credentialEnv.ts), so a YAML
39
+ * cannot send a framework secret to a token endpoint. No token, client
40
+ * secret or provider error text reaches a result, an error, a log line or
41
+ * a span: a failure is reported by kind (ToolCredentialError).
42
+ *
43
+ * WHERE A TOKEN MAY GO: a token is sent only over https, or http to a
44
+ * loopback host (local development), to the server the YAML names, and
45
+ * only to a host the operator's allowlist binds to the provider
46
+ * (lib/tools/oauthHosts.ts, ADR 0114): with none configured, an
47
+ * authorization_code grant is refused. The allowlist is checked when a
48
+ * served syndicate loads (`syndicateOAuthHostProblems`), when the tools
49
+ * compile (the server, the token endpoint and the authorization
50
+ * endpoint), and again before each call sends a token
51
+ * (`oauthCallProblem`). The client-credentials token endpoint passes the
52
+ * same SSRF guard as the server it serves (ALLOW_PRIVATE_OPENAPI or
53
+ * ALLOW_PRIVATE_MCP), and no redirect from it is followed.
54
+ */
55
+ import { blockedHostReason, checkHost } from '../net/addressGuard.js';
56
+ import { PROVIDER_NAME, ToolCredentialError } from './auth.js';
57
+ import { readCredentialEnv } from './credentialEnv.js';
58
+ import { oauthHostProblem, oauthHosts } from './oauthHosts.js';
59
+ /** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server. */
60
+ export function agentOAuthGrants(agent, prefix = '') {
61
+ const name = `${prefix}${agent.name ?? ''}`;
62
+ const out = [];
63
+ for (const entry of agent.openapi ?? []) {
64
+ if (entry?.auth?.oauth2)
65
+ out.push({ agent: name, tools: `openapi ${entry.spec ?? ''}`, oauth2: entry.auth.oauth2 });
66
+ }
67
+ if (agent.mcp_auth?.oauth2)
68
+ out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, oauth2: agent.mcp_auth.oauth2 });
69
+ return out;
70
+ }
71
+ /** The environment variables a grant reads, by role. */
72
+ export function oauthEnvNames(oauth2) {
73
+ return [oauth2.client_id_env, oauth2.client_secret_env].filter((e) => typeof e === 'string' && e !== '');
74
+ }
75
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
76
+ /** Why a token may not be sent to `raw`, or null: https, or http to a loopback host. */
77
+ export function tokenTransportProblem(raw) {
78
+ let url;
79
+ try {
80
+ url = new URL(raw);
81
+ }
82
+ catch {
83
+ return `'${raw.slice(0, 200)}' is not a URL`;
84
+ }
85
+ if (url.username || url.password)
86
+ return `${url.hostname} must not carry credentials in its URL`;
87
+ if (url.protocol === 'https:')
88
+ return null;
89
+ if (url.protocol === 'http:' && LOOPBACK.has(url.hostname))
90
+ return null;
91
+ return `an OAuth token is sent only over https (http only to a loopback host), not to ${url.protocol}//${url.hostname}`;
92
+ }
93
+ // ── The operator's host allowlist (ADR 0114) ─────────────────────────────────
94
+ /**
95
+ * Why this grant may not be declared for these servers, one line per
96
+ * refusal: each server the tools call, the token endpoint and the
97
+ * authorization endpoint must be hosts the operator's allowlist binds to the
98
+ * provider (lib/tools/oauthHosts.ts). With no allowlist, an
99
+ * authorization_code grant is refused and a client_credentials one allowed.
100
+ * `allowlist` defaults to the one in force; `null` means none is configured.
101
+ */
102
+ export function oauthGrantHostProblems(oauth2, servers, allowlist) {
103
+ const list = allowlist === undefined ? oauthHosts() ?? null : allowlist;
104
+ const urls = servers.map((s) => ['server', s]);
105
+ urls.push(['token_url', oauth2.token_url]);
106
+ if (oauth2.authorization_url)
107
+ urls.push(['authorization_url', oauth2.authorization_url]);
108
+ const out = new Set();
109
+ for (const [role, url] of urls) {
110
+ const problem = oauthHostProblem(oauth2, url, role, list);
111
+ if (problem)
112
+ out.add(problem);
113
+ }
114
+ return [...out];
115
+ }
116
+ /**
117
+ * Why a call may not send this grant's token (or secret) to `destination`
118
+ * now, or null: the transport rule and the allowlist in force, checked again
119
+ * at each call. The compile checked the same; this holds if a request is
120
+ * aimed elsewhere, or the allowlist changed. A malformed allowlist refuses.
121
+ */
122
+ export function oauthCallProblem(oauth2, destination) {
123
+ const transport = tokenTransportProblem(destination);
124
+ if (transport)
125
+ return transport;
126
+ try {
127
+ return oauthHostProblem(oauth2, destination, 'server', oauthHosts() ?? null);
128
+ }
129
+ catch {
130
+ return 'the OAuth host allowlist is malformed';
131
+ }
132
+ }
133
+ const MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
134
+ /** A token response, read whole up to a bound, as a JSON object; undefined otherwise. */
135
+ async function readTokenJson(res) {
136
+ const reader = res.body?.getReader();
137
+ if (!reader)
138
+ return undefined;
139
+ const chunks = [];
140
+ let size = 0;
141
+ for (;;) {
142
+ const { done, value } = await reader.read();
143
+ if (done)
144
+ break;
145
+ size += value.byteLength;
146
+ if (size > MAX_TOKEN_RESPONSE_BYTES) {
147
+ await reader.cancel().catch(() => { });
148
+ return undefined;
149
+ }
150
+ chunks.push(value);
151
+ }
152
+ try {
153
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
154
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ }
160
+ /** Why the server may not call this token endpoint, or null (the full guard, with DNS). */
161
+ async function tokenEndpointProblem(raw, allowPrivate) {
162
+ const transport = tokenTransportProblem(raw);
163
+ if (transport)
164
+ return transport;
165
+ if (allowPrivate)
166
+ return null;
167
+ const reason = await checkHost(new URL(raw).hostname);
168
+ return reason ? `refusing token endpoint ${new URL(raw).hostname}: ${reason}` : null;
169
+ }
170
+ /**
171
+ * One token-endpoint request (RFC 6749 4.4, 6): the endpoint checked by the
172
+ * guard first, no redirect followed, a time limit, a bounded response. Any
173
+ * failure is ToolCredentialError('grant_failed'), never the provider's text,
174
+ * which may echo what it refused.
175
+ */
176
+ async function tokenRequest(tokenUrl, body, provider, options) {
177
+ if (await tokenEndpointProblem(tokenUrl, options.allowPrivate))
178
+ throw new ToolCredentialError('grant_failed', provider);
179
+ let json;
180
+ try {
181
+ const res = await options.doFetch(tokenUrl, {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
184
+ body,
185
+ // A redirect would carry the client secret (or the refresh token) to wherever it points.
186
+ redirect: 'error',
187
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs)]) : AbortSignal.timeout(options.timeoutMs),
188
+ });
189
+ json = await readTokenJson(res);
190
+ if (!res.ok)
191
+ json = undefined;
192
+ }
193
+ catch {
194
+ json = undefined;
195
+ }
196
+ const accessToken = json?.access_token;
197
+ const tokenType = json?.token_type;
198
+ if (typeof accessToken !== 'string' || !accessToken)
199
+ throw new ToolCredentialError('grant_failed', provider);
200
+ if (tokenType !== undefined && (typeof tokenType !== 'string' || tokenType.toLowerCase() !== 'bearer'))
201
+ throw new ToolCredentialError('grant_failed', provider);
202
+ const refreshToken = typeof json?.refresh_token === 'string' && json.refresh_token ? json.refresh_token : undefined;
203
+ const expiresIn = typeof json?.expires_in === 'number' ? json.expires_in : typeof json?.expires_in === 'string' ? Number(json.expires_in) : undefined;
204
+ const scopes = typeof json?.scope === 'string' ? json.scope.split(' ').filter((x) => x !== '') : undefined;
205
+ return {
206
+ accessToken,
207
+ ...(refreshToken ? { refreshToken } : {}),
208
+ ...(expiresIn !== undefined && Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresAt: new Date(options.now() + expiresIn * 1000) } : {}),
209
+ ...(scopes ? { scopes } : {}),
210
+ };
211
+ }
212
+ /**
213
+ * The server's own token for a provider (RFC 6749 4.4), fetched when first
214
+ * needed and again shortly before it expires, one request at a time. A
215
+ * failure throws ToolCredentialError('grant_failed'), never the provider's
216
+ * text, which may echo what it refused.
217
+ */
218
+ export function clientCredentialsGrant(options) {
219
+ const now = options.now ?? Date.now;
220
+ const skew = options.refreshSkewMs ?? 60_000;
221
+ const doFetch = options.fetch ?? fetch;
222
+ const timeoutMs = options.timeoutMs ?? 10_000;
223
+ let held;
224
+ let pending;
225
+ const request = async (signal) => {
226
+ const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: options.clientId, client_secret: options.clientSecret });
227
+ if (options.scopes?.length)
228
+ body.set('scope', options.scopes.join(' '));
229
+ const issued = await tokenRequest(options.tokenUrl, body, options.provider, { allowPrivate: options.allowPrivate === true, doFetch, timeoutMs, now, ...(signal ? { signal } : {}) });
230
+ held = { accessToken: issued.accessToken, ...(issued.expiresAt ? { expiresAt: issued.expiresAt.getTime() } : {}) };
231
+ return issued.accessToken;
232
+ };
233
+ return {
234
+ async token(signal) {
235
+ if (held && (held.expiresAt === undefined || held.expiresAt - skew > now()))
236
+ return held.accessToken;
237
+ if (!pending)
238
+ pending = request(signal).finally(() => (pending = undefined));
239
+ return pending;
240
+ },
241
+ };
242
+ }
243
+ /**
244
+ * The token source a declared `oauth2` block gives its tools. Reads the
245
+ * client-credentials variables now, so a variable that is not set (or that
246
+ * the allowlist refuses) fails the compile, naming the variable only, and
247
+ * so does a host the operator's OAuth host allowlist does not bind to the
248
+ * provider (ADR 0114). `where` prefixes those messages; `server` is the URL
249
+ * (or URLs) the token is sent to.
250
+ */
251
+ export function oauthTokenSource(oauth2, where, server, options = {}) {
252
+ const servers = typeof server === 'string' ? [server] : [...server];
253
+ for (const s of servers) {
254
+ const problem = tokenTransportProblem(s);
255
+ if (problem)
256
+ throw new Error(`${where}: ${problem}`);
257
+ }
258
+ if (!PROVIDER_NAME.test(oauth2.provider))
259
+ throw new Error(`${where}: provider must match ${PROVIDER_NAME}`);
260
+ const hostProblems = oauthGrantHostProblems(oauth2, servers);
261
+ if (hostProblems.length)
262
+ throw new Error(`${where}: ${hostProblems.join('; ')}`);
263
+ const provider = oauth2.provider;
264
+ /** The call-time check (ADR 0114): before any token is read or fetched. */
265
+ const refuse = (destination) => {
266
+ if (oauthCallProblem(oauth2, destination))
267
+ throw new ToolCredentialError('host_refused', provider);
268
+ };
269
+ if (oauth2.grant === 'authorization_code') {
270
+ return async (ctx, destination) => {
271
+ refuse(destination);
272
+ if (!ctx?.accessToken)
273
+ throw new ToolCredentialError('unavailable', provider);
274
+ return ctx.accessToken(provider);
275
+ };
276
+ }
277
+ const env = options.env ?? process.env;
278
+ if (!oauth2.client_secret_env)
279
+ throw new Error(`${where}: a client_credentials grant needs client_secret_env`);
280
+ const transport = tokenTransportProblem(oauth2.token_url);
281
+ if (transport)
282
+ throw new Error(`${where}: token_url: ${transport}`);
283
+ if (options.allowPrivate !== true) {
284
+ const blocked = blockedHostReason(new URL(oauth2.token_url).hostname);
285
+ if (blocked)
286
+ throw new Error(`${where}: refusing token endpoint ${new URL(oauth2.token_url).hostname}: ${blocked}`);
287
+ }
288
+ const grant = clientCredentialsGrant({
289
+ provider,
290
+ tokenUrl: oauth2.token_url,
291
+ clientId: oauth2.client_id ?? readCredentialEnv(oauth2.client_id_env ?? '', where, env),
292
+ clientSecret: readCredentialEnv(oauth2.client_secret_env, where, env),
293
+ ...(oauth2.scopes?.length ? { scopes: [...oauth2.scopes] } : {}),
294
+ ...(options.allowPrivate ? { allowPrivate: true } : {}),
295
+ ...(options.fetch ? { fetch: options.fetch } : {}),
296
+ });
297
+ return async (ctx, destination) => {
298
+ refuse(destination);
299
+ // The client secret goes to the token endpoint: held to the same rule.
300
+ refuse(oauth2.token_url);
301
+ return grant.token(ctx?.signal);
302
+ };
303
+ }
304
+ /**
305
+ * The OAuth clients the consent step needs for these syndicates' declared
306
+ * authorization-code grants, by provider: pass them as
307
+ * `oauthConsent({ providers })` (lib/tools/oauthConsent.ts). Client ids and
308
+ * secrets are read from the variables the YAML names. Two declarations of
309
+ * one provider must agree on its endpoints, client and scopes, or this
310
+ * throws: a provider's grant is one grant, whichever tool asks for it.
311
+ * `load` reads a nested `yaml_reference:` file, when given.
312
+ */
313
+ export function oauthClientsFor(configs, options = {}) {
314
+ const env = options.env ?? process.env;
315
+ const allowlist = options.allowlist === undefined ? oauthHosts(env) ?? null : options.allowlist;
316
+ const clients = {};
317
+ const signature = new Map();
318
+ for (const use of syndicateOAuthGrants(configs, options.load)) {
319
+ const o = use.oauth2;
320
+ if (o.grant !== 'authorization_code')
321
+ continue;
322
+ const where = `${use.agent} · ${use.tools} · oauth2`;
323
+ if (!o.authorization_url)
324
+ throw new Error(`${where}: an authorization_code grant needs authorization_url`);
325
+ // The consent step sends the client secret, the code and later the refresh token to these endpoints (ADR 0114).
326
+ const hostProblems = oauthGrantHostProblems(o, [], allowlist);
327
+ if (hostProblems.length)
328
+ throw new Error(`${where}: ${hostProblems.join('; ')}`);
329
+ const sig = JSON.stringify([o.authorization_url, o.token_url, o.client_id ?? null, o.client_id_env ?? null, o.client_secret_env ?? null, [...(o.scopes ?? [])].sort(), o.authorization_params ?? {}]);
330
+ const before = signature.get(o.provider);
331
+ if (before !== undefined) {
332
+ if (before !== sig)
333
+ throw new Error(`${where}: provider "${o.provider}" is declared twice with different endpoints, client or scopes`);
334
+ continue;
335
+ }
336
+ signature.set(o.provider, sig);
337
+ clients[o.provider] = {
338
+ authorizationUrl: o.authorization_url,
339
+ tokenUrl: o.token_url,
340
+ clientId: o.client_id ?? readCredentialEnv(o.client_id_env ?? '', where, env),
341
+ ...(o.client_secret_env ? { clientSecret: readCredentialEnv(o.client_secret_env, where, env) } : {}),
342
+ scopes: [...(o.scopes ?? [])],
343
+ ...(o.authorization_params ? { authorizationParams: { ...o.authorization_params } } : {}),
344
+ };
345
+ }
346
+ return clients;
347
+ }
348
+ /**
349
+ * Every OAuth grant these syndicates declare, orchestrators and subagents,
350
+ * following a nested `yaml_reference:` through `load` when given (once per
351
+ * file; agent names then read "Parent › Child").
352
+ */
353
+ export function syndicateOAuthGrants(configs, load) {
354
+ const out = [];
355
+ const seen = new Set();
356
+ const visit = (config, prefix) => {
357
+ const agents = [config.orchestrator, ...(config.subagents ?? [])].filter((a) => !!a);
358
+ for (const agent of agents) {
359
+ if (agent.yaml_reference) {
360
+ if (!load || seen.has(agent.yaml_reference))
361
+ continue;
362
+ seen.add(agent.yaml_reference);
363
+ visit(load(agent.yaml_reference), `${prefix}${agent.name ?? agent.yaml_reference} › `);
364
+ continue;
365
+ }
366
+ out.push(...agentOAuthGrants(agent, prefix));
367
+ }
368
+ };
369
+ for (const config of configs)
370
+ visit(config, '');
371
+ return out;
372
+ }
373
+ /**
374
+ * Why these syndicates may not be served here, one line per refusal
375
+ * (ADR 0114): each declared grant's MCP server, token endpoint and
376
+ * authorization endpoint against the operator's OAuth host allowlist (an
377
+ * OpenAPI entry's servers are in its spec, so its compile checks those).
378
+ * The A2A server checks every syndicate it loads to serve; the compile
379
+ * checks again, and each call once more.
380
+ */
381
+ export function syndicateOAuthHostProblems(configs, options = {}) {
382
+ const allowlist = options.allowlist === undefined ? oauthHosts() ?? null : options.allowlist;
383
+ const out = [];
384
+ for (const use of syndicateOAuthGrants(configs, options.load)) {
385
+ const servers = use.tools.startsWith('mcp ') && use.tools.length > 4 ? [use.tools.slice(4)] : [];
386
+ for (const problem of oauthGrantHostProblems(use.oauth2, servers, allowlist))
387
+ out.push(`${use.agent} · ${use.tools} · oauth2: ${problem}`);
388
+ }
389
+ return out;
390
+ }
391
+ /**
392
+ * The credential store's refresh hooks for the same providers
393
+ * (`credentialStore({ providers })`, lib/tools/credentialStore.ts): an
394
+ * expired user token is renewed at the provider's token endpoint with its
395
+ * refresh token (RFC 6749 6) instead of asking the person again. Built from
396
+ * `oauthClientsFor`'s clients; `allowPrivate` lets a private or loopback
397
+ * token endpoint through the guard (development).
398
+ */
399
+ export function oauthRefreshProviders(clients, options = {}) {
400
+ const providers = {};
401
+ for (const [name, client] of Object.entries(clients)) {
402
+ providers[name] = {
403
+ refresh: async (refreshToken, context) => {
404
+ // A refresh token goes only to a token endpoint the allowlist binds to its provider (ADR 0114).
405
+ if (oauthCallProblem({ provider: name, grant: 'authorization_code' }, client.tokenUrl))
406
+ throw new ToolCredentialError('host_refused', name);
407
+ const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: client.clientId });
408
+ if (client.clientSecret)
409
+ body.set('client_secret', client.clientSecret);
410
+ return tokenRequest(client.tokenUrl, body, name, {
411
+ allowPrivate: options.allowPrivate === true,
412
+ doFetch: options.fetch ?? fetch,
413
+ timeoutMs: options.timeoutMs ?? 10_000,
414
+ now: options.now ?? Date.now,
415
+ ...(context.signal ? { signal: context.signal } : {}),
416
+ });
417
+ },
418
+ };
419
+ }
420
+ return providers;
421
+ }
@@ -1,4 +1,6 @@
1
1
  import type { Tool } from './tool.ts';
2
+ import type { OAuth2AuthConfig } from './oauthTools.ts';
3
+ export { credentialEnvProblem } from './credentialEnv.ts';
2
4
  export { namesTool, toSnake } from './openapi/parse.ts';
3
5
  export interface OpenApiAuthConfig {
4
6
  /** Environment variable holding a bearer token (`Authorization: Bearer …`). */
@@ -9,6 +11,8 @@ export interface OpenApiAuthConfig {
9
11
  in: 'header' | 'query';
10
12
  name: string;
11
13
  };
14
+ /** An OAuth access token per call: the run's user's (authorization_code) or the server's (client_credentials). ADR 0112. */
15
+ oauth2?: OAuth2AuthConfig;
12
16
  }
13
17
  /** One entry of an agent's `openapi:` list. */
14
18
  export interface OpenApiConfig {
@@ -29,12 +33,6 @@ export declare const MAX_RESULT_CHARS = 20000;
29
33
  * operationId (lib/compile.ts). Its value is the operationId.
30
34
  */
31
35
  export declare const OPENAPI_TOOL: unique symbol;
32
- /**
33
- * Why an `auth` may not read this variable, or null. With
34
- * OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
35
- * it, anything but the framework's own variables is.
36
- */
37
- export declare function credentialEnvProblem(name: string, env?: NodeJS.ProcessEnv): string | null;
38
36
  /**
39
37
  * Cut an API result to MAX_RESULT_CHARS, saying so. Kept for consumers of
40
38
  * this module; the tools cut with capResult (lib/tools/tool.ts).