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.
- package/.env.example +9 -0
- package/CHANGELOG.md +141 -0
- package/DOCUMENTATION.md +107 -10
- package/ONBOARDING.md +50 -11
- package/QUICKSTART.md +1 -1
- package/README.md +22 -2
- package/config/agents/examples/connectors.yaml +82 -0
- package/config/agents/syndicate.schema.json +324 -26
- package/dist/lib/a2a/app.js +3 -0
- package/dist/lib/a2a/oauthSetup.js +20 -5
- package/dist/lib/chatgpt/adapter.d.ts +50 -0
- package/dist/lib/chatgpt/adapter.js +107 -0
- package/dist/lib/chatgpt/cli.d.ts +23 -0
- package/dist/lib/chatgpt/cli.js +77 -0
- package/dist/lib/chatgpt/oauth.d.ts +103 -0
- package/dist/lib/chatgpt/oauth.js +448 -0
- package/dist/lib/chatgpt/state.d.ts +98 -0
- package/dist/lib/chatgpt/state.js +145 -0
- package/dist/lib/chatgpt/store.d.ts +24 -0
- package/dist/lib/chatgpt/store.js +94 -0
- package/dist/lib/compile.js +40 -25
- package/dist/lib/dispatch.js +6 -2
- package/dist/lib/doctor.d.ts +34 -3
- package/dist/lib/doctor.js +63 -5
- package/dist/lib/loadSyndicate.d.ts +6 -0
- package/dist/lib/mcp/server.d.ts +195 -0
- package/dist/lib/mcp/server.js +564 -0
- package/dist/lib/memory/supabaseMemoryService.js +2 -2
- package/dist/lib/models/adapterResolver.js +6 -1
- package/dist/lib/models/gateway.d.ts +1 -1
- package/dist/lib/models/gateway.js +4 -2
- package/dist/lib/onboarding.d.ts +4 -3
- package/dist/lib/onboarding.js +48 -9
- package/dist/lib/runtime/syndicateTurn.js +30 -16
- package/dist/lib/syndicateSchema.d.ts +146 -8
- package/dist/lib/syndicateSchema.js +118 -11
- package/dist/lib/tools/credentialUses.d.ts +7 -1
- package/dist/lib/tools/credentialUses.js +10 -2
- package/dist/lib/tools/generateImageTool.js +2 -1
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
- package/dist/lib/tools/mcpToolFactory.js +71 -25
- package/dist/lib/tools/mcpTransports.d.ts +12 -0
- package/dist/lib/tools/mcpTransports.js +11 -0
- package/dist/lib/tools/oauthConsent.d.ts +18 -3
- package/dist/lib/tools/oauthConsent.js +49 -14
- package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
- package/dist/lib/tools/oauthDiscovery.js +616 -0
- package/dist/lib/tools/oauthTools.d.ts +44 -12
- package/dist/lib/tools/oauthTools.js +79 -22
- package/dist/lib/tools/xApiSearchTool.js +2 -2
- package/dist/scripts/a2a_server.js +8 -0
- package/dist/scripts/assistant_worker.js +11 -1
- package/dist/scripts/doctor.d.ts +1 -1
- package/dist/scripts/doctor.js +1 -1
- package/dist/scripts/mcp_server.d.ts +12 -0
- package/dist/scripts/mcp_server.js +345 -0
- package/dist/scripts/setup.d.ts +2 -0
- package/dist/scripts/setup.js +27 -0
- package/dist/scripts/syndicate_chat.js +12 -3
- package/package.json +5 -2
- package/skills/README.md +2 -2
- package/skills/briefs/skills-readme.md +1 -1
- package/skills/melchizedek-onboard/SKILL.md +28 -6
- package/skills/melchizedek-onboard-keys/SKILL.md +17 -5
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chatgpt/state.ts — where a Sign in with ChatGPT credential lives, whether
|
|
3
|
+
* it is the OpenAI path, and the served-surface refusal (ADR 0126).
|
|
4
|
+
*
|
|
5
|
+
* WHY a leaf (node:fs, node:os, node:path only):
|
|
6
|
+
* The gateway's transport rule (lib/models/gateway.ts), the doctor, the
|
|
7
|
+
* resolver and every served bin ask the same two questions — is a sign-in
|
|
8
|
+
* stored, and does it carry OpenAI ids here — so the answer lives in one
|
|
9
|
+
* module with no adapter or SDK in its import graph.
|
|
10
|
+
*
|
|
11
|
+
* LOCAL ONLY:
|
|
12
|
+
* OpenAI's plan-usage flow is for open-source apps that run on the user's
|
|
13
|
+
* own machine (developers.openai.com/siwc/token-sharing-open-source). The
|
|
14
|
+
* A2A server, the worker and any other served surface call
|
|
15
|
+
* `refuseChatGptSignInOnServedSurface` at startup and stop while the
|
|
16
|
+
* sign-in would carry OpenAI ids; `markServedSurface` also makes the
|
|
17
|
+
* adapter refuse at call time, so a surface that skipped the startup check
|
|
18
|
+
* still never spends a person's ChatGPT plan on someone else's request.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT NEVER DOES: return or print a token. The status carries presence,
|
|
21
|
+
* times and the file's path.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
/** Where the credential file lives, when not the default (a path; never a value). */
|
|
27
|
+
export const CHATGPT_SIGNIN_FILE_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE';
|
|
28
|
+
/** `off` ignores a stored sign-in (a served process on a machine that also signed in). */
|
|
29
|
+
export const CHATGPT_SIGNIN_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN';
|
|
30
|
+
/** The scope that lets the token use the person's ChatGPT plan. */
|
|
31
|
+
export const PLAN_SCOPE = 'chatgpt.tokens.use.direct';
|
|
32
|
+
/** The credential file: MELCHIZEDEK_CHATGPT_SIGNIN_FILE, else ~/.melchizedek/chatgpt-signin.json. */
|
|
33
|
+
export function signInFile(env = process.env) {
|
|
34
|
+
const set = env[CHATGPT_SIGNIN_FILE_ENV]?.trim();
|
|
35
|
+
return set ? resolve(set) : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
|
|
36
|
+
}
|
|
37
|
+
/** The git work tree a path sits inside, if any (an ancestor holding `.git`). */
|
|
38
|
+
export function insideGitWorkTree(file) {
|
|
39
|
+
let dir = dirname(resolve(file));
|
|
40
|
+
for (;;) {
|
|
41
|
+
if (existsSync(join(dir, '.git')))
|
|
42
|
+
return dir;
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir)
|
|
45
|
+
return undefined;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Whether a parsed value has the stored shape. */
|
|
50
|
+
export function isStoredSignIn(v) {
|
|
51
|
+
if (!v || typeof v !== 'object')
|
|
52
|
+
return false;
|
|
53
|
+
const o = v;
|
|
54
|
+
return o.version === 1 && typeof o.issuer === 'string' && typeof o.hostId === 'string';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the credential file: undefined when absent; throws, naming the file
|
|
58
|
+
* and never its content, when it is readable by others or malformed.
|
|
59
|
+
*/
|
|
60
|
+
export function readStoredSignIn(file) {
|
|
61
|
+
if (!existsSync(file))
|
|
62
|
+
return undefined;
|
|
63
|
+
if (process.platform !== 'win32' && (statSync(file).mode & 0o077) !== 0) {
|
|
64
|
+
throw new Error(`${file} is readable by other users; run \`chmod 600 ${file}\` (or sign in again).`);
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`${file} is not a Sign in with ChatGPT credential file; delete it and sign in again.`);
|
|
72
|
+
}
|
|
73
|
+
if (!isStoredSignIn(parsed))
|
|
74
|
+
throw new Error(`${file} is not a Sign in with ChatGPT credential file; delete it and sign in again.`);
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
/** Signed in: a refresh token, and the plan scope granted. */
|
|
78
|
+
export function isSignedIn(s) {
|
|
79
|
+
return !!s?.refreshToken && !!s.accessToken && (s.scopes ?? []).includes(PLAN_SCOPE);
|
|
80
|
+
}
|
|
81
|
+
/** The stored sign-in's status under this environment. */
|
|
82
|
+
export function chatGptSignInStatus(env = process.env) {
|
|
83
|
+
const file = signInFile(env);
|
|
84
|
+
const enabled = env[CHATGPT_SIGNIN_ENV]?.trim().toLowerCase() !== 'off';
|
|
85
|
+
const present = existsSync(file);
|
|
86
|
+
if (!present)
|
|
87
|
+
return { enabled, file, present, signedIn: false };
|
|
88
|
+
try {
|
|
89
|
+
const s = readStoredSignIn(file);
|
|
90
|
+
const signedIn = isSignedIn(s);
|
|
91
|
+
return { enabled, file, present, signedIn, ...(signedIn && s?.expiresAt ? { expiresAt: s.expiresAt } : {}) };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return { enabled, file, present, signedIn: false, problem: err instanceof Error ? err.message : String(err) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether OpenAI ids would run on the sign-in here: it is enabled and signed
|
|
99
|
+
* in, no OPENAI_API_KEY is set, and OpenAI is on its own API (no
|
|
100
|
+
* OPENAI_PLATFORM other than direct, no OPENAI_BASE_URL proxy, which must
|
|
101
|
+
* never receive the token).
|
|
102
|
+
*/
|
|
103
|
+
export function chatGptSignInRoutesOpenAi(env = process.env) {
|
|
104
|
+
if (env.OPENAI_API_KEY?.trim())
|
|
105
|
+
return false;
|
|
106
|
+
const platform = env.OPENAI_PLATFORM?.trim().toLowerCase();
|
|
107
|
+
if (platform && platform !== 'direct')
|
|
108
|
+
return false;
|
|
109
|
+
if (env.OPENAI_BASE_URL?.trim())
|
|
110
|
+
return false;
|
|
111
|
+
const status = chatGptSignInStatus(env);
|
|
112
|
+
return status.enabled && status.signedIn;
|
|
113
|
+
}
|
|
114
|
+
// ── Local only ───────────────────────────────────────────────────────────────
|
|
115
|
+
let served;
|
|
116
|
+
/** Mark this process a served surface: the sign-in adapter refuses every call from here on. */
|
|
117
|
+
export function markServedSurface(surface) {
|
|
118
|
+
served ??= surface;
|
|
119
|
+
}
|
|
120
|
+
/** The served surface this process was marked as, if any. */
|
|
121
|
+
export function servedSurface() {
|
|
122
|
+
return served;
|
|
123
|
+
}
|
|
124
|
+
/** Test seam: forget the mark. */
|
|
125
|
+
export function resetServedSurfaceForTests() {
|
|
126
|
+
served = undefined;
|
|
127
|
+
}
|
|
128
|
+
/** The message a served surface stops with. */
|
|
129
|
+
export function servedSurfaceMessage(surface, env = process.env) {
|
|
130
|
+
return (`${surface} will not start: Sign in with ChatGPT is local only, and it is the OpenAI path here ` +
|
|
131
|
+
`(a sign-in is stored at ${signInFile(env)} and OPENAI_API_KEY is not set). ` +
|
|
132
|
+
`A served surface answers other people, and a ChatGPT plan pays only for its owner's own use on their own machine. ` +
|
|
133
|
+
`Set OPENAI_API_KEY (or Azure OpenAI), or set ${CHATGPT_SIGNIN_ENV}=off for this process, ` +
|
|
134
|
+
`or sign out with \`melchizedek-setup --chatgpt-signout\`.`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Called by every served surface at startup: marks the process served, and
|
|
138
|
+
* throws when the sign-in would carry OpenAI ids here. The A2A app, the A2A
|
|
139
|
+
* server bin and the worker call it; a new served surface calls it too.
|
|
140
|
+
*/
|
|
141
|
+
export function refuseChatGptSignInOnServedSurface(surface, env = process.env) {
|
|
142
|
+
markServedSurface(surface);
|
|
143
|
+
if (chatGptSignInRoutesOpenAi(env))
|
|
144
|
+
throw new Error(servedSurfaceMessage(surface, env));
|
|
145
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chatgpt/store.ts — writing the Sign in with ChatGPT credential file and
|
|
3
|
+
* serializing its refreshes (ADR 0126).
|
|
4
|
+
*
|
|
5
|
+
* The file is the person's own: mode 600 in a mode-700 directory, outside any
|
|
6
|
+
* git work tree, replaced atomically (a new mode-600 file renamed over the
|
|
7
|
+
* old one). OpenAI's refresh tokens rotate, so two processes must not refresh
|
|
8
|
+
* the same session at once (developers.openai.com/siwc/token-sharing-open-source/
|
|
9
|
+
* profiles-and-sessions): a lock file beside the credential serializes them.
|
|
10
|
+
*/
|
|
11
|
+
import type { StoredSignIn } from './state.ts';
|
|
12
|
+
/** Write the credential file: mode 600, atomically, never inside a git work tree. */
|
|
13
|
+
export declare function writeStoredSignIn(file: string, value: StoredSignIn): void;
|
|
14
|
+
/** The stored sign-in with its tokens removed; the host id and the issued client id stay (OpenAI asks apps to reuse them). */
|
|
15
|
+
export declare function withoutTokens(s: StoredSignIn): StoredSignIn;
|
|
16
|
+
/** Remove the tokens from the file, keeping the registration. */
|
|
17
|
+
export declare function clearTokens(file: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Run `fn` holding the credential's lock file (created exclusively). A lock
|
|
20
|
+
* older than a minute is a crashed holder's and is taken over.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withSignInLock<T>(file: string, fn: () => Promise<T>, opts?: {
|
|
23
|
+
waitMs?: number;
|
|
24
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chatgpt/store.ts — writing the Sign in with ChatGPT credential file and
|
|
3
|
+
* serializing its refreshes (ADR 0126).
|
|
4
|
+
*
|
|
5
|
+
* The file is the person's own: mode 600 in a mode-700 directory, outside any
|
|
6
|
+
* git work tree, replaced atomically (a new mode-600 file renamed over the
|
|
7
|
+
* old one). OpenAI's refresh tokens rotate, so two processes must not refresh
|
|
8
|
+
* the same session at once (developers.openai.com/siwc/token-sharing-open-source/
|
|
9
|
+
* profiles-and-sessions): a lock file beside the credential serializes them.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, writeSync } from 'node:fs';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
15
|
+
import { insideGitWorkTree, readStoredSignIn } from './state.js';
|
|
16
|
+
/** Write the credential file: mode 600, atomically, never inside a git work tree. */
|
|
17
|
+
export function writeStoredSignIn(file, value) {
|
|
18
|
+
const repo = insideGitWorkTree(file);
|
|
19
|
+
if (repo) {
|
|
20
|
+
throw new Error(`refusing to store a ChatGPT sign-in inside the git work tree ${repo}; set MELCHIZEDEK_CHATGPT_SIGNIN_FILE to a path outside it.`);
|
|
21
|
+
}
|
|
22
|
+
const dir = dirname(file);
|
|
23
|
+
if (!existsSync(dir))
|
|
24
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
26
|
+
const fd = openSync(tmp, 'wx', 0o600);
|
|
27
|
+
try {
|
|
28
|
+
writeSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
closeSync(fd);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
if (process.platform !== 'win32')
|
|
35
|
+
chmodSync(tmp, 0o600);
|
|
36
|
+
renameSync(tmp, file);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
rmSync(tmp, { force: true });
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The stored sign-in with its tokens removed; the host id and the issued client id stay (OpenAI asks apps to reuse them). */
|
|
44
|
+
export function withoutTokens(s) {
|
|
45
|
+
const { accessToken: _a, refreshToken: _r, expiresAt: _e, earliestRefreshAt: _n, scopes: _s, ...kept } = s;
|
|
46
|
+
return { ...kept, savedAt: new Date().toISOString() };
|
|
47
|
+
}
|
|
48
|
+
/** Remove the tokens from the file, keeping the registration. */
|
|
49
|
+
export function clearTokens(file) {
|
|
50
|
+
const s = readStoredSignIn(file);
|
|
51
|
+
if (s)
|
|
52
|
+
writeStoredSignIn(file, withoutTokens(s));
|
|
53
|
+
}
|
|
54
|
+
const LOCK_STALE_MS = 60_000;
|
|
55
|
+
const LOCK_WAIT_MS = 20_000;
|
|
56
|
+
/**
|
|
57
|
+
* Run `fn` holding the credential's lock file (created exclusively). A lock
|
|
58
|
+
* older than a minute is a crashed holder's and is taken over.
|
|
59
|
+
*/
|
|
60
|
+
export async function withSignInLock(file, fn, opts = {}) {
|
|
61
|
+
const lock = `${file}.lock`;
|
|
62
|
+
const dir = dirname(file);
|
|
63
|
+
if (!existsSync(dir))
|
|
64
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
65
|
+
const deadline = Date.now() + (opts.waitMs ?? LOCK_WAIT_MS);
|
|
66
|
+
for (;;) {
|
|
67
|
+
try {
|
|
68
|
+
closeSync(openSync(lock, 'wx', 0o600));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code !== 'EEXIST')
|
|
73
|
+
throw err;
|
|
74
|
+
try {
|
|
75
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
76
|
+
rmSync(lock, { force: true });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() > deadline)
|
|
84
|
+
throw new Error(`another process holds ${lock}; try again, or delete it if no melchizedek process is running.`);
|
|
85
|
+
await delay(100);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return await fn();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
rmSync(lock, { force: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/lib/compile.js
CHANGED
|
@@ -145,10 +145,10 @@ export function examplesTool(examples) {
|
|
|
145
145
|
const tool = examplesInstructionTool(examples);
|
|
146
146
|
return tool ? [tool] : [];
|
|
147
147
|
}
|
|
148
|
-
async function resolveAgentTools(
|
|
149
|
-
const tools = [...resolveNamedTools(
|
|
148
|
+
async function resolveAgentTools(agent, opts) {
|
|
149
|
+
const tools = [...resolveNamedTools(agent.tools, opts.onUnknownTool), ...examplesTool(agent.examples)];
|
|
150
150
|
// OpenAPI operations become tools here, so require_approval can name them.
|
|
151
|
-
for (const entry of openapi ?? []) {
|
|
151
|
+
for (const entry of agent.openapi ?? []) {
|
|
152
152
|
const built = await buildOpenApiTools(entry);
|
|
153
153
|
opts.log?.(`openapi · ${relative(process.cwd(), entry.spec) || entry.spec}: ${built.map((t) => t.name).join(', ') || '(no operations)'}`);
|
|
154
154
|
for (const t of built) {
|
|
@@ -157,28 +157,43 @@ async function resolveAgentTools(toolNames, mcpServerUrl, opts, openapi, example
|
|
|
157
157
|
tools.push(t);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
160
|
+
/** One server's tools, narrowed to the names the YAML allows (a server's list is its own to change). */
|
|
161
|
+
const offeredBy = async (label, url, allowedNames, oauth2, transport) => {
|
|
162
|
+
opts.log?.(`Loading MCP tools: ${label}`);
|
|
163
|
+
// An authorization_code server lists nothing at startup: its tools are the allowed names, each on its user's own connection (ADR 0112).
|
|
164
|
+
const offered = await createMcpTools(url, { ...(allowedNames ? { tools: allowedNames } : {}), ...(oauth2 ? { oauth2 } : {}), ...(transport ? { transport } : {}) });
|
|
165
|
+
if (!allowedNames)
|
|
166
|
+
return offered;
|
|
167
|
+
// A name the server does not offer is reported, and a gate on it fails the compile in gateTools.
|
|
168
|
+
const allowed = new Set(allowedNames);
|
|
169
|
+
const kept = offered.filter((t) => allowed.has(t.name));
|
|
170
|
+
const missing = [...allowed].filter((n) => !offered.some((t) => t.name === n));
|
|
171
|
+
if (missing.length)
|
|
172
|
+
opts.log?.(`MCP ${label} does not offer ${missing.map((n) => `'${n}'`).join(', ')}`);
|
|
173
|
+
const hidden = offered.length - kept.length;
|
|
174
|
+
if (hidden)
|
|
175
|
+
opts.log?.(`MCP ${label}: ${hidden} tool(s) not named for this agent are not exposed`);
|
|
176
|
+
return kept;
|
|
177
|
+
};
|
|
178
|
+
if (agent.mcp_server_url) {
|
|
179
|
+
// The single-server form: a name the agent already has keeps the agent's tool.
|
|
180
|
+
for (const mcpTool of await offeredBy(agent.mcp_server_url, agent.mcp_server_url, agent.mcp_tools, agent.mcp_auth?.oauth2, agent.mcp_transport)) {
|
|
178
181
|
if (!tools.some((t) => t.name === mcpTool.name))
|
|
179
182
|
tools.push(mcpTool);
|
|
180
183
|
}
|
|
181
184
|
}
|
|
185
|
+
// mcp_servers (ADR 0124): the loader refused a declared name twice; a
|
|
186
|
+
// server that answers with a name already taken is refused here too, so
|
|
187
|
+
// no server's tool can stand in for another's or the agent's own.
|
|
188
|
+
const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
|
|
189
|
+
for (const [i, offered] of servers.entries()) {
|
|
190
|
+
const server = agent.mcp_servers[i];
|
|
191
|
+
for (const mcpTool of offered) {
|
|
192
|
+
if (tools.some((t) => t?.name === mcpTool.name))
|
|
193
|
+
throw new Error(`mcp_servers ${server.name}: tool '${mcpTool.name}' collides with another of this agent's tools (ADR 0124)`);
|
|
194
|
+
tools.push(mcpTool);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
182
197
|
return tools;
|
|
183
198
|
}
|
|
184
199
|
/**
|
|
@@ -413,7 +428,7 @@ export async function compileSubagentSpec(subCfg, opts = {}) {
|
|
|
413
428
|
}
|
|
414
429
|
// YAML v2 (ADR 0115): the engine form, for a config built in code (a loaded one already is).
|
|
415
430
|
const agent = toEngineAgent(subCfg);
|
|
416
|
-
const gated = gateTools(await resolveAgentTools(agent
|
|
431
|
+
const gated = gateTools(await resolveAgentTools(agent, opts), agent.require_approval, agent.name);
|
|
417
432
|
const model = resolveAgentModel(agent.model, opts);
|
|
418
433
|
// model_overrides for the provider the agent runs on, before the skills index is appended.
|
|
419
434
|
const { instruction, tools } = await withSkills(instructionFor(agent, model.modelId), gated, agent.skills, agent.name, opts);
|
|
@@ -452,9 +467,9 @@ export async function compileSpec(config, opts = {}, overrideName, overrideDescr
|
|
|
452
467
|
const orchestrator = toEngineAgent(config.orchestrator); // YAML v2 (ADR 0115), for a config built in code
|
|
453
468
|
const name = overrideName || orchestrator.name;
|
|
454
469
|
// The orchestrator's own tools resolve as a subagent's do: registry
|
|
455
|
-
// names, OpenAPI operations, and the MCP
|
|
456
|
-
// mcp_tools, under its
|
|
457
|
-
const own = gateTools(await resolveAgentTools(orchestrator
|
|
470
|
+
// names, OpenAPI operations, and the MCP servers' tools (narrowed by
|
|
471
|
+
// mcp_tools or each mcp_servers entry's tools, under its grant), so a one-agent syndicate can reach an MCP server.
|
|
472
|
+
const own = gateTools(await resolveAgentTools(orchestrator, opts), orchestrator.require_approval, name);
|
|
458
473
|
const model = resolveAgentModel(orchestrator.model, opts);
|
|
459
474
|
// The skills toolset goes last, after the delegations and the agent's own tools; the
|
|
460
475
|
// skills index goes after the instruction with its model_overrides entry applied.
|
package/dist/lib/dispatch.js
CHANGED
|
@@ -73,6 +73,8 @@ function matchSubagent(candidate, subagents) {
|
|
|
73
73
|
return undefined;
|
|
74
74
|
return subagents.find(sub => norm(sub.name) === target);
|
|
75
75
|
}
|
|
76
|
+
/** What a subagent name can look like: one token, linear to test. */
|
|
77
|
+
const ROUTE_SHAPED = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
76
78
|
/**
|
|
77
79
|
* Turn the router's raw output into a route that definitely exists.
|
|
78
80
|
*
|
|
@@ -108,7 +110,8 @@ export function resolveRoute(rawOutput, config) {
|
|
|
108
110
|
parsed = JSON.parse(unfence(rawOutput));
|
|
109
111
|
}
|
|
110
112
|
catch {
|
|
111
|
-
|
|
113
|
+
// The reason reaches the server log: the size, never the text (the router can echo the person).
|
|
114
|
+
return defaultResolution(`router output was not JSON (${rawOutput.trim().length} chars)`);
|
|
112
115
|
}
|
|
113
116
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
114
117
|
return defaultResolution('router output was not a JSON object');
|
|
@@ -120,7 +123,8 @@ export function resolveRoute(rawOutput, config) {
|
|
|
120
123
|
}
|
|
121
124
|
const matched = matchSubagent(rawRoute, subagents);
|
|
122
125
|
if (!matched) {
|
|
123
|
-
|
|
126
|
+
// A route-shaped value is named; anything else is model text and is only measured.
|
|
127
|
+
return defaultResolution(ROUTE_SHAPED.test(rawRoute) ? `router chose unknown route '${rawRoute}'` : `router chose an unknown route (${rawRoute.length} chars)`);
|
|
124
128
|
}
|
|
125
129
|
const rawReason = record[reasonKey];
|
|
126
130
|
return {
|
package/dist/lib/doctor.d.ts
CHANGED
|
@@ -167,6 +167,25 @@ export interface DoctorResult {
|
|
|
167
167
|
providers: ProviderPath[];
|
|
168
168
|
/** How the A2A server would authenticate and bill, when any of its variables is set. */
|
|
169
169
|
serving?: DoctorServing;
|
|
170
|
+
/** A stored Sign in with ChatGPT credential (ADR 0126), when one is present or its variables are set. */
|
|
171
|
+
chatgpt?: DoctorChatGpt;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A Sign in with ChatGPT credential (ADR 0126): where it is, whether it is
|
|
175
|
+
* signed in and carries OpenAI ids here, and what stops it. Never a token.
|
|
176
|
+
*/
|
|
177
|
+
export interface DoctorChatGpt {
|
|
178
|
+
file: string;
|
|
179
|
+
/** False when MELCHIZEDEK_CHATGPT_SIGNIN=off. */
|
|
180
|
+
enabled: boolean;
|
|
181
|
+
present: boolean;
|
|
182
|
+
signedIn: boolean;
|
|
183
|
+
/** OpenAI ids run on it here (no OPENAI_API_KEY, OpenAI on its own API). */
|
|
184
|
+
routesOpenAi: boolean;
|
|
185
|
+
/** Epoch milliseconds the stored access token expires (it refreshes on use). */
|
|
186
|
+
expiresAt?: number;
|
|
187
|
+
/** A served configuration on it, or a file that cannot be used. `--check` fails on any. */
|
|
188
|
+
problems: string[];
|
|
170
189
|
}
|
|
171
190
|
/**
|
|
172
191
|
* How one provider would be reached under this environment (the doctor's
|
|
@@ -183,6 +202,8 @@ export interface ProviderPath {
|
|
|
183
202
|
/** `local` for Ollama; a cloud platform (vertex, bedrock, azure) or `direct` otherwise. */
|
|
184
203
|
platform: Platform | 'local';
|
|
185
204
|
keyEnv: string | null;
|
|
205
|
+
/** `chatgpt-signin` when a stored Sign in with ChatGPT funds the provider instead of its key (ADR 0126). */
|
|
206
|
+
credential?: 'chatgpt-signin';
|
|
186
207
|
}
|
|
187
208
|
/**
|
|
188
209
|
* The A2A server's identity and billing settings (scripts/a2a_server.ts),
|
|
@@ -263,11 +284,21 @@ export declare function runDoctor(options?: {
|
|
|
263
284
|
agentsDir?: string;
|
|
264
285
|
scripts?: Record<string, string>;
|
|
265
286
|
}): DoctorResult;
|
|
287
|
+
/**
|
|
288
|
+
* The Sign in with ChatGPT line (ADR 0126), when a credential file is present
|
|
289
|
+
* or one of its variables is set. `served`: a serving variable is set, so
|
|
290
|
+
* this configuration is meant for melchizedek-serve, which refuses to start
|
|
291
|
+
* while the sign-in is the OpenAI path.
|
|
292
|
+
*/
|
|
293
|
+
export declare function chatGptReport(served: boolean, env?: NodeJS.ProcessEnv): DoctorChatGpt | undefined;
|
|
266
294
|
/**
|
|
267
295
|
* Why `melchizedek-doctor --check` exits non-zero, one line each; empty when
|
|
268
|
-
* it passes. A blocked syndicate, a runtime problem,
|
|
269
|
-
*
|
|
270
|
-
*
|
|
296
|
+
* it passes. A blocked syndicate, a runtime problem, a `MODEL_GATEWAY` that
|
|
297
|
+
* names no gateway (named with the accepted values only: the value may be a
|
|
298
|
+
* key pasted into the wrong variable), the OAuth setup's and the credential
|
|
299
|
+
* allowlist's problems, and each grant whose hosts the OAuth host allowlist
|
|
300
|
+
* refuses (ADR 0114), and the Sign in with ChatGPT line's problems (ADR 0126).
|
|
301
|
+
* Names and hosts only, never a value.
|
|
271
302
|
*/
|
|
272
303
|
export declare function checkProblems(result: DoctorResult): string[];
|
|
273
304
|
export declare function renderDoctor(result: DoctorResult, opts?: {
|
package/dist/lib/doctor.js
CHANGED
|
@@ -34,6 +34,7 @@ import { credentialSource, endpointFromEnv, endpointLabel, endpointProblems, PLA
|
|
|
34
34
|
import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAYS, gatewayConfig, gatewayProblem, gatewayUsable, } from './models/gateway.js';
|
|
35
35
|
import { PROVIDERS } from './models/providerMap.js';
|
|
36
36
|
import { describeRuntime } from './runtime/runtimeFlag.js';
|
|
37
|
+
import { CHATGPT_SIGNIN_ENV, CHATGPT_SIGNIN_FILE_ENV, chatGptSignInRoutesOpenAi, chatGptSignInStatus } from './chatgpt/state.js';
|
|
37
38
|
import { agentOAuthGrants, oauthEnvNames } from './tools/oauthTools.js';
|
|
38
39
|
import { grantHostProblems, OAUTH_REDIRECT_URI_ENV, oauthEnvProblems } from './a2a/oauthSetup.js';
|
|
39
40
|
import { CREDENTIAL_KEY_ENV } from './tools/credentialCipher.js';
|
|
@@ -41,6 +42,8 @@ import { OAUTH_HOSTS_ENV } from './tools/oauthHosts.js';
|
|
|
41
42
|
import { CREDENTIAL_HOSTS_ENV, credentialHosts } from './tools/credentialHosts.js';
|
|
42
43
|
import { syndicateCredentialUses } from './tools/credentialUses.js';
|
|
43
44
|
import { credentialHostEnvProblems, unboundCredentialWarning } from './a2a/oauthSetup.js';
|
|
45
|
+
/** The `gateway.id` the doctor reports when `MODEL_GATEWAY` names no known gateway; the value itself is never kept. */
|
|
46
|
+
const UNRECOGNISED_GATEWAY = 'unrecognised';
|
|
44
47
|
function resolvable(module) {
|
|
45
48
|
try {
|
|
46
49
|
createRequire(import.meta.url).resolve(module);
|
|
@@ -115,6 +118,7 @@ export function providerPaths() {
|
|
|
115
118
|
...(r.gateway ? { gateway: r.gateway } : {}),
|
|
116
119
|
platform: p === 'ollama' ? 'local' : (r.platform ?? 'direct'),
|
|
117
120
|
keyEnv: r.keyEnv,
|
|
121
|
+
...(p === 'openai' && r.funded && r.transport === 'direct' && chatGptSignInRoutesOpenAi() ? { credential: 'chatgpt-signin' } : {}),
|
|
118
122
|
};
|
|
119
123
|
});
|
|
120
124
|
}
|
|
@@ -484,7 +488,7 @@ export function runDoctor(options = {}) {
|
|
|
484
488
|
const gateway = rawGateway
|
|
485
489
|
? {
|
|
486
490
|
// An unrecognised value is never echoed: it may be a key pasted into the wrong variable.
|
|
487
|
-
id: cfg?.gateway.id ??
|
|
491
|
+
id: cfg?.gateway.id ?? UNRECOGNISED_GATEWAY,
|
|
488
492
|
label: cfg?.gateway.label ?? 'an unrecognised gateway',
|
|
489
493
|
usable: gatewayUsable(),
|
|
490
494
|
...(gatewayProblem() ? { problem: gatewayProblem() } : {}),
|
|
@@ -523,6 +527,7 @@ export function runDoctor(options = {}) {
|
|
|
523
527
|
credentials = { set: credentialSet, envs, ...(warning ? { warning } : {}), problems };
|
|
524
528
|
}
|
|
525
529
|
const serving = servingReport();
|
|
530
|
+
const chatgpt = chatGptReport(!!serving);
|
|
526
531
|
return {
|
|
527
532
|
agentsDir,
|
|
528
533
|
runtime: runtimeReport(),
|
|
@@ -535,6 +540,34 @@ export function runDoctor(options = {}) {
|
|
|
535
540
|
...(credentials ? { credentials } : {}),
|
|
536
541
|
providers: providerPaths(),
|
|
537
542
|
...(serving ? { serving } : {}),
|
|
543
|
+
...(chatgpt ? { chatgpt } : {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* The Sign in with ChatGPT line (ADR 0126), when a credential file is present
|
|
548
|
+
* or one of its variables is set. `served`: a serving variable is set, so
|
|
549
|
+
* this configuration is meant for melchizedek-serve, which refuses to start
|
|
550
|
+
* while the sign-in is the OpenAI path.
|
|
551
|
+
*/
|
|
552
|
+
export function chatGptReport(served, env = process.env) {
|
|
553
|
+
const status = chatGptSignInStatus(env);
|
|
554
|
+
if (!status.present && !env[CHATGPT_SIGNIN_FILE_ENV]?.trim() && !env[CHATGPT_SIGNIN_ENV]?.trim())
|
|
555
|
+
return undefined;
|
|
556
|
+
const routesOpenAi = chatGptSignInRoutesOpenAi(env);
|
|
557
|
+
const problems = [];
|
|
558
|
+
if (status.problem)
|
|
559
|
+
problems.push(status.problem);
|
|
560
|
+
if (served && routesOpenAi) {
|
|
561
|
+
problems.push(`Sign in with ChatGPT is local only, and it is the OpenAI path in this served configuration: melchizedek-serve and melchizedek-worker refuse to start. Set OPENAI_API_KEY (or Azure OpenAI), or ${CHATGPT_SIGNIN_ENV}=off for the served process.`);
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
file: status.file,
|
|
565
|
+
enabled: status.enabled,
|
|
566
|
+
present: status.present,
|
|
567
|
+
signedIn: status.signedIn,
|
|
568
|
+
routesOpenAi,
|
|
569
|
+
...(status.expiresAt ? { expiresAt: status.expiresAt } : {}),
|
|
570
|
+
problems,
|
|
538
571
|
};
|
|
539
572
|
}
|
|
540
573
|
// ── Rendering ────────────────────────────────────────────────────────────────
|
|
@@ -558,9 +591,12 @@ const PROBE_ID = {
|
|
|
558
591
|
};
|
|
559
592
|
/**
|
|
560
593
|
* Why `melchizedek-doctor --check` exits non-zero, one line each; empty when
|
|
561
|
-
* it passes. A blocked syndicate, a runtime problem,
|
|
562
|
-
*
|
|
563
|
-
*
|
|
594
|
+
* it passes. A blocked syndicate, a runtime problem, a `MODEL_GATEWAY` that
|
|
595
|
+
* names no gateway (named with the accepted values only: the value may be a
|
|
596
|
+
* key pasted into the wrong variable), the OAuth setup's and the credential
|
|
597
|
+
* allowlist's problems, and each grant whose hosts the OAuth host allowlist
|
|
598
|
+
* refuses (ADR 0114), and the Sign in with ChatGPT line's problems (ADR 0126).
|
|
599
|
+
* Names and hosts only, never a value.
|
|
564
600
|
*/
|
|
565
601
|
export function checkProblems(result) {
|
|
566
602
|
const problems = [];
|
|
@@ -568,10 +604,15 @@ export function checkProblems(result) {
|
|
|
568
604
|
problems.push(`${result.counts.blocked} syndicate(s) blocked`);
|
|
569
605
|
if (result.runtime.problem)
|
|
570
606
|
problems.push(result.runtime.problem);
|
|
607
|
+
if (result.gateway?.id === UNRECOGNISED_GATEWAY) {
|
|
608
|
+
problems.push(`gateway: ${GATEWAY_ENV} is set to an unrecognised value (not shown); it must be one of: ${Object.keys(GATEWAYS).join(', ')}`);
|
|
609
|
+
}
|
|
571
610
|
for (const p of result.oauth?.problems ?? [])
|
|
572
611
|
problems.push(`oauth: ${p}`);
|
|
573
612
|
for (const p of result.credentials?.problems ?? [])
|
|
574
613
|
problems.push(`credentials: ${p}`);
|
|
614
|
+
for (const p of result.chatgpt?.problems ?? [])
|
|
615
|
+
problems.push(`chatgpt: ${p}`);
|
|
575
616
|
for (const s of result.syndicates) {
|
|
576
617
|
for (const g of s.grants ?? [])
|
|
577
618
|
for (const p of g.hostProblems ?? [])
|
|
@@ -607,7 +648,7 @@ export function renderDoctor(result, opts = {}) {
|
|
|
607
648
|
// xAI's key beside Moonshot).
|
|
608
649
|
const providerBits = (result.providers ?? providerPaths()).map((r) => {
|
|
609
650
|
const mark = !r.funded ? `${c.red}✗${c.reset}` : r.transport === 'gateway' ? `${c.yellow}◇${c.reset}` : `${c.green}✓${c.reset}`;
|
|
610
|
-
const how = !r.funded ? `${c.dim}${r.keyEnv} not set${c.reset}` : r.transport === 'gateway' ? `${c.dim}via gateway:${r.gateway}${c.reset}` : r.platform === 'local' ? `${c.dim}local${c.reset}` : `${c.dim}${r.platform}${c.reset}`;
|
|
651
|
+
const how = !r.funded ? `${c.dim}${r.keyEnv} not set${c.reset}` : r.transport === 'gateway' ? `${c.dim}via gateway:${r.gateway}${c.reset}` : r.platform === 'local' ? `${c.dim}local${c.reset}` : r.credential === 'chatgpt-signin' ? `${c.dim}ChatGPT sign-in, local only${c.reset}` : `${c.dim}${r.platform}${c.reset}`;
|
|
611
652
|
return `${mark} ${r.label} ${how}`;
|
|
612
653
|
});
|
|
613
654
|
lines.push('providers ' + providerBits.join(' '));
|
|
@@ -632,6 +673,23 @@ export function renderDoctor(result, opts = {}) {
|
|
|
632
673
|
for (const p of sv.problems)
|
|
633
674
|
lines.push(` ${c.red}✗ ${p}${c.reset}`);
|
|
634
675
|
}
|
|
676
|
+
if (result.chatgpt) {
|
|
677
|
+
// Presence and the file's path, never a token (ADR 0126).
|
|
678
|
+
const g = result.chatgpt;
|
|
679
|
+
const mark = g.problems.length ? `${c.red}✗${c.reset}` : g.routesOpenAi ? `${c.green}✓${c.reset}` : `${c.dim}·${c.reset}`;
|
|
680
|
+
const state = !g.enabled
|
|
681
|
+
? `${CHATGPT_SIGNIN_ENV}=off, ignored`
|
|
682
|
+
: !g.present
|
|
683
|
+
? 'not signed in'
|
|
684
|
+
: !g.signedIn
|
|
685
|
+
? 'signed out (run melchizedek-setup --chatgpt-signin)'
|
|
686
|
+
: g.routesOpenAi
|
|
687
|
+
? 'signed in · carries OpenAI ids · local only'
|
|
688
|
+
: 'signed in · not used (OPENAI_API_KEY or another OpenAI endpoint wins)';
|
|
689
|
+
lines.push(`chatgpt ${mark} ${state} ${c.dim}· ${g.file}${c.reset}`);
|
|
690
|
+
for (const p of g.problems)
|
|
691
|
+
lines.push(` ${c.red}✗ ${p}${c.reset}`);
|
|
692
|
+
}
|
|
635
693
|
if (result.oauth) {
|
|
636
694
|
// Names only: which variables are set, never their values (ADR 0114).
|
|
637
695
|
const set = Object.entries(result.oauth.set).map(([name, on]) => `${name} ${on ? 'set' : 'unset'}`).join(' · ');
|
|
@@ -3,6 +3,8 @@ export type { DispatchConfig } from './dispatch.ts';
|
|
|
3
3
|
import type { SkillsConfig } from './tools/skillToolset.ts';
|
|
4
4
|
import type { OpenApiConfig } from './tools/openapiTools.ts';
|
|
5
5
|
import type { McpAuthConfig } from './tools/oauthTools.ts';
|
|
6
|
+
import type { McpTransportKind } from './tools/mcpTransports.ts';
|
|
7
|
+
import type { McpServerConfig } from './tools/mcpToolFactory.ts';
|
|
6
8
|
import type { ContextConfig, ExampleConfig } from './compile.ts';
|
|
7
9
|
export type { OpenApiConfig } from './tools/openapiTools.ts';
|
|
8
10
|
export type { SkillsConfig } from './tools/skillToolset.ts';
|
|
@@ -153,6 +155,10 @@ export interface AgentYamlConfig {
|
|
|
153
155
|
mcp_tools?: string[];
|
|
154
156
|
/** The OAuth grant the MCP server takes (ADR 0112): each user's, or the server's own. */
|
|
155
157
|
mcp_auth?: McpAuthConfig;
|
|
158
|
+
/** How mcp_server_url is reached: auto (the default), streamable_http or sse (ADR 0124). */
|
|
159
|
+
mcp_transport?: McpTransportKind;
|
|
160
|
+
/** Several MCP servers, each with its tools, grant and transport (ADR 0124). Not with mcp_server_url. */
|
|
161
|
+
mcp_servers?: McpServerConfig[];
|
|
156
162
|
/** Answers when `model` fails provider-side, or its provider's circuit is open (ADR 0044). */
|
|
157
163
|
fallback_model?: string;
|
|
158
164
|
/**
|