melchizedek-agents 1.3.1 → 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 +134 -0
- package/DOCUMENTATION.md +61 -5
- package/ONBOARDING.md +3 -3
- package/README.md +1 -0
- package/config/agents/examples/connectors.yaml +4 -1
- package/config/agents/examples/event_assistant.yaml +82 -0
- 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/compileNative.js +1 -0
- 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/runtime/credentials.d.ts +21 -0
- package/dist/lib/runtime/credentials.js +32 -0
- package/dist/lib/runtime/native/delegate.d.ts +2 -0
- package/dist/lib/runtime/native/delegate.js +9 -2
- package/dist/lib/runtime/native/interrupts.d.ts +33 -3
- package/dist/lib/runtime/native/interrupts.js +99 -9
- package/dist/lib/runtime/syndicateTurn.js +41 -11
- 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 +63 -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 +200 -27
- package/dist/lib/tools/mcpTransports.d.ts +4 -2
- package/dist/lib/tools/mcpTransports.js +4 -2
- package/dist/lib/tools/taskTools.js +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/workflow/agentNode.d.ts +19 -3
- package/dist/lib/workflow/agentNode.js +106 -13
- package/dist/lib/workflow/resume.d.ts +8 -7
- package/dist/lib/workflow/resume.js +17 -10
- package/dist/lib/workflow/turn.d.ts +12 -2
- package/dist/lib/workflow/turn.js +22 -12
- package/dist/scripts/a2a_server.js +5 -0
- package/package.json +11 -4
|
@@ -70,7 +70,7 @@ import { traceAgentRun } from '../observability/tracer.js';
|
|
|
70
70
|
import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
|
|
71
71
|
import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
|
|
72
72
|
import { pendingQuestion, questionAnswerPart, questionFrom, turnStartOfCall } from './questions.js';
|
|
73
|
-
import { deepestPause, delegatedPauses, nestedWorkflowPause, routePause } from './native/interrupts.js';
|
|
73
|
+
import { deepestPause, delegatedPauses, nestedWorkflowPause, routePause, walkConsents } from './native/interrupts.js';
|
|
74
74
|
import { consentPinnedTo, entrySession } from './native/delegate.js';
|
|
75
75
|
import { workflowPauseEvent } from '../workflow/pause.js';
|
|
76
76
|
import { INPUT_REQUEST } from '../workflowConfig.js';
|
|
@@ -485,6 +485,15 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
485
485
|
ev.log?.(`⏸ Authorization needed: ${(pending.path ?? [pending.agent]).join(' → ')} → ${pending.provider}`);
|
|
486
486
|
return finish();
|
|
487
487
|
};
|
|
488
|
+
/** Whether the grant a consent request waits for is stored (an expired or unreadable one is not). */
|
|
489
|
+
const grantStored = async (open) => {
|
|
490
|
+
try {
|
|
491
|
+
return !!open.provider && !!(await credentialStore?.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
488
497
|
let granting;
|
|
489
498
|
// A call waiting for a grant inside a delegated subagent (ADR 0118): the grant's answer is stored here, and the open call carries it down.
|
|
490
499
|
let grantingBelow;
|
|
@@ -508,14 +517,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
508
517
|
result.error = { code: 'CONSENT_UNAVAILABLE', message: `A call waits for a ${open.provider} authorization, but this turn has no credential store.` };
|
|
509
518
|
return finish();
|
|
510
519
|
}
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
granted = !!open.provider && !!(await credentialStore.get({ appName: credentialApp, userId, provider: open.provider }));
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
// Expired or unreadable: not granted. The request stands.
|
|
517
|
-
}
|
|
518
|
-
if (!granted)
|
|
520
|
+
if (!(await grantStored(open)))
|
|
519
521
|
return consentPause(open);
|
|
520
522
|
// The route's own turn carries the grant's answer down to its call; the message goes to it as it came.
|
|
521
523
|
if (routeWaits)
|
|
@@ -527,6 +529,28 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
527
529
|
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
528
530
|
}
|
|
529
531
|
}
|
|
532
|
+
// A walk waiting for grants (ADR 0128): a node's own consent request, or one a node raised again from below it. Once every
|
|
533
|
+
// grant is stored, the message becomes their answers and the walk resumes those nodes; until then it repeats the first
|
|
534
|
+
// request still missing and stores nothing, as at the top.
|
|
535
|
+
if (!decision && isWorkflowSyndicate(config)) {
|
|
536
|
+
const waiting = await walkConsents(pauseKey, existing?.events ?? []);
|
|
537
|
+
if (waiting.length > 0) {
|
|
538
|
+
if (!credentialStore) {
|
|
539
|
+
result.status = 'failed';
|
|
540
|
+
result.error = { code: 'CONSENT_UNAVAILABLE', message: `A call waits for a ${waiting[0].provider} authorization, but this turn has no credential store.` };
|
|
541
|
+
return finish();
|
|
542
|
+
}
|
|
543
|
+
for (const open of waiting)
|
|
544
|
+
if (!(await grantStored(open)))
|
|
545
|
+
return consentPause(open);
|
|
546
|
+
granting = waiting[0];
|
|
547
|
+
parts = waiting.map((open) => credentialResponsePart(open.id, open.provider));
|
|
548
|
+
for (const open of waiting)
|
|
549
|
+
ev.log?.(`✓ Authorization granted: ${(open.path ?? [open.agent]).join(' → ')} → ${open.provider}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** The parts a paused workflow route's walk resumes on once its grant is stored (ADR 0128): the grant's answer to its credential request. */
|
|
553
|
+
const routeGrant = consentRoute?.consent ? [credentialResponsePart(consentRoute.consent.id, consentRoute.consent.provider)] : undefined;
|
|
530
554
|
// ── Questions (lib/runtime/questions.ts) ───────────────────────────────────
|
|
531
555
|
// While an agent's `ask_user` call is open, a plain-text message is its
|
|
532
556
|
// answer: it becomes that call's response, and the agent that asked
|
|
@@ -651,6 +675,8 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
651
675
|
memory: nativeMemory(opts.memoryService),
|
|
652
676
|
...(selfCorrection ? { selfCorrection } : {}),
|
|
653
677
|
...(credentialStore ? { credentials: credentialStore } : {}),
|
|
678
|
+
// The consent step, its flows bound to the app the grants are read under, wherever the walk's session is filed (ADR 0128).
|
|
679
|
+
...(consentStep ? { consent: consentPinnedTo(consentStep, credentialApp) } : {}),
|
|
654
680
|
...(compileOpts.log ? { log: compileOpts.log } : {}),
|
|
655
681
|
});
|
|
656
682
|
}
|
|
@@ -747,7 +773,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
747
773
|
throw new Error(`Session not found: ${sessionId} (appName=${appName}, userId=${userId})`);
|
|
748
774
|
const kept = (state) => Object.fromEntries(Object.entries(state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
|
|
749
775
|
// Filed under the agent path (ADR 0119), else continued under the route's name alone where ADR 0106 filed it.
|
|
750
|
-
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined;
|
|
776
|
+
const resumingWalk = resumingRoute !== undefined || routeAnswer !== undefined || consentRoute !== undefined;
|
|
751
777
|
const child = await entrySession(store, { appName, userId, sessionId, events: shared.events }, route, 'route', kept(shared.state), resumingWalk);
|
|
752
778
|
const childKey = { appName: child.appName, userId, sessionId };
|
|
753
779
|
const before = new Map(Object.entries(kept(child.state)).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
@@ -760,7 +786,7 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
760
786
|
};
|
|
761
787
|
let walked;
|
|
762
788
|
try {
|
|
763
|
-
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
789
|
+
walked = await runAgent({ agent, sid: sessionId, appName: child.appName, userParts: routeAnswer ?? routeGrant ?? parts, sessions: sessionService, stage: 'dispatch', route: resolution, publishToolStatus: true, errorPolicy: 'collect' });
|
|
764
790
|
}
|
|
765
791
|
catch (err) {
|
|
766
792
|
const last = control.stopReason ? undefined : err;
|
|
@@ -1087,6 +1113,10 @@ async function runTurnInner(opts, control, _runtime, nestedTurn) {
|
|
|
1087
1113
|
const pending = pendingApproval(after?.events ?? []);
|
|
1088
1114
|
if (pending)
|
|
1089
1115
|
return pause(await deepen(pending));
|
|
1116
|
+
// A node waiting for a grant (ADR 0128): its own request, or one from below it, with the path from the node down.
|
|
1117
|
+
const [consent] = await walkConsents(pauseKey, after?.events ?? []);
|
|
1118
|
+
if (consent)
|
|
1119
|
+
return consentPause(consent);
|
|
1090
1120
|
if (answer.inputRequests.length)
|
|
1091
1121
|
return question(await deepen(answer.inputRequests[answer.inputRequests.length - 1]));
|
|
1092
1122
|
result.text = answer.text;
|
|
@@ -19,6 +19,7 @@ import { PostgresSessionService } from './sessionService.ts';
|
|
|
19
19
|
import { PostgresTaskStore } from './taskStore.ts';
|
|
20
20
|
import type { CredentialStoreOptions } from '../../tools/credentialStore.ts';
|
|
21
21
|
import type { CredentialStore } from '../../tools/auth.ts';
|
|
22
|
+
import type { PayloadPolicy } from '../../observability/supabaseSpanExporter.ts';
|
|
22
23
|
import type { AuditSink } from '../../observability/audit.ts';
|
|
23
24
|
import type { RlsHardeningStatus } from '../rlsStatus.ts';
|
|
24
25
|
import type { TaskBackend, TaskBackendOptions } from '../../tools/taskTools.ts';
|
|
@@ -28,6 +29,8 @@ export { PostgresSessionService } from './sessionService.ts';
|
|
|
28
29
|
export { PostgresTaskStore } from './taskStore.ts';
|
|
29
30
|
export { postgresMemoryStore } from './memoryStore.ts';
|
|
30
31
|
export { postgresCredentialRows, postgresCredentialStore } from './credentialStore.ts';
|
|
32
|
+
export { inProcessTurnLock } from '../../a2a/turnLock.ts';
|
|
33
|
+
export type { TurnLock } from '../../a2a/turnLock.ts';
|
|
31
34
|
export interface PostgresStorageOptions {
|
|
32
35
|
/** A Postgres URL. Ignored when `pool` is given. */
|
|
33
36
|
connectionString?: string;
|
|
@@ -54,6 +57,28 @@ export interface PostgresStorageOptions {
|
|
|
54
57
|
};
|
|
55
58
|
/** Owner of an A2A call, for the task store. Default: the SDK's user scope. */
|
|
56
59
|
taskOwner?: PostgresTaskStoreOptions['ownerResolver'];
|
|
60
|
+
/**
|
|
61
|
+
* The turn lock to use instead of the advisory lock below. The advisory
|
|
62
|
+
* lock is what makes several instances on one database take turns; it
|
|
63
|
+
* needs a database that gives each connection its own session. Embedded
|
|
64
|
+
* Postgres (PGlite behind pglite-socket) runs every connection in ONE
|
|
65
|
+
* session, where an advisory lock is re-entrant and so excludes nothing:
|
|
66
|
+
* there, one process owns the database, and `inProcessTurnLock()`
|
|
67
|
+
* (exported here) is the correct lock. When given, no turn-lock
|
|
68
|
+
* pool is opened.
|
|
69
|
+
*/
|
|
70
|
+
turnLock?: TurnLock;
|
|
71
|
+
/**
|
|
72
|
+
* Write the telemetry ledger (adk_turns, adk_telemetry, adk_payloads) to
|
|
73
|
+
* this database (ADR 0131): the rows the Supabase sink writes, with the
|
|
74
|
+
* same payload policy (TELEMETRY_PAYLOADS, or `policy`) and redaction.
|
|
75
|
+
* Needs db/telemetry.sql applied. `close()` flushes and detaches it.
|
|
76
|
+
* Default off.
|
|
77
|
+
*/
|
|
78
|
+
ledger?: boolean | {
|
|
79
|
+
policy?: PayloadPolicy;
|
|
80
|
+
deadLetterFile?: string;
|
|
81
|
+
};
|
|
57
82
|
/**
|
|
58
83
|
* Connections reserved for turn locks: each running turn holds one for its
|
|
59
84
|
* length, apart from the main pool so a long turn never starves queries.
|
|
@@ -22,12 +22,16 @@ import { postgresCredentialStore } from './credentialStore.js';
|
|
|
22
22
|
import { searchPathOption } from '../schema.js';
|
|
23
23
|
import { POSTGRES_RLS_QUERY, evaluatePostgresRls } from '../rlsStatus.js';
|
|
24
24
|
import { postgresAuditSink } from '../../observability/audit.js';
|
|
25
|
+
import { attachLedger } from '../../observability/tracer.js';
|
|
26
|
+
import { postgresLedgerClient } from '../../observability/supabaseSpanExporter.js';
|
|
25
27
|
import { createHash, randomUUID } from 'node:crypto';
|
|
26
28
|
import { hostname } from 'node:os';
|
|
27
29
|
export { PostgresSessionService } from './sessionService.js';
|
|
28
30
|
export { PostgresTaskStore } from './taskStore.js';
|
|
29
31
|
export { postgresMemoryStore } from './memoryStore.js';
|
|
30
32
|
export { postgresCredentialRows, postgresCredentialStore } from './credentialStore.js';
|
|
33
|
+
// The single-process turn lock, for an embedded database (`turnLock` below).
|
|
34
|
+
export { inProcessTurnLock } from '../../a2a/turnLock.js';
|
|
31
35
|
/**
|
|
32
36
|
* A pool emits 'error' when an idle connection dies (a database restart, a
|
|
33
37
|
* failover, an administrator's terminate); unhandled, that event crashes the
|
|
@@ -69,7 +73,7 @@ export function postgresStorage(options) {
|
|
|
69
73
|
...(options.pool ? options.pool.options : { connectionString: options.connectionString, ...options.poolConfig }),
|
|
70
74
|
max: options.lockPoolMax ?? 20,
|
|
71
75
|
}), 'turn-lock pool'));
|
|
72
|
-
const
|
|
76
|
+
const advisoryTurnLock = async (key, { waitMs, signal }) => {
|
|
73
77
|
const lockId = advisoryLockId(key);
|
|
74
78
|
const client = await locks().connect();
|
|
75
79
|
const deadline = Date.now() + Math.max(0, waitMs);
|
|
@@ -105,6 +109,10 @@ export function postgresStorage(options) {
|
|
|
105
109
|
};
|
|
106
110
|
return release;
|
|
107
111
|
};
|
|
112
|
+
const turnLock = options.turnLock ?? advisoryTurnLock;
|
|
113
|
+
const detachLedger = options.ledger
|
|
114
|
+
? attachLedger(postgresLedgerClient(pool), options.ledger === true ? {} : options.ledger)
|
|
115
|
+
: undefined;
|
|
108
116
|
const audit = postgresAuditSink(pool);
|
|
109
117
|
const credentials = options.credentials ? postgresCredentialStore(pool, { ...options.credentials, audit }) : undefined;
|
|
110
118
|
return {
|
|
@@ -160,6 +168,7 @@ export function postgresStorage(options) {
|
|
|
160
168
|
return counts;
|
|
161
169
|
},
|
|
162
170
|
async close() {
|
|
171
|
+
await detachLedger?.();
|
|
163
172
|
if (lockPool)
|
|
164
173
|
await lockPool.end();
|
|
165
174
|
if (owned)
|
|
@@ -28,6 +28,8 @@ export declare const REASONING_LEVELS: readonly ['none', 'low', 'medium', 'high'
|
|
|
28
28
|
export declare const OUTPUT_MIMES: readonly ['application/json', 'text/plain'];
|
|
29
29
|
export declare const TOOL_CHOICE_MODES: readonly ['auto', 'none', 'required'];
|
|
30
30
|
export declare const SKILL_SCRIPT_MODES: readonly ['none', 'local'];
|
|
31
|
+
/** An `env` key that names a credential: such a value belongs in pass_env, never in the YAML (ADR 0132). */
|
|
32
|
+
export declare const SECRET_ENV_KEY: RegExp;
|
|
31
33
|
/** An `mcp_servers` entry's name: lowercase, bounded, one character class (no backtracking). */
|
|
32
34
|
export declare const MCP_SERVER_NAME: RegExp;
|
|
33
35
|
export declare const agentSchema: z.ZodObject<{
|
|
@@ -150,7 +152,12 @@ export declare const agentSchema: z.ZodObject<{
|
|
|
150
152
|
}>>;
|
|
151
153
|
mcp_servers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
152
154
|
name: z.ZodString;
|
|
153
|
-
url: z.ZodString
|
|
155
|
+
url: z.ZodOptional<z.ZodString>;
|
|
156
|
+
command: z.ZodOptional<z.ZodString>;
|
|
157
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
158
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
159
|
+
pass_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
160
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
154
161
|
tools: z.ZodArray<z.ZodString>;
|
|
155
162
|
auth: z.ZodOptional<z.ZodObject<{
|
|
156
163
|
oauth2: z.ZodObject<{
|
|
@@ -359,7 +366,12 @@ export declare const subagentSchema: z.ZodObject<{
|
|
|
359
366
|
}>>;
|
|
360
367
|
mcp_servers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
361
368
|
name: z.ZodString;
|
|
362
|
-
url: z.ZodString
|
|
369
|
+
url: z.ZodOptional<z.ZodString>;
|
|
370
|
+
command: z.ZodOptional<z.ZodString>;
|
|
371
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
372
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
373
|
+
pass_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
374
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
363
375
|
tools: z.ZodArray<z.ZodString>;
|
|
364
376
|
auth: z.ZodOptional<z.ZodObject<{
|
|
365
377
|
oauth2: z.ZodObject<{
|
|
@@ -572,7 +584,12 @@ export declare const syndicateSchema: z.ZodObject<{
|
|
|
572
584
|
}>>;
|
|
573
585
|
mcp_servers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
574
586
|
name: z.ZodString;
|
|
575
|
-
url: z.ZodString
|
|
587
|
+
url: z.ZodOptional<z.ZodString>;
|
|
588
|
+
command: z.ZodOptional<z.ZodString>;
|
|
589
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
590
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
591
|
+
pass_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
592
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
576
593
|
tools: z.ZodArray<z.ZodString>;
|
|
577
594
|
auth: z.ZodOptional<z.ZodObject<{
|
|
578
595
|
oauth2: z.ZodObject<{
|
|
@@ -775,7 +792,12 @@ export declare const syndicateSchema: z.ZodObject<{
|
|
|
775
792
|
}>>;
|
|
776
793
|
mcp_servers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
777
794
|
name: z.ZodString;
|
|
778
|
-
url: z.ZodString
|
|
795
|
+
url: z.ZodOptional<z.ZodString>;
|
|
796
|
+
command: z.ZodOptional<z.ZodString>;
|
|
797
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
798
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
799
|
+
pass_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
800
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
779
801
|
tools: z.ZodArray<z.ZodString>;
|
|
780
802
|
auth: z.ZodOptional<z.ZodObject<{
|
|
781
803
|
oauth2: z.ZodObject<{
|
|
@@ -272,6 +272,10 @@ const oauth2Auth = z
|
|
|
272
272
|
const mcpTransport = z
|
|
273
273
|
.enum(MCP_TRANSPORTS)
|
|
274
274
|
.describe('How the MCP server is reached: auto (the default) tries Streamable HTTP and falls back to the legacy SSE transport on the spec\'s signal; streamable_http or sse use that one only (ADR 0124).');
|
|
275
|
+
/** An environment variable name, as a stdio MCP entry's env and pass_env keys must be. */
|
|
276
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
277
|
+
/** An `env` key that names a credential: such a value belongs in pass_env, never in the YAML (ADR 0132). */
|
|
278
|
+
export const SECRET_ENV_KEY = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)/i;
|
|
275
279
|
/** An `mcp_servers` entry's name: lowercase, bounded, one character class (no backtracking). */
|
|
276
280
|
export const MCP_SERVER_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
277
281
|
const mcpServerEntry = z
|
|
@@ -280,7 +284,23 @@ const mcpServerEntry = z
|
|
|
280
284
|
.string()
|
|
281
285
|
.regex(MCP_SERVER_NAME, 'lowercase letters, digits, _ and -, starting with a letter (at most 64)')
|
|
282
286
|
.describe('A short name for the server, unique on this agent: logs and errors name it.'),
|
|
283
|
-
url: z.string().url().describe('The MCP server\'s URL: its Streamable HTTP endpoint, or a legacy SSE endpoint. Checked by the SSRF guard.'),
|
|
287
|
+
url: z.string().url().optional().describe('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.'),
|
|
288
|
+
command: z
|
|
289
|
+
.string()
|
|
290
|
+
.min(1)
|
|
291
|
+
.optional()
|
|
292
|
+
.describe('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.'),
|
|
293
|
+
args: z.array(z.string()).max(64).optional().describe('The command\'s arguments (stdio only).'),
|
|
294
|
+
env: z
|
|
295
|
+
.record(z.string().regex(ENV_NAME, 'an environment variable name'), z.string())
|
|
296
|
+
.optional()
|
|
297
|
+
.describe('Non-secret variables set for the command (stdio only). A credential is never written here: name it in pass_env.'),
|
|
298
|
+
pass_env: z
|
|
299
|
+
.array(z.string().regex(ENV_NAME, 'an environment variable name'))
|
|
300
|
+
.max(32)
|
|
301
|
+
.optional()
|
|
302
|
+
.describe('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.'),
|
|
303
|
+
cwd: z.string().min(1).optional().describe('The command\'s working directory (stdio only); relative to the YAML file\'s directory.'),
|
|
284
304
|
tools: z
|
|
285
305
|
.array(z.string().min(1))
|
|
286
306
|
.min(1)
|
|
@@ -759,6 +779,30 @@ function mcpServersProblems(agent, path) {
|
|
|
759
779
|
if (!isObj(entry))
|
|
760
780
|
return;
|
|
761
781
|
const name = typeof entry.name === 'string' ? entry.name : `#${i}`;
|
|
782
|
+
// url or command (ADR 0132), and each one's own keys only.
|
|
783
|
+
const at = (key) => [...path, 'mcp_servers', i, key];
|
|
784
|
+
if (entry.url === undefined && entry.command === undefined)
|
|
785
|
+
out.push({ path: at('url'), message: 'give url (a remote server) or command (a local stdio server) (ADR 0132)' });
|
|
786
|
+
if (entry.url !== undefined && entry.command !== undefined)
|
|
787
|
+
out.push({ path: at('command'), message: 'url and command cannot both be given: one entry is one server (ADR 0132)' });
|
|
788
|
+
if (entry.command !== undefined) {
|
|
789
|
+
for (const key of ['transport', 'auth']) {
|
|
790
|
+
if (entry[key] !== undefined)
|
|
791
|
+
out.push({ path: at(key), message: `${key} is how a url is reached; a command entry speaks stdio and takes no ${key} (ADR 0132)` });
|
|
792
|
+
}
|
|
793
|
+
if (isObj(entry.env)) {
|
|
794
|
+
for (const key of Object.keys(entry.env)) {
|
|
795
|
+
if (SECRET_ENV_KEY.test(key))
|
|
796
|
+
out.push({ path: [...at('env'), key], message: `'${key}' looks like a credential: never write its value in the YAML; list the name in pass_env instead (ADR 0132)` });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
for (const key of ['args', 'env', 'pass_env', 'cwd']) {
|
|
802
|
+
if (entry[key] !== undefined)
|
|
803
|
+
out.push({ path: at(key), message: `${key} configures a command entry; this entry has a url (ADR 0132)` });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
762
806
|
if (typeof entry.name === 'string') {
|
|
763
807
|
if (names.has(entry.name))
|
|
764
808
|
out.push({ path: [...path, 'mcp_servers', i, 'name'], message: `'${entry.name}' names mcp_servers entry ${names.get(entry.name)} too; each server's name is unique on the agent` });
|
|
@@ -1153,12 +1197,30 @@ function workflowProblems(raw, subs) {
|
|
|
1153
1197
|
if (typeof agent.name === 'string' && mapped.has(agent.name) && isObj(agent.skills) && agent.skills.scripts === 'local') {
|
|
1154
1198
|
out.push({ path: [...path, 'skills', 'scripts'], message: 'skill scripts (an approval pause) are not supported on an agent a map node runs: a map item cannot pause the walk' });
|
|
1155
1199
|
}
|
|
1200
|
+
// An authorization_code grant pauses for consent (ADR 0128): allowed on a node, not on a map item.
|
|
1201
|
+
if (typeof agent.name === 'string' && mapped.has(agent.name)) {
|
|
1202
|
+
for (const where of authorizationCodeGrants(agent)) {
|
|
1203
|
+
out.push({ path: [...path, ...where], message: 'an authorization_code grant (a consent pause) is not supported on an agent a map node runs: a map item cannot pause the walk' });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1156
1206
|
if (typeof agent.a2a_agent_url === 'string') {
|
|
1157
1207
|
out.push({ path: [...path, 'a2a_agent_url'], message: 'a remote agent cannot be a workflow node yet' });
|
|
1158
1208
|
}
|
|
1159
1209
|
}
|
|
1160
1210
|
return out;
|
|
1161
1211
|
}
|
|
1212
|
+
/** Where an agent declares an authorization_code grant (mcp_auth, mcp_servers[].auth, openapi[].auth), by its path below the agent. */
|
|
1213
|
+
function authorizationCodeGrants(agent) {
|
|
1214
|
+
const out = [];
|
|
1215
|
+
const isCode = (auth) => isObj(auth) && isObj(auth.oauth2) && auth.oauth2.grant === 'authorization_code';
|
|
1216
|
+
if (isCode(agent.mcp_auth))
|
|
1217
|
+
out.push(['mcp_auth', 'oauth2', 'grant']);
|
|
1218
|
+
if (Array.isArray(agent.mcp_servers))
|
|
1219
|
+
agent.mcp_servers.forEach((e, i) => isObj(e) && isCode(e.auth) && out.push(['mcp_servers', i, 'auth', 'oauth2', 'grant']));
|
|
1220
|
+
if (Array.isArray(agent.openapi))
|
|
1221
|
+
agent.openapi.forEach((e, i) => isObj(e) && isCode(e.auth) && out.push(['openapi', i, 'auth', 'oauth2', 'grant']));
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1162
1224
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
1163
1225
|
/**
|
|
1164
1226
|
* Thrown by validateSyndicateConfig. `problems` holds one `<file>: <path> —
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { GoogleGenAI } from '@google/genai';
|
|
18
18
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
19
|
-
import { join
|
|
19
|
+
import { join } from 'node:path';
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import { asTool, defineTool } from './toolContract.js';
|
|
22
|
+
import { outputsDir } from '../paths.js';
|
|
23
|
+
import { credentialValue } from '../models/credentialSource.js';
|
|
22
24
|
const IMAGE_MODEL = 'gemini-3.1-flash-image-preview';
|
|
23
25
|
export const generateImageContract = defineTool({
|
|
24
26
|
name: 'generate_image',
|
|
@@ -32,7 +34,7 @@ export const generateImageContract = defineTool({
|
|
|
32
34
|
}),
|
|
33
35
|
execute: async ({ prompt, style, aspect_ratio, color_palette }) => {
|
|
34
36
|
console.log("[ImageTool] Starting image generation execution...");
|
|
35
|
-
const apiKey =
|
|
37
|
+
const apiKey = credentialValue('GEMINI_API_KEY') || credentialValue('GOOGLE_GENAI_API_KEY');
|
|
36
38
|
if (!apiKey) {
|
|
37
39
|
const err = new Error('Gemini API Key is not configured (neither GEMINI_API_KEY nor GOOGLE_GENAI_API_KEY was found in environment).');
|
|
38
40
|
console.error(`[ImageTool ERROR] Initialization failed: ${err.message}`);
|
|
@@ -74,10 +76,8 @@ export const generateImageContract = defineTool({
|
|
|
74
76
|
if (inlineData?.data) {
|
|
75
77
|
const { mimeType, data } = inlineData;
|
|
76
78
|
const ext = mimeType?.split('/')[1] ?? 'png';
|
|
77
|
-
// OUTPUTS_DIR
|
|
78
|
-
const outputDir =
|
|
79
|
-
? resolve(process.env.OUTPUTS_DIR)
|
|
80
|
-
: join(process.cwd(), 'outputs');
|
|
79
|
+
// OUTPUTS_DIR, else MELCHIZEDEK_DATA_DIR/outputs, else ./outputs (lib/paths.ts).
|
|
80
|
+
const outputDir = outputsDir();
|
|
81
81
|
console.log(`[ImageTool] Found inline image data. MimeType: ${mimeType}, Data Length: ${data.length} chars.`);
|
|
82
82
|
console.log(`[ImageTool] Ensuring output directory exists: ${outputDir}`);
|
|
83
83
|
mkdirSync(outputDir, { recursive: true });
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* SpecAuditor subagent, which sees spec + inventory but never the image.
|
|
18
18
|
*/
|
|
19
19
|
import { z } from 'zod';
|
|
20
|
+
/** A path generate_image reported, inside the outputs directory: relative paths are read there, a leading `outputs/` dropped. */
|
|
21
|
+
export declare function resolveOutputPath(imagePath: string): string;
|
|
20
22
|
export declare const inspectImageContract: import("./toolContract.ts").DefinedTool<z.ZodObject<{
|
|
21
23
|
image_path: z.ZodString;
|
|
22
24
|
}, z.core.$strip>, string>;
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { GoogleGenAI } from '@google/genai';
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
21
|
-
import { resolve, extname, sep } from 'node:path';
|
|
21
|
+
import { isAbsolute, resolve, extname, sep } from 'node:path';
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
import { asTool, defineTool } from './toolContract.js';
|
|
24
|
+
import { outputsDir } from '../paths.js';
|
|
25
|
+
import { credentialValue } from '../models/credentialSource.js';
|
|
24
26
|
const VISION_MODEL = 'gemini-3.8-flash';
|
|
25
27
|
const INVENTORY_PROTOCOL = `You are a blind visual inventory agent. You know NOTHING about what this image was supposed to be, who requested it, or why. Enumerate ONLY what is actually present:
|
|
26
28
|
|
|
@@ -37,6 +39,13 @@ RULES:
|
|
|
37
39
|
- ZERO guesses about intent or purpose.
|
|
38
40
|
- If something is genuinely ambiguous, write "uncertain:" and describe what is visible.
|
|
39
41
|
Return a plain, factual, numbered inventory.`;
|
|
42
|
+
/** A path generate_image reported, inside the outputs directory: relative paths are read there, a leading `outputs/` dropped. */
|
|
43
|
+
export function resolveOutputPath(imagePath) {
|
|
44
|
+
if (isAbsolute(imagePath))
|
|
45
|
+
return resolve(imagePath);
|
|
46
|
+
const rel = imagePath.replace(/^(\.\/)?outputs[\\/]/, '');
|
|
47
|
+
return resolve(outputsDir(), rel);
|
|
48
|
+
}
|
|
40
49
|
export const inspectImageContract = defineTool({
|
|
41
50
|
name: 'inspect_image',
|
|
42
51
|
description: 'Performs a BLIND visual inventory of a previously saved image (subjects with exact counts, ' +
|
|
@@ -47,17 +56,15 @@ export const inspectImageContract = defineTool({
|
|
|
47
56
|
}),
|
|
48
57
|
execute: async ({ image_path }) => {
|
|
49
58
|
console.log(`[InspectTool] Blind inventory requested (path ${image_path.length} chars).`);
|
|
50
|
-
// Confine reads to the outputs
|
|
51
|
-
// generated artifacts, not the filesystem.
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const fullPath = resolve(process.cwd(), image_path);
|
|
57
|
-
if (!fullPath.startsWith(outputsDir + sep)) {
|
|
59
|
+
// Confine reads to the outputs directory — this tool inventories
|
|
60
|
+
// generated artifacts, not the filesystem. generate_image reports
|
|
61
|
+
// `outputs/<file>` wherever that directory is (lib/paths.ts), so a
|
|
62
|
+
// relative path is read inside it, with that prefix dropped.
|
|
63
|
+
const fullPath = resolveOutputPath(image_path);
|
|
64
|
+
if (!fullPath.startsWith(outputsDir() + sep)) {
|
|
58
65
|
throw new Error('inspect_image only reads files under outputs/');
|
|
59
66
|
}
|
|
60
|
-
const apiKey =
|
|
67
|
+
const apiKey = credentialValue('GEMINI_API_KEY') || credentialValue('GOOGLE_GENAI_API_KEY');
|
|
61
68
|
if (!apiKey) {
|
|
62
69
|
throw new Error('Gemini API Key is not configured (neither GEMINI_API_KEY nor GOOGLE_GENAI_API_KEY was found in environment).');
|
|
63
70
|
}
|
|
@@ -34,6 +34,30 @@ export type { McpTransportKind };
|
|
|
34
34
|
* the credential, which another transport would not change.
|
|
35
35
|
*/
|
|
36
36
|
export declare function isSseFallbackSignal(error: unknown): boolean;
|
|
37
|
+
/** A local MCP server: the program, its arguments, its environment and its directory. */
|
|
38
|
+
export interface StdioMcpSpec {
|
|
39
|
+
command: string;
|
|
40
|
+
args?: string[];
|
|
41
|
+
env?: Record<string, string>;
|
|
42
|
+
pass_env?: string[];
|
|
43
|
+
cwd?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether the operator allows `command` to run as an MCP server: its exact
|
|
47
|
+
* value is one of MELCHIZEDEK_STDIO_MCP's comma-separated entries. Unset
|
|
48
|
+
* means none. Read at each connect, so an embedding application can set it
|
|
49
|
+
* as its user approves servers.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stdioCommandAllowed(command: string, env?: NodeJS.ProcessEnv): boolean;
|
|
52
|
+
/** The command's environment: the SDK's minimal default set, then `pass_env` by name (the credential source first, then the environment), then `env`. Throws naming an unset variable, never a value. */
|
|
53
|
+
export declare function stdioEnvironment(spec: StdioMcpSpec, label: string, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* The tools a local stdio MCP server offers, as own Tools (ADR 0132). A
|
|
56
|
+
* command the operator has not allowed (MELCHIZEDEK_STDIO_MCP) or an unset
|
|
57
|
+
* pass_env variable fails the compile (thrown); a server that cannot be
|
|
58
|
+
* started or listed is logged and yields no tools, as a remote one does.
|
|
59
|
+
*/
|
|
60
|
+
export declare function loadStdioMcpTools(name: string, spec: StdioMcpSpec): Promise<Tool[]>;
|
|
37
61
|
/**
|
|
38
62
|
* Close every open MCP connection. A connection otherwise lives as long as
|
|
39
63
|
* the process (its tools are called for the life of the compiled agent), and
|
|
@@ -69,7 +93,16 @@ export interface McpToolOptions {
|
|
|
69
93
|
export interface McpServerConfig {
|
|
70
94
|
/** A short name for the server, unique on the agent: logs and errors name it. */
|
|
71
95
|
name: string;
|
|
72
|
-
url
|
|
96
|
+
/** A remote server. Exactly one of url and command is set (ADR 0132). */
|
|
97
|
+
url?: string;
|
|
98
|
+
/** A local server, started and spoken to over stdio (ADR 0132). */
|
|
99
|
+
command?: string;
|
|
100
|
+
args?: string[];
|
|
101
|
+
/** Non-secret variables for the command. */
|
|
102
|
+
env?: Record<string, string>;
|
|
103
|
+
/** Variables copied by name from the engine's environment. */
|
|
104
|
+
pass_env?: string[];
|
|
105
|
+
cwd?: string;
|
|
73
106
|
/** The server's tools this agent may use, by name. Required: names across servers are refused on collision at load. */
|
|
74
107
|
tools: string[];
|
|
75
108
|
auth?: {
|