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.
@@ -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: readFileSync6 } = __require("node:fs");
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] ?? readFileSync6(webIdentityTokenFile, { encoding: "ascii" }),
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 import_yaml = __toESM(require_dist(), 1);
47329
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "node:fs";
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("mxenv://") || entry.includes("mx-env://")) {
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 resolveSecretEnv(entries, policy) {
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
- const resolver = new import_mxenv_core.ReferenceResolver((0, import_mxenv_core.loadConfigLayers)({ cwd: homedir2(), homeDir: homedir2() }), {});
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
- if (result[ref.delivered] !== void 0) continue;
47414
- const { value } = await resolver.valueOf(ref.profile, ref.key);
47415
- result[ref.delivered] = value;
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 existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
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 (existsSync2(configPath)) {
47531
+ if (existsSync3(configPath)) {
47484
47532
  let parsed;
47485
47533
  try {
47486
- parsed = JSON.parse(readFileSync2(configPath, "utf-8"));
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 (existsSync2(instructionsPath)) {
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()) layer.skills.push({ name, dir });
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 (!existsSync3(path)) {
47713
+ if (!existsSync4(path)) {
47656
47714
  return {};
47657
47715
  }
47658
- const parsed = (0, import_yaml.parse)(readFileSync3(path, "utf-8"));
47716
+ const parsed = (0, import_yaml2.parse)(readFileSync4(path, "utf-8"));
47659
47717
  return parsed ?? {};
47660
47718
  }
47661
47719
  function loadServersYaml(path) {
47662
- if (!existsSync3(path)) {
47720
+ if (!existsSync4(path)) {
47663
47721
  return { servers: {}, knownHostInjected: [] };
47664
47722
  }
47665
- const parsed = (0, import_yaml.parse)(readFileSync3(path, "utf-8")) ?? {};
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 (!existsSync3(path)) {
47764
+ if (!existsSync4(path)) {
47707
47765
  return { allowedVars: [], rejectPatterns: [] };
47708
47766
  }
47709
- const parsed = (0, import_yaml.parse)(readFileSync3(path, "utf-8")) ?? {};
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 (!existsSync3(path)) {
47775
+ if (!existsSync4(path)) {
47718
47776
  return [];
47719
47777
  }
47720
- const parsed = (0, import_yaml.parse)(readFileSync3(path, "utf-8")) ?? {};
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 (!scope) return true;
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 (!existsSync3(root7)) {
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 }) => ({ name, dir, scope: scopeFor(scopeYaml.skills, name) }));
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 names = [...def.env ?? [], ...Object.values(def.envAliases ?? {})];
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 = parsed.find((p5) => p5.kind === "invalid");
47900
- if (invalid) return invalid.entry;
47901
- const resolved = await resolveSecretEnv(names, policy);
47902
- return names.find((name) => {
47903
- const p5 = parseEnvEntry(name);
47904
- return !resolved[p5.kind === "invalid" ? name : p5.delivered];
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
- if (agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
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 resolved = await resolveSecretEnv(names, secretsPolicy);
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.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => resolved[name] ?? "");
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 existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
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 (!existsSync4(path)) return void 0;
85770
+ if (!existsSync5(path)) return void 0;
85665
85771
  try {
85666
- return JSON.parse(readFileSync4(path, "utf-8"));
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 existsSync5, mkdirSync as mkdirSync4, openSync, closeSync, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
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(readFileSync5(path, "utf-8"));
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 (existsSync5(path) && isStale(path, now())) {
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 import_yaml3 = __toESM(require_dist(), 1);
85935
+ var import_yaml4 = __toESM(require_dist(), 1);
85830
85936
 
85831
85937
  // src/lib/skillManifest.ts
85832
- var import_yaml2 = __toESM(require_dist(), 1);
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
 
@@ -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 # exceptions to "shared with all agents" —
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. Every
136
- scopable item — skill, subagent profile, memory entry, MCP server —
137
- defaults to "shared with all managed agents," and can be restricted with an explicit
138
- `scope` (skills/agents/memories) or inline `agents:` (MCP servers, see
139
- below). Restriction is the exception you declare, not something you
140
- configure for the common case.
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
@@ -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, and never scopes a migrated
469
- skill (or MCP server) to just the source agent — once in canonical, it's
470
- visible to every agent by default (see `sync`, below). If migrate reports
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
- By default the imported Skill is unscoped, which means every Agent currently
682
- listed in `~/.trellis/managed.yaml` receives it on sync. Restrict delivery to
683
- managed Agents with `--agent`; comma-separated values and repeated flags are
684
- accepted. `--agent '*'` writes the current managed set as an explicit scope.
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`, resolved from the encrypted mxenv store at consumption
227
- point, delivered under the `KEY` segment.
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.21.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",