agent-trellis 0.21.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/mcpPlan.d.ts +3 -1
- package/dist/adapters/mcpPlan.js +43 -16
- package/dist/cli.js +9 -6
- package/dist/commands/mcpGateway.js +1 -1
- package/dist/commands/migrate.js +11 -1
- package/dist/commands/remoteSkill.js +18 -1
- package/dist/commands/secretsAudit.js +17 -4
- 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 +15 -2
- package/dist/lib/mcpConnect.js +18 -4
- package/dist/lib/secretEnv.d.ts +8 -4
- package/dist/lib/secretEnv.js +44 -15
- package/dist/lib/skillScope.d.ts +14 -0
- package/dist/lib/skillScope.js +40 -0
- package/dist/lib/workspace.js +7 -2
- package/dist/pi-bridge/bundle.js +164 -58
- package/docs/architecture.md +27 -7
- package/docs/getting-started.md +29 -9
- package/docs/research.md +15 -2
- package/package.json +1 -1
- package/schema/scope.example.yaml +15 -3
- package/schema/servers.example.yaml +6 -0
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. */
|
package/dist/lib/mcpConnect.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Client, StreamableHTTPClientTransport, SSEClientTransport } from "@modelcontextprotocol/client";
|
|
16
16
|
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
|
|
17
|
-
import { deliveredName, resolveSecretEnv } from "./secretEnv.js";
|
|
18
|
-
import { extractTemplateVarNames } from "./envVarNames.js";
|
|
17
|
+
import { deliveredName, isValidSecretVarName, parseEnvEntry, resolveSecretEntries, resolveSecretEnv } from "./secretEnv.js";
|
|
18
|
+
import { extractTemplateVarNames, invalidHeaderTemplate, replaceTemplateVars } from "./envVarNames.js";
|
|
19
19
|
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
20
20
|
const FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1_000;
|
|
21
21
|
/**
|
|
@@ -105,11 +105,25 @@ export async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
105
105
|
export async function resolveHeaders(def, secretsPolicy) {
|
|
106
106
|
if (!def.headers || Object.keys(def.headers).length === 0)
|
|
107
107
|
return undefined;
|
|
108
|
+
const invalidHeader = invalidHeaderTemplate(def.headers);
|
|
109
|
+
if (invalidHeader !== undefined)
|
|
110
|
+
throw new Error(`[trellis] invalid reference template in header ${invalidHeader} — use \${VAR} or \${mxenv://<profile>/<KEY>}`);
|
|
108
111
|
const names = Object.keys(def.headers).flatMap((key) => extractTemplateVarNames(def.headers[key]));
|
|
109
|
-
const
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
const entry = parseEnvEntry(name);
|
|
114
|
+
if (entry.kind === "invalid" || (entry.kind === "name" && !isValidSecretVarName(name))) {
|
|
115
|
+
throw new Error(`[trellis] invalid header reference: ${name} — expected a variable name or mxenv://<profile>/<KEY>`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const resolved = await resolveSecretEntries(names, secretsPolicy);
|
|
110
119
|
const result = {};
|
|
111
120
|
for (const [key, template] of Object.entries(def.headers)) {
|
|
112
|
-
result[key] = template
|
|
121
|
+
result[key] = replaceTemplateVars(template, (name) => {
|
|
122
|
+
if (parseEnvEntry(name).kind === "ref" && !resolved[name]) {
|
|
123
|
+
throw new Error(`[trellis] unresolved header reference: ${name}`);
|
|
124
|
+
}
|
|
125
|
+
return resolved[name] ?? "";
|
|
126
|
+
});
|
|
113
127
|
}
|
|
114
128
|
return result;
|
|
115
129
|
}
|
package/dist/lib/secretEnv.d.ts
CHANGED
|
@@ -50,9 +50,6 @@ export type SecretEnvEntry = {
|
|
|
50
50
|
export declare function deliveredName(entry: string): string;
|
|
51
51
|
export declare function parseEnvEntry(entry: string): SecretEnvEntry;
|
|
52
52
|
/**
|
|
53
|
-
* Result keyed by DELIVERED name: the `KEY` segment for a reference,
|
|
54
|
-
* the entry itself for a bare name (mx-env-secret-source spec).
|
|
55
|
-
*
|
|
56
53
|
* `policy.envFile` set: it is the SOLE source for bare names — a name
|
|
57
54
|
* absent from it resolves to `undefined`, `process.env` is never
|
|
58
55
|
* consulted (a silent fallback would defeat the isolation this exists
|
|
@@ -66,7 +63,14 @@ export declare function parseEnvEntry(entry: string): SecretEnvEntry;
|
|
|
66
63
|
* before this point (the bridge only ever sees pre-checked defs).
|
|
67
64
|
* Compositions are cached per batch (one `ReferenceResolver` per call,
|
|
68
65
|
* not per name).
|
|
69
|
-
|
|
66
|
+
* Raw-entry keys preserve provenance when headers mix two profiles with
|
|
67
|
+
* the same KEY, or a reference and a bare name. Values stay in memory. */
|
|
68
|
+
export declare function resolveSecretEntries(entries: string[], policy: SecretsPolicy): Promise<Record<string, string | undefined>>;
|
|
69
|
+
export declare class SecretReferenceError extends Error {
|
|
70
|
+
readonly entry: string;
|
|
71
|
+
constructor(entry: string);
|
|
72
|
+
}
|
|
73
|
+
/** Compatibility view keyed by delivered name (KEY for a reference). */
|
|
70
74
|
export declare function resolveSecretEnv(entries: string[], policy: SecretsPolicy): Promise<Record<string, string | undefined>>;
|
|
71
75
|
export type WriteLocalSecretOutcome = "created" | "already-present" | "conflict";
|
|
72
76
|
/**
|
package/dist/lib/secretEnv.js
CHANGED
|
@@ -51,7 +51,7 @@ export function deliveredName(entry) {
|
|
|
51
51
|
return parsed.kind === "invalid" ? entry : parsed.delivered;
|
|
52
52
|
}
|
|
53
53
|
export function parseEnvEntry(entry) {
|
|
54
|
-
if (entry.includes("
|
|
54
|
+
if (entry.includes("://")) {
|
|
55
55
|
try {
|
|
56
56
|
const refs = findMxEnvReferences(entry);
|
|
57
57
|
if (refs.length === 1 && `mxenv://${refs[0].profile}/${refs[0].key}` === entry) {
|
|
@@ -66,9 +66,6 @@ export function parseEnvEntry(entry) {
|
|
|
66
66
|
return { kind: "name", delivered: entry };
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Result keyed by DELIVERED name: the `KEY` segment for a reference,
|
|
70
|
-
* the entry itself for a bare name (mx-env-secret-source spec).
|
|
71
|
-
*
|
|
72
69
|
* `policy.envFile` set: it is the SOLE source for bare names — a name
|
|
73
70
|
* absent from it resolves to `undefined`, `process.env` is never
|
|
74
71
|
* consulted (a silent fallback would defeat the isolation this exists
|
|
@@ -82,27 +79,38 @@ export function parseEnvEntry(entry) {
|
|
|
82
79
|
* before this point (the bridge only ever sees pre-checked defs).
|
|
83
80
|
* Compositions are cached per batch (one `ReferenceResolver` per call,
|
|
84
81
|
* not per name).
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
* Raw-entry keys preserve provenance when headers mix two profiles with
|
|
83
|
+
* the same KEY, or a reference and a bare name. Values stay in memory. */
|
|
84
|
+
export async function resolveSecretEntries(entries, policy) {
|
|
87
85
|
const parsed = entries.map(parseEnvEntry);
|
|
88
86
|
const invalid = parsed.find((p) => p.kind === "invalid");
|
|
89
87
|
if (invalid) {
|
|
90
88
|
throw new Error(`[trellis] invalid mxenv:// reference entry: ${invalid.entry} — expected mxenv://<profile>/<KEY>`);
|
|
91
89
|
}
|
|
92
90
|
const result = {};
|
|
93
|
-
//
|
|
94
|
-
// references are authoritative — a composed value never loses to a bare
|
|
95
|
-
// name's ambient/file value (ambient must not shadow references); a
|
|
96
|
-
// DEFINED value never loses to undefined; within one class the first
|
|
97
|
-
// declaration wins; bare names only fill keys nothing has filled.
|
|
91
|
+
// Cache compositions once per batch; raw keys keep profiles distinct.
|
|
98
92
|
const refs = parsed.filter((p) => p.kind === "ref");
|
|
99
93
|
if (refs.length > 0) {
|
|
100
|
-
|
|
94
|
+
let resolver;
|
|
95
|
+
try {
|
|
96
|
+
resolver = new ReferenceResolver(loadConfigLayers({ cwd: homedir(), homeDir: homedir() }), {});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new SecretReferenceError(entries[parsed.indexOf(refs[0])]);
|
|
100
|
+
}
|
|
101
101
|
for (const ref of refs) {
|
|
102
|
-
|
|
102
|
+
const entry = `mxenv://${ref.profile}/${ref.key}`;
|
|
103
|
+
if (Object.hasOwn(result, entry))
|
|
103
104
|
continue;
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
try {
|
|
106
|
+
const { value } = await resolver.valueOf(ref.profile, ref.key);
|
|
107
|
+
result[entry] = value;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Provider errors may contain credential material. Report only the
|
|
111
|
+
// reference, never propagate provider output to logs or audit.
|
|
112
|
+
throw new SecretReferenceError(entry);
|
|
113
|
+
}
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
const names = parsed.filter((p) => p.kind === "name");
|
|
@@ -116,6 +124,27 @@ export async function resolveSecretEnv(entries, policy) {
|
|
|
116
124
|
}
|
|
117
125
|
return result;
|
|
118
126
|
}
|
|
127
|
+
export class SecretReferenceError extends Error {
|
|
128
|
+
entry;
|
|
129
|
+
constructor(entry) {
|
|
130
|
+
super(`[trellis] could not resolve mxenv reference ${entry} — check the profile, KEY and provider configuration`);
|
|
131
|
+
this.entry = entry;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Compatibility view keyed by delivered name (KEY for a reference). */
|
|
135
|
+
export async function resolveSecretEnv(entries, policy) {
|
|
136
|
+
const values = await resolveSecretEntries(entries, policy);
|
|
137
|
+
const result = {};
|
|
138
|
+
// Preserve the existing delivered-name collision rules for env/envAliases.
|
|
139
|
+
const ordered = [...entries.filter((entry) => parseEnvEntry(entry).kind === "ref"),
|
|
140
|
+
...entries.filter((entry) => parseEnvEntry(entry).kind !== "ref")];
|
|
141
|
+
for (const entry of ordered) {
|
|
142
|
+
const name = deliveredName(entry);
|
|
143
|
+
if (result[name] === undefined)
|
|
144
|
+
result[name] = values[entry];
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
119
148
|
/**
|
|
120
149
|
* `trellis migrate`'s static-env secret extraction
|
|
121
150
|
* (trellis-migrate-extract-static-env-secrets design.md D4) — appends
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AgentId } from "../core/types.js";
|
|
2
|
+
import { type ScopeSelectorRaw } from "./scopeSelection.js";
|
|
3
|
+
export declare function registeredSkillScope(value: unknown): AgentId[];
|
|
4
|
+
export declare function readSkillScopes(path: string): Record<string, unknown>;
|
|
5
|
+
export declare function selectSkillScope(raw: ScopeSelectorRaw, managed: readonly AgentId[]): {
|
|
6
|
+
scope: AgentId[];
|
|
7
|
+
} | {
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function requestSkillScope(raw: ScopeSelectorRaw, managed: readonly AgentId[], interactive: boolean): Promise<{
|
|
11
|
+
scope: AgentId[];
|
|
12
|
+
} | {
|
|
13
|
+
error: string;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Skill scope is a registry, not an exceptions list. Unregistered or
|
|
2
|
+
* invalid entries stay paused until applicable agents are selected. */
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { ALL_AGENTS } from "../core/types.js";
|
|
6
|
+
import { resolveScopeSelection } from "./scopeSelection.js";
|
|
7
|
+
import { canUseInteractivePicker, runMultiSelectPicker } from "./terminalPicker.js";
|
|
8
|
+
export function registeredSkillScope(value) {
|
|
9
|
+
return Array.isArray(value) && value.every((id) => ALL_AGENTS.includes(id)) ? [...new Set(value)] : [];
|
|
10
|
+
}
|
|
11
|
+
export function readSkillScopes(path) {
|
|
12
|
+
if (!existsSync(path))
|
|
13
|
+
return {};
|
|
14
|
+
const doc = parseYaml(readFileSync(path, "utf8"));
|
|
15
|
+
if (doc?.skills === undefined)
|
|
16
|
+
return {};
|
|
17
|
+
if (typeof doc.skills !== "object" || doc.skills === null || Array.isArray(doc.skills)) {
|
|
18
|
+
throw new Error(`${path}: skills must be a map of skill names to agent lists`);
|
|
19
|
+
}
|
|
20
|
+
return doc.skills;
|
|
21
|
+
}
|
|
22
|
+
export function selectSkillScope(raw, managed) {
|
|
23
|
+
const selected = resolveScopeSelection(raw, managed);
|
|
24
|
+
if (!selected.ok)
|
|
25
|
+
return { error: selected.detail };
|
|
26
|
+
// Even "all" records today's set. Managing another agent later must
|
|
27
|
+
// never expand the compatibility decision made for an existing skill.
|
|
28
|
+
return { scope: selected.scope ?? [...managed] };
|
|
29
|
+
}
|
|
30
|
+
export async function requestSkillScope(raw, managed, interactive) {
|
|
31
|
+
if (raw.agents !== undefined || raw.all || raw.none)
|
|
32
|
+
return selectSkillScope(raw, managed);
|
|
33
|
+
if (!interactive || !canUseInteractivePicker()) {
|
|
34
|
+
return { error: "choose applicable agents with --agents <ids>, --all or --none; no scope is selected automatically" };
|
|
35
|
+
}
|
|
36
|
+
if (managed.length === 0)
|
|
37
|
+
return { error: "no managed agents — run trellis onboard, or use --none to import a paused skill" };
|
|
38
|
+
const indices = await runMultiSelectPicker([...managed], managed.map(() => false), undefined, "选择这个 skill 适用的 agent(不选则暂停分发)");
|
|
39
|
+
return indices === null ? { error: "skill import cancelled" } : { scope: indices.map((index) => managed[index]) };
|
|
40
|
+
}
|
package/dist/lib/workspace.js
CHANGED
|
@@ -12,6 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { oauthClientMetadata } from "../core/types.js";
|
|
14
14
|
import { fromServerDefYaml } from "../core/mcpServerDef.js";
|
|
15
|
+
import { readSkillScopes, registeredSkillScope } from "./skillScope.js";
|
|
15
16
|
export const WORKSPACE_DIR_NAME = ".agents";
|
|
16
17
|
/** The only `config.json` schema version this implementation understands. */
|
|
17
18
|
export const WORKSPACE_CONFIG_VERSION = 1;
|
|
@@ -103,6 +104,7 @@ export function loadWorkspaceLayer(workspaceDir, policy) {
|
|
|
103
104
|
layer.instructionsFile = instructionsPath;
|
|
104
105
|
}
|
|
105
106
|
const skillsRoot = join(layerRoot, "skills");
|
|
107
|
+
const scopes = readSkillScopes(join(layerRoot, "scope.yaml"));
|
|
106
108
|
let skillEntries = [];
|
|
107
109
|
try {
|
|
108
110
|
skillEntries = readdirSync(skillsRoot);
|
|
@@ -113,8 +115,11 @@ export function loadWorkspaceLayer(workspaceDir, policy) {
|
|
|
113
115
|
for (const name of skillEntries) {
|
|
114
116
|
const dir = join(skillsRoot, name);
|
|
115
117
|
try {
|
|
116
|
-
if (statSync(dir).isDirectory())
|
|
117
|
-
layer.skills.push({ name, dir });
|
|
118
|
+
if (statSync(dir).isDirectory()) {
|
|
119
|
+
layer.skills.push({ name, dir, scope: registeredSkillScope(scopes[name]) });
|
|
120
|
+
if (!Object.hasOwn(scopes, name))
|
|
121
|
+
diagnostics.push(`.agents/scope.yaml: skill "${name}" has no registered agents — Trellis distribution paused`);
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
catch {
|
|
120
125
|
// unreadable/broken entry — skip, not a load failure
|