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.
Files changed (127) hide show
  1. package/dist/adapters/claude-code.js +2 -2
  2. package/dist/adapters/codex.js +3 -3
  3. package/dist/adapters/jsonMcp.d.ts +1 -1
  4. package/dist/adapters/jsonMcp.js +2 -2
  5. package/dist/adapters/kimi-code.js +2 -2
  6. package/dist/adapters/kiro.js +11 -9
  7. package/dist/adapters/mcpPlan.d.ts +1 -1
  8. package/dist/adapters/mcpPlan.js +35 -12
  9. package/dist/adapters/pi.js +5 -5
  10. package/dist/adapters/zcode.d.ts +1 -1
  11. package/dist/adapters/zcode.js +6 -6
  12. package/dist/cli.js +12 -1
  13. package/dist/commands/doctor.d.ts +9 -1
  14. package/dist/commands/doctor.js +92 -7
  15. package/dist/commands/evolve.d.ts +43 -0
  16. package/dist/commands/evolve.js +479 -0
  17. package/dist/commands/mcp.js +1 -1
  18. package/dist/commands/mcpAuth.js +1 -1
  19. package/dist/commands/mcpGateway.d.ts +30 -2
  20. package/dist/commands/mcpGateway.js +44 -10
  21. package/dist/commands/mcpImport.js +2 -1
  22. package/dist/commands/secretsAudit.d.ts +1 -1
  23. package/dist/commands/secretsAudit.js +36 -13
  24. package/dist/evolve/agentView.d.ts +32 -0
  25. package/dist/evolve/agentView.js +34 -0
  26. package/dist/evolve/apply.d.ts +71 -0
  27. package/dist/evolve/apply.js +162 -0
  28. package/dist/evolve/archive.d.ts +23 -0
  29. package/dist/evolve/archive.js +58 -0
  30. package/dist/evolve/erase.d.ts +17 -0
  31. package/dist/evolve/erase.js +65 -0
  32. package/dist/evolve/extension.d.ts +37 -0
  33. package/dist/evolve/extension.js +63 -0
  34. package/dist/evolve/gate.d.ts +95 -0
  35. package/dist/evolve/gate.js +295 -0
  36. package/dist/evolve/importHistory.d.ts +91 -0
  37. package/dist/evolve/importHistory.js +239 -0
  38. package/dist/evolve/jobMain.d.ts +22 -0
  39. package/dist/evolve/jobMain.js +52 -0
  40. package/dist/evolve/jobRun.d.ts +94 -0
  41. package/dist/evolve/jobRun.js +273 -0
  42. package/dist/evolve/jobShell.d.ts +46 -0
  43. package/dist/evolve/jobShell.js +75 -0
  44. package/dist/evolve/l2.d.ts +93 -0
  45. package/dist/evolve/l2.js +81 -0
  46. package/dist/evolve/lesson.d.ts +57 -0
  47. package/dist/evolve/lesson.js +97 -0
  48. package/dist/evolve/pending.d.ts +69 -0
  49. package/dist/evolve/pending.js +116 -0
  50. package/dist/evolve/probeWorkspace.d.ts +57 -0
  51. package/dist/evolve/probeWorkspace.js +69 -0
  52. package/dist/evolve/propose.d.ts +73 -0
  53. package/dist/evolve/propose.js +136 -0
  54. package/dist/evolve/recorder.d.ts +68 -0
  55. package/dist/evolve/recorder.js +169 -0
  56. package/dist/evolve/report.d.ts +26 -0
  57. package/dist/evolve/report.js +37 -0
  58. package/dist/evolve/reportView.d.ts +96 -0
  59. package/dist/evolve/reportView.js +311 -0
  60. package/dist/evolve/risk.d.ts +106 -0
  61. package/dist/evolve/risk.js +224 -0
  62. package/dist/evolve/sources.d.ts +19 -0
  63. package/dist/evolve/sources.js +29 -0
  64. package/dist/evolve/stateDir.d.ts +21 -0
  65. package/dist/evolve/stateDir.js +56 -0
  66. package/dist/evolve/surface.d.ts +34 -0
  67. package/dist/evolve/surface.js +72 -0
  68. package/dist/evolve/trajectory.d.ts +25 -0
  69. package/dist/evolve/trajectory.js +88 -0
  70. package/dist/evolve/treeToCanonical.d.ts +28 -0
  71. package/dist/evolve/treeToCanonical.js +79 -0
  72. package/dist/evolve/trellisSubstrate.d.ts +65 -0
  73. package/dist/evolve/trellisSubstrate.js +154 -0
  74. package/dist/evolve/trigger.d.ts +177 -0
  75. package/dist/evolve/trigger.js +400 -0
  76. package/dist/evolve/types.d.ts +126 -0
  77. package/dist/evolve/types.js +15 -0
  78. package/dist/evolve/validate.d.ts +139 -0
  79. package/dist/evolve/validate.js +315 -0
  80. package/dist/host/checks.d.ts +57 -0
  81. package/dist/host/checks.js +126 -0
  82. package/dist/host/delivery.d.ts +42 -0
  83. package/dist/host/delivery.js +59 -0
  84. package/dist/host/edits.d.ts +87 -0
  85. package/dist/host/edits.js +102 -0
  86. package/dist/host/hostEvents.d.ts +74 -0
  87. package/dist/host/hostEvents.js +45 -0
  88. package/dist/host/index.d.ts +22 -0
  89. package/dist/host/index.js +17 -0
  90. package/dist/host/levels.d.ts +106 -0
  91. package/dist/host/levels.js +206 -0
  92. package/dist/host/staging.d.ts +56 -0
  93. package/dist/host/staging.js +106 -0
  94. package/dist/host/surface.d.ts +36 -0
  95. package/dist/host/surface.js +20 -0
  96. package/dist/lib/backup.js +12 -3
  97. package/dist/lib/envVarNames.js +7 -0
  98. package/dist/lib/fsAtomic.d.ts +7 -0
  99. package/dist/lib/fsAtomic.js +14 -0
  100. package/dist/lib/hostEventBus.d.ts +47 -0
  101. package/dist/lib/hostEventBus.js +94 -0
  102. package/dist/lib/hostExtensions.d.ts +75 -0
  103. package/dist/lib/hostExtensions.js +196 -0
  104. package/dist/lib/mcpConnect.d.ts +1 -1
  105. package/dist/lib/mcpConnect.js +19 -8
  106. package/dist/lib/mcpMigrateRead.js +14 -2
  107. package/dist/lib/mcpRuntime.d.ts +12 -1
  108. package/dist/lib/mcpRuntime.js +34 -2
  109. package/dist/lib/mcpToolRegistry.d.ts +18 -2
  110. package/dist/lib/mcpToolRegistry.js +29 -9
  111. package/dist/lib/secretEnv.d.ts +53 -8
  112. package/dist/lib/secretEnv.js +78 -15
  113. package/dist/lib/tomlSection.js +3 -2
  114. package/dist/lib/workspaceDoctor.d.ts +1 -1
  115. package/dist/lib/workspaceDoctor.js +8 -4
  116. package/dist/lib/workspaceMcp.d.ts +1 -1
  117. package/dist/lib/workspaceMcp.js +5 -5
  118. package/dist/pi-bridge/bundle.js +42236 -2701
  119. package/dist/pi-bridge/index.js +2 -2
  120. package/docs/magpie-gateway-research.md +422 -0
  121. package/docs/research.md +4 -1
  122. package/docs/roadmap.md +22 -12
  123. package/docs/runtime-infra.md +41 -22
  124. package/docs/self-evolution.md +138 -44
  125. package/package.json +8 -2
  126. package/schema/secrets.policy.example.yaml +5 -0
  127. 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
+ }
@@ -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
  /**
@@ -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
- const resolved = resolveSecretEnv([...(def.env ?? []), ...Object.values(envAliases)], secretsPolicy);
77
- const namedEnv = Object.fromEntries((def.env ?? []).map((name) => [name, resolved[name] ?? ""]));
78
- const aliasEnv = Object.fromEntries(Object.entries(envAliases).map(([targetKey, sourceName]) => [targetKey, resolved[sourceName] ?? ""]));
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
- const nameRefs = Object.fromEntries((def.env ?? []).map((name) => [name, `\${${name}}`]));
46
- const aliasRefs = Object.fromEntries(Object.entries(def.envAliases ?? {}).map(([targetKey, sourceName]) => [targetKey, `\${${sourceName}}`]));
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) {
@@ -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[]>;
@@ -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
- return await provider.callTool(originalName, args, context);
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 `description`/`inputSchema` and any additional
98
- * fields the upstream sent are preserved verbatim; only `name` changes. */
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. */