agent-trellis 0.20.0 → 0.22.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 +3 -1
- package/dist/adapters/mcpPlan.js +65 -15
- 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 +9 -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/migrate.js +11 -1
- package/dist/commands/remoteSkill.js +18 -1
- package/dist/commands/secretsAudit.d.ts +1 -1
- package/dist/commands/secretsAudit.js +49 -13
- package/dist/commands/skill.d.ts +11 -6
- package/dist/commands/skill.js +85 -32
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/core/canonical.d.ts +1 -2
- package/dist/core/canonical.js +16 -9
- package/dist/core/types.d.ts +8 -7
- package/dist/lib/envVarNames.d.ts +5 -1
- package/dist/lib/envVarNames.js +22 -2
- package/dist/lib/mcpConnect.d.ts +1 -1
- package/dist/lib/mcpConnect.js +35 -10
- package/dist/lib/mcpMigrateRead.js +14 -2
- package/dist/lib/secretEnv.d.ts +57 -8
- package/dist/lib/secretEnv.js +108 -16
- package/dist/lib/skillScope.d.ts +14 -0
- package/dist/lib/skillScope.js +40 -0
- package/dist/lib/tomlSection.js +3 -2
- package/dist/lib/workspace.js +7 -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 +42424 -2793
- package/dist/pi-bridge/index.js +2 -2
- package/docs/architecture.md +27 -7
- package/docs/getting-started.md +29 -9
- package/docs/research.md +17 -1
- package/package.json +2 -1
- package/schema/scope.example.yaml +15 -3
- package/schema/secrets.policy.example.yaml +5 -0
- package/schema/servers.example.yaml +11 -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,6 @@ 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
|
|
94
|
+
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy, options?: {
|
|
95
|
+
hosted?: boolean;
|
|
96
|
+
}): Promise<McpPlanResult>;
|
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -20,7 +20,8 @@ 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 { deliveredName, isValidSecretVarName, parseEnvEntry, resolveSecretEntries, resolveSecretEnv, SecretReferenceError } from "../lib/secretEnv.js";
|
|
24
|
+
import { extractTemplateVarNames, invalidHeaderTemplate } from "../lib/envVarNames.js";
|
|
24
25
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
25
26
|
export const GATEWAY_ENTRY_NAME = "trellis";
|
|
26
27
|
export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
|
|
@@ -138,14 +139,38 @@ export function findLiteralSecret(def) {
|
|
|
138
139
|
* the target key it would have been written under
|
|
139
140
|
* (trellis-migrate-env-var-alias).
|
|
140
141
|
*/
|
|
141
|
-
function findUnresolvedEnvName(def, policy) {
|
|
142
|
-
const
|
|
142
|
+
async function findUnresolvedEnvName(def, policy) {
|
|
143
|
+
const headerRefs = Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames)
|
|
144
|
+
.filter((entry) => !isValidSecretVarName(entry));
|
|
145
|
+
const envNames = [...(def.env ?? []), ...Object.values(def.envAliases ?? {})];
|
|
146
|
+
const names = [...envNames, ...headerRefs];
|
|
143
147
|
if (names.length === 0)
|
|
144
148
|
return undefined;
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
// A malformed `mxenv://` entry conflicts here too — it is rejected input,
|
|
150
|
+
// never a silently-resolved bare name (mx-env-secret-source D6). The
|
|
151
|
+
// returned text is the DECLARED entry; resolution checks use delivered
|
|
152
|
+
// names (the `KEY` segment for a reference).
|
|
153
|
+
const parsed = names.map(parseEnvEntry);
|
|
154
|
+
const invalid = names.find((entry, i) => parsed[i].kind === "invalid" || (headerRefs.includes(entry) && parsed[i].kind === "name"));
|
|
155
|
+
if (invalid !== undefined)
|
|
156
|
+
return invalid;
|
|
157
|
+
try {
|
|
158
|
+
// Retain env/envAliases' existing delivered-name collision behavior.
|
|
159
|
+
// Header references instead remain source-bound, with no bare-name fallback.
|
|
160
|
+
const env = await resolveSecretEnv(envNames, policy);
|
|
161
|
+
const missingEnv = envNames.find((entry) => !env[deliveredName(entry)]);
|
|
162
|
+
if (missingEnv !== undefined)
|
|
163
|
+
return missingEnv;
|
|
164
|
+
const headers = await resolveSecretEntries(headerRefs, policy);
|
|
165
|
+
return headerRefs.find((entry) => !headers[entry]);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof SecretReferenceError)
|
|
169
|
+
return error.entry;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
147
172
|
}
|
|
148
|
-
export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
173
|
+
export async function resolveMcpPlan(agentId, mcp, managedAgents, policy, options = {}) {
|
|
149
174
|
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
150
175
|
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
151
176
|
// Checked before `hub`: only one entry can be "the" MCP entry for an
|
|
@@ -161,7 +186,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
161
186
|
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
|
|
162
187
|
};
|
|
163
188
|
}
|
|
164
|
-
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
189
|
+
const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
165
190
|
// Every ordinary per-server check (enabled, scope, literal secrets,
|
|
166
191
|
// unresolved env names, Codex's header shape) still runs at gateway
|
|
167
192
|
// startup. OAuth servers are the deliberate exception: they are written
|
|
@@ -237,7 +262,7 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
237
262
|
// Runtime mode owns the agent-facing MCP edge. It resolves the same
|
|
238
263
|
// direct/gateway-compatible upstream route at startup, so native
|
|
239
264
|
// per-server entries are not duplicated here.
|
|
240
|
-
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
265
|
+
const oauth = await planDirectServers(agentId, mcp, managedAgents, policy, route, isAgentOwnedOAuth);
|
|
241
266
|
return {
|
|
242
267
|
desired: [{
|
|
243
268
|
name: RUNTIME_ENTRY_NAME,
|
|
@@ -246,14 +271,14 @@ export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
246
271
|
conflicts: oauth.conflicts,
|
|
247
272
|
};
|
|
248
273
|
}
|
|
249
|
-
const native = planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
274
|
+
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route, undefined, options.hosted || agentId === "pi");
|
|
250
275
|
const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
|
|
251
276
|
return {
|
|
252
277
|
desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
|
|
253
278
|
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
279
|
};
|
|
255
280
|
}
|
|
256
|
-
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
281
|
+
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter, hosted = false) {
|
|
257
282
|
const desired = [];
|
|
258
283
|
const conflicts = [];
|
|
259
284
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
@@ -293,12 +318,17 @@ function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
|
293
318
|
});
|
|
294
319
|
continue;
|
|
295
320
|
}
|
|
321
|
+
const invalidHeader = invalidHeaderTemplate(def.headers);
|
|
322
|
+
if (invalidHeader !== undefined) {
|
|
323
|
+
conflicts.push({ name, message: `refusing to write MCP server "${name}": invalid reference template in header ${invalidHeader}`, remediation: "use ${VAR} or ${mxenv://<profile>/<KEY>} in headers" });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
296
326
|
// Codex has no generic headers concept — only the single
|
|
297
327
|
// Authorization-bearer-token shape is expressible there
|
|
298
328
|
// (trellis-mcp-transport-auth design.md D4). Any other shape is
|
|
299
329
|
// refused for Codex specifically; every other in-scope agent for
|
|
300
330
|
// the same server is unaffected (this loop runs once per agentId).
|
|
301
|
-
if (agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
|
|
331
|
+
if (!hosted && agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
|
|
302
332
|
conflicts.push({
|
|
303
333
|
name,
|
|
304
334
|
message: `refusing to write MCP server "${name}" for codex: its "headers" field isn't the single { Authorization: "Bearer \${VAR}" } shape Codex's own config format can express — Codex has no generic headers concept, only \`bearer_token_env_var\`. Still written normally for every other in-scope agent.`,
|
|
@@ -306,13 +336,33 @@ function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
|
306
336
|
});
|
|
307
337
|
continue;
|
|
308
338
|
}
|
|
309
|
-
const unresolvedName = findUnresolvedEnvName(def, policy);
|
|
310
|
-
if (unresolvedName) {
|
|
311
|
-
const
|
|
339
|
+
const unresolvedName = await findUnresolvedEnvName(def, policy);
|
|
340
|
+
if (unresolvedName !== undefined) {
|
|
341
|
+
const entry = parseEnvEntry(unresolvedName);
|
|
342
|
+
if (entry.kind === "invalid" || !isValidSecretVarName(unresolvedName) && entry.kind === "name") {
|
|
343
|
+
conflicts.push({
|
|
344
|
+
name,
|
|
345
|
+
message: `refusing to write MCP server "${name}": its declared env entry "${unresolvedName}" is not a valid mxenv://<profile>/<KEY> reference (mx-env-secret-source)`,
|
|
346
|
+
remediation: `fix the entry in servers.yaml to the mxenv://<profile>/<KEY> form or a plain variable name, then re-run mcp sync`,
|
|
347
|
+
});
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const source = entry.kind === "ref"
|
|
351
|
+
? `the mxenv composition (profile "${entry.profile}")`
|
|
352
|
+
: (policy.envFile ?? "process environment");
|
|
353
|
+
const remediationTarget = entry.kind === "ref" ? entry.key : unresolvedName;
|
|
312
354
|
conflicts.push({
|
|
313
355
|
name,
|
|
314
356
|
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 "${
|
|
357
|
+
remediation: `set "${remediationTarget}" in ${source}, or fix the name in servers.yaml if it was a typo, then re-run mcp sync`,
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!hosted && Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames).some((entry) => parseEnvEntry(entry).kind === "ref")) {
|
|
362
|
+
conflicts.push({
|
|
363
|
+
name,
|
|
364
|
+
message: `refusing to write MCP server "${name}": mxenv header references require Trellis gateway/runtime delivery; native clients cannot resolve them`,
|
|
365
|
+
remediation: `enable gateway/runtime delivery for ${agentId}, keeping transport and headers in servers.yaml`,
|
|
316
366
|
});
|
|
317
367
|
continue;
|
|
318
368
|
}
|
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");
|
package/dist/cli.js
CHANGED
|
@@ -118,7 +118,7 @@ Commands:
|
|
|
118
118
|
--skill, -s <name> required unless --list
|
|
119
119
|
--branch <ref> Git branch, tag, or ref; default is
|
|
120
120
|
the source repository's default branch
|
|
121
|
-
--agent, -a <ids
|
|
121
|
+
--agent, -a <ids|*|none> choose applicable managed Agents
|
|
122
122
|
--list show discovered Skills without import
|
|
123
123
|
--global, --yes accepted skills-CLI compatibility flags
|
|
124
124
|
--dry-run fetch and preview; write nothing
|
|
@@ -205,19 +205,22 @@ Commands:
|
|
|
205
205
|
--dry-run preview the plan, write nothing
|
|
206
206
|
--json machine-readable output, no report text
|
|
207
207
|
skill list
|
|
208
|
-
List canonical skills
|
|
208
|
+
List canonical skills; unregistered skills are paused
|
|
209
|
+
--agent <id> filter by agent and show actual delivery status
|
|
209
210
|
--json machine-readable output, no report text
|
|
210
211
|
skill add <name> --from <path>
|
|
211
212
|
Import a real skill directory into canonical (refuses on
|
|
212
213
|
an existing name with different content, no overwrite)
|
|
213
214
|
--workspace copy into .agents/skills/ instead of canonical
|
|
215
|
+
--agents <ids> | --all | --none
|
|
216
|
+
required without an interactive terminal
|
|
214
217
|
--dry-run preview the plan, write nothing
|
|
215
218
|
--json machine-readable output, no report text
|
|
216
219
|
skill remove <name>
|
|
217
220
|
skill scope <name> --agents <ids> | --all | --none
|
|
218
221
|
Choose which managed agents receive a canonical skill, then
|
|
219
|
-
sync it.
|
|
220
|
-
scope is not editable
|
|
222
|
+
sync it. --all records today's managed set; newly managed
|
|
223
|
+
agents need explicit registration. Built-in scope is not editable
|
|
221
224
|
--dry-run preview the plan, write nothing
|
|
222
225
|
--json machine-readable output, no report text
|
|
223
226
|
skill update-builtin
|
|
@@ -527,7 +530,7 @@ async function main(argv) {
|
|
|
527
530
|
const json = skillRest.includes("--json");
|
|
528
531
|
const dryRun = skillRest.includes("--dry-run");
|
|
529
532
|
if (subcommand === "list") {
|
|
530
|
-
process.exitCode = runSkillList({ json }).exitCode;
|
|
533
|
+
process.exitCode = runSkillList({ json, agent: flagValue(skillRest, "--agent") }).exitCode;
|
|
531
534
|
return;
|
|
532
535
|
}
|
|
533
536
|
if (subcommand === "add") {
|
|
@@ -538,7 +541,7 @@ async function main(argv) {
|
|
|
538
541
|
process.exitCode = 1;
|
|
539
542
|
return;
|
|
540
543
|
}
|
|
541
|
-
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace") })).exitCode;
|
|
544
|
+
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace"), selection: parseScopeArgs(skillRest) })).exitCode;
|
|
542
545
|
return;
|
|
543
546
|
}
|
|
544
547
|
if (subcommand === "remove") {
|
|
@@ -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, { hosted: true });
|
|
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
|
}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -19,7 +19,7 @@ import { deepEqual } from "../lib/deepEqual.js";
|
|
|
19
19
|
import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, readKimiCodeMcpDefs, readZcodeMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
|
|
20
20
|
import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
|
|
21
21
|
import { findLiteralSecret } from "../adapters/mcpPlan.js";
|
|
22
|
-
import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
|
|
22
|
+
import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, validateSkillScopeYaml, writeSecretsPolicyExtraction, writeSkillScopeYaml } from "../core/canonical.js";
|
|
23
23
|
import { ALL_AGENTS } from "../core/types.js";
|
|
24
24
|
import { discoverNativeMemory, MAX_NATIVE_MEMORY_BYTES, renderNativeMemory } from "../lib/nativeMemory.js";
|
|
25
25
|
/** Where a `staticEnv` literal secret's real value goes when
|
|
@@ -390,6 +390,11 @@ function copyDirectoryThroughBackup(sourceDir, destDir, backup) {
|
|
|
390
390
|
}
|
|
391
391
|
export function applyMigratePlan(plan, homeDir = homedir(), backup) {
|
|
392
392
|
const canonicalRoot = join(homeDir, ".trellis");
|
|
393
|
+
if (plan.items.some((item) => item.kind === "skill" && item.action === "create")) {
|
|
394
|
+
const validation = validateSkillScopeYaml(join(canonicalRoot, "scope.yaml"));
|
|
395
|
+
if (!validation.ok)
|
|
396
|
+
throw new Error(validation.error);
|
|
397
|
+
}
|
|
393
398
|
for (const item of plan.items) {
|
|
394
399
|
if (item.action !== "create" && item.action !== "reclassify" && item.action !== "extract-secret")
|
|
395
400
|
continue;
|
|
@@ -400,6 +405,11 @@ export function applyMigratePlan(plan, homeDir = homedir(), backup) {
|
|
|
400
405
|
copyDirectoryThroughBackup(item.sourceDir, dest, backup);
|
|
401
406
|
else
|
|
402
407
|
cpSync(item.sourceDir, dest, { recursive: true });
|
|
408
|
+
// Migration proves applicability to its source agent only. Sharing
|
|
409
|
+
// with other agents is a separate, explicit compatibility decision.
|
|
410
|
+
const scope = writeSkillScopeYaml(join(canonicalRoot, "scope.yaml"), item.name, [plan.agent], backup);
|
|
411
|
+
if (!scope.ok)
|
|
412
|
+
throw new Error(scope.error);
|
|
403
413
|
}
|
|
404
414
|
else if (item.kind === "instructions" && item.sourceContent !== undefined) {
|
|
405
415
|
mkdirSync(canonicalRoot, { recursive: true });
|