agent-trellis 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.js +2 -2
- package/dist/adapters/codex.js +3 -3
- package/dist/adapters/jsonMcp.d.ts +1 -1
- package/dist/adapters/jsonMcp.js +2 -2
- package/dist/adapters/kimi-code.js +2 -2
- package/dist/adapters/kiro.js +11 -9
- package/dist/adapters/mcpPlan.d.ts +1 -1
- package/dist/adapters/mcpPlan.js +35 -12
- package/dist/adapters/pi.js +5 -5
- package/dist/adapters/zcode.d.ts +1 -1
- package/dist/adapters/zcode.js +6 -6
- package/dist/cli.js +12 -1
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +92 -7
- package/dist/commands/evolve.d.ts +43 -0
- package/dist/commands/evolve.js +479 -0
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mcpAuth.js +1 -1
- package/dist/commands/mcpGateway.d.ts +30 -2
- package/dist/commands/mcpGateway.js +44 -10
- package/dist/commands/mcpImport.js +2 -1
- package/dist/commands/secretsAudit.d.ts +1 -1
- package/dist/commands/secretsAudit.js +36 -13
- package/dist/evolve/agentView.d.ts +32 -0
- package/dist/evolve/agentView.js +34 -0
- package/dist/evolve/apply.d.ts +71 -0
- package/dist/evolve/apply.js +162 -0
- package/dist/evolve/archive.d.ts +23 -0
- package/dist/evolve/archive.js +58 -0
- package/dist/evolve/erase.d.ts +17 -0
- package/dist/evolve/erase.js +65 -0
- package/dist/evolve/extension.d.ts +37 -0
- package/dist/evolve/extension.js +63 -0
- package/dist/evolve/gate.d.ts +95 -0
- package/dist/evolve/gate.js +295 -0
- package/dist/evolve/importHistory.d.ts +91 -0
- package/dist/evolve/importHistory.js +239 -0
- package/dist/evolve/jobMain.d.ts +22 -0
- package/dist/evolve/jobMain.js +52 -0
- package/dist/evolve/jobRun.d.ts +94 -0
- package/dist/evolve/jobRun.js +273 -0
- package/dist/evolve/jobShell.d.ts +46 -0
- package/dist/evolve/jobShell.js +75 -0
- package/dist/evolve/l2.d.ts +93 -0
- package/dist/evolve/l2.js +81 -0
- package/dist/evolve/lesson.d.ts +57 -0
- package/dist/evolve/lesson.js +97 -0
- package/dist/evolve/pending.d.ts +69 -0
- package/dist/evolve/pending.js +116 -0
- package/dist/evolve/probeWorkspace.d.ts +57 -0
- package/dist/evolve/probeWorkspace.js +69 -0
- package/dist/evolve/propose.d.ts +73 -0
- package/dist/evolve/propose.js +136 -0
- package/dist/evolve/recorder.d.ts +68 -0
- package/dist/evolve/recorder.js +169 -0
- package/dist/evolve/report.d.ts +26 -0
- package/dist/evolve/report.js +37 -0
- package/dist/evolve/reportView.d.ts +96 -0
- package/dist/evolve/reportView.js +311 -0
- package/dist/evolve/risk.d.ts +106 -0
- package/dist/evolve/risk.js +224 -0
- package/dist/evolve/sources.d.ts +19 -0
- package/dist/evolve/sources.js +29 -0
- package/dist/evolve/stateDir.d.ts +21 -0
- package/dist/evolve/stateDir.js +56 -0
- package/dist/evolve/surface.d.ts +34 -0
- package/dist/evolve/surface.js +72 -0
- package/dist/evolve/trajectory.d.ts +25 -0
- package/dist/evolve/trajectory.js +88 -0
- package/dist/evolve/treeToCanonical.d.ts +28 -0
- package/dist/evolve/treeToCanonical.js +79 -0
- package/dist/evolve/trellisSubstrate.d.ts +65 -0
- package/dist/evolve/trellisSubstrate.js +154 -0
- package/dist/evolve/trigger.d.ts +177 -0
- package/dist/evolve/trigger.js +400 -0
- package/dist/evolve/types.d.ts +126 -0
- package/dist/evolve/types.js +15 -0
- package/dist/evolve/validate.d.ts +139 -0
- package/dist/evolve/validate.js +315 -0
- package/dist/host/checks.d.ts +57 -0
- package/dist/host/checks.js +126 -0
- package/dist/host/delivery.d.ts +42 -0
- package/dist/host/delivery.js +59 -0
- package/dist/host/edits.d.ts +87 -0
- package/dist/host/edits.js +102 -0
- package/dist/host/hostEvents.d.ts +74 -0
- package/dist/host/hostEvents.js +45 -0
- package/dist/host/index.d.ts +22 -0
- package/dist/host/index.js +17 -0
- package/dist/host/levels.d.ts +106 -0
- package/dist/host/levels.js +206 -0
- package/dist/host/staging.d.ts +56 -0
- package/dist/host/staging.js +106 -0
- package/dist/host/surface.d.ts +36 -0
- package/dist/host/surface.js +20 -0
- package/dist/lib/backup.js +12 -3
- package/dist/lib/envVarNames.js +7 -0
- package/dist/lib/fsAtomic.d.ts +7 -0
- package/dist/lib/fsAtomic.js +14 -0
- package/dist/lib/hostEventBus.d.ts +47 -0
- package/dist/lib/hostEventBus.js +94 -0
- package/dist/lib/hostExtensions.d.ts +75 -0
- package/dist/lib/hostExtensions.js +196 -0
- package/dist/lib/mcpConnect.d.ts +1 -1
- package/dist/lib/mcpConnect.js +19 -8
- package/dist/lib/mcpMigrateRead.js +14 -2
- package/dist/lib/mcpRuntime.d.ts +12 -1
- package/dist/lib/mcpRuntime.js +34 -2
- package/dist/lib/mcpToolRegistry.d.ts +18 -2
- package/dist/lib/mcpToolRegistry.js +29 -9
- package/dist/lib/secretEnv.d.ts +53 -8
- package/dist/lib/secretEnv.js +78 -15
- package/dist/lib/tomlSection.js +3 -2
- package/dist/lib/workspaceDoctor.d.ts +1 -1
- package/dist/lib/workspaceDoctor.js +8 -4
- package/dist/lib/workspaceMcp.d.ts +1 -1
- package/dist/lib/workspaceMcp.js +5 -5
- package/dist/pi-bridge/bundle.js +42236 -2701
- package/dist/pi-bridge/index.js +2 -2
- package/docs/magpie-gateway-research.md +422 -0
- package/docs/research.md +4 -1
- package/docs/roadmap.md +22 -12
- package/docs/runtime-infra.md +41 -22
- package/docs/self-evolution.md +138 -44
- package/package.json +8 -2
- package/schema/secrets.policy.example.yaml +5 -0
- package/schema/servers.example.yaml +5 -1
|
@@ -81,12 +81,23 @@ export function allocateExposedToolNames(candidates) {
|
|
|
81
81
|
// registry (for example `fixture__env` reaching the Runtime provider).
|
|
82
82
|
// Do not add a second provider prefix around it.
|
|
83
83
|
const sourceQualified = normalized.includes("__") || normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
84
|
+
// Narrower than `sourceQualified`: for forced-prefix candidates the
|
|
85
|
+
// exemption applies only when the tool name already starts with ITS
|
|
86
|
+
// OWN server's name (a second prefix would be redundant, e.g.
|
|
87
|
+
// `tanka__tanka_memo_search`). A tool that merely contains `__`
|
|
88
|
+
// (`b__c`) or gained a `__hash` length bound still needs its server
|
|
89
|
+
// prefix to be identifiable.
|
|
90
|
+
const serverRedundant = normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
84
91
|
const unique = (counts.get(candidate.toolName) ?? 0) === 1;
|
|
85
|
-
const base =
|
|
86
|
-
?
|
|
87
|
-
: unique && sourceQualified
|
|
92
|
+
const base = candidate.alwaysPrefix
|
|
93
|
+
? serverRedundant
|
|
88
94
|
? normalized
|
|
89
|
-
: prefixedToolName(candidate.serverName, candidate.toolName)
|
|
95
|
+
: prefixedToolName(candidate.serverName, candidate.toolName)
|
|
96
|
+
: unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH
|
|
97
|
+
? normalized
|
|
98
|
+
: unique && sourceQualified
|
|
99
|
+
? normalized
|
|
100
|
+
: prefixedToolName(candidate.serverName, candidate.toolName);
|
|
90
101
|
let exposed = base;
|
|
91
102
|
let suffix = 2;
|
|
92
103
|
while (used.has(exposed)) {
|
|
@@ -122,7 +133,7 @@ export class McpToolRegistry {
|
|
|
122
133
|
*/
|
|
123
134
|
add(serverName, upstream, tools) {
|
|
124
135
|
for (const tool of tools)
|
|
125
|
-
this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
136
|
+
this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
126
137
|
this.rebuild();
|
|
127
138
|
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== undefined);
|
|
128
139
|
return { added, skipped: [] };
|
|
@@ -141,7 +152,7 @@ export class McpToolRegistry {
|
|
|
141
152
|
this.candidates.length = 0;
|
|
142
153
|
this.candidates.push(...kept);
|
|
143
154
|
for (const tool of tools)
|
|
144
|
-
this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
155
|
+
this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
145
156
|
this.rebuild();
|
|
146
157
|
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== undefined);
|
|
147
158
|
return { added, skipped: [] };
|
|
@@ -154,8 +165,13 @@ export class McpToolRegistry {
|
|
|
154
165
|
return undefined;
|
|
155
166
|
}
|
|
156
167
|
/** Every registered tool, under its prefixed name, in registration
|
|
157
|
-
* order. Each tool's own `
|
|
158
|
-
*
|
|
168
|
+
* order. Each tool's own `inputSchema` and any additional fields the
|
|
169
|
+
* upstream sent are preserved verbatim; `name` changes, and the
|
|
170
|
+
* description gains a trailing `Source: <server>` line so an Agent can
|
|
171
|
+
* confirm the owning server without relying on the name alone
|
|
172
|
+
* (mcp-gateway-hosting: "Resold tool metadata identifies its upstream
|
|
173
|
+
* source"). The upstream's own description text is never altered or
|
|
174
|
+
* removed — the source line is appended. */
|
|
159
175
|
listTools() {
|
|
160
176
|
this.rebuild();
|
|
161
177
|
return this.order.map((name) => {
|
|
@@ -163,7 +179,11 @@ export class McpToolRegistry {
|
|
|
163
179
|
const title = name === entry.toolName || entry.tool.title !== undefined
|
|
164
180
|
? entry.tool.title
|
|
165
181
|
: `${entry.serverName} / ${entry.toolName}`;
|
|
166
|
-
|
|
182
|
+
const sourceLine = `Source: ${entry.serverName}`;
|
|
183
|
+
const description = entry.tool.description
|
|
184
|
+
? `${entry.tool.description}\n\n${sourceLine}`
|
|
185
|
+
: sourceLine;
|
|
186
|
+
return { ...entry.tool, name, description, ...(title === undefined ? {} : { title }) };
|
|
167
187
|
});
|
|
168
188
|
}
|
|
169
189
|
/** The owning upstream and original tool name for a prefixed name, or
|
package/dist/lib/secretEnv.d.ts
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* `process.env[name]` lookup — so they can never disagree about where a
|
|
5
5
|
* declared name's value comes from (design.md D1).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Entries are either bare environment variable names (byte-identical
|
|
8
|
+
* legacy semantics: `env_file` sole-source when set, otherwise ambient
|
|
9
|
+
* `process.env`) or `mxenv://<profile>/<KEY>` references resolved
|
|
10
|
+
* through the `@morphixai/mxenv-core` SDK and delivered under the `KEY`
|
|
11
|
+
* segment (mx-env-secret-source spec). The dotenv half stays
|
|
12
|
+
* dependency-free — a full dotenv library's quoting/escaping/
|
|
13
|
+
* interpolation rules are more than this narrow need requires — while
|
|
14
|
+
* the reference grammar is owned by the SDK and never re-implemented
|
|
15
|
+
* here.
|
|
10
16
|
*/
|
|
11
17
|
import type { SecretsPolicy } from "../core/types.js";
|
|
12
18
|
import type { BackupSession } from "./backup.js";
|
|
@@ -17,12 +23,51 @@ import type { BackupSession } from "./backup.js";
|
|
|
17
23
|
export declare function isValidSecretVarName(name: string): boolean;
|
|
18
24
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
19
25
|
/**
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* One declared `env` entry / `envAliases` source value, classified.
|
|
27
|
+
* Strict whole-string matching: an entry is a reference only when the
|
|
28
|
+
* entire entry is exactly one `mxenv://<profile>/<KEY>` (reconstructed
|
|
29
|
+
* from the SDK parser's match). Anything containing reference-shaped
|
|
30
|
+
* text that does not fully match — including the rejected legacy
|
|
31
|
+
* `mx-env://` scheme, which the SDK parser throws on — is `invalid`,
|
|
32
|
+
* never a silently-resolved bare name (mx-env-secret-source D6).
|
|
24
33
|
*/
|
|
25
|
-
export
|
|
34
|
+
export type SecretEnvEntry = {
|
|
35
|
+
kind: "ref";
|
|
36
|
+
profile: string;
|
|
37
|
+
key: string;
|
|
38
|
+
delivered: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "name";
|
|
41
|
+
delivered: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "invalid";
|
|
44
|
+
entry: string;
|
|
45
|
+
};
|
|
46
|
+
/** Delivered name for an entry (KEY segment for a reference, the entry
|
|
47
|
+
* itself otherwise; an invalid entry passes through unchanged so error
|
|
48
|
+
* paths can still name it). The single derivation every renderer and
|
|
49
|
+
* consumer uses — never re-derive from the raw text. */
|
|
50
|
+
export declare function deliveredName(entry: string): string;
|
|
51
|
+
export declare function parseEnvEntry(entry: string): SecretEnvEntry;
|
|
52
|
+
/**
|
|
53
|
+
* Result keyed by DELIVERED name: the `KEY` segment for a reference,
|
|
54
|
+
* the entry itself for a bare name (mx-env-secret-source spec).
|
|
55
|
+
*
|
|
56
|
+
* `policy.envFile` set: it is the SOLE source for bare names — a name
|
|
57
|
+
* absent from it resolves to `undefined`, `process.env` is never
|
|
58
|
+
* consulted (a silent fallback would defeat the isolation this exists
|
|
59
|
+
* to offer). Unset: bare names read `process.env` directly, identical
|
|
60
|
+
* to every pre-existing caller. References NEVER consult `env_file` or
|
|
61
|
+
* ambient env: they resolve from the composed mxenv profile only, so a
|
|
62
|
+
* same-named ambient variable cannot shadow the composed value.
|
|
63
|
+
*
|
|
64
|
+
* Invalid entries reject the batch with a descriptive error; audit and
|
|
65
|
+
* the plan pre-check pre-classify to turn them into findings/conflicts
|
|
66
|
+
* before this point (the bridge only ever sees pre-checked defs).
|
|
67
|
+
* Compositions are cached per batch (one `ReferenceResolver` per call,
|
|
68
|
+
* not per name).
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveSecretEnv(entries: string[], policy: SecretsPolicy): Promise<Record<string, string | undefined>>;
|
|
26
71
|
export type WriteLocalSecretOutcome = "created" | "already-present" | "conflict";
|
|
27
72
|
/**
|
|
28
73
|
* `trellis migrate`'s static-env secret extraction
|
package/dist/lib/secretEnv.js
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
* `process.env[name]` lookup — so they can never disagree about where a
|
|
5
5
|
* declared name's value comes from (design.md D1).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Entries are either bare environment variable names (byte-identical
|
|
8
|
+
* legacy semantics: `env_file` sole-source when set, otherwise ambient
|
|
9
|
+
* `process.env`) or `mxenv://<profile>/<KEY>` references resolved
|
|
10
|
+
* through the `@morphixai/mxenv-core` SDK and delivered under the `KEY`
|
|
11
|
+
* segment (mx-env-secret-source spec). The dotenv half stays
|
|
12
|
+
* dependency-free — a full dotenv library's quoting/escaping/
|
|
13
|
+
* interpolation rules are more than this narrow need requires — while
|
|
14
|
+
* the reference grammar is owned by the SDK and never re-implemented
|
|
15
|
+
* here.
|
|
10
16
|
*/
|
|
11
17
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
12
19
|
import { dirname } from "node:path";
|
|
20
|
+
import { findMxEnvReferences, loadConfigLayers, ReferenceResolver } from "@morphixai/mxenv-core";
|
|
13
21
|
const LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
14
22
|
const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
23
|
/** The name half of LINE_RE, standing alone: what counts as a variable
|
|
@@ -34,22 +42,77 @@ export function parseDotenv(content) {
|
|
|
34
42
|
function loadEnvFile(path) {
|
|
35
43
|
return existsSync(path) ? parseDotenv(readFileSync(path, "utf-8")) : {};
|
|
36
44
|
}
|
|
45
|
+
/** Delivered name for an entry (KEY segment for a reference, the entry
|
|
46
|
+
* itself otherwise; an invalid entry passes through unchanged so error
|
|
47
|
+
* paths can still name it). The single derivation every renderer and
|
|
48
|
+
* consumer uses — never re-derive from the raw text. */
|
|
49
|
+
export function deliveredName(entry) {
|
|
50
|
+
const parsed = parseEnvEntry(entry);
|
|
51
|
+
return parsed.kind === "invalid" ? entry : parsed.delivered;
|
|
52
|
+
}
|
|
53
|
+
export function parseEnvEntry(entry) {
|
|
54
|
+
if (entry.includes("mxenv://") || entry.includes("mx-env://")) {
|
|
55
|
+
try {
|
|
56
|
+
const refs = findMxEnvReferences(entry);
|
|
57
|
+
if (refs.length === 1 && `mxenv://${refs[0].profile}/${refs[0].key}` === entry) {
|
|
58
|
+
return { kind: "ref", profile: refs[0].profile, key: refs[0].key, delivered: refs[0].key };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// legacy scheme rejection from the SDK parser — falls through to invalid
|
|
63
|
+
}
|
|
64
|
+
return { kind: "invalid", entry };
|
|
65
|
+
}
|
|
66
|
+
return { kind: "name", delivered: entry };
|
|
67
|
+
}
|
|
37
68
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
69
|
+
* Result keyed by DELIVERED name: the `KEY` segment for a reference,
|
|
70
|
+
* the entry itself for a bare name (mx-env-secret-source spec).
|
|
71
|
+
*
|
|
72
|
+
* `policy.envFile` set: it is the SOLE source for bare names — a name
|
|
73
|
+
* absent from it resolves to `undefined`, `process.env` is never
|
|
74
|
+
* consulted (a silent fallback would defeat the isolation this exists
|
|
75
|
+
* to offer). Unset: bare names read `process.env` directly, identical
|
|
76
|
+
* to every pre-existing caller. References NEVER consult `env_file` or
|
|
77
|
+
* ambient env: they resolve from the composed mxenv profile only, so a
|
|
78
|
+
* same-named ambient variable cannot shadow the composed value.
|
|
79
|
+
*
|
|
80
|
+
* Invalid entries reject the batch with a descriptive error; audit and
|
|
81
|
+
* the plan pre-check pre-classify to turn them into findings/conflicts
|
|
82
|
+
* before this point (the bridge only ever sees pre-checked defs).
|
|
83
|
+
* Compositions are cached per batch (one `ReferenceResolver` per call,
|
|
84
|
+
* not per name).
|
|
42
85
|
*/
|
|
43
|
-
export function resolveSecretEnv(
|
|
86
|
+
export async function resolveSecretEnv(entries, policy) {
|
|
87
|
+
const parsed = entries.map(parseEnvEntry);
|
|
88
|
+
const invalid = parsed.find((p) => p.kind === "invalid");
|
|
89
|
+
if (invalid) {
|
|
90
|
+
throw new Error(`[trellis] invalid mxenv:// reference entry: ${invalid.entry} — expected mxenv://<profile>/<KEY>`);
|
|
91
|
+
}
|
|
44
92
|
const result = {};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
// Delivered-name collisions (several entries delivering one name):
|
|
94
|
+
// references are authoritative — a composed value never loses to a bare
|
|
95
|
+
// name's ambient/file value (ambient must not shadow references); a
|
|
96
|
+
// DEFINED value never loses to undefined; within one class the first
|
|
97
|
+
// declaration wins; bare names only fill keys nothing has filled.
|
|
98
|
+
const refs = parsed.filter((p) => p.kind === "ref");
|
|
99
|
+
if (refs.length > 0) {
|
|
100
|
+
const resolver = new ReferenceResolver(loadConfigLayers({ cwd: homedir(), homeDir: homedir() }), {});
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
if (result[ref.delivered] !== undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const { value } = await resolver.valueOf(ref.profile, ref.key);
|
|
105
|
+
result[ref.delivered] = value;
|
|
106
|
+
}
|
|
49
107
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
108
|
+
const names = parsed.filter((p) => p.kind === "name");
|
|
109
|
+
if (names.length > 0) {
|
|
110
|
+
const fileMap = policy.envFile ? loadEnvFile(policy.envFile) : undefined;
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
if (result[name.delivered] !== undefined)
|
|
113
|
+
continue;
|
|
114
|
+
result[name.delivered] = fileMap ? fileMap[name.delivered] : process.env[name.delivered];
|
|
115
|
+
}
|
|
53
116
|
}
|
|
54
117
|
return result;
|
|
55
118
|
}
|
package/dist/lib/tomlSection.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* ever needs to recognize table headers and render a bounded, known
|
|
9
9
|
* shape (`McpServerDef`), never general TOML.
|
|
10
10
|
*/
|
|
11
|
+
import { deliveredName } from "./secretEnv.js";
|
|
11
12
|
/**
|
|
12
13
|
* Matches a TOML table header line, and *only* a table header line — the
|
|
13
14
|
* entire trimmed line must be exactly `[key.path]` or `[[key.path]]`,
|
|
@@ -146,7 +147,7 @@ export function renderServerSection(name, def) {
|
|
|
146
147
|
if (def.args && def.args.length > 0)
|
|
147
148
|
lines.push(`args = ${tomlStringArray(def.args)}`);
|
|
148
149
|
if (def.env && def.env.length > 0)
|
|
149
|
-
lines.push(`env_vars = ${tomlStringArray(def.env)}`);
|
|
150
|
+
lines.push(`env_vars = ${tomlStringArray(def.env.map(deliveredName))}`);
|
|
150
151
|
}
|
|
151
152
|
else {
|
|
152
153
|
if (def.url)
|
|
@@ -160,7 +161,7 @@ export function renderServerSection(name, def) {
|
|
|
160
161
|
// expands it from its own ambient environment, the same mechanism
|
|
161
162
|
// `env_vars` already relies on for a same-named reference
|
|
162
163
|
// (trellis-migrate-env-var-alias D3).
|
|
163
|
-
const aliasEntries = Object.entries(def.envAliases ?? {}).map(([targetKey, sourceName]) => [targetKey, `\${${sourceName}}`]);
|
|
164
|
+
const aliasEntries = Object.entries(def.envAliases ?? {}).map(([targetKey, sourceName]) => [targetKey, `\${${deliveredName(sourceName)}}`]);
|
|
164
165
|
const staticEnvEntries = [...aliasEntries, ...Object.entries(def.staticEnv ?? {})];
|
|
165
166
|
if (staticEnvEntries.length > 0) {
|
|
166
167
|
lines.push(`[${envHeader(name)}]`);
|
|
@@ -56,4 +56,4 @@ export interface WorkspaceDoctorSection {
|
|
|
56
56
|
/** zcode's user-level-first deviation notices (empty otherwise). */
|
|
57
57
|
precedenceWarnings: string[];
|
|
58
58
|
}
|
|
59
|
-
export declare function collectWorkspaceDoctorSection(canonical: CanonicalSource): WorkspaceDoctorSection | undefined
|
|
59
|
+
export declare function collectWorkspaceDoctorSection(canonical: CanonicalSource): Promise<WorkspaceDoctorSection | undefined>;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { parse as parseToml } from "smol-toml";
|
|
14
|
-
import { resolveSecretEnv } from "./secretEnv.js";
|
|
14
|
+
import { deliveredName, parseEnvEntry, resolveSecretEnv } from "./secretEnv.js";
|
|
15
15
|
import { loadWorkspaceRendered, workspaceMcpConfigPath, workspacePrecedenceWarnings } from "./workspaceMcp.js";
|
|
16
16
|
const VAR_REF_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
17
17
|
function collectVarRefs(canonical) {
|
|
@@ -86,7 +86,7 @@ function unmarkedEntries(agentId, configPath, recorded) {
|
|
|
86
86
|
}
|
|
87
87
|
return { kind: "ok", names: names.filter((name) => recorded[name] === undefined) };
|
|
88
88
|
}
|
|
89
|
-
export function collectWorkspaceDoctorSection(canonical) {
|
|
89
|
+
export async function collectWorkspaceDoctorSection(canonical) {
|
|
90
90
|
const workspace = canonical.workspace;
|
|
91
91
|
if (!workspace)
|
|
92
92
|
return undefined;
|
|
@@ -101,8 +101,12 @@ export function collectWorkspaceDoctorSection(canonical) {
|
|
|
101
101
|
// overridden.
|
|
102
102
|
const tombstoned = workspace.shadowed.filter((entry) => entry.startsWith("mcp:") && canonical.mcp.servers[entry.slice(4)] === undefined);
|
|
103
103
|
const refs = collectVarRefs(canonical);
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// Malformed `mxenv://` entries are unresolved as-is (they name themselves);
|
|
105
|
+
// they must never reach resolveSecretEnv, which rejects the batch.
|
|
106
|
+
const invalid = refs.filter((r) => parseEnvEntry(r.name).kind === "invalid");
|
|
107
|
+
const resolvableRefs = refs.filter((r) => parseEnvEntry(r.name).kind !== "invalid");
|
|
108
|
+
const resolved = await resolveSecretEnv([...new Set(resolvableRefs.map((r) => r.name))], canonical.secretsPolicy);
|
|
109
|
+
const unresolvedVars = [...invalid, ...resolvableRefs.filter((r) => resolved[deliveredName(r.name)] === undefined)];
|
|
106
110
|
const manifest = loadWorkspaceRendered(workspace.root);
|
|
107
111
|
const unmigrated = [];
|
|
108
112
|
const tomlFindings = [];
|
|
@@ -53,5 +53,5 @@ export declare function renderWorkspaceManifestEntry(agentId: AgentId, name: str
|
|
|
53
53
|
* `applyWorkspaceMcp`; this exists only for adoption, where no delivery
|
|
54
54
|
* write to that agent's file has happened yet. */
|
|
55
55
|
export declare function recordWorkspaceRendered(workspaceRoot: string, agentId: AgentId, name: string, rendered: unknown, backup: BackupSession): void;
|
|
56
|
-
export declare function planWorkspaceMcp(agentId: AgentId, canonical: CanonicalSource): AdapterPlanItem[]
|
|
56
|
+
export declare function planWorkspaceMcp(agentId: AgentId, canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
|
|
57
57
|
export declare function applyWorkspaceMcp(agentId: AgentId, canonical: CanonicalSource, items: AdapterPlanItem[], backup: BackupSession): void;
|
package/dist/lib/workspaceMcp.js
CHANGED
|
@@ -140,7 +140,7 @@ export function recordWorkspaceRendered(workspaceRoot, agentId, name, rendered,
|
|
|
140
140
|
mkdirSync(dirname(path), { recursive: true });
|
|
141
141
|
backup.writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
142
142
|
}
|
|
143
|
-
export function planWorkspaceMcp(agentId, canonical) {
|
|
143
|
+
export async function planWorkspaceMcp(agentId, canonical) {
|
|
144
144
|
const workspace = canonical.workspace;
|
|
145
145
|
if (!workspace)
|
|
146
146
|
return [];
|
|
@@ -151,7 +151,7 @@ export function planWorkspaceMcp(agentId, canonical) {
|
|
|
151
151
|
const layer = (item) => ({ ...item, layer: "workspace" });
|
|
152
152
|
if (agentId === "codex") {
|
|
153
153
|
const content = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
154
|
-
const { desired, conflicts } = resolveMcpPlan(agentId, mcp, canonical.managedAgents, canonical.secretsPolicy);
|
|
154
|
+
const { desired, conflicts } = await resolveMcpPlan(agentId, mcp, canonical.managedAgents, canonical.secretsPolicy);
|
|
155
155
|
const items = [];
|
|
156
156
|
for (const { name, def } of desired) {
|
|
157
157
|
const current = currentServerSectionText(content, name);
|
|
@@ -183,13 +183,13 @@ export function planWorkspaceMcp(agentId, canonical) {
|
|
|
183
183
|
}
|
|
184
184
|
if (agentId === "zcode") {
|
|
185
185
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
186
|
-
const items = planZcodeMcp({ configPath, parsed, mcp, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership }).map(layer);
|
|
186
|
+
const items = (await planZcodeMcp({ configPath, parsed, mcp, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership })).map(layer);
|
|
187
187
|
const servers = (parsed?.mcp?.servers ?? {});
|
|
188
188
|
return guardUnmarkedOverwrite(items, (item) => servers[item.mcpWrite.name] !== undefined && ownership[item.mcpWrite.name] === undefined);
|
|
189
189
|
}
|
|
190
190
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
191
191
|
const render = agentId === "pi" ? piRenderer : undefined;
|
|
192
|
-
const planned = planJsonMcp({
|
|
192
|
+
const planned = (await planJsonMcp({
|
|
193
193
|
configPath,
|
|
194
194
|
parsed,
|
|
195
195
|
mcp,
|
|
@@ -198,7 +198,7 @@ export function planWorkspaceMcp(agentId, canonical) {
|
|
|
198
198
|
policy: canonical.secretsPolicy,
|
|
199
199
|
ownership,
|
|
200
200
|
...(render ? { render } : {}),
|
|
201
|
-
}).map(layer);
|
|
201
|
+
})).map(layer);
|
|
202
202
|
const existing = (parsed?.mcpServers ?? {});
|
|
203
203
|
const guarded = guardUnmarkedOverwrite(planned, (item) => existing[item.mcpWrite.name] !== undefined && ownership[item.mcpWrite.name] === undefined);
|
|
204
204
|
if (agentId !== "pi")
|