@tiangong-ai/cli 0.0.43 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/AGENTS.md +3 -1
  2. package/README.md +76 -8
  3. package/dist/research/orchestration.js +192 -8
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/setup-command.js +13 -1
  6. package/dist/research/setup-command.js.map +1 -1
  7. package/dist/research/workspace/audit-bundle.js +1 -0
  8. package/dist/research/workspace/audit-bundle.js.map +1 -1
  9. package/dist/research/workspace/executor.d.ts +9 -0
  10. package/dist/research/workspace/executor.js +161 -4
  11. package/dist/research/workspace/executor.js.map +1 -1
  12. package/dist/research/workspace/inference.js +6 -2
  13. package/dist/research/workspace/inference.js.map +1 -1
  14. package/dist/research/workspace/preflight.d.ts +2 -0
  15. package/dist/research/workspace/preflight.js +11 -2
  16. package/dist/research/workspace/preflight.js.map +1 -1
  17. package/dist/research/workspace/projects.js +11 -1
  18. package/dist/research/workspace/projects.js.map +1 -1
  19. package/dist/research/workspace/publication-workflow.js +1 -1
  20. package/dist/research/workspace/publication-workflow.js.map +1 -1
  21. package/dist/research/workspace/review-executor.d.ts +50 -0
  22. package/dist/research/workspace/review-executor.js +854 -0
  23. package/dist/research/workspace/review-executor.js.map +1 -0
  24. package/dist/research/workspace/runtime.js +63 -15
  25. package/dist/research/workspace/runtime.js.map +1 -1
  26. package/dist/research/workspace/scientific-design.d.ts +5 -5
  27. package/dist/research/workspace/scientific-design.js +92 -16
  28. package/dist/research/workspace/scientific-design.js.map +1 -1
  29. package/dist/research/workspace/scientific-objects.d.ts +53 -0
  30. package/dist/research/workspace/scientific-objects.js +395 -0
  31. package/dist/research/workspace/scientific-objects.js.map +1 -0
  32. package/dist/research/workspace/scientific-review.d.ts +4 -0
  33. package/dist/research/workspace/scientific-review.js +87 -25
  34. package/dist/research/workspace/scientific-review.js.map +1 -1
  35. package/dist/research/workspace/setup-catalog.js +22 -2
  36. package/dist/research/workspace/setup-catalog.js.map +1 -1
  37. package/dist/research/workspace/setup-declarative.d.ts +4 -0
  38. package/dist/research/workspace/setup-declarative.js +16 -1
  39. package/dist/research/workspace/setup-declarative.js.map +1 -1
  40. package/dist/research/workspace/setup-wizard.js +46 -3
  41. package/dist/research/workspace/setup-wizard.js.map +1 -1
  42. package/dist/research/workspace/setup.d.ts +3 -1
  43. package/dist/research/workspace/setup.js +76 -18
  44. package/dist/research/workspace/setup.js.map +1 -1
  45. package/dist/research/workspace/storage.d.ts +1 -0
  46. package/dist/research/workspace/storage.js +7 -0
  47. package/dist/research/workspace/storage.js.map +1 -1
  48. package/dist/research/workspace/types.d.ts +43 -1
  49. package/dist/research/workspace/workspace.js +62 -5
  50. package/dist/research/workspace/workspace.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,854 @@
