agent-trellis 0.1.0 → 0.3.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.
Files changed (67) hide show
  1. package/README.md +69 -17
  2. package/dist/adapters/claude-code.d.ts +5 -3
  3. package/dist/adapters/claude-code.js +27 -14
  4. package/dist/adapters/codex.d.ts +8 -4
  5. package/dist/adapters/codex.js +47 -16
  6. package/dist/adapters/jsonMcp.d.ts +16 -5
  7. package/dist/adapters/jsonMcp.js +38 -29
  8. package/dist/adapters/kiro.d.ts +5 -3
  9. package/dist/adapters/kiro.js +29 -16
  10. package/dist/adapters/mcpPlan.d.ts +11 -6
  11. package/dist/adapters/mcpPlan.js +40 -7
  12. package/dist/adapters/pi.d.ts +2 -1
  13. package/dist/adapters/pi.js +4 -4
  14. package/dist/adapters/symlinkPlan.d.ts +7 -3
  15. package/dist/adapters/symlinkPlan.js +42 -16
  16. package/dist/cli.js +161 -18
  17. package/dist/commands/init.js +11 -0
  18. package/dist/commands/mcp.d.ts +114 -7
  19. package/dist/commands/mcp.js +258 -17
  20. package/dist/commands/memory.d.ts +39 -0
  21. package/dist/commands/memory.js +78 -0
  22. package/dist/commands/migrate.d.ts +30 -4
  23. package/dist/commands/migrate.js +83 -16
  24. package/dist/commands/onboard.d.ts +52 -7
  25. package/dist/commands/onboard.js +318 -35
  26. package/dist/commands/rollback.d.ts +44 -0
  27. package/dist/commands/rollback.js +201 -0
  28. package/dist/commands/secretsAudit.d.ts +7 -0
  29. package/dist/commands/secretsAudit.js +14 -7
  30. package/dist/commands/skill.d.ts +51 -0
  31. package/dist/commands/skill.js +104 -0
  32. package/dist/commands/sync.d.ts +13 -0
  33. package/dist/commands/sync.js +31 -5
  34. package/dist/core/adapter.d.ts +28 -11
  35. package/dist/core/adapter.js +2 -2
  36. package/dist/core/canonical.d.ts +26 -1
  37. package/dist/core/canonical.js +103 -3
  38. package/dist/core/types.d.ts +29 -1
  39. package/dist/core/types.js +11 -2
  40. package/dist/lib/backup.d.ts +56 -0
  41. package/dist/lib/backup.js +98 -0
  42. package/dist/lib/deepEqual.d.ts +8 -0
  43. package/dist/lib/deepEqual.js +26 -0
  44. package/dist/lib/dirEquals.d.ts +9 -0
  45. package/dist/lib/dirEquals.js +15 -1
  46. package/dist/lib/installAgent.d.ts +26 -0
  47. package/dist/lib/installAgent.js +46 -0
  48. package/dist/lib/mcpMigrateRead.d.ts +69 -0
  49. package/dist/lib/mcpMigrateRead.js +188 -0
  50. package/dist/lib/mcpOwnership.d.ts +25 -0
  51. package/dist/lib/mcpOwnership.js +50 -0
  52. package/dist/lib/memoryGraph.d.ts +60 -0
  53. package/dist/lib/memoryGraph.js +101 -0
  54. package/dist/lib/realHomeSnapshot.d.ts +26 -0
  55. package/dist/lib/realHomeSnapshot.js +77 -0
  56. package/dist/lib/terminalPicker.d.ts +45 -0
  57. package/dist/lib/terminalPicker.js +193 -0
  58. package/dist/lib/tomlSection.d.ts +20 -6
  59. package/dist/lib/tomlSection.js +78 -12
  60. package/dist/pi-bridge/bundle.js +100 -51
  61. package/dist/pi-bridge/index.js +14 -2
  62. package/dist/probes/codex.js +10 -2
  63. package/docs/architecture.md +7 -4
  64. package/docs/getting-started.md +267 -33
  65. package/docs/roadmap.md +444 -0
  66. package/package.json +1 -1
  67. package/schema/servers.example.yaml +39 -2
