agent-trellis 0.7.0 → 0.8.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 (104) hide show
  1. package/README.md +97 -38
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/cli.js +121 -21
  15. package/dist/commands/doctor.d.ts +7 -2
  16. package/dist/commands/doctor.js +38 -2
  17. package/dist/commands/init.d.ts +4 -2
  18. package/dist/commands/init.js +26 -13
  19. package/dist/commands/kimi.d.ts +16 -0
  20. package/dist/commands/kimi.js +64 -0
  21. package/dist/commands/manage.d.ts +39 -0
  22. package/dist/commands/manage.js +126 -0
  23. package/dist/commands/mcp.d.ts +22 -1
  24. package/dist/commands/mcp.js +65 -2
  25. package/dist/commands/mcpGateway.d.ts +6 -0
  26. package/dist/commands/mcpGateway.js +34 -14
  27. package/dist/commands/mcpImport.d.ts +27 -0
  28. package/dist/commands/mcpImport.js +336 -0
  29. package/dist/commands/memory.d.ts +5 -3
  30. package/dist/commands/memory.js +12 -8
  31. package/dist/commands/migrate.d.ts +16 -5
  32. package/dist/commands/migrate.js +90 -17
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +458 -71
  35. package/dist/commands/rollback.js +4 -1
  36. package/dist/commands/skill.d.ts +17 -0
  37. package/dist/commands/skill.js +47 -2
  38. package/dist/commands/sync.js +2 -0
  39. package/dist/core/canonical.d.ts +15 -7
  40. package/dist/core/canonical.js +101 -13
  41. package/dist/core/capabilitySelection.d.ts +45 -0
  42. package/dist/core/capabilitySelection.js +124 -0
  43. package/dist/core/types.d.ts +25 -2
  44. package/dist/core/types.js +7 -0
  45. package/dist/lib/backup.d.ts +2 -0
  46. package/dist/lib/backup.js +4 -0
  47. package/dist/lib/builtinSkills.d.ts +2 -0
  48. package/dist/lib/builtinSkills.js +4 -0
  49. package/dist/lib/cliMetadata.d.ts +1 -0
  50. package/dist/lib/cliMetadata.js +14 -0
  51. package/dist/lib/gatewayBackend.d.ts +28 -0
  52. package/dist/lib/gatewayBackend.js +72 -1
  53. package/dist/lib/installAgent.d.ts +7 -5
  54. package/dist/lib/installAgent.js +4 -3
  55. package/dist/lib/mcpConnect.js +5 -1
  56. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  57. package/dist/lib/mcpMigrateRead.js +3 -0
  58. package/dist/lib/mcpRuntime.d.ts +46 -0
  59. package/dist/lib/mcpRuntime.js +185 -0
  60. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  61. package/dist/lib/mcpStatusProvider.js +25 -0
  62. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  63. package/dist/lib/mcpToolRegistry.js +107 -14
  64. package/dist/lib/memoryGraph.d.ts +1 -1
  65. package/dist/lib/memoryGraph.js +9 -3
  66. package/dist/lib/memoryProvider.d.ts +58 -0
  67. package/dist/lib/memoryProvider.js +175 -0
  68. package/dist/lib/memoryStatus.d.ts +17 -0
  69. package/dist/lib/memoryStatus.js +88 -0
  70. package/dist/lib/nativeMemory.d.ts +19 -0
  71. package/dist/lib/nativeMemory.js +129 -0
  72. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  73. package/dist/lib/runtimeControlProvider.js +168 -0
  74. package/dist/lib/secretEnv.d.ts +2 -1
  75. package/dist/lib/secretEnv.js +6 -2
  76. package/dist/lib/skillProvider.d.ts +13 -0
  77. package/dist/lib/skillProvider.js +210 -0
  78. package/dist/lib/taskProvider.d.ts +7 -0
  79. package/dist/lib/taskProvider.js +43 -0
  80. package/dist/lib/taskStore.d.ts +46 -0
  81. package/dist/lib/taskStore.js +156 -0
  82. package/dist/lib/terminalPicker.d.ts +23 -49
  83. package/dist/lib/terminalPicker.js +53 -218
  84. package/dist/pi-bridge/bundle.js +475 -37
  85. package/dist/pi-bridge/index.js +42 -21
  86. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  87. package/dist/pi-bridge/schemaTranslate.js +4 -8
  88. package/dist/probes/kimi-code.d.ts +12 -0
  89. package/dist/probes/kimi-code.js +55 -0
  90. package/dist/sdk.d.ts +1 -1
  91. package/docs/architecture.md +224 -26
  92. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  93. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  94. package/docs/assets/trellis-mark.svg +12 -0
  95. package/docs/getting-started.md +328 -35
  96. package/docs/overview.md +131 -0
  97. package/docs/overview.zh-CN.md +100 -0
  98. package/docs/research.md +59 -0
  99. package/docs/roadmap.md +9 -5
  100. package/package.json +3 -2
  101. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  102. package/schema/capability-selection.example.yaml +23 -0
  103. package/schema/scope.example.yaml +1 -1
  104. package/schema/servers.example.yaml +15 -1
@@ -15,29 +15,17 @@ import { homedir } from "node:os";
15
15
  import { loadCanonicalSource } from "../core/canonical.js";
16
16
  import { resolveMcpPlan } from "../adapters/mcpPlan.js";
17
17
  import { connectServer, withTimeout, DEFAULT_CONNECT_TIMEOUT_MS } from "../lib/mcpConnect.js";
