melchizedek-agents 1.2.0 → 1.3.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 (65) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +141 -0
  3. package/DOCUMENTATION.md +107 -10
  4. package/ONBOARDING.md +50 -11
  5. package/QUICKSTART.md +1 -1
  6. package/README.md +22 -2
  7. package/config/agents/examples/connectors.yaml +82 -0
  8. package/config/agents/syndicate.schema.json +324 -26
  9. package/dist/lib/a2a/app.js +3 -0
  10. package/dist/lib/a2a/oauthSetup.js +20 -5
  11. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  12. package/dist/lib/chatgpt/adapter.js +107 -0
  13. package/dist/lib/chatgpt/cli.d.ts +23 -0
  14. package/dist/lib/chatgpt/cli.js +77 -0
  15. package/dist/lib/chatgpt/oauth.d.ts +103 -0
  16. package/dist/lib/chatgpt/oauth.js +448 -0
  17. package/dist/lib/chatgpt/state.d.ts +98 -0
  18. package/dist/lib/chatgpt/state.js +145 -0
  19. package/dist/lib/chatgpt/store.d.ts +24 -0
  20. package/dist/lib/chatgpt/store.js +94 -0
  21. package/dist/lib/compile.js +40 -25
  22. package/dist/lib/dispatch.js +6 -2
  23. package/dist/lib/doctor.d.ts +34 -3
  24. package/dist/lib/doctor.js +63 -5
  25. package/dist/lib/loadSyndicate.d.ts +6 -0
  26. package/dist/lib/mcp/server.d.ts +195 -0
  27. package/dist/lib/mcp/server.js +564 -0
  28. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  29. package/dist/lib/models/adapterResolver.js +6 -1
  30. package/dist/lib/models/gateway.d.ts +1 -1
  31. package/dist/lib/models/gateway.js +4 -2
  32. package/dist/lib/onboarding.d.ts +4 -3
  33. package/dist/lib/onboarding.js +48 -9
  34. package/dist/lib/runtime/syndicateTurn.js +30 -16
  35. package/dist/lib/syndicateSchema.d.ts +146 -8
  36. package/dist/lib/syndicateSchema.js +118 -11
  37. package/dist/lib/tools/credentialUses.d.ts +7 -1
  38. package/dist/lib/tools/credentialUses.js +10 -2
  39. package/dist/lib/tools/generateImageTool.js +2 -1
  40. package/dist/lib/tools/inspectImageTool.js +1 -1
  41. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  42. package/dist/lib/tools/mcpToolFactory.js +71 -25
  43. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  44. package/dist/lib/tools/mcpTransports.js +11 -0
  45. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  46. package/dist/lib/tools/oauthConsent.js +49 -14
  47. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  48. package/dist/lib/tools/oauthDiscovery.js +616 -0
  49. package/dist/lib/tools/oauthTools.d.ts +44 -12
  50. package/dist/lib/tools/oauthTools.js +79 -22
  51. package/dist/lib/tools/xApiSearchTool.js +2 -2
  52. package/dist/scripts/a2a_server.js +8 -0
  53. package/dist/scripts/assistant_worker.js +11 -1
  54. package/dist/scripts/doctor.d.ts +1 -1
  55. package/dist/scripts/doctor.js +1 -1
  56. package/dist/scripts/mcp_server.d.ts +12 -0
  57. package/dist/scripts/mcp_server.js +345 -0
  58. package/dist/scripts/setup.d.ts +2 -0
  59. package/dist/scripts/setup.js +27 -0
  60. package/dist/scripts/syndicate_chat.js +12 -3
  61. package/package.json +5 -2
  62. package/skills/README.md +2 -2
  63. package/skills/briefs/skills-readme.md +1 -1
  64. package/skills/melchizedek-onboard/SKILL.md +28 -6
  65. package/skills/melchizedek-onboard-keys/SKILL.md +17 -5
@@ -85,7 +85,9 @@ export class ConsentError extends Error {
85
85
  ? `The authorization at ${p} could not be completed.`
86
86
  : code === 'store_failed'
87
87
  ? `The authorization at ${p} could not be saved.`
88
- : `No OAuth client is configured for ${p}.`);
88
+ : code === 'registration_failed'
89
+ ? `The authorization at ${p} could not be set up.`
90
+ : `No OAuth client is configured for ${p}.`);
89
91
  this.name = 'ConsentError';
