diff --git a/DISASMER_PUBLIC_TREE.json b/DISASMER_PUBLIC_TREE.json index c72b811..8ca5e60 100644 --- a/DISASMER_PUBLIC_TREE.json +++ b/DISASMER_PUBLIC_TREE.json @@ -1,7 +1,7 @@ { "kind": "disasmer-filtered-public-tree", - "source_commit": "d0af06088a23d83911b49a6b05073b892f70385b", - "release_name": "dryrun-d0af06088a23", + "source_commit": "17dcc0b92ebee00867a5d6521d214d3ed52b0ece", + "release_name": "dryrun-17dcc0b92ebe", "filtered_out": [ "private/**", "experiments/**", diff --git a/scripts/cli-error-exit-smoke.js b/scripts/cli-error-exit-smoke.js index f47809b..3437293 100755 --- a/scripts/cli-error-exit-smoke.js +++ b/scripts/cli-error-exit-smoke.js @@ -2,11 +2,14 @@ const assert = require("assert"); const cp = require("child_process"); +const fs = require("fs"); const net = require("net"); +const os = require("os"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const project = path.join(repo, "examples/launch-build-demo"); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-error-")); function runDisasmer(args) { return new Promise((resolve) => { @@ -56,6 +59,37 @@ async function runWithOneCoordinatorResponse(buildArgs, response) { } async function main() { + const environmentProject = path.join(tempRoot, "missing-env-project"); + fs.mkdirSync(path.join(environmentProject, "src"), { recursive: true }); + fs.writeFileSync( + path.join(environmentProject, "Cargo.toml"), + "[package]\nname = \"missing-env-project\"\nversion = \"0.1.0\"\nedition = \"2021\"\n" + ); + fs.writeFileSync( + path.join(environmentProject, "src", "main.rs"), + "fn main() { let _target = env!(\"linux\"); }\n" + ); + const environmentFailure = await runDisasmer([ + "build", + "--project", + environmentProject, + "--json", + ]); + assert.strictEqual(environmentFailure.signal, null, environmentFailure.stderr); + assert.strictEqual(environmentFailure.code, 26, environmentFailure.stderr); + const environmentReport = JSON.parse(environmentFailure.stdout); + assert.strictEqual(environmentReport.status, "blocked_before_schedule"); + assert.strictEqual(environmentReport.scheduled_work, false); + assert.strictEqual(environmentReport.machine_error.category, "environment"); + assert.strictEqual( + environmentReport.machine_error.process_exit_code_applied, + true + ); + assert( + environmentReport.machine_error.next_actions.includes("disasmer inspect") + ); + assert.strictEqual(environmentReport.diagnostics[0].code, "missing_environment"); + const nonInteractive = await runDisasmer([ "run", "build", @@ -141,6 +175,106 @@ async function main() { report.run_start.machine_error.next_actions.includes("disasmer quota status") ); + const capabilityFailure = await runWithOneCoordinatorResponse( + (coordinator) => [ + "run", + "build", + "--project", + project, + "--coordinator", + coordinator, + "--json", + ], + { + type: "error", + message: + "scheduler placement failed: no capable node for placement: missing capability Command", + } + ); + assert.strictEqual(capabilityFailure.result.signal, null, capabilityFailure.result.stderr); + assert.strictEqual(capabilityFailure.result.code, 24, capabilityFailure.result.stderr); + assert.match(capabilityFailure.request, /"type":"start_process"/); + const capabilityReport = JSON.parse(capabilityFailure.result.stdout); + assert.strictEqual( + capabilityReport.run_start.machine_error.category, + "capability" + ); + assert.strictEqual( + capabilityReport.run_start.machine_error.process_exit_code_applied, + true + ); + assert( + capabilityReport.run_start.machine_error.next_actions.includes( + "attach a node with the required capabilities" + ) + ); + + const nodePolicyFailure = await runWithOneCoordinatorResponse( + (coordinator) => [ + "run", + "build", + "--project", + project, + "--coordinator", + coordinator, + "--json", + ], + { + type: "error", + message: "node policy denied native command execution", + } + ); + assert.strictEqual(nodePolicyFailure.result.signal, null, nodePolicyFailure.result.stderr); + assert.strictEqual(nodePolicyFailure.result.code, 23, nodePolicyFailure.result.stderr); + assert.match(nodePolicyFailure.request, /"type":"start_process"/); + const nodePolicyReport = JSON.parse(nodePolicyFailure.result.stdout); + assert.strictEqual( + nodePolicyReport.run_start.machine_error.category, + "policy" + ); + assert.strictEqual( + nodePolicyReport.run_start.machine_error.process_exit_code_applied, + true + ); + assert( + nodePolicyReport.run_start.machine_error.next_actions.includes( + "check coordinator policy for this action" + ) + ); + + const programFailure = await runWithOneCoordinatorResponse( + (coordinator) => [ + "task", + "list", + "--coordinator", + coordinator, + "--json", + ], + { + type: "task_events", + events: [ + { + process: "vp-current", + task: "compile", + terminal_state: "failed", + environment: "linux", + node: "node-linux", + status_code: 1, + stderr_tail: "task exited with status 1", + }, + ], + } + ); + assert.strictEqual(programFailure.result.signal, null, programFailure.result.stderr); + assert.strictEqual(programFailure.result.code, 0, programFailure.result.stderr); + assert.match(programFailure.request, /"type":"list_task_events"/); + const programReport = JSON.parse(programFailure.result.stdout); + assert.strictEqual(programReport.tasks[0].machine_error.category, "program"); + assert.strictEqual(programReport.tasks[0].machine_error.stable_exit_code, 27); + assert( + programReport.tasks[0].machine_error.next_actions.includes("disasmer logs") + ); + const artifactDownload = await runWithOneCoordinatorResponse( (coordinator) => [ "artifact", diff --git a/scripts/cli-first-contract-smoke.js b/scripts/cli-first-contract-smoke.js index a13bc9c..669a83c 100644 --- a/scripts/cli-first-contract-smoke.js +++ b/scripts/cli-first-contract-smoke.js @@ -341,6 +341,11 @@ expect( "node attach smoke verifies auto-detection evidence", /plan\.detection\.auto_detected[\s\S]*plan\.detection\.command_backend[\s\S]*source_provider_backends/ ); +expect( + nodeAttachSmoke, + "node attach smoke verifies policy-limited grant disclosures", + /grant_disclosures\.length > 0[\s\S]*coordinator_policy_limited === true[\s\S]*native_command_execution[\s\S]*source_access/ +); for (const [name, pattern] of [ ["DAP smoke verifies source request", /client\.send\("source"[\s\S]*assert\.match\(source\.content, \/compile_linux\//], ["DAP smoke verifies next step-over", /client\.send\("next", \{ threadId: 2 \}\)[\s\S]*await client\.response\(nextRequest, "next"\)[\s\S]*reason === "step"/], @@ -677,12 +682,19 @@ for (const [name, pattern] of [ } for (const [name, pattern] of [ + ["environment build rejection exit code", /environmentFailure[\s\S]*assert\.strictEqual\(environmentFailure\.code, 26/], + ["environment rejection machine category", /environmentReport\.machine_error\.category, "environment"/], ["fake coordinator quota rejection", /quota unavailable: resource limit exceeded for api_calls/], ["run uses JSON mode", /"run"[\s\S]*"--json"/], ["actual quota exit code", /assert\.strictEqual\(result\.code, 22/], ["actual quota resource-category assertion", /machine_error\.resource_category, "api_calls"/], ["actual quota community-tier label assertion", /machine_error\.community_tier_label,[\s\S]*"community tier"/], ["actual quota abuse-heuristics assertion", /machine_error\.private_abuse_heuristics_exposed,[\s\S]*false/], + ["capability rejection exit code", /capabilityFailure[\s\S]*assert\.strictEqual\(capabilityFailure\.result\.code, 24/], + ["capability rejection next action", /attach a node with the required capabilities/], + ["node policy rejection exit code", /nodePolicyFailure[\s\S]*assert\.strictEqual\(nodePolicyFailure\.result\.code, 23/], + ["node policy rejection next action", /check coordinator policy for this action/], + ["program task failure category", /programReport\.tasks\[0\]\.machine_error\.category, "program"/], ["artifact download rejection exit code", /artifactDownload[\s\S]*assert\.strictEqual\(artifactDownload\.result\.code, 21/], ["artifact export rejection exit code", /artifactExport[\s\S]*assert\.strictEqual\(artifactExport\.result\.code, 25/], ["exit-code application in JSON", /process_exit_code_applied[\s\S]*true/], diff --git a/scripts/node-attach-smoke.js b/scripts/node-attach-smoke.js index e0f2508..3b37576 100755 --- a/scripts/node-attach-smoke.js +++ b/scripts/node-attach-smoke.js @@ -229,6 +229,28 @@ function runAttachedNodeWork(addr) { (provider) => provider.provider === "filesystem" && provider.detected ) ); + assert( + report.grant_disclosures.length > 0, + "node attach should disclose capability grants before reporting capabilities" + ); + assert( + report.grant_disclosures.every( + (disclosure) => disclosure.coordinator_policy_limited === true + ), + "node attach should mark all capability grants as coordinator-policy-limited" + ); + assert( + report.grant_disclosures.some( + (disclosure) => disclosure.grant === "native_command_execution" + ), + "node attach should disclose native command execution when detected" + ); + assert( + report.grant_disclosures.some( + (disclosure) => disclosure.grant === "source_access" + ), + "node attach should disclose source access when detected" + ); assert.strictEqual(report.boundary.cli_contacted_coordinator, true); assert.strictEqual(report.boundary.used_enrollment_exchange, true); assert.strictEqual(report.boundary.coordinator_session_requests, 3);