snipara-companion 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/dist/index.js +62 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Release notes for `snipara-companion`, newest first.
|
|
4
4
|
|
|
5
|
-
## 4.0.
|
|
5
|
+
## 4.0.1
|
|
6
|
+
|
|
7
|
+
- Delegate scoped local validation runs to Orchestrator for shared execution exclusion and freshly verified Sandbox receipt reuse. Preserve original run identity and report reuse separately.
|
|
8
|
+
- Keep legacy unscoped execution available; require compatible Orchestrator for coordinated runs and never silently fall back after uncertain execution.
|
|
9
|
+
- Match Sandbox's capability-bound scope v3 while preserving empty-capability v2 identities.
|
|
10
|
+
|
|
11
|
+
## 4.0.0
|
|
6
12
|
|
|
7
13
|
- Breaking CLI JSON contract: `workflow resume --json` returns a versioned compact overview by default. Existing full-payload consumers must add `--detail`; HTTP What Changed stays full by default. This migration requires a major Companion release.
|
|
8
14
|
|
package/dist/index.js
CHANGED
|
@@ -348,7 +348,7 @@ function expectedSandboxScope(requestFile, scope) {
|
|
|
348
348
|
const raw = JSON.parse(text3);
|
|
349
349
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid request");
|
|
350
350
|
const r = raw;
|
|
351
|
-
const known = /* @__PURE__ */ new Set(["schema_version", "workspace", "argv", "image", "profile", "cwd", "timeout_seconds", "cpus", "memory_mb", "pids", "disk_mb", "artifacts"]);
|
|
351
|
+
const known = /* @__PURE__ */ new Set(["schema_version", "workspace", "argv", "image", "profile", "cwd", "timeout_seconds", "cpus", "memory_mb", "pids", "disk_mb", "artifacts", "requirements"]);
|
|
352
352
|
if (Object.keys(r).some((key) => !known.has(key))) throw new Error("Unknown request fields");
|
|
353
353
|
const schema = r.schema_version === void 0 ? "snipara.sandbox.validation.v1" : r.schema_version;
|
|
354
354
|
if (schema !== "snipara.sandbox.validation.v1" || typeof r.workspace !== "string" || !(0, import_node_path.isAbsolute)(r.workspace) || r.workspace.split("/").includes("..") || typeof r.image !== "string" || !r.image || r.image.startsWith("-") || /\s/.test(r.image)) throw new Error("Unsupported request identity");
|
|
@@ -368,6 +368,24 @@ function expectedSandboxScope(requestFile, scope) {
|
|
|
368
368
|
const cpus = r.cpus === void 0 ? 2 : r.cpus;
|
|
369
369
|
if (typeof cpus !== "number" || !Number.isFinite(cpus) || cpus < 0.01 || cpus > 8) throw new Error("Invalid CPU limit");
|
|
370
370
|
const parts = ["snipara.validation.scope.v2", scope, schema, workspace, profile, r.image, import_node_path.posix.normalize(cwd).replace(/\/+$/, "") || ".", String(integer2("timeout_seconds", 120, 1, 900)), String(Math.trunc(cpus * 1e9)), String(integer2("memory_mb", 2048, 64, 8192)), String(integer2("pids", 128, 16, 512)), String(integer2("disk_mb", 2048, 16, 4096))];
|
|
371
|
+
const rawRequirements = r.requirements === void 0 ? {} : r.requirements;
|
|
372
|
+
if (!rawRequirements || typeof rawRequirements !== "object" || Array.isArray(rawRequirements)) throw new Error("Invalid requirements");
|
|
373
|
+
const requirements = rawRequirements;
|
|
374
|
+
const patterns = {
|
|
375
|
+
python_modules: /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/,
|
|
376
|
+
node_modules: /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/,
|
|
377
|
+
commands: /^[A-Za-z0-9][A-Za-z0-9._+-]*$/
|
|
378
|
+
};
|
|
379
|
+
if (Object.keys(requirements).some((name) => !Object.hasOwn(patterns, name))) throw new Error("Unknown requirements fields");
|
|
380
|
+
const names = ["python_modules", "node_modules", "commands"].map((name) => {
|
|
381
|
+
const values = requirements[name] === void 0 ? [] : requirements[name];
|
|
382
|
+
if (!Array.isArray(values) || values.length > 32 || values.some((value) => typeof value !== "string" || value.length > 128 || patterns[name].exec(value)?.[0] !== value)) throw new Error("Invalid requirement names");
|
|
383
|
+
return [...new Set(values)];
|
|
384
|
+
});
|
|
385
|
+
if (names.some((values) => values.length > 0)) {
|
|
386
|
+
parts[0] = "snipara.validation.scope.v3";
|
|
387
|
+
parts.push(...names.map((values) => JSON.stringify(values)));
|
|
388
|
+
}
|
|
371
389
|
if (parts.some((value) => Buffer.from(value, "utf8").toString("utf8") !== value)) throw new Error("Invalid Unicode identity");
|
|
372
390
|
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts), "utf8").digest("hex");
|
|
373
391
|
}
|
|
@@ -413,6 +431,9 @@ async function invokeSandboxValidation(action2, options) {
|
|
|
413
431
|
if (options.probeTimeoutSeconds !== void 0 && (!diagnostic2 || !Number.isInteger(options.probeTimeoutSeconds) || options.probeTimeoutSeconds < 1 || options.probeTimeoutSeconds > MAX_PROBE_TIMEOUT_SECONDS)) {
|
|
414
432
|
return { exitCode: 2, result: { ...sandboxUnavailable, stage: "adapter_input", reason_code: "invalid_probe_timeout", reason: "probeTimeoutSeconds is an integer from 1 to 60, supported only for diagnose and gate." } };
|
|
415
433
|
}
|
|
434
|
+
if ((options.noReuse || options.orchestratorCommand !== void 0) && (action2 !== "run" || !options.scope?.trim()) || action2 === "run" && options.scope !== void 0 && !options.scope.trim()) {
|
|
435
|
+
return { exitCode: 2, result: { ...sandboxUnavailable, stage: "adapter_input", reason_code: "explicit_scope_required", reason: "Coordinated run controls require validation run with an explicit workflow or lot scope." } };
|
|
436
|
+
}
|
|
416
437
|
if (process.platform === "win32") return { exitCode: 2, result: { ...sandboxUnavailable, reason: "Repository validation currently qualifies macOS/Linux Docker only." } };
|
|
417
438
|
const requiresRequest = ["gate", "choose", "diagnose", "run", "verify"].includes(action2);
|
|
418
439
|
const requiresRun = ["verify", "status", "cancel"].includes(action2);
|
|
@@ -420,37 +441,69 @@ async function invokeSandboxValidation(action2, options) {
|
|
|
420
441
|
return { exitCode: 2, result: { ...sandboxUnavailable, reason: "Explicit request, workflow scope and valid run identity are required for the selected action." } };
|
|
421
442
|
}
|
|
422
443
|
if (action2 === "choose" && (!options.acknowledgeUserChoice || !["alternative", "install", "local", "skip"].includes(options.choice || "") || options.choice === "alternative" && !options.environment)) return { exitCode: 2, result: { ...sandboxUnavailable, reason: "An explicit acknowledged user choice is required." } };
|
|
444
|
+
const delegated = action2 === "run" && !!options.scope;
|
|
423
445
|
const args = ["validation", action2];
|
|
424
|
-
|
|
446
|
+
const parameters = delegated ? { request: options.request, scope: options.scope, store: options.store, "sandbox-command": options.sandboxCommand } : { request: options.request, scope: action2 === "run" ? void 0 : options.scope, "run-id": options.runId, store: options.store, choice: options.choice, environment: options.environment };
|
|
447
|
+
for (const [flag, value] of Object.entries(parameters)) {
|
|
425
448
|
if (value !== void 0) args.push(`--${flag}`, value);
|
|
426
449
|
}
|
|
427
|
-
if (options.
|
|
450
|
+
if (delegated && options.noReuse) args.push("--no-reuse");
|
|
451
|
+
if (!delegated && options.acknowledgeUserChoice) args.push("--acknowledge-user-choice");
|
|
428
452
|
if (options.probeTimeoutSeconds !== void 0) args.push("--probe-timeout-seconds", String(options.probeTimeoutSeconds));
|
|
429
453
|
const env2 = {};
|
|
430
454
|
for (const name of ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL", "DOCKER_HOST", "DOCKER_CONTEXT", "DOCKER_CONFIG"]) {
|
|
431
455
|
if (process.env[name]) env2[name] = process.env[name];
|
|
432
456
|
}
|
|
457
|
+
const command2 = delegated ? options.orchestratorCommand || "snipara-orchestrator" : options.sandboxCommand || "snipara-sandbox";
|
|
458
|
+
if (delegated) {
|
|
459
|
+
try {
|
|
460
|
+
const capability = await executeBounded(command2, ["validation", "run", "--help"], env2, 1e4);
|
|
461
|
+
if (capability.status !== 0 || !/(?:^|\s)--no-reuse(?:\s|,|$)/m.test(capability.stdout)) throw new Error("The Orchestrator validation run command does not expose --no-reuse.");
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return { exitCode: 2, result: { status: "orchestrator_update_required", stage: "adapter_capability", reason_code: "orchestrator_update_required", reason: `Scoped validation requires Orchestrator 1.14 or newer with validation reuse support. ${redactDiagnostic(error.message)}`, execution_authorized_by_gate: false } };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
433
466
|
let child;
|
|
434
467
|
const timeoutMs = diagnostic2 ? ((options.probeTimeoutSeconds ?? DEFAULT_PROBE_TIMEOUT_SECONDS) + DIAGNOSTIC_CONTROL_GRACE_SECONDS + ADAPTER_STARTUP_ALLOWANCE_SECONDS) * 1e3 : action2 === "run" ? 96e4 : 3e4;
|
|
435
468
|
try {
|
|
436
|
-
child = await executeBounded(
|
|
469
|
+
child = await executeBounded(command2, args, env2, timeoutMs);
|
|
437
470
|
} catch (error) {
|
|
438
471
|
const failure = error;
|
|
472
|
+
if (delegated) return { exitCode: 2, result: { status: "validation_reuse_needs_review", work_reuse: { state: "needs_review" }, stage: "adapter_process", reason_code: failure.code === "ETIMEDOUT" ? "orchestrator_process_timeout" : failure.code === "EOUTPUT" ? "orchestrator_output_limit" : "orchestrator_process_failed", reason: redactDiagnostic(failure.message || "Orchestrator process failed."), adapter_timeout_seconds: timeoutMs / 1e3, execution_authorized_by_gate: false } };
|
|
439
473
|
return { exitCode: 2, result: { ...sandboxUnavailable, stage: "adapter_process", reason_code: failure.code === "ETIMEDOUT" ? "sandbox_process_timeout" : failure.code === "EOUTPUT" ? "sandbox_output_limit" : "sandbox_process_failed", reason: redactDiagnostic(failure.message || "Sandbox process failed to start."), adapter_timeout_seconds: timeoutMs / 1e3 } };
|
|
440
474
|
}
|
|
441
475
|
try {
|
|
442
476
|
const value = JSON.parse(child.stdout);
|
|
443
477
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Sandbox response");
|
|
444
478
|
const result = value;
|
|
479
|
+
const workReuse = result.work_reuse && typeof result.work_reuse === "object" && !Array.isArray(result.work_reuse) ? result.work_reuse : void 0;
|
|
480
|
+
if (delegated && workReuse?.state === "needs_review" && (result.status === "needs_user_choice" || result.status === "user_choice_recorded")) {
|
|
481
|
+
if (result.run_id !== void 0 || result.execution_authorized_by_gate !== false || result.sandbox_used !== void 0 && result.sandbox_used !== false || Array.from(options.scope).length > 256) throw new Error("Invalid nonexecution gate response");
|
|
482
|
+
const scope = result.scope_sha256 === void 0 ? void 0 : expectedSandboxScope(options.request, options.scope);
|
|
483
|
+
if (result.scope_sha256 !== scope) throw new Error("Invalid gate scope binding");
|
|
484
|
+
if (result.status === "needs_user_choice") {
|
|
485
|
+
const allowed = ["alternative", "install", "continue_without_sandbox"];
|
|
486
|
+
if (typeof result.prompt !== "string" || !result.prompt.trim() || result.prompt.length > 2e3 || !Array.isArray(result.choices) || result.choices.length < 1 || result.choices.length > allowed.length || new Set(result.choices).size !== result.choices.length || !result.choices.every((value2) => typeof value2 === "string" && allowed.includes(value2))) throw new Error("Invalid unavailable gate choices");
|
|
487
|
+
return { exitCode: 2, result: sanitizeDiagnostics({ ...result, prompt: redactDiagnostic(result.prompt) }) };
|
|
488
|
+
}
|
|
489
|
+
const choice2 = result.choice;
|
|
490
|
+
if (!scope || result.sandbox_used !== false || result.proof_requirement_waived !== false || !validChoice(choice2, scope) || choice2?.choice === "install") throw new Error("Invalid recorded gate choice");
|
|
491
|
+
return { exitCode: 2, result: sanitizeDiagnostics(result) };
|
|
492
|
+
}
|
|
493
|
+
if (delegated && result.run_id === void 0 && (result.status === "validation_in_progress" && workReuse?.state === "in_progress" || result.status === "validation_reuse_needs_review" && workReuse?.state === "needs_review")) {
|
|
494
|
+
return { exitCode: 2, result: sanitizeDiagnostics(result) };
|
|
495
|
+
}
|
|
496
|
+
if (delegated && (!workReuse || typeof workReuse.state !== "string" || !["executed", "reused", "needs_review"].includes(workReuse.state))) throw new Error("Invalid validation reuse metadata");
|
|
445
497
|
const schema = result.schema_version === "snipara.sandbox.validation.v1";
|
|
446
498
|
const identity = ["gate", "choose"].includes(action2) && typeof result.scope_sha256 === "string" && result.scope_sha256 === expectedSandboxScope(options.request, options.scope);
|
|
447
499
|
const ready = result.readiness;
|
|
448
500
|
const choice = result.choice;
|
|
449
501
|
const valid = action2 === "gate" ? identity && result.sandbox_used === false && result.execution_authorized_by_gate === false && (result.status === "sandbox_ready" && ready?.schema_version === "snipara.sandbox.validation.v1" && ready.ready === true && ready.backend === "docker" && typeof ready.image_id === "string" && !!ready.image_id || result.status === "needs_user_choice" && result.execution_authorized_by_gate === false && Array.isArray(result.choices) && result.choices.length > 0 || result.status === "user_choice_recorded" && result.proof_requirement_waived === false && validChoice(choice, result.scope_sha256) && choice?.choice !== "install") : action2 === "choose" ? identity && validChoice(result, result.scope_sha256) && result.choice === options.choice && result.environment === (options.environment || "") : action2 === "diagnose" ? typeof result.ready === "boolean" && schema && result.backend === "docker" : action2 === "cancel" ? result.cancel_requested === true && result.run_id === options.runId : action2 === "verify" ? schema && typeof result.valid === "boolean" && result.run_id === options.runId && Array.isArray(result.reasons) && (result.valid === false || result.reasons.length === 0 && validVerifiedReceipt(result.receipt, options.runId)) : schema && typeof result.run_id === "string" && /^[a-f0-9]{32}$/.test(result.run_id) && (!options.runId || result.run_id === options.runId) && typeof result.status === "string" && ["preparing", "running", "passed", "failed", "timeout", "cancelled", "environment_incompatible", "artifact_error", "collection_error", "cleanup_failed", "error", "interrupted"].includes(String(result.status)) && typeof result.cleanup_complete === "boolean";
|
|
450
502
|
if (!valid) throw new Error("Unsupported Sandbox response schema");
|
|
451
|
-
const failed = result.status === "needs_user_choice" || action2 === "verify" && result.valid !== true || action2 === "diagnose" && result.ready !== true || action2 === "run" && (result.status !== "passed" || result.cleanup_complete !== true);
|
|
503
|
+
const failed = result.status === "needs_user_choice" || action2 === "verify" && result.valid !== true || action2 === "diagnose" && result.ready !== true || action2 === "run" && (result.status !== "passed" || result.cleanup_complete !== true) || delegated && workReuse?.state === "needs_review";
|
|
452
504
|
return { exitCode: failed ? 2 : child.status ?? 2, result: sanitizeDiagnostics(result) };
|
|
453
505
|
} catch {
|
|
506
|
+
if (delegated) return { exitCode: 2, result: { status: "validation_reuse_needs_review", work_reuse: { state: "needs_review" }, stage: "adapter_response", reason_code: "orchestrator_response_invalid", reason: child.stderr ? redactDiagnostic(child.stderr) : "Orchestrator returned an unsupported or malformed response after execution was attempted; inspect its evidence before retrying.", execution_authorized_by_gate: false } };
|
|
454
507
|
return { exitCode: 2, result: { ...sandboxUnavailable, stage: "adapter_response", reason_code: "sandbox_response_invalid", reason: child.stderr ? redactDiagnostic(child.stderr) : "Sandbox returned an unsupported or malformed response; update/configure it or select an alternative." } };
|
|
455
508
|
}
|
|
456
509
|
}
|
|
@@ -51417,6 +51470,9 @@ validation.command("preview").description("Read a bounded local request preview;
|
|
|
51417
51470
|
});
|
|
51418
51471
|
for (const action2 of ["gate", "choose", "diagnose", "run", "verify", "status", "cancel"]) {
|
|
51419
51472
|
const validationCommand = validation.command(action2).option("--request <file>", "Standalone Sandbox validation request JSON").option("--scope <id>", "Authorized workflow or lot identity").option("--run-id <id>", "Sandbox run identity").option("--store <path>", "Durable Sandbox evidence directory").option("--choice <choice>", "Explicit user choice: alternative, install, local or skip").option("--environment <name>", "Name the chosen alternative environment").option("--acknowledge-user-choice", "Record an actual explicit user choice; never inferred").option("--sandbox-command <path>", "Explicit Sandbox executable; no shell interpolation");
|
|
51473
|
+
if (action2 === "run") {
|
|
51474
|
+
validationCommand.option("--no-reuse", "Request a fresh scoped run while retaining the shared validation lock").option("--orchestrator-command <path>", "Compatible Orchestrator executable for scoped validation");
|
|
51475
|
+
}
|
|
51420
51476
|
if (["gate", "diagnose"].includes(action2)) {
|
|
51421
51477
|
validationCommand.option(
|
|
51422
51478
|
"--probe-timeout-seconds <seconds>",
|
|
@@ -51425,7 +51481,7 @@ for (const action2 of ["gate", "choose", "diagnose", "run", "verify", "status",
|
|
|
51425
51481
|
);
|
|
51426
51482
|
}
|
|
51427
51483
|
validationCommand.action(async (options) => {
|
|
51428
|
-
const outcome = await invokeSandboxValidation(action2, options);
|
|
51484
|
+
const outcome = await invokeSandboxValidation(action2, action2 === "run" ? { ...options, noReuse: options.reuse === false } : options);
|
|
51429
51485
|
console.log(JSON.stringify(outcome.result));
|
|
51430
51486
|
process.exitCode = outcome.exitCode;
|
|
51431
51487
|
});
|