@tiangong-ai/cli 0.0.49 → 0.0.50
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 +43 -4
- package/dist/research/setup-command.js +52 -0
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/setup-audit-bundle.d.ts +69 -0
- package/dist/research/workspace/setup-audit-bundle.js +1695 -0
- package/dist/research/workspace/setup-audit-bundle.js.map +1 -0
- package/dist/research/workspace/setup-catalog.d.ts +1 -1
- package/dist/research/workspace/setup-catalog.js +176 -9
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup.js +414 -23
- package/dist/research/workspace/setup.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CliError } from "../../errors.js";
|
|
5
|
+
import { parseCapabilityDeclarations, verifyCapabilities } from "./capabilities.js";
|
|
6
|
+
import { configuredResearchSecrets, sanitizeResearchText } from "./sanitization.js";
|
|
7
|
+
import { loadAndVerifyResearchSetupPlan } from "./setup.js";
|
|
8
|
+
import { canonicalJson, isObject, pathExists, regularTreeFiles, resolveContained, safeRelativePath, sha256Bytes, sha256File, sha256Text, workspacePaths, writeBytesAtomic, writeJsonAtomic, } from "./storage.js";
|
|
9
|
+
import { verifyDoctorAttestation, withWorkspaceLock } from "./workspace.js";
|
|
10
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
11
|
+
const MAX_TEXT_SCAN_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const MAX_SECRET_STORE_BYTES = 64 * 1024;
|
|
13
|
+
export async function exportSetupAuditBundle(input) {
|
|
14
|
+
return withWorkspaceLock(input.root, "research.setup.audit.export", async () => {
|
|
15
|
+
const root = await realpath(resolve(input.root));
|
|
16
|
+
const destination = await validateNewDestination(input.destination);
|
|
17
|
+
const paths = workspacePaths(root);
|
|
18
|
+
const secrets = await setupAuditSecrets(paths, input.environment ?? process.env);
|
|
19
|
+
const temporary = join(dirname(destination), `.${basename(destination)}.${process.pid}.${randomUUID()}.tmp`);
|
|
20
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
21
|
+
try {
|
|
22
|
+
const sources = {
|
|
23
|
+
setupPlan: await requiredSourceSnapshot(paths.setupPlan, "setup plan"),
|
|
24
|
+
setupState: await requiredSourceSnapshot(paths.setupState, "setup state"),
|
|
25
|
+
setupReport: await optionalSourceSnapshot(paths.setupReport, "setup report"),
|
|
26
|
+
runtimeLock: await optionalSourceSnapshot(paths.runtimeLock, "runtime lock"),
|
|
27
|
+
capabilityDeclarations: await optionalSourceSnapshot(paths.capabilityDeclarations, "capability declarations"),
|
|
28
|
+
capabilityLock: await optionalSourceSnapshot(paths.capabilityLock, "capability lock"),
|
|
29
|
+
doctorAttestation: await optionalSourceSnapshot(paths.doctorAttestation, "doctor attestation"),
|
|
30
|
+
setupDeclarationBinding: await optionalSourceSnapshot(paths.setupDeclarationBinding, "setup declaration binding"),
|
|
31
|
+
};
|
|
32
|
+
const planSnapshotPath = join(temporary, ".source-setup-plan.json");
|
|
33
|
+
await writeBytesAtomic(planSnapshotPath, sources.setupPlan.bytes, 0o600);
|
|
34
|
+
const plan = await loadAndVerifyResearchSetupPlan(planSnapshotPath);
|
|
35
|
+
await rm(planSnapshotPath, { force: true });
|
|
36
|
+
if (resolve(plan.workspace.path) !== root) {
|
|
37
|
+
throw setupAuditError("Setup plan workspace binding does not match the export workspace.");
|
|
38
|
+
}
|
|
39
|
+
const stateProof = portableSetupState(sources.setupState.value, sources.setupState.sha256);
|
|
40
|
+
const reportProof = sources.setupReport
|
|
41
|
+
? portableSetupReport(sources.setupReport.value, sources.setupReport.sha256)
|
|
42
|
+
: null;
|
|
43
|
+
await writeJsonAtomic(join(temporary, "control", "setup-plan.portable.json"), portableSetupPlan(plan, sources.setupPlan.sha256), 0o444);
|
|
44
|
+
await writeJsonAtomic(join(temporary, "control", "setup-state.portable.json"), stateProof, 0o444);
|
|
45
|
+
if (reportProof) {
|
|
46
|
+
await writeJsonAtomic(join(temporary, "control", "setup-report.portable.json"), reportProof, 0o444);
|
|
47
|
+
}
|
|
48
|
+
if (sources.runtimeLock) {
|
|
49
|
+
parseRuntimeLock(sources.runtimeLock.value);
|
|
50
|
+
await writeBytesAtomic(join(temporary, "control", "runtime-lock.json"), sources.runtimeLock.bytes, 0o444);
|
|
51
|
+
}
|
|
52
|
+
if (sources.capabilityDeclarations) {
|
|
53
|
+
const declarations = parseCapabilityDeclarations(sources.capabilityDeclarations.value);
|
|
54
|
+
await writeJsonAtomic(join(temporary, "control", "capabilities.portable.json"), portableCapabilities(declarations, sources.capabilityDeclarations.sha256), 0o444);
|
|
55
|
+
}
|
|
56
|
+
if (sources.capabilityLock) {
|
|
57
|
+
if (!sources.capabilityDeclarations) {
|
|
58
|
+
throw setupAuditError("Capability lock exists without capability declarations.");
|
|
59
|
+
}
|
|
60
|
+
const verification = await verifyCapabilities(root);
|
|
61
|
+
if (verification.status !== "verified") {
|
|
62
|
+
throw setupAuditError("Capability lock is not verified and cannot be exported.");
|
|
63
|
+
}
|
|
64
|
+
await Promise.all([
|
|
65
|
+
assertSourceUnchanged(paths.capabilityDeclarations, sources.capabilityDeclarations, "capability declarations"),
|
|
66
|
+
assertSourceUnchanged(paths.capabilityLock, sources.capabilityLock, "capability lock"),
|
|
67
|
+
]);
|
|
68
|
+
const lock = parseCapabilityLock(sources.capabilityLock.value);
|
|
69
|
+
await writeJsonAtomic(join(temporary, "control", "capabilities-lock.portable.json"), portableCapabilityLock(lock, sources.capabilityLock.sha256), 0o444);
|
|
70
|
+
}
|
|
71
|
+
let doctorBinding = null;
|
|
72
|
+
if (sources.doctorAttestation) {
|
|
73
|
+
const attestation = parseDoctorAttestation(sources.doctorAttestation.value);
|
|
74
|
+
const verification = await verifyDoctorAttestation(root);
|
|
75
|
+
if (verification.status === "missing" ||
|
|
76
|
+
verification.status === "invalid" ||
|
|
77
|
+
!verification.attestation) {
|
|
78
|
+
throw new CliError("Doctor attestation is invalid and cannot be exported.", {
|
|
79
|
+
code: "RESEARCH_SETUP_AUDIT_ATTESTATION_INVALID",
|
|
80
|
+
exitCode: 3,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (verification.attestation.attestationSha256 !== attestation.attestationSha256) {
|
|
84
|
+
throw setupAuditError("Doctor attestation changed during setup audit export.");
|
|
85
|
+
}
|
|
86
|
+
await assertSourceUnchanged(paths.doctorAttestation, sources.doctorAttestation, "doctor attestation");
|
|
87
|
+
await writeJsonAtomic(join(temporary, "control", "doctor-attestation.json"), portableDoctorAttestation(attestation, sources.doctorAttestation.sha256), 0o444);
|
|
88
|
+
doctorBinding = {
|
|
89
|
+
attestationSha256: attestation.attestationSha256,
|
|
90
|
+
sourceFileSha256: sources.doctorAttestation.sha256,
|
|
91
|
+
verificationStatus: verification.status,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (sources.setupDeclarationBinding) {
|
|
95
|
+
assertPortableJsonValue(sources.setupDeclarationBinding.value, [], secrets);
|
|
96
|
+
parseSetupDeclarationBinding(sources.setupDeclarationBinding.value);
|
|
97
|
+
await writeBytesAtomic(join(temporary, "control", "setup-declaration-binding.json"), sources.setupDeclarationBinding.bytes, 0o444);
|
|
98
|
+
}
|
|
99
|
+
await assertPortableTextFiles(temporary, [root, ...plan.install.targets.map((item) => item.root)], secrets);
|
|
100
|
+
const files = await bundleFileRecords(temporary);
|
|
101
|
+
const reportReadiness = portableReadiness(reportProof, stateProof.status);
|
|
102
|
+
const manifestCore = {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
kind: "tiangong-setup-audit-bundle",
|
|
105
|
+
createdAt: new Date().toISOString(),
|
|
106
|
+
setup: {
|
|
107
|
+
planId: plan.planId,
|
|
108
|
+
planSha256: plan.planSha256,
|
|
109
|
+
cliVersion: plan.cli.version,
|
|
110
|
+
mode: plan.workspace.mode,
|
|
111
|
+
selectedSkillIds: [...plan.selection.skillIds],
|
|
112
|
+
},
|
|
113
|
+
readiness: reportReadiness,
|
|
114
|
+
sourceBindings: {
|
|
115
|
+
setupPlan: {
|
|
116
|
+
planSha256: plan.planSha256,
|
|
117
|
+
sourceFileSha256: sources.setupPlan.sha256,
|
|
118
|
+
},
|
|
119
|
+
setupStateFileSha256: sources.setupState.sha256,
|
|
120
|
+
setupReportFileSha256: sources.setupReport?.sha256 ?? null,
|
|
121
|
+
runtimeLockFileSha256: sources.runtimeLock?.sha256 ?? null,
|
|
122
|
+
capabilityDeclarationsFileSha256: sources.capabilityDeclarations?.sha256 ?? null,
|
|
123
|
+
capabilityLockFileSha256: sources.capabilityLock?.sha256 ?? null,
|
|
124
|
+
doctorAttestation: doctorBinding,
|
|
125
|
+
setupDeclarationBindingFileSha256: sources.setupDeclarationBinding?.sha256 ?? null,
|
|
126
|
+
sourceWorkspacePathSha256: sha256Text(root),
|
|
127
|
+
},
|
|
128
|
+
availability: {
|
|
129
|
+
setupReport: sources.setupReport !== null,
|
|
130
|
+
runtimeLock: sources.runtimeLock !== null,
|
|
131
|
+
capabilityDeclarations: sources.capabilityDeclarations !== null,
|
|
132
|
+
capabilityLock: sources.capabilityLock !== null,
|
|
133
|
+
doctorAttestation: sources.doctorAttestation !== null,
|
|
134
|
+
setupDeclarationBinding: sources.setupDeclarationBinding !== null,
|
|
135
|
+
},
|
|
136
|
+
exclusions: [
|
|
137
|
+
"credential values and credential environment names",
|
|
138
|
+
"setup.env, setup-adapters.env, .env, and other owner secret stores",
|
|
139
|
+
"setup source caches and installed Skill trees",
|
|
140
|
+
"browser profiles, cookies, sessions, and authentication material",
|
|
141
|
+
"host-specific absolute paths and mutable setup lock state",
|
|
142
|
+
"raw provider responses and command stdout/stderr",
|
|
143
|
+
"unrelated workspace and project files",
|
|
144
|
+
],
|
|
145
|
+
files,
|
|
146
|
+
};
|
|
147
|
+
const manifest = {
|
|
148
|
+
...manifestCore,
|
|
149
|
+
manifestSha256: sha256Text(canonicalJson(manifestCore)),
|
|
150
|
+
};
|
|
151
|
+
await writeJsonAtomic(join(temporary, "manifest.json"), manifest, 0o444);
|
|
152
|
+
await assertPortableTextFiles(temporary, [root, ...plan.install.targets.map((item) => item.root)], secrets);
|
|
153
|
+
await verifySetupAuditBundle(temporary, {
|
|
154
|
+
expectedManifestSha256: manifest.manifestSha256,
|
|
155
|
+
});
|
|
156
|
+
await rename(temporary, destination);
|
|
157
|
+
return manifest;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await rm(temporary, { recursive: true, force: true }).catch(() => undefined);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export async function verifySetupAuditBundle(bundlePath, options) {
|
|
166
|
+
if (!SHA256.test(options.expectedManifestSha256)) {
|
|
167
|
+
throw setupAuditPathError("Setup audit verification requires a valid external expected manifest SHA-256.");
|
|
168
|
+
}
|
|
169
|
+
if (!isAbsolute(bundlePath) || resolve(bundlePath) !== bundlePath) {
|
|
170
|
+
throw setupAuditPathError("Setup audit bundle path must be absolute and normalized.");
|
|
171
|
+
}
|
|
172
|
+
const info = await lstat(bundlePath).catch(() => undefined);
|
|
173
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) {
|
|
174
|
+
throw setupAuditPathError("Setup audit bundle must be a regular directory and not a symbolic link.");
|
|
175
|
+
}
|
|
176
|
+
const manifestPath = join(bundlePath, "manifest.json");
|
|
177
|
+
const manifestSnapshot = await requiredSourceSnapshot(manifestPath, "bundle manifest");
|
|
178
|
+
const manifest = parseManifest(manifestSnapshot.value);
|
|
179
|
+
if (manifest.manifestSha256 !== options.expectedManifestSha256) {
|
|
180
|
+
throw setupAuditError("Setup audit manifest does not match the external expected digest.");
|
|
181
|
+
}
|
|
182
|
+
const { manifestSha256, ...core } = manifest;
|
|
183
|
+
if (sha256Text(canonicalJson(core)) !== manifestSha256) {
|
|
184
|
+
throw setupAuditError("Setup audit manifest failed its hash binding.");
|
|
185
|
+
}
|
|
186
|
+
const initialFiles = await setupAuditTreeFiles(bundlePath);
|
|
187
|
+
const actualFiles = initialFiles.map((path) => relative(bundlePath, path).split(sep).join("/"));
|
|
188
|
+
const expectedPaths = manifest.files.map((file) => file.path);
|
|
189
|
+
const expectedTreePaths = ["manifest.json", ...expectedPaths].sort();
|
|
190
|
+
if (actualFiles.length !== expectedTreePaths.length ||
|
|
191
|
+
actualFiles.some((path, index) => path !== expectedTreePaths[index])) {
|
|
192
|
+
throw setupAuditError("Setup audit bundle contains missing, extra, or unordered files.");
|
|
193
|
+
}
|
|
194
|
+
const snapshots = new Map([
|
|
195
|
+
["manifest.json", manifestSnapshot],
|
|
196
|
+
]);
|
|
197
|
+
for (const record of manifest.files) {
|
|
198
|
+
const path = resolveContained(bundlePath, record.path);
|
|
199
|
+
const snapshot = await requiredSourceSnapshot(path, "bound bundle file");
|
|
200
|
+
if (snapshot.bytes.length !== record.bytes || snapshot.sha256 !== record.sha256) {
|
|
201
|
+
throw setupAuditError(`Setup audit file failed its exact binding: ${record.path}`);
|
|
202
|
+
}
|
|
203
|
+
snapshots.set(record.path, snapshot);
|
|
204
|
+
}
|
|
205
|
+
const allowedPaths = setupAuditAllowedPaths(manifest);
|
|
206
|
+
if (expectedPaths.length !== allowedPaths.length ||
|
|
207
|
+
expectedPaths.some((path, index) => path !== allowedPaths[index])) {
|
|
208
|
+
throw setupAuditError("Setup audit manifest does not match the closed file allowlist.");
|
|
209
|
+
}
|
|
210
|
+
await options.afterSnapshotBound?.();
|
|
211
|
+
for (const snapshot of snapshots.values()) {
|
|
212
|
+
assertPortableSnapshot(snapshot, [], []);
|
|
213
|
+
}
|
|
214
|
+
verifySetupAuditSemantics(manifest, snapshots);
|
|
215
|
+
const finalInfo = await lstat(bundlePath).catch(() => null);
|
|
216
|
+
if (!finalInfo?.isDirectory() || !sameSourceIdentity(info, finalInfo)) {
|
|
217
|
+
throw setupAuditError("Setup audit bundle directory changed during verification.");
|
|
218
|
+
}
|
|
219
|
+
const finalFiles = (await setupAuditTreeFiles(bundlePath)).map((path) => relative(bundlePath, path).split(sep).join("/"));
|
|
220
|
+
if (finalFiles.length !== expectedTreePaths.length ||
|
|
221
|
+
finalFiles.some((path, index) => path !== expectedTreePaths[index])) {
|
|
222
|
+
throw setupAuditError("Setup audit bundle tree changed during verification.");
|
|
223
|
+
}
|
|
224
|
+
await Promise.all([...snapshots].map(([logical, snapshot]) => assertSourceUnchanged(resolveContained(bundlePath, logical), snapshot, "bound bundle file")));
|
|
225
|
+
return {
|
|
226
|
+
status: "verified",
|
|
227
|
+
planSha256: manifest.setup.planSha256,
|
|
228
|
+
manifestSha256,
|
|
229
|
+
files: manifest.files.length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function portableSetupPlan(plan, sourceFileSha256) {
|
|
233
|
+
return {
|
|
234
|
+
schemaVersion: 1,
|
|
235
|
+
kind: "tiangong-setup-plan-proof",
|
|
236
|
+
sourceFileSha256,
|
|
237
|
+
planId: plan.planId,
|
|
238
|
+
planSha256: plan.planSha256,
|
|
239
|
+
createdAt: plan.createdAt,
|
|
240
|
+
cli: plan.cli,
|
|
241
|
+
workspace: {
|
|
242
|
+
nameSha256: sha256Text(plan.workspace.name),
|
|
243
|
+
mode: plan.workspace.mode,
|
|
244
|
+
},
|
|
245
|
+
install: {
|
|
246
|
+
scope: plan.install.scope,
|
|
247
|
+
agents: plan.install.agents,
|
|
248
|
+
mode: plan.install.mode,
|
|
249
|
+
installer: plan.install.installer,
|
|
250
|
+
targets: plan.install.targets.map((target) => ({
|
|
251
|
+
agent: target.agent,
|
|
252
|
+
rootSha256: sha256Text(target.root),
|
|
253
|
+
})),
|
|
254
|
+
},
|
|
255
|
+
selection: plan.selection,
|
|
256
|
+
sources: plan.sources,
|
|
257
|
+
skills: plan.skills,
|
|
258
|
+
acceptedLicenses: plan.acceptedLicenses,
|
|
259
|
+
credentialSources: plan.credentialSources.map((source) => ({
|
|
260
|
+
id: source.id,
|
|
261
|
+
storage: source.storage,
|
|
262
|
+
configured: true,
|
|
263
|
+
})),
|
|
264
|
+
settings: Object.entries(plan.settings).map(([id, value]) => ({
|
|
265
|
+
id,
|
|
266
|
+
valueSha256: sha256Text(value),
|
|
267
|
+
})),
|
|
268
|
+
agentRoutes: plan.agentRoutes,
|
|
269
|
+
reviewerExecution: plan.reviewerExecution,
|
|
270
|
+
checks: plan.checks,
|
|
271
|
+
confirmations: plan.confirmations,
|
|
272
|
+
mutations: plan.mutations.map((mutation) => ({
|
|
273
|
+
step: mutation.step,
|
|
274
|
+
targetSha256: sha256Text(mutation.target),
|
|
275
|
+
reason: mutation.reason,
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function portableSetupState(value, sourceFileSha256) {
|
|
280
|
+
if (!isObject(value))
|
|
281
|
+
throw setupAuditError("Setup state proof source is invalid.");
|
|
282
|
+
const lastError = isObject(value.lastError) ? value.lastError : null;
|
|
283
|
+
const proof = {
|
|
284
|
+
schemaVersion: 1,
|
|
285
|
+
kind: "tiangong-setup-state-proof",
|
|
286
|
+
sourceFileSha256,
|
|
287
|
+
planSha256: stringOrNull(value.planSha256),
|
|
288
|
+
status: stringOrNull(value.status),
|
|
289
|
+
currentStep: stringOrNull(value.currentStep),
|
|
290
|
+
completedSteps: stringArray(value.completedSteps),
|
|
291
|
+
attempts: Number.isSafeInteger(value.attempts) ? value.attempts : null,
|
|
292
|
+
updatedAt: stringOrNull(value.updatedAt),
|
|
293
|
+
lastError: lastError === null
|
|
294
|
+
? null
|
|
295
|
+
: {
|
|
296
|
+
code: stringOrNull(lastError.code),
|
|
297
|
+
step: stringOrNull(lastError.step),
|
|
298
|
+
reasonSha256: typeof lastError.reason === "string" ? sha256Text(lastError.reason) : null,
|
|
299
|
+
minimumActionSha256: typeof lastError.minimumAction === "string"
|
|
300
|
+
? sha256Text(lastError.minimumAction)
|
|
301
|
+
: null,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
parsePortableSetupState(proof);
|
|
305
|
+
return proof;
|
|
306
|
+
}
|
|
307
|
+
function portableSetupReport(value, sourceFileSha256) {
|
|
308
|
+
if (!isObject(value) || value.schemaVersion !== 1 || !Array.isArray(value.checks)) {
|
|
309
|
+
throw setupAuditError("Setup report proof source is invalid.");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
schemaVersion: 1,
|
|
313
|
+
kind: "tiangong-setup-report-proof",
|
|
314
|
+
sourceFileSha256,
|
|
315
|
+
planSha256: stringOrNull(value.planSha256),
|
|
316
|
+
checkedAt: stringOrNull(value.checkedAt),
|
|
317
|
+
mode: stringOrNull(value.mode),
|
|
318
|
+
readiness: stringOrNull(value.readiness),
|
|
319
|
+
researchReadiness: stringOrNull(value.researchReadiness),
|
|
320
|
+
preprocessingReadiness: stringOrNull(value.preprocessingReadiness),
|
|
321
|
+
acquisitionReadiness: stringOrNull(value.acquisitionReadiness),
|
|
322
|
+
authoringReadiness: stringOrNull(value.authoringReadiness),
|
|
323
|
+
overallReadiness: stringOrNull(value.overallReadiness),
|
|
324
|
+
checks: value.checks.map((check) => portableSetupCheck(check)),
|
|
325
|
+
summary: isObject(value.summary)
|
|
326
|
+
? {
|
|
327
|
+
pass: numberOrNull(value.summary.pass),
|
|
328
|
+
warn: numberOrNull(value.summary.warn),
|
|
329
|
+
fail: numberOrNull(value.summary.fail),
|
|
330
|
+
}
|
|
331
|
+
: null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function portableSetupCheck(value) {
|
|
335
|
+
if (!isObject(value))
|
|
336
|
+
throw setupAuditError("Setup report contains an invalid check.");
|
|
337
|
+
const diagnostics = isObject(value.diagnostics) ? value.diagnostics : null;
|
|
338
|
+
return {
|
|
339
|
+
id: stringOrNull(value.id),
|
|
340
|
+
category: stringOrNull(value.category),
|
|
341
|
+
status: stringOrNull(value.status),
|
|
342
|
+
scope: stringOrNull(value.scope),
|
|
343
|
+
componentIds: stringArray(value.componentIds),
|
|
344
|
+
requiredFor: stringArray(value.requiredFor),
|
|
345
|
+
blocking: typeof value.blocking === "boolean" ? value.blocking : null,
|
|
346
|
+
componentGate: typeof value.componentGate === "boolean" ? value.componentGate : null,
|
|
347
|
+
skippedBecauseSha256: typeof value.skippedBecause === "string" ? sha256Text(value.skippedBecause) : null,
|
|
348
|
+
diagnostics: diagnostics === null
|
|
349
|
+
? null
|
|
350
|
+
: {
|
|
351
|
+
code: stringOrNull(diagnostics.code),
|
|
352
|
+
executionMode: stringOrNull(diagnostics.executionMode),
|
|
353
|
+
credentialScope: stringOrNull(diagnostics.credentialScope),
|
|
354
|
+
networkAttempted: typeof diagnostics.networkAttempted === "boolean"
|
|
355
|
+
? diagnostics.networkAttempted
|
|
356
|
+
: null,
|
|
357
|
+
httpStatus: numberOrNull(diagnostics.httpStatus),
|
|
358
|
+
retryAfterSeconds: numberOrNull(diagnostics.retryAfterSeconds),
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function portableCapabilities(value, sourceFileSha256) {
|
|
363
|
+
return {
|
|
364
|
+
schemaVersion: 1,
|
|
365
|
+
kind: "tiangong-capability-declarations-proof",
|
|
366
|
+
sourceFileSha256,
|
|
367
|
+
capabilities: value.capabilities.map((capability) => ({
|
|
368
|
+
id: capability.id,
|
|
369
|
+
skillPath: `skills/${basename(capability.skillPath)}`,
|
|
370
|
+
source: portableCapabilitySource(capability.source),
|
|
371
|
+
requiredForDiscovery: capability.requiredForDiscovery,
|
|
372
|
+
permissions: capability.permissions,
|
|
373
|
+
allowedHosts: capability.allowedHosts,
|
|
374
|
+
http: portableCapabilityHttp(capability.http),
|
|
375
|
+
coverage: capability.coverage,
|
|
376
|
+
credentials: capability.credentials.map((credential) => ({
|
|
377
|
+
id: credential.id,
|
|
378
|
+
allowedHosts: credential.allowedHosts,
|
|
379
|
+
headerName: credential.headerName,
|
|
380
|
+
prefixSha256: sha256Text(credential.prefix),
|
|
381
|
+
})),
|
|
382
|
+
healthCheck: capability.healthCheck === null
|
|
383
|
+
? null
|
|
384
|
+
: {
|
|
385
|
+
targetSha256: sha256Text(capability.healthCheck.url),
|
|
386
|
+
credentialId: capability.healthCheck.credentialId,
|
|
387
|
+
expectedContentTypes: capability.healthCheck.expectedContentTypes,
|
|
388
|
+
method: capability.healthCheck.method,
|
|
389
|
+
bodySha256: capability.healthCheck.body === null
|
|
390
|
+
? null
|
|
391
|
+
: sha256Text(canonicalJson(capability.healthCheck.body)),
|
|
392
|
+
},
|
|
393
|
+
})),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function portableCapabilityHttp(value) {
|
|
397
|
+
return value === null
|
|
398
|
+
? null
|
|
399
|
+
: {
|
|
400
|
+
endpoint: value.endpoint,
|
|
401
|
+
method: value.method,
|
|
402
|
+
accept: value.accept,
|
|
403
|
+
allowedContentTypes: value.allowedContentTypes,
|
|
404
|
+
staticHeaderBindings: Object.entries(value.staticHeaders)
|
|
405
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
406
|
+
.map(([name, headerValue]) => ({
|
|
407
|
+
name,
|
|
408
|
+
valueSha256: sha256Text(headerValue),
|
|
409
|
+
})),
|
|
410
|
+
maxRequestBytes: value.maxRequestBytes,
|
|
411
|
+
maxResponseBytes: value.maxResponseBytes,
|
|
412
|
+
maxItems: value.maxItems,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function portableCapabilityLock(value, sourceFileSha256) {
|
|
416
|
+
return {
|
|
417
|
+
schemaVersion: 1,
|
|
418
|
+
kind: "tiangong-capability-lock-proof",
|
|
419
|
+
sourceFileSha256,
|
|
420
|
+
generatedAt: value.generatedAt,
|
|
421
|
+
capabilities: value.capabilities.map(({ skillPath: _skillPath, ...record }) => ({
|
|
422
|
+
...record,
|
|
423
|
+
source: portableCapabilitySource(record.source),
|
|
424
|
+
skillPath: `skills/${record.skillName}`,
|
|
425
|
+
})),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function portableCapabilitySource(value) {
|
|
429
|
+
return value === null
|
|
430
|
+
? null
|
|
431
|
+
: {
|
|
432
|
+
type: value.type,
|
|
433
|
+
locatorSha256: sha256Text(value.locator),
|
|
434
|
+
immutableRef: value.immutableRef,
|
|
435
|
+
expectedTreeSha256: value.expectedTreeSha256,
|
|
436
|
+
license: value.license,
|
|
437
|
+
catalogId: value.catalogId,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function portableDoctorAttestation(value, sourceFileSha256) {
|
|
441
|
+
return {
|
|
442
|
+
schemaVersion: 1,
|
|
443
|
+
kind: "tiangong-doctor-attestation-proof",
|
|
444
|
+
sourceFileSha256,
|
|
445
|
+
attestationSha256: value.attestationSha256,
|
|
446
|
+
workspaceIdSha256: sha256Text(value.workspaceId),
|
|
447
|
+
checkedAt: value.checkedAt,
|
|
448
|
+
expiresAt: value.expiresAt,
|
|
449
|
+
configSha256: value.configSha256,
|
|
450
|
+
runtimeLockSha256: value.runtimeLockSha256,
|
|
451
|
+
capabilityDeclarationsSha256: value.capabilityDeclarationsSha256,
|
|
452
|
+
capabilityLockSha256: value.capabilityLockSha256,
|
|
453
|
+
doctorSchemaSha256: value.doctorSchemaSha256,
|
|
454
|
+
reviewerExecution: value.reviewerExecution,
|
|
455
|
+
runtimes: value.runtimes.map((runtime) => ({
|
|
456
|
+
agent: runtime.agent,
|
|
457
|
+
modelSha256: runtime.model === null ? null : sha256Text(runtime.model),
|
|
458
|
+
effort: runtime.effort ?? null,
|
|
459
|
+
verbosity: runtime.verbosity ?? null,
|
|
460
|
+
binarySha256: runtime.binarySha256,
|
|
461
|
+
wrapperSha256: runtime.wrapperSha256,
|
|
462
|
+
adapterSha256: runtime.adapterSha256,
|
|
463
|
+
binaryVersionSha256: sha256Text(runtime.binaryVersion),
|
|
464
|
+
platform: runtime.platform,
|
|
465
|
+
architecture: runtime.architecture,
|
|
466
|
+
})),
|
|
467
|
+
capabilitySmoke: value.capabilitySmoke.map((row) => ({
|
|
468
|
+
id: row.id,
|
|
469
|
+
status: row.status,
|
|
470
|
+
code: row.code,
|
|
471
|
+
hostSha256: row.host === null ? null : sha256Text(row.host),
|
|
472
|
+
targetSha256: row.targetSha256,
|
|
473
|
+
httpStatus: row.httpStatus,
|
|
474
|
+
})),
|
|
475
|
+
smokeUsage: value.smokeUsage.map(({ telemetry, ...usage }) => ({
|
|
476
|
+
...usage,
|
|
477
|
+
telemetrySha256: telemetry === undefined ? null : sha256Text(canonicalJson(telemetry)),
|
|
478
|
+
})),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function portableReadiness(report, setupState) {
|
|
482
|
+
if (!isObject(report)) {
|
|
483
|
+
return {
|
|
484
|
+
setupState,
|
|
485
|
+
research: "NOT_CHECKED",
|
|
486
|
+
preprocessing: "NOT_CHECKED",
|
|
487
|
+
acquisition: "NOT_CHECKED",
|
|
488
|
+
authoring: "NOT_CHECKED",
|
|
489
|
+
overall: "NOT_CHECKED",
|
|
490
|
+
checkedAt: null,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
setupState,
|
|
495
|
+
research: String(report.researchReadiness ?? report.readiness ?? "NOT_CHECKED"),
|
|
496
|
+
preprocessing: String(report.preprocessingReadiness ?? "NOT_CHECKED"),
|
|
497
|
+
acquisition: String(report.acquisitionReadiness ?? "NOT_CHECKED"),
|
|
498
|
+
authoring: String(report.authoringReadiness ?? "NOT_CHECKED"),
|
|
499
|
+
overall: String(report.overallReadiness ?? "NOT_CHECKED"),
|
|
500
|
+
checkedAt: typeof report.checkedAt === "string" ? report.checkedAt : null,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function bundleFileRecords(root) {
|
|
504
|
+
const files = await setupAuditTreeFiles(root);
|
|
505
|
+
return Promise.all(files
|
|
506
|
+
.map((path) => ({ path, logical: relative(root, path).split(sep).join("/") }))
|
|
507
|
+
.filter((item) => item.logical !== "manifest.json")
|
|
508
|
+
.map(async ({ path, logical }) => {
|
|
509
|
+
const info = await lstat(path);
|
|
510
|
+
return { path: logical, sha256: await sha256File(path), bytes: info.size };
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
async function setupAuditSecrets(paths, environment) {
|
|
514
|
+
const values = new Set(configuredResearchSecrets(environment));
|
|
515
|
+
for (const path of [paths.env, paths.setupDeclarationEnv, paths.setupAdapterEnv]) {
|
|
516
|
+
const info = await lstat(path).catch(() => undefined);
|
|
517
|
+
if (!info)
|
|
518
|
+
continue;
|
|
519
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_SECRET_STORE_BYTES) {
|
|
520
|
+
throw setupAuditError("A setup credential store is unsafe or oversized.");
|
|
521
|
+
}
|
|
522
|
+
const text = await readFile(path, "utf8");
|
|
523
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
524
|
+
const separator = line.indexOf("=");
|
|
525
|
+
if (separator < 0)
|
|
526
|
+
continue;
|
|
527
|
+
const raw = line.slice(separator + 1).trim();
|
|
528
|
+
if (raw.length >= 8)
|
|
529
|
+
values.add(raw);
|
|
530
|
+
try {
|
|
531
|
+
collectStringLeaves(JSON.parse(raw), values);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Literal non-JSON environment values are already included above.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return [...values].sort((left, right) => right.length - left.length);
|
|
539
|
+
}
|
|
540
|
+
function collectStringLeaves(value, output) {
|
|
541
|
+
if (typeof value === "string") {
|
|
542
|
+
if (value.length >= 8)
|
|
543
|
+
output.add(value);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (Array.isArray(value)) {
|
|
547
|
+
for (const item of value)
|
|
548
|
+
collectStringLeaves(item, output);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (!isObject(value))
|
|
552
|
+
return;
|
|
553
|
+
for (const item of Object.values(value))
|
|
554
|
+
collectStringLeaves(item, output);
|
|
555
|
+
}
|
|
556
|
+
async function assertPortableTextFiles(root, forbiddenRoots, secrets) {
|
|
557
|
+
for (const path of await setupAuditTreeFiles(root)) {
|
|
558
|
+
assertPortableSnapshot(await requiredSourceSnapshot(path, "portable bundle file"), forbiddenRoots, secrets);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function assertPortableSnapshot(snapshot, forbiddenRoots, secrets) {
|
|
562
|
+
const text = snapshot.bytes.toString("utf8");
|
|
563
|
+
if (forbiddenRoots.some((item) => item.length > 1 && text.includes(item))) {
|
|
564
|
+
throw new CliError("Setup audit bundle contains a host-specific path.", {
|
|
565
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_NONPORTABLE",
|
|
566
|
+
exitCode: 3,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (sanitizeResearchText(text, secrets) !== text) {
|
|
570
|
+
throw new CliError("Setup audit bundle contains sensitive text.", {
|
|
571
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_SENSITIVE",
|
|
572
|
+
exitCode: 3,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
assertPortableJsonValue(snapshot.value, forbiddenRoots, secrets);
|
|
576
|
+
}
|
|
577
|
+
function assertPortableJsonValue(value, forbiddenRoots, secrets) {
|
|
578
|
+
if (typeof value === "string") {
|
|
579
|
+
if (forbiddenRoots.some((item) => item.length > 1 && value.includes(item))) {
|
|
580
|
+
throw new CliError("Setup audit bundle contains a host-specific path.", {
|
|
581
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_NONPORTABLE",
|
|
582
|
+
exitCode: 3,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (sanitizeResearchText(value, secrets) !== value) {
|
|
586
|
+
throw new CliError("Setup audit bundle contains sensitive text.", {
|
|
587
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_SENSITIVE",
|
|
588
|
+
exitCode: 3,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (Array.isArray(value)) {
|
|
594
|
+
for (const item of value)
|
|
595
|
+
assertPortableJsonValue(item, forbiddenRoots, secrets);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!isObject(value))
|
|
599
|
+
return;
|
|
600
|
+
for (const [key, item] of Object.entries(value)) {
|
|
601
|
+
assertPortableJsonValue(key, forbiddenRoots, secrets);
|
|
602
|
+
assertPortableJsonValue(item, forbiddenRoots, secrets);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function setupAuditTreeFiles(root) {
|
|
606
|
+
try {
|
|
607
|
+
return await regularTreeFiles(root);
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
throw setupAuditError("Setup audit bundle contains an unsupported filesystem entry.");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function requiredSourceSnapshot(path, label) {
|
|
614
|
+
const value = await optionalSourceSnapshot(path, label);
|
|
615
|
+
if (!value)
|
|
616
|
+
throw setupAuditError(`Required setup audit source is missing: ${label}.`);
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
async function optionalSourceSnapshot(path, label) {
|
|
620
|
+
const before = await lstat(path).catch((error) => {
|
|
621
|
+
if (error.code === "ENOENT")
|
|
622
|
+
return null;
|
|
623
|
+
throw error;
|
|
624
|
+
});
|
|
625
|
+
if (before === null)
|
|
626
|
+
return null;
|
|
627
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_TEXT_SCAN_BYTES) {
|
|
628
|
+
throw setupAuditError(`Setup audit source is not a regular non-symlink file: ${label}.`);
|
|
629
|
+
}
|
|
630
|
+
const handle = await open(path, "r");
|
|
631
|
+
let bytes;
|
|
632
|
+
try {
|
|
633
|
+
const opened = await handle.stat();
|
|
634
|
+
if (!opened.isFile() || !sameSourceIdentity(before, opened)) {
|
|
635
|
+
throw setupAuditError(`Setup audit source changed before it could be read: ${label}.`);
|
|
636
|
+
}
|
|
637
|
+
bytes = await handle.readFile();
|
|
638
|
+
const after = await handle.stat();
|
|
639
|
+
if (!sameSourceIdentity(opened, after) ||
|
|
640
|
+
after.size !== bytes.length ||
|
|
641
|
+
after.size > MAX_TEXT_SCAN_BYTES) {
|
|
642
|
+
throw setupAuditError(`Setup audit source changed while it was read: ${label}.`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
finally {
|
|
646
|
+
await handle.close();
|
|
647
|
+
}
|
|
648
|
+
const current = await lstat(path).catch(() => null);
|
|
649
|
+
if (!current || current.isSymbolicLink() || !sameSourceIdentity(before, current)) {
|
|
650
|
+
throw setupAuditError(`Setup audit source changed after it was read: ${label}.`);
|
|
651
|
+
}
|
|
652
|
+
let value;
|
|
653
|
+
try {
|
|
654
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
throw setupAuditError(`Setup audit source is not valid JSON: ${label}.`);
|
|
658
|
+
}
|
|
659
|
+
return { bytes, sha256: sha256Bytes(bytes), value };
|
|
660
|
+
}
|
|
661
|
+
function sameSourceIdentity(left, right) {
|
|
662
|
+
const inodeMatches = left.ino === 0 || right.ino === 0 || left.ino === right.ino;
|
|
663
|
+
return (inodeMatches &&
|
|
664
|
+
left.dev === right.dev &&
|
|
665
|
+
left.size === right.size &&
|
|
666
|
+
left.mtimeMs === right.mtimeMs &&
|
|
667
|
+
left.ctimeMs === right.ctimeMs);
|
|
668
|
+
}
|
|
669
|
+
async function assertSourceUnchanged(path, snapshot, label) {
|
|
670
|
+
const current = await requiredSourceSnapshot(path, label);
|
|
671
|
+
if (current.sha256 !== snapshot.sha256) {
|
|
672
|
+
throw setupAuditError(`Setup audit source changed during export: ${label}.`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function validateNewDestination(value) {
|
|
676
|
+
if (!isAbsolute(value) || resolve(value) !== value) {
|
|
677
|
+
throw setupAuditPathError("Setup audit export destination must be absolute and normalized.");
|
|
678
|
+
}
|
|
679
|
+
if (await pathExists(value)) {
|
|
680
|
+
throw setupAuditPathError("Setup audit export destination must not already exist.");
|
|
681
|
+
}
|
|
682
|
+
const parent = dirname(value);
|
|
683
|
+
const parentInfo = await lstat(parent).catch(() => undefined);
|
|
684
|
+
if (!parentInfo?.isDirectory() || parentInfo.isSymbolicLink()) {
|
|
685
|
+
throw setupAuditPathError("Setup audit export parent must be an existing regular directory.");
|
|
686
|
+
}
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
function parseManifest(value) {
|
|
690
|
+
if (!isObject(value) ||
|
|
691
|
+
!hasExactKeys(value, [
|
|
692
|
+
"schemaVersion",
|
|
693
|
+
"kind",
|
|
694
|
+
"createdAt",
|
|
695
|
+
"setup",
|
|
696
|
+
"readiness",
|
|
697
|
+
"sourceBindings",
|
|
698
|
+
"availability",
|
|
699
|
+
"exclusions",
|
|
700
|
+
"files",
|
|
701
|
+
"manifestSha256",
|
|
702
|
+
]) ||
|
|
703
|
+
value.schemaVersion !== 1 ||
|
|
704
|
+
value.kind !== "tiangong-setup-audit-bundle" ||
|
|
705
|
+
typeof value.createdAt !== "string" ||
|
|
706
|
+
!isObject(value.setup) ||
|
|
707
|
+
!hasExactKeys(value.setup, [
|
|
708
|
+
"planId",
|
|
709
|
+
"planSha256",
|
|
710
|
+
"cliVersion",
|
|
711
|
+
"mode",
|
|
712
|
+
"selectedSkillIds",
|
|
713
|
+
]) ||
|
|
714
|
+
typeof value.setup.planId !== "string" ||
|
|
715
|
+
typeof value.setup.planSha256 !== "string" ||
|
|
716
|
+
!SHA256.test(value.setup.planSha256) ||
|
|
717
|
+
typeof value.setup.cliVersion !== "string" ||
|
|
718
|
+
!["smoke-test", "production-research"].includes(String(value.setup.mode)) ||
|
|
719
|
+
!Array.isArray(value.setup.selectedSkillIds) ||
|
|
720
|
+
value.setup.selectedSkillIds.some((item) => typeof item !== "string") ||
|
|
721
|
+
!isObject(value.readiness) ||
|
|
722
|
+
!hasExactKeys(value.readiness, [
|
|
723
|
+
"setupState",
|
|
724
|
+
"research",
|
|
725
|
+
"preprocessing",
|
|
726
|
+
"acquisition",
|
|
727
|
+
"authoring",
|
|
728
|
+
"overall",
|
|
729
|
+
"checkedAt",
|
|
730
|
+
]) ||
|
|
731
|
+
!["pending", "applying", "partially-ready", "ready", "blocked"].includes(String(value.readiness.setupState)) ||
|
|
732
|
+
!["READY", "BLOCKED", "NOT_CHECKED"].includes(String(value.readiness.research)) ||
|
|
733
|
+
[value.readiness.preprocessing, value.readiness.acquisition, value.readiness.authoring].some((item) => !["READY", "DEGRADED", "BLOCKED", "NOT_REQUIRED", "NOT_CHECKED"].includes(String(item))) ||
|
|
734
|
+
!["READY", "PARTIALLY_READY", "BLOCKED", "NOT_CHECKED"].includes(String(value.readiness.overall)) ||
|
|
735
|
+
!(value.readiness.checkedAt === null || typeof value.readiness.checkedAt === "string") ||
|
|
736
|
+
!isObject(value.sourceBindings) ||
|
|
737
|
+
!validSourceBindings(value.sourceBindings) ||
|
|
738
|
+
!isObject(value.availability) ||
|
|
739
|
+
!hasExactKeys(value.availability, [
|
|
740
|
+
"setupReport",
|
|
741
|
+
"runtimeLock",
|
|
742
|
+
"capabilityDeclarations",
|
|
743
|
+
"capabilityLock",
|
|
744
|
+
"doctorAttestation",
|
|
745
|
+
"setupDeclarationBinding",
|
|
746
|
+
]) ||
|
|
747
|
+
Object.values(value.availability).some((item) => typeof item !== "boolean") ||
|
|
748
|
+
!Array.isArray(value.exclusions) ||
|
|
749
|
+
value.exclusions.some((item) => typeof item !== "string") ||
|
|
750
|
+
!Array.isArray(value.files) ||
|
|
751
|
+
typeof value.manifestSha256 !== "string" ||
|
|
752
|
+
!SHA256.test(value.manifestSha256)) {
|
|
753
|
+
throw setupAuditError("Setup audit manifest shape is invalid.");
|
|
754
|
+
}
|
|
755
|
+
const files = value.files;
|
|
756
|
+
if (files.some((file) => !isObject(file) ||
|
|
757
|
+
!hasExactKeys(file, ["path", "sha256", "bytes"]) ||
|
|
758
|
+
typeof file.path !== "string" ||
|
|
759
|
+
safePathOrNull(file.path) === null ||
|
|
760
|
+
typeof file.sha256 !== "string" ||
|
|
761
|
+
!SHA256.test(file.sha256) ||
|
|
762
|
+
!Number.isSafeInteger(file.bytes) ||
|
|
763
|
+
Number(file.bytes) < 0)) {
|
|
764
|
+
throw setupAuditError("Setup audit manifest file records are invalid.");
|
|
765
|
+
}
|
|
766
|
+
const paths = files.map((file) => String(file.path));
|
|
767
|
+
if (new Set(paths).size !== paths.length ||
|
|
768
|
+
paths.some((path, index) => index > 0 && paths[index - 1] >= path)) {
|
|
769
|
+
throw setupAuditError("Setup audit manifest file paths must be unique and sorted.");
|
|
770
|
+
}
|
|
771
|
+
const manifest = value;
|
|
772
|
+
if (manifest.sourceBindings.setupPlan.planSha256 !== manifest.setup.planSha256 ||
|
|
773
|
+
manifest.availability.setupReport !==
|
|
774
|
+
(manifest.sourceBindings.setupReportFileSha256 !== null) ||
|
|
775
|
+
manifest.availability.runtimeLock !==
|
|
776
|
+
(manifest.sourceBindings.runtimeLockFileSha256 !== null) ||
|
|
777
|
+
manifest.availability.capabilityDeclarations !==
|
|
778
|
+
(manifest.sourceBindings.capabilityDeclarationsFileSha256 !== null) ||
|
|
779
|
+
manifest.availability.capabilityLock !==
|
|
780
|
+
(manifest.sourceBindings.capabilityLockFileSha256 !== null) ||
|
|
781
|
+
manifest.availability.doctorAttestation !==
|
|
782
|
+
(manifest.sourceBindings.doctorAttestation !== null) ||
|
|
783
|
+
manifest.availability.setupDeclarationBinding !==
|
|
784
|
+
(manifest.sourceBindings.setupDeclarationBindingFileSha256 !== null)) {
|
|
785
|
+
throw setupAuditError("Setup audit availability and source bindings are inconsistent.");
|
|
786
|
+
}
|
|
787
|
+
return manifest;
|
|
788
|
+
}
|
|
789
|
+
function validSourceBindings(value) {
|
|
790
|
+
if (!hasExactKeys(value, [
|
|
791
|
+
"setupPlan",
|
|
792
|
+
"setupStateFileSha256",
|
|
793
|
+
"setupReportFileSha256",
|
|
794
|
+
"runtimeLockFileSha256",
|
|
795
|
+
"capabilityDeclarationsFileSha256",
|
|
796
|
+
"capabilityLockFileSha256",
|
|
797
|
+
"doctorAttestation",
|
|
798
|
+
"setupDeclarationBindingFileSha256",
|
|
799
|
+
"sourceWorkspacePathSha256",
|
|
800
|
+
]) ||
|
|
801
|
+
!isObject(value.setupPlan) ||
|
|
802
|
+
!hasExactKeys(value.setupPlan, ["planSha256", "sourceFileSha256"]) ||
|
|
803
|
+
!isSha(value.setupPlan.planSha256) ||
|
|
804
|
+
!isSha(value.setupPlan.sourceFileSha256) ||
|
|
805
|
+
!isSha(value.setupStateFileSha256) ||
|
|
806
|
+
!isNullableSha(value.setupReportFileSha256) ||
|
|
807
|
+
!isNullableSha(value.runtimeLockFileSha256) ||
|
|
808
|
+
!isNullableSha(value.capabilityDeclarationsFileSha256) ||
|
|
809
|
+
!isNullableSha(value.capabilityLockFileSha256) ||
|
|
810
|
+
!isNullableSha(value.setupDeclarationBindingFileSha256) ||
|
|
811
|
+
!isSha(value.sourceWorkspacePathSha256)) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
if (value.doctorAttestation === null)
|
|
815
|
+
return true;
|
|
816
|
+
return (isObject(value.doctorAttestation) &&
|
|
817
|
+
hasExactKeys(value.doctorAttestation, [
|
|
818
|
+
"attestationSha256",
|
|
819
|
+
"sourceFileSha256",
|
|
820
|
+
"verificationStatus",
|
|
821
|
+
]) &&
|
|
822
|
+
isSha(value.doctorAttestation.attestationSha256) &&
|
|
823
|
+
isSha(value.doctorAttestation.sourceFileSha256) &&
|
|
824
|
+
["verified", "expired", "drifted"].includes(String(value.doctorAttestation.verificationStatus)));
|
|
825
|
+
}
|
|
826
|
+
function setupAuditAllowedPaths(manifest) {
|
|
827
|
+
return [
|
|
828
|
+
"control/setup-plan.portable.json",
|
|
829
|
+
"control/setup-state.portable.json",
|
|
830
|
+
...(manifest.availability.setupReport ? ["control/setup-report.portable.json"] : []),
|
|
831
|
+
...(manifest.availability.runtimeLock ? ["control/runtime-lock.json"] : []),
|
|
832
|
+
...(manifest.availability.capabilityDeclarations ? ["control/capabilities.portable.json"] : []),
|
|
833
|
+
...(manifest.availability.capabilityLock ? ["control/capabilities-lock.portable.json"] : []),
|
|
834
|
+
...(manifest.availability.doctorAttestation ? ["control/doctor-attestation.json"] : []),
|
|
835
|
+
...(manifest.availability.setupDeclarationBinding
|
|
836
|
+
? ["control/setup-declaration-binding.json"]
|
|
837
|
+
: []),
|
|
838
|
+
].sort();
|
|
839
|
+
}
|
|
840
|
+
function verifySetupAuditSemantics(manifest, snapshots) {
|
|
841
|
+
const plan = parsePortableSetupPlan(readBundleJson(snapshots, "control/setup-plan.portable.json"));
|
|
842
|
+
const state = parsePortableSetupState(readBundleJson(snapshots, "control/setup-state.portable.json"));
|
|
843
|
+
if (plan.kind !== "tiangong-setup-plan-proof" ||
|
|
844
|
+
plan.planId !== manifest.setup.planId ||
|
|
845
|
+
plan.planSha256 !== manifest.setup.planSha256 ||
|
|
846
|
+
plan.sourceFileSha256 !== manifest.sourceBindings.setupPlan.sourceFileSha256 ||
|
|
847
|
+
!isObject(plan.cli) ||
|
|
848
|
+
plan.cli.version !== manifest.setup.cliVersion ||
|
|
849
|
+
!isObject(plan.workspace) ||
|
|
850
|
+
plan.workspace.mode !== manifest.setup.mode ||
|
|
851
|
+
!isObject(plan.selection) ||
|
|
852
|
+
canonicalJson(plan.selection.skillIds) !== canonicalJson(manifest.setup.selectedSkillIds) ||
|
|
853
|
+
state.kind !== "tiangong-setup-state-proof" ||
|
|
854
|
+
state.planSha256 !== manifest.setup.planSha256 ||
|
|
855
|
+
state.sourceFileSha256 !== manifest.sourceBindings.setupStateFileSha256 ||
|
|
856
|
+
state.status !== manifest.readiness.setupState) {
|
|
857
|
+
throw setupAuditError("Setup audit plan/state proofs are not cross-bound.");
|
|
858
|
+
}
|
|
859
|
+
if (manifest.availability.setupReport) {
|
|
860
|
+
const report = parsePortableSetupReport(readBundleJson(snapshots, "control/setup-report.portable.json"));
|
|
861
|
+
if (report.kind !== "tiangong-setup-report-proof" ||
|
|
862
|
+
report.planSha256 !== manifest.setup.planSha256 ||
|
|
863
|
+
report.sourceFileSha256 !== manifest.sourceBindings.setupReportFileSha256 ||
|
|
864
|
+
report.researchReadiness !== manifest.readiness.research ||
|
|
865
|
+
report.preprocessingReadiness !== manifest.readiness.preprocessing ||
|
|
866
|
+
report.acquisitionReadiness !== manifest.readiness.acquisition ||
|
|
867
|
+
report.authoringReadiness !== manifest.readiness.authoring ||
|
|
868
|
+
report.overallReadiness !== manifest.readiness.overall ||
|
|
869
|
+
report.checkedAt !== manifest.readiness.checkedAt) {
|
|
870
|
+
throw setupAuditError("Setup audit report proof is not cross-bound.");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (manifest.availability.runtimeLock) {
|
|
874
|
+
const runtime = parseRuntimeLock(readBundleJson(snapshots, "control/runtime-lock.json"));
|
|
875
|
+
if (runtime.packageName !== "@tiangong-ai/cli" ||
|
|
876
|
+
runtime.packageVersion !== manifest.setup.cliVersion ||
|
|
877
|
+
boundFileSha(manifest, "control/runtime-lock.json") !==
|
|
878
|
+
manifest.sourceBindings.runtimeLockFileSha256) {
|
|
879
|
+
throw setupAuditError("Setup audit runtime lock is not bound to the setup CLI.");
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (manifest.availability.capabilityDeclarations) {
|
|
883
|
+
const capabilities = parsePortableCapabilities(readBundleJson(snapshots, "control/capabilities.portable.json"));
|
|
884
|
+
if (capabilities.kind !== "tiangong-capability-declarations-proof" ||
|
|
885
|
+
capabilities.sourceFileSha256 !== manifest.sourceBindings.capabilityDeclarationsFileSha256) {
|
|
886
|
+
throw setupAuditError("Setup audit capability declaration proof is not cross-bound.");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (manifest.availability.capabilityLock) {
|
|
890
|
+
const lock = parsePortableCapabilityLock(readBundleJson(snapshots, "control/capabilities-lock.portable.json"));
|
|
891
|
+
if (lock.kind !== "tiangong-capability-lock-proof" ||
|
|
892
|
+
lock.sourceFileSha256 !== manifest.sourceBindings.capabilityLockFileSha256) {
|
|
893
|
+
throw setupAuditError("Setup audit capability lock proof is not cross-bound.");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (manifest.availability.doctorAttestation) {
|
|
897
|
+
const attestation = parsePortableDoctorAttestation(readBundleJson(snapshots, "control/doctor-attestation.json"));
|
|
898
|
+
const doctorBinding = manifest.sourceBindings.doctorAttestation;
|
|
899
|
+
const currentControlHashesMatch = attestation.runtimeLockSha256 === manifest.sourceBindings.runtimeLockFileSha256 &&
|
|
900
|
+
attestation.capabilityDeclarationsSha256 ===
|
|
901
|
+
manifest.sourceBindings.capabilityDeclarationsFileSha256 &&
|
|
902
|
+
attestation.capabilityLockSha256 === manifest.sourceBindings.capabilityLockFileSha256;
|
|
903
|
+
if (attestation.attestationSha256 !== doctorBinding?.attestationSha256 ||
|
|
904
|
+
attestation.sourceFileSha256 !== doctorBinding?.sourceFileSha256 ||
|
|
905
|
+
(doctorBinding?.verificationStatus !== "drifted" && !currentControlHashesMatch)) {
|
|
906
|
+
throw setupAuditError("Setup audit doctor attestation is not cross-bound.");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (manifest.availability.setupDeclarationBinding) {
|
|
910
|
+
const binding = parseSetupDeclarationBinding(readBundleJson(snapshots, "control/setup-declaration-binding.json"));
|
|
911
|
+
if (binding.planSha256 !== manifest.setup.planSha256 ||
|
|
912
|
+
boundFileSha(manifest, "control/setup-declaration-binding.json") !==
|
|
913
|
+
manifest.sourceBindings.setupDeclarationBindingFileSha256) {
|
|
914
|
+
throw setupAuditError("Setup audit declaration binding does not match the plan.");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function boundFileSha(manifest, logical) {
|
|
919
|
+
return manifest.files.find((file) => file.path === logical)?.sha256 ?? null;
|
|
920
|
+
}
|
|
921
|
+
function parsePortableSetupPlan(value) {
|
|
922
|
+
const invalid = () => setupAuditError("Setup audit plan proof shape is invalid.");
|
|
923
|
+
if (!hasExactKeys(value, [
|
|
924
|
+
"schemaVersion",
|
|
925
|
+
"kind",
|
|
926
|
+
"sourceFileSha256",
|
|
927
|
+
"planId",
|
|
928
|
+
"planSha256",
|
|
929
|
+
"createdAt",
|
|
930
|
+
"cli",
|
|
931
|
+
"workspace",
|
|
932
|
+
"install",
|
|
933
|
+
"selection",
|
|
934
|
+
"sources",
|
|
935
|
+
"skills",
|
|
936
|
+
"acceptedLicenses",
|
|
937
|
+
"credentialSources",
|
|
938
|
+
"settings",
|
|
939
|
+
"agentRoutes",
|
|
940
|
+
"reviewerExecution",
|
|
941
|
+
"checks",
|
|
942
|
+
"confirmations",
|
|
943
|
+
"mutations",
|
|
944
|
+
]) ||
|
|
945
|
+
value.schemaVersion !== 1 ||
|
|
946
|
+
value.kind !== "tiangong-setup-plan-proof" ||
|
|
947
|
+
!isSha(value.sourceFileSha256) ||
|
|
948
|
+
typeof value.planId !== "string" ||
|
|
949
|
+
!isSha(value.planSha256) ||
|
|
950
|
+
typeof value.createdAt !== "string" ||
|
|
951
|
+
!validCliBinding(value.cli) ||
|
|
952
|
+
!validPlanWorkspace(value.workspace) ||
|
|
953
|
+
!validPlanInstall(value.install) ||
|
|
954
|
+
!validPlanSelection(value.selection) ||
|
|
955
|
+
!closedObjectArray(value.sources, ["id", "repository", "locator", "immutableRef"], (item) => Object.values(item).every((field) => typeof field === "string")) ||
|
|
956
|
+
!closedObjectArray(value.skills, [
|
|
957
|
+
"id",
|
|
958
|
+
"skillName",
|
|
959
|
+
"sourceId",
|
|
960
|
+
"sourceRelativePath",
|
|
961
|
+
"expectedTreeSha256",
|
|
962
|
+
"role",
|
|
963
|
+
"licenseId",
|
|
964
|
+
], (item) => ["id", "skillName", "sourceId", "sourceRelativePath", "role", "licenseId"].every((key) => typeof item[key] === "string") && isSha(item.expectedTreeSha256)) ||
|
|
965
|
+
!closedObjectArray(value.acceptedLicenses, ["skillId", "licenseId", "accepted"], (item) => Boolean(typeof item.skillId === "string" &&
|
|
966
|
+
typeof item.licenseId === "string" &&
|
|
967
|
+
item.accepted === true)) ||
|
|
968
|
+
!closedObjectArray(value.credentialSources, ["id", "storage", "configured"], (item) => Boolean(typeof item.id === "string" && typeof item.storage === "string" && item.configured === true)) ||
|
|
969
|
+
!closedObjectArray(value.settings, ["id", "valueSha256"], (item) => Boolean(typeof item.id === "string" && isSha(item.valueSha256))) ||
|
|
970
|
+
!validAgentRoutes(value.agentRoutes) ||
|
|
971
|
+
!isObject(value.reviewerExecution) ||
|
|
972
|
+
!hasExactKeys(value.reviewerExecution, ["transport", "isolationProvider"]) ||
|
|
973
|
+
!["native-direct", "sandbox-bridge"].includes(String(value.reviewerExecution.transport)) ||
|
|
974
|
+
value.reviewerExecution.isolationProvider !== "platform-capsule" ||
|
|
975
|
+
!isObject(value.checks) ||
|
|
976
|
+
!hasExactKeys(value.checks, ["live", "allowSyntheticUnstructureUpload", "agentSmoke"]) ||
|
|
977
|
+
Object.values(value.checks).some((item) => typeof item !== "boolean") ||
|
|
978
|
+
!isObject(value.confirmations) ||
|
|
979
|
+
!hasExactKeys(value.confirmations, ["networkDownloads", "globalMutation", "agentSmokeCost"]) ||
|
|
980
|
+
Object.values(value.confirmations).some((item) => typeof item !== "boolean") ||
|
|
981
|
+
!closedObjectArray(value.mutations, ["step", "targetSha256", "reason"], (item) => Boolean(typeof item.step === "string" &&
|
|
982
|
+
isSha(item.targetSha256) &&
|
|
983
|
+
typeof item.reason === "string"))) {
|
|
984
|
+
throw invalid();
|
|
985
|
+
}
|
|
986
|
+
return value;
|
|
987
|
+
}
|
|
988
|
+
function validCliBinding(value) {
|
|
989
|
+
return (isObject(value) &&
|
|
990
|
+
hasExactKeys(value, ["package", "version"]) &&
|
|
991
|
+
value.package === "@tiangong-ai/cli" &&
|
|
992
|
+
typeof value.version === "string");
|
|
993
|
+
}
|
|
994
|
+
function validPlanWorkspace(value) {
|
|
995
|
+
return (isObject(value) &&
|
|
996
|
+
hasExactKeys(value, ["nameSha256", "mode"]) &&
|
|
997
|
+
isSha(value.nameSha256) &&
|
|
998
|
+
["smoke-test", "production-research"].includes(String(value.mode)));
|
|
999
|
+
}
|
|
1000
|
+
function validPlanInstall(value) {
|
|
1001
|
+
if (!isObject(value) ||
|
|
1002
|
+
!hasExactKeys(value, ["scope", "agents", "mode", "installer", "targets"]) ||
|
|
1003
|
+
!["project", "global"].includes(String(value.scope)) ||
|
|
1004
|
+
value.mode !== "copy" ||
|
|
1005
|
+
!stringArrayIs(value.agents, ["codex", "claude-code"])) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
const installer = value.installer;
|
|
1009
|
+
return (isObject(installer) &&
|
|
1010
|
+
hasExactKeys(installer, [
|
|
1011
|
+
"package",
|
|
1012
|
+
"version",
|
|
1013
|
+
"npmIntegrity",
|
|
1014
|
+
"npmShasum",
|
|
1015
|
+
"gitHead",
|
|
1016
|
+
"runtimeInstall",
|
|
1017
|
+
]) &&
|
|
1018
|
+
["package", "version", "npmIntegrity", "npmShasum", "gitHead"].every((key) => typeof installer[key] === "string") &&
|
|
1019
|
+
installer.runtimeInstall === false &&
|
|
1020
|
+
closedObjectArray(value.targets, ["agent", "rootSha256"], (item) => Boolean(["codex", "claude-code"].includes(String(item.agent)) && isSha(item.rootSha256))));
|
|
1021
|
+
}
|
|
1022
|
+
function validPlanSelection(value) {
|
|
1023
|
+
return (isObject(value) &&
|
|
1024
|
+
hasExactKeys(value, ["evidenceProfile", "skillIds"]) &&
|
|
1025
|
+
typeof value.evidenceProfile === "string" &&
|
|
1026
|
+
stringArrayIs(value.skillIds));
|
|
1027
|
+
}
|
|
1028
|
+
function validAgentRoutes(value) {
|
|
1029
|
+
if (!isObject(value) ||
|
|
1030
|
+
!hasExactKeys(value, [
|
|
1031
|
+
"producerAgent",
|
|
1032
|
+
"reviewerAgent",
|
|
1033
|
+
"producerModel",
|
|
1034
|
+
"reviewerModel",
|
|
1035
|
+
"producerPricing",
|
|
1036
|
+
"reviewerPricing",
|
|
1037
|
+
]) ||
|
|
1038
|
+
!["codex", "claude", "workbuddy", "codebuddy"].includes(String(value.producerAgent)) ||
|
|
1039
|
+
!["codex", "claude"].includes(String(value.reviewerAgent)) ||
|
|
1040
|
+
!nullableString(value.producerModel) ||
|
|
1041
|
+
!nullableString(value.reviewerModel)) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
return [value.producerPricing, value.reviewerPricing].every(validPricing);
|
|
1045
|
+
}
|
|
1046
|
+
function validPricing(value) {
|
|
1047
|
+
return (value === null ||
|
|
1048
|
+
(isObject(value) &&
|
|
1049
|
+
hasExactKeys(value, [
|
|
1050
|
+
"inputUsdPerMillionTokens",
|
|
1051
|
+
"cachedInputUsdPerMillionTokens",
|
|
1052
|
+
"outputUsdPerMillionTokens",
|
|
1053
|
+
]) &&
|
|
1054
|
+
Object.values(value).every((item) => typeof item === "number" && Number.isFinite(item))));
|
|
1055
|
+
}
|
|
1056
|
+
function parsePortableSetupState(value) {
|
|
1057
|
+
const validLastError = value.lastError === null ||
|
|
1058
|
+
(isObject(value.lastError) &&
|
|
1059
|
+
hasExactKeys(value.lastError, ["code", "step", "reasonSha256", "minimumActionSha256"]) &&
|
|
1060
|
+
nullableString(value.lastError.code) &&
|
|
1061
|
+
nullableString(value.lastError.step) &&
|
|
1062
|
+
isNullableSha(value.lastError.reasonSha256) &&
|
|
1063
|
+
isNullableSha(value.lastError.minimumActionSha256));
|
|
1064
|
+
if (!hasExactKeys(value, [
|
|
1065
|
+
"schemaVersion",
|
|
1066
|
+
"kind",
|
|
1067
|
+
"sourceFileSha256",
|
|
1068
|
+
"planSha256",
|
|
1069
|
+
"status",
|
|
1070
|
+
"currentStep",
|
|
1071
|
+
"completedSteps",
|
|
1072
|
+
"attempts",
|
|
1073
|
+
"updatedAt",
|
|
1074
|
+
"lastError",
|
|
1075
|
+
]) ||
|
|
1076
|
+
value.schemaVersion !== 1 ||
|
|
1077
|
+
value.kind !== "tiangong-setup-state-proof" ||
|
|
1078
|
+
!isSha(value.sourceFileSha256) ||
|
|
1079
|
+
!isSha(value.planSha256) ||
|
|
1080
|
+
!["pending", "applying", "partially-ready", "ready", "blocked"].includes(String(value.status)) ||
|
|
1081
|
+
!nullableString(value.currentStep) ||
|
|
1082
|
+
!stringArrayIs(value.completedSteps) ||
|
|
1083
|
+
!Number.isSafeInteger(value.attempts) ||
|
|
1084
|
+
Number(value.attempts) < 0 ||
|
|
1085
|
+
typeof value.updatedAt !== "string" ||
|
|
1086
|
+
!validLastError) {
|
|
1087
|
+
throw setupAuditError("Setup audit state proof shape is invalid.");
|
|
1088
|
+
}
|
|
1089
|
+
return value;
|
|
1090
|
+
}
|
|
1091
|
+
function parsePortableSetupReport(value) {
|
|
1092
|
+
const checksValid = closedObjectArray(value.checks, [
|
|
1093
|
+
"id",
|
|
1094
|
+
"category",
|
|
1095
|
+
"status",
|
|
1096
|
+
"scope",
|
|
1097
|
+
"componentIds",
|
|
1098
|
+
"requiredFor",
|
|
1099
|
+
"blocking",
|
|
1100
|
+
"componentGate",
|
|
1101
|
+
"skippedBecauseSha256",
|
|
1102
|
+
"diagnostics",
|
|
1103
|
+
], (item) => nullableString(item.id) &&
|
|
1104
|
+
nullableString(item.category) &&
|
|
1105
|
+
nullableString(item.status) &&
|
|
1106
|
+
nullableString(item.scope) &&
|
|
1107
|
+
stringArrayIs(item.componentIds) &&
|
|
1108
|
+
stringArrayIs(item.requiredFor) &&
|
|
1109
|
+
nullableBoolean(item.blocking) &&
|
|
1110
|
+
nullableBoolean(item.componentGate) &&
|
|
1111
|
+
isNullableSha(item.skippedBecauseSha256) &&
|
|
1112
|
+
validPortableDiagnostics(item.diagnostics));
|
|
1113
|
+
const summaryValid = value.summary === null ||
|
|
1114
|
+
(isObject(value.summary) &&
|
|
1115
|
+
hasExactKeys(value.summary, ["pass", "warn", "fail"]) &&
|
|
1116
|
+
Object.values(value.summary).every(nullableNumber));
|
|
1117
|
+
if (!hasExactKeys(value, [
|
|
1118
|
+
"schemaVersion",
|
|
1119
|
+
"kind",
|
|
1120
|
+
"sourceFileSha256",
|
|
1121
|
+
"planSha256",
|
|
1122
|
+
"checkedAt",
|
|
1123
|
+
"mode",
|
|
1124
|
+
"readiness",
|
|
1125
|
+
"researchReadiness",
|
|
1126
|
+
"preprocessingReadiness",
|
|
1127
|
+
"acquisitionReadiness",
|
|
1128
|
+
"authoringReadiness",
|
|
1129
|
+
"overallReadiness",
|
|
1130
|
+
"checks",
|
|
1131
|
+
"summary",
|
|
1132
|
+
]) ||
|
|
1133
|
+
value.schemaVersion !== 1 ||
|
|
1134
|
+
value.kind !== "tiangong-setup-report-proof" ||
|
|
1135
|
+
!isSha(value.sourceFileSha256) ||
|
|
1136
|
+
!isSha(value.planSha256) ||
|
|
1137
|
+
[
|
|
1138
|
+
value.checkedAt,
|
|
1139
|
+
value.mode,
|
|
1140
|
+
value.readiness,
|
|
1141
|
+
value.researchReadiness,
|
|
1142
|
+
value.preprocessingReadiness,
|
|
1143
|
+
value.acquisitionReadiness,
|
|
1144
|
+
value.authoringReadiness,
|
|
1145
|
+
value.overallReadiness,
|
|
1146
|
+
].some((item) => !nullableString(item)) ||
|
|
1147
|
+
!checksValid ||
|
|
1148
|
+
!summaryValid) {
|
|
1149
|
+
throw setupAuditError("Setup audit report proof shape is invalid.");
|
|
1150
|
+
}
|
|
1151
|
+
return value;
|
|
1152
|
+
}
|
|
1153
|
+
function validPortableDiagnostics(value) {
|
|
1154
|
+
return (value === null ||
|
|
1155
|
+
(isObject(value) &&
|
|
1156
|
+
hasExactKeys(value, [
|
|
1157
|
+
"code",
|
|
1158
|
+
"executionMode",
|
|
1159
|
+
"credentialScope",
|
|
1160
|
+
"networkAttempted",
|
|
1161
|
+
"httpStatus",
|
|
1162
|
+
"retryAfterSeconds",
|
|
1163
|
+
]) &&
|
|
1164
|
+
nullableString(value.code) &&
|
|
1165
|
+
nullableString(value.executionMode) &&
|
|
1166
|
+
nullableString(value.credentialScope) &&
|
|
1167
|
+
nullableBoolean(value.networkAttempted) &&
|
|
1168
|
+
nullableNumber(value.httpStatus) &&
|
|
1169
|
+
nullableNumber(value.retryAfterSeconds)));
|
|
1170
|
+
}
|
|
1171
|
+
function parseRuntimeLock(value) {
|
|
1172
|
+
if (!isObject(value) ||
|
|
1173
|
+
!hasExactKeys(value, [
|
|
1174
|
+
"schemaVersion",
|
|
1175
|
+
"protocolVersion",
|
|
1176
|
+
"packageName",
|
|
1177
|
+
"packageVersion",
|
|
1178
|
+
"workspaceId",
|
|
1179
|
+
]) ||
|
|
1180
|
+
value.schemaVersion !== 1 ||
|
|
1181
|
+
value.protocolVersion !== 1 ||
|
|
1182
|
+
value.packageName !== "@tiangong-ai/cli" ||
|
|
1183
|
+
typeof value.packageVersion !== "string" ||
|
|
1184
|
+
typeof value.workspaceId !== "string") {
|
|
1185
|
+
throw setupAuditError("Setup audit runtime lock shape is invalid.");
|
|
1186
|
+
}
|
|
1187
|
+
return value;
|
|
1188
|
+
}
|
|
1189
|
+
function parsePortableCapabilities(value) {
|
|
1190
|
+
if (!hasExactKeys(value, ["schemaVersion", "kind", "sourceFileSha256", "capabilities"]) ||
|
|
1191
|
+
value.schemaVersion !== 1 ||
|
|
1192
|
+
value.kind !== "tiangong-capability-declarations-proof" ||
|
|
1193
|
+
!isSha(value.sourceFileSha256) ||
|
|
1194
|
+
!closedObjectArray(value.capabilities, [
|
|
1195
|
+
"id",
|
|
1196
|
+
"skillPath",
|
|
1197
|
+
"source",
|
|
1198
|
+
"requiredForDiscovery",
|
|
1199
|
+
"permissions",
|
|
1200
|
+
"allowedHosts",
|
|
1201
|
+
"http",
|
|
1202
|
+
"coverage",
|
|
1203
|
+
"credentials",
|
|
1204
|
+
"healthCheck",
|
|
1205
|
+
], validPortableCapability)) {
|
|
1206
|
+
throw setupAuditError("Setup audit capability declaration proof shape is invalid.");
|
|
1207
|
+
}
|
|
1208
|
+
return value;
|
|
1209
|
+
}
|
|
1210
|
+
function validPortableCapability(value) {
|
|
1211
|
+
return (typeof value.id === "string" &&
|
|
1212
|
+
typeof value.skillPath === "string" &&
|
|
1213
|
+
validPortableCapabilitySource(value.source) &&
|
|
1214
|
+
typeof value.requiredForDiscovery === "boolean" &&
|
|
1215
|
+
stringArrayIs(value.permissions) &&
|
|
1216
|
+
stringArrayIs(value.allowedHosts) &&
|
|
1217
|
+
validCapabilityHttp(value.http) &&
|
|
1218
|
+
validCapabilityCoverage(value.coverage) &&
|
|
1219
|
+
closedObjectArray(value.credentials, ["id", "allowedHosts", "headerName", "prefixSha256"], (item) => typeof item.id === "string" &&
|
|
1220
|
+
stringArrayIs(item.allowedHosts) &&
|
|
1221
|
+
typeof item.headerName === "string" &&
|
|
1222
|
+
isSha(item.prefixSha256)) &&
|
|
1223
|
+
validPortableHealthCheck(value.healthCheck));
|
|
1224
|
+
}
|
|
1225
|
+
function validCapabilitySource(value) {
|
|
1226
|
+
return (value === null ||
|
|
1227
|
+
(isObject(value) &&
|
|
1228
|
+
hasExactKeys(value, [
|
|
1229
|
+
"type",
|
|
1230
|
+
"locator",
|
|
1231
|
+
"immutableRef",
|
|
1232
|
+
"expectedTreeSha256",
|
|
1233
|
+
"license",
|
|
1234
|
+
"catalogId",
|
|
1235
|
+
]) &&
|
|
1236
|
+
["git", "registry", "local"].includes(String(value.type)) &&
|
|
1237
|
+
typeof value.locator === "string" &&
|
|
1238
|
+
typeof value.immutableRef === "string" &&
|
|
1239
|
+
isSha(value.expectedTreeSha256) &&
|
|
1240
|
+
typeof value.license === "string" &&
|
|
1241
|
+
nullableString(value.catalogId)));
|
|
1242
|
+
}
|
|
1243
|
+
function validPortableCapabilitySource(value) {
|
|
1244
|
+
return (value === null ||
|
|
1245
|
+
(isObject(value) &&
|
|
1246
|
+
hasExactKeys(value, [
|
|
1247
|
+
"type",
|
|
1248
|
+
"locatorSha256",
|
|
1249
|
+
"immutableRef",
|
|
1250
|
+
"expectedTreeSha256",
|
|
1251
|
+
"license",
|
|
1252
|
+
"catalogId",
|
|
1253
|
+
]) &&
|
|
1254
|
+
["git", "registry", "local"].includes(String(value.type)) &&
|
|
1255
|
+
isSha(value.locatorSha256) &&
|
|
1256
|
+
typeof value.immutableRef === "string" &&
|
|
1257
|
+
isSha(value.expectedTreeSha256) &&
|
|
1258
|
+
typeof value.license === "string" &&
|
|
1259
|
+
nullableString(value.catalogId)));
|
|
1260
|
+
}
|
|
1261
|
+
function validCapabilityHttp(value) {
|
|
1262
|
+
return (value === null ||
|
|
1263
|
+
(isObject(value) &&
|
|
1264
|
+
hasExactKeys(value, [
|
|
1265
|
+
"endpoint",
|
|
1266
|
+
"method",
|
|
1267
|
+
"accept",
|
|
1268
|
+
"allowedContentTypes",
|
|
1269
|
+
"staticHeaderBindings",
|
|
1270
|
+
"maxRequestBytes",
|
|
1271
|
+
"maxResponseBytes",
|
|
1272
|
+
"maxItems",
|
|
1273
|
+
]) &&
|
|
1274
|
+
typeof value.endpoint === "string" &&
|
|
1275
|
+
["GET", "POST"].includes(String(value.method)) &&
|
|
1276
|
+
typeof value.accept === "string" &&
|
|
1277
|
+
stringArrayIs(value.allowedContentTypes) &&
|
|
1278
|
+
closedObjectArray(value.staticHeaderBindings, ["name", "valueSha256"], (item) => Boolean(typeof item.name === "string" && isSha(item.valueSha256))) &&
|
|
1279
|
+
[value.maxRequestBytes, value.maxResponseBytes, value.maxItems].every((item) => Number.isSafeInteger(item) && Number(item) >= 0)));
|
|
1280
|
+
}
|
|
1281
|
+
function validCapabilityCoverage(value) {
|
|
1282
|
+
return (value === null ||
|
|
1283
|
+
(isObject(value) &&
|
|
1284
|
+
hasExactKeys(value, [
|
|
1285
|
+
"dimensions",
|
|
1286
|
+
"sourceTypes",
|
|
1287
|
+
"discoveryScopes",
|
|
1288
|
+
"fullText",
|
|
1289
|
+
"publicationDates",
|
|
1290
|
+
]) &&
|
|
1291
|
+
stringArrayIs(value.dimensions) &&
|
|
1292
|
+
stringArrayIs(value.sourceTypes) &&
|
|
1293
|
+
stringArrayIs(value.discoveryScopes) &&
|
|
1294
|
+
typeof value.fullText === "boolean" &&
|
|
1295
|
+
typeof value.publicationDates === "boolean"));
|
|
1296
|
+
}
|
|
1297
|
+
function validPortableHealthCheck(value) {
|
|
1298
|
+
return (value === null ||
|
|
1299
|
+
(isObject(value) &&
|
|
1300
|
+
hasExactKeys(value, [
|
|
1301
|
+
"targetSha256",
|
|
1302
|
+
"credentialId",
|
|
1303
|
+
"expectedContentTypes",
|
|
1304
|
+
"method",
|
|
1305
|
+
"bodySha256",
|
|
1306
|
+
]) &&
|
|
1307
|
+
isSha(value.targetSha256) &&
|
|
1308
|
+
nullableString(value.credentialId) &&
|
|
1309
|
+
stringArrayIs(value.expectedContentTypes) &&
|
|
1310
|
+
["GET", "POST"].includes(String(value.method)) &&
|
|
1311
|
+
isNullableSha(value.bodySha256)));
|
|
1312
|
+
}
|
|
1313
|
+
function parseCapabilityLock(value) {
|
|
1314
|
+
if (!isObject(value) ||
|
|
1315
|
+
!hasExactKeys(value, ["schemaVersion", "generatedAt", "capabilities"]) ||
|
|
1316
|
+
value.schemaVersion !== 1 ||
|
|
1317
|
+
typeof value.generatedAt !== "string" ||
|
|
1318
|
+
!closedObjectArray(value.capabilities, capabilityLockRecordKeys(), (record) => validCapabilityLockRecord(record, false))) {
|
|
1319
|
+
throw setupAuditError("Setup audit capability lock source shape is invalid.");
|
|
1320
|
+
}
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
function parsePortableCapabilityLock(value) {
|
|
1324
|
+
if (!hasExactKeys(value, [
|
|
1325
|
+
"schemaVersion",
|
|
1326
|
+
"kind",
|
|
1327
|
+
"sourceFileSha256",
|
|
1328
|
+
"generatedAt",
|
|
1329
|
+
"capabilities",
|
|
1330
|
+
]) ||
|
|
1331
|
+
value.schemaVersion !== 1 ||
|
|
1332
|
+
value.kind !== "tiangong-capability-lock-proof" ||
|
|
1333
|
+
!isSha(value.sourceFileSha256) ||
|
|
1334
|
+
typeof value.generatedAt !== "string" ||
|
|
1335
|
+
!closedObjectArray(value.capabilities, capabilityLockRecordKeys(), (record) => validCapabilityLockRecord(record, true))) {
|
|
1336
|
+
throw setupAuditError("Setup audit capability lock proof shape is invalid.");
|
|
1337
|
+
}
|
|
1338
|
+
return value;
|
|
1339
|
+
}
|
|
1340
|
+
function capabilityLockRecordKeys() {
|
|
1341
|
+
return [
|
|
1342
|
+
"id",
|
|
1343
|
+
"skillName",
|
|
1344
|
+
"skillPath",
|
|
1345
|
+
"treeSha256",
|
|
1346
|
+
"policySha256",
|
|
1347
|
+
"source",
|
|
1348
|
+
"requiredForDiscovery",
|
|
1349
|
+
"permissions",
|
|
1350
|
+
"credentialIds",
|
|
1351
|
+
"discoveryScopes",
|
|
1352
|
+
"healthTargetSha256",
|
|
1353
|
+
];
|
|
1354
|
+
}
|
|
1355
|
+
function validCapabilityLockRecord(value, portable) {
|
|
1356
|
+
return (typeof value.id === "string" &&
|
|
1357
|
+
typeof value.skillName === "string" &&
|
|
1358
|
+
typeof value.skillPath === "string" &&
|
|
1359
|
+
isSha(value.treeSha256) &&
|
|
1360
|
+
isSha(value.policySha256) &&
|
|
1361
|
+
(portable
|
|
1362
|
+
? validPortableCapabilitySource(value.source)
|
|
1363
|
+
: validCapabilitySource(value.source)) &&
|
|
1364
|
+
typeof value.requiredForDiscovery === "boolean" &&
|
|
1365
|
+
stringArrayIs(value.permissions) &&
|
|
1366
|
+
stringArrayIs(value.credentialIds) &&
|
|
1367
|
+
stringArrayIs(value.discoveryScopes) &&
|
|
1368
|
+
isNullableSha(value.healthTargetSha256));
|
|
1369
|
+
}
|
|
1370
|
+
function parseSetupDeclarationBinding(value) {
|
|
1371
|
+
if (!isObject(value) ||
|
|
1372
|
+
!hasExactKeys(value, ["schemaVersion", "kind", "configurationSha256", "planSha256"]) ||
|
|
1373
|
+
value.schemaVersion !== 1 ||
|
|
1374
|
+
value.kind !== "tiangong-research-setup-declaration-binding" ||
|
|
1375
|
+
!isSha(value.configurationSha256) ||
|
|
1376
|
+
!isSha(value.planSha256)) {
|
|
1377
|
+
throw setupAuditError("Setup audit declaration binding shape is invalid.");
|
|
1378
|
+
}
|
|
1379
|
+
return value;
|
|
1380
|
+
}
|
|
1381
|
+
function parsePortableDoctorAttestation(value) {
|
|
1382
|
+
if (!hasExactKeys(value, [
|
|
1383
|
+
"schemaVersion",
|
|
1384
|
+
"kind",
|
|
1385
|
+
"sourceFileSha256",
|
|
1386
|
+
"attestationSha256",
|
|
1387
|
+
"workspaceIdSha256",
|
|
1388
|
+
"checkedAt",
|
|
1389
|
+
"expiresAt",
|
|
1390
|
+
"configSha256",
|
|
1391
|
+
"runtimeLockSha256",
|
|
1392
|
+
"capabilityDeclarationsSha256",
|
|
1393
|
+
"capabilityLockSha256",
|
|
1394
|
+
"doctorSchemaSha256",
|
|
1395
|
+
"reviewerExecution",
|
|
1396
|
+
"runtimes",
|
|
1397
|
+
"capabilitySmoke",
|
|
1398
|
+
"smokeUsage",
|
|
1399
|
+
]) ||
|
|
1400
|
+
value.schemaVersion !== 1 ||
|
|
1401
|
+
value.kind !== "tiangong-doctor-attestation-proof" ||
|
|
1402
|
+
![
|
|
1403
|
+
value.sourceFileSha256,
|
|
1404
|
+
value.attestationSha256,
|
|
1405
|
+
value.workspaceIdSha256,
|
|
1406
|
+
value.configSha256,
|
|
1407
|
+
value.runtimeLockSha256,
|
|
1408
|
+
value.capabilityDeclarationsSha256,
|
|
1409
|
+
value.capabilityLockSha256,
|
|
1410
|
+
value.doctorSchemaSha256,
|
|
1411
|
+
].every(isSha) ||
|
|
1412
|
+
typeof value.checkedAt !== "string" ||
|
|
1413
|
+
typeof value.expiresAt !== "string" ||
|
|
1414
|
+
!validDoctorReviewerExecution(value.reviewerExecution) ||
|
|
1415
|
+
!closedObjectArray(value.runtimes, [
|
|
1416
|
+
"agent",
|
|
1417
|
+
"modelSha256",
|
|
1418
|
+
"effort",
|
|
1419
|
+
"verbosity",
|
|
1420
|
+
"binarySha256",
|
|
1421
|
+
"wrapperSha256",
|
|
1422
|
+
"adapterSha256",
|
|
1423
|
+
"binaryVersionSha256",
|
|
1424
|
+
"platform",
|
|
1425
|
+
"architecture",
|
|
1426
|
+
], (runtime) => ["codex", "claude", "workbuddy", "codebuddy"].includes(String(runtime.agent)) &&
|
|
1427
|
+
isNullableSha(runtime.modelSha256) &&
|
|
1428
|
+
nullableString(runtime.effort) &&
|
|
1429
|
+
nullableString(runtime.verbosity) &&
|
|
1430
|
+
isSha(runtime.binarySha256) &&
|
|
1431
|
+
isSha(runtime.wrapperSha256) &&
|
|
1432
|
+
isSha(runtime.adapterSha256) &&
|
|
1433
|
+
isSha(runtime.binaryVersionSha256) &&
|
|
1434
|
+
typeof runtime.platform === "string" &&
|
|
1435
|
+
typeof runtime.architecture === "string") ||
|
|
1436
|
+
!closedObjectArray(value.capabilitySmoke, ["id", "status", "code", "hostSha256", "targetSha256", "httpStatus"], (row) => typeof row.id === "string" &&
|
|
1437
|
+
["pass", "not-applicable"].includes(String(row.status)) &&
|
|
1438
|
+
typeof row.code === "string" &&
|
|
1439
|
+
isNullableSha(row.hostSha256) &&
|
|
1440
|
+
isNullableSha(row.targetSha256) &&
|
|
1441
|
+
nullableNumber(row.httpStatus)) ||
|
|
1442
|
+
!closedObjectArray(value.smokeUsage, [
|
|
1443
|
+
"agent",
|
|
1444
|
+
"tokens",
|
|
1445
|
+
"inputTokens",
|
|
1446
|
+
"cachedInputTokens",
|
|
1447
|
+
"outputTokens",
|
|
1448
|
+
"costUsd",
|
|
1449
|
+
"wallSeconds",
|
|
1450
|
+
"telemetrySha256",
|
|
1451
|
+
], (usage) => ["codex", "claude", "workbuddy", "codebuddy"].includes(String(usage.agent)) &&
|
|
1452
|
+
[
|
|
1453
|
+
usage.tokens,
|
|
1454
|
+
usage.inputTokens,
|
|
1455
|
+
usage.cachedInputTokens,
|
|
1456
|
+
usage.outputTokens,
|
|
1457
|
+
usage.costUsd,
|
|
1458
|
+
usage.wallSeconds,
|
|
1459
|
+
].every((item) => typeof item === "number" && Number.isFinite(item)) &&
|
|
1460
|
+
isNullableSha(usage.telemetrySha256))) {
|
|
1461
|
+
throw setupAuditError("Setup audit doctor attestation proof shape is invalid.");
|
|
1462
|
+
}
|
|
1463
|
+
return value;
|
|
1464
|
+
}
|
|
1465
|
+
function parseDoctorAttestation(value) {
|
|
1466
|
+
if (!isObject(value) || !validDoctorAttestationShape(value)) {
|
|
1467
|
+
throw new CliError("Doctor attestation is invalid and cannot be exported.", {
|
|
1468
|
+
code: "RESEARCH_SETUP_AUDIT_ATTESTATION_INVALID",
|
|
1469
|
+
exitCode: 3,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
const { attestationSha256, ...core } = value;
|
|
1473
|
+
if (sha256Text(canonicalJson(core)) !== attestationSha256) {
|
|
1474
|
+
throw new CliError("Doctor attestation is invalid and cannot be exported.", {
|
|
1475
|
+
code: "RESEARCH_SETUP_AUDIT_ATTESTATION_INVALID",
|
|
1476
|
+
exitCode: 3,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return value;
|
|
1480
|
+
}
|
|
1481
|
+
function validDoctorAttestationShape(value) {
|
|
1482
|
+
return (hasExactKeys(value, [
|
|
1483
|
+
"schemaVersion",
|
|
1484
|
+
"workspaceId",
|
|
1485
|
+
"checkedAt",
|
|
1486
|
+
"expiresAt",
|
|
1487
|
+
"configSha256",
|
|
1488
|
+
"runtimeLockSha256",
|
|
1489
|
+
"capabilityDeclarationsSha256",
|
|
1490
|
+
"capabilityLockSha256",
|
|
1491
|
+
"doctorSchemaSha256",
|
|
1492
|
+
"reviewerExecution",
|
|
1493
|
+
"runtimes",
|
|
1494
|
+
"capabilitySmoke",
|
|
1495
|
+
"smokeUsage",
|
|
1496
|
+
"attestationSha256",
|
|
1497
|
+
]) &&
|
|
1498
|
+
value.schemaVersion === 1 &&
|
|
1499
|
+
typeof value.workspaceId === "string" &&
|
|
1500
|
+
typeof value.checkedAt === "string" &&
|
|
1501
|
+
typeof value.expiresAt === "string" &&
|
|
1502
|
+
[
|
|
1503
|
+
value.configSha256,
|
|
1504
|
+
value.runtimeLockSha256,
|
|
1505
|
+
value.capabilityDeclarationsSha256,
|
|
1506
|
+
value.capabilityLockSha256,
|
|
1507
|
+
value.doctorSchemaSha256,
|
|
1508
|
+
value.attestationSha256,
|
|
1509
|
+
].every(isSha) &&
|
|
1510
|
+
validDoctorReviewerExecution(value.reviewerExecution) &&
|
|
1511
|
+
Array.isArray(value.runtimes) &&
|
|
1512
|
+
value.runtimes.every(validDoctorRuntime) &&
|
|
1513
|
+
closedObjectArray(value.capabilitySmoke, ["id", "status", "code", "host", "targetSha256", "httpStatus"], (item) => typeof item.id === "string" &&
|
|
1514
|
+
["pass", "not-applicable"].includes(String(item.status)) &&
|
|
1515
|
+
typeof item.code === "string" &&
|
|
1516
|
+
nullableString(item.host) &&
|
|
1517
|
+
isNullableSha(item.targetSha256) &&
|
|
1518
|
+
nullableNumber(item.httpStatus)) &&
|
|
1519
|
+
Array.isArray(value.smokeUsage) &&
|
|
1520
|
+
value.smokeUsage.every(validDoctorUsage));
|
|
1521
|
+
}
|
|
1522
|
+
function validDoctorReviewerExecution(value) {
|
|
1523
|
+
return (isObject(value) &&
|
|
1524
|
+
hasExactKeys(value, [
|
|
1525
|
+
"transport",
|
|
1526
|
+
"isolationProvider",
|
|
1527
|
+
"policySha256",
|
|
1528
|
+
"signerKeyFingerprint",
|
|
1529
|
+
]) &&
|
|
1530
|
+
["native-direct", "sandbox-bridge"].includes(String(value.transport)) &&
|
|
1531
|
+
["sandbox-exec", "bubblewrap"].includes(String(value.isolationProvider)) &&
|
|
1532
|
+
isSha(value.policySha256) &&
|
|
1533
|
+
nullableString(value.signerKeyFingerprint));
|
|
1534
|
+
}
|
|
1535
|
+
function validDoctorRuntime(value) {
|
|
1536
|
+
if (!isObject(value))
|
|
1537
|
+
return false;
|
|
1538
|
+
const allowed = [
|
|
1539
|
+
"agent",
|
|
1540
|
+
"model",
|
|
1541
|
+
"effort",
|
|
1542
|
+
"verbosity",
|
|
1543
|
+
"binarySha256",
|
|
1544
|
+
"wrapperSha256",
|
|
1545
|
+
"adapterSha256",
|
|
1546
|
+
"binaryVersion",
|
|
1547
|
+
"platform",
|
|
1548
|
+
"architecture",
|
|
1549
|
+
];
|
|
1550
|
+
return (hasOnlyKeys(value, allowed) &&
|
|
1551
|
+
[
|
|
1552
|
+
"agent",
|
|
1553
|
+
"model",
|
|
1554
|
+
"binarySha256",
|
|
1555
|
+
"wrapperSha256",
|
|
1556
|
+
"adapterSha256",
|
|
1557
|
+
"binaryVersion",
|
|
1558
|
+
"platform",
|
|
1559
|
+
"architecture",
|
|
1560
|
+
].every((key) => Object.hasOwn(value, key)) &&
|
|
1561
|
+
["codex", "claude", "workbuddy", "codebuddy"].includes(String(value.agent)) &&
|
|
1562
|
+
nullableString(value.model) &&
|
|
1563
|
+
(value.effort === undefined || typeof value.effort === "string") &&
|
|
1564
|
+
(value.verbosity === undefined || nullableString(value.verbosity)) &&
|
|
1565
|
+
isSha(value.binarySha256) &&
|
|
1566
|
+
isSha(value.wrapperSha256) &&
|
|
1567
|
+
isSha(value.adapterSha256) &&
|
|
1568
|
+
typeof value.binaryVersion === "string" &&
|
|
1569
|
+
typeof value.platform === "string" &&
|
|
1570
|
+
typeof value.architecture === "string");
|
|
1571
|
+
}
|
|
1572
|
+
function validDoctorUsage(value) {
|
|
1573
|
+
if (!isObject(value))
|
|
1574
|
+
return false;
|
|
1575
|
+
return (hasOnlyKeys(value, [
|
|
1576
|
+
"agent",
|
|
1577
|
+
"tokens",
|
|
1578
|
+
"inputTokens",
|
|
1579
|
+
"cachedInputTokens",
|
|
1580
|
+
"outputTokens",
|
|
1581
|
+
"costUsd",
|
|
1582
|
+
"wallSeconds",
|
|
1583
|
+
"telemetry",
|
|
1584
|
+
]) &&
|
|
1585
|
+
[
|
|
1586
|
+
"agent",
|
|
1587
|
+
"tokens",
|
|
1588
|
+
"inputTokens",
|
|
1589
|
+
"cachedInputTokens",
|
|
1590
|
+
"outputTokens",
|
|
1591
|
+
"costUsd",
|
|
1592
|
+
"wallSeconds",
|
|
1593
|
+
].every((key) => Object.hasOwn(value, key)) &&
|
|
1594
|
+
["codex", "claude", "workbuddy", "codebuddy"].includes(String(value.agent)) &&
|
|
1595
|
+
[
|
|
1596
|
+
value.tokens,
|
|
1597
|
+
value.inputTokens,
|
|
1598
|
+
value.cachedInputTokens,
|
|
1599
|
+
value.outputTokens,
|
|
1600
|
+
value.costUsd,
|
|
1601
|
+
value.wallSeconds,
|
|
1602
|
+
].every((item) => typeof item === "number" && Number.isFinite(item)) &&
|
|
1603
|
+
(value.telemetry === undefined || validDoctorTelemetry(value.telemetry)));
|
|
1604
|
+
}
|
|
1605
|
+
function validDoctorTelemetry(value) {
|
|
1606
|
+
return (isObject(value) &&
|
|
1607
|
+
hasExactKeys(value, [
|
|
1608
|
+
"eventCounts",
|
|
1609
|
+
"itemCounts",
|
|
1610
|
+
"toolCalls",
|
|
1611
|
+
"providerTurns",
|
|
1612
|
+
"reasoningOutputTokens",
|
|
1613
|
+
"providerErrors",
|
|
1614
|
+
]) &&
|
|
1615
|
+
numberRecordIs(value.eventCounts) &&
|
|
1616
|
+
numberRecordIs(value.itemCounts) &&
|
|
1617
|
+
typeof value.toolCalls === "number" &&
|
|
1618
|
+
nullableNumber(value.providerTurns) &&
|
|
1619
|
+
typeof value.reasoningOutputTokens === "number" &&
|
|
1620
|
+
stringArrayIs(value.providerErrors));
|
|
1621
|
+
}
|
|
1622
|
+
function readBundleJson(snapshots, logical) {
|
|
1623
|
+
const value = snapshots.get(logical)?.value;
|
|
1624
|
+
if (!isObject(value))
|
|
1625
|
+
throw setupAuditError(`Setup audit proof is invalid: ${logical}.`);
|
|
1626
|
+
return value;
|
|
1627
|
+
}
|
|
1628
|
+
function hasExactKeys(value, expected) {
|
|
1629
|
+
const actual = Object.keys(value).sort();
|
|
1630
|
+
const sortedExpected = [...expected].sort();
|
|
1631
|
+
return (actual.length === sortedExpected.length &&
|
|
1632
|
+
actual.every((key, index) => key === sortedExpected[index]));
|
|
1633
|
+
}
|
|
1634
|
+
function hasOnlyKeys(value, allowed) {
|
|
1635
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
1636
|
+
}
|
|
1637
|
+
function closedObjectArray(value, keys, predicate) {
|
|
1638
|
+
return (Array.isArray(value) &&
|
|
1639
|
+
value.every((item) => isObject(item) && hasExactKeys(item, keys) && predicate(item)));
|
|
1640
|
+
}
|
|
1641
|
+
function stringArrayIs(value, allowed) {
|
|
1642
|
+
return (Array.isArray(value) &&
|
|
1643
|
+
value.every((item) => typeof item === "string" && (allowed === undefined || allowed.includes(item))));
|
|
1644
|
+
}
|
|
1645
|
+
function numberRecordIs(value) {
|
|
1646
|
+
return (isObject(value) &&
|
|
1647
|
+
Object.values(value).every((item) => typeof item === "number" && Number.isFinite(item)));
|
|
1648
|
+
}
|
|
1649
|
+
function nullableString(value) {
|
|
1650
|
+
return value === null || typeof value === "string";
|
|
1651
|
+
}
|
|
1652
|
+
function nullableBoolean(value) {
|
|
1653
|
+
return value === null || typeof value === "boolean";
|
|
1654
|
+
}
|
|
1655
|
+
function nullableNumber(value) {
|
|
1656
|
+
return value === null || (typeof value === "number" && Number.isFinite(value));
|
|
1657
|
+
}
|
|
1658
|
+
function isSha(value) {
|
|
1659
|
+
return typeof value === "string" && SHA256.test(value);
|
|
1660
|
+
}
|
|
1661
|
+
function isNullableSha(value) {
|
|
1662
|
+
return value === null || isSha(value);
|
|
1663
|
+
}
|
|
1664
|
+
function safePathOrNull(value) {
|
|
1665
|
+
try {
|
|
1666
|
+
return safeRelativePath(value, "Setup audit manifest path");
|
|
1667
|
+
}
|
|
1668
|
+
catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function stringOrNull(value) {
|
|
1673
|
+
return typeof value === "string" ? value : null;
|
|
1674
|
+
}
|
|
1675
|
+
function stringArray(value) {
|
|
1676
|
+
return Array.isArray(value)
|
|
1677
|
+
? value.filter((item) => typeof item === "string")
|
|
1678
|
+
: [];
|
|
1679
|
+
}
|
|
1680
|
+
function numberOrNull(value) {
|
|
1681
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1682
|
+
}
|
|
1683
|
+
function setupAuditError(message) {
|
|
1684
|
+
return new CliError(message, {
|
|
1685
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_INVALID",
|
|
1686
|
+
exitCode: 3,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function setupAuditPathError(message) {
|
|
1690
|
+
return new CliError(message, {
|
|
1691
|
+
code: "RESEARCH_SETUP_AUDIT_BUNDLE_PATH_INVALID",
|
|
1692
|
+
exitCode: 2,
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
//# sourceMappingURL=setup-audit-bundle.js.map
|