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/pi-bridge/bundle.js
CHANGED
|
@@ -36608,7 +36608,7 @@ var require_dist_cjs18 = __commonJS({
|
|
|
36608
36608
|
"node_modules/@aws-sdk/credential-provider-web-identity/dist-cjs/index.js"(exports) {
|
|
36609
36609
|
var { setCredentialFeature: setCredentialFeature2 } = (init_client3(), __toCommonJS(client_exports2));
|
|
36610
36610
|
var { CredentialsProviderError: CredentialsProviderError2, externalDataInterceptor: externalDataInterceptor2 } = (init_config2(), __toCommonJS(config_exports));
|
|
36611
|
-
var { readFileSync:
|
|
36611
|
+
var { readFileSync: readFileSync7 } = __require("node:fs");
|
|
36612
36612
|
var fromWebToken = (init) => async (awsIdentityProperties) => {
|
|
36613
36613
|
init.logger?.debug("@aws-sdk/credential-provider-web-identity - fromWebToken");
|
|
36614
36614
|
const { roleArn, roleSessionName, webIdentityToken, providerId, policyArns, policy, durationSeconds } = init;
|
|
@@ -36649,7 +36649,7 @@ var require_dist_cjs18 = __commonJS({
|
|
|
36649
36649
|
}
|
|
36650
36650
|
const credentials = await fromWebToken({
|
|
36651
36651
|
...init,
|
|
36652
|
-
webIdentityToken: externalDataInterceptor2?.getTokenRecord?.()[webIdentityTokenFile] ??
|
|
36652
|
+
webIdentityToken: externalDataInterceptor2?.getTokenRecord?.()[webIdentityTokenFile] ?? readFileSync7(webIdentityTokenFile, { encoding: "ascii" }),
|
|
36653
36653
|
roleArn,
|
|
36654
36654
|
roleSessionName
|
|
36655
36655
|
})(awsIdentityProperties);
|
|
@@ -47325,8 +47325,8 @@ var require_cross_spawn = __commonJS({
|
|
|
47325
47325
|
import { homedir as homedir4 } from "node:os";
|
|
47326
47326
|
|
|
47327
47327
|
// src/core/canonical.ts
|
|
47328
|
-
var
|
|
47329
|
-
import { existsSync as
|
|
47328
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
47329
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
47330
47330
|
import { homedir as homedir3 } from "node:os";
|
|
47331
47331
|
import { basename, dirname as dirname2, join as join6 } from "node:path";
|
|
47332
47332
|
|
|
@@ -47387,7 +47387,7 @@ function deliveredName(entry) {
|
|
|
47387
47387
|
return parsed.kind === "invalid" ? entry : parsed.delivered;
|
|
47388
47388
|
}
|
|
47389
47389
|
function parseEnvEntry(entry) {
|
|
47390
|
-
if (entry.includes("
|
|
47390
|
+
if (entry.includes("://")) {
|
|
47391
47391
|
try {
|
|
47392
47392
|
const refs = (0, import_mxenv_core.findMxEnvReferences)(entry);
|
|
47393
47393
|
if (refs.length === 1 && `mxenv://${refs[0].profile}/${refs[0].key}` === entry) {
|
|
@@ -47399,7 +47399,7 @@ function parseEnvEntry(entry) {
|
|
|
47399
47399
|
}
|
|
47400
47400
|
return { kind: "name", delivered: entry };
|
|
47401
47401
|
}
|
|
47402
|
-
async function
|
|
47402
|
+
async function resolveSecretEntries(entries, policy) {
|
|
47403
47403
|
const parsed = entries.map(parseEnvEntry);
|
|
47404
47404
|
const invalid = parsed.find((p5) => p5.kind === "invalid");
|
|
47405
47405
|
if (invalid) {
|
|
@@ -47408,11 +47408,21 @@ async function resolveSecretEnv(entries, policy) {
|
|
|
47408
47408
|
const result = {};
|
|
47409
47409
|
const refs = parsed.filter((p5) => p5.kind === "ref");
|
|
47410
47410
|
if (refs.length > 0) {
|
|
47411
|
-
|
|
47411
|
+
let resolver;
|
|
47412
|
+
try {
|
|
47413
|
+
resolver = new import_mxenv_core.ReferenceResolver((0, import_mxenv_core.loadConfigLayers)({ cwd: homedir2(), homeDir: homedir2() }), {});
|
|
47414
|
+
} catch {
|
|
47415
|
+
throw new SecretReferenceError(entries[parsed.indexOf(refs[0])]);
|
|
47416
|
+
}
|
|
47412
47417
|
for (const ref of refs) {
|
|
47413
|
-
|
|
47414
|
-
|
|
47415
|
-
|
|
47418
|
+
const entry = `mxenv://${ref.profile}/${ref.key}`;
|
|
47419
|
+
if (Object.hasOwn(result, entry)) continue;
|
|
47420
|
+
try {
|
|
47421
|
+
const { value } = await resolver.valueOf(ref.profile, ref.key);
|
|
47422
|
+
result[entry] = value;
|
|
47423
|
+
} catch {
|
|
47424
|
+
throw new SecretReferenceError(entry);
|
|
47425
|
+
}
|
|
47416
47426
|
}
|
|
47417
47427
|
}
|
|
47418
47428
|
const names = parsed.filter((p5) => p5.kind === "name");
|
|
@@ -47425,6 +47435,26 @@ async function resolveSecretEnv(entries, policy) {
|
|
|
47425
47435
|
}
|
|
47426
47436
|
return result;
|
|
47427
47437
|
}
|
|
47438
|
+
var SecretReferenceError = class extends Error {
|
|
47439
|
+
constructor(entry) {
|
|
47440
|
+
super(`[trellis] could not resolve mxenv reference ${entry} \u2014 check the profile, KEY and provider configuration`);
|
|
47441
|
+
this.entry = entry;
|
|
47442
|
+
}
|
|
47443
|
+
entry;
|
|
47444
|
+
};
|
|
47445
|
+
async function resolveSecretEnv(entries, policy) {
|
|
47446
|
+
const values = await resolveSecretEntries(entries, policy);
|
|
47447
|
+
const result = {};
|
|
47448
|
+
const ordered = [
|
|
47449
|
+
...entries.filter((entry) => parseEnvEntry(entry).kind === "ref"),
|
|
47450
|
+
...entries.filter((entry) => parseEnvEntry(entry).kind !== "ref")
|
|
47451
|
+
];
|
|
47452
|
+
for (const entry of ordered) {
|
|
47453
|
+
const name = deliveredName(entry);
|
|
47454
|
+
if (result[name] === void 0) result[name] = values[entry];
|
|
47455
|
+
}
|
|
47456
|
+
return result;
|
|
47457
|
+
}
|
|
47428
47458
|
|
|
47429
47459
|
// src/core/mcpServerDef.ts
|
|
47430
47460
|
function fromServerDefYaml(serverName, def, source = "mcp/servers.yaml") {
|
|
@@ -47467,8 +47497,26 @@ function fromAuthYaml(serverName, auth2, source) {
|
|
|
47467
47497
|
}
|
|
47468
47498
|
|
|
47469
47499
|
// src/lib/workspace.ts
|
|
47470
|
-
import { existsSync as
|
|
47500
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
|
|
47471
47501
|
import { dirname, join as join5, resolve } from "node:path";
|
|
47502
|
+
|
|
47503
|
+
// src/lib/skillScope.ts
|
|
47504
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
47505
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
47506
|
+
function registeredSkillScope(value) {
|
|
47507
|
+
return Array.isArray(value) && value.every((id) => ALL_AGENTS.includes(id)) ? [...new Set(value)] : [];
|
|
47508
|
+
}
|
|
47509
|
+
function readSkillScopes(path) {
|
|
47510
|
+
if (!existsSync2(path)) return {};
|
|
47511
|
+
const doc = (0, import_yaml.parse)(readFileSync2(path, "utf8"));
|
|
47512
|
+
if (doc?.skills === void 0) return {};
|
|
47513
|
+
if (typeof doc.skills !== "object" || doc.skills === null || Array.isArray(doc.skills)) {
|
|
47514
|
+
throw new Error(`${path}: skills must be a map of skill names to agent lists`);
|
|
47515
|
+
}
|
|
47516
|
+
return doc.skills;
|
|
47517
|
+
}
|
|
47518
|
+
|
|
47519
|
+
// src/lib/workspace.ts
|
|
47472
47520
|
var WORKSPACE_DIR_NAME = ".agents";
|
|
47473
47521
|
var WORKSPACE_CONFIG_VERSION = 1;
|
|
47474
47522
|
var CONFIG_SOURCE_LABEL = ".agents/config.json";
|
|
@@ -47480,10 +47528,10 @@ function loadWorkspaceLayer(workspaceDir, policy) {
|
|
|
47480
47528
|
const diagnostics = [];
|
|
47481
47529
|
const layer = { root: workspaceDir, servers: {}, tombstones: [], skills: [], diagnostics };
|
|
47482
47530
|
const configPath = join5(layerRoot, "config.json");
|
|
47483
|
-
if (
|
|
47531
|
+
if (existsSync3(configPath)) {
|
|
47484
47532
|
let parsed;
|
|
47485
47533
|
try {
|
|
47486
|
-
parsed = JSON.parse(
|
|
47534
|
+
parsed = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
47487
47535
|
} catch (err) {
|
|
47488
47536
|
throw new Error(`${CONFIG_SOURCE_LABEL}: invalid JSON \u2014 ${err.message}`);
|
|
47489
47537
|
}
|
|
@@ -47524,10 +47572,11 @@ function loadWorkspaceLayer(workspaceDir, policy) {
|
|
|
47524
47572
|
}
|
|
47525
47573
|
}
|
|
47526
47574
|
const instructionsPath = join5(layerRoot, "AGENTS.md");
|
|
47527
|
-
if (
|
|
47575
|
+
if (existsSync3(instructionsPath)) {
|
|
47528
47576
|
layer.instructionsFile = instructionsPath;
|
|
47529
47577
|
}
|
|
47530
47578
|
const skillsRoot = join5(layerRoot, "skills");
|
|
47579
|
+
const scopes = readSkillScopes(join5(layerRoot, "scope.yaml"));
|
|
47531
47580
|
let skillEntries = [];
|
|
47532
47581
|
try {
|
|
47533
47582
|
skillEntries = readdirSync(skillsRoot);
|
|
@@ -47536,7 +47585,10 @@ function loadWorkspaceLayer(workspaceDir, policy) {
|
|
|
47536
47585
|
for (const name of skillEntries) {
|
|
47537
47586
|
const dir = join5(skillsRoot, name);
|
|
47538
47587
|
try {
|
|
47539
|
-
if (statSync(dir).isDirectory())
|
|
47588
|
+
if (statSync(dir).isDirectory()) {
|
|
47589
|
+
layer.skills.push({ name, dir, scope: registeredSkillScope(scopes[name]) });
|
|
47590
|
+
if (!Object.hasOwn(scopes, name)) diagnostics.push(`.agents/scope.yaml: skill "${name}" has no registered agents \u2014 Trellis distribution paused`);
|
|
47591
|
+
}
|
|
47540
47592
|
} catch {
|
|
47541
47593
|
}
|
|
47542
47594
|
}
|
|
@@ -47619,6 +47671,12 @@ function mergeWorkspaceLayer(base, layer) {
|
|
|
47619
47671
|
};
|
|
47620
47672
|
}
|
|
47621
47673
|
|
|
47674
|
+
// src/lib/builtinSkills.ts
|
|
47675
|
+
var TRELLIS_RUNTIME_SKILL_NAME = "trellis-runtime";
|
|
47676
|
+
function isBuiltinSkillName(name) {
|
|
47677
|
+
return name === TRELLIS_RUNTIME_SKILL_NAME;
|
|
47678
|
+
}
|
|
47679
|
+
|
|
47622
47680
|
// src/core/canonical.ts
|
|
47623
47681
|
function trellisRoot(homeDir) {
|
|
47624
47682
|
return join6(homeDir, ".trellis");
|
|
@@ -47652,17 +47710,17 @@ function listSkillDirs(dir) {
|
|
|
47652
47710
|
return skills;
|
|
47653
47711
|
}
|
|
47654
47712
|
function loadScopeYaml(path) {
|
|
47655
|
-
if (!
|
|
47713
|
+
if (!existsSync4(path)) {
|
|
47656
47714
|
return {};
|
|
47657
47715
|
}
|
|
47658
|
-
const parsed = (0,
|
|
47716
|
+
const parsed = (0, import_yaml2.parse)(readFileSync4(path, "utf-8"));
|
|
47659
47717
|
return parsed ?? {};
|
|
47660
47718
|
}
|
|
47661
47719
|
function loadServersYaml(path) {
|
|
47662
|
-
if (!
|
|
47720
|
+
if (!existsSync4(path)) {
|
|
47663
47721
|
return { servers: {}, knownHostInjected: [] };
|
|
47664
47722
|
}
|
|
47665
|
-
const parsed = (0,
|
|
47723
|
+
const parsed = (0, import_yaml2.parse)(readFileSync4(path, "utf-8")) ?? {};
|
|
47666
47724
|
const servers = Object.fromEntries(Object.entries(parsed.servers ?? {}).map(([name, def]) => [name, fromServerDefYaml(name, def)]));
|
|
47667
47725
|
return {
|
|
47668
47726
|
servers,
|
|
@@ -47703,10 +47761,10 @@ function fromGatewayYaml(gateway) {
|
|
|
47703
47761
|
return out;
|
|
47704
47762
|
}
|
|
47705
47763
|
function loadSecretsPolicyYaml(path, homeDir) {
|
|
47706
|
-
if (!
|
|
47764
|
+
if (!existsSync4(path)) {
|
|
47707
47765
|
return { allowedVars: [], rejectPatterns: [] };
|
|
47708
47766
|
}
|
|
47709
|
-
const parsed = (0,
|
|
47767
|
+
const parsed = (0, import_yaml2.parse)(readFileSync4(path, "utf-8")) ?? {};
|
|
47710
47768
|
return {
|
|
47711
47769
|
allowedVars: parsed.allowed_vars ?? [],
|
|
47712
47770
|
rejectPatterns: (parsed.reject_patterns ?? []).map((pattern) => new RegExp(pattern)),
|
|
@@ -47714,10 +47772,10 @@ function loadSecretsPolicyYaml(path, homeDir) {
|
|
|
47714
47772
|
};
|
|
47715
47773
|
}
|
|
47716
47774
|
function loadManagedYaml(path, diagnostics) {
|
|
47717
|
-
if (!
|
|
47775
|
+
if (!existsSync4(path)) {
|
|
47718
47776
|
return [];
|
|
47719
47777
|
}
|
|
47720
|
-
const parsed = (0,
|
|
47778
|
+
const parsed = (0, import_yaml2.parse)(readFileSync4(path, "utf-8")) ?? {};
|
|
47721
47779
|
const raw = parsed.agents ?? [];
|
|
47722
47780
|
const valid = [];
|
|
47723
47781
|
for (const id of raw) {
|
|
@@ -47733,8 +47791,8 @@ function scopeFor(map3, name) {
|
|
|
47733
47791
|
return map3?.[name];
|
|
47734
47792
|
}
|
|
47735
47793
|
function validAgentIds(scope) {
|
|
47736
|
-
if (
|
|
47737
|
-
return scope.every((id) => ALL_AGENTS.includes(id));
|
|
47794
|
+
if (scope === void 0) return true;
|
|
47795
|
+
return Array.isArray(scope) && scope.every((id) => ALL_AGENTS.includes(id));
|
|
47738
47796
|
}
|
|
47739
47797
|
function assertAuthMetadataClean(mcp, policy) {
|
|
47740
47798
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
@@ -47759,7 +47817,7 @@ function loadCanonicalSource(arg = {}) {
|
|
|
47759
47817
|
const options = typeof arg === "string" ? { homeDir: arg } : arg;
|
|
47760
47818
|
const homeDir = options.homeDir ?? homedir3();
|
|
47761
47819
|
const root7 = trellisRoot(homeDir);
|
|
47762
|
-
if (!
|
|
47820
|
+
if (!existsSync4(root7)) {
|
|
47763
47821
|
throw new Error(
|
|
47764
47822
|
`No canonical source at ${root7}. Run trellis init before trellis sync.`
|
|
47765
47823
|
);
|
|
@@ -47769,7 +47827,13 @@ function loadCanonicalSource(arg = {}) {
|
|
|
47769
47827
|
const scopeYaml = loadScopeYaml(join6(root7, "scope.yaml"));
|
|
47770
47828
|
const skillDirs = listSkillDirs(join6(root7, "skills"));
|
|
47771
47829
|
const knownSkillNames = new Set(skillDirs.map((s3) => s3.name));
|
|
47772
|
-
const skills = skillDirs.map(({ name, dir }) =>
|
|
47830
|
+
const skills = skillDirs.map(({ name, dir }) => {
|
|
47831
|
+
const declared = scopeFor(scopeYaml.skills, name);
|
|
47832
|
+
if (declared === void 0 && !isBuiltinSkillName(name)) {
|
|
47833
|
+
diagnostics.push(`scope.yaml: skill "${name}" has no registered agents \u2014 distribution paused; run trellis skill scope ${name} --agents <ids>`);
|
|
47834
|
+
}
|
|
47835
|
+
return { name, dir, scope: isBuiltinSkillName(name) && declared === void 0 ? void 0 : registeredSkillScope(declared) };
|
|
47836
|
+
});
|
|
47773
47837
|
const agentFiles = listMarkdownFiles(join6(root7, "agents"));
|
|
47774
47838
|
const knownAgentProfileNames = new Set(agentFiles.map((a8) => a8.name));
|
|
47775
47839
|
const agents = agentFiles.map(({ name, file: file2 }) => ({
|
|
@@ -47794,7 +47858,7 @@ function loadCanonicalSource(arg = {}) {
|
|
|
47794
47858
|
if (!known.has(name)) {
|
|
47795
47859
|
diagnostics.push(`scope.yaml: "${section}.${name}" does not match any known ${section.slice(0, -1)} \u2014 ignored`);
|
|
47796
47860
|
} else if (!validAgentIds(map3?.[name])) {
|
|
47797
|
-
diagnostics.push(`scope.yaml: "${section}.${name}" lists an unrecognized agent id \u2014 ignored`);
|
|
47861
|
+
diagnostics.push(`scope.yaml: "${section}.${name}" lists an unrecognized agent id${section === "skills" ? " \u2014 distribution paused" : " \u2014 ignored"}`);
|
|
47798
47862
|
}
|
|
47799
47863
|
}
|
|
47800
47864
|
}
|
|
@@ -47835,6 +47899,21 @@ function codexBearerTokenEnvVar(def) {
|
|
|
47835
47899
|
return BEARER_TOKEN_VALUE_RE.exec(value)?.[1];
|
|
47836
47900
|
}
|
|
47837
47901
|
|
|
47902
|
+
// src/lib/envVarNames.ts
|
|
47903
|
+
var TEMPLATE_VAR_RE = /\$\{([^{}]*)\}/g;
|
|
47904
|
+
function replaceTemplateVars(value, replace) {
|
|
47905
|
+
return value.replace(TEMPLATE_VAR_RE, (_match, entry) => replace(entry));
|
|
47906
|
+
}
|
|
47907
|
+
function invalidHeaderTemplate(headers) {
|
|
47908
|
+
return Object.entries(headers ?? {}).find(([, value]) => {
|
|
47909
|
+
const literal2 = replaceTemplateVars(value, () => "");
|
|
47910
|
+
return literal2.includes("${") || /mx-?env:\/\//.test(literal2);
|
|
47911
|
+
})?.[0];
|
|
47912
|
+
}
|
|
47913
|
+
function extractTemplateVarNames(value) {
|
|
47914
|
+
return [...value.matchAll(TEMPLATE_VAR_RE)].map((m6) => m6[1]);
|
|
47915
|
+
}
|
|
47916
|
+
|
|
47838
47917
|
// src/adapters/mcpPlan.ts
|
|
47839
47918
|
var HUB_ENTRY_NAME = "trellis-hub";
|
|
47840
47919
|
var GATEWAY_ENTRY_NAME = "trellis";
|
|
@@ -47893,18 +47972,25 @@ function findLiteralSecret(def) {
|
|
|
47893
47972
|
return void 0;
|
|
47894
47973
|
}
|
|
47895
47974
|
async function findUnresolvedEnvName(def, policy) {
|
|
47896
|
-
const
|
|
47975
|
+
const headerRefs = Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames).filter((entry) => !isValidSecretVarName(entry));
|
|
47976
|
+
const envNames = [...def.env ?? [], ...Object.values(def.envAliases ?? {})];
|
|
47977
|
+
const names = [...envNames, ...headerRefs];
|
|
47897
47978
|
if (names.length === 0) return void 0;
|
|
47898
47979
|
const parsed = names.map(parseEnvEntry);
|
|
47899
|
-
const invalid =
|
|
47900
|
-
if (invalid) return invalid
|
|
47901
|
-
|
|
47902
|
-
|
|
47903
|
-
const
|
|
47904
|
-
|
|
47905
|
-
|
|
47980
|
+
const invalid = names.find((entry, i8) => parsed[i8].kind === "invalid" || headerRefs.includes(entry) && parsed[i8].kind === "name");
|
|
47981
|
+
if (invalid !== void 0) return invalid;
|
|
47982
|
+
try {
|
|
47983
|
+
const env2 = await resolveSecretEnv(envNames, policy);
|
|
47984
|
+
const missingEnv = envNames.find((entry) => !env2[deliveredName(entry)]);
|
|
47985
|
+
if (missingEnv !== void 0) return missingEnv;
|
|
47986
|
+
const headers = await resolveSecretEntries(headerRefs, policy);
|
|
47987
|
+
return headerRefs.find((entry) => !headers[entry]);
|
|
47988
|
+
} catch (error62) {
|
|
47989
|
+
if (error62 instanceof SecretReferenceError) return error62.entry;
|
|
47990
|
+
throw error62;
|
|
47991
|
+
}
|
|
47906
47992
|
}
|
|
47907
|
-
async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
47993
|
+
async function resolveMcpPlan(agentId, mcp, managedAgents, policy, options = {}) {
|
|
47908
47994
|
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
47909
47995
|
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
47910
47996
|
if (route.mode === "gateway") {
|
|
@@ -47986,14 +48072,14 @@ async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
47986
48072
|
conflicts: oauth.conflicts
|
|
47987
48073
|
};
|
|
47988
48074
|
}
|
|
47989
|
-
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
48075
|
+
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route, void 0, options.hosted || agentId === "pi");
|
|
47990
48076
|
const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
|
|
47991
48077
|
return {
|
|
47992
48078
|
desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
|
|
47993
48079
|
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" }))]
|
|
47994
48080
|
};
|
|
47995
48081
|
}
|
|
47996
|
-
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
48082
|
+
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter, hosted = false) {
|
|
47997
48083
|
const desired = [];
|
|
47998
48084
|
const conflicts = [];
|
|
47999
48085
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
@@ -48020,7 +48106,12 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
48020
48106
|
});
|
|
48021
48107
|
continue;
|
|
48022
48108
|
}
|
|
48023
|
-
|
|
48109
|
+
const invalidHeader = invalidHeaderTemplate(def.headers);
|
|
48110
|
+
if (invalidHeader !== void 0) {
|
|
48111
|
+
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" });
|
|
48112
|
+
continue;
|
|
48113
|
+
}
|
|
48114
|
+
if (!hosted && agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
|
|
48024
48115
|
conflicts.push({
|
|
48025
48116
|
name,
|
|
48026
48117
|
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 \u2014 Codex has no generic headers concept, only \`bearer_token_env_var\`. Still written normally for every other in-scope agent.`,
|
|
@@ -48029,9 +48120,9 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
48029
48120
|
continue;
|
|
48030
48121
|
}
|
|
48031
48122
|
const unresolvedName = await findUnresolvedEnvName(def, policy);
|
|
48032
|
-
if (unresolvedName) {
|
|
48123
|
+
if (unresolvedName !== void 0) {
|
|
48033
48124
|
const entry = parseEnvEntry(unresolvedName);
|
|
48034
|
-
if (entry.kind === "invalid") {
|
|
48125
|
+
if (entry.kind === "invalid" || !isValidSecretVarName(unresolvedName) && entry.kind === "name") {
|
|
48035
48126
|
conflicts.push({
|
|
48036
48127
|
name,
|
|
48037
48128
|
message: `refusing to write MCP server "${name}": its declared env entry "${unresolvedName}" is not a valid mxenv://<profile>/<KEY> reference (mx-env-secret-source)`,
|
|
@@ -48048,6 +48139,14 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
48048
48139
|
});
|
|
48049
48140
|
continue;
|
|
48050
48141
|
}
|
|
48142
|
+
if (!hosted && Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames).some((entry) => parseEnvEntry(entry).kind === "ref")) {
|
|
48143
|
+
conflicts.push({
|
|
48144
|
+
name,
|
|
48145
|
+
message: `refusing to write MCP server "${name}": mxenv header references require Trellis gateway/runtime delivery; native clients cannot resolve them`,
|
|
48146
|
+
remediation: `enable gateway/runtime delivery for ${agentId}, keeping transport and headers in servers.yaml`
|
|
48147
|
+
});
|
|
48148
|
+
continue;
|
|
48149
|
+
}
|
|
48051
48150
|
desired.push({ name, def });
|
|
48052
48151
|
}
|
|
48053
48152
|
return { desired, conflicts };
|
|
@@ -85364,12 +85463,6 @@ var StdioClientTransport = class {
|
|
|
85364
85463
|
}
|
|
85365
85464
|
};
|
|
85366
85465
|
|
|
85367
|
-
// src/lib/envVarNames.ts
|
|
85368
|
-
var TEMPLATE_VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
85369
|
-
function extractTemplateVarNames(value) {
|
|
85370
|
-
return [...value.matchAll(TEMPLATE_VAR_RE)].map((m6) => m6[1]);
|
|
85371
|
-
}
|
|
85372
|
-
|
|
85373
85466
|
// src/lib/mcpConnect.ts
|
|
85374
85467
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
85375
85468
|
var FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1e3;
|
|
@@ -85432,11 +85525,24 @@ async function connectStdio(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
85432
85525
|
}
|
|
85433
85526
|
async function resolveHeaders(def, secretsPolicy) {
|
|
85434
85527
|
if (!def.headers || Object.keys(def.headers).length === 0) return void 0;
|
|
85528
|
+
const invalidHeader = invalidHeaderTemplate(def.headers);
|
|
85529
|
+
if (invalidHeader !== void 0) throw new Error(`[trellis] invalid reference template in header ${invalidHeader} \u2014 use \${VAR} or \${mxenv://<profile>/<KEY>}`);
|
|
85435
85530
|
const names = Object.keys(def.headers).flatMap((key) => extractTemplateVarNames(def.headers[key]));
|
|
85436
|
-
const
|
|
85531
|
+
for (const name of names) {
|
|
85532
|
+
const entry = parseEnvEntry(name);
|
|
85533
|
+
if (entry.kind === "invalid" || entry.kind === "name" && !isValidSecretVarName(name)) {
|
|
85534
|
+
throw new Error(`[trellis] invalid header reference: ${name} \u2014 expected a variable name or mxenv://<profile>/<KEY>`);
|
|
85535
|
+
}
|
|
85536
|
+
}
|
|
85537
|
+
const resolved = await resolveSecretEntries(names, secretsPolicy);
|
|
85437
85538
|
const result = {};
|
|
85438
85539
|
for (const [key, template] of Object.entries(def.headers)) {
|
|
85439
|
-
result[key] = template
|
|
85540
|
+
result[key] = replaceTemplateVars(template, (name) => {
|
|
85541
|
+
if (parseEnvEntry(name).kind === "ref" && !resolved[name]) {
|
|
85542
|
+
throw new Error(`[trellis] unresolved header reference: ${name}`);
|
|
85543
|
+
}
|
|
85544
|
+
return resolved[name] ?? "";
|
|
85545
|
+
});
|
|
85440
85546
|
}
|
|
85441
85547
|
return result;
|
|
85442
85548
|
}
|
|
@@ -85641,7 +85747,7 @@ ${sourceLine}` : sourceLine;
|
|
|
85641
85747
|
};
|
|
85642
85748
|
|
|
85643
85749
|
// src/lib/oauth/store.ts
|
|
85644
|
-
import { chmodSync, existsSync as
|
|
85750
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
85645
85751
|
import { join as join7 } from "node:path";
|
|
85646
85752
|
function oauthDir(homeDir) {
|
|
85647
85753
|
return join7(homeDir, ".trellis", "mcp", "oauth");
|
|
@@ -85661,9 +85767,9 @@ function assertSafeServerName(serverName) {
|
|
|
85661
85767
|
}
|
|
85662
85768
|
function readToken(homeDir, serverName) {
|
|
85663
85769
|
const path = tokenPath(homeDir, serverName);
|
|
85664
|
-
if (!
|
|
85770
|
+
if (!existsSync5(path)) return void 0;
|
|
85665
85771
|
try {
|
|
85666
|
-
return JSON.parse(
|
|
85772
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
85667
85773
|
} catch {
|
|
85668
85774
|
return void 0;
|
|
85669
85775
|
}
|
|
@@ -85685,7 +85791,7 @@ function isExpired(token, skewMs = 6e4, now = Date.now()) {
|
|
|
85685
85791
|
}
|
|
85686
85792
|
|
|
85687
85793
|
// src/lib/oauth/lock.ts
|
|
85688
|
-
import { existsSync as
|
|
85794
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, openSync, closeSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
85689
85795
|
import { dirname as dirname3 } from "node:path";
|
|
85690
85796
|
var STALE_AFTER_MS = 6e4;
|
|
85691
85797
|
var POLL_INTERVAL_MS = 50;
|
|
@@ -85701,7 +85807,7 @@ function processAlive(pid) {
|
|
|
85701
85807
|
function isStale(path, now) {
|
|
85702
85808
|
let contents;
|
|
85703
85809
|
try {
|
|
85704
|
-
contents = JSON.parse(
|
|
85810
|
+
contents = JSON.parse(readFileSync6(path, "utf-8"));
|
|
85705
85811
|
} catch {
|
|
85706
85812
|
return true;
|
|
85707
85813
|
}
|
|
@@ -85726,7 +85832,7 @@ async function withServerLock(homeDir, serverName, fn, opts = {}) {
|
|
|
85726
85832
|
break;
|
|
85727
85833
|
} catch (err) {
|
|
85728
85834
|
if (err.code !== "EEXIST") throw err;
|
|
85729
|
-
if (
|
|
85835
|
+
if (existsSync6(path) && isStale(path, now())) {
|
|
85730
85836
|
rmSync2(path, { force: true });
|
|
85731
85837
|
continue;
|
|
85732
85838
|
}
|
|
@@ -85826,10 +85932,10 @@ async function safeText(response) {
|
|
|
85826
85932
|
}
|
|
85827
85933
|
|
|
85828
85934
|
// src/lib/upstreamResources.ts
|
|
85829
|
-
var
|
|
85935
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
85830
85936
|
|
|
85831
85937
|
// src/lib/skillManifest.ts
|
|
85832
|
-
var
|
|
85938
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
85833
85939
|
var MAX_SKILL_BYTES = 16 * 1024 * 1024;
|
|
85834
85940
|
var MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
85835
85941
|
|
package/docs/architecture.md
CHANGED
|
@@ -43,7 +43,7 @@ Aligned to the `.agents Protocol` draft, extended where the draft is silent:
|
|
|
43
43
|
├── mcp/servers.yaml # single MCP source, values are var-name
|
|
44
44
|
│ # references only, never literals
|
|
45
45
|
├── memories/*.md # shared memory entries (server-memory backed)
|
|
46
|
-
├── scope.yaml #
|
|
46
|
+
├── scope.yaml # skill compatibility registry; agent/memory scope
|
|
47
47
|
│ # see "Private / agent-specific capabilities"
|
|
48
48
|
└── secrets.policy.yaml # which var names are allowed, nothing else
|
|
49
49
|
|
|
@@ -132,12 +132,14 @@ one `trellis rollback` restores originals and removes generated
|
|
|
132
132
|
|
|
133
133
|
Not everything belongs to all six agents. A skill built around Claude
|
|
134
134
|
Code's Task-based subagent delegation has no equivalent to delegate to on
|
|
135
|
-
Codex; an MCP server might only make sense for one agent's workflow.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
Codex; an MCP server might only make sense for one agent's workflow.
|
|
136
|
+
Every custom skill must register applicable agents in `scope.yaml`.
|
|
137
|
+
Missing, empty or invalid skill scope pauses Trellis distribution and Runtime
|
|
138
|
+
access; sync removes only Trellis-owned links. `skill scope --all` records
|
|
139
|
+
the current managed set, so adding another agent never silently expands
|
|
140
|
+
compatibility. Package-owned built-ins retain their package-defined policy.
|
|
141
|
+
Subagent profiles, memory entries and MCP servers retain their existing
|
|
142
|
+
all-managed default and explicit restrictions.
|
|
141
143
|
|
|
142
144
|
**Skills, subagent profiles, and memory entries declare scope in
|
|
143
145
|
`.trellis/scope.yaml`, never inside the artifact file itself.** This
|
|
@@ -150,6 +152,18 @@ portable file exactly as its target agent's own spec expects; Trellis's
|
|
|
150
152
|
own bookkeeping lives next to it, not inside it. See
|
|
151
153
|
`schema/scope.example.yaml`.
|
|
152
154
|
|
|
155
|
+
Compatibility is explicit: Claude-specific workflows list `claude-code`,
|
|
156
|
+
Codex-specific workflows list `codex`, and portable workflows list each
|
|
157
|
+
reviewed agent. Publisher provenance does not prove compatibility. Hermes
|
|
158
|
+
skills belong to Hermes's own curator/review/archive lifecycle; Trellis
|
|
159
|
+
never manages or writes its skill directory.
|
|
160
|
+
|
|
161
|
+
Workspace skills use `.agents/scope.yaml` for Trellis-managed delivery and
|
|
162
|
+
Runtime access. Agents that discover `.agents/skills` natively can still
|
|
163
|
+
read that shared source directly; per-agent Trellis registration does not
|
|
164
|
+
override a native agent's own discovery. Keep agent-specific skills in the
|
|
165
|
+
global canonical store when native discovery must be isolated.
|
|
166
|
+
|
|
153
167
|
**MCP servers are the one exception** — `agents:` is declared inline per
|
|
154
168
|
server in `mcp/servers.yaml` (see `schema/servers.example.yaml`), because
|
|
155
169
|
that file is Trellis's own format and is never handed to an agent
|
|
@@ -629,6 +643,12 @@ acceptance criteria already describes this shape ("point at a scratch
|
|
|
629
643
|
section exists to make it a project-wide rule that every later phase's
|
|
630
644
|
acceptance criteria must follow, not a detail specific to P1.
|
|
631
645
|
|
|
646
|
+
CLI fixtures must isolate the working directory as well as HOME. Running
|
|
647
|
+
from the real checkout with only HOME changed can make ancestor discovery
|
|
648
|
+
mistake the developer's `~/.agents/` for a workspace. Run the CLI by absolute
|
|
649
|
+
path from the scratch directory; otherwise read-only parity checks can
|
|
650
|
+
compare different layers and write tests can reach real project artifacts.
|
|
651
|
+
|
|
632
652
|
This is also why P0 needing to resolve pi's actual skill-discovery path
|
|
633
653
|
(§D5 in `openspec/changes/trellis-doctor-p0/design.md`) is investigated
|
|
634
654
|
directly against a real pi installation *as a read-only observation*, and
|
package/docs/getting-started.md
CHANGED
|
@@ -465,9 +465,10 @@ $ trellis migrate --from codex --only instructions
|
|
|
465
465
|
| `skip-case-broken` | Found as `skill.md` instead of `SKILL.md` — fix the case on the source agent first. |
|
|
466
466
|
| `skip-unsupported` | The source capability can't be safely represented (including native memory without a supported adapter); nothing was written for it. |
|
|
467
467
|
|
|
468
|
-
Migrate never overwrites a genuine conflict
|
|
469
|
-
|
|
470
|
-
|
|
468
|
+
Migrate never overwrites a genuine conflict. A newly migrated skill is
|
|
469
|
+
registered only for its source agent; review its compatibility before
|
|
470
|
+
sharing it through `trellis skill scope <name> --agents <ids>`.
|
|
471
|
+
MCP servers retain their existing scope behavior. If migrate reports
|
|
471
472
|
a `conflict`, open the two files/entries it names and decide by hand
|
|
472
473
|
which content should actually be canonical, then re-run.
|
|
473
474
|
|
|
@@ -663,10 +664,10 @@ remains the command for a local directory:
|
|
|
663
664
|
npx trellis add mattpocock/skills --list
|
|
664
665
|
|
|
665
666
|
# Fetch and inspect the exact plan without writing canonical or Agent files.
|
|
666
|
-
npx trellis add mattpocock/skills --skill loop-me --dry-run
|
|
667
|
+
npx trellis add mattpocock/skills --skill loop-me --agent codex --dry-run
|
|
667
668
|
|
|
668
669
|
# Import to the user-level canonical store, then sync to every managed Agent.
|
|
669
|
-
npx trellis add mattpocock/skills --skill loop-me --yes
|
|
670
|
+
npx trellis add mattpocock/skills --skill loop-me --agent '*' --yes
|
|
670
671
|
```
|
|
671
672
|
|
|
672
673
|
The source may be `owner/repository` or a credential-free HTTPS
|
|
@@ -678,10 +679,11 @@ dependencies, or write into `.agents/skills`, `~/.agents/skills`, or any
|
|
|
678
679
|
Agent-specific Skill directory during import. Review the imported
|
|
679
680
|
`SKILL.md` before relying on it in an Agent session.
|
|
680
681
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
682
|
+
Imports require an explicit choice of applicable agents. Interactive terminals
|
|
683
|
+
offer a selection with no agents preselected; noninteractive/JSON calls must
|
|
684
|
+
supply `--agent`. Comma-separated values and repeated flags are accepted.
|
|
685
|
+
`--agent '*'` records the current managed set; `--agent none` imports a paused
|
|
686
|
+
skill. A newly managed agent does not receive existing skills automatically.
|
|
685
687
|
An Agent that is installed but unmanaged is refused before any canonical or
|
|
686
688
|
native write. Add it to Trellis's managed boundary first:
|
|
687
689
|
|
|
@@ -785,6 +787,24 @@ the first place (see `mcp sync`'s own explanation of `env` vs.
|
|
|
785
787
|
All four of `skill add`/`skill remove`/`mcp add`/`mcp remove` support
|
|
786
788
|
`--dry-run` and `--json`, same convention as every other command.
|
|
787
789
|
|
|
790
|
+
Local imports use the same compatibility rule:
|
|
791
|
+
|
|
792
|
+
```sh
|
|
793
|
+
trellis skill add review --from ./skills/review --agents claude-code,codex
|
|
794
|
+
trellis skill add pending --from ./skills/pending --none
|
|
795
|
+
trellis skill list --agent codex --json
|
|
796
|
+
trellis skill scope review --all
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Every custom skill needs a `skills.<name>: [agent ids]` entry in
|
|
800
|
+
`~/.trellis/scope.yaml` (or `.agents/scope.yaml` for workspace delivery).
|
|
801
|
+
Unregistered skills pause until reviewed; sync removes only their
|
|
802
|
+
Trellis-owned links and leaves independent files untouched. `skill list
|
|
803
|
+
--agent` shows scoped skills with `linked`, `missing`, `conflict` or `runtime`
|
|
804
|
+
delivery status. Codex's global delivery root is `~/.agents/skills`.
|
|
805
|
+
Use `trellis sync skills --dry-run` before converging an older installation.
|
|
806
|
+
Hermes maintains its own skills; Trellis never writes its skill store.
|
|
807
|
+
|
|
788
808
|
## `trellis sync`
|
|
789
809
|
|
|
790
810
|
Distributes canonical skills and instructions to every **managed** agent
|
package/docs/research.md
CHANGED
|
@@ -223,8 +223,21 @@ which was silently invalid). Trellis's contribution here is `trellis secrets
|
|
|
223
223
|
audit` — a linter that fails a build if any adapter output contains a raw
|
|
224
224
|
credential — not a new secret store. The concrete reference form is
|
|
225
225
|
`mxenv://<profile>/<KEY>` (mx-env-secret-source): written in `servers.yaml`
|
|
226
|
-
`env`/`envAliases`,
|
|
227
|
-
|
|
226
|
+
`env`/`envAliases`, or inline in a header template such as
|
|
227
|
+
`Authorization: Bearer ${mxenv://legacy/SERVICE_TOKEN}`, resolved from the
|
|
228
|
+
mxenv profile at consumption time. Env references are delivered under the
|
|
229
|
+
`KEY` segment; header references are substituted in memory and retain their
|
|
230
|
+
profile identity even when two profiles use the same key. Missing keys,
|
|
231
|
+
unknown profiles and malformed header references fail explicitly, without
|
|
232
|
+
printing provider output or substituting an empty credential. Bare `${VAR}`
|
|
233
|
+
headers retain their existing env-file/process-env behavior.
|
|
234
|
+
|
|
235
|
+
Header references require Trellis gateway/runtime delivery (or pi's bridge),
|
|
236
|
+
which resolves them independently of the GUI or shell's inherited environment.
|
|
237
|
+
Native clients do not understand `mxenv://`; direct native sync reports a
|
|
238
|
+
conflict with instructions to enable gateway/runtime, rather than writing an
|
|
239
|
+
unusable reference or persisting its resolved value. The canonical server
|
|
240
|
+
still uses `transport: http`/`sse`; no third-party stdio shim is needed.
|
|
228
241
|
|
|
229
242
|
A third real incident during migrate-in: a source config's `${VAR}`
|
|
230
243
|
reference doesn't always name itself (`OPENAPI_MCP_HEADERS:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|