niceeval 0.5.4 → 0.6.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/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +5 -5
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +11 -11
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +12 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +54 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +34 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
package/src/agents/bub.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { defineSandboxAgent } from "../define.ts";
|
|
2
2
|
import { requireEnv, getEnv } from "../util.ts";
|
|
3
3
|
import { shared } from "./shared.ts";
|
|
4
|
+
import {
|
|
5
|
+
appendProjectInstruction,
|
|
6
|
+
installSkills,
|
|
7
|
+
installedSkillNames,
|
|
8
|
+
skillDiscoveryInstruction,
|
|
9
|
+
} from "./skills.ts";
|
|
10
|
+
import { writeAgentSetupManifest } from "./manifest.ts";
|
|
4
11
|
import { createCheckpoint, restoreCheckpoint } from "../sandbox/checkpoint.ts";
|
|
5
12
|
import { mapBubSpans } from "../o11y/otlp/mappers/bub.ts";
|
|
6
|
-
import type { Agent, Sandbox } from "../types.ts";
|
|
13
|
+
import type { Agent, AgentContext, AgentSetupManifest, Sandbox, SkillSpec } from "../types.ts";
|
|
7
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
15
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
9
16
|
import { homedir } from "node:os";
|
|
@@ -20,31 +27,50 @@ import { t } from "../i18n/index.ts";
|
|
|
20
27
|
// · 记忆:tape(总是开),落盘在 ~/.bub/tapes/<md5(ws)[:16]>__<md5(sess)[:16]>.jsonl。
|
|
21
28
|
// ───────────────────────────────────────────────────────────────────────────
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Bub 的扩展单元 —— **只属于 Bub**:Bub 的插件是运行环境里的 Python Package,
|
|
32
|
+
* 与 Claude Code / Codex 的 native plugin 没有共同的安装协议,不共用类型。
|
|
33
|
+
*/
|
|
34
|
+
export interface PythonPluginSpec {
|
|
35
|
+
/** PyPI Package、Version Specifier 或 Git URL(如 `bub-plugin-memory==1.3.0`、`git+https://…@8f3c1a2`)。 */
|
|
36
|
+
package: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
export interface BubConfig {
|
|
24
40
|
/** OpenAI 兼容代理的 API key。省略时读 BUB_API_KEY env。 */
|
|
25
41
|
apiKey?: string;
|
|
26
42
|
/** OpenAI 兼容代理的 base URL。省略时读 BUB_API_BASE env。 */
|
|
27
43
|
apiBase?: string;
|
|
28
44
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
45
|
+
* 装进沙箱的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
|
|
46
|
+
* 落在 `.agents/skills/<name>/`,并写一段发现指引进 AGENTS.md(bub 没有原生 Skill 加载机制)。
|
|
47
|
+
*/
|
|
48
|
+
skills?: SkillSpec[];
|
|
49
|
+
/**
|
|
50
|
+
* 额外装进 bub tool 环境的 Python Package,每个沙箱 setup 时进 `uv tool install … --with <pkg>`。
|
|
51
|
+
* 规范化后的 package 列表进安装 checkpoint key:plugin 集合不同的两个 agent 变体不会复用同一个
|
|
52
|
+
* 安装 checkpoint(否则第二个变体会静默拿到第一个变体的环境)。
|
|
32
53
|
*/
|
|
33
|
-
pythonPlugins?:
|
|
54
|
+
pythonPlugins?: PythonPluginSpec[];
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
const UV = "$HOME/.local/bin/uv";
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
/** bub 的 skill 目录(`skills` 生态的「通用」目录);bub 不原生扫描它,靠 AGENTS.md 的发现指引。 */
|
|
60
|
+
const SKILL_DIR = ".agents/skills";
|
|
61
|
+
|
|
62
|
+
// TODO(upstream): BUB_OVERRIDE 钉在个人 fork 的修复分支上(tool-call 分支丢助手文本的修复
|
|
63
|
+
// 尚未进上游,见 memory/bub-tapestore-otel…drift.md 台账),等上游包含后改回发布版。
|
|
39
64
|
// 可用 NICEEVAL_BUB_OVERRIDE / NICEEVAL_BUB_OTEL_PLUGIN 覆盖,不必改源码。
|
|
40
65
|
const BUB_OVERRIDE =
|
|
41
66
|
getEnv("NICEEVAL_BUB_OVERRIDE") ??
|
|
42
67
|
"bub @ git+https://github.com/CorrectRoadH/bub.git@fix/tape-assistant-text-with-tool-calls";
|
|
43
68
|
const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
|
|
69
|
+
// otel 插件跟上游 main 走(bub-contrib#50 起从 bub.tape 导入,要求 bub ≥ 0.3.10dev,
|
|
70
|
+
// 与上面的 override 分支兼容)。插件不发 PyPI,git 依赖是唯一安装方式。
|
|
44
71
|
const OTEL_PLUGIN =
|
|
45
72
|
getEnv("NICEEVAL_BUB_OTEL_PLUGIN") ??
|
|
46
|
-
"git+https://github.com/
|
|
47
|
-
"#subdirectory=packages/bub-tapestore-otel";
|
|
73
|
+
"git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-tapestore-otel";
|
|
48
74
|
|
|
49
75
|
// override 钉在 git ref 上时,镜像里烘焙的 bub 不可信:模板构建时间早于 ref 当前指向的
|
|
50
76
|
// commit 的话,`command -v bub` 命中的就是修复前的旧构建 —— e2b 模板 fasteval-agents 上
|
|
@@ -57,41 +83,83 @@ const BUB_PINNED = BUB_OVERRIDE.includes("git+");
|
|
|
57
83
|
// (预制模板)烘焙在 PATH 上的 bub(装到 /usr/local/bin,见 sandbox/docker/Dockerfile)。
|
|
58
84
|
const BUB = BUB_PINNED ? "$HOME/.local/bin/bub" : "$(command -v bub || echo $HOME/.local/bin/bub)";
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
// checkpoint 只打 $HOME/.local:uv 装的 python 工具链、bub 的 tool venv 和 bin shim 全在
|
|
87
|
+
// 这里,restore 后即可运行。~/.cache/uv 是 wheel/构建缓存,只在「下一次安装」有用,而
|
|
88
|
+
// restore 场景 bub 已经装好、不会再装——打进去只是把单次 HTTP 传输撑到 100MB+,在 e2b
|
|
89
|
+
// 文件 API 上超时/连接重置概率明显偏高。子目录列表参与 INSTALL_HASH:改它会换缓存文件
|
|
90
|
+
// 名,不会继续复用老的大 checkpoint。
|
|
91
|
+
const CHECKPOINT_SUBDIRS = [".local"];
|
|
92
|
+
|
|
93
|
+
/** 规范化 python plugin:去空白、丢空串、去重 —— 安装命令与 checkpoint key 用同一份列表。 */
|
|
94
|
+
function normalizePackages(plugins?: readonly PythonPluginSpec[]): string[] {
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
for (const p of plugins ?? []) {
|
|
97
|
+
const pkg = p.package.trim();
|
|
98
|
+
if (pkg) seen.add(pkg);
|
|
99
|
+
}
|
|
100
|
+
return [...seen].sort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 安装环境的完整描述:bub 版本 + otel 插件 + python plugin 集合 + checkpoint 覆盖面。 */
|
|
104
|
+
function installSpecOf(packages: readonly string[]): string {
|
|
105
|
+
const plugins = packages.length ? ` --with ${packages.join(" --with ")}` : "";
|
|
106
|
+
return `bub --override(${BUB_OVERRIDE}) --with ${OTEL_PLUGIN}${plugins} --checkpoint(${CHECKPOINT_SUBDIRS.join(",")})`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installHashOf(packages: readonly string[]): string {
|
|
110
|
+
return createHash("md5").update(installSpecOf(packages)).digest("hex").slice(0, 12);
|
|
111
|
+
}
|
|
62
112
|
|
|
63
|
-
function diskCachePath(home: string): string {
|
|
113
|
+
function diskCachePath(home: string, installHash: string): string {
|
|
64
114
|
const homeKey = createHash("md5").update(home).digest("hex").slice(0, 8);
|
|
65
|
-
return join(homedir(), ".cache", "niceeval", `bub-checkpoint-${homeKey}-${
|
|
115
|
+
return join(homedir(), ".cache", "niceeval", `bub-checkpoint-${homeKey}-${installHash}.bin`);
|
|
66
116
|
}
|
|
67
117
|
|
|
68
|
-
// in-memory checkpoint + mutex keyed by sandbox $HOME,
|
|
69
|
-
//
|
|
118
|
+
// in-memory checkpoint + mutex keyed by (sandbox $HOME, 安装规格):
|
|
119
|
+
// $HOME 分开 Docker(/home/node)と Vercel(/home/vercel-sandbox);安装规格分开 python plugin
|
|
120
|
+
// 集合不同的 agent 变体 —— 少了后者,装了 plugin 的变体会静默复用 baseline 变体的环境。
|
|
70
121
|
const memCheckpoints = new Map<string, Buffer>();
|
|
71
122
|
const installsInProgress = new Map<string, Promise<void>>();
|
|
72
123
|
|
|
73
|
-
async function ensureBub(
|
|
124
|
+
async function ensureBub(
|
|
125
|
+
sb: Sandbox,
|
|
126
|
+
home: string,
|
|
127
|
+
log: AgentContext["log"],
|
|
128
|
+
packages: readonly string[],
|
|
129
|
+
): Promise<void> {
|
|
74
130
|
// 预制模板已把 bub 烘焙进镜像(PATH 上)→ 直接用,跳过 uv 安装 + checkpoint 全套。
|
|
75
131
|
// pinned(git ref override)时不走此捷径:烘焙的 bub 无法验证是不是 ref 当前指向的
|
|
76
|
-
// 构建,必须按 override 真装(见 BUB_PINNED 注释)
|
|
77
|
-
|
|
132
|
+
// 构建,必须按 override 真装(见 BUB_PINNED 注释)。有 python plugin 时同理不能走捷径:
|
|
133
|
+
// 镜像里烘焙的 bub 环境没有这些包。
|
|
134
|
+
if (!BUB_PINNED && packages.length === 0 && (await sb.runShell("command -v bub >/dev/null 2>&1")).exitCode === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
78
137
|
|
|
79
|
-
const
|
|
80
|
-
const
|
|
138
|
+
const installHash = installHashOf(packages);
|
|
139
|
+
const cacheKey = `${home}::${installHash}`;
|
|
140
|
+
const withPlugins = packages.map((p) => `--with '${p}'`).join(" ");
|
|
141
|
+
const checkpointPaths = CHECKPOINT_SUBDIRS.map((d) => `${home}/${d}`);
|
|
142
|
+
const cachePath = diskCachePath(home, installHash);
|
|
81
143
|
|
|
82
|
-
|
|
83
|
-
|
|
144
|
+
// restore 失败(多为 e2b 文件 API 对大 buffer 的瞬态超时/连接重置)不终结 attempt:
|
|
145
|
+
// 缓存只是加速手段,落空就往下走全量安装。
|
|
146
|
+
const mem = memCheckpoints.get(cacheKey);
|
|
147
|
+
if (mem) {
|
|
148
|
+
try { await restoreCheckpoint(sb, mem); return; } catch (e) {
|
|
149
|
+
log(t("bub.checkpointRestoreFailed", { error: e instanceof Error ? e.message : String(e) }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
84
152
|
|
|
85
153
|
const disk = await readFile(cachePath).catch(() => undefined);
|
|
86
154
|
if (disk) {
|
|
87
|
-
try { await restoreCheckpoint(sb, disk); memCheckpoints.set(
|
|
155
|
+
try { await restoreCheckpoint(sb, disk); memCheckpoints.set(cacheKey, disk); return; } catch { /* 损坏,回退 */ }
|
|
88
156
|
}
|
|
89
157
|
|
|
90
|
-
const inflight = installsInProgress.get(
|
|
158
|
+
const inflight = installsInProgress.get(cacheKey);
|
|
91
159
|
if (inflight) {
|
|
92
160
|
// leader 失败(多为沙箱瞬态网络错)不级联杀 waiter:兜掉后走下面自己的安装路径重试。
|
|
93
161
|
await inflight.catch(() => {});
|
|
94
|
-
const after = memCheckpoints.get(
|
|
162
|
+
const after = memCheckpoints.get(cacheKey);
|
|
95
163
|
if (after) { await restoreCheckpoint(sb, after); return; }
|
|
96
164
|
}
|
|
97
165
|
|
|
@@ -101,15 +169,17 @@ async function ensureBub(sb: Sandbox, home: string): Promise<void> {
|
|
|
101
169
|
// 失败经由下方 throw e 传播给本 attempt;这把锁可能自始至终没有 waiter,
|
|
102
170
|
// 不兜住 rejection 会变 unhandledRejection,把整个 runner 进程连同全矩阵杀掉。
|
|
103
171
|
installPromise.catch(() => {});
|
|
104
|
-
installsInProgress.set(
|
|
172
|
+
installsInProgress.set(cacheKey, installPromise);
|
|
105
173
|
|
|
106
174
|
try {
|
|
107
175
|
await sb.runShell(`test -x ${UV} || (curl -LsSf https://astral.sh/uv/install.sh | sh)`);
|
|
108
176
|
await sb.runShell(`printf '%s\\n' '${BUB_OVERRIDE}' > ${BUB_OVERRIDE_FILE}`);
|
|
109
177
|
let last = { stdout: "", stderr: "" };
|
|
110
178
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
179
|
+
// python plugin 与 bub 同一条 uv 命令装完:分两条(先装 bub、再 --reinstall 带 --with)
|
|
180
|
+
// 会让 checkpoint 抓到的环境与 key 描述的环境错位,且第二条命令白白重装一遍 bub。
|
|
111
181
|
const install = await sb.runShell(
|
|
112
|
-
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${OTEL_PLUGIN}'`,
|
|
182
|
+
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${OTEL_PLUGIN}'${withPlugins ? ` ${withPlugins}` : ""}`,
|
|
113
183
|
);
|
|
114
184
|
if (install.exitCode === 0) break;
|
|
115
185
|
last = install;
|
|
@@ -120,17 +190,23 @@ async function ensureBub(sb: Sandbox, home: string): Promise<void> {
|
|
|
120
190
|
}));
|
|
121
191
|
}
|
|
122
192
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
193
|
+
// 到这里 bub 已装进本沙箱,checkpoint 只是给后续沙箱的缓存回填:capture/下载失败
|
|
194
|
+
// (大 buffer 在 e2b 文件 API 上的瞬态错误)降级为警告,绝不反过来杀掉已就绪的 attempt。
|
|
195
|
+
try {
|
|
196
|
+
const cp = await createCheckpoint(sb, checkpointPaths);
|
|
197
|
+
memCheckpoints.set(cacheKey, cp);
|
|
198
|
+
await mkdir(dirname(cachePath), { recursive: true }).catch(() => {});
|
|
199
|
+
await writeFile(cachePath, cp).catch(() => {});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
log(t("bub.checkpointCaptureFailed", { error: e instanceof Error ? e.message : String(e) }));
|
|
202
|
+
}
|
|
127
203
|
resolveInstall();
|
|
128
204
|
} catch (e) {
|
|
129
205
|
rejectInstall(e);
|
|
130
206
|
throw e;
|
|
131
207
|
} finally {
|
|
132
208
|
// 成功/失败都清锁:锁只表达「正在装」,装完后 memCheckpoints 是唯一缓存事实源。
|
|
133
|
-
installsInProgress.delete(
|
|
209
|
+
installsInProgress.delete(cacheKey);
|
|
134
210
|
}
|
|
135
211
|
}
|
|
136
212
|
|
|
@@ -159,21 +235,18 @@ export function bubAgent(config?: BubConfig): Agent {
|
|
|
159
235
|
}),
|
|
160
236
|
},
|
|
161
237
|
|
|
162
|
-
async setup(sb) {
|
|
163
|
-
// home 必须来自运行时探测:各 sandbox
|
|
164
|
-
//
|
|
238
|
+
async setup(sb, ctx) {
|
|
239
|
+
// home 必须来自运行时探测:各 sandbox provider 不同(/home/node、/home/vercel-sandbox…),
|
|
240
|
+
// 兜一个 provider 专属常量会静默走错路径(tape 读不到 → 空事件流 → 负断言假通过)。
|
|
165
241
|
const home = (await sb.runShell("printf '%s' $HOME")).stdout.trim();
|
|
166
242
|
if (!home) throw new Error(t("bub.homeDetectFailed"));
|
|
167
243
|
const workspace = sb.workdir;
|
|
168
244
|
sessionInfo.set(sb.sandboxId, { home, workspace });
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${OTEL_PLUGIN}' ${extraWith}`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
245
|
+
// ensureBub 的 checkpoint 缓存回填在模块级共享锁(installsInProgress)里,天然可能
|
|
246
|
+
// 跨多个 attempt 复用同一次安装:警告归属到「触发这次安装的那个 attempt」的 log,
|
|
247
|
+
// 不追求归属到全部受益 attempt(已裁决口径)。
|
|
248
|
+
const packages = normalizePackages(config?.pythonPlugins);
|
|
249
|
+
await ensureBub(sb, home, ctx.log, packages);
|
|
177
250
|
|
|
178
251
|
if (!(await sb.fileExists(`${workspace}/AGENTS.md`))) {
|
|
179
252
|
await shared.writeFile(
|
|
@@ -192,6 +265,20 @@ export function bubAgent(config?: BubConfig): Agent {
|
|
|
192
265
|
].join("\n"),
|
|
193
266
|
);
|
|
194
267
|
}
|
|
268
|
+
|
|
269
|
+
const manifest: AgentSetupManifest = { skills: [] };
|
|
270
|
+
if (config?.skills?.length) {
|
|
271
|
+
manifest.skills = await installSkills(sb, config.skills, { dir: SKILL_DIR });
|
|
272
|
+
// bub 没有原生 Skill 加载机制:装进目录不等于会被读到,发现指引跟着一起写。
|
|
273
|
+
await appendProjectInstruction(
|
|
274
|
+
sb,
|
|
275
|
+
skillDiscoveryInstruction(SKILL_DIR, installedSkillNames(manifest.skills)),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (packages.length) manifest.pythonPlugins = packages.map((pkg) => ({ package: pkg }));
|
|
279
|
+
if (manifest.skills.length || manifest.pythonPlugins?.length) {
|
|
280
|
+
await writeAgentSetupManifest(sb, manifest);
|
|
281
|
+
}
|
|
195
282
|
},
|
|
196
283
|
|
|
197
284
|
async send(input, ctx) {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Claude Code native plugin 安装(installPlugins)的单测:单 plugin 的命令构造、同名
|
|
2
|
+
// marketplace 的去重、ref 钉定时改走 clone+checkout+本地路径连接、resolvedVersion 取不到
|
|
3
|
+
// 时优雅省略、marketplace/plugin 安装失败的报错。沙箱是内存 fake,风格与
|
|
4
|
+
// src/agents/skills.test.ts 一致(记命令的 FakeSandbox + 按命令前缀打脚本),不另起一套。
|
|
5
|
+
// 定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md。
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { installPlugins, type ClaudeCodePluginSpec } from "./claude-code.ts";
|
|
9
|
+
import type { CommandResult, Sandbox, SandboxFile } from "../types.ts";
|
|
10
|
+
|
|
11
|
+
/** 内存沙箱:runShell 记命令(可按命令包含的子串打脚本化输出)。 */
|
|
12
|
+
class FakeSandbox implements Partial<Sandbox> {
|
|
13
|
+
readonly workdir = "/workspace";
|
|
14
|
+
readonly sandboxId = "fake";
|
|
15
|
+
readonly otlpHost = null;
|
|
16
|
+
readonly commands: string[] = [];
|
|
17
|
+
script: { match: string; result: (cmd: string) => Partial<CommandResult> }[] = [];
|
|
18
|
+
|
|
19
|
+
async runShell(script: string): Promise<CommandResult> {
|
|
20
|
+
this.commands.push(script);
|
|
21
|
+
const hit = this.script.find((s) => script.includes(s.match));
|
|
22
|
+
return { stdout: "", stderr: "", exitCode: 0, ...hit?.result(script) };
|
|
23
|
+
}
|
|
24
|
+
async writeFiles(): Promise<void> {}
|
|
25
|
+
async uploadFiles(_files: SandboxFile[]): Promise<void> {}
|
|
26
|
+
async fileExists(): Promise<boolean> {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
async readFile(): Promise<string> {
|
|
30
|
+
throw new Error("not used in this test");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sb = (s?: FakeSandbox["script"]): FakeSandbox => {
|
|
35
|
+
const box = new FakeSandbox();
|
|
36
|
+
if (s) box.script = s;
|
|
37
|
+
return box;
|
|
38
|
+
};
|
|
39
|
+
const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
|
|
40
|
+
|
|
41
|
+
describe("claude-code installPlugins · 命令构造", () => {
|
|
42
|
+
it("单 plugin:先连 marketplace 再装 plugin,manifest 记 marketplace/name(无 ref 时不带 ref 键)", async () => {
|
|
43
|
+
const box = sb();
|
|
44
|
+
const plugins: ClaudeCodePluginSpec[] = [
|
|
45
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
46
|
+
];
|
|
47
|
+
const out = await installPlugins(asSandbox(box), plugins);
|
|
48
|
+
|
|
49
|
+
expect(box.commands).toEqual([
|
|
50
|
+
"claude plugin marketplace add 'acme/claude-code-plugins'",
|
|
51
|
+
"claude plugin install 'safe-shell@acme'",
|
|
52
|
+
"claude plugin list --json",
|
|
53
|
+
]);
|
|
54
|
+
expect(out).toEqual([
|
|
55
|
+
{ agent: "claude-code", marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("同名 marketplace 只连一次:两个 plugin 共用一个 marketplace.name → 只有一条 marketplace add,两条 install", async () => {
|
|
60
|
+
const box = sb();
|
|
61
|
+
const plugins: ClaudeCodePluginSpec[] = [
|
|
62
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
63
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "repo-map" },
|
|
64
|
+
];
|
|
65
|
+
const out = await installPlugins(asSandbox(box), plugins);
|
|
66
|
+
|
|
67
|
+
const marketplaceAdds = box.commands.filter((c) => c.startsWith("claude plugin marketplace add"));
|
|
68
|
+
expect(marketplaceAdds).toHaveLength(1);
|
|
69
|
+
const installs = box.commands.filter((c) => c.startsWith("claude plugin install"));
|
|
70
|
+
expect(installs).toEqual(["claude plugin install 'safe-shell@acme'", "claude plugin install 'repo-map@acme'"]);
|
|
71
|
+
expect(out.map((p) => p.name)).toEqual(["safe-shell", "repo-map"]);
|
|
72
|
+
expect(out.every((p) => p.marketplace.name === "acme")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("ref 钉定:先 git clone + checkout,再以本地 clone 路径连接 marketplace(claude CLI 没有钉 ref 的入口);manifest 保留 ref", async () => {
|
|
76
|
+
const box = sb();
|
|
77
|
+
const plugins: ClaudeCodePluginSpec[] = [
|
|
78
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins", ref: "v1.3.0" }, name: "safe-shell" },
|
|
79
|
+
];
|
|
80
|
+
const out = await installPlugins(asSandbox(box), plugins);
|
|
81
|
+
|
|
82
|
+
const clone = box.commands.find((c) => c.includes("git clone"))!;
|
|
83
|
+
expect(clone).toContain("https://github.com/acme/claude-code-plugins.git");
|
|
84
|
+
expect(clone).not.toContain("--depth 1"); // ref 可能是任意 commit,浅克隆 checkout 不到
|
|
85
|
+
expect(clone).toContain("checkout --quiet 'v1.3.0'");
|
|
86
|
+
const cloneDir = /rm -rf '([^']+)'/.exec(clone)?.[1];
|
|
87
|
+
expect(cloneDir).toBeTruthy();
|
|
88
|
+
|
|
89
|
+
// marketplace add 用的是 clone 出来的本地路径,不是原始 "acme/claude-code-plugins" 字符串
|
|
90
|
+
const add = box.commands.find((c) => c.startsWith("claude plugin marketplace add"))!;
|
|
91
|
+
expect(add).toBe(`claude plugin marketplace add '${cloneDir}'`);
|
|
92
|
+
|
|
93
|
+
expect(out).toEqual([
|
|
94
|
+
{
|
|
95
|
+
agent: "claude-code",
|
|
96
|
+
marketplace: { name: "acme", source: "acme/claude-code-plugins", ref: "v1.3.0" },
|
|
97
|
+
name: "safe-shell",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("resolvedVersion:装完读 `claude plugin list --json` 命中 → manifest 记版本", async () => {
|
|
103
|
+
const box = sb([
|
|
104
|
+
{
|
|
105
|
+
match: "claude plugin list --json",
|
|
106
|
+
result: () => ({ stdout: JSON.stringify([{ id: "safe-shell@acme", version: "1.2.3" }]) }),
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
const out = await installPlugins(asSandbox(box), [
|
|
110
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
111
|
+
]);
|
|
112
|
+
expect(out[0]?.resolvedVersion).toBe("1.2.3");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resolvedVersion 取不到时优雅省略(不阻断安装):list 命令失败 → manifest 里没有 resolvedVersion 键", async () => {
|
|
116
|
+
const box = sb([{ match: "claude plugin list --json", result: () => ({ exitCode: 1 }) }]);
|
|
117
|
+
const out = await installPlugins(asSandbox(box), [
|
|
118
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
119
|
+
]);
|
|
120
|
+
expect(out).toHaveLength(1);
|
|
121
|
+
expect(out[0]).not.toHaveProperty("resolvedVersion");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("resolvedVersion 取不到时优雅省略:list 输出不是合法 JSON(如空 stdout)同样不阻断安装", async () => {
|
|
125
|
+
const box = sb(); // 默认 stdout 为空字符串,JSON.parse("") 抛错,installedVersion 内部吞掉
|
|
126
|
+
const out = await installPlugins(asSandbox(box), [
|
|
127
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
128
|
+
]);
|
|
129
|
+
expect(out[0]).not.toHaveProperty("resolvedVersion");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("claude-code installPlugins · 失败语义", () => {
|
|
134
|
+
it("marketplace 连接失败:抛错并点名 marketplace 名与来源,不继续装 plugin", async () => {
|
|
135
|
+
const box = sb([{ match: "claude plugin marketplace add", result: () => ({ exitCode: 1, stderr: "boom" }) }]);
|
|
136
|
+
await expect(
|
|
137
|
+
installPlugins(asSandbox(box), [
|
|
138
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
139
|
+
]),
|
|
140
|
+
).rejects.toThrow(/acme/);
|
|
141
|
+
expect(box.commands.some((c) => c.startsWith("claude plugin install"))).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("plugin 安装失败:抛错并点名 plugin 名", async () => {
|
|
145
|
+
const box = sb([{ match: "claude plugin install", result: () => ({ exitCode: 1, stderr: "boom" }) }]);
|
|
146
|
+
await expect(
|
|
147
|
+
installPlugins(asSandbox(box), [
|
|
148
|
+
{ marketplace: { name: "acme", source: "acme/claude-code-plugins" }, name: "safe-shell" },
|
|
149
|
+
]),
|
|
150
|
+
).rejects.toThrow(/safe-shell/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import { defineSandboxAgent } from "../define.ts";
|
|
2
2
|
import { requireEnv, getEnv } from "../util.ts";
|
|
3
3
|
import { shared } from "./shared.ts";
|
|
4
|
+
import { cloneRepo, installSkills } from "./skills.ts";
|
|
5
|
+
import { writeAgentSetupManifest } from "./manifest.ts";
|
|
4
6
|
import { mapClaudeCodeSpans } from "../o11y/otlp/mappers/claude-code.ts";
|
|
5
|
-
import
|
|
7
|
+
import { t } from "../i18n/index.ts";
|
|
8
|
+
import type { Agent, AgentSetupManifest, McpServer, Sandbox, SkillSpec } from "../types.ts";
|
|
6
9
|
|
|
7
10
|
// ───────────────────────────────────────────────────────────────────────────
|
|
8
11
|
// Claude Code 的 agent adapter(沙箱型)。
|
|
9
12
|
//
|
|
10
13
|
// 连接方式:在沙箱里 spawn `claude` CLI,跑完读回 transcript JSONL → 标准事件流。
|
|
14
|
+
// 扩展(skill / plugin / MCP)全部是构造参数,setup 里翻译成 Claude Code 的原生形态,
|
|
15
|
+
// 装完写一份 manifest(见 docs/feature/adapters/architecture/coding-agent-extensions.md)。
|
|
11
16
|
// ───────────────────────────────────────────────────────────────────────────
|
|
12
17
|
|
|
18
|
+
/** Claude Code 的 skill 目录(project 级):CLI 原生扫描它,不需要额外的发现指引。 */
|
|
19
|
+
const SKILL_DIR = ".claude/skills";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Claude Code 的原生 Plugin —— **只属于 Claude Code**,不能传给 Codex(Codex 有自己的
|
|
23
|
+
* {@link import("./codex.ts").CodexPluginSpec})。每一项同时声明 Marketplace 连接和其中的 Plugin 名:
|
|
24
|
+
* 连上 Marketplace 不等于启用它的全部 Plugin。
|
|
25
|
+
*/
|
|
26
|
+
export interface ClaudeCodePluginSpec {
|
|
27
|
+
marketplace: {
|
|
28
|
+
/** Marketplace 在 Claude Code 配置中的连接名(`claude plugin install <plugin>@<name>` 里的那个名字)。 */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Marketplace 来源:GitHub `owner/repo`、Git URL 或路径。 */
|
|
31
|
+
source: string;
|
|
32
|
+
/** 固定 Marketplace 的 Tag、Commit 或 Branch;给了就先按 ref clone 下来再以本地路径连接。 */
|
|
33
|
+
ref?: string;
|
|
34
|
+
};
|
|
35
|
+
/** Marketplace 中的 Plugin 名。 */
|
|
36
|
+
name: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
export interface ClaudeCodeConfig {
|
|
14
40
|
/** Anthropic API key。省略时读 ANTHROPIC_API_KEY env。 */
|
|
15
41
|
apiKey?: string;
|
|
@@ -29,11 +55,12 @@ export interface ClaudeCodeConfig {
|
|
|
29
55
|
*/
|
|
30
56
|
mcpServers?: McpServer[];
|
|
31
57
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* 结果写进沙箱工作区的 skills-lock.json,claude CLI 启动时自动读取。
|
|
58
|
+
* 装进沙箱的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
|
|
59
|
+
* 落在 project 级 `.claude/skills/<name>/`,claude CLI 原生发现。
|
|
35
60
|
*/
|
|
36
|
-
skills?:
|
|
61
|
+
skills?: SkillSpec[];
|
|
62
|
+
/** Claude Code 原生 Plugin(先连 Marketplace,再从中装指定 Plugin)。 */
|
|
63
|
+
plugins?: ClaudeCodePluginSpec[];
|
|
37
64
|
}
|
|
38
65
|
|
|
39
66
|
export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
|
|
@@ -77,15 +104,24 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
|
|
|
77
104
|
await shared.writeFile(sb, "~/.claude.json", JSON.stringify({ mcpServers: servers }, null, 2));
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
const manifest: AgentSetupManifest = { skills: [] };
|
|
80
108
|
if (config?.skills?.length) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
manifest.skills = await installSkills(sb, config.skills, { dir: SKILL_DIR });
|
|
110
|
+
}
|
|
111
|
+
if (config?.plugins?.length) {
|
|
112
|
+
manifest.nativePlugins = await installPlugins(sb, config.plugins);
|
|
113
|
+
}
|
|
114
|
+
if (config?.mcpServers?.length) {
|
|
115
|
+
// manifest 里只记「挂了哪个 server、怎么起」;env 里可能有 token,不落盘。
|
|
116
|
+
manifest.mcpServers = config.mcpServers.map((s) => ({
|
|
117
|
+
name: s.name,
|
|
118
|
+
command: s.command,
|
|
119
|
+
...(s.args?.length ? { args: [...s.args] } : {}),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
// 什么都没装就不写 manifest:空 artifact 不落文件(同 results 的落盘规则)。
|
|
123
|
+
if (manifest.skills.length || manifest.nativePlugins?.length || manifest.mcpServers?.length) {
|
|
124
|
+
await writeAgentSetupManifest(sb, manifest);
|
|
89
125
|
}
|
|
90
126
|
},
|
|
91
127
|
|
|
@@ -116,4 +152,82 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
|
|
|
116
152
|
});
|
|
117
153
|
}
|
|
118
154
|
|
|
155
|
+
/**
|
|
156
|
+
* 先按 `marketplace.name` 建立 Marketplace 连接(同名只连一次),再从该连接装指定 Plugin。
|
|
157
|
+
* `claude plugin marketplace add` 没有钉 ref 的入口,所以要钉 ref 时先自己按 ref clone 下来,
|
|
158
|
+
* 再以本地路径连接(CLI 支持 path 形态的 marketplace 源)—— 「来源必须可复现」不因 CLI 少个
|
|
159
|
+
* flag 就打折。
|
|
160
|
+
*/
|
|
161
|
+
export async function installPlugins(
|
|
162
|
+
sb: Sandbox,
|
|
163
|
+
plugins: readonly ClaudeCodePluginSpec[],
|
|
164
|
+
): Promise<NonNullable<AgentSetupManifest["nativePlugins"]>> {
|
|
165
|
+
const connected = new Set<string>();
|
|
166
|
+
const out: NonNullable<AgentSetupManifest["nativePlugins"]> = [];
|
|
167
|
+
|
|
168
|
+
for (const plugin of plugins) {
|
|
169
|
+
const { marketplace } = plugin;
|
|
170
|
+
if (!connected.has(marketplace.name)) {
|
|
171
|
+
const source = marketplace.ref
|
|
172
|
+
? await cloneRepo(sb, marketplace.source, marketplace.ref)
|
|
173
|
+
: marketplace.source;
|
|
174
|
+
const add = await sb.runShell(`claude plugin marketplace add ${shared.shellQuote(source)}`);
|
|
175
|
+
if (add.exitCode !== 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
t("plugin.marketplaceFailed", {
|
|
178
|
+
agent: "claude-code",
|
|
179
|
+
name: marketplace.name,
|
|
180
|
+
source: marketplace.source,
|
|
181
|
+
ref: marketplace.ref ?? "(default)",
|
|
182
|
+
tail: outputTail(add),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
connected.add(marketplace.name);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const id = `${plugin.name}@${marketplace.name}`;
|
|
190
|
+
const install = await sb.runShell(`claude plugin install ${shared.shellQuote(id)}`);
|
|
191
|
+
if (install.exitCode !== 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
t("plugin.installFailed", {
|
|
194
|
+
agent: "claude-code",
|
|
195
|
+
name: plugin.name,
|
|
196
|
+
marketplace: marketplace.name,
|
|
197
|
+
tail: outputTail(install),
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resolvedVersion = await installedVersion(sb, id);
|
|
203
|
+
out.push({
|
|
204
|
+
agent: "claude-code",
|
|
205
|
+
marketplace: {
|
|
206
|
+
name: marketplace.name,
|
|
207
|
+
source: marketplace.source,
|
|
208
|
+
...(marketplace.ref !== undefined ? { ref: marketplace.ref } : {}),
|
|
209
|
+
},
|
|
210
|
+
name: plugin.name,
|
|
211
|
+
...(resolvedVersion !== undefined ? { resolvedVersion } : {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** `claude plugin list --json` → `[{ id: "<plugin>@<marketplace>", version, … }]`;取不到版本不阻断安装。 */
|
|
218
|
+
async function installedVersion(sb: Sandbox, id: string): Promise<string | undefined> {
|
|
219
|
+
try {
|
|
220
|
+
const res = await sb.runShell("claude plugin list --json");
|
|
221
|
+
if (res.exitCode !== 0) return undefined;
|
|
222
|
+
const list = JSON.parse(res.stdout) as { id?: string; version?: string }[];
|
|
223
|
+
return list.find((p) => p.id === id)?.version;
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function outputTail(res: { stdout: string; stderr: string }, n = 12): string {
|
|
230
|
+
return (res.stdout + res.stderr).trim().split("\n").slice(-n).join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
119
233
|
export default claudeCodeAgent();
|