1
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify, } from "node:crypto";
2
+ import { createServer, request as httpRequest } from "node:http";
3
+ import { cp, chmod, lstat, mkdir, open, readFile, realpath, rm } from "node:fs/promises";
4
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { CliError } from "../../errors.js";
6
+ import { packageVersion, RESEARCH_PACKAGE_NAME } from "./constants.js";
7
+ import { executeAgent, fingerprintAgentRoute, probeNativeCapsuleIsolation, } from "./executor.js";
8
+ import { configuredResearchSecrets, sanitizeResearchText, sanitizeResearchValue, } from "./sanitization.js";
9
+ import { canonicalJson, ensureDirectory, hashRegularTree, isObject, sha256Bytes, sha256File, sha256Text, workspacePaths, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
10
+ import { loadWorkspaceConfig, loadWorkspaceMarker, requireCurrentRuntimeLock, } from "./workspace.js";
11
+ const REVIEW_BRIDGE_PROTOCOL_VERSION = 1;
12
+ const REVIEW_BRIDGE_MAX_BODY_BYTES = 8 * 1024 * 1024;
13
+ const REVIEW_BRIDGE_REQUEST_MAX_AGE_MS = 5 * 60 * 1000;
14
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
15
+ export function reviewerBridgePaths(root) {
16
+ const runtime = workspacePaths(root).runtime;
17
+ return {
18
+ // Darwin limits Unix-domain socket paths to roughly one hundred bytes.
19
+ // Workspaces are user-selected and may be arbitrarily deep, so bind the
20
+ // owner-only socket to a stable short path while keeping the secret client
21
+ // connection record inside the selected workspace runtime.
22
+ socket: join("/tmp", `tiangong-review-${sha256Text(resolve(root)).slice(0, 32)}.sock`),
23
+ connection: join(runtime, "reviewer-bridge.connection.json"),
24
+ };
25
+ }
26
+ export function createReviewExecutor(input) {
27
+ const executeNative = input.executeNative ?? executeAgent;
28
+ const fingerprintNative = input.fingerprintNative ?? fingerprintAgentRoute;
29
+ if (input.execution.transport === "native-direct") {
30
+ return {
31
+ transport: "native-direct",
32
+ execute: executeNative,
33
+ fingerprint: fingerprintNative,
34
+ };
35
+ }
36
+ return {
37
+ transport: "sandbox-bridge",
38
+ execute: (request) => executeThroughBridge(input.root, request, input.nonceFactory ?? secureNonce),
39
+ fingerprint: (route) => fingerprintThroughBridge(input.root, route, input.nonceFactory ?? secureNonce),
40
+ };
41
+ }
42
+ export async function inspectReviewerBridgeStatus(root) {
43
+ const binding = await bridgeBinding(root, "status", "reviewer-bridge-status", secureNonce);
44
+ const core = { ...binding.core, action: "status" };
45
+ const bridgeRequest = {
46
+ ...core,
47
+ requestSha256: sha256Text(canonicalJson(core)),
48
+ };
49
+ const response = await sendBridgeRequest(binding.connection, bridgeRequest);
50
+ const result = verifyBridgeResponse(binding.connection, bridgeRequest, response);
51
+ if (!isObject(result) ||
52
+ result.status !== "ready" ||
53
+ result.workspaceId !== binding.connection.workspaceId ||
54
+ result.packageVersion !== packageVersion() ||
55
+ result.keyFingerprint !== binding.connection.keyFingerprint ||
56
+ !isReviewerBridgeNegativeProbes(result.negativeProbes)) {
57
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge status response does not match its workspace and version binding.");
58
+ }
59
+ return result;
60
+ }
61
+ export async function startReviewerBridgeSidecar(input) {
62
+ const root = resolve(input.root);
63
+ const stateDirectory = await requireExternalStateDirectory(root, input.stateDirectory);
64
+ const marker = await loadWorkspaceMarker(root);
65
+ const runtimeLock = await requireCurrentRuntimeLock(root, marker);
66
+ const config = await loadWorkspaceConfig(root);
67
+ requireBridgeSelection(config);
68
+ const paths = reviewerBridgePaths(root);
69
+ await ensureDirectory(workspacePaths(root).runtime);
70
+ const key = await loadOrCreateSigningKey(stateDirectory);
71
+ const filesystemProbe = await probeNativeCapsuleIsolation({
72
+ workspaceRoot: root,
73
+ stateDirectory,
74
+ });
75
+ const negativeProbes = {
76
+ ...filesystemProbe,
77
+ arbitraryCommandSurfaceAbsent: true,
78
+ reviewerToolsDisabled: true,
79
+ };
80
+ const clientToken = randomBytes(32).toString("hex");
81
+ const connection = {
82
+ schemaVersion: 1,
83
+ protocolVersion: REVIEW_BRIDGE_PROTOCOL_VERSION,
84
+ workspaceId: marker.workspaceId,
85
+ packageName: RESEARCH_PACKAGE_NAME,
86
+ packageVersion: packageVersion(),
87
+ socketPath: paths.socket,
88
+ publicKey: key.publicKeyPem,
89
+ keyFingerprint: key.fingerprint,
90
+ clientToken,
91
+ createdAt: new Date().toISOString(),
92
+ };
93
+ await removeStaleSocket(paths.socket);
94
+ await writeJsonAtomic(paths.connection, connection, 0o600);
95
+ const nonceDirectory = join(stateDirectory, "nonces");
96
+ await ensureDirectory(nonceDirectory);
97
+ const executeNative = input.executeNative ?? executeAgent;
98
+ const fingerprintNative = input.fingerprintNative ?? fingerprintAgentRoute;
99
+ const server = createServer((request, response) => {
100
+ void handleSidecarRequest({
101
+ request,
102
+ response,
103
+ root,
104
+ stateDirectory,
105
+ expectedClientToken: clientToken,
106
+ connection,
107
+ privateKeyPem: key.privateKeyPem,
108
+ nonceDirectory,
109
+ environment: input.environment,
110
+ executeNative,
111
+ fingerprintNative,
112
+ negativeProbes,
113
+ });
114
+ });
115
+ await listenOnSocket(server, paths.socket);
116
+ await chmod(paths.socket, 0o600);
117
+ let closed = false;
118
+ return {
119
+ workspaceId: marker.workspaceId,
120
+ keyFingerprint: key.fingerprint,
121
+ close: async () => {
122
+ if (closed)
123
+ return;
124
+ closed = true;
125
+ await closeServer(server);
126
+ await Promise.all([
127
+ rm(paths.socket, { force: true }).catch(() => undefined),
128
+ rm(paths.connection, { force: true }).catch(() => undefined),
129
+ ]);
130
+ },
131
+ };
132
+ }
133
+ async function executeThroughBridge(root, request, nonceFactory) {
134
+ if (request.toolPolicy !== "none" || request.brokerUrl !== null) {
135
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_SANDBOX_POLICY_INVALID", "sandbox-bridge accepts only a tool-free reviewer request with no broker or MCP route.");
136
+ }
137
+ const binding = await bridgeBinding(root, "execute", request.requestId, nonceFactory);
138
+ const capsuleSha256 = await hashRegularTree(request.projectRoot);
139
+ const payload = {
140
+ reviewer: {
141
+ agent: request.route.agent,
142
+ model: request.route.model,
143
+ effort: request.route.effort ?? null,
144
+ verbosity: request.route.verbosity ?? null,
145
+ },
146
+ prompt: request.prompt,
147
+ outputSchema: request.outputSchema,
148
+ purpose: request.purpose,
149
+ capsuleRoot: request.capsuleRoot,
150
+ projectRoot: request.projectRoot,
151
+ capsuleSha256,
152
+ timeoutSeconds: request.timeoutSeconds,
153
+ maxTurns: request.maxTurns,
154
+ maxOutputTokens: request.maxOutputTokens,
155
+ maxToolContextTokens: request.maxToolContextTokens ?? 0,
156
+ maxCostUsd: request.maxCostUsd,
157
+ expectedRuntime: request.expectedRuntime ?? null,
158
+ toolPolicy: "none",
159
+ brokerUrl: null,
160
+ };
161
+ const core = { ...binding.core, action: "execute", payload };
162
+ const bridgeRequest = {
163
+ ...core,
164
+ requestSha256: sha256Text(canonicalJson(core)),
165
+ };
166
+ const response = await sendBridgeRequest(binding.connection, bridgeRequest);
167
+ const result = verifyBridgeResponse(binding.connection, bridgeRequest, response);
168
+ if (!isExecutionResult(result)) {
169
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer sidecar returned an invalid execution result.");
170
+ }
171
+ return { ...result, reviewAttestation: response.attestation };
172
+ }
173
+ async function fingerprintThroughBridge(root, route, nonceFactory) {
174
+ const binding = await bridgeBinding(root, "fingerprint", `fingerprint-${route.agent}`, nonceFactory);
175
+ const core = { ...binding.core, action: "fingerprint" };
176
+ const bridgeRequest = {
177
+ ...core,
178
+ requestSha256: sha256Text(canonicalJson(core)),
179
+ };
180
+ const response = await sendBridgeRequest(binding.connection, bridgeRequest);
181
+ const result = verifyBridgeResponse(binding.connection, bridgeRequest, response);
182
+ if (!isAgentRuntimeFingerprint(result) ||
183
+ result.agent !== route.agent ||
184
+ result.model !== route.model) {
185
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_MODEL_MISMATCH", "The reviewer sidecar runtime does not match the configured reviewer model.");
186
+ }
187
+ return result;
188
+ }
189
+ async function bridgeBinding(root, action, requestId, nonceFactory) {
190
+ const connection = await loadBridgeConnection(root);
191
+ const marker = await loadWorkspaceMarker(root);
192
+ const runtimeLock = await requireCurrentRuntimeLock(root, marker);
193
+ const config = await loadWorkspaceConfig(root);
194
+ requireBridgeSelection(config);
195
+ if (connection.protocolVersion !== REVIEW_BRIDGE_PROTOCOL_VERSION ||
196
+ connection.packageVersion !== packageVersion() ||
197
+ connection.packageName !== RESEARCH_PACKAGE_NAME ||
198
+ runtimeLock.packageVersion !== connection.packageVersion) {
199
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_VERSION_MISMATCH", "The reviewer bridge, workspace runtime lock, and active CLI must use the same exact version.");
200
+ }
201
+ if (connection.workspaceId !== marker.workspaceId) {
202
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge is bound to a different workspace.");
203
+ }
204
+ const nonce = nonceFactory();
205
+ if (!HASH_PATTERN.test(nonce)) {
206
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge nonce source returned an invalid value.");
207
+ }
208
+ return {
209
+ connection,
210
+ core: {
211
+ schemaVersion: 1,
212
+ protocolVersion: REVIEW_BRIDGE_PROTOCOL_VERSION,
213
+ action,
214
+ requestId,
215
+ nonce,
216
+ issuedAt: new Date().toISOString(),
217
+ workspaceId: marker.workspaceId,
218
+ packageName: RESEARCH_PACKAGE_NAME,
219
+ packageVersion: packageVersion(),
220
+ runtimeLockSha256: await sha256File(workspacePaths(root).runtimeLock),
221
+ configSha256: await sha256File(workspacePaths(root).config),
222
+ },
223
+ };
224
+ }
225
+ async function handleSidecarRequest(input) {
226
+ try {
227
+ if (input.request.method !== "POST" || input.request.url !== "/v1/review") {
228
+ writeBridgeFailure(input.response, 404, bridgeError("RESEARCH_REVIEW_BRIDGE_ACTION_INVALID", "The reviewer sidecar exposes only the fixed review protocol."));
229
+ return;
230
+ }
231
+ if (!authorized(input.request.headers.authorization, input.expectedClientToken)) {
232
+ writeBridgeFailure(input.response, 401, bridgeError("RESEARCH_REVIEW_BRIDGE_UNAVAILABLE", "The reviewer bridge client binding is unavailable or invalid."));
233
+ return;
234
+ }
235
+ const value = JSON.parse(await readBoundedBody(input.request));
236
+ const request = parseBridgeRequest(value);
237
+ await validateServerBinding(input.root, input.connection, request);
238
+ await consumeNonce(input.nonceDirectory, request.nonce, request.issuedAt);
239
+ const config = await loadWorkspaceConfig(input.root);
240
+ let result;
241
+ let capsuleSha256 = sha256Text("not-applicable");
242
+ let isolationProvider;
243
+ let policySha256;
244
+ if (request.action === "execute") {
245
+ const executed = await executeSidecarReview({
246
+ root: input.root,
247
+ stateDirectory: input.stateDirectory,
248
+ request,
249
+ config,
250
+ environment: input.environment,
251
+ executeNative: input.executeNative,
252
+ });
253
+ result = executed.result;
254
+ capsuleSha256 = request.payload.capsuleSha256;
255
+ isolationProvider = executed.result.isolation.provider;
256
+ policySha256 = executed.result.isolation.policySha256;
257
+ }
258
+ else if (request.action === "fingerprint") {
259
+ result = await input.fingerprintNative(config.reviewer, input.environment);
260
+ isolationProvider = process.platform === "darwin" ? "sandbox-exec" : "bubblewrap";
261
+ policySha256 = sha256Text("fingerprint-only");
262
+ }
263
+ else {
264
+ result = {
265
+ status: "ready",
266
+ workspaceId: input.connection.workspaceId,
267
+ packageVersion: input.connection.packageVersion,
268
+ keyFingerprint: input.connection.keyFingerprint,
269
+ supportedActions: ["execute", "fingerprint", "status"],
270
+ negativeProbes: input.negativeProbes,
271
+ };
272
+ isolationProvider = process.platform === "darwin" ? "sandbox-exec" : "bubblewrap";
273
+ policySha256 = sha256Text("status-only");
274
+ }
275
+ const safeResult = sanitizeBridgeResult(result, input.environment);
276
+ const attestation = signBridgeAttestation({
277
+ request,
278
+ result: safeResult,
279
+ capsuleSha256,
280
+ isolationProvider,
281
+ policySha256,
282
+ keyFingerprint: input.connection.keyFingerprint,
283
+ privateKeyPem: input.privateKeyPem,
284
+ });
285
+ const response = {
286
+ schemaVersion: 1,
287
+ protocolVersion: REVIEW_BRIDGE_PROTOCOL_VERSION,
288
+ ok: true,
289
+ requestId: request.requestId,
290
+ nonce: request.nonce,
291
+ requestSha256: request.requestSha256,
292
+ result: safeResult,
293
+ attestation,
294
+ };
295
+ input.response.writeHead(200, { "content-type": "application/json" });
296
+ input.response.end(`${JSON.stringify(response)}\n`);
297
+ }
298
+ catch (error) {
299
+ writeBridgeFailure(input.response, 400, normalizeBridgeError(error, configuredResearchSecrets(input.environment)));
300
+ }
301
+ }
302
+ async function executeSidecarReview(input) {
303
+ const payload = input.request.payload;
304
+ if (payload.toolPolicy !== "none" || payload.brokerUrl !== null) {
305
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_SANDBOX_POLICY_INVALID", "The reviewer sidecar rejected a request that could enable tools, MCP, browser, or broker access.");
306
+ }
307
+ if (payload.reviewer.agent !== input.config.reviewer.agent ||
308
+ payload.reviewer.model !== input.config.reviewer.model) {
309
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_MODEL_MISMATCH", "The requested reviewer family or model does not match the sidecar workspace binding.");
310
+ }
311
+ const sourceCapsule = await requireWorkspaceCapsule(input.root, payload.capsuleRoot, payload.projectRoot);
312
+ if ((await hashRegularTree(sourceCapsule.projectRoot)) !== payload.capsuleSha256) {
313
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer capsule changed after the bridge request was prepared.");
314
+ }
315
+ const privateCapsule = join(input.stateDirectory, "runs", `${safeIdentifier(input.request.requestId)}-${input.request.nonce.slice(0, 16)}`);
316
+ const privateProject = join(privateCapsule, "project");
317
+ await ensureDirectory(join(input.stateDirectory, "runs"));
318
+ await mkdir(privateCapsule, { recursive: false, mode: 0o700 });
319
+ try {
320
+ await cp(sourceCapsule.projectRoot, privateProject, {
321
+ recursive: true,
322
+ force: false,
323
+ errorOnExist: true,
324
+ dereference: false,
325
+ preserveTimestamps: false,
326
+ });
327
+ if ((await hashRegularTree(privateProject)) !== payload.capsuleSha256) {
328
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The private reviewer capsule does not match the signed source capsule.");
329
+ }
330
+ const result = await input.executeNative({
331
+ route: input.config.reviewer,
332
+ prompt: payload.prompt,
333
+ outputSchema: payload.outputSchema,
334
+ requestId: input.request.requestId,
335
+ purpose: payload.purpose,
336
+ capsuleRoot: privateCapsule,
337
+ projectRoot: privateProject,
338
+ workspaceRoot: input.root,
339
+ timeoutSeconds: payload.timeoutSeconds,
340
+ maxTurns: payload.maxTurns,
341
+ maxOutputTokens: payload.maxOutputTokens,
342
+ maxToolContextTokens: payload.maxToolContextTokens,
343
+ maxCostUsd: payload.maxCostUsd,
344
+ expectedRuntime: payload.expectedRuntime ?? undefined,
345
+ toolPolicy: "none",
346
+ environment: input.environment,
347
+ brokerUrl: null,
348
+ });
349
+ if (!result.isolation ||
350
+ result.isolation.toolPolicy !== "none" ||
351
+ result.isolation.networkPolicy !== "reviewer-provider-only" ||
352
+ !HASH_PATTERN.test(result.isolation.policySha256)) {
353
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_SANDBOX_POLICY_INVALID", "The native reviewer did not return the required platform-capsule policy binding.");
354
+ }
355
+ if (!result.runtime ||
356
+ result.runtime.agent !== input.config.reviewer.agent ||
357
+ result.runtime.model !== input.config.reviewer.model) {
358
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_MODEL_MISMATCH", "The executed reviewer runtime does not match the configured reviewer model.");
359
+ }
360
+ if ((result.telemetry?.toolCalls ?? 0) !== 0) {
361
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_SANDBOX_POLICY_INVALID", "The reviewer attempted a tool call despite the tool-free bridge policy.");
362
+ }
363
+ return { result };
364
+ }
365
+ finally {
366
+ await rm(privateCapsule, { recursive: true, force: true });
367
+ }
368
+ }
369
+ function signBridgeAttestation(input) {
370
+ const core = {
371
+ schemaVersion: 1,
372
+ protocolVersion: REVIEW_BRIDGE_PROTOCOL_VERSION,
373
+ transport: "sandbox-bridge",
374
+ isolationProvider: input.isolationProvider,
375
+ toolPolicy: "none",
376
+ workspaceId: input.request.workspaceId,
377
+ requestId: input.request.requestId,
378
+ requestSha256: input.request.requestSha256,
379
+ resultSha256: sha256Text(canonicalJson(input.result)),
380
+ capsuleSha256: input.capsuleSha256,
381
+ runtimeLockSha256: input.request.runtimeLockSha256,
382
+ configSha256: input.request.configSha256,
383
+ policySha256: input.policySha256,
384
+ signerKeyFingerprint: input.keyFingerprint,
385
+ };
386
+ const attestationSha256 = sha256Text(canonicalJson(core));
387
+ const signed = { ...core, attestationSha256 };
388
+ const signature = sign(null, Buffer.from(canonicalJson(signed), "utf8"), createPrivateKey(input.privateKeyPem)).toString("base64");
389
+ return { ...signed, signature };
390
+ }
391
+ function verifyBridgeResponse(connection, request, response) {
392
+ if (response.schemaVersion !== 1 ||
393
+ response.protocolVersion !== REVIEW_BRIDGE_PROTOCOL_VERSION ||
394
+ response.requestId !== request.requestId ||
395
+ response.nonce !== request.nonce ||
396
+ response.requestSha256 !== request.requestSha256) {
397
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge response does not match its request binding.");
398
+ }
399
+ const attestation = response.attestation;
400
+ const { signature, attestationSha256, ...core } = attestation;
401
+ if (attestation.workspaceId !== request.workspaceId ||
402
+ attestation.requestId !== request.requestId ||
403
+ attestation.requestSha256 !== request.requestSha256 ||
404
+ attestation.runtimeLockSha256 !== request.runtimeLockSha256 ||
405
+ attestation.configSha256 !== request.configSha256 ||
406
+ attestation.signerKeyFingerprint !== connection.keyFingerprint ||
407
+ attestation.resultSha256 !== sha256Text(canonicalJson(response.result)) ||
408
+ sha256Text(canonicalJson(core)) !== attestationSha256 ||
409
+ publicKeyFingerprint(connection.publicKey) !== connection.keyFingerprint) {
410
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge attestation is invalid or does not match the exact result.");
411
+ }
412
+ const signed = { ...core, attestationSha256 };
413
+ let signatureValid = false;
414
+ try {
415
+ signatureValid = verify(null, Buffer.from(canonicalJson(signed), "utf8"), createPublicKey(connection.publicKey), Buffer.from(signature, "base64"));
416
+ }
417
+ catch {
418
+ signatureValid = false;
419
+ }
420
+ if (!signatureValid) {
421
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge signature is invalid.");
422
+ }
423
+ return response.result;
424
+ }
425
+ async function sendBridgeRequest(connection, value) {
426
+ const body = Buffer.from(JSON.stringify(value), "utf8");
427
+ if (body.byteLength > REVIEW_BRIDGE_MAX_BODY_BYTES) {
428
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge request exceeds the bounded protocol size.");
429
+ }
430
+ let raw;
431
+ try {
432
+ raw = await new Promise((resolvePromise, reject) => {
433
+ const request = httpRequest({
434
+ socketPath: connection.socketPath,
435
+ path: "/v1/review",
436
+ method: "POST",
437
+ headers: {
438
+ authorization: `Bearer ${connection.clientToken}`,
439
+ "content-type": "application/json",
440
+ "content-length": body.byteLength,
441
+ },
442
+ }, (response) => {
443
+ const chunks = [];
444
+ let bytes = 0;
445
+ response.on("data", (chunk) => {
446
+ bytes += chunk.byteLength;
447
+ if (bytes <= REVIEW_BRIDGE_MAX_BODY_BYTES)
448
+ chunks.push(chunk);
449
+ });
450
+ response.on("end", () => {
451
+ if (bytes > REVIEW_BRIDGE_MAX_BODY_BYTES) {
452
+ reject(new Error("response too large"));
453
+ return;
454
+ }
455
+ resolvePromise(Buffer.concat(chunks).toString("utf8"));
456
+ });
457
+ });
458
+ request.on("error", reject);
459
+ request.end(body);
460
+ });
461
+ }
462
+ catch {
463
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_UNAVAILABLE", "The sandbox-bridge reviewer is unavailable. Start the exact-version reviewer sidecar outside the IDE sandbox, then rerun doctor.", {
464
+ retryable: false,
465
+ minimumAction: "Start tiangong-ai research reviewer serve for this workspace from an owner-controlled native terminal.",
466
+ });
467
+ }
468
+ let parsed;
469
+ try {
470
+ parsed = JSON.parse(raw);
471
+ }
472
+ catch {
473
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge returned malformed protocol bytes.");
474
+ }
475
+ if (isBridgeFailureResponse(parsed)) {
476
+ throw new CliError(parsed.error.message, {
477
+ code: parsed.error.code,
478
+ exitCode: 3,
479
+ details: sanitizeResearchValue(parsed.error.details),
480
+ });
481
+ }
482
+ if (!isBridgeSuccessResponse(parsed)) {
483
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge returned an unsupported protocol response.");
484
+ }
485
+ return parsed;
486
+ }
487
+ async function loadBridgeConnection(root) {
488
+ const path = reviewerBridgePaths(root).connection;
489
+ const info = await lstat(path).catch(() => undefined);
490
+ if (!info?.isFile() ||
491
+ info.isSymbolicLink() ||
492
+ (process.platform !== "win32" && (info.mode & 0o077) !== 0)) {
493
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_UNAVAILABLE", "The sandbox-bridge reviewer is unavailable. Start the owner-controlled reviewer sidecar outside the IDE sandbox.", {
494
+ retryable: false,
495
+ minimumAction: "Start tiangong-ai research reviewer serve for this workspace from an owner-controlled native terminal.",
496
+ });
497
+ }
498
+ let value;
499
+ try {
500
+ value = JSON.parse(await readFile(path, "utf8"));
501
+ }
502
+ catch {
503
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_UNAVAILABLE", "The sandbox-bridge client binding is missing or invalid.");
504
+ }
505
+ if (!isReviewerBridgeConnection(value)) {
506
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_VERSION_MISMATCH", "The sandbox-bridge client binding uses an unsupported protocol or version.");
507
+ }
508
+ return value;
509
+ }
510
+ async function validateServerBinding(root, connection, request) {
511
+ const marker = await loadWorkspaceMarker(root);
512
+ const runtimeLock = await requireCurrentRuntimeLock(root, marker);
513
+ const config = await loadWorkspaceConfig(root);
514
+ requireBridgeSelection(config);
515
+ const age = Math.abs(Date.now() - Date.parse(request.issuedAt));
516
+ if (!Number.isFinite(age) ||
517
+ age > REVIEW_BRIDGE_REQUEST_MAX_AGE_MS ||
518
+ request.protocolVersion !== REVIEW_BRIDGE_PROTOCOL_VERSION ||
519
+ request.packageName !== RESEARCH_PACKAGE_NAME ||
520
+ request.packageVersion !== packageVersion() ||
521
+ request.packageVersion !== connection.packageVersion ||
522
+ runtimeLock.packageVersion !== request.packageVersion) {
523
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_VERSION_MISMATCH", "The reviewer bridge request version or validity window does not match the sidecar.");
524
+ }
525
+ if (request.workspaceId !== marker.workspaceId ||
526
+ request.workspaceId !== connection.workspaceId ||
527
+ request.runtimeLockSha256 !== (await sha256File(workspacePaths(root).runtimeLock)) ||
528
+ request.configSha256 !== (await sha256File(workspacePaths(root).config))) {
529
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge request does not match the workspace runtime and configuration binding.");
530
+ }
531
+ const { requestSha256, ...core } = request;
532
+ if (sha256Text(canonicalJson(core)) !== requestSha256) {
533
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge request hash is invalid.");
534
+ }
535
+ }
536
+ async function consumeNonce(directory, nonce, seenAt) {
537
+ await ensureDirectory(directory);
538
+ const claimPath = join(directory, nonce);
539
+ let handle;
540
+ try {
541
+ handle = await open(claimPath, "wx", 0o600);
542
+ }
543
+ catch (error) {
544
+ if (error.code === "EEXIST") {
545
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_NONCE_REPLAY", "The reviewer bridge rejected a replayed request nonce.");
546
+ }
547
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "The reviewer bridge could not persist its replay-protection claim.");
548
+ }
549
+ try {
550
+ await handle.writeFile(`${seenAt}\n`, "utf8");
551
+ await handle.sync();
552
+ }
553
+ finally {
554
+ await handle.close();
555
+ }
556
+ }
557
+ async function requireWorkspaceCapsule(root, capsuleRoot, projectRoot) {
558
+ const runtimeRoot = await realpath(workspacePaths(root).runtime);
559
+ const capsule = await realpath(capsuleRoot).catch(() => "");
560
+ const project = await realpath(projectRoot).catch(() => "");
561
+ const capsuleInfo = capsule ? await lstat(capsule).catch(() => undefined) : undefined;
562
+ const projectInfo = project ? await lstat(project).catch(() => undefined) : undefined;
563
+ if (!capsuleInfo?.isDirectory() ||
564
+ capsuleInfo.isSymbolicLink() ||
565
+ !projectInfo?.isDirectory() ||
566
+ projectInfo.isSymbolicLink() ||
567
+ relative(runtimeRoot, capsule).startsWith("..") ||
568
+ project !== join(capsule, "project")) {
569
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge capsule is outside the current workspace runtime or has an invalid shape.");
570
+ }
571
+ return { capsuleRoot: capsule, projectRoot: project };
572
+ }
573
+ async function requireExternalStateDirectory(root, value) {
574
+ if (!isAbsolute(value) || resolve(value) !== value) {
575
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "Reviewer sidecar state must use an explicit absolute directory.");
576
+ }
577
+ const selectedInfo = await lstat(value).catch(() => undefined);
578
+ if (selectedInfo?.isSymbolicLink() || (selectedInfo && !selectedInfo.isDirectory())) {
579
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "Reviewer sidecar private state must be a regular non-symlink directory.");
580
+ }
581
+ await ensureDirectory(value);
582
+ const canonical = await realpath(value);
583
+ const info = await lstat(canonical);
584
+ if (!info.isDirectory() ||
585
+ info.isSymbolicLink() ||
586
+ canonical === root ||
587
+ canonical.startsWith(`${root}${sep}`)) {
588
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "Reviewer sidecar private state must be a regular directory outside the research workspace.");
589
+ }
590
+ return canonical;
591
+ }
592
+ async function loadOrCreateSigningKey(stateDirectory) {
593
+ const privateKeyPath = join(stateDirectory, "reviewer-bridge-private-key.pem");
594
+ let privateKeyPem;
595
+ const info = await lstat(privateKeyPath).catch(() => undefined);
596
+ if (info) {
597
+ if (!info.isFile() ||
598
+ info.isSymbolicLink() ||
599
+ (process.platform !== "win32" && (info.mode & 0o077) !== 0)) {
600
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "Reviewer sidecar signing material must be an owner-only regular file.");
601
+ }
602
+ privateKeyPem = await readFile(privateKeyPath, "utf8");
603
+ }
604
+ else {
605
+ const generated = generateKeyPairSync("ed25519");
606
+ privateKeyPem = generated.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
607
+ await writeTextAtomic(privateKeyPath, privateKeyPem, 0o600);
608
+ }
609
+ let publicKeyPem;
610
+ try {
611
+ publicKeyPem = createPublicKey(createPrivateKey(privateKeyPem))
612
+ .export({ type: "spki", format: "pem" })
613
+ .toString();
614
+ }
615
+ catch {
616
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "Reviewer sidecar signing material is invalid.");
617
+ }
618
+ return {
619
+ privateKeyPem,
620
+ publicKeyPem,
621
+ fingerprint: publicKeyFingerprint(publicKeyPem),
622
+ };
623
+ }
624
+ function publicKeyFingerprint(publicKeyPem) {
625
+ const der = createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
626
+ return sha256Bytes(der);
627
+ }
628
+ function sanitizeBridgeResult(value, environment) {
629
+ const secrets = configuredResearchSecrets(environment);
630
+ if (isExecutionResult(value)) {
631
+ return {
632
+ ...value,
633
+ stdout: sanitizeResearchText(value.stdout, secrets),
634
+ stderr: sanitizeResearchText(value.stderr, secrets),
635
+ telemetry: value.telemetry
636
+ ? sanitizeResearchValue(value.telemetry, secrets)
637
+ : undefined,
638
+ reviewAttestation: undefined,
639
+ };
640
+ }
641
+ return sanitizeResearchValue(value, secrets);
642
+ }
643
+ function requireBridgeSelection(config) {
644
+ if (config.reviewerExecution.transport !== "sandbox-bridge" ||
645
+ config.reviewerExecution.isolationProvider !== "platform-capsule") {
646
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_NOT_SELECTED", "This workspace did not explicitly select sandbox-bridge reviewer execution.");
647
+ }
648
+ }
649
+ function secureNonce() {
650
+ return randomBytes(32).toString("hex");
651
+ }
652
+ function safeIdentifier(value) {
653
+ const normalized = value.replaceAll(/[^A-Za-z0-9._-]/g, "-").slice(0, 80);
654
+ return normalized || "review";
655
+ }
656
+ function authorized(header, expectedToken) {
657
+ const prefix = "Bearer ";
658
+ if (!header?.startsWith(prefix))
659
+ return false;
660
+ const actual = Buffer.from(header.slice(prefix.length), "utf8");
661
+ const expected = Buffer.from(expectedToken, "utf8");
662
+ return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected);
663
+ }
664
+ async function readBoundedBody(request) {
665
+ const chunks = [];
666
+ let bytes = 0;
667
+ for await (const chunk of request) {
668
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
669
+ bytes += buffer.byteLength;
670
+ if (bytes > REVIEW_BRIDGE_MAX_BODY_BYTES) {
671
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", "The reviewer bridge request exceeds the bounded protocol size.");
672
+ }
673
+ chunks.push(buffer);
674
+ }
675
+ return Buffer.concat(chunks).toString("utf8");
676
+ }
677
+ function parseBridgeRequest(value) {
678
+ if (!isObject(value) ||
679
+ !isBridgeRequestCore(value) ||
680
+ !HASH_PATTERN.test(String(value.requestSha256))) {
681
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge request is malformed.");
682
+ }
683
+ if (value.action === "execute") {
684
+ if (!isExecutePayload(value.payload)) {
685
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ATTESTATION_INVALID", "The reviewer bridge execution payload is malformed.");
686
+ }
687
+ return value;
688
+ }
689
+ if (value.action === "fingerprint" || value.action === "status") {
690
+ return value;
691
+ }
692
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_ACTION_INVALID", "The reviewer sidecar exposes only execute, fingerprint, and status actions.");
693
+ }
694
+ function isBridgeRequestCore(value) {
695
+ return (value.schemaVersion === 1 &&
696
+ value.protocolVersion === 1 &&
697
+ ["execute", "fingerprint", "status"].includes(String(value.action)) &&
698
+ typeof value.requestId === "string" &&
699
+ value.requestId.length > 0 &&
700
+ typeof value.nonce === "string" &&
701
+ HASH_PATTERN.test(value.nonce) &&
702
+ typeof value.issuedAt === "string" &&
703
+ Number.isFinite(Date.parse(value.issuedAt)) &&
704
+ typeof value.workspaceId === "string" &&
705
+ value.packageName === RESEARCH_PACKAGE_NAME &&
706
+ typeof value.packageVersion === "string" &&
707
+ typeof value.runtimeLockSha256 === "string" &&
708
+ HASH_PATTERN.test(value.runtimeLockSha256) &&
709
+ typeof value.configSha256 === "string" &&
710
+ HASH_PATTERN.test(value.configSha256));
711
+ }
712
+ function isExecutePayload(value) {
713
+ return (isObject(value) &&
714
+ isObject(value.reviewer) &&
715
+ (value.reviewer.agent === "codex" || value.reviewer.agent === "claude") &&
716
+ (value.reviewer.model === null || typeof value.reviewer.model === "string") &&
717
+ typeof value.prompt === "string" &&
718
+ isObject(value.outputSchema) &&
719
+ ["primary", "repair", "doctor"].includes(String(value.purpose)) &&
720
+ typeof value.capsuleRoot === "string" &&
721
+ typeof value.projectRoot === "string" &&
722
+ typeof value.capsuleSha256 === "string" &&
723
+ HASH_PATTERN.test(value.capsuleSha256) &&
724
+ positiveInteger(value.timeoutSeconds) &&
725
+ positiveInteger(value.maxTurns) &&
726
+ positiveInteger(value.maxOutputTokens) &&
727
+ Number.isInteger(value.maxToolContextTokens) &&
728
+ Number(value.maxToolContextTokens) >= 0 &&
729
+ typeof value.maxCostUsd === "number" &&
730
+ value.maxCostUsd >= 0 &&
731
+ value.toolPolicy === "none" &&
732
+ value.brokerUrl === null);
733
+ }
734
+ function isReviewerBridgeConnection(value) {
735
+ return (isObject(value) &&
736
+ value.schemaVersion === 1 &&
737
+ value.protocolVersion === 1 &&
738
+ typeof value.workspaceId === "string" &&
739
+ value.packageName === RESEARCH_PACKAGE_NAME &&
740
+ typeof value.packageVersion === "string" &&
741
+ typeof value.socketPath === "string" &&
742
+ isAbsolute(value.socketPath) &&
743
+ typeof value.publicKey === "string" &&
744
+ typeof value.keyFingerprint === "string" &&
745
+ HASH_PATTERN.test(value.keyFingerprint) &&
746
+ typeof value.clientToken === "string" &&
747
+ /^[a-f0-9]{64}$/.test(value.clientToken) &&
748
+ typeof value.createdAt === "string");
749
+ }
750
+ function isExecutionResult(value) {
751
+ return (isObject(value) &&
752
+ typeof value.exitCode === "number" &&
753
+ typeof value.stdout === "string" &&
754
+ typeof value.stderr === "string" &&
755
+ typeof value.tokens === "number" &&
756
+ typeof value.inputTokens === "number" &&
757
+ typeof value.cachedInputTokens === "number" &&
758
+ typeof value.outputTokens === "number" &&
759
+ typeof value.costUsd === "number" &&
760
+ typeof value.wallSeconds === "number" &&
761
+ (value.model === null || typeof value.model === "string") &&
762
+ (value.runtime === null || isAgentRuntimeFingerprint(value.runtime)) &&
763
+ isObject(value.isolation));
764
+ }
765
+ function isAgentRuntimeFingerprint(value) {
766
+ return (isObject(value) &&
767
+ (value.agent === "codex" || value.agent === "claude") &&
768
+ (value.model === null || typeof value.model === "string") &&
769
+ typeof value.binarySha256 === "string" &&
770
+ HASH_PATTERN.test(value.binarySha256) &&
771
+ typeof value.wrapperSha256 === "string" &&
772
+ HASH_PATTERN.test(value.wrapperSha256) &&
773
+ typeof value.adapterSha256 === "string" &&
774
+ HASH_PATTERN.test(value.adapterSha256) &&
775
+ typeof value.binaryVersion === "string" &&
776
+ typeof value.platform === "string" &&
777
+ typeof value.architecture === "string");
778
+ }
779
+ function isReviewerBridgeNegativeProbes(value) {
780
+ return (isObject(value) &&
781
+ value.outsideReadBlocked === true &&
782
+ value.outsideWriteBlocked === true &&
783
+ value.workspaceCredentialReadBlocked === true &&
784
+ value.arbitraryCommandSurfaceAbsent === true &&
785
+ value.reviewerToolsDisabled === true);
786
+ }
787
+ function isBridgeFailureResponse(value) {
788
+ return (isObject(value) &&
789
+ value.schemaVersion === 1 &&
790
+ value.protocolVersion === 1 &&
791
+ value.ok === false &&
792
+ isObject(value.error) &&
793
+ typeof value.error.code === "string" &&
794
+ typeof value.error.message === "string");
795
+ }
796
+ function isBridgeSuccessResponse(value) {
797
+ return (isObject(value) &&
798
+ value.schemaVersion === 1 &&
799
+ value.protocolVersion === 1 &&
800
+ value.ok === true &&
801
+ typeof value.requestId === "string" &&
802
+ typeof value.nonce === "string" &&
803
+ typeof value.requestSha256 === "string" &&
804
+ isObject(value.attestation) &&
805
+ "result" in value);
806
+ }
807
+ function positiveInteger(value) {
808
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
809
+ }
810
+ function bridgeError(code, message, details) {
811
+ return new CliError(message, { code, exitCode: 3, details: sanitizeResearchValue(details) });
812
+ }
813
+ function normalizeBridgeError(error, secrets) {
814
+ if (error instanceof CliError) {
815
+ return bridgeError(error.code, sanitizeResearchText(error.message, secrets), error.details);
816
+ }
817
+ return bridgeError("RESEARCH_REVIEW_BRIDGE_RESULT_BINDING_INVALID", `The reviewer sidecar rejected an invalid request: ${sanitizeResearchText(error instanceof Error ? error.message : String(error), secrets).slice(0, 500)}`);
818
+ }
819
+ function writeBridgeFailure(response, status, error) {
820
+ const value = {
821
+ schemaVersion: 1,
822
+ protocolVersion: REVIEW_BRIDGE_PROTOCOL_VERSION,
823
+ ok: false,
824
+ error: {
825
+ code: error.code,
826
+ message: sanitizeResearchText(error.message),
827
+ ...(error.details === undefined ? {} : { details: sanitizeResearchValue(error.details) }),
828
+ },
829
+ };
830
+ response.writeHead(status, { "content-type": "application/json" });
831
+ response.end(`${JSON.stringify(value)}\n`);
832
+ }
833
+ async function removeStaleSocket(path) {
834
+ const info = await lstat(path).catch(() => undefined);
835
+ if (!info)
836
+ return;
837
+ if (!info.isSocket() || info.isSymbolicLink()) {
838
+ throw bridgeError("RESEARCH_REVIEW_BRIDGE_STATE_INVALID", "The reviewer bridge socket path is occupied by a non-socket entry.");
839
+ }
840
+ await rm(path, { force: true });
841
+ }
842
+ async function listenOnSocket(server, socketPath) {
843
+ await new Promise((resolvePromise, reject) => {
844
+ server.once("error", reject);
845
+ server.listen(socketPath, () => {
846
+ server.off("error", reject);
847
+ resolvePromise();
848
+ });
849
+ });
850
+ }
851
+ async function closeServer(server) {
852
+ await new Promise((resolvePromise) => server.close(() => resolvePromise()));
853
+ }
854
+ //# sourceMappingURL=review-executor.js.map