@ninemind/agentgem 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/gem/acpRecommender.js +259 -0
- package/dist/gem/acpRun.js +156 -0
- package/dist/gem/acpSession.js +79 -0
- package/dist/gem/analysisCache.js +55 -0
- package/dist/gem/archive.js +17 -0
- package/dist/gem/binPath.js +9 -0
- package/dist/gem/buildGem.js +4 -1
- package/dist/gem/channels.js +29 -0
- package/dist/gem/credentials.js +3 -2
- package/dist/gem/distill.js +162 -0
- package/dist/gem/draftStage.js +77 -0
- package/dist/gem/gemVerify.js +35 -0
- package/dist/gem/inputError.js +21 -0
- package/dist/gem/registry.js +23 -4
- package/dist/gem/runGem.js +161 -0
- package/dist/gem/safeFetch.js +112 -0
- package/dist/gem/sandbox.js +37 -0
- package/dist/gem/sandboxLaunch.js +55 -0
- package/dist/gem/scrub.js +108 -0
- package/dist/gem/search.js +34 -0
- package/dist/gem/share.js +21 -0
- package/dist/gem/targets.js +280 -16
- package/dist/gem/testbedFlavors.js +1 -0
- package/dist/gem/workflowScan.js +0 -0
- package/dist/gem/workspaces.js +4 -3
- package/dist/gem.controller.js +151 -16
- package/dist/gem.tools.js +53 -5
- package/dist/gemRunStream.js +67 -0
- package/dist/index.js +15 -0
- package/dist/originGuard.js +36 -0
- package/dist/public/index.html +444 -10
- package/dist/schemas.js +180 -7
- package/dist/workflowStream.js +78 -0
- package/package.json +7 -2
package/dist/gem/targets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { channelScaffold } from "./channels.js";
|
|
1
2
|
import { tomlMcpServers } from "./toml.js";
|
|
2
3
|
import { stdioProxyRunner, PROXY_BASE_PORT, PROXY_HOST } from "./mcpProxy.js";
|
|
3
4
|
export function safePathSegment(name) {
|
|
@@ -19,6 +20,17 @@ const fluePascal = (name) => flueName(name).split("-").filter(Boolean).map((w) =
|
|
|
19
20
|
// Flue skills live under src/ (the agent file is src/agents/<name>.ts and imports ../skills/...).
|
|
20
21
|
const skillFlueMd = (a) => ({ [`src/skills/${safePathSegment(a.name)}/SKILL.md`]: a.content });
|
|
21
22
|
const rendered = (files) => ({ files, skipped: [] });
|
|
23
|
+
// MCP header secrets -> [headerName, envVarName] entries, Authorization first. Shared by the HTTP/SSE
|
|
24
|
+
// renderers (flue / openai-sandbox / a2a); the env-var NAME is emitted, never a value. Callers that
|
|
25
|
+
// require header-only auth check separately for a non-`headers.` secret (the unsupported case).
|
|
26
|
+
const headerSecretEntries = (refs) => {
|
|
27
|
+
const authorization = refs.find((r) => r.location.toLowerCase() === "headers.authorization");
|
|
28
|
+
return [
|
|
29
|
+
...(authorization ? [["Authorization", authorization.name]] : []),
|
|
30
|
+
...refs.filter((r) => /^headers\./i.test(r.location) && r !== authorization)
|
|
31
|
+
.map((r) => [r.location.slice("headers.".length), r.name]),
|
|
32
|
+
];
|
|
33
|
+
};
|
|
22
34
|
// ── shared convention renderers ──
|
|
23
35
|
const skillSkillMd = (a) => ({ [`skills/${safePathSegment(a.name)}/SKILL.md`]: a.content });
|
|
24
36
|
const skillDescriptionMd = (a) => ({ [`skills/${safePathSegment(a.name)}/DESCRIPTION.md`]: a.content });
|
|
@@ -220,13 +232,7 @@ function escapeTemplate(s) {
|
|
|
220
232
|
// env var name, never a value). stdio -> a localhost connection plus a generated proxy runner under
|
|
221
233
|
// proxies/ that bridges the stdio server to HTTP (same mechanism as Eve).
|
|
222
234
|
const flueConnection = (server, url) => {
|
|
223
|
-
const
|
|
224
|
-
const authorization = refs.find((r) => r.location.toLowerCase() === "headers.authorization");
|
|
225
|
-
const headerEntries = [
|
|
226
|
-
...(authorization ? [["Authorization", authorization.name]] : []),
|
|
227
|
-
...refs.filter((r) => /^headers\./i.test(r.location) && r !== authorization)
|
|
228
|
-
.map((r) => [r.location.slice("headers.".length), r.name]),
|
|
229
|
-
];
|
|
235
|
+
const headerEntries = headerSecretEntries(server.secretRefs ?? []);
|
|
230
236
|
const transport = server.transport === "sse" ? `,\n transport: "sse"` : "";
|
|
231
237
|
const headers = headerEntries.length
|
|
232
238
|
? `,\n headers: { ${headerEntries.map(([h, env]) => `${JSON.stringify(h)}: process.env[${JSON.stringify(env)}]!`).join(", ")} }`
|
|
@@ -339,11 +345,7 @@ const sandboxMcpServer = (s) => {
|
|
|
339
345
|
const unsupported = refs.find((r) => !/^headers\./i.test(r.location));
|
|
340
346
|
if (unsupported)
|
|
341
347
|
return { skip: `OpenAI sandbox cannot map secret at ${unsupported.location}` };
|
|
342
|
-
const
|
|
343
|
-
const headerEntries = [
|
|
344
|
-
...(authorization ? [["Authorization", authorization.name]] : []),
|
|
345
|
-
...refs.filter((r) => /^headers\./i.test(r.location) && r !== authorization).map((r) => [r.location.slice("headers.".length), r.name]),
|
|
346
|
-
];
|
|
348
|
+
const headerEntries = headerSecretEntries(refs);
|
|
347
349
|
const requestInit = headerEntries.length
|
|
348
350
|
? `, requestInit: { headers: { ${headerEntries.map(([h, e]) => `${JSON.stringify(h)}: process.env[${JSON.stringify(e)}]!`).join(", ")} } }`
|
|
349
351
|
: "";
|
|
@@ -475,10 +477,10 @@ const evePackageJson = (gemName) => JSON.stringify({
|
|
|
475
477
|
type: "module",
|
|
476
478
|
imports: { "#*": "./agent/*", "#evals/*": "./evals/*" },
|
|
477
479
|
scripts: { build: "eve build", dev: "eve dev", start: "eve start", typecheck: "tsgo" },
|
|
478
|
-
dependencies: { "@vercel/connect": "0.2.2", ai: "7.0.
|
|
480
|
+
dependencies: { "@vercel/connect": "0.2.2", ai: "7.0.2", eve: "^0.15.0", microsandbox: "^0.5.0", zod: "4.4.3" },
|
|
479
481
|
devDependencies: { "@types/node": "24.x", "@typescript/native-preview": "7.0.0-dev.20260523.1" },
|
|
480
|
-
overrides: { ai: "7.0.
|
|
481
|
-
resolutions: { ai: "7.0.
|
|
482
|
+
overrides: { ai: "7.0.2" },
|
|
483
|
+
resolutions: { ai: "7.0.2" },
|
|
482
484
|
engines: { node: "24.x" },
|
|
483
485
|
}, null, 2) + "\n";
|
|
484
486
|
// Cross-cutting scaffold: the files `eve init` provides so the rendered agent/ source is runnable.
|
|
@@ -499,6 +501,255 @@ const eveComposeProject = (gem, opts = {}) => {
|
|
|
499
501
|
}
|
|
500
502
|
return rendered(files);
|
|
501
503
|
};
|
|
504
|
+
// Eve channel files: one agent/channels/<name>.ts per declared channel, from the platform registry
|
|
505
|
+
// scaffold. "eve" is reserved for the always-on web/auth channel that eveComposeProject emits.
|
|
506
|
+
const channelEve = (channels) => {
|
|
507
|
+
const files = {};
|
|
508
|
+
const skipped = [];
|
|
509
|
+
for (const c of channels) {
|
|
510
|
+
const seg = eveSegment(c.name);
|
|
511
|
+
const path = `agent/channels/${seg}.ts`;
|
|
512
|
+
if (seg === "eve") {
|
|
513
|
+
skipped.push({ artifact: c.name, type: "channel", reason: "channel name 'eve' is reserved for the web channel" });
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (path in files) {
|
|
517
|
+
skipped.push({ artifact: c.name, type: "channel", reason: `path collision with an earlier channel at ${path}` });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
files[path] = channelScaffold(c.platform);
|
|
521
|
+
}
|
|
522
|
+
return { files, skipped };
|
|
523
|
+
};
|
|
524
|
+
// ── A2A (Agent2Agent) target ──
|
|
525
|
+
// Card primitive: materialize(gem, "a2a") emits a runtime-free Agent Card derived from the gem — the
|
|
526
|
+
// A2A discovery surface, publishable to the registry. The Card is the part native to AgentGem's
|
|
527
|
+
// "describe an agent" mission; a runnable A2A server is a planned opt-in flavor (MaterializeOpts).
|
|
528
|
+
const A2A_PROTOCOL_VERSION = "0.3.0";
|
|
529
|
+
const a2aSkillCard = (a) => ({
|
|
530
|
+
id: safePathSegment(a.name),
|
|
531
|
+
name: a.name,
|
|
532
|
+
description: a.description?.trim() || `The ${a.name} skill.`,
|
|
533
|
+
tags: ["skill"],
|
|
534
|
+
});
|
|
535
|
+
// A one-line card description from an instruction artifact: prefer the first non-empty *prose* line
|
|
536
|
+
// (instruction files usually open with a throwaway "# Title" heading); fall back to the de-headed
|
|
537
|
+
// first line if the doc is headings-only. Only ATX headings ("# " … "###### ") count as headings, so a
|
|
538
|
+
// prose line that merely starts with '#' (e.g. "#launch") is kept. Bounded so the card carries a label,
|
|
539
|
+
// not a paragraph.
|
|
540
|
+
const a2aFirstLine = (s) => {
|
|
541
|
+
const lines = s.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
542
|
+
const line = lines.find((l) => !/^#{1,6}\s/.test(l)) ?? lines[0]?.replace(/^#+\s*/, "") ?? "";
|
|
543
|
+
return line.length > 200 ? line.slice(0, 197).replace(/\s+\S*$/, "") + "…" : line;
|
|
544
|
+
};
|
|
545
|
+
// Pure Gem -> AgentCard projection. Skills advertise as A2A skills (metadata, not bodies); the first
|
|
546
|
+
// instruction line becomes the card description; a skill-less Gem gets a synthesized `chat` skill
|
|
547
|
+
// (A2A requires >=1). Emits no secret values (skills/instructions carry none post-redaction).
|
|
548
|
+
export const a2aAgentCard = (gem) => {
|
|
549
|
+
const skills = gem.artifacts.filter((a) => a.type === "skill");
|
|
550
|
+
const instr = gem.artifacts.filter((a) => a.type === "instructions");
|
|
551
|
+
const cardSkills = skills.map(a2aSkillCard);
|
|
552
|
+
return {
|
|
553
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
554
|
+
name: gem.name,
|
|
555
|
+
description: a2aFirstLine(instr[0]?.content ?? "") || `An agent packaged by AgentGem from ${skills.length} skill(s).`,
|
|
556
|
+
version: "0.1.0",
|
|
557
|
+
// Non-resolving placeholder (RFC 6761 reserved TLD): a published card must NOT carry a localhost url
|
|
558
|
+
// a consumer would dial against its own machine. Server mode rebinds this from PUBLIC_URL at boot.
|
|
559
|
+
url: "https://set-public-url.invalid/a2a/jsonrpc",
|
|
560
|
+
capabilities: { streaming: false, pushNotifications: false },
|
|
561
|
+
defaultInputModes: ["text"],
|
|
562
|
+
defaultOutputModes: ["text"],
|
|
563
|
+
skills: cardSkills.length ? cardSkills
|
|
564
|
+
: [{ id: "chat", name: "chat", description: `Converse with ${gem.name}.`, tags: ["chat"] }],
|
|
565
|
+
};
|
|
566
|
+
};
|
|
567
|
+
// ── A2A server mode (opt-in via MaterializeOpts.a2aServer) ──
|
|
568
|
+
// Runtime is Vercel AI SDK v7 (`ai` + `@ai-sdk/mcp`), vendor-neutral via the gateway model string —
|
|
569
|
+
// deliberately NOT @openai/agents, so sandboxMcpServer is not reused (a2aMcpClient is its analogue).
|
|
570
|
+
const A2A_MODEL = "anthropic/claude-sonnet-4-6";
|
|
571
|
+
const a2aMcpClient = (s) => {
|
|
572
|
+
const url = typeof s.config.url === "string" ? s.config.url : "";
|
|
573
|
+
if (/^https?:\/\//.test(url)) {
|
|
574
|
+
const refs = s.secretRefs ?? [];
|
|
575
|
+
const unsupported = refs.find((r) => !/^headers\./i.test(r.location));
|
|
576
|
+
if (unsupported)
|
|
577
|
+
return { skip: `A2A (AI SDK) cannot map secret at ${unsupported.location}` };
|
|
578
|
+
const headerEntries = headerSecretEntries(refs);
|
|
579
|
+
const headers = headerEntries.length
|
|
580
|
+
? `, headers: { ${headerEntries.map(([h, e]) => `${JSON.stringify(h)}: process.env[${JSON.stringify(e)}]!`).join(", ")} }`
|
|
581
|
+
: "";
|
|
582
|
+
const type = s.transport === "sse" ? "sse" : "http";
|
|
583
|
+
return { code: ` createMCPClient({ transport: { type: ${JSON.stringify(type)}, url: ${JSON.stringify(url)}${headers} } }),`, stdio: false };
|
|
584
|
+
}
|
|
585
|
+
if (s.transport === "stdio" && typeof s.config.command === "string") {
|
|
586
|
+
const args = Array.isArray(s.config.args) ? s.config.args.filter((a) => typeof a === "string") : [];
|
|
587
|
+
const envNames = (s.secretRefs ?? []).map((r) => r.name);
|
|
588
|
+
const envStr = envNames.length ? `, env: { ${envNames.map((n) => `${JSON.stringify(n)}: process.env[${JSON.stringify(n)}]!`).join(", ")} }` : "";
|
|
589
|
+
return { code: ` createMCPClient({ transport: new Experimental_StdioMCPTransport({ command: ${JSON.stringify(s.config.command)}, args: ${JSON.stringify(args)}${envStr} }) }),`, stdio: true };
|
|
590
|
+
}
|
|
591
|
+
return { skip: `${s.transport} MCP has no usable URL or stdio command` };
|
|
592
|
+
};
|
|
593
|
+
// A2A projects authenticate via plain process.env. Deliberately NOT agentcoreSecretsMd (its
|
|
594
|
+
// `agentcore add credential` / ${arn:...} body is wrong for an A2A/AI-SDK project).
|
|
595
|
+
const a2aSecretsMd = (secrets) => {
|
|
596
|
+
const model = `## Model access\n\nThe agent calls \`${A2A_MODEL}\` via the AI SDK. Set \`AI_GATEWAY_API_KEY\` ` +
|
|
597
|
+
`(Vercel AI Gateway) or a direct provider key (e.g. \`ANTHROPIC_API_KEY\`).\n`;
|
|
598
|
+
const access = `## Access control (optional)\n\nSet \`A2A_API_KEY\` to require \`Authorization: Bearer <key>\` on the ` +
|
|
599
|
+
`agent's JSON-RPC/REST routes. Agent Card discovery (the \`.well-known\` endpoint) stays open.\n`;
|
|
600
|
+
const mcp = secrets.length
|
|
601
|
+
? `## MCP credentials\n\nSet these before \`npm start\` (e.g. a \`.env\` file):\n\n` +
|
|
602
|
+
`${secrets.map((s) => `- \`${s.name}\` (for ${s.artifact} at ${s.location})`).join("\n")}\n`
|
|
603
|
+
: `## MCP credentials\n\nThis agent declares no MCP secrets.\n`;
|
|
604
|
+
return `# Secrets\n\n${model}\n${access}\n${mcp}`;
|
|
605
|
+
};
|
|
606
|
+
const a2aPackageJson = (gemName) => JSON.stringify({
|
|
607
|
+
name: safePathSegment(gemName).toLowerCase(), version: "0.1.0", private: true, type: "module",
|
|
608
|
+
scripts: { build: "tsc", start: "node dist/server.js", dev: "tsx src/server.ts" },
|
|
609
|
+
// Verified pins: ai v7 GA pairs with @ai-sdk/mcp v2 GA (both on @ai-sdk/provider@4); @a2a-js/sdk 0.3.x.
|
|
610
|
+
dependencies: { "@a2a-js/sdk": "^0.3.13", ai: "7.0.2", "@ai-sdk/mcp": "2.0.0", express: "^5", uuid: "^11" },
|
|
611
|
+
devDependencies: { "@types/express": "^5", "@types/node": "^24", tsx: "^4", typescript: "^5" },
|
|
612
|
+
}, null, 2) + "\n";
|
|
613
|
+
// The runnable A2A server: an AI SDK `streamText` tool loop behind the @a2a-js/sdk JSON-RPC handler.
|
|
614
|
+
// Streams incrementally via the A2A task lifecycle (submitted -> working -> artifact-update* ->
|
|
615
|
+
// completed); the same executor serves message/send (aggregated) and message/stream (SSE). The served
|
|
616
|
+
// card advertises streaming: true and rebinds `url` from PUBLIC_URL (the static card carries neither).
|
|
617
|
+
const a2aServerTs = (system, clientCodes, usesStdio) => {
|
|
618
|
+
const mcpImports = clientCodes.length
|
|
619
|
+
? `import { createMCPClient } from "@ai-sdk/mcp";\n${usesStdio ? `import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";\n` : ""}`
|
|
620
|
+
: "";
|
|
621
|
+
const bootBlock = clientCodes.length
|
|
622
|
+
? `const mcpClients = await Promise.all([\n${clientCodes.join("\n")}\n]);
|
|
623
|
+
const tools = Object.assign({}, ...(await Promise.all(mcpClients.map((c) => c.tools()))));
|
|
624
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const)
|
|
625
|
+
process.on(sig, () => { Promise.allSettled(mcpClients.map((c) => c.close())).finally(() => process.exit(0)); });`
|
|
626
|
+
: `const tools = {};`;
|
|
627
|
+
return `import express from "express";
|
|
628
|
+
import { streamText, stepCountIs } from "ai";
|
|
629
|
+
${mcpImports}import { type AgentCard, AGENT_CARD_PATH } from "@a2a-js/sdk";
|
|
630
|
+
import { type AgentExecutor, type RequestContext, type ExecutionEventBus,
|
|
631
|
+
DefaultRequestHandler, InMemoryTaskStore, InMemoryPushNotificationStore, DefaultPushNotificationSender } from "@a2a-js/sdk/server";
|
|
632
|
+
import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder } from "@a2a-js/sdk/server/express";
|
|
633
|
+
import { v4 as uuid } from "uuid";
|
|
634
|
+
import cardBase from "../agent-card.json" with { type: "json" };
|
|
635
|
+
|
|
636
|
+
const MODEL = ${JSON.stringify(A2A_MODEL)};
|
|
637
|
+
const SYSTEM = \`${escapeTemplate(system)}\`;
|
|
638
|
+
|
|
639
|
+
const port = Number(process.env.PORT ?? 41241);
|
|
640
|
+
const baseUrl = process.env.PUBLIC_URL ?? \`http://localhost:\${port}\`;
|
|
641
|
+
const API_KEY = process.env.A2A_API_KEY; // when set, require \`Authorization: Bearer <key>\` on the RPC/REST routes
|
|
642
|
+
const card: AgentCard = { ...(cardBase as AgentCard), url: \`\${baseUrl}/a2a/jsonrpc\`,
|
|
643
|
+
capabilities: { ...(cardBase as AgentCard).capabilities, streaming: true, pushNotifications: true },
|
|
644
|
+
additionalInterfaces: [
|
|
645
|
+
{ url: \`\${baseUrl}/a2a/jsonrpc\`, transport: "JSONRPC" },
|
|
646
|
+
{ url: \`\${baseUrl}/a2a/rest\`, transport: "HTTP+JSON" },
|
|
647
|
+
],
|
|
648
|
+
...(API_KEY ? { securitySchemes: { bearer: { type: "http", scheme: "bearer" } }, security: [{ bearer: [] }] } : {}) };
|
|
649
|
+
|
|
650
|
+
${bootBlock}
|
|
651
|
+
|
|
652
|
+
// Streaming executor: drive the tool loop and publish A2A task-lifecycle + artifact-update events.
|
|
653
|
+
class GemExecutor implements AgentExecutor {
|
|
654
|
+
private inflight = new Map<string, AbortController>();
|
|
655
|
+
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
|
|
656
|
+
const { taskId, contextId, userMessage, task } = ctx;
|
|
657
|
+
const text = (userMessage.parts ?? []).filter((p: any) => p.kind === "text").map((p: any) => p.text).join("\\n").trim();
|
|
658
|
+
// Guard: an A2A message may carry no text parts (file/data only). streamText rejects an empty
|
|
659
|
+
// prompt, so reply directly instead of failing the request.
|
|
660
|
+
if (!text) {
|
|
661
|
+
bus.publish({ kind: "message", messageId: uuid(), role: "agent", contextId,
|
|
662
|
+
parts: [{ kind: "text", text: "Please include a text message for the agent." }] });
|
|
663
|
+
bus.finished();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const ac = new AbortController();
|
|
667
|
+
this.inflight.set(taskId, ac);
|
|
668
|
+
if (!task) bus.publish({ kind: "task", id: taskId, contextId, status: { state: "submitted", timestamp: new Date().toISOString() }, history: [userMessage] });
|
|
669
|
+
bus.publish({ kind: "status-update", taskId, contextId, status: { state: "working", timestamp: new Date().toISOString() }, final: false });
|
|
670
|
+
const artifactId = uuid();
|
|
671
|
+
let started = false;
|
|
672
|
+
try {
|
|
673
|
+
const result = streamText({ model: MODEL, system: SYSTEM, tools, stopWhen: stepCountIs(10), prompt: text, abortSignal: ac.signal });
|
|
674
|
+
for await (const delta of result.textStream) {
|
|
675
|
+
bus.publish({ kind: "artifact-update", taskId, contextId, append: started, lastChunk: false,
|
|
676
|
+
artifact: { artifactId, name: "response", parts: [{ kind: "text", text: delta }] } });
|
|
677
|
+
started = true;
|
|
678
|
+
}
|
|
679
|
+
// Only close an artifact that was actually opened (empty/tool-only completions stream nothing).
|
|
680
|
+
if (started) bus.publish({ kind: "artifact-update", taskId, contextId, append: true, lastChunk: true, artifact: { artifactId, parts: [] } });
|
|
681
|
+
bus.publish({ kind: "status-update", taskId, contextId, status: { state: "completed", timestamp: new Date().toISOString() }, final: true });
|
|
682
|
+
} catch (err) {
|
|
683
|
+
const state = ac.signal.aborted ? "canceled" : "failed";
|
|
684
|
+
bus.publish({ kind: "status-update", taskId, contextId, status: { state, timestamp: new Date().toISOString() }, final: true });
|
|
685
|
+
} finally {
|
|
686
|
+
this.inflight.delete(taskId);
|
|
687
|
+
bus.finished();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
cancelTask = async (taskId: string): Promise<void> => { this.inflight.get(taskId)?.abort(); };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const pushStore = new InMemoryPushNotificationStore();
|
|
694
|
+
const requestHandler = new DefaultRequestHandler(card, new InMemoryTaskStore(), new GemExecutor(),
|
|
695
|
+
undefined, pushStore, new DefaultPushNotificationSender(pushStore));
|
|
696
|
+
const app = express();
|
|
697
|
+
// Discovery (the /.well-known Agent Card) stays open; gate only the invocation routes when A2A_API_KEY is set.
|
|
698
|
+
const requireAuth: express.RequestHandler = (req, res, next) => {
|
|
699
|
+
if (!API_KEY || req.headers.authorization === \`Bearer \${API_KEY}\`) return next();
|
|
700
|
+
res.status(401).json({ error: "unauthorized" });
|
|
701
|
+
};
|
|
702
|
+
app.use("/a2a", requireAuth);
|
|
703
|
+
app.use(\`/\${AGENT_CARD_PATH}\`, agentCardHandler({ agentCardProvider: requestHandler }));
|
|
704
|
+
app.use("/a2a/jsonrpc", jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
|
|
705
|
+
app.use("/a2a/rest", restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
|
|
706
|
+
app.listen(port, () => console.log(\`A2A agent "\${card.name}" listening on :\${port}\`));
|
|
707
|
+
`;
|
|
708
|
+
};
|
|
709
|
+
// A2A is wholly compose-driven: per-type renderers are no-ops (so materialize never auto-skip-reports),
|
|
710
|
+
// and compose owns ALL skip reporting for both modes. Hooks are never expressible by A2A (card or
|
|
711
|
+
// server). MCP is not expressible by a *Card* (card-only -> all MCP skipped), but the *server* wires it
|
|
712
|
+
// (server mode -> only unmappable MCP skipped). This keeps compatibility() honest: card-only reflects
|
|
713
|
+
// that a Card carries identity + skills, not MCP/hooks, instead of over-claiming full support.
|
|
714
|
+
const a2aComposeProject = (gem, opts = {}) => {
|
|
715
|
+
const files = { "agent-card.json": JSON.stringify(a2aAgentCard(gem), null, 2) + "\n" };
|
|
716
|
+
const mcps = gem.artifacts.filter((a) => a.type === "mcp_server");
|
|
717
|
+
const hooks = gem.artifacts.filter((a) => a.type === "hook");
|
|
718
|
+
const hookSkips = hooks.map((h) => ({ artifact: h.name, type: "hook", reason: "A2A has no hook concept" }));
|
|
719
|
+
if (!opts.a2aServer) {
|
|
720
|
+
// Card-only: an Agent Card represents identity + skills, not MCP servers or hooks.
|
|
721
|
+
const cardSkips = mcps.map((s) => ({ artifact: s.name, type: "mcp_server", reason: "an Agent Card cannot express MCP servers (materialize with a2aServer to wire them)" }));
|
|
722
|
+
return { files, skipped: [...cardSkips, ...hookSkips] };
|
|
723
|
+
}
|
|
724
|
+
const skills = gem.artifacts.filter((a) => a.type === "skill");
|
|
725
|
+
const instr = gem.artifacts.filter((a) => a.type === "instructions");
|
|
726
|
+
// AI SDK has no skills primitive -> fold skill bodies (frontmatter-stripped) into the system prompt.
|
|
727
|
+
const instrText = instr.map((i) => `## ${i.name}\n\n${i.content}`).join("\n\n---\n\n");
|
|
728
|
+
const skillText = skills.map((s) => `## Skill: ${s.name}\n\n${stripYamlFrontmatter(s.content)}`).join("\n\n---\n\n");
|
|
729
|
+
const system = [instrText, skillText].filter(Boolean).join("\n\n---\n\n");
|
|
730
|
+
// Server mode: the server wires MCP, so only UNMAPPABLE MCP is skipped; hooks remain unsupported.
|
|
731
|
+
const skipped = [...hookSkips];
|
|
732
|
+
const clientCodes = [];
|
|
733
|
+
let usesStdio = false;
|
|
734
|
+
for (const s of mcps) {
|
|
735
|
+
const r = a2aMcpClient(s);
|
|
736
|
+
if ("skip" in r) {
|
|
737
|
+
skipped.push({ artifact: s.name, type: "mcp_server", reason: r.skip });
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
clientCodes.push(r.code);
|
|
741
|
+
usesStdio ||= r.stdio;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
files: {
|
|
745
|
+
...files,
|
|
746
|
+
"src/server.ts": a2aServerTs(system, clientCodes, usesStdio),
|
|
747
|
+
"package.json": a2aPackageJson(gem.name),
|
|
748
|
+
"SECRETS.md": a2aSecretsMd(gem.requiredSecrets),
|
|
749
|
+
},
|
|
750
|
+
skipped,
|
|
751
|
+
};
|
|
752
|
+
};
|
|
502
753
|
// ── targets compose the shared renderers (convergence is literal, not duplicated) ──
|
|
503
754
|
export const TARGET_REGISTRY = {
|
|
504
755
|
claude: { id: "claude", label: "Claude", skill: skillSkillMd, instructions: instructionsClaudeMd, mcp: mcpDotMcpJson, hook: hooksSettingsJson },
|
|
@@ -506,7 +757,7 @@ export const TARGET_REGISTRY = {
|
|
|
506
757
|
agents: { id: "agents", label: "Agents", skill: skillSkillMd, instructions: instructionsAgentsMd },
|
|
507
758
|
hermes: { id: "hermes", label: "Hermes", skill: skillDescriptionMd, instructions: instructionsSoulMd },
|
|
508
759
|
// Eve project layout (agent/...). Hooks are event-reacting code in Eve, not config -> unsupported.
|
|
509
|
-
eve: { id: "eve", label: "Eve", skill: skillEveMd, instructions: concatInstructions("agent/instructions.md"), mcp: mcpEveConnections, compose: eveComposeProject },
|
|
760
|
+
eve: { id: "eve", label: "Eve", skill: skillEveMd, instructions: concatInstructions("agent/instructions.md"), mcp: mcpEveConnections, channel: channelEve, compose: eveComposeProject },
|
|
510
761
|
// Flue project layout. Skills reuse SKILL.md; instructions fold into the composed agent file (no
|
|
511
762
|
// standalone file -> the empty instructions renderer marks them handled, not skipped). MCP added in Task 2.
|
|
512
763
|
flue: { id: "flue", label: "Flue", skill: skillFlueMd, instructions: () => ({}), mcp: mcpFlueConnections, compose: flueComposeAgent },
|
|
@@ -516,6 +767,9 @@ export const TARGET_REGISTRY = {
|
|
|
516
767
|
// AgentCore harness project (app/<gem>/harness.json + container-baked skills). Instructions/MCP
|
|
517
768
|
// fold into the composed harness.json; stdio MCP is reported skipped by compose; hooks unsupported.
|
|
518
769
|
agentcore: { id: "agentcore", label: "AgentCore", skill: skillAgentcoreMd, instructions: () => ({}), mcp: () => ({ files: {}, skipped: [] }), compose: agentcoreComposeProject },
|
|
770
|
+
// A2A Agent Card primitive. Wholly compose-driven (all per-type renderers no-op); compose emits the
|
|
771
|
+
// runtime-free agent-card.json. Card-only mode reports nothing skipped.
|
|
772
|
+
a2a: { id: "a2a", label: "A2A", skill: () => ({}), instructions: () => ({}), mcp: () => ({ files: {}, skipped: [] }), hook: () => ({}), compose: a2aComposeProject },
|
|
519
773
|
};
|
|
520
774
|
export function materialize(gem, target, opts = {}) {
|
|
521
775
|
const spec = TARGET_REGISTRY[target];
|
|
@@ -535,6 +789,7 @@ export function materialize(gem, target, opts = {}) {
|
|
|
535
789
|
const mcp = gem.artifacts.filter((a) => a.type === "mcp_server");
|
|
536
790
|
const instr = gem.artifacts.filter((a) => a.type === "instructions");
|
|
537
791
|
const hooks = gem.artifacts.filter((a) => a.type === "hook");
|
|
792
|
+
const channels = gem.artifacts.filter((a) => a.type === "channel");
|
|
538
793
|
if (spec.skill)
|
|
539
794
|
for (const s of skills)
|
|
540
795
|
merge(spec.skill(s), s.name, "skill");
|
|
@@ -561,6 +816,15 @@ export function materialize(gem, target, opts = {}) {
|
|
|
561
816
|
else
|
|
562
817
|
skipAll(hooks, "hook");
|
|
563
818
|
}
|
|
819
|
+
if (channels.length) {
|
|
820
|
+
if (spec.channel) {
|
|
821
|
+
const result = spec.channel(channels);
|
|
822
|
+
merge(result.files, channels.map((c) => c.name).join(", "), "channel");
|
|
823
|
+
skipped.push(...result.skipped);
|
|
824
|
+
}
|
|
825
|
+
else
|
|
826
|
+
skipAll(channels, "channel");
|
|
827
|
+
}
|
|
564
828
|
if (spec.compose) {
|
|
565
829
|
const result = spec.compose(gem, opts);
|
|
566
830
|
merge(result.files, "(composed agent)", "instructions"); // collisions reported; agent file derives from instructions+skills
|
|
@@ -133,6 +133,7 @@ export function discoverProjects(dirs) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
return [...best.values()]
|
|
136
|
+
.filter((p) => !p.path.includes("/.agentgem/")) // hide agentgem's own internal workspaces (e.g. the analysis cwd)
|
|
136
137
|
.sort((a, b) => b.lastUsedMs - a.lastUsedMs)
|
|
137
138
|
.map((p) => ({
|
|
138
139
|
path: p.path,
|
|
Binary file
|
package/dist/gem/workspaces.js
CHANGED
|
@@ -8,6 +8,7 @@ import { mkdirSync, rmSync, readdirSync, statSync, existsSync, readFileSync } fr
|
|
|
8
8
|
import { materialize, compatibility, TARGET_REGISTRY, safePathSegment } from "./targets.js";
|
|
9
9
|
import { writeGemArchive, readGemArchive } from "./archive.js";
|
|
10
10
|
import { writeArchiveDir, readArchiveDir } from "./archiveFs.js";
|
|
11
|
+
import { InvalidInputError } from "./inputError.js";
|
|
11
12
|
const TARGETS_DIR = ".targets";
|
|
12
13
|
export function workspacesRoot() {
|
|
13
14
|
const home = process.env.AGENTGEM_HOME ?? join(homedir(), ".agentgem");
|
|
@@ -18,7 +19,7 @@ export function workspacesRoot() {
|
|
|
18
19
|
export function workspaceName(name) {
|
|
19
20
|
const seg = safePathSegment(name);
|
|
20
21
|
if (seg !== name)
|
|
21
|
-
throw new
|
|
22
|
+
throw new InvalidInputError(`invalid workspace name '${name}' — use only [A-Za-z0-9._-], no separators`);
|
|
22
23
|
return seg;
|
|
23
24
|
}
|
|
24
25
|
export function workspaceDir(name) {
|
|
@@ -73,12 +74,12 @@ export function readWorkspace(name) {
|
|
|
73
74
|
const gem = readGemArchive(files); // verifies the lock
|
|
74
75
|
return { ...summary(workspaceName(name), files["gem.json"], dir), files, compatibility: compatibility(gem) };
|
|
75
76
|
}
|
|
76
|
-
export function renderTarget(name, target) {
|
|
77
|
+
export function renderTarget(name, target, opts = {}) {
|
|
77
78
|
const dir = workspaceDir(name);
|
|
78
79
|
if (!existsSync(join(dir, "gem.json")))
|
|
79
80
|
throw new Error(`no workspace '${name}'`);
|
|
80
81
|
const gem = readGemArchive(readArchiveDir(dir));
|
|
81
|
-
const { files, skipped } = materialize(gem, target);
|
|
82
|
+
const { files, skipped } = materialize(gem, target, opts);
|
|
82
83
|
const out = join(dir, TARGETS_DIR, target);
|
|
83
84
|
rmSync(out, { recursive: true, force: true }); // clear stale renders
|
|
84
85
|
mkdirSync(out, { recursive: true });
|