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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process host event bus (host-extension-points spec).
|
|
3
|
+
*
|
|
4
|
+
* Dispatch is synchronous and ordered: `emit` walks listeners at the event
|
|
5
|
+
* point so a session-end event is observably delivered before upstream
|
|
6
|
+
* release continues. Failures are contained per listener — a synchronous
|
|
7
|
+
* throw or a rejected promise is swallowed and warned, never propagated to
|
|
8
|
+
* the tool call or to sibling listeners. With no subscribers `emit` is a
|
|
9
|
+
* single length check (zero overhead; runtime behavior stays byte-identical
|
|
10
|
+
* to the pre-event world). Listeners are expected to return promptly; slow
|
|
11
|
+
* work belongs behind the returned promise, which the caller never awaits.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { sessionEndEvent, sessionStartEvent, toolCallEvent, } from "../host/hostEvents.js";
|
|
15
|
+
export class HostEventBus {
|
|
16
|
+
warn;
|
|
17
|
+
listeners = [];
|
|
18
|
+
constructor(warn = console.error) {
|
|
19
|
+
this.warn = warn;
|
|
20
|
+
}
|
|
21
|
+
/** Register a listener; returns its unsubscribe function. Registration
|
|
22
|
+
* failure isolation for extensions is the loader's job (8.6) — this method
|
|
23
|
+
* only ever pushes. */
|
|
24
|
+
subscribe(listener) {
|
|
25
|
+
this.listeners.push(listener);
|
|
26
|
+
return () => {
|
|
27
|
+
const index = this.listeners.indexOf(listener);
|
|
28
|
+
if (index >= 0)
|
|
29
|
+
this.listeners.splice(index, 1);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
emit(event) {
|
|
33
|
+
if (this.listeners.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
for (const listener of this.listeners) {
|
|
36
|
+
try {
|
|
37
|
+
const pending = listener(event);
|
|
38
|
+
if (pending && typeof pending.then === "function") {
|
|
39
|
+
void pending.catch((err) => {
|
|
40
|
+
this.warn(`trellis-host-events: subscriber failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.warn(`trellis-host-events: subscriber failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
get subscriberCount() {
|
|
50
|
+
return this.listeners.length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Stamps events with process-level identity. One instance per gateway
|
|
55
|
+
* process: `sessionId` is generated here and carried unchanged on every
|
|
56
|
+
* event the process emits ("会话开始"/tool calls/"会话结束").
|
|
57
|
+
*/
|
|
58
|
+
export class HostEventEmitter {
|
|
59
|
+
opts;
|
|
60
|
+
sessionId;
|
|
61
|
+
constructor(opts) {
|
|
62
|
+
this.opts = opts;
|
|
63
|
+
this.sessionId = opts.sessionId ?? randomUUID();
|
|
64
|
+
}
|
|
65
|
+
ts() {
|
|
66
|
+
return (this.opts.now ?? Date.now)();
|
|
67
|
+
}
|
|
68
|
+
sessionStart() {
|
|
69
|
+
this.opts.bus.emit(sessionStartEvent({
|
|
70
|
+
ts: this.ts(),
|
|
71
|
+
agent: this.opts.agent,
|
|
72
|
+
session: this.sessionId,
|
|
73
|
+
workspaceRoot: this.opts.workspaceRoot,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
toolCall(info) {
|
|
77
|
+
this.opts.bus.emit(toolCallEvent({
|
|
78
|
+
ts: this.ts(),
|
|
79
|
+
agent: this.opts.agent,
|
|
80
|
+
session: this.sessionId,
|
|
81
|
+
provider: info.provider,
|
|
82
|
+
tool: info.tool,
|
|
83
|
+
durationMs: info.durationMs,
|
|
84
|
+
result: info.result,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
sessionEnd() {
|
|
88
|
+
this.opts.bus.emit(sessionEndEvent({
|
|
89
|
+
ts: this.ts(),
|
|
90
|
+
agent: this.opts.agent,
|
|
91
|
+
session: this.sessionId,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External host extension loading (host-extension-points spec, 8.6).
|
|
3
|
+
*
|
|
4
|
+
* The ONLY way an external module becomes a Trellis provider or event
|
|
5
|
+
* subscriber: it is declared in the canonical extension manifest at
|
|
6
|
+
* `~/.trellis/mcp/extensions.yaml` (entries carry `id` + a `load` module
|
|
7
|
+
* specifier) and its module exports `activate(ctx)` returning
|
|
8
|
+
* `{ providers?, subscribers? }`. There is no directory scan and no
|
|
9
|
+
* naming convention — a module on disk that the manifest does not name
|
|
10
|
+
* is never imported.
|
|
11
|
+
*
|
|
12
|
+
* Fault isolation is per extension and total: a manifest that fails to
|
|
13
|
+
* parse, an entry of the wrong shape, an import that throws, an
|
|
14
|
+
* `activate` that throws, or an activation of the wrong shape are each
|
|
15
|
+
* recorded as a failure (and warned), and none of them affect the core
|
|
16
|
+
* providers, upstream servers, or any other extension. With no manifest
|
|
17
|
+
* the loader returns empty everything and the gateway is byte-identical
|
|
18
|
+
* to the pre-extension world.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentId } from "../core/types.js";
|
|
21
|
+
import type { HostEventListener } from "./hostEventBus.js";
|
|
22
|
+
import type { TrellisProvider } from "./mcpRuntime.js";
|
|
23
|
+
/** Manifest location relative to the home directory — canonical, not
|
|
24
|
+
* user-home-adjacent: extension declarations are canonical content. */
|
|
25
|
+
export declare const EXTENSIONS_MANIFEST_REL: string;
|
|
26
|
+
export declare function extensionsManifestPath(homeDir: string): string;
|
|
27
|
+
export interface HostExtensionEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
/** Module specifier for dynamic import — an absolute path or a file:
|
|
30
|
+
* URL is the reliable form; bare specifiers resolve by Node's rules
|
|
31
|
+
* from the trellis package itself. */
|
|
32
|
+
load: string;
|
|
33
|
+
}
|
|
34
|
+
/** What an extension's `activate` receives. Metadata only — the extension
|
|
35
|
+
* never gets the bus or registry itself; it RETURNS providers and
|
|
36
|
+
* subscribers and the host wires them through the same registration
|
|
37
|
+
* surface as everything else. */
|
|
38
|
+
export interface HostExtensionContext {
|
|
39
|
+
homeDir: string;
|
|
40
|
+
agent: AgentId;
|
|
41
|
+
workspaceRoot: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface HostExtensionActivation {
|
|
44
|
+
providers?: TrellisProvider[];
|
|
45
|
+
subscribers?: HostEventListener[];
|
|
46
|
+
}
|
|
47
|
+
export type HostExtensionFailureStage = "manifest" | "invalid" | "import" | "activate";
|
|
48
|
+
export interface HostExtensionFailure {
|
|
49
|
+
/** The manifest entry id, or the manifest itself when it could not be
|
|
50
|
+
* read as an extension list at all. */
|
|
51
|
+
id: string;
|
|
52
|
+
stage: HostExtensionFailureStage;
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
export interface LoadedHostExtensions {
|
|
56
|
+
providers: TrellisProvider[];
|
|
57
|
+
subscribers: HostEventListener[];
|
|
58
|
+
failures: HostExtensionFailure[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load every extension the canonical manifest declares. Never throws:
|
|
62
|
+
* the worst case is a `failures` entry per broken extension.
|
|
63
|
+
*/
|
|
64
|
+
export declare function loadHostExtensions(context: HostExtensionContext, warn?: (message: string) => void): Promise<LoadedHostExtensions>;
|
|
65
|
+
/**
|
|
66
|
+
* 写入(或原位更新)一条清单条目;幂等——同 id 旧条目被替换,其余条目
|
|
67
|
+
* 与未知键原样保留。清单是 canonical 内容,写入是显式管理动作(进化
|
|
68
|
+
* 启用命令 8.1),loader 侧永不写。
|
|
69
|
+
*/
|
|
70
|
+
export declare function upsertExtensionEntry(homeDir: string, entry: HostExtensionEntry): void;
|
|
71
|
+
/**
|
|
72
|
+
* 移除一条清单条目;不存在时是无副作用的 no-op(返回 false)。不清空
|
|
73
|
+
* 文件、不动其他条目——清单还可能声明别的扩展。
|
|
74
|
+
*/
|
|
75
|
+
export declare function removeExtensionEntry(homeDir: string, id: string): boolean;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External host extension loading (host-extension-points spec, 8.6).
|
|
3
|
+
*
|
|
4
|
+
* The ONLY way an external module becomes a Trellis provider or event
|
|
5
|
+
* subscriber: it is declared in the canonical extension manifest at
|
|
6
|
+
* `~/.trellis/mcp/extensions.yaml` (entries carry `id` + a `load` module
|
|
7
|
+
* specifier) and its module exports `activate(ctx)` returning
|
|
8
|
+
* `{ providers?, subscribers? }`. There is no directory scan and no
|
|
9
|
+
* naming convention — a module on disk that the manifest does not name
|
|
10
|
+
* is never imported.
|
|
11
|
+
*
|
|
12
|
+
* Fault isolation is per extension and total: a manifest that fails to
|
|
13
|
+
* parse, an entry of the wrong shape, an import that throws, an
|
|
14
|
+
* `activate` that throws, or an activation of the wrong shape are each
|
|
15
|
+
* recorded as a failure (and warned), and none of them affect the core
|
|
16
|
+
* providers, upstream servers, or any other extension. With no manifest
|
|
17
|
+
* the loader returns empty everything and the gateway is byte-identical
|
|
18
|
+
* to the pre-extension world.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
23
|
+
import { writeFileAtomicSync } from "./fsAtomic.js";
|
|
24
|
+
/** Manifest location relative to the home directory — canonical, not
|
|
25
|
+
* user-home-adjacent: extension declarations are canonical content. */
|
|
26
|
+
export const EXTENSIONS_MANIFEST_REL = join(".trellis", "mcp", "extensions.yaml");
|
|
27
|
+
export function extensionsManifestPath(homeDir) {
|
|
28
|
+
return join(homeDir, EXTENSIONS_MANIFEST_REL);
|
|
29
|
+
}
|
|
30
|
+
function failureMessage(err) {
|
|
31
|
+
return err instanceof Error ? err.message : String(err);
|
|
32
|
+
}
|
|
33
|
+
/** Minimal structural check for one provider handed back by an
|
|
34
|
+
* extension: it must at least be able to answer a call. */
|
|
35
|
+
function isProviderShape(value) {
|
|
36
|
+
return (typeof value === "object" && value !== null &&
|
|
37
|
+
typeof value.id === "string" &&
|
|
38
|
+
typeof value.callTool === "function");
|
|
39
|
+
}
|
|
40
|
+
function isSubscriberShape(value) {
|
|
41
|
+
return typeof value === "function";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load every extension the canonical manifest declares. Never throws:
|
|
45
|
+
* the worst case is a `failures` entry per broken extension.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadHostExtensions(context, warn = console.error) {
|
|
48
|
+
const providers = [];
|
|
49
|
+
const subscribers = [];
|
|
50
|
+
const failures = [];
|
|
51
|
+
const report = (failure) => {
|
|
52
|
+
failures.push(failure);
|
|
53
|
+
warn(`trellis-mcp-gateway: extension "${failure.id}" failed (${failure.stage}): ${failure.message}`);
|
|
54
|
+
};
|
|
55
|
+
const manifestPath = extensionsManifestPath(context.homeDir);
|
|
56
|
+
if (!existsSync(manifestPath)) {
|
|
57
|
+
return { providers, subscribers, failures };
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = (parseYaml(readFileSync(manifestPath, "utf-8")) ?? {});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
report({ id: "<manifest>", stage: "manifest", message: failureMessage(err) });
|
|
65
|
+
return { providers, subscribers, failures };
|
|
66
|
+
}
|
|
67
|
+
const rawEntries = parsed.extensions;
|
|
68
|
+
if (rawEntries === undefined || rawEntries === null) {
|
|
69
|
+
return { providers, subscribers, failures };
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(rawEntries)) {
|
|
72
|
+
report({ id: "<manifest>", stage: "manifest", message: "extensions must be a list of {id, load} entries" });
|
|
73
|
+
return { providers, subscribers, failures };
|
|
74
|
+
}
|
|
75
|
+
for (const raw of rawEntries) {
|
|
76
|
+
const entry = raw;
|
|
77
|
+
if (typeof entry !== "object" || entry === null ||
|
|
78
|
+
typeof entry.id !== "string" || entry.id === "" ||
|
|
79
|
+
typeof entry.load !== "string" || entry.load === "") {
|
|
80
|
+
report({ id: "<manifest>", stage: "invalid", message: `entry is not a {id, load} pair: ${JSON.stringify(raw)}` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let mod;
|
|
84
|
+
try {
|
|
85
|
+
mod = await import(entry.load);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
report({ id: entry.id, stage: "import", message: failureMessage(err) });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (typeof mod.activate !== "function") {
|
|
92
|
+
report({ id: entry.id, stage: "invalid", message: "module does not export an activate function" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let activation;
|
|
96
|
+
try {
|
|
97
|
+
activation = (await mod.activate(context)) ?? {};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
report({ id: entry.id, stage: "activate", message: failureMessage(err) });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeof activation !== "object") {
|
|
104
|
+
report({ id: entry.id, stage: "invalid", message: "activate did not return an object" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const returnedProviders = Array.isArray(activation.providers) ? activation.providers : [];
|
|
108
|
+
for (const provider of returnedProviders) {
|
|
109
|
+
if (isProviderShape(provider)) {
|
|
110
|
+
providers.push(provider);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
report({ id: entry.id, stage: "invalid", message: "activation.providers contains a non-provider entry" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const returnedSubscribers = Array.isArray(activation.subscribers) ? activation.subscribers : [];
|
|
117
|
+
for (const subscriber of returnedSubscribers) {
|
|
118
|
+
if (isSubscriberShape(subscriber)) {
|
|
119
|
+
subscribers.push(subscriber);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
report({ id: entry.id, stage: "invalid", message: "activation.subscribers contains a non-function entry" });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { providers, subscribers, failures };
|
|
127
|
+
}
|
|
128
|
+
// ---------- 清单写入(声明面的唯一写路径) ----------
|
|
129
|
+
/**
|
|
130
|
+
* 写入(或原位更新)一条清单条目;幂等——同 id 旧条目被替换,其余条目
|
|
131
|
+
* 与未知键原样保留。清单是 canonical 内容,写入是显式管理动作(进化
|
|
132
|
+
* 启用命令 8.1),loader 侧永不写。
|
|
133
|
+
*/
|
|
134
|
+
export function upsertExtensionEntry(homeDir, entry) {
|
|
135
|
+
if (typeof entry.id !== "string" || entry.id === "" || typeof entry.load !== "string" || entry.load === "") {
|
|
136
|
+
throw new Error(`extensions.yaml: entry must be a non-empty {id, load} pair, got ${JSON.stringify(entry)}`);
|
|
137
|
+
}
|
|
138
|
+
const manifestPath = extensionsManifestPath(homeDir);
|
|
139
|
+
let parsed = {};
|
|
140
|
+
if (existsSync(manifestPath)) {
|
|
141
|
+
const raw = parseYaml(readFileSync(manifestPath, "utf-8"));
|
|
142
|
+
if (raw !== null && raw !== undefined) {
|
|
143
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
144
|
+
throw new Error("extensions.yaml: top level must be a mapping with an `extensions` list");
|
|
145
|
+
}
|
|
146
|
+
parsed = raw;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const existing = parsed.extensions;
|
|
150
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
151
|
+
// 非数组的 extensions 键是手误产物:抛错而不是静默重置,避免
|
|
152
|
+
// 其余条目无声丢失。
|
|
153
|
+
throw new Error("extensions.yaml: `extensions` must be a list of {id, load} entries");
|
|
154
|
+
}
|
|
155
|
+
const entries = (existing ?? []);
|
|
156
|
+
const next = [];
|
|
157
|
+
let replaced = false;
|
|
158
|
+
for (const raw of entries) {
|
|
159
|
+
const candidate = raw;
|
|
160
|
+
if (candidate !== null && typeof candidate === "object" && candidate.id === entry.id) {
|
|
161
|
+
next.push({ id: entry.id, load: entry.load });
|
|
162
|
+
replaced = true;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
next.push(raw);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!replaced)
|
|
169
|
+
next.push({ id: entry.id, load: entry.load });
|
|
170
|
+
parsed.extensions = next;
|
|
171
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
172
|
+
writeFileAtomicSync(manifestPath, stringifyYaml(parsed));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 移除一条清单条目;不存在时是无副作用的 no-op(返回 false)。不清空
|
|
176
|
+
* 文件、不动其他条目——清单还可能声明别的扩展。
|
|
177
|
+
*/
|
|
178
|
+
export function removeExtensionEntry(homeDir, id) {
|
|
179
|
+
const manifestPath = extensionsManifestPath(homeDir);
|
|
180
|
+
if (!existsSync(manifestPath))
|
|
181
|
+
return false;
|
|
182
|
+
const raw = parseYaml(readFileSync(manifestPath, "utf-8"));
|
|
183
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
184
|
+
return false;
|
|
185
|
+
const parsed = raw;
|
|
186
|
+
const existing = Array.isArray(parsed.extensions) ? parsed.extensions : [];
|
|
187
|
+
const next = existing.filter((item) => {
|
|
188
|
+
const candidate = item;
|
|
189
|
+
return !(candidate !== null && typeof candidate === "object" && candidate.id === id);
|
|
190
|
+
});
|
|
191
|
+
if (next.length === existing.length)
|
|
192
|
+
return false;
|
|
193
|
+
parsed.extensions = next;
|
|
194
|
+
writeFileAtomicSync(manifestPath, stringifyYaml(parsed));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
package/dist/lib/mcpConnect.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare function connectWithCleanup<T, TTransport extends {
|
|
|
45
45
|
connect(transport: TTransport): Promise<unknown>;
|
|
46
46
|
}, transport: TTransport, timeoutMs: number, message: string): Promise<T>;
|
|
47
47
|
export declare function connectStdio(def: McpServerDef, secretsPolicy: SecretsPolicy, timeoutMs: number, clientInfo: McpClientInfo): Promise<Client>;
|
|
48
|
-
export declare function resolveHeaders(def: McpServerDef, secretsPolicy: SecretsPolicy): Record<string, string> | undefined
|
|
48
|
+
export declare function resolveHeaders(def: McpServerDef, secretsPolicy: SecretsPolicy): Promise<Record<string, string> | undefined>;
|
|
49
49
|
export declare function connectHttp(def: McpServerDef, secretsPolicy: SecretsPolicy, timeoutMs: number, clientInfo: McpClientInfo, authProvider?: AuthProvider): Promise<Client>;
|
|
50
50
|
export declare function connectSse(def: McpServerDef, secretsPolicy: SecretsPolicy, timeoutMs: number, clientInfo: McpClientInfo, authProvider?: AuthProvider): Promise<Client>;
|
|
51
51
|
/**
|
package/dist/lib/mcpConnect.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Client, StreamableHTTPClientTransport, SSEClientTransport } from "@modelcontextprotocol/client";
|
|
16
16
|
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
|
|
17
|
-
import { resolveSecretEnv } from "./secretEnv.js";
|
|
17
|
+
import { deliveredName, resolveSecretEnv } from "./secretEnv.js";
|
|
18
18
|
import { extractTemplateVarNames } from "./envVarNames.js";
|
|
19
19
|
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
20
20
|
const FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1_000;
|
|
@@ -73,9 +73,20 @@ export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
73
73
|
// `${sourceName}` placeholder text a consumer with no `${VAR}` runtime
|
|
74
74
|
// of its own (like this bridge) would otherwise crash on parsing
|
|
75
75
|
// (trellis-migrate-env-var-alias, the real notion-on-pi bug).
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
//
|
|
77
|
+
// Entries may be `mxenv://` references: the resolved map is keyed by
|
|
78
|
+
// DELIVERED name (the `KEY` segment for a reference, the entry itself
|
|
79
|
+
// for a bare name), and the spawned server receives values under
|
|
80
|
+
// delivered names — never the raw reference text as a variable name
|
|
81
|
+
// (mx-env-secret-source spec).
|
|
82
|
+
const resolved = await resolveSecretEnv([...(def.env ?? []), ...Object.values(envAliases)], secretsPolicy);
|
|
83
|
+
const namedEnv = Object.fromEntries((def.env ?? []).map((entry) => {
|
|
84
|
+
const delivered = deliveredName(entry);
|
|
85
|
+
return [delivered, resolved[delivered] ?? ""];
|
|
86
|
+
}));
|
|
87
|
+
const aliasEnv = Object.fromEntries(Object.entries(envAliases).map(([targetKey, sourceName]) => {
|
|
88
|
+
return [targetKey, resolved[deliveredName(sourceName)] ?? ""];
|
|
89
|
+
}));
|
|
79
90
|
const transport = new StdioClientTransport({
|
|
80
91
|
command: def.command,
|
|
81
92
|
args: def.args,
|
|
@@ -91,11 +102,11 @@ export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
91
102
|
});
|
|
92
103
|
return connectWithCleanup(client, transport, timeoutMs, `connect timed out after ${timeoutMs}ms`);
|
|
93
104
|
}
|
|
94
|
-
export function resolveHeaders(def, secretsPolicy) {
|
|
105
|
+
export async function resolveHeaders(def, secretsPolicy) {
|
|
95
106
|
if (!def.headers || Object.keys(def.headers).length === 0)
|
|
96
107
|
return undefined;
|
|
97
108
|
const names = Object.keys(def.headers).flatMap((key) => extractTemplateVarNames(def.headers[key]));
|
|
98
|
-
const resolved = resolveSecretEnv(names, secretsPolicy);
|
|
109
|
+
const resolved = await resolveSecretEnv(names, secretsPolicy);
|
|
99
110
|
const result = {};
|
|
100
111
|
for (const [key, template] of Object.entries(def.headers)) {
|
|
101
112
|
result[key] = template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => resolved[name] ?? "");
|
|
@@ -104,7 +115,7 @@ export function resolveHeaders(def, secretsPolicy) {
|
|
|
104
115
|
}
|
|
105
116
|
export async function connectHttp(def, secretsPolicy, timeoutMs, clientInfo, authProvider) {
|
|
106
117
|
const client = new Client(clientInfo, { capabilities: {}, versionNegotiation: { mode: "auto", probe: { timeoutMs } } });
|
|
107
|
-
const headers = resolveHeaders(def, secretsPolicy);
|
|
118
|
+
const headers = await resolveHeaders(def, secretsPolicy);
|
|
108
119
|
// authProvider is the SDK's per-request credential seam: `token()` runs
|
|
109
120
|
// as each request's headers are assembled and its result overrides
|
|
110
121
|
// `requestInit`'s Authorization — the alternative to burning a Bearer
|
|
@@ -118,7 +129,7 @@ export async function connectHttp(def, secretsPolicy, timeoutMs, clientInfo, aut
|
|
|
118
129
|
export async function connectSse(def, secretsPolicy, timeoutMs, clientInfo, authProvider) {
|
|
119
130
|
// HTTP+SSE is a legacy transport; it cannot carry modern discovery.
|
|
120
131
|
const client = new Client(clientInfo, { capabilities: {} });
|
|
121
|
-
const headers = resolveHeaders(def, secretsPolicy);
|
|
132
|
+
const headers = await resolveHeaders(def, secretsPolicy);
|
|
122
133
|
const opts = {
|
|
123
134
|
...(headers ? { requestInit: { headers } } : {}),
|
|
124
135
|
...(authProvider ? { authProvider } : {}),
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { parseEnvEntry } from "./secretEnv.js";
|
|
16
17
|
import { readJsonFile } from "./probeCommon.js";
|
|
17
18
|
import { readServerEnvTable } from "./tomlSection.js";
|
|
18
19
|
import { resolveZcodeProfile } from "../probes/zcode.js";
|
|
@@ -42,8 +43,19 @@ const VAR_REF_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
|
42
43
|
* independent derivations of it.
|
|
43
44
|
*/
|
|
44
45
|
export function resolvedEnvTextMap(def) {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// `mxenv://` reference entries render as `${KEY}` under their DELIVERED
|
|
47
|
+
// name (the KEY segment) — the raw reference text never appears as a
|
|
48
|
+
// variable name in a native-client config (mx-env-secret-source spec).
|
|
49
|
+
const nameRefs = Object.fromEntries((def.env ?? []).map((entry) => {
|
|
50
|
+
const parsed = parseEnvEntry(entry);
|
|
51
|
+
const delivered = parsed.kind === "invalid" ? entry : parsed.delivered;
|
|
52
|
+
return [delivered, `\${${delivered}}`];
|
|
53
|
+
}));
|
|
54
|
+
const aliasRefs = Object.fromEntries(Object.entries(def.envAliases ?? {}).map(([targetKey, source]) => {
|
|
55
|
+
const parsed = parseEnvEntry(source);
|
|
56
|
+
const delivered = parsed.kind === "invalid" ? source : parsed.delivered;
|
|
57
|
+
return [targetKey, `\${${delivered}}`];
|
|
58
|
+
}));
|
|
47
59
|
return { ...nameRefs, ...aliasRefs, ...(def.staticEnv ?? {}) };
|
|
48
60
|
}
|
|
49
61
|
function splitJsonEnvMap(env) {
|
package/dist/lib/mcpRuntime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResou
|
|
|
3
3
|
import type { AgentId } from "../core/types.js";
|
|
4
4
|
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
5
5
|
import type { SkillEntry } from "./skillManifest.js";
|
|
6
|
+
import type { ToolCallInfo } from "../host/hostEvents.js";
|
|
6
7
|
export interface RuntimeContext {
|
|
7
8
|
agentId: AgentId;
|
|
8
9
|
homeDir: string;
|
|
@@ -23,14 +24,24 @@ export interface TrellisProvider {
|
|
|
23
24
|
export declare class BuiltinRegistry {
|
|
24
25
|
private readonly providers;
|
|
25
26
|
private readonly warn;
|
|
27
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
28
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
29
|
+
private readonly onToolCall?;
|
|
26
30
|
private readonly toolOwners;
|
|
27
31
|
private readonly originalToolOwners;
|
|
28
32
|
private readonly exposedToOriginal;
|
|
29
33
|
private readonly resourceOwners;
|
|
30
34
|
private readonly promptOwners;
|
|
31
|
-
constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void
|
|
35
|
+
constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void,
|
|
36
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
37
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
38
|
+
onToolCall?: ((info: ToolCallInfo) => void) | undefined);
|
|
32
39
|
listTools(context: RuntimeContext): Promise<Tool[]>;
|
|
33
40
|
callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
|
|
41
|
+
/** Metadata-only event emission at the call choke point. Fully
|
|
42
|
+
* transparent: a failing observer never changes the tool call's result or
|
|
43
|
+
* error semantics (host-events transparency requirement). */
|
|
44
|
+
private emitToolCall;
|
|
34
45
|
listResources(context: RuntimeContext): Promise<Resource[]>;
|
|
35
46
|
readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
|
|
36
47
|
listSkills(context: RuntimeContext): Promise<SkillEntry[]>;
|
package/dist/lib/mcpRuntime.js
CHANGED
|
@@ -8,17 +8,32 @@ function compactBuiltinToolName(name) {
|
|
|
8
8
|
function providerError(provider, error) {
|
|
9
9
|
return `trellis-mcp-runtime: provider "${provider}" failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
10
10
|
}
|
|
11
|
+
function resultCategory(result) {
|
|
12
|
+
if (!result?.isError)
|
|
13
|
+
return "ok";
|
|
14
|
+
const text = JSON.stringify(result.content ?? "");
|
|
15
|
+
return /timed? ?out/i.test(text) ? "timeout" : "error";
|
|
16
|
+
}
|
|
17
|
+
function thrownCategory(err) {
|
|
18
|
+
const message = err instanceof Error ? `${err.name} ${err.message}` : String(err);
|
|
19
|
+
return /timed? ?out/i.test(message) ? "timeout" : "error";
|
|
20
|
+
}
|
|
11
21
|
export class BuiltinRegistry {
|
|
12
22
|
providers;
|
|
13
23
|
warn;
|
|
24
|
+
onToolCall;
|
|
14
25
|
toolOwners = new Map();
|
|
15
26
|
originalToolOwners = new Map();
|
|
16
27
|
exposedToOriginal = new Map();
|
|
17
28
|
resourceOwners = new Map();
|
|
18
29
|
promptOwners = new Map();
|
|
19
|
-
constructor(providers, warn = console.error
|
|
30
|
+
constructor(providers, warn = console.error,
|
|
31
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
32
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
33
|
+
onToolCall) {
|
|
20
34
|
this.providers = providers;
|
|
21
35
|
this.warn = warn;
|
|
36
|
+
this.onToolCall = onToolCall;
|
|
22
37
|
}
|
|
23
38
|
async listTools(context) {
|
|
24
39
|
const candidates = [];
|
|
@@ -67,13 +82,30 @@ export class BuiltinRegistry {
|
|
|
67
82
|
}
|
|
68
83
|
if (!provider)
|
|
69
84
|
return { content: [{ type: "text", text: `unknown Trellis tool "${name}"` }], isError: true };
|
|
85
|
+
const started = Date.now();
|
|
70
86
|
try {
|
|
71
|
-
|
|
87
|
+
const result = await provider.callTool(originalName, args, context);
|
|
88
|
+
this.emitToolCall({ provider: provider.id, tool: name, durationMs: Date.now() - started, result: resultCategory(result) });
|
|
89
|
+
return result;
|
|
72
90
|
}
|
|
73
91
|
catch (err) {
|
|
92
|
+
this.emitToolCall({ provider: provider.id, tool: name, durationMs: Date.now() - started, result: thrownCategory(err) });
|
|
74
93
|
return { content: [{ type: "text", text: providerError(provider.id, err) }], isError: true };
|
|
75
94
|
}
|
|
76
95
|
}
|
|
96
|
+
/** Metadata-only event emission at the call choke point. Fully
|
|
97
|
+
* transparent: a failing observer never changes the tool call's result or
|
|
98
|
+
* error semantics (host-events transparency requirement). */
|
|
99
|
+
emitToolCall(info) {
|
|
100
|
+
if (!this.onToolCall)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
this.onToolCall(info);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
this.warn(`trellis-mcp-runtime: tool-call observer failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
77
109
|
async listResources(context) {
|
|
78
110
|
const resources = [];
|
|
79
111
|
this.resourceOwners.clear();
|
|
@@ -52,6 +52,17 @@ export declare function prefixedToolName(serverName: string, toolName: string):
|
|
|
52
52
|
export interface ToolNameCandidate {
|
|
53
53
|
serverName: string;
|
|
54
54
|
toolName: string;
|
|
55
|
+
/**
|
|
56
|
+
* Force the `server__tool` prefix regardless of uniqueness or length.
|
|
57
|
+
* Gateway aggregation sets this on every candidate so an Agent can always
|
|
58
|
+
* read the owning server from the exposed name (mcp-tool-name-preservation:
|
|
59
|
+
* "Prefix every upstream tool with its server name"). A tool whose name
|
|
60
|
+
* already starts with its server's name (e.g. `tanka_memo_search` on
|
|
61
|
+
* `tanka`) is not prefixed again — the name is already source-qualified.
|
|
62
|
+
* Other consumers (e.g. the built-in Runtime registry) leave it unset to
|
|
63
|
+
* keep their own naming contracts.
|
|
64
|
+
*/
|
|
65
|
+
alwaysPrefix?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/** Allocate all exposed names together so normalization collisions are
|
|
57
68
|
* deterministic and no upstream tool is silently lost. The returned array
|
|
@@ -94,8 +105,13 @@ export declare class McpToolRegistry {
|
|
|
94
105
|
replace(serverName: string, upstream: ToolUpstream, tools: readonly AggregatedTool[]): AddResult;
|
|
95
106
|
private exposedNameFor;
|
|
96
107
|
/** Every registered tool, under its prefixed name, in registration
|
|
97
|
-
* order. Each tool's own `
|
|
98
|
-
*
|
|
108
|
+
* order. Each tool's own `inputSchema` and any additional fields the
|
|
109
|
+
* upstream sent are preserved verbatim; `name` changes, and the
|
|
110
|
+
* description gains a trailing `Source: <server>` line so an Agent can
|
|
111
|
+
* confirm the owning server without relying on the name alone
|
|
112
|
+
* (mcp-gateway-hosting: "Resold tool metadata identifies its upstream
|
|
113
|
+
* source"). The upstream's own description text is never altered or
|
|
114
|
+
* removed — the source line is appended. */
|
|
99
115
|
listTools(): AggregatedTool[];
|
|
100
116
|
/** The owning upstream and original tool name for a prefixed name, or
|
|
101
117
|
* `undefined` if nothing owns it. */
|