melchizedek-agents 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -2
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +524 -0
- package/DOCUMENTATION.md +178 -43
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +6 -2
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +7 -7
- package/config/agents/examples/critic.yaml +24 -24
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +37 -35
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +859 -13
- package/config/agents/syndicateSchema.yaml +122 -73
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +64 -62
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +24 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +64 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +43 -5
- package/dist/lib/a2a/oauthSetup.d.ts +106 -0
- package/dist/lib/a2a/oauthSetup.js +259 -0
- package/dist/lib/agentDialect.d.ts +76 -0
- package/dist/lib/agentDialect.js +151 -0
- package/dist/lib/compile.d.ts +42 -6
- package/dist/lib/compile.js +97 -45
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +20 -1
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +110 -1
- package/dist/lib/doctor.js +205 -9
- package/dist/lib/loadSyndicate.d.ts +54 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +25 -1
- package/dist/lib/models/capabilities.js +79 -1
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +2 -1
- package/dist/lib/models/gateway.js +4 -2
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/onboarding.d.ts +134 -0
- package/dist/lib/onboarding.js +539 -0
- package/dist/lib/runtime/approvals.d.ts +8 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
- package/dist/lib/runtime/native/agentLoop.js +107 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
- package/dist/lib/runtime/native/checkpoint.js +359 -0
- package/dist/lib/runtime/native/delegate.d.ts +222 -18
- package/dist/lib/runtime/native/delegate.js +390 -39
- package/dist/lib/runtime/native/interrupts.d.ts +184 -2
- package/dist/lib/runtime/native/interrupts.js +391 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
- package/dist/lib/runtime/syndicateTurn.js +353 -68
- package/dist/lib/runtime/turnControl.d.ts +56 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +12 -2
- package/dist/lib/storage/postgres/index.js +8 -3
- package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
- package/dist/lib/storage/postgres/taskQueue.js +29 -5
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +316 -1
- package/dist/lib/syndicateSchema.js +203 -29
- package/dist/lib/tools/auth.d.ts +20 -6
- package/dist/lib/tools/auth.js +21 -5
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/credentialUses.d.ts +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +73 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +242 -0
- package/dist/lib/tools/oauthTools.js +447 -0
- package/dist/lib/tools/openapiTools.d.ts +11 -6
- package/dist/lib/tools/openapiTools.js +88 -38
- package/dist/lib/tools/taskTools.d.ts +61 -6
- package/dist/lib/tools/taskTools.js +145 -21
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +14 -7
- package/dist/lib/workflow.js +14 -7
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +131 -1
- package/dist/scripts/assistant_worker.d.ts +12 -0
- package/dist/scripts/assistant_worker.js +81 -22
- package/dist/scripts/doctor.d.ts +7 -2
- package/dist/scripts/doctor.js +9 -4
- package/dist/scripts/setup.d.ts +23 -0
- package/dist/scripts/setup.js +154 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/yaml_codemod.d.ts +62 -0
- package/dist/scripts/yaml_codemod.js +652 -0
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +7 -4
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- package/skills/melchizedek-models/SKILL.md +11 -4
- package/skills/melchizedek-onboard/SKILL.md +49 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/onboarding.ts — the authentication levels and their startup guides.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* A newcomer arrives with something: no key, one provider's key, several, a
|
|
6
|
+
* gateway key, a cloud account, a server to put in front of other people,
|
|
7
|
+
* or a ChatGPT / Claude.ai / Gemini CLI sign-in. Each of those is a
|
|
8
|
+
* different first ten minutes. This module names those levels once, from
|
|
9
|
+
* what the engine actually supports (lib/models/providerMap.ts,
|
|
10
|
+
* endpoints.ts, gateway.ts, the A2A server's identity modes and the OAuth
|
|
11
|
+
* grant setup), and renders one startup guide per level. The
|
|
12
|
+
* `melchizedek-setup` menu prints these guides and ONBOARDING.md is
|
|
13
|
+
* generated from the same function, so the two cannot drift
|
|
14
|
+
* (tests/onboarding.test.ts holds them equal).
|
|
15
|
+
*
|
|
16
|
+
* DETECTION IS THE DOCTOR'S:
|
|
17
|
+
* `detectLevels` reads a DoctorResult (lib/doctor.ts: the providers line,
|
|
18
|
+
* the gateway, the cloud endpoints, the serving and OAuth lines). It never
|
|
19
|
+
* reads a key itself, so `--auto` cannot disagree with the doctor.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IT NEVER DOES:
|
|
22
|
+
* print a value. Guides carry variable NAMES and SHAPES; detection carries
|
|
23
|
+
* names and set/unset. Writing `.env` (writeEnvForLevel) copies
|
|
24
|
+
* `.env.example`, leaves every chosen name blank, refuses to overwrite an
|
|
25
|
+
* existing file, and refuses unless `git check-ignore` says `.env` is
|
|
26
|
+
* ignored (secrets-hygiene).
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { closeSync, existsSync, fchmodSync, openSync, readFileSync, readdirSync, writeSync } from 'node:fs';
|
|
30
|
+
import { dirname, join, resolve } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { diagnoseSyndicate } from './doctor.js';
|
|
33
|
+
import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAYS } from './models/gateway.js';
|
|
34
|
+
import { PROVIDERS } from './models/providerMap.js';
|
|
35
|
+
import { CREDENTIAL_KEY_ENV } from './tools/credentialCipher.js';
|
|
36
|
+
import { OAUTH_HOSTS_ENV } from './tools/oauthHosts.js';
|
|
37
|
+
import { OAUTH_REDIRECT_URI_ENV } from './a2a/oauthSetup.js';
|
|
38
|
+
// ── Where the package lives ──────────────────────────────────────────────────
|
|
39
|
+
/** The package (or clone) root: the directory holding config/agents/syndicate.schema.json. */
|
|
40
|
+
export function packageRoot(from = fileURLToPath(import.meta.url)) {
|
|
41
|
+
let dir = dirname(from);
|
|
42
|
+
for (let i = 0; i < 6; i++) {
|
|
43
|
+
if (existsSync(join(dir, 'config', 'agents', 'syndicate.schema.json')))
|
|
44
|
+
return dir;
|
|
45
|
+
const parent = dirname(dir);
|
|
46
|
+
if (parent === dir)
|
|
47
|
+
break;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('config/agents/ not found beside the package');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Index the shipped templates and examples by tier, using the doctor's own
|
|
54
|
+
* diagnosis (lib/doctor.ts `diagnoseSyndicate`: the tier the models imply,
|
|
55
|
+
* and the OAuth grants the tools declare). The tier does not depend on the
|
|
56
|
+
* environment, so the index is the same wherever it is computed.
|
|
57
|
+
*/
|
|
58
|
+
export function shippedIndex(root = packageRoot()) {
|
|
59
|
+
const agentsDir = join(root, 'config', 'agents');
|
|
60
|
+
const index = { byTier: {}, withGrants: [], grantExamples: [], templates: [] };
|
|
61
|
+
for (const dir of ['templates', 'examples']) {
|
|
62
|
+
const full = join(agentsDir, dir);
|
|
63
|
+
if (!existsSync(full))
|
|
64
|
+
continue;
|
|
65
|
+
for (const f of readdirSync(full).filter((x) => x.endsWith('.yaml')).sort()) {
|
|
66
|
+
const name = f.slice(0, -5);
|
|
67
|
+
const label = `${name} (${dir.slice(0, -1)})`;
|
|
68
|
+
if (dir === 'templates')
|
|
69
|
+
index.templates.push(name);
|
|
70
|
+
const d = diagnoseSyndicate(`${dir}/${f}`, agentsDir);
|
|
71
|
+
if (d.error)
|
|
72
|
+
continue;
|
|
73
|
+
(index.byTier[d.tier] ??= []).push(label);
|
|
74
|
+
if (d.grants?.length)
|
|
75
|
+
index.withGrants.push(label);
|
|
76
|
+
else if (/^\s*#\s+oauth2:\s*$/m.test(readFileSync(join(full, f), 'utf-8')))
|
|
77
|
+
index.grantExamples.push(label);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return index;
|
|
81
|
+
}
|
|
82
|
+
// ── Commands, spelled per form ───────────────────────────────────────────────
|
|
83
|
+
const CMD = {
|
|
84
|
+
doctor: { package: 'npx melchizedek-doctor', clone: 'npm run doctor' },
|
|
85
|
+
setup: { package: 'npx melchizedek-setup', clone: 'npm run setup --' },
|
|
86
|
+
};
|
|
87
|
+
function init(form, template) {
|
|
88
|
+
return form === 'package' ? `npx melchizedek-init --template ${template}` : `npm run init -- --template ${template}`;
|
|
89
|
+
}
|
|
90
|
+
function chat(form, name) {
|
|
91
|
+
return form === 'package' ? `npx melchizedek-chat --syndicate ${name}` : `npm run chat:syndicate -- --syndicate ${name}`;
|
|
92
|
+
}
|
|
93
|
+
function serve(form, file) {
|
|
94
|
+
return form === 'package' ? `npx melchizedek-serve ${file}` : `npm run start:a2a -- ${file}`;
|
|
95
|
+
}
|
|
96
|
+
function newCaller(form, name) {
|
|
97
|
+
const flags = `--new-caller ${name} --token-file "$HOME/${name}.token"`;
|
|
98
|
+
return form === 'package' ? `npx melchizedek-serve ${flags}` : `npm run start:a2a -- ${flags}`;
|
|
99
|
+
}
|
|
100
|
+
// ── The levels ───────────────────────────────────────────────────────────────
|
|
101
|
+
const PROVIDER_CONSOLE = {
|
|
102
|
+
gemini: 'https://aistudio.google.com',
|
|
103
|
+
anthropic: 'https://console.anthropic.com',
|
|
104
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
105
|
+
xai: 'https://console.x.ai',
|
|
106
|
+
moonshot: 'https://platform.moonshot.ai',
|
|
107
|
+
};
|
|
108
|
+
const PREFIX = {
|
|
109
|
+
gemini: 'gemini-* (and any id without another prefix)',
|
|
110
|
+
anthropic: 'claude-*',
|
|
111
|
+
openai: 'gpt-*, o<digit>*',
|
|
112
|
+
xai: 'grok-*',
|
|
113
|
+
moonshot: 'kimi-*',
|
|
114
|
+
};
|
|
115
|
+
const CLOUD_PROVIDERS = Object.keys(PROVIDER_CONSOLE);
|
|
116
|
+
function providerKeyLines() {
|
|
117
|
+
return CLOUD_PROVIDERS.map((p) => ({
|
|
118
|
+
name: PROVIDERS[p].keyEnv,
|
|
119
|
+
shape: `an API key from ${PROVIDER_CONSOLE[p]}`,
|
|
120
|
+
purpose: `${PROVIDERS[p].label}: model ids ${PREFIX[p]}${p === 'gemini' ? '; also the default embedder for long-term memory (GEMINI_API_KEY is accepted too)' : ''}`,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
function list(items) {
|
|
124
|
+
return items?.length ? items.join(', ') : 'no shipped file yet (change a `model:` line to use one)';
|
|
125
|
+
}
|
|
126
|
+
const SERVE_POSTURE = [
|
|
127
|
+
{ name: 'PUBLIC_URL', shape: 'https://agents.example.com', purpose: 'only on a public deployment; it then requires the three below' },
|
|
128
|
+
{ name: 'A2A_SERVED_AGENTS', shape: 'comma list of agent ids, or *', purpose: 'which agents the server answers (required with PUBLIC_URL)' },
|
|
129
|
+
{ name: 'A2A_TRUST_PROXY', shape: '1 behind one load balancer, or false', purpose: 'required with PUBLIC_URL' },
|
|
130
|
+
];
|
|
131
|
+
export const LEVELS = [
|
|
132
|
+
{
|
|
133
|
+
id: 'local',
|
|
134
|
+
menu: 'Nothing yet: run open-weight models locally (Ollama, no key)',
|
|
135
|
+
title: 'No keys: local models only',
|
|
136
|
+
summary: 'Every agent whose model id starts with `ollama/` runs on your machine through Ollama. No account, no key, and no data leaves the machine. Install Ollama from https://ollama.com and pull a model: `ollama pull qwen3:8b`.',
|
|
137
|
+
env: [
|
|
138
|
+
{ name: 'OLLAMA_BASE_URL', shape: 'http://localhost:11434/v1', purpose: 'optional: only for a non-default Ollama endpoint' },
|
|
139
|
+
],
|
|
140
|
+
confirm: 'the providers line shows `Ollama (local) local`, and the keyless files read `ready — local, no key`.',
|
|
141
|
+
runs: (s) => [`keyless: ${list(s.byTier.keyless)}`],
|
|
142
|
+
first: {
|
|
143
|
+
package: [init('package', 'conversational'), chat('package', 'conversational')],
|
|
144
|
+
clone: ['npm run syndicate:assistant'],
|
|
145
|
+
},
|
|
146
|
+
notes: [
|
|
147
|
+
'Server-side search tools (web_search, google_search, x_search) are not available on Ollama; the doctor lists what each agent drops.',
|
|
148
|
+
'Ollama serves a 4,096-token context by default; start it with `OLLAMA_CONTEXT_LENGTH=16384 ollama serve` before summarizing web pages.',
|
|
149
|
+
],
|
|
150
|
+
skill: 'melchizedek-onboard-local',
|
|
151
|
+
detectable: true,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'one-provider',
|
|
155
|
+
menu: "One provider's API key (Gemini, Anthropic, OpenAI, xAI or Moonshot)",
|
|
156
|
+
title: "One provider's API key",
|
|
157
|
+
summary: "The model id's prefix picks the provider, and that provider's key funds it. One key runs every file whose agents all use that provider. A Gemini key runs the most shipped files and also powers long-term memory's embeddings.",
|
|
158
|
+
env: providerKeyLines(),
|
|
159
|
+
confirm: 'the providers line shows a ✓ beside your provider, and the files of that tier read `ready`.',
|
|
160
|
+
runs: (s) => [
|
|
161
|
+
...CLOUD_PROVIDERS.map((p) => `${PROVIDERS[p].keyEnv}: ${list(s.byTier[p])}`),
|
|
162
|
+
`with no key at all: ${list(s.byTier.keyless)}`,
|
|
163
|
+
],
|
|
164
|
+
first: {
|
|
165
|
+
package: [init('package', 'research_brief'), chat('package', 'research_brief')],
|
|
166
|
+
clone: ['npm run chat:syndicate', 'npm run syndicate:claude # with ANTHROPIC_API_KEY instead'],
|
|
167
|
+
},
|
|
168
|
+
notes: [
|
|
169
|
+
'Set only the one variable you have; leave the others blank. The doctor names, per file, the variable that would unblock it.',
|
|
170
|
+
'To run a shipped file on your provider, change its `model:` lines (for example `gemini-3.8-flash` to `claude-sonnet-4-6`); `doctor` then shows it ready.',
|
|
171
|
+
],
|
|
172
|
+
skill: 'melchizedek-onboard-keys',
|
|
173
|
+
detectable: true,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'several-providers',
|
|
177
|
+
menu: 'API keys for several providers',
|
|
178
|
+
title: 'Several providers',
|
|
179
|
+
summary: 'Each key funds its own provider, and a syndicate may mix them: one agent on Claude, another on Gemini, a third local. A direct key always serves its provider at full fidelity (native search included).',
|
|
180
|
+
env: providerKeyLines(),
|
|
181
|
+
confirm: 'a ✓ beside each provider you set; `multi-provider` files read `ready` once every provider they use has its key.',
|
|
182
|
+
runs: (s) => [
|
|
183
|
+
`multi-provider: ${list(s.byTier['multi-provider'])}`,
|
|
184
|
+
'plus every single-provider file whose key you set (see level 2)',
|
|
185
|
+
],
|
|
186
|
+
first: {
|
|
187
|
+
package: [init('package', 'model_zoo'), chat('package', 'model_zoo')],
|
|
188
|
+
clone: ['npm run demo:models # one prompt to every provider you have a key for'],
|
|
189
|
+
},
|
|
190
|
+
notes: [
|
|
191
|
+
'A provider you have no key for can still be served by a gateway (level 4); a direct key set beside it wins for its own provider.',
|
|
192
|
+
],
|
|
193
|
+
skill: 'melchizedek-onboard-keys',
|
|
194
|
+
detectable: true,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'gateway',
|
|
198
|
+
menu: 'One gateway key (Vercel AI Gateway or OpenRouter)',
|
|
199
|
+
title: 'A gateway key',
|
|
200
|
+
summary: "One key serves every cloud model id whose direct key is absent, through the gateway's OpenAI-compatible endpoint. It is a fallback, never a preempt: a direct key always wins for its provider. Ollama never goes through it.",
|
|
201
|
+
env: [
|
|
202
|
+
{ name: GATEWAY_ENV, shape: 'vercel | openrouter', purpose: 'which gateway' },
|
|
203
|
+
{ name: GATEWAY_KEY_ENV, shape: 'the gateway key (https://vercel.com/ai-gateway or https://openrouter.ai/keys)', purpose: 'funds every id with no direct key' },
|
|
204
|
+
{ name: 'MODEL_GATEWAY_BASE_URL', shape: 'https URL', purpose: 'optional: a self-hosted proxy speaking the same dialect (LiteLLM)' },
|
|
205
|
+
{ name: 'MODEL_GATEWAY_MODEL_MAP', shape: 'yaml-id=gateway-id,yaml-id=gateway-id', purpose: 'optional: fix an id the default mapping gets wrong' },
|
|
206
|
+
],
|
|
207
|
+
confirm: 'a `gateway` line with no ✗, and providers without a direct key marked `◇ via gateway`.',
|
|
208
|
+
runs: (s) => [
|
|
209
|
+
`every cloud file: ${list([...CLOUD_PROVIDERS.flatMap((p) => s.byTier[p] ?? []), ...(s.byTier['multi-provider'] ?? [])])}`,
|
|
210
|
+
],
|
|
211
|
+
first: {
|
|
212
|
+
package: [init('package', 'research_brief'), chat('package', 'research_brief')],
|
|
213
|
+
clone: ['npm run chat:syndicate'],
|
|
214
|
+
},
|
|
215
|
+
notes: [
|
|
216
|
+
'Native search is lost on the gateway path (Gemini grounding, Anthropic and OpenAI web_search, xAI x_search); the doctor marks each dropped tool.',
|
|
217
|
+
'Long-term memory embeds through Gemini by default, which the gateway does not serve: set GOOGLE_GENAI_API_KEY, or MEMORY_EMBEDDING_PROVIDER=openai or ollama.',
|
|
218
|
+
],
|
|
219
|
+
skill: 'melchizedek-onboard-keys',
|
|
220
|
+
detectable: true,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'cloud-platform',
|
|
224
|
+
menu: 'Cloud platform credentials (Vertex AI, Amazon Bedrock, Azure OpenAI)',
|
|
225
|
+
title: 'Cloud platform credentials',
|
|
226
|
+
summary: "The model id still picks the provider; these variables pick the cloud it is reached through, authenticated by that cloud's own credential chain instead of a vendor API key. Gemini and Claude on Vertex AI use Google Application Default Credentials (`gcloud auth application-default login`, or a service account); Claude on Bedrock uses the AWS credential chain; GPT on Azure uses an Azure key or Entra ID.",
|
|
227
|
+
env: [
|
|
228
|
+
{ name: 'GEMINI_PLATFORM', shape: 'vertex', purpose: 'Gemini on Vertex AI (ADC)' },
|
|
229
|
+
{ name: 'GOOGLE_CLOUD_PROJECT', shape: 'a Google Cloud project id', purpose: 'Vertex AI project' },
|
|
230
|
+
{ name: 'GOOGLE_CLOUD_LOCATION', shape: 'a region, or global', purpose: 'Vertex AI location' },
|
|
231
|
+
{ name: 'ANTHROPIC_PLATFORM', shape: 'bedrock | vertex', purpose: 'Claude on Bedrock (`npm install @anthropic-ai/bedrock-sdk`) or Vertex AI (`npm install @anthropic-ai/vertex-sdk`)' },
|
|
232
|
+
{ name: 'AWS_REGION', shape: 'an AWS region, e.g. us-east-1', purpose: 'Bedrock region; credentials come from the AWS chain (profile, role, SSO)' },
|
|
233
|
+
{ name: 'ANTHROPIC_VERTEX_PROJECT_ID', shape: 'a Google Cloud project id', purpose: 'Claude on Vertex AI (falls back to GOOGLE_CLOUD_PROJECT)' },
|
|
234
|
+
{ name: 'CLOUD_ML_REGION', shape: 'a region', purpose: 'Claude on Vertex AI (falls back to GOOGLE_CLOUD_LOCATION)' },
|
|
235
|
+
{ name: 'OPENAI_PLATFORM', shape: 'azure', purpose: 'GPT on Azure OpenAI' },
|
|
236
|
+
{ name: 'AZURE_OPENAI_ENDPOINT', shape: 'https://<resource>.openai.azure.com', purpose: 'the Azure OpenAI resource' },
|
|
237
|
+
{ name: 'AZURE_OPENAI_API_KEY', shape: 'an Azure OpenAI key', purpose: 'optional: without it, Entra ID (`npm install @azure/identity`)' },
|
|
238
|
+
{ name: 'ANTHROPIC_MODEL_MAP', shape: 'JSON: {"yaml-id":"platform-id"}', purpose: 'optional: Bedrock or Vertex model ids (OPENAI_MODEL_MAP for Azure deployments, GEMINI_MODEL_MAP likewise)' },
|
|
239
|
+
],
|
|
240
|
+
confirm: 'one `endpoint` line per provider on a cloud platform, with no ✗; the credential chain itself is not exercised by the doctor.',
|
|
241
|
+
runs: (s) => [
|
|
242
|
+
`Gemini on Vertex AI: ${list(s.byTier.gemini)}`,
|
|
243
|
+
`Claude on Bedrock or Vertex AI: ${list(s.byTier.anthropic)}`,
|
|
244
|
+
`GPT on Azure OpenAI: ${list(s.byTier.openai)}`,
|
|
245
|
+
],
|
|
246
|
+
first: {
|
|
247
|
+
package: [init('package', 'research_brief'), chat('package', 'research_brief')],
|
|
248
|
+
clone: ['npm run chat:syndicate'],
|
|
249
|
+
},
|
|
250
|
+
notes: [
|
|
251
|
+
'The Vertex AI, Bedrock and Azure paths are tested against mocks and have not been run against the live clouds from this repository; the doctor says so.',
|
|
252
|
+
"Anthropic's and OpenAI's server-side web_search are not sent on Bedrock, Claude on Vertex AI or Azure; Gemini grounding works on Vertex AI.",
|
|
253
|
+
],
|
|
254
|
+
skill: 'melchizedek-onboard-cloud',
|
|
255
|
+
detectable: true,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'byok',
|
|
259
|
+
menu: 'Serving over A2A where each caller brings its own model key (BYOK)',
|
|
260
|
+
title: 'Per-caller keys when serving over A2A (BYOK)',
|
|
261
|
+
summary: "The A2A server lets each caller fund its own inference: the caller sends its key in `X-API-Key` and the provider in `X-Provider`, and agents on that provider run on that key for that request. Agents on other providers, tools and memory extraction still run on the server's keys.",
|
|
262
|
+
env: [
|
|
263
|
+
{ name: 'A2A_KEY_MODE', shape: 'byok', purpose: "callers' X-API-Key funds X-Provider (default: server)" },
|
|
264
|
+
{ name: 'A2A_SERVER_SECRET', shape: '64 hex characters (`openssl rand -hex 32`)', purpose: 'the bearer secret callers present (or caller tokens: level 7)' },
|
|
265
|
+
],
|
|
266
|
+
confirm: 'a `serving` line reading `A2A_KEY_MODE=byok` with no ✗.',
|
|
267
|
+
runs: (s) => [`any file; start from a template: ${s.templates.join(', ')}`],
|
|
268
|
+
first: {
|
|
269
|
+
package: [init('package', 'conversational'), serve('package', 'conversational.yaml')],
|
|
270
|
+
clone: [serve('clone', 'syndicate.yaml'), 'node demo/a2a_demo.mjs # sends your Gemini key as X-API-Key'],
|
|
271
|
+
},
|
|
272
|
+
notes: [
|
|
273
|
+
'Under A2A_AUTH=secret, byok also scopes data by a hash of the caller\'s key; with caller tokens or JWTs the identity scopes data and byok only pays.',
|
|
274
|
+
'Without A2A_SERVER_SECRET or caller tokens the server binds 127.0.0.1 only; set one before anyone else calls it.',
|
|
275
|
+
],
|
|
276
|
+
skill: 'melchizedek-onboard-serve',
|
|
277
|
+
detectable: true,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'caller-tokens',
|
|
281
|
+
menu: 'Serving to other backends or users with their own tokens (A2A_AUTH)',
|
|
282
|
+
title: 'Serving with caller tokens (A2A_AUTH)',
|
|
283
|
+
summary: 'A2A_AUTH decides who a caller is, and so whose data a request touches. `callers` gives each calling backend its own bearer token (the config holds only its SHA-256); `jwt` accepts your identity provider\'s tokens; `header` trusts a user header set by an authenticating gateway that alone holds the secret; `secret` (the default) is one shared bearer secret.',
|
|
284
|
+
env: [
|
|
285
|
+
{ name: 'A2A_AUTH', shape: 'callers | jwt | header | secret', purpose: 'the identity mode (required with PUBLIC_URL)' },
|
|
286
|
+
{ name: 'A2A_CALLERS', shape: 'name:sha256[:scope];name:sha256[:scope]', purpose: `callers: mint each with \`melchizedek-serve --new-caller <name>\`` },
|
|
287
|
+
{ name: 'A2A_JWT_ISSUER', shape: 'https://your-idp.example/', purpose: 'jwt: required iss' },
|
|
288
|
+
{ name: 'A2A_JWT_AUDIENCE', shape: 'an audience string', purpose: 'jwt: required aud' },
|
|
289
|
+
{ name: 'A2A_JWT_JWKS_URL', shape: 'https URL of the JWKS', purpose: 'jwt: RS256/ES256 keys (or A2A_JWT_SECRET, HS256, 32+ characters)' },
|
|
290
|
+
{ name: 'A2A_TRUSTED_USER_HEADER', shape: 'a header name, e.g. X-Authenticated-User', purpose: 'header: set by your gateway' },
|
|
291
|
+
{ name: 'A2A_SERVER_SECRET', shape: '64 hex characters (`openssl rand -hex 32`)', purpose: 'secret and header modes' },
|
|
292
|
+
...SERVE_POSTURE,
|
|
293
|
+
],
|
|
294
|
+
confirm: 'a `serving` line naming your A2A_AUTH mode with no ✗.',
|
|
295
|
+
runs: (s) => [`any file; start from a template: ${s.templates.join(', ')}`],
|
|
296
|
+
first: {
|
|
297
|
+
package: [init('package', 'support_triage'), newCaller('package', 'my-backend'), serve('package', 'support_triage.yaml')],
|
|
298
|
+
clone: [newCaller('clone', 'my-backend'), serve('clone', 'support_triage.yaml')],
|
|
299
|
+
},
|
|
300
|
+
notes: [
|
|
301
|
+
'`--new-caller --token-file` writes the token to a new mode-600 file outside the project and prints only the `A2A_CALLERS` entry (a hash); move the token into the calling backend\'s secret store and delete the file. Without `--token-file` the token is printed once, so do not run it where a transcript is kept.',
|
|
302
|
+
'Prefer callers or jwt on a public URL: under `secret`, any holder of the secret can act as any user by naming them in X-User-Id.',
|
|
303
|
+
],
|
|
304
|
+
skill: 'melchizedek-onboard-serve',
|
|
305
|
+
detectable: true,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'oauth-grants',
|
|
309
|
+
menu: "Tools that act for end users on third-party APIs (OAuth grants)",
|
|
310
|
+
title: 'OAuth tool grants for end users',
|
|
311
|
+
summary: "A tool declared with `auth: { oauth2 }` (or an MCP server with `mcp_auth`) sends a third-party token. An authorization_code grant pauses the turn until the user consents; the token is sealed per user with the credential key and only sent to hosts the operator binds. A client_credentials grant uses the server's own client.",
|
|
312
|
+
env: [
|
|
313
|
+
{ name: CREDENTIAL_KEY_ENV, shape: '32 random bytes, base64 or 64 hex (`openssl rand -base64 32`)', purpose: "seals each user's tokens; keep it out of the database and its backups" },
|
|
314
|
+
{ name: OAUTH_REDIRECT_URI_ENV, shape: 'https://agents.example.com/oauth/callback (no query)', purpose: 'the consent callback, registered at every provider; needs the key' },
|
|
315
|
+
{ name: OAUTH_HOSTS_ENV, shape: 'provider=host,host;provider=host', purpose: "the hosts each provider's tokens may go to; unset, authorization_code grants are refused" },
|
|
316
|
+
{ name: 'OAUTH_CALLBACK_IDENTITY', shape: 'required | state', purpose: 'required (default) needs A2A_AUTH=header behind a gateway' },
|
|
317
|
+
{ name: 'DATABASE_URL', shape: 'postgres://…', purpose: 'durable sealed store; without it, grants live in process memory' },
|
|
318
|
+
],
|
|
319
|
+
confirm: 'an `oauth` line with no ✗, and each `⚿` grant line naming no unset variable.',
|
|
320
|
+
runs: (s) => [
|
|
321
|
+
`declares a grant: ${s.withGrants.length ? s.withGrants.join(', ') : 'no shipped file declares one active'}`,
|
|
322
|
+
`carries a commented grant to uncomment: ${s.grantExamples.length ? s.grantExamples.join(', ') : 'none'}`,
|
|
323
|
+
],
|
|
324
|
+
first: {
|
|
325
|
+
package: [init('package', 'systems_operator'), '# uncomment its mcp_auth block, then:', 'npx melchizedek-doctor # names the client id and secret variables the grant reads', serve('package', 'systems_operator.yaml')],
|
|
326
|
+
clone: [init('clone', 'systems_operator'), '# uncomment the mcp_auth block in config/agents/systems_operator.yaml, then:', 'npm run doctor # names the client id and secret variables the grant reads', serve('clone', 'systems_operator.yaml')],
|
|
327
|
+
},
|
|
328
|
+
notes: [
|
|
329
|
+
"Each grant's client id and secret are read from variables the YAML names (client_id_env, client_secret_env); the framework's own settings are refused there.",
|
|
330
|
+
'This level sits on top of a serving level: set A2A_AUTH first (level 7).',
|
|
331
|
+
],
|
|
332
|
+
skill: 'melchizedek-onboard-serve',
|
|
333
|
+
detectable: true,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 'subscription-signin',
|
|
337
|
+
menu: 'A ChatGPT / Codex, Claude.ai or Gemini CLI sign-in (no API key)',
|
|
338
|
+
title: 'Subscription sign-ins (ChatGPT / Codex, Claude.ai, Gemini CLI)',
|
|
339
|
+
summary: "Not supported, on purpose. A consumer subscription sign-in authorizes that vendor's own apps. The engine does not read, reuse or relay those tokens: Anthropic and Google say third-party apps may not use them, and OpenAI's plan-usage sign-in is a preview for approved or locally hosted apps that this engine has not integrated. Use the same vendor's sanctioned route instead.",
|
|
340
|
+
env: [
|
|
341
|
+
{ name: 'OPENAI_API_KEY', shape: 'an API key from https://platform.openai.com/api-keys', purpose: 'instead of a ChatGPT / Codex sign-in (or Azure OpenAI: level 5)' },
|
|
342
|
+
{ name: 'ANTHROPIC_API_KEY', shape: 'an API key from https://console.anthropic.com', purpose: 'instead of a Claude.ai sign-in (or Bedrock / Vertex AI: level 5)' },
|
|
343
|
+
{ name: 'GOOGLE_GENAI_API_KEY', shape: 'an API key from https://aistudio.google.com (free tier)', purpose: 'instead of a Gemini CLI sign-in (or Vertex AI: level 5)' },
|
|
344
|
+
],
|
|
345
|
+
confirm: 'the provider you chose shows a ✓; nothing reads a subscription sign-in.',
|
|
346
|
+
runs: () => ['as level 2 (one key) or level 5 (cloud platform)'],
|
|
347
|
+
first: {
|
|
348
|
+
package: [init('package', 'research_brief'), chat('package', 'research_brief')],
|
|
349
|
+
clone: ['npm run chat:syndicate'],
|
|
350
|
+
},
|
|
351
|
+
notes: [
|
|
352
|
+
'Anthropic: Claude.ai (Pro/Max) OAuth is for Claude Code and its own apps; third-party developers may not offer Claude.ai login or route requests through those credentials. Use an API key, Bedrock or Vertex AI.',
|
|
353
|
+
"Google: reaching Gemini CLI's services with its sign-in from third-party software is against Gemini CLI's terms. Use an AI Studio key or Vertex AI.",
|
|
354
|
+
"OpenAI: an API key or Azure OpenAI. OpenAI's Sign in with ChatGPT plan-usage flow is a documented preview for open-source, locally hosted apps; the engine does not implement it, and reusing the Codex CLI's own token is not that flow.",
|
|
355
|
+
'A coding agent signed in with a subscription (Claude Code, Codex, Gemini CLI) can still drive this repository: the sign-in pays for the coding agent, and the engine runs on the keys above.',
|
|
356
|
+
],
|
|
357
|
+
skill: 'melchizedek-onboard-keys',
|
|
358
|
+
detectable: false,
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
export function levelById(id) {
|
|
362
|
+
return LEVELS.find((l) => l.id === id);
|
|
363
|
+
}
|
|
364
|
+
/** Every variable name the guides mention. */
|
|
365
|
+
export function guideEnvNames() {
|
|
366
|
+
return [...new Set(LEVELS.flatMap((l) => l.env.map((e) => e.name)))].sort();
|
|
367
|
+
}
|
|
368
|
+
// ── Rendering ────────────────────────────────────────────────────────────────
|
|
369
|
+
/** One level's startup guide, as Markdown (the terminal prints the same text). */
|
|
370
|
+
export function renderGuide(level, form, shipped = shippedIndex()) {
|
|
371
|
+
const n = LEVELS.indexOf(level) + 1;
|
|
372
|
+
const out = [];
|
|
373
|
+
out.push(`## ${n}. ${level.title}`, '', level.summary, '');
|
|
374
|
+
out.push(level.detectable ? '**Set in `.env`** (names and shapes only; you type the values yourself):' : '**Use instead** (names and shapes only):', '');
|
|
375
|
+
out.push('| Variable | Shape | What it does |', '|---|---|---|');
|
|
376
|
+
for (const e of level.env)
|
|
377
|
+
out.push(`| \`${e.name}\` | ${e.shape} | ${e.purpose} |`);
|
|
378
|
+
out.push('');
|
|
379
|
+
out.push(`**Confirm:** \`${CMD.doctor[form]}\`: ${level.confirm}`, '');
|
|
380
|
+
out.push('**Runs at this level:**', '');
|
|
381
|
+
for (const r of level.runs(shipped))
|
|
382
|
+
out.push(`- ${r}`);
|
|
383
|
+
out.push('', '**First commands:**', '', '```bash', ...level.first[form], '```', '');
|
|
384
|
+
if (level.notes.length) {
|
|
385
|
+
out.push('**Notes:**', '');
|
|
386
|
+
for (const note of level.notes)
|
|
387
|
+
out.push(`- ${note}`);
|
|
388
|
+
out.push('');
|
|
389
|
+
}
|
|
390
|
+
out.push(`**Onboarding skill:** \`${level.skill}\` (\`${CMD.setup[form]} --level ${level.id}\` prints this guide)`);
|
|
391
|
+
return out.join('\n');
|
|
392
|
+
}
|
|
393
|
+
/** The menu: one numbered line per level. */
|
|
394
|
+
export function renderMenu() {
|
|
395
|
+
return LEVELS.map((l, i) => ` ${String(i + 1).padStart(2)}. ${l.menu} [${l.id}]`).join('\n');
|
|
396
|
+
}
|
|
397
|
+
/** ONBOARDING.md, generated in full from the levels (package spelling). */
|
|
398
|
+
export function renderOnboardingDoc(shipped = shippedIndex()) {
|
|
399
|
+
const head = [
|
|
400
|
+
'<!-- Generated by lib/onboarding.ts: `npm run setup -- --markdown > ONBOARDING.md`. Do not edit by hand; tests/onboarding.test.ts holds this file equal to the generator. -->',
|
|
401
|
+
'',
|
|
402
|
+
'# Onboarding: start from what you have',
|
|
403
|
+
'',
|
|
404
|
+
'Start with `npx melchizedek-setup` (in a clone, `npm run setup`). It opens a menu of the levels below and prints the guide for the one you pick; `--auto` reads your environment, names only, and prints the guide for the highest level it detects. Every guide lists variable names and shapes, never values: you type the values into `.env` yourself.',
|
|
405
|
+
'',
|
|
406
|
+
'In a clone, the commands are spelled as npm scripts (`npm run doctor`, `npm run chat:syndicate -- --syndicate <name>`); the menu prints that spelling there.',
|
|
407
|
+
'',
|
|
408
|
+
'| # | You have | Guide |',
|
|
409
|
+
'|---|---|---|',
|
|
410
|
+
...LEVELS.map((l, i) => `| ${i + 1} | ${l.menu} | \`--level ${l.id}\` |`),
|
|
411
|
+
].join('\n');
|
|
412
|
+
return `${[head, ...LEVELS.map((l) => renderGuide(l, 'package', shipped))].join('\n\n')}\n`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Which levels the environment is set up for, read from the doctor's result.
|
|
416
|
+
* Local is always available (whether Ollama is running is not checked).
|
|
417
|
+
*/
|
|
418
|
+
export function detectLevels(result) {
|
|
419
|
+
const direct = result.providers.filter((p) => p.platform === 'direct' && p.funded && p.transport === 'direct');
|
|
420
|
+
const names = direct.map((p) => `${p.label} (${p.keyEnv})`);
|
|
421
|
+
const cloud = result.endpoints.filter((e) => e.platform === 'vertex' || e.platform === 'bedrock' || e.platform === 'azure' || e.platform === 'invalid');
|
|
422
|
+
const serving = result.serving;
|
|
423
|
+
const knownGateway = !!result.gateway && Object.prototype.hasOwnProperty.call(GATEWAYS, result.gateway.id);
|
|
424
|
+
const oauthSet = result.oauth ? Object.entries(result.oauth.set).filter(([, on]) => on).map(([n]) => n) : [];
|
|
425
|
+
const out = {
|
|
426
|
+
local: { detected: true, evidence: 'always available; needs Ollama running (not checked)', problems: [] },
|
|
427
|
+
'one-provider': {
|
|
428
|
+
detected: direct.length >= 1,
|
|
429
|
+
evidence: direct.length ? `direct key set: ${names.join(', ')}` : 'no provider key set',
|
|
430
|
+
problems: [],
|
|
431
|
+
},
|
|
432
|
+
'several-providers': {
|
|
433
|
+
detected: direct.length >= 2,
|
|
434
|
+
evidence: `${direct.length} provider key(s) set`,
|
|
435
|
+
problems: [],
|
|
436
|
+
},
|
|
437
|
+
gateway: {
|
|
438
|
+
detected: !!result.gateway?.usable,
|
|
439
|
+
// An unknown MODEL_GATEWAY value is never echoed, here or on the doctor's own line.
|
|
440
|
+
evidence: !result.gateway
|
|
441
|
+
? `${GATEWAY_ENV} unset`
|
|
442
|
+
: knownGateway
|
|
443
|
+
? `${GATEWAY_ENV} set (${result.gateway.label}), ${GATEWAY_KEY_ENV} ${result.gateway.usable ? 'set' : 'unset'}`
|
|
444
|
+
: `${GATEWAY_ENV} set to an unknown gateway`,
|
|
445
|
+
problems: !result.gateway?.problem ? [] : knownGateway ? [result.gateway.problem] : [`${GATEWAY_ENV} must be one of ${Object.keys(GATEWAYS).join(', ')}`],
|
|
446
|
+
},
|
|
447
|
+
'cloud-platform': {
|
|
448
|
+
detected: cloud.length > 0,
|
|
449
|
+
evidence: cloud.length ? cloud.map((e) => e.label).join(', ') : 'no provider on a cloud platform',
|
|
450
|
+
problems: cloud.flatMap((e) => e.problems),
|
|
451
|
+
},
|
|
452
|
+
byok: {
|
|
453
|
+
detected: serving?.keyMode === 'byok',
|
|
454
|
+
evidence: `A2A_KEY_MODE=${serving?.keyMode ?? 'server'}`,
|
|
455
|
+
problems: [],
|
|
456
|
+
},
|
|
457
|
+
'caller-tokens': {
|
|
458
|
+
detected: !!serving?.authDeclared,
|
|
459
|
+
evidence: serving ? `A2A_AUTH=${serving.auth}${serving.authDeclared ? '' : ' (default)'}, A2A_SERVER_SECRET ${serving.set.A2A_SERVER_SECRET ? 'set' : 'unset'}` : 'no serving variable set',
|
|
460
|
+
problems: serving?.problems ?? [],
|
|
461
|
+
},
|
|
462
|
+
'oauth-grants': {
|
|
463
|
+
detected: oauthSet.length > 0,
|
|
464
|
+
evidence: oauthSet.length ? `set: ${oauthSet.join(', ')}` : `${CREDENTIAL_KEY_ENV} unset`,
|
|
465
|
+
problems: result.oauth?.problems ?? [],
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
return LEVELS.filter((l) => l.detectable).map((l) => ({ id: l.id, ...out[l.id] }));
|
|
469
|
+
}
|
|
470
|
+
/** The highest detected level (the last in menu order). */
|
|
471
|
+
export function highestLevel(detections) {
|
|
472
|
+
const hit = [...detections].reverse().find((d) => d.detected);
|
|
473
|
+
return levelById(hit?.id ?? 'local');
|
|
474
|
+
}
|
|
475
|
+
/** `--auto`: what the environment has (names only), then the guide for the highest level. */
|
|
476
|
+
export function renderAuto(result, form, shipped = shippedIndex()) {
|
|
477
|
+
const detections = detectLevels(result);
|
|
478
|
+
const top = highestLevel(detections);
|
|
479
|
+
const lines = ['melchizedek setup --auto: what this environment has (variable names only; no value is read out)', ''];
|
|
480
|
+
for (const d of detections) {
|
|
481
|
+
const n = LEVELS.findIndex((l) => l.id === d.id) + 1;
|
|
482
|
+
lines.push(` ${d.detected ? '✓' : '·'} ${String(n).padStart(2)}. ${d.id.padEnd(18)} ${d.evidence}`);
|
|
483
|
+
for (const p of d.problems)
|
|
484
|
+
lines.push(` ✗ ${p}`);
|
|
485
|
+
}
|
|
486
|
+
lines.push('', `Highest level detected: ${LEVELS.indexOf(top) + 1}. ${top.title}`, '', renderGuide(top, form, shipped));
|
|
487
|
+
return lines.join('\n');
|
|
488
|
+
}
|
|
489
|
+
/** True when git confirms `.env` in `cwd` is ignored (exit 0 from `git check-ignore`). */
|
|
490
|
+
export function gitIgnoresEnv(cwd) {
|
|
491
|
+
const r = spawnSync('git', ['check-ignore', '-q', '.env'], { cwd, stdio: 'ignore' });
|
|
492
|
+
if (r.status === 0)
|
|
493
|
+
return { ignored: true, reason: 'git check-ignore: .env is ignored' };
|
|
494
|
+
if (r.status === 1)
|
|
495
|
+
return { ignored: false, reason: '.env is not ignored by git here: add `.env` to .gitignore first' };
|
|
496
|
+
return { ignored: false, reason: 'git check-ignore could not confirm .env is ignored (not a git repository, or git is missing): run `git init` and add `.env` to .gitignore first' };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Create `.env` from `.env.example` for a level: the template verbatim, then
|
|
500
|
+
* every name the level lists that the template does not already assign, left
|
|
501
|
+
* blank. Never overwrites, never writes a value, and refuses unless git
|
|
502
|
+
* confirms `.env` is ignored. The file is created mode 600.
|
|
503
|
+
*/
|
|
504
|
+
export function writeEnvForLevel(level, opts = {}) {
|
|
505
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
506
|
+
const target = join(cwd, '.env');
|
|
507
|
+
// An existing .env is never touched: the exclusive open below refuses it (EEXIST → 'exists').
|
|
508
|
+
const ignore = (opts.checkIgnored ?? gitIgnoresEnv)(cwd);
|
|
509
|
+
if (!ignore.ignored)
|
|
510
|
+
return { status: 'not-ignored', path: target, reason: ignore.reason };
|
|
511
|
+
const source = join(opts.root ?? packageRoot(), '.env.example');
|
|
512
|
+
if (!existsSync(source))
|
|
513
|
+
return { status: 'no-template', path: target };
|
|
514
|
+
const template = readFileSync(source, 'utf-8');
|
|
515
|
+
const assigned = new Set(template.split(/\r?\n/).map((l) => l.match(/^([A-Z_][A-Z0-9_]*)=/)?.[1]).filter((n) => !!n));
|
|
516
|
+
const added = level.env.map((e) => e.name).filter((n) => !assigned.has(n));
|
|
517
|
+
const block = added.length
|
|
518
|
+
? `\n# ── Chosen with melchizedek-setup: ${level.title} ──\n# Names only; fill in the values yourself (shapes: ONBOARDING.md, level ${LEVELS.indexOf(level) + 1}).\n${added.map((n) => `${n}=`).join('\n')}\n`
|
|
519
|
+
: '';
|
|
520
|
+
// One handle: created exclusively (an existing file or a symlink planted at the
|
|
521
|
+
// path fails with EEXIST), written and narrowed to 0600 through the descriptor.
|
|
522
|
+
let fd;
|
|
523
|
+
try {
|
|
524
|
+
fd = openSync(target, 'wx', 0o600);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
if (err.code === 'EEXIST')
|
|
528
|
+
return { status: 'exists', path: target };
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
fchmodSync(fd, 0o600);
|
|
533
|
+
writeSync(fd, template.endsWith('\n') ? template + block : `${template}\n${block}`);
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
closeSync(fd);
|
|
537
|
+
}
|
|
538
|
+
return { status: 'written', path: target, added };
|
|
539
|
+
}
|
|
@@ -24,7 +24,15 @@ export interface PendingApproval {
|
|
|
24
24
|
args: Record<string, unknown>;
|
|
25
25
|
/** The original call's id. */
|
|
26
26
|
callId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* For a call gated inside a delegated subagent (ADR 0110): the agents from
|
|
29
|
+
* the turn's own agent down to the one that asked, `agent` last. Absent
|
|
30
|
+
* when the turn's own agent asked.
|
|
31
|
+
*/
|
|
32
|
+
path?: string[];
|
|
27
33
|
}
|
|
34
|
+
/** The approval request an `adk_request_confirmation` call in `e` makes, as pendingApproval reports it. */
|
|
35
|
+
export declare const approvalRequestOf: (e: Event, call: Record<string, any>) => PendingApproval;
|
|
28
36
|
/**
|
|
29
37
|
* The approval request still waiting for an answer, or undefined. A request
|
|
30
38
|
* the user moved on from (a new text message after it) is not pending: its
|
|
@@ -17,7 +17,8 @@ function workflowPauseIds(e) {
|
|
|
17
17
|
return undefined;
|
|
18
18
|
return e.longRunningToolIds?.length ? [...e.longRunningToolIds] : undefined;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
/** The approval request an `adk_request_confirmation` call in `e` makes, as pendingApproval reports it. */
|
|
21
|
+
export const approvalRequestOf = (e, call) => {
|
|
21
22
|
const original = call.args?.originalFunctionCall ?? {};
|
|
22
23
|
return {
|
|
23
24
|
id: call.id,
|
|
@@ -47,7 +48,7 @@ export function pendingApproval(events) {
|
|
|
47
48
|
for (let j = i - 1; j >= 0 && open.size > 0; j--) {
|
|
48
49
|
const request = partsOf(events[j]).find((p) => p.functionCall?.name === APPROVAL_REQUEST && open.has(p.functionCall.id));
|
|
49
50
|
if (request)
|
|
50
|
-
return
|
|
51
|
+
return approvalRequestOf(events[j], request.functionCall);
|
|
51
52
|
}
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
@@ -61,7 +62,7 @@ export function pendingApproval(events) {
|
|
|
61
62
|
const call = p.functionCall;
|
|
62
63
|
if (call?.name !== APPROVAL_REQUEST || !call.id || answered.has(call.id))
|
|
63
64
|
continue;
|
|
64
|
-
return
|
|
65
|
+
return approvalRequestOf(e, call);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
return undefined;
|
|
@@ -36,6 +36,12 @@ export interface PendingConsent {
|
|
|
36
36
|
/** The state nonce the URL carries. */
|
|
37
37
|
state: string;
|
|
38
38
|
scopes: string[];
|
|
39
|
+
/**
|
|
40
|
+
* For a call paused inside a delegated subagent (ADR 0118): the agents
|
|
41
|
+
* from the turn's own agent down to the one that asked, `agent` last.
|
|
42
|
+
* Absent when the turn's own agent asked.
|
|
43
|
+
*/
|
|
44
|
+
path?: string[];
|
|
39
45
|
}
|
|
40
46
|
/** The request an `adk_request_credential` call carries, or undefined when it is not one this engine reads. */
|
|
41
47
|
export declare function consentFrom(author: string | undefined, call: {
|