90
92
  this.code = code;
91
93
  if (provider !== undefined)
@@ -120,7 +122,21 @@ function checkEndpoint(value, what) {
120
122
  throw new Error(`${what} must not carry credentials.`);
121
123
  return url;
122
124
  }
123
- const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope']);
125
+ const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope', 'resource']);
126
+ /** A client's configuration, checked: the endpoints, the client id, the parameters. Throws, naming the provider. */
127
+ function checkClient(name, client) {
128
+ checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
129
+ checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
130
+ if (typeof client.clientId !== 'string' || !client.clientId)
131
+ throw new Error(`${name}: clientId is required.`);
132
+ for (const key of Object.keys(client.authorizationParams ?? {})) {
133
+ if (RESERVED_PARAMS.has(key))
134
+ throw new Error(`${name}: authorizationParams may not set "${key}".`);
135
+ }
136
+ if (client.resource !== undefined)
137
+ checkEndpoint(client.resource, `${name}: resource`);
138
+ return { ...client, scopes: [...(client.scopes ?? [])] };
139
+ }
124
140
  /** The consent manager over a credential store. Validates the configuration at once: a bad URL throws here, at boot. */
125
141
  export function oauthConsent(options) {
126
142
  const redirect = checkEndpoint(options.redirectUri, 'The OAuth redirect URI');
@@ -130,16 +146,23 @@ export function oauthConsent(options) {
130
146
  for (const [name, client] of Object.entries(options.providers)) {
131
147
  if (!PROVIDER_NAME.test(name))
132
148
  throw new Error(`OAuth provider "${name.slice(0, 64)}" must match ${PROVIDER_NAME}.`);
133
- checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
134
- checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
135
- if (typeof client.clientId !== 'string' || !client.clientId)
136
- throw new Error(`${name}: clientId is required.`);
137
- for (const key of Object.keys(client.authorizationParams ?? {})) {
138
- if (RESERVED_PARAMS.has(key))
139
- throw new Error(`${name}: authorizationParams may not set "${key}".`);
140
- }
141
- providers.set(name, { ...client, scopes: [...(client.scopes ?? [])] });
149
+ // A source is checked each time it answers; a configured client here, at boot.
150
+ providers.set(name, typeof client === 'function' ? client : checkClient(name, client));
142
151
  }
152
+ /** The provider's client now, or undefined; a source that fails is a ConsentError('registration_failed'). */
153
+ const clientOf = async (name) => {
154
+ const entry = providers.get(name);
155
+ if (typeof entry !== 'function')
156
+ return entry;
157
+ try {
158
+ return checkClient(name, await entry());
159
+ }
160
+ catch (err) {
161
+ // A discovery refusal names roles and hosts, never a value (lib/tools/oauthDiscovery.ts); anything else is named by kind only.
162
+ console.warn(`[OAuth] ${name}: the client could not be set up: ${err instanceof Error && err.name === 'OAuthDiscoveryError' ? err.message : err instanceof Error ? err.name : 'unknown error'}`);
163
+ throw new ConsentError('registration_failed', name);
164
+ }
165
+ };
143
166
  const states = options.states ?? memoryConsentStates({ ...(options.now ? { now: options.now } : {}) });
144
167
  const ttlMs = options.ttlMs ?? 10 * 60_000;
145
168
  const now = options.now ?? Date.now;
@@ -193,6 +216,8 @@ export function oauthConsent(options) {
193
216
  client_id: client.clientId,
194
217
  code_verifier: flow.codeVerifier,
195
218
  });
219
+ if (client.resource)
220
+ body.set('resource', client.resource);
196
221
  if (client.clientSecret) {
197
222
  // The secret goes only to a host the operator binds its variable to (ADR 0122), checked now:
198
223
  // the allowlist can change after boot. Refused, nothing is sent.
@@ -238,9 +263,9 @@ export function oauthConsent(options) {
238
263
  redirectUri: options.redirectUri,
239
264
  has: (provider) => typeof provider === 'string' && providers.has(provider),
240
265
  async begin(binding) {
241
- const client = providers.get(binding.provider);
242
- if (!client)
266
+ if (!providers.has(binding.provider))
243
267
  throw new ConsentError('unknown_provider', PROVIDER_NAME.test(binding.provider) ? binding.provider : undefined);
268
+ const client = (await clientOf(binding.provider));
244
269
  for (const field of ['appName', 'userId', 'sessionId', 'functionCallId']) {
245
270
  if (typeof binding[field] !== 'string' || !binding[field])
246
271
  throw new Error(`A consent flow needs the paused call's ${field}.`);
@@ -269,6 +294,8 @@ export function oauthConsent(options) {
269
294
  url.searchParams.set('state', state);
270
295
  url.searchParams.set('code_challenge', s256Challenge(codeVerifier));
271
296
  url.searchParams.set('code_challenge_method', 'S256');
297
+ if (client.resource)
298
+ url.searchParams.set('resource', client.resource);
272
299
  const authUri = url.toString();
273
300
  const authConfig = {
274
301
  credentialKey: binding.provider,
@@ -314,7 +341,15 @@ export function oauthConsent(options) {
314
341
  const code = input.code;
315
342
  if (typeof code !== 'string' || !CODE_FORMAT.test(code))
316
343
  fail('invalid_request');
317
- const client = providers.get(flow.provider);
344
+ if (!providers.has(flow.provider))
345
+ fail('unknown_provider');
346
+ let client;
347
+ try {
348
+ client = await clientOf(flow.provider);
349
+ }
350
+ catch {
351
+ return fail('registration_failed');
352
+ }
318
353
  if (!client)
319
354
  fail('unknown_provider');
320
355
  let tokens;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * lib/tools/oauthDiscovery.ts — an MCP server's authorization server,
3
+ * discovered, and this deployment registered at it as an OAuth client
4
+ * (ADR 0124).
5
+ *
6
+ * WHY this file exists:
7
+ * The official remote MCP servers (an issue tracker's, a wiki's, a code
8
+ * host's) take each user's own OAuth token, and most issue no client id in
9
+ * advance: the MCP authorization spec has a client find the authorization
10
+ * server from the MCP server (protected-resource metadata, RFC 9728; then
11
+ * authorization-server metadata, RFC 8414 or OpenID discovery) and register
12
+ * itself (dynamic client registration, RFC 7591). A YAML says so with
13
+ * `client_registration: dynamic` on the server's `oauth2` grant; this module
14
+ * does the discovery and the registration, and keeps the registered client.
15
+ *
16
+ * WHAT IT GUARANTEES:
17
+ * - The MCP server is asked first, as the MCP authorization spec has a
18
+ * client do: an unauthenticated initialize request, and on a 401 the
19
+ * Bearer challenge's `resource_metadata` (and `scope`) parameters in its
20
+ * WWW-Authenticate header (RFC 9728 5.1). The URL it names is read first;
21
+ * the well-known URLs are the fallback. The header is read by a linear,
22
+ * length-bounded tokenizer (no regular expression), and never logged.
23
+ * - Discovery never widens the operator's OAuth host allowlist (ADR 0114):
24
+ * every URL it fetches (the MCP server's challenge, the metadata
25
+ * documents, including one the header names, the registration
26
+ * endpoint) and every endpoint it returns (authorization, token) must be
27
+ * a host the allowlist binds to the provider, checked before the request
28
+ * and again whenever the client is used. A discovered host outside it is
29
+ * refused by name, not skipped.
30
+ * - Every request passes the transport rule (https, or http to a loopback
31
+ * host) and the SSRF guard (lib/net/addressGuard.ts) unless private hosts
32
+ * are allowed (ALLOW_PRIVATE_MCP), follows no redirect, has a time limit,
33
+ * and reads a bounded JSON body.
34
+ * - The authorization server must advertise PKCE S256; one that does not is
35
+ * refused (MCP authorization spec). The client registers as a public
36
+ * client (`token_endpoint_auth_method: none`) for the one configured
37
+ * redirect URI, with the authorization_code and refresh_token grants.
38
+ * - The registered client is kept per provider, MCP server and redirect URI
39
+ * in the credential store's rows, sealed by its cipher
40
+ * (MELCHIZEDEK_CREDENTIAL_KEY), under an app name no run can have (it
41
+ * carries `:`, which a memory namespace cannot), so no tool call can read
42
+ * it as a token. A row this key cannot open, an expired client secret, or
43
+ * endpoints that moved are registered again.
44
+ * - No client secret, metadata body or server error text reaches a log
45
+ * line, an error message or a result: refusals name roles and hosts.
46
+ */
47
+ import type { CredentialKey } from './auth.ts';
48
+ import type { CredentialCipher } from './credentialCipher.ts';
49
+ import type { CredentialRows } from './credentialStore.ts';
50
+ import type { OAuthHostAllowlist } from './oauthHosts.ts';
51
+ import type { OAuthClientSource } from './oauthConsent.ts';
52
+ /** A discovery or registration that cannot proceed. The message names roles and hosts, never a value. */
53
+ export declare class OAuthDiscoveryError extends Error {
54
+ readonly provider: string;
55
+ constructor(provider: string, message: string);
56
+ }
57
+ /** What discovery found: the MCP server as a resource, and its authorization server's endpoints. */
58
+ export interface DiscoveredAuthorization {
59
+ /** The MCP server's canonical URI, sent as the RFC 8707 `resource` parameter. */
60
+ resource: string;
61
+ /** The authorization server's issuer identifier. */
62
+ issuer: string;
63
+ authorizationEndpoint: string;
64
+ tokenEndpoint: string;
65
+ registrationEndpoint: string;
66
+ /**
67
+ * The scopes the MCP server's 401 challenge named (its `scope` parameter),
68
+ * when it named any: the MCP spec's first choice when the grant names none.
69
+ */
70
+ challengeScopes?: string[];
71
+ }
72
+ export interface DiscoveryOptions {
73
+ /** The provider the grant names: the allowlist is read for it. */
74
+ provider: string;
75
+ /** Private and loopback hosts pass the SSRF guard (ALLOW_PRIVATE_MCP). The allowlist still applies. */
76
+ allowPrivate?: boolean;
77
+ /** The allowlist; default the one in force (`null`: none configured, which refuses). */
78
+ allowlist?: OAuthHostAllowlist | null;
79
+ /** Each request's time limit, ms. Default 10 s. */
80
+ timeoutMs?: number;
81
+ /** For tests. */
82
+ fetch?: typeof fetch;
83
+ }
84
+ /** The MCP server's canonical URI (RFC 8707, MCP authorization spec): no fragment, no trailing slash on a path. */
85
+ export declare function canonicalResource(serverUrl: string): string;
86
+ /** A WWW-Authenticate header longer than this is ignored whole. */
87
+ export declare const MAX_CHALLENGE_HEADER: number;
88
+ /** What a Bearer challenge tells a client that has no token yet. */
89
+ export interface BearerChallenge {
90
+ /** The `resource_metadata` parameter: the protected-resource metadata URL (RFC 9728 5.1), unparsed. */
91
+ resourceMetadata?: string;
92
+ /** The `scope` parameter, split, each a valid scope token (RFC 6750 3). */
93
+ scopes?: string[];
94
+ }
95
+ /**
96
+ * The first Bearer challenge's parameters in a WWW-Authenticate header, or
97
+ * undefined (no Bearer challenge, or a header that is too long or malformed:
98
+ * either is ignored whole, and discovery falls back to the well-known URLs).
99
+ * One pass over the header, each step advancing: linear in its length, with
100
+ * no regular expression. Several challenges in one header, quoted-string
101
+ * values with escapes, case-insensitive scheme and parameter names, and a
102
+ * token68 credential on another scheme are understood. A parameter given
103
+ * twice in the Bearer challenge makes it ambiguous, and malformed.
104
+ */
105
+ export declare function parseBearerChallenge(header: string | null | undefined): BearerChallenge | undefined;
106
+ /** RFC 9728 3.1: the protected-resource metadata URLs for a server, path-specific first. */
107
+ export declare function protectedResourceMetadataUrls(serverUrl: string): URL[];
108
+ /** RFC 8414 3.1 and OpenID discovery, in the order the MCP authorization spec gives. */
109
+ export declare function authorizationServerMetadataUrls(issuer: string): URL[];
110
+ /**
111
+ * The MCP server's authorization server, as the MCP authorization spec finds
112
+ * it: the protected-resource metadata URL named by the server's 401 challenge
113
+ * (WWW-Authenticate, RFC 9728 5.1), then the well-known URLs (RFC 9728 3.1);
114
+ * the metadata names the authorization server (a server with none is its
115
+ * own, as the 2025-03-26 revision had it), and its own metadata (RFC 8414, or
116
+ * OpenID discovery) names the endpoints. Every host involved, the one the
117
+ * header names included, passes the provider's allowlist before any request;
118
+ * PKCE S256 and a registration endpoint are required. Throws
119
+ * OAuthDiscoveryError.
120
+ */
121
+ export declare function discoverAuthorization(serverUrl: string, options: DiscoveryOptions): Promise<DiscoveredAuthorization>;
122
+ /** A client this deployment registered at an authorization server. */
123
+ export interface RegisteredClient {
124
+ clientId: string;
125
+ /** A confidential client's secret, when the server issued one anyway. */
126
+ clientSecret?: string;
127
+ /** Epoch ms after which the secret stops working; absent when it does not expire. */
128
+ secretExpiresAt?: number;
129
+ }
130
+ export interface RegistrationOptions extends DiscoveryOptions {
131
+ /** The consent callback, exactly as every authorization will send it. */
132
+ redirectUri: string;
133
+ /** Shown to the person on the provider's consent screen. Default "Melchizedek agents". */
134
+ clientName?: string;
135
+ scopes?: string[];
136
+ }
137
+ /**
138
+ * Registers this deployment at the authorization server (RFC 7591) as a
139
+ * public PKCE client for the one redirect URI. Throws OAuthDiscoveryError,
140
+ * never the server's text.
141
+ */
142
+ export declare function registerOAuthClient(discovered: DiscoveredAuthorization, options: RegistrationOptions): Promise<RegisteredClient>;
143
+ /**
144
+ * The app name registered clients are filed under. It carries `:`, which a
145
+ * memory namespace (the app a run pins, `[A-Za-z0-9._-]`) cannot, so no
146
+ * run's credential key ever reaches one of these rows.
147
+ */
148
+ export declare const REGISTERED_CLIENTS_APP = "oauth-client:dcr";
149
+ /** The row a provider's registered client is kept in: one per MCP server (resource) and redirect URI. */
150
+ export declare function registeredClientKey(provider: string, resource: string, redirectUri: string): CredentialKey;
151
+ /** What a sealed registration holds. */
152
+ interface StoredRegistration {
153
+ v: 1;
154
+ clientId: string;
155
+ clientSecret?: string;
156
+ issuer: string;
157
+ authorizationEndpoint: string;
158
+ tokenEndpoint: string;
159
+ resource: string;
160
+ redirectUri: string;
161
+ }
162
+ export interface OAuthClientRegistry {
163
+ /** The registration kept for this key, or undefined (none, unreadable under this key, or its secret expired). */
164
+ get(key: CredentialKey): Promise<(StoredRegistration & {
165
+ secretExpiresAt?: number;
166
+ }) | undefined>;
167
+ put(key: CredentialKey, registration: Omit<StoredRegistration, 'v'>, secretExpiresAt?: number): Promise<void>;
168
+ }
169
+ /** Registered clients over the credential store's rows, sealed by its cipher (the same key as every user's tokens). */
170
+ export declare function oauthClientRegistry(options: {
171
+ rows: CredentialRows;
172
+ cipher: CredentialCipher;
173
+ now?: () => number;
174
+ }): OAuthClientRegistry;
175
+ /** A dynamic grant as a served YAML declares it (lib/tools/oauthTools.ts dynamicOAuthGrantsFor). */
176
+ export interface DynamicOAuthGrant {
177
+ provider: string;
178
+ /** The MCP server the grant is for. */
179
+ server: string;
180
+ scopes: string[];
181
+ authorizationParams?: Record<string, string>;
182
+ }
183
+ export interface DynamicClientOptions {
184
+ /** The consent callback, as configured (OAUTH_REDIRECT_URI). */
185
+ redirectUri: string;
186
+ /** Where registrations are kept; without one, each process registers once in memory. */
187
+ registry?: OAuthClientRegistry;
188
+ allowPrivate?: boolean;
189
+ clientName?: string;
190
+ timeoutMs?: number;
191
+ fetch?: typeof fetch;
192
+ /** For tests. */
193
+ allowlist?: OAuthHostAllowlist | null;
194
+ }
195
+ /**
196
+ * The consent step's client for a dynamic grant (oauthConsent and
197
+ * oauthRefreshProviders take it in place of a fixed config): discovered and
198
+ * registered on first use, kept, and re-checked against the allowlist in
199
+ * force on every use. A failure is not cached; the next use tries again.
200
+ */
201
+ export declare function dynamicOAuthClient(grant: DynamicOAuthGrant, options: DynamicClientOptions): OAuthClientSource;
202
+ export {};