@@ -7818,7 +7818,7 @@ var require_identity = __commonJS({
7818
7818
  var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
7819
7819
  var isAlias = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === ALIAS;
7820
7820
  var isDocument = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === DOC;
7821
- var isMap = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === MAP;
7821
+ var isMap2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === MAP;
7822
7822
  var isPair = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === PAIR;
7823
7823
  var isScalar = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SCALAR;
7824
7824
  var isSeq = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SEQ;
@@ -7854,7 +7854,7 @@ var require_identity = __commonJS({
7854
7854
  exports.isAlias = isAlias;
7855
7855
  exports.isCollection = isCollection;
7856
7856
  exports.isDocument = isDocument;
7857
- exports.isMap = isMap;
7857
+ exports.isMap = isMap2;
7858
7858
  exports.isNode = isNode;
7859
7859
  exports.isPair = isPair;
7860
7860
  exports.isScalar = isScalar;
@@ -11966,9 +11966,9 @@ var require_resolve_flow_collection = __commonJS({
11966
11966
  var blockMsg = "Block collections are not allowed within flow collections";
11967
11967
  var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
11968
11968
  function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
11969
- const isMap = fc.start.source === "{";
11970
- const fcName = isMap ? "flow map" : "flow sequence";
11971
- const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
11969
+ const isMap2 = fc.start.source === "{";
11970
+ const fcName = isMap2 ? "flow map" : "flow sequence";
11971
+ const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
11972
11972
  const coll = new NodeClass(ctx.schema);
11973
11973
  coll.flow = true;
11974
11974
  const atRoot = ctx.atRoot;
@@ -12004,7 +12004,7 @@ var require_resolve_flow_collection = __commonJS({
12004
12004
  offset = props.end;
12005
12005
  continue;
12006
12006
  }
12007
- if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
12007
+ if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
12008
12008
  onError(
12009
12009
  key,
12010
12010
  // checked by containsNewline()
@@ -12044,7 +12044,7 @@ var require_resolve_flow_collection = __commonJS({
12044
12044
  }
12045
12045
  }
12046
12046
  }
12047
- if (!isMap && !sep && !props.found) {
12047
+ if (!isMap2 && !sep && !props.found) {
12048
12048
  const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
12049
12049
  coll.items.push(valueNode);
12050
12050
  offset = valueNode.range[2];
@@ -12067,7 +12067,7 @@ var require_resolve_flow_collection = __commonJS({
12067
12067
  startOnNewline: false
12068
12068
  });
12069
12069
  if (valueProps.found) {
12070
- if (!isMap && !props.found && ctx.options.strict) {
12070
+ if (!isMap2 && !props.found && ctx.options.strict) {
12071
12071
  if (sep)
12072
12072
  for (const st of sep) {
12073
12073
  if (st === valueProps.found)
@@ -12099,7 +12099,7 @@ var require_resolve_flow_collection = __commonJS({
12099
12099
  const pair = new Pair.Pair(keyNode, valueNode);
12100
12100
  if (ctx.options.keepSourceTokens)
12101
12101
  pair.srcToken = collItem;
12102
- if (isMap) {
12102
+ if (isMap2) {
12103
12103
  const map = coll;
12104
12104
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
12105
12105
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -12115,7 +12115,7 @@ var require_resolve_flow_collection = __commonJS({
12115
12115
  offset = valueNode ? valueNode.range[2] : valueProps.end;
12116
12116
  }
12117
12117
  }
12118
- const expectedEnd = isMap ? "}" : "]";
12118
+ const expectedEnd = isMap2 ? "}" : "]";
12119
12119
  const [ce, ...ee] = fc.end;
12120
12120
  let cePos = offset;
12121
12121
  if (ce?.source === expectedEnd)
@@ -15016,7 +15016,7 @@ var require_public_api = __commonJS({
15016
15016
  return docs;
15017
15017
  return Object.assign([], { empty: true }, composer$1.streamInfo());
15018
15018
  }
15019
- function parseDocument(source, options = {}) {
15019
+ function parseDocument2(source, options = {}) {
15020
15020
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
15021
15021
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
15022
15022
  const composer$1 = new composer.Composer(options);
@@ -15042,7 +15042,7 @@ var require_public_api = __commonJS({
15042
15042
  } else if (options === void 0 && reviver && typeof reviver === "object") {
15043
15043
  options = reviver;
15044
15044
  }
15045
- const doc = parseDocument(src, options);
15045
+ const doc = parseDocument2(src, options);
15046
15046
  if (!doc)
15047
15047
  return null;
15048
15048
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -15078,7 +15078,7 @@ var require_public_api = __commonJS({
15078
15078
  }
15079
15079
  exports.parse = parse3;
15080
15080
  exports.parseAllDocuments = parseAllDocuments;
15081
- exports.parseDocument = parseDocument;
15081
+ exports.parseDocument = parseDocument2;
15082
15082
  exports.stringify = stringify;
15083
15083
  }
15084
15084
  });
@@ -27243,7 +27243,7 @@ import { homedir as homedir2 } from "node:os";
27243
27243
 
27244
27244
  // src/core/canonical.ts
27245
27245
  var import_yaml = __toESM(require_dist2(), 1);
27246
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
27246
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
27247
27247
  import { homedir } from "node:os";
27248
27248
  import { basename, join } from "node:path";
27249
27249
 
@@ -27254,11 +27254,16 @@ var ALL_AGENTS = [
27254
27254
  "kiro",
27255
27255
  "pi"
27256
27256
  ];
27257
- function resolveScope(scope) {
27258
- return scope ?? ALL_AGENTS;
27257
+ function resolveScope(scope, managedAgents) {
27258
+ const candidates = scope ?? managedAgents;
27259
+ return candidates.filter((id) => managedAgents.includes(id));
27259
27260
  }
27260
27261
 
27261
27262
  // src/core/canonical.ts
27263
+ function fromServerDefYaml(def) {
27264
+ const { static_env, ...rest } = def;
27265
+ return static_env ? { ...rest, staticEnv: static_env } : rest;
27266
+ }
27262
27267
  function trellisRoot(homeDir) {
27263
27268
  return join(homeDir, ".trellis");
27264
27269
  }
@@ -27302,8 +27307,9 @@ function loadServersYaml(path) {
27302
27307
  return { servers: {}, knownHostInjected: [] };
27303
27308
  }
27304
27309
  const parsed = (0, import_yaml.parse)(readFileSync(path, "utf-8")) ?? {};
27310
+ const servers = Object.fromEntries(Object.entries(parsed.servers ?? {}).map(([name, def]) => [name, fromServerDefYaml(def)]));
27305
27311
  return {
27306
- servers: parsed.servers ?? {},
27312
+ servers,
27307
27313
  knownHostInjected: parsed.known_host_injected ?? [],
27308
27314
  hub: parsed.hub
27309
27315
  };
@@ -27319,6 +27325,22 @@ function loadSecretsPolicyYaml(path, homeDir) {
27319
27325
  envFile: parsed.env_file ? parsed.env_file.replace(/^~(?=$|\/)/, homeDir) : void 0
27320
27326
  };
27321
27327
  }
27328
+ function loadManagedYaml(path, diagnostics) {
27329
+ if (!existsSync(path)) {
27330
+ return [];
27331
+ }
27332
+ const parsed = (0, import_yaml.parse)(readFileSync(path, "utf-8")) ?? {};
27333
+ const raw = parsed.agents ?? [];
27334
+ const valid = [];
27335
+ for (const id of raw) {
27336
+ if (ALL_AGENTS.includes(id)) {
27337
+ valid.push(id);
27338
+ } else {
27339
+ diagnostics.push(`managed.yaml: "${id}" is not a recognized agent id \u2014 ignored`);
27340
+ }
27341
+ }
27342
+ return valid;
27343
+ }
27322
27344
  function scopeFor(map, name) {
27323
27345
  return map?.[name];
27324
27346
  }
@@ -27334,6 +27356,7 @@ function loadCanonicalSource(homeDir = homedir()) {
27334
27356
  );
27335
27357
  }
27336
27358
  const diagnostics = [];
27359
+ const managedAgents = loadManagedYaml(join(root, "managed.yaml"), diagnostics);
27337
27360
  const scopeYaml = loadScopeYaml(join(root, "scope.yaml"));
27338
27361
  const skillDirs = listSkillDirs(join(root, "skills"));
27339
27362
  const knownSkillNames = new Set(skillDirs.map((s) => s.name));
@@ -27368,6 +27391,7 @@ function loadCanonicalSource(homeDir = homedir()) {
27368
27391
  }
27369
27392
  return {
27370
27393
  instructionsFile: join(root, "agents.md"),
27394
+ managedAgents,
27371
27395
  skills,
27372
27396
  agents,
27373
27397
  memories,
@@ -27381,8 +27405,8 @@ function loadCanonicalSource(homeDir = homedir()) {
27381
27405
  }
27382
27406
 
27383
27407
  // src/core/adapter.ts
27384
- function isInScope(id, scope) {
27385
- return resolveScope(scope).includes(id);
27408
+ function isInScope(id, scope, managedAgents) {
27409
+ return resolveScope(scope, managedAgents).includes(id);
27386
27410
  }
27387
27411
 
27388
27412
  // src/lib/tomlSection.ts
@@ -27397,6 +27421,33 @@ function codexBearerTokenEnvVar(def) {
27397
27421
  return BEARER_TOKEN_VALUE_RE.exec(value)?.[1];
27398
27422
  }
27399
27423
 
27424
+ // src/lib/secretEnv.ts
27425
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
27426
+ var LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
27427
+ function parseDotenv(content) {
27428
+ const result = {};
27429
+ for (const rawLine of content.split("\n")) {
27430
+ const line = rawLine.trim();
27431
+ if (line === "" || line.startsWith("#")) continue;
27432
+ const match = LINE_RE.exec(line);
27433
+ if (match) result[match[1]] = match[2];
27434
+ }
27435
+ return result;
27436
+ }
27437
+ function loadEnvFile(path) {
27438
+ return existsSync2(path) ? parseDotenv(readFileSync2(path, "utf-8")) : {};
27439
+ }
27440
+ function resolveSecretEnv(names, policy) {
27441
+ const result = {};
27442
+ if (policy.envFile) {
27443
+ const map = loadEnvFile(policy.envFile);
27444
+ for (const name of names) result[name] = map[name];
27445
+ } else {
27446
+ for (const name of names) result[name] = process.env[name];
27447
+ }
27448
+ return result;
27449
+ }
27450
+
27400
27451
  // src/adapters/mcpPlan.ts
27401
27452
  var HUB_ENTRY_NAME = "trellis-hub";
27402
27453
  var CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server \u2014 see docs/research.md.';
@@ -27410,7 +27461,9 @@ var DANGEROUS_LITERAL_PATTERNS = [
27410
27461
  { label: "mcp-router token (mcpr_)", pattern: /mcpr_/ }
27411
27462
  ];
27412
27463
  function findLiteralSecret(def) {
27413
- const candidates = [def.command, def.url, ...def.args ?? [], ...Object.values(def.headers ?? {})].filter((v) => typeof v === "string");
27464
+ const candidates = [def.command, def.url, ...def.args ?? [], ...Object.values(def.headers ?? {}), ...Object.values(def.staticEnv ?? {})].filter(
27465
+ (v) => typeof v === "string"
27466
+ );
27414
27467
  for (const candidate of candidates) {
27415
27468
  for (const { label, pattern } of DANGEROUS_LITERAL_PATTERNS) {
27416
27469
  if (pattern.test(candidate)) {
@@ -27420,7 +27473,13 @@ function findLiteralSecret(def) {
27420
27473
  }
27421
27474
  return void 0;
27422
27475
  }
27423
- function resolveMcpPlan(agentId, mcp) {
27476
+ function findUnresolvedEnvName(def, policy) {
27477
+ const names = def.env ?? [];
27478
+ if (names.length === 0) return void 0;
27479
+ const resolved = resolveSecretEnv(names, policy);
27480
+ return names.find((name) => !resolved[name]);
27481
+ }
27482
+ function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
27424
27483
  if (mcp.hub) {
27425
27484
  if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
27426
27485
  return { desired: [], conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId) }] };
@@ -27430,7 +27489,10 @@ function resolveMcpPlan(agentId, mcp) {
27430
27489
  const desired = [];
27431
27490
  const conflicts = [];
27432
27491
  for (const [name, def] of Object.entries(mcp.servers)) {
27433
- if (!isInScope(agentId, def.agents)) {
27492
+ if (def.enabled === false) {
27493
+ continue;
27494
+ }
27495
+ if (!isInScope(agentId, def.agents, managedAgents)) {
27434
27496
  continue;
27435
27497
  }
27436
27498
  if (mcp.knownHostInjected.includes(name)) {
@@ -27452,38 +27514,20 @@ function resolveMcpPlan(agentId, mcp) {
27452
27514
  });
27453
27515
  continue;
27454
27516
  }
27517
+ const unresolvedName = findUnresolvedEnvName(def, policy);
27518
+ if (unresolvedName) {
27519
+ const source = policy.envFile ?? "process environment";
27520
+ conflicts.push({
27521
+ name,
27522
+ message: `refusing to write MCP server "${name}": its declared env var "${unresolvedName}" has no resolvable value in ${source} \u2014 writing it now would silently break this server's connection once the agent starts it (trellis-mcp-static-env-and-disabled-servers)`
27523
+ });
27524
+ continue;
27525
+ }
27455
27526
  desired.push({ name, def });
27456
27527
  }
27457
27528
  return { desired, conflicts };
27458
27529
  }
27459
27530
 
27460
- // src/lib/secretEnv.ts
27461
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
27462
- var LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
27463
- function parseDotenv(content) {
27464
- const result = {};
27465
- for (const rawLine of content.split("\n")) {
27466
- const line = rawLine.trim();
27467
- if (line === "" || line.startsWith("#")) continue;
27468
- const match = LINE_RE.exec(line);
27469
- if (match) result[match[1]] = match[2];
27470
- }
27471
- return result;
27472
- }
27473
- function loadEnvFile(path) {
27474
- return existsSync2(path) ? parseDotenv(readFileSync2(path, "utf-8")) : {};
27475
- }
27476
- function resolveSecretEnv(names, policy) {
27477
- const result = {};
27478
- if (policy.envFile) {
27479
- const map = loadEnvFile(policy.envFile);
27480
- for (const name of names) result[name] = map[name];
27481
- } else {
27482
- for (const name of names) result[name] = process.env[name];
27483
- }
27484
- return result;
27485
- }
27486
-
27487
27531
  // src/lib/envVarNames.ts
27488
27532
  var TEMPLATE_VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
27489
27533
  function extractTemplateVarNames(value) {
@@ -31955,7 +31999,12 @@ async function connectStdio(def, secretsPolicy, timeoutMs) {
31955
31999
  const transport = new StdioClientTransport({
31956
32000
  command: def.command,
31957
32001
  args: def.args,
31958
- env: { ...getDefaultEnvironment(), ...namedEnv }
32002
+ // staticEnv merges last: a literal, intentionally-plain value (an
32003
+ // email, an environment tag) always wins over an unresolved name-only
32004
+ // entry's empty-string fallback for the same key — though in practice
32005
+ // resolveMcpPlan's D6 refusal never lets an unresolved name reach
32006
+ // this point at all (trellis-mcp-static-env-and-disabled-servers).
32007
+ env: { ...getDefaultEnvironment(), ...namedEnv, ...def.staticEnv ?? {} }
31959
32008
  });
31960
32009
  return connectWithCleanup(client, transport, timeoutMs, `connect timed out after ${timeoutMs}ms`);
31961
32010
  }
@@ -32010,7 +32059,7 @@ function registerServerTools(pi, serverName, client, timeoutMs) {
32010
32059
  }
32011
32060
  async function trellisMcpBridge(pi, homeDir = homedir2(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
32012
32061
  const canonical = loadCanonicalSource(homeDir);
32013
- const { desired } = resolveMcpPlan("pi", canonical.mcp);
32062
+ const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
32014
32063
  const clients = /* @__PURE__ */ new Set();
32015
32064
  const closeClient = async (client) => {
32016
32065
  if (!clients.delete(client)) return;
@@ -20,6 +20,7 @@ import { loadCanonicalSource } from "../core/canonical.js";
20
20
  import { resolveMcpPlan } from "../adapters/mcpPlan.js";
21
21
  import { resolveSecretEnv } from "../lib/secretEnv.js";
22
22
  import { extractTemplateVarNames } from "../lib/envVarNames.js";
23
+ import { ALL_AGENTS } from "../core/types.js";
23
24
  import { bridgedToolName, toParametersSchema, toPiContent } from "./schemaTranslate.js";
24
25
  const CLIENT_INFO = { name: "trellis-mcp-bridge", version: "0.0.0" };
25
26
  const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
@@ -74,7 +75,12 @@ async function connectStdio(def, secretsPolicy, timeoutMs) {
74
75
  const transport = new StdioClientTransport({
75
76
  command: def.command,
76
77
  args: def.args,
77
- env: { ...getDefaultEnvironment(), ...namedEnv },
78
+ // staticEnv merges last: a literal, intentionally-plain value (an
79
+ // email, an environment tag) always wins over an unresolved name-only
80
+ // entry's empty-string fallback for the same key — though in practice
81
+ // resolveMcpPlan's D6 refusal never lets an unresolved name reach
82
+ // this point at all (trellis-mcp-static-env-and-disabled-servers).
83
+ env: { ...getDefaultEnvironment(), ...namedEnv, ...(def.staticEnv ?? {}) },
78
84
  });
79
85
  return connectWithCleanup(client, transport, timeoutMs, `connect timed out after ${timeoutMs}ms`);
80
86
  }
@@ -128,7 +134,13 @@ function registerServerTools(pi, serverName, client, timeoutMs) {
128
134
  */
129
135
  export default async function trellisMcpBridge(pi, homeDir = homedir(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
130
136
  const canonical = loadCanonicalSource(homeDir);
131
- const { desired } = resolveMcpPlan("pi", canonical.mcp);
137
+ // Deliberately ALL_AGENTS, not canonical.managedAgents: this bridge only
138
+ // ever runs because pi itself loaded the extension — that's already the
139
+ // strongest possible consent signal (trellis-managed-agents design.md),
140
+ // independent of whether `trellis onboard` was ever run to add pi to
141
+ // managed.yaml. managedAgents governs static config-file writes; this
142
+ // is pi reading canonical directly at its own runtime, P4's own concern.
143
+ const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
132
144
  const clients = new Set();
133
145
  const closeClient = async (client) => {
134
146
  if (!clients.delete(client))
@@ -36,16 +36,24 @@ export async function probe(homeDir = homedir(), opts = {}) {
36
36
  return { agent: "codex", present: false, skillRoots: [], mcpServers: [], diagnostics: [] };
37
37
  }
38
38
  const diagnostics = [];
39
+ // `codex` resolves its own config via $HOME (confirmed by running it with
40
+ // an overridden HOME against an empty scratch dir — it returns `[]`, not
41
+ // the real machine's servers), so every subprocess call here must be
42
+ // scoped to `homeDir` explicitly — otherwise a caller probing a non-
43
+ // default `homeDir` would silently get this real machine's real MCP
44
+ // server list instead (trellis-migrate-mcp-servers found this gap in
45
+ // src/lib/mcpMigrateRead.ts's own equivalent call; fixed there first).
46
+ const codexEnv = { ...process.env, HOME: homeDir };
39
47
  let version;
40
48
  try {
41
- version = execFileSync("codex", ["--version"], { encoding: "utf-8", timeout: 5_000 }).trim();
49
+ version = execFileSync("codex", ["--version"], { encoding: "utf-8", timeout: 5_000, env: codexEnv }).trim();
42
50
  }
43
51
  catch {
44
52
  // config exists without the binary on PATH — unusual, still probeable
45
53
  }
46
54
  let mcpEntries = [];
47
55
  try {
48
- const raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000 });
56
+ const raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env: codexEnv });
49
57
  mcpEntries = JSON.parse(raw);
50
58
  }
51
59
  catch (err) {
@@ -45,10 +45,13 @@ Aligned to the `.agents Protocol` draft, extended where the draft is silent:
45
45
  ├── memories/*.md # shared memory entries (server-memory backed)
46
46
  ├── scope.yaml # exceptions to "shared with all agents" —
47
47
  │ # see "Private / agent-specific capabilities"
48
- ├── secrets.policy.yaml # which var names are allowed, nothing else
49
- └── trellis.lock.json # NOT YET BUILT — planned ownership-tracking
50
- # file for MCP removal (docs/roadmap.md's P2
51
- # note); nothing in src/ reads or writes this
48
+ └── secrets.policy.yaml # which var names are allowed, nothing else
49
+
50
+ # Not user-authored, so not shown in the tree above — Trellis's own
51
+ # bookkeeping, written/read only by `trellis mcp sync` itself:
52
+ # mcp/ownership.json # what Trellis last wrote per (agent, server name) —
53
+ # # the ownership marker MCP removal needed
54
+ # # (docs/roadmap.md P14, src/lib/mcpOwnership.ts)
52
55
  ```
53
56
 
54
57
  ## Global vs. workspace scope