melchizedek-agents 1.4.0 → 1.5.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 +15 -0
- package/CHANGELOG.md +77 -0
- package/DOCUMENTATION.md +43 -2
- package/config/agents/syndicate.schema.json +80 -6
- package/db/hardening.sql +38 -12
- package/dist/lib/a2a/app.js +2 -1
- package/dist/lib/chatgpt/state.js +4 -4
- package/dist/lib/compile.js +15 -2
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.js +4 -0
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/models/claudeAdapter.js +2 -1
- package/dist/lib/models/credentialSource.d.ts +28 -0
- package/dist/lib/models/credentialSource.js +49 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.js +2 -1
- package/dist/lib/models/geminiAdapter.js +2 -1
- package/dist/lib/models/gptAdapter.js +2 -1
- package/dist/lib/models/grokAdapter.js +2 -1
- package/dist/lib/models/kimiAdapter.js +2 -1
- package/dist/lib/models/providerMap.js +3 -2
- package/dist/lib/observability/embeddings.js +2 -1
- package/dist/lib/observability/lineage.js +11 -3
- package/dist/lib/observability/supabaseSpanExporter.d.ts +19 -1
- package/dist/lib/observability/supabaseSpanExporter.js +37 -1
- package/dist/lib/observability/tracer.d.ts +9 -0
- package/dist/lib/observability/tracer.js +45 -0
- package/dist/lib/paths.d.ts +31 -0
- package/dist/lib/paths.js +76 -0
- package/dist/lib/storage/postgres/index.d.ts +25 -0
- package/dist/lib/storage/postgres/index.js +10 -1
- package/dist/lib/syndicateSchema.d.ts +26 -4
- package/dist/lib/syndicateSchema.js +45 -1
- package/dist/lib/tools/generateImageTool.js +6 -6
- package/dist/lib/tools/inspectImageTool.d.ts +2 -0
- package/dist/lib/tools/inspectImageTool.js +17 -10
- package/dist/lib/tools/mcpToolFactory.d.ts +34 -1
- package/dist/lib/tools/mcpToolFactory.js +117 -17
- package/dist/lib/tools/taskTools.js +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/scripts/a2a_server.js +5 -0
- package/package.json +7 -4
package/.env.example
CHANGED
|
@@ -136,6 +136,21 @@ SUPABASE_SERVICE_ROLE_KEY=
|
|
|
136
136
|
# adk_telemetry table. Provision it first: run db/telemetry.sql in the Supabase
|
|
137
137
|
# SQL Editor, then re-run db/hardening.sql. See DOCUMENTATION.md §5.
|
|
138
138
|
TELEMETRY_SUPABASE=false
|
|
139
|
+
# Or, with DATABASE_URL set, write the same ledger to that Postgres instead
|
|
140
|
+
# (db/telemetry.sql applied there). Use one sink, not both.
|
|
141
|
+
TELEMETRY_POSTGRES=false
|
|
142
|
+
|
|
143
|
+
# ── Optional: local MCP servers over stdio ───────────────────────────────────
|
|
144
|
+
# Comma-separated `command` values an mcp_servers entry may start (exact
|
|
145
|
+
# match, e.g. "npx,uvx"). Unset: no stdio server runs. See ADR 0132.
|
|
146
|
+
MELCHIZEDEK_STDIO_MCP=
|
|
147
|
+
|
|
148
|
+
# ── Optional: where the engine writes ─────────────────────────────────────────
|
|
149
|
+
# One directory for the on-disk defaults (outputs/, outputs/tasks.json,
|
|
150
|
+
# chatgpt-signin.json). OUTPUTS_DIR, MELCHIZEDEK_TASKS_FILE and
|
|
151
|
+
# MELCHIZEDEK_CHATGPT_SIGNIN_FILE still win. Unset: ./outputs and
|
|
152
|
+
# ~/.melchizedek as before. See ADR 0134.
|
|
153
|
+
MELCHIZEDEK_DATA_DIR=
|
|
139
154
|
|
|
140
155
|
# ── Optional: A2A Server ──────────────────────────────────────────────────────
|
|
141
156
|
# A2A_SERVER_SECRET: Bearer token used to authenticate incoming A2A requests.
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,83 @@ the starter pack and the templates), not the repo's full history.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 1.5.0 — 2026-10-15
|
|
10
|
+
|
|
11
|
+
A minor release that makes the engine embeddable in a single-process
|
|
12
|
+
application with no database server and no keys in its environment, and
|
|
13
|
+
adds local MCP servers. Embedded Postgres (PGlite) runs the schema and
|
|
14
|
+
`postgresStorage` unchanged (ADR 0130); the telemetry ledger can be written
|
|
15
|
+
to any Postgres through the storage's own pool (ADR 0131); an `mcp_servers`
|
|
16
|
+
entry can start a local server over stdio, behind an operator allowlist
|
|
17
|
+
(ADR 0132); an application supplies credentials from its own store through
|
|
18
|
+
one source asked by variable name (ADR 0133); and one variable,
|
|
19
|
+
`MELCHIZEDEK_DATA_DIR`, roots the engine's files (ADR 0134). Nothing
|
|
20
|
+
changes for an existing deployment that sets none of the new options. Two
|
|
21
|
+
fixes ride along: `inspect_image` with `OUTPUTS_DIR` set, and the ledger's
|
|
22
|
+
`engine_version` for an installed engine.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`postgresStorage({ turnLock })` and `inProcessTurnLock` exported from
|
|
27
|
+
`melchizedek-agents/storage/postgres` (ADR 0130).** The storage uses the
|
|
28
|
+
given lock instead of its advisory lock and opens no turn-lock pool. Pass
|
|
29
|
+
`inProcessTurnLock()` when one process owns an embedded database such as
|
|
30
|
+
PGlite behind pglite-socket, where every connection shares one session and
|
|
31
|
+
an advisory lock excludes nothing. The default is unchanged.
|
|
32
|
+
|
|
33
|
+
- **The telemetry ledger on any Postgres (ADR 0131).**
|
|
34
|
+
`postgresStorage({ ledger: true })` writes `adk_turns`, `adk_telemetry` and
|
|
35
|
+
`adk_payloads` through the storage's own pool, with the rows, payload
|
|
36
|
+
policy and redaction the Supabase sink uses; `close()` flushes it. The A2A
|
|
37
|
+
bin turns it on with `TELEMETRY_POSTGRES=true` beside `DATABASE_URL`.
|
|
38
|
+
New exports from `melchizedek-agents/observability/*`: `attachLedger`
|
|
39
|
+
(tracer), `postgresLedgerClient`, `LedgerClient` and `LEDGER_TABLES`
|
|
40
|
+
(supabaseSpanExporter).
|
|
41
|
+
|
|
42
|
+
- **Local MCP servers over stdio (ADR 0132).** An `mcp_servers` entry may give
|
|
43
|
+
`command` (with `args`, `env`, `pass_env`, `cwd`) instead of `url`; the
|
|
44
|
+
server is started over stdio, one process per spec for the life of the
|
|
45
|
+
engine. It runs only when `MELCHIZEDEK_STDIO_MCP` lists the exact command.
|
|
46
|
+
The child gets a minimal environment plus `pass_env` by name and `env`
|
|
47
|
+
literals; a credential-looking `env` key is refused at load. New exports
|
|
48
|
+
from `melchizedek-agents/tools/mcpToolFactory`: `loadStdioMcpTools`,
|
|
49
|
+
`stdioCommandAllowed`, `stdioEnvironment`, `StdioMcpSpec`; `McpServerConfig.url`
|
|
50
|
+
is now optional. `pass_env` reads through the credential source (below)
|
|
51
|
+
before the environment, so a key held in an application's keychain reaches
|
|
52
|
+
the server by name.
|
|
53
|
+
|
|
54
|
+
- **`setCredentialSource`, `credentialValue` and `CredentialSource` in the
|
|
55
|
+
barrel (ADR 0133).** An embedding application supplies keys from its own
|
|
56
|
+
store, asked by environment variable name (`ANTHROPIC_API_KEY`, …) before
|
|
57
|
+
`process.env`, at each use and never cached. Every key read in the engine
|
|
58
|
+
goes through it: the five adapters, the gateway, provider presence and
|
|
59
|
+
`providerStatuses()`, endpoints, embeddings, the memory providers and the
|
|
60
|
+
Gemini image and vision tools. Without a source, behaviour is unchanged.
|
|
61
|
+
|
|
62
|
+
- **`MELCHIZEDEK_DATA_DIR` (ADR 0134).** When set, the engine's on-disk
|
|
63
|
+
defaults go under it: `outputs/` (generated images, the telemetry spool),
|
|
64
|
+
`outputs/tasks.json` and `chatgpt-signin.json`. `OUTPUTS_DIR`,
|
|
65
|
+
`MELCHIZEDEK_TASKS_FILE` and `MELCHIZEDEK_CHATGPT_SIGNIN_FILE` still win;
|
|
66
|
+
without any, the defaults are unchanged.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **`db/hardening.sql` applies on Postgres without Supabase's API roles
|
|
71
|
+
(ADR 0130).** It revoked from `anon` and `authenticated` by name and failed
|
|
72
|
+
where they do not exist (plain Postgres, PGlite). It now revokes from
|
|
73
|
+
whichever of the two exist, through helpers in `pg_temp` that leave nothing
|
|
74
|
+
behind; on Supabase the result is the same as before. RLS and the `PUBLIC`
|
|
75
|
+
revokes apply everywhere.
|
|
76
|
+
|
|
77
|
+
- **`inspect_image` reads the image `generate_image` reported when
|
|
78
|
+
`OUTPUTS_DIR` is set (ADR 0134).** It resolved `outputs/<file>` against the
|
|
79
|
+
working directory and refused it whenever the outputs directory was
|
|
80
|
+
elsewhere. A relative path is now read inside the outputs directory; the
|
|
81
|
+
confinement is unchanged.
|
|
82
|
+
- **The ledger's `engine_version` is the engine's (ADR 0134).** It read
|
|
83
|
+
`package.json` and the git commit from the working directory, so an
|
|
84
|
+
installed engine reported the embedding application's version and commit.
|
|
85
|
+
|
|
9
86
|
## 1.4.0 — 2026-10-13
|
|
10
87
|
|
|
11
88
|
A minor release with two additions and no breaking change. OAuth consent
|
package/DOCUMENTATION.md
CHANGED
|
@@ -150,7 +150,7 @@ Field reference:
|
|
|
150
150
|
| `mcp_auth` | subagent | `{ oauth2 }`: the OAuth grant the MCP server takes, the same block as an OpenAPI `auth.oauth2`. `client_credentials` sends the server's own token; `authorization_code` sends each user's own token on their own connection (the consent pause asks for it) and needs `mcp_tools` ([ADR 0112](./wiki/decisions/0112-oauth-grants-declared-beside-the-tool.md)). With `client_registration: dynamic` and no client id or endpoints, the authorization server is discovered from the MCP server and this deployment registers itself as a client, every discovered host held to `MELCHIZEDEK_OAUTH_HOSTS` (ADR 0124). |
|
|
151
151
|
| `mcp_server_url` | subagent | Discover this subagent's tools from a remote MCP server at load time (§3). SSRF-guarded; `ALLOW_PRIVATE_MCP=true` permits localhost for development. |
|
|
152
152
|
| `mcp_transport` | subagent | How `mcp_server_url` is reached: `auto` (default: Streamable HTTP, falling back to the legacy SSE transport on the spec's signal), `streamable_http` or `sse` ([ADR 0124](./wiki/decisions/0124-mcp-client-streamable-http-several-servers-and-dynamic-registration.md)). |
|
|
153
|
-
| `mcp_servers` | any agent | Several MCP servers: a list of `{ name, url, tools, auth?, transport? }
|
|
153
|
+
| `mcp_servers` | any agent | Several MCP servers: a list of `{ name, url, tools, auth?, transport? }`, or for a local server `{ name, command, args?, env?, pass_env?, cwd?, tools }` started over stdio when `MELCHIZEDEK_STDIO_MCP` lists the command (ADR 0132). `tools` is required on each; a tool name on two servers, or shared with the agent's own tools, is refused at load. `require_approval` may name any server's tool. Not with `mcp_server_url` (ADR 0124). |
|
|
154
154
|
| `openapi` | any agent | HTTP APIs as tools, each from an OpenAPI 3 spec file (§3, OpenAPI tools): `spec`, and optionally `operations` (default: the GET operations only), `auth` (from environment variables), `base_url`, `prefix`. |
|
|
155
155
|
| `code_execution` | any Gemini agent | `"gemini"`: the model writes Python and Gemini runs it in Google's server-side sandbox, returning the output to the model; nothing runs on this host. For arithmetic, data and checks a model gets wrong in its head. Gemini models only ([ADR 0033](./wiki/decisions/0033-context-task-code.md)). |
|
|
156
156
|
| `context` | orchestrator | Compacts a long conversation: when the last request's prompt passed `compact_after_tokens`, earlier turns become one summary (written by `summary_model`, default the agent's own) and the last `keep_recent_events` stay verbatim. The full history stays stored; only what the model reads shrinks. The orchestrator of a delegate syndicate only: a dispatch route already reads a bounded projection, a workflow node sees only its input. |
|
|
@@ -207,6 +207,23 @@ loopback/private hosts unless `ALLOW_PRIVATE_MCP=true` (SSRF guard);
|
|
|
207
207
|
treat any remote MCP server as an untrusted tool vendor whose results
|
|
208
208
|
are data, never instructions.
|
|
209
209
|
|
|
210
|
+
A local MCP server is an `mcp_servers` entry with `command` instead of
|
|
211
|
+
`url`, started over stdio (ADR 0132):
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
mcp_servers:
|
|
215
|
+
- name: github
|
|
216
|
+
command: npx
|
|
217
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
218
|
+
pass_env: [GITHUB_PERSONAL_ACCESS_TOKEN] # copied by name; never a value here
|
|
219
|
+
tools: [search_issues, get_issue]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
It runs only when the operator lists the exact command,
|
|
223
|
+
`MELCHIZEDEK_STDIO_MCP=npx`. The child sees a minimal environment plus
|
|
224
|
+
the `pass_env` names and the non-secret `env` values, nothing else of the
|
|
225
|
+
engine's. One process serves every turn and is restarted if it exits.
|
|
226
|
+
|
|
210
227
|
**OpenAPI tools** turn any HTTP API with an OpenAPI 3 spec into an
|
|
211
228
|
agent's tools, with no tool code (`lib/tools/openapiTools.ts`, on the
|
|
212
229
|
engine's own parser and caller in `lib/tools/openapi/`;
|
|
@@ -302,6 +319,14 @@ nothing is changed, and two applies started together run one after the other.
|
|
|
302
319
|
Rolling back a migration that succeeded is a restore from backup (§6), since
|
|
303
320
|
the migrations only move forward.
|
|
304
321
|
|
|
322
|
+
The same install runs on plain Postgres without Supabase's `anon` and
|
|
323
|
+
`authenticated` roles (the hardening skips the revokes by role name there), and
|
|
324
|
+
on embedded Postgres: an application can run PGlite with pgvector in-process,
|
|
325
|
+
serve it with pglite-socket, and pass `postgresStorage({ connectionString,
|
|
326
|
+
turnLock: inProcessTurnLock() })`. Every connection to PGlite shares one
|
|
327
|
+
session, so the default advisory turn lock would exclude nothing; one process
|
|
328
|
+
owns the data directory (ADR 0130).
|
|
329
|
+
|
|
305
330
|
The tables go in the `public` schema by default. To keep them out of reach of
|
|
306
331
|
a REST layer (Supabase exposes `public`), install into a private schema and
|
|
307
332
|
run the server with the same setting:
|
|
@@ -568,13 +593,29 @@ helpers. A project that only calls models imports from there. Its
|
|
|
568
593
|
`resolveAdapter` gives a Gemini id `GeminiAdapter`; `resolveModel` is not
|
|
569
594
|
in it.
|
|
570
595
|
|
|
596
|
+
An application that keeps keys outside the environment (an OS keychain,
|
|
597
|
+
a secret manager) sets one credential source, asked by the same variable
|
|
598
|
+
names at each use, before `process.env` (ADR 0133):
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { setCredentialSource } from 'melchizedek-agents';
|
|
602
|
+
setCredentialSource((name) => keychain.get(name)); // e.g. 'ANTHROPIC_API_KEY'
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Every key read goes through it: the adapters, the gateway, provider
|
|
606
|
+
status, embeddings and the image tools. A key removed from the source
|
|
607
|
+
stops being sent on the next request.
|
|
608
|
+
|
|
571
609
|
Every model request also emits an `llm.request` OpenTelemetry span
|
|
572
610
|
(provider, model, input/output/thinking tokens, latency). Scripts print it
|
|
573
611
|
as an `[OTEL_SPAN_JSON]` line; `melchizedek-chat` and the `syndicate:*`
|
|
574
612
|
scripts keep those lines off unless `OTEL_CONSOLE_SPANS=true`. Set
|
|
575
613
|
`TELEMETRY_SUPABASE=true` to persist
|
|
576
614
|
spans to the `adk_telemetry` table (run `db/telemetry.sql`, then
|
|
577
|
-
re-run `db/hardening.sql` — schema below)
|
|
615
|
+
re-run `db/hardening.sql` — schema below). On `DATABASE_URL`, set
|
|
616
|
+
`TELEMETRY_POSTGRES=true` instead to write the same rows through the
|
|
617
|
+
storage's own pool; an application that builds its own storage passes
|
|
618
|
+
`postgresStorage({ ledger: true })` (ADR 0131). Use one sink:
|
|
578
619
|
|
|
579
620
|
`db/telemetry.sql` (idempotent) creates three tables: `adk_turns` — one
|
|
580
621
|
row per turn with input, output, the responding agent, the plan-dispatch
|
|
@@ -583,9 +583,47 @@
|
|
|
583
583
|
"description": "A short name for the server, unique on this agent: logs and errors name it."
|
|
584
584
|
},
|
|
585
585
|
"url": {
|
|
586
|
+
"description": "The MCP server's URL: its Streamable HTTP endpoint, or a legacy SSE endpoint. Checked by the SSRF guard. Give url or command, not both.",
|
|
586
587
|
"type": "string",
|
|
587
|
-
"format": "uri"
|
|
588
|
-
|
|
588
|
+
"format": "uri"
|
|
589
|
+
},
|
|
590
|
+
"command": {
|
|
591
|
+
"description": "A local MCP server to start and speak to over stdio (ADR 0132), instead of url: the program to run. It runs only when the operator lists this exact value in MELCHIZEDEK_STDIO_MCP. One process per distinct command, args, env and cwd, kept for the life of the engine.",
|
|
592
|
+
"type": "string",
|
|
593
|
+
"minLength": 1
|
|
594
|
+
},
|
|
595
|
+
"args": {
|
|
596
|
+
"description": "The command's arguments (stdio only).",
|
|
597
|
+
"maxItems": 64,
|
|
598
|
+
"type": "array",
|
|
599
|
+
"items": {
|
|
600
|
+
"type": "string"
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
"env": {
|
|
604
|
+
"description": "Non-secret variables set for the command (stdio only). A credential is never written here: name it in pass_env.",
|
|
605
|
+
"type": "object",
|
|
606
|
+
"propertyNames": {
|
|
607
|
+
"type": "string",
|
|
608
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
|
|
609
|
+
},
|
|
610
|
+
"additionalProperties": {
|
|
611
|
+
"type": "string"
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
"pass_env": {
|
|
615
|
+
"description": "Variables copied by name from the engine's environment into the command's (stdio only); the command sees no other variable of the engine's beyond a minimal default set (PATH, HOME, …). A name that is unset fails the compile.",
|
|
616
|
+
"maxItems": 32,
|
|
617
|
+
"type": "array",
|
|
618
|
+
"items": {
|
|
619
|
+
"type": "string",
|
|
620
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
"cwd": {
|
|
624
|
+
"description": "The command's working directory (stdio only); relative to the YAML file's directory.",
|
|
625
|
+
"type": "string",
|
|
626
|
+
"minLength": 1
|
|
589
627
|
},
|
|
590
628
|
"tools": {
|
|
591
629
|
"minItems": 1,
|
|
@@ -694,7 +732,6 @@
|
|
|
694
732
|
},
|
|
695
733
|
"required": [
|
|
696
734
|
"name",
|
|
697
|
-
"url",
|
|
698
735
|
"tools"
|
|
699
736
|
],
|
|
700
737
|
"additionalProperties": false,
|
|
@@ -1570,9 +1607,47 @@
|
|
|
1570
1607
|
"description": "A short name for the server, unique on this agent: logs and errors name it."
|
|
1571
1608
|
},
|
|
1572
1609
|
"url": {
|
|
1610
|
+
"description": "The MCP server's URL: its Streamable HTTP endpoint, or a legacy SSE endpoint. Checked by the SSRF guard. Give url or command, not both.",
|
|
1573
1611
|
"type": "string",
|
|
1574
|
-
"format": "uri"
|
|
1575
|
-
|
|
1612
|
+
"format": "uri"
|
|
1613
|
+
},
|
|
1614
|
+
"command": {
|
|
1615
|
+
"description": "A local MCP server to start and speak to over stdio (ADR 0132), instead of url: the program to run. It runs only when the operator lists this exact value in MELCHIZEDEK_STDIO_MCP. One process per distinct command, args, env and cwd, kept for the life of the engine.",
|
|
1616
|
+
"type": "string",
|
|
1617
|
+
"minLength": 1
|
|
1618
|
+
},
|
|
1619
|
+
"args": {
|
|
1620
|
+
"description": "The command's arguments (stdio only).",
|
|
1621
|
+
"maxItems": 64,
|
|
1622
|
+
"type": "array",
|
|
1623
|
+
"items": {
|
|
1624
|
+
"type": "string"
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
"env": {
|
|
1628
|
+
"description": "Non-secret variables set for the command (stdio only). A credential is never written here: name it in pass_env.",
|
|
1629
|
+
"type": "object",
|
|
1630
|
+
"propertyNames": {
|
|
1631
|
+
"type": "string",
|
|
1632
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
|
|
1633
|
+
},
|
|
1634
|
+
"additionalProperties": {
|
|
1635
|
+
"type": "string"
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
"pass_env": {
|
|
1639
|
+
"description": "Variables copied by name from the engine's environment into the command's (stdio only); the command sees no other variable of the engine's beyond a minimal default set (PATH, HOME, …). A name that is unset fails the compile.",
|
|
1640
|
+
"maxItems": 32,
|
|
1641
|
+
"type": "array",
|
|
1642
|
+
"items": {
|
|
1643
|
+
"type": "string",
|
|
1644
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
"cwd": {
|
|
1648
|
+
"description": "The command's working directory (stdio only); relative to the YAML file's directory.",
|
|
1649
|
+
"type": "string",
|
|
1650
|
+
"minLength": 1
|
|
1576
1651
|
},
|
|
1577
1652
|
"tools": {
|
|
1578
1653
|
"minItems": 1,
|
|
@@ -1681,7 +1756,6 @@
|
|
|
1681
1756
|
},
|
|
1682
1757
|
"required": [
|
|
1683
1758
|
"name",
|
|
1684
|
-
"url",
|
|
1685
1759
|
"tools"
|
|
1686
1760
|
],
|
|
1687
1761
|
"additionalProperties": false,
|
package/db/hardening.sql
CHANGED
|
@@ -30,6 +30,27 @@
|
|
|
30
30
|
-- The A2A server checks at boot whether this file has been applied and
|
|
31
31
|
-- prints a prominent warning if not (see lib/persistence/supabaseProvider.ts).
|
|
32
32
|
|
|
33
|
+
-- ── The API roles ────────────────────────────────────────────────────────
|
|
34
|
+
-- anon and authenticated are Supabase's PostgREST roles. Plain Postgres and
|
|
35
|
+
-- embedded Postgres (PGlite) have no REST API and normally no such roles;
|
|
36
|
+
-- there, the PUBLIC revokes and RLS below are the whole lockdown, and the
|
|
37
|
+
-- revokes by role name are skipped rather than failing. These two helpers
|
|
38
|
+
-- live in pg_temp: they exist for this session only and leave nothing behind.
|
|
39
|
+
CREATE OR REPLACE FUNCTION pg_temp.melchizedek_api_roles() RETURNS text
|
|
40
|
+
LANGUAGE sql AS $$
|
|
41
|
+
SELECT string_agg(quote_ident(rolname), ', ' ORDER BY rolname)
|
|
42
|
+
FROM pg_roles WHERE rolname IN ('anon', 'authenticated');
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION pg_temp.melchizedek_revoke_api(target text) RETURNS void
|
|
46
|
+
LANGUAGE plpgsql AS $$
|
|
47
|
+
DECLARE roles text := pg_temp.melchizedek_api_roles();
|
|
48
|
+
BEGIN
|
|
49
|
+
IF roles IS NOT NULL THEN
|
|
50
|
+
EXECUTE format('REVOKE ALL ON %s FROM %s', target, roles);
|
|
51
|
+
END IF;
|
|
52
|
+
END $$;
|
|
53
|
+
|
|
33
54
|
-- ── Tier 1: lock the public API paths ────────────────────────────────────
|
|
34
55
|
|
|
35
56
|
-- Enable RLS. With RLS on and NO policies defined, anon/authenticated get
|
|
@@ -40,8 +61,11 @@ ALTER TABLE adk_sessions ENABLE ROW LEVEL SECURITY;
|
|
|
40
61
|
-- Belt and suspenders: also revoke the default table privileges from the
|
|
41
62
|
-- API roles, so even a future accidentally-created permissive policy
|
|
42
63
|
-- cannot re-expose the tables.
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
DO $$
|
|
65
|
+
BEGIN
|
|
66
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_memory_facts');
|
|
67
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_sessions');
|
|
68
|
+
END $$;
|
|
45
69
|
|
|
46
70
|
-- Optional observability ledger (db/telemetry.sql). Guarded: the tables only
|
|
47
71
|
-- exist when the operator opted into TELEMETRY_SUPABASE. adk_turns holds
|
|
@@ -54,14 +78,14 @@ BEGIN
|
|
|
54
78
|
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage', 'melchizedek_audit'] LOOP
|
|
55
79
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
56
80
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
|
57
|
-
|
|
81
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
|
|
58
82
|
END IF;
|
|
59
83
|
END LOOP;
|
|
60
84
|
-- The production view and the retention function read/delete those
|
|
61
85
|
-- tables on behalf of the caller; neither belongs on the public API.
|
|
62
86
|
FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly'] LOOP
|
|
63
87
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
64
|
-
|
|
88
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
|
|
65
89
|
END IF;
|
|
66
90
|
END LOOP;
|
|
67
91
|
-- Function privileges for these are handled in the FUNCTIONS block below.
|
|
@@ -73,7 +97,7 @@ DO $$
|
|
|
73
97
|
BEGIN
|
|
74
98
|
IF to_regclass('public.melchizedek_schema_version') IS NOT NULL THEN
|
|
75
99
|
EXECUTE 'ALTER TABLE melchizedek_schema_version ENABLE ROW LEVEL SECURITY';
|
|
76
|
-
|
|
100
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_schema_version');
|
|
77
101
|
END IF;
|
|
78
102
|
END $$;
|
|
79
103
|
|
|
@@ -84,29 +108,29 @@ DO $$
|
|
|
84
108
|
BEGIN
|
|
85
109
|
IF to_regclass('public.adk_agent_registry') IS NOT NULL THEN
|
|
86
110
|
EXECUTE 'ALTER TABLE adk_agent_registry ENABLE ROW LEVEL SECURITY';
|
|
87
|
-
|
|
111
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry');
|
|
88
112
|
END IF;
|
|
89
113
|
-- The task tools' lists and job queue (migration 0009).
|
|
90
114
|
IF to_regclass('public.melchizedek_tasks') IS NOT NULL THEN
|
|
91
115
|
EXECUTE 'ALTER TABLE melchizedek_tasks ENABLE ROW LEVEL SECURITY';
|
|
92
116
|
EXECUTE 'ALTER TABLE melchizedek_task_owners ENABLE ROW LEVEL SECURITY';
|
|
93
|
-
|
|
117
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tasks, melchizedek_task_owners');
|
|
94
118
|
END IF;
|
|
95
119
|
-- The memory processed marker (migration 0007): identifiers only.
|
|
96
120
|
IF to_regclass('public.melchizedek_memory_ingest') IS NOT NULL THEN
|
|
97
121
|
EXECUTE 'ALTER TABLE melchizedek_memory_ingest ENABLE ROW LEVEL SECURITY';
|
|
98
|
-
|
|
122
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_memory_ingest');
|
|
99
123
|
END IF;
|
|
100
124
|
-- Third-party tokens held for tools (migration 0013): ciphertext only,
|
|
101
125
|
-- still nothing an API role should read.
|
|
102
126
|
IF to_regclass('public.melchizedek_tool_credentials') IS NOT NULL THEN
|
|
103
127
|
EXECUTE 'ALTER TABLE melchizedek_tool_credentials ENABLE ROW LEVEL SECURITY';
|
|
104
|
-
|
|
128
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tool_credentials');
|
|
105
129
|
END IF;
|
|
106
130
|
-- Its history (migration 0005): every definition an id has held.
|
|
107
131
|
IF to_regclass('public.adk_agent_registry_versions') IS NOT NULL THEN
|
|
108
132
|
EXECUTE 'ALTER TABLE adk_agent_registry_versions ENABLE ROW LEVEL SECURITY';
|
|
109
|
-
|
|
133
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry_versions');
|
|
110
134
|
END IF;
|
|
111
135
|
END $$;
|
|
112
136
|
|
|
@@ -141,7 +165,8 @@ BEGIN
|
|
|
141
165
|
'melchizedek_memory_dimensions', 'melchizedek_prune_memory_ingest',
|
|
142
166
|
'melchizedek_prune_memory', 'melchizedek_prune_audit')
|
|
143
167
|
LOOP
|
|
144
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC
|
|
168
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC', fn);
|
|
169
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('FUNCTION %s', fn));
|
|
145
170
|
IF has_service_role THEN
|
|
146
171
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);
|
|
147
172
|
END IF;
|
|
@@ -170,9 +195,10 @@ AS $$
|
|
|
170
195
|
'melchizedek_audit', 'melchizedek_tool_credentials');
|
|
171
196
|
$$;
|
|
172
197
|
|
|
173
|
-
REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC
|
|
198
|
+
REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC;
|
|
174
199
|
DO $$
|
|
175
200
|
BEGIN
|
|
201
|
+
PERFORM pg_temp.melchizedek_revoke_api('FUNCTION melchizedek_rls_status()');
|
|
176
202
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
177
203
|
GRANT EXECUTE ON FUNCTION melchizedek_rls_status() TO service_role;
|
|
178
204
|
END IF;
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -68,6 +68,7 @@ import { setOAuthHosts } from '../tools/oauthHosts.js';
|
|
|
68
68
|
import { setCredentialHosts } from '../tools/credentialHosts.js';
|
|
69
69
|
import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
|
|
70
70
|
import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
|
|
71
|
+
import { credentialValue } from '../models/credentialSource.js';
|
|
71
72
|
const SURFACE_HEADERS = [
|
|
72
73
|
['x-surface', 'name'],
|
|
73
74
|
['x-surface-guild', 'guild'],
|
|
@@ -335,7 +336,7 @@ export async function createA2AApp(options) {
|
|
|
335
336
|
// Embeddings and fact extraction use the SERVER's Gemini key: memory is
|
|
336
337
|
// operator infrastructure, like tools.
|
|
337
338
|
const services = await createSupabaseServices({
|
|
338
|
-
apiKey:
|
|
339
|
+
apiKey: credentialValue('GOOGLE_GENAI_API_KEY') || credentialValue('GEMINI_API_KEY') || '',
|
|
339
340
|
withMemory: true,
|
|
340
341
|
extractor: options.memory?.extractor,
|
|
341
342
|
embedder: options.memory?.embedder,
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* times and the file's path.
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { chatGptSignInFile } from '../paths.js';
|
|
26
|
+
import { credentialEnv } from '../models/credentialSource.js';
|
|
26
27
|
/** Where the credential file lives, when not the default (a path; never a value). */
|
|
27
28
|
export const CHATGPT_SIGNIN_FILE_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE';
|
|
28
29
|
/** `off` ignores a stored sign-in (a served process on a machine that also signed in). */
|
|
@@ -31,8 +32,7 @@ export const CHATGPT_SIGNIN_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN';
|
|
|
31
32
|
export const PLAN_SCOPE = 'chatgpt.tokens.use.direct';
|
|
32
33
|
/** The credential file: MELCHIZEDEK_CHATGPT_SIGNIN_FILE, else ~/.melchizedek/chatgpt-signin.json. */
|
|
33
34
|
export function signInFile(env = process.env) {
|
|
34
|
-
|
|
35
|
-
return set ? resolve(set) : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
|
|
35
|
+
return chatGptSignInFile(env);
|
|
36
36
|
}
|
|
37
37
|
/** The git work tree a path sits inside, if any (an ancestor holding `.git`). */
|
|
38
38
|
export function insideGitWorkTree(file) {
|
|
@@ -100,7 +100,7 @@ export function chatGptSignInStatus(env = process.env) {
|
|
|
100
100
|
* OPENAI_PLATFORM other than direct, no OPENAI_BASE_URL proxy, which must
|
|
101
101
|
* never receive the token).
|
|
102
102
|
*/
|
|
103
|
-
export function chatGptSignInRoutesOpenAi(env =
|
|
103
|
+
export function chatGptSignInRoutesOpenAi(env = credentialEnv()) {
|
|
104
104
|
if (env.OPENAI_API_KEY?.trim())
|
|
105
105
|
return false;
|
|
106
106
|
const platform = env.OPENAI_PLATFORM?.trim().toLowerCase();
|
package/dist/lib/compile.js
CHANGED
|
@@ -42,7 +42,7 @@ import { REASONING_OLDER_SPELLING } from './syndicateSchema.js';
|
|
|
42
42
|
import { reasoningConfig } from './models/reasoning.js';
|
|
43
43
|
import { instructionFor, toEngineAgent } from './agentDialect.js';
|
|
44
44
|
import { resolveTools as resolveNamedTools } from './toolRegistry.js';
|
|
45
|
-
import { createMcpTools } from './tools/mcpToolFactory.js';
|
|
45
|
+
import { createMcpTools, loadStdioMcpTools } from './tools/mcpToolFactory.js';
|
|
46
46
|
import { requireApproval, toolOf } from './tools/tool.js';
|
|
47
47
|
import { examplesInstructionTool } from './tools/examples.js';
|
|
48
48
|
import { capabilitySummary, describeCapabilities } from './models/capabilities.js';
|
|
@@ -145,6 +145,17 @@ export function examplesTool(examples) {
|
|
|
145
145
|
const tool = examplesInstructionTool(examples);
|
|
146
146
|
return tool ? [tool] : [];
|
|
147
147
|
}
|
|
148
|
+
/** A stdio server's tools (ADR 0132), narrowed to the names the entry lists. */
|
|
149
|
+
async function stdioOfferedBy(server, opts) {
|
|
150
|
+
const label = `${server.name} (stdio: ${server.command})`;
|
|
151
|
+
opts.log?.(`Loading MCP tools: ${label}`);
|
|
152
|
+
const offered = await loadStdioMcpTools(server.name, { command: server.command, args: server.args, env: server.env, pass_env: server.pass_env, cwd: server.cwd });
|
|
153
|
+
const allowed = new Set(server.tools);
|
|
154
|
+
const missing = server.tools.filter((n) => !offered.some((t) => t.name === n));
|
|
155
|
+
if (missing.length)
|
|
156
|
+
opts.log?.(`MCP ${label} does not offer ${missing.map((n) => `'${n}'`).join(', ')}`);
|
|
157
|
+
return offered.filter((t) => allowed.has(t.name));
|
|
158
|
+
}
|
|
148
159
|
async function resolveAgentTools(agent, opts) {
|
|
149
160
|
const tools = [...resolveNamedTools(agent.tools, opts.onUnknownTool), ...examplesTool(agent.examples)];
|
|
150
161
|
// OpenAPI operations become tools here, so require_approval can name them.
|
|
@@ -185,7 +196,9 @@ async function resolveAgentTools(agent, opts) {
|
|
|
185
196
|
// mcp_servers (ADR 0124): the loader refused a declared name twice; a
|
|
186
197
|
// server that answers with a name already taken is refused here too, so
|
|
187
198
|
// 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) =>
|
|
199
|
+
const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => server.command !== undefined
|
|
200
|
+
? stdioOfferedBy(server, opts)
|
|
201
|
+
: offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
|
|
189
202
|
for (const [i, offered] of servers.entries()) {
|
|
190
203
|
const server = agent.mcp_servers[i];
|
|
191
204
|
for (const mcpTool of offered) {
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -57,3 +57,5 @@ export type { MemoryStore } from './memory/store.ts';
|
|
|
57
57
|
export { validateSyndicateConfig, syndicateJsonSchema, SyndicateValidationError } from './syndicateSchema.ts';
|
|
58
58
|
export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logProviderStatuses, resolveModel, describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.ts';
|
|
59
59
|
export { loadEnv } from './loadEnv.ts';
|
|
60
|
+
export { setCredentialSource, credentialValue } from './models/credentialSource.ts';
|
|
61
|
+
export type { CredentialSource } from './models/credentialSource.ts';
|
package/dist/lib/index.js
CHANGED
|
@@ -48,3 +48,5 @@ export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logP
|
|
|
48
48
|
// subpath until 0.16.0.
|
|
49
49
|
describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.js';
|
|
50
50
|
export { loadEnv } from './loadEnv.js';
|
|
51
|
+
// Where credentials are read, by environment variable name (ADR 0133).
|
|
52
|
+
export { setCredentialSource, credentialValue } from './models/credentialSource.js';
|
|
@@ -261,6 +261,10 @@ function anchorSpecPaths(config, dir) {
|
|
|
261
261
|
for (const entry of agent?.openapi ?? [])
|
|
262
262
|
if (!path.isAbsolute(entry.spec))
|
|
263
263
|
entry.spec = path.resolve(dir, entry.spec);
|
|
264
|
+
// A stdio server's working directory, like a spec, is the YAML's (ADR 0132).
|
|
265
|
+
for (const entry of agent?.mcp_servers ?? [])
|
|
266
|
+
if (entry.cwd && !path.isAbsolute(entry.cwd))
|
|
267
|
+
entry.cwd = path.resolve(dir, entry.cwd);
|
|
264
268
|
};
|
|
265
269
|
anchor(config.orchestrator);
|
|
266
270
|
for (const sub of config.subagents ?? [])
|
|
@@ -37,6 +37,7 @@ import { providerForModel } from '../models/providerMap.js';
|
|
|
37
37
|
import { resolveModel } from '../models/registry.js';
|
|
38
38
|
import { startEngineSpan, traceLlmGeneration } from '../observability/tracer.js';
|
|
39
39
|
import { trimTrailingSlashes } from '../models/urls.js';
|
|
40
|
+
import { credentialEnv } from '../models/credentialSource.js';
|
|
40
41
|
/**
|
|
41
42
|
* Extraction on any model id. The key passed here reaches only a model of
|
|
42
43
|
* the matching provider (resolveModel's BYOK rule); other providers read
|
|
@@ -194,7 +195,7 @@ export const DEFAULT_EMBEDDING_MODELS = {
|
|
|
194
195
|
* already passes for memory; it is used only when the configured extraction
|
|
195
196
|
* model or embedder is Gemini.
|
|
196
197
|
*/
|
|
197
|
-
export function memoryProvidersFromEnv(env =
|
|
198
|
+
export function memoryProvidersFromEnv(env = credentialEnv(), geminiKey) {
|
|
198
199
|
const extractionModel = env.MEMORY_EXTRACTION_MODEL?.trim() || MEMORY_EXTRACTION_MODEL;
|
|
199
200
|
const extractor = modelExtractor({
|
|
200
201
|
model: extractionModel,
|
|
@@ -47,6 +47,7 @@ import { currentTurnStart, providerStateOf, withProviderState } from './provider
|
|
|
47
47
|
import { errorDecision, errorText } from './errorResponse.js';
|
|
48
48
|
import { adaptiveThinkingFor, claudeGeneration, claudeReasoningOf, claudeUrlImagesOn, THINKING_BINDING_BETA } from './claudeModels.js';
|
|
49
49
|
import { isAboveHigh } from './reasoning.js';
|
|
50
|
+
import { credentialValue } from './credentialSource.js';
|
|
50
51
|
/** The provider id this adapter reports and writes its state under (lib/models/providerMap.ts). */
|
|
51
52
|
export const ANTHROPIC_PROVIDER = 'anthropic';
|
|
52
53
|
/** The providerState kind this adapter writes: the signed blocks that preceded a part. */
|
|
@@ -217,7 +218,7 @@ export class ClaudeAdapter {
|
|
|
217
218
|
yield setupFailure('ENDPOINT_MISCONFIGURED', messageOf(err));
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
|
-
const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey ||
|
|
221
|
+
const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey || credentialValue('ANTHROPIC_API_KEY'));
|
|
221
222
|
if ('error' in spec) {
|
|
222
223
|
yield setupFailure(endpoint.platform === 'direct' ? 'MISSING_API_KEY' : 'ENDPOINT_MISCONFIGURED', spec.error);
|
|
223
224
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/credentialSource.ts — where the engine reads a credential by
|
|
3
|
+
* its environment variable name (ADR 0133).
|
|
4
|
+
*
|
|
5
|
+
* By default every provider key, gateway key and embedding key is read from
|
|
6
|
+
* `process.env`. An embedding application that holds keys elsewhere (an OS
|
|
7
|
+
* keychain, a secret manager) sets a source once:
|
|
8
|
+
*
|
|
9
|
+
* setCredentialSource((name) => keychain.get(name));
|
|
10
|
+
*
|
|
11
|
+
* and every read below asks the source first, by the same names the
|
|
12
|
+
* environment uses (ANTHROPIC_API_KEY, MODEL_GATEWAY_API_KEY, …), at the
|
|
13
|
+
* moment of use: nothing is cached, so a key the user removes stops working
|
|
14
|
+
* on the next call. A source that returns undefined for a name falls back to
|
|
15
|
+
* the environment. A dependency-free leaf, so providerMap.ts stays one.
|
|
16
|
+
*/
|
|
17
|
+
/** Returns the credential held under an environment variable name, or undefined. Must not throw. */
|
|
18
|
+
export type CredentialSource = (name: string) => string | undefined;
|
|
19
|
+
/** Sets (or, with undefined, clears) the process's credential source. */
|
|
20
|
+
export declare function setCredentialSource(next: CredentialSource | undefined): void;
|
|
21
|
+
/** The credential named `name`: the source's, else `env`'s. A source that throws counts as holding nothing. */
|
|
22
|
+
export declare function credentialValue(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* `env` as the engine's env-reading helpers see it: a read by name asks the
|
|
25
|
+
* source first. For functions that take an `env` parameter; enumeration and
|
|
26
|
+
* spreading see only `env`'s own variables.
|
|
27
|
+
*/
|
|
28
|
+
export declare function credentialEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|