18
+ import { McpToolRegistry } from "../lib/mcpToolRegistry.js";
19
+ import { withOAuthHeader } from "../lib/gatewayBackend.js";
18
20
  import { ALL_AGENTS } from "../core/types.js";
19
- import { bridgedToolName, toParametersSchema, toPiContent } from "./schemaTranslate.js";
21
+ import { toParametersSchema, toPiContent } from "./schemaTranslate.js";
20
22
  /** Re-exported so `trellis-mcp-connect-timeout`'s existing test keeps
21
23
  * importing it from the module it was written against — proving the
22
24
  * extraction changed nothing it could observe. */
23
25
  export { withTimeout };
24
26
  const CLIENT_INFO = { name: "trellis-mcp-bridge", version: "0.0.0" };
25
- function registerServerTools(pi, serverName, client, timeoutMs) {
26
- return withTimeout(client.listTools(), timeoutMs, `listTools timed out after ${timeoutMs}ms`).then((result) => {
27
- for (const tool of result.tools) {
28
- pi.registerTool({
29
- name: bridgedToolName(serverName, tool.name),
30
- label: tool.name,
31
- description: tool.description ?? `MCP tool "${tool.name}" from server "${serverName}"`,
32
- parameters: toParametersSchema(tool.inputSchema),
33
- async execute(_toolCallId, params) {
34
- const result = await client.callTool({ name: tool.name, arguments: params });
35
- const content = Array.isArray(result.content) ? result.content : [];
36
- return { content: toPiContent(content), details: result };
37
- },
38
- });
39
- }
40
- });
27
+ function listServerTools(client, timeoutMs) {
28
+ return withTimeout(client.listTools(), timeoutMs, `listTools timed out after ${timeoutMs}ms`).then((result) => result.tools);
41
29
  }
42
30
  /**
43
31
  * `homeDir`/`connectTimeoutMs` default to the real `~` / 10s — only
@@ -57,6 +45,7 @@ export default async function trellisMcpBridge(pi, homeDir = homedir(), connectT
57
45
  // is pi reading canonical directly at its own runtime, P4's own concern.
58
46
  const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
59
47
  const clients = new Set();
48
+ const registry = new McpToolRegistry();
60
49
  const closeClient = async (client) => {
61
50
  if (!clients.delete(client))
62
51
  return;
@@ -83,25 +72,57 @@ export default async function trellisMcpBridge(pi, homeDir = homedir(), connectT
83
72
  // pi 0.85.x emits this before tearing down an extension runtime. Keep the
84
73
  // hook optional so the bridge remains loadable in older compatible hosts.
85
74
  pi.on?.("session_shutdown", closeAllClients);
86
- await Promise.all(desired.map(async ({ name, def }) => {
75
+ const discovered = await Promise.all(desired.map(async ({ name, def }) => {
87
76
  let client;
88
77
  try {
89
- client = await connectServer(def, canonical.secretsPolicy, connectTimeoutMs, CLIENT_INFO);
78
+ const effectiveDef = def.auth === "oauth" && def.url
79
+ ? await withOAuthHeader(name, def, homeDir)
80
+ : def;
81
+ client = await connectServer(effectiveDef, canonical.secretsPolicy, connectTimeoutMs, CLIENT_INFO);
90
82
  }
91
83
  catch (err) {
92
84
  // One unreachable/misconfigured (including permanently hanging —
93
85
  // trellis-mcp-connect-timeout) server must never prevent every
94
86
  // other server's tools from registering (tasks.md 3.2).
95
87
  console.error(`trellis-mcp-bridge: failed to connect to MCP server "${name}": ${err instanceof Error ? err.message : String(err)}`);
96
- return;
88
+ return undefined;
97
89
  }
98
90
  clients.add(client);
99
91
  try {
100
- await registerServerTools(pi, name, client, connectTimeoutMs);
92
+ const tools = await listServerTools(client, connectTimeoutMs);
93
+ return { name, client, tools };
101
94
  }
102
95
  catch (err) {
103
96
  console.error(`trellis-mcp-bridge: failed to list tools for MCP server "${name}": ${err instanceof Error ? err.message : String(err)}`);
104
97
  await closeClient(client);
98
+ return undefined;
105
99
  }
106
100
  }));
101
+ // Add in canonical plan order, not completion order, so allocation remains
102
+ // stable when multiple upstreams answer concurrently.
103
+ for (const result of discovered) {
104
+ if (!result)
105
+ continue;
106
+ registry.add(result.name, result.client, result.tools);
107
+ }
108
+ for (const tool of registry.listTools()) {
109
+ const target = registry.resolve(tool.name);
110
+ if (!target)
111
+ continue;
112
+ const originalName = target.toolName;
113
+ const serverName = target.serverName;
114
+ pi.registerTool({
115
+ name: tool.name,
116
+ label: originalName,
117
+ description: tool.description ?? `MCP tool "${originalName}" from server "${serverName}"`,
118
+ parameters: toParametersSchema(tool.inputSchema),
119
+ async execute(_toolCallId, params) {
120
+ const result = await registry.callTool(tool.name, params);
121
+ const content = Array.isArray(result.content)
122
+ ? result.content
123
+ : [];
124
+ return { content: toPiContent(content), details: result };
125
+ },
126
+ });
127
+ }
107
128
  }
@@ -50,12 +50,8 @@ export declare function toParametersSchema(inputSchema: unknown): TSchema;
50
50
  * own `AgentToolResult.content` has no representation for them
51
51
  * (design.md D3). */
52
52
  export declare function toPiContent(items: McpContentItem[]): PiContent[];
53
- /** `${serverName}__${toolName}` — deterministic, collision-free across
54
- * servers sharing pi.registerTool()'s single flat namespace (design.md D5).
55
- *
56
- * Now one implementation shared with the gateway's own aggregation rather
57
- * than two identical ones: the naming convention is the contract between
58
- * a prefixed name and the server it routes to, and two copies of it could
59
- * drift into routing tools to the wrong upstream
60
- * (trellis-mcp-gateway-hosting design.md D3). */
53
+ /** Legacy direct prefix helper kept for callers/tests that need to render a
54
+ * source-qualified name. The live bridge uses `McpToolRegistry` so it can
55
+ * allocate short names across all connected servers, normalize invalid
56
+ * characters, and retain the raw name for routing. */
61
57
  export { prefixedToolName as bridgedToolName } from "../lib/mcpToolRegistry.js";
@@ -33,12 +33,8 @@ export function toPiContent(items) {
33
33
  }
34
34
  });
