agent-trellis 0.19.0 → 0.21.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/dist/adapters/claude-code.js +2 -2
- package/dist/adapters/codex.js +3 -3
- package/dist/adapters/jsonMcp.d.ts +1 -1
- package/dist/adapters/jsonMcp.js +2 -2
- package/dist/adapters/kimi-code.js +2 -2
- package/dist/adapters/kiro.js +11 -9
- package/dist/adapters/mcpPlan.d.ts +1 -1
- package/dist/adapters/mcpPlan.js +35 -12
- package/dist/adapters/pi.js +5 -5
- package/dist/adapters/zcode.d.ts +1 -1
- package/dist/adapters/zcode.js +6 -6
- package/dist/cli.js +12 -1
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +92 -7
- package/dist/commands/evolve.d.ts +43 -0
- package/dist/commands/evolve.js +479 -0
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mcpAuth.js +1 -1
- package/dist/commands/mcpGateway.d.ts +30 -2
- package/dist/commands/mcpGateway.js +44 -10
- package/dist/commands/mcpImport.js +2 -1
- package/dist/commands/secretsAudit.d.ts +1 -1
- package/dist/commands/secretsAudit.js +36 -13
- package/dist/evolve/agentView.d.ts +32 -0
- package/dist/evolve/agentView.js +34 -0
- package/dist/evolve/apply.d.ts +71 -0
- package/dist/evolve/apply.js +162 -0
- package/dist/evolve/archive.d.ts +23 -0
- package/dist/evolve/archive.js +58 -0
- package/dist/evolve/erase.d.ts +17 -0
- package/dist/evolve/erase.js +65 -0
- package/dist/evolve/extension.d.ts +37 -0
- package/dist/evolve/extension.js +63 -0
- package/dist/evolve/gate.d.ts +95 -0
- package/dist/evolve/gate.js +295 -0
- package/dist/evolve/importHistory.d.ts +91 -0
- package/dist/evolve/importHistory.js +239 -0
- package/dist/evolve/jobMain.d.ts +22 -0
- package/dist/evolve/jobMain.js +52 -0
- package/dist/evolve/jobRun.d.ts +94 -0
- package/dist/evolve/jobRun.js +273 -0
- package/dist/evolve/jobShell.d.ts +46 -0
- package/dist/evolve/jobShell.js +75 -0
- package/dist/evolve/l2.d.ts +93 -0
- package/dist/evolve/l2.js +81 -0
- package/dist/evolve/lesson.d.ts +57 -0
- package/dist/evolve/lesson.js +97 -0
- package/dist/evolve/pending.d.ts +69 -0
- package/dist/evolve/pending.js +116 -0
- package/dist/evolve/probeWorkspace.d.ts +57 -0
- package/dist/evolve/probeWorkspace.js +69 -0
- package/dist/evolve/propose.d.ts +73 -0
- package/dist/evolve/propose.js +136 -0
- package/dist/evolve/recorder.d.ts +68 -0
- package/dist/evolve/recorder.js +169 -0
- package/dist/evolve/report.d.ts +26 -0
- package/dist/evolve/report.js +37 -0
- package/dist/evolve/reportView.d.ts +96 -0
- package/dist/evolve/reportView.js +311 -0
- package/dist/evolve/risk.d.ts +106 -0
- package/dist/evolve/risk.js +224 -0
- package/dist/evolve/sources.d.ts +19 -0
- package/dist/evolve/sources.js +29 -0
- package/dist/evolve/stateDir.d.ts +21 -0
- package/dist/evolve/stateDir.js +56 -0
- package/dist/evolve/surface.d.ts +34 -0
- package/dist/evolve/surface.js +72 -0
- package/dist/evolve/trajectory.d.ts +25 -0
- package/dist/evolve/trajectory.js +88 -0
- package/dist/evolve/treeToCanonical.d.ts +28 -0
- package/dist/evolve/treeToCanonical.js +79 -0
- package/dist/evolve/trellisSubstrate.d.ts +65 -0
- package/dist/evolve/trellisSubstrate.js +154 -0
- package/dist/evolve/trigger.d.ts +177 -0
- package/dist/evolve/trigger.js +400 -0
- package/dist/evolve/types.d.ts +126 -0
- package/dist/evolve/types.js +15 -0
- package/dist/evolve/validate.d.ts +139 -0
- package/dist/evolve/validate.js +315 -0
- package/dist/host/checks.d.ts +57 -0
- package/dist/host/checks.js +126 -0
- package/dist/host/delivery.d.ts +42 -0
- package/dist/host/delivery.js +59 -0
- package/dist/host/edits.d.ts +87 -0
- package/dist/host/edits.js +102 -0
- package/dist/host/hostEvents.d.ts +74 -0
- package/dist/host/hostEvents.js +45 -0
- package/dist/host/index.d.ts +22 -0
- package/dist/host/index.js +17 -0
- package/dist/host/levels.d.ts +106 -0
- package/dist/host/levels.js +206 -0
- package/dist/host/staging.d.ts +56 -0
- package/dist/host/staging.js +106 -0
- package/dist/host/surface.d.ts +36 -0
- package/dist/host/surface.js +20 -0
- package/dist/lib/backup.js +12 -3
- package/dist/lib/envVarNames.js +7 -0
- package/dist/lib/fsAtomic.d.ts +7 -0
- package/dist/lib/fsAtomic.js +14 -0
- package/dist/lib/hostEventBus.d.ts +47 -0
- package/dist/lib/hostEventBus.js +94 -0
- package/dist/lib/hostExtensions.d.ts +75 -0
- package/dist/lib/hostExtensions.js +196 -0
- package/dist/lib/mcpConnect.d.ts +1 -1
- package/dist/lib/mcpConnect.js +19 -8
- package/dist/lib/mcpMigrateRead.js +14 -2
- package/dist/lib/mcpRuntime.d.ts +12 -1
- package/dist/lib/mcpRuntime.js +34 -2
- package/dist/lib/mcpToolRegistry.d.ts +18 -2
- package/dist/lib/mcpToolRegistry.js +29 -9
- package/dist/lib/secretEnv.d.ts +53 -8
- package/dist/lib/secretEnv.js +78 -15
- package/dist/lib/tomlSection.js +3 -2
- package/dist/lib/workspaceDoctor.d.ts +1 -1
- package/dist/lib/workspaceDoctor.js +8 -4
- package/dist/lib/workspaceMcp.d.ts +1 -1
- package/dist/lib/workspaceMcp.js +5 -5
- package/dist/pi-bridge/bundle.js +42236 -2701
- package/dist/pi-bridge/index.js +2 -2
- package/docs/magpie-gateway-research.md +422 -0
- package/docs/research.md +4 -1
- package/docs/roadmap.md +22 -12
- package/docs/runtime-infra.md +41 -22
- package/docs/self-evolution.md +138 -44
- package/package.json +8 -2
- package/schema/secrets.policy.example.yaml +5 -0
- package/schema/servers.example.yaml +5 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自我进化管理面命令(trellis-evolve 任务 12.4)。
|
|
3
|
+
*
|
|
4
|
+
* 消费者归属(8.7):本文件是自我进化子系统的一部分,物理移出消费者时
|
|
5
|
+
* 与 src/evolve/**、CLI 挂载(src/cli.ts 的 CONSUMER_WIRING 接线行)一并
|
|
6
|
+
* 删除;hostingIndependence.test.ts 的 CONSUMER_OWNED/CONSUMER_WIRING
|
|
7
|
+
* 登记了这条移出清单。
|
|
8
|
+
*
|
|
9
|
+
* `trellis evolve <status | report | pending | approve | reject | apply | run | import | enable | disable | rollback>`
|
|
10
|
+
*
|
|
11
|
+
* 批准只走这里,且**仅限交互式终端**(spec"批准只走带外通道"):非交互
|
|
12
|
+
* 环境一律拒绝,提案保持 pending。不存在任何 `--yes` / `--confirm` 之类的
|
|
13
|
+
* 非交互批准捷径 —— agent 传入的"确认"参数不构成批准,加了也只会被当
|
|
14
|
+
* 未知参数忽略。循环副作用边界:除 `run`(唯一命令行触发入口,手动
|
|
15
|
+
* 触发一次运行)外的命令不写信号、不触发观察/提案/probe。approve
|
|
16
|
+
* 决策后**立即应用**(spec"批准即应用与显式应用入口":持单飞作业锁经
|
|
17
|
+
* 备份会话写入,锁忙暂缓并告知);apply 幂等应用全部已批准未应用的编辑
|
|
18
|
+
* (失败重试与滞留消化入口);reject 只做状态迁移 + 档案;enable/disable
|
|
19
|
+
* 写 canonical 级别策略,并按 design 8.1 同步扩展清单条目——级别升至
|
|
20
|
+
* observe 及以上时写入、置回 off 时移除;rollback 转发既有备份会话回滚。
|
|
21
|
+
*/
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { classifyManagedObservation, HostEditError, LEVELS, loadEvolveLevelPolicy, triggerRenderSync, writeEvolveLevelPolicy, } from "../host/index.js";
|
|
24
|
+
import { removeExtensionEntry, upsertExtensionEntry } from "../lib/hostExtensions.js";
|
|
25
|
+
import { appendArchiveRecord } from "../evolve/archive.js";
|
|
26
|
+
import { EVOLVE_EXTENSION_ID, evolveExtensionLoadSpecifier } from "../evolve/extension.js";
|
|
27
|
+
import { ensureEvolveStateDir, ensureStateGitignored } from "../evolve/stateDir.js";
|
|
28
|
+
import { importHistory, ImportSourceError } from "../evolve/importHistory.js";
|
|
29
|
+
import { runJobShell } from "../evolve/jobShell.js";
|
|
30
|
+
import { anyProposalLevel } from "../evolve/jobRun.js";
|
|
31
|
+
import { buildEvolveReport } from "../evolve/reportView.js";
|
|
32
|
+
import { applyApprovedProposals } from "../evolve/apply.js";
|
|
33
|
+
import { acquireJobLock, collectTriggerState, evaluateTrigger, releaseJobLock, resolveTriggerLimits, setRunRequest, TRIGGER_DECISION_KIND, } from "../evolve/trigger.js";
|
|
34
|
+
import { runRollback } from "./rollback.js";
|
|
35
|
+
import { decideProposal, readPendingRecord, readProposalsByStatus, } from "../evolve/pending.js";
|
|
36
|
+
const USAGE = [
|
|
37
|
+
"Usage: trellis evolve <subcommand>",
|
|
38
|
+
"",
|
|
39
|
+
" status [--json] 级别与提案状态总览(只读,零副作用)",
|
|
40
|
+
" report [--json] 进化报告:时间线/前后测量/趋势(只读,零副作用)",
|
|
41
|
+
" pending 列出待批提案(摘要/证据/风险级别)",
|
|
42
|
+
" approve <id> 批准并应用提案(仅交互式终端;批准即应用)",
|
|
43
|
+
" reject <id> 拒绝提案(仅交互式终端)",
|
|
44
|
+
" apply 应用全部已批准未应用的提案(幂等;",
|
|
45
|
+
" 失败重试与滞留消化入口)",
|
|
46
|
+
" run [--dry-run] 手动触发一次运行(唯一命令行触发入口;",
|
|
47
|
+
" 跳过阈值/冷却,仍守单飞锁与预算护栏;",
|
|
48
|
+
" dry-run 预览,不拉起不写入)",
|
|
49
|
+
" import <path>... [--workspace <root>]",
|
|
50
|
+
" 导入历史会话数据为观察信号(显式路径、",
|
|
51
|
+
" 源只读、台账幂等;不触发任何作业)",
|
|
52
|
+
" enable [--level <level>] 开启进化(default level,缺省 observe);",
|
|
53
|
+
" 同步写入扩展清单条目(observe 及以上)",
|
|
54
|
+
" disable 关闭进化(default level = off);移除扩展清单条目",
|
|
55
|
+
" rollback [runId | --list | --dry-run | --json]",
|
|
56
|
+
" 备份会话回滚(同 `trellis rollback`)",
|
|
57
|
+
"",
|
|
58
|
+
"级别:" + LEVELS.join(" | "),
|
|
59
|
+
].join("\n");
|
|
60
|
+
function printUsage() {
|
|
61
|
+
console.log(USAGE);
|
|
62
|
+
}
|
|
63
|
+
function describeRecord(record) {
|
|
64
|
+
const lines = [
|
|
65
|
+
`${record.id} [${record.status}] risk=${record.riskClass}(${record.riskRuleId})`,
|
|
66
|
+
` kind: ${record.proposal.kind} target: ${record.proposal.target}`,
|
|
67
|
+
` ${record.proposal.summary}`,
|
|
68
|
+
` evidence: ${record.proposal.evidence
|
|
69
|
+
.map((link) => {
|
|
70
|
+
const ref = link.kind === "run" ? link.runId : link.kind === "doctor-finding" ? link.findingId : link.signalId;
|
|
71
|
+
return `${link.kind}:${ref}`;
|
|
72
|
+
})
|
|
73
|
+
.join("; ")}`,
|
|
74
|
+
];
|
|
75
|
+
for (const edit of record.proposal.edits) {
|
|
76
|
+
lines.push(` edit: ${edit.op} ${edit.path}`);
|
|
77
|
+
}
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function isInteractiveTTY(options) {
|
|
81
|
+
return options.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
82
|
+
}
|
|
83
|
+
function setDefaultLevel(homeDir, level) {
|
|
84
|
+
const current = loadEvolveLevelPolicy(homeDir);
|
|
85
|
+
const next = {
|
|
86
|
+
defaultLevel: level,
|
|
87
|
+
agents: current.agents,
|
|
88
|
+
workspaces: current.workspaces,
|
|
89
|
+
...(current.trigger !== undefined ? { trigger: current.trigger } : {}),
|
|
90
|
+
};
|
|
91
|
+
writeEvolveLevelPolicy(homeDir, next);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 按 8.1 定案同步扩展清单条目:任一粒度(default/agent/workspace)升至
|
|
96
|
+
* `observe` 及以上 → 写入(幂等 upsert);全部回到 `off` → 移除。粒度
|
|
97
|
+
* 覆盖场景(如 default off + workspace observe)下条目同样在——订阅者的
|
|
98
|
+
* 逐会话级别判定才是细粒度闸,清单条目只是"可装载"。
|
|
99
|
+
*/
|
|
100
|
+
function syncEvolveExtensionEntry(homeDir, policy) {
|
|
101
|
+
const levels = [
|
|
102
|
+
policy.defaultLevel,
|
|
103
|
+
...Object.values(policy.agents),
|
|
104
|
+
...Object.values(policy.workspaces),
|
|
105
|
+
];
|
|
106
|
+
const wanted = levels.some((l) => l === "observe" || l === "propose" || l === "auto-A");
|
|
107
|
+
if (wanted) {
|
|
108
|
+
upsertExtensionEntry(homeDir, { id: EVOLVE_EXTENSION_ID, load: evolveExtensionLoadSpecifier() });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
removeExtensionEntry(homeDir, EVOLVE_EXTENSION_ID);
|
|
112
|
+
}
|
|
113
|
+
return wanted;
|
|
114
|
+
}
|
|
115
|
+
/** status 文本渲染(与 `--json` 同源:同一份 EvolveReport 聚合)。 */
|
|
116
|
+
function renderStatusText(report) {
|
|
117
|
+
console.log(`default level: ${report.levels.defaultLevel ?? "off"}`);
|
|
118
|
+
for (const [agent, level] of Object.entries(report.levels.agents)) {
|
|
119
|
+
console.log(`agent ${agent}: ${level}`);
|
|
120
|
+
}
|
|
121
|
+
for (const [root, level] of Object.entries(report.levels.workspaces)) {
|
|
122
|
+
console.log(`workspace ${root}: ${level}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(`proposals: ${report.counts.pending} pending, ${report.counts.approved} approved, ${report.counts.rejected} rejected`);
|
|
125
|
+
if (report.counts.approvedUnapplied > 0) {
|
|
126
|
+
console.log(`approved 未应用 ${report.counts.approvedUnapplied} 条:运行 \`trellis evolve apply\` 立即写入(或等待下次作业应用阶段)。`);
|
|
127
|
+
}
|
|
128
|
+
if (report.notices.length > 0) {
|
|
129
|
+
console.log("最近的自动采纳通知:");
|
|
130
|
+
for (const notice of report.notices.slice(-3))
|
|
131
|
+
console.log(` ${notice}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** report 文本渲染:趋势 + 时间线(数值与 `--json` 同源)。 */
|
|
135
|
+
function renderReportText(report) {
|
|
136
|
+
const stateLabel = report.state === "not-enabled" ? "未开启(未记录任何进化活动)" :
|
|
137
|
+
report.state === "enabled-idle" ? "已开启,尚未运行" : "有运行历史";
|
|
138
|
+
console.log(`状态: ${stateLabel}`);
|
|
139
|
+
console.log("趋势(口径随数据导出,可从档案重算;不含合成评分):");
|
|
140
|
+
for (const metric of report.trends) {
|
|
141
|
+
const value = metric.value === null ? "无测量记录" : Number.isInteger(metric.value) ? String(metric.value) : metric.value.toFixed(3);
|
|
142
|
+
console.log(` ${metric.label}: ${value} [来源:${metric.source}]`);
|
|
143
|
+
}
|
|
144
|
+
if (report.jobRuns.length > 0) {
|
|
145
|
+
console.log(`作业运行(${report.jobRuns.length} 次):`);
|
|
146
|
+
for (const run of report.jobRuns) {
|
|
147
|
+
const label = run.trigger === "manual" ? "手动" : run.trigger === "chain" ? "补跑" : "事件";
|
|
148
|
+
console.log(` ${run.startedAt} [${label}] ${run.ok ? "ok" : `error: ${run.error ?? "?"}`}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (report.timeline.length === 0) {
|
|
152
|
+
console.log("提案时间线: 空(尚无提案)");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.log("提案时间线:");
|
|
156
|
+
for (const trace of report.timeline) {
|
|
157
|
+
console.log(` ${trace.id} [${trace.status}] ${trace.summary}`);
|
|
158
|
+
for (const step of trace.chain)
|
|
159
|
+
console.log(` - ${step.at} ${step.stage}: ${step.note}`);
|
|
160
|
+
if (trace.measurement === null) {
|
|
161
|
+
console.log(" 测量: 无测量记录(未推断)");
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const m = trace.measurement;
|
|
165
|
+
console.log(` 测量: ${m.decision} — 消除 ${m.resolvedIds.length}/${m.targetIds.length},新引入 ${m.introduced.length}` +
|
|
166
|
+
`(${m.reason})`);
|
|
167
|
+
}
|
|
168
|
+
if (trace.backupRunId !== null)
|
|
169
|
+
console.log(` 回滚: trellis evolve rollback ${trace.backupRunId}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function runDecision(homeDir, id, decision, options) {
|
|
174
|
+
if (id === undefined || id === "") {
|
|
175
|
+
console.error(`trellis evolve: ${decision === "approved" ? "approve" : "reject"} 需要提案 id`);
|
|
176
|
+
console.error(USAGE);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
// 批准只走带外通道:非交互环境(含 agent 脚本)一律拒绝,提案保持
|
|
180
|
+
// pending。任何 --yes/--confirm 之类参数不构成批准(未知参数忽略)。
|
|
181
|
+
if (!isInteractiveTTY(options)) {
|
|
182
|
+
console.error(`trellis evolve: ${decision === "approved" ? "批准" : "拒绝"}仅限交互式终端。` +
|
|
183
|
+
"非交互环境不接受任何确认参数,提案保持 pending。");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
const result = decideProposal({
|
|
187
|
+
homeDir,
|
|
188
|
+
id,
|
|
189
|
+
decision,
|
|
190
|
+
...(options.now !== undefined ? { now: options.now } : {}),
|
|
191
|
+
});
|
|
192
|
+
if (!result.ok) {
|
|
193
|
+
if (result.error === "not-found") {
|
|
194
|
+
console.error(`trellis evolve: 提案 ${id} 不存在`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const current = readPendingRecord(homeDir, id);
|
|
198
|
+
console.error(`trellis evolve: 提案 ${id} 已决策(${current?.status ?? "?"}),不重复决策`);
|
|
199
|
+
}
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
if (decision === "approved") {
|
|
203
|
+
console.log(`已批准提案 ${id}(risk=${result.record.riskClass});批准即应用。`);
|
|
204
|
+
return await applyApprovedAndReport(homeDir, options);
|
|
205
|
+
}
|
|
206
|
+
console.log(`已拒绝提案 ${id}。`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 应用全部已批准未应用的提案并汇报(spec"批准即应用与显式应用入口")。
|
|
211
|
+
* 与作业应用阶段互斥:复用单飞作业锁防并发双写;锁忙时暂缓并如实告知
|
|
212
|
+
* (决策已完成、提案保持 approved,由本入口或作业应用阶段收尾)。
|
|
213
|
+
*/
|
|
214
|
+
async function applyApprovedAndReport(homeDir, options) {
|
|
215
|
+
const agents = options.applyAgents ??
|
|
216
|
+
classifyManagedObservation(homeDir).map((entry) => ({
|
|
217
|
+
agent: entry.agent,
|
|
218
|
+
observation: entry.observation,
|
|
219
|
+
}));
|
|
220
|
+
const triggerSync = options.triggerSync ?? (async () => await triggerRenderSync({ homeDir }));
|
|
221
|
+
if (!acquireJobLock(homeDir, process.pid, Date.now())) {
|
|
222
|
+
console.log("已有作业在运行:本次应用暂缓(决策已完成,提案保持 approved)。" +
|
|
223
|
+
"作业的应用阶段会写入,也可稍后运行 `trellis evolve apply`。");
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const result = await applyApprovedProposals({ homeDir, agents, triggerSync });
|
|
228
|
+
return renderApplyResult(result);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
if (err instanceof HostEditError) {
|
|
232
|
+
console.error(`应用部分失败:${err.message}`);
|
|
233
|
+
console.error(`已应用部分可回滚:trellis evolve rollback ${err.runId}。` +
|
|
234
|
+
"提案保持 approved,可经 `trellis evolve apply` 重试(编辑幂等重放)。");
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
releaseJobLock(homeDir, process.pid);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function renderApplyResult(result) {
|
|
244
|
+
for (const id of result.skippedInvalidIds) {
|
|
245
|
+
console.log(`跳过 ${id}:应用前重校验不通过(pending 磁盘状态漂移?),未写入。`);
|
|
246
|
+
}
|
|
247
|
+
if (result.appliedProposalIds.length === 0) {
|
|
248
|
+
console.log(result.alreadyAppliedIds.length > 0
|
|
249
|
+
? "没有已批准未应用的提案(已应用过的不重复写入)。"
|
|
250
|
+
: "没有已批准未应用的提案。");
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
console.log(`已应用 ${result.appliedProposalIds.length} 条提案:${result.appliedProposalIds.join(", ")}。`);
|
|
254
|
+
console.log(`备份会话 ${result.runId}。回滚:trellis evolve rollback ${result.runId}`);
|
|
255
|
+
for (const effect of result.effects) {
|
|
256
|
+
console.log(`生效时点 ${effect.agent}(${effect.observation}):${effect.effect}`);
|
|
257
|
+
}
|
|
258
|
+
if (result.syncError !== null) {
|
|
259
|
+
console.log(`注意:渲染同步触发失败:${result.syncError};可手动运行 \`trellis sync\`。`);
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 手动触发一次运行(`trellis evolve run`;run-backfill D1/D2)。唯一命令
|
|
265
|
+
* 行触发入口:阈值/冷却豁免(用户明确意图),单飞锁与预算护栏照旧;
|
|
266
|
+
* `--dry-run` 只预览(零副作用:不抢锁、不写档案);observe 及以下作业体
|
|
267
|
+
* 零副作用早退并说明级别与调整方法。不设 TTY 门——手动触发是显式动作,
|
|
268
|
+
* 输出走 stdout。
|
|
269
|
+
*/
|
|
270
|
+
async function runManualJob(homeDir, dryRun, options) {
|
|
271
|
+
const now = options.now ?? Date.now;
|
|
272
|
+
const policy = loadEvolveLevelPolicy(homeDir);
|
|
273
|
+
if (!anyProposalLevel(policy)) {
|
|
274
|
+
console.log(`当前级别 ${policy.defaultLevel ?? "off"}:作业体零副作用早退(observe 只采集信号,off 关闭)。` +
|
|
275
|
+
"要手动运行作业体,先 `trellis evolve enable --level propose`(或 auto-A)。");
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const limits = resolveTriggerLimits(policy.trigger);
|
|
279
|
+
const state = collectTriggerState(homeDir, now());
|
|
280
|
+
const gate = evaluateTrigger(state, limits, { threshold: true, cooldown: true });
|
|
281
|
+
if (dryRun) {
|
|
282
|
+
console.log("dry-run:预览本次手动触发(不拉起作业、不写入任何状态)。");
|
|
283
|
+
console.log(` 累计信号:${state.pendingSignalCount}`);
|
|
284
|
+
console.log(` 护栏判定:${gate.decision === "launch" ? "将运行(阈值/冷却已豁免,预算未超限)" : `将跳过(${gate.reason}):${gate.note}`}`);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
if (gate.decision === "skip") {
|
|
288
|
+
let note = gate.note;
|
|
289
|
+
if (gate.reason === "already-running") {
|
|
290
|
+
setRunRequest(homeDir, "trigger:manual", now());
|
|
291
|
+
note += ";已置补跑标记(当前作业收尾后补跑至多一次)";
|
|
292
|
+
}
|
|
293
|
+
appendArchiveRecord(homeDir, { kind: TRIGGER_DECISION_KIND, payload: { decision: "skip", reason: gate.reason, note, trigger: "manual" } }, () => new Date(now()));
|
|
294
|
+
console.log(`本次运行被跳过(${gate.reason}):${note}`);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
const result = await runJobShell(homeDir, { trigger: "manual", now });
|
|
298
|
+
if (result.status === "lock-busy") {
|
|
299
|
+
// 竞态窗口:门检通过后另有作业抢到锁 → 与运行中触发同语义,置补跑标记。
|
|
300
|
+
setRunRequest(homeDir, "trigger:manual", now());
|
|
301
|
+
console.log("已有作业在运行(竞态窗口):本次运行未启动,已置补跑标记(当前作业收尾后补跑至多一次)。");
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
if (result.status === "error") {
|
|
305
|
+
console.error(`作业体失败:${result.message}(运行档案已记录错误,可重试)。`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const s = result.summary;
|
|
309
|
+
console.log(`作业完成:信号 ${s.signalsConsidered} 条、提案 ${s.enqueuedIds.length} 条待批、` +
|
|
310
|
+
`自动采纳 ${s.autoAdoptedIds.length} 条、应用 ${s.applied.appliedProposalIds.length} 条。`);
|
|
311
|
+
for (const notice of s.notices)
|
|
312
|
+
console.log(` ${notice}`);
|
|
313
|
+
console.log("运行档案已记录(trigger=manual)。");
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 历史数据导入(`trellis evolve import`;run-backfill D4)。仅处理用户
|
|
318
|
+
* 显式选定的源路径、对源只读;整批先校验后写入(任何源不可解析即非零
|
|
319
|
+
* 退出且零写入);台账幂等(重复导入为空操作);导入不触发任何作业。
|
|
320
|
+
*/
|
|
321
|
+
function runImport(homeDir, paths, workspaceRoot) {
|
|
322
|
+
if (paths.length === 0) {
|
|
323
|
+
console.error("trellis evolve import: 需要至少一个源路径(仅处理显式选定的文件,不扫描目录)");
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const result = importHistory({ homeDir, paths, ...(workspaceRoot !== undefined ? { workspaceRoot } : {}) });
|
|
328
|
+
for (const source of result.sources) {
|
|
329
|
+
if (source.duplicate) {
|
|
330
|
+
console.log(`跳过 ${source.path}:已导入过(台账记账,空操作)。`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
console.log(`导入 ${source.path}:${source.entries} 条信号(坏行跳过 ${source.badLines},无工具名记录跳过 ${source.skippedNoTool})。`);
|
|
334
|
+
}
|
|
335
|
+
console.log(`共写入 ${result.written} 条观察信号,台账已记账(重复导入为空操作)。` +
|
|
336
|
+
"导入不触发作业,信号待后续作业按水位线消费。");
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
if (err instanceof ImportSourceError) {
|
|
341
|
+
console.error(`trellis evolve import: ${err.message}`);
|
|
342
|
+
console.error("本次导入零写入(信号存储与台账保持不变)。");
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 管理面命令入口。`args` 为 `evolve` 之后的 argv(子命令 + 参数),
|
|
350
|
+
* 由 cli 原样传入 —— 不经 shell 拼接。
|
|
351
|
+
*/
|
|
352
|
+
export async function runEvolveCommand(args, options = {}) {
|
|
353
|
+
const homeDir = options.homeDir ?? homedir();
|
|
354
|
+
const [subcommand, ...rest] = args;
|
|
355
|
+
if (subcommand === undefined || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
356
|
+
printUsage();
|
|
357
|
+
return { exitCode: subcommand === undefined ? 1 : 0 };
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
if (subcommand === "status" || subcommand === "report") {
|
|
361
|
+
// 只读出口:同一份 buildEvolveReport 聚合,文本/JSON 两副渲染壳。
|
|
362
|
+
const report = buildEvolveReport(homeDir);
|
|
363
|
+
const json = rest.includes("--json");
|
|
364
|
+
if (json) {
|
|
365
|
+
console.log(JSON.stringify(report, null, 2));
|
|
366
|
+
}
|
|
367
|
+
else if (subcommand === "status") {
|
|
368
|
+
renderStatusText(report);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
renderReportText(report);
|
|
372
|
+
}
|
|
373
|
+
return { exitCode: 0 };
|
|
374
|
+
}
|
|
375
|
+
if (subcommand === "pending") {
|
|
376
|
+
const pending = readProposalsByStatus(homeDir, "pending");
|
|
377
|
+
if (pending.length === 0) {
|
|
378
|
+
console.log("没有待批提案。");
|
|
379
|
+
return { exitCode: 0 };
|
|
380
|
+
}
|
|
381
|
+
for (const record of pending)
|
|
382
|
+
console.log(describeRecord(record) + "\n");
|
|
383
|
+
return { exitCode: 0 };
|
|
384
|
+
}
|
|
385
|
+
if (subcommand === "approve" || subcommand === "reject") {
|
|
386
|
+
const id = rest.find((arg) => !arg.startsWith("-"));
|
|
387
|
+
return {
|
|
388
|
+
exitCode: await runDecision(homeDir, id ?? "", subcommand === "approve" ? "approved" : "rejected", options),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (subcommand === "apply") {
|
|
392
|
+
return { exitCode: await applyApprovedAndReport(homeDir, options) };
|
|
393
|
+
}
|
|
394
|
+
if (subcommand === "run") {
|
|
395
|
+
return { exitCode: await runManualJob(homeDir, rest.includes("--dry-run"), options) };
|
|
396
|
+
}
|
|
397
|
+
if (subcommand === "import") {
|
|
398
|
+
const wsIdx = rest.indexOf("--workspace");
|
|
399
|
+
const workspaceRoot = wsIdx >= 0 ? rest[wsIdx + 1] : undefined;
|
|
400
|
+
if (wsIdx >= 0 && (workspaceRoot === undefined || workspaceRoot.startsWith("-"))) {
|
|
401
|
+
console.error("trellis evolve import: --workspace 需要 workspace 根路径");
|
|
402
|
+
return { exitCode: 1 };
|
|
403
|
+
}
|
|
404
|
+
const paths = rest.filter((arg, i) => !arg.startsWith("-") && (wsIdx < 0 || i !== wsIdx + 1));
|
|
405
|
+
return { exitCode: runImport(homeDir, paths, workspaceRoot) };
|
|
406
|
+
}
|
|
407
|
+
if (subcommand === "enable" || subcommand === "disable") {
|
|
408
|
+
const levelAt = rest.indexOf("--level");
|
|
409
|
+
if (subcommand === "disable" && levelAt >= 0) {
|
|
410
|
+
console.error("trellis evolve: disable 不接受 --level(关闭即 default off;调整级别用 enable --level)");
|
|
411
|
+
return { exitCode: 1 };
|
|
412
|
+
}
|
|
413
|
+
let level = subcommand === "enable" ? "observe" : "off";
|
|
414
|
+
if (levelAt >= 0) {
|
|
415
|
+
const raw = rest[levelAt + 1];
|
|
416
|
+
if (raw === undefined || !LEVELS.includes(raw)) {
|
|
417
|
+
console.error(`trellis evolve: --level 必须是 ${LEVELS.join(" | ")}`);
|
|
418
|
+
return { exitCode: 1 };
|
|
419
|
+
}
|
|
420
|
+
level = raw;
|
|
421
|
+
}
|
|
422
|
+
// 自治升级与批准同级闸门:propose/auto-A 会产出甚至自动应用编辑,
|
|
423
|
+
// 只限交互式终端——非交互的 agent 会话不得自行提升自治级别
|
|
424
|
+
// (observe 只采集信号,不设门)。
|
|
425
|
+
if (subcommand === "enable" && (level === "propose" || level === "auto-A") && !isInteractiveTTY(options)) {
|
|
426
|
+
console.error(`trellis evolve: 升至 ${level} 仅限交互式终端(与批准同级闸门)。` +
|
|
427
|
+
"非交互环境请先 enable 到 observe。");
|
|
428
|
+
return { exitCode: 1 };
|
|
429
|
+
}
|
|
430
|
+
const policy = setDefaultLevel(homeDir, level);
|
|
431
|
+
let gitignoreNote = null;
|
|
432
|
+
if (subcommand === "enable") {
|
|
433
|
+
ensureEvolveStateDir(homeDir);
|
|
434
|
+
// 状态根在 git work tree 内(如 dotfiles 仓库)时把 evolve/ 加入
|
|
435
|
+
// .gitignore:只追加状态(信号/pending)不进版本库。
|
|
436
|
+
if (ensureStateGitignored(homeDir) === "added") {
|
|
437
|
+
gitignoreNote = "已把 evolve/ 加入 .trellis/.gitignore(只追加状态不进版本库)。";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const entryPresent = syncEvolveExtensionEntry(homeDir, policy);
|
|
441
|
+
console.log(subcommand === "enable"
|
|
442
|
+
? `已开启自我进化(default level: ${policy.defaultLevel ?? "off"})。`
|
|
443
|
+
: "已关闭自我进化(default level: off)。");
|
|
444
|
+
console.log(entryPresent
|
|
445
|
+
? `扩展清单条目 "${EVOLVE_EXTENSION_ID}" 已写入(observe 及以上:gateway 会话将装载观察/触发订阅者)。`
|
|
446
|
+
: `扩展清单条目 "${EVOLVE_EXTENSION_ID}" 已移除(全部级别为 off)。`);
|
|
447
|
+
if (gitignoreNote !== null)
|
|
448
|
+
console.log(gitignoreNote);
|
|
449
|
+
if (subcommand === "disable" && entryPresent) {
|
|
450
|
+
const overrides = [
|
|
451
|
+
...Object.entries(policy.agents).map(([agent, l]) => `agent ${agent}=${l}`),
|
|
452
|
+
...Object.entries(policy.workspaces).map(([root, l]) => `workspace ${root}=${l}`),
|
|
453
|
+
];
|
|
454
|
+
console.log(`注意:仍存在非 off 覆盖(${overrides.join(", ")}),这些粒度上进化仍在生效。`);
|
|
455
|
+
}
|
|
456
|
+
return { exitCode: 0 };
|
|
457
|
+
}
|
|
458
|
+
if (subcommand === "rollback") {
|
|
459
|
+
const list = rest.includes("--list");
|
|
460
|
+
const dryRun = rest.includes("--dry-run");
|
|
461
|
+
const json = rest.includes("--json");
|
|
462
|
+
const runId = rest.find((arg) => !arg.startsWith("-"));
|
|
463
|
+
return await runRollback({
|
|
464
|
+
...(runId !== undefined ? { runId } : {}),
|
|
465
|
+
list,
|
|
466
|
+
dryRun,
|
|
467
|
+
json,
|
|
468
|
+
homeDir,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
console.error(`trellis evolve: 未知子命令 "${subcommand}"`);
|
|
472
|
+
printUsage();
|
|
473
|
+
return { exitCode: 1 };
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
console.error(`trellis evolve: ${err.message}`);
|
|
477
|
+
return { exitCode: 1 };
|
|
478
|
+
}
|
|
479
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function collectMcpSyncReport(opts = {}) {
|
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
67
|
const globalItems = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
|
|
68
|
-
const wsItems = planWorkspaceMcp(adapter.id, canonical);
|
|
68
|
+
const wsItems = await planWorkspaceMcp(adapter.id, canonical);
|
|
69
69
|
if (writeMode) {
|
|
70
70
|
await adapter.apply(globalItems, backup);
|
|
71
71
|
applyWorkspaceMcp(adapter.id, canonical, wsItems, backup);
|
package/dist/commands/mcpAuth.js
CHANGED
|
@@ -114,7 +114,7 @@ export async function authorizeMcpServer(opts) {
|
|
|
114
114
|
const client = oauthClientMetadata(def.auth);
|
|
115
115
|
let clientSecret;
|
|
116
116
|
if (client.clientSecretEnv) {
|
|
117
|
-
const { [client.clientSecretEnv]: value } = resolveSecretEnv([client.clientSecretEnv], canonical.secretsPolicy);
|
|
117
|
+
const { [client.clientSecretEnv]: value } = await resolveSecretEnv([client.clientSecretEnv], canonical.secretsPolicy);
|
|
118
118
|
if (value === undefined) {
|
|
119
119
|
throw new Error(`auth.client_secret_env is "${client.clientSecretEnv}", but no value for that variable was found in ${secretSourceLabel(canonical.secretsPolicy)} — set it there, or drop the field if this provider issues a public client`);
|
|
120
120
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
18
18
|
import { type GatewayBackend, type UpstreamSpec } from "../lib/gatewayBackend.js";
|
|
19
|
+
import { BuiltinRegistry } from "../lib/mcpRuntime.js";
|
|
20
|
+
import { HostEventEmitter } from "../lib/hostEventBus.js";
|
|
19
21
|
import type { AgentId } from "../core/types.js";
|
|
20
22
|
export interface RunMcpGatewayOptions {
|
|
21
23
|
agentId: AgentId;
|
|
@@ -44,11 +46,11 @@ export declare function parseMcpGatewayArgs(argv: readonly string[]): {
|
|
|
44
46
|
* connect to itself. Resolving against a gateway-less view of the same
|
|
45
47
|
* canonical gives the set gateway mode replaced.
|
|
46
48
|
*/
|
|
47
|
-
export declare function resolveGatewayUpstreams(agentId: AgentId, canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): {
|
|
49
|
+
export declare function resolveGatewayUpstreams(agentId: AgentId, canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Promise<{
|
|
48
50
|
upstreams: UpstreamSpec[];
|
|
49
51
|
oauthServers: string[];
|
|
50
52
|
conflicts: string[];
|
|
51
|
-
}
|
|
53
|
+
}>;
|
|
52
54
|
export declare function runMcpGateway(opts: RunMcpGatewayOptions): Promise<{
|
|
53
55
|
exitCode: number;
|
|
54
56
|
}>;
|
|
@@ -58,3 +60,29 @@ export declare function runMcpGateway(opts: RunMcpGatewayOptions): Promise<{
|
|
|
58
60
|
export declare function runMcpRuntime(opts: RunMcpGatewayOptions): Promise<{
|
|
59
61
|
exitCode: number;
|
|
60
62
|
}>;
|
|
63
|
+
type ShutdownSignal = "SIGTERM" | "SIGINT" | "SIGHUP";
|
|
64
|
+
/**
|
|
65
|
+
* Teardown on client disconnect — the piece neither layer below provides.
|
|
66
|
+
*
|
|
67
|
+
* `StdioServerTransport.start()` registers listeners for `'data'` and
|
|
68
|
+
* `'error'` only, so stdin reaching EOF never reaches `onclose`; and POSIX
|
|
69
|
+
* re-parents an orphan to launchd rather than killing it, while this
|
|
70
|
+
* process's own event loop is held open by every upstream it connected.
|
|
71
|
+
* Without this, each agent session would permanently leak a gateway plus
|
|
72
|
+
* its entire upstream process set (design.md D13).
|
|
73
|
+
*
|
|
74
|
+
* The "session end" event is emitted here, before upstream release
|
|
75
|
+
* (host-extension-points lifecycle requirement). SIGKILL/crash gets no
|
|
76
|
+
* such event by design — consumers must tolerate a missing end event.
|
|
77
|
+
*/
|
|
78
|
+
export declare function waitForShutdown(registry: BuiltinRegistry, server: {
|
|
79
|
+
close(): Promise<void>;
|
|
80
|
+
}, events: HostEventEmitter,
|
|
81
|
+
/** Test seams, exactly as every other Trellis entry point exposes them:
|
|
82
|
+
* the trigger sources only. Production (and runMcpEdge) leave these unset
|
|
83
|
+
* and the function listens on the real process. */
|
|
84
|
+
seams?: {
|
|
85
|
+
stdin?: Pick<NodeJS.EventEmitter, "on">;
|
|
86
|
+
onceSignal?: (signal: ShutdownSignal, listener: () => void) => void;
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
export {};
|
|
@@ -21,6 +21,9 @@ import { isGatewayAgent, isAgentOwnedOAuth, resolveMcpPlan } from "../adapters/m
|
|
|
21
21
|
import { LocalBackend } from "../lib/gatewayBackend.js";
|
|
22
22
|
import { DEFAULT_CONNECT_TIMEOUT_MS } from "../lib/mcpConnect.js";
|
|
23
23
|
import { BuiltinRegistry, createRuntimeServer, UpstreamProvider } from "../lib/mcpRuntime.js";
|
|
24
|
+
import { HostEventBus, HostEventEmitter } from "../lib/hostEventBus.js";
|
|
25
|
+
import { loadHostExtensions } from "../lib/hostExtensions.js";
|
|
26
|
+
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
24
27
|
import { McpStatusProvider } from "../lib/mcpStatusProvider.js";
|
|
25
28
|
import { RuntimeControlProvider } from "../lib/runtimeControlProvider.js";
|
|
26
29
|
import { TaskProvider } from "../lib/taskProvider.js";
|
|
@@ -56,7 +59,7 @@ export function parseMcpGatewayArgs(argv) {
|
|
|
56
59
|
* connect to itself. Resolving against a gateway-less view of the same
|
|
57
60
|
* canonical gives the set gateway mode replaced.
|
|
58
61
|
*/
|
|
59
|
-
export function resolveGatewayUpstreams(agentId, canonical) {
|
|
62
|
+
export async function resolveGatewayUpstreams(agentId, canonical) {
|
|
60
63
|
const route = canonical.mcp.routes?.[agentId];
|
|
61
64
|
const hubActive = route ? route.mode === "hub" : Boolean(canonical.mcp.hub) && !isGatewayAgent(agentId, canonical.mcp, canonical.managedAgents);
|
|
62
65
|
if (hubActive) {
|
|
@@ -87,7 +90,7 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
87
90
|
// ownership-gated removal clears it. Serving that stale entry would hand
|
|
88
91
|
// every server, and every secret resolved into it, to the one agent the
|
|
89
92
|
// user just said to stop involving.
|
|
90
|
-
const { desired, conflicts } = resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
93
|
+
const { desired, conflicts } = await resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
91
94
|
// Agent-owned OAuth servers bypass the gateway as direct entries, so no
|
|
92
95
|
// connection state exists for them here — but Trellis owns the token
|
|
93
96
|
// store, so `trellis.mcp.status` reports their credential health for
|
|
@@ -98,7 +101,7 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
98
101
|
const agentOwnedServers = Object.fromEntries(Object.entries(canonical.mcp.servers).filter(([, def]) => isAgentOwnedOAuth(def)));
|
|
99
102
|
const oauthServers = Object.keys(agentOwnedServers).length === 0
|
|
100
103
|
? []
|
|
101
|
-
: resolveMcpPlan(agentId, { ...direct, servers: agentOwnedServers }, canonical.managedAgents, canonical.secretsPolicy).desired.map(({ name }) => name);
|
|
104
|
+
: (await resolveMcpPlan(agentId, { ...direct, servers: agentOwnedServers }, canonical.managedAgents, canonical.secretsPolicy)).desired.map(({ name }) => name);
|
|
102
105
|
return {
|
|
103
106
|
upstreams: desired.map(({ name }) => ({ name, def: canonical.mcp.servers[name] })),
|
|
104
107
|
oauthServers,
|
|
@@ -128,7 +131,7 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
|
128
131
|
console.error(`trellis-mcp-gateway: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
132
|
return { exitCode: 1 };
|
|
130
133
|
}
|
|
131
|
-
const { upstreams, oauthServers, conflicts } = resolveGatewayUpstreams(opts.agentId, canonical);
|
|
134
|
+
const { upstreams, oauthServers, conflicts } = await resolveGatewayUpstreams(opts.agentId, canonical);
|
|
132
135
|
for (const message of conflicts) {
|
|
133
136
|
console.error(`trellis-mcp-gateway: ${message}`);
|
|
134
137
|
}
|
|
@@ -152,9 +155,29 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
|
152
155
|
...upstreams.filter((u) => isOAuthAuth(u.def.auth)).map((u) => ({ name: u.name, owner: "trellis" })),
|
|
153
156
|
...oauthServers.map((name) => ({ name, owner: "agent" })),
|
|
154
157
|
];
|
|
155
|
-
|
|
158
|
+
// Host-extension-point events (host-extension-points spec): one bus per
|
|
159
|
+
// process, one session id stamped on every event. No subscribers yet means
|
|
160
|
+
// emit is a no-op and runtime behavior stays byte-identical to the
|
|
161
|
+
// pre-event world; extensions loaded from the canonical manifest (8.6)
|
|
162
|
+
// subscribe here.
|
|
163
|
+
const bus = new HostEventBus();
|
|
164
|
+
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
|
165
|
+
const events = new HostEventEmitter({
|
|
166
|
+
bus,
|
|
167
|
+
agent: opts.agentId,
|
|
168
|
+
workspaceRoot,
|
|
169
|
+
});
|
|
170
|
+
// External extensions (8.6): manifest-only loading from
|
|
171
|
+
// ~/.trellis/mcp/extensions.yaml. Providers and event subscribers come
|
|
172
|
+
// through this one surface; a broken extension is contained and reported
|
|
173
|
+
// by the loader and never changes what the core set below can do.
|
|
174
|
+
const extensions = await loadHostExtensions({ homeDir, agent: opts.agentId, workspaceRoot });
|
|
175
|
+
for (const subscriber of extensions.subscribers)
|
|
176
|
+
bus.subscribe(subscriber);
|
|
177
|
+
const registry = new BuiltinRegistry([new SkillProvider(), new RuntimeMemoryProvider(), new RuntimeControlProvider(backend), new McpStatusProvider(backend, { homeDir, oauthServers: oauthReport }), new TaskProvider(), new AgentBridgeProvider(), new UpstreamProvider(backend), ...extensions.providers], undefined, (info) => events.toolCall(info));
|
|
156
178
|
const server = serveStdio(() => createRuntimeServer(serverInfo, { agentId: opts.agentId, homeDir }, registry));
|
|
157
|
-
|
|
179
|
+
events.sessionStart();
|
|
180
|
+
await waitForShutdown(registry, server, events);
|
|
158
181
|
return { exitCode: 0 };
|
|
159
182
|
}
|
|
160
183
|
/**
|
|
@@ -166,14 +189,25 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
|
166
189
|
* process's own event loop is held open by every upstream it connected.
|
|
167
190
|
* Without this, each agent session would permanently leak a gateway plus
|
|
168
191
|
* its entire upstream process set (design.md D13).
|
|
192
|
+
*
|
|
193
|
+
* The "session end" event is emitted here, before upstream release
|
|
194
|
+
* (host-extension-points lifecycle requirement). SIGKILL/crash gets no
|
|
195
|
+
* such event by design — consumers must tolerate a missing end event.
|
|
169
196
|
*/
|
|
170
|
-
function waitForShutdown(registry, server
|
|
197
|
+
export function waitForShutdown(registry, server, events,
|
|
198
|
+
/** Test seams, exactly as every other Trellis entry point exposes them:
|
|
199
|
+
* the trigger sources only. Production (and runMcpEdge) leave these unset
|
|
200
|
+
* and the function listens on the real process. */
|
|
201
|
+
seams) {
|
|
202
|
+
const stdin = seams?.stdin ?? process.stdin;
|
|
203
|
+
const onceSignal = seams?.onceSignal ?? ((signal, listener) => { process.once(signal, listener); });
|
|
171
204
|
return new Promise((resolve) => {
|
|
172
205
|
let settled = false;
|
|
173
206
|
const shutdown = async () => {
|
|
174
207
|
if (settled)
|
|
175
208
|
return;
|
|
176
209
|
settled = true;
|
|
210
|
+
events.sessionEnd();
|
|
177
211
|
try {
|
|
178
212
|
await registry.close();
|
|
179
213
|
}
|
|
@@ -188,10 +222,10 @@ function waitForShutdown(registry, server) {
|
|
|
188
222
|
}
|
|
189
223
|
resolve();
|
|
190
224
|
};
|
|
191
|
-
|
|
192
|
-
|
|
225
|
+
stdin.on("end", shutdown);
|
|
226
|
+
stdin.on("close", shutdown);
|
|
193
227
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
194
|
-
|
|
228
|
+
onceSignal(signal, shutdown);
|
|
195
229
|
}
|
|
196
230
|
});
|
|
197
231
|
}
|