@catalyst-cloud/cli 0.14.6 → 0.14.7
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/CHANGELOG.md +5 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
|
@@ -0,0 +1,2937 @@
|
|
|
1
|
+
// packages/environment/src/validation-helpers.ts
|
|
2
|
+
class EnvironmentContractError extends TypeError {
|
|
3
|
+
reason;
|
|
4
|
+
constructor(message, reason) {
|
|
5
|
+
super(`invalid repo environment declaration: ${message}`);
|
|
6
|
+
this.name = "EnvironmentContractError";
|
|
7
|
+
if (reason !== undefined)
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var PROVENANCE_ID_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function closedRecord(value, allowedFields, path) {
|
|
16
|
+
if (!isRecord(value))
|
|
17
|
+
throw new EnvironmentContractError(`${path} must be an object`);
|
|
18
|
+
for (const key of Object.keys(value)) {
|
|
19
|
+
if (!allowedFields.has(key)) {
|
|
20
|
+
throw new EnvironmentContractError(`unknown field ${JSON.stringify(`${path}.${key}`)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function enumValue(value, allowed, path) {
|
|
26
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
27
|
+
throw new EnvironmentContractError(`${path} must be one of ${allowed.map((item) => JSON.stringify(item)).join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function boundedArray(value, path, maximum) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
throw new EnvironmentContractError(`${path} must be an array`);
|
|
34
|
+
if (value.length > maximum) {
|
|
35
|
+
throw new EnvironmentContractError(`${path} must contain at most ${maximum} items`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function boundedString(value, path, re, maxLength, describe) {
|
|
40
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || !re.test(value)) {
|
|
41
|
+
throw new EnvironmentContractError(`${path} must be ${describe}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function boundedInteger(value, path, min, max) {
|
|
46
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
47
|
+
throw new EnvironmentContractError(`${path} must be an integer between ${min} and ${max}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function sortedUnique(values, key) {
|
|
52
|
+
const byKey = new Map;
|
|
53
|
+
for (const value of values)
|
|
54
|
+
byKey.set(key(value), value);
|
|
55
|
+
return [...byKey.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, value]) => value);
|
|
56
|
+
}
|
|
57
|
+
var PRINTABLE_RE = /^[\x21-\x7E ]+$/;
|
|
58
|
+
function validateRelativePath(value, path) {
|
|
59
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 512) {
|
|
60
|
+
throw new EnvironmentContractError(`${path} must be a non-empty path of at most 512 characters`);
|
|
61
|
+
}
|
|
62
|
+
if (value.startsWith("/") || value.startsWith("\\")) {
|
|
63
|
+
throw new EnvironmentContractError(`${path} must not be an absolute path`);
|
|
64
|
+
}
|
|
65
|
+
if (value.split(/[\\/]/).includes("..")) {
|
|
66
|
+
throw new EnvironmentContractError(`${path} must not contain ".." path segments`);
|
|
67
|
+
}
|
|
68
|
+
if (!PRINTABLE_RE.test(value)) {
|
|
69
|
+
throw new EnvironmentContractError(`${path} must contain only printable characters`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function validateProvenanceIdRefs(value, path, knownIds) {
|
|
74
|
+
return sortedUnique(boundedArray(value, path, 16).map((entry, i) => {
|
|
75
|
+
const id = boundedString(entry, `${path}[${i}]`, PROVENANCE_ID_RE, 64, "a lowercase kebab-case provenance id");
|
|
76
|
+
if (!knownIds.has(id)) {
|
|
77
|
+
throw new EnvironmentContractError(`${path}[${i}] references unknown provenance id ${JSON.stringify(id)}`);
|
|
78
|
+
}
|
|
79
|
+
return id;
|
|
80
|
+
}), (id) => id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// packages/environment/src/provenance.ts
|
|
84
|
+
var CONFIDENCES = ["high", "medium", "low"];
|
|
85
|
+
var PROVENANCE_FIELDS = new Set([
|
|
86
|
+
"id",
|
|
87
|
+
"kind",
|
|
88
|
+
"path",
|
|
89
|
+
"lineStart",
|
|
90
|
+
"lineEnd",
|
|
91
|
+
"confidence",
|
|
92
|
+
"explanation"
|
|
93
|
+
]);
|
|
94
|
+
var KIND_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
95
|
+
var EXPLANATION_RE = /^[\s\S]{1,512}$/;
|
|
96
|
+
var MAX_LINE = 1e6;
|
|
97
|
+
function validateProvenanceRecord(value, path) {
|
|
98
|
+
const raw = closedRecord(value, PROVENANCE_FIELDS, path);
|
|
99
|
+
const id = boundedString(raw.id, `${path}.id`, PROVENANCE_ID_RE, 64, "a lowercase kebab-case identifier of at most 64 characters");
|
|
100
|
+
const kind = boundedString(raw.kind, `${path}.kind`, KIND_RE, 64, "a lowercase kebab-case identifier of at most 64 characters");
|
|
101
|
+
const recordPath = validateRelativePath(raw.path, `${path}.path`);
|
|
102
|
+
const confidence = enumValue(raw.confidence, CONFIDENCES, `${path}.confidence`);
|
|
103
|
+
const explanation = boundedString(raw.explanation, `${path}.explanation`, EXPLANATION_RE, 512, "a value-free explanation of at most 512 characters");
|
|
104
|
+
let lineStart;
|
|
105
|
+
let lineEnd;
|
|
106
|
+
if (raw.lineStart !== undefined) {
|
|
107
|
+
lineStart = boundedInteger(raw.lineStart, `${path}.lineStart`, 1, MAX_LINE);
|
|
108
|
+
}
|
|
109
|
+
if (raw.lineEnd !== undefined) {
|
|
110
|
+
lineEnd = boundedInteger(raw.lineEnd, `${path}.lineEnd`, 1, MAX_LINE);
|
|
111
|
+
}
|
|
112
|
+
if (lineStart !== undefined && lineEnd !== undefined && lineEnd < lineStart) {
|
|
113
|
+
throw new EnvironmentContractError(`${path}.lineEnd must be >= ${path}.lineStart`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
kind,
|
|
118
|
+
path: recordPath,
|
|
119
|
+
...lineStart !== undefined ? { lineStart } : {},
|
|
120
|
+
...lineEnd !== undefined ? { lineEnd } : {},
|
|
121
|
+
confidence,
|
|
122
|
+
explanation
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// packages/environment/src/secret-material.ts
|
|
127
|
+
var SECRET_ASSIGNMENT_RE = /\b([A-Za-z_][A-Za-z0-9_]*(?:_TOKEN|_SECRET|_KEY|_PASSWORD|_CREDENTIAL)|TOKEN|SECRET|PASSWORD)=(\S{4,})/;
|
|
128
|
+
var AUTH_KEY_RE = /(^authorization$|api[-_]?key$|_token$|_secret$|_key$|_password$|_credential$|^token$|^secret$|^password$)/i;
|
|
129
|
+
function escapeRegExp(value) {
|
|
130
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
131
|
+
}
|
|
132
|
+
var TOKEN_START_SEPARATOR_CLASS = `[\\s=:,;"'/?&@]`;
|
|
133
|
+
function mixedBody(chars, min) {
|
|
134
|
+
return `(?=[${chars}]*[0-9])(?=[${chars}]*[A-Z])[${chars}]{${min},}`;
|
|
135
|
+
}
|
|
136
|
+
var SECRET_TOKEN_SHAPES = [
|
|
137
|
+
{
|
|
138
|
+
kind: "GitHub token",
|
|
139
|
+
prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"],
|
|
140
|
+
source: "(gh[pousr]_)[A-Za-z0-9]{36,}"
|
|
141
|
+
},
|
|
142
|
+
{ kind: "GitHub token", prefixes: ["github_pat_"], source: "(github_pat_)[A-Za-z0-9_]{22,}" },
|
|
143
|
+
{
|
|
144
|
+
kind: "Stripe key",
|
|
145
|
+
prefixes: ["sk_live_", "sk_test_"],
|
|
146
|
+
source: "(sk_(?:live|test)_)[A-Za-z0-9]{16,}"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
kind: "OpenAI-style API key",
|
|
150
|
+
prefixes: ["sk-"],
|
|
151
|
+
source: `(sk-)${mixedBody("A-Za-z0-9", 32)}`
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
kind: "OpenAI-style API key",
|
|
155
|
+
prefixes: ["sk-"],
|
|
156
|
+
source: `(sk-)(?:proj|svcacct|admin)-${mixedBody("A-Za-z0-9_-", 40)}`
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
kind: "Anthropic API key",
|
|
160
|
+
prefixes: ["sk-"],
|
|
161
|
+
source: `(sk-)ant-(?:api|admin|oat)\\d{2}-${mixedBody("A-Za-z0-9_-", 40)}`
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
kind: "Slack token",
|
|
165
|
+
prefixes: ["xoxb-", "xoxp-", "xoxa-", "xoxr-", "xoxs-"],
|
|
166
|
+
source: "(xox[bpars]-)(?=[0-9])[A-Za-z0-9-]{10,}"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: "AWS access key",
|
|
170
|
+
prefixes: ["AKIA", "ASIA"],
|
|
171
|
+
source: "(AKIA|ASIA)[A-Z0-9]{16}(?![A-Za-z0-9])"
|
|
172
|
+
},
|
|
173
|
+
{ kind: "Google API key", prefixes: ["AIza"], source: "(AIza)[0-9A-Za-z_-]{35}" },
|
|
174
|
+
{
|
|
175
|
+
kind: "private key",
|
|
176
|
+
prefixes: ["-----BEGIN"],
|
|
177
|
+
source: "(-----BEGIN) [A-Z ]*PRIVATE KEY(?: BLOCK)?-----"
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
var SECRET_TOKEN_RES = SECRET_TOKEN_SHAPES.map((shape) => ({
|
|
181
|
+
kind: shape.kind,
|
|
182
|
+
re: new RegExp(`(?:^|${TOKEN_START_SEPARATOR_CLASS})${shape.source}`)
|
|
183
|
+
}));
|
|
184
|
+
function secretTokenIn(value) {
|
|
185
|
+
let best = null;
|
|
186
|
+
for (const { kind, re } of SECRET_TOKEN_RES) {
|
|
187
|
+
const m = re.exec(value);
|
|
188
|
+
if (m === null)
|
|
189
|
+
continue;
|
|
190
|
+
const index = m.index + m[0].indexOf(m[1]);
|
|
191
|
+
if (best === null || index < best.index)
|
|
192
|
+
best = { kind, prefix: m[1], index };
|
|
193
|
+
}
|
|
194
|
+
return best === null ? null : { kind: best.kind, prefix: best.prefix };
|
|
195
|
+
}
|
|
196
|
+
function secretTokenMessage(path, kind) {
|
|
197
|
+
return `${path} looks like ${/^[AEIOU]/.test(kind) ? "an" : "a"} ${kind}; ` + "declarations name secrets, they never hold them. " + "Store the value as a repository secret (Settings → Repositories → the repository → Environment → Secrets).";
|
|
198
|
+
}
|
|
199
|
+
var PERSONAL_GITHUB_TOKEN_PREFIXES = [
|
|
200
|
+
"ghp_",
|
|
201
|
+
"gho_",
|
|
202
|
+
"ghu_",
|
|
203
|
+
"ghr_",
|
|
204
|
+
"github_pat_"
|
|
205
|
+
];
|
|
206
|
+
var PERSONAL_TOKEN_START_RE = new RegExp(`(?:^|${TOKEN_START_SEPARATOR_CLASS})(${PERSONAL_GITHUB_TOKEN_PREFIXES.map(escapeRegExp).join("|")})`);
|
|
207
|
+
|
|
208
|
+
// packages/environment/src/agent-assets.ts
|
|
209
|
+
var AGENT_ASSET_KINDS = ["skill", "mcp-server", "cli"];
|
|
210
|
+
var AGENT_ASSET_HARNESSES = ["claude", "codex", "opencode"];
|
|
211
|
+
var TIERS = ["platform", "repository", "tenant-channel"];
|
|
212
|
+
var SKILL_FIELDS = new Set([
|
|
213
|
+
"kind",
|
|
214
|
+
"id",
|
|
215
|
+
"tier",
|
|
216
|
+
"reference",
|
|
217
|
+
"digest",
|
|
218
|
+
"requiredEnvNames",
|
|
219
|
+
"provenanceIds"
|
|
220
|
+
]);
|
|
221
|
+
var MCP_STDIO_FIELDS = new Set([
|
|
222
|
+
"kind",
|
|
223
|
+
"id",
|
|
224
|
+
"transport",
|
|
225
|
+
"command",
|
|
226
|
+
"args",
|
|
227
|
+
"env",
|
|
228
|
+
"requiredEnvNames",
|
|
229
|
+
"harnesses",
|
|
230
|
+
"provenanceIds"
|
|
231
|
+
]);
|
|
232
|
+
var MCP_HTTP_FIELDS = new Set([
|
|
233
|
+
"kind",
|
|
234
|
+
"id",
|
|
235
|
+
"transport",
|
|
236
|
+
"url",
|
|
237
|
+
"headers",
|
|
238
|
+
"requiredEnvNames",
|
|
239
|
+
"harnesses",
|
|
240
|
+
"provenanceIds"
|
|
241
|
+
]);
|
|
242
|
+
var MCP_PORTAL_FIELDS = new Set([
|
|
243
|
+
"kind",
|
|
244
|
+
"id",
|
|
245
|
+
"transport",
|
|
246
|
+
"via",
|
|
247
|
+
"requiredEnvNames",
|
|
248
|
+
"harnesses",
|
|
249
|
+
"provenanceIds"
|
|
250
|
+
]);
|
|
251
|
+
var CLI_FIELDS = new Set(["kind", "id", "install", "provenanceIds"]);
|
|
252
|
+
var CLI_IMAGE_FIELDS = new Set(["source"]);
|
|
253
|
+
var CLI_NPM_FIELDS = new Set(["source", "package", "version"]);
|
|
254
|
+
var ID_RE = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
255
|
+
var TOOL_ID_MAX = 64;
|
|
256
|
+
var TOOL_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
257
|
+
function carriesReference(value) {
|
|
258
|
+
return value.includes("$") || value.includes("{env:");
|
|
259
|
+
}
|
|
260
|
+
var DIGEST_RE = /^[0-9a-f]{64}$/;
|
|
261
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
262
|
+
var REFERENCE_RE = /^[\x21-\x7E ]{1,512}$/;
|
|
263
|
+
var VAULT_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
264
|
+
var WHOLE_REF_RE = /^\$(?:\{([A-Z][A-Z0-9_]*)\}|([A-Z][A-Z0-9_]*))$/;
|
|
265
|
+
var BEARER_REF_RE = /^Bearer \$(?:\{([A-Z][A-Z0-9_]*)\}|([A-Z][A-Z0-9_]*))$/;
|
|
266
|
+
var COMMAND_RE = /^[\x21-\x7E]{1,512}$/;
|
|
267
|
+
var ARG_RE = /^[\x20-\x7E]{1,1024}$/;
|
|
268
|
+
var URL_RE = /^https?:\/\/[\x21-\x7E]{1,2040}$/;
|
|
269
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9-_]{0,127}$/;
|
|
270
|
+
var LITERAL_RE = /^[\x20-\x7E]{0,1024}$/;
|
|
271
|
+
var NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
272
|
+
var NPM_VERSION_RE = /^[A-Za-z0-9^~<>=][A-Za-z0-9.+_\-^~<>=*]{0,63}$/;
|
|
273
|
+
var MAX_ARGS = 64;
|
|
274
|
+
var MAX_MAP_ENTRIES = 32;
|
|
275
|
+
var NON_PORTABLE_AUTH_KEYS = [
|
|
276
|
+
"auth",
|
|
277
|
+
"oauth",
|
|
278
|
+
"authorization_server",
|
|
279
|
+
"keychain",
|
|
280
|
+
"keyring",
|
|
281
|
+
"oauth_client_id",
|
|
282
|
+
"client_id"
|
|
283
|
+
];
|
|
284
|
+
var AUTH_NOT_PORTABLE_MESSAGE = "a browser OAuth session or an OS keyring cannot be carried into a container; declare the " + "server's token/API-key mode with the key stored as a vault secret and referenced as $NAME in " + "headers (http) or listed in requiredEnvNames (stdio), or use a stdio server whose command reads " + "its auth from env";
|
|
285
|
+
function agentAssetKey(entry) {
|
|
286
|
+
return `${entry.kind}:${entry.id}`;
|
|
287
|
+
}
|
|
288
|
+
function refNameOf(value) {
|
|
289
|
+
const match = WHOLE_REF_RE.exec(value) ?? BEARER_REF_RE.exec(value);
|
|
290
|
+
return match ? match[1] ?? match[2] ?? null : null;
|
|
291
|
+
}
|
|
292
|
+
function looksLikeSecretValue(key, value) {
|
|
293
|
+
if (refNameOf(value) !== null)
|
|
294
|
+
return false;
|
|
295
|
+
if (secretTokenIn(value) !== null)
|
|
296
|
+
return true;
|
|
297
|
+
return AUTH_KEY_RE.test(key) && value.length >= 4;
|
|
298
|
+
}
|
|
299
|
+
function refuse(message, reason) {
|
|
300
|
+
throw new EnvironmentContractError(message, reason);
|
|
301
|
+
}
|
|
302
|
+
function tagged(reason, run) {
|
|
303
|
+
try {
|
|
304
|
+
return run();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err instanceof EnvironmentContractError && err.reason === undefined) {
|
|
307
|
+
throw new EnvironmentContractError(err.message.replace(/^invalid repo environment declaration: /, ""), reason);
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function stringMap(value, path, keyRe, describeKey) {
|
|
313
|
+
if (!isRecord(value))
|
|
314
|
+
refuse(`${path} must be an object`, "invalid_shape");
|
|
315
|
+
const entries = Object.entries(value);
|
|
316
|
+
if (entries.length > MAX_MAP_ENTRIES) {
|
|
317
|
+
refuse(`${path} must contain at most ${MAX_MAP_ENTRIES} entries`, "invalid_shape");
|
|
318
|
+
}
|
|
319
|
+
const out = {};
|
|
320
|
+
for (const [key, item] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
321
|
+
if (!keyRe.test(key))
|
|
322
|
+
refuse(`${path} key ${JSON.stringify(key)} must be ${describeKey}`, "invalid_shape");
|
|
323
|
+
if (typeof item !== "string" || !LITERAL_RE.test(item)) {
|
|
324
|
+
refuse(`${path}.${key} must be a printable string of at most 1024 characters`, "invalid_shape");
|
|
325
|
+
}
|
|
326
|
+
out[key] = item;
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function vaultNames(value, path) {
|
|
331
|
+
const names = tagged("invalid_shape", () => boundedArray(value, path, MAX_MAP_ENTRIES)).map((entry, i) => {
|
|
332
|
+
if (typeof entry !== "string" || entry.length > 128 || !VAULT_NAME_RE.test(entry)) {
|
|
333
|
+
refuse(`${path}[${i}] must be a vault secret name matching ^[A-Z][A-Z0-9_]*$`, "invalid_reference");
|
|
334
|
+
}
|
|
335
|
+
return entry;
|
|
336
|
+
});
|
|
337
|
+
return sortedUnique(names, (name) => name);
|
|
338
|
+
}
|
|
339
|
+
function toolId(value, path) {
|
|
340
|
+
return tagged("invalid_name", () => boundedString(value, path, TOOL_ID_RE, TOOL_ID_MAX, `a lowercase identifier of at most ${TOOL_ID_MAX} characters without "."`));
|
|
341
|
+
}
|
|
342
|
+
function validateSkill(raw, path, known) {
|
|
343
|
+
tagged("invalid_shape", () => closedRecord(raw, SKILL_FIELDS, path));
|
|
344
|
+
const id = tagged("invalid_name", () => boundedString(raw.id, `${path}.id`, ID_RE, 128, "a lowercase identifier of at most 128 characters"));
|
|
345
|
+
return tagged("invalid_shape", () => {
|
|
346
|
+
const tier = enumValue(raw.tier, TIERS, `${path}.tier`);
|
|
347
|
+
const reference = tier === "repository" ? validateRelativePath(raw.reference, `${path}.reference`) : boundedString(raw.reference, `${path}.reference`, REFERENCE_RE, 512, "a printable reference of at most 512 characters");
|
|
348
|
+
let digest;
|
|
349
|
+
if (raw.digest !== undefined) {
|
|
350
|
+
digest = boundedString(raw.digest, `${path}.digest`, DIGEST_RE, 64, "lowercase hexadecimal ([0-9a-f]{64})");
|
|
351
|
+
}
|
|
352
|
+
if (tier === "repository" && digest === undefined) {
|
|
353
|
+
throw new EnvironmentContractError(`${path}.digest is required for tier "repository"`);
|
|
354
|
+
}
|
|
355
|
+
const requiredEnvNames = sortedUnique(boundedArray(raw.requiredEnvNames, `${path}.requiredEnvNames`, 32).map((entry, i) => boundedString(entry, `${path}.requiredEnvNames[${i}]`, ENV_NAME_RE, 128, "an environment variable name")), (name) => name);
|
|
356
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, known);
|
|
357
|
+
return {
|
|
358
|
+
kind: "skill",
|
|
359
|
+
id,
|
|
360
|
+
tier,
|
|
361
|
+
reference,
|
|
362
|
+
...digest !== undefined ? { digest } : {},
|
|
363
|
+
requiredEnvNames,
|
|
364
|
+
provenanceIds
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function validateUrl(value, path) {
|
|
369
|
+
if (typeof value !== "string" || !URL_RE.test(value)) {
|
|
370
|
+
refuse(`${path} must be an http(s) URL of at most 2048 printable characters`, "invalid_shape");
|
|
371
|
+
}
|
|
372
|
+
if (carriesReference(value)) {
|
|
373
|
+
refuse(`${path} must not carry a reference; put the credential in a header as $NAME`, "invalid_reference");
|
|
374
|
+
}
|
|
375
|
+
const query = value.indexOf("?");
|
|
376
|
+
if (query !== -1) {
|
|
377
|
+
for (const pair of value.slice(query + 1).split("&")) {
|
|
378
|
+
const eq = pair.indexOf("=");
|
|
379
|
+
if (eq === -1)
|
|
380
|
+
continue;
|
|
381
|
+
if (looksLikeSecretValue(pair.slice(0, eq), pair.slice(eq + 1))) {
|
|
382
|
+
refuse(`${path} carries a credential-shaped query parameter ${JSON.stringify(pair.slice(0, eq))}; ` + "store the value as a vault secret and send it as a header referencing $NAME", "literal_secret");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (secretTokenIn(value) !== null) {
|
|
387
|
+
refuse(`${path} appears to contain a live credential; declarations carry names, never values`, "literal_secret");
|
|
388
|
+
}
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
function validateHeaders(value, path) {
|
|
392
|
+
const headers = stringMap(value, path, HEADER_NAME_RE, "an HTTP header name");
|
|
393
|
+
for (const [name, headerValue] of Object.entries(headers)) {
|
|
394
|
+
const at = `${path}.${name}`;
|
|
395
|
+
if (WHOLE_REF_RE.test(headerValue))
|
|
396
|
+
continue;
|
|
397
|
+
if (BEARER_REF_RE.test(headerValue)) {
|
|
398
|
+
if (name.toLowerCase() === "authorization")
|
|
399
|
+
continue;
|
|
400
|
+
refuse(`${at}: "Bearer $NAME" is accepted on Authorization only; use a whole $NAME here`, "invalid_reference");
|
|
401
|
+
}
|
|
402
|
+
if (carriesReference(headerValue)) {
|
|
403
|
+
refuse(`${at} must be a literal, a whole $NAME / \${NAME} reference, or "Bearer $NAME" on Authorization ` + "(names match ^[A-Z][A-Z0-9_]*$)", "invalid_reference");
|
|
404
|
+
}
|
|
405
|
+
if (looksLikeSecretValue(name, headerValue)) {
|
|
406
|
+
refuse(`${at} looks like a credential literal; store it as a vault secret and reference it as $NAME`, "literal_secret");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return headers;
|
|
410
|
+
}
|
|
411
|
+
function validateLiteralEnv(value, path) {
|
|
412
|
+
const env = stringMap(value, path, ENV_NAME_RE, "an environment variable name");
|
|
413
|
+
for (const [name, literal] of Object.entries(env)) {
|
|
414
|
+
if (carriesReference(literal)) {
|
|
415
|
+
refuse(`${path}.${name} must be a literal; a secret the server reads is listed in requiredEnvNames under ` + "its own name and passed through, never renamed", "env_reference_must_match_key");
|
|
416
|
+
}
|
|
417
|
+
if (looksLikeSecretValue(name, literal)) {
|
|
418
|
+
refuse(`${path}.${name} looks like a credential literal; store it as a vault secret and list it in requiredEnvNames`, "literal_secret");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return env;
|
|
422
|
+
}
|
|
423
|
+
function validateMcpServer(raw, path, known) {
|
|
424
|
+
const transport = tagged("invalid_shape", () => enumValue(raw.transport, ["stdio", "http"], `${path}.transport`));
|
|
425
|
+
const via = raw.via;
|
|
426
|
+
if (via !== undefined && (via !== "portal" || transport !== "http")) {
|
|
427
|
+
refuse(`${path}.via must be "portal" on an http server`, "invalid_shape");
|
|
428
|
+
}
|
|
429
|
+
tagged("invalid_shape", () => closedRecord(raw, transport === "stdio" ? MCP_STDIO_FIELDS : via === "portal" ? MCP_PORTAL_FIELDS : MCP_HTTP_FIELDS, path));
|
|
430
|
+
const id = toolId(raw.id, `${path}.id`);
|
|
431
|
+
let harnesses;
|
|
432
|
+
if (raw.harnesses !== undefined) {
|
|
433
|
+
harnesses = tagged("invalid_shape", () => {
|
|
434
|
+
const list = boundedArray(raw.harnesses, `${path}.harnesses`, AGENT_ASSET_HARNESSES.length).map((h, i) => enumValue(h, AGENT_ASSET_HARNESSES, `${path}.harnesses[${i}]`));
|
|
435
|
+
if (list.length === 0)
|
|
436
|
+
throw new EnvironmentContractError(`${path}.harnesses must not be empty (omit it for all)`);
|
|
437
|
+
return sortedUnique(list, (h) => h);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const provenanceIds = tagged("invalid_shape", () => validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, known));
|
|
441
|
+
if (transport === "stdio") {
|
|
442
|
+
const command = tagged("invalid_shape", () => boundedString(raw.command, `${path}.command`, COMMAND_RE, 512, "a printable executable of at most 512 characters"));
|
|
443
|
+
let args;
|
|
444
|
+
if (raw.args !== undefined) {
|
|
445
|
+
args = tagged("invalid_shape", () => boundedArray(raw.args, `${path}.args`, MAX_ARGS).map((arg, i) => boundedString(arg, `${path}.args[${i}]`, ARG_RE, 1024, "a printable argument of at most 1024 characters")));
|
|
446
|
+
}
|
|
447
|
+
for (const [at, value] of [
|
|
448
|
+
[`${path}.command`, command],
|
|
449
|
+
...(args ?? []).map((arg, i) => [`${path}.args[${i}]`, arg])
|
|
450
|
+
]) {
|
|
451
|
+
if (carriesReference(value)) {
|
|
452
|
+
refuse(`${at} must be literal; a secret the server reads is listed in requiredEnvNames and read ` + "from its environment", "invalid_reference");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const env = raw.env === undefined ? undefined : validateLiteralEnv(raw.env, `${path}.env`);
|
|
456
|
+
const requiredEnvNames = vaultNames(raw.requiredEnvNames, `${path}.requiredEnvNames`);
|
|
457
|
+
for (const name of requiredEnvNames) {
|
|
458
|
+
if (env && name in env) {
|
|
459
|
+
refuse(`${path}.env.${name} is also in requiredEnvNames; a name is either passed through or literal`, "invalid_shape");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
kind: "mcp-server",
|
|
464
|
+
id,
|
|
465
|
+
transport,
|
|
466
|
+
command,
|
|
467
|
+
...args !== undefined ? { args } : {},
|
|
468
|
+
...env !== undefined ? { env } : {},
|
|
469
|
+
requiredEnvNames,
|
|
470
|
+
...harnesses !== undefined ? { harnesses } : {},
|
|
471
|
+
provenanceIds
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (raw.requiredEnvNames !== undefined && !(Array.isArray(raw.requiredEnvNames) && raw.requiredEnvNames.length === 0)) {
|
|
475
|
+
refuse(`${path}.requiredEnvNames must be empty for an http server; its auth is a header referencing $NAME`, "invalid_shape");
|
|
476
|
+
}
|
|
477
|
+
if (via === "portal") {
|
|
478
|
+
return {
|
|
479
|
+
kind: "mcp-server",
|
|
480
|
+
id,
|
|
481
|
+
transport: "http",
|
|
482
|
+
via: "portal",
|
|
483
|
+
requiredEnvNames: [],
|
|
484
|
+
...harnesses !== undefined ? { harnesses } : {},
|
|
485
|
+
provenanceIds
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const url = validateUrl(raw.url, `${path}.url`);
|
|
489
|
+
const headers = raw.headers === undefined ? undefined : validateHeaders(raw.headers, `${path}.headers`);
|
|
490
|
+
return {
|
|
491
|
+
kind: "mcp-server",
|
|
492
|
+
id,
|
|
493
|
+
transport,
|
|
494
|
+
url,
|
|
495
|
+
...headers !== undefined ? { headers } : {},
|
|
496
|
+
requiredEnvNames: [],
|
|
497
|
+
...harnesses !== undefined ? { harnesses } : {},
|
|
498
|
+
provenanceIds
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function validateCli(raw, path, known) {
|
|
502
|
+
tagged("invalid_shape", () => closedRecord(raw, CLI_FIELDS, path));
|
|
503
|
+
const id = toolId(raw.id, `${path}.id`);
|
|
504
|
+
const install = tagged("invalid_shape", () => {
|
|
505
|
+
const installPath = `${path}.install`;
|
|
506
|
+
if (!isRecord(raw.install))
|
|
507
|
+
throw new EnvironmentContractError(`${installPath} must be an object`);
|
|
508
|
+
const source = enumValue(raw.install.source, ["image", "npm"], `${installPath}.source`);
|
|
509
|
+
if (source === "image") {
|
|
510
|
+
closedRecord(raw.install, CLI_IMAGE_FIELDS, installPath);
|
|
511
|
+
return { source };
|
|
512
|
+
}
|
|
513
|
+
const npm = closedRecord(raw.install, CLI_NPM_FIELDS, installPath);
|
|
514
|
+
const pkg = boundedString(npm.package, `${installPath}.package`, NPM_NAME_RE, 214, "an npm package name");
|
|
515
|
+
if (npm.version === undefined)
|
|
516
|
+
return { source, package: pkg };
|
|
517
|
+
const version = boundedString(npm.version, `${installPath}.version`, NPM_VERSION_RE, 64, "an npm version, tag or single range without spaces");
|
|
518
|
+
return { source, package: pkg, version };
|
|
519
|
+
});
|
|
520
|
+
const provenanceIds = tagged("invalid_shape", () => validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, known));
|
|
521
|
+
return { kind: "cli", id, install, provenanceIds };
|
|
522
|
+
}
|
|
523
|
+
function validateAgentAssetEntry(value, path, knownProvenanceIds) {
|
|
524
|
+
if (!isRecord(value))
|
|
525
|
+
refuse(`${path} must be an object`, "invalid_shape");
|
|
526
|
+
for (const key of NON_PORTABLE_AUTH_KEYS) {
|
|
527
|
+
if (key in value)
|
|
528
|
+
refuse(`${path}.${key}: ${AUTH_NOT_PORTABLE_MESSAGE}`, "auth_not_portable");
|
|
529
|
+
}
|
|
530
|
+
const kind = tagged("invalid_shape", () => enumValue(value.kind, AGENT_ASSET_KINDS, `${path}.kind`));
|
|
531
|
+
switch (kind) {
|
|
532
|
+
case "skill":
|
|
533
|
+
return validateSkill(value, path, knownProvenanceIds);
|
|
534
|
+
case "mcp-server":
|
|
535
|
+
return validateMcpServer(value, path, knownProvenanceIds);
|
|
536
|
+
case "cli":
|
|
537
|
+
return validateCli(value, path, knownProvenanceIds);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function assertNoSecretMaterialInAgentAssets(agentAssets) {
|
|
541
|
+
agentAssets.forEach((asset, i) => {
|
|
542
|
+
const at = `agentAssets[${i}]`;
|
|
543
|
+
const scan = (value, path) => {
|
|
544
|
+
const assignment = SECRET_ASSIGNMENT_RE.exec(value);
|
|
545
|
+
if (assignment) {
|
|
546
|
+
refuse(`${path} appears to contain an inline secret assignment (${assignment[1]}=...); ` + "declarations may reference environment/secret NAMES only, never values", "literal_secret");
|
|
547
|
+
}
|
|
548
|
+
const token = secretTokenIn(value);
|
|
549
|
+
if (token !== null)
|
|
550
|
+
refuse(secretTokenMessage(path, token.kind), "literal_secret");
|
|
551
|
+
};
|
|
552
|
+
switch (asset.kind) {
|
|
553
|
+
case "skill":
|
|
554
|
+
scan(asset.reference, `${at}.reference`);
|
|
555
|
+
return;
|
|
556
|
+
case "mcp-server":
|
|
557
|
+
if (asset.command !== undefined)
|
|
558
|
+
scan(asset.command, `${at}.command`);
|
|
559
|
+
asset.args?.forEach((arg, j) => scan(arg, `${at}.args[${j}]`));
|
|
560
|
+
if (asset.url !== undefined)
|
|
561
|
+
scan(asset.url, `${at}.url`);
|
|
562
|
+
for (const [name, value] of Object.entries(asset.headers ?? {})) {
|
|
563
|
+
if (looksLikeSecretValue(name, value)) {
|
|
564
|
+
refuse(`${at}.headers.${name} looks like a credential literal`, "literal_secret");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (const [name, value] of Object.entries(asset.env ?? {})) {
|
|
568
|
+
if (looksLikeSecretValue(name, value)) {
|
|
569
|
+
refuse(`${at}.env.${name} looks like a credential literal`, "literal_secret");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
case "cli":
|
|
574
|
+
if (asset.install.source === "npm")
|
|
575
|
+
scan(asset.install.package, `${at}.install.package`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// packages/environment/src/contract.ts
|
|
582
|
+
var TOP_LEVEL_FIELDS = new Set([
|
|
583
|
+
"version",
|
|
584
|
+
"toolchains",
|
|
585
|
+
"systemPackages",
|
|
586
|
+
"setup",
|
|
587
|
+
"verify",
|
|
588
|
+
"services",
|
|
589
|
+
"environment",
|
|
590
|
+
"agentAssets",
|
|
591
|
+
"provenance"
|
|
592
|
+
]);
|
|
593
|
+
var TOOLCHAIN_FIELDS = new Set(["name", "version", "provenanceIds"]);
|
|
594
|
+
var SYSTEM_PACKAGE_FIELDS = new Set(["name", "version", "provenanceIds"]);
|
|
595
|
+
var EXECUTABLE_STEP_FIELDS = new Set([
|
|
596
|
+
"id",
|
|
597
|
+
"command",
|
|
598
|
+
"cwd",
|
|
599
|
+
"timeoutSeconds",
|
|
600
|
+
"rerunWhenChanged",
|
|
601
|
+
"provenanceIds"
|
|
602
|
+
]);
|
|
603
|
+
var SERVICE_FIELDS = new Set(["name", "image", "readiness", "provenanceIds"]);
|
|
604
|
+
var SERVICE_READINESS_COMMAND_FIELDS = new Set(["kind", "command", "timeoutSeconds"]);
|
|
605
|
+
var SERVICE_READINESS_PORT_FIELDS = new Set(["kind", "port"]);
|
|
606
|
+
var ENVIRONMENT_NAME_FIELDS = new Set(["name", "required", "secret", "provenanceIds"]);
|
|
607
|
+
var DECLARATION_NAME_RE = /^[a-z][a-z0-9+._-]{0,63}$/;
|
|
608
|
+
var DECLARATION_VERSION_RE = /^[A-Za-z0-9^~*<>=][A-Za-z0-9.+_\-^~*<>=| ]{0,63}$/;
|
|
609
|
+
var STEP_ID_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
610
|
+
var ENV_NAME_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
611
|
+
var IMAGE_RE = /^[\x21-\x7E]{1,256}$/;
|
|
612
|
+
var MAX_TOOLCHAINS = 32;
|
|
613
|
+
var MAX_SYSTEM_PACKAGES = 128;
|
|
614
|
+
var MAX_STEPS = 64;
|
|
615
|
+
var MAX_SERVICES = 16;
|
|
616
|
+
var MAX_ENV_REFS = 128;
|
|
617
|
+
var MAX_AGENT_ASSETS = 64;
|
|
618
|
+
var MAX_PROVENANCE = 512;
|
|
619
|
+
var MAX_ARGV_LENGTH = 64;
|
|
620
|
+
var MAX_ARGV_ITEM_LENGTH = 1024;
|
|
621
|
+
var MAX_RERUN_ENTRIES = 64;
|
|
622
|
+
var MAX_RERUN_ENTRY_LENGTH = 256;
|
|
623
|
+
var MIN_TIMEOUT_SECONDS = 1;
|
|
624
|
+
var MAX_TIMEOUT_SECONDS = 3600;
|
|
625
|
+
function validateCommandArgv(value, path) {
|
|
626
|
+
const arr = boundedArray(value, path, MAX_ARGV_LENGTH);
|
|
627
|
+
if (arr.length === 0)
|
|
628
|
+
throw new EnvironmentContractError(`${path} must contain at least one argument`);
|
|
629
|
+
return arr.map((entry, i) => {
|
|
630
|
+
if (typeof entry !== "string" || entry.length === 0 || entry.length > MAX_ARGV_ITEM_LENGTH) {
|
|
631
|
+
throw new EnvironmentContractError(`${path}[${i}] must be a non-empty string of at most ${MAX_ARGV_ITEM_LENGTH} characters`);
|
|
632
|
+
}
|
|
633
|
+
return entry;
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function validateToolchainEntry(value, path, knownProvenanceIds) {
|
|
637
|
+
const raw = closedRecord(value, TOOLCHAIN_FIELDS, path);
|
|
638
|
+
const name = boundedString(raw.name, `${path}.name`, DECLARATION_NAME_RE, 64, "a lowercase identifier of at most 64 characters");
|
|
639
|
+
const version = boundedString(raw.version, `${path}.version`, DECLARATION_VERSION_RE, 64, "a version string of at most 64 characters");
|
|
640
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, knownProvenanceIds);
|
|
641
|
+
return { name, version, provenanceIds };
|
|
642
|
+
}
|
|
643
|
+
function validateSystemPackageEntry(value, path, knownProvenanceIds) {
|
|
644
|
+
const raw = closedRecord(value, SYSTEM_PACKAGE_FIELDS, path);
|
|
645
|
+
const name = boundedString(raw.name, `${path}.name`, DECLARATION_NAME_RE, 64, "a lowercase identifier of at most 64 characters");
|
|
646
|
+
let version;
|
|
647
|
+
if (raw.version !== undefined) {
|
|
648
|
+
version = boundedString(raw.version, `${path}.version`, DECLARATION_VERSION_RE, 64, "a version string of at most 64 characters");
|
|
649
|
+
}
|
|
650
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, knownProvenanceIds);
|
|
651
|
+
return { name, ...version !== undefined ? { version } : {}, provenanceIds };
|
|
652
|
+
}
|
|
653
|
+
function validateRerunWhenChanged(value, path) {
|
|
654
|
+
const arr = boundedArray(value, path, MAX_RERUN_ENTRIES);
|
|
655
|
+
if (arr.length === 0) {
|
|
656
|
+
throw new EnvironmentContractError(`${path} must contain at least one entry`);
|
|
657
|
+
}
|
|
658
|
+
const entries = arr.map((entry, i) => {
|
|
659
|
+
const entryPath = `${path}[${i}]`;
|
|
660
|
+
const validated = validateRelativePath(entry, entryPath);
|
|
661
|
+
if (validated.length > MAX_RERUN_ENTRY_LENGTH) {
|
|
662
|
+
throw new EnvironmentContractError(`${entryPath} must be at most ${MAX_RERUN_ENTRY_LENGTH} characters`);
|
|
663
|
+
}
|
|
664
|
+
return validated;
|
|
665
|
+
});
|
|
666
|
+
return sortedUnique(entries, (entry) => entry);
|
|
667
|
+
}
|
|
668
|
+
function validateExecutableStep(value, path, knownProvenanceIds) {
|
|
669
|
+
const raw = closedRecord(value, EXECUTABLE_STEP_FIELDS, path);
|
|
670
|
+
const id = boundedString(raw.id, `${path}.id`, STEP_ID_RE, 64, "a lowercase kebab-case identifier of at most 64 characters");
|
|
671
|
+
const command = validateCommandArgv(raw.command, `${path}.command`);
|
|
672
|
+
let cwd;
|
|
673
|
+
if (raw.cwd !== undefined)
|
|
674
|
+
cwd = validateRelativePath(raw.cwd, `${path}.cwd`);
|
|
675
|
+
const timeoutSeconds = boundedInteger(raw.timeoutSeconds, `${path}.timeoutSeconds`, MIN_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
|
676
|
+
let rerunWhenChanged;
|
|
677
|
+
if (raw.rerunWhenChanged !== undefined) {
|
|
678
|
+
rerunWhenChanged = validateRerunWhenChanged(raw.rerunWhenChanged, `${path}.rerunWhenChanged`);
|
|
679
|
+
}
|
|
680
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, knownProvenanceIds);
|
|
681
|
+
return {
|
|
682
|
+
id,
|
|
683
|
+
command,
|
|
684
|
+
...cwd !== undefined ? { cwd } : {},
|
|
685
|
+
timeoutSeconds,
|
|
686
|
+
...rerunWhenChanged !== undefined ? { rerunWhenChanged } : {},
|
|
687
|
+
provenanceIds
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function validateServiceReadiness(value, path) {
|
|
691
|
+
if (!isRecord(value))
|
|
692
|
+
throw new EnvironmentContractError(`${path} must be an object`);
|
|
693
|
+
if (value.kind === "command") {
|
|
694
|
+
const raw = closedRecord(value, SERVICE_READINESS_COMMAND_FIELDS, path);
|
|
695
|
+
const command = validateCommandArgv(raw.command, `${path}.command`);
|
|
696
|
+
const timeoutSeconds = boundedInteger(raw.timeoutSeconds, `${path}.timeoutSeconds`, MIN_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
|
697
|
+
return { kind: "command", command, timeoutSeconds };
|
|
698
|
+
}
|
|
699
|
+
if (value.kind === "port") {
|
|
700
|
+
const raw = closedRecord(value, SERVICE_READINESS_PORT_FIELDS, path);
|
|
701
|
+
const port = boundedInteger(raw.port, `${path}.port`, 1, 65535);
|
|
702
|
+
return { kind: "port", port };
|
|
703
|
+
}
|
|
704
|
+
throw new EnvironmentContractError(`${path}.kind must be one of "command", "port"`);
|
|
705
|
+
}
|
|
706
|
+
function validateServiceEntry(value, path, knownProvenanceIds) {
|
|
707
|
+
const raw = closedRecord(value, SERVICE_FIELDS, path);
|
|
708
|
+
const name = boundedString(raw.name, `${path}.name`, DECLARATION_NAME_RE, 64, "a lowercase identifier of at most 64 characters");
|
|
709
|
+
let image;
|
|
710
|
+
if (raw.image !== undefined) {
|
|
711
|
+
image = boundedString(raw.image, `${path}.image`, IMAGE_RE, 256, "a printable image reference of at most 256 characters");
|
|
712
|
+
}
|
|
713
|
+
const readiness = validateServiceReadiness(raw.readiness, `${path}.readiness`);
|
|
714
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, knownProvenanceIds);
|
|
715
|
+
return { name, ...image !== undefined ? { image } : {}, readiness, provenanceIds };
|
|
716
|
+
}
|
|
717
|
+
function validateEnvironmentNameRef(value, path, knownProvenanceIds) {
|
|
718
|
+
const raw = closedRecord(value, ENVIRONMENT_NAME_FIELDS, path);
|
|
719
|
+
const name = boundedString(raw.name, `${path}.name`, ENV_NAME_RE2, 128, "an environment variable name");
|
|
720
|
+
if (typeof raw.required !== "boolean") {
|
|
721
|
+
throw new EnvironmentContractError(`${path}.required must be a boolean`);
|
|
722
|
+
}
|
|
723
|
+
let secret;
|
|
724
|
+
if (raw.secret !== undefined) {
|
|
725
|
+
if (typeof raw.secret !== "boolean") {
|
|
726
|
+
throw new EnvironmentContractError(`${path}.secret must be a boolean`);
|
|
727
|
+
}
|
|
728
|
+
secret = raw.secret;
|
|
729
|
+
}
|
|
730
|
+
const provenanceIds = validateProvenanceIdRefs(raw.provenanceIds, `${path}.provenanceIds`, knownProvenanceIds);
|
|
731
|
+
return {
|
|
732
|
+
name,
|
|
733
|
+
required: raw.required,
|
|
734
|
+
...secret !== undefined ? { secret } : {},
|
|
735
|
+
provenanceIds
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function assertUniqueStepIds(steps, path) {
|
|
739
|
+
const seen = new Set;
|
|
740
|
+
for (const step of steps) {
|
|
741
|
+
if (seen.has(step.id))
|
|
742
|
+
throw new EnvironmentContractError(`${path} contains duplicate id ${JSON.stringify(step.id)}`);
|
|
743
|
+
seen.add(step.id);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function assertUniqueServiceNames(services) {
|
|
747
|
+
const seen = new Set;
|
|
748
|
+
for (const service of services) {
|
|
749
|
+
if (seen.has(service.name)) {
|
|
750
|
+
throw new EnvironmentContractError(`services contains duplicate name ${JSON.stringify(service.name)}`);
|
|
751
|
+
}
|
|
752
|
+
seen.add(service.name);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function scanForSecretMaterial(value, path) {
|
|
756
|
+
const assignmentMatch = SECRET_ASSIGNMENT_RE.exec(value);
|
|
757
|
+
if (assignmentMatch) {
|
|
758
|
+
throw new EnvironmentContractError(`${path} appears to contain an inline secret assignment (${assignmentMatch[1]}=...); ` + "declarations may reference environment/secret NAMES only, never values");
|
|
759
|
+
}
|
|
760
|
+
const token = secretTokenIn(value);
|
|
761
|
+
if (token !== null)
|
|
762
|
+
throw new EnvironmentContractError(secretTokenMessage(path, token.kind));
|
|
763
|
+
}
|
|
764
|
+
function assertNoSecretMaterial(declaration) {
|
|
765
|
+
const scanStep = (step, path) => {
|
|
766
|
+
step.command.forEach((arg, i) => scanForSecretMaterial(arg, `${path}.command[${i}]`));
|
|
767
|
+
};
|
|
768
|
+
declaration.setup.forEach((step, i) => scanStep(step, `setup[${i}]`));
|
|
769
|
+
declaration.verify.forEach((step, i) => scanStep(step, `verify[${i}]`));
|
|
770
|
+
declaration.services.forEach((service, i) => {
|
|
771
|
+
if (service.readiness.kind === "command") {
|
|
772
|
+
service.readiness.command.forEach((arg, j) => scanForSecretMaterial(arg, `services[${i}].readiness.command[${j}]`));
|
|
773
|
+
}
|
|
774
|
+
if (service.image !== undefined)
|
|
775
|
+
scanForSecretMaterial(service.image, `services[${i}].image`);
|
|
776
|
+
});
|
|
777
|
+
assertNoSecretMaterialInProvenance(declaration.provenance);
|
|
778
|
+
assertNoSecretMaterialInAgentAssets(declaration.agentAssets);
|
|
779
|
+
}
|
|
780
|
+
function assertNoSecretMaterialInProvenance(provenance) {
|
|
781
|
+
provenance.forEach((record, i) => scanForSecretMaterial(record.explanation, `provenance[${i}].explanation`));
|
|
782
|
+
}
|
|
783
|
+
function validateProvenanceList(value) {
|
|
784
|
+
const rawProvenance = boundedArray(value, "provenance", MAX_PROVENANCE);
|
|
785
|
+
const rawProvenanceIds = rawProvenance.map((entry) => isRecord(entry) ? entry.id : undefined);
|
|
786
|
+
if (new Set(rawProvenanceIds).size !== rawProvenanceIds.length) {
|
|
787
|
+
throw new EnvironmentContractError("provenance ids must be unique");
|
|
788
|
+
}
|
|
789
|
+
const provenance = sortedUnique(rawProvenance.map((entry, i) => validateProvenanceRecord(entry, `provenance[${i}]`)), (p) => p.id);
|
|
790
|
+
return provenance;
|
|
791
|
+
}
|
|
792
|
+
function validateRepoEnvironmentDeclaration(input) {
|
|
793
|
+
const raw = closedRecord(input, TOP_LEVEL_FIELDS, "declaration");
|
|
794
|
+
if (raw.version !== 1) {
|
|
795
|
+
throw new EnvironmentContractError(`version must be exactly 1 (got ${JSON.stringify(raw.version)})`);
|
|
796
|
+
}
|
|
797
|
+
const provenance = validateProvenanceList(raw.provenance);
|
|
798
|
+
const knownProvenanceIds = new Set(provenance.map((p) => p.id));
|
|
799
|
+
const toolchains = sortedUnique(boundedArray(raw.toolchains, "toolchains", MAX_TOOLCHAINS).map((entry, i) => validateToolchainEntry(entry, `toolchains[${i}]`, knownProvenanceIds)), (t) => t.name);
|
|
800
|
+
const systemPackages = sortedUnique(boundedArray(raw.systemPackages, "systemPackages", MAX_SYSTEM_PACKAGES).map((entry, i) => validateSystemPackageEntry(entry, `systemPackages[${i}]`, knownProvenanceIds)), (p) => p.name);
|
|
801
|
+
const setup = boundedArray(raw.setup, "setup", MAX_STEPS).map((entry, i) => validateExecutableStep(entry, `setup[${i}]`, knownProvenanceIds));
|
|
802
|
+
assertUniqueStepIds(setup, "setup");
|
|
803
|
+
const verify = boundedArray(raw.verify, "verify", MAX_STEPS).map((entry, i) => validateExecutableStep(entry, `verify[${i}]`, knownProvenanceIds));
|
|
804
|
+
assertUniqueStepIds(verify, "verify");
|
|
805
|
+
const services = boundedArray(raw.services, "services", MAX_SERVICES).map((entry, i) => validateServiceEntry(entry, `services[${i}]`, knownProvenanceIds));
|
|
806
|
+
assertUniqueServiceNames(services);
|
|
807
|
+
const environment = sortedUnique(boundedArray(raw.environment, "environment", MAX_ENV_REFS).map((entry, i) => validateEnvironmentNameRef(entry, `environment[${i}]`, knownProvenanceIds)), (e) => e.name);
|
|
808
|
+
const agentAssets = sortedUnique(boundedArray(raw.agentAssets, "agentAssets", MAX_AGENT_ASSETS).map((entry, i) => validateAgentAssetEntry(entry, `agentAssets[${i}]`, knownProvenanceIds)), agentAssetKey);
|
|
809
|
+
const declaration = {
|
|
810
|
+
version: 1,
|
|
811
|
+
toolchains,
|
|
812
|
+
systemPackages,
|
|
813
|
+
setup,
|
|
814
|
+
verify,
|
|
815
|
+
services,
|
|
816
|
+
environment,
|
|
817
|
+
agentAssets,
|
|
818
|
+
provenance
|
|
819
|
+
};
|
|
820
|
+
assertNoSecretMaterial(declaration);
|
|
821
|
+
return declaration;
|
|
822
|
+
}
|
|
823
|
+
// packages/environment/src/account-contract.ts
|
|
824
|
+
var TOP_LEVEL_FIELDS2 = new Set(["version", "environment", "agentAssets", "provenance"]);
|
|
825
|
+
// packages/environment/src/setup-key.ts
|
|
826
|
+
var RERUN_WHEN_CHANGED_KEY = "cache_key_files";
|
|
827
|
+
var EXCLUDED_SEGMENTS = new Set(["node_modules", ".git"]);
|
|
828
|
+
// packages/environment/src/local-image/runner-base.ts
|
|
829
|
+
var RUNNER_BASE_REF_PREFIX = "registry.cloudflare.com/080cccc297a4b9cceca559f1095c0d10/catalyst-runner";
|
|
830
|
+
var RUNNER_BASE_IMAGE_TAG = "catalyst-runner:1c477b2690b68d0b14386741156ee70b1fd48137";
|
|
831
|
+
var RUNNER_BASE_REF = `${RUNNER_BASE_REF_PREFIX}:${RUNNER_BASE_IMAGE_TAG.split(":")[1]}`;
|
|
832
|
+
var BAKED_TOOLCHAINS = Object.freeze({
|
|
833
|
+
bun: "1.4.2",
|
|
834
|
+
node: "22.23.2",
|
|
835
|
+
python: "3.12.14",
|
|
836
|
+
go: "1.27.1",
|
|
837
|
+
ruby: "3.4.10",
|
|
838
|
+
java: "temurin-21.0.11+10.0.LTS",
|
|
839
|
+
rust: "1.98.0"
|
|
840
|
+
});
|
|
841
|
+
var BAKED_TOOLCHAIN_ARGS = Object.freeze({
|
|
842
|
+
node: "NODE_VERSION",
|
|
843
|
+
python: "PYTHON_VERSION",
|
|
844
|
+
go: "GO_VERSION",
|
|
845
|
+
ruby: "RUBY_VERSION",
|
|
846
|
+
java: "JAVA_VERSION",
|
|
847
|
+
rust: "RUST_VERSION"
|
|
848
|
+
});
|
|
849
|
+
// packages/environment/src/local-image/mise-riding-tools.ts
|
|
850
|
+
var MISE_RIDING_TOOLS = Object.freeze({
|
|
851
|
+
node: "node",
|
|
852
|
+
nodejs: "node",
|
|
853
|
+
python: "python"
|
|
854
|
+
});
|
|
855
|
+
// packages/protocol/src/usage-accounting.ts
|
|
856
|
+
var USAGE_RECORD_MAX_BYTES = 64 * 1024;
|
|
857
|
+
var USAGE_COUNTER_DESCRIPTORS = {
|
|
858
|
+
cpuUsec: {
|
|
859
|
+
aggregation: "cumulative-counter",
|
|
860
|
+
unit: "microseconds",
|
|
861
|
+
displayUnit: "CPU-seconds",
|
|
862
|
+
displayDivisor: 1e6,
|
|
863
|
+
label: "CPU time",
|
|
864
|
+
kind: "cpu"
|
|
865
|
+
},
|
|
866
|
+
allocatedMemoryByteSeconds: {
|
|
867
|
+
aggregation: "integral",
|
|
868
|
+
unit: "byte-seconds",
|
|
869
|
+
displayUnit: "GiB-seconds",
|
|
870
|
+
displayDivisor: 1024 * 1024 * 1024,
|
|
871
|
+
label: "Allocated memory",
|
|
872
|
+
kind: "memory"
|
|
873
|
+
},
|
|
874
|
+
memoryCurrentBytes: {
|
|
875
|
+
aggregation: "gauge",
|
|
876
|
+
unit: "bytes",
|
|
877
|
+
displayUnit: "bytes",
|
|
878
|
+
displayDivisor: 1,
|
|
879
|
+
label: "Current memory",
|
|
880
|
+
kind: "memory"
|
|
881
|
+
},
|
|
882
|
+
memoryPeakBytes: {
|
|
883
|
+
aggregation: "peak",
|
|
884
|
+
unit: "bytes",
|
|
885
|
+
displayUnit: "bytes",
|
|
886
|
+
displayDivisor: 1,
|
|
887
|
+
label: "Peak memory",
|
|
888
|
+
kind: "memory"
|
|
889
|
+
},
|
|
890
|
+
ioReadBytes: {
|
|
891
|
+
aggregation: "cumulative-counter",
|
|
892
|
+
unit: "bytes",
|
|
893
|
+
displayUnit: "bytes",
|
|
894
|
+
displayDivisor: 1,
|
|
895
|
+
label: "Disk read",
|
|
896
|
+
kind: "disk-io"
|
|
897
|
+
},
|
|
898
|
+
ioWriteBytes: {
|
|
899
|
+
aggregation: "cumulative-counter",
|
|
900
|
+
unit: "bytes",
|
|
901
|
+
displayUnit: "bytes",
|
|
902
|
+
displayDivisor: 1,
|
|
903
|
+
label: "Disk write",
|
|
904
|
+
kind: "disk-io"
|
|
905
|
+
},
|
|
906
|
+
networkRxBytes: {
|
|
907
|
+
aggregation: "cumulative-counter",
|
|
908
|
+
unit: "bytes",
|
|
909
|
+
displayUnit: "bytes",
|
|
910
|
+
displayDivisor: 1,
|
|
911
|
+
label: "Network received",
|
|
912
|
+
kind: "network"
|
|
913
|
+
},
|
|
914
|
+
networkTxBytes: {
|
|
915
|
+
aggregation: "cumulative-counter",
|
|
916
|
+
unit: "bytes",
|
|
917
|
+
displayUnit: "bytes",
|
|
918
|
+
displayDivisor: 1,
|
|
919
|
+
label: "Network sent",
|
|
920
|
+
kind: "network"
|
|
921
|
+
},
|
|
922
|
+
oomEvents: {
|
|
923
|
+
aggregation: "cumulative-counter",
|
|
924
|
+
unit: "count",
|
|
925
|
+
displayUnit: "count",
|
|
926
|
+
displayDivisor: 1,
|
|
927
|
+
label: "OOM events",
|
|
928
|
+
kind: "reliability"
|
|
929
|
+
},
|
|
930
|
+
oomKillEvents: {
|
|
931
|
+
aggregation: "cumulative-counter",
|
|
932
|
+
unit: "count",
|
|
933
|
+
displayUnit: "count",
|
|
934
|
+
displayDivisor: 1,
|
|
935
|
+
label: "OOM kills",
|
|
936
|
+
kind: "reliability"
|
|
937
|
+
},
|
|
938
|
+
oomGroupKillEvents: {
|
|
939
|
+
aggregation: "cumulative-counter",
|
|
940
|
+
unit: "count",
|
|
941
|
+
displayUnit: "count",
|
|
942
|
+
displayDivisor: 1,
|
|
943
|
+
label: "OOM group kills",
|
|
944
|
+
kind: "reliability"
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
var COUNTER_KEYS = Object.keys(USAGE_COUNTER_DESCRIPTORS);
|
|
948
|
+
var counterAggregations = Object.fromEntries(Object.entries(USAGE_COUNTER_DESCRIPTORS).map(([key, descriptor]) => [
|
|
949
|
+
key,
|
|
950
|
+
descriptor.aggregation
|
|
951
|
+
]));
|
|
952
|
+
|
|
953
|
+
// packages/protocol/src/project-workspace.ts
|
|
954
|
+
var PROJECT_WORKSPACE_MAX_REPOSITORIES = 16;
|
|
955
|
+
var INTAKE_MAX_REPOSITORIES = PROJECT_WORKSPACE_MAX_REPOSITORIES - 1;
|
|
956
|
+
// packages/protocol/src/provider-retry-window.ts
|
|
957
|
+
var RESET_VOCAB = String.raw`\b(?:will\s+reset|resets?|limit\s+resets?|try\s+again)\b[^\n]{0,40}?\bat\b`;
|
|
958
|
+
var ANCHORED_ISO = new RegExp(RESET_VOCAB + String.raw`[^\n]{0,10}?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)`, "i");
|
|
959
|
+
var ANCHORED_EPOCH = new RegExp(RESET_VOCAB + String.raw`[^\n]{0,10}?\|?\s*(\d{10})\b`, "i");
|
|
960
|
+
var SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
|
|
961
|
+
var CLOCK_RESET_PASSED_GRACE_MS = 60 * 60 * 1000;
|
|
962
|
+
var QUARTER_HOUR_MS = 15 * 60 * 1000;
|
|
963
|
+
var MAX_ZONE_OFFSET_MS = 14 * 60 * 60 * 1000;
|
|
964
|
+
var MAX_FOLD_MS = 60 * 60 * 1000;
|
|
965
|
+
// packages/protocol/src/worker-agent-id.ts
|
|
966
|
+
var ROSTER_LIVE_WITHIN_MS = 10 * 60000;
|
|
967
|
+
// packages/protocol/src/context-policy.ts
|
|
968
|
+
var DEFAULT_CONTEXT_POLICY = Object.freeze({
|
|
969
|
+
default: Object.freeze({ handoffAt: 250000, nativeCompactAt: 350000 }),
|
|
970
|
+
phases: Object.freeze({
|
|
971
|
+
intake: Object.freeze({ handoffAt: 200000, nativeCompactAt: 350000 }),
|
|
972
|
+
pr: Object.freeze({ handoffAt: 200000, nativeCompactAt: 350000 })
|
|
973
|
+
})
|
|
974
|
+
});
|
|
975
|
+
var OFF_CONTEXT_POLICY = Object.freeze({
|
|
976
|
+
mode: "off",
|
|
977
|
+
handoffAt: null,
|
|
978
|
+
nativeCompactAt: null
|
|
979
|
+
});
|
|
980
|
+
var MODES = new Set([
|
|
981
|
+
"off",
|
|
982
|
+
"native",
|
|
983
|
+
"shadow",
|
|
984
|
+
"enforce"
|
|
985
|
+
]);
|
|
986
|
+
// packages/protocol/src/subscription-account.ts
|
|
987
|
+
var MAX_DECLARED_VALIDITY_MS = 180 * 24 * 60 * 60 * 1000;
|
|
988
|
+
// packages/protocol/src/providers.ts
|
|
989
|
+
var CATALOG = {
|
|
990
|
+
claude: { protocol: "anthropic", harness: "claude-cli" },
|
|
991
|
+
codex: { protocol: "openai", harness: "codex" },
|
|
992
|
+
glm: {
|
|
993
|
+
protocol: "anthropic",
|
|
994
|
+
harness: "claude-cli",
|
|
995
|
+
anthropicBaseUrl: "https://api.z.ai/api/anthropic"
|
|
996
|
+
},
|
|
997
|
+
qwen: {
|
|
998
|
+
protocol: "anthropic",
|
|
999
|
+
harness: "claude-cli",
|
|
1000
|
+
anthropicBaseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic"
|
|
1001
|
+
},
|
|
1002
|
+
"glm-opencode": {
|
|
1003
|
+
protocol: "anthropic",
|
|
1004
|
+
harness: "opencode",
|
|
1005
|
+
anthropicBaseUrl: "https://api.z.ai/api/anthropic",
|
|
1006
|
+
credentialProvider: "glm"
|
|
1007
|
+
},
|
|
1008
|
+
"qwen-opencode": {
|
|
1009
|
+
protocol: "anthropic",
|
|
1010
|
+
harness: "opencode",
|
|
1011
|
+
anthropicBaseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic",
|
|
1012
|
+
credentialProvider: "qwen",
|
|
1013
|
+
openCodeModelLimits: { "qwen3.8-max": { context: 983616, output: 131072 } }
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
var RUNTIME_PROVIDERS = CATALOG;
|
|
1017
|
+
function isRuntimeProvider(value) {
|
|
1018
|
+
return Object.hasOwn(RUNTIME_PROVIDERS, value);
|
|
1019
|
+
}
|
|
1020
|
+
// packages/protocol/src/session-messages.ts
|
|
1021
|
+
var SESSION_MESSAGE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1022
|
+
var SESSION_MESSAGE_INBOX_MAX_BYTES = 1024 * 1024;
|
|
1023
|
+
var SESSION_MESSAGE_DELIVERED_MAX_BYTES = 256 * 1024;
|
|
1024
|
+
|
|
1025
|
+
// packages/protocol/src/prompt-run.ts
|
|
1026
|
+
var PROMPT_CAPABLE_PROVIDERS = new Set(["claude"]);
|
|
1027
|
+
|
|
1028
|
+
// packages/protocol/src/job-execution.ts
|
|
1029
|
+
var EXECUTION_JOB_MAX_BYTES = 64 * 1024;
|
|
1030
|
+
var ASK_TRIAGE_EXECUTION_JOB_MAX_BYTES = 160 * 1024;
|
|
1031
|
+
var EXECUTION_RESULT_MAX_BYTES = 64 * 1024;
|
|
1032
|
+
var EXECUTION_MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
1033
|
+
var EXECUTION_MAX_TOTAL_ARTIFACT_BYTES = 256 * 1024 * 1024;
|
|
1034
|
+
var PREPARATION_MAX_PART_BYTES = 32 * 1024 * 1024;
|
|
1035
|
+
var EXECUTION_HANDOVER_GATE_PHASES = new Set(["implement", "remediate"]);
|
|
1036
|
+
var EXECUTION_NO_REPO_SECRETS_PHASES = new Set(["intake", "respond"]);
|
|
1037
|
+
var utf8Bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
1038
|
+
var REPAIR_SCOPE_KEY_BYTES = utf8Bytes({ a: 0, repairScope: 0 }) - utf8Bytes({ a: 0 }) - 1;
|
|
1039
|
+
var PRIOR_RECHECK_KEY_BYTES = utf8Bytes({ a: 0, priorRecheck: 0 }) - utf8Bytes({ a: 0 }) - 1;
|
|
1040
|
+
var EXECUTION_EXIT_CODE_MIN = -(2 ** 31);
|
|
1041
|
+
var EXECUTION_EXIT_CODE_MAX = 2 ** 32 - 1;
|
|
1042
|
+
var VERDICT_UNREPORTED_REASONS = [
|
|
1043
|
+
"no_block",
|
|
1044
|
+
"malformed_json",
|
|
1045
|
+
"unsupported_version",
|
|
1046
|
+
"not_an_object",
|
|
1047
|
+
"missing_exit_code"
|
|
1048
|
+
];
|
|
1049
|
+
var BUILD_UNREPORTED_REASONS = [...VERDICT_UNREPORTED_REASONS, "missing_attempted"];
|
|
1050
|
+
var GATE_UNREPORTED_REASONS = [...VERDICT_UNREPORTED_REASONS, "missing_ran"];
|
|
1051
|
+
|
|
1052
|
+
// packages/protocol/src/client.ts
|
|
1053
|
+
var LADDER_MAX_SERIALIZED_BYTES = 8 * 1024;
|
|
1054
|
+
var OFF_PLAN_MAX_SERIALIZED_BYTES = 16 * 1024;
|
|
1055
|
+
// packages/protocol/src/environment-check.ts
|
|
1056
|
+
var ENV_CHECK_REPORT_MAX_BYTES = 8 * 1024;
|
|
1057
|
+
// packages/protocol/src/container-instance.ts
|
|
1058
|
+
var CONTAINER_INSTANCE_SPECS = {
|
|
1059
|
+
lite: { vcpu: 1 / 16, memoryMiB: 256, diskGB: 2 },
|
|
1060
|
+
dev: { vcpu: 1 / 16, memoryMiB: 256, diskGB: 2 },
|
|
1061
|
+
basic: { vcpu: 1 / 4, memoryMiB: 1024, diskGB: 4 },
|
|
1062
|
+
"standard-1": { vcpu: 1 / 2, memoryMiB: 4096, diskGB: 8 },
|
|
1063
|
+
standard: { vcpu: 1 / 2, memoryMiB: 4096, diskGB: 8 },
|
|
1064
|
+
"standard-2": { vcpu: 1, memoryMiB: 6144, diskGB: 12 },
|
|
1065
|
+
"standard-3": { vcpu: 2, memoryMiB: 8192, diskGB: 16 },
|
|
1066
|
+
"standard-4": { vcpu: 4, memoryMiB: 12288, diskGB: 20 },
|
|
1067
|
+
"custom-2x6144x12000": { vcpu: 2, memoryMiB: 6144, diskGB: 12 },
|
|
1068
|
+
"custom-2x7168x14000": { vcpu: 2, memoryMiB: 7168, diskGB: 14 }
|
|
1069
|
+
};
|
|
1070
|
+
var RUNNER_SIZE_CLASSES = ["standard", "large"];
|
|
1071
|
+
// packages/protocol/src/phase-resources.ts
|
|
1072
|
+
var GiB = 1024 ** 3;
|
|
1073
|
+
var PHASE_SIZE_CLASSES = Object.freeze({
|
|
1074
|
+
gate: Object.freeze({ cpuMillis: 6000, memoryBytes: 9 * GiB, pidsLimit: 4096 }),
|
|
1075
|
+
standard: Object.freeze({ cpuMillis: 2000, memoryBytes: 6 * GiB, pidsLimit: 512 })
|
|
1076
|
+
});
|
|
1077
|
+
var GATE_PHASES = Object.freeze(["implement", "validate", "remediate"]);
|
|
1078
|
+
var PHASE_SIZE_CLASS = Object.freeze({
|
|
1079
|
+
intake: "standard",
|
|
1080
|
+
research: "standard",
|
|
1081
|
+
plan: "standard",
|
|
1082
|
+
implement: "gate",
|
|
1083
|
+
validate: "gate",
|
|
1084
|
+
pr: "standard",
|
|
1085
|
+
remediate: "gate",
|
|
1086
|
+
respond: "standard",
|
|
1087
|
+
prompt: "standard",
|
|
1088
|
+
"ask-triage": "standard"
|
|
1089
|
+
});
|
|
1090
|
+
// packages/protocol/src/container-pricing.ts
|
|
1091
|
+
var MAX_DURATION_MS = 7 * 24 * 60 * 60000;
|
|
1092
|
+
// packages/protocol/src/attempt-git.ts
|
|
1093
|
+
var ATTEMPT_GIT_TOKEN_MAX_TTL_MS = 60 * 60 * 1000;
|
|
1094
|
+
// packages/protocol/src/host-enrollment.ts
|
|
1095
|
+
var JOIN_TOKEN_DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
1096
|
+
var JOIN_TOKEN_MAX_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1097
|
+
var HOST_CREDENTIAL_ROTATE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1098
|
+
// packages/protocol/src/phase-prerequisites.ts
|
|
1099
|
+
var PHASE_PREREQUISITES = [
|
|
1100
|
+
{
|
|
1101
|
+
id: "harness.claude",
|
|
1102
|
+
harness: "claude-cli",
|
|
1103
|
+
probe: { kind: "cli", names: ["claude"] },
|
|
1104
|
+
severity: { container: "required", workstation: "warn" },
|
|
1105
|
+
remedy: "install Claude Code (`claude`) on PATH"
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
id: "harness.codex",
|
|
1109
|
+
harness: "codex",
|
|
1110
|
+
probe: { kind: "cli", names: ["codex"] },
|
|
1111
|
+
severity: { container: "required", workstation: "warn" },
|
|
1112
|
+
remedy: "install the Codex CLI (`codex`) on PATH"
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
id: "harness.opencode",
|
|
1116
|
+
harness: "opencode",
|
|
1117
|
+
probe: { kind: "cli", names: ["opencode"] },
|
|
1118
|
+
severity: { container: "required", workstation: "warn" },
|
|
1119
|
+
remedy: "install OpenCode (`opencode`) on PATH"
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
id: "toolchain.node",
|
|
1123
|
+
probe: { kind: "cli", names: ["node"] },
|
|
1124
|
+
severity: { container: "required", workstation: "required" },
|
|
1125
|
+
remedy: "install Node.js on PATH"
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
id: "toolchain.npm",
|
|
1129
|
+
probe: { kind: "cli", names: ["npm"] },
|
|
1130
|
+
severity: { container: "required", workstation: "required" },
|
|
1131
|
+
remedy: "install npm (it ships with Node.js) on PATH"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: "toolchain.bun",
|
|
1135
|
+
probe: { kind: "cli", names: ["bun"] },
|
|
1136
|
+
severity: { container: "required", workstation: "warn" },
|
|
1137
|
+
remedy: "install Bun on PATH"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
id: "toolchain.git",
|
|
1141
|
+
probe: { kind: "cli", names: ["git"] },
|
|
1142
|
+
severity: { container: "required", workstation: "required" },
|
|
1143
|
+
remedy: "install Git on PATH"
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
id: "toolchain.mise",
|
|
1147
|
+
probe: { kind: "cli", names: ["mise"] },
|
|
1148
|
+
severity: { container: "required" },
|
|
1149
|
+
remedy: "the runner image must bake mise on PATH"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
id: "browser.chromium",
|
|
1153
|
+
probe: { kind: "cli", names: ["chromium"] },
|
|
1154
|
+
severity: { container: "required" },
|
|
1155
|
+
remedy: "the runner image must bake Debian's `chromium` wrapper on PATH"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
id: "browser.chromium-flags",
|
|
1159
|
+
probe: { kind: "file", path: "/etc/chromium.d/zz-catalyst" },
|
|
1160
|
+
severity: { container: "required" },
|
|
1161
|
+
remedy: "the runner image must write /etc/chromium.d/zz-catalyst (--no-sandbox, --disable-dev-shm-usage); without it Chromium dies in the container"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
id: "browser.chrome",
|
|
1165
|
+
probe: {
|
|
1166
|
+
kind: "cli",
|
|
1167
|
+
names: ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"],
|
|
1168
|
+
paths: ["/Applications/Google Chrome.app", "/Applications/Chromium.app"]
|
|
1169
|
+
},
|
|
1170
|
+
severity: { workstation: "warn" },
|
|
1171
|
+
remedy: "install Chrome or Chromium so browser-driven checks can run"
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
id: "skills.bundle",
|
|
1175
|
+
probe: { kind: "skills", location: "bundle" },
|
|
1176
|
+
severity: { container: "required" },
|
|
1177
|
+
remedy: "the baked plugin bundle must list every phase skill under skills/"
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
id: "skills.home",
|
|
1181
|
+
probe: { kind: "skills", location: "home" },
|
|
1182
|
+
severity: { container: "warn", workstation: "required" },
|
|
1183
|
+
remedy: "install the Catalyst skill packs so $HOME/.agents/skills or $HOME/.claude/skills lists them"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
id: "env.otel-endpoint",
|
|
1187
|
+
suppliedBy: "host",
|
|
1188
|
+
probe: { kind: "env", anyOf: [["OTEL_EXPORTER_OTLP_ENDPOINT"]] },
|
|
1189
|
+
severity: { container: "required", workstation: "warn" },
|
|
1190
|
+
remedy: "in a container the endpoint was cleared, not forgotten: the image bakes it and the guest keeps it only when the host also passes the ingest pair, so check the launcher and the host's pair; on a workstation, set OTEL_EXPORTER_OTLP_ENDPOINT to export telemetry"
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
id: "env.otel-credentials",
|
|
1194
|
+
suppliedBy: "host",
|
|
1195
|
+
probe: {
|
|
1196
|
+
kind: "env",
|
|
1197
|
+
anyOf: [
|
|
1198
|
+
["OTEL_INGEST_CLIENT_ID", "OTEL_INGEST_CLIENT_SECRET", "CATALYST_SUBSTRATE_HOST_NAME"]
|
|
1199
|
+
]
|
|
1200
|
+
},
|
|
1201
|
+
severity: { container: "required" },
|
|
1202
|
+
remedy: "the host must pass the OTel ingest client id and secret and CATALYST_SUBSTRATE_HOST_NAME to the phase container; without them the phase is neither billed nor observed"
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
id: "env.claude-code-telemetry",
|
|
1206
|
+
harness: "claude-cli",
|
|
1207
|
+
probe: {
|
|
1208
|
+
kind: "env",
|
|
1209
|
+
anyOf: [
|
|
1210
|
+
[
|
|
1211
|
+
"CLAUDE_CODE_ENABLE_TELEMETRY",
|
|
1212
|
+
"OTEL_METRICS_EXPORTER",
|
|
1213
|
+
"OTEL_LOGS_EXPORTER",
|
|
1214
|
+
"OTEL_TRACES_EXPORTER",
|
|
1215
|
+
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
|
|
1216
|
+
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
1217
|
+
"OTEL_LOG_USER_PROMPTS",
|
|
1218
|
+
"OTEL_LOG_TOOL_DETAILS",
|
|
1219
|
+
"OTEL_LOG_TOOL_CONTENT"
|
|
1220
|
+
]
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
1223
|
+
severity: { container: "required" },
|
|
1224
|
+
remedy: "the runner image must set Claude Code's telemetry environment (images/runner/Dockerfile ENV)"
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
id: "env.claude-code-telemetry-enabled",
|
|
1228
|
+
probe: { kind: "env", anyOf: [["CLAUDE_CODE_ENABLE_TELEMETRY"]] },
|
|
1229
|
+
severity: { workstation: "warn" },
|
|
1230
|
+
remedy: "set CLAUDE_CODE_ENABLE_TELEMETRY=1 to send Claude Code telemetry (optional on a workstation)"
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
id: "host.docker-reachable",
|
|
1234
|
+
probe: { kind: "host" },
|
|
1235
|
+
severity: { host: "required" },
|
|
1236
|
+
remedy: "the Docker engine must be reachable through the mounted socket; nothing runs without it"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
id: "host.docker-socket-gid",
|
|
1240
|
+
probe: { kind: "host" },
|
|
1241
|
+
severity: { host: "required" },
|
|
1242
|
+
remedy: "DOCKER_SOCKET_GID must equal the Docker socket's group, or the non-root services get EACCES"
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
id: "host.disk-mode",
|
|
1246
|
+
probe: { kind: "host" },
|
|
1247
|
+
severity: { host: "required" },
|
|
1248
|
+
remedy: "CATALYST_SLOT_DISK_MODE=filesystem needs loop devices, which macOS refuses (losetup EPERM); use CATALYST_SLOT_DISK_MODE=budget"
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: "host.attempt-label-namespace",
|
|
1252
|
+
probe: { kind: "host" },
|
|
1253
|
+
severity: { host: "required" },
|
|
1254
|
+
remedy: "the attempt-label namespace must be disjoint from any other stack on the box; foreign attempt containers are refused, never touched"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
id: "host.image-platform",
|
|
1258
|
+
probe: { kind: "host" },
|
|
1259
|
+
severity: { host: "warn" },
|
|
1260
|
+
remedy: "the runner image is built for another CPU architecture, so phases run under emulation, slowly; expected until the multi-arch image ships (CTC-3356)"
|
|
1261
|
+
}
|
|
1262
|
+
];
|
|
1263
|
+
var PREREQUISITE_HARNESS_CLIS = PHASE_PREREQUISITES.flatMap((entry) => entry.harness !== undefined && entry.probe.kind === "cli" ? entry.probe.names : []);
|
|
1264
|
+
// packages/environment/src/import/agents.ts
|
|
1265
|
+
var SHELL_LANGS = new Set(["bash", "sh", "shell", "zsh"]);
|
|
1266
|
+
|
|
1267
|
+
// packages/environment/src/import/devcontainer.ts
|
|
1268
|
+
var IGNORED_STRUCTURAL_KEYS = new Set([
|
|
1269
|
+
"name",
|
|
1270
|
+
"workspaceFolder",
|
|
1271
|
+
"workspaceMount",
|
|
1272
|
+
"customizations",
|
|
1273
|
+
"forwardPorts",
|
|
1274
|
+
"portsAttributes",
|
|
1275
|
+
"runArgs",
|
|
1276
|
+
"shutdownAction",
|
|
1277
|
+
"$schema",
|
|
1278
|
+
"remoteUser",
|
|
1279
|
+
"containerUser"
|
|
1280
|
+
]);
|
|
1281
|
+
|
|
1282
|
+
// packages/environment/src/import/workflow.ts
|
|
1283
|
+
import { LineCounter, isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
1284
|
+
// packages/environment/src/propose.ts
|
|
1285
|
+
var DECISIONS_FIELDS = new Set(["placeAsSetup", "placeAsVerify", "timeoutSecondsByKey"]);
|
|
1286
|
+
class IdAllocator {
|
|
1287
|
+
used;
|
|
1288
|
+
counts = new Map;
|
|
1289
|
+
constructor(existingIds) {
|
|
1290
|
+
this.used = new Set(existingIds);
|
|
1291
|
+
}
|
|
1292
|
+
next(kind) {
|
|
1293
|
+
let n = (this.counts.get(kind) ?? 0) + 1;
|
|
1294
|
+
let id = `${kind}-import-${n}`;
|
|
1295
|
+
while (this.used.has(id)) {
|
|
1296
|
+
n += 1;
|
|
1297
|
+
id = `${kind}-import-${n}`;
|
|
1298
|
+
}
|
|
1299
|
+
this.counts.set(kind, n);
|
|
1300
|
+
this.used.add(id);
|
|
1301
|
+
return id;
|
|
1302
|
+
}
|
|
1303
|
+
peek(kind) {
|
|
1304
|
+
let n = (this.counts.get(kind) ?? 0) + 1;
|
|
1305
|
+
let id = `${kind}-import-${n}`;
|
|
1306
|
+
while (this.used.has(id)) {
|
|
1307
|
+
n += 1;
|
|
1308
|
+
id = `${kind}-import-${n}`;
|
|
1309
|
+
}
|
|
1310
|
+
return id;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
// packages/environment/src/redact.ts
|
|
1314
|
+
var TOKEN_BODY_CHARS = "A-Za-z0-9_+/=-";
|
|
1315
|
+
var OPAQUE_TOKEN_RE = new RegExp(`(?=[${TOKEN_BODY_CHARS}]*[0-9])(?=[${TOKEN_BODY_CHARS}]*[A-Za-z])[${TOKEN_BODY_CHARS}]{20,}`, "g");
|
|
1316
|
+
var ABSOLUTE_PATH_RE = new RegExp(`(?<![${TOKEN_BODY_CHARS}])/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+`, "g");
|
|
1317
|
+
// packages/environment/src/declaration-path.ts
|
|
1318
|
+
var ENVIRONMENT_DECLARATION_PATH = "catalyst.env.json";
|
|
1319
|
+
var RULES = [
|
|
1320
|
+
{
|
|
1321
|
+
rule: "environment declaration under .catalyst/",
|
|
1322
|
+
re: /\.catalyst\/environment\.(?:json|toml|ya?ml)\b/
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
rule: "environment declaration under catalyst/",
|
|
1326
|
+
re: /(?:^|[\s(`"'])catalyst\/environment\.(?:json|toml|ya?ml)\b/
|
|
1327
|
+
}
|
|
1328
|
+
];
|
|
1329
|
+
var MISNAMED_DECLARATION_RULE_NAMES = RULES.map((r) => r.rule);
|
|
1330
|
+
// packages/environment/src/minimal-example.ts
|
|
1331
|
+
var MINIMAL_DECLARATION_EXAMPLE = {
|
|
1332
|
+
version: 1,
|
|
1333
|
+
toolchains: [{ name: "bun", version: "1.3.14", provenanceIds: ["human-1"] }],
|
|
1334
|
+
systemPackages: [],
|
|
1335
|
+
setup: [
|
|
1336
|
+
{
|
|
1337
|
+
id: "install",
|
|
1338
|
+
command: ["bun", "install"],
|
|
1339
|
+
timeoutSeconds: 900,
|
|
1340
|
+
provenanceIds: ["human-1"]
|
|
1341
|
+
}
|
|
1342
|
+
],
|
|
1343
|
+
verify: [
|
|
1344
|
+
{
|
|
1345
|
+
id: "check",
|
|
1346
|
+
command: ["bun", "run", "check"],
|
|
1347
|
+
timeoutSeconds: 3600,
|
|
1348
|
+
provenanceIds: ["human-1"]
|
|
1349
|
+
}
|
|
1350
|
+
],
|
|
1351
|
+
services: [],
|
|
1352
|
+
environment: [{ name: "DATABASE_URL", required: true, provenanceIds: ["human-1"] }],
|
|
1353
|
+
agentAssets: [],
|
|
1354
|
+
provenance: [
|
|
1355
|
+
{
|
|
1356
|
+
id: "human-1",
|
|
1357
|
+
kind: "human",
|
|
1358
|
+
path: ENVIRONMENT_DECLARATION_PATH,
|
|
1359
|
+
confidence: "high",
|
|
1360
|
+
explanation: "Written by hand."
|
|
1361
|
+
}
|
|
1362
|
+
]
|
|
1363
|
+
};
|
|
1364
|
+
var MINIMAL_DECLARATION_EXAMPLE_JSON = JSON.stringify(MINIMAL_DECLARATION_EXAMPLE, null, 2);
|
|
1365
|
+
// packages/environment/src/system-package-capture.ts
|
|
1366
|
+
var MAX_PACKAGE_INVENTORY_BYTES = 256 * 1024;
|
|
1367
|
+
// packages/environment/src/environment-probe.ts
|
|
1368
|
+
var BROWSER_PROGRAM = String.raw`import{spawnSync as s}from"node:child_process";import{mkdirSync as m,writeFileSync as w}from"node:fs";import{dirname as d}from"node:path";const p=process.argv[1],r=o=>{m(d(p),{recursive:true});w(p,JSON.stringify({version:1,probe:"browser",...o}))},f=x=>x.error?.code==="ENOENT"?"not_found":x.error?.code==="ETIMEDOUT"?"timeout":"spawn_failed";try{const v=s("chromium",["--version"],{encoding:"utf8",timeout:3e4});if(v.error)r({launched:false,reason:f(v)});else if(v.status!==0)r({launched:false,reason:"version_failed"});else{const h=s("chromium",["--headless=new","--dump-dom","about:blank"],{encoding:"utf8",timeout:6e4});r(h.error?{launched:false,reason:f(h)}:h.status===0&&h.stdout.includes("<html")?{launched:true,browserVersion:v.stdout.trim().slice(0,128)}:{launched:false,reason:"headless_failed"})}}catch{r({launched:false,reason:"probe_error"})}`;
|
|
1369
|
+
var MCP_PROGRAM = String.raw`import{spawn as S}from"node:child_process";import{mkdirSync as m,writeFileSync as w}from"node:fs";let k;const[,p,z]=process.argv,c=JSON.parse(z),r=o=>{k?.kill("SIGKILL");m(p.replace(/\/[^/]*$/,""),{recursive:!0});w(p,JSON.stringify({version:1,probe:"mcp",...o}));process.exit()},x=e=>r({status:"refused",reason:e}),v=t=>{for(const l of[t,...t.split("\n")]){let j;try{j=JSON.parse(l.replace(/^data:\s*/,""))}catch{continue}if(j?.jsonrpc==="2.0"&&j.id===1){const q=j.result;q?.protocolVersion&&q.capabilities&&q.serverInfo?r({status:"completed"}):x("initialize_error")}}};setTimeout(x,c.timeoutMs,"timeout");if(c.url){try{const h=await fetch(c.url,c.init);h.ok||x("http_status");v(await h.text());x("no_response")}catch{x("connect_failed")}}else{let b="";k=S(c.command,c.args,{env:{...process.env,...c.env},stdio:["pipe","pipe","ignore"]});k.on("error",_=>x("spawn_failed")).on("close",_=>x("exited"));k.stdin.on("error",_=>0);k.stdout.on("data",e=>{b+=e;v(b)});k.stdin.write(c.request+"\n")}`;
|
|
1370
|
+
var TOOLCHAIN_PROGRAM = String.raw`import{spawnSync as s}from"node:child_process";import{mkdirSync as m,writeFileSync as w}from"node:fs";import{dirname as d}from"node:path";const p=process.argv[1],r=[];for(const e of process.argv[2].split(",")){const[n,f]=e.split("|"),v=s(n,[f||"--version"],{encoding:"utf8",timeout:5e3}),t=((v.stdout||v.stderr||"").split("\n")[0]||"").trim().slice(0,128),o={name:n,runs:v.status===0};if(t)o.version=t;r.push(o)}m(d(p),{recursive:!0});w(p,JSON.stringify({version:1,probe:"toolchain",results:r}))`;
|
|
1371
|
+
var MCP_INITIALIZE_REQUEST = JSON.stringify({
|
|
1372
|
+
jsonrpc: "2.0",
|
|
1373
|
+
id: 1,
|
|
1374
|
+
method: "initialize",
|
|
1375
|
+
params: {
|
|
1376
|
+
protocolVersion: "2025-06-18",
|
|
1377
|
+
capabilities: {},
|
|
1378
|
+
clientInfo: { name: "catalyst-environment-probe", version: "1" }
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
var ENVIRONMENT_PROBE_PROGRAMS = Object.freeze({
|
|
1382
|
+
browser: BROWSER_PROGRAM,
|
|
1383
|
+
mcp: MCP_PROGRAM,
|
|
1384
|
+
toolchain: TOOLCHAIN_PROGRAM
|
|
1385
|
+
});
|
|
1386
|
+
// packages/environment/src/tool-prefix.ts
|
|
1387
|
+
var BAKED_RUST_HOMES = Object.freeze({
|
|
1388
|
+
cargoHome: "/opt/catalyst/toolchain/cargo",
|
|
1389
|
+
rustupHome: "/opt/catalyst/toolchain/rustup"
|
|
1390
|
+
});
|
|
1391
|
+
// packages/environment/src/env-file.ts
|
|
1392
|
+
var ENV_IMPORT_MAX_BYTES = 256 * 1024;
|
|
1393
|
+
var OPEN_KEYWORDS = new Set(["if", "for", "while", "until", "case", "function"]);
|
|
1394
|
+
var CLOSE_KEYWORDS = new Set(["fi", "done", "esac"]);
|
|
1395
|
+
var CONTROL_ONLY_KEYWORDS = new Set(["then", "else", "elif", "do", "test"]);
|
|
1396
|
+
var COMMAND_PREFIX_WORDS = new Set(["then", "else", "elif", "do", "!"]);
|
|
1397
|
+
var SECRET_EXACT_NAMES = new Set(["TOKEN", "KEY", "SECRET"]);
|
|
1398
|
+
// packages/settings/src/contract.ts
|
|
1399
|
+
class SettingsValidationError extends Error {
|
|
1400
|
+
path;
|
|
1401
|
+
reason;
|
|
1402
|
+
constructor(path, reason, message) {
|
|
1403
|
+
super(message);
|
|
1404
|
+
this.path = path;
|
|
1405
|
+
this.reason = reason;
|
|
1406
|
+
this.name = "SettingsValidationError";
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
var REVIEW_REQUEST_POLICIES = ["on-open", "every-push", "on-demand"];
|
|
1410
|
+
var PROJECT_WORKFLOW_SLOTS = [
|
|
1411
|
+
"dispatch",
|
|
1412
|
+
"intake",
|
|
1413
|
+
"research",
|
|
1414
|
+
"plan",
|
|
1415
|
+
"implement",
|
|
1416
|
+
"remediate",
|
|
1417
|
+
"verify",
|
|
1418
|
+
"review",
|
|
1419
|
+
"pr",
|
|
1420
|
+
"done",
|
|
1421
|
+
"canceled"
|
|
1422
|
+
];
|
|
1423
|
+
var PROJECT_ROUTING_STAGES = [
|
|
1424
|
+
"intake",
|
|
1425
|
+
"research",
|
|
1426
|
+
"plan",
|
|
1427
|
+
"implement",
|
|
1428
|
+
"validate",
|
|
1429
|
+
"pr",
|
|
1430
|
+
"remediate",
|
|
1431
|
+
"merge"
|
|
1432
|
+
];
|
|
1433
|
+
var PROJECT_WORKFLOW_MODES = ["adopted-recommended", "mapped-existing", "mixed"];
|
|
1434
|
+
var PROJECT_GIT_AUTOMATIONS = ["off", "managed"];
|
|
1435
|
+
var PROJECT_EXECUTION_MODES = [
|
|
1436
|
+
"local",
|
|
1437
|
+
"connected-host",
|
|
1438
|
+
"cluster",
|
|
1439
|
+
"cloud-native"
|
|
1440
|
+
];
|
|
1441
|
+
var PROJECT_ACTIVATION_POLICIES = ["disabled", "manual", "when-ready"];
|
|
1442
|
+
var PROJECT_TARGET_KINDS = ["host", "cluster", "cloud-region"];
|
|
1443
|
+
var PRINTABLE_RE2 = /^[\x21-\x7E ]+$/;
|
|
1444
|
+
function isSettingsRecord(value) {
|
|
1445
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1446
|
+
}
|
|
1447
|
+
function closedTable(value, allowedFields, path) {
|
|
1448
|
+
if (!isSettingsRecord(value)) {
|
|
1449
|
+
throw new SettingsValidationError(path, "invalid_shape", `${path} must be a table`);
|
|
1450
|
+
}
|
|
1451
|
+
for (const key of Object.keys(value)) {
|
|
1452
|
+
if (!allowedFields.has(key)) {
|
|
1453
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_shape", `unknown field "${path}.${key}"`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return value;
|
|
1457
|
+
}
|
|
1458
|
+
function tableArray(value, path, maximum) {
|
|
1459
|
+
if (!Array.isArray(value)) {
|
|
1460
|
+
throw new SettingsValidationError(path, "invalid_shape", `${path} must be an array of tables`);
|
|
1461
|
+
}
|
|
1462
|
+
if (value.length > maximum) {
|
|
1463
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must contain at most ${maximum} items`);
|
|
1464
|
+
}
|
|
1465
|
+
return value;
|
|
1466
|
+
}
|
|
1467
|
+
function stringAt(record, key, path) {
|
|
1468
|
+
const value = record[key];
|
|
1469
|
+
if (typeof value !== "string") {
|
|
1470
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_shape", `${path}.${key} must be a string`);
|
|
1471
|
+
}
|
|
1472
|
+
return value;
|
|
1473
|
+
}
|
|
1474
|
+
function boundedStringValue(value, path, maxLength, re, describe) {
|
|
1475
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || !re.test(value)) {
|
|
1476
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must be ${describe}`);
|
|
1477
|
+
}
|
|
1478
|
+
return value;
|
|
1479
|
+
}
|
|
1480
|
+
function integerBetween(value, path, min, max) {
|
|
1481
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
1482
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must be an integer between ${min} and ${max}`);
|
|
1483
|
+
}
|
|
1484
|
+
return value;
|
|
1485
|
+
}
|
|
1486
|
+
function booleanAt(record, key, path) {
|
|
1487
|
+
const value = record[key];
|
|
1488
|
+
if (typeof value !== "boolean") {
|
|
1489
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_shape", `${path}.${key} must be a boolean`);
|
|
1490
|
+
}
|
|
1491
|
+
return value;
|
|
1492
|
+
}
|
|
1493
|
+
function stringList(value, path, options) {
|
|
1494
|
+
if (!Array.isArray(value)) {
|
|
1495
|
+
throw new SettingsValidationError(path, "invalid_shape", `${path} must be an array of strings`);
|
|
1496
|
+
}
|
|
1497
|
+
if (value.length < options.minItems || value.length > options.maxItems) {
|
|
1498
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must contain between ${options.minItems} and ${options.maxItems} items`);
|
|
1499
|
+
}
|
|
1500
|
+
const seen = new Set;
|
|
1501
|
+
return value.map((entry, i) => {
|
|
1502
|
+
const item = boundedStringValue(entry, `${path}[${i}]`, options.maxLength, PRINTABLE_RE2, options.describe);
|
|
1503
|
+
if (options.unique) {
|
|
1504
|
+
if (seen.has(item)) {
|
|
1505
|
+
throw new SettingsValidationError(path, "duplicate_entry", `${path} contains duplicate entry ${JSON.stringify(item)}`);
|
|
1506
|
+
}
|
|
1507
|
+
seen.add(item);
|
|
1508
|
+
}
|
|
1509
|
+
return item;
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
function stringMapAt(record, key, path) {
|
|
1513
|
+
const value = record[key];
|
|
1514
|
+
if (value === undefined)
|
|
1515
|
+
return;
|
|
1516
|
+
if (!isSettingsRecord(value)) {
|
|
1517
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_shape", `${path}.${key} must be a table of strings`);
|
|
1518
|
+
}
|
|
1519
|
+
const out = {};
|
|
1520
|
+
for (const [mapKey, mapValue] of Object.entries(value)) {
|
|
1521
|
+
if (typeof mapValue !== "string") {
|
|
1522
|
+
throw new SettingsValidationError(`${path}.${key}.${mapKey}`, "invalid_shape", `${path}.${key}.${mapKey} must be a string`);
|
|
1523
|
+
}
|
|
1524
|
+
out[mapKey] = mapValue;
|
|
1525
|
+
}
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
function stringArrayAt(record, key, path, maximum) {
|
|
1529
|
+
const value = record[key];
|
|
1530
|
+
if (value === undefined)
|
|
1531
|
+
return;
|
|
1532
|
+
if (!Array.isArray(value)) {
|
|
1533
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_shape", `${path}.${key} must be an array of strings`);
|
|
1534
|
+
}
|
|
1535
|
+
if (value.length > maximum) {
|
|
1536
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_value", `${path}.${key} must contain at most ${maximum} items`);
|
|
1537
|
+
}
|
|
1538
|
+
return value.map((entry, i) => {
|
|
1539
|
+
if (typeof entry !== "string") {
|
|
1540
|
+
throw new SettingsValidationError(`${path}.${key}[${i}]`, "invalid_shape", `${path}.${key}[${i}] must be a string`);
|
|
1541
|
+
}
|
|
1542
|
+
return entry;
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// packages/settings/src/agents-section.ts
|
|
1547
|
+
var AGENTS_FIELDS = new Set(["harnesses", "credentials", "context"]);
|
|
1548
|
+
var CONTEXT_FIELDS = new Set(["instructions", "prompts", "hooks"]);
|
|
1549
|
+
var CODING_ACCOUNT_FIELDS = new Set(["source", "provider"]);
|
|
1550
|
+
var VARIABLE_FIELDS = new Set(["source", "variable", "provider"]);
|
|
1551
|
+
var HARNESS_VOCABULARY = new Set(AGENT_ASSET_HARNESSES);
|
|
1552
|
+
function harnessToProviderHarness(harness) {
|
|
1553
|
+
switch (harness) {
|
|
1554
|
+
case "claude":
|
|
1555
|
+
return "claude-cli";
|
|
1556
|
+
case "codex":
|
|
1557
|
+
return "codex";
|
|
1558
|
+
case "opencode":
|
|
1559
|
+
return "opencode";
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
var ENROLLABLE_HARNESSES = new Set(Object.values(RUNTIME_PROVIDERS).filter((spec) => spec.credentialProvider === undefined).map((spec) => spec.harness));
|
|
1563
|
+
var INSTRUCTION_FILES = new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
1564
|
+
var HOOKS_FILES = new Set([".claude/settings.json"]);
|
|
1565
|
+
var CONTEXT_PATH_MAX = 512;
|
|
1566
|
+
var CONTEXT_ENTRIES_MAX = 16;
|
|
1567
|
+
function isRelativeRepoPath(path) {
|
|
1568
|
+
if (path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path))
|
|
1569
|
+
return false;
|
|
1570
|
+
const segments = path.split("/");
|
|
1571
|
+
return !segments.includes("..") && !segments.includes(".");
|
|
1572
|
+
}
|
|
1573
|
+
function validateContextEntry(kind, path, at) {
|
|
1574
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1575
|
+
throw new SettingsValidationError(at, "invalid_shape", `${at} must be a string`);
|
|
1576
|
+
}
|
|
1577
|
+
if (path.length > CONTEXT_PATH_MAX) {
|
|
1578
|
+
throw new SettingsValidationError(at, "invalid_value", `${at} must be at most ${CONTEXT_PATH_MAX} characters`);
|
|
1579
|
+
}
|
|
1580
|
+
if (!isRelativeRepoPath(path)) {
|
|
1581
|
+
throw new SettingsValidationError(at, "context_path_not_relative", `${at} must be a repository-relative path without ".." segments`);
|
|
1582
|
+
}
|
|
1583
|
+
if (kind === "instructions" && !INSTRUCTION_FILES.has(path)) {
|
|
1584
|
+
throw new SettingsValidationError(at, "context_path_not_loadable", `${at} must be AGENTS.md or CLAUDE.md — the instruction files the harnesses read natively in v1`);
|
|
1585
|
+
}
|
|
1586
|
+
if (kind === "hooks" && !HOOKS_FILES.has(path)) {
|
|
1587
|
+
throw new SettingsValidationError(at, "context_path_not_loadable", `${at} must be .claude/settings.json — the only hooks location any harness has in v1`);
|
|
1588
|
+
}
|
|
1589
|
+
return path;
|
|
1590
|
+
}
|
|
1591
|
+
function validateContextPaths(value) {
|
|
1592
|
+
const raw = closedTable(value, CONTEXT_FIELDS, "agents.context");
|
|
1593
|
+
const context = {};
|
|
1594
|
+
for (const kind of CONTEXT_FIELDS) {
|
|
1595
|
+
const entries = raw[kind];
|
|
1596
|
+
if (entries === undefined)
|
|
1597
|
+
continue;
|
|
1598
|
+
if (!Array.isArray(entries)) {
|
|
1599
|
+
throw new SettingsValidationError(`agents.context.${kind}`, "invalid_shape", `agents.context.${kind} must be an array of paths`);
|
|
1600
|
+
}
|
|
1601
|
+
if (entries.length > CONTEXT_ENTRIES_MAX) {
|
|
1602
|
+
throw new SettingsValidationError(`agents.context.${kind}`, "invalid_value", `agents.context.${kind} must contain at most ${CONTEXT_ENTRIES_MAX} entries`);
|
|
1603
|
+
}
|
|
1604
|
+
const seen = new Set;
|
|
1605
|
+
context[kind] = entries.map((entry, i) => {
|
|
1606
|
+
const path = validateContextEntry(kind, entry, `agents.context.${kind}[${i}]`);
|
|
1607
|
+
if (seen.has(path)) {
|
|
1608
|
+
throw new SettingsValidationError(`agents.context.${kind}`, "duplicate_entry", `agents.context.${kind} contains duplicate entry ${JSON.stringify(path)}`);
|
|
1609
|
+
}
|
|
1610
|
+
seen.add(path);
|
|
1611
|
+
return path;
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
return context;
|
|
1615
|
+
}
|
|
1616
|
+
function validateCodingAccountCredential(raw, harness, at) {
|
|
1617
|
+
closedTable(raw, CODING_ACCOUNT_FIELDS, at);
|
|
1618
|
+
const provider = raw.provider;
|
|
1619
|
+
if (provider === undefined) {
|
|
1620
|
+
throw new SettingsValidationError(`${at}.source`, "invalid_shape", `${at}.source = "coding-account" requires a provider`);
|
|
1621
|
+
}
|
|
1622
|
+
if (typeof provider !== "string" || !isRuntimeProvider(provider)) {
|
|
1623
|
+
throw new SettingsValidationError(`${at}.provider`, "unknown_provider", `${at}.provider must be a runtime provider catalog key (${Object.keys(RUNTIME_PROVIDERS).map((key) => JSON.stringify(key)).join(", ")}), not a protocol name`);
|
|
1624
|
+
}
|
|
1625
|
+
const spec = RUNTIME_PROVIDERS[provider];
|
|
1626
|
+
if (spec.credentialProvider !== undefined) {
|
|
1627
|
+
throw new SettingsValidationError(`${at}.provider`, "provider_not_enrollable", `${at}.provider ${JSON.stringify(provider)} is a twin view of ${JSON.stringify(spec.credentialProvider)} and is never enrollable; name the enrollable provider instead`);
|
|
1628
|
+
}
|
|
1629
|
+
const expected = harnessToProviderHarness(harness);
|
|
1630
|
+
if (!ENROLLABLE_HARNESSES.has(expected)) {
|
|
1631
|
+
throw new SettingsValidationError(`${at}.provider`, "provider_not_enrollable", `no enrollable provider rides the ${expected} harness; ${at} must use source = "variable"`);
|
|
1632
|
+
}
|
|
1633
|
+
if (spec.harness !== expected) {
|
|
1634
|
+
throw new SettingsValidationError(`${at}.provider`, "provider_harness_mismatch", `${at}.provider ${JSON.stringify(provider)} rides the ${spec.harness} harness, not ${expected}`);
|
|
1635
|
+
}
|
|
1636
|
+
return { source: "coding-account", provider };
|
|
1637
|
+
}
|
|
1638
|
+
function validateVariableCredential(raw, harness, at, declaredVariables) {
|
|
1639
|
+
closedTable(raw, VARIABLE_FIELDS, at);
|
|
1640
|
+
const variable = raw.variable;
|
|
1641
|
+
if (typeof variable !== "string" || variable.length === 0) {
|
|
1642
|
+
throw new SettingsValidationError(`${at}.variable`, "invalid_shape", `${at}.source = "variable" requires a variable name`);
|
|
1643
|
+
}
|
|
1644
|
+
const declared = declaredVariables.find((entry) => entry.name === variable);
|
|
1645
|
+
if (declared === undefined) {
|
|
1646
|
+
throw new SettingsValidationError(`${at}.variable`, "undeclared_variable_reference", `${at}.variable names ${JSON.stringify(variable)}, which is not declared in [[environment.variables]]`);
|
|
1647
|
+
}
|
|
1648
|
+
if (declared.secret !== true) {
|
|
1649
|
+
throw new SettingsValidationError(`${at}.variable`, "credential_variable_not_secret", `${at}.variable names ${JSON.stringify(variable)}, which is declared without secret = true — a credential reference must name a secret variable`);
|
|
1650
|
+
}
|
|
1651
|
+
const provider = raw.provider;
|
|
1652
|
+
if (provider === undefined) {
|
|
1653
|
+
return { source: "variable", variable };
|
|
1654
|
+
}
|
|
1655
|
+
if (typeof provider !== "string" || !isRuntimeProvider(provider)) {
|
|
1656
|
+
throw new SettingsValidationError(`${at}.provider`, "unknown_provider", `${at}.provider must be a runtime provider catalog key (${Object.keys(RUNTIME_PROVIDERS).map((key) => JSON.stringify(key)).join(", ")}), not a protocol name`);
|
|
1657
|
+
}
|
|
1658
|
+
const spec = RUNTIME_PROVIDERS[provider];
|
|
1659
|
+
const expected = harnessToProviderHarness(harness);
|
|
1660
|
+
if (spec.harness !== expected) {
|
|
1661
|
+
throw new SettingsValidationError(`${at}.provider`, "provider_harness_mismatch", `${at}.provider ${JSON.stringify(provider)} rides the ${spec.harness} harness, not ${expected}`);
|
|
1662
|
+
}
|
|
1663
|
+
return { source: "variable", variable, provider };
|
|
1664
|
+
}
|
|
1665
|
+
function agentsSectionFromToml(value, declaredVariables) {
|
|
1666
|
+
const raw = closedTable(value, AGENTS_FIELDS, "agents");
|
|
1667
|
+
const rawHarnesses = raw.harnesses;
|
|
1668
|
+
if (!Array.isArray(rawHarnesses) || rawHarnesses.length === 0) {
|
|
1669
|
+
throw new SettingsValidationError("agents.harnesses", "invalid_shape", "agents.harnesses must be a non-empty array of harness names");
|
|
1670
|
+
}
|
|
1671
|
+
const seenHarnesses = new Set;
|
|
1672
|
+
const harnesses = rawHarnesses.map((entry, i) => {
|
|
1673
|
+
if (typeof entry !== "string" || !HARNESS_VOCABULARY.has(entry)) {
|
|
1674
|
+
throw new SettingsValidationError(`agents.harnesses[${i}]`, "unknown_harness", `agents.harnesses[${i}] must be one of ${AGENT_ASSET_HARNESSES.map((h) => JSON.stringify(h)).join(", ")}`);
|
|
1675
|
+
}
|
|
1676
|
+
if (seenHarnesses.has(entry)) {
|
|
1677
|
+
throw new SettingsValidationError("agents.harnesses", "duplicate_entry", `agents.harnesses contains duplicate entry ${JSON.stringify(entry)}`);
|
|
1678
|
+
}
|
|
1679
|
+
seenHarnesses.add(entry);
|
|
1680
|
+
return entry;
|
|
1681
|
+
});
|
|
1682
|
+
const section = { harnesses };
|
|
1683
|
+
if (raw.credentials !== undefined) {
|
|
1684
|
+
const credentialsTable = raw.credentials;
|
|
1685
|
+
if (!isSettingsRecord(credentialsTable)) {
|
|
1686
|
+
throw new SettingsValidationError("agents.credentials", "invalid_shape", "agents.credentials must be a table keyed by harness name");
|
|
1687
|
+
}
|
|
1688
|
+
const credentials = {};
|
|
1689
|
+
for (const [harness, entry] of Object.entries(credentialsTable)) {
|
|
1690
|
+
const at = `agents.credentials.${harness}`;
|
|
1691
|
+
if (!HARNESS_VOCABULARY.has(harness)) {
|
|
1692
|
+
throw new SettingsValidationError(at, "unknown_harness", `${at} must be one of ${AGENT_ASSET_HARNESSES.map((h) => JSON.stringify(h)).join(", ")}`);
|
|
1693
|
+
}
|
|
1694
|
+
if (!seenHarnesses.has(harness)) {
|
|
1695
|
+
throw new SettingsValidationError(at, "harness_not_allowed", `${at} names a harness outside agents.harnesses`);
|
|
1696
|
+
}
|
|
1697
|
+
if (!isSettingsRecord(entry)) {
|
|
1698
|
+
throw new SettingsValidationError(at, "invalid_shape", `${at} must be a table`);
|
|
1699
|
+
}
|
|
1700
|
+
if (entry.source === "coding-account") {
|
|
1701
|
+
credentials[harness] = validateCodingAccountCredential(entry, harness, at);
|
|
1702
|
+
} else if (entry.source === "variable") {
|
|
1703
|
+
credentials[harness] = validateVariableCredential(entry, harness, at, declaredVariables);
|
|
1704
|
+
} else {
|
|
1705
|
+
throw new SettingsValidationError(`${at}.source`, "invalid_value", `${at}.source must be "coding-account" or "variable"`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
section.credentials = credentials;
|
|
1709
|
+
}
|
|
1710
|
+
if (raw.context !== undefined) {
|
|
1711
|
+
section.context = validateContextPaths(raw.context);
|
|
1712
|
+
}
|
|
1713
|
+
return section;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// packages/settings/src/environment-section.ts
|
|
1717
|
+
var ENVIRONMENT_SECTION_FIELDS = new Set([
|
|
1718
|
+
"toolchains",
|
|
1719
|
+
"system_packages",
|
|
1720
|
+
"setup",
|
|
1721
|
+
"verify",
|
|
1722
|
+
"services",
|
|
1723
|
+
"variables",
|
|
1724
|
+
"mcp_servers",
|
|
1725
|
+
"skills",
|
|
1726
|
+
"clis",
|
|
1727
|
+
"start",
|
|
1728
|
+
"container",
|
|
1729
|
+
"files"
|
|
1730
|
+
]);
|
|
1731
|
+
var TOOLCHAIN_FIELDS2 = new Set(["name", "version"]);
|
|
1732
|
+
var SYSTEM_PACKAGE_FIELDS2 = new Set(["name", "version"]);
|
|
1733
|
+
var STEP_FIELDS = new Set(["name", "run", "cwd", "timeout_seconds"]);
|
|
1734
|
+
var CWD_PARENT_SEGMENT_RE = /(?:^|\/)\.\.(?:\/|$)/;
|
|
1735
|
+
var SETUP_STEP_FIELDS = new Set([...STEP_FIELDS, RERUN_WHEN_CHANGED_KEY]);
|
|
1736
|
+
var SERVICE_FIELDS2 = new Set(["name", "image", "readiness"]);
|
|
1737
|
+
var SERVICE_READINESS_COMMAND_FIELDS2 = new Set(["kind", "run", "timeout_seconds"]);
|
|
1738
|
+
var SERVICE_READINESS_PORT_FIELDS2 = new Set(["kind", "port"]);
|
|
1739
|
+
var VARIABLE_FIELDS2 = new Set(["name", "required", "secret"]);
|
|
1740
|
+
var MCP_FIELDS = new Set([
|
|
1741
|
+
"name",
|
|
1742
|
+
"transport",
|
|
1743
|
+
"url",
|
|
1744
|
+
"headers",
|
|
1745
|
+
"command",
|
|
1746
|
+
"args",
|
|
1747
|
+
"env",
|
|
1748
|
+
"required_env_names",
|
|
1749
|
+
"harnesses"
|
|
1750
|
+
]);
|
|
1751
|
+
var MCP_HTTP_FIELDS2 = new Set([
|
|
1752
|
+
"name",
|
|
1753
|
+
"transport",
|
|
1754
|
+
"url",
|
|
1755
|
+
"headers",
|
|
1756
|
+
"required_env_names",
|
|
1757
|
+
"harnesses"
|
|
1758
|
+
]);
|
|
1759
|
+
var MCP_STDIO_FIELDS2 = new Set([
|
|
1760
|
+
"name",
|
|
1761
|
+
"transport",
|
|
1762
|
+
"command",
|
|
1763
|
+
"args",
|
|
1764
|
+
"env",
|
|
1765
|
+
"required_env_names",
|
|
1766
|
+
"harnesses"
|
|
1767
|
+
]);
|
|
1768
|
+
var SKILL_FIELDS2 = new Set(["name", "tier", "reference", "digest", "required_env_names"]);
|
|
1769
|
+
var CLI_FIELDS2 = new Set(["name", "install"]);
|
|
1770
|
+
var CLI_IMAGE_FIELDS2 = new Set(["source"]);
|
|
1771
|
+
var CLI_NPM_FIELDS2 = new Set(["source", "package", "version"]);
|
|
1772
|
+
var STEP_ID_RE2 = /^[a-z][a-z0-9-]{0,63}$/;
|
|
1773
|
+
var IMAGE_RE2 = /^[\x21-\x7E]{1,256}$/;
|
|
1774
|
+
var RERUN_PATH_RE = /^[\x21-\x2E\x30-\x7E][\x21-\x7E]{0,255}$/;
|
|
1775
|
+
var PARENT_SEGMENT_RE = /(?:^|\/)\.\.(?:\/|$)/;
|
|
1776
|
+
var MAX_RERUN_ENTRIES2 = 64;
|
|
1777
|
+
var MAX_RERUN_ENTRY_LENGTH2 = 256;
|
|
1778
|
+
var DEFAULT_TIMEOUT_SECONDS = 3600;
|
|
1779
|
+
var MIN_TIMEOUT_SECONDS2 = 1;
|
|
1780
|
+
var MAX_TIMEOUT_SECONDS2 = 3600;
|
|
1781
|
+
var MAX_ARGV = 64;
|
|
1782
|
+
var MAX_ARGV_ITEM = 1024;
|
|
1783
|
+
var ARG_RE2 = /^[\x20-\x7E]{1,1024}$/;
|
|
1784
|
+
function contractFailure(err) {
|
|
1785
|
+
const body = err.message.replace(/^invalid repo environment declaration: /, "");
|
|
1786
|
+
const token = body.split(/\s+/, 1)[0] ?? "";
|
|
1787
|
+
const reason = err.reason === "literal_secret" || /secret/.test(body) ? "literal_secret" : /duplicate/.test(body) ? "duplicate_entry" : "invalid_value";
|
|
1788
|
+
if (/[.[]/.test(token)) {
|
|
1789
|
+
return new SettingsValidationError(token, reason, body);
|
|
1790
|
+
}
|
|
1791
|
+
const prefixed = `environment.${body}`;
|
|
1792
|
+
return new SettingsValidationError(token.length > 0 ? `environment.${token}` : "environment", reason, prefixed);
|
|
1793
|
+
}
|
|
1794
|
+
function timeoutAt(value, path) {
|
|
1795
|
+
return integerBetween(value, path, MIN_TIMEOUT_SECONDS2, MAX_TIMEOUT_SECONDS2);
|
|
1796
|
+
}
|
|
1797
|
+
function contractValidated(run) {
|
|
1798
|
+
try {
|
|
1799
|
+
return run();
|
|
1800
|
+
} catch (err) {
|
|
1801
|
+
if (err instanceof EnvironmentContractError)
|
|
1802
|
+
throw contractFailure(err);
|
|
1803
|
+
throw err;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function optionalTableArray(value, path, maximum) {
|
|
1807
|
+
if (value === undefined)
|
|
1808
|
+
return [];
|
|
1809
|
+
return tableArray(value, path, maximum);
|
|
1810
|
+
}
|
|
1811
|
+
function assertNoDuplicateIdentities(entries, identityOf, path) {
|
|
1812
|
+
const seen = new Set;
|
|
1813
|
+
for (const entry of entries) {
|
|
1814
|
+
const identity = identityOf(entry);
|
|
1815
|
+
if (seen.has(identity)) {
|
|
1816
|
+
throw new SettingsValidationError(path, "duplicate_entry", `${path} contains duplicate name ${JSON.stringify(identity)}`);
|
|
1817
|
+
}
|
|
1818
|
+
seen.add(identity);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function validateArgv(value, path) {
|
|
1822
|
+
if (!Array.isArray(value)) {
|
|
1823
|
+
throw new SettingsValidationError(path, "invalid_shape", `${path} must be an array of strings`);
|
|
1824
|
+
}
|
|
1825
|
+
if (value.length === 0) {
|
|
1826
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must contain at least one argument`);
|
|
1827
|
+
}
|
|
1828
|
+
if (value.length > MAX_ARGV) {
|
|
1829
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must contain at most ${MAX_ARGV} items`);
|
|
1830
|
+
}
|
|
1831
|
+
return value.map((entry, i) => boundedStringValue(entry, `${path}[${i}]`, MAX_ARGV_ITEM, ARG_RE2, "a printable argument of at most 1024 characters"));
|
|
1832
|
+
}
|
|
1833
|
+
function inferTransport(raw, path) {
|
|
1834
|
+
const hasUrl = raw.url !== undefined;
|
|
1835
|
+
const hasCommand = raw.command !== undefined;
|
|
1836
|
+
if (hasUrl && hasCommand) {
|
|
1837
|
+
throw new SettingsValidationError(`${path}.transport`, "invalid_shape", `${path}.transport is ambiguous: both url and command are present`);
|
|
1838
|
+
}
|
|
1839
|
+
if (!hasUrl && !hasCommand) {
|
|
1840
|
+
throw new SettingsValidationError(`${path}.transport`, "invalid_shape", `${path}.transport cannot be inferred: set either url (http) or command (stdio)`);
|
|
1841
|
+
}
|
|
1842
|
+
const inferred = hasUrl ? "http" : "stdio";
|
|
1843
|
+
if (raw.transport !== undefined && raw.transport !== inferred) {
|
|
1844
|
+
throw new SettingsValidationError(`${path}.transport`, "invalid_shape", `${path}.transport is ${JSON.stringify(raw.transport)} but the fields imply ${JSON.stringify(inferred)}`);
|
|
1845
|
+
}
|
|
1846
|
+
return inferred;
|
|
1847
|
+
}
|
|
1848
|
+
function environmentStepsFromToml(value, arrayPath, opts) {
|
|
1849
|
+
const allowRerun = opts?.allowRerunWhenChanged === true;
|
|
1850
|
+
const fields = allowRerun ? SETUP_STEP_FIELDS : STEP_FIELDS;
|
|
1851
|
+
return optionalTableArray(value, arrayPath, 64).map((entry, i) => {
|
|
1852
|
+
const path = `${arrayPath}[${i}]`;
|
|
1853
|
+
const table = closedTable(entry, fields, path);
|
|
1854
|
+
const id = boundedStringValue(stringAt(table, "name", path), `${path}.name`, 64, STEP_ID_RE2, "a lowercase kebab-case identifier of at most 64 characters");
|
|
1855
|
+
const command = validateArgv(table.run, `${path}.run`);
|
|
1856
|
+
const cwd = table.cwd === undefined ? undefined : boundedStringValue(table.cwd, `${path}.cwd`, 512, /^[\x21-\x7E ]+$/, "a printable path of at most 512 characters");
|
|
1857
|
+
if (cwd !== undefined && CWD_PARENT_SEGMENT_RE.test(cwd)) {
|
|
1858
|
+
throw new SettingsValidationError(`${path}.cwd`, "invalid_value", `${path}.cwd must stay inside the repository (no ".." segment)`);
|
|
1859
|
+
}
|
|
1860
|
+
const timeoutSeconds = table.timeout_seconds === undefined ? DEFAULT_TIMEOUT_SECONDS : timeoutAt(table.timeout_seconds, `${path}.timeout_seconds`);
|
|
1861
|
+
const rawRerun = table[RERUN_WHEN_CHANGED_KEY];
|
|
1862
|
+
const rerunWhenChanged = rawRerun === undefined ? undefined : stringList(rawRerun, `${path}.${RERUN_WHEN_CHANGED_KEY}`, {
|
|
1863
|
+
minItems: 1,
|
|
1864
|
+
maxItems: MAX_RERUN_ENTRIES2,
|
|
1865
|
+
minLength: 1,
|
|
1866
|
+
maxLength: MAX_RERUN_ENTRY_LENGTH2,
|
|
1867
|
+
unique: true,
|
|
1868
|
+
describe: `a repository-relative path or glob of 1-${MAX_RERUN_ENTRY_LENGTH2} printable characters`
|
|
1869
|
+
}).map((glob, j) => {
|
|
1870
|
+
const posix = glob.replace(/\\/g, "/");
|
|
1871
|
+
if (!RERUN_PATH_RE.test(posix) || PARENT_SEGMENT_RE.test(posix)) {
|
|
1872
|
+
throw new SettingsValidationError(`${path}.${RERUN_WHEN_CHANGED_KEY}[${j}]`, "invalid_value", `${path}.${RERUN_WHEN_CHANGED_KEY}[${j}] must be repository-relative (no leading "/" and no ".." segment)`);
|
|
1873
|
+
}
|
|
1874
|
+
return glob;
|
|
1875
|
+
});
|
|
1876
|
+
return {
|
|
1877
|
+
id,
|
|
1878
|
+
command,
|
|
1879
|
+
...cwd !== undefined ? { cwd } : {},
|
|
1880
|
+
timeoutSeconds,
|
|
1881
|
+
...rerunWhenChanged !== undefined ? { rerunWhenChanged } : {},
|
|
1882
|
+
provenanceIds: []
|
|
1883
|
+
};
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
function declarationFromEnvironmentSection(section) {
|
|
1887
|
+
const raw = closedTable(section, ENVIRONMENT_SECTION_FIELDS, "environment");
|
|
1888
|
+
const toolchains = optionalTableArray(raw.toolchains, "environment.toolchains", 32).map((entry, i) => {
|
|
1889
|
+
const path = `environment.toolchains[${i}]`;
|
|
1890
|
+
const table = closedTable(entry, TOOLCHAIN_FIELDS2, path);
|
|
1891
|
+
return {
|
|
1892
|
+
name: boundedStringValue(stringAt(table, "name", path), `${path}.name`, 64, DECLARATION_NAME_RE, "a lowercase identifier of at most 64 characters"),
|
|
1893
|
+
version: boundedStringValue(stringAt(table, "version", path), `${path}.version`, 64, DECLARATION_VERSION_RE, "a version string of at most 64 characters"),
|
|
1894
|
+
provenanceIds: []
|
|
1895
|
+
};
|
|
1896
|
+
});
|
|
1897
|
+
const systemPackages = optionalTableArray(raw.system_packages, "environment.system_packages", 128).map((entry, i) => {
|
|
1898
|
+
const path = `environment.system_packages[${i}]`;
|
|
1899
|
+
const table = closedTable(entry, SYSTEM_PACKAGE_FIELDS2, path);
|
|
1900
|
+
const name = boundedStringValue(stringAt(table, "name", path), `${path}.name`, 64, DECLARATION_NAME_RE, "a lowercase identifier of at most 64 characters");
|
|
1901
|
+
const version = table.version === undefined ? undefined : boundedStringValue(table.version, `${path}.version`, 64, DECLARATION_VERSION_RE, "a version string of at most 64 characters");
|
|
1902
|
+
return { name, ...version !== undefined ? { version } : {}, provenanceIds: [] };
|
|
1903
|
+
});
|
|
1904
|
+
const setup = environmentStepsFromToml(raw.setup, "environment.setup", {
|
|
1905
|
+
allowRerunWhenChanged: true
|
|
1906
|
+
});
|
|
1907
|
+
const verify = environmentStepsFromToml(raw.verify, "environment.verify");
|
|
1908
|
+
const services = optionalTableArray(raw.services, "environment.services", 16).map((entry, i) => {
|
|
1909
|
+
const path = `environment.services[${i}]`;
|
|
1910
|
+
const table = closedTable(entry, SERVICE_FIELDS2, path);
|
|
1911
|
+
const name = boundedStringValue(stringAt(table, "name", path), `${path}.name`, 64, DECLARATION_NAME_RE, "a lowercase identifier of at most 64 characters");
|
|
1912
|
+
const image = table.image === undefined ? undefined : boundedStringValue(table.image, `${path}.image`, 256, IMAGE_RE2, "a printable image reference of at most 256 characters");
|
|
1913
|
+
const readiness = table.readiness;
|
|
1914
|
+
if (!isSettingsRecord(readiness)) {
|
|
1915
|
+
throw new SettingsValidationError(`${path}.readiness`, "invalid_shape", `${path}.readiness must be a table`);
|
|
1916
|
+
}
|
|
1917
|
+
if (readiness.kind === "command") {
|
|
1918
|
+
closedTable(readiness, SERVICE_READINESS_COMMAND_FIELDS2, `${path}.readiness`);
|
|
1919
|
+
const command = validateArgv(readiness.run, `${path}.readiness.run`);
|
|
1920
|
+
const timeoutSeconds = readiness.timeout_seconds === undefined ? DEFAULT_TIMEOUT_SECONDS : timeoutAt(readiness.timeout_seconds, `${path}.readiness.timeout_seconds`);
|
|
1921
|
+
return {
|
|
1922
|
+
name,
|
|
1923
|
+
...image !== undefined ? { image } : {},
|
|
1924
|
+
readiness: { kind: "command", command, timeoutSeconds },
|
|
1925
|
+
provenanceIds: []
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
if (readiness.kind === "port") {
|
|
1929
|
+
closedTable(readiness, SERVICE_READINESS_PORT_FIELDS2, `${path}.readiness`);
|
|
1930
|
+
const port = integerBetween(readiness.port, `${path}.readiness.port`, 1, 65535);
|
|
1931
|
+
return {
|
|
1932
|
+
name,
|
|
1933
|
+
...image !== undefined ? { image } : {},
|
|
1934
|
+
readiness: { kind: "port", port },
|
|
1935
|
+
provenanceIds: []
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
throw new SettingsValidationError(`${path}.readiness.kind`, "invalid_value", `${path}.readiness.kind must be one of "command", "port"`);
|
|
1939
|
+
});
|
|
1940
|
+
const variables = optionalTableArray(raw.variables, "environment.variables", 128).map((entry, i) => {
|
|
1941
|
+
const path = `environment.variables[${i}]`;
|
|
1942
|
+
const table = closedTable(entry, VARIABLE_FIELDS2, path);
|
|
1943
|
+
if (table.name === undefined) {
|
|
1944
|
+
throw new SettingsValidationError(`${path}.name`, "invalid_shape", `${path}.name must be a string`);
|
|
1945
|
+
}
|
|
1946
|
+
const required = table.required === undefined ? false : booleanAt(table, "required", path);
|
|
1947
|
+
const secret = table.secret === undefined ? false : booleanAt(table, "secret", path);
|
|
1948
|
+
return contractValidated(() => validateEnvironmentNameRef({
|
|
1949
|
+
name: table.name,
|
|
1950
|
+
required,
|
|
1951
|
+
...table.secret !== undefined ? { secret } : {},
|
|
1952
|
+
provenanceIds: []
|
|
1953
|
+
}, path, new Set));
|
|
1954
|
+
});
|
|
1955
|
+
const mcpServers = optionalTableArray(raw.mcp_servers, "environment.mcp_servers", 64).map((entry, i) => {
|
|
1956
|
+
const path = `environment.mcp_servers[${i}]`;
|
|
1957
|
+
const table = closedTable(entry, MCP_FIELDS, path);
|
|
1958
|
+
const transport = inferTransport(table, path);
|
|
1959
|
+
closedTable(table, transport === "http" ? MCP_HTTP_FIELDS2 : MCP_STDIO_FIELDS2, path);
|
|
1960
|
+
const id = stringAt(table, "name", path);
|
|
1961
|
+
const url = optionalString(table, "url", path);
|
|
1962
|
+
const headers = stringMapAt(table, "headers", path);
|
|
1963
|
+
const command = optionalString(table, "command", path);
|
|
1964
|
+
const args = stringArrayAt(table, "args", path, MAX_ARGV);
|
|
1965
|
+
const env = stringMapAt(table, "env", path);
|
|
1966
|
+
const requiredEnvNames = stringArrayAt(table, "required_env_names", path, 32) ?? [];
|
|
1967
|
+
const harnesses = stringArrayAt(table, "harnesses", path, 3);
|
|
1968
|
+
const built = {
|
|
1969
|
+
kind: "mcp-server",
|
|
1970
|
+
id,
|
|
1971
|
+
transport,
|
|
1972
|
+
...url !== undefined ? { url } : {},
|
|
1973
|
+
...headers !== undefined ? { headers } : {},
|
|
1974
|
+
...command !== undefined ? { command } : {},
|
|
1975
|
+
...args !== undefined ? { args } : {},
|
|
1976
|
+
...env !== undefined ? { env } : {},
|
|
1977
|
+
requiredEnvNames,
|
|
1978
|
+
...harnesses !== undefined ? { harnesses } : {},
|
|
1979
|
+
provenanceIds: []
|
|
1980
|
+
};
|
|
1981
|
+
return contractValidated(() => validateAgentAssetEntry(built, path, new Set));
|
|
1982
|
+
});
|
|
1983
|
+
const skills = optionalTableArray(raw.skills, "environment.skills", 64).map((entry, i) => {
|
|
1984
|
+
const path = `environment.skills[${i}]`;
|
|
1985
|
+
const table = closedTable(entry, SKILL_FIELDS2, path);
|
|
1986
|
+
if (table.tier === "platform") {
|
|
1987
|
+
throw new SettingsValidationError(`${path}.tier`, "platform_managed", "Catalyst ships platform skills with every repository; remove this entry");
|
|
1988
|
+
}
|
|
1989
|
+
const built = {
|
|
1990
|
+
kind: "skill",
|
|
1991
|
+
id: stringAt(table, "name", path),
|
|
1992
|
+
tier: table.tier,
|
|
1993
|
+
reference: table.reference,
|
|
1994
|
+
...table.digest !== undefined ? { digest: table.digest } : {},
|
|
1995
|
+
requiredEnvNames: stringArrayAt(table, "required_env_names", path, 32) ?? [],
|
|
1996
|
+
provenanceIds: []
|
|
1997
|
+
};
|
|
1998
|
+
return contractValidated(() => validateAgentAssetEntry(built, path, new Set));
|
|
1999
|
+
});
|
|
2000
|
+
const clis = optionalTableArray(raw.clis, "environment.clis", 64).map((entry, i) => {
|
|
2001
|
+
const path = `environment.clis[${i}]`;
|
|
2002
|
+
const table = closedTable(entry, CLI_FIELDS2, path);
|
|
2003
|
+
const install = table.install;
|
|
2004
|
+
if (!isSettingsRecord(install)) {
|
|
2005
|
+
throw new SettingsValidationError(`${path}.install`, "invalid_shape", `${path}.install must be a table`);
|
|
2006
|
+
}
|
|
2007
|
+
if (install.source === "image") {
|
|
2008
|
+
closedTable(install, CLI_IMAGE_FIELDS2, `${path}.install`);
|
|
2009
|
+
} else if (install.source === "npm") {
|
|
2010
|
+
closedTable(install, CLI_NPM_FIELDS2, `${path}.install`);
|
|
2011
|
+
}
|
|
2012
|
+
const built = {
|
|
2013
|
+
kind: "cli",
|
|
2014
|
+
id: stringAt(table, "name", path),
|
|
2015
|
+
install,
|
|
2016
|
+
provenanceIds: []
|
|
2017
|
+
};
|
|
2018
|
+
return contractValidated(() => validateAgentAssetEntry(built, path, new Set));
|
|
2019
|
+
});
|
|
2020
|
+
assertNoDuplicateIdentities(toolchains, (t) => t.name, "environment.toolchains");
|
|
2021
|
+
assertNoDuplicateIdentities(systemPackages, (p) => p.name, "environment.system_packages");
|
|
2022
|
+
assertNoDuplicateIdentities(variables, (v) => v.name, "environment.variables");
|
|
2023
|
+
assertNoDuplicateIdentities(mcpServers, (a) => a.id, "environment.mcp_servers");
|
|
2024
|
+
assertNoDuplicateIdentities(skills, (a) => a.id, "environment.skills");
|
|
2025
|
+
assertNoDuplicateIdentities(clis, (a) => a.id, "environment.clis");
|
|
2026
|
+
const declaration = {
|
|
2027
|
+
version: 1,
|
|
2028
|
+
toolchains,
|
|
2029
|
+
systemPackages,
|
|
2030
|
+
setup,
|
|
2031
|
+
verify,
|
|
2032
|
+
services,
|
|
2033
|
+
environment: variables,
|
|
2034
|
+
agentAssets: [...mcpServers, ...skills, ...clis],
|
|
2035
|
+
provenance: []
|
|
2036
|
+
};
|
|
2037
|
+
try {
|
|
2038
|
+
return validateRepoEnvironmentDeclaration(declaration);
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
if (err instanceof EnvironmentContractError)
|
|
2041
|
+
throw contractFailure(err);
|
|
2042
|
+
throw err;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function optionalString(record, key, path) {
|
|
2046
|
+
if (record[key] === undefined)
|
|
2047
|
+
return;
|
|
2048
|
+
return stringAt(record, key, path);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// packages/protocol/src/merge-evidence.ts
|
|
2052
|
+
var CLEAN_PASS_LINE_RE = new RegExp("^(?:codex review:\\s*)?(?:" + "no major issues(?: found)?" + "|(?:i )?(?:did|do|could|have|ca)\\s?n't (?:find|found|see|seen|spot|spotted) any major issues" + "|(?:i )?(?:found|find|see|saw|spotted) no major issues" + ")$");
|
|
2053
|
+
var CODEX_SIGN_OFFS = new Set([
|
|
2054
|
+
"keep it up",
|
|
2055
|
+
"another round soon, please",
|
|
2056
|
+
"more of your lovely prs please",
|
|
2057
|
+
"keep them coming",
|
|
2058
|
+
"chef's kiss",
|
|
2059
|
+
"nice work",
|
|
2060
|
+
"breezy",
|
|
2061
|
+
":rocket:",
|
|
2062
|
+
"you're on a roll",
|
|
2063
|
+
"what shall we delve into next?",
|
|
2064
|
+
"bravo",
|
|
2065
|
+
"hooray",
|
|
2066
|
+
":tada:",
|
|
2067
|
+
"can't wait for the next one",
|
|
2068
|
+
":+1:",
|
|
2069
|
+
"delightful",
|
|
2070
|
+
"already looking forward to the next diff",
|
|
2071
|
+
"swish"
|
|
2072
|
+
]);
|
|
2073
|
+
var SECURITY_KEYWORD_RE = new RegExp([
|
|
2074
|
+
"\\bauth(?:n|z|entication|enticat\\w*|ori[sz]ation|ori[sz]e[sd]?|ori[sz]ing)?\\b",
|
|
2075
|
+
"\\bsecrets?\\b",
|
|
2076
|
+
"\\btokens?\\b",
|
|
2077
|
+
"\\bcredentials?\\b",
|
|
2078
|
+
"\\binjection\\b",
|
|
2079
|
+
"\\bsql\\b",
|
|
2080
|
+
"\\bxss\\b",
|
|
2081
|
+
"\\bssrf\\b",
|
|
2082
|
+
"\\bdata[ -]exposure\\b",
|
|
2083
|
+
"\\btenant[ -]isolation\\b",
|
|
2084
|
+
"\\bcross[ -]tenant\\b"
|
|
2085
|
+
].join("|"), "i");
|
|
2086
|
+
var MERGE_EVIDENCE_POLICIES = [
|
|
2087
|
+
"codex-attestation",
|
|
2088
|
+
"codex-attestation-strict",
|
|
2089
|
+
"checks-and-threads"
|
|
2090
|
+
];
|
|
2091
|
+
var REVIEW_EVIDENCE_LEGS = new Set([
|
|
2092
|
+
"codex",
|
|
2093
|
+
"thread-ancestry"
|
|
2094
|
+
]);
|
|
2095
|
+
var QUEUE_CONDITION_KEYS = new Set(["queue_conditions", "merge_conditions"]);
|
|
2096
|
+
|
|
2097
|
+
// packages/settings/src/variable-refs.ts
|
|
2098
|
+
var VARIABLE_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
2099
|
+
var REFERENCE_EXPANDED_MAP_FIELDS = ["headers", "env"];
|
|
2100
|
+
function walkStrings(value, path, visit) {
|
|
2101
|
+
if (typeof value === "string") {
|
|
2102
|
+
visit(value, path);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (Array.isArray(value)) {
|
|
2106
|
+
value.forEach((entry, i) => walkStrings(entry, `${path}[${i}]`, visit));
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
if (isSettingsRecord(value)) {
|
|
2110
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2111
|
+
walkStrings(entry, path === "" ? key : `${path}.${key}`, visit);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function declaredVariableNames(parsed) {
|
|
2116
|
+
const names = new Set;
|
|
2117
|
+
if (!isSettingsRecord(parsed))
|
|
2118
|
+
return names;
|
|
2119
|
+
const environment = parsed.environment;
|
|
2120
|
+
if (!isSettingsRecord(environment) || !Array.isArray(environment.variables))
|
|
2121
|
+
return names;
|
|
2122
|
+
for (const entry of environment.variables) {
|
|
2123
|
+
if (isSettingsRecord(entry) && typeof entry.name === "string")
|
|
2124
|
+
names.add(entry.name);
|
|
2125
|
+
}
|
|
2126
|
+
return names;
|
|
2127
|
+
}
|
|
2128
|
+
function lintVariableReferences(parsed) {
|
|
2129
|
+
const declared = declaredVariableNames(parsed);
|
|
2130
|
+
const issues = [];
|
|
2131
|
+
const check = (text, path) => {
|
|
2132
|
+
VARIABLE_REF_RE.lastIndex = 0;
|
|
2133
|
+
for (const match of text.matchAll(VARIABLE_REF_RE)) {
|
|
2134
|
+
const name = match[1] ?? match[2];
|
|
2135
|
+
if (name !== undefined && !declared.has(name)) {
|
|
2136
|
+
issues.push({
|
|
2137
|
+
path,
|
|
2138
|
+
reason: "undeclared_variable_reference",
|
|
2139
|
+
message: `${path} references ${match[0]}, which is not declared in [[environment.variables]]`
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
if (!isSettingsRecord(parsed) || !isSettingsRecord(parsed.environment))
|
|
2145
|
+
return issues;
|
|
2146
|
+
const servers = parsed.environment.mcp_servers;
|
|
2147
|
+
if (!Array.isArray(servers))
|
|
2148
|
+
return issues;
|
|
2149
|
+
servers.forEach((server, i) => {
|
|
2150
|
+
if (!isSettingsRecord(server))
|
|
2151
|
+
return;
|
|
2152
|
+
for (const field of REFERENCE_EXPANDED_MAP_FIELDS) {
|
|
2153
|
+
const map = server[field];
|
|
2154
|
+
if (!isSettingsRecord(map))
|
|
2155
|
+
continue;
|
|
2156
|
+
for (const [name, value] of Object.entries(map)) {
|
|
2157
|
+
if (typeof value === "string") {
|
|
2158
|
+
check(value, `environment.mcp_servers[${i}].${field}.${name}`);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
return issues;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// packages/settings/src/sections.ts
|
|
2167
|
+
var SETTINGS_MERGE_POLICIES = MERGE_EVIDENCE_POLICIES;
|
|
2168
|
+
var MERGE_FIELDS = new Set([
|
|
2169
|
+
"policy",
|
|
2170
|
+
"hold_labels",
|
|
2171
|
+
"queue",
|
|
2172
|
+
"required_checks",
|
|
2173
|
+
"hand_merge_paths"
|
|
2174
|
+
]);
|
|
2175
|
+
var REVIEW_FIELDS = new Set(["reviewers", "request", "max_requests"]);
|
|
2176
|
+
var PULL_REQUESTS_FIELDS = new Set([
|
|
2177
|
+
"draft_until_validated",
|
|
2178
|
+
"branch_pattern",
|
|
2179
|
+
"title_format",
|
|
2180
|
+
"commit_convention"
|
|
2181
|
+
]);
|
|
2182
|
+
var MERGE_QUEUES = ["mergify", "github"];
|
|
2183
|
+
var COMMIT_CONVENTIONS = ["conventional", "free-form"];
|
|
2184
|
+
var HAND_MERGE_PATH_RE = /^[\x21-\x2E\x30-\x7E][\x21-\x7E]{0,255}$/;
|
|
2185
|
+
var PARENT_SEGMENT_RE2 = /(?:^|\/)\.\.(?:\/|$)/;
|
|
2186
|
+
var RUNNER_FIELDS = new Set(["concurrency", "context"]);
|
|
2187
|
+
var RUNNER_CONTEXT_FIELDS = new Set(["handoff_at", "native_compact_at", "phases"]);
|
|
2188
|
+
var RUNNER_CONTEXT_THRESHOLD_FIELDS = new Set(["handoff_at", "native_compact_at"]);
|
|
2189
|
+
var RUNNER_PHASE_FIELDS = new Set(PROJECT_ROUTING_STAGES);
|
|
2190
|
+
var DEFAULT_HANDOFF_AT = 250000;
|
|
2191
|
+
var DEFAULT_NATIVE_COMPACT_AT = 350000;
|
|
2192
|
+
var THOUGHTS_KEYS = [
|
|
2193
|
+
"repo_owner",
|
|
2194
|
+
"repo_name",
|
|
2195
|
+
"default_branch",
|
|
2196
|
+
"repos_subdir",
|
|
2197
|
+
"directory"
|
|
2198
|
+
];
|
|
2199
|
+
var THOUGHTS_FIELDS = new Set(THOUGHTS_KEYS);
|
|
2200
|
+
var PRINTABLE_RE3 = /^[\x21-\x7E ]+$/;
|
|
2201
|
+
var ANY_TEXT_RE = /^[\s\S]+$/u;
|
|
2202
|
+
var hasControlCharacter = (text) => [...text].some((c) => c.charCodeAt(0) <= 31 || c.charCodeAt(0) === 127);
|
|
2203
|
+
function mergeSectionFromToml(value) {
|
|
2204
|
+
const raw = closedTable(value, MERGE_FIELDS, "merge");
|
|
2205
|
+
const section = {};
|
|
2206
|
+
if (raw.policy !== undefined) {
|
|
2207
|
+
if (typeof raw.policy !== "string" || !SETTINGS_MERGE_POLICIES.includes(raw.policy)) {
|
|
2208
|
+
throw new SettingsValidationError("merge.policy", "invalid_value", `merge.policy must be one of ${SETTINGS_MERGE_POLICIES.map((p) => JSON.stringify(p)).join(", ")}`);
|
|
2209
|
+
}
|
|
2210
|
+
section.policy = raw.policy;
|
|
2211
|
+
}
|
|
2212
|
+
if (raw.hold_labels !== undefined) {
|
|
2213
|
+
section.hold_labels = stringList(raw.hold_labels, "merge.hold_labels", {
|
|
2214
|
+
minItems: 0,
|
|
2215
|
+
maxItems: 16,
|
|
2216
|
+
minLength: 1,
|
|
2217
|
+
maxLength: 64,
|
|
2218
|
+
unique: true,
|
|
2219
|
+
describe: "a label of 1-64 printable characters"
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
if (raw.queue !== undefined) {
|
|
2223
|
+
if (typeof raw.queue !== "string" || !MERGE_QUEUES.includes(raw.queue)) {
|
|
2224
|
+
throw new SettingsValidationError("merge.queue", "invalid_value", `merge.queue must be one of ${MERGE_QUEUES.map((q) => JSON.stringify(q)).join(", ")}`);
|
|
2225
|
+
}
|
|
2226
|
+
section.queue = raw.queue;
|
|
2227
|
+
}
|
|
2228
|
+
if (raw.required_checks !== undefined) {
|
|
2229
|
+
section.required_checks = stringList(raw.required_checks, "merge.required_checks", {
|
|
2230
|
+
minItems: 0,
|
|
2231
|
+
maxItems: 32,
|
|
2232
|
+
minLength: 1,
|
|
2233
|
+
maxLength: 128,
|
|
2234
|
+
unique: true,
|
|
2235
|
+
describe: "a check name of 1-128 printable characters"
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
if (raw.hand_merge_paths !== undefined) {
|
|
2239
|
+
section.hand_merge_paths = stringList(raw.hand_merge_paths, "merge.hand_merge_paths", {
|
|
2240
|
+
minItems: 0,
|
|
2241
|
+
maxItems: 64,
|
|
2242
|
+
minLength: 1,
|
|
2243
|
+
maxLength: 256,
|
|
2244
|
+
unique: true,
|
|
2245
|
+
describe: "a repository-relative glob of 1-256 printable characters"
|
|
2246
|
+
}).map((glob, i) => {
|
|
2247
|
+
if (!HAND_MERGE_PATH_RE.test(glob) || PARENT_SEGMENT_RE2.test(glob)) {
|
|
2248
|
+
throw new SettingsValidationError(`merge.hand_merge_paths[${i}]`, "invalid_value", `merge.hand_merge_paths[${i}] must be repository-relative (no leading "/" and no ".." segment)`);
|
|
2249
|
+
}
|
|
2250
|
+
return glob;
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
return section;
|
|
2254
|
+
}
|
|
2255
|
+
function reviewSectionFromToml(value) {
|
|
2256
|
+
const raw = closedTable(value, REVIEW_FIELDS, "review");
|
|
2257
|
+
const section = {};
|
|
2258
|
+
if (raw.reviewers !== undefined) {
|
|
2259
|
+
section.reviewers = stringList(raw.reviewers, "review.reviewers", {
|
|
2260
|
+
minItems: 1,
|
|
2261
|
+
maxItems: 8,
|
|
2262
|
+
minLength: 1,
|
|
2263
|
+
maxLength: 64,
|
|
2264
|
+
unique: true,
|
|
2265
|
+
describe: "a reviewer name of 1-64 printable characters"
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
if (raw.request !== undefined) {
|
|
2269
|
+
if (typeof raw.request !== "string" || !REVIEW_REQUEST_POLICIES.includes(raw.request)) {
|
|
2270
|
+
throw new SettingsValidationError("review.request", "invalid_value", `review.request must be one of ${REVIEW_REQUEST_POLICIES.map((p) => JSON.stringify(p)).join(", ")}`);
|
|
2271
|
+
}
|
|
2272
|
+
section.request = raw.request;
|
|
2273
|
+
}
|
|
2274
|
+
if (raw.max_requests !== undefined) {
|
|
2275
|
+
section.max_requests = integerBetween(raw.max_requests, "review.max_requests", 0, 20);
|
|
2276
|
+
}
|
|
2277
|
+
return section;
|
|
2278
|
+
}
|
|
2279
|
+
function pullRequestsSectionFromToml(value) {
|
|
2280
|
+
const raw = closedTable(value, PULL_REQUESTS_FIELDS, "pull_requests");
|
|
2281
|
+
const section = {};
|
|
2282
|
+
if (raw.draft_until_validated !== undefined) {
|
|
2283
|
+
section.draft_until_validated = booleanAt(raw, "draft_until_validated", "pull_requests");
|
|
2284
|
+
}
|
|
2285
|
+
const templated = (key, required) => {
|
|
2286
|
+
const text = boundedStringValue(raw[key], `pull_requests.${key}`, 128, key === "title_format" ? ANY_TEXT_RE : PRINTABLE_RE3, key === "title_format" ? "text of at most 128 characters without control characters" : "a printable string of at most 128 characters");
|
|
2287
|
+
if (hasControlCharacter(text)) {
|
|
2288
|
+
throw new SettingsValidationError(`pull_requests.${key}`, "invalid_value", `pull_requests.${key} must be text of at most 128 characters without control characters`);
|
|
2289
|
+
}
|
|
2290
|
+
const missing = required.filter((token) => !text.includes(token));
|
|
2291
|
+
if (missing.length > 0) {
|
|
2292
|
+
throw new SettingsValidationError(`pull_requests.${key}`, "invalid_value", `pull_requests.${key} must contain ${missing.join(" and ")}`);
|
|
2293
|
+
}
|
|
2294
|
+
return text;
|
|
2295
|
+
};
|
|
2296
|
+
if (raw.branch_pattern !== undefined) {
|
|
2297
|
+
section.branch_pattern = templated("branch_pattern", ["{ticket}"]);
|
|
2298
|
+
}
|
|
2299
|
+
if (raw.title_format !== undefined) {
|
|
2300
|
+
section.title_format = templated("title_format", ["{ticket}", "{summary}"]);
|
|
2301
|
+
}
|
|
2302
|
+
if (raw.commit_convention !== undefined) {
|
|
2303
|
+
if (typeof raw.commit_convention !== "string" || !COMMIT_CONVENTIONS.includes(raw.commit_convention)) {
|
|
2304
|
+
throw new SettingsValidationError("pull_requests.commit_convention", "invalid_value", `pull_requests.commit_convention must be one of ${COMMIT_CONVENTIONS.map((c) => JSON.stringify(c)).join(", ")}`);
|
|
2305
|
+
}
|
|
2306
|
+
section.commit_convention = raw.commit_convention;
|
|
2307
|
+
}
|
|
2308
|
+
return section;
|
|
2309
|
+
}
|
|
2310
|
+
function runnerSectionFromToml(value) {
|
|
2311
|
+
const raw = closedTable(value, RUNNER_FIELDS, "runner");
|
|
2312
|
+
const section = {};
|
|
2313
|
+
if (raw.concurrency !== undefined) {
|
|
2314
|
+
section.concurrency = integerBetween(raw.concurrency, "runner.concurrency", 1, 100);
|
|
2315
|
+
}
|
|
2316
|
+
if (raw.context !== undefined) {
|
|
2317
|
+
const context = closedTable(raw.context, RUNNER_CONTEXT_FIELDS, "runner.context");
|
|
2318
|
+
const thresholds = contextThresholds(context, "runner.context");
|
|
2319
|
+
const resolvedHandoff = thresholds.handoff_at ?? DEFAULT_HANDOFF_AT;
|
|
2320
|
+
const resolvedNative = thresholds.native_compact_at ?? DEFAULT_NATIVE_COMPACT_AT;
|
|
2321
|
+
checkContextOrder(thresholds, "runner.context", resolvedHandoff, resolvedNative);
|
|
2322
|
+
const result = { ...thresholds };
|
|
2323
|
+
if (context.phases !== undefined) {
|
|
2324
|
+
const phases = closedTable(context.phases, RUNNER_PHASE_FIELDS, "runner.context.phases");
|
|
2325
|
+
result.phases = {};
|
|
2326
|
+
for (const phase of PROJECT_ROUTING_STAGES) {
|
|
2327
|
+
if (phases[phase] === undefined)
|
|
2328
|
+
continue;
|
|
2329
|
+
const path = `runner.context.phases.${phase}`;
|
|
2330
|
+
const phaseRaw = closedTable(phases[phase], RUNNER_CONTEXT_THRESHOLD_FIELDS, path);
|
|
2331
|
+
const override = contextThresholds(phaseRaw, path);
|
|
2332
|
+
checkContextOrder(override, path, override.handoff_at ?? thresholds.handoff_at ?? (phase === "intake" || phase === "pr" ? 200000 : DEFAULT_HANDOFF_AT), override.native_compact_at ?? resolvedNative);
|
|
2333
|
+
result.phases[phase] = override;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
section.context = result;
|
|
2337
|
+
}
|
|
2338
|
+
return section;
|
|
2339
|
+
}
|
|
2340
|
+
function contextThresholds(raw, path) {
|
|
2341
|
+
const result = {};
|
|
2342
|
+
for (const key of ["handoff_at", "native_compact_at"]) {
|
|
2343
|
+
if (raw[key] !== undefined) {
|
|
2344
|
+
result[key] = integerBetween(raw[key], `${path}.${key}`, 1, Number.MAX_SAFE_INTEGER);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return result;
|
|
2348
|
+
}
|
|
2349
|
+
function checkContextOrder(explicit, path, handoff, native) {
|
|
2350
|
+
if (handoff < native)
|
|
2351
|
+
return;
|
|
2352
|
+
const key = explicit.handoff_at !== undefined ? "handoff_at" : "native_compact_at";
|
|
2353
|
+
throw new SettingsValidationError(`${path}.${key}`, "invalid_value", `${path}.handoff_at must be less than ${path}.native_compact_at`);
|
|
2354
|
+
}
|
|
2355
|
+
function thoughtsSectionFromToml(value) {
|
|
2356
|
+
const raw = closedTable(value, THOUGHTS_FIELDS, "thoughts");
|
|
2357
|
+
const section = {};
|
|
2358
|
+
for (const key of THOUGHTS_KEYS) {
|
|
2359
|
+
const rawValue = raw[key];
|
|
2360
|
+
if (rawValue === undefined)
|
|
2361
|
+
continue;
|
|
2362
|
+
section[key] = boundedStringValue(stringAt(raw, key, "thoughts"), `thoughts.${key}`, 128, PRINTABLE_RE3, "a printable string of at most 128 characters");
|
|
2363
|
+
}
|
|
2364
|
+
return section;
|
|
2365
|
+
}
|
|
2366
|
+
function scanNonEnvironmentSection(value, section) {
|
|
2367
|
+
walkStrings(value, section, (text, path) => {
|
|
2368
|
+
try {
|
|
2369
|
+
scanForSecretMaterial(text, path);
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
if (err instanceof EnvironmentContractError) {
|
|
2372
|
+
const body = err.message.replace(/^invalid repo environment declaration: /, "");
|
|
2373
|
+
throw new SettingsValidationError(path, "literal_secret", body);
|
|
2374
|
+
}
|
|
2375
|
+
throw err;
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// packages/settings/src/environment-runtime.ts
|
|
2381
|
+
var CONTAINER_SIZE_PATTERN = "^[a-z][a-z0-9-]{0,31}$";
|
|
2382
|
+
var DEFAULT_FILE_MODE = "0600";
|
|
2383
|
+
var EGRESS_HOST_PATTERN = "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$";
|
|
2384
|
+
var FILE_MODE_PATTERN = "^0[0-7]{3}$";
|
|
2385
|
+
var RUNTIME_FILE_PATH_PATTERN = "^[\\x21-\\x2E\\x30-\\x7E][\\x21-\\x7E]{0,255}$";
|
|
2386
|
+
var CONTAINER_FIELDS = new Set(["size", "egress", "git"]);
|
|
2387
|
+
var GIT_FIELDS = new Set(["submodules", "lfs"]);
|
|
2388
|
+
var FILE_FIELDS = new Set(["path", "from", "mode"]);
|
|
2389
|
+
var ENV_NAME_RE4 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2390
|
+
var PARENT_SEGMENT_RE3 = /(?:^|\/)\.\.(?:\/|$)/;
|
|
2391
|
+
function containerFromToml(value) {
|
|
2392
|
+
const raw = closedTable(value, CONTAINER_FIELDS, "environment.container");
|
|
2393
|
+
const out = {};
|
|
2394
|
+
if (raw.size !== undefined) {
|
|
2395
|
+
const size = boundedStringValue(raw.size, "environment.container.size", 32, new RegExp(CONTAINER_SIZE_PATTERN), 'a size class name such as "standard"');
|
|
2396
|
+
if (!RUNNER_SIZE_CLASSES.includes(size)) {
|
|
2397
|
+
throw new SettingsValidationError("environment.container.size", "unknown_size_class", `environment.container.size ${JSON.stringify(size)} is not one of the platform's size classes: ${RUNNER_SIZE_CLASSES.join(", ")}`);
|
|
2398
|
+
}
|
|
2399
|
+
out.size = size;
|
|
2400
|
+
}
|
|
2401
|
+
if (raw.egress !== undefined) {
|
|
2402
|
+
out.egress = stringList(raw.egress, "environment.container.egress", {
|
|
2403
|
+
minItems: 0,
|
|
2404
|
+
maxItems: 128,
|
|
2405
|
+
minLength: 1,
|
|
2406
|
+
maxLength: 253,
|
|
2407
|
+
unique: true,
|
|
2408
|
+
describe: "a hostname"
|
|
2409
|
+
}).map((host, i) => boundedStringValue(host, `environment.container.egress[${i}]`, 253, new RegExp(EGRESS_HOST_PATTERN), 'a lowercase hostname such as "registry.npmjs.org" or "*.stripe.com"'));
|
|
2410
|
+
}
|
|
2411
|
+
if (raw.git !== undefined) {
|
|
2412
|
+
const git = closedTable(raw.git, GIT_FIELDS, "environment.container.git");
|
|
2413
|
+
out.git = {};
|
|
2414
|
+
if (git.submodules !== undefined) {
|
|
2415
|
+
out.git.submodules = booleanAt(git, "submodules", "environment.container.git");
|
|
2416
|
+
}
|
|
2417
|
+
if (git.lfs !== undefined)
|
|
2418
|
+
out.git.lfs = booleanAt(git, "lfs", "environment.container.git");
|
|
2419
|
+
}
|
|
2420
|
+
return out;
|
|
2421
|
+
}
|
|
2422
|
+
function declaredSecretNames(section) {
|
|
2423
|
+
const declared = new Set;
|
|
2424
|
+
const secret = new Set;
|
|
2425
|
+
if (Array.isArray(section.variables)) {
|
|
2426
|
+
for (const entry of section.variables) {
|
|
2427
|
+
if (!isSettingsRecord(entry) || typeof entry.name !== "string")
|
|
2428
|
+
continue;
|
|
2429
|
+
declared.add(entry.name);
|
|
2430
|
+
if (entry.secret === true)
|
|
2431
|
+
secret.add(entry.name);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return { declared, secret };
|
|
2435
|
+
}
|
|
2436
|
+
function filesFromToml(value, section) {
|
|
2437
|
+
const { declared, secret } = declaredSecretNames(section);
|
|
2438
|
+
const seenPaths = new Set;
|
|
2439
|
+
return tableArray(value, "environment.files", 32).map((entry, i) => {
|
|
2440
|
+
const path = `environment.files[${i}]`;
|
|
2441
|
+
const table = closedTable(entry, FILE_FIELDS, path);
|
|
2442
|
+
if (table.path === undefined || table.from === undefined) {
|
|
2443
|
+
throw new SettingsValidationError(table.path === undefined ? `${path}.path` : `${path}.from`, "missing_required", `${path} needs both path and from`);
|
|
2444
|
+
}
|
|
2445
|
+
const filePath = boundedStringValue(table.path, `${path}.path`, 256, new RegExp(RUNTIME_FILE_PATH_PATTERN), "a repository-relative path");
|
|
2446
|
+
if (PARENT_SEGMENT_RE3.test(filePath)) {
|
|
2447
|
+
throw new SettingsValidationError(`${path}.path`, "invalid_value", `${path}.path must stay inside the repository (no ".." segment)`);
|
|
2448
|
+
}
|
|
2449
|
+
if (seenPaths.has(filePath)) {
|
|
2450
|
+
throw new SettingsValidationError(`${path}.path`, "duplicate_entry", `environment.files writes ${JSON.stringify(filePath)} more than once`);
|
|
2451
|
+
}
|
|
2452
|
+
seenPaths.add(filePath);
|
|
2453
|
+
const from = boundedStringValue(table.from, `${path}.from`, 128, ENV_NAME_RE4, "a variable name");
|
|
2454
|
+
if (!declared.has(from)) {
|
|
2455
|
+
throw new SettingsValidationError(`${path}.from`, "undeclared_variable_reference", `${path}.from names ${from}, which is not declared in [[environment.variables]]`);
|
|
2456
|
+
}
|
|
2457
|
+
if (!secret.has(from)) {
|
|
2458
|
+
throw new SettingsValidationError(`${path}.from`, "invalid_value", `${path}.from names ${from}, which must be declared with secret = true to be written to a file`);
|
|
2459
|
+
}
|
|
2460
|
+
const file = {
|
|
2461
|
+
path: filePath,
|
|
2462
|
+
from,
|
|
2463
|
+
mode: table.mode === undefined ? DEFAULT_FILE_MODE : boundedStringValue(table.mode, `${path}.mode`, 4, new RegExp(FILE_MODE_PATTERN), 'an octal permission string such as "0600"')
|
|
2464
|
+
};
|
|
2465
|
+
return file;
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function environmentRuntimeFromToml(section) {
|
|
2469
|
+
if (!isSettingsRecord(section))
|
|
2470
|
+
return {};
|
|
2471
|
+
const runtime = {};
|
|
2472
|
+
if (section.start !== undefined) {
|
|
2473
|
+
const start = environmentStepsFromToml(section.start, "environment.start");
|
|
2474
|
+
const ids = new Set;
|
|
2475
|
+
for (const [i, step] of start.entries()) {
|
|
2476
|
+
if (ids.has(step.id)) {
|
|
2477
|
+
throw new SettingsValidationError(`environment.start[${i}].name`, "duplicate_entry", `environment.start has two steps named ${JSON.stringify(step.id)}`);
|
|
2478
|
+
}
|
|
2479
|
+
ids.add(step.id);
|
|
2480
|
+
}
|
|
2481
|
+
runtime.start = start;
|
|
2482
|
+
}
|
|
2483
|
+
if (section.container !== undefined)
|
|
2484
|
+
runtime.container = containerFromToml(section.container);
|
|
2485
|
+
if (section.files !== undefined)
|
|
2486
|
+
runtime.files = filesFromToml(section.files, section);
|
|
2487
|
+
for (const key of ["start", "container", "files"]) {
|
|
2488
|
+
if (section[key] !== undefined)
|
|
2489
|
+
scanNonEnvironmentSection(section[key], `environment.${key}`);
|
|
2490
|
+
}
|
|
2491
|
+
return runtime;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// packages/settings/src/project-section.ts
|
|
2495
|
+
var LINEAR_TEAM_KEY_PATTERN = "^[A-Z][A-Z0-9]{0,15}$";
|
|
2496
|
+
var LINEAR_WORKSPACE_PATTERN = "^[a-z0-9][a-z0-9-]{0,63}$";
|
|
2497
|
+
var GITHUB_REPOSITORY_PATTERN = "^[A-Za-z0-9][A-Za-z0-9-]{0,38}/[A-Za-z0-9._-]{1,100}$";
|
|
2498
|
+
var ROUTING_PROVIDER_PATTERN = "^[a-z][a-z0-9-]{0,31}$";
|
|
2499
|
+
var ROUTING_MODEL_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$";
|
|
2500
|
+
var ROUTING_EFFORT_PATTERN = "^[a-z]{1,16}$";
|
|
2501
|
+
var CAPABILITY_PATTERN = "^[a-z][a-z0-9._-]{0,63}$";
|
|
2502
|
+
var EXECUTION_TARGET_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$";
|
|
2503
|
+
var PRINTABLE_RE4 = /^[\x21-\x7E ]+$/;
|
|
2504
|
+
var LABEL_OPTIONS = {
|
|
2505
|
+
minItems: 0,
|
|
2506
|
+
maxItems: 32,
|
|
2507
|
+
minLength: 1,
|
|
2508
|
+
maxLength: 64,
|
|
2509
|
+
unique: true,
|
|
2510
|
+
describe: "a label name of 1-64 printable characters"
|
|
2511
|
+
};
|
|
2512
|
+
var PROJECT_FIELDS = new Set([
|
|
2513
|
+
"linear_team",
|
|
2514
|
+
"linear_workspace",
|
|
2515
|
+
"linear",
|
|
2516
|
+
"routing",
|
|
2517
|
+
"ladder",
|
|
2518
|
+
"thoughts",
|
|
2519
|
+
"context",
|
|
2520
|
+
"intake",
|
|
2521
|
+
"repositories",
|
|
2522
|
+
"runner",
|
|
2523
|
+
"execution"
|
|
2524
|
+
]);
|
|
2525
|
+
var LINEAR_FIELDS = new Set(["states", "mode", "git_automation", "labels"]);
|
|
2526
|
+
var LINEAR_LABEL_FIELDS = new Set(["ask", "hold", "release"]);
|
|
2527
|
+
var ROUTING_ENTRY_FIELDS = new Set(["provider", "model", "effort"]);
|
|
2528
|
+
var LADDER_FIELDS = new Set(["intake", "remediate_round_cap", "park_after_failures"]);
|
|
2529
|
+
var THOUGHTS_FIELDS2 = new Set(["repository", "directory", "profile", "default_branch"]);
|
|
2530
|
+
var CONTEXT_FIELDS2 = new Set(["index", "wiki_notes"]);
|
|
2531
|
+
var INTAKE_FIELDS = new Set(["include_labels", "exclude_labels"]);
|
|
2532
|
+
var REPOSITORIES_FIELDS = new Set(["default", "rules", "list"]);
|
|
2533
|
+
var RULE_FIELDS = new Set(["label", "linear_project", "repository"]);
|
|
2534
|
+
var RUNNER_FIELDS2 = new Set(["concurrency"]);
|
|
2535
|
+
var EXECUTION_FIELDS = new Set(["mode", "activation", "targets", "requires"]);
|
|
2536
|
+
var TARGET_FIELDS = new Set(["kind", "id"]);
|
|
2537
|
+
function pattern(value, path, source, maxLength, describe) {
|
|
2538
|
+
return boundedStringValue(value, path, maxLength, new RegExp(source), describe);
|
|
2539
|
+
}
|
|
2540
|
+
function oneOf(value, path, allowed) {
|
|
2541
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
2542
|
+
throw new SettingsValidationError(path, "invalid_value", `${path} must be one of ${allowed.map((v) => JSON.stringify(v)).join(", ")}`);
|
|
2543
|
+
}
|
|
2544
|
+
return value;
|
|
2545
|
+
}
|
|
2546
|
+
function printable(value, path, maxLength) {
|
|
2547
|
+
return boundedStringValue(value, path, maxLength, PRINTABLE_RE4, `a printable string of at most ${maxLength} characters`);
|
|
2548
|
+
}
|
|
2549
|
+
function linearSection(value) {
|
|
2550
|
+
const raw = closedTable(value, LINEAR_FIELDS, "project.linear");
|
|
2551
|
+
const out = {};
|
|
2552
|
+
if (raw.states !== undefined) {
|
|
2553
|
+
const states = closedTable(raw.states, new Set(PROJECT_WORKFLOW_SLOTS), "project.linear.states");
|
|
2554
|
+
out.states = {};
|
|
2555
|
+
for (const [slot, name] of Object.entries(states)) {
|
|
2556
|
+
out.states[slot] = printable(name, `project.linear.states.${slot}`, 128);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (raw.mode !== undefined)
|
|
2560
|
+
out.mode = oneOf(raw.mode, "project.linear.mode", PROJECT_WORKFLOW_MODES);
|
|
2561
|
+
if (raw.git_automation !== undefined) {
|
|
2562
|
+
out.git_automation = oneOf(raw.git_automation, "project.linear.git_automation", PROJECT_GIT_AUTOMATIONS);
|
|
2563
|
+
}
|
|
2564
|
+
if (raw.labels !== undefined) {
|
|
2565
|
+
const labels = closedTable(raw.labels, LINEAR_LABEL_FIELDS, "project.linear.labels");
|
|
2566
|
+
out.labels = {};
|
|
2567
|
+
for (const key of ["ask", "hold", "release"]) {
|
|
2568
|
+
if (labels[key] !== undefined) {
|
|
2569
|
+
out.labels[key] = printable(labels[key], `project.linear.labels.${key}`, 64);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
return out;
|
|
2574
|
+
}
|
|
2575
|
+
function routingSection(value) {
|
|
2576
|
+
const raw = closedTable(value, new Set(PROJECT_ROUTING_STAGES), "project.routing");
|
|
2577
|
+
const out = {};
|
|
2578
|
+
for (const [stage, entry] of Object.entries(raw)) {
|
|
2579
|
+
const path = `project.routing.${stage}`;
|
|
2580
|
+
const table = closedTable(entry, ROUTING_ENTRY_FIELDS, path);
|
|
2581
|
+
const routed = {
|
|
2582
|
+
provider: pattern(stringAt(table, "provider", path), `${path}.provider`, ROUTING_PROVIDER_PATTERN, 32, 'a provider key such as "claude", "codex" or "glm"')
|
|
2583
|
+
};
|
|
2584
|
+
if (table.model !== undefined) {
|
|
2585
|
+
routed.model = pattern(table.model, `${path}.model`, ROUTING_MODEL_PATTERN, 128, "a model id");
|
|
2586
|
+
}
|
|
2587
|
+
if (table.effort !== undefined) {
|
|
2588
|
+
routed.effort = pattern(table.effort, `${path}.effort`, ROUTING_EFFORT_PATTERN, 16, 'an effort level such as "low", "high" or "max"');
|
|
2589
|
+
}
|
|
2590
|
+
out[stage] = routed;
|
|
2591
|
+
}
|
|
2592
|
+
return out;
|
|
2593
|
+
}
|
|
2594
|
+
function ladderSection(value) {
|
|
2595
|
+
const raw = closedTable(value, LADDER_FIELDS, "project.ladder");
|
|
2596
|
+
const out = {};
|
|
2597
|
+
if (raw.intake !== undefined)
|
|
2598
|
+
out.intake = booleanAt(raw, "intake", "project.ladder");
|
|
2599
|
+
if (raw.remediate_round_cap !== undefined) {
|
|
2600
|
+
out.remediate_round_cap = integerBetween(raw.remediate_round_cap, "project.ladder.remediate_round_cap", 1, 10);
|
|
2601
|
+
}
|
|
2602
|
+
if (raw.park_after_failures !== undefined) {
|
|
2603
|
+
out.park_after_failures = integerBetween(raw.park_after_failures, "project.ladder.park_after_failures", 1, 10);
|
|
2604
|
+
}
|
|
2605
|
+
return out;
|
|
2606
|
+
}
|
|
2607
|
+
function thoughtsSection(value) {
|
|
2608
|
+
const raw = closedTable(value, THOUGHTS_FIELDS2, "project.thoughts");
|
|
2609
|
+
const out = {};
|
|
2610
|
+
if (raw.repository !== undefined) {
|
|
2611
|
+
out.repository = pattern(raw.repository, "project.thoughts.repository", GITHUB_REPOSITORY_PATTERN, 140, 'a GitHub repository written "owner/name"');
|
|
2612
|
+
}
|
|
2613
|
+
for (const key of ["directory", "profile", "default_branch"]) {
|
|
2614
|
+
if (raw[key] !== undefined)
|
|
2615
|
+
out[key] = printable(raw[key], `project.thoughts.${key}`, 128);
|
|
2616
|
+
}
|
|
2617
|
+
return out;
|
|
2618
|
+
}
|
|
2619
|
+
function contextSection(value) {
|
|
2620
|
+
const raw = closedTable(value, CONTEXT_FIELDS2, "project.context");
|
|
2621
|
+
const out = {};
|
|
2622
|
+
if (raw.index !== undefined)
|
|
2623
|
+
out.index = booleanAt(raw, "index", "project.context");
|
|
2624
|
+
if (raw.wiki_notes !== undefined) {
|
|
2625
|
+
if (!Array.isArray(raw.wiki_notes) || raw.wiki_notes.length > 32) {
|
|
2626
|
+
throw new SettingsValidationError("project.context.wiki_notes", "invalid_value", "project.context.wiki_notes must be an array of at most 32 strings");
|
|
2627
|
+
}
|
|
2628
|
+
out.wiki_notes = raw.wiki_notes.map((note, i) => {
|
|
2629
|
+
if (typeof note !== "string" || note.length === 0 || note.length > 4000) {
|
|
2630
|
+
throw new SettingsValidationError(`project.context.wiki_notes[${i}]`, "invalid_value", `project.context.wiki_notes[${i}] must be a non-empty string of at most 4000 characters`);
|
|
2631
|
+
}
|
|
2632
|
+
return note;
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
return out;
|
|
2636
|
+
}
|
|
2637
|
+
function intakeSection(value) {
|
|
2638
|
+
const raw = closedTable(value, INTAKE_FIELDS, "project.intake");
|
|
2639
|
+
const out = {};
|
|
2640
|
+
if (raw.include_labels !== undefined) {
|
|
2641
|
+
out.include_labels = stringList(raw.include_labels, "project.intake.include_labels", LABEL_OPTIONS);
|
|
2642
|
+
}
|
|
2643
|
+
if (raw.exclude_labels !== undefined) {
|
|
2644
|
+
out.exclude_labels = stringList(raw.exclude_labels, "project.intake.exclude_labels", LABEL_OPTIONS);
|
|
2645
|
+
}
|
|
2646
|
+
const overlap = (out.include_labels ?? []).filter((l) => (out.exclude_labels ?? []).includes(l));
|
|
2647
|
+
if (overlap.length > 0) {
|
|
2648
|
+
throw new SettingsValidationError("project.intake.exclude_labels", "invalid_value", `project.intake: ${JSON.stringify(overlap[0])} is both included and excluded`);
|
|
2649
|
+
}
|
|
2650
|
+
return out;
|
|
2651
|
+
}
|
|
2652
|
+
function repositoriesSection(value) {
|
|
2653
|
+
const raw = closedTable(value, REPOSITORIES_FIELDS, "project.repositories");
|
|
2654
|
+
const out = {};
|
|
2655
|
+
const repo = (v, path) => pattern(v, path, GITHUB_REPOSITORY_PATTERN, 140, 'a GitHub repository written "owner/name"');
|
|
2656
|
+
if (raw.default !== undefined)
|
|
2657
|
+
out.default = repo(raw.default, "project.repositories.default");
|
|
2658
|
+
if (raw.rules !== undefined) {
|
|
2659
|
+
out.rules = tableArray(raw.rules, "project.repositories.rules", 64).map((entry, i) => {
|
|
2660
|
+
const path = `project.repositories.rules[${i}]`;
|
|
2661
|
+
const table = closedTable(entry, RULE_FIELDS, path);
|
|
2662
|
+
const hasLabel = table.label !== undefined;
|
|
2663
|
+
const hasProject = table.linear_project !== undefined;
|
|
2664
|
+
if (hasLabel === hasProject) {
|
|
2665
|
+
throw new SettingsValidationError(path, "invalid_shape", `${path} must match on exactly one of label or linear_project`);
|
|
2666
|
+
}
|
|
2667
|
+
const rule = {
|
|
2668
|
+
repository: repo(stringAt(table, "repository", path), `${path}.repository`)
|
|
2669
|
+
};
|
|
2670
|
+
if (hasLabel)
|
|
2671
|
+
rule.label = printable(table.label, `${path}.label`, 64);
|
|
2672
|
+
if (hasProject)
|
|
2673
|
+
rule.linear_project = printable(table.linear_project, `${path}.linear_project`, 128);
|
|
2674
|
+
return rule;
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
if (raw.list !== undefined) {
|
|
2678
|
+
if (!Array.isArray(raw.list) || raw.list.length > 64) {
|
|
2679
|
+
throw new SettingsValidationError("project.repositories.list", "invalid_value", "project.repositories.list must be an array of at most 64 GitHub repositories");
|
|
2680
|
+
}
|
|
2681
|
+
const seen = new Set;
|
|
2682
|
+
out.list = raw.list.map((entry, i) => {
|
|
2683
|
+
const path = `project.repositories.list[${i}]`;
|
|
2684
|
+
const value2 = repo(entry, path);
|
|
2685
|
+
const key = value2.toLowerCase();
|
|
2686
|
+
if (seen.has(key)) {
|
|
2687
|
+
throw new SettingsValidationError(path, "duplicate_entry", `${path} duplicates an earlier entry (case-insensitive): ${JSON.stringify(value2)}`);
|
|
2688
|
+
}
|
|
2689
|
+
seen.add(key);
|
|
2690
|
+
return value2;
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
return out;
|
|
2694
|
+
}
|
|
2695
|
+
function executionSection(value) {
|
|
2696
|
+
const raw = closedTable(value, EXECUTION_FIELDS, "project.execution");
|
|
2697
|
+
const out = {};
|
|
2698
|
+
if (raw.mode !== undefined)
|
|
2699
|
+
out.mode = oneOf(raw.mode, "project.execution.mode", PROJECT_EXECUTION_MODES);
|
|
2700
|
+
if (raw.activation !== undefined) {
|
|
2701
|
+
out.activation = oneOf(raw.activation, "project.execution.activation", PROJECT_ACTIVATION_POLICIES);
|
|
2702
|
+
}
|
|
2703
|
+
if (raw.targets !== undefined) {
|
|
2704
|
+
out.targets = tableArray(raw.targets, "project.execution.targets", 16).map((entry, i) => {
|
|
2705
|
+
const path = `project.execution.targets[${i}]`;
|
|
2706
|
+
const table = closedTable(entry, TARGET_FIELDS, path);
|
|
2707
|
+
return {
|
|
2708
|
+
kind: oneOf(table.kind, `${path}.kind`, PROJECT_TARGET_KINDS),
|
|
2709
|
+
id: pattern(stringAt(table, "id", path), `${path}.id`, EXECUTION_TARGET_ID_PATTERN, 128, "a target id of letters, digits and . _ : - (at most 128 characters)")
|
|
2710
|
+
};
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
if (raw.requires !== undefined) {
|
|
2714
|
+
out.requires = stringList(raw.requires, "project.execution.requires", {
|
|
2715
|
+
minItems: 0,
|
|
2716
|
+
maxItems: 32,
|
|
2717
|
+
minLength: 1,
|
|
2718
|
+
maxLength: 64,
|
|
2719
|
+
unique: true,
|
|
2720
|
+
describe: "a capability name of 1-64 printable characters"
|
|
2721
|
+
}).map((cap, i) => pattern(cap, `project.execution.requires[${i}]`, CAPABILITY_PATTERN, 64, "a lowercase capability name"));
|
|
2722
|
+
}
|
|
2723
|
+
return out;
|
|
2724
|
+
}
|
|
2725
|
+
function projectSectionFromToml(value) {
|
|
2726
|
+
const raw = closedTable(value, PROJECT_FIELDS, "project");
|
|
2727
|
+
if (raw.linear_team === undefined) {
|
|
2728
|
+
throw new SettingsValidationError("project.linear_team", "missing_required", `project.linear_team is required: name the Linear team this repository's work belongs to, e.g. linear_team = "CTC"`);
|
|
2729
|
+
}
|
|
2730
|
+
const section = {
|
|
2731
|
+
linear_team: pattern(raw.linear_team, "project.linear_team", LINEAR_TEAM_KEY_PATTERN, 16, 'a Linear team key: uppercase letters and digits, e.g. "CTC"')
|
|
2732
|
+
};
|
|
2733
|
+
if (raw.linear_workspace !== undefined) {
|
|
2734
|
+
section.linear_workspace = pattern(raw.linear_workspace, "project.linear_workspace", LINEAR_WORKSPACE_PATTERN, 64, 'a Linear workspace URL key, e.g. "coalesce-labs"');
|
|
2735
|
+
}
|
|
2736
|
+
if (raw.linear !== undefined)
|
|
2737
|
+
section.linear = linearSection(raw.linear);
|
|
2738
|
+
if (raw.routing !== undefined)
|
|
2739
|
+
section.routing = routingSection(raw.routing);
|
|
2740
|
+
if (raw.ladder !== undefined)
|
|
2741
|
+
section.ladder = ladderSection(raw.ladder);
|
|
2742
|
+
if (raw.thoughts !== undefined)
|
|
2743
|
+
section.thoughts = thoughtsSection(raw.thoughts);
|
|
2744
|
+
if (raw.context !== undefined)
|
|
2745
|
+
section.context = contextSection(raw.context);
|
|
2746
|
+
if (raw.intake !== undefined)
|
|
2747
|
+
section.intake = intakeSection(raw.intake);
|
|
2748
|
+
if (raw.repositories !== undefined)
|
|
2749
|
+
section.repositories = repositoriesSection(raw.repositories);
|
|
2750
|
+
if (raw.runner !== undefined) {
|
|
2751
|
+
const runner = closedTable(raw.runner, RUNNER_FIELDS2, "project.runner");
|
|
2752
|
+
section.runner = runner.concurrency === undefined ? {} : { concurrency: integerBetween(runner.concurrency, "project.runner.concurrency", 1, 100) };
|
|
2753
|
+
}
|
|
2754
|
+
if (raw.execution !== undefined)
|
|
2755
|
+
section.execution = executionSection(raw.execution);
|
|
2756
|
+
return section;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// packages/settings/src/toml.ts
|
|
2760
|
+
import { parse, stringify, TomlError } from "smol-toml";
|
|
2761
|
+
function parseSettingsToml(text) {
|
|
2762
|
+
try {
|
|
2763
|
+
return { ok: true, value: parse(text) };
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
if (err instanceof TomlError) {
|
|
2766
|
+
const message = err.message.split(`
|
|
2767
|
+
`, 1)[0] ?? "malformed TOML document";
|
|
2768
|
+
return {
|
|
2769
|
+
ok: false,
|
|
2770
|
+
issue: {
|
|
2771
|
+
path: "",
|
|
2772
|
+
reason: "toml_parse_error",
|
|
2773
|
+
message,
|
|
2774
|
+
...typeof err.line === "number" ? { line: err.line } : {}
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
throw err;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// packages/settings/src/validate.ts
|
|
2783
|
+
var TOP_LEVEL_FIELDS3 = new Set([
|
|
2784
|
+
"project",
|
|
2785
|
+
"merge",
|
|
2786
|
+
"review",
|
|
2787
|
+
"runner",
|
|
2788
|
+
"thoughts",
|
|
2789
|
+
"pull_requests",
|
|
2790
|
+
"environment",
|
|
2791
|
+
"agents"
|
|
2792
|
+
]);
|
|
2793
|
+
function issueFrom(err) {
|
|
2794
|
+
if (err instanceof SettingsValidationError) {
|
|
2795
|
+
return { path: err.path, reason: err.reason, message: err.message };
|
|
2796
|
+
}
|
|
2797
|
+
throw err;
|
|
2798
|
+
}
|
|
2799
|
+
function declaredVariables(parsed) {
|
|
2800
|
+
if (!isSettingsRecord(parsed))
|
|
2801
|
+
return [];
|
|
2802
|
+
const environment = parsed.environment;
|
|
2803
|
+
if (!isSettingsRecord(environment) || !Array.isArray(environment.variables))
|
|
2804
|
+
return [];
|
|
2805
|
+
const out = [];
|
|
2806
|
+
for (const entry of environment.variables) {
|
|
2807
|
+
if (isSettingsRecord(entry) && typeof entry.name === "string") {
|
|
2808
|
+
out.push(entry.secret === true ? { name: entry.name, secret: true } : { name: entry.name });
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
return out;
|
|
2812
|
+
}
|
|
2813
|
+
function validateSettingsValue(value) {
|
|
2814
|
+
if (!isSettingsRecord(value)) {
|
|
2815
|
+
return {
|
|
2816
|
+
ok: false,
|
|
2817
|
+
issues: [
|
|
2818
|
+
{
|
|
2819
|
+
path: "(root)",
|
|
2820
|
+
reason: "invalid_shape",
|
|
2821
|
+
message: "the settings file root must be a TOML table"
|
|
2822
|
+
}
|
|
2823
|
+
]
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
const issues = [];
|
|
2827
|
+
const settings = {};
|
|
2828
|
+
let environment;
|
|
2829
|
+
let runtime;
|
|
2830
|
+
for (const key of Object.keys(value)) {
|
|
2831
|
+
if (!TOP_LEVEL_FIELDS3.has(key)) {
|
|
2832
|
+
issues.push({
|
|
2833
|
+
path: key,
|
|
2834
|
+
reason: "invalid_shape",
|
|
2835
|
+
message: `unknown field "${key}"`
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (value.project === undefined) {
|
|
2840
|
+
issues.push({
|
|
2841
|
+
path: "project",
|
|
2842
|
+
reason: "missing_required",
|
|
2843
|
+
message: `[project] is required: name the Linear team this repository's work belongs to, e.g. [project] linear_team = "CTC"`
|
|
2844
|
+
});
|
|
2845
|
+
} else {
|
|
2846
|
+
try {
|
|
2847
|
+
settings.project = projectSectionFromToml(value.project);
|
|
2848
|
+
scanNonEnvironmentSection(value.project, "project");
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
issues.push(issueFrom(err));
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
if (value.environment !== undefined) {
|
|
2854
|
+
try {
|
|
2855
|
+
environment = declarationFromEnvironmentSection(value.environment);
|
|
2856
|
+
settings.environment = value.environment;
|
|
2857
|
+
} catch (err) {
|
|
2858
|
+
issues.push(issueFrom(err));
|
|
2859
|
+
}
|
|
2860
|
+
try {
|
|
2861
|
+
const projection = environmentRuntimeFromToml(value.environment);
|
|
2862
|
+
if (projection.start !== undefined || projection.container !== undefined || projection.files !== undefined) {
|
|
2863
|
+
runtime = projection;
|
|
2864
|
+
}
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
issues.push(issueFrom(err));
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
const sectionValidators = [
|
|
2870
|
+
["merge", mergeSectionFromToml],
|
|
2871
|
+
["review", reviewSectionFromToml],
|
|
2872
|
+
["runner", runnerSectionFromToml],
|
|
2873
|
+
["thoughts", thoughtsSectionFromToml],
|
|
2874
|
+
["pull_requests", pullRequestsSectionFromToml]
|
|
2875
|
+
];
|
|
2876
|
+
for (const [key, validateSection] of sectionValidators) {
|
|
2877
|
+
if (value[key] === undefined)
|
|
2878
|
+
continue;
|
|
2879
|
+
try {
|
|
2880
|
+
settings[key] = validateSection(value[key]);
|
|
2881
|
+
scanNonEnvironmentSection(value[key], key);
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
issues.push(issueFrom(err));
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
if (value.agents !== undefined) {
|
|
2887
|
+
try {
|
|
2888
|
+
settings.agents = agentsSectionFromToml(value.agents, declaredVariables(value));
|
|
2889
|
+
} catch (err) {
|
|
2890
|
+
issues.push(issueFrom(err));
|
|
2891
|
+
}
|
|
2892
|
+
try {
|
|
2893
|
+
scanNonEnvironmentSection(value.agents, "agents");
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
issues.push(issueFrom(err));
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
issues.push(...lintVariableReferences(value));
|
|
2899
|
+
const ok = issues.length === 0;
|
|
2900
|
+
return ok ? {
|
|
2901
|
+
ok: true,
|
|
2902
|
+
issues: [],
|
|
2903
|
+
settings,
|
|
2904
|
+
...environment !== undefined ? { environment } : {},
|
|
2905
|
+
...runtime !== undefined ? { runtime } : {}
|
|
2906
|
+
} : { ok: false, issues };
|
|
2907
|
+
}
|
|
2908
|
+
function validateCatalystSettings(text) {
|
|
2909
|
+
const parsed = parseSettingsToml(text);
|
|
2910
|
+
if (!parsed.ok)
|
|
2911
|
+
return { ok: false, issues: [parsed.issue] };
|
|
2912
|
+
return validateSettingsValue(parsed.value);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// entry.ts
|
|
2916
|
+
function validateFullSettings(text) {
|
|
2917
|
+
try {
|
|
2918
|
+
if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > 1048576)
|
|
2919
|
+
return { ok: false, errors: ["Settings file could not be fully validated"] };
|
|
2920
|
+
const result = validateCatalystSettings(text);
|
|
2921
|
+
if (!result.ok || !result.settings?.project)
|
|
2922
|
+
return { ok: false, errors: ["Settings file could not be fully validated"] };
|
|
2923
|
+
const entries = result.environment?.environment ?? [];
|
|
2924
|
+
return {
|
|
2925
|
+
ok: true,
|
|
2926
|
+
errors: [],
|
|
2927
|
+
linearTeam: result.settings.project.linear_team,
|
|
2928
|
+
variableNames: entries.filter((entry) => entry.secret !== true).map((entry) => entry.name).sort(),
|
|
2929
|
+
secretNames: entries.filter((entry) => entry.secret === true).map((entry) => entry.name).sort()
|
|
2930
|
+
};
|
|
2931
|
+
} catch {
|
|
2932
|
+
return { ok: false, errors: ["Settings file could not be fully validated"] };
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
export {
|
|
2936
|
+
validateFullSettings
|
|
2937
|
+
};
|