35
35
  }
36
- /** `${serverName}__${toolName}` — deterministic, collision-free across
37
- * servers sharing pi.registerTool()'s single flat namespace (design.md D5).
38
- *
39
- * Now one implementation shared with the gateway's own aggregation rather
40
- * than two identical ones: the naming convention is the contract between
41
- * a prefixed name and the server it routes to, and two copies of it could
42
- * drift into routing tools to the wrong upstream
43
- * (trellis-mcp-gateway-hosting design.md D3). */
36
+ /** Legacy direct prefix helper kept for callers/tests that need to render a
37
+ * source-qualified name. The live bridge uses `McpToolRegistry` so it can
38
+ * allocate short names across all connected servers, normalize invalid
39
+ * characters, and retain the raw name for routing. */
44
40
  export { prefixedToolName as bridgedToolName } from "../lib/mcpToolRegistry.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Kimi Code probe. Kimi 2.x stores user MCP declarations in
3
+ * `~/.kimi-code/mcp.json`, user Skills in `~/.kimi-code/skills`, and global
4
+ * instructions in `~/.kimi-code/AGENTS.md`. Kimi also discovers the generic
5
+ * `~/.agents/skills` root; it is reported so doctor can observe it, while the
6
+ * Runtime-only launcher can explicitly isolate it later.
7
+ */
8
+ import type { AgentSnapshot } from "../core/types.js";
9
+ export interface ProbeOptions {
10
+ probeMcp?: boolean;
11
+ }
12
+ export declare function probe(homeDir?: string, opts?: ProbeOptions): Promise<AgentSnapshot>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Kimi Code probe. Kimi 2.x stores user MCP declarations in
3
+ * `~/.kimi-code/mcp.json`, user Skills in `~/.kimi-code/skills`, and global
4
+ * instructions in `~/.kimi-code/AGENTS.md`. Kimi also discovers the generic
5
+ * `~/.agents/skills` root; it is reported so doctor can observe it, while the
6
+ * Runtime-only launcher can explicitly isolate it later.
7
+ */
8
+ import { execFileSync } from "node:child_process";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { probeMcpServer } from "../lib/mcpProbe.js";
12
+ import { pathRef, readJsonFile, resolveEnvRefs, scanSkillRoot } from "../lib/probeCommon.js";
13
+ function kimiEnv(homeDir) {
14
+ return { ...process.env, HOME: homeDir, KIMI_CODE_HOME: join(homeDir, ".kimi-code") };
15
+ }
16
+ export async function probe(homeDir = homedir(), opts = {}) {
17
+ const root = join(homeDir, ".kimi-code");
18
+ const mcpPath = join(root, "mcp.json");
19
+ const mcpConfig = readJsonFile(mcpPath);
20
+ const kimiRoot = pathRef(root);
21
+ if (kimiRoot === undefined) {
22
+ return { agent: "kimi-code", present: false, skillRoots: [], mcpServers: [], diagnostics: [] };
23
+ }
24
+ let version;
25
+ try {
26
+ version = execFileSync("kimi", ["--version"], { encoding: "utf-8", timeout: 5_000, env: kimiEnv(homeDir) }).trim();
27
+ }
28
+ catch {
29
+ // A config-only fixture remains probeable without a binary on PATH.
30
+ }
31
+ const mcpServers = Object.entries(mcpConfig?.mcpServers ?? {}).map(([name, def]) => ({
32
+ name,
33
+ transport: def.url ? (def.transport === "sse" ? "sse" : "http") : "stdio",
34
+ }));
35
+ if (opts.probeMcp) {
36
+ await Promise.all(Object.entries(mcpConfig?.mcpServers ?? {}).map(async ([name, def], index) => {
37
+ if (def.url || !def.command)
38
+ return;
39
+ mcpServers[index].probe = await probeMcpServer({ transport: "stdio", command: def.command, args: def.args }, resolveEnvRefs(def.env, kimiEnv(homeDir)));
40
+ }));
41
+ }
42
+ const skillRoots = [
43
+ scanSkillRoot(join(root, "skills")),
44
+ scanSkillRoot(join(homeDir, ".agents", "skills")),
45
+ ].filter((entry) => entry !== undefined);
46
+ return {
47
+ agent: "kimi-code",
48
+ present: true,
49
+ version,
50
+ skillRoots,
51
+ mcpServers,
52
+ instructionsFile: pathRef(join(root, "AGENTS.md")),
53
+ diagnostics: [],
54
+ };
55
+ }
package/dist/sdk.d.ts CHANGED
@@ -11,4 +11,4 @@
11
11
  */
