@tiangong-ai/cli 0.0.38 → 0.0.40
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/AGENTS.md +2 -2
- package/README.md +81 -2
- package/dist/research/setup-command.js +77 -7
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/setup-catalog.d.ts +1 -0
- package/dist/research/workspace/setup-catalog.js +8 -2
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup-declarative.d.ts +126 -0
- package/dist/research/workspace/setup-declarative.js +926 -0
- package/dist/research/workspace/setup-declarative.js.map +1 -0
- package/dist/research/workspace/setup-wizard.js +8 -11
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +1 -0
- package/dist/research/workspace/setup.js +23 -0
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/storage.js +4 -0
- package/dist/research/workspace/storage.js.map +1 -1
- package/dist/research/workspace/types.d.ts +4 -0
- package/dist/research/workspace/workspace.js +12 -2
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import { open, lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
4
|
+
import { isAlias, parseDocument, stringify, visit } from "yaml";
|
|
5
|
+
import { CliError } from "../../errors.js";
|
|
6
|
+
import { EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, } from "./external-skills.js";
|
|
7
|
+
import { configuredResearchSecrets, sanitizeResearchRecord, sanitizeResearchText, sanitizeResearchValue, } from "./sanitization.js";
|
|
8
|
+
import { RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, } from "./setup-catalog.js";
|
|
9
|
+
import { exactResearchCliCommand } from "./setup-invocation.js";
|
|
10
|
+
import { applyResearchSetupPlan, createResearchSetupPlan, loadAndVerifyResearchSetupPlan, resolveResearchSetupWorkspacePath, } from "./setup.js";
|
|
11
|
+
import { canonicalJson, ensureDirectory, isObject, pathExists, readJsonFile, sha256Text, workspacePaths, } from "./storage.js";
|
|
12
|
+
const MAX_DECLARATION_BYTES = 256 * 1024;
|
|
13
|
+
const MAX_DECLARATION_ENV_BYTES = 64 * 1024;
|
|
14
|
+
const MAX_DECLARATION_ENV_VALUE_BYTES = 16 * 1024;
|
|
15
|
+
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
16
|
+
const BRAVE_SKILL_IDS = new Set(RESEARCH_SETUP_SKILLS.filter((skill) => skill.sourceId === "brave-search-skills").map((skill) => skill.id));
|
|
17
|
+
export async function initializeResearchSetupDeclaration(workspace) {
|
|
18
|
+
const root = await resolveResearchSetupWorkspacePath(workspace);
|
|
19
|
+
const paths = workspacePaths(root);
|
|
20
|
+
const targets = [
|
|
21
|
+
paths.setupDeclaration,
|
|
22
|
+
paths.setupDeclarationEnvExample,
|
|
23
|
+
resolve(paths.control, ".gitignore"),
|
|
24
|
+
];
|
|
25
|
+
if ((await Promise.all(targets.map(pathExists))).some(Boolean)) {
|
|
26
|
+
throw declarationError({
|
|
27
|
+
code: "RESEARCH_SETUP_DECLARATION_EXISTS",
|
|
28
|
+
step: "declarative-init",
|
|
29
|
+
reason: "Declarative setup files already exist and will not be overwritten.",
|
|
30
|
+
minimumAction: "Review the existing setup.yaml and setup.env.example files in place.",
|
|
31
|
+
retryArgs: ["research", "setup", "--workspace", root, "--json"],
|
|
32
|
+
exitCode: 3,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
await ensureDirectory(paths.control);
|
|
36
|
+
const declaration = defaultDeclaration(root);
|
|
37
|
+
await writeNewTextFile(paths.setupDeclaration, declarationTemplate(declaration), 0o644);
|
|
38
|
+
await writeNewTextFile(paths.setupDeclarationEnvExample, declarationEnvironmentExample(declaration), 0o600);
|
|
39
|
+
await writeNewTextFile(resolve(paths.control, ".gitignore"), "setup.env\n", 0o644);
|
|
40
|
+
return {
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
workspace: root,
|
|
43
|
+
configurationPath: paths.setupDeclaration,
|
|
44
|
+
environmentExamplePath: paths.setupDeclarationEnvExample,
|
|
45
|
+
next: {
|
|
46
|
+
minimumAction: "Review setup.yaml, explicitly accept licenses and costs, optionally copy setup.env.example to owner-only setup.env, then run setup again.",
|
|
47
|
+
setupCommand: exactResearchCliCommand(["research", "setup", "--workspace", root, "--json"]),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function discoverResearchSetupDeclaration(workspace, options = {}) {
|
|
52
|
+
const requestedRoot = resolve(workspace);
|
|
53
|
+
const rootInfo = await lstat(requestedRoot).catch(() => null);
|
|
54
|
+
if (!rootInfo && options.configurationPath === undefined)
|
|
55
|
+
return null;
|
|
56
|
+
const root = await resolveResearchSetupWorkspacePath(requestedRoot);
|
|
57
|
+
const paths = workspacePaths(root);
|
|
58
|
+
const configurationPath = options.configurationPath
|
|
59
|
+
? requireAbsoluteDeclarationPath(options.configurationPath, "--config")
|
|
60
|
+
: paths.setupDeclaration;
|
|
61
|
+
if (!(await pathExists(configurationPath))) {
|
|
62
|
+
if (!options.configurationPath)
|
|
63
|
+
return null;
|
|
64
|
+
throw declarationError({
|
|
65
|
+
code: "RESEARCH_SETUP_DECLARATION_NOT_FOUND",
|
|
66
|
+
step: "declarative-discovery",
|
|
67
|
+
reason: "The explicitly selected declarative setup file does not exist.",
|
|
68
|
+
minimumAction: "Create the reviewed YAML file or remove --config to use the Wizard.",
|
|
69
|
+
retryArgs: ["research", "setup", "init", "--workspace", root, "--json"],
|
|
70
|
+
exitCode: 2,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const environmentPath = options.environmentPath
|
|
74
|
+
? requireAbsoluteDeclarationPath(options.environmentPath, "--env-file")
|
|
75
|
+
: (await pathExists(paths.setupDeclarationEnv))
|
|
76
|
+
? paths.setupDeclarationEnv
|
|
77
|
+
: null;
|
|
78
|
+
if (options.environmentPath && !(await pathExists(environmentPath))) {
|
|
79
|
+
throw declarationError({
|
|
80
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_INVALID",
|
|
81
|
+
step: "declarative-environment",
|
|
82
|
+
reason: "The explicitly selected setup environment file does not exist.",
|
|
83
|
+
minimumAction: "Create the owner-only env file or omit --env-file.",
|
|
84
|
+
retryArgs: ["research", "setup", "--workspace", root, "--json"],
|
|
85
|
+
exitCode: 2,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return { configurationPath, environmentPath };
|
|
89
|
+
}
|
|
90
|
+
export async function loadResearchSetupDeclaration(input) {
|
|
91
|
+
const root = await resolveResearchSetupWorkspacePath(input.workspace);
|
|
92
|
+
const discovered = await discoverResearchSetupDeclaration(root, {
|
|
93
|
+
...(input.configurationPath === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { configurationPath: input.configurationPath }),
|
|
96
|
+
...(input.environmentPath === undefined ? {} : { environmentPath: input.environmentPath }),
|
|
97
|
+
});
|
|
98
|
+
if (!discovered) {
|
|
99
|
+
throw declarationError({
|
|
100
|
+
code: "RESEARCH_SETUP_DECLARATION_NOT_FOUND",
|
|
101
|
+
step: "declarative-discovery",
|
|
102
|
+
reason: "No workspace-local declarative setup file was found.",
|
|
103
|
+
minimumAction: "Run setup init to create a reviewed template, or use the interactive Wizard.",
|
|
104
|
+
retryArgs: ["research", "setup", "init", "--workspace", root, "--json"],
|
|
105
|
+
exitCode: 2,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const configurationText = await readRegularBoundedFile({
|
|
109
|
+
path: discovered.configurationPath,
|
|
110
|
+
maximumBytes: MAX_DECLARATION_BYTES,
|
|
111
|
+
code: "RESEARCH_SETUP_DECLARATION_INVALID",
|
|
112
|
+
step: "declarative-configuration",
|
|
113
|
+
label: "Declarative setup YAML",
|
|
114
|
+
});
|
|
115
|
+
const declaration = parseResearchSetupDeclaration(configurationText);
|
|
116
|
+
validateExplicitCatalogDeclaration(declaration);
|
|
117
|
+
validateRequiredVerification(declaration);
|
|
118
|
+
const resolvedSelection = resolveDeclarationSelection(declaration);
|
|
119
|
+
const configurationSha256 = sha256Text(canonicalJson(declaration));
|
|
120
|
+
const sourceEnvironment = input.environment ?? process.env;
|
|
121
|
+
const environment = { ...sourceEnvironment };
|
|
122
|
+
const referencedNames = new Set(Object.values(declaration.credentials).map((credential) => credential.environment));
|
|
123
|
+
if (discovered.environmentPath) {
|
|
124
|
+
const fileEnvironment = await readDeclarationEnvironment(discovered.environmentPath);
|
|
125
|
+
const undeclared = [...fileEnvironment.keys()].filter((name) => !referencedNames.has(name));
|
|
126
|
+
if (undeclared.length) {
|
|
127
|
+
throw declarationError({
|
|
128
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_INVALID",
|
|
129
|
+
step: "declarative-environment",
|
|
130
|
+
reason: "The setup environment file contains variables not referenced by setup.yaml.",
|
|
131
|
+
minimumAction: "Keep only environment variable names explicitly listed by setup.yaml credentials.",
|
|
132
|
+
retryArgs: ["research", "setup", "--workspace", root, "--json"],
|
|
133
|
+
exitCode: 2,
|
|
134
|
+
diagnostics: { undeclaredVariableCount: undeclared.length },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
for (const [name, value] of fileEnvironment) {
|
|
138
|
+
const ambient = sourceEnvironment[name];
|
|
139
|
+
if (ambient !== undefined && ambient !== value) {
|
|
140
|
+
throw declarationError({
|
|
141
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_CONFLICT",
|
|
142
|
+
step: "declarative-environment",
|
|
143
|
+
reason: "A credential source differs between the owner environment and setup.env.",
|
|
144
|
+
minimumAction: "Remove one source or make the named values identical; setup will not choose silently.",
|
|
145
|
+
retryArgs: ["research", "setup", "--workspace", root, "--json"],
|
|
146
|
+
exitCode: 2,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
environment[name] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const [credentialId, credential] of Object.entries(declaration.credentials)) {
|
|
153
|
+
if (!credential.enabled &&
|
|
154
|
+
Buffer.byteLength(environment[credential.environment] ?? "", "utf8") > 0) {
|
|
155
|
+
throw declarationError({
|
|
156
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_INVALID",
|
|
157
|
+
step: "declarative-environment",
|
|
158
|
+
reason: `A disabled credential has a non-empty configured value: ${credentialId}.`,
|
|
159
|
+
minimumAction: "Enable the credential explicitly in setup.yaml or remove its value from setup.env and the owner environment.",
|
|
160
|
+
retryArgs: ["research", "setup", "--workspace", root, "--json"],
|
|
161
|
+
exitCode: 2,
|
|
162
|
+
diagnostics: { disabledCredentialId: credentialId },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const credentialEnvironment = {};
|
|
167
|
+
for (const [credentialId, credential] of Object.entries(declaration.credentials)) {
|
|
168
|
+
if (credential.enabled)
|
|
169
|
+
credentialEnvironment[credentialId] = credential.environment;
|
|
170
|
+
}
|
|
171
|
+
const settings = {};
|
|
172
|
+
for (const [settingId, setting] of Object.entries(declaration.settings)) {
|
|
173
|
+
if (setting.enabled)
|
|
174
|
+
settings[settingId] = setting.value;
|
|
175
|
+
}
|
|
176
|
+
const planInput = {
|
|
177
|
+
...(declaration.workspace.name === undefined ? {} : { name: declaration.workspace.name }),
|
|
178
|
+
mode: declaration.workspace.mode,
|
|
179
|
+
evidenceProfile: resolvedSelection.planEvidenceProfile,
|
|
180
|
+
skillIds: [...resolvedSelection.nonBraveSkillIds],
|
|
181
|
+
scope: declaration.install.scope,
|
|
182
|
+
agents: [...declaration.install.agents],
|
|
183
|
+
acceptedLicenseIds: [...declaration.acceptedLicenseIds],
|
|
184
|
+
credentialEnvironment,
|
|
185
|
+
settings,
|
|
186
|
+
agentRoutes: structuredClone(declaration.agentRoutes),
|
|
187
|
+
liveChecks: declaration.verification.live,
|
|
188
|
+
allowSyntheticUnstructureUpload: declaration.verification.allowSyntheticUnstructureUpload,
|
|
189
|
+
agentSmoke: declaration.verification.agentSmoke,
|
|
190
|
+
confirmNetworkDownloads: declaration.confirmations.networkDownloads,
|
|
191
|
+
confirmGlobalMutation: declaration.confirmations.globalMutation,
|
|
192
|
+
confirmAgentSmokeCost: declaration.confirmations.agentSmokeCost,
|
|
193
|
+
replacePlan: declaration.replaceExistingPlan,
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
workspace: root,
|
|
197
|
+
configurationPath: discovered.configurationPath,
|
|
198
|
+
environmentPath: discovered.environmentPath,
|
|
199
|
+
configurationSha256,
|
|
200
|
+
planInput,
|
|
201
|
+
environment,
|
|
202
|
+
publicSummary: {
|
|
203
|
+
mode: declaration.workspace.mode,
|
|
204
|
+
evidenceProfile: resolvedSelection.declarativeEvidenceProfile,
|
|
205
|
+
selectedSkillIds: [...resolvedSelection.selectedSkillIds],
|
|
206
|
+
enabledCredentialIds: Object.keys(credentialEnvironment).sort(),
|
|
207
|
+
declaredCredentialIds: Object.keys(declaration.credentials).sort(),
|
|
208
|
+
verification: { ...declaration.verification },
|
|
209
|
+
environmentFileUsed: discovered.environmentPath !== null,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export async function executeResearchSetupDeclaration(input) {
|
|
214
|
+
const loaded = await loadResearchSetupDeclaration(input);
|
|
215
|
+
const operations = {
|
|
216
|
+
createPlan: input.operations?.createPlan ?? createResearchSetupPlan,
|
|
217
|
+
loadPlan: input.operations?.loadPlan ?? loadAndVerifyResearchSetupPlan,
|
|
218
|
+
applyPlan: input.operations?.applyPlan ?? applyResearchSetupPlan,
|
|
219
|
+
};
|
|
220
|
+
const paths = workspacePaths(loaded.workspace);
|
|
221
|
+
let reusedPlan = false;
|
|
222
|
+
let plan;
|
|
223
|
+
if (await pathExists(paths.setupPlan)) {
|
|
224
|
+
const existingPlan = await operations.loadPlan(paths.setupPlan);
|
|
225
|
+
const binding = await loadDeclarationBinding(paths.setupDeclarationBinding);
|
|
226
|
+
if (binding &&
|
|
227
|
+
binding.configurationSha256 === loaded.configurationSha256 &&
|
|
228
|
+
binding.planSha256 === existingPlan.planSha256) {
|
|
229
|
+
reusedPlan = true;
|
|
230
|
+
plan = existingPlan;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
if (!loaded.planInput.replacePlan) {
|
|
234
|
+
throw declarationError({
|
|
235
|
+
code: "RESEARCH_SETUP_DECLARATION_CHANGED",
|
|
236
|
+
step: "declarative-plan",
|
|
237
|
+
reason: "The current immutable plan is not bound to this declarative configuration.",
|
|
238
|
+
minimumAction: "Review the configuration change and set replaceExistingPlan: true for one explicit replacement.",
|
|
239
|
+
retryArgs: ["research", "setup", "status", "--workspace", loaded.workspace, "--json"],
|
|
240
|
+
exitCode: 3,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
plan = await operations.createPlan({
|
|
244
|
+
...loaded.planInput,
|
|
245
|
+
workspace: loaded.workspace,
|
|
246
|
+
environment: loaded.environment,
|
|
247
|
+
replacePlan: true,
|
|
248
|
+
declarativeConfigurationSha256: loaded.configurationSha256,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
if (await pathExists(paths.setupDeclarationBinding)) {
|
|
254
|
+
throw declarationError({
|
|
255
|
+
code: "RESEARCH_SETUP_DECLARATION_BINDING_INVALID",
|
|
256
|
+
step: "declarative-plan",
|
|
257
|
+
reason: "A declarative binding exists without its immutable setup plan.",
|
|
258
|
+
minimumAction: "Stop and audit the setup control directory; do not reconstruct state by hand.",
|
|
259
|
+
retryArgs: ["research", "setup", "status", "--workspace", loaded.workspace, "--json"],
|
|
260
|
+
exitCode: 3,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
plan = await operations.createPlan({
|
|
264
|
+
...loaded.planInput,
|
|
265
|
+
workspace: loaded.workspace,
|
|
266
|
+
environment: loaded.environment,
|
|
267
|
+
replacePlan: false,
|
|
268
|
+
declarativeConfigurationSha256: loaded.configurationSha256,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const applied = await operations.applyPlan(paths.setupPlan, {
|
|
272
|
+
environment: loaded.environment,
|
|
273
|
+
});
|
|
274
|
+
const ready = applied.state.status === "ready" && applied.report?.overallReadiness === "READY";
|
|
275
|
+
const result = {
|
|
276
|
+
schemaVersion: 1,
|
|
277
|
+
mode: "declarative",
|
|
278
|
+
status: ready ? "ready" : "incomplete",
|
|
279
|
+
exitCode: ready ? 0 : 3,
|
|
280
|
+
reusedPlan,
|
|
281
|
+
configuration: {
|
|
282
|
+
path: loaded.configurationPath,
|
|
283
|
+
sha256: loaded.configurationSha256,
|
|
284
|
+
environmentFileUsed: loaded.environmentPath !== null,
|
|
285
|
+
},
|
|
286
|
+
plan,
|
|
287
|
+
state: applied.state,
|
|
288
|
+
report: applied.report,
|
|
289
|
+
};
|
|
290
|
+
return sanitizeResearchValue(result, configuredResearchSecrets(loaded.environment));
|
|
291
|
+
}
|
|
292
|
+
function parseResearchSetupDeclaration(source) {
|
|
293
|
+
const document = parseDocument(source, {
|
|
294
|
+
schema: "core",
|
|
295
|
+
strict: true,
|
|
296
|
+
uniqueKeys: true,
|
|
297
|
+
version: "1.2",
|
|
298
|
+
});
|
|
299
|
+
if (document.errors.length || document.warnings.length) {
|
|
300
|
+
throw invalidDeclaration("The declarative setup YAML is malformed.", {
|
|
301
|
+
yamlErrorCount: document.errors.length,
|
|
302
|
+
yamlWarningCount: document.warnings.length,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
let containsAliasOrAnchor = false;
|
|
306
|
+
visit(document, (_key, node) => {
|
|
307
|
+
if (isAlias(node) ||
|
|
308
|
+
(typeof node === "object" &&
|
|
309
|
+
node !== null &&
|
|
310
|
+
"anchor" in node &&
|
|
311
|
+
typeof node.anchor === "string" &&
|
|
312
|
+
node.anchor.length > 0)) {
|
|
313
|
+
containsAliasOrAnchor = true;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (containsAliasOrAnchor) {
|
|
317
|
+
throw invalidDeclaration("YAML aliases and anchors are not supported.");
|
|
318
|
+
}
|
|
319
|
+
let value;
|
|
320
|
+
try {
|
|
321
|
+
value = document.toJS({ maxAliasCount: 0 });
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
throw invalidDeclaration("The declarative setup YAML could not be converted safely.");
|
|
325
|
+
}
|
|
326
|
+
if (!validateDeclaration(value)) {
|
|
327
|
+
throw invalidDeclaration("The declarative setup YAML does not match the closed schema.", {
|
|
328
|
+
schemaErrors: declarationSchemaErrors(validateDeclaration.errors),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
function validateRequiredVerification(declaration) {
|
|
334
|
+
const missing = [];
|
|
335
|
+
if (!declaration.verification.live)
|
|
336
|
+
missing.push("/verification/live");
|
|
337
|
+
if (!declaration.verification.agentSmoke)
|
|
338
|
+
missing.push("/verification/agentSmoke");
|
|
339
|
+
if (!declaration.confirmations.agentSmokeCost) {
|
|
340
|
+
missing.push("/confirmations/agentSmokeCost");
|
|
341
|
+
}
|
|
342
|
+
if ((declaration.install.scope === "global") !== declaration.confirmations.globalMutation) {
|
|
343
|
+
missing.push("/confirmations/globalMutation");
|
|
344
|
+
}
|
|
345
|
+
if (missing.length) {
|
|
346
|
+
throw invalidDeclaration("Declarative setup must authorize full live and independent-reviewer verification.", { incompleteFields: missing });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function readDeclarationEnvironment(path) {
|
|
350
|
+
const info = await lstat(path).catch(() => null);
|
|
351
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
352
|
+
throw unsafeDeclarationEnvironment("The setup environment file must be a regular non-symlink file.");
|
|
353
|
+
}
|
|
354
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
355
|
+
throw unsafeDeclarationEnvironment("The setup environment file must not be readable or writable by group or other users.");
|
|
356
|
+
}
|
|
357
|
+
if (info.size > MAX_DECLARATION_ENV_BYTES) {
|
|
358
|
+
throw unsafeDeclarationEnvironment("The setup environment file exceeds 64 KiB.");
|
|
359
|
+
}
|
|
360
|
+
const content = await readFile(path, "utf8");
|
|
361
|
+
if (content.includes("\0")) {
|
|
362
|
+
throw invalidDeclarationEnvironment("The setup environment file contains a NUL byte.");
|
|
363
|
+
}
|
|
364
|
+
const values = new Map();
|
|
365
|
+
for (const rawLine of content.split(/\r\n|\n|\r/)) {
|
|
366
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith("#"))
|
|
367
|
+
continue;
|
|
368
|
+
if (rawLine.startsWith("export ")) {
|
|
369
|
+
throw invalidDeclarationEnvironment("Shell export syntax is not supported in setup.env.");
|
|
370
|
+
}
|
|
371
|
+
const equals = rawLine.indexOf("=");
|
|
372
|
+
if (equals <= 0) {
|
|
373
|
+
throw invalidDeclarationEnvironment("Each setup.env entry must use NAME=value syntax.");
|
|
374
|
+
}
|
|
375
|
+
const name = rawLine.slice(0, equals);
|
|
376
|
+
let value = rawLine.slice(equals + 1);
|
|
377
|
+
if (!ENVIRONMENT_NAME.test(name)) {
|
|
378
|
+
throw invalidDeclarationEnvironment("A setup.env variable name is malformed.");
|
|
379
|
+
}
|
|
380
|
+
if (values.has(name)) {
|
|
381
|
+
throw invalidDeclarationEnvironment("The setup environment file contains a duplicate name.");
|
|
382
|
+
}
|
|
383
|
+
if (value.length >= 2 &&
|
|
384
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
385
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
386
|
+
value = value.slice(1, -1);
|
|
387
|
+
}
|
|
388
|
+
if (Buffer.byteLength(value, "utf8") > MAX_DECLARATION_ENV_VALUE_BYTES) {
|
|
389
|
+
throw invalidDeclarationEnvironment("A setup.env value exceeds the supported bound.");
|
|
390
|
+
}
|
|
391
|
+
values.set(name, value);
|
|
392
|
+
}
|
|
393
|
+
return values;
|
|
394
|
+
}
|
|
395
|
+
async function readRegularBoundedFile(input) {
|
|
396
|
+
const info = await lstat(input.path).catch(() => null);
|
|
397
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size > input.maximumBytes) {
|
|
398
|
+
throw declarationError({
|
|
399
|
+
code: input.code,
|
|
400
|
+
step: input.step,
|
|
401
|
+
reason: `${input.label} must be a bounded regular non-symlink file.`,
|
|
402
|
+
minimumAction: "Restore the reviewed workspace-local file and retry.",
|
|
403
|
+
retryArgs: ["research", "setup", "--help"],
|
|
404
|
+
exitCode: 2,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return readFile(input.path, "utf8");
|
|
408
|
+
}
|
|
409
|
+
async function loadDeclarationBinding(path) {
|
|
410
|
+
if (!(await pathExists(path)))
|
|
411
|
+
return null;
|
|
412
|
+
const info = await lstat(path).catch(() => null);
|
|
413
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size > 16 * 1024) {
|
|
414
|
+
throw invalidDeclarationBinding();
|
|
415
|
+
}
|
|
416
|
+
const value = await readJsonFile(path, "Declarative setup binding").catch(() => null);
|
|
417
|
+
if (!isObject(value) ||
|
|
418
|
+
Object.keys(value).length !== 4 ||
|
|
419
|
+
value.schemaVersion !== 1 ||
|
|
420
|
+
value.kind !== "tiangong-research-setup-declaration-binding" ||
|
|
421
|
+
typeof value.configurationSha256 !== "string" ||
|
|
422
|
+
!/^[0-9a-f]{64}$/.test(value.configurationSha256) ||
|
|
423
|
+
typeof value.planSha256 !== "string" ||
|
|
424
|
+
!/^[0-9a-f]{64}$/.test(value.planSha256)) {
|
|
425
|
+
throw invalidDeclarationBinding();
|
|
426
|
+
}
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
async function writeNewTextFile(path, content, mode) {
|
|
430
|
+
const handle = await open(path, "wx", mode).catch(() => null);
|
|
431
|
+
if (!handle) {
|
|
432
|
+
throw declarationError({
|
|
433
|
+
code: "RESEARCH_SETUP_DECLARATION_EXISTS",
|
|
434
|
+
step: "declarative-init",
|
|
435
|
+
reason: "A declarative setup target appeared and was not overwritten.",
|
|
436
|
+
minimumAction: "Review the existing file before retrying.",
|
|
437
|
+
retryArgs: ["research", "setup", "--help"],
|
|
438
|
+
exitCode: 3,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
await handle.writeFile(content, "utf8");
|
|
443
|
+
await handle.sync();
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
await handle.close();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function defaultDeclaration(root) {
|
|
450
|
+
const enabledSkillIds = new Set(RESEARCH_SETUP_SKILLS.filter((skill) => skill.defaultSelected).map((skill) => skill.id));
|
|
451
|
+
return {
|
|
452
|
+
schemaVersion: 2,
|
|
453
|
+
kind: "tiangong-research-setup",
|
|
454
|
+
workspace: { name: basename(root), mode: "production-research" },
|
|
455
|
+
install: { scope: "project", agents: ["codex"] },
|
|
456
|
+
selection: {
|
|
457
|
+
skills: Object.fromEntries(RESEARCH_SETUP_SKILLS.map((skill) => [
|
|
458
|
+
skill.id,
|
|
459
|
+
{
|
|
460
|
+
enabled: enabledSkillIds.has(skill.id),
|
|
461
|
+
licenseId: skill.license.id,
|
|
462
|
+
},
|
|
463
|
+
])),
|
|
464
|
+
},
|
|
465
|
+
acceptedLicenseIds: [],
|
|
466
|
+
credentials: Object.fromEntries(RESEARCH_SETUP_CREDENTIALS.map((credential) => [
|
|
467
|
+
credential.id,
|
|
468
|
+
{
|
|
469
|
+
requirement: declarationRequirement(credential, enabledSkillIds),
|
|
470
|
+
appliesTo: [...credential.requiredBy],
|
|
471
|
+
enabled: credential.required &&
|
|
472
|
+
credential.requiredBy.some((skillId) => enabledSkillIds.has(skillId)),
|
|
473
|
+
environment: credential.defaultEnvironmentName,
|
|
474
|
+
},
|
|
475
|
+
])),
|
|
476
|
+
settings: Object.fromEntries(RESEARCH_SETUP_SETTINGS.map((setting) => [
|
|
477
|
+
setting.id,
|
|
478
|
+
{
|
|
479
|
+
requirement: declarationRequirement(setting, enabledSkillIds),
|
|
480
|
+
appliesTo: [...setting.requiredBy],
|
|
481
|
+
enabled: setting.required && setting.requiredBy.some((skillId) => enabledSkillIds.has(skillId)),
|
|
482
|
+
value: setting.defaultValue,
|
|
483
|
+
},
|
|
484
|
+
])),
|
|
485
|
+
agentRoutes: {
|
|
486
|
+
producerAgent: "codex",
|
|
487
|
+
reviewerAgent: "claude",
|
|
488
|
+
producerModel: null,
|
|
489
|
+
reviewerModel: null,
|
|
490
|
+
producerPricing: null,
|
|
491
|
+
reviewerPricing: null,
|
|
492
|
+
},
|
|
493
|
+
verification: {
|
|
494
|
+
live: true,
|
|
495
|
+
allowSyntheticUnstructureUpload: false,
|
|
496
|
+
agentSmoke: true,
|
|
497
|
+
},
|
|
498
|
+
confirmations: {
|
|
499
|
+
networkDownloads: false,
|
|
500
|
+
globalMutation: false,
|
|
501
|
+
agentSmokeCost: false,
|
|
502
|
+
},
|
|
503
|
+
replaceExistingPlan: false,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function declarationTemplate(value) {
|
|
507
|
+
return [
|
|
508
|
+
"# Every current catalog Skill, credential, and setting is explicit below.",
|
|
509
|
+
"# Review each enabled flag, license, endpoint, model, price, and confirmation.",
|
|
510
|
+
"# This file contains no secret values. Credential values belong only in owner-only",
|
|
511
|
+
"# setup.env, the same named owner environment variables, or the logical stores.",
|
|
512
|
+
"# Set confirmations to true only after the human owner accepts each action/cost.",
|
|
513
|
+
stringify(value, { lineWidth: 0 }).trimEnd(),
|
|
514
|
+
"",
|
|
515
|
+
].join("\n");
|
|
516
|
+
}
|
|
517
|
+
function declarationEnvironmentExample(declaration) {
|
|
518
|
+
return [
|
|
519
|
+
"# Copy this file to setup.env and run chmod 600 .tiangong-research/setup.env.",
|
|
520
|
+
"# Every catalog credential is visible. Leave disabled optional values empty.",
|
|
521
|
+
"# A non-empty disabled value is rejected instead of being used implicitly.",
|
|
522
|
+
"# Values are imported into owner-only logical stores and never into setup.yaml.",
|
|
523
|
+
...RESEARCH_SETUP_CREDENTIALS.flatMap((definition) => {
|
|
524
|
+
const choice = declaration.credentials[definition.id];
|
|
525
|
+
return [
|
|
526
|
+
"",
|
|
527
|
+
`# ${definition.id}: ${choice.requirement}; enabled=${String(choice.enabled)}`,
|
|
528
|
+
`${choice.environment}=`,
|
|
529
|
+
];
|
|
530
|
+
}),
|
|
531
|
+
"",
|
|
532
|
+
].join("\n");
|
|
533
|
+
}
|
|
534
|
+
function declarationRequirement(input, selectedSkillIds) {
|
|
535
|
+
if (!input.required)
|
|
536
|
+
return "optional";
|
|
537
|
+
if (input.requiredBy.length === 0)
|
|
538
|
+
return "required";
|
|
539
|
+
return input.requiredBy.some((skillId) => selectedSkillIds.has(skillId))
|
|
540
|
+
? "required"
|
|
541
|
+
: "conditional";
|
|
542
|
+
}
|
|
543
|
+
function requireAbsoluteDeclarationPath(path, option) {
|
|
544
|
+
if (!isAbsolute(path)) {
|
|
545
|
+
throw declarationError({
|
|
546
|
+
code: "RESEARCH_SETUP_DECLARATION_PATH_INVALID",
|
|
547
|
+
step: "declarative-discovery",
|
|
548
|
+
reason: `${option} must be an absolute file path.`,
|
|
549
|
+
minimumAction: "Pass an absolute reviewed path or use workspace-local auto-discovery.",
|
|
550
|
+
retryArgs: ["research", "setup", "--help"],
|
|
551
|
+
exitCode: 2,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return resolve(path);
|
|
555
|
+
}
|
|
556
|
+
function resolveDeclarationSelection(declaration) {
|
|
557
|
+
const selectedSkillIds = RESEARCH_SETUP_SKILLS.filter((skill) => declaration.selection.skills[skill.id].enabled).map((skill) => skill.id);
|
|
558
|
+
const enabledBraveIds = selectedSkillIds.filter((skillId) => BRAVE_SKILL_IDS.has(skillId));
|
|
559
|
+
const profiles = [
|
|
560
|
+
{ declarative: "none", plan: "none", skillIds: [] },
|
|
561
|
+
{
|
|
562
|
+
declarative: "brave-baseline",
|
|
563
|
+
plan: EXTERNAL_SKILL_PROFILE,
|
|
564
|
+
skillIds: ["brave.web-search", "brave.news-search"],
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
declarative: "brave-context",
|
|
568
|
+
plan: EXTERNAL_SKILL_CONTEXT_PROFILE,
|
|
569
|
+
skillIds: ["brave.web-search", "brave.news-search", "brave.llm-context"],
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
declarative: "brave-media",
|
|
573
|
+
plan: EXTERNAL_SKILL_MEDIA_PROFILE,
|
|
574
|
+
skillIds: [
|
|
575
|
+
"brave.web-search",
|
|
576
|
+
"brave.news-search",
|
|
577
|
+
"brave.llm-context",
|
|
578
|
+
"brave.images-search",
|
|
579
|
+
"brave.videos-search",
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
const matched = profiles.find((profile) => sameStringSet(enabledBraveIds, profile.skillIds));
|
|
584
|
+
if (!matched) {
|
|
585
|
+
throw invalidDeclaration("Enabled Brave Skills must form one complete supported evidence profile.", {
|
|
586
|
+
enabledBraveSkillIds: enabledBraveIds,
|
|
587
|
+
supportedProfiles: profiles.map((profile) => ({
|
|
588
|
+
id: profile.declarative,
|
|
589
|
+
skillIds: profile.skillIds,
|
|
590
|
+
})),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
selectedSkillIds,
|
|
595
|
+
nonBraveSkillIds: selectedSkillIds.filter((skillId) => !BRAVE_SKILL_IDS.has(skillId)),
|
|
596
|
+
declarativeEvidenceProfile: matched.declarative,
|
|
597
|
+
planEvidenceProfile: matched.plan,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function validateExplicitCatalogDeclaration(declaration) {
|
|
601
|
+
const problems = [];
|
|
602
|
+
validateExactRecordKeys(declaration.selection.skills, RESEARCH_SETUP_SKILLS.map((skill) => skill.id), "/selection/skills", problems);
|
|
603
|
+
validateExactRecordKeys(declaration.credentials, RESEARCH_SETUP_CREDENTIALS.map((credential) => credential.id), "/credentials", problems);
|
|
604
|
+
validateExactRecordKeys(declaration.settings, RESEARCH_SETUP_SETTINGS.map((setting) => setting.id), "/settings", problems);
|
|
605
|
+
const selectedSkillIds = new Set(RESEARCH_SETUP_SKILLS.filter((skill) => declaration.selection.skills[skill.id]?.enabled).map((skill) => skill.id));
|
|
606
|
+
for (const skill of RESEARCH_SETUP_SKILLS) {
|
|
607
|
+
const choice = declaration.selection.skills[skill.id];
|
|
608
|
+
if (choice && choice.licenseId !== skill.license.id) {
|
|
609
|
+
problems.push({ path: `/selection/skills/${skill.id}/licenseId`, rule: "catalog-drift" });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const environmentOwners = new Map();
|
|
613
|
+
for (const definition of RESEARCH_SETUP_CREDENTIALS) {
|
|
614
|
+
const choice = declaration.credentials[definition.id];
|
|
615
|
+
if (!choice)
|
|
616
|
+
continue;
|
|
617
|
+
validateCatalogChoiceMetadata(`/credentials/${definition.id}`, choice, definition, selectedSkillIds, problems);
|
|
618
|
+
const applies = definition.requiredBy.some((skillId) => selectedSkillIds.has(skillId));
|
|
619
|
+
if (definition.required && choice.enabled !== applies) {
|
|
620
|
+
problems.push({
|
|
621
|
+
path: `/credentials/${definition.id}/enabled`,
|
|
622
|
+
rule: applies ? "required-when-selected" : "disabled-when-unused",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
else if (!definition.required && !applies && choice.enabled) {
|
|
626
|
+
problems.push({
|
|
627
|
+
path: `/credentials/${definition.id}/enabled`,
|
|
628
|
+
rule: "disabled-when-unused",
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const priorOwner = environmentOwners.get(choice.environment);
|
|
632
|
+
if (priorOwner) {
|
|
633
|
+
problems.push({
|
|
634
|
+
path: `/credentials/${definition.id}/environment`,
|
|
635
|
+
rule: "duplicate-environment-name",
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
environmentOwners.set(choice.environment, definition.id);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const definition of RESEARCH_SETUP_SETTINGS) {
|
|
643
|
+
const choice = declaration.settings[definition.id];
|
|
644
|
+
if (!choice)
|
|
645
|
+
continue;
|
|
646
|
+
validateCatalogChoiceMetadata(`/settings/${definition.id}`, choice, definition, selectedSkillIds, problems);
|
|
647
|
+
const applies = definition.requiredBy.some((skillId) => selectedSkillIds.has(skillId));
|
|
648
|
+
if (definition.required && choice.enabled !== applies) {
|
|
649
|
+
problems.push({
|
|
650
|
+
path: `/settings/${definition.id}/enabled`,
|
|
651
|
+
rule: applies ? "required-when-selected" : "disabled-when-unused",
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
else if (!definition.required && !applies && choice.enabled) {
|
|
655
|
+
problems.push({
|
|
656
|
+
path: `/settings/${definition.id}/enabled`,
|
|
657
|
+
rule: "disabled-when-unused",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (choice.enabled && (choice.value === null || choice.value.length === 0)) {
|
|
661
|
+
problems.push({ path: `/settings/${definition.id}/value`, rule: "enabled-value-required" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (problems.length) {
|
|
665
|
+
throw invalidDeclaration("The declarative setup must explicitly and exactly match the current catalog.", { catalogProblems: problems.slice(0, 32), catalogProblemCount: problems.length });
|
|
666
|
+
}
|
|
667
|
+
resolveDeclarationSelection(declaration);
|
|
668
|
+
}
|
|
669
|
+
function validateCatalogChoiceMetadata(path, choice, definition, selectedSkillIds, problems) {
|
|
670
|
+
if (choice.requirement !== declarationRequirement(definition, selectedSkillIds)) {
|
|
671
|
+
problems.push({ path: `${path}/requirement`, rule: "catalog-drift" });
|
|
672
|
+
}
|
|
673
|
+
if (!sameStringSet(choice.appliesTo, definition.requiredBy)) {
|
|
674
|
+
problems.push({ path: `${path}/appliesTo`, rule: "catalog-drift" });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function validateExactRecordKeys(value, expectedKeys, path, problems) {
|
|
678
|
+
const expected = new Set(expectedKeys);
|
|
679
|
+
for (const key of expectedKeys) {
|
|
680
|
+
if (!Object.hasOwn(value, key))
|
|
681
|
+
problems.push({ path: `${path}/${key}`, rule: "missing" });
|
|
682
|
+
}
|
|
683
|
+
for (const key of Object.keys(value)) {
|
|
684
|
+
if (!expected.has(key))
|
|
685
|
+
problems.push({ path: `${path}/${key}`, rule: "unknown" });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function sameStringSet(left, right) {
|
|
689
|
+
return left.length === right.length && left.every((value) => right.includes(value));
|
|
690
|
+
}
|
|
691
|
+
function invalidDeclaration(reason, diagnostics) {
|
|
692
|
+
return declarationError({
|
|
693
|
+
code: "RESEARCH_SETUP_DECLARATION_INVALID",
|
|
694
|
+
step: "declarative-configuration",
|
|
695
|
+
reason,
|
|
696
|
+
minimumAction: "Correct setup.yaml against the generated closed template; secret values belong only in setup.env or the owner environment.",
|
|
697
|
+
retryArgs: ["research", "setup", "--help"],
|
|
698
|
+
exitCode: 2,
|
|
699
|
+
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function unsafeDeclarationEnvironment(reason) {
|
|
703
|
+
return declarationError({
|
|
704
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_UNSAFE",
|
|
705
|
+
step: "declarative-environment",
|
|
706
|
+
reason,
|
|
707
|
+
minimumAction: "Use a regular non-symlink file and run chmod 600 .tiangong-research/setup.env.",
|
|
708
|
+
retryArgs: ["research", "setup", "--help"],
|
|
709
|
+
exitCode: 2,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function invalidDeclarationEnvironment(reason) {
|
|
713
|
+
return declarationError({
|
|
714
|
+
code: "RESEARCH_SETUP_DECLARATION_ENV_INVALID",
|
|
715
|
+
step: "declarative-environment",
|
|
716
|
+
reason,
|
|
717
|
+
minimumAction: "Use one literal NAME=value line for each variable referenced by setup.yaml; shell evaluation is not supported.",
|
|
718
|
+
retryArgs: ["research", "setup", "--help"],
|
|
719
|
+
exitCode: 2,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function invalidDeclarationBinding() {
|
|
723
|
+
return declarationError({
|
|
724
|
+
code: "RESEARCH_SETUP_DECLARATION_BINDING_INVALID",
|
|
725
|
+
step: "declarative-plan",
|
|
726
|
+
reason: "The declarative configuration binding is malformed or unsafe.",
|
|
727
|
+
minimumAction: "Stop and audit the immutable plan and binding; do not edit either by hand.",
|
|
728
|
+
retryArgs: ["research", "setup", "status", "--json"],
|
|
729
|
+
exitCode: 3,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function declarationError(input) {
|
|
733
|
+
const reason = sanitizeResearchText(input.reason);
|
|
734
|
+
return new CliError(reason, {
|
|
735
|
+
code: input.code,
|
|
736
|
+
exitCode: input.exitCode,
|
|
737
|
+
details: sanitizeResearchRecord({
|
|
738
|
+
step: input.step,
|
|
739
|
+
reason,
|
|
740
|
+
minimumAction: input.minimumAction,
|
|
741
|
+
retryCommand: exactResearchCliCommand(input.retryArgs),
|
|
742
|
+
...(input.diagnostics === undefined ? {} : { diagnostics: input.diagnostics }),
|
|
743
|
+
}),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function declarationSchemaErrors(errors) {
|
|
747
|
+
return (errors ?? []).slice(0, 16).map((error) => ({
|
|
748
|
+
path: error.instancePath || "/",
|
|
749
|
+
rule: error.keyword,
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
const pricingSchema = {
|
|
753
|
+
anyOf: [
|
|
754
|
+
{ type: "null" },
|
|
755
|
+
{
|
|
756
|
+
type: "object",
|
|
757
|
+
additionalProperties: false,
|
|
758
|
+
required: [
|
|
759
|
+
"inputUsdPerMillionTokens",
|
|
760
|
+
"cachedInputUsdPerMillionTokens",
|
|
761
|
+
"outputUsdPerMillionTokens",
|
|
762
|
+
],
|
|
763
|
+
properties: {
|
|
764
|
+
inputUsdPerMillionTokens: { type: "number", minimum: 0 },
|
|
765
|
+
cachedInputUsdPerMillionTokens: { type: "number", minimum: 0 },
|
|
766
|
+
outputUsdPerMillionTokens: { type: "number", minimum: 0 },
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
};
|
|
771
|
+
const declarationSchema = {
|
|
772
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
773
|
+
type: "object",
|
|
774
|
+
additionalProperties: false,
|
|
775
|
+
required: [
|
|
776
|
+
"schemaVersion",
|
|
777
|
+
"kind",
|
|
778
|
+
"workspace",
|
|
779
|
+
"install",
|
|
780
|
+
"selection",
|
|
781
|
+
"acceptedLicenseIds",
|
|
782
|
+
"credentials",
|
|
783
|
+
"settings",
|
|
784
|
+
"agentRoutes",
|
|
785
|
+
"verification",
|
|
786
|
+
"confirmations",
|
|
787
|
+
"replaceExistingPlan",
|
|
788
|
+
],
|
|
789
|
+
properties: {
|
|
790
|
+
schemaVersion: { const: 2 },
|
|
791
|
+
kind: { const: "tiangong-research-setup" },
|
|
792
|
+
workspace: {
|
|
793
|
+
type: "object",
|
|
794
|
+
additionalProperties: false,
|
|
795
|
+
required: ["mode"],
|
|
796
|
+
properties: {
|
|
797
|
+
name: { type: "string", minLength: 1, maxLength: 100 },
|
|
798
|
+
mode: { enum: ["smoke-test", "production-research"] },
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
install: {
|
|
802
|
+
type: "object",
|
|
803
|
+
additionalProperties: false,
|
|
804
|
+
required: ["scope", "agents"],
|
|
805
|
+
properties: {
|
|
806
|
+
scope: { enum: ["project", "global"] },
|
|
807
|
+
agents: {
|
|
808
|
+
type: "array",
|
|
809
|
+
minItems: 1,
|
|
810
|
+
maxItems: 2,
|
|
811
|
+
uniqueItems: true,
|
|
812
|
+
items: { enum: ["codex", "claude-code"] },
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
selection: {
|
|
817
|
+
type: "object",
|
|
818
|
+
additionalProperties: false,
|
|
819
|
+
required: ["skills"],
|
|
820
|
+
properties: {
|
|
821
|
+
skills: {
|
|
822
|
+
type: "object",
|
|
823
|
+
minProperties: 1,
|
|
824
|
+
maxProperties: 64,
|
|
825
|
+
propertyNames: { type: "string", minLength: 1, maxLength: 200 },
|
|
826
|
+
additionalProperties: {
|
|
827
|
+
type: "object",
|
|
828
|
+
additionalProperties: false,
|
|
829
|
+
required: ["enabled", "licenseId"],
|
|
830
|
+
properties: {
|
|
831
|
+
enabled: { type: "boolean" },
|
|
832
|
+
licenseId: { type: "string", minLength: 1, maxLength: 200 },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
acceptedLicenseIds: {
|
|
839
|
+
type: "array",
|
|
840
|
+
maxItems: 64,
|
|
841
|
+
uniqueItems: true,
|
|
842
|
+
items: { type: "string", minLength: 1, maxLength: 200 },
|
|
843
|
+
},
|
|
844
|
+
credentials: {
|
|
845
|
+
type: "object",
|
|
846
|
+
minProperties: 1,
|
|
847
|
+
maxProperties: 64,
|
|
848
|
+
propertyNames: { type: "string", minLength: 1, maxLength: 200 },
|
|
849
|
+
additionalProperties: {
|
|
850
|
+
type: "object",
|
|
851
|
+
additionalProperties: false,
|
|
852
|
+
required: ["requirement", "appliesTo", "enabled", "environment"],
|
|
853
|
+
properties: {
|
|
854
|
+
requirement: { enum: ["required", "conditional", "optional"] },
|
|
855
|
+
appliesTo: {
|
|
856
|
+
type: "array",
|
|
857
|
+
maxItems: 64,
|
|
858
|
+
uniqueItems: true,
|
|
859
|
+
items: { type: "string", minLength: 1, maxLength: 200 },
|
|
860
|
+
},
|
|
861
|
+
enabled: { type: "boolean" },
|
|
862
|
+
environment: { type: "string", pattern: ENVIRONMENT_NAME.source },
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
settings: {
|
|
867
|
+
type: "object",
|
|
868
|
+
minProperties: 1,
|
|
869
|
+
maxProperties: 64,
|
|
870
|
+
propertyNames: { type: "string", minLength: 1, maxLength: 200 },
|
|
871
|
+
additionalProperties: {
|
|
872
|
+
type: "object",
|
|
873
|
+
additionalProperties: false,
|
|
874
|
+
required: ["requirement", "appliesTo", "enabled", "value"],
|
|
875
|
+
properties: {
|
|
876
|
+
requirement: { enum: ["required", "conditional", "optional"] },
|
|
877
|
+
appliesTo: {
|
|
878
|
+
type: "array",
|
|
879
|
+
maxItems: 64,
|
|
880
|
+
uniqueItems: true,
|
|
881
|
+
items: { type: "string", minLength: 1, maxLength: 200 },
|
|
882
|
+
},
|
|
883
|
+
enabled: { type: "boolean" },
|
|
884
|
+
value: { type: ["string", "null"], maxLength: 4096 },
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
agentRoutes: {
|
|
889
|
+
type: "object",
|
|
890
|
+
additionalProperties: false,
|
|
891
|
+
required: ["producerAgent", "reviewerAgent"],
|
|
892
|
+
properties: {
|
|
893
|
+
producerAgent: { enum: ["codex", "claude"] },
|
|
894
|
+
reviewerAgent: { enum: ["codex", "claude"] },
|
|
895
|
+
producerModel: { type: ["string", "null"], maxLength: 200 },
|
|
896
|
+
reviewerModel: { type: ["string", "null"], maxLength: 200 },
|
|
897
|
+
producerPricing: pricingSchema,
|
|
898
|
+
reviewerPricing: pricingSchema,
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
verification: {
|
|
902
|
+
type: "object",
|
|
903
|
+
additionalProperties: false,
|
|
904
|
+
required: ["live", "allowSyntheticUnstructureUpload", "agentSmoke"],
|
|
905
|
+
properties: {
|
|
906
|
+
live: { type: "boolean" },
|
|
907
|
+
allowSyntheticUnstructureUpload: { type: "boolean" },
|
|
908
|
+
agentSmoke: { type: "boolean" },
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
confirmations: {
|
|
912
|
+
type: "object",
|
|
913
|
+
additionalProperties: false,
|
|
914
|
+
required: ["networkDownloads", "globalMutation", "agentSmokeCost"],
|
|
915
|
+
properties: {
|
|
916
|
+
networkDownloads: { type: "boolean" },
|
|
917
|
+
globalMutation: { type: "boolean" },
|
|
918
|
+
agentSmokeCost: { type: "boolean" },
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
replaceExistingPlan: { type: "boolean" },
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
|
|
925
|
+
const validateDeclaration = ajv.compile(declarationSchema);
|
|
926
|
+
//# sourceMappingURL=setup-declarative.js.map
|