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
|
@@ -15,6 +15,7 @@ import { isBuiltinSkillName } from "../lib/builtinSkills.js";
|
|
|
15
15
|
import { emptyRemoteSkillLock, readRemoteSkillLock, writeRemoteSkillLock } from "../lib/remoteSkillLock.js";
|
|
16
16
|
import { fetchRemoteRepository, normalizeGitHubRepository } from "../lib/remoteSkillSource.js";
|
|
17
17
|
import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
|
|
18
|
+
import { requestSkillScope } from "../lib/skillScope.js";
|
|
18
19
|
function safeSkillName(name) {
|
|
19
20
|
return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name) && name !== "." && name !== "..";
|
|
20
21
|
}
|
|
@@ -41,7 +42,9 @@ function scopeForRequest(homeDir, agents) {
|
|
|
41
42
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
42
43
|
}
|
|
43
44
|
if (!agents || agents.length === 0)
|
|
44
|
-
return {
|
|
45
|
+
return { error: 'choose applicable agents with --agent <ids> or --agent "*"' };
|
|
46
|
+
if (agents.length === 1 && agents[0] === "none")
|
|
47
|
+
return { scope: [] };
|
|
45
48
|
const values = agents.flatMap((value) => value.split(",").map((id) => id.trim()).filter(Boolean));
|
|
46
49
|
if (values.length === 0)
|
|
47
50
|
return { error: "--agent needs at least one Agent id" };
|
|
@@ -164,6 +167,20 @@ export async function runRemoteSkillAdd(sourceInput, name, opts = {}) {
|
|
|
164
167
|
console.error(plan.detail);
|
|
165
168
|
return { exitCode: 1 };
|
|
166
169
|
}
|
|
170
|
+
if (!opts.agents) {
|
|
171
|
+
const home = opts.homeDir ?? homedir();
|
|
172
|
+
const managed = existsSync(join(home, ".trellis")) ? loadCanonicalSource(home).managedAgents : [];
|
|
173
|
+
const selected = await requestSkillScope({}, managed, !opts.json);
|
|
174
|
+
if ("error" in selected) {
|
|
175
|
+
const detail = `${selected.error}; remote import uses --agent <ids>, --agent "*" or --agent none`;
|
|
176
|
+
if (opts.json)
|
|
177
|
+
console.log(JSON.stringify({ action: "invalid-options", detail }));
|
|
178
|
+
else
|
|
179
|
+
console.error(detail);
|
|
180
|
+
return { exitCode: 1 };
|
|
181
|
+
}
|
|
182
|
+
opts = { ...opts, agents: selected.scope.length ? selected.scope : ["none"] };
|
|
183
|
+
}
|
|
167
184
|
let fetched;
|
|
168
185
|
try {
|
|
169
186
|
fetched = fetchRemoteRepository(repository, { branch: opts.branch });
|
|
@@ -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 {
|
|
32
|
+
import { parseEnvEntry, resolveSecretEntries, SecretReferenceError } 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,52 @@ 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 pending = [...resolvable];
|
|
80
|
+
let resolved = {};
|
|
81
|
+
// One broken profile must be a finding, not abort the entire audit.
|
|
82
|
+
while (pending.length) {
|
|
83
|
+
try {
|
|
84
|
+
resolved = await resolveSecretEntries(pending, policy);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!(error instanceof SecretReferenceError))
|
|
89
|
+
throw error;
|
|
90
|
+
pending.splice(pending.indexOf(error.entry), 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const entry of resolvable) {
|
|
94
|
+
const p = parseEnvEntry(entry);
|
|
95
|
+
const delivered = p.kind === "invalid" ? entry : p.delivered;
|
|
96
|
+
if (!resolved[entry]) {
|
|
97
|
+
findings.push({
|
|
98
|
+
agent: "environment",
|
|
99
|
+
file: p.kind === "ref" ? `the mxenv composition (profile "${p.profile}")` : (policy.envFile ?? "process environment"),
|
|
100
|
+
kind: "missing-env-value",
|
|
101
|
+
detail: `"${delivered}" is declared by a canonical MCP server's env or headers but has no resolvable value`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return findings;
|
|
70
106
|
}
|
|
71
107
|
/**
|
|
72
108
|
* Reject-pattern-only scan of canonical's own `mcp/servers.yaml` (design.md
|
|
@@ -118,7 +154,7 @@ export async function collectSecretsAuditReport(opts = {}) {
|
|
|
118
154
|
const content = readFileSync(file, "utf-8");
|
|
119
155
|
findings.push(...auditFile(agent.id, file, content, canonical.secretsPolicy, agent.extractNames));
|
|
120
156
|
}
|
|
121
|
-
findings.push(...findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
|
|
157
|
+
findings.push(...await findMissingEnvValues(canonical.mcp.servers, canonical.secretsPolicy));
|
|
122
158
|
findings.push(...auditCanonicalServersYaml(homeDir, canonical.secretsPolicy));
|
|
123
159
|
return { findings };
|
|
124
160
|
}
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -31,12 +31,18 @@ export declare function runSkillUpdateBuiltin(opts?: {
|
|
|
31
31
|
export interface SkillListEntry {
|
|
32
32
|
name: string;
|
|
33
33
|
scope: readonly AgentId[];
|
|
34
|
+
registered?: boolean;
|
|
35
|
+
delivery?: {
|
|
36
|
+
state: "linked" | "missing" | "conflict" | "runtime";
|
|
37
|
+
path?: string;
|
|
38
|
+
};
|
|
34
39
|
remote?: Pick<RemoteSkillLockEntry, "source" | "requestedRef" | "commit" | "subdirectory" | "digest">;
|
|
35
40
|
}
|
|
36
|
-
export declare function collectSkillList(homeDir?: string): SkillListEntry[];
|
|
41
|
+
export declare function collectSkillList(homeDir?: string, agent?: AgentId): SkillListEntry[];
|
|
37
42
|
export declare function runSkillList(opts?: {
|
|
38
43
|
homeDir?: string;
|
|
39
44
|
json?: boolean;
|
|
45
|
+
agent?: string;
|
|
40
46
|
}): {
|
|
41
47
|
exitCode: number;
|
|
42
48
|
};
|
|
@@ -46,6 +52,7 @@ export interface SkillAddPlan {
|
|
|
46
52
|
action: SkillAddAction;
|
|
47
53
|
detail: string;
|
|
48
54
|
sourceDir?: string;
|
|
55
|
+
scope?: AgentId[];
|
|
49
56
|
}
|
|
50
57
|
export declare function collectSkillAddPlan(name: string, fromPath: string, homeDir?: string): SkillAddPlan;
|
|
51
58
|
export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string, backup?: BackupSession): void;
|
|
@@ -84,6 +91,7 @@ export declare function runSkillAdd(name: string, fromPath: string, opts?: {
|
|
|
84
91
|
dryRun?: boolean;
|
|
85
92
|
workspace?: boolean;
|
|
86
93
|
workspaceDir?: string;
|
|
94
|
+
selection?: ScopeSelectorRaw;
|
|
87
95
|
}): Promise<{
|
|
88
96
|
exitCode: number;
|
|
89
97
|
}>;
|
|
@@ -117,13 +125,10 @@ export interface SkillScopePlan {
|
|
|
117
125
|
name: string;
|
|
118
126
|
action: SkillScopeAction;
|
|
119
127
|
detail: string;
|
|
120
|
-
/**
|
|
121
|
-
* scope so the skill reaches every managed agent. Set when the selection was
|
|
122
|
-
* valid, whatever the action. */
|
|
128
|
+
/** Applicable agents recorded in scope.yaml. Even --all is a fixed list. */
|
|
123
129
|
scope?: AgentId[];
|
|
124
130
|
mode?: "all" | "none" | "explicit";
|
|
125
|
-
/**
|
|
126
|
-
* (trellis-scope-editing-and-auth-status design.md D3). */
|
|
131
|
+
/** Retained for API compatibility; skill lists are never normalized away. */
|
|
127
132
|
normalizedFromFull?: boolean;
|
|
128
133
|
/** The agents that will actually receive the skill afterwards. */
|
|
129
134
|
effective?: readonly AgentId[];
|
package/dist/commands/skill.js
CHANGED
|
@@ -14,14 +14,17 @@ import { dirname } from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { decideDirImport } from "../lib/dirEquals.js";
|
|
16
16
|
import { findSkillFile } from "../lib/skillFile.js";
|
|
17
|
-
import { loadCanonicalSource, writeSkillScopeYaml } from "../core/canonical.js";
|
|
17
|
+
import { loadCanonicalSource, validateSkillScopeYaml, writeSkillScopeYaml } from "../core/canonical.js";
|
|
18
18
|
import { resolveScope } from "../core/types.js";
|
|
19
19
|
import { isBuiltinSkillName } from "../lib/builtinSkills.js";
|
|
20
20
|
import { openBackupSession } from "../lib/backup.js";
|
|
21
21
|
import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
|
|
22
22
|
import { remoteSkillProvenance } from "./remoteSkill.js";
|
|
23
|
-
import {
|
|
23
|
+
import { sameScope } from "../lib/scopeSelection.js";
|
|
24
24
|
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
25
|
+
import { readSkillScopes, requestSkillScope, selectSkillScope } from "../lib/skillScope.js";
|
|
26
|
+
import { isSymlinkTo } from "../lib/fsIdentity.js";
|
|
27
|
+
import { usesNativeCapabilityDelivery, ALL_AGENTS } from "../core/types.js";
|
|
25
28
|
function builtinSkillTemplate() {
|
|
26
29
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
return readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
|
|
@@ -58,17 +61,34 @@ export function runSkillUpdateBuiltin(opts = {}) {
|
|
|
58
61
|
console.log(`${opts.dryRun ? "[dry run] " : ""}skill update-builtin\n [${plan.action}] ${plan.detail}`);
|
|
59
62
|
return { exitCode: 0 };
|
|
60
63
|
}
|
|
61
|
-
export function collectSkillList(homeDir = homedir()) {
|
|
64
|
+
export function collectSkillList(homeDir = homedir(), agent) {
|
|
62
65
|
const canonical = loadCanonicalSource(homeDir);
|
|
63
66
|
const provenance = remoteSkillProvenance(homeDir);
|
|
64
|
-
|
|
67
|
+
const registry = readSkillScopes(join(homeDir, ".trellis", "scope.yaml"));
|
|
68
|
+
const roots = {
|
|
69
|
+
"claude-code": join(homeDir, ".claude", "skills"), codex: join(homeDir, ".agents", "skills"),
|
|
70
|
+
kiro: join(homeDir, ".kiro", "skills"), pi: join(homeDir, ".pi", "agent", "skills"),
|
|
71
|
+
"kimi-code": join(homeDir, ".kimi-code", "skills"), zcode: "",
|
|
72
|
+
};
|
|
73
|
+
return canonical.skills.filter((skill) => agent ? resolveScope(skill.scope, canonical.managedAgents).includes(agent) : !isBuiltinSkillName(skill.name)).map((skill) => ({
|
|
65
74
|
name: skill.name,
|
|
66
75
|
scope: resolveScope(skill.scope, canonical.managedAgents),
|
|
76
|
+
...(agent ? {
|
|
77
|
+
registered: Object.hasOwn(registry, skill.name) || isBuiltinSkillName(skill.name),
|
|
78
|
+
delivery: !usesNativeCapabilityDelivery(agent, canonical.mcp) || agent === "zcode"
|
|
79
|
+
? { state: "runtime" }
|
|
80
|
+
: { state: isSymlinkTo(join(roots[agent], skill.name), skill.dir) ? "linked"
|
|
81
|
+
: existsSync(join(roots[agent], skill.name)) ? "conflict" : "missing", path: join(roots[agent], skill.name) },
|
|
82
|
+
} : {}),
|
|
67
83
|
...(provenance[skill.name] ? { remote: provenance[skill.name] } : {}),
|
|
68
84
|
}));
|
|
69
85
|
}
|
|
70
86
|
export function runSkillList(opts = {}) {
|
|
71
|
-
|
|
87
|
+
if (opts.agent !== undefined && !ALL_AGENTS.includes(opts.agent)) {
|
|
88
|
+
console.error(`unknown agent: ${opts.agent}; choose ${ALL_AGENTS.join(", ")}`);
|
|
89
|
+
return { exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
const entries = collectSkillList(opts.homeDir ?? homedir(), opts.agent);
|
|
72
92
|
if (opts.json) {
|
|
73
93
|
console.log(JSON.stringify(entries, null, 2));
|
|
74
94
|
}
|
|
@@ -76,9 +96,9 @@ export function runSkillList(opts = {}) {
|
|
|
76
96
|
console.log("No skills in canonical source yet.");
|
|
77
97
|
}
|
|
78
98
|
else {
|
|
79
|
-
for (const { name, scope, remote } of entries) {
|
|
99
|
+
for (const { name, scope, remote, delivery } of entries) {
|
|
80
100
|
const provenance = remote ? ` — remote: ${remote.source}@${remote.commit.slice(0, 12)}` : "";
|
|
81
|
-
console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(no managed agent
|
|
101
|
+
console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(paused: no applicable managed agent)"}${delivery ? ` [${delivery.state}]` : ""}${provenance}`);
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
return { exitCode: 0 };
|
|
@@ -105,15 +125,26 @@ export function collectSkillAddPlan(name, fromPath, homeDir = homedir()) {
|
|
|
105
125
|
}
|
|
106
126
|
}
|
|
107
127
|
export function applySkillAddPlan(plan, homeDir = homedir(), backup) {
|
|
108
|
-
if (plan.action !== "create"
|
|
128
|
+
if (plan.action !== "create" && !(plan.action === "already-present" && plan.scope !== undefined))
|
|
109
129
|
return;
|
|
110
|
-
|
|
111
|
-
if (backup) {
|
|
112
|
-
backup.createDirFromSource(dest, plan.sourceDir);
|
|
130
|
+
if (plan.action === "create" && !plan.sourceDir)
|
|
113
131
|
return;
|
|
132
|
+
const scopePath = join(homeDir, ".trellis", "scope.yaml");
|
|
133
|
+
const validation = validateSkillScopeYaml(scopePath);
|
|
134
|
+
if (!validation.ok)
|
|
135
|
+
throw new Error(validation.error);
|
|
136
|
+
const dest = join(homeDir, ".trellis", "skills", plan.name);
|
|
137
|
+
if (plan.action === "create") {
|
|
138
|
+
if (backup)
|
|
139
|
+
backup.createDirFromSource(dest, plan.sourceDir);
|
|
140
|
+
else {
|
|
141
|
+
mkdirSync(dest, { recursive: true });
|
|
142
|
+
cpSync(plan.sourceDir, dest, { recursive: true });
|
|
143
|
+
}
|
|
114
144
|
}
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
const written = writeSkillScopeYaml(scopePath, plan.name, plan.scope ?? [], backup);
|
|
146
|
+
if (!written.ok)
|
|
147
|
+
throw new Error(written.error);
|
|
117
148
|
}
|
|
118
149
|
/** The workspace twin of `collectSkillAddPlan` (trellis-workspace-scope
|
|
119
150
|
* task 3.4): same source validation, destination is `.agents/skills/`
|
|
@@ -137,29 +168,40 @@ export function collectSkillAddWorkspacePlan(name, fromPath, workspaceRoot) {
|
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
export function applySkillAddWorkspacePlan(plan, workspaceRoot, backup) {
|
|
140
|
-
if (plan.action !== "create"
|
|
171
|
+
if (plan.action !== "create" && !(plan.action === "already-present" && plan.scope !== undefined))
|
|
141
172
|
return;
|
|
142
|
-
|
|
143
|
-
if (backup) {
|
|
144
|
-
backup.createDirFromSource(dest, plan.sourceDir);
|
|
173
|
+
if (plan.action === "create" && !plan.sourceDir)
|
|
145
174
|
return;
|
|
175
|
+
const scopePath = join(workspaceRoot, ".agents", "scope.yaml");
|
|
176
|
+
const validation = validateSkillScopeYaml(scopePath);
|
|
177
|
+
if (!validation.ok)
|
|
178
|
+
throw new Error(validation.error);
|
|
179
|
+
const dest = join(workspaceRoot, ".agents", "skills", plan.name);
|
|
180
|
+
if (plan.action === "create") {
|
|
181
|
+
if (backup)
|
|
182
|
+
backup.createDirFromSource(dest, plan.sourceDir);
|
|
183
|
+
else {
|
|
184
|
+
mkdirSync(dest, { recursive: true });
|
|
185
|
+
cpSync(plan.sourceDir, dest, { recursive: true });
|
|
186
|
+
}
|
|
146
187
|
}
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
const written = writeSkillScopeYaml(scopePath, plan.name, plan.scope ?? [], backup);
|
|
189
|
+
if (!written.ok)
|
|
190
|
+
throw new Error(written.error);
|
|
149
191
|
}
|
|
150
192
|
/** Same own-backup-session + cascade-sync posture as
|
|
151
193
|
* `applySkillAddWithSync`, scoped to the workspace layer. */
|
|
152
194
|
export async function applySkillAddWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
153
195
|
const homeDir = opts.homeDir ?? homedir();
|
|
154
|
-
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
196
|
+
const ownSession = !opts.dryRun && !opts.backupSession && (plan.action === "create" || plan.action === "already-present" && plan.scope !== undefined) ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
155
197
|
const session = opts.backupSession ?? ownSession;
|
|
156
|
-
if (!opts.dryRun && plan.action === "create") {
|
|
198
|
+
if (!opts.dryRun && (plan.action === "create" || plan.action === "already-present")) {
|
|
157
199
|
applySkillAddWorkspacePlan(plan, workspaceRoot, session);
|
|
158
200
|
}
|
|
159
201
|
ownSession?.finalize();
|
|
160
202
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
161
203
|
let syncReport;
|
|
162
|
-
if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
204
|
+
if (!failed && (plan.action === "create" || plan.scope !== undefined) && existsSync(join(homeDir, ".trellis"))) {
|
|
163
205
|
syncReport = await collectSyncReport({ target: "skills", homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
164
206
|
}
|
|
165
207
|
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
@@ -174,21 +216,30 @@ export async function applySkillAddWorkspaceWithSync(plan, workspaceRoot, opts =
|
|
|
174
216
|
*/
|
|
175
217
|
export async function applySkillAddWithSync(plan, opts = {}) {
|
|
176
218
|
const homeDir = opts.homeDir ?? homedir();
|
|
177
|
-
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
219
|
+
const ownSession = !opts.dryRun && !opts.backupSession && (plan.action === "create" || plan.action === "already-present" && plan.scope !== undefined) ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
178
220
|
const session = opts.backupSession ?? ownSession;
|
|
179
|
-
if (!opts.dryRun && plan.action === "create") {
|
|
221
|
+
if (!opts.dryRun && (plan.action === "create" || plan.action === "already-present")) {
|
|
180
222
|
applySkillAddPlan(plan, homeDir, session);
|
|
181
223
|
}
|
|
182
224
|
ownSession?.finalize();
|
|
183
225
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
184
226
|
let syncReport;
|
|
185
|
-
if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
227
|
+
if (!failed && (plan.action === "create" || plan.scope !== undefined) && existsSync(join(homeDir, ".trellis"))) {
|
|
186
228
|
syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
|
|
187
229
|
}
|
|
188
230
|
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
189
231
|
}
|
|
190
232
|
export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
191
233
|
const homeDir = opts.homeDir ?? homedir();
|
|
234
|
+
const managed = existsSync(join(homeDir, ".trellis")) ? loadCanonicalSource(homeDir).managedAgents : [];
|
|
235
|
+
const selected = await requestSkillScope(opts.selection ?? {}, managed, !opts.json);
|
|
236
|
+
if ("error" in selected) {
|
|
237
|
+
if (opts.json)
|
|
238
|
+
console.log(JSON.stringify({ name, action: "invalid-source", detail: selected.error }));
|
|
239
|
+
else
|
|
240
|
+
console.error(selected.error);
|
|
241
|
+
return { exitCode: 1 };
|
|
242
|
+
}
|
|
192
243
|
if (opts.workspace) {
|
|
193
244
|
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
194
245
|
if (!workspaceRoot) {
|
|
@@ -196,6 +247,7 @@ export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
|
196
247
|
return { exitCode: 1 };
|
|
197
248
|
}
|
|
198
249
|
const plan = collectSkillAddWorkspacePlan(name, fromPath, workspaceRoot);
|
|
250
|
+
plan.scope = selected.scope;
|
|
199
251
|
const { sync: syncReport } = await applySkillAddWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
200
252
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
201
253
|
if (opts.json) {
|
|
@@ -211,6 +263,7 @@ export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
|
211
263
|
return { exitCode: failed || syncConflict ? 1 : 0 };
|
|
212
264
|
}
|
|
213
265
|
const plan = collectSkillAddPlan(name, fromPath, homeDir);
|
|
266
|
+
plan.scope = selected.scope;
|
|
214
267
|
const { sync: syncReport } = await applySkillAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
|
|
215
268
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
216
269
|
if (opts.json) {
|
|
@@ -289,9 +342,10 @@ export function collectSkillScopePlan(name, raw, homeDir = homedir()) {
|
|
|
289
342
|
return { name, action: "invalid-input", detail: `"${name}" is a Trellis package-owned Skill — its scope is not editable` };
|
|
290
343
|
}
|
|
291
344
|
const canonical = loadCanonicalSource(homeDir);
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
return { name, action: "invalid-input", detail:
|
|
345
|
+
const chosen = selectSkillScope(raw, canonical.managedAgents);
|
|
346
|
+
if ("error" in chosen)
|
|
347
|
+
return { name, action: "invalid-input", detail: chosen.error };
|
|
348
|
+
const selection = { scope: chosen.scope, mode: raw.all ? "all" : raw.none ? "none" : "explicit", normalizedFromFull: false };
|
|
295
349
|
const skill = canonical.skills.find((candidate) => candidate.name === name);
|
|
296
350
|
if (!skill)
|
|
297
351
|
return { name, action: "not-found", detail: `no canonical skill named "${name}"` };
|
|
@@ -302,12 +356,11 @@ export function collectSkillScopePlan(name, raw, homeDir = homedir()) {
|
|
|
302
356
|
normalizedFromFull: selection.normalizedFromFull,
|
|
303
357
|
effective: resolveScope(selection.scope, canonical.managedAgents),
|
|
304
358
|
};
|
|
305
|
-
const summary =
|
|
306
|
-
|
|
307
|
-
if (sameScope(skill.scope, selection.scope)) {
|
|
359
|
+
const summary = selection.scope.length ? selection.scope.join(", ") : "no agent (paused)";
|
|
360
|
+
if (sameScope(skill.scope, selection.scope) && Object.hasOwn(readSkillScopes(join(homeDir, ".trellis", "scope.yaml")), name)) {
|
|
308
361
|
return { ...base, action: "already-set", detail: `scope is already ${summary}` };
|
|
309
362
|
}
|
|
310
|
-
return { ...base, action: "updated", detail: `set scope to ${summary}
|
|
363
|
+
return { ...base, action: "updated", detail: `set scope to ${summary}` };
|
|
311
364
|
}
|
|
312
365
|
/** Write inside a backup session so `trellis rollback` covers it, then the
|
|
313
366
|
* same skills sync `add` / `remove` run — narrowing is converged by
|
package/dist/commands/sync.d.ts
CHANGED
package/dist/commands/sync.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function collectSyncReport(opts = {}) {
|
|
|
83
83
|
reports.push({ agent: adapter.id, present: true, items: allItems, ...(warnings.length > 0 ? { warnings } : {}) });
|
|
84
84
|
}
|
|
85
85
|
ownSession?.finalize();
|
|
86
|
-
return { reports, ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
86
|
+
return { reports, ...(canonical.diagnostics.length ? { diagnostics: canonical.diagnostics } : {}), ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
87
87
|
}
|
|
88
88
|
export async function runSync(opts = {}) {
|
|
89
89
|
let report;
|
|
@@ -118,6 +118,8 @@ export function printReport(report, dryRun, check = false) {
|
|
|
118
118
|
console.log("[dry run]");
|
|
119
119
|
if (check)
|
|
120
120
|
console.log("[check]");
|
|
121
|
+
for (const diagnostic of report.diagnostics ?? [])
|
|
122
|
+
console.log(` ${diagnostic}`);
|
|
121
123
|
if (report.workspaceRoot) {
|
|
122
124
|
console.log(`Workspace layer: ${report.workspaceRoot} (.agents/)`);
|
|
123
125
|
}
|
package/dist/core/canonical.d.ts
CHANGED
|
@@ -27,8 +27,7 @@ export type ScopeYamlWriteResult = {
|
|
|
27
27
|
export declare function validateSkillScopeYaml(path: string): ScopeYamlWriteResult;
|
|
28
28
|
/**
|
|
29
29
|
* Sets one Skill's canonical scope without rebuilding unrelated hand-authored
|
|
30
|
-
* scope entries.
|
|
31
|
-
* therefore restores the normal all-managed default.
|
|
30
|
+
* scope entries. `undefined` removes registration, pausing distribution.
|
|
32
31
|
*/
|
|
33
32
|
export declare function writeSkillScopeYaml(path: string, name: string, scope: Scope, backup?: BackupSession): ScopeYamlWriteResult;
|
|
34
33
|
export declare function upsertServerYaml(path: string, name: string, def: McpServerDef, backup?: BackupSession): ServersYamlWriteResult;
|
package/dist/core/canonical.js
CHANGED
|
@@ -13,6 +13,8 @@ import { ALL_AGENTS, oauthClientMetadata } from "./types.js";
|
|
|
13
13
|
import { fromServerDefYaml, toServerDefYaml } from "./mcpServerDef.js";
|
|
14
14
|
import { loadWorkspaceLayer } from "../lib/workspace.js";
|
|
15
15
|
import { mergeWorkspaceLayer } from "../lib/workspaceMerge.js";
|
|
16
|
+
import { registeredSkillScope } from "../lib/skillScope.js";
|
|
17
|
+
import { isBuiltinSkillName } from "../lib/builtinSkills.js";
|
|
16
18
|
export { toServerDefYaml } from "./mcpServerDef.js";
|
|
17
19
|
function trellisRoot(homeDir) {
|
|
18
20
|
return join(homeDir, ".trellis");
|
|
@@ -166,6 +168,9 @@ export function validateSkillScopeYaml(path) {
|
|
|
166
168
|
const doc = parseDocument(readFileSync(path, "utf-8"));
|
|
167
169
|
if (doc.errors.length > 0)
|
|
168
170
|
throw new Error(doc.errors.map((error) => error.message).join("; "));
|
|
171
|
+
const skills = doc.get("skills", true);
|
|
172
|
+
if (skills !== undefined && !isMap(skills))
|
|
173
|
+
throw new Error("skills must be a map of names to agent lists");
|
|
169
174
|
return { ok: true };
|
|
170
175
|
}
|
|
171
176
|
catch (err) {
|
|
@@ -174,8 +179,7 @@ export function validateSkillScopeYaml(path) {
|
|
|
174
179
|
}
|
|
175
180
|
/**
|
|
176
181
|
* Sets one Skill's canonical scope without rebuilding unrelated hand-authored
|
|
177
|
-
* scope entries.
|
|
178
|
-
* therefore restores the normal all-managed default.
|
|
182
|
+
* scope entries. `undefined` removes registration, pausing distribution.
|
|
179
183
|
*/
|
|
180
184
|
export function writeSkillScopeYaml(path, name, scope, backup) {
|
|
181
185
|
if (!existsSync(path) && scope === undefined)
|
|
@@ -428,9 +432,6 @@ export function ensureShellEnvSource(rcPath, envFilePath, backup) {
|
|
|
428
432
|
mkdirSync(dirname(rcPath), { recursive: true });
|
|
429
433
|
persistText(rcPath, `${existing}${separator}${block}`, backup);
|
|
430
434
|
}
|
|
431
|
-
/** `undefined` if the name has no entry in scope.yaml's map — "shared with
|
|
432
|
-
* all managed agents by default. A recognized but empty list is left as
|
|
433
|
-
* authored (an explicitly agent-less scope), not coerced to "all". */
|
|
434
435
|
/** Missing file and `agents: []` both resolve to `[]` — zero managed
|
|
435
436
|
* agents, never "everyone." An unrecognized id is dropped with a
|
|
436
437
|
* diagnostic, same posture as an unrecognized scope.yaml agent id. */
|
|
@@ -455,9 +456,9 @@ function scopeFor(map, name) {
|
|
|
455
456
|
return map?.[name];
|
|
456
457
|
}
|
|
457
458
|
function validAgentIds(scope) {
|
|
458
|
-
if (
|
|
459
|
+
if (scope === undefined)
|
|
459
460
|
return true;
|
|
460
|
-
return scope.every((id) => ALL_AGENTS.includes(id));
|
|
461
|
+
return Array.isArray(scope) && scope.every((id) => ALL_AGENTS.includes(id));
|
|
461
462
|
}
|
|
462
463
|
/**
|
|
463
464
|
* Load-time literal-value scan for OAuth client metadata
|
|
@@ -497,7 +498,13 @@ export function loadCanonicalSource(arg = {}) {
|
|
|
497
498
|
const scopeYaml = loadScopeYaml(join(root, "scope.yaml"));
|
|
498
499
|
const skillDirs = listSkillDirs(join(root, "skills"));
|
|
499
500
|
const knownSkillNames = new Set(skillDirs.map((s) => s.name));
|
|
500
|
-
const skills = skillDirs.map(({ name, dir }) =>
|
|
501
|
+
const skills = skillDirs.map(({ name, dir }) => {
|
|
502
|
+
const declared = scopeFor(scopeYaml.skills, name);
|
|
503
|
+
if (declared === undefined && !isBuiltinSkillName(name)) {
|
|
504
|
+
diagnostics.push(`scope.yaml: skill "${name}" has no registered agents — distribution paused; run trellis skill scope ${name} --agents <ids>`);
|
|
505
|
+
}
|
|
506
|
+
return { name, dir, scope: isBuiltinSkillName(name) && declared === undefined ? undefined : registeredSkillScope(declared) };
|
|
507
|
+
});
|
|
501
508
|
const agentFiles = listMarkdownFiles(join(root, "agents"));
|
|
502
509
|
const knownAgentProfileNames = new Set(agentFiles.map((a) => a.name));
|
|
503
510
|
const agents = agentFiles.map(({ name, file }) => ({
|
|
@@ -523,7 +530,7 @@ export function loadCanonicalSource(arg = {}) {
|
|
|
523
530
|
diagnostics.push(`scope.yaml: "${section}.${name}" does not match any known ${section.slice(0, -1)} — ignored`);
|
|
524
531
|
}
|
|
525
532
|
else if (!validAgentIds(map?.[name])) {
|
|
526
|
-
diagnostics.push(`scope.yaml: "${section}.${name}" lists an unrecognized agent id — ignored`);
|
|
533
|
+
diagnostics.push(`scope.yaml: "${section}.${name}" lists an unrecognized agent id${section === "skills" ? " — distribution paused" : " — ignored"}`);
|
|
527
534
|
}
|
|
528
535
|
}
|
|
529
536
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -39,11 +39,9 @@ export declare function oauthClientMetadata(auth: McpAuthMode | McpAuthConfig |
|
|
|
39
39
|
export type AgentId = "claude-code" | "codex" | "kiro" | "pi" | "kimi-code" | "zcode";
|
|
40
40
|
export declare const ALL_AGENTS: readonly AgentId[];
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* exception and must be declared explicitly. See docs/architecture.md
|
|
46
|
-
* "Private / agent-specific capabilities".
|
|
42
|
+
* Omitted scope resolves to the managed set. Custom skills are registered
|
|
43
|
+
* explicitly by the canonical/workspace loaders: missing or invalid skill
|
|
44
|
+
* registration becomes `[]` (paused). Other capability defaults are unchanged.
|
|
47
45
|
*/
|
|
48
46
|
export type Scope = AgentId[] | undefined;
|
|
49
47
|
/**
|
|
@@ -69,7 +67,9 @@ export interface McpServerDef {
|
|
|
69
67
|
/** http/sse only */
|
|
70
68
|
url?: string;
|
|
71
69
|
/**
|
|
72
|
-
* http/sse only. Values are `${VAR}`
|
|
70
|
+
* http/sse only. Values are `${VAR}` or `${mxenv://profile/KEY}` templates,
|
|
71
|
+
* the latter requiring Trellis-hosted resolution (gateway/runtime/pi).
|
|
72
|
+
* Same discipline as
|
|
73
73
|
* `env` — never a real value. Codex has no generic headers concept;
|
|
74
74
|
* only the single shape `{ Authorization: "Bearer ${VAR}" }` is
|
|
75
75
|
* expressible there (trellis-mcp-transport-auth design.md D4) — any
|
|
@@ -190,7 +190,8 @@ export interface SkillRef {
|
|
|
190
190
|
* filename is case-sensitive on at least one target agent (Codex). */
|
|
191
191
|
dir: string;
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
193
|
+
* Explicit applicable agents; unregistered custom skills load as `[]`.
|
|
194
|
+
* Package-owned built-ins may supply their own default. NOT read from
|
|
194
195
|
* SKILL.md's own frontmatter — Codex validates SKILL.md frontmatter
|
|
195
196
|
* against an allow-list of known keys and rejects unknown ones (see
|
|
196
197
|
* docs/research.md), so a Trellis-only field embedded there would break
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* still more machinery than this one narrow extraction needs.
|
|
8
8
|
*/
|
|
9
9
|
import type { McpServerDef } from "../core/types.js";
|
|
10
|
-
|
|
10
|
+
export declare function replaceTemplateVars(value: string, replace: (entry: string) => string): string;
|
|
11
|
+
/** Report only the header's name, never a potentially sensitive template. */
|
|
12
|
+
export declare function invalidHeaderTemplate(headers: Record<string, string> | undefined): string | undefined;
|
|
13
|
+
/** Every `${NAME}` or `${mxenv://profile/KEY}` occurrence in a string,
|
|
14
|
+
* including malformed placeholders for validation — a header value can in
|
|
11
15
|
* principle reference more than one name (trellis-mcp-transport-auth
|
|
12
16
|
* design.md D6), same extraction `env`'s single-name-per-value case
|
|
13
17
|
* already needed, just generalized. */
|
package/dist/lib/envVarNames.js
CHANGED
|
@@ -6,8 +6,21 @@
|
|
|
6
6
|
* there is no round-trip-fidelity concern here, but a full parser is
|
|
7
7
|
* still more machinery than this one narrow extraction needs.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Keep malformed placeholders visible too: a misspelled reference scheme
|
|
10
|
+
// must reach validation rather than silently survive as literal header text.
|
|
11
|
+
const TEMPLATE_VAR_RE = /\$\{([^{}]*)\}/g;
|
|
12
|
+
export function replaceTemplateVars(value, replace) {
|
|
13
|
+
return value.replace(TEMPLATE_VAR_RE, (_match, entry) => replace(entry));
|
|
14
|
+
}
|
|
15
|
+
/** Report only the header's name, never a potentially sensitive template. */
|
|
16
|
+
export function invalidHeaderTemplate(headers) {
|
|
17
|
+
return Object.entries(headers ?? {}).find(([, value]) => {
|
|
18
|
+
const literal = replaceTemplateVars(value, () => "");
|
|
19
|
+
return literal.includes("${") || /mx-?env:\/\//.test(literal);
|
|
20
|
+
})?.[0];
|
|
21
|
+
}
|
|
22
|
+
/** Every `${NAME}` or `${mxenv://profile/KEY}` occurrence in a string,
|
|
23
|
+
* including malformed placeholders for validation — a header value can in
|
|
11
24
|
* principle reference more than one name (trellis-mcp-transport-auth
|
|
12
25
|
* design.md D6), same extraction `env`'s single-name-per-value case
|
|
13
26
|
* already needed, just generalized. */
|
|
@@ -22,7 +35,14 @@ export function extractTemplateVarNames(value) {
|
|
|
22
35
|
* same answer to "what names does this def reference," not two
|
|
23
36
|
* separately-maintained collections. */
|
|
24
37
|
export function declaredEnvNames(def) {
|
|
38
|
+
// RAW entries, including `envAliases` sources (mx-env-secret-source
|
|
39
|
+
// secrets-audit delta): consumers that RESOLVE must see the original
|
|
40
|
+
// `mxenv://` text so references go through composition, then look the
|
|
41
|
+
// result up by delivered name; consumers that want the delivered NAMES
|
|
42
|
+
// map through `deliveredName` at their own use site (e.g. Kiro).
|
|
25
43
|
const names = new Set(def.env ?? []);
|
|
44
|
+
for (const source of Object.values(def.envAliases ?? {}))
|
|
45
|
+
names.add(source);
|
|
26
46
|
for (const value of Object.values(def.headers ?? {})) {
|
|
27
47
|
for (const name of extractTemplateVarNames(value))
|
|
28
48
|
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
|
/**
|