12
12
  export { loadCanonicalSource } from "./core/canonical.js";
13
13
  export { ALL_AGENTS, resolveScope } from "./core/types.js";
14
- export type { AgentId, AgentProfile, CanonicalSource, HubConfig, McpConfig, McpServerDef, MemoryEntry, Scope, SecretsPolicy, SkillRef, Transport, } from "./core/types.js";
14
+ export type { AgentId, AgentProfile, CapabilityDelivery, CanonicalSource, HubConfig, McpConfig, McpRuntimeConfig, McpServerDef, MemoryEntry, Scope, SecretsPolicy, SkillRef, Transport, } from "./core/types.js";
@@ -18,7 +18,7 @@ required reading for that.
18
18
  │ trellis sync / trellis doctor
19
19
  ┌───────┼────────┬─────────────┬──────────────┐
20
20
  ▼ ▼ ▼ ▼
21
- Claude Code Codex Kiro pi
21
+ Claude Code Codex Kiro pi Kimi Code
22
22
  (symlink adapter) (incremental (symlink adapter) (bridge
23
23
  TOML writer) extension)
24
24
  ```
@@ -57,7 +57,7 @@ Aligned to the `.agents Protocol` draft, extended where the draft is silent:
57
57
  ## Global vs. workspace scope
58
58
 
59
59
  **Global only, for now.** Trellis manages `~/.trellis` — one canonical
60
- source per machine, applied to that machine's four agents. It does not yet
60
+ source per machine, applied to that machine's five supported agents. It does not yet
61
61
  read or merge a project-local `.trellis/` in a specific repo.
62
62
 
63
63
  The `.agents Protocol` draft (see `research.md`) describes a two-layer
@@ -72,11 +72,11 @@ that's scope creep against this section, not a natural extension.
72
72
 
73
73
  ## Private / agent-specific capabilities
74
74
 
75
- Not everything belongs to all four agents. A skill built around Claude
75
+ Not everything belongs to all five agents. A skill built around Claude
76
76
  Code's Task-based subagent delegation has no equivalent to delegate to on
77
77
  Codex; an MCP server might only make sense for one agent's workflow. Every
78
78
  scopable item — skill, subagent profile, memory entry, MCP server —
79
- defaults to "shared with all four," and can be restricted with an explicit
79
+ defaults to "shared with all managed agents," and can be restricted with an explicit
80
80
  `scope` (skills/agents/memories) or inline `agents:` (MCP servers, see
81
81
  below). Restriction is the exception you declare, not something you
82
82
  configure for the common case.
@@ -173,8 +173,22 @@ the hub instead — meaningfully less code and one fewer class of failure
173
173
  the project that is an agent runtime extension, not a config generator,
174
174
  regardless of hub mode.
175
175
 
176
+ The pi bridge symlink follows the currently executing Trellis installation.
177
+ When switching from a checkout to a global package (or between installation
178
+ roots), sync can repair an outside-root link only if its target ends in
179
+ `dist/pi-bridge/bundle.js` and its bytes match the current bridge bundle.
180
+ The repair is backed up, including the old symlink target. Modified, unrelated,
181
+ or dangling outside-root links remain conflicts; this exception applies only
182
+ to the bridge extension, not to Skill or instruction symlinks.
183
+
176
184
  ## MCP hub mode
177
185
 
186
+ For onboarding, the short rule is: **Gateway is the recommended local
187
+ Trellis mode; Hub is for an already-running external service**. Gateway is a
188
+ local stdio subprocess that Trellis starts for the Agent session. Hub is an
189
+ HTTP endpoint operated outside this process; Trellis only writes the URL and
190
+ does not deploy, configure, or health-manage that Hub.
191
+
178
192
  Every agent's MCP surface can be either N direct server definitions
179
193
  (default) or one static entry pointing at a single HTTP endpoint — `mcp.hub.url`
180
194
  in `mcp/servers.yaml` is the field, but the supported way to set it is
@@ -197,7 +211,7 @@ to the code writing one entry that points at it.
197
211
  - Every adapter writes ONE entry instead of N. This is most of the value:
198
212
  adding a new backend server means editing wherever the hub's own config
199
213
  lives (this project doesn't prescribe that either) and never touching
200
- any of the four agents' configs or restarting them, if the hub supports
214
+ any of the five agents' configs or restarting them, if the hub supports
201
215
  live reload (mcp-hub does, via SSE).
202
216
  - The collision check against `known_host_injected` (docs/research.md
203
217
  §"Codex — three hard constraints") shrinks to checking one name instead
@@ -245,23 +259,29 @@ Codex's `codex mcp login`/`logout` pair, Kiro's `oauth`/`oauthScopes`
245
259
  fields — and pi, whose bridge is Trellis's own code, simply couldn't
246
260
  reach such a server at all.
247
261
 
248
- That changed with gateway mode (below), which made it unavoidable: in
249
- gateway mode the agent connects to Trellis, and Trellis connects to the
250
- remote server, so the agent's own OAuth flow is no longer on the path.
251
- Trellis now implements the client half itself — discovery (RFC 8414 /
252
- RFC 9728), dynamic client registration (RFC 7591), authorization code
253
- with PKCE S256 (RFC 7636), and the refresh grant — in `src/lib/oauth/`.
262
+ Gateway mode does not automatically take ownership of OAuth servers. Mark a
263
+ server explicitly with `auth: oauth` (or `trellis mcp set <name> --auth oauth`)
264
+ and gateway planning keeps it direct. This preserves the native Agent OAuth
265
+ flow for Claude Code/Codex/Kiro; Pi's bridge is the client for its direct OAuth
266
+ connection and may use Trellis's own OAuth store. Trellis never infers this
267
+ classification from a URL or a transient 401.
268
+
269
+ Trellis still implements the client half itself — discovery (RFC 8414 /
270
+ RFC 9728), dynamic client registration (RFC 7591), authorization code with
271
+ PKCE S256 (RFC 7636), and the refresh grant — in `src/lib/oauth/`, but that
272
+ path is currently used for Pi/direct bridge connections rather than silently
273
+ replacing native Agent authorization.
254
274
 
255
275
  Two rules shape it, both from the constraint that the gateway is spawned
256
276
  silently by an agent with no terminal attached:
257
277
 
258
- - **Only `trellis mcp auth <server>` ever opens a browser.** It is run by
259
- a human, once. The gateway never initiates an interactive flow under
260
- any circumstance; a server with no stored credential is skipped the
261
- same way an unreachable one is.
278
+ - **Only an explicit human authorization command opens a browser.** Native
279
+ clients use their own login command for direct OAuth entries. Pi's direct
280
+ bridge uses `trellis mcp auth <server>`. The gateway never initiates an
281
+ interactive flow and never connects an OAuth-marked server.
262
282
  - **Refreshing is silent, and serialized by a lock.** A `refresh_token`
263
- grant needs neither a browser nor a callback, so the gateway does it
264
- at connect time. The lock (`~/.trellis/mcp/oauth/<name>.lock`) exists
283
+ grant needs neither a browser nor a callback, so the Pi direct bridge can
284
+ do it at connect time. The lock (`~/.trellis/mcp/oauth/<name>.lock`) exists
265
285
  because most authorization servers rotate the refresh token and
266
286
  invalidate its predecessor — two concurrent refreshes would leave one
267
287
  party holding a credential that is already dead.
@@ -272,8 +292,9 @@ mode 0600, never in `servers.yaml` and never in the OS keychain (macOS's
272
292
  non-interactive, no-GUI-session context the gateway runs in — measured,
273
293
  not assumed).
274
294
 
275
- In direct mode nothing here applies: each agent keeps using its own
276
- native OAuth flow, exactly as before.
295
+ In direct mode nothing here changes: each agent keeps using its own native
296
+ OAuth flow. In gateway mode, an OAuth-marked server is also kept direct; only
297
+ ordinary MCP servers enter the gateway.
277
298
 
278
299
  ## MCP gateway mode
279
300
 
@@ -311,15 +332,25 @@ true rather than aspirational:
311
332
  would have written for that agent (same `resolveMcpPlan`, so per-server
312
333
  `agents:` scope, `enabled: false`, host-injected collisions,
313
334
  literal-secret refusals and unresolved `env` names all behave
314
- identically), connects each one, and exposes their tools as
315
- `<server>__<tool>`. Secrets resolve through `resolveSecretEnv` exactly as
335
+ identically), connects each one, and exposes their tools through the shared
336
+ Agent-safe name allocator. Names contain only `[A-Za-z0-9_-]` and are at most
337
+ 64 characters; unique short names stay short, while duplicate names retain a
338
+ normalized `<server>__<tool>` identity. Secrets resolve through
339
+ `resolveSecretEnv` exactly as
316
340
  in direct mode — the gateway introduces no second path by which a value
317
341
  could reach disk.
318
342
 
319
343
  **What it changes for the agents:** Codex's single-`bearer_token_env_var`
320
344
  limitation stops applying, because Codex no longer receives any server's
321
- `headers` — the gateway holds them. Remote servers needing real OAuth
322
- become reachable from every agent, including pi, for the same reason.
345
+ `headers` — the gateway holds them. OAuth-marked servers are intentionally
346
+ kept direct; ordinary remote servers remain shared through the gateway. An
347
+ upstream tool keeps its original name when that name is unique and longer than
348
+ the short-name threshold; generic short names receive a compact
349
+ `<server>__<tool>` prefix even when unique. A tool already carrying its source
350
+ prefix is not repeated. Dotted or otherwise invalid names are normalized, and long names
351
+ receive a deterministic hash suffix within the 64-character budget. If
352
+ normalized names still collide, Trellis adds a stable suffix instead of
353
+ dropping a tool.
323
354
 
324
355
  **The seam.** `src/commands/mcpGateway.ts` depends on a `GatewayBackend`
325
356
  interface and nothing below it — never a connection, transport, or
@@ -332,12 +363,117 @@ substitution at one construction site: the entry written into each
332
363
  agent's config is identical either way, so converging later requires no
333
364
  re-sync and nothing the user notices.
334
365
 
366
+ ## MCP Runtime direction
367
+
368
+ Gateway mode is the first consumer of a broader runtime shape. The runtime
369
+ edge is available as `trellis mcp-runtime --agent <id>`. Existing
370
+ `trellis mcp-gateway --agent <id>` entries are a compatibility alias and now
371
+ use the same provider registry. Both edges can expose Trellis-owned
372
+ capabilities and selected upstream MCP capabilities:
373
+
374
+ ```text
375
+ Agent
376
+ │ one MCP connection
377
+ ▼
378
+ Trellis MCP Runtime
379
+ ├─ BuiltinRegistry
380
+ │ ├─ SkillProvider (read/search canonical skills)
381
+ │ ├─ RuntimeMemoryProvider
382
+ │ │ └─ CanonicalMemoryProvider (read-only canonical Markdown)
383
+ │ └─ StatusProvider (future read-only diagnostics)
384
+ └─ UpstreamProvider
385
+ └─ existing GatewayBackend / future RemoteBackend
386
+ ```
387
+
388
+ The runtime is a protocol edge; providers are the capability boundary. A
389
+ provider may expose tools, resources, prompts, or a combination. The current
390
+ `GatewayBackend` remains the upstream tool aggregation implementation and is
391
+ mounted as an `UpstreamProvider`, so connection, OAuth, timeout, namespace,
392
+ and cleanup logic stay in one place.
393
+
394
+ Native skills remain the default delivery. Runtime MCP delivery is configured
395
+ per agent under `mcp.runtime.delivery` and supports `native`, `mcp`, or
396
+ `both`. The built-in providers are intentionally read-only and scope-filtered:
397
+ SkillProvider reads canonical skills, while RuntimeMemoryProvider adapts a
398
+ CanonicalMemoryProvider that reads canonical Markdown memories through
399
+ `trellis.memory.search`, `trellis.memory.read`, and
400
+ `trellis://memories/<name>.md`. Neither executes scripts or mutates canonical
401
+ state. When the documented `memory` MCP backend is enabled, Agent-created
402
+ entities and observations are written through that upstream MCP server; the
403
+ Runtime provider does not duplicate its graph protocol. `trellis memory
404
+ extract` is the explicit, conflict-safe graph-to-canonical path. A future
405
+ local-graph/OpenViking source can implement the same MemoryProvider contract,
406
+ and native/private Agent stores are never scraped implicitly.
407
+
408
+ `trellis.runtime.status` reports Memory readiness separately: backend
409
+ configuration, graph existence/readiness, canonical count, write authority,
410
+ and which managed Agents receive the shared route. This prevents an empty
411
+ canonical directory from being mistaken for a missing or unusable backend.
412
+
413
+ Onboarding keeps two Memory decisions independent:
414
+
415
+ - `--memory-migrate on|off` imports supported native Markdown memory into
416
+ canonical `memories/*.md`.
417
+ - `--memory on|off` enables the shared Memory MCP backend and graph.
418
+
419
+ The first adapter is Claude Code's exact current-workspace project-memory
420
+ directory. Kimi Code and pi session stores, and Kiro databases, are reported as
421
+ unsupported until a format-specific adapter is reviewed; Trellis does not
422
+ interpret sessions or caches as memory.
423
+
424
+ Kimi Code is Runtime-first. Its adapter writes one `trellis` entry to
425
+ `~/.kimi-code/mcp.json`; it does not copy canonical Skills into
426
+ `~/.kimi-code/skills` when delivery is `mcp`. Use `trellis kimi` for this mode:
427
+ the launcher passes Kimi's documented `--skills-dir` override an empty
428
+ temporary directory, preventing Kimi's automatic `~/.agents/skills` discovery
429
+ from duplicating the Runtime SkillProvider. Kimi's native mode remains
430
+ available when delivery is `native` or `both`.
431
+
432
+ Kimi's user MCP registry is `~/.kimi-code/mcp.json`; the adapter preserves
433
+ unowned entries and uses the existing MCP ownership ledger for the one entry
434
+ Trellis owns. The Kimi CLI itself remains responsible for login and OAuth.
435
+
436
+ ## Built-in Trellis Skill lifecycle
437
+
438
+ `schema/builtin-skills/trellis-runtime/SKILL.md` is part of the Trellis
439
+ package, not a user-specific copy. `trellis init` bootstraps it into the
440
+ canonical source at `~/.trellis/skills/trellis-runtime/SKILL.md`; subsequent
441
+ changes are made once there and distributed through the normal adapter path.
442
+
443
+ The Skill has two contracts:
444
+
445
+ - **Takeover contract:** describe the safe operator flow — inspect, dry-run,
446
+ choose the migration source and managed boundary, select capabilities,
447
+ authorize OAuth, sync, audit, verify, and rollback. This is a procedure for
448
+ the human or an Agent assisting the human; it is not a license for an Agent
449
+ to silently rewrite native configuration.
450
+ - **Runtime contract:** describe how a managed Agent consumes
451
+ `SkillProvider`, `RuntimeMemoryProvider`, the instruction provider, status
452
+ providers, and task handoff providers through the current MCP edge.
453
+
454
+ Delivery determines how the second contract reaches an Agent:
455
+
456
+ | Delivery | Projection | Runtime Skill source |
457
+ | --- | --- | --- |
458
+ | `native` | Trellis-owned native symlink | Agent's native Skill discovery |
459
+ | `mcp` | No native Trellis Skill projection | `SkillProvider` through `trellis mcp-runtime` |
460
+ | `both` | Native projection plus Runtime entry | Both, intentionally during compatibility or transition |
461
+
462
+ This keeps takeover instructions and post-takeover capability instructions in
463
+ one versioned Skill while preserving the security boundary: Runtime providers
464
+ are external context, native instructions remain authoritative at startup,
465
+ and task mutations require explicit confirmation. The MCP client's generated
466
+ namespace (for example `mcp__trellis__skills_search`) is not part of the
467
+ portable Skill contract.
468
+
335
469
  ## What Trellis explicitly does not build
