agent-trellis 0.20.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/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mcpAuth.js +1 -1
- package/dist/commands/mcpGateway.d.ts +2 -2
- package/dist/commands/mcpGateway.js +4 -4
- package/dist/commands/mcpImport.js +2 -1
- package/dist/commands/secretsAudit.d.ts +1 -1
- package/dist/commands/secretsAudit.js +36 -13
- package/dist/lib/envVarNames.js +7 -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/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 +42220 -2695
- package/dist/pi-bridge/index.js +2 -2
- package/docs/research.md +4 -1
- package/package.json +2 -1
- package/schema/secrets.policy.example.yaml +5 -0
- package/schema/servers.example.yaml +5 -1
|
@@ -48,10 +48,10 @@ export class ClaudeCodeAdapter {
|
|
|
48
48
|
canonicalRoot,
|
|
49
49
|
kind: "instructions",
|
|
50
50
|
});
|
|
51
|
-
const mcpItems = this.planMcp(canonical);
|
|
51
|
+
const mcpItems = await this.planMcp(canonical);
|
|
52
52
|
return [...skillItems, ...instructionsItems, ...mcpItems];
|
|
53
53
|
}
|
|
54
|
-
planMcp(canonical) {
|
|
54
|
+
async planMcp(canonical) {
|
|
55
55
|
const configPath = join(this.homeDir, ".claude.json");
|
|
56
56
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
57
57
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
package/dist/adapters/codex.js
CHANGED
|
@@ -50,7 +50,7 @@ export class CodexAdapter {
|
|
|
50
50
|
canonicalRoot: join(canonicalRoot, "skills"),
|
|
51
51
|
kind: "skill",
|
|
52
52
|
});
|
|
53
|
-
const mcpItems = this.planMcp(canonical);
|
|
53
|
+
const mcpItems = await this.planMcp(canonical);
|
|
54
54
|
const configuredInstructionsPath = readInstructionsPath(join(this.homeDir, ".codex", "config.toml"));
|
|
55
55
|
if (!configuredInstructionsPath) {
|
|
56
56
|
// Codex has no `instructions` key set — not Trellis's to decide.
|
|
@@ -65,10 +65,10 @@ export class CodexAdapter {
|
|
|
65
65
|
});
|
|
66
66
|
return [...skillItems, ...instructionsItems, ...mcpItems];
|
|
67
67
|
}
|
|
68
|
-
planMcp(canonical) {
|
|
68
|
+
async planMcp(canonical) {
|
|
69
69
|
const configTomlPath = join(this.homeDir, ".codex", "config.toml");
|
|
70
70
|
const content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
|
|
71
|
-
const { desired, conflicts } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
71
|
+
const { desired, conflicts } = await resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
72
72
|
const items = [];
|
|
73
73
|
for (const { name, def } of desired) {
|
|
74
74
|
const current = currentServerSectionText(content, name);
|
|
@@ -29,7 +29,7 @@ export declare function planJsonMcp(opts: {
|
|
|
29
29
|
* zero removal candidates. */
|
|
30
30
|
ownership?: Record<string, unknown>;
|
|
31
31
|
render?: JsonMcpEntryRenderer;
|
|
32
|
-
}): AdapterPlanItem[]
|
|
32
|
+
}): Promise<AdapterPlanItem[]>;
|
|
33
33
|
/** Merges every `"create"` MCP item into `parsed` (or a fresh `{}` if the
|
|
34
34
|
* file didn't exist) and deletes every `"remove"` item's key, returning
|
|
35
35
|
* the object to stringify — every sibling top-level key on `parsed` is
|
package/dist/adapters/jsonMcp.js
CHANGED
|
@@ -30,10 +30,10 @@ export function renderJsonServerEntry(def) {
|
|
|
30
30
|
}
|
|
31
31
|
return entry;
|
|
32
32
|
}
|
|
33
|
-
export function planJsonMcp(opts) {
|
|
33
|
+
export async function planJsonMcp(opts) {
|
|
34
34
|
const { configPath, parsed, mcp, agentId, managedAgents, policy, ownership } = opts;
|
|
35
35
|
const render = opts.render ?? ((def, _name) => renderJsonServerEntry(def));
|
|
36
|
-
const { desired, conflicts } = resolveMcpPlan(agentId, mcp, managedAgents, policy);
|
|
36
|
+
const { desired, conflicts } = await resolveMcpPlan(agentId, mcp, managedAgents, policy);
|
|
37
37
|
const existingServers = parsed?.mcpServers ?? {};
|
|
38
38
|
const items = [];
|
|
39
39
|
for (const { name, def } of desired) {
|
|
@@ -63,7 +63,7 @@ export class KimiCodeAdapter {
|
|
|
63
63
|
const configPath = join(this.homeDir, ".kimi-code", "mcp.json");
|
|
64
64
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
65
65
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
66
|
-
items.push(...planJsonMcp({
|
|
66
|
+
items.push(...(await planJsonMcp({
|
|
67
67
|
configPath,
|
|
68
68
|
parsed,
|
|
69
69
|
mcp: globalLayerMcp(canonical),
|
|
@@ -72,7 +72,7 @@ export class KimiCodeAdapter {
|
|
|
72
72
|
policy: canonical.secretsPolicy,
|
|
73
73
|
ownership,
|
|
74
74
|
render: renderKimiServerEntry,
|
|
75
|
-
}));
|
|
75
|
+
})));
|
|
76
76
|
return items;
|
|
77
77
|
}
|
|
78
78
|
async apply(plan, backup) {
|
package/dist/adapters/kiro.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
|
+
import { deliveredName } from "../lib/secretEnv.js";
|
|
21
22
|
import { isInScope } from "../core/adapter.js";
|
|
22
23
|
import { usesNativeCapabilityDelivery } from "../core/types.js";
|
|
23
24
|
import * as kiroProbe from "../probes/kiro.js";
|
|
@@ -64,11 +65,11 @@ export class KiroAdapter {
|
|
|
64
65
|
canonicalRoot,
|
|
65
66
|
kind: "instructions",
|
|
66
67
|
});
|
|
67
|
-
const mcpItems = this.planMcp(canonical);
|
|
68
|
-
const approvedEnvVarsItems = this.planApprovedEnvVars(canonical);
|
|
68
|
+
const mcpItems = await this.planMcp(canonical);
|
|
69
|
+
const approvedEnvVarsItems = await this.planApprovedEnvVars(canonical);
|
|
69
70
|
return [...skillItems, ...instructionsItems, ...mcpItems, ...approvedEnvVarsItems];
|
|
70
71
|
}
|
|
71
|
-
planMcp(canonical) {
|
|
72
|
+
async planMcp(canonical) {
|
|
72
73
|
const configPath = join(this.homeDir, ".kiro", "settings", "mcp.json");
|
|
73
74
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
74
75
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
@@ -77,17 +78,18 @@ export class KiroAdapter {
|
|
|
77
78
|
/** Names come from `resolveMcpPlan`, not a raw scan of `canonical.mcp.servers`
|
|
78
79
|
* (design.md D2) — a server scoped away from Kiro, or refused for a
|
|
79
80
|
* known_host_injected collision, never contributes a name here either. */
|
|
80
|
-
desiredApprovedEnvVars(canonical) {
|
|
81
|
-
const { desired } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
81
|
+
async desiredApprovedEnvVars(canonical) {
|
|
82
|
+
const { desired } = await resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
82
83
|
const names = new Set();
|
|
83
84
|
for (const { def } of desired) {
|
|
85
|
+
// Approved list holds DELIVERED names (what renderers write), not raw entries.
|
|
84
86
|
for (const name of declaredEnvNames(def))
|
|
85
|
-
names.add(name);
|
|
87
|
+
names.add(deliveredName(name));
|
|
86
88
|
}
|
|
87
89
|
return [...names];
|
|
88
90
|
}
|
|
89
|
-
planApprovedEnvVars(canonical) {
|
|
90
|
-
const desired = this.desiredApprovedEnvVars(canonical);
|
|
91
|
+
async planApprovedEnvVars(canonical) {
|
|
92
|
+
const desired = await this.desiredApprovedEnvVars(canonical);
|
|
91
93
|
if (desired.length === 0) {
|
|
92
94
|
return [];
|
|
93
95
|
}
|
|
@@ -188,7 +190,7 @@ export class KiroAdapter {
|
|
|
188
190
|
return [];
|
|
189
191
|
}
|
|
190
192
|
})();
|
|
191
|
-
for (const name of this.desiredApprovedEnvVars(canonical)) {
|
|
193
|
+
for (const name of await this.desiredApprovedEnvVars(canonical)) {
|
|
192
194
|
if (!approved.includes(name)) {
|
|
193
195
|
mismatches.push(`env var "${name}" is needed by a canonical MCP server for kiro but missing from ${APPROVED_ENV_VARS_KEY}`);
|
|
194
196
|
}
|
|
@@ -91,4 +91,4 @@ export interface LiteralSecretMatch {
|
|
|
91
91
|
* as before this function reported a field at all.
|
|
92
92
|
*/
|
|
93
93
|
export declare function findLiteralSecret(def: McpServerDef): LiteralSecretMatch | undefined;
|
|
94
|
-
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy): McpPlanResult
|
|
94
|
+
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy): Promise<McpPlanResult>;
|
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -20,7 +20,7 @@ import { isInScope } from "../core/adapter.js";
|
|
|
20
20
|
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
21
21
|
import { isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
22
22
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
23
|
-
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
23
|
+
import { parseEnvEntry, resolveSecretEnv } from "../lib/secretEnv.js";
|
|
24
24
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
25
25
|
export const GATEWAY_ENTRY_NAME = "trellis";
|
|
26
26
|
export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
|
|
@@ -138,14 +138,25 @@ export function findLiteralSecret(def) {
|
|
|
138
138
|
* the target key it would have been written under
|
|
139
139
|
* (trellis-migrate-env-var-alias).
|
|
140
140
|
*/
|
|
141
|
-
function findUnresolvedEnvName(def, policy) {
|
|
141
|
+
async function findUnresolvedEnvName(def, policy) {
|
|
142
142
|
const names = [...(def.env ?? []), ...Object.values(def.envAliases ?? {})];
|
|
143
143
|
if (names.length === 0)
|
|
144
144
|
return undefined;
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// A malformed `mxenv://` entry conflicts here too — it is rejected input,
|
|
146
|
+
// never a silently-resolved bare name (mx-env-secret-source D6). The
|
|
147
|
+
// returned text is the DECLARED entry; resolution checks use delivered
|
|
148
|
+
// names (the `KEY` segment for a reference).
|
|
149
|
+
const parsed = names.map(parseEnvEntry);
|
|
150
|
+
const invalid = parsed.find((p) => p.kind === "invalid");
|
|
151
|
+
if (invalid)
|
|
152
|
+
return invalid.entry;
|
|
153
|
+
const resolved = await resolveSecretEnv(names, policy);
|
|
154
|
+
return names.find((name) => {
|
|
155
|
+
const p = parseEnvEntry(name);
|
|
156
|
+
return !resolved[p.kind === "invalid" ? name : p.delivered];
|
|
157
|
+
});
|
|
147
158
|
}
|
|
148
|
-
export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
159
|
+
export async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
149
160
|
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
150
161
|
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
151
162
|
// Checked before `hub`: only one entry can be "the" MCP entry for an
|
|
@@ -161,7 +172,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
161
172
|
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
|
-
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
175
|
+
const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
165
176
|
// Every ordinary per-server check (enabled, scope, literal secrets,
|
|
166
177
|
// unresolved env names, Codex's header shape) still runs at gateway
|
|
167
178
|
// startup. OAuth servers are the deliberate exception: they are written
|
|
@@ -237,7 +248,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
237
248
|
// Runtime mode owns the agent-facing MCP edge. It resolves the same
|
|
238
249
|
// direct/gateway-compatible upstream route at startup, so native
|
|
239
250
|
// per-server entries are not duplicated here.
|
|
240
|
-
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
251
|
+
const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
241
252
|
return {
|
|
242
253
|
desired: [{
|
|
243
254
|
name: RUNTIME_ENTRY_NAME,
|
|
@@ -246,14 +257,14 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
246
257
|
conflicts: oauth.conflicts,
|
|
247
258
|
};
|
|
248
259
|
}
|
|
249
|
-
const native = planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
260
|
+
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
250
261
|
const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
|
|
251
262
|
return {
|
|
252
263
|
desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
|
|
253
264
|
conflicts: [...native.conflicts, ...hosted.map(({ name }) => ({ name, message: `Trellis-owned OAuth server "${name}" requires gateway or runtime delivery for ${agentId}`, remediation: "enable gateway/runtime delivery or set auth.owner to agent" }))],
|
|
254
265
|
};
|
|
255
266
|
}
|
|
256
|
-
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
267
|
+
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
257
268
|
const desired = [];
|
|
258
269
|
const conflicts = [];
|
|
259
270
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
@@ -306,13 +317,25 @@ function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
|
306
317
|
});
|
|
307
318
|
continue;
|
|
308
319
|
}
|
|
309
|
-
const unresolvedName = findUnresolvedEnvName(def, policy);
|
|
320
|
+
const unresolvedName = await findUnresolvedEnvName(def, policy);
|
|
310
321
|
if (unresolvedName) {
|
|
311
|
-
const
|
|
322
|
+
const entry = parseEnvEntry(unresolvedName);
|
|
323
|
+
if (entry.kind === "invalid") {
|
|
324
|
+
conflicts.push({
|
|
325
|
+
name,
|
|
326
|
+
message: `refusing to write MCP server "${name}": its declared env entry "${unresolvedName}" is not a valid mxenv://<profile>/<KEY> reference (mx-env-secret-source)`,
|
|
327
|
+
remediation: `fix the entry in servers.yaml to the mxenv://<profile>/<KEY> form or a plain variable name, then re-run mcp sync`,
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const source = entry.kind === "ref"
|
|
332
|
+
? `the mxenv composition (profile "${entry.profile}")`
|
|
333
|
+
: (policy.envFile ?? "process environment");
|
|
334
|
+
const remediationTarget = entry.kind === "ref" ? entry.key : unresolvedName;
|
|
312
335
|
conflicts.push({
|
|
313
336
|
name,
|
|
314
337
|
message: `refusing to write MCP server "${name}": its declared env var "${unresolvedName}" has no resolvable value in ${source} — writing it now would silently break this server's connection once the agent starts it (trellis-mcp-static-env-and-disabled-servers)`,
|
|
315
|
-
remediation: `set "${
|
|
338
|
+
remediation: `set "${remediationTarget}" in ${source}, or fix the name in servers.yaml if it was a typo, then re-run mcp sync`,
|
|
316
339
|
});
|
|
317
340
|
continue;
|
|
318
341
|
}
|
package/dist/adapters/pi.js
CHANGED
|
@@ -114,7 +114,7 @@ export class PiAdapter {
|
|
|
114
114
|
kind: "instructions",
|
|
115
115
|
});
|
|
116
116
|
if (supportsNativeMcp(this.versionProbe())) {
|
|
117
|
-
return [...skillItems, ...instructionsItems, ...this.planNativeMcp(canonical)];
|
|
117
|
+
return [...skillItems, ...instructionsItems, ...await this.planNativeMcp(canonical)];
|
|
118
118
|
}
|
|
119
119
|
const { file: bridgeFile, symlinkName } = resolveBridgeFile();
|
|
120
120
|
const extensionItems = planSymlinks({
|
|
@@ -139,12 +139,12 @@ export class PiAdapter {
|
|
|
139
139
|
* apply, and a bare skills/instructions sync can neither remove the
|
|
140
140
|
* bridge early nor resurrect it afterwards.
|
|
141
141
|
*/
|
|
142
|
-
planNativeMcp(canonical) {
|
|
142
|
+
async planNativeMcp(canonical) {
|
|
143
143
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
144
144
|
const configPath = join(agentDir, "mcp.json");
|
|
145
145
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
146
146
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
147
|
-
const items = planJsonMcp({
|
|
147
|
+
const items = (await planJsonMcp({
|
|
148
148
|
configPath,
|
|
149
149
|
parsed,
|
|
150
150
|
mcp: globalLayerMcp(canonical),
|
|
@@ -153,7 +153,7 @@ export class PiAdapter {
|
|
|
153
153
|
policy: canonical.secretsPolicy,
|
|
154
154
|
ownership,
|
|
155
155
|
render: piRenderer,
|
|
156
|
-
}).map((item) => {
|
|
156
|
+
})).map((item) => {
|
|
157
157
|
// pi speaks stdio and streamable HTTP only — an sse server gets an
|
|
158
158
|
// honest conflict instead of an entry pi could never connect.
|
|
159
159
|
const write = item.mcpWrite;
|
|
@@ -251,7 +251,7 @@ export class PiAdapter {
|
|
|
251
251
|
// definition drift. The plan only ever names Trellis-owned entries
|
|
252
252
|
// (the ownership ledger gates removals), so user-authored servers
|
|
253
253
|
// can never surface here.
|
|
254
|
-
for (const item of this.planNativeMcp(canonical)) {
|
|
254
|
+
for (const item of await this.planNativeMcp(canonical)) {
|
|
255
255
|
if (item.action === "create" && item.mcpWrite) {
|
|
256
256
|
mismatches.push(`mcp.json entry "${item.mcpWrite.name}" is missing or differs from canonical — run trellis mcp sync`);
|
|
257
257
|
}
|
package/dist/adapters/zcode.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function planZcodeMcp(opts: {
|
|
|
16
16
|
managedAgents: readonly import("../core/types.js").AgentId[];
|
|
17
17
|
policy: SecretsPolicy;
|
|
18
18
|
ownership: JsonRecord;
|
|
19
|
-
}): AdapterPlanItem[]
|
|
19
|
+
}): Promise<AdapterPlanItem[]>;
|
|
20
20
|
export declare function applyZcodeMcp(parsed: JsonRecord | undefined, items: readonly AdapterPlanItem[]): JsonRecord;
|
|
21
21
|
export declare class ZcodeAdapter implements TrellisAdapter {
|
|
22
22
|
private readonly homeDir;
|
package/dist/adapters/zcode.js
CHANGED
|
@@ -29,8 +29,8 @@ function record(value) {
|
|
|
29
29
|
function zcodeServers(parsed) {
|
|
30
30
|
return record(record(parsed?.mcp).servers);
|
|
31
31
|
}
|
|
32
|
-
export function planZcodeMcp(opts) {
|
|
33
|
-
const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
|
|
32
|
+
export async function planZcodeMcp(opts) {
|
|
33
|
+
const { desired, conflicts } = await resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
|
|
34
34
|
const existing = zcodeServers(opts.parsed);
|
|
35
35
|
const items = [];
|
|
36
36
|
for (const { name, def } of desired) {
|
|
@@ -136,14 +136,14 @@ export class ZcodeAdapter {
|
|
|
136
136
|
canonicalRoot,
|
|
137
137
|
kind: "instructions",
|
|
138
138
|
}));
|
|
139
|
-
items.push(...planZcodeMcp({
|
|
139
|
+
items.push(...(await planZcodeMcp({
|
|
140
140
|
configPath: profile.configPath,
|
|
141
141
|
parsed,
|
|
142
142
|
mcp: globalLayerMcp(canonical),
|
|
143
143
|
managedAgents: canonical.managedAgents,
|
|
144
144
|
policy: canonical.secretsPolicy,
|
|
145
145
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
146
|
-
}));
|
|
146
|
+
})));
|
|
147
147
|
const current = currentSkillControls(parsed);
|
|
148
148
|
const ownership = loadZcodeSettingsOwnership(this.homeDir);
|
|
149
149
|
if (runtimeOnly && (current.featuresSkill !== false || current.skillsEnabled !== false)) {
|
|
@@ -214,14 +214,14 @@ export class ZcodeAdapter {
|
|
|
214
214
|
for (const name of desiredNativeSkills)
|
|
215
215
|
if (!actualNativeSkills.has(name))
|
|
216
216
|
mismatches.push(`skill "${name}" is missing on ZCode`);
|
|
217
|
-
const expectedMcp = planZcodeMcp({
|
|
217
|
+
const expectedMcp = (await planZcodeMcp({
|
|
218
218
|
configPath: profile.configPath,
|
|
219
219
|
parsed: readConfig(profile.configPath),
|
|
220
220
|
mcp: globalLayerMcp(canonical),
|
|
221
221
|
managedAgents: canonical.managedAgents,
|
|
222
222
|
policy: canonical.secretsPolicy,
|
|
223
223
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
224
|
-
}).filter((item) => item.action === "create" || item.action === "conflict");
|
|
224
|
+
})).filter((item) => item.action === "create" || item.action === "conflict");
|
|
225
225
|
mismatches.push(...expectedMcp.map((item) => item.description));
|
|
226
226
|
if (!snapshot.instructionsFile?.isSymlink || snapshot.instructionsFile.target !== canonical.instructionsFile)
|
|
227
227
|
mismatches.push("shared instructions are not linked into ~/.zcode/AGENTS.md");
|
|
@@ -137,7 +137,7 @@ export declare function detectMcpUnreachable(snapshots: AgentSnapshot[]): Findin
|
|
|
137
137
|
/** Compares runtime entries against the same canonical MCP plan that sync
|
|
138
138
|
* uses. Only missing Trellis-owned runtime edges are reported; extra
|
|
139
139
|
* user-authored MCP entries remain outside doctor ownership. */
|
|
140
|
-
export declare function detectRuntimeDrift(snapshots: AgentSnapshot[], canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Finding[]
|
|
140
|
+
export declare function detectRuntimeDrift(snapshots: AgentSnapshot[], canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Promise<Finding[]>;
|
|
141
141
|
/** Exported so `trellis onboard` can reuse it verbatim for its own final
|
|
142
142
|
* stage (trellis-onboard-closed-loop) rather than a second copy of this
|
|
143
143
|
* formatting. */
|
package/dist/commands/doctor.js
CHANGED
|
@@ -178,7 +178,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
178
178
|
// `collectWorkspaceDoctorSection`'s per-file inspection is what we want.
|
|
179
179
|
let workspace;
|
|
180
180
|
if (canonical?.workspace) {
|
|
181
|
-
workspace = collectWorkspaceDoctorSection(canonical);
|
|
181
|
+
workspace = await collectWorkspaceDoctorSection(canonical);
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
184
184
|
const workspaceRoot = workspaceDir != null ? findWorkspaceRoot(workspaceDir) : null;
|
|
@@ -198,7 +198,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
198
198
|
diagnostics: [],
|
|
199
199
|
workspace: { root: workspaceRoot, mcpProvenance: {}, mcpGlobalServers: {}, skillProvenance: {}, globalSkills: [], shadowed: [] },
|
|
200
200
|
};
|
|
201
|
-
workspace = collectWorkspaceDoctorSection(minimal);
|
|
201
|
+
workspace = await collectWorkspaceDoctorSection(minimal);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
// Workspace TOML findings become top-level findings so the exit code
|
|
@@ -215,7 +215,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
|
|
218
|
-
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
218
|
+
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? await detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
219
219
|
// 分级开启(trellis-evolve 10.2):级别策略是显式用户选择,读取零副作用。
|
|
220
220
|
// 未配置(或全空)时整段缺席 —— 从未开启自我进化的机器输出与未安装时
|
|
221
221
|
// 一致;损坏的策略文件不拖垮扫描,降级为段内诊断呈现。
|
|
@@ -427,12 +427,12 @@ export function detectMcpUnreachable(snapshots) {
|
|
|
427
427
|
/** Compares runtime entries against the same canonical MCP plan that sync
|
|
428
428
|
* uses. Only missing Trellis-owned runtime edges are reported; extra
|
|
429
429
|
* user-authored MCP entries remain outside doctor ownership. */
|
|
430
|
-
export function detectRuntimeDrift(snapshots, canonical) {
|
|
430
|
+
export async function detectRuntimeDrift(snapshots, canonical) {
|
|
431
431
|
const findings = [];
|
|
432
432
|
for (const snap of snapshots) {
|
|
433
433
|
if (!snap.present || !canonical.managedAgents.includes(snap.agent) || snap.agent === "pi")
|
|
434
434
|
continue;
|
|
435
|
-
const expected = resolveMcpPlan(snap.agent, canonical.mcp, canonical.managedAgents, canonical.secretsPolicy).desired
|
|
435
|
+
const expected = (await resolveMcpPlan(snap.agent, canonical.mcp, canonical.managedAgents, canonical.secretsPolicy)).desired
|
|
436
436
|
.map((entry) => entry.name)
|
|
437
437
|
.filter((name) => name === RUNTIME_ENTRY_NAME || name === GATEWAY_ENTRY_NAME);
|
|
438
438
|
const actual = new Set(snap.mcpServers.map((server) => server.name));
|
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
|
}
|
|
@@ -46,11 +46,11 @@ export declare function parseMcpGatewayArgs(argv: readonly string[]): {
|
|
|
46
46
|
* connect to itself. Resolving against a gateway-less view of the same
|
|
47
47
|
* canonical gives the set gateway mode replaced.
|
|
48
48
|
*/
|
|
49
|
-
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<{
|
|
50
50
|
upstreams: UpstreamSpec[];
|
|
51
51
|
oauthServers: string[];
|
|
52
52
|
conflicts: string[];
|
|
53
|
-
}
|
|
53
|
+
}>;
|
|
54
54
|
export declare function runMcpGateway(opts: RunMcpGatewayOptions): Promise<{
|
|
55
55
|
exitCode: number;
|
|
56
56
|
}>;
|
|
@@ -59,7 +59,7 @@ export function parseMcpGatewayArgs(argv) {
|
|
|
59
59
|
* connect to itself. Resolving against a gateway-less view of the same
|
|
60
60
|
* canonical gives the set gateway mode replaced.
|
|
61
61
|
*/
|
|
62
|
-
export function resolveGatewayUpstreams(agentId, canonical) {
|
|
62
|
+
export async function resolveGatewayUpstreams(agentId, canonical) {
|
|
63
63
|
const route = canonical.mcp.routes?.[agentId];
|
|
64
64
|
const hubActive = route ? route.mode === "hub" : Boolean(canonical.mcp.hub) && !isGatewayAgent(agentId, canonical.mcp, canonical.managedAgents);
|
|
65
65
|
if (hubActive) {
|
|
@@ -90,7 +90,7 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
90
90
|
// ownership-gated removal clears it. Serving that stale entry would hand
|
|
91
91
|
// every server, and every secret resolved into it, to the one agent the
|
|
92
92
|
// user just said to stop involving.
|
|
93
|
-
const { desired, conflicts } = resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
93
|
+
const { desired, conflicts } = await resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
94
94
|
// Agent-owned OAuth servers bypass the gateway as direct entries, so no
|
|
95
95
|
// connection state exists for them here — but Trellis owns the token
|
|
96
96
|
// store, so `trellis.mcp.status` reports their credential health for
|
|
@@ -101,7 +101,7 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
101
101
|
const agentOwnedServers = Object.fromEntries(Object.entries(canonical.mcp.servers).filter(([, def]) => isAgentOwnedOAuth(def)));
|
|
102
102
|
const oauthServers = Object.keys(agentOwnedServers).length === 0
|
|
103
103
|
? []
|
|
104
|
-
: 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);
|
|
105
105
|
return {
|
|
106
106
|
upstreams: desired.map(({ name }) => ({ name, def: canonical.mcp.servers[name] })),
|
|
107
107
|
oauthServers,
|
|
@@ -131,7 +131,7 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
|
131
131
|
console.error(`trellis-mcp-gateway: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
132
|
return { exitCode: 1 };
|
|
133
133
|
}
|
|
134
|
-
const { upstreams, oauthServers, conflicts } = resolveGatewayUpstreams(opts.agentId, canonical);
|
|
134
|
+
const { upstreams, oauthServers, conflicts } = await resolveGatewayUpstreams(opts.agentId, canonical);
|
|
135
135
|
for (const message of conflicts) {
|
|
136
136
|
console.error(`trellis-mcp-gateway: ${message}`);
|
|
137
137
|
}
|
|
@@ -9,6 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { loadCanonicalSource, ensureGitignoreEntry, ensureShellEnvSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
|
|
12
|
+
import { deliveredName } from "../lib/secretEnv.js";
|
|
12
13
|
import { openBackupSession } from "../lib/backup.js";
|
|
13
14
|
import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
|
|
14
15
|
import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
|
|
@@ -115,7 +116,7 @@ function semanticIdentity(def) {
|
|
|
115
116
|
enabled: def.enabled,
|
|
116
117
|
agents: def.agents ? sorted(def.agents) : undefined,
|
|
117
118
|
staticEnv: def.staticEnv,
|
|
118
|
-
envKeys: sorted([...(def.env ?? []), ...Object.keys(def.envAliases ?? {})]),
|
|
119
|
+
envKeys: sorted([...(def.env ?? []).map(deliveredName), ...Object.keys(def.envAliases ?? {})]),
|
|
119
120
|
headerKeys: sorted(Object.keys(def.headers ?? {})),
|
|
120
121
|
});
|
|
121
122
|
}
|
|
@@ -35,7 +35,7 @@ export interface SecretsFinding {
|
|
|
35
35
|
* `~/.trellis/mcp/servers.yaml` itself (see module doc comment). */
|
|
36
36
|
agent: AgentId | "environment" | "canonical";
|
|
37
37
|
file: string;
|
|
38
|
-
kind: "literal-secret" | "unexpected-var-name" | "missing-env-value";
|
|
38
|
+
kind: "literal-secret" | "unexpected-var-name" | "missing-env-value" | "malformed-reference";
|
|
39
39
|
detail: string;
|
|
40
40
|
}
|
|
41
41
|
export interface SecretsAuditReport {
|
|
@@ -29,7 +29,7 @@ import { KiroAdapter } from "../adapters/kiro.js";
|
|
|
29
29
|
import { ZcodeAdapter } from "../adapters/zcode.js";
|
|
30
30
|
import { declaredEnvNames, extractJsonEnvVarNames, extractTomlEnvVarNames, extractZcodeEnvVarNames } from "../lib/envVarNames.js";
|
|
31
31
|
import { resolveZcodeProfile } from "../probes/zcode.js";
|
|
32
|
-
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
32
|
+
import { parseEnvEntry, resolveSecretEnv } from "../lib/secretEnv.js";
|
|
33
33
|
/** Only agents in `managedAgents` — a present-but-unmanaged agent's config
|
|
34
34
|
* file is never even read (trellis-managed-agents), same restriction as
|
|
35
35
|
* `sync`/`mcp sync`. pi has no static, generated MCP config file to audit
|
|
@@ -49,7 +49,7 @@ function auditedAgents(homeDir, managedAgents) {
|
|
|
49
49
|
* a problem regardless of which agents' `servers.yaml` `agents:` field
|
|
50
50
|
* would route it to (trellis-secrets-env-management design.md D3).
|
|
51
51
|
*/
|
|
52
|
-
function findMissingEnvValues(servers, policy) {
|
|
52
|
+
async function findMissingEnvValues(servers, policy) {
|
|
53
53
|
const names = new Set();
|
|
54
54
|
for (const def of Object.values(servers)) {
|
|
55
55
|
for (const name of declaredEnvNames(def))
|
|
@@ -57,16 +57,39 @@ function findMissingEnvValues(servers, policy) {
|
|
|
57
57
|
}
|
|
58
58
|
if (names.size === 0)
|
|
59
59
|
return [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
// Malformed `mxenv://` entries are findings in their own right — never
|
|
61
|
+
// skipped, never treated as bare names (mx-env-secret-source spec).
|
|
62
|
+
const entries = [...names];
|
|
63
|
+
const parsed = entries.map(parseEnvEntry);
|
|
64
|
+
const findings = [];
|
|
65
|
+
for (const p of parsed) {
|
|
66
|
+
if (p.kind === "invalid") {
|
|
67
|
+
findings.push({
|
|
68
|
+
agent: "environment",
|
|
69
|
+
file: policy.envFile ?? "process environment",
|
|
70
|
+
kind: "malformed-reference",
|
|
71
|
+
detail: `"${p.entry}" is not a valid mxenv://<profile>/<KEY> reference`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const resolvable = entries.filter((_, i) => parsed[i].kind !== "invalid");
|
|
76
|
+
if (resolvable.length === 0)
|
|
77
|
+
return findings;
|
|
78
|
+
// Findings name the DELIVERED name (the KEY segment for a reference).
|
|
79
|
+
const resolved = await resolveSecretEnv(resolvable, policy);
|
|
80
|
+
for (const entry of resolvable) {
|
|
81
|
+
const p = parseEnvEntry(entry);
|
|
82
|
+
const delivered = p.kind === "invalid" ? entry : p.delivered;
|
|
83
|
+
if (!resolved[delivered]) {
|
|
84
|
+
findings.push({
|
|
85
|
+
agent: "environment",
|
|
86
|
+
file: p.kind === "ref" ? `the mxenv composition (profile "${p.profile}")` : (policy.envFile ?? "process environment"),
|
|
87
|
+
kind: "missing-env-value",
|
|
88
|
+
detail: `"${delivered}" is declared by a canonical MCP server's env but has no resolvable value`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
70
93
|
}
|
|
71
94
|
/**
|
|
72
95
|
* Reject-pattern-only scan of canonical's own `mcp/servers.yaml` (design.md
|
|
@@ -118,7 +141,7 @@ export async function collectSecretsAuditReport(opts = {}) {
|
|
|
118
141
|
const content = readFileSync(file, "utf-8");
|
|
119
142
|
findings.push(...auditFile(agent.id, file, content, canonical.secretsPolicy, agent.extractNames));
|
|
120
143
|
}
|
|
121
|
-
findings.push(...findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
|
|
144
|
+
findings.push(...await findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
|
|
122
145
|
findings.push(...auditCanonicalServersYaml(homeDir, canonical.secretsPolicy));
|
|
123
146
|
return { findings };
|
|
124
147
|
}
|
package/dist/lib/envVarNames.js
CHANGED
|
@@ -22,7 +22,14 @@ export function extractTemplateVarNames(value) {
|
|
|
22
22
|
* same answer to "what names does this def reference," not two
|
|
23
23
|
* separately-maintained collections. */
|
|
24
24
|
export function declaredEnvNames(def) {
|
|
25
|
+
// RAW entries, including `envAliases` sources (mx-env-secret-source
|
|
26
|
+
// secrets-audit delta): consumers that RESOLVE must see the original
|
|
27
|
+
// `mxenv://` text so references go through composition, then look the
|
|
28
|
+
// result up by delivered name; consumers that want the delivered NAMES
|
|
29
|
+
// map through `deliveredName` at their own use site (e.g. Kiro).
|
|
25
30
|
const names = new Set(def.env ?? []);
|
|
31
|
+
for (const source of Object.values(def.envAliases ?? {}))
|
|
32
|
+
names.add(source);
|
|
26
33
|
for (const value of Object.values(def.headers ?? {})) {
|
|
27
34
|
for (const name of extractTemplateVarNames(value))
|
|
28
35
|
names.add(name);
|
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
|
/**
|