@ninemind/agentgem 0.2.0 → 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 +3 -1
- package/dist/gem/acpRecommender.js +19 -63
- package/dist/gem/acpRun.js +156 -0
- package/dist/gem/acpSession.js +79 -0
- package/dist/gem/analysisCache.js +6 -2
- 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 +85 -39
- package/dist/gem/workflowScan.js +0 -0
- package/dist/gem/workspaces.js +4 -3
- package/dist/gem.controller.js +121 -17
- package/dist/gem.tools.js +53 -5
- package/dist/gemRunStream.js +67 -0
- package/dist/index.js +12 -2
- package/dist/originGuard.js +36 -0
- package/dist/public/index.html +261 -4
- package/dist/schemas.js +149 -8
- package/dist/workflowStream.js +10 -4
- package/package.json +6 -2
package/dist/gem.controller.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
// src/gem.controller.ts
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { basename } from "node:path";
|
|
3
|
+
import { existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { basename, resolve, sep } from "node:path";
|
|
5
5
|
import { api, get, post } from "@agentback/openapi";
|
|
6
6
|
import { introspectConfig, introspectProject } from "./gem/introspect.js";
|
|
7
7
|
import { buildGem } from "./gem/buildGem.js";
|
|
@@ -12,58 +12,87 @@ import { createWorkspace, listWorkspaces, readWorkspace, renderTarget, deleteWor
|
|
|
12
12
|
import { writeGemArchive, readGemArchive } from "./gem/archive.js";
|
|
13
13
|
import { writeArchiveDir, readArchiveDir } from "./gem/archiveFs.js";
|
|
14
14
|
import { packTar } from "./gem/archiveTar.js";
|
|
15
|
+
import { importGem } from "./gem/share.js";
|
|
16
|
+
import { fetchGemBytes } from "./gem/safeFetch.js";
|
|
15
17
|
import { readDeployRecord, writeDeployRecord, clearDeployRecord } from "./gem/deployRecord.js";
|
|
16
18
|
import { undeployManagedAgent, anthropicPublishClient } from "./publish.js";
|
|
17
19
|
import { undeployAgentcoreHarness, realAgentcoreControlClient } from "./gem/agentcorePublish.js";
|
|
18
|
-
import { InventorySchema, GemSchema, GemRequestSchema, DirQuerySchema, PickQuerySchema, PickFolderSchema, ScaffoldChecksRequestSchema, ScaffoldChecksResponseSchema, MaterializeRequestSchema, MaterializeResponseSchema, PublishPreviewRequestSchema, PublishRequestSchema, PublishPreviewResponseSchema, PublishReadyResponseSchema, PublishResultSchema, DeployTargetsResponseSchema, DeployReadyQuerySchema, ArchiveRequestSchema, ArchiveResponseSchema, CreateWorkspaceRequestSchema, WorkspaceQuerySchema, RenderRequestSchema, WorkspaceNameRequestSchema, WorkspaceSummarySchema, WorkspaceDetailSchema, RenderResultSchema, ListWorkspacesResponseSchema, DeleteWorkspaceResponseSchema, RunReadyQuerySchema, RunReadyResponseSchema, RunRequestSchema, RunStatusQuerySchema, RunStateSchema, RunStopRequestSchema, RunStopResponseSchema, CredentialRequestSchema, CredentialResponseSchema, TestbedDetectQuerySchema, TestbedDetectResponseSchema, TestbedSuggestionQuerySchema, TestbedSuggestionResponseSchema, TestbedRecentsResponseSchema, TestbedProjectsQuerySchema, TestbedProjectsResponseSchema, TestbedScaffoldRequestSchema, TestbedScaffoldResponseSchema, TestbedImportRequestSchema, TestbedImportResponseSchema, AgentcoreReadyResponseSchema, AgentcoreDeployRequestSchema, AgentcoreStatusQuerySchema, AgentcoreDeployStateSchema, RegistryReadyResponseSchema, RegistryIndexResponseSchema, RegistryResolveRequestSchema, RegistryResolveResponseSchema, RegistryInstallRequestSchema, RegistryInstallResponseSchema, RegistryPublishRequestSchema, RegistryPublishResponseSchema, UndeployRequestSchema, UndeployResponseSchema, DeployRecordQuerySchema, DeployRecordResponseSchema, WorkflowAnalyzeRequestSchema, WorkflowAnalyzeResponseSchema, } from "./schemas.js";
|
|
20
|
+
import { InventorySchema, GemSchema, GemRequestSchema, DirQuerySchema, PickQuerySchema, PickFolderSchema, ScaffoldChecksRequestSchema, ScaffoldChecksResponseSchema, MaterializeRequestSchema, MaterializeResponseSchema, PublishPreviewRequestSchema, PublishRequestSchema, PublishPreviewResponseSchema, PublishReadyResponseSchema, PublishResultSchema, DeployTargetsResponseSchema, DeployReadyQuerySchema, ArchiveRequestSchema, ArchiveResponseSchema, CreateWorkspaceRequestSchema, WorkspaceQuerySchema, RenderRequestSchema, WorkspaceNameRequestSchema, WorkspaceSummarySchema, WorkspaceDetailSchema, RenderResultSchema, ListWorkspacesResponseSchema, DeleteWorkspaceResponseSchema, RunReadyQuerySchema, RunReadyResponseSchema, RunRequestSchema, RunStatusQuerySchema, RunStateSchema, RunStopRequestSchema, RunStopResponseSchema, CredentialRequestSchema, CredentialResponseSchema, TestbedDetectQuerySchema, TestbedDetectResponseSchema, TestbedSuggestionQuerySchema, TestbedSuggestionResponseSchema, TestbedRecentsResponseSchema, TestbedProjectsQuerySchema, TestbedProjectsResponseSchema, TestbedScaffoldRequestSchema, TestbedScaffoldResponseSchema, TestbedImportRequestSchema, TestbedImportResponseSchema, AgentcoreReadyResponseSchema, AgentcoreDeployRequestSchema, AgentcoreStatusQuerySchema, AgentcoreDeployStateSchema, RegistryReadyResponseSchema, RegistryIndexResponseSchema, RegistrySearchQuerySchema, RegistrySearchResponseSchema, RegistryResolveRequestSchema, RegistryResolveResponseSchema, RegistryInstallRequestSchema, RegistryInstallResponseSchema, RegistryPublishRequestSchema, RegistryPublishResponseSchema, UndeployRequestSchema, UndeployResponseSchema, DeployRecordQuerySchema, DeployRecordResponseSchema, WorkflowAnalyzeRequestSchema, WorkflowAnalyzeResponseSchema, DistilledSkillSchema, WorkflowDraftWriteResponseSchema, GemRunRequestSchema, GemRunResponseSchema, GemRunPrepareRequestSchema, GemRunPrepareResponseSchema, } from "./schemas.js";
|
|
19
21
|
import { claudeTranscriptsForCwd, scanWorkflow } from "./gem/workflowScan.js";
|
|
20
22
|
import { recommendWorkflow, recommendationToSelection } from "./gem/acpRecommender.js";
|
|
23
|
+
import { distillWorkflow } from "./gem/distill.js";
|
|
24
|
+
import { writeDistilledDraft, stageDraftsByEvidence } from "./gem/draftStage.js";
|
|
21
25
|
import { runReadiness, startLocal, stopLocal, getRunStatus, deployVercel, deployCloudflare, undeployVercel, undeployCloudflare } from "./gem/run.js";
|
|
22
26
|
import { setCredential } from "./gem/credentials.js";
|
|
23
27
|
import { agentcoreReadiness, deployAgentcore, getAgentcoreStatus } from "./gem/agentcoreRun.js";
|
|
24
28
|
import { scaffoldTestbed, importArtifacts } from "./gem/testbed.js";
|
|
29
|
+
import { materializeAndRunGem, materializeGemToTestbed, registerRun, AGENT_ADAPTERS } from "./gem/runGem.js";
|
|
25
30
|
import { detectFlavor, suggestTestbed, discoverProjects } from "./gem/testbedFlavors.js";
|
|
26
31
|
import { readRecents, upsertRecent } from "./gem/recents.js";
|
|
27
32
|
import { resolveInstall, publishGem } from "./gem/registry.js";
|
|
33
|
+
import { searchIndex } from "./gem/search.js";
|
|
28
34
|
import { githubRegistrySource, githubRegistryPublisher, registryConfigFromEnv, registryReady } from "./gem/registryGithub.js";
|
|
29
35
|
import { resolveDirs, resolveProject, agentgemHome } from "./resolveDir.js";
|
|
30
36
|
import { pickFolder } from "./pickFolder.js";
|
|
37
|
+
// Server-derived run directory for a Gem. NEVER taken from client input: a caller-controlled path is
|
|
38
|
+
// a path-injection sink, and the ACP agent then runs there with tool permissions. The gem name is
|
|
39
|
+
// sanitized to one path segment; the sanitizer keeps '.', so a name of ".." must not escape — we
|
|
40
|
+
// assert the resolved path stays inside the runs root.
|
|
41
|
+
function deriveRunDir(gemName) {
|
|
42
|
+
let safeName = gemName.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
43
|
+
if (safeName === "" || safeName === "." || safeName === "..")
|
|
44
|
+
safeName = "gem";
|
|
45
|
+
const runsRoot = resolve(agentgemHome(), ".agentgem", "runs");
|
|
46
|
+
const runDir = resolve(runsRoot, safeName);
|
|
47
|
+
if (!runDir.startsWith(runsRoot + sep))
|
|
48
|
+
throw new Error("derived run dir escaped the runs root");
|
|
49
|
+
return runDir;
|
|
50
|
+
}
|
|
31
51
|
let GemController = class GemController {
|
|
32
52
|
async inventory(input) {
|
|
33
53
|
return introspectAll(input.query.dir, parseProjectsQuery(input.query.projects));
|
|
34
54
|
}
|
|
35
55
|
async gem(input) {
|
|
36
56
|
const dirs = resolveDirs(input.body.dir);
|
|
37
|
-
|
|
57
|
+
// Fold any accepted distilled drafts into the inventory (by evidence.root)
|
|
58
|
+
// before resolution, so a selection can reference one by name (proposal §7b).
|
|
59
|
+
const inventory = stageDraftsByEvidence(introspectAll(input.body.dir, input.body.projects), input.body.distilledDrafts ?? []);
|
|
38
60
|
return buildGem(inventory, input.body.selection, {
|
|
39
61
|
name: input.body.name ?? "gem",
|
|
40
62
|
createdFrom: dirs.claudeDir,
|
|
41
63
|
checks: input.body.checks,
|
|
64
|
+
channels: input.body.channels,
|
|
42
65
|
});
|
|
43
66
|
}
|
|
44
67
|
async scaffoldChecks(input) {
|
|
45
68
|
const dirs = resolveDirs(input.body.dir);
|
|
46
|
-
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
69
|
+
const inventory = stageDraftsByEvidence(introspectAll(input.body.dir, input.body.projects), input.body.distilledDrafts ?? []);
|
|
47
70
|
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
48
71
|
return { checks: scaffoldChecks(gem) };
|
|
49
72
|
}
|
|
50
73
|
async materialize(input) {
|
|
51
74
|
const target = input.body.target;
|
|
52
75
|
let gem;
|
|
53
|
-
if (input.body.
|
|
76
|
+
if (input.body.gemPath || input.body.gemUrl) {
|
|
77
|
+
const bytes = input.body.gemUrl
|
|
78
|
+
? await fetchGemBytes(input.body.gemUrl) // SSRF-guarded: rejects non-public hosts
|
|
79
|
+
: readFileSync(input.body.gemPath);
|
|
80
|
+
gem = importGem(bytes).gem; // unpack + verify gem.lock; throws on tampering
|
|
81
|
+
}
|
|
82
|
+
else if (input.body.archivePath) {
|
|
54
83
|
gem = readGemArchive(readArchiveDir(input.body.archivePath));
|
|
55
84
|
}
|
|
56
85
|
else {
|
|
57
86
|
const dirs = resolveDirs(input.body.dir);
|
|
58
87
|
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
59
|
-
gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
88
|
+
gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
|
|
60
89
|
}
|
|
61
90
|
return { target, ...materialize(gem, target, { a2aServer: input.body.a2aServer }), compatibility: compatibility(gem) };
|
|
62
91
|
}
|
|
63
92
|
async archive(input) {
|
|
64
93
|
const dirs = resolveDirs(input.body.dir);
|
|
65
94
|
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
66
|
-
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
95
|
+
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
|
|
67
96
|
const { files, skipped } = writeGemArchive(gem, { version: input.body.version });
|
|
68
97
|
const lock = JSON.parse(files["gem.lock"]);
|
|
69
98
|
let path = null;
|
|
@@ -71,13 +100,18 @@ let GemController = class GemController {
|
|
|
71
100
|
writeArchiveDir(input.body.outDir, files);
|
|
72
101
|
path = input.body.outDir;
|
|
73
102
|
}
|
|
103
|
+
let gemFile = null;
|
|
104
|
+
if (input.body.outFile) {
|
|
105
|
+
writeFileSync(input.body.outFile, packTar(files));
|
|
106
|
+
gemFile = input.body.outFile;
|
|
107
|
+
}
|
|
74
108
|
const tarGz = input.body.tar ? packTar(files).toString("base64") : null;
|
|
75
|
-
return { files, lock, skipped, path, tarGz };
|
|
109
|
+
return { files, lock, skipped, path, gemFile, tarGz };
|
|
76
110
|
}
|
|
77
111
|
async createWorkspace(input) {
|
|
78
112
|
const dirs = resolveDirs(input.body.dir);
|
|
79
113
|
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
80
|
-
const gem = buildGem(inventory, input.body.selection, { name: input.body.name, createdFrom: dirs.claudeDir });
|
|
114
|
+
const gem = buildGem(inventory, input.body.selection, { name: input.body.name, createdFrom: dirs.claudeDir, channels: input.body.channels });
|
|
81
115
|
return createWorkspace(input.body.name, gem, { version: input.body.version });
|
|
82
116
|
}
|
|
83
117
|
async listWorkspaces(_input) {
|
|
@@ -87,7 +121,7 @@ let GemController = class GemController {
|
|
|
87
121
|
return readWorkspace(input.query.name);
|
|
88
122
|
}
|
|
89
123
|
async renderWorkspace(input) {
|
|
90
|
-
return renderTarget(input.body.name, input.body.target);
|
|
124
|
+
return renderTarget(input.body.name, input.body.target, { a2aServer: input.body.a2aServer });
|
|
91
125
|
}
|
|
92
126
|
async deleteWorkspace(input) {
|
|
93
127
|
deleteWorkspace(input.body.name);
|
|
@@ -132,7 +166,7 @@ let GemController = class GemController {
|
|
|
132
166
|
async publishPreview(input) {
|
|
133
167
|
const dirs = resolveDirs(input.body.dir);
|
|
134
168
|
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
135
|
-
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
169
|
+
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
|
|
136
170
|
const target = (input.body.target ?? "claude-managed");
|
|
137
171
|
return DEPLOY_REGISTRY[target].preview(gem);
|
|
138
172
|
}
|
|
@@ -146,7 +180,7 @@ let GemController = class GemController {
|
|
|
146
180
|
async publish(input) {
|
|
147
181
|
const dirs = resolveDirs(input.body.dir);
|
|
148
182
|
const inventory = introspectAll(input.body.dir, input.body.projects);
|
|
149
|
-
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
183
|
+
const gem = buildGem(inventory, input.body.selection, { name: input.body.name ?? "gem", createdFrom: dirs.claudeDir, channels: input.body.channels });
|
|
150
184
|
const target = (input.body.target ?? "claude-managed");
|
|
151
185
|
const result = await DEPLOY_REGISTRY[target].deploy(gem, input.body.requestId);
|
|
152
186
|
if (input.body.wsName) {
|
|
@@ -229,6 +263,32 @@ let GemController = class GemController {
|
|
|
229
263
|
const rawInv = introspectConfig({ ...resolveDirs(input.body.dir), redact: false });
|
|
230
264
|
return importArtifacts(resolveProject(input.body.root), input.body.selection, rawInv, (input.body.flavor ?? "claude"));
|
|
231
265
|
}
|
|
266
|
+
// Test-run a Gem from a .gem archive with a locally-installed ACP coding agent:
|
|
267
|
+
// materialize into a runnable testbed dir, drive the agent against the task,
|
|
268
|
+
// and (when expectations are given) attach a verification report.
|
|
269
|
+
async runGem(input) {
|
|
270
|
+
const b = input.body;
|
|
271
|
+
const gem = b.archivePath
|
|
272
|
+
? readGemArchive(readArchiveDir(b.archivePath))
|
|
273
|
+
: buildGem(introspectAll(b.dir, b.projects), b.selection, { name: b.name ?? "gem", createdFrom: resolveDirs(b.dir).claudeDir });
|
|
274
|
+
const agent = (b.agent ?? "claude");
|
|
275
|
+
const runDir = deriveRunDir(gem.name);
|
|
276
|
+
const out = await materializeAndRunGem({ gem, dir: runDir, task: b.task, agent, expectations: b.expectations });
|
|
277
|
+
return { dir: runDir, agent: out.agent, materialized: out.materialized, run: out.run, verification: out.verification };
|
|
278
|
+
}
|
|
279
|
+
// Step 1 of the streaming flow: materialize the Gem (carries the full selection
|
|
280
|
+
// over POST) and hand back an opaque runId. GET /api/gem/run/stream then runs it.
|
|
281
|
+
async prepareGemRun(input) {
|
|
282
|
+
const b = input.body;
|
|
283
|
+
const gem = b.archivePath
|
|
284
|
+
? readGemArchive(readArchiveDir(b.archivePath))
|
|
285
|
+
: buildGem(introspectAll(b.dir, b.projects), b.selection, { name: b.name ?? "gem", createdFrom: resolveDirs(b.dir).claudeDir });
|
|
286
|
+
const agent = (b.agent ?? "claude");
|
|
287
|
+
const runDir = deriveRunDir(gem.name);
|
|
288
|
+
const materialized = materializeGemToTestbed(gem, runDir, AGENT_ADAPTERS[agent].flavor);
|
|
289
|
+
const runId = registerRun(runDir, agent);
|
|
290
|
+
return { runId, runDir, agent, materialized };
|
|
291
|
+
}
|
|
232
292
|
// Resolve the configured registry source, or throw a clear error the UI can surface.
|
|
233
293
|
registrySource() {
|
|
234
294
|
const cfg = registryConfigFromEnv();
|
|
@@ -242,15 +302,19 @@ let GemController = class GemController {
|
|
|
242
302
|
async registryIndex(_input) {
|
|
243
303
|
return this.registrySource().source.getIndex();
|
|
244
304
|
}
|
|
305
|
+
async registrySearch(input) {
|
|
306
|
+
const index = await this.registrySource().source.getIndex();
|
|
307
|
+
return { results: searchIndex(index, input.query.q ?? "", { kind: input.query.kind, tag: input.query.tag, limit: input.query.limit }) };
|
|
308
|
+
}
|
|
245
309
|
async registryResolve(input) {
|
|
246
310
|
const { source } = this.registrySource();
|
|
247
|
-
const { plan } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source });
|
|
311
|
+
const { plan } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source, a2aServer: input.body.a2aServer });
|
|
248
312
|
return { plan };
|
|
249
313
|
}
|
|
250
314
|
// Apply: materialize into `dest`, or land the merged Gem in the workspace store.
|
|
251
315
|
async registryInstall(input) {
|
|
252
316
|
const { source } = this.registrySource();
|
|
253
|
-
const { plan, gem } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source });
|
|
317
|
+
const { plan, gem } = await resolveInstall({ refs: input.body.refs, mode: input.body.mode, target: input.body.target, source, a2aServer: input.body.a2aServer });
|
|
254
318
|
if (input.body.mode === "materialize") {
|
|
255
319
|
if (!input.body.dest)
|
|
256
320
|
throw new Error("materialize mode requires `dest`");
|
|
@@ -270,6 +334,7 @@ let GemController = class GemController {
|
|
|
270
334
|
return publishGem({
|
|
271
335
|
gem, scope: input.body.scope, name: input.body.name, version: input.body.version,
|
|
272
336
|
dependencies: input.body.dependencies, index, publisher: githubRegistryPublisher(cfg),
|
|
337
|
+
description: input.body.description, tags: input.body.tags,
|
|
273
338
|
});
|
|
274
339
|
}
|
|
275
340
|
// Pop the OS-native folder picker and return the chosen absolute path (null if cancelled).
|
|
@@ -289,16 +354,31 @@ let GemController = class GemController {
|
|
|
289
354
|
// The top-level inventory IS the global/plugin inventory; the project section
|
|
290
355
|
// is namespaced separately. Scan + recommend over both.
|
|
291
356
|
const scanInv = { project, global: { skills: inventory.skills, mcpServers: inventory.mcpServers, hooks: inventory.hooks } };
|
|
292
|
-
const signal = scanWorkflow(paths, scanInv);
|
|
293
|
-
|
|
357
|
+
const signal = scanWorkflow(paths, scanInv, { retainSequences: true });
|
|
358
|
+
// Selective recommendation + skill distillation run concurrently — both
|
|
359
|
+
// never throw, so wall-clock stays max(...) not sum (proposal §5).
|
|
360
|
+
const [{ analysis, degraded }, distill] = await Promise.all([
|
|
361
|
+
recommendWorkflow(signal, scanInv),
|
|
362
|
+
distillWorkflow(signal, scanInv),
|
|
363
|
+
]);
|
|
294
364
|
const candidates = analysis.candidates.map((c) => ({ ...c, selection: recommendationToSelection(c) }));
|
|
295
365
|
return {
|
|
296
366
|
candidates,
|
|
297
367
|
gaps: analysis.gaps,
|
|
368
|
+
distilled: distill.distilled,
|
|
298
369
|
signalSummary: { sessionsScanned: signal.sessions.scanned, spanDays: signal.sessions.spanDays, notes: signal.notes },
|
|
299
370
|
degraded,
|
|
300
371
|
};
|
|
301
372
|
}
|
|
373
|
+
// Accept a distilled draft: persist it to .agentgem/distilled/<name>/SKILL.md for
|
|
374
|
+
// the user to review/promote (proposal §7) — NOT into .claude/skills/. The name is
|
|
375
|
+
// re-validated as a kebab slug here (defense in depth) since it composes a path.
|
|
376
|
+
async writeWorkflowDraft(input) {
|
|
377
|
+
const skill = input.body;
|
|
378
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.name))
|
|
379
|
+
throw new Error(`invalid draft name '${skill.name}'`);
|
|
380
|
+
return { path: writeDistilledDraft(skill) };
|
|
381
|
+
}
|
|
302
382
|
};
|
|
303
383
|
__decorate([
|
|
304
384
|
get("/inventory", { query: DirQuerySchema, response: InventorySchema }),
|
|
@@ -480,6 +560,18 @@ __decorate([
|
|
|
480
560
|
__metadata("design:paramtypes", [Object]),
|
|
481
561
|
__metadata("design:returntype", Promise)
|
|
482
562
|
], GemController.prototype, "importTestbed", null);
|
|
563
|
+
__decorate([
|
|
564
|
+
post("/gem/run", { body: GemRunRequestSchema, response: GemRunResponseSchema }),
|
|
565
|
+
__metadata("design:type", Function),
|
|
566
|
+
__metadata("design:paramtypes", [Object]),
|
|
567
|
+
__metadata("design:returntype", Promise)
|
|
568
|
+
], GemController.prototype, "runGem", null);
|
|
569
|
+
__decorate([
|
|
570
|
+
post("/gem/run/prepare", { body: GemRunPrepareRequestSchema, response: GemRunPrepareResponseSchema }),
|
|
571
|
+
__metadata("design:type", Function),
|
|
572
|
+
__metadata("design:paramtypes", [Object]),
|
|
573
|
+
__metadata("design:returntype", Promise)
|
|
574
|
+
], GemController.prototype, "prepareGemRun", null);
|
|
483
575
|
__decorate([
|
|
484
576
|
get("/registry/ready", { query: PickQuerySchema, response: RegistryReadyResponseSchema }),
|
|
485
577
|
__metadata("design:type", Function),
|
|
@@ -492,6 +584,12 @@ __decorate([
|
|
|
492
584
|
__metadata("design:paramtypes", [Object]),
|
|
493
585
|
__metadata("design:returntype", Promise)
|
|
494
586
|
], GemController.prototype, "registryIndex", null);
|
|
587
|
+
__decorate([
|
|
588
|
+
get("/registry/search", { query: RegistrySearchQuerySchema, response: RegistrySearchResponseSchema }),
|
|
589
|
+
__metadata("design:type", Function),
|
|
590
|
+
__metadata("design:paramtypes", [Object]),
|
|
591
|
+
__metadata("design:returntype", Promise)
|
|
592
|
+
], GemController.prototype, "registrySearch", null);
|
|
495
593
|
__decorate([
|
|
496
594
|
post("/registry/resolve", { body: RegistryResolveRequestSchema, response: RegistryResolveResponseSchema }),
|
|
497
595
|
__metadata("design:type", Function),
|
|
@@ -522,6 +620,12 @@ __decorate([
|
|
|
522
620
|
__metadata("design:paramtypes", [Object]),
|
|
523
621
|
__metadata("design:returntype", Promise)
|
|
524
622
|
], GemController.prototype, "workflowAnalyze", null);
|
|
623
|
+
__decorate([
|
|
624
|
+
post("/workflow/draft", { body: DistilledSkillSchema, response: WorkflowDraftWriteResponseSchema }),
|
|
625
|
+
__metadata("design:type", Function),
|
|
626
|
+
__metadata("design:paramtypes", [Object]),
|
|
627
|
+
__metadata("design:returntype", Promise)
|
|
628
|
+
], GemController.prototype, "writeWorkflowDraft", null);
|
|
525
629
|
GemController = __decorate([
|
|
526
630
|
api({ basePath: "/api" })
|
|
527
631
|
], GemController);
|
package/dist/gem.tools.js
CHANGED
|
@@ -7,13 +7,20 @@ import { buildGem } from "./gem/buildGem.js";
|
|
|
7
7
|
import { GemSelectionSchema } from "./schemas.js";
|
|
8
8
|
import { resolveDirs, resolveProject } from "./resolveDir.js";
|
|
9
9
|
import { resolveInstall, publishGem } from "./gem/registry.js";
|
|
10
|
+
import { searchIndex } from "./gem/search.js";
|
|
10
11
|
import { githubRegistrySource, githubRegistryPublisher, registryConfigFromEnv } from "./gem/registryGithub.js";
|
|
11
12
|
import { readWorkspace } from "./gem/workspaces.js";
|
|
12
13
|
import { readGemArchive } from "./gem/archive.js";
|
|
14
|
+
import { exportGem, importGem } from "./gem/share.js";
|
|
15
|
+
import { fetchGemBytes } from "./gem/safeFetch.js";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
13
17
|
const InventoryInput = z.object({ dir: z.string().optional(), projects: z.array(z.string()).optional() });
|
|
14
18
|
const GemInput = z.object({ selection: GemSelectionSchema, name: z.string().optional(), dir: z.string().optional(), projects: z.array(z.string()).optional() });
|
|
15
|
-
const RegistryRefsInput = z.object({ refs: z.array(z.string()).min(1), mode: z.enum(["materialize", "workspace"]), target: z.string().optional() });
|
|
16
|
-
const RegistryPublishInput = z.object({ workspace: z.string(), scope: z.string(), name: z.string().optional(), version: z.string(), dependencies: z.array(z.string()).optional() });
|
|
19
|
+
const RegistryRefsInput = z.object({ refs: z.array(z.string()).min(1), mode: z.enum(["materialize", "workspace"]), target: z.string().optional(), a2aServer: z.boolean().optional() });
|
|
20
|
+
const RegistryPublishInput = z.object({ workspace: z.string(), scope: z.string(), name: z.string().optional(), version: z.string(), dependencies: z.array(z.string()).optional(), description: z.string().optional(), tags: z.array(z.string()).optional() });
|
|
21
|
+
const RegistrySearchInput = z.object({ q: z.string().optional(), kind: z.string().optional(), tag: z.string().optional(), limit: z.number().int().positive().max(100).optional() });
|
|
22
|
+
const GemExportInput = z.object({ selection: GemSelectionSchema, name: z.string().optional(), version: z.string().optional(), dir: z.string().optional(), projects: z.array(z.string()).optional() });
|
|
23
|
+
const GemInstallInput = z.object({ gemUrl: z.string().optional(), gemPath: z.string().optional(), bytesBase64: z.string().optional() });
|
|
17
24
|
function registrySourceOrThrow() {
|
|
18
25
|
const cfg = registryConfigFromEnv();
|
|
19
26
|
if (!cfg)
|
|
@@ -35,24 +42,41 @@ let GemTools = class GemTools {
|
|
|
35
42
|
const dirs = resolveDirs(input.dir);
|
|
36
43
|
return buildGem(introspectAll(input.dir, input.projects), input.selection, { name: input.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
37
44
|
}
|
|
45
|
+
async gemExport(input) {
|
|
46
|
+
const dirs = resolveDirs(input.dir);
|
|
47
|
+
const gem = buildGem(introspectAll(input.dir, input.projects), input.selection, { name: input.name ?? "gem", createdFrom: dirs.claudeDir });
|
|
48
|
+
const { filename, bytes, skipped } = exportGem(gem, { version: input.version });
|
|
49
|
+
return { filename, bytesBase64: bytes.toString("base64"), skipped };
|
|
50
|
+
}
|
|
51
|
+
async gemInstall(input) {
|
|
52
|
+
const bytes = input.gemUrl ? await fetchGemBytes(input.gemUrl)
|
|
53
|
+
: input.gemPath ? readFileSync(input.gemPath)
|
|
54
|
+
: input.bytesBase64 ? Buffer.from(input.bytesBase64, "base64")
|
|
55
|
+
: (() => { throw new Error("provide one of gemUrl, gemPath, or bytesBase64"); })();
|
|
56
|
+
return importGem(bytes);
|
|
57
|
+
}
|
|
38
58
|
async registryIndex(_input) {
|
|
39
59
|
return registrySourceOrThrow().source.getIndex();
|
|
40
60
|
}
|
|
61
|
+
async registrySearch(input) {
|
|
62
|
+
const index = await registrySourceOrThrow().source.getIndex();
|
|
63
|
+
return { results: searchIndex(index, input.q ?? "", { kind: input.kind, tag: input.tag, limit: input.limit }) };
|
|
64
|
+
}
|
|
41
65
|
async registryResolve(input) {
|
|
42
66
|
const { source } = registrySourceOrThrow();
|
|
43
|
-
const { plan } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source });
|
|
67
|
+
const { plan } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source, a2aServer: input.a2aServer });
|
|
44
68
|
return plan;
|
|
45
69
|
}
|
|
46
70
|
async registryInstall(input) {
|
|
47
71
|
const { source } = registrySourceOrThrow();
|
|
48
|
-
const { plan, gem } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source });
|
|
72
|
+
const { plan, gem } = await resolveInstall({ refs: input.refs, mode: input.mode, target: input.target, source, a2aServer: input.a2aServer });
|
|
49
73
|
return { plan, gem };
|
|
50
74
|
}
|
|
51
75
|
async registryPublish(input) {
|
|
52
76
|
const { cfg, source } = registrySourceOrThrow();
|
|
53
77
|
const gem = readGemArchive(readWorkspace(input.workspace).files);
|
|
54
78
|
const index = await source.getIndex();
|
|
55
|
-
return publishGem({ gem, scope: input.scope, name: input.name, version: input.version, dependencies: input.dependencies, index, publisher: githubRegistryPublisher(cfg) });
|
|
79
|
+
return publishGem({ gem, scope: input.scope, name: input.name, version: input.version, dependencies: input.dependencies, index, publisher: githubRegistryPublisher(cfg), description: input.description, tags: input.tags });
|
|
56
80
|
}
|
|
57
81
|
};
|
|
58
82
|
__decorate([
|
|
@@ -73,12 +97,36 @@ __decorate([
|
|
|
73
97
|
__metadata("design:paramtypes", [Object]),
|
|
74
98
|
__metadata("design:returntype", Promise)
|
|
75
99
|
], GemTools.prototype, "gem", null);
|
|
100
|
+
__decorate([
|
|
101
|
+
tool("gem_export", {
|
|
102
|
+
description: "Export a Gem (built from a selection of the local config) as a single portable .gem archive, returned base64-encoded. Share those bytes as a file/upload/gist; install elsewhere with gem_install. Secrets are redacted; no registry required.",
|
|
103
|
+
input: GemExportInput,
|
|
104
|
+
}),
|
|
105
|
+
__metadata("design:type", Function),
|
|
106
|
+
__metadata("design:paramtypes", [Object]),
|
|
107
|
+
__metadata("design:returntype", Promise)
|
|
108
|
+
], GemTools.prototype, "gemExport", null);
|
|
109
|
+
__decorate([
|
|
110
|
+
tool("gem_install", {
|
|
111
|
+
description: "Read and verify a shared .gem from a URL, local file path, or base64 bytes — returning the lock-verified Gem and its manifest meta. URL fetches are SSRF-guarded; tampered archives are rejected. Disk placement is performed via the REST /materialize endpoint.",
|
|
112
|
+
input: GemInstallInput,
|
|
113
|
+
}),
|
|
114
|
+
__metadata("design:type", Function),
|
|
115
|
+
__metadata("design:paramtypes", [Object]),
|
|
116
|
+
__metadata("design:returntype", Promise)
|
|
117
|
+
], GemTools.prototype, "gemInstall", null);
|
|
76
118
|
__decorate([
|
|
77
119
|
tool("registry_index", { description: "List the gems available in the configured registry (names, versions, dependencies).", input: z.object({}) }),
|
|
78
120
|
__metadata("design:type", Function),
|
|
79
121
|
__metadata("design:paramtypes", [Object]),
|
|
80
122
|
__metadata("design:returntype", Promise)
|
|
81
123
|
], GemTools.prototype, "registryIndex", null);
|
|
124
|
+
__decorate([
|
|
125
|
+
tool("registry_search", { description: "Search the configured registry for gems by name/tags/description. Pass a `kind` (skill|mcp_server|instructions|hook) or `tag` to filter; an empty query browses the catalog. Returns ranked hits with latest version and description.", input: RegistrySearchInput }),
|
|
126
|
+
__metadata("design:type", Function),
|
|
127
|
+
__metadata("design:paramtypes", [Object]),
|
|
128
|
+
__metadata("design:returntype", Promise)
|
|
129
|
+
], GemTools.prototype, "registrySearch", null);
|
|
82
130
|
__decorate([
|
|
83
131
|
tool("registry_resolve", { description: "Resolve registry refs into an install plan (items, artifacts, required secrets, and a materialize preview for a target). No writes.", input: RegistryRefsInput }),
|
|
84
132
|
__metadata("design:type", Function),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/gemRunStream.ts
|
|
2
|
+
//
|
|
3
|
+
// SSE endpoint for running an already-prepared Gem with a local ACP coding agent.
|
|
4
|
+
// This is step 2 of the streaming flow: POST /api/gem/run/prepare materializes the
|
|
5
|
+
// Gem and returns an opaque runId; this GET streams the agent run for that runId
|
|
6
|
+
// (materializing → running → per-tool + token deltas → done). Splitting prepare
|
|
7
|
+
// (POST, carries the selection) from stream (GET, simple query params) lets the UI
|
|
8
|
+
// use native EventSource while keeping the run dir off the wire — the client only
|
|
9
|
+
// ever holds the opaque id, never a path it could redirect the agent to.
|
|
10
|
+
import { runGemWithAgent, hasTestConnectFn } from "./gem/acpRun.js";
|
|
11
|
+
import { resolveRun, resolveOrFetchAdapter, AGENT_ADAPTERS } from "./gem/runGem.js";
|
|
12
|
+
import { verifyGemRun } from "./gem/gemVerify.js";
|
|
13
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
14
|
+
export async function streamGemRun(req, res) {
|
|
15
|
+
const runId = str(req.query.runId);
|
|
16
|
+
const task = str(req.query.task);
|
|
17
|
+
const expectTools = str(req.query.expectTools)
|
|
18
|
+
? str(req.query.expectTools).split(",").map((s) => s.trim()).filter(Boolean)
|
|
19
|
+
: undefined;
|
|
20
|
+
const expectText = str(req.query.expectText) || undefined;
|
|
21
|
+
res.writeHead(200, {
|
|
22
|
+
"Content-Type": "text/event-stream",
|
|
23
|
+
"Cache-Control": "no-cache, no-transform",
|
|
24
|
+
Connection: "keep-alive",
|
|
25
|
+
"X-Accel-Buffering": "no",
|
|
26
|
+
});
|
|
27
|
+
const send = (event, data) => {
|
|
28
|
+
res.write(`event: ${event}\n`);
|
|
29
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const reg = resolveRun(runId);
|
|
33
|
+
if (!reg) {
|
|
34
|
+
send("failed", { message: "unknown or expired runId — prepare the run again" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!task) {
|
|
38
|
+
send("failed", { message: "missing task" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Resolve the adapter (fetching on demand if needed), streaming a phase so a
|
|
42
|
+
// one-time download shows progress instead of a hang.
|
|
43
|
+
const adapter = AGENT_ADAPTERS[reg.agent];
|
|
44
|
+
const command = hasTestConnectFn()
|
|
45
|
+
? [adapter.bin]
|
|
46
|
+
: await resolveOrFetchAdapter(adapter, {
|
|
47
|
+
onFetch: () => send("phase", { phase: "preparing-adapter", agent: reg.agent, pkg: adapter.pkg }),
|
|
48
|
+
});
|
|
49
|
+
send("phase", { phase: "running", agent: reg.agent });
|
|
50
|
+
const run = await runGemWithAgent({
|
|
51
|
+
dir: reg.dir,
|
|
52
|
+
task,
|
|
53
|
+
descriptor: { id: adapter.id, name: adapter.name, command },
|
|
54
|
+
onToolCall: (t) => send("tool", t),
|
|
55
|
+
onDelta: (c) => send("delta", { text: c }),
|
|
56
|
+
});
|
|
57
|
+
const expectations = expectTools || expectText ? { expectTools, expectText } : undefined;
|
|
58
|
+
const verification = expectations ? verifyGemRun(run, expectations) : undefined;
|
|
59
|
+
send("done", { runId, agent: reg.agent, run, verification });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
send("failed", { message: err?.message ?? String(err) });
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
res.end();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ import { installMcpHttp } from "@agentback/mcp-http";
|
|
|
18
18
|
import { GemController } from "./gem.controller.js";
|
|
19
19
|
import { GemTools } from "./gem.tools.js";
|
|
20
20
|
import { streamWorkflowAnalyze } from "./workflowStream.js";
|
|
21
|
+
import { streamGemRun } from "./gemRunStream.js";
|
|
22
|
+
import { originGuard } from "./originGuard.js";
|
|
21
23
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
24
|
function pageHtml() {
|
|
23
25
|
for (const p of [join(here, "public", "index.html"), join(here, "..", "src", "public", "index.html")]) {
|
|
@@ -35,6 +37,10 @@ export async function createApp(port) {
|
|
|
35
37
|
app.configure("servers.MCPServer").to({ name: "agentgem", version: "0.1.0", transports: { stdio: false } });
|
|
36
38
|
app.restController(GemController);
|
|
37
39
|
app.service(GemTools);
|
|
40
|
+
// CSRF / drive-by guard: reject browser-initiated cross-site requests to the loopback API
|
|
41
|
+
// (controller routes). Same-origin UI and non-browser clients (CLI/MCP/tests) pass. Mounted in
|
|
42
|
+
// the framework middleware chain so it runs before controller dispatch.
|
|
43
|
+
app.expressMiddleware("middleware.originGuard", originGuard);
|
|
38
44
|
await installExplorer(app, { title: "agentgem API" });
|
|
39
45
|
await installMcpHttp(app);
|
|
40
46
|
const server = await app.restServer;
|
|
@@ -42,8 +48,12 @@ export async function createApp(port) {
|
|
|
42
48
|
server.expressApp.get("/", (_req, res) => res.type("html").send(html));
|
|
43
49
|
// SSE progress stream for workflow analysis (raw Express — the decorator
|
|
44
50
|
// framework only returns single JSON bodies). The POST /api/workflow/analyze
|
|
45
|
-
// route stays for programmatic/test callers.
|
|
46
|
-
|
|
51
|
+
// route stays for programmatic/test callers. originGuard is applied per-route because these raw
|
|
52
|
+
// routes are registered directly on expressApp, outside the controller dispatch chain.
|
|
53
|
+
server.expressApp.get("/api/workflow/analyze/stream", originGuard, streamWorkflowAnalyze);
|
|
54
|
+
// SSE progress stream for running a Gem with a local ACP agent (materialize →
|
|
55
|
+
// run → tool/token deltas → done). POST /api/gem/run stays for programmatic callers.
|
|
56
|
+
server.expressApp.get("/api/gem/run/stream", originGuard, streamGemRun);
|
|
47
57
|
return app;
|
|
48
58
|
}
|
|
49
59
|
// Start the server and print where its surfaces live. Shared by the default
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Methods with no side effect — a direct navigation (Sec-Fetch-Site: none) to one of these is benign.
|
|
2
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
3
|
+
function block(res) {
|
|
4
|
+
res.status(403).type("application/json").send(JSON.stringify({ error: "cross-site request blocked" }));
|
|
5
|
+
}
|
|
6
|
+
export function originGuard(req, res, next) {
|
|
7
|
+
const site = req.get("sec-fetch-site");
|
|
8
|
+
if (site !== undefined) {
|
|
9
|
+
if (site === "same-origin") {
|
|
10
|
+
next();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// "none" = user-initiated (typed URL, bookmark) — allow only for safe methods. A state-changing
|
|
14
|
+
// POST is never legitimately "none" from our same-origin UI (its fetch() sends same-origin), so
|
|
15
|
+
// requiring same-origin there closes the top-level-navigation/form-POST drive-by.
|
|
16
|
+
if (site === "none" && SAFE_METHODS.has(req.method.toUpperCase())) {
|
|
17
|
+
next();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
block(res);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const origin = req.get("origin");
|
|
24
|
+
if (origin === undefined) {
|
|
25
|
+
next();
|
|
26
|
+
return;
|
|
27
|
+
} // non-browser client: no ambient browser context
|
|
28
|
+
try {
|
|
29
|
+
if (new URL(origin).host === req.get("host")) {
|
|
30
|
+
next();
|
|
31
|
+
return;
|
|
32
|
+
} // same-origin
|
|
33
|
+
}
|
|
34
|
+
catch { /* malformed Origin -> fall through to block */ }
|
|
35
|
+
block(res);
|
|
36
|
+
}
|