336
470
 
337
471
  - A resident MCP gateway daemon. Gateway mode above is a per-session
338
472
  subprocess with no lifecycle; hub mode points at something you operate.
339
473
  Neither is a service Trellis starts, supervises, or keeps running.
340
- - A memory backend (defaults to `@modelcontextprotocol/server-memory`,
474
+ - A semantic/external memory backend (the built-in provider only exposes
475
+ read-only canonical Markdown; the default graph backend remains
476
+ `@modelcontextprotocol/server-memory`,
341
477
  turned on with `trellis onboard --memory on` (trellis-onboard-mcp-mode)
342
478
  rather than hand-editing `servers.yaml`, documented in
343
479
  `schema/servers.example.yaml`; mem0/OpenMemory and totalrecallai-class
@@ -419,6 +555,53 @@ once; it is never where you repeatedly verify against it.
419
555
  ```
420
556
  scripts/sandbox.sh # interactive shell in the sandbox
421
557
  scripts/sandbox.sh npm run dev doctor # run a command in the sandbox
558
+ scripts/sandbox.sh --runtime # divergent multi-agent Runtime lab
559
+ scripts/sandbox.sh --migration # Kiro -> Codex post-migration lab
560
+ scripts/sandbox.sh --management # steady-state unified management lab
561
+ scripts/sandbox.sh --failure # hanging-upstream isolation lab
562
+ scripts/sandbox-matrix.sh # run the full scenario matrix
563
+ scripts/agent-sandbox.sh # real Codex/Claude/Kiro/pi/Kimi CLI config smoke
564
+ ```
565
+
566
+ `agent-sandbox.sh` 默认使用一次性 HOME。需要进行授权时,显式使用专用
567
+ Docker volume(默认名为 `trellis-agent-auth-home`):
568
+
569
+ 如果宿主机已经登录 Codex 或 pi,可以显式复用单个认证文件:
570
+
571
+ ```
572
+ scripts/agent-sandbox.sh --host-auth codex
573
+ scripts/agent-sandbox.sh --host-auth pi
574
+ ```
575
+
576
+ 这两个命令只读取宿主机对应的 `auth.json`,不读取整个 agent 配置目录。
577
+
578
+ ```
579
+ scripts/agent-sandbox.sh --login codex
580
+ scripts/agent-sandbox.sh --login claude
581
+ scripts/agent-sandbox.sh --login kiro
582
+ scripts/agent-sandbox.sh --status codex
583
+ scripts/agent-sandbox.sh --status claude
584
+ scripts/agent-sandbox.sh --status kiro
585
+ scripts/agent-sandbox.sh --auth codex exec --help
586
+ scripts/agent-sandbox.sh --auth claude -p --help
587
+ scripts/agent-sandbox.sh --prepare
588
+ scripts/agent-sandbox.sh --host-auth codex
589
+ scripts/agent-sandbox.sh --host-auth pi
590
+ ```
591
+
592
+ 授权只写入这个 Docker volume,不会复制宿主机的登录态,也不会进入
593
+ Git、镜像或 fixture。`--login` 会分别执行真实 agent 的登录命令:
594
+ Codex 使用 device auth,Claude Code 使用 Claude subscription 登录,
595
+ Kiro CLI 使用 free license 的 device flow。Kiro 在未登录时会明确拒绝
596
+ `mcp list`,这是预期的授权前置条件。需要清空授权 volume 时,应由用户
597
+ 显式执行 `docker volume rm <volume-name>`;Trellis 不会自动删除它。
598
+
599
+ 授权后若要在同一登录态中运行真实 agent,使用 `--auth` 前缀,例如:
600
+
601
+ ```
602
+ scripts/agent-sandbox.sh --prepare
603
+ scripts/agent-sandbox.sh --auth codex exec ...
604
+ scripts/agent-sandbox.sh --auth claude -p ...
422
605
  ```
423
606
 
424
607
  Backed by OrbStack (this machine's Docker context — a Mac-native,
@@ -449,5 +632,20 @@ known findings, not just to look plausible:
449
632
  exercise the case-sensitivity check.
450
633
  - `test/fixtures/sample-mcp-server.js` — a minimal real MCP server (reads
451
634
  `initialize` over stdio, replies with fixed `serverInfo`) so
452
- `probeMcpServer` has something deterministic to handshake against inside
453
- the container without depending on a real npm package or network access.
635
+ `probeMcpServer` has something deterministic to handshake against inside
636
+ the container without depending on a real npm package or network access.
637
+
638
+ For the MCP Runtime phase, the sandbox has a separate intentionally
639
+ divergent fixture rather than expanding the baseline one:
640
+
641
+ ```
642
+ scripts/sandbox.sh --runtime
643
+ ```
644
+
645
+ This scenario gives Claude Code, Codex, Kiro, pi, and Kimi Code different native
646
+ skills, instructions, and MCP entries; adds canonical scope, per-agent
647
+ direct/gateway routes, `native`/`mcp`/`both` delivery, a deliberate
648
+ host-injected collision, and canonical memory content. The lab then runs
649
+ native sync, MCP sync, memory sync, secrets audit, and real MCP client
650
+ handshakes against Claude/Codex/pi/Kimi runtime views. All writes stay inside
651
+ the container-local HOME.
@@ -0,0 +1,80 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" role="img" aria-labelledby="title desc">
2
+ <title id="title">Agent Trellis — Unified Code Agent Runtime</title>
3
+ <desc id="desc">A horizontal architecture diagram showing Code Agents running on top of Agent Trellis Runtime, which manages Skills, MCP, Memory, and Instructions.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#0b1118"/>
7
+ <stop offset="1" stop-color="#111c24"/>
8
+ </linearGradient>
9
+ <linearGradient id="base" x1="0" y1="0" x2="0" y2="1">
10
+ <stop offset="0" stop-color="#273742"/>
11
+ <stop offset="1" stop-color="#17232c"/>
12
+ </linearGradient>
13
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="160%">
14
+ <feDropShadow dx="0" dy="18" stdDeviation="16" flood-color="#000" flood-opacity=".35"/>
15
+ </filter>
16
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
17
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#a9efd2"/>
18
+ </marker>
19
+ <style>
20
+ .sans { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
21
+ .mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; }
22
+ .title { fill: #f5efe3; font-size: 42px; font-weight: 720; letter-spacing: -.8px; }
23
+ .subtitle { fill: #a9efd2; font-size: 18px; font-weight: 620; letter-spacing: 2px; }
24
+ .label { fill: #f5efe3; font-size: 19px; font-weight: 680; }
25
+ .small { fill: #9aaab4; font-size: 14px; }
26
+ .tiny { fill: #7f919d; font-size: 12px; }
27
+ </style>
28
+ </defs>
29
+
30
+ <rect width="1600" height="900" fill="url(#bg)"/>
31
+ <path d="M0 120H1600M0 780H1600M120 0V900M1480 0V900" stroke="#24333d" stroke-width="1" opacity=".55"/>
32
+
33
+ <g class="sans">
34
+ <text x="100" y="94" class="title">Agent Trellis</text>
35
+ <text x="102" y="127" class="subtitle">UNIFIED CODE AGENT RUNTIME</text>
36
+ <text x="1500" y="96" text-anchor="end" class="small">one runtime · many coding agents</text>
37
+
38
+ <!-- Agent layer -->
39
+ <text x="800" y="190" text-anchor="middle" class="small" letter-spacing="2">CODE AGENTS</text>
40
+ <g filter="url(#shadow)">
41
+ <rect x="110" y="224" width="240" height="112" rx="16" fill="#17242c" stroke="#a9efd2" stroke-width="2"/>
42
+ <rect x="380" y="224" width="240" height="112" rx="16" fill="#17242c" stroke="#a9efd2" stroke-width="2"/>
43
+ <rect x="650" y="224" width="300" height="112" rx="16" fill="#17242c" stroke="#f2b35b" stroke-width="3"/>
44
+ <rect x="980" y="224" width="240" height="112" rx="16" fill="#17242c" stroke="#a9efd2" stroke-width="2"/>
45
+ <rect x="1250" y="224" width="240" height="112" rx="16" fill="#17242c" stroke="#a9efd2" stroke-width="2"/>
46
+ </g>
47
+ <text x="230" y="274" text-anchor="middle" class="label">Claude Code</text>
48
+ <text x="230" y="303" text-anchor="middle" class="tiny">native adapter</text>
49
+ <text x="500" y="274" text-anchor="middle" class="label">Codex</text>
50
+ <text x="500" y="303" text-anchor="middle" class="tiny">native adapter</text>
51
+ <text x="800" y="274" text-anchor="middle" class="label">Kimi Code</text>
52
+ <text x="800" y="303" text-anchor="middle" class="tiny">Runtime-first</text>
53
+ <text x="1100" y="274" text-anchor="middle" class="label">Kiro</text>
54
+ <text x="1100" y="303" text-anchor="middle" class="tiny">native adapter</text>
55
+ <text x="1370" y="274" text-anchor="middle" class="label">pi</text>
56
+ <text x="1370" y="303" text-anchor="middle" class="tiny">bridge adapter</text>
57
+
58
+ <path d="M230 338V405M500 338V405M800 338V405M1100 338V405M1370 338V405" stroke="#a9efd2" stroke-width="2" marker-end="url(#arrow)"/>
59
+
60
+ <!-- Runtime foundation -->
61
+ <g filter="url(#shadow)">
62
+ <rect x="140" y="420" width="1320" height="220" rx="24" fill="url(#base)" stroke="#f2b35b" stroke-width="2"/>
63
+ <rect x="165" y="445" width="1270" height="68" rx="14" fill="#0e1820" stroke="#41515b"/>
64
+ <rect x="165" y="528" width="1270" height="68" rx="14" fill="#0e1820" stroke="#41515b"/>
65
+ </g>
66
+ <text x="800" y="475" text-anchor="middle" class="label" font-size="24">Agent Trellis Runtime</text>
67
+ <text x="800" y="501" text-anchor="middle" class="tiny">canonical source · policy · providers · adapters</text>
68
+ <text x="325" y="570" text-anchor="middle" class="label">Skills</text>
69
+ <text x="625" y="570" text-anchor="middle" class="label">MCP</text>
70
+ <text x="925" y="570" text-anchor="middle" class="label">Memory</text>
71
+ <text x="1225" y="570" text-anchor="middle" class="label">Instructions</text>
72
+ <path d="M325 540v-20M625 540v-20M925 540v-20M1225 540v-20" stroke="#f2b35b" stroke-width="3"/>
73
+
74
+ <!-- Canonical source -->
75
+ <path d="M800 640V700" stroke="#f2b35b" stroke-width="3" marker-end="url(#arrow)"/>
76
+ <rect x="400" y="714" width="800" height="86" rx="18" fill="#101a22" stroke="#53636c"/>
77
+ <text x="800" y="750" text-anchor="middle" class="label">Canonical Source</text>
78
+ <text x="800" y="777" text-anchor="middle" class="mono small">~/.trellis · one source of truth</text>
79
+ </g>
80
+ </svg>