Public dry run dryrun-20b59dc46b72
Source commit: 20b59dc46b72103c2f8a516c692b5cc3d54fab19 Public tree identity: sha256:aaa5ac7b58b2a0d23c6b11e5f76324bf3839ca1f62653a83deb736932adcfbd9
This commit is contained in:
commit
2bef715211
221 changed files with 79792 additions and 0 deletions
83
scripts/acceptance-cli-first.sh
Executable file
83
scripts/acceptance-cli-first.sh
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo"
|
||||
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_E2E:-}" == "1" ]]; then
|
||||
echo "CLI-first acceptance does not run final public-release e2e; unset DISASMER_PUBLIC_RELEASE_DRYRUN_E2E" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:-}" == "1" ]]; then
|
||||
echo "CLI-first acceptance does not run final public-release evidence; unset DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node scripts/acceptance-report.js cli-first
|
||||
node scripts/cli-first-contract-smoke.js
|
||||
node scripts/acceptance-report-smoke.js
|
||||
node scripts/acceptance-doc-contract-smoke.js
|
||||
node scripts/acceptance-environment-contract-smoke.js
|
||||
node scripts/acceptance-evidence-contract-smoke.js
|
||||
node scripts/code-size-guard.js
|
||||
node scripts/public-private-boundary-smoke.js
|
||||
node scripts/release-blocker-smoke.js
|
||||
node scripts/resource-metering-contract-smoke.js
|
||||
node scripts/hostile-input-contract-smoke.js
|
||||
node scripts/tenant-isolation-contract-smoke.js
|
||||
node scripts/public-story-contract-smoke.js
|
||||
|
||||
if [[ "${DISASMER_CLI_FIRST_PREPARE_PUBLIC_RELEASE:-1}" == "1" ]]; then
|
||||
node scripts/prepare-public-release-dryrun.js
|
||||
fi
|
||||
|
||||
node scripts/public-release-dryrun-contract-smoke.js
|
||||
node scripts/public-browser-login-contract-smoke.js
|
||||
node scripts/self-hosted-coordinator-smoke.js
|
||||
if [[ "${DISASMER_CLI_HAPPY_PATH_LIVE:-}" == "1" ]]; then
|
||||
node scripts/cli-happy-path-live-smoke.js
|
||||
fi
|
||||
node scripts/public-local-demo-matrix-smoke.js
|
||||
scripts/release-source-scan.sh
|
||||
|
||||
cargo fmt --all --check
|
||||
cargo test --workspace
|
||||
cargo build --workspace --bins
|
||||
|
||||
node scripts/docs-smoke.js
|
||||
node scripts/cli-output-mode-smoke.js
|
||||
node scripts/cli-login-smoke.js
|
||||
node scripts/cli-error-exit-smoke.js
|
||||
node scripts/cli-browser-login-flow-smoke.js
|
||||
node scripts/cli-install-smoke.js
|
||||
node scripts/user-session-token-boundary-smoke.js
|
||||
node scripts/sdk-spawn-runtime-smoke.js
|
||||
node scripts/node-lifecycle-contract-smoke.js
|
||||
node scripts/wasmtime-node-smoke.js
|
||||
node scripts/wasmtime-assignment-smoke.js
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
node scripts/podman-backend-smoke.js
|
||||
elif command -v nix >/dev/null 2>&1; then
|
||||
nix shell nixpkgs#podman --command node scripts/podman-backend-smoke.js
|
||||
else
|
||||
node scripts/podman-backend-smoke.js
|
||||
fi
|
||||
node scripts/vscode-extension-smoke.js
|
||||
node scripts/vscode-f5-smoke.js
|
||||
node scripts/node-attach-smoke.js
|
||||
node scripts/cli-local-run-smoke.js
|
||||
node scripts/artifact-download-smoke.js
|
||||
node scripts/artifact-export-smoke.js
|
||||
node scripts/operator-panel-smoke.js
|
||||
node scripts/source-preparation-smoke.js
|
||||
node scripts/scheduler-placement-smoke.js
|
||||
node scripts/windows-best-effort-smoke.js
|
||||
node scripts/windows-validation-contract-smoke.js
|
||||
node scripts/quic-smoke.js
|
||||
node scripts/dap-smoke.js
|
||||
node scripts/flagship-demo-smoke.js
|
||||
|
||||
if [[ "${DISASMER_CLI_FIRST_INCLUDE_PRIVATE:-0}" == "1" ]]; then
|
||||
scripts/acceptance-private.sh
|
||||
fi
|
||||
205
scripts/acceptance-doc-contract-smoke.js
Executable file
205
scripts/acceptance-doc-contract-smoke.js
Executable file
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
assertPreFinalOrFinalLedger,
|
||||
} = require("./phase3-ledger");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
if (
|
||||
fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json")) &&
|
||||
!fs.existsSync(path.join(repo, "acceptance_criteria.md"))
|
||||
) {
|
||||
console.log(
|
||||
"Acceptance doc contract smoke skipped: root acceptance markdown is filtered from this public tree"
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function criterionLines(source) {
|
||||
return source
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => /^- \[[ x]\] \*\*/.test(line));
|
||||
}
|
||||
|
||||
function assertEveryCriterionHasStatus(source, name) {
|
||||
const lines = criterionLines(source);
|
||||
assert(lines.length > 0, `${name} must contain acceptance criteria`);
|
||||
for (const line of lines) {
|
||||
assert.match(
|
||||
line,
|
||||
/^- \[[ x]\] \*\*(Passed|Partial|Open|Postponed)(?: \([^)]+\))?:\*\*/,
|
||||
`${name} criterion lacks an explicit status prefix: ${line}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoOpenCriteria(source, name) {
|
||||
const open = criterionLines(source).filter((line) => /\*\*Open(?::| \()/.test(line));
|
||||
assert.deepStrictEqual(open, [], `${name} still has Open criteria`);
|
||||
}
|
||||
|
||||
function assertPhase3CriterionHeadingsHaveStatus(source, name) {
|
||||
const headings = source
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => /^## /.test(line) && /P3-[A-Z]+-\d{3}:/.test(line));
|
||||
assert(headings.length > 0, `${name} must contain Phase 3 criteria`);
|
||||
for (const line of headings) {
|
||||
assert.match(
|
||||
line,
|
||||
/^## \*\*(Passed|Partial|Open|Postponed):\*\* P3-[A-Z]+-\d{3}:/,
|
||||
`${name} criterion heading lacks an explicit status prefix: ${line}`
|
||||
);
|
||||
}
|
||||
assertPreFinalOrFinalLedger(source);
|
||||
}
|
||||
|
||||
const phase2 = read("acceptance_criteria_phase2.md");
|
||||
const base = read("acceptance_criteria.md");
|
||||
const cliFirst = read("cli_acceptance_criteria.md");
|
||||
const website = read("website_mvp_inventory.md");
|
||||
const phase3 = read("phase_3_acceptance_criteria.md");
|
||||
const docsSmoke = read("scripts/docs-smoke.js");
|
||||
const releaseBlockerSmoke = read("scripts/release-blocker-smoke.js");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
|
||||
assert.match(
|
||||
phase2,
|
||||
/phase 2 superset of `acceptance_criteria\.md`/,
|
||||
"phase 2 criteria must declare that they are a superset of the base criteria"
|
||||
);
|
||||
assert.match(
|
||||
phase2,
|
||||
/Existing `acceptance_criteria\.md` remains required unless it conflicts with this stricter release document; this document wins in conflicts/,
|
||||
"phase 2 criteria must keep base acceptance criteria required unless stricter phase 2 criteria conflict"
|
||||
);
|
||||
assert.match(
|
||||
phase2,
|
||||
/- \[x\] \*\*Passed:\*\* Existing `acceptance_criteria\.md` remains required unless it conflicts with this stricter release document; this document wins in conflicts\./,
|
||||
"phase 2 cross-document requirement must be marked passed only when this guard is wired"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
phase3,
|
||||
/Canonical Phase 3 gate:[\s\S]*active Phase 3 acceptance document/,
|
||||
"phase 3 criteria must declare itself as the canonical current gate"
|
||||
);
|
||||
assert.match(
|
||||
phase3,
|
||||
/Important reading note:[\s\S]*not necessarily always mean adding code/,
|
||||
"phase 3 criteria must keep the no-automatic-code-work disclaimer at the top"
|
||||
);
|
||||
assert.match(
|
||||
phase3,
|
||||
/Every criterion heading below is prefixed with \*\*Passed\*\*, \*\*Partial\*\*, or \*\*Open\*\*/,
|
||||
"phase 3 criteria must explain the explicit status-prefix convention"
|
||||
);
|
||||
assertPhase3CriterionHeadingsHaveStatus(phase3, "phase_3_acceptance_criteria.md");
|
||||
|
||||
for (const [source, name] of [
|
||||
[base, "acceptance_criteria.md"],
|
||||
[phase2, "acceptance_criteria_phase2.md"],
|
||||
[cliFirst, "cli_acceptance_criteria.md"],
|
||||
]) {
|
||||
assertEveryCriterionHasStatus(source, name);
|
||||
assertNoOpenCriteria(source, name);
|
||||
assert.match(
|
||||
source,
|
||||
/Design-document references to billing, paid plans, or plan flags are future metadata placeholders; they do not require MVP/,
|
||||
`${name} must keep billing/paid-plan placeholders outside MVP implementation scope`
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/not the canonical Phase 3 release gate; current Phase 3 status and release verification live in `phase_3_acceptance_criteria\.md`/,
|
||||
`${name} must point current Phase 3 status to phase_3_acceptance_criteria.md`
|
||||
);
|
||||
}
|
||||
|
||||
assertEveryCriterionHasStatus(website, "website_mvp_inventory.md");
|
||||
assert.match(
|
||||
website,
|
||||
/private hosted website addendum to `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/,
|
||||
"website acceptance criteria must declare itself as the private hosted website addendum"
|
||||
);
|
||||
assert.match(
|
||||
website,
|
||||
/barebones functional HTML with no CSS/,
|
||||
"website acceptance criteria must keep the MVP website barebones with no CSS"
|
||||
);
|
||||
assert.match(
|
||||
website,
|
||||
/Billing is not part of the MVP[\s\S]*Design-document references to billing, paid plans, or plan flags are future metadata placeholders; they do not require MVP website routes/,
|
||||
"website acceptance criteria must keep billing and paid-plan placeholders outside MVP website scope"
|
||||
);
|
||||
assert.match(
|
||||
website,
|
||||
/- \[ \] \*\*Open:\*\* The private hosted website acceptance gate exists and exercises the barebones website through `disasmer\.michelpaulissen\.com`/,
|
||||
"website acceptance criteria must leave the private website deployment gate explicit and open"
|
||||
);
|
||||
assert.match(
|
||||
website,
|
||||
/not the canonical Phase 3 release gate; current Phase 3 status and release verification live in `phase_3_acceptance_criteria\.md`/,
|
||||
"website acceptance criteria must point current Phase 3 status to phase_3_acceptance_criteria.md"
|
||||
);
|
||||
|
||||
for (const file of [
|
||||
"MVP.md",
|
||||
"acceptance_criteria.md",
|
||||
"acceptance_criteria_phase2.md",
|
||||
"cli_acceptance_criteria.md",
|
||||
"website_mvp_inventory.md",
|
||||
"phase_3_acceptance_criteria.md",
|
||||
]) {
|
||||
assert(
|
||||
docsSmoke.includes(`"${file}"`),
|
||||
`docs smoke must include ${file} as user-facing acceptance context`
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
releaseBlockerSmoke.includes('const phase2 = read("acceptance_criteria_phase2.md")'),
|
||||
"release-blocker smoke must read phase 2 acceptance criteria"
|
||||
);
|
||||
assert(
|
||||
releaseBlockerSmoke.includes('const base = read("acceptance_criteria.md")'),
|
||||
"release-blocker smoke must read base acceptance criteria"
|
||||
);
|
||||
|
||||
for (const [source, name] of [
|
||||
[base, "acceptance_criteria.md"],
|
||||
[phase2, "acceptance_criteria_phase2.md"],
|
||||
]) {
|
||||
for (const [label, pattern] of [
|
||||
["MVP selected locals", /selected (?:top-level )?locals|selected real source locals/],
|
||||
["MVP task args", /task arguments|task args/],
|
||||
["MVP handle inspection", /Artifact.*SourceSnapshot.*Blob|Disasmer handles/],
|
||||
["MVP stdout stderr", /stdout\/stderr/],
|
||||
["MVP unavailable locals", /cannot be inspected|unavailable-local/],
|
||||
["MVP required DAP surface", /initialize[\s\S]*launch.*attach[\s\S]*setBreakpoints[\s\S]*configurationDone[\s\S]*threads[\s\S]*stackTrace[\s\S]*scopes[\s\S]*variables[\s\S]*continue[\s\S]*pause/],
|
||||
]) {
|
||||
assert.match(source, pattern, `${name} must include MVP debugging criterion: ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["private acceptance", privateAcceptance],
|
||||
["CLI-first acceptance", cliFirstAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
assert(
|
||||
script.includes("node scripts/acceptance-doc-contract-smoke.js"),
|
||||
`${scriptName} must run acceptance-doc-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Acceptance doc contract smoke passed");
|
||||
262
scripts/acceptance-environment-contract-smoke.js
Executable file
262
scripts/acceptance-environment-contract-smoke.js
Executable file
|
|
@ -0,0 +1,262 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(relativePath) {
|
||||
const absolute = path.join(repo, relativePath);
|
||||
if (!fs.existsSync(absolute)) return null;
|
||||
return fs.readFileSync(absolute, "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing acceptance environment evidence: ${name}`);
|
||||
}
|
||||
|
||||
function expectIncludes(source, name, text) {
|
||||
assert(source.includes(text), `missing acceptance environment evidence: ${name}`);
|
||||
}
|
||||
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const acceptanceReport = read("scripts/acceptance-report.js");
|
||||
const acceptanceReportSmoke = read("scripts/acceptance-report-smoke.js");
|
||||
const readme = read("README.md");
|
||||
const windowsWorkflow = read(".forgejo/workflows/windows-validation.yml");
|
||||
const publicDryrunServiceSmoke = maybeRead("private/hosted-policy/scripts/public-release-dryrun-service-smoke.js");
|
||||
const publicDryrunDeployPrep = maybeRead("private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js");
|
||||
const publicDryrunSystemd = maybeRead("private/hosted-policy/deploy/disasmer-public-release-dryrun.service");
|
||||
const publicDryrunRunbook = maybeRead("private/hosted-policy/deploy/README.md");
|
||||
const hostedClientCompatSmoke = maybeRead("private/hosted-policy/scripts/hosted-client-compat-smoke.js");
|
||||
const hostedService = maybeRead("private/hosted-policy/src/bin/disasmer-hosted-service.rs");
|
||||
const hostedStartup = maybeRead(
|
||||
"private/hosted-policy/src/bin/disasmer-hosted-service/startup.rs"
|
||||
);
|
||||
const hostedOperatorAuth = maybeRead(
|
||||
"private/hosted-policy/src/bin/disasmer-hosted-service/operator_auth.rs"
|
||||
);
|
||||
const publicDryrunE2e = read("scripts/public-release-dryrun-e2e.js");
|
||||
const finalDryrunEvidence = read("scripts/public-release-dryrun-final-evidence.js");
|
||||
|
||||
for (const [name, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["private acceptance", privateAcceptance],
|
||||
]) {
|
||||
expect(script, `${name} writes acceptance environment report first`, /node scripts\/acceptance-report\.js (public|private)[\s\S]*node scripts\/acceptance-report-smoke\.js/);
|
||||
expectIncludes(
|
||||
script,
|
||||
`${name} runs acceptance environment contract`,
|
||||
"node scripts/acceptance-environment-contract-smoke.js"
|
||||
);
|
||||
}
|
||||
|
||||
for (const [name, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
for (const smoke of [
|
||||
"scripts/wasmtime-assignment-smoke.js",
|
||||
"scripts/node-attach-smoke.js",
|
||||
"scripts/cli-local-run-smoke.js",
|
||||
"scripts/vscode-f5-smoke.js",
|
||||
"scripts/dap-smoke.js",
|
||||
"scripts/artifact-download-smoke.js",
|
||||
"scripts/artifact-export-smoke.js",
|
||||
"scripts/public-local-demo-matrix-smoke.js",
|
||||
]) {
|
||||
assert(script.includes(`node ${smoke}`), `${name} must run ${smoke}`);
|
||||
}
|
||||
}
|
||||
|
||||
expectIncludes(publicAcceptance, "public gate runs rootless Podman backend smoke", "node scripts/podman-backend-smoke.js");
|
||||
expectIncludes(publicAcceptance, "public gate runs Wasmtime node smoke", "node scripts/wasmtime-node-smoke.js");
|
||||
expectIncludes(privateAcceptance, "private gate runs hosted deployment smoke", "node private/hosted-policy/scripts/hosted-deployment-smoke.js");
|
||||
expectIncludes(privateAcceptance, "private gate prepares public dry-run deployment bundle", "node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js");
|
||||
expectIncludes(privateAcceptance, "private gate runs hosted Client compatibility smoke", "node private/hosted-policy/scripts/hosted-client-compat-smoke.js");
|
||||
expectIncludes(privateAcceptance, "private gate runs standalone Core coordinator smoke", "node scripts/self-hosted-coordinator-smoke.js");
|
||||
expectIncludes(privateAcceptance, "private gate runs Postgres durable smoke", "node private/hosted-policy/scripts/postgres-durable-smoke.js");
|
||||
expectIncludes(privateAcceptance, "private gate can run public release dry-run service smoke", "node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js");
|
||||
expectIncludes(privateAcceptance, "public release dry-run service smoke is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR");
|
||||
expectIncludes(privateAcceptance, "private gate runs hosted policy cargo tests", "cargo test --manifest-path private/hosted-policy/Cargo.toml");
|
||||
expectIncludes(publicAcceptance, "public gate can run final dry-run evidence verifier", "node scripts/public-release-dryrun-final-evidence.js");
|
||||
expectIncludes(publicAcceptance, "public gate can run public release dry-run e2e", "node scripts/public-release-dryrun-e2e.js");
|
||||
expectIncludes(publicAcceptance, "public release dry-run e2e is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_E2E");
|
||||
expectIncludes(privateAcceptance, "private gate can run final dry-run evidence verifier", "node scripts/public-release-dryrun-final-evidence.js");
|
||||
expectIncludes(publicAcceptance, "public final dry-run verifier is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL");
|
||||
expectIncludes(privateAcceptance, "private final dry-run verifier is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL");
|
||||
|
||||
expect(publicSplit, "public split excludes private modules", /--exclude='\.\/private'/);
|
||||
expect(publicSplit, "public split excludes experiments", /--exclude='\.\/experiments'/);
|
||||
expect(publicSplit, "public split tests copied workspace", /cargo test --workspace --manifest-path "\$tmp_dir\/Cargo\.toml"/);
|
||||
expect(publicSplit, "public split builds copied workspace binaries", /cargo build --workspace --bins --manifest-path "\$tmp_dir\/Cargo\.toml"/);
|
||||
expectIncludes(
|
||||
publicSplit,
|
||||
"public split runs acceptance environment contract from copied tree",
|
||||
'(cd "$tmp_dir" && node scripts/acceptance-environment-contract-smoke.js)'
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["commit SHA fallback", /process\.env\.DISASMER_ACCEPTANCE_COMMIT \|\| commandOutput\("git", \["rev-parse", "HEAD"\]\)/],
|
||||
["tree status", /tree_status: \(commandOutput\("git", \["status", "--short"\]\) \|\| ""\)[\s\S]*\.filter\(Boolean\)/],
|
||||
["OS report", /platform: os\.platform\(\)[\s\S]*kernel: os\.release\(\)/],
|
||||
["Rust report", /rustc: commandOutput\("rustc", \["--version"\]\)/],
|
||||
["Node report", /version: process\.version/],
|
||||
["Podman report", /function podmanReport\(\)/],
|
||||
["Postgres report", /postgres: \{[\s\S]*commandOutput\("postgres", \["--version"\]\) \|\|[\s\S]*commandOutput\("psql", \["--version"\]\)/],
|
||||
["browser harness report", /browser_harness:/],
|
||||
["VS Code harness report", /vscode_harness:/],
|
||||
["Windows validation report", /windows_validation: process\.env\.DISASMER_WINDOWS_VALIDATION \|\| "not-run"/],
|
||||
]) {
|
||||
expect(acceptanceReport, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["acceptance report validates Podman incomplete state", /assertPodmanReport/],
|
||||
["acceptance report validates Windows not-run", /assertReport\(runReport\(mode\), mode, "not-run"\)/],
|
||||
["acceptance report validates Windows runner mode", /DISASMER_WINDOWS_VALIDATION: "forgejo-windows-runner"/],
|
||||
]) {
|
||||
expect(acceptanceReportSmoke, name, pattern);
|
||||
}
|
||||
|
||||
expect(readme, "README documents public acceptance script", /scripts\/acceptance-public\.sh/);
|
||||
expect(readme, "README documents private acceptance script", /scripts\/acceptance-private\.sh/);
|
||||
expect(readme, "README documents rootless Podman incomplete handling", /Podman backend behavior is marked `incomplete`/);
|
||||
expect(readme, "README documents Postgres discovery in environment report", /Podman\/Postgres discovery/);
|
||||
expect(readme, "README documents manual Windows validation", /manual `Windows validation`\s+workflow/);
|
||||
|
||||
expect(windowsWorkflow, "Windows workflow is manual", /workflow_dispatch/);
|
||||
expect(windowsWorkflow, "Windows workflow uses intermittent Windows runner", /runs-on:\s*windows/);
|
||||
expect(windowsWorkflow, "Windows workflow writes acceptance report", /node scripts\/acceptance-report\.js windows/);
|
||||
|
||||
if (publicDryrunServiceSmoke && publicDryrunDeployPrep && publicDryrunSystemd && publicDryrunRunbook) {
|
||||
for (const [name, pattern] of [
|
||||
["service smoke requires external service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR is required/],
|
||||
["service smoke requires browser test driver", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND/],
|
||||
["service smoke loads release manifest", /public-release-manifest\.json/],
|
||||
["service smoke rejects stale release manifest", /manifest\.source_commit[\s\S]*expectedCommit/],
|
||||
["service smoke records source commit", /source_commit: release\.sourceCommit/],
|
||||
["service smoke records release name", /release_name: release\.releaseName/],
|
||||
["service smoke connects through public domain", /addr\.host[\s\S]*serviceHost/],
|
||||
["service smoke verifies DNS state", /\["not-published", "published"\]\.includes\(dnsPublicationState\)/],
|
||||
["service smoke begins a server-owned OIDC login", /type: "begin_oidc_browser_login"/],
|
||||
["service smoke polls with opaque credentials", /type: "poll_oidc_browser_login"[\s\S]*transaction_id[\s\S]*polling_secret/],
|
||||
["service smoke rejects missing hosted callback completion", /timed out waiting for the hosted OIDC callback/],
|
||||
["service smoke reads server-created project", /type: "list_projects"/],
|
||||
["service smoke enrolls signed node", /type: "create_node_enrollment_grant"[\s\S]*exchange_node_enrollment_grant/],
|
||||
["service smoke starts session-authorized process", /type: "start_process"/],
|
||||
["service smoke reports signed node capabilities", /signedNodeRequest[\s\S]*report_node_capabilities/],
|
||||
["service smoke reads authorized debug state", /type: "debug_attach"/],
|
||||
["service smoke aborts its probe process", /type: "abort_process"/],
|
||||
["service smoke records private hosted coordinator", /coordinator_implementation:[\s\S]*"hosted-policy-coordinator"/],
|
||||
["service smoke writes evidence report", /public-release-dryrun-service\.json/],
|
||||
]) {
|
||||
expect(publicDryrunServiceSmoke, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, source, pattern] of [
|
||||
["deployment prep builds hosted service release", publicDryrunDeployPrep, /cargo"[\s\S]*"build"[\s\S]*"--release"[\s\S]*"private\/hosted-policy\/Cargo\.toml"[\s\S]*"disasmer-hosted-service"/],
|
||||
["deployment prep stages systemd unit", publicDryrunDeployPrep, /disasmer-public-release-dryrun\.service/],
|
||||
["deployment prep writes manifest", publicDryrunDeployPrep, /deployment-manifest\.json/],
|
||||
["deployment prep records private hosted coordinator", publicDryrunDeployPrep, /coordinator_implementation:[\s\S]*"hosted-policy-coordinator"/],
|
||||
["deployment prep records service address", publicDryrunDeployPrep, /service_addr:[\s\S]*`\$\{serviceHost\}:\$\{servicePort\}`/],
|
||||
["deployment prep records DNS state", publicDryrunDeployPrep, /dns_publication_state: dnsPublicationState/],
|
||||
["deployment prep records service smoke command", publicDryrunDeployPrep, /public-release-dryrun-service-smoke\.js/],
|
||||
["systemd keeps hosted HTTP upstream on loopback", publicDryrunSystemd, /--listen 127\.0\.0\.1:9080/],
|
||||
["systemd loads root-owned operator credentials", publicDryrunSystemd, /EnvironmentFile=\/etc\/disasmer-public-release-dryrun\/operator\.env/],
|
||||
["systemd avoids privileged port capability", publicDryrunSystemd, /NoNewPrivileges=true/],
|
||||
["systemd uses dedicated user", publicDryrunSystemd, /User=disasmer[\s\S]*Group=disasmer/],
|
||||
["runbook says externally reachable", publicDryrunRunbook, /externally reachable host/],
|
||||
["runbook documents DNS pending fallback", publicDryrunRunbook, /Until the `disasmer\.michelpaulissen\.com` DNS record is deployed/],
|
||||
["runbook gives hosts entry", publicDryrunRunbook, /<deployment-ip> disasmer\.michelpaulissen\.com/],
|
||||
["runbook separates authority vocabulary", publicDryrunRunbook, /Client and signed Node protocols[\s\S]*Identity boundary[\s\S]*Operator boundary/],
|
||||
["runbook documents operator credential", publicDryrunRunbook, /DISASMER_HOSTED_OPERATOR_TOKEN/],
|
||||
]) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (hostedClientCompatSmoke && hostedService && hostedStartup && hostedOperatorAuth) {
|
||||
const hostedServiceSecurity = `${hostedService}\n${hostedStartup}\n${hostedOperatorAuth}`;
|
||||
for (const [name, pattern] of [
|
||||
["hosted service embeds core coordinator runtime", /core_coordinator: CoordinatorService/],
|
||||
["hosted service parses the unified identity, operator, and client protocol", /decode_incoming_request/],
|
||||
["hosted service delegates client requests", /handle_client_request/],
|
||||
["hosted service routes decoded client requests to Core", /IncomingRequest::Client\(request\)/],
|
||||
["hosted service requires replay-resistant operator envelope proofs", /hosted_operator_request[\s\S]*HostedOperatorAuth[\s\S]*replay_nonces[\s\S]*verify_request[\s\S]*hosted_operator_request_proof_from_token_digest/],
|
||||
["hosted service issues the authenticated Core CLI session", /core_coordinator[\s\S]*\.issue_cli_session/],
|
||||
["hosted service creates the authenticated default project through Core", /AuthenticatedCoordinatorRequest::CreateProject/],
|
||||
]) {
|
||||
expect(hostedServiceSecurity, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["compat smoke starts hosted service", /disasmer-hosted-service/],
|
||||
["compat smoke creates hosted project", /type: "create_project"/],
|
||||
["compat smoke creates session-authorized enrollment grant", /type: "create_node_enrollment_grant"/],
|
||||
["compat smoke runs public CLI attach", /"disasmer-cli"[\s\S]*"node"[\s\S]*"attach"/],
|
||||
["compat smoke verifies public enrollment exchange", /node_enrollment_exchanged/],
|
||||
["compat smoke runs public CLI browser login", /"login"[\s\S]*"--browser"[\s\S]*DISASMER_BROWSER_OPEN_COMMAND/],
|
||||
["compat smoke derives hosted scope", /const session = login\.coordinator_response\.session/],
|
||||
["compat smoke rejects forged identity", /forged_unsigned_project/],
|
||||
["compat smoke rejects an actually expired hosted session", /DISASMER_HOSTED_CLI_SESSION_TTL_SECONDS[\s\S]*expired; run disasmer login --browser again/],
|
||||
["compat smoke denies cross-tenant task-event reads", /crossTenantTaskEventsDenied[\s\S]*type: "list_task_events"[\s\S]*vp-victim/],
|
||||
["compat smoke writes evidence report", /hosted-client-compat\.json/],
|
||||
]) {
|
||||
expect(hostedClientCompatSmoke, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E/],
|
||||
["e2e runner requires the honestly reset pre-final ledger", /assertPreFinalLedger\(readPhase3Ledger\(repo\)\)/],
|
||||
["e2e runner requires public domain service address", /serviceAddr[\s\S]*serviceHost/],
|
||||
["e2e runner downloads release assets", /downloadReleaseAssets/],
|
||||
["e2e runner verifies release checksums", /verifyChecksums/],
|
||||
["e2e runner clones public repo", /git"[\s\S]*"clone"[\s\S]*publicRepositoryUrl/],
|
||||
["e2e runner uses default hosted coordinator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/],
|
||||
["e2e runner uses server-owned browser login", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND[\s\S]*DISASMER_BROWSER_OPEN_COMMAND/],
|
||||
["e2e runner attaches user node", /node"[\s\S]*"attach"/],
|
||||
["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/],
|
||||
["e2e runner launches released product through CLI", /"run"[\s\S]*"build"[\s\S]*runReport\.status[\s\S]*main_launched/],
|
||||
["standalone Core proof launches a real Wasm TaskSpec", /type: "launch_task"[\s\S]*task_spec:[\s\S]*kind: "coordinator_node_wasm"[\s\S]*bundle_digest: manifest\.bundle_digest/],
|
||||
["e2e runner verifies public assignment polling", /worker_assignment_poll_protocol/],
|
||||
["e2e runner validates standalone Core coordinator", /validateStandaloneCoreCoordinator/],
|
||||
["e2e runner records standalone Core coordinator", /core_coordinator_implementation/],
|
||||
["e2e runner verifies task events", /list_task_events/],
|
||||
["e2e runner creates artifact download link", /create_artifact_download_link/],
|
||||
["e2e runner verifies VS Code debugger", /vscode-f5-smoke\.js/],
|
||||
["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/],
|
||||
]) {
|
||||
expect(publicDryrunE2e, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["final verifier requires public release manifest", /public-release-manifest\.json/],
|
||||
["final verifier requires Forgejo release evidence", /public-release-dryrun-forgejo-release\.json/],
|
||||
["final verifier requires deployment manifest", /deployment-manifest\.json/],
|
||||
["final verifier requires service smoke evidence", /public-release-dryrun-service\.json/],
|
||||
["final verifier requires all 191 Phase 3 criteria passed", /assertFinalLedger\(readPhase3Ledger\(repo\)\)/],
|
||||
["final verifier requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final verifier requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final verifier requires hosted Client compatibility evidence", /hosted-client-compat\.json/],
|
||||
["final verifier requires current Client compatibility source", /compat\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final verifier requires current Client compatibility release", /compat\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final verifier requires Core coordinator compatibility evidence", /core-coordinator-compat\.json/],
|
||||
["final verifier requires current public coordinator source", /coreCoordinator\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final verifier requires current public coordinator release", /coreCoordinator\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final verifier requires public e2e evidence", /public-release-dryrun-e2e\.json/],
|
||||
["final verifier records both coordinator validations", /coordinator_validation/],
|
||||
["final verifier writes final evidence", /public-release-dryrun-final\.json/],
|
||||
]) {
|
||||
expect(finalDryrunEvidence, name, pattern);
|
||||
}
|
||||
|
||||
console.log("Acceptance environment contract smoke passed");
|
||||
1080
scripts/acceptance-evidence-contract-smoke.js
Executable file
1080
scripts/acceptance-evidence-contract-smoke.js
Executable file
File diff suppressed because it is too large
Load diff
39
scripts/acceptance-private.sh
Executable file
39
scripts/acceptance-private.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo"
|
||||
|
||||
node scripts/acceptance-report.js private
|
||||
node scripts/acceptance-report-smoke.js
|
||||
node scripts/acceptance-doc-contract-smoke.js
|
||||
node scripts/acceptance-environment-contract-smoke.js
|
||||
node scripts/acceptance-evidence-contract-smoke.js
|
||||
node private/hosted-policy/scripts/phase3-live-infra-contract-smoke.js
|
||||
node scripts/code-size-guard.js
|
||||
node scripts/public-private-boundary-smoke.js
|
||||
node scripts/release-blocker-smoke.js
|
||||
node scripts/resource-metering-contract-smoke.js
|
||||
node scripts/hostile-input-contract-smoke.js
|
||||
node scripts/tenant-isolation-contract-smoke.js
|
||||
node scripts/cli-error-exit-smoke.js
|
||||
scripts/release-source-scan.sh
|
||||
cargo test --manifest-path private/hosted-policy/Cargo.toml
|
||||
node private/hosted-policy/scripts/hosted-signup-contract-smoke.js
|
||||
node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js
|
||||
node private/hosted-policy/scripts/hosted-deployment-smoke.js
|
||||
node private/hosted-policy/scripts/hosted-client-compat-smoke.js
|
||||
node scripts/self-hosted-coordinator-smoke.js
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
node private/hosted-policy/scripts/postgres-durable-smoke.js
|
||||
elif command -v nix >/dev/null 2>&1; then
|
||||
nix shell nixpkgs#podman --command node private/hosted-policy/scripts/postgres-durable-smoke.js
|
||||
else
|
||||
node private/hosted-policy/scripts/postgres-durable-smoke.js
|
||||
fi
|
||||
if [[ -n "${DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR:-}" ]]; then
|
||||
node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js
|
||||
fi
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:-}" == "1" ]]; then
|
||||
node scripts/public-release-dryrun-final-evidence.js
|
||||
fi
|
||||
72
scripts/acceptance-public.sh
Executable file
72
scripts/acceptance-public.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo"
|
||||
|
||||
node scripts/acceptance-report.js public
|
||||
node scripts/acceptance-report-smoke.js
|
||||
node scripts/acceptance-doc-contract-smoke.js
|
||||
node scripts/acceptance-environment-contract-smoke.js
|
||||
node scripts/acceptance-evidence-contract-smoke.js
|
||||
node scripts/code-size-guard.js
|
||||
node scripts/public-private-boundary-smoke.js
|
||||
node scripts/release-blocker-smoke.js
|
||||
node scripts/resource-metering-contract-smoke.js
|
||||
node scripts/hostile-input-contract-smoke.js
|
||||
node scripts/tenant-isolation-contract-smoke.js
|
||||
node scripts/public-story-contract-smoke.js
|
||||
node scripts/public-release-dryrun-contract-smoke.js
|
||||
node scripts/public-browser-login-contract-smoke.js
|
||||
node scripts/self-hosted-coordinator-smoke.js
|
||||
node scripts/public-local-demo-matrix-smoke.js
|
||||
scripts/release-source-scan.sh
|
||||
node scripts/prepare-public-release-dryrun.js
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_PREFLIGHT:-}" == "1" ]]; then
|
||||
node scripts/public-release-dryrun-preflight.js
|
||||
fi
|
||||
if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]; then
|
||||
node scripts/publish-public-release-dryrun.js
|
||||
fi
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_E2E:-}" == "1" ]]; then
|
||||
node scripts/public-release-dryrun-e2e.js
|
||||
fi
|
||||
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:-}" == "1" ]]; then
|
||||
node scripts/public-release-dryrun-final-evidence.js
|
||||
fi
|
||||
cargo fmt --all --check
|
||||
cargo test --workspace
|
||||
cargo build --workspace --bins
|
||||
node scripts/docs-smoke.js
|
||||
node scripts/cli-output-mode-smoke.js
|
||||
node scripts/cli-login-smoke.js
|
||||
node scripts/cli-error-exit-smoke.js
|
||||
node scripts/cli-browser-login-flow-smoke.js
|
||||
node scripts/cli-install-smoke.js
|
||||
node scripts/user-session-token-boundary-smoke.js
|
||||
node scripts/sdk-spawn-runtime-smoke.js
|
||||
node scripts/node-lifecycle-contract-smoke.js
|
||||
node scripts/wasmtime-node-smoke.js
|
||||
node scripts/wasmtime-assignment-smoke.js
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
node scripts/podman-backend-smoke.js
|
||||
elif command -v nix >/dev/null 2>&1; then
|
||||
nix shell nixpkgs#podman --command node scripts/podman-backend-smoke.js
|
||||
else
|
||||
node scripts/podman-backend-smoke.js
|
||||
fi
|
||||
node scripts/vscode-extension-smoke.js
|
||||
node scripts/vscode-f5-smoke.js
|
||||
node scripts/node-attach-smoke.js
|
||||
node scripts/cli-local-run-smoke.js
|
||||
node scripts/artifact-download-smoke.js
|
||||
node scripts/artifact-export-smoke.js
|
||||
node scripts/operator-panel-smoke.js
|
||||
node scripts/source-preparation-smoke.js
|
||||
node scripts/scheduler-placement-smoke.js
|
||||
node scripts/windows-best-effort-smoke.js
|
||||
node scripts/windows-validation-contract-smoke.js
|
||||
node scripts/quic-smoke.js
|
||||
node scripts/dap-smoke.js
|
||||
node scripts/flagship-demo-smoke.js
|
||||
scripts/verify-public-split.sh
|
||||
113
scripts/acceptance-report-smoke.js
Normal file
113
scripts/acceptance-report-smoke.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function runReport(mode, env = {}) {
|
||||
const output = cp.execFileSync(
|
||||
"node",
|
||||
["scripts/acceptance-report.js", mode],
|
||||
{
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...env },
|
||||
}
|
||||
);
|
||||
const report = JSON.parse(output);
|
||||
const persistedPath = path.join(
|
||||
repo,
|
||||
"target",
|
||||
"acceptance",
|
||||
`${mode}-environment.json`
|
||||
);
|
||||
const persisted = JSON.parse(fs.readFileSync(persistedPath, "utf8"));
|
||||
assert.deepStrictEqual(persisted, report, `${mode} report was not persisted`);
|
||||
return report;
|
||||
}
|
||||
|
||||
function assertString(value, name) {
|
||||
assert.strictEqual(typeof value, "string", `${name} must be a string`);
|
||||
assert(value.length > 0, `${name} must not be empty`);
|
||||
}
|
||||
|
||||
function assertNullableString(value, name) {
|
||||
if (value === null) return;
|
||||
assertString(value, name);
|
||||
}
|
||||
|
||||
function assertPodmanReport(podman) {
|
||||
assert(podman && typeof podman === "object", "podman report must be an object");
|
||||
assert(
|
||||
["available", "incomplete"].includes(podman.status),
|
||||
"podman.status must be available or incomplete"
|
||||
);
|
||||
assertNullableString(podman.version, "podman.version");
|
||||
assertNullableString(podman.rootless, "podman.rootless");
|
||||
assertNullableString(podman.incomplete_reason, "podman.incomplete_reason");
|
||||
if (podman.status === "available") {
|
||||
assertString(podman.version, "podman.version");
|
||||
assert.strictEqual(podman.rootless, "true");
|
||||
assert.strictEqual(podman.incomplete_reason, null);
|
||||
} else {
|
||||
assertString(podman.incomplete_reason, "podman.incomplete_reason");
|
||||
}
|
||||
}
|
||||
|
||||
function assertReport(report, mode, expectedWindowsValidation) {
|
||||
assert.strictEqual(report.kind, "disasmer_acceptance_environment");
|
||||
assert.strictEqual(report.mode, mode);
|
||||
assert.match(report.generated_at, /^\d{4}-\d{2}-\d{2}T/);
|
||||
assert.match(report.commit, /^[0-9a-f]{40}$/);
|
||||
assert(Array.isArray(report.tree_status), "tree_status must be an array");
|
||||
|
||||
assertString(report.os.platform, "os.platform");
|
||||
assertString(report.os.release, "os.release");
|
||||
assertString(report.os.kernel, "os.kernel");
|
||||
assertString(report.os.arch, "os.arch");
|
||||
|
||||
assertString(report.rust.rustc, "rust.rustc");
|
||||
assertString(report.rust.cargo, "rust.cargo");
|
||||
assertString(report.node.version, "node.version");
|
||||
assert(report.node.version.startsWith("v"), "node.version must be Node.js style");
|
||||
|
||||
assertPodmanReport(report.podman);
|
||||
assertNullableString(report.postgres.version, "postgres.version");
|
||||
assertNullableString(report.browser_harness.version, "browser_harness.version");
|
||||
assertNullableString(report.browser_harness.command, "browser_harness.command");
|
||||
assertString(report.browser_harness.configured, "browser_harness.configured");
|
||||
|
||||
assert(Array.isArray(report.vscode_harness.smokes), "vscode smokes must be listed");
|
||||
assert(
|
||||
report.vscode_harness.smokes.includes("scripts/vscode-extension-smoke.js"),
|
||||
"VS Code extension smoke must be recorded"
|
||||
);
|
||||
assert(
|
||||
report.vscode_harness.smokes.includes("scripts/vscode-f5-smoke.js"),
|
||||
"VS Code F5 smoke must be recorded"
|
||||
);
|
||||
assertString(report.vscode_harness.engine, "vscode_harness.engine");
|
||||
assertString(
|
||||
report.vscode_harness.extension_version,
|
||||
"vscode_harness.extension_version"
|
||||
);
|
||||
|
||||
assert.strictEqual(report.windows_validation, expectedWindowsValidation);
|
||||
}
|
||||
|
||||
for (const mode of ["public", "private"]) {
|
||||
assertReport(runReport(mode), mode, "not-run");
|
||||
}
|
||||
|
||||
assertReport(
|
||||
runReport("windows", {
|
||||
DISASMER_WINDOWS_VALIDATION: "forgejo-windows-runner",
|
||||
}),
|
||||
"windows",
|
||||
"forgejo-windows-runner"
|
||||
);
|
||||
|
||||
console.log("Acceptance report smoke passed");
|
||||
139
scripts/acceptance-report.js
Executable file
139
scripts/acceptance-report.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const mode = process.argv[2] || "public";
|
||||
|
||||
function commandOutput(command, args = []) {
|
||||
try {
|
||||
return cp
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function packageJson() {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(path.join(repo, "vscode-extension/package.json"), "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
function firstCommandOutput(candidates) {
|
||||
for (const [command, args] of candidates) {
|
||||
const output = commandOutput(command, args);
|
||||
if (output) {
|
||||
return { command, version: output };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function podmanReport() {
|
||||
const version = commandOutput("podman", ["--version"]);
|
||||
if (!version) {
|
||||
return {
|
||||
status: "incomplete",
|
||||
version: null,
|
||||
rootless: null,
|
||||
incomplete_reason: "podman command is unavailable"
|
||||
};
|
||||
}
|
||||
const rootless = commandOutput("podman", [
|
||||
"info",
|
||||
"--format",
|
||||
"{{.Host.Security.Rootless}}"
|
||||
]);
|
||||
if (!rootless) {
|
||||
return {
|
||||
status: "incomplete",
|
||||
version,
|
||||
rootless: null,
|
||||
incomplete_reason: "podman info did not report rootless status"
|
||||
};
|
||||
}
|
||||
if (rootless !== "true") {
|
||||
return {
|
||||
status: "incomplete",
|
||||
version,
|
||||
rootless,
|
||||
incomplete_reason: "podman is not running in rootless mode"
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "available",
|
||||
version,
|
||||
rootless,
|
||||
incomplete_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const extensionPackage = packageJson();
|
||||
const sourceCommit =
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT || commandOutput("git", ["rev-parse", "HEAD"]);
|
||||
const browserVersion = firstCommandOutput([
|
||||
["chromium", ["--version"]],
|
||||
["chromium-browser", ["--version"]],
|
||||
["google-chrome", ["--version"]],
|
||||
["firefox", ["--version"]]
|
||||
]);
|
||||
const report = {
|
||||
kind: "disasmer_acceptance_environment",
|
||||
mode,
|
||||
commit: sourceCommit,
|
||||
tree_status: (commandOutput("git", ["status", "--short"]) || "")
|
||||
.split("\n")
|
||||
.filter(Boolean),
|
||||
generated_at: new Date().toISOString(),
|
||||
os: {
|
||||
platform: os.platform(),
|
||||
release: os.release(),
|
||||
kernel: os.release(),
|
||||
arch: os.arch()
|
||||
},
|
||||
rust: {
|
||||
rustc: commandOutput("rustc", ["--version"]),
|
||||
cargo: commandOutput("cargo", ["--version"])
|
||||
},
|
||||
node: {
|
||||
version: process.version
|
||||
},
|
||||
podman: podmanReport(),
|
||||
postgres: {
|
||||
version:
|
||||
commandOutput("postgres", ["--version"]) ||
|
||||
commandOutput("psql", ["--version"])
|
||||
},
|
||||
browser_harness: {
|
||||
version: browserVersion && browserVersion.version,
|
||||
command: browserVersion && browserVersion.command,
|
||||
configured: process.env.DISASMER_BROWSER_HARNESS || "not-configured"
|
||||
},
|
||||
vscode_harness: {
|
||||
smokes: [
|
||||
"scripts/vscode-extension-smoke.js",
|
||||
"scripts/vscode-f5-smoke.js"
|
||||
],
|
||||
engine: extensionPackage.engines && extensionPackage.engines.vscode,
|
||||
extension_version: extensionPackage.version
|
||||
},
|
||||
windows_validation: process.env.DISASMER_WINDOWS_VALIDATION || "not-run"
|
||||
};
|
||||
|
||||
const outDir = path.join(repo, "target/acceptance");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, `${mode}-environment.json`),
|
||||
`${JSON.stringify(report, null, 2)}\n`
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
95
scripts/agent-signing.js
Normal file
95
scripts/agent-signing.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
const crypto = require("crypto");
|
||||
|
||||
const { nodeIdentity, signedRequestPayloadDigest } = require("./node-signing");
|
||||
|
||||
function agentIdentity(seedPrefix, agent) {
|
||||
const identity = nodeIdentity(seedPrefix, agent);
|
||||
return {
|
||||
...identity,
|
||||
publicKeyFingerprint: `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(identity.publicKey)
|
||||
.digest("hex")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function agentWorkflowSignatureMessage({
|
||||
tenant,
|
||||
project,
|
||||
agent,
|
||||
requestKind,
|
||||
process: processId,
|
||||
task = "",
|
||||
payloadDigest,
|
||||
nonce,
|
||||
issuedAtEpochSeconds,
|
||||
}) {
|
||||
const parts = [
|
||||
"disasmer-agent-workflow-signature:v2",
|
||||
tenant,
|
||||
project,
|
||||
agent,
|
||||
requestKind,
|
||||
processId,
|
||||
task,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
String(issuedAtEpochSeconds),
|
||||
];
|
||||
return Buffer.concat(
|
||||
parts.flatMap((part) => [
|
||||
Buffer.from(`${Buffer.byteLength(part)}:`),
|
||||
Buffer.from(part),
|
||||
Buffer.from("\n"),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function signedAgentWorkflowProof(identity, request, options = {}) {
|
||||
const nonce =
|
||||
options.nonce ||
|
||||
`${request.type}-${process.pid}-${Date.now()}-${crypto
|
||||
.randomBytes(8)
|
||||
.toString("hex")}`;
|
||||
const issuedAtEpochSeconds =
|
||||
options.issuedAtEpochSeconds || Math.floor(Date.now() / 1000);
|
||||
const signature = crypto.sign(
|
||||
null,
|
||||
agentWorkflowSignatureMessage({
|
||||
tenant: request.tenant,
|
||||
project: request.project,
|
||||
agent: request.actor_agent,
|
||||
requestKind: request.type,
|
||||
process: request.process,
|
||||
task: request.task || "",
|
||||
payloadDigest: signedRequestPayloadDigest(request),
|
||||
nonce,
|
||||
issuedAtEpochSeconds,
|
||||
}),
|
||||
identity.privateKeyObject
|
||||
);
|
||||
return {
|
||||
nonce,
|
||||
issued_at_epoch_seconds: issuedAtEpochSeconds,
|
||||
signature: `ed25519:${signature.toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function signedAgentWorkflowRequest(identity, request, options = {}) {
|
||||
const unsignedRequest = {
|
||||
...request,
|
||||
agent_public_key_fingerprint:
|
||||
options.publicKeyFingerprint || identity.publicKeyFingerprint,
|
||||
};
|
||||
return {
|
||||
...unsignedRequest,
|
||||
agent_signature: signedAgentWorkflowProof(identity, unsignedRequest, options),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
agentIdentity,
|
||||
agentWorkflowSignatureMessage,
|
||||
signedAgentWorkflowProof,
|
||||
signedAgentWorkflowRequest,
|
||||
};
|
||||
401
scripts/artifact-download-smoke.js
Executable file
401
scripts/artifact-download-smoke.js
Executable file
|
|
@ -0,0 +1,401 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
const {
|
||||
ensureRootlessPodman,
|
||||
flagshipNodeCapabilities,
|
||||
launchFlagship,
|
||||
repo,
|
||||
runFlagshipWorker,
|
||||
send,
|
||||
waitForJsonLine,
|
||||
} = require("./real-flagship-harness");
|
||||
|
||||
const downloadNode = "node-download";
|
||||
const downloadNodeIdentity = nodeIdentity("artifact-download-smoke", downloadNode);
|
||||
|
||||
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
|
||||
async function downloadRetainedBytes(addr, link, artifact, expectedSize) {
|
||||
const chunks = [];
|
||||
let offset = 0;
|
||||
for (let attempt = 0; attempt < 500; attempt += 1) {
|
||||
const response = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
chunk_bytes: 256 * 1024,
|
||||
});
|
||||
assert.strictEqual(response.type, "artifact_download_stream");
|
||||
if (!response.content_bytes_available) {
|
||||
assert.strictEqual(response.content_source, "retaining_node_reverse_stream_pending");
|
||||
await delay(10);
|
||||
continue;
|
||||
}
|
||||
assert.strictEqual(response.content_source, "retaining_node_reverse_stream");
|
||||
assert.strictEqual(response.content_offset, offset);
|
||||
const bytes = Buffer.from(response.content_base64, "base64");
|
||||
assert.strictEqual(response.streamed_bytes, bytes.length);
|
||||
chunks.push(bytes);
|
||||
offset += bytes.length;
|
||||
if (response.content_eof) {
|
||||
const content = Buffer.concat(chunks);
|
||||
assert.strictEqual(content.length, expectedSize);
|
||||
return { content, response };
|
||||
}
|
||||
}
|
||||
throw new Error("timed out waiting for retained artifact reverse stream");
|
||||
}
|
||||
|
||||
function downloadNodeCapabilities() {
|
||||
return flagshipNodeCapabilities();
|
||||
}
|
||||
|
||||
(async () => {
|
||||
ensureRootlessPodman();
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback",
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
|
||||
let worker;
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
worker = await runFlagshipWorker(addr, downloadNode, downloadNodeIdentity);
|
||||
const workerReady = await worker.ready;
|
||||
assert.strictEqual(workerReady.node_status, "ready");
|
||||
assert.strictEqual(workerReady.mode, "worker");
|
||||
const { compileEvent, packageEvent, process: virtualProcess } = await launchFlagship(addr);
|
||||
assert.strictEqual(compileEvent.status_code, 0);
|
||||
assert.strictEqual(packageEvent.status_code, 0);
|
||||
assert.deepStrictEqual(compileEvent.result, {
|
||||
Artifact: {
|
||||
id: compileEvent.artifact_path.slice("/vfs/artifacts/".length),
|
||||
digest: compileEvent.artifact_digest,
|
||||
size_bytes: compileEvent.artifact_size_bytes,
|
||||
},
|
||||
});
|
||||
assert.deepStrictEqual(packageEvent.result, {
|
||||
Artifact: {
|
||||
id: packageEvent.artifact_path.slice("/vfs/artifacts/".length),
|
||||
digest: packageEvent.artifact_digest,
|
||||
size_bytes: packageEvent.artifact_size_bytes,
|
||||
},
|
||||
});
|
||||
assert.ok(compileEvent.artifact_size_bytes > 0);
|
||||
assert.ok(packageEvent.artifact_size_bytes > compileEvent.artifact_size_bytes);
|
||||
assert.match(compileEvent.artifact_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.match(packageEvent.artifact_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
const artifactPath = packageEvent.artifact_path;
|
||||
assert.match(artifactPath, /^\/vfs\/artifacts\/release\.tar-[0-9a-f]{64}$/);
|
||||
const artifact = artifactPath.slice("/vfs/artifacts/".length);
|
||||
|
||||
const disconnectedReport = await send(addr, signedNodeRequest(downloadNode, downloadNodeIdentity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: downloadNode,
|
||||
capabilities: downloadNodeCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [artifact],
|
||||
direct_connectivity: false,
|
||||
online: true,
|
||||
}));
|
||||
assert.strictEqual(disconnectedReport.type, "node_capabilities_recorded");
|
||||
|
||||
const disconnectedLink = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(disconnectedLink.type, "artifact_download_link");
|
||||
|
||||
const connectedReport = await send(addr, signedNodeRequest(downloadNode, downloadNodeIdentity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: downloadNode,
|
||||
capabilities: downloadNodeCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [artifact],
|
||||
direct_connectivity: true,
|
||||
online: true,
|
||||
}));
|
||||
assert.strictEqual(connectedReport.type, "node_capabilities_recorded");
|
||||
|
||||
const link = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(link.type, "artifact_download_link");
|
||||
assert.strictEqual(link.link.tenant, "tenant");
|
||||
assert.strictEqual(link.link.project, "project");
|
||||
assert.strictEqual(link.link.process, virtualProcess);
|
||||
assert.deepStrictEqual(link.link.actor, { User: "user" });
|
||||
assert.match(link.link.policy_context_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.ok(link.link.expires_at_epoch_seconds > Math.floor(Date.now() / 1000));
|
||||
assert.ok(link.link.expires_at_epoch_seconds <= Math.floor(Date.now() / 1000) + 60);
|
||||
assert.ok(
|
||||
link.link.url_path.endsWith(
|
||||
`/artifacts/tenant/project/${virtualProcess}/${artifact}`
|
||||
)
|
||||
);
|
||||
assert.deepStrictEqual(link.link.source, { RetainedNode: "node-download" });
|
||||
|
||||
const crossTenant = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "other",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(crossTenant.type, "error");
|
||||
assert.match(crossTenant.message, /tenant mismatch/);
|
||||
|
||||
const crossProject = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "other-project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(crossProject.type, "error");
|
||||
assert.match(crossProject.message, /project mismatch/);
|
||||
|
||||
const crossTenantOpen = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "other",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
chunk_bytes: 1,
|
||||
});
|
||||
assert.strictEqual(crossTenantOpen.type, "error");
|
||||
assert.match(crossTenantOpen.message, /tenant mismatch/);
|
||||
|
||||
const crossProjectOpen = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "tenant",
|
||||
project: "other-project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
chunk_bytes: 1,
|
||||
});
|
||||
assert.strictEqual(crossProjectOpen.type, "error");
|
||||
assert.match(crossProjectOpen.message, /project mismatch/);
|
||||
|
||||
const guessed = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: "sha256:guessed",
|
||||
chunk_bytes: 1,
|
||||
});
|
||||
assert.strictEqual(guessed.type, "error");
|
||||
assert.match(guessed.message, /token is invalid/);
|
||||
|
||||
const crossActorOpen = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "other-user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
chunk_bytes: 1,
|
||||
});
|
||||
assert.strictEqual(crossActorOpen.type, "error");
|
||||
assert.match(crossActorOpen.message, /token is invalid/);
|
||||
|
||||
const downloaded = await downloadRetainedBytes(
|
||||
addr,
|
||||
link,
|
||||
artifact,
|
||||
packageEvent.artifact_size_bytes,
|
||||
);
|
||||
const downloadedDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(downloaded.content)
|
||||
.digest("hex")}`;
|
||||
assert.strictEqual(downloadedDigest, packageEvent.artifact_digest);
|
||||
const inspect = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-release-"));
|
||||
try {
|
||||
const archive = path.join(inspect, "release.tar");
|
||||
fs.writeFileSync(archive, downloaded.content);
|
||||
const listing = cp.execFileSync("tar", ["-tf", archive], { encoding: "utf8" });
|
||||
assert.strictEqual(listing.trim(), "hello-disasmer");
|
||||
cp.execFileSync("tar", ["-xf", archive, "-C", inspect]);
|
||||
fs.chmodSync(path.join(inspect, "hello-disasmer"), 0o755);
|
||||
assert.strictEqual(
|
||||
cp.execFileSync(path.join(inspect, "hello-disasmer"), { encoding: "utf8" }),
|
||||
"hello from a real Disasmer build\n"
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(inspect, { recursive: true, force: true });
|
||||
}
|
||||
const cliDownloadDirectory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "disasmer-cli-download-")
|
||||
);
|
||||
try {
|
||||
const cliDownloadPath = path.join(cliDownloadDirectory, "release.tar");
|
||||
const cliDownload = JSON.parse(
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--",
|
||||
"artifact", "download", artifact,
|
||||
"--to", cliDownloadPath,
|
||||
"--max-bytes", "1048576",
|
||||
"--coordinator", `disasmer+tcp://${addr.host}:${addr.port}`,
|
||||
"--tenant", "tenant",
|
||||
"--project-id", "project",
|
||||
"--user", "user",
|
||||
"--json",
|
||||
],
|
||||
{ cwd: repo, env: process.env, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(cliDownload.command, "artifact download");
|
||||
assert.strictEqual(cliDownload.local_download.status, "local_bytes_written");
|
||||
assert.strictEqual(
|
||||
cliDownload.local_download.verified_digest,
|
||||
packageEvent.artifact_digest
|
||||
);
|
||||
assert.strictEqual(
|
||||
`sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(fs.readFileSync(cliDownloadPath))
|
||||
.digest("hex")}`,
|
||||
packageEvent.artifact_digest
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(cliDownloadDirectory, { recursive: true, force: true });
|
||||
}
|
||||
assert.strictEqual(downloaded.response.content_eof, true);
|
||||
assert.strictEqual(
|
||||
downloaded.response.charged_download_bytes,
|
||||
packageEvent.artifact_size_bytes
|
||||
);
|
||||
assert.strictEqual(downloaded.response.link.artifact, artifact);
|
||||
|
||||
const crossActorRevoke = await send(addr, {
|
||||
type: "revoke_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "other-user",
|
||||
artifact,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
});
|
||||
assert.strictEqual(crossActorRevoke.type, "error");
|
||||
assert.match(crossActorRevoke.message, /token is invalid/);
|
||||
|
||||
const revoked = await send(addr, {
|
||||
type: "revoke_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
});
|
||||
assert.strictEqual(revoked.type, "artifact_download_link_revoked");
|
||||
assert.strictEqual(revoked.link.scoped_token_digest, link.link.scoped_token_digest);
|
||||
|
||||
const revokedOpen = await send(addr, {
|
||||
type: "open_artifact_download_stream",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_digest: link.link.scoped_token_digest,
|
||||
chunk_bytes: 1,
|
||||
});
|
||||
assert.strictEqual(revokedOpen.type, "error");
|
||||
assert.match(revokedOpen.message, /revoked/);
|
||||
|
||||
const gcReport = await send(addr, signedNodeRequest(downloadNode, downloadNodeIdentity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: downloadNode,
|
||||
capabilities: downloadNodeCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: false,
|
||||
online: true,
|
||||
}));
|
||||
assert.strictEqual(gcReport.type, "node_capabilities_recorded");
|
||||
|
||||
const collectedLink = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(collectedLink.type, "error");
|
||||
assert.match(collectedLink.message, /unavailable from current retention/);
|
||||
} finally {
|
||||
worker?.child.kill("SIGTERM");
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Artifact download smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
262
scripts/artifact-export-smoke.js
Normal file
262
scripts/artifact-export-smoke.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
const {
|
||||
ensureRootlessPodman,
|
||||
flagshipNodeCapabilities,
|
||||
launchFlagship,
|
||||
repo,
|
||||
runFlagshipWorker,
|
||||
send,
|
||||
waitForJsonLine,
|
||||
} = require("./real-flagship-harness");
|
||||
|
||||
const sourceNode = "node-export-source";
|
||||
const sourceIdentity = nodeIdentity("artifact-export-smoke", sourceNode);
|
||||
|
||||
function nodeCapabilities() {
|
||||
return flagshipNodeCapabilities();
|
||||
}
|
||||
|
||||
function runJson(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(command, args, { cwd: repo, ...options });
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`${command} ${args.join(" ")} failed with code ${code}\n${stderr}\n${stdout}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch (error) {
|
||||
reject(new Error(`${command} did not return JSON\n${stdout}\n${error.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function reportNode(
|
||||
addr,
|
||||
node,
|
||||
identity,
|
||||
{ directConnectivity = true, online = true, artifacts = [] } = {}
|
||||
) {
|
||||
const response = await send(addr, signedNodeRequest(node, identity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node,
|
||||
capabilities: nodeCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: artifacts,
|
||||
direct_connectivity: directConnectivity,
|
||||
online,
|
||||
}));
|
||||
assert.strictEqual(response.type, "node_capabilities_recorded");
|
||||
assert.strictEqual(response.node, node);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
ensureRootlessPodman();
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback",
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
|
||||
let worker;
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
worker = await runFlagshipWorker(addr, sourceNode, sourceIdentity);
|
||||
const workerReady = await worker.ready;
|
||||
assert.strictEqual(workerReady.node_status, "ready");
|
||||
assert.strictEqual(workerReady.mode, "worker");
|
||||
const firstNodeTaskCompletion = waitForJsonLine(worker.child);
|
||||
const { compileEvent, process: virtualProcess } = await launchFlagship(addr);
|
||||
const workerCompletion = await firstNodeTaskCompletion;
|
||||
assert.strictEqual(workerCompletion.node_status, "completed");
|
||||
assert.strictEqual(
|
||||
workerCompletion.task_assignment_response.task_spec.task_definition,
|
||||
"prepare_source"
|
||||
);
|
||||
assert.strictEqual(
|
||||
workerCompletion.virtual_thread,
|
||||
workerCompletion.task_assignment_response.task_spec.task_instance
|
||||
);
|
||||
assert.strictEqual(compileEvent.status_code, 0);
|
||||
assert.match(
|
||||
compileEvent.artifact_path,
|
||||
/^\/vfs\/artifacts\/hello-disasmer-[0-9a-f]{64}$/
|
||||
);
|
||||
const artifact = compileEvent.artifact_path.slice("/vfs/artifacts/".length);
|
||||
|
||||
await reportNode(addr, sourceNode, sourceIdentity, {
|
||||
artifacts: [artifact],
|
||||
});
|
||||
|
||||
const receiverIdentity = nodeIdentity("artifact-export-smoke", "node-export-receiver");
|
||||
const attachedReceiver = await send(addr, {
|
||||
type: "attach_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "node-export-receiver",
|
||||
public_key: receiverIdentity.publicKey,
|
||||
});
|
||||
assert.strictEqual(attachedReceiver.type, "node_attached");
|
||||
await reportNode(addr, "node-export-receiver", receiverIdentity);
|
||||
|
||||
const exportPlan = await send(addr, {
|
||||
type: "export_artifact_to_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
receiver_node: "node-export-receiver",
|
||||
direct_connectivity: true,
|
||||
failure_reason: "",
|
||||
});
|
||||
assert.strictEqual(exportPlan.type, "artifact_export_plan");
|
||||
assert.strictEqual(exportPlan.source_node, "node-export-source");
|
||||
assert.strictEqual(exportPlan.receiver_node, "node-export-receiver");
|
||||
assert.strictEqual(exportPlan.plan.transport, "NativeQuic");
|
||||
assert.strictEqual(exportPlan.plan.scope.tenant, "tenant");
|
||||
assert.strictEqual(exportPlan.plan.scope.project, "project");
|
||||
assert.strictEqual(exportPlan.plan.scope.process, virtualProcess);
|
||||
assert.deepStrictEqual(exportPlan.plan.scope.object, { Artifact: artifact });
|
||||
assert.strictEqual(exportPlan.plan.source.node, "node-export-source");
|
||||
assert.strictEqual(exportPlan.plan.destination.node, "node-export-receiver");
|
||||
assert.strictEqual(exportPlan.plan.coordinator_assisted_rendezvous, true);
|
||||
assert.strictEqual(exportPlan.plan.coordinator_bulk_relay_allowed, false);
|
||||
assert.match(exportPlan.plan.authorization_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.strictEqual(exportPlan.artifact_size_bytes, compileEvent.artifact_size_bytes);
|
||||
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-artifact-export-"));
|
||||
const exportPath = path.join(temp, "hello-disasmer");
|
||||
const cliExport = await runJson("cargo", [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"artifact",
|
||||
"export",
|
||||
"--coordinator",
|
||||
`${addr.host}:${addr.port}`,
|
||||
"--tenant",
|
||||
"tenant",
|
||||
"--project-id",
|
||||
"project",
|
||||
"--user",
|
||||
"user",
|
||||
"--json",
|
||||
artifact,
|
||||
"--receiver-node",
|
||||
"node-export-receiver",
|
||||
"--to",
|
||||
exportPath,
|
||||
]);
|
||||
assert.strictEqual(cliExport.command, "artifact export");
|
||||
assert.strictEqual(cliExport.export_plan.local_bytes_written_by_cli, true);
|
||||
assert.strictEqual(cliExport.export_plan.local_export_status, "local_bytes_written");
|
||||
assert.strictEqual(
|
||||
cliExport.export_plan.bytes_written,
|
||||
compileEvent.artifact_size_bytes
|
||||
);
|
||||
assert.strictEqual(cliExport.local_export.stream.content_material_returned_in_report, false);
|
||||
assert.strictEqual(
|
||||
cliExport.local_export.verified_digest,
|
||||
compileEvent.artifact_digest
|
||||
);
|
||||
fs.chmodSync(exportPath, 0o755);
|
||||
assert.strictEqual(
|
||||
cp.execFileSync(exportPath, { encoding: "utf8" }),
|
||||
"hello from a real Disasmer build\n"
|
||||
);
|
||||
|
||||
const crossTenant = await send(addr, {
|
||||
type: "export_artifact_to_node",
|
||||
tenant: "other",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
receiver_node: "node-export-receiver",
|
||||
direct_connectivity: true,
|
||||
failure_reason: "",
|
||||
});
|
||||
assert.strictEqual(crossTenant.type, "error");
|
||||
assert.match(crossTenant.message, /tenant mismatch/);
|
||||
|
||||
const failedDirect = await send(addr, {
|
||||
type: "export_artifact_to_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
receiver_node: "node-export-receiver",
|
||||
direct_connectivity: false,
|
||||
failure_reason: "nat traversal failed",
|
||||
});
|
||||
assert.strictEqual(failedDirect.type, "error");
|
||||
assert.match(failedDirect.message, /nat traversal failed/);
|
||||
assert.match(failedDirect.message, /coordinator bulk relay is disabled/);
|
||||
|
||||
await reportNode(addr, "node-export-receiver", receiverIdentity, { online: false });
|
||||
const offlineReceiver = await send(addr, {
|
||||
type: "export_artifact_to_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact,
|
||||
receiver_node: "node-export-receiver",
|
||||
direct_connectivity: true,
|
||||
failure_reason: "",
|
||||
});
|
||||
assert.strictEqual(offlineReceiver.type, "error");
|
||||
assert.match(offlineReceiver.message, /offline/);
|
||||
} finally {
|
||||
worker?.child.kill("SIGTERM");
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Artifact export smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
223
scripts/cli-browser-login-flow-smoke.js
Normal file
223
scripts/cli-browser-login-flow-smoke.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow");
|
||||
const project = path.join(tmp, "project");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
|
||||
function writeOpener() {
|
||||
const opener = path.join(tmp, "browser-opener.js");
|
||||
const trace = path.join(tmp, "browser-opener.log");
|
||||
fs.writeFileSync(
|
||||
opener,
|
||||
`#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const trace = ${JSON.stringify(trace)};
|
||||
const loginUrl = new URL(process.argv[2]);
|
||||
const state = loginUrl.searchParams.get("state");
|
||||
const nonce = loginUrl.searchParams.get("nonce");
|
||||
const challenge = loginUrl.searchParams.get("code_challenge");
|
||||
if (loginUrl.protocol !== "https:" || !state || !nonce || !challenge) {
|
||||
fs.appendFileSync(trace, "missing server-owned OIDC parameters\\n");
|
||||
process.exit(1);
|
||||
}
|
||||
fs.appendFileSync(trace, "server-owned browser transaction\\n");
|
||||
// Model a real browser/opener that remains alive after the CLI transaction.
|
||||
// Its descriptors must not keep the invoking CLI process open.
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
`
|
||||
);
|
||||
fs.chmodSync(opener, 0o755);
|
||||
return opener;
|
||||
}
|
||||
|
||||
function startCoordinator() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requests = [];
|
||||
const server = net.createServer((socket) => {
|
||||
let buffered = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffered += chunk.toString("utf8");
|
||||
while (buffered.includes("\n")) {
|
||||
const newline = buffered.indexOf("\n");
|
||||
const line = buffered.slice(0, newline);
|
||||
buffered = buffered.slice(newline + 1);
|
||||
const envelope = JSON.parse(line);
|
||||
requests.push(envelope);
|
||||
assert.strictEqual(envelope.type, "coordinator_request");
|
||||
assert.strictEqual(envelope.protocol_version, 1);
|
||||
assert.strictEqual(envelope.authentication.kind, "none");
|
||||
const request = envelope.payload;
|
||||
|
||||
if (requests.length === 1) {
|
||||
assert.strictEqual(envelope.request_id, "cli-1");
|
||||
assert.strictEqual(envelope.operation, "begin_oidc_browser_login");
|
||||
assert.deepStrictEqual(request, {
|
||||
type: "begin_oidc_browser_login",
|
||||
requested_project: "project-smoke",
|
||||
});
|
||||
socket.write(
|
||||
`${JSON.stringify({
|
||||
type: "oidc_browser_login_started",
|
||||
transaction_id: "login-transaction",
|
||||
polling_secret: "opaque-polling-secret",
|
||||
authorization_url:
|
||||
"https://auth.michelpaulissen.com/application/o/authorize/?state=server-state&nonce=server-nonce&code_challenge=server-pkce&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fdisasmer.michelpaulissen.com%2Fauth%2Fcallback",
|
||||
expires_at_epoch_seconds: 1800000000,
|
||||
})}\n`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
assert.strictEqual(envelope.request_id, "cli-2");
|
||||
assert.strictEqual(envelope.operation, "poll_oidc_browser_login");
|
||||
assert.deepStrictEqual(request, {
|
||||
type: "poll_oidc_browser_login",
|
||||
transaction_id: "login-transaction",
|
||||
polling_secret: "opaque-polling-secret",
|
||||
});
|
||||
socket.end(
|
||||
`${JSON.stringify({
|
||||
type: "oidc_browser_session",
|
||||
session: {
|
||||
tenant: "tenant-smoke",
|
||||
project: "project-smoke",
|
||||
user: "user-smoke",
|
||||
cli_session_credential_kind: "CliDeviceSession",
|
||||
cli_session_secret: "scoped-cli-session-secret",
|
||||
expires_at_epoch_seconds: 1800000000,
|
||||
provider_tokens_sent_to_nodes: false,
|
||||
},
|
||||
})}\n`
|
||||
);
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
resolve({
|
||||
url: `${address.address}:${address.port}`,
|
||||
requests,
|
||||
close: () =>
|
||||
new Promise((closeResolve) => {
|
||||
if (!server.listening) closeResolve();
|
||||
else server.close(() => closeResolve());
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runDisasmer(args, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"--manifest-path",
|
||||
path.join(repo, "Cargo.toml"),
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
...args,
|
||||
],
|
||||
{ cwd: project, env, stdio: ["ignore", "pipe", "pipe"] }
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
|
||||
child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8")));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
if (code === 0) resolve(stdout);
|
||||
else reject(new Error(`disasmer exited ${code}\n${stderr}\n${stdout}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const opener = writeOpener();
|
||||
const coordinator = await startCoordinator();
|
||||
try {
|
||||
const loginStarted = Date.now();
|
||||
const report = JSON.parse(
|
||||
await runDisasmer(
|
||||
[
|
||||
"login",
|
||||
"--browser",
|
||||
"--json",
|
||||
"--coordinator",
|
||||
coordinator.url,
|
||||
"--project-id",
|
||||
"project-smoke",
|
||||
],
|
||||
{
|
||||
...process.env,
|
||||
DISASMER_BROWSER_OPEN_COMMAND: opener,
|
||||
DISASMER_BROWSER_LOGIN_TIMEOUT_SECONDS: "5",
|
||||
}
|
||||
)
|
||||
);
|
||||
assert(
|
||||
Date.now() - loginStarted < 3000,
|
||||
"a long-lived browser opener must not keep CLI output pipes or login completion open"
|
||||
);
|
||||
assert.strictEqual(report.plan.coordinator, coordinator.url);
|
||||
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
|
||||
assert.strictEqual(report.boundary.scoped_cli_session_received, true);
|
||||
assert.strictEqual(report.boundary.local_cli_session_file_written, true);
|
||||
assert.strictEqual(report.boundary.provider_tokens_persisted_locally, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false);
|
||||
assert.strictEqual(report.boundary.coordinator_session_requests, 2);
|
||||
assert.strictEqual(coordinator.requests.length, 2);
|
||||
|
||||
const sessionFile = path.join(project, ".disasmer", "session.json");
|
||||
const sessionText = fs.readFileSync(sessionFile, "utf8");
|
||||
const session = JSON.parse(sessionText);
|
||||
assert.strictEqual(session.kind, "human");
|
||||
assert.strictEqual(session.coordinator, coordinator.url);
|
||||
assert.strictEqual(session.tenant, "tenant-smoke");
|
||||
assert.strictEqual(session.project, "project-smoke");
|
||||
assert.strictEqual(session.user, "user-smoke");
|
||||
assert.strictEqual(session.cli_session_credential_kind, "CliDeviceSession");
|
||||
assert.strictEqual(session.token_expiry_posture, "expires_at");
|
||||
assert.strictEqual(session.expires_at, "1800000000");
|
||||
assert.strictEqual(session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(session.provider_tokens_sent_to_nodes, false);
|
||||
assert.doesNotMatch(
|
||||
sessionText,
|
||||
/access_token|refresh_token|id_token|provider-secret|authorization_code|Bearer/
|
||||
);
|
||||
|
||||
const authStatus = JSON.parse(
|
||||
await runDisasmer(["auth", "status", "--json"], process.env)
|
||||
);
|
||||
assert.strictEqual(authStatus.active_coordinator, coordinator.url);
|
||||
assert.strictEqual(authStatus.principal, "user-smoke");
|
||||
assert.strictEqual(authStatus.tenant, "tenant-smoke");
|
||||
assert.strictEqual(authStatus.project, "project-smoke");
|
||||
assert.strictEqual(authStatus.session.kind, "human");
|
||||
assert.strictEqual(authStatus.session.source, "session_file");
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_nodes, false);
|
||||
} finally {
|
||||
await coordinator.close();
|
||||
}
|
||||
console.log("CLI browser login flow smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
365
scripts/cli-error-exit-smoke.js
Executable file
365
scripts/cli-error-exit-smoke.js
Executable file
|
|
@ -0,0 +1,365 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
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) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
|
||||
{
|
||||
cwd: repo,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
resolve({ code, signal, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runWithOneCoordinatorResponse(buildArgs, response) {
|
||||
let request = "";
|
||||
const server = http.createServer((incoming, outgoing) => {
|
||||
incoming.setEncoding("utf8");
|
||||
incoming.on("data", (chunk) => {
|
||||
request += chunk;
|
||||
});
|
||||
incoming.on("end", () => {
|
||||
outgoing.writeHead(200, { "content-type": "application/json" });
|
||||
outgoing.end(JSON.stringify(response));
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
|
||||
const address = await new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => resolve(server.address()));
|
||||
});
|
||||
const coordinator = `http://${address.address}:${address.port}`;
|
||||
const result = await runDisasmer(buildArgs(coordinator));
|
||||
return { request, result };
|
||||
}
|
||||
|
||||
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",
|
||||
"--project",
|
||||
project,
|
||||
"--non-interactive",
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(nonInteractive.signal, null, nonInteractive.stderr);
|
||||
assert.strictEqual(nonInteractive.code, 20, nonInteractive.stderr);
|
||||
assert.doesNotMatch(nonInteractive.stderr, /Opening Disasmer browser login/);
|
||||
const nonInteractiveReport = JSON.parse(nonInteractive.stdout);
|
||||
assert.strictEqual(nonInteractiveReport.status, "authentication_required");
|
||||
assert.strictEqual(nonInteractiveReport.non_interactive, true);
|
||||
assert.strictEqual(nonInteractiveReport.browser_opened, false);
|
||||
assert.strictEqual(nonInteractiveReport.machine_error.category, "authentication");
|
||||
assert.strictEqual(nonInteractiveReport.machine_error.stable_exit_code, 20);
|
||||
assert.strictEqual(
|
||||
nonInteractiveReport.machine_error.process_exit_code_applied,
|
||||
true
|
||||
);
|
||||
assert(
|
||||
nonInteractiveReport.machine_error.next_actions.includes(
|
||||
"pass --local to run against local services"
|
||||
)
|
||||
);
|
||||
|
||||
let request = "";
|
||||
const server = http.createServer((incoming, outgoing) => {
|
||||
incoming.setEncoding("utf8");
|
||||
incoming.on("data", (chunk) => {
|
||||
request += chunk;
|
||||
});
|
||||
incoming.on("end", () => {
|
||||
outgoing.writeHead(200, { "content-type": "application/json" });
|
||||
outgoing.end(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "quota unavailable: resource limit exceeded for api_calls",
|
||||
})
|
||||
);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
|
||||
const address = await new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => resolve(server.address()));
|
||||
});
|
||||
const coordinator = `http://${address.address}:${address.port}`;
|
||||
|
||||
const result = await runDisasmer([
|
||||
"run",
|
||||
"build",
|
||||
"--project",
|
||||
project,
|
||||
"--coordinator",
|
||||
coordinator,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
assert.strictEqual(result.signal, null, result.stderr);
|
||||
assert.strictEqual(result.code, 22, result.stderr);
|
||||
assert.match(request, /"type":"start_process"/);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.strictEqual(report.status, "coordinator_rejected");
|
||||
assert.strictEqual(report.run_start.machine_error.category, "quota");
|
||||
assert.strictEqual(report.run_start.machine_error.resource_category, "api_calls");
|
||||
assert.strictEqual(report.run_start.machine_error.community_tier_language, true);
|
||||
assert.strictEqual(
|
||||
report.run_start.machine_error.community_tier_label,
|
||||
"community tier"
|
||||
);
|
||||
assert.doesNotMatch(result.stdout, new RegExp(["free", "tier"].join(" "), "i"));
|
||||
assert.strictEqual(
|
||||
report.run_start.machine_error.private_abuse_heuristics_exposed,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(report.run_start.machine_error.stable_exit_code, 22);
|
||||
assert.strictEqual(
|
||||
report.run_start.machine_error.process_exit_code_applied,
|
||||
true
|
||||
);
|
||||
assert(
|
||||
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",
|
||||
"download",
|
||||
"app.txt",
|
||||
"--coordinator",
|
||||
coordinator,
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
type: "artifact_download_denied",
|
||||
message: "artifact download unauthorized for project",
|
||||
}
|
||||
);
|
||||
assert.strictEqual(artifactDownload.result.signal, null, artifactDownload.result.stderr);
|
||||
assert.strictEqual(artifactDownload.result.code, 21, artifactDownload.result.stderr);
|
||||
assert.match(artifactDownload.request, /"type":"create_artifact_download_link"/);
|
||||
const artifactDownloadReport = JSON.parse(artifactDownload.result.stdout);
|
||||
assert.strictEqual(
|
||||
artifactDownloadReport.download_session.machine_error.category,
|
||||
"authorization"
|
||||
);
|
||||
assert.strictEqual(
|
||||
artifactDownloadReport.download_session.machine_error.process_exit_code_applied,
|
||||
true
|
||||
);
|
||||
|
||||
const artifactExport = await runWithOneCoordinatorResponse(
|
||||
(coordinator) => [
|
||||
"artifact",
|
||||
"export",
|
||||
"app.txt",
|
||||
"--to",
|
||||
path.join(project, "target", "blocked-artifact.txt"),
|
||||
"--coordinator",
|
||||
coordinator,
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
type: "artifact_export_unavailable",
|
||||
message: "direct connectivity unavailable for artifact export",
|
||||
}
|
||||
);
|
||||
assert.strictEqual(artifactExport.result.signal, null, artifactExport.result.stderr);
|
||||
assert.strictEqual(artifactExport.result.code, 25, artifactExport.result.stderr);
|
||||
assert.match(artifactExport.request, /"type":"export_artifact_to_node"/);
|
||||
const artifactExportReport = JSON.parse(artifactExport.result.stdout);
|
||||
assert.strictEqual(
|
||||
artifactExportReport.export_plan.machine_error.category,
|
||||
"connectivity"
|
||||
);
|
||||
assert.strictEqual(
|
||||
artifactExportReport.export_plan.machine_error.process_exit_code_applied,
|
||||
true
|
||||
);
|
||||
|
||||
const confirmation = await runDisasmer([
|
||||
"process",
|
||||
"cancel",
|
||||
"--coordinator",
|
||||
"127.0.0.1:9",
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(confirmation.signal, null, confirmation.stderr);
|
||||
assert.strictEqual(confirmation.code, 23, confirmation.stderr);
|
||||
const confirmationReport = JSON.parse(confirmation.stdout);
|
||||
assert.strictEqual(confirmationReport.status, "confirmation_required");
|
||||
assert.strictEqual(confirmationReport.coordinator_request_sent, false);
|
||||
assert.strictEqual(confirmationReport.machine_error.category, "policy");
|
||||
assert.strictEqual(
|
||||
confirmationReport.machine_error.process_exit_code_applied,
|
||||
true
|
||||
);
|
||||
assert(
|
||||
confirmationReport.next_actions.some((action) => action.includes("--yes"))
|
||||
);
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log("CLI error exit smoke passed");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
773
scripts/cli-first-contract-smoke.js
Normal file
773
scripts/cli-first-contract-smoke.js
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
const fullPath = path.join(repo, relativePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
if (fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json"))) {
|
||||
console.log(
|
||||
`CLI-first contract smoke skipped: ${relativePath} is filtered from this public tree`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`${relativePath} is missing`);
|
||||
}
|
||||
return fs.readFileSync(fullPath, "utf8");
|
||||
}
|
||||
|
||||
function readRustTree(relativePath) {
|
||||
const root = path.join(repo, relativePath);
|
||||
const sourcePaths = [];
|
||||
|
||||
function visit(directory) {
|
||||
for (const entry of fs
|
||||
.readdirSync(directory, { withFileTypes: true })
|
||||
.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
visit(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".rs")) {
|
||||
sourcePaths.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(root);
|
||||
const priority = (filePath) => {
|
||||
const name = path.basename(filePath);
|
||||
if (name === "main.rs" || name === "service.rs") return 0;
|
||||
if (name === "lib.rs") return 1;
|
||||
if (name === "protocol.rs") return 2;
|
||||
if (name === "tests.rs") return 100;
|
||||
return 10;
|
||||
};
|
||||
sourcePaths.sort(
|
||||
(left, right) =>
|
||||
priority(left) - priority(right) || left.localeCompare(right)
|
||||
);
|
||||
return sourcePaths
|
||||
.map((filePath) => fs.readFileSync(filePath, "utf8"))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function criterionLines(source) {
|
||||
return source
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => /^- \[[ x]\] \*\*/.test(line));
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing CLI-first contract evidence: ${name}`);
|
||||
}
|
||||
|
||||
const criteria = read("cli_acceptance_criteria.md");
|
||||
// Phase 3 deliberately split the former CLI and coordinator mega-files into
|
||||
// focused modules. Contract evidence therefore follows the complete source
|
||||
// trees instead of assuming every implementation and test still lives in one
|
||||
// entrypoint file.
|
||||
const cli = readRustTree("crates/disasmer-cli/src");
|
||||
const coordinator = readRustTree("crates/disasmer-coordinator/src");
|
||||
const coordinatorLib = read("crates/disasmer-coordinator/src/lib.rs");
|
||||
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
||||
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
||||
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
||||
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
||||
const nodeAttachSmoke = read("scripts/node-attach-smoke.js");
|
||||
const dapSmoke = read("scripts/dap-smoke.js");
|
||||
const cliHappyPathSmoke = read("scripts/cli-happy-path-live-smoke.js");
|
||||
|
||||
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
|
||||
expect(
|
||||
criteria,
|
||||
"addendum status",
|
||||
/\*\*Status:\*\* CLI-first addendum to `acceptance_criteria\.md` and `acceptance_criteria_phase2\.md`/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"website exception",
|
||||
/hosted account creation as the only intentional private website exception/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"no duplicate work note",
|
||||
/does not automatically mean new product code, a new feature, or even actual implementation work is required/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"future hosted business non-goal",
|
||||
/billing, paid-plan checkout, team\/org management, provider setup wizards, secret-manager UI, full support tooling, broad moderation consoles, and durable account\/business-process management are intentionally outside this CLI-first MVP slice/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"billing plan flags are placeholders",
|
||||
/Design-document references to billing, paid plans, or plan flags are future metadata placeholders; they do not require MVP CLI commands, coordinator routes, schemas, migrations, website controls, or service logic/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted signup remains private Authentik-backed flow",
|
||||
/Hosted account creation is available only through the private hosted identity flow, backed by Authentik[\s\S]*first-login account\/project creation stays inside/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted signup provider and password policy criteria",
|
||||
/approved external identity-provider requirement[\s\S]*disasmer_native_password_signup_allowed: false/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted provider normalization criteria",
|
||||
/Google sign-in may be treated as OIDC; GitHub sign-in is OAuth2 social login[\s\S]*normalizes Google as `google_oidc` and GitHub as `github_oauth2` from an Authentik-issued OIDC session[\s\S]*does not consume provider-specific tokens directly/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted CLI cannot create accounts directly",
|
||||
/The CLI cannot create a hosted account directly[\s\S]*parser coverage rejects `disasmer signup`, `disasmer account create`, and `disasmer login --create-account`/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted safe claim failure criteria",
|
||||
/If identity is ambiguous, missing required claims, unverified where required, or blocked by policy[\s\S]*exactly one approved external identity provider[\s\S]*stable subject[\s\S]*verified email/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted signup sanitized failure criteria",
|
||||
/Signup failures do not disclose moderation, abuse-scoring, or allow\/deny internals[\s\S]*stripping issuer, allowlist, abuse-score, moderation-note, and provider-secret details/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"hosted account state privacy criteria",
|
||||
/Account suspension, deletion, manual review, and abuse handling are private hosted-admin concerns[\s\S]*public CLI displays clear suspended, disabled, deleted, and manual-review status[\s\S]*strips private moderation\/signup details/
|
||||
);
|
||||
|
||||
const lines = criterionLines(criteria);
|
||||
assert(lines.length > 0, "CLI-first criteria must contain criteria lines");
|
||||
for (const line of lines) {
|
||||
assert.match(
|
||||
line,
|
||||
/^- \[[ x]\] \*\*(Passed|Partial|Open|Postponed)(?: \([^)]+\))?:\*\*/,
|
||||
`CLI-first criterion lacks an explicit status prefix: ${line}`
|
||||
);
|
||||
}
|
||||
|
||||
const openCriteria = lines.filter((line) => /\*\*Open(?::| \()/.test(line));
|
||||
assert.deepStrictEqual(
|
||||
openCriteria,
|
||||
[],
|
||||
"CLI-first criteria should not leave Open items once the non-e2e gate exists; remaining unfinished facts stay Partial"
|
||||
);
|
||||
|
||||
expect(
|
||||
criteria,
|
||||
"CLI-first non-e2e gate wording",
|
||||
/scripts\/acceptance-cli-first\.sh[\s\S]*scripts\/cli-happy-path-live-smoke\.js[\s\S]*live hosted coordinator/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first gate can run live happy path when explicitly enabled",
|
||||
/DISASMER_CLI_HAPPY_PATH_LIVE[\s\S]*cli-happy-path-live-smoke\.js/
|
||||
);
|
||||
for (const [name, pattern] of [
|
||||
["happy path uses released binaries", /disasmer-public-binaries-/],
|
||||
["happy path completes server-owned browser login", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND[\s\S]*DISASMER_BROWSER_OPEN_COMMAND/],
|
||||
["happy path initializes project", /"project"[\s\S]*"init"/],
|
||||
["happy path inspects project", /"inspect"[\s\S]*"--project"/],
|
||||
["happy path enrolls node", /"node"[\s\S]*"enroll"/],
|
||||
["happy path attaches node", /"node"[\s\S]*"attach"/],
|
||||
["happy path starts worker", /--worker/],
|
||||
["happy path runs build", /"run"[\s\S]*"build"/],
|
||||
["happy path checks logs", /"logs"/],
|
||||
["happy path checks artifacts", /"artifact"[\s\S]*"list"/],
|
||||
["happy path downloads artifact", /"artifact"[\s\S]*"download"/],
|
||||
["happy path restarts process", /"process"[\s\S]*"restart"/],
|
||||
["happy path cancels process", /"process"[\s\S]*"cancel"/],
|
||||
["happy path writes evidence", /cli-happy-path-live\.json/],
|
||||
]) {
|
||||
expect(cliHappyPathSmoke, name, pattern);
|
||||
}
|
||||
expect(
|
||||
criteria,
|
||||
"artifact export explicit local byte write",
|
||||
/artifact export <id> --to <path>` writes bytes[\s\S]*explicit bounded download stream[\s\S]*complete staged content is available/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"artifact download grant disclosure criteria",
|
||||
/artifact download <id>` creates a secure[\s\S]*explicit artifact-download grant disclosure[\s\S]*Download links or sessions are not guessable public URLs[\s\S]*guessable_public_url: false[\s\S]*cross-tenant no-reuse[\s\S]*unauthorized-project no-reuse[\s\S]*Every command that grants[\s\S]*artifact download ability[\s\S]*grant_disclosures/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"locality failure safe guidance",
|
||||
/If direct transfer or locality assumptions fail[\s\S]*connectivity-category safe failures[\s\S]*coordinator bulk relay was not used/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"mutating commands require confirmation",
|
||||
/Mutating or dangerous commands support `--yes`[\s\S]*confirmation-required safe failure[\s\S]*do not send coordinator requests/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"log redaction criteria",
|
||||
/Logs are capped, truncated honestly[\s\S]*preserve byte counts and truncation flags[\s\S]*Secret-like values are redacted[\s\S]*common token\/password\/bearer patterns/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"doctor node readiness criteria",
|
||||
/`disasmer doctor` reports missing local dependencies[\s\S]*explicit node readiness summary[\s\S]*missing local dependencies[\s\S]*node next actions/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"admin bootstrap self-hosted sequence criteria",
|
||||
/admin bootstrap now reports a CLI-only self-hosted sequence[\s\S]*node enrollment\/attach[\s\S]*quota status[\s\S]*node revoke/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"quota resource-category criteria",
|
||||
/Hitting a quota produces a clear error[\s\S]*resource category[\s\S]*private abuse heuristics[\s\S]*quota machine errors now extract the resource category/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"quota before work criteria",
|
||||
/Quotas are checked before expensive work starts[\s\S]*workflow spawns now charge `Spawn` before coordinator-side process\/task state is created or queued[\s\S]*debug, artifact-download, and rendezvous metering/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"community tier CLI wording criteria",
|
||||
/Community tier language is used instead of "free[ -]tier" in user-facing CLI output/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"DAP launch-critical surface criteria",
|
||||
/launch-critical DAP surface required by the MVP[\s\S]*`initialize`[\s\S]*`source`[\s\S]*`next`\/step-over[\s\S]*`restartFrame`/
|
||||
);
|
||||
expect(
|
||||
criteria,
|
||||
"DAP variables experience criteria",
|
||||
/normal debugger variables path exposes the MVP debugging experience[\s\S]*selected source locals around Disasmer API calls[\s\S]*runtime-captured Wasmtime frame locals/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance report",
|
||||
/node scripts\/acceptance-report\.js cli-first/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance refuses final e2e",
|
||||
/DISASMER_PUBLIC_RELEASE_DRYRUN_E2E[\s\S]*does not run final public-release e2e/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance refuses final evidence",
|
||||
/DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL[\s\S]*does not run final public-release evidence/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance composes public API contracts",
|
||||
/node scripts\/cli-output-mode-smoke\.js[\s\S]*node scripts\/cli-login-smoke\.js[\s\S]*node scripts\/cli-error-exit-smoke\.js[\s\S]*node scripts\/cli-browser-login-flow-smoke\.js/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance composes service boundary checks",
|
||||
/node scripts\/wasmtime-assignment-smoke\.js[\s\S]*node scripts\/cli-local-run-smoke\.js/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance composes self-hosted checks",
|
||||
/node scripts\/self-hosted-coordinator-smoke\.js/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance composes debug and artifact checks",
|
||||
/node scripts\/vscode-f5-smoke\.js[\s\S]*node scripts\/artifact-download-smoke\.js[\s\S]*node scripts\/artifact-export-smoke\.js/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance composes DAP checks",
|
||||
/node scripts\/dap-smoke\.js/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first acceptance can include private hosted gate",
|
||||
/DISASMER_CLI_FIRST_INCLUDE_PRIVATE[\s\S]*scripts\/acceptance-private\.sh/
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
cliFirstAcceptance,
|
||||
/node scripts\/public-release-dryrun-e2e\.js|node scripts\/public-release-dryrun-final-evidence\.js/,
|
||||
"CLI-first non-e2e gate must not invoke final public release e2e or final evidence verifier"
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["top-level version metadata", /#\[command\([\s\S]*name = "disasmer"[\s\S]*version[\s\S]*arg_required_else_help = true[\s\S]*\)\]/],
|
||||
["top-level primary workflow help", /after_help = "Primary workflow:[\s\S]*disasmer login --browser[\s\S]*disasmer node attach; disasmer-node --worker[\s\S]*Disasmer: Launch Virtual Process[\s\S]*Hosted account creation happens in the browser login flow\."/],
|
||||
["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/],
|
||||
["login non-interactive flag", /struct LoginArgs[\s\S]*non_interactive: bool/],
|
||||
["run non-interactive flag", /struct RunArgs[\s\S]*non_interactive: bool/],
|
||||
["stored CLI session model", /struct StoredCliSession[\s\S]*cli_session_credential_kind[\s\S]*provider_tokens_exposed_to_cli[\s\S]*provider_tokens_sent_to_nodes/],
|
||||
["read CLI session helper", /fn read_cli_session\(project: &Path\) -> Result<Option<StoredCliSession>>/],
|
||||
["write CLI session helper", /fn write_cli_session\(project: &Path, session: &StoredCliSession\) -> Result<PathBuf>/],
|
||||
["session source fallback", /fn session_from_sources\(project: &Path\) -> Result<CliSession>[\s\S]*read_cli_session\(project\)\?\.is_some\(\)/],
|
||||
["browser login writes local CLI session", /local_cli_session_file_written[\s\S]*write_cli_session\(&cwd, &stored_session\)\?/],
|
||||
["browser login does not persist provider tokens", /provider_tokens_persisted_locally:\s*false/],
|
||||
["non-interactive auth report", /fn non_interactive_auth_machine_error[\s\S]*browser_opened[\s\S]*false/],
|
||||
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
||||
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
||||
["doctor command", /Doctor\(DoctorArgs\)/],
|
||||
["doctor node readiness summary", /fn node_readiness_summary[\s\S]*ready_to_attach[\s\S]*explicit_attach_required[\s\S]*missing_local_dependencies[\s\S]*next_actions/],
|
||||
["auth status command", /enum AuthCommands[\s\S]*Status\(AuthStatusArgs\)/],
|
||||
["auth logout command", /enum AuthCommands[\s\S]*Logout\(AuthLogoutArgs\)/],
|
||||
["key lifecycle commands", /enum KeyCommands[\s\S]*Add\(KeyAddArgs\)[\s\S]*List\(KeyListArgs\)[\s\S]*Revoke\(KeyRevokeArgs\)/],
|
||||
["project commands", /enum ProjectCommands[\s\S]*Init\(ProjectInitArgs\)[\s\S]*Status\(ProjectStatusArgs\)[\s\S]*List\(ProjectListArgs\)[\s\S]*Select\(ProjectSelectArgs\)/],
|
||||
["inspect command", /Inspect\(BundleInspectArgs\)/],
|
||||
["build command", /Build\(BuildArgs\)/],
|
||||
["node lifecycle commands", /enum NodeCommands[\s\S]*Attach\(AttachArgs\)[\s\S]*Enroll\(NodeEnrollArgs\)[\s\S]*List\(NodeListArgs\)[\s\S]*Status\(NodeStatusArgs\)[\s\S]*Revoke\(NodeRevokeArgs\)/],
|
||||
["process commands", /enum ProcessCommands[\s\S]*Status\(ProcessStatusArgs\)[\s\S]*Restart\(ProcessRestartArgs\)[\s\S]*Cancel\(ProcessCancelArgs\)/],
|
||||
["task lifecycle commands", /enum TaskCommands[\s\S]*List\(TaskListArgs\)[\s\S]*Restart\(TaskRestartArgs\)/],
|
||||
["logs command", /Logs\(LogsArgs\)/],
|
||||
["artifact commands", /enum ArtifactCommands[\s\S]*List\(ArtifactListArgs\)[\s\S]*Download\(ArtifactDownloadArgs\)[\s\S]*Export\(ArtifactExportArgs\)/],
|
||||
["DAP command", /Dap\(DapArgs\)/],
|
||||
["debug attach command", /enum DebugCommands[\s\S]*Attach\(DebugAttachArgs\)/],
|
||||
["quota command", /enum QuotaCommands[\s\S]*Status\(QuotaStatusArgs\)/],
|
||||
["admin commands", /enum AdminCommands[\s\S]*Status\(AdminStatusArgs\)[\s\S]*Bootstrap\(AdminBootstrapArgs\)[\s\S]*RevokeNode\(NodeRevokeArgs\)[\s\S]*StopProcess\(ProcessCancelArgs\)[\s\S]*SuspendTenant\(AdminSuspendTenantArgs\)/],
|
||||
]) {
|
||||
expect(cli, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["CLI parse coverage", /fn cli_first_mvp_command_surface_parses\(\)/],
|
||||
["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/],
|
||||
["CLI non-interactive run auth coverage", /fn non_interactive_run_without_session_requires_explicit_auth_or_local\(\)/],
|
||||
["CLI non-interactive browser login coverage", /fn browser_login_non_interactive_fails_before_opening_browser\(\)/],
|
||||
["CLI stored browser session coverage", /fn stored_browser_login_session_omits_provider_token_values\(\)/],
|
||||
["CLI auth status session-file coverage", /fn auth_status_reads_stored_cli_session_without_provider_tokens\(\)/],
|
||||
["CLI auth status account privacy coverage", /fn auth_status_queries_coordinator_account_state_without_private_moderation_details\(\)/],
|
||||
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
||||
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
||||
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
||||
["CLI key lifecycle coverage", /fn key_lifecycle_reports_project_scoped_agent_credentials\(\)/],
|
||||
["CLI agent workflow actor coverage", /fn run_with_agent_public_key_sends_attributable_workflow_actor\(\)/],
|
||||
["CLI node revoke coverage", /fn node_revoke_reports_scoped_credential_revocation\(\)/],
|
||||
["CLI admin public API coverage", /fn admin_status_and_suspend_use_public_coordinator_api\(\)/],
|
||||
["CLI admin bootstrap coverage", /fn admin_bootstrap_reports_self_hosted_cli_only_path\(\)/],
|
||||
["CLI debug attach coverage", /fn debug_attach_reports_public_authorization\(\)/],
|
||||
["doctor unchecked reachability coverage", /fn doctor_reports_unchecked_coordinator_reachability_without_config\(\)/],
|
||||
["doctor ping reachability coverage", /fn doctor_pings_configured_coordinator\(\)/],
|
||||
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
||||
["project init public create coverage", /fn project_init_uses_public_create_before_writing_local_config\(\)/],
|
||||
["project public API list/select coverage", /fn project_list_and_select_use_public_api_without_website\(\)/],
|
||||
["project coordinator status coverage", /fn project_status_queries_public_coordinator_state\(\)/],
|
||||
["run coordinator active-process coverage", /fn run_contacts_configured_coordinator_and_reports_active_process_conflicts\(\)/],
|
||||
["CLI error classifier coverage", /fn cli_error_classifier_distinguishes_mvp_failure_categories\(\)/],
|
||||
["CLI command exit-code coverage", /fn command_report_exit_code_marks_command_failures_only\(\)/],
|
||||
["CLI top-level logout alias coverage", /fn top_level_logout_alias_removes_only_cli_session_state\(\)/],
|
||||
["CLI confirmation gate helper", /fn confirmation_required_report[\s\S]*coordinator_request_sent[\s\S]*confirmation_required/],
|
||||
["CLI mutating confirmation coverage", /fn mutating_commands_require_yes_before_side_effects\(\)/],
|
||||
["CLI run rejection category coverage", /fn run_rejection_reports_machine_readable_error_category\(\)/],
|
||||
["CLI locality failure classifier", /fn classify_cli_error_message\(message: &str\)[\s\S]*message_mentions_locality_failure\(&message\)[\s\S]*return "connectivity"/],
|
||||
["CLI locality failure report helper", /fn task_locality_failure_from_reason\(reason: &Value\) -> Value[\s\S]*coordinator_bulk_relay_used[\s\S]*safe_next_actions/],
|
||||
["CLI locality failure human output", /fn push_task_locality_failures\(lines: &mut Vec<String>, tasks: &\[Value\]\)[\s\S]*locality \{task_name\}/],
|
||||
["node attach auto-detection coverage", /fn node_attach_detects_and_accepts_capability_overrides\(\)[\s\S]*detection\.auto_detected[\s\S]*command_backend[\s\S]*source_provider_backends/],
|
||||
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
|
||||
["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/],
|
||||
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
|
||||
["quota coordinator usage coverage", /fn quota_status_queries_public_coordinator_usage\(\)/],
|
||||
["task event summary coverage", /fn process_task_log_and_artifact_reports_summarize_task_events\(\)/],
|
||||
["task locality failure summary coverage", /fn process_task_log_and_artifact_reports_summarize_task_events\(\)[\s\S]*source snapshot unavailable and direct connectivity unavailable[\s\S]*locality_failure/],
|
||||
["log secret redaction coverage", /fn log_and_task_reports_redact_secret_like_values\(\)/],
|
||||
["artifact download/export report coverage", /fn artifact_download_and_export_reports_expose_safe_session_boundaries\(\)/],
|
||||
["process control report coverage", /fn process_restart_cancel_and_abort_reports_expose_control_boundaries\(\)/],
|
||||
["task restart report coverage", /fn task_restart_reports_clean_boundary_requirements\(\)/],
|
||||
["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/],
|
||||
["bundle rebuild restart compatibility coverage", /fn bundle_rebuild_after_source_edit_keeps_restart_compatibility_contract\(\)/],
|
||||
["inspect missing environment coverage", /fn bundle_inspect_reports_missing_environment_references_before_schedule\(\)/],
|
||||
["inspect source provider override coverage", /fn bundle_inspect_reports_source_provider_overrides_before_schedule\(\)/],
|
||||
["build missing environment gate coverage", /fn build_blocks_before_schedule_on_missing_environment_reference\(\)/],
|
||||
["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/],
|
||||
]) {
|
||||
expect(cli, name, pattern);
|
||||
}
|
||||
|
||||
expect(
|
||||
browserLoginFlowSmoke,
|
||||
"browser login smoke checks session file",
|
||||
/provider_tokens_persisted_locally[\s\S]*path\.join\(project, "\.disasmer", "session\.json"\)[\s\S]*\["auth", "status", "--json"\]/
|
||||
);
|
||||
expect(
|
||||
browserLoginFlowSmoke,
|
||||
"browser login smoke rejects provider token persistence",
|
||||
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
|
||||
);
|
||||
expect(
|
||||
nodeAttachSmoke,
|
||||
"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, \/build_main\//],
|
||||
["DAP smoke rejects synthetic source stepping", /client\.send\("next"[\s\S]*client\.failure\(step, "next"\)[\s\S]*source stepping is not yet available[\s\S]*synthetic step/],
|
||||
["DAP smoke verifies DAP variables scopes", /Source Locals[\s\S]*Wasm Frame Locals[\s\S]*Task Args and Handles[\s\S]*Disasmer Runtime/],
|
||||
["DAP smoke reports unavailable source locals truthfully", /unavailable-local-diagnostic[\s\S]*cannot be inspected/],
|
||||
["DAP smoke reports unavailable Wasm frame locals truthfully", /wasm-local-diagnostic[\s\S]*did not report inspectable Wasm frame locals/],
|
||||
["DAP smoke verifies real local-services all-stop", /runtimeBackend: "local-services"[\s\S]*allThreadsStopped, true[\s\S]*confirmed by every active participant/],
|
||||
["DAP smoke verifies coordinator checkpoint refusal", /client\.send\("restartFrame"[\s\S]*checkpoint boundary\|still active[\s\S]*whole virtual-process restart/],
|
||||
]) {
|
||||
expect(dapSmoke, name, pattern);
|
||||
}
|
||||
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator whole-process cancellation coverage",
|
||||
/fn service_cancels_whole_process_and_blocks_new_task_launches\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator single active process coverage",
|
||||
/fn service_rejects_second_active_process_unless_restarting_same_process\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator agent key lifecycle coverage",
|
||||
/fn service_manages_project_scoped_agent_public_keys\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator agent workflow dispatch coverage",
|
||||
/fn service_runs_agent_workflows_with_scoped_key_attribution\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator agent workflow authorization",
|
||||
/authorize_agent_project_run\([\s\S]*project:run/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator agent workflow actor fields",
|
||||
/pub struct WorkflowActor[\s\S]*agent: Option<AgentId>[\s\S]*public_key_fingerprint: Option<Digest>[\s\S]*authenticated_without_browser/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator node revoke coverage",
|
||||
/fn service_revokes_node_credentials_and_live_descriptors\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator public admin suspension coverage",
|
||||
/fn service_reports_and_enforces_public_admin_tenant_suspension\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator debug attach coverage",
|
||||
/fn service_authorizes_debug_attach_through_public_api\(\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator task restart boundary coverage",
|
||||
/fn service_reports_task_restart_boundary_through_public_api\(\)/
|
||||
);
|
||||
for (const [name, pattern] of [
|
||||
["task completion placement field", /pub struct TaskCompletionEvent[\s\S]*placement: Option<Placement>/],
|
||||
["task placement state", /task_placements/],
|
||||
["task completion retains placement", /event\.placement = self\.task_placements\.remove/],
|
||||
]) {
|
||||
expect(coordinator, `coordinator task events retain placement reasons: ${name}`, pattern);
|
||||
}
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator workflow spawn metering",
|
||||
/struct MeterKey[\s\S]*kind: LimitKind[\s\S]*window: u64[\s\S]*fn can_charge_workflow_spawn[\s\S]*self\.can_charge\(tenant, project, LimitKind::Spawn, 1, now_epoch_seconds\)[\s\S]*fn charge_workflow_spawn[\s\S]*self\.charge\(tenant, project, LimitKind::Spawn, 1, now_epoch_seconds\)[\s\S]*charged_spawns/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator spawn quota before work coverage",
|
||||
/fn service_checks_spawn_quota_before_process_or_task_work_starts\(\)[\s\S]*quota\.set_workflow_limits\(ResourceLimits[\s\S]*LimitKind::Spawn[\s\S]*compile-linux-denied[\s\S]*active_tasks\.contains[\s\S]*other-project[\s\S]*fn project_quota_resets_at_the_configured_window_boundary\(\)[\s\S]*set_server_time\(59\)[\s\S]*set_server_time\(60\)/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"public debug operation audit event",
|
||||
/pub struct DebugAuditEvent[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"public debug operation metering",
|
||||
/record_debug_audit_event\([\s\S]*charge_debug_read\([\s\S]*&tenant[\s\S]*&project[\s\S]*DEBUG_CONTROL_READ_BYTES[\s\S]*used_debug_read_bytes\(&tenant, &project, now_epoch_seconds\)/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI surfaces debug audit and quota fields",
|
||||
/debug_reads_quota_limited[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI sends agent workflow actor fields",
|
||||
/fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes node attach detection evidence",
|
||||
/struct NodeAttachDetectionEvidence[\s\S]*command_backend[\s\S]*container_backend[\s\S]*source_provider_backends[\s\S]*manual_capability_overrides[\s\S]*os_arch_capabilities_require_manual_flags/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI renders node attach detection evidence",
|
||||
/fn push_node_attach_detection[\s\S]*command backend[\s\S]*container backend[\s\S]*source providers[\s\S]*capability overrides/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes node attach grant disclosures",
|
||||
/struct CapabilityGrantDisclosure[\s\S]*coordinator_policy_limited[\s\S]*fn capability_grant_disclosures/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes artifact download grant disclosures",
|
||||
/fn artifact_download_grant_disclosures[\s\S]*"grant": "artifact_download"[\s\S]*"coordinator_policy_limited": true[\s\S]*"authorization_required": true[\s\S]*"guessable_public_url": false[\s\S]*"cross_tenant_reuse_allowed": false[\s\S]*"unauthorized_project_reuse_allowed": false/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes normalized node enrollment grants",
|
||||
/fn node_enroll_report[\s\S]*create_node_enrollment_grant[\s\S]*enrollment_grant[\s\S]*private_website_required[\s\S]*fn node_enrollment_grant_summary[\s\S]*short_lived[\s\S]*node_credentials_separate_from_user_session/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes task placement reasons",
|
||||
/fn task_summaries[\s\S]*node_placement[\s\S]*reasons[\s\S]*explanation_available/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI project init report current-directory link",
|
||||
/fn project_init_report[\s\S]*current_directory_link[\s\S]*links_current_directory[\s\S]*safe_defaults[\s\S]*coordinator_create_before_local_write/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI project init writes config after public create",
|
||||
/let coordinator_response = if let Some\(coordinator\)[\s\S]*"type": "create_project"[\s\S]*coordinator_session_requests = session\.requests\(\)[\s\S]*write_project_config\(&cwd, &config\)\?/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI project init renders current-directory link",
|
||||
/current_directory_link[\s\S]*current directory linked: true[\s\S]*current directory config/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI project list/select report public API boundary",
|
||||
/fn project_list_report[\s\S]*public_coordinator_api[\s\S]*private_website_required[\s\S]*fn project_select_report[\s\S]*project_config_written[\s\S]*private_website_required/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI project select writes config after coordinator response",
|
||||
/fn project_select_report[\s\S]*let coordinator_response = if let Some\(coordinator\)[\s\S]*let request = authenticated_or_local_trusted_request\([\s\S]*"type": "select_project"[\s\S]*Some\(session\.request\(request\)\?\)[\s\S]*write_project_config\(&cwd, &config\)\?/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI renders task placement reasons",
|
||||
/fn push_task_placement_reasons[\s\S]*placement \{task_name\}: \{node\}/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI redacts secret-like log values",
|
||||
/fn log_entries[\s\S]*redact_secret_like_text[\s\S]*secret_like_values_redacted[\s\S]*redacted_fields[\s\S]*fn redact_secret_like_text[\s\S]*access_token=[\s\S]*password=[\s\S]*bearer /
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI classifies machine-readable error categories",
|
||||
/fn classify_cli_error_message[\s\S]*"authentication"[\s\S]*"authorization"[\s\S]*"quota"[\s\S]*"policy"[\s\S]*"capability"[\s\S]*"connectivity"[\s\S]*"environment"[\s\S]*"program"/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes quota machine-error posture",
|
||||
/fn cli_error_summary_for_category[\s\S]*resource_category[\s\S]*quota_error_resource_category[\s\S]*community_tier_language[\s\S]*community_tier_label[\s\S]*private_abuse_heuristics_exposed[\s\S]*fn quota_error_resource_category[\s\S]*resource limit exceeded for /
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator exposes sanitized auth status API",
|
||||
/AuthStatus \{[\s\S]*tenant: String[\s\S]*project: String[\s\S]*actor_user: String[\s\S]*AuthStatus \{[\s\S]*account_status: String[\s\S]*suspended: bool[\s\S]*disabled: bool[\s\S]*private_moderation_details_exposed: bool[\s\S]*signup_failure_details_exposed: bool/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI auth status queries public account state",
|
||||
/fn coordinator_auth_status_summary[\s\S]*"type": "auth_status"[\s\S]*"private_moderation_details_exposed": false[\s\S]*"signup_failure_details_exposed": false/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI auth status does not expose raw private moderation response fields",
|
||||
/fn auth_status_queries_coordinator_account_state_without_private_moderation_details[\s\S]*abuse_score[\s\S]*moderation_notes[\s\S]*!serialized\.contains\("abuse_score"\)[\s\S]*!serialized\.contains\("moderation_notes"\)/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI auth status reports inactive account states safely",
|
||||
/fn auth_status_reports_disabled_deleted_and_manual_review_safely[\s\S]*account_status[\s\S]*disabled[\s\S]*deleted[\s\S]*manual_review[\s\S]*!serialized\.contains\("signup_policy_trace"\)/
|
||||
);
|
||||
expect(
|
||||
coordinatorLib,
|
||||
"coordinator summarizes private account policy state safely",
|
||||
/fn tenant_disabled\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:disabled[\s\S]*fn tenant_deleted\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:deleted[\s\S]*fn tenant_manual_review\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:manual_review[\s\S]*fn account_policy_state\(&self, tenant: &TenantId\) -> AccountPolicyState[\s\S]*"deleted"[\s\S]*"disabled"[\s\S]*"manual_review"[\s\S]*"account or tenant is pending hosted review"/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI renders sanitized account status",
|
||||
/coordinator_account_status[\s\S]*account status checked: \{checked\}[\s\S]*account status[\s\S]*private moderation details exposed/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI renders community tier wording",
|
||||
/push_string_field\(&mut lines, value, "quota_tier", "quota tier"\)[\s\S]*community_tier_label[\s\S]*quota tier: \{tier\}/
|
||||
);
|
||||
assert.doesNotMatch(cli, /free[- ]tier/i, "CLI source should use community tier wording");
|
||||
expect(
|
||||
cli,
|
||||
"CLI reports stable error-code contract",
|
||||
/fn cli_error_exit_code[\s\S]*"authentication" => 20[\s\S]*"authorization" => 21[\s\S]*"quota" => 22[\s\S]*"policy" => 23[\s\S]*"capability" => 24[\s\S]*"connectivity" => 25[\s\S]*"environment" => 26[\s\S]*"program" => 27/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI attaches machine errors to run and task reports",
|
||||
/run_start_summary[\s\S]*"machine_error": machine_error/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI attaches machine errors to task failures",
|
||||
/task_failure_machine_error[\s\S]*cli_error_summary_with_default/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI applies process exit code after printing command failure report",
|
||||
/fn emit_report<T: Serialize>[\s\S]*apply_command_report_exit_code[\s\S]*std::process::exit\(exit_code\)[\s\S]*fn human_report/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI applies artifact nested failure exit codes",
|
||||
/fn apply_command_report_exit_code[\s\S]*"\/download_session\/machine_error"[\s\S]*"\/export_plan\/machine_error"[\s\S]*"\/local_export\/machine_error"[\s\S]*"\/local_export\/download_session\/machine_error"[\s\S]*"\/local_export\/stream\/machine_error"/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI wraps fallible main with classified exit",
|
||||
/fn main\(\)[\s\S]*cli_error_summary\(&message\)[\s\S]*std::process::exit\(exit_code\)/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI parses dangerous capability overrides",
|
||||
/"host-filesystem"[\s\S]*Capability::HostFilesystem[\s\S]*"network"[\s\S]*Capability::Network[\s\S]*"secrets"[\s\S]*Capability::Secrets/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes source provider diagnostics",
|
||||
/source_provider_statuses[\s\S]*source_provider_selection[\s\S]*source_provider_unsupported/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes environment pre-schedule diagnostics",
|
||||
/environment_diagnostics_for_inputs[\s\S]*diagnose_environment_references[\s\S]*missing_environment/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI bundle metadata exposes MVP build facets",
|
||||
/BundleIdentityInputs[\s\S]*entrypoints[\s\S]*default_entrypoint[\s\S]*source_transfer_policy[\s\S]*wasm_source_proxy_digest[\s\S]*task_abi_digest/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI bundle coverage verifies Dockerfile and debug metadata",
|
||||
/fn bundle_inspect_discovers_environments_selected_inputs_and_source_providers\(\)[\s\S]*envs\/docker\/Dockerfile[\s\S]*task_metadata[\s\S]*source_metadata[\s\S]*debug_metadata/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI build coverage verifies inspectable metadata",
|
||||
/fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)[\s\S]*wasm_code[\s\S]*task_metadata[\s\S]*source_metadata[\s\S]*debug_metadata/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI build coverage verifies large input handle posture",
|
||||
/large_input_policy[\s\S]*selected_inputs_are_content_digests[\s\S]*selected_input_bytes_included[\s\S]*silent_task_argument_serialization[\s\S]*supported_handle_types/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI bundle coverage verifies restart compatibility metadata",
|
||||
/restart_compatibility[\s\S]*source_edits_can_restart_from_clean_task_boundary[\s\S]*requires_clean_checkpoint_boundary[\s\S]*compares_task_abi[\s\S]*incompatible_changes_require_whole_process_restart/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI blocks build before scheduling unsafe inputs",
|
||||
/blocked_before_schedule[\s\S]*scheduled_work[\s\S]*false/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI artifact export writes explicit local bytes from stream content",
|
||||
/fn artifact_export_local_write_followup[\s\S]*open_artifact_download_stream[\s\S]*content_base64[\s\S]*std::fs::write/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI artifact export keeps content out of reports",
|
||||
/fn artifact_stream_summary[\s\S]*content_material_returned_in_report[\s\S]*false/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI artifact export carries download grant disclosure",
|
||||
/fn artifact_export_local_write_followup[\s\S]*artifact_download_grant_disclosures[\s\S]*"grant_disclosures": grant_disclosures/
|
||||
);
|
||||
expect(
|
||||
cli,
|
||||
"CLI exposes admin bootstrap self-hosted path",
|
||||
/fn admin_bootstrap_report[\s\S]*self_hosted_cli_only[\s\S]*bootstrap_sequence[\s\S]*create_node_enrollment_grant[\s\S]*attach_worker_node[\s\S]*inspect_status_logs_artifacts[\s\S]*revoke_access/
|
||||
);
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator reverse-streams verified retained artifact bytes through a bounded spool",
|
||||
/struct ArtifactReverseTransfer[\s\S]*spool: tempfile::NamedTempFile[\s\S]*received_bytes: u64[\s\S]*delivered_offset: u64[\s\S]*handle_open_artifact_download_stream[\s\S]*read_exact\(&mut content\)[\s\S]*charge_download\([\s\S]*delivered_offset = end[\s\S]*BASE64_STANDARD\.encode\(content\)/
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["bundle inspect --json flag", /struct BundleInspectArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["build --json flag", /struct BuildArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["run --json flag", /struct RunArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["node attach --json flag", /struct AttachArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["DAP plan --json flag", /struct DapArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
["shared scope --json flag", /struct CliScopeArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||
]) {
|
||||
expect(cli, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["human default assertion", /default output should be human-readable text, not JSON/],
|
||||
[
|
||||
"login plan JSON mode",
|
||||
/"login",\s*"--plan",\s*"--coordinator",\s*"https:\/\/coord\.example\.test",\s*"--json"/,
|
||||
],
|
||||
["doctor human mode", /\["doctor"\]/],
|
||||
["doctor reachability JSON mode", /doctorJson\.coordinator_reachability\.status/],
|
||||
["doctor node readiness JSON mode", /doctorJson\.node_readiness_summary\.status[\s\S]*explicit_attach_required[\s\S]*missing_local_dependencies/],
|
||||
["bundle inspect JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/],
|
||||
["bundle inspect large input JSON mode", /large_input_policy[\s\S]*SourceSnapshot/],
|
||||
["bundle inspect restart compatibility JSON mode", /restart_compatibility[\s\S]*source_edits_can_restart_from_clean_task_boundary/],
|
||||
["auth expiry posture", /token_expiry_posture[\s\S]*expires_at/],
|
||||
]) {
|
||||
expect(outputModeSmoke, name, pattern);
|
||||
}
|
||||
|
||||
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/],
|
||||
]) {
|
||||
expect(errorExitSmoke, name, pattern);
|
||||
}
|
||||
|
||||
console.log("CLI-first contract smoke passed");
|
||||
2116
scripts/cli-happy-path-live-smoke.js
Normal file
2116
scripts/cli-happy-path-live-smoke.js
Normal file
File diff suppressed because it is too large
Load diff
61
scripts/cli-install-smoke.js
Executable file
61
scripts/cli-install-smoke.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-install-"));
|
||||
const installRoot = path.join(temp, "install");
|
||||
const targetDir =
|
||||
process.env.DISASMER_CLI_INSTALL_CARGO_TARGET_DIR ||
|
||||
process.env.CARGO_TARGET_DIR ||
|
||||
path.join(repo, "target");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
const binName = process.platform === "win32" ? "disasmer.exe" : "disasmer";
|
||||
const installedBin = path.join(installRoot, "bin", binName);
|
||||
|
||||
try {
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"install",
|
||||
"--path",
|
||||
"crates/disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--root",
|
||||
installRoot,
|
||||
"--debug"
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
CARGO_TARGET_DIR: targetDir
|
||||
},
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
|
||||
assert(fs.existsSync(installedBin), "installed disasmer binary must exist");
|
||||
|
||||
const inspection = JSON.parse(
|
||||
cp.execFileSync(
|
||||
installedBin,
|
||||
["bundle", "inspect", "--project", project, "--json"],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
|
||||
assert.strictEqual(inspection.project, project);
|
||||
assert.strictEqual(inspection.metadata.embeds_full_container_images, false);
|
||||
assert(inspection.metadata.environments.some((env) => env.name === "linux"));
|
||||
assert(inspection.metadata.selected_inputs.some((input) => input.path === "src/build.rs"));
|
||||
} finally {
|
||||
fs.rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("CLI install smoke passed");
|
||||
269
scripts/cli-local-run-smoke.js
Executable file
269
scripts/cli-local-run-smoke.js
Executable file
|
|
@ -0,0 +1,269 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { configurePodmanTestEnvironment } = require("./podman-test-env");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
configurePodmanTestEnvironment(repo);
|
||||
if (
|
||||
!process.env.DISASMER_PODMAN_NIX_SHELL &&
|
||||
cp.spawnSync("podman", ["--version"], { stdio: "ignore" }).status !== 0 &&
|
||||
cp.spawnSync("nix", ["--version"], { stdio: "ignore" }).status === 0
|
||||
) {
|
||||
cp.execFileSync(
|
||||
"nix",
|
||||
["shell", "nixpkgs#podman", "--command", "node", __filename],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_PODMAN_NIX_SHELL: "1",
|
||||
},
|
||||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
["build", "-q", "-p", "disasmer-node", "--bin", "disasmer-node"],
|
||||
{ cwd: repo, stdio: "inherit" }
|
||||
);
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function runCli(args, env = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
...env
|
||||
}
|
||||
}
|
||||
);
|
||||
const cliPid = child.pid;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`CLI run failed with code ${code}\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve({ pid: cliPid, report: JSON.parse(stdout) });
|
||||
} catch (error) {
|
||||
reject(new Error(`CLI output was not JSON: ${stdout}\n${error.stack || error.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
assert(Number.isInteger(coordinator.pid));
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const { pid: cliPid, report } = await runCli([
|
||||
"run",
|
||||
"--coordinator",
|
||||
`${addr.host}:${addr.port}`,
|
||||
"--project",
|
||||
project,
|
||||
"--json",
|
||||
]);
|
||||
assert(Number.isInteger(cliPid));
|
||||
assert.notStrictEqual(cliPid, coordinator.pid);
|
||||
assert.strictEqual(report.plan.entry, "build");
|
||||
assert.deepStrictEqual(report.plan.session, "Anonymous");
|
||||
assert.strictEqual(report.boundary.cli_process_started_node_process, true);
|
||||
assert.strictEqual(report.boundary.cli_process_started_coordinator_process, false);
|
||||
assert(Number.isInteger(report.boundary.spawned_node_process_id));
|
||||
assert.notStrictEqual(report.boundary.spawned_node_process_id, cliPid);
|
||||
assert.notStrictEqual(report.boundary.spawned_node_process_id, coordinator.pid);
|
||||
assert.strictEqual(report.boundary.node_session_requests, 0);
|
||||
assert.strictEqual(report.node_report.node_status, "completed");
|
||||
assert.strictEqual(report.node_report.execution_substrate, "wasm");
|
||||
assert.strictEqual(report.node_report.task_spawn_host_import, true);
|
||||
assert.strictEqual(
|
||||
report.node_report.pre_node_process_status.processes.length,
|
||||
1
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
report.node_report.pre_node_process_status.processes[0].connected_nodes,
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(
|
||||
report.node_report.pre_node_process_status.processes[0].main_state,
|
||||
"running"
|
||||
);
|
||||
assert.strictEqual(
|
||||
report.node_report.pre_node_process_status.processes[0].main_wait_state,
|
||||
"waiting_for_node",
|
||||
"the coordinator must expose that the capless main is parked on placement before a node exists"
|
||||
);
|
||||
assert.strictEqual(
|
||||
report.node_report.pre_node_process_status.processes[0].main_task_instance,
|
||||
report.node_report.run.task_instance
|
||||
);
|
||||
assert.strictEqual(report.node_report.run.status, "main_launched");
|
||||
assert.strictEqual(report.node_report.join.type, "task_joined");
|
||||
const process = report.node_report.run.process;
|
||||
assert.strictEqual(process, "vp-current");
|
||||
|
||||
const events = await send(addr, {
|
||||
type: "list_task_events",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process
|
||||
});
|
||||
assert.strictEqual(events.type, "task_events");
|
||||
assert(events.events.length >= 4);
|
||||
assert(
|
||||
events.events
|
||||
.filter((event) => event.executor === "node")
|
||||
.every((event) => event.node === "node-cli-local")
|
||||
);
|
||||
assert(
|
||||
events.events.some(
|
||||
(event) =>
|
||||
event.executor === "coordinator_main" &&
|
||||
event.node === "coordinator-main"
|
||||
)
|
||||
);
|
||||
assert(events.events.every((event) => event.process === process));
|
||||
assert.deepStrictEqual(
|
||||
new Set(events.events.map((event) => event.task_definition)),
|
||||
new Set([
|
||||
report.node_report.run.task_definition,
|
||||
"prepare_source",
|
||||
"compile_linux",
|
||||
"package_release",
|
||||
])
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Set(events.events.map((event) => event.task)).size,
|
||||
events.events.length,
|
||||
"every live task event must retain its unique instance identity"
|
||||
);
|
||||
assert(
|
||||
events.events.some(
|
||||
(event) => event.task === report.node_report.run.task_instance
|
||||
)
|
||||
);
|
||||
assert(events.events.some((event) => event.task.endsWith(":child:1")));
|
||||
assert(events.events.some((event) => event.task.endsWith(":child:2")));
|
||||
assert(events.events.some((event) => event.task.endsWith(":child:3")));
|
||||
assert(events.events.some((event) => event.artifact_path));
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
const { pid: autoCliPid, report: autoReport } = await runCli([
|
||||
"run",
|
||||
"--local",
|
||||
"--project",
|
||||
project,
|
||||
"--json",
|
||||
]);
|
||||
assert(Number.isInteger(autoCliPid));
|
||||
assert.strictEqual(autoReport.plan.entry, "build");
|
||||
assert.deepStrictEqual(autoReport.plan.coordinator, "LocalOnly");
|
||||
assert.deepStrictEqual(autoReport.plan.session, "Anonymous");
|
||||
assert.strictEqual(autoReport.boundary.cli_process_started_node_process, true);
|
||||
assert.strictEqual(autoReport.boundary.cli_process_started_coordinator_process, true);
|
||||
assert.match(autoReport.boundary.coordinator_address, /^127\.0\.0\.1:\d+$/);
|
||||
assert(Number.isInteger(autoReport.boundary.coordinator_process_id));
|
||||
assert(Number.isInteger(autoReport.boundary.spawned_node_process_id));
|
||||
assert.notStrictEqual(autoReport.boundary.coordinator_process_id, autoCliPid);
|
||||
assert.notStrictEqual(autoReport.boundary.spawned_node_process_id, autoCliPid);
|
||||
assert.notStrictEqual(
|
||||
autoReport.boundary.spawned_node_process_id,
|
||||
autoReport.boundary.coordinator_process_id
|
||||
);
|
||||
assert.strictEqual(autoReport.boundary.node_session_requests, 0);
|
||||
assert.strictEqual(autoReport.node_report.node_status, "completed");
|
||||
assert.strictEqual(autoReport.node_report.execution_substrate, "wasm");
|
||||
assert.strictEqual(autoReport.node_report.task_spawn_host_import, true);
|
||||
assert.strictEqual(autoReport.node_report.run.status, "main_launched");
|
||||
assert.strictEqual(autoReport.node_report.join.type, "task_joined");
|
||||
|
||||
console.log("CLI local run smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
72
scripts/cli-login-smoke.js
Normal file
72
scripts/cli-login-smoke.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const coordinator = "https://coord.example.test";
|
||||
const defaultHostedCoordinatorEndpoint = "https://disasmer.michelpaulissen.com";
|
||||
|
||||
function disasmer(args) {
|
||||
return JSON.parse(
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function disasmerRaw(args, env = {}) {
|
||||
return cp.spawnSync(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
|
||||
{
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const browser = disasmer(["login", "--plan", "--coordinator", coordinator, "--json"]);
|
||||
assert.strictEqual(browser.coordinator, coordinator);
|
||||
assert(browser.human_flow.Browser, "browser login should be available for human users");
|
||||
assert.strictEqual(browser.human_flow.Browser.authorization_url, null);
|
||||
assert.strictEqual(browser.human_flow.Browser.server_owns_state, true);
|
||||
assert.strictEqual(browser.human_flow.Browser.server_owns_nonce, true);
|
||||
assert.strictEqual(browser.human_flow.Browser.pkce_required, true);
|
||||
assert.strictEqual(browser.human_flow.Browser.hosted_callback, true);
|
||||
assert.strictEqual(browser.human_flow.Browser.cli_receives_provider_authorization_code, false);
|
||||
assert.strictEqual(browser.human_flow.Browser.cli_submits_identity_claims, false);
|
||||
|
||||
const defaultBrowser = disasmer(["login", "--plan", "--json"]);
|
||||
assert.strictEqual(defaultBrowser.coordinator, defaultHostedCoordinatorEndpoint);
|
||||
assert(defaultBrowser.human_flow.Browser);
|
||||
|
||||
const nonInteractiveBrowser = disasmerRaw(
|
||||
["login", "--browser", "--non-interactive", "--coordinator", coordinator, "--json"],
|
||||
{
|
||||
DISASMER_BROWSER_OPEN_COMMAND:
|
||||
"node -e 'require(\"fs\").writeFileSync(\"/tmp/disasmer-browser-should-not-open\", \"opened\")'",
|
||||
}
|
||||
);
|
||||
assert.strictEqual(nonInteractiveBrowser.status, 20, nonInteractiveBrowser.stderr);
|
||||
assert.doesNotMatch(nonInteractiveBrowser.stderr, /Opening Disasmer browser login/);
|
||||
const nonInteractiveReport = JSON.parse(nonInteractiveBrowser.stdout);
|
||||
assert.strictEqual(nonInteractiveReport.status, "authentication_required");
|
||||
assert.strictEqual(nonInteractiveReport.non_interactive, true);
|
||||
assert.strictEqual(nonInteractiveReport.browser_opened, false);
|
||||
assert.strictEqual(nonInteractiveReport.machine_error.category, "authentication");
|
||||
assert.strictEqual(nonInteractiveReport.machine_error.stable_exit_code, 20);
|
||||
assert(
|
||||
nonInteractiveReport.machine_error.next_actions.includes(
|
||||
"rerun without --non-interactive to open the browser"
|
||||
)
|
||||
);
|
||||
|
||||
console.log("CLI login smoke passed");
|
||||
182
scripts/cli-output-mode-smoke.js
Normal file
182
scripts/cli-output-mode-smoke.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
const isolatedCwd = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-output-"));
|
||||
|
||||
function disasmer(args, env = {}, cwd = repo) {
|
||||
return cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"--manifest-path",
|
||||
path.join(repo, "Cargo.toml"),
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
...args,
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function json(args, env, cwd) {
|
||||
return JSON.parse(disasmer(args, env, cwd));
|
||||
}
|
||||
|
||||
function assertHuman(name, output, requiredPatterns) {
|
||||
assert(
|
||||
!output.trimStart().startsWith("{"),
|
||||
`${name} default output should be human-readable text, not JSON`
|
||||
);
|
||||
for (const pattern of requiredPatterns) {
|
||||
assert.match(output, pattern, `${name} human output missing ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
const helpHuman = disasmer(["help"]);
|
||||
assertHuman("help", helpHuman, [
|
||||
/Primary workflow:/,
|
||||
/disasmer login --browser/,
|
||||
/disasmer project init/,
|
||||
/disasmer node attach; disasmer-node --worker/,
|
||||
/Disasmer: Launch Virtual Process/,
|
||||
/Hosted account creation happens in the browser login flow/,
|
||||
/--json/,
|
||||
]);
|
||||
|
||||
const loginHuman = disasmer([
|
||||
"login",
|
||||
"--plan",
|
||||
"--coordinator",
|
||||
"https://coord.example.test",
|
||||
]);
|
||||
assertHuman("login", loginHuman, [
|
||||
/Disasmer login/,
|
||||
/flow: browser/,
|
||||
]);
|
||||
|
||||
const loginJson = json([
|
||||
"login",
|
||||
"--plan",
|
||||
"--coordinator",
|
||||
"https://coord.example.test",
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(loginJson.coordinator, "https://coord.example.test");
|
||||
assert(loginJson.human_flow.Browser);
|
||||
assert.strictEqual(loginJson.human_flow.Browser.hosted_callback, true);
|
||||
assert.strictEqual(loginJson.human_flow.Browser.cli_submits_identity_claims, false);
|
||||
|
||||
const doctorHuman = disasmer(["doctor"]);
|
||||
assertHuman("doctor", doctorHuman, [
|
||||
/Disasmer doctor/,
|
||||
/coordinator reachability: not_configured/,
|
||||
/dependencies:/,
|
||||
/auth:/,
|
||||
/node capabilities:/,
|
||||
/node readiness: (ready_to_attach|local_dependencies_missing|limited_capabilities)/,
|
||||
/node next:/,
|
||||
]);
|
||||
|
||||
const doctorJson = json(["doctor", "--json"]);
|
||||
assert.strictEqual(doctorJson.coordinator_reachability.checked, false);
|
||||
assert.strictEqual(doctorJson.coordinator_reachability.status, "not_configured");
|
||||
assert(
|
||||
["ready_to_attach", "local_dependencies_missing", "limited_capabilities"].includes(
|
||||
doctorJson.node_readiness_summary.status
|
||||
)
|
||||
);
|
||||
assert.strictEqual(doctorJson.node_readiness_summary.explicit_attach_required, true);
|
||||
assert.strictEqual(
|
||||
doctorJson.node_readiness_summary.command_execution_capability,
|
||||
true
|
||||
);
|
||||
assert(Array.isArray(doctorJson.node_readiness_summary.missing_local_dependencies));
|
||||
assert(doctorJson.node_readiness_summary.next_actions.length >= 2);
|
||||
|
||||
const authJson = json(["auth", "status", "--json"], {
|
||||
DISASMER_TOKEN: "token",
|
||||
DISASMER_TOKEN_EXPIRES_AT: "2026-07-04T00:00:00Z",
|
||||
}, isolatedCwd);
|
||||
assert.strictEqual(authJson.session.kind, "human");
|
||||
assert.strictEqual(authJson.session.token_expiry_posture, "expires_at");
|
||||
assert.strictEqual(authJson.session.expires_at, "2026-07-04T00:00:00Z");
|
||||
assert.strictEqual(authJson.coordinator_account_status.checked, false);
|
||||
assert.strictEqual(authJson.coordinator_account_status.account_status, "unknown");
|
||||
assert.strictEqual(
|
||||
authJson.coordinator_account_status.private_moderation_details_exposed,
|
||||
false
|
||||
);
|
||||
|
||||
const inspectHuman = disasmer(["bundle", "inspect", "--project", project]);
|
||||
assertHuman("bundle inspect", inspectHuman, [
|
||||
/Disasmer bundle inspect/,
|
||||
/bundle: sha256:/,
|
||||
/environments:/,
|
||||
]);
|
||||
|
||||
const inspectJson = json(["bundle", "inspect", "--project", project, "--json"]);
|
||||
assert.strictEqual(inspectJson.project, project);
|
||||
assert.match(inspectJson.metadata.identity, /^sha256:/);
|
||||
assert.match(inspectJson.metadata.wasm_code, /^sha256:/);
|
||||
assert.strictEqual(inspectJson.metadata.task_metadata.default_entrypoint, "build");
|
||||
assert.deepStrictEqual(inspectJson.metadata.task_metadata.entrypoints, [
|
||||
"build",
|
||||
"fail",
|
||||
"restart",
|
||||
]);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.source_metadata.transfer_policy.coordinator_receives_source_bytes_by_default,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.source_metadata.transfer_policy.default_full_repo_tarball,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(inspectJson.metadata.debug_metadata.dap_virtual_process, true);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.large_input_policy.selected_inputs_are_content_digests,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(inspectJson.metadata.large_input_policy.selected_input_bytes_included, false);
|
||||
assert.strictEqual(inspectJson.metadata.large_input_policy.full_repository_bytes_included, false);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.large_input_policy.silent_task_argument_serialization,
|
||||
false
|
||||
);
|
||||
assert(inspectJson.metadata.large_input_policy.supported_handle_types.includes("SourceSnapshot"));
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.restart_compatibility.source_edits_can_restart_from_clean_task_boundary,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.restart_compatibility.requires_clean_checkpoint_boundary,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.restart_compatibility.compares_task_abi,
|
||||
inspectJson.metadata.task_metadata.task_abi
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspectJson.metadata.restart_compatibility.incompatible_changes_require_whole_process_restart,
|
||||
true
|
||||
);
|
||||
|
||||
console.log("CLI output mode smoke passed");
|
||||
86
scripts/code-size-guard.js
Normal file
86
scripts/code-size-guard.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const maxProductionLines = 1000;
|
||||
|
||||
const ignoredDirs = new Set([".git", ".cache", ".local", "target", "node_modules"]);
|
||||
const rootIgnoredDirs = new Set(["experiments"]);
|
||||
|
||||
function isTestRustFile(relativePath) {
|
||||
const normalized = relativePath.split(path.sep).join("/");
|
||||
return (
|
||||
normalized.endsWith("/tests.rs") ||
|
||||
normalized.includes("/tests/") ||
|
||||
normalized.endsWith("_test.rs")
|
||||
);
|
||||
}
|
||||
|
||||
function lineCount(source) {
|
||||
return source.endsWith("\n")
|
||||
? source.split("\n").length - 1
|
||||
: source.split("\n").length;
|
||||
}
|
||||
|
||||
function productionSource(source) {
|
||||
// A trailing cfg(test) module is not compiled into the product and therefore
|
||||
// is not production business logic. Keeping it next to the implementation is
|
||||
// useful when it exercises private invariants; count only the source before
|
||||
// that explicitly test-only suffix.
|
||||
const testModule = /^#\[cfg\(test\)\]\r?\nmod\s+[A-Za-z_][A-Za-z0-9_]*\s*\{/m.exec(source);
|
||||
return testModule ? source.slice(0, testModule.index) : source;
|
||||
}
|
||||
|
||||
function walk(directory, results = []) {
|
||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
const relativePath = path.relative(repo, fullPath);
|
||||
if (entry.isDirectory()) {
|
||||
if (ignoredDirs.has(entry.name)) continue;
|
||||
if (!relativePath.includes(path.sep) && rootIgnoredDirs.has(entry.name)) continue;
|
||||
walk(fullPath, results);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".rs")) {
|
||||
results.push(relativePath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const oversizedProductionFiles = [];
|
||||
const oversizedTestFiles = [];
|
||||
|
||||
for (const relativePath of walk(repo)) {
|
||||
const source = fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
const testOnlyFile = isTestRustFile(relativePath);
|
||||
const lines = lineCount(testOnlyFile ? source : productionSource(source));
|
||||
if (lines <= maxProductionLines) continue;
|
||||
|
||||
const entry = { path: relativePath, lines };
|
||||
if (testOnlyFile) {
|
||||
oversizedTestFiles.push(entry);
|
||||
} else {
|
||||
oversizedProductionFiles.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
oversizedProductionFiles,
|
||||
[],
|
||||
`production Rust files must stay at or below ${maxProductionLines} lines; split business logic before adding more code`
|
||||
);
|
||||
|
||||
if (oversizedTestFiles.length > 0) {
|
||||
console.log(
|
||||
`Code-size guard ignored ${oversizedTestFiles.length} oversized test-only Rust file(s): ${oversizedTestFiles
|
||||
.map((entry) => `${entry.path} (${entry.lines})`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Code-size guard passed");
|
||||
43
scripts/coordinator-wire.js
Normal file
43
scripts/coordinator-wire.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
let requestId = 0;
|
||||
|
||||
function coordinatorWireRequest(payload, prefix = "acceptance") {
|
||||
if (payload && payload.type === "coordinator_request") return payload;
|
||||
if (!payload || typeof payload.type !== "string" || !payload.type.trim()) {
|
||||
throw new Error("coordinator payload must have a non-empty type");
|
||||
}
|
||||
requestId += 1;
|
||||
return {
|
||||
type: "coordinator_request",
|
||||
protocol_version: 1,
|
||||
request_id: `${prefix}-${process.pid}-${requestId}`,
|
||||
operation: payload.type,
|
||||
authentication: authenticationMetadata(payload),
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function authenticationMetadata(payload) {
|
||||
if (payload.type === "authenticated") {
|
||||
return {
|
||||
kind: "cli_session",
|
||||
session: true,
|
||||
request_operation: payload.request?.type || "unknown",
|
||||
};
|
||||
}
|
||||
if (payload.type === "signed_node" || payload.node_signature) {
|
||||
return { kind: "node_signature", node: payload.node || null };
|
||||
}
|
||||
if (payload.agent_signature) {
|
||||
return {
|
||||
kind: "agent_signature",
|
||||
agent: payload.actor_agent || null,
|
||||
fingerprint: payload.agent_public_key_fingerprint || null,
|
||||
};
|
||||
}
|
||||
if (payload.admin_token) {
|
||||
return { kind: "admin_credential" };
|
||||
}
|
||||
return { kind: "none" };
|
||||
}
|
||||
|
||||
module.exports = { coordinatorWireRequest };
|
||||
132
scripts/dap-client.js
Normal file
132
scripts/dap-client.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
const cp = require("child_process");
|
||||
|
||||
class DapClient {
|
||||
constructor({
|
||||
cwd = process.cwd(),
|
||||
env = process.env,
|
||||
command = "cargo",
|
||||
args = [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-dap",
|
||||
"--bin",
|
||||
"disasmer-debug-dap",
|
||||
],
|
||||
} = {}) {
|
||||
this.child = cp.spawn(
|
||||
command,
|
||||
args,
|
||||
{ cwd, env }
|
||||
);
|
||||
this.seq = 1;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.messages = [];
|
||||
this.waiters = [];
|
||||
this.stderr = "";
|
||||
|
||||
this.child.stdout.on("data", (chunk) => {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
this.parse();
|
||||
});
|
||||
this.child.stderr.on("data", (chunk) => {
|
||||
this.stderr += chunk.toString();
|
||||
});
|
||||
this.child.on("exit", () => this.flushWaiters());
|
||||
}
|
||||
|
||||
send(command, args = {}) {
|
||||
const seq = this.seq++;
|
||||
const message = { seq, type: "request", command, arguments: args };
|
||||
const payload = Buffer.from(JSON.stringify(message));
|
||||
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
|
||||
this.child.stdin.write(payload);
|
||||
return seq;
|
||||
}
|
||||
|
||||
async response(seq, command) {
|
||||
const message = await this.waitFor(
|
||||
(item) =>
|
||||
item.type === "response" &&
|
||||
item.request_seq === seq &&
|
||||
item.command === command
|
||||
);
|
||||
if (!message.success) {
|
||||
throw new Error(
|
||||
`DAP ${command} failed: ${message.message || JSON.stringify(message)}\nAdapter stderr:\n${this.stderr}`
|
||||
);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
async failure(seq, command) {
|
||||
const message = await this.waitFor(
|
||||
(item) =>
|
||||
item.type === "response" &&
|
||||
item.request_seq === seq &&
|
||||
item.command === command
|
||||
);
|
||||
if (message.success) {
|
||||
throw new Error(`DAP ${command} unexpectedly succeeded`);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
waitFor(predicate, timeoutMs = 120000) {
|
||||
const existing = this.messages.find(predicate);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.child.kill("SIGKILL");
|
||||
const recent = this.messages
|
||||
.slice(-10)
|
||||
.map((message) => JSON.stringify(message))
|
||||
.join("\n");
|
||||
reject(
|
||||
new Error(
|
||||
`timed out waiting for DAP message\nRecent DAP messages:\n${recent}\nAdapter stderr:\n${this.stderr}`
|
||||
)
|
||||
);
|
||||
}, timeoutMs);
|
||||
this.waiters.push({ predicate, resolve, timer });
|
||||
});
|
||||
}
|
||||
|
||||
parse() {
|
||||
while (true) {
|
||||
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.buffer.slice(0, headerEnd).toString();
|
||||
const match = header.match(/Content-Length: (\d+)/i);
|
||||
if (!match) throw new Error(`bad DAP header: ${header}`);
|
||||
const length = Number(match[1]);
|
||||
const start = headerEnd + 4;
|
||||
const end = start + length;
|
||||
if (this.buffer.length < end) return;
|
||||
const payload = this.buffer.slice(start, end).toString();
|
||||
this.buffer = this.buffer.slice(end);
|
||||
this.messages.push(JSON.parse(payload));
|
||||
this.flushWaiters();
|
||||
}
|
||||
}
|
||||
|
||||
flushWaiters() {
|
||||
for (const waiter of [...this.waiters]) {
|
||||
const message = this.messages.find(waiter.predicate);
|
||||
if (!message) continue;
|
||||
clearTimeout(waiter.timer);
|
||||
this.waiters.splice(this.waiters.indexOf(waiter), 1);
|
||||
waiter.resolve(message);
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.child.exitCode !== null) return;
|
||||
const seq = this.send("disconnect");
|
||||
await this.response(seq, "disconnect");
|
||||
this.child.stdin.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DapClient };
|
||||
363
scripts/dap-smoke.js
Normal file
363
scripts/dap-smoke.js
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { DapClient } = require("./dap-client");
|
||||
|
||||
(async () => {
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
const sourcePath = fs.realpathSync(path.join(project, "src/build.rs"));
|
||||
const sourceLines = fs.readFileSync(sourcePath, "utf8").split(/\r?\n/);
|
||||
const buildMainLine =
|
||||
sourceLines.findIndex((line) => line.includes("pub async fn build_main()")) + 1;
|
||||
assert(buildMainLine > 0, "flagship source must contain build_main");
|
||||
|
||||
const client = new DapClient();
|
||||
try {
|
||||
const initialize = client.send("initialize", {
|
||||
adapterID: "disasmer",
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true,
|
||||
});
|
||||
await client.response(initialize, "initialize");
|
||||
|
||||
const launch = client.send("launch", {
|
||||
entry: "build",
|
||||
project,
|
||||
runtimeBackend: "local-services",
|
||||
});
|
||||
await client.response(launch, "launch");
|
||||
await client.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
|
||||
const breakpoints = client.send("setBreakpoints", {
|
||||
source: { path: sourcePath },
|
||||
breakpoints: [{ line: buildMainLine }],
|
||||
});
|
||||
const breakpointResponse = await client.response(
|
||||
breakpoints,
|
||||
"setBreakpoints"
|
||||
);
|
||||
assert.strictEqual(breakpointResponse.body.breakpoints.length, 1);
|
||||
assert.strictEqual(breakpointResponse.body.breakpoints[0].verified, true);
|
||||
|
||||
const configurationDone = client.send("configurationDone");
|
||||
await client.response(configurationDone, "configurationDone");
|
||||
const stopped = await client.waitFor(
|
||||
(message) =>
|
||||
message.type === "event" &&
|
||||
message.event === "stopped" &&
|
||||
message.body.reason === "breakpoint"
|
||||
);
|
||||
assert.strictEqual(stopped.body.allThreadsStopped, true);
|
||||
assert.match(stopped.body.description, /confirmed by every active participant/i);
|
||||
|
||||
const threadsRequest = client.send("threads");
|
||||
const threads = (await client.response(threadsRequest, "threads")).body
|
||||
.threads;
|
||||
const mainThread = threads.find((thread) =>
|
||||
thread.name.includes("build virtual process")
|
||||
);
|
||||
assert(mainThread, "the running Wasm entrypoint must be a DAP virtual thread");
|
||||
assert.strictEqual(stopped.body.threadId, mainThread.id);
|
||||
|
||||
const stackRequest = client.send("stackTrace", {
|
||||
threadId: mainThread.id,
|
||||
startFrame: 0,
|
||||
levels: 1,
|
||||
});
|
||||
const stack = (await client.response(stackRequest, "stackTrace")).body
|
||||
.stackFrames;
|
||||
assert.strictEqual(stack.length, 1);
|
||||
assert.strictEqual(stack[0].line, buildMainLine);
|
||||
assert.strictEqual(stack[0].source.path, sourcePath);
|
||||
assert.match(stack[0].name, /build_main::wasm/);
|
||||
assert.doesNotMatch(stack[0].name, /podman|cmd\.exe|powershell|pid|native child/i);
|
||||
|
||||
const sourceRequest = client.send("source", { source: stack[0].source });
|
||||
const source = (await client.response(sourceRequest, "source")).body;
|
||||
assert.match(source.content, /build_main/);
|
||||
assert.match(source.mimeType, /rust/);
|
||||
|
||||
const scopesRequest = client.send("scopes", { frameId: stack[0].id });
|
||||
const scopes = (await client.response(scopesRequest, "scopes")).body.scopes;
|
||||
const localsScope = scopes.find((scope) => scope.name === "Source Locals");
|
||||
const wasmScope = scopes.find((scope) => scope.name === "Wasm Frame Locals");
|
||||
const argsScope = scopes.find(
|
||||
(scope) => scope.name === "Task Args and Handles"
|
||||
);
|
||||
const runtimeScope = scopes.find(
|
||||
(scope) => scope.name === "Disasmer Runtime"
|
||||
);
|
||||
assert(localsScope && wasmScope && argsScope && runtimeScope);
|
||||
|
||||
const localsRequest = client.send("variables", {
|
||||
variablesReference: localsScope.variablesReference,
|
||||
});
|
||||
const locals = (await client.response(localsRequest, "variables")).body
|
||||
.variables;
|
||||
assert(
|
||||
locals.some(
|
||||
(variable) =>
|
||||
variable.name === "unavailable-local-diagnostic" &&
|
||||
String(variable.value).includes("cannot be inspected")
|
||||
)
|
||||
);
|
||||
|
||||
const wasmRequest = client.send("variables", {
|
||||
variablesReference: wasmScope.variablesReference,
|
||||
});
|
||||
const wasmLocals = (await client.response(wasmRequest, "variables")).body
|
||||
.variables;
|
||||
assert.deepStrictEqual(
|
||||
wasmLocals.map((variable) => variable.name),
|
||||
["wasm-local-diagnostic"]
|
||||
);
|
||||
assert.match(wasmLocals[0].value, /did not report inspectable Wasm frame locals/);
|
||||
|
||||
const argsRequest = client.send("variables", {
|
||||
variablesReference: argsScope.variablesReference,
|
||||
});
|
||||
const args = (await client.response(argsRequest, "variables")).body.variables;
|
||||
assert.deepStrictEqual(
|
||||
args.map((variable) => variable.name),
|
||||
["runtime-boundary-diagnostic"]
|
||||
);
|
||||
assert.match(args[0].value, /reported no task arguments or handles/);
|
||||
|
||||
const runtimeRequest = client.send("variables", {
|
||||
variablesReference: runtimeScope.variablesReference,
|
||||
});
|
||||
const runtime = (await client.response(runtimeRequest, "variables")).body
|
||||
.variables;
|
||||
const value = (name) => runtime.find((variable) => variable.name === name)?.value;
|
||||
assert.strictEqual(value("runtime_backend"), "LocalServices");
|
||||
assert.strictEqual(value("state"), "Frozen");
|
||||
assert.strictEqual(value("debug_epoch"), 1);
|
||||
assert.strictEqual(value("coordinator_task_events"), 0);
|
||||
assert.match(
|
||||
String(value("command_status")),
|
||||
/frozen through local services at executing Wasm probe/
|
||||
);
|
||||
|
||||
const step = client.send("next", { threadId: mainThread.id });
|
||||
const stepFailure = await client.failure(step, "next");
|
||||
assert.match(stepFailure.message, /source stepping is not yet available/i);
|
||||
assert.match(stepFailure.message, /synthetic step/i);
|
||||
|
||||
const restart = client.send("restartFrame", { frameId: stack[0].id });
|
||||
const restartFailure = await client.failure(restart, "restartFrame");
|
||||
assert.match(restartFailure.message, /checkpoint boundary|still active/i);
|
||||
|
||||
const incompatibleRestart = client.send("restartFrame", {
|
||||
frameId: stack[0].id,
|
||||
sourceCompatibility: "incompatible",
|
||||
});
|
||||
const incompatibleFailure = await client.failure(
|
||||
incompatibleRestart,
|
||||
"restartFrame"
|
||||
);
|
||||
assert.match(incompatibleFailure.message, /incompatible source edit/i);
|
||||
assert.match(incompatibleFailure.message, /whole virtual-process restart/i);
|
||||
|
||||
await client.close();
|
||||
} catch (error) {
|
||||
if (client.child.exitCode === null) client.child.kill("SIGKILL");
|
||||
throw error;
|
||||
}
|
||||
|
||||
const failMainLine =
|
||||
sourceLines.findIndex((line) => line.includes("pub async fn fail_main()")) + 1;
|
||||
assert(failMainLine > 0, "flagship source must contain fail_main");
|
||||
const taskTrapLine =
|
||||
sourceLines.findIndex((line) => line.includes("fn task_trap(")) + 1;
|
||||
assert(taskTrapLine > 0, "flagship source must contain task_trap");
|
||||
const restartClient = new DapClient();
|
||||
try {
|
||||
const initialize = restartClient.send("initialize", {
|
||||
adapterID: "disasmer",
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true,
|
||||
});
|
||||
await restartClient.response(initialize, "initialize");
|
||||
const launch = restartClient.send("launch", {
|
||||
entry: "fail",
|
||||
project,
|
||||
runtimeBackend: "local-services",
|
||||
});
|
||||
await restartClient.response(launch, "launch");
|
||||
await restartClient.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
const breakpoints = restartClient.send("setBreakpoints", {
|
||||
source: { path: sourcePath },
|
||||
breakpoints: [{ line: failMainLine }, { line: taskTrapLine }],
|
||||
});
|
||||
const breakpointResponse = await restartClient.response(
|
||||
breakpoints,
|
||||
"setBreakpoints"
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
breakpointResponse.body.breakpoints.map((breakpoint) => breakpoint.verified),
|
||||
[true, true]
|
||||
);
|
||||
const configurationDone = restartClient.send("configurationDone");
|
||||
await restartClient.response(configurationDone, "configurationDone");
|
||||
const initialStop = await restartClient.waitFor(
|
||||
(message) =>
|
||||
message.type === "event" &&
|
||||
message.event === "stopped" &&
|
||||
message.body.reason === "breakpoint"
|
||||
);
|
||||
assert.strictEqual(initialStop.body.allThreadsStopped, true);
|
||||
const threadsRequest = restartClient.send("threads");
|
||||
const threads = (
|
||||
await restartClient.response(threadsRequest, "threads")
|
||||
).body.threads;
|
||||
const failThread = threads.find(
|
||||
(thread) => thread.id === initialStop.body.threadId
|
||||
);
|
||||
assert(failThread, "failed entrypoint must remain a virtual task thread");
|
||||
const stackRequest = restartClient.send("stackTrace", {
|
||||
threadId: failThread.id,
|
||||
startFrame: 0,
|
||||
levels: 1,
|
||||
});
|
||||
const failedStack = (
|
||||
await restartClient.response(stackRequest, "stackTrace")
|
||||
).body.stackFrames;
|
||||
assert.strictEqual(failedStack[0].line, failMainLine);
|
||||
|
||||
const continueRequest = restartClient.send("continue", {
|
||||
threadId: failThread.id,
|
||||
});
|
||||
await restartClient.response(continueRequest, "continue");
|
||||
const childStop = await restartClient.waitFor(
|
||||
(message) =>
|
||||
message.seq > initialStop.seq &&
|
||||
message.type === "event" &&
|
||||
message.event === "stopped" &&
|
||||
message.body.reason === "breakpoint",
|
||||
70000
|
||||
);
|
||||
assert.strictEqual(childStop.body.allThreadsStopped, true);
|
||||
|
||||
const childThreadsRequest = restartClient.send("threads");
|
||||
const childThreads = (
|
||||
await restartClient.response(childThreadsRequest, "threads")
|
||||
).body.threads;
|
||||
const childThread = childThreads.find(
|
||||
(thread) => thread.id === childStop.body.threadId
|
||||
);
|
||||
assert(childThread, "executing child task must become a DAP virtual thread");
|
||||
assert.notStrictEqual(childThread.id, failThread.id);
|
||||
assert.match(childThread.name, /task trap/i);
|
||||
|
||||
const childStackRequest = restartClient.send("stackTrace", {
|
||||
threadId: childThread.id,
|
||||
startFrame: 0,
|
||||
levels: 1,
|
||||
});
|
||||
const childStack = (
|
||||
await restartClient.response(childStackRequest, "stackTrace")
|
||||
).body.stackFrames;
|
||||
assert.strictEqual(childStack[0].line, taskTrapLine);
|
||||
assert.match(childStack[0].name, /task_trap::wasm/);
|
||||
|
||||
const childScopesRequest = restartClient.send("scopes", {
|
||||
frameId: childStack[0].id,
|
||||
});
|
||||
const childScopes = (
|
||||
await restartClient.response(childScopesRequest, "scopes")
|
||||
).body.scopes;
|
||||
const childArgsScope = childScopes.find(
|
||||
(scope) => scope.name === "Task Args and Handles"
|
||||
);
|
||||
assert(childArgsScope, "child task argument scope must be present");
|
||||
const childArgsRequest = restartClient.send("variables", {
|
||||
variablesReference: childArgsScope.variablesReference,
|
||||
});
|
||||
const childArgs = (
|
||||
await restartClient.response(childArgsRequest, "variables")
|
||||
).body.variables;
|
||||
assert(
|
||||
childArgs.some(
|
||||
(variable) =>
|
||||
variable.name === "arg_0" &&
|
||||
/SmallJson\(Number\(0\)\)/.test(String(variable.value))
|
||||
),
|
||||
"child task argument must come from the frozen node participant"
|
||||
);
|
||||
|
||||
const parentScopesRequest = restartClient.send("scopes", {
|
||||
frameId: failedStack[0].id,
|
||||
});
|
||||
const parentScopes = (
|
||||
await restartClient.response(parentScopesRequest, "scopes")
|
||||
).body.scopes;
|
||||
const parentArgsScope = parentScopes.find(
|
||||
(scope) => scope.name === "Task Args and Handles"
|
||||
);
|
||||
const parentArgsRequest = restartClient.send("variables", {
|
||||
variablesReference: parentArgsScope.variablesReference,
|
||||
});
|
||||
const parentArgs = (
|
||||
await restartClient.response(parentArgsRequest, "variables")
|
||||
).body.variables;
|
||||
assert(
|
||||
parentArgs.some(
|
||||
(variable) =>
|
||||
/^task_handle_\d+$/.test(variable.name) &&
|
||||
/definition=task_trap instance=ti:.*:child:\d+ state=active/.test(
|
||||
variable.value
|
||||
) &&
|
||||
variable.type === "runtime-handle"
|
||||
),
|
||||
"parent task handle must come from its live Wasm host registry"
|
||||
);
|
||||
|
||||
const continueChildRequest = restartClient.send("continue", {
|
||||
threadId: childThread.id,
|
||||
});
|
||||
await restartClient.response(continueChildRequest, "continue");
|
||||
await restartClient.waitFor(
|
||||
(message) =>
|
||||
message.seq > childStop.seq &&
|
||||
message.type === "event" &&
|
||||
message.event === "terminated"
|
||||
);
|
||||
|
||||
const restartRequest = restartClient.send("restartFrame", {
|
||||
frameId: failedStack[0].id,
|
||||
});
|
||||
await restartClient.response(restartRequest, "restartFrame");
|
||||
const restartedStop = await restartClient.waitFor(
|
||||
(message) =>
|
||||
message.type === "event" &&
|
||||
message.event === "stopped" &&
|
||||
/Restarted main from the rebuilt bundle/.test(message.body.description)
|
||||
);
|
||||
assert.strictEqual(restartedStop.body.allThreadsStopped, true);
|
||||
const restartedStackRequest = restartClient.send("stackTrace", {
|
||||
threadId: restartedStop.body.threadId,
|
||||
startFrame: 0,
|
||||
levels: 1,
|
||||
});
|
||||
const restartedStack = (
|
||||
await restartClient.response(restartedStackRequest, "stackTrace")
|
||||
).body.stackFrames;
|
||||
assert.strictEqual(restartedStack[0].line, failMainLine);
|
||||
await restartClient.close();
|
||||
} catch (error) {
|
||||
if (restartClient.child.exitCode === null) restartClient.child.kill("SIGKILL");
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log("DAP smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
413
scripts/docs-smoke.js
Normal file
413
scripts/docs-smoke.js
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const readmePath = path.join(repo, "README.md");
|
||||
const filteredPublicTree = fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json"));
|
||||
if (!fs.existsSync(readmePath)) throw new Error("README.md is missing");
|
||||
const readme = fs.readFileSync(readmePath, "utf8");
|
||||
const canonicalPublicDocs = [
|
||||
"architecture.md",
|
||||
"security.md",
|
||||
"task-abi.md",
|
||||
"artifacts.md",
|
||||
"debugging.md",
|
||||
"self-hosting.md",
|
||||
];
|
||||
for (const file of canonicalPublicDocs) {
|
||||
const documentationPath = path.join(repo, "docs", file);
|
||||
assert(
|
||||
fs.existsSync(documentationPath),
|
||||
`canonical public documentation is missing docs/${file}`
|
||||
);
|
||||
assert(
|
||||
fs.readFileSync(documentationPath, "utf8").trim().length > 400,
|
||||
`canonical public documentation docs/${file} is unexpectedly empty`
|
||||
);
|
||||
}
|
||||
if (filteredPublicTree) {
|
||||
assert.match(readme, /^# Disasmer/m, "filtered public README must identify the product");
|
||||
assert.match(readme, /## Quickstart/, "filtered public README must retain the quickstart");
|
||||
assert.match(
|
||||
readme,
|
||||
/docs\/architecture\.md/,
|
||||
"filtered public README must link canonical public documentation"
|
||||
);
|
||||
assert.match(
|
||||
readme,
|
||||
/cargo install --path crates\/disasmer-cli --bin disasmer/,
|
||||
"filtered public README must retain CLI installation"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
readme,
|
||||
/DISASMER_PUBLISH_PUBLIC_TREE|DISASMER_FORGEJO_TOKEN|public-release-manifest\.json|public-release-dryrun-e2e\.json/,
|
||||
"filtered public README must not expose release-internal dry-run mechanics"
|
||||
);
|
||||
console.log("Docs smoke passed for filtered public product README");
|
||||
process.exit(0);
|
||||
}
|
||||
const dryrunDoc = fs.readFileSync(
|
||||
path.join(repo, "public_release_dryrun.md"),
|
||||
"utf8"
|
||||
);
|
||||
const userFacingDocs = [
|
||||
"README.md",
|
||||
"public_release_dryrun.md",
|
||||
"MVP.md",
|
||||
"acceptance_criteria.md",
|
||||
"acceptance_criteria_phase2.md",
|
||||
"cli_acceptance_criteria.md",
|
||||
"website_mvp_inventory.md",
|
||||
"phase_3_acceptance_criteria.md",
|
||||
].map((file) => [file, fs.readFileSync(path.join(repo, file), "utf8")]);
|
||||
const publicAcceptance = fs.readFileSync(
|
||||
path.join(repo, "scripts/acceptance-public.sh"),
|
||||
"utf8"
|
||||
);
|
||||
const publicSplit = fs.readFileSync(
|
||||
path.join(repo, "scripts/verify-public-split.sh"),
|
||||
"utf8"
|
||||
);
|
||||
const privateAcceptance = fs.readFileSync(
|
||||
path.join(repo, "scripts/acceptance-private.sh"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const requiredReadmePatterns = [
|
||||
["quickstart heading", /## Quickstart/],
|
||||
["workspace build", /cargo build --workspace/],
|
||||
["CLI install", /cargo install --path crates\/disasmer-cli --bin disasmer/],
|
||||
["node install", /cargo install --path crates\/disasmer-node --bin disasmer-node/],
|
||||
[
|
||||
"coordinator install",
|
||||
/cargo install --path crates\/disasmer-coordinator --bin disasmer-coordinator/,
|
||||
],
|
||||
["DAP install", /cargo install --path crates\/disasmer-dap --bin disasmer-debug-dap/],
|
||||
["VS Code local extension", /code --extensionDevelopmentPath/],
|
||||
["local coordinator", /disasmer-coordinator --listen/],
|
||||
["node attach", /disasmer node attach --coordinator/],
|
||||
["automatic local run", /disasmer run --local --project examples\/launch-build-demo build/],
|
||||
["demo run", /disasmer run --local --coordinator/],
|
||||
["entrypoint selection", /disasmer run \[entry\]/],
|
||||
["implicit hosted mode", /uses the hosted coordinator/],
|
||||
["local override", /force local coordinator mode/],
|
||||
["project override", /--project[\s\S]*overrides the project\s+directory/],
|
||||
["VS Code debug", /Disasmer: Launch\s+Virtual Process/],
|
||||
["artifact download smoke", /node scripts\/artifact-download-smoke\.js/],
|
||||
["artifact export smoke", /node scripts\/artifact-export-smoke\.js/],
|
||||
["acceptance report smoke", /node scripts\/acceptance-report-smoke\.js/],
|
||||
["CLI-first acceptance gate", /scripts\/acceptance-cli-first\.sh/],
|
||||
["CLI-first acceptance before e2e", /CLI-first non-e2e gate before any final public-release e2e attempt/],
|
||||
["public private boundary smoke", /node scripts\/public-private-boundary-smoke\.js/],
|
||||
["release blocker smoke", /node scripts\/release-blocker-smoke\.js/],
|
||||
["explicit export", /attached receiver node or user-provided\s+storage integration/],
|
||||
["cleanup", /Cleanup for the local quickstart/],
|
||||
["flush docs", /`flush\(\)` publishes metadata/],
|
||||
["sync docs", /`sync\(\)` is explicit/],
|
||||
["storage integration is user code", /User-provided storage\/export integrations are ordinary project code or external\s+commands/],
|
||||
["no managed artifact store feature", /does not provide\s+or manage an explicit artifact-store feature/],
|
||||
["best-effort retention", /best-effort retained on nodes/],
|
||||
[
|
||||
"secure downloads",
|
||||
/Download links are scoped to the tenant, project, process, artifact, actor, and policy context, expire after a bounded TTL, can be revoked/,
|
||||
],
|
||||
["node trust", /Users attach their own nodes for real work/],
|
||||
["self-hosted trusted teams", /self-hosted local clouds, trusted teams, or VPN deployments/],
|
||||
["self-hosted coordinator smoke", /node scripts\/self-hosted-coordinator-smoke\.js/],
|
||||
["wasmtime node smoke", /node scripts\/wasmtime-node-smoke\.js/],
|
||||
["wasmtime command host import", /versioned `disasmer\.command_run_v1` host capability/],
|
||||
["Windows sandbox limitation", /Production-grade managed Windows sandboxing is behind an explicit backend stub/],
|
||||
["hosted community limit", /community tier does not provide arbitrary hosted native commands or hosted containers/],
|
||||
["browser login", /disasmer login --browser/],
|
||||
["public-key agents", /disasmer agent enroll --public-key/],
|
||||
["noninteractive agent CLI", /DISASMER_AGENT_PRIVATE_KEY=<agent-private-key> disasmer run --non-interactive build/],
|
||||
["agent key lifecycle", /register, list, rotate, and revoke an agent key/],
|
||||
["capability auto-detect", /auto-detects OS, architecture/],
|
||||
["capability override", /--cap <name>/],
|
||||
["non-Git source provider", /Non-Git source providers can implement the public source-provider interface/],
|
||||
["first-run diagnostics", /## First-Run Diagnostics/],
|
||||
["missing nodes diagnostic", /Missing nodes/],
|
||||
["missing environment diagnostic", /Missing environments/],
|
||||
["quota diagnostic", /Quota limits/],
|
||||
["unavailable artifact diagnostic", /Unavailable artifacts/],
|
||||
["auth diagnostic", /Auth failures/],
|
||||
["debug freeze diagnostic", /Failed debug freezes/],
|
||||
["source-provider diagnostic", /Source-provider capability gaps/],
|
||||
["browser and VS Code report metadata", /browser\/VS Code harness metadata/],
|
||||
["Podman incomplete report", /Linux Podman backend behavior is marked `incomplete`/],
|
||||
["manual Windows validation workflow", /manual `Windows validation`\s+workflow/],
|
||||
["intermittent Forgejo Windows runner", /intermittent Windows runner/],
|
||||
["Windows validation env gate", /DISASMER_WINDOWS_VALIDATION = "forgejo-windows-runner"/],
|
||||
["Windows validation attach", /runs `disasmer node attach`/],
|
||||
];
|
||||
|
||||
const requiredDryrunPatterns = [
|
||||
["dry-run runbook status", /source-side release runbook, not public product quickstart/],
|
||||
["public dry-run hosted coordinator endpoint", /https:\/\/disasmer\.michelpaulissen\.com/],
|
||||
["public dry-run DNS record", /record is live[\s\S]*no resolver override is required|DNS record is deployed[\s\S]*no resolver override is required/],
|
||||
["public dry-run real deployment", /real externally reachable service/],
|
||||
["public dry-run selected users", /shared with selected users/],
|
||||
["public dry-run Forgejo repo", /git\.michelpaulissen\.com/],
|
||||
["public dry-run Forgejo Release assets", /Forgejo Release publishes compiled\s+assets/],
|
||||
["public dry-run filters internal root markdown", /internal root Markdown/],
|
||||
["public dry-run retains product README", /product-facing `README\.md` remains in the public repository/],
|
||||
["public dry-run filters Forgejo workflows by default", /\.forgejo\/\*\*` by\s+default/],
|
||||
["public dry-run Forgejo workflow opt in", /--include-forgejo-workflows/],
|
||||
["public dry-run selected-user quickstart", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\*\.md/],
|
||||
["public dry-run selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\*\.md/],
|
||||
["public dry-run resolver instructions env", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS=<instructions>/],
|
||||
["public dry-run fallback hosts entry env", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY="<ip-address> disasmer\.michelpaulissen\.com"/],
|
||||
["public dry-run deployment IP env", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP=<ip-address>/],
|
||||
["public dry-run prep script", /node scripts\/prepare-public-release-dryrun\.js/],
|
||||
["public dry-run publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE=1/],
|
||||
["public dry-run public repo remote", /DISASMER_PUBLIC_REPO_REMOTE=ssh:\/\/git\.michelpaulissen\.com/],
|
||||
["public dry-run Forgejo workflow", /Public release dry run assets/],
|
||||
["public dry-run manifest", /public-release-manifest\.json/],
|
||||
["public dry-run release publisher", /node scripts\/publish-public-release-dryrun\.js/],
|
||||
["public dry-run non-e2e preflight", /node scripts\/public-release-dryrun-preflight\.js/],
|
||||
["public dry-run Forgejo token", /DISASMER_FORGEJO_TOKEN=<token>/],
|
||||
["public dry-run publisher infers repo", /publisher infers the Forgejo owner and repository name/],
|
||||
["public dry-run Forgejo repo owner override", /DISASMER_PUBLIC_REPO_OWNER=<owner>/],
|
||||
["public dry-run Forgejo repo name override", /DISASMER_PUBLIC_REPO_NAME=<public-repo>/],
|
||||
["public dry-run service smoke", /public-release-dryrun-service-smoke\.js/],
|
||||
["public dry-run service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR=disasmer\.michelpaulissen\.com:443/],
|
||||
["public dry-run hosted coordinator", /hosted coordinator[\s\S]*disasmer\.michelpaulissen\.com[\s\S]*private\/hosted-policy/],
|
||||
["public dry-run distinguishes Core", /standalone Core coordinator remains available for local\/self-hosted use/],
|
||||
["public dry-run validates both coordinators", /dry-run acceptance validates both coordinator deployments/],
|
||||
["public dry-run validates Core coordinator separately", /standalone Core coordinator is validated separately/],
|
||||
["public browser login control entry", /POST https:\/\/disasmer\.michelpaulissen\.com\/api\/v1\/control/],
|
||||
["public browser login hosted callback", /hosted[\s\S]*\/auth\/callback|\/auth\/callback[\s\S]*hosted/],
|
||||
["public browser login server authority", /hosted service creates the OIDC state, nonce, and PKCE verifier/],
|
||||
["public browser test driver", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND/],
|
||||
["browser login plan diagnostic", /disasmer login --browser --plan/],
|
||||
["public dry-run deployment prep", /prepare-public-release-dryrun-deployment\.js/],
|
||||
["public dry-run systemd unit", /systemd unit[\s\S]*127\.0\.0\.1:9080|loopback[\s\S]*127\.0\.0\.1:9080/],
|
||||
["public dry-run e2e runner", /public-release-dryrun-e2e\.js/],
|
||||
["public dry-run e2e gate env", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/],
|
||||
["public dry-run e2e service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR=disasmer\.michelpaulissen\.com:443/],
|
||||
["public dry-run final verifier", /public-release-dryrun-final-evidence\.js/],
|
||||
["public dry-run final gate env", /DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL=1/],
|
||||
["public dry-run e2e evidence", /public-release-dryrun-e2e\.json/],
|
||||
["public dry-run e2e checks", /downloaded release assets[\s\S]*VS Code debugger behavior[\s\S]*artifact metadata/],
|
||||
["hosted Client compatibility smoke", /hosted-client-compat-smoke\.js/],
|
||||
["hosted Client compatibility purpose", /Client CLI browser login[\s\S]*scoped session[\s\S]*node enrollment[\s\S]*cross-tenant denial/],
|
||||
];
|
||||
|
||||
for (const [name, pattern] of requiredReadmePatterns) {
|
||||
assert.match(readme, pattern, `README missing ${name}`);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of requiredDryrunPatterns) {
|
||||
assert.match(dryrunDoc, pattern, `public release dry-run runbook missing ${name}`);
|
||||
}
|
||||
|
||||
assert.doesNotMatch(
|
||||
readme,
|
||||
/DISASMER_PUBLISH_PUBLIC_TREE|DISASMER_FORGEJO_TOKEN|public-release-manifest\.json|public-release-dryrun-e2e\.json/,
|
||||
"README must stay product-facing; release-internal dry-run commands belong in public_release_dryrun.md"
|
||||
);
|
||||
|
||||
for (const [file, contents] of userFacingDocs) {
|
||||
assert.doesNotMatch(
|
||||
contents,
|
||||
/community-tier/i,
|
||||
`${file} should use "community tier" in user-facing prose`
|
||||
);
|
||||
}
|
||||
|
||||
for (const script of [publicAcceptance, publicSplit]) {
|
||||
assert(
|
||||
script.includes("node scripts/docs-smoke.js"),
|
||||
"public acceptance gates must run docs-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/cli-install-smoke.js"),
|
||||
"public acceptance gates must run cli-install-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/cli-output-mode-smoke.js"),
|
||||
"public acceptance gates must run cli-output-mode-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/cli-login-smoke.js"),
|
||||
"public acceptance gates must run cli-login-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/cli-error-exit-smoke.js"),
|
||||
"public acceptance gates must run cli-error-exit-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/acceptance-report-smoke.js"),
|
||||
"public acceptance gates must run acceptance-report-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/acceptance-doc-contract-smoke.js"),
|
||||
"public acceptance gates must run acceptance-doc-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/acceptance-environment-contract-smoke.js"),
|
||||
"public acceptance gates must run acceptance-environment-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/acceptance-evidence-contract-smoke.js"),
|
||||
"public acceptance gates must run acceptance-evidence-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/public-private-boundary-smoke.js"),
|
||||
"public acceptance gates must run public-private-boundary-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/release-blocker-smoke.js"),
|
||||
"public acceptance gates must run release-blocker-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/resource-metering-contract-smoke.js"),
|
||||
"public acceptance gates must run resource-metering-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/hostile-input-contract-smoke.js"),
|
||||
"public acceptance gates must run hostile-input-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/tenant-isolation-contract-smoke.js"),
|
||||
"public acceptance gates must run tenant-isolation-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/public-story-contract-smoke.js"),
|
||||
"public acceptance gates must run public-story-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/public-release-dryrun-contract-smoke.js"),
|
||||
"public acceptance gates must run public-release-dryrun-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/prepare-public-release-dryrun.js"),
|
||||
"public acceptance gates must run prepare-public-release-dryrun.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/self-hosted-coordinator-smoke.js"),
|
||||
"public acceptance gates must run self-hosted-coordinator-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/public-local-demo-matrix-smoke.js"),
|
||||
"public acceptance gates must run public-local-demo-matrix-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("scripts/release-source-scan.sh"),
|
||||
"public acceptance gates must run release-source-scan.sh"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/flagship-demo-smoke.js"),
|
||||
"public acceptance gates must run flagship-demo-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/wasmtime-assignment-smoke.js"),
|
||||
"public acceptance gates must run the real coordinator-to-Wasm assignment smoke"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/sdk-spawn-runtime-smoke.js"),
|
||||
"public acceptance gates must run sdk-spawn-runtime-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/node-lifecycle-contract-smoke.js"),
|
||||
"public acceptance gates must run node-lifecycle-contract-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/artifact-export-smoke.js"),
|
||||
"public acceptance gates must run artifact-export-smoke.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/windows-validation-contract-smoke.js"),
|
||||
"public acceptance gates must run windows-validation-contract-smoke.js"
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
publicAcceptance.includes("node scripts/podman-backend-smoke.js"),
|
||||
"public acceptance must run the Linux Podman backend smoke when Podman is available"
|
||||
);
|
||||
|
||||
assert(
|
||||
publicAcceptance.includes("node scripts/wasmtime-node-smoke.js"),
|
||||
"public acceptance must run the Wasmtime node smoke"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/resource-metering-contract-smoke.js"),
|
||||
"private acceptance must run resource-metering-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/hostile-input-contract-smoke.js"),
|
||||
"private acceptance must run hostile-input-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/tenant-isolation-contract-smoke.js"),
|
||||
"private acceptance must run tenant-isolation-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/acceptance-doc-contract-smoke.js"),
|
||||
"private acceptance must run acceptance-doc-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/acceptance-environment-contract-smoke.js"),
|
||||
"private acceptance must run acceptance-environment-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/acceptance-evidence-contract-smoke.js"),
|
||||
"private acceptance must run acceptance-evidence-contract-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-deployment-smoke.js"),
|
||||
"private acceptance must run hosted-deployment-smoke.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-client-compat-smoke.js"),
|
||||
"private acceptance must run hosted-client-compat-smoke.js"
|
||||
);
|
||||
assert(
|
||||
privateAcceptance.includes("node scripts/self-hosted-coordinator-smoke.js"),
|
||||
"private acceptance must run self-hosted-coordinator-smoke.js before final dry-run evidence"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js"),
|
||||
"private acceptance must prepare public release dry-run deployment bundle"
|
||||
);
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js"),
|
||||
"private acceptance must be able to run public-release-dryrun-service-smoke.js"
|
||||
);
|
||||
|
||||
for (const script of [publicAcceptance, privateAcceptance]) {
|
||||
assert(
|
||||
script.includes("node scripts/public-release-dryrun-final-evidence.js"),
|
||||
"acceptance must be able to run public-release-dryrun-final-evidence.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"),
|
||||
"acceptance must gate public-release-dryrun-final-evidence.js"
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
publicAcceptance.includes("node scripts/public-release-dryrun-e2e.js"),
|
||||
"public acceptance must be able to run public-release-dryrun-e2e.js"
|
||||
);
|
||||
|
||||
assert(
|
||||
publicAcceptance.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"),
|
||||
"public acceptance must gate public-release-dryrun-e2e.js"
|
||||
);
|
||||
|
||||
console.log("Docs smoke passed");
|
||||
148
scripts/flagship-demo-smoke.js
Normal file
148
scripts/flagship-demo-smoke.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const extension = require("../vscode-extension/extension");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
const source = fs.readFileSync(path.join(project, "src/build.rs"), "utf8");
|
||||
const forbiddenSourceAssumptions =
|
||||
/\b(?:std::fs|std::process|git|podman|docker|localhost|127\.0\.0\.1)|\/home\/|\/Users\/|C:\\Users\\/i;
|
||||
|
||||
const envs = extension.discoverEnvironmentNames(project);
|
||||
assert.deepStrictEqual(envs, ["linux", "windows"]);
|
||||
assert.deepStrictEqual(extension.diagnoseEnvReferences(source, envs), []);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
forbiddenSourceAssumptions,
|
||||
"flagship build source must not bypass Disasmer host capabilities or rely on coordinator-side filesystem, Git, container, or machine-local assumptions"
|
||||
);
|
||||
|
||||
const inspection = JSON.parse(
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"bundle",
|
||||
"inspect",
|
||||
"--project",
|
||||
project,
|
||||
"--json"
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
|
||||
assert.strictEqual(inspection.project, project);
|
||||
assert.strictEqual(
|
||||
inspection.source_provider_manifest.coordinator_requires_checkout_access,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspection.source_provider_manifest.transfer_policy.local_source_bytes_remain_node_local,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspection.source_provider_manifest.transfer_policy.coordinator_receives_source_bytes_by_default,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
inspection.source_provider_manifest.transfer_policy.default_full_repo_tarball,
|
||||
false
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
inspection.source_provider_manifest.transfer_policy.allowed_remote_transfer.sort(),
|
||||
["ExplicitSnapshotChunks", "RequiredContent"]
|
||||
);
|
||||
assert.strictEqual(inspection.metadata.embeds_full_container_images, false);
|
||||
assert(inspection.metadata.environments.some((env) => env.name === "linux"));
|
||||
assert(inspection.metadata.environments.some((env) => env.name === "windows"));
|
||||
assert(inspection.metadata.selected_inputs.some((input) => input.path === "src/build.rs"));
|
||||
|
||||
const bundleDirectory = path.join(repo, "target/acceptance/flagship-bundle");
|
||||
const build = JSON.parse(
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"build",
|
||||
"--project",
|
||||
project,
|
||||
"--output",
|
||||
bundleDirectory,
|
||||
"--json",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(build.status, "built");
|
||||
assert.strictEqual(build.bundle_artifact.task_descriptor_count, 9);
|
||||
assert.strictEqual(build.bundle_artifact.entrypoint_count, 3);
|
||||
assert.deepStrictEqual(build.bundle_artifact.files.sort(), [
|
||||
"debug-metadata.json",
|
||||
"entrypoints.json",
|
||||
"environments.json",
|
||||
"manifest.json",
|
||||
"module.wasm",
|
||||
"source-provider.json",
|
||||
"task-descriptors.json",
|
||||
"vfs-seed.json",
|
||||
]);
|
||||
const bundleManifest = JSON.parse(
|
||||
fs.readFileSync(path.join(bundleDirectory, "manifest.json"), "utf8")
|
||||
);
|
||||
const bundleEntrypoints = JSON.parse(
|
||||
fs.readFileSync(path.join(bundleDirectory, "entrypoints.json"), "utf8")
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
bundleEntrypoints.map((entrypoint) => entrypoint.name).sort(),
|
||||
["build", "fail", "restart"]
|
||||
);
|
||||
const bundleModule = fs.readFileSync(path.join(bundleDirectory, "module.wasm"));
|
||||
assert.strictEqual(bundleManifest.kind, "disasmer-bundle");
|
||||
assert.strictEqual(
|
||||
bundleManifest.bundle_digest,
|
||||
`sha256:${crypto.createHash("sha256").update(bundleModule).digest("hex")}`
|
||||
);
|
||||
assert.strictEqual(bundleManifest.embeds_full_repository, false);
|
||||
assert.strictEqual(
|
||||
bundleManifest.coordinator_receives_source_bytes_by_default,
|
||||
false
|
||||
);
|
||||
const taskDescriptors = JSON.parse(
|
||||
fs.readFileSync(path.join(bundleDirectory, "task-descriptors.json"), "utf8")
|
||||
);
|
||||
assert(
|
||||
taskDescriptors.some(
|
||||
(task) =>
|
||||
task.name === "task_add_one" &&
|
||||
task.argument_schema === "input : i32" &&
|
||||
task.result_schema === "i32" &&
|
||||
task.restart_compatibility_hash.startsWith("sha256:") &&
|
||||
task.probe_symbol === "disasmer.probe.task_add_one"
|
||||
)
|
||||
);
|
||||
|
||||
cp.execFileSync("cargo", ["test", "-p", "launch-build-demo"], {
|
||||
cwd: repo,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
console.log("Flagship demo smoke passed");
|
||||
198
scripts/hostile-input-contract-smoke.js
Normal file
198
scripts/hostile-input-contract-smoke.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(segments) {
|
||||
const fullPath = path.join(repo, ...segments);
|
||||
if (!fs.existsSync(fullPath)) return null;
|
||||
return fs.readFileSync(fullPath, "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing hostile-input evidence: ${name}`);
|
||||
}
|
||||
|
||||
function expectGate(script, gateName) {
|
||||
assert(
|
||||
script.includes("node scripts/hostile-input-contract-smoke.js"),
|
||||
`${gateName} must run hostile-input-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
const coreSource = read("crates/disasmer-core/src/source.rs");
|
||||
const coreCapabilities = read("crates/disasmer-core/src/capability.rs");
|
||||
const coordinatorService = [
|
||||
read("crates/disasmer-coordinator/src/service.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/routing.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/signed_nodes.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/logs.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/tests.rs"),
|
||||
].join("\n");
|
||||
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
||||
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
|
||||
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
|
||||
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["source manifests validate shape", /pub fn validate_public_mvp\(&self\)[\s\S]*self\.validate_shape\(\)\?/],
|
||||
["source manifests reject invalid digests", /SourceManifestError::InvalidDigest/],
|
||||
["source manifests reject invalid custom providers", /SourceManifestError::InvalidProviderId/],
|
||||
["source manifests reject control characters", /DescriptionControlCharacter/],
|
||||
["source manifests reject coordinator checkout access", /CoordinatorCheckoutAccess/],
|
||||
["source manifests reject default source-byte upload", /CoordinatorReceivesSourceBytes/],
|
||||
]) {
|
||||
expect(coreSource, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["capability reports validate public shape", /pub fn validate_public_report\(&self\)/],
|
||||
["capability reports validate architecture labels", /InvalidArchitecture/],
|
||||
["capability reports validate OS labels", /InvalidOsLabel/],
|
||||
["capability reports validate source providers", /InvalidSourceProvider/],
|
||||
["source provider ids reject path traversal", /valid_source_provider_id/],
|
||||
]) {
|
||||
expect(coreCapabilities, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["coordinator task log tails are bounded", /MAX_TASK_LOG_TAIL_BYTES: usize = 256 \* 1024/],
|
||||
["coordinator validates reported stdout tails", /ReportTaskLog[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
|
||||
["coordinator validates completed task stdout tails", /TaskCompleted[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
|
||||
["coordinator rejects oversized log tail in unit coverage", /"x"\.repeat\(MAX_TASK_LOG_TAIL_BYTES \+ 1\)/],
|
||||
]) {
|
||||
expect(coordinatorService, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["service rejects malformed node capability report", /fn service_rejects_malformed_node_capability_report\(\)/],
|
||||
["capability report rejection leaves descriptors empty", /assert!\(service\.node_descriptors\.is_empty\(\)\)/],
|
||||
["node capability report rejects cross-scope writes", /fn service_rejects_node_capability_report_outside_enrollment_scope\(\)/],
|
||||
["task completion rejects cross-scope writes", /task completion outside node scope|outside/],
|
||||
]) {
|
||||
expect(coordinatorService, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, source, patterns] of [
|
||||
[
|
||||
"artifact download smoke",
|
||||
artifactDownloadSmoke,
|
||||
[
|
||||
/const crossTenant = await send/,
|
||||
/const crossProject = await send/,
|
||||
/const guessed = await send/,
|
||||
/const crossActorOpen = await send/,
|
||||
/token is invalid/,
|
||||
/tenant mismatch/,
|
||||
/project mismatch/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"operator panel smoke",
|
||||
operatorPanelSmoke,
|
||||
[
|
||||
/render_operator_panel/,
|
||||
/submit_panel_event/,
|
||||
/assert\(!JSON\.stringify\(panel\)\.includes\("<script"\)\)/,
|
||||
/assert\(!JSON\.stringify\(panel\)\.toLowerCase\(\)\.includes\("oauth"\)\)/,
|
||||
/rate limit/i,
|
||||
/exceeds download limit/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"scheduler smoke",
|
||||
schedulerSmoke,
|
||||
[/const crossTenantReport = await send/, /report_node_capabilities/, /tenant\\\/project scope/],
|
||||
],
|
||||
[
|
||||
"source preparation smoke",
|
||||
sourcePreparationSmoke,
|
||||
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
|
||||
],
|
||||
]) {
|
||||
for (const pattern of patterns) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
expectGate(publicAcceptance, "public acceptance");
|
||||
expectGate(publicSplit, "public split acceptance");
|
||||
expectGate(privateAcceptance, "private acceptance");
|
||||
|
||||
const hostedServiceMain = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"src",
|
||||
"bin",
|
||||
"disasmer-hosted-service.rs",
|
||||
]);
|
||||
const hostedValidation = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"src",
|
||||
"bin",
|
||||
"disasmer-hosted-service",
|
||||
"validation.rs",
|
||||
]);
|
||||
const hostedWire = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"src",
|
||||
"bin",
|
||||
"disasmer-hosted-service",
|
||||
"wire.rs",
|
||||
]);
|
||||
const hostedProtocol = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"src",
|
||||
"bin",
|
||||
"disasmer-hosted-service",
|
||||
"hosted_service_protocol.rs",
|
||||
]);
|
||||
const hostedService =
|
||||
hostedServiceMain && hostedValidation && hostedWire && hostedProtocol
|
||||
? [hostedServiceMain, hostedValidation, hostedWire, hostedProtocol].join("\n")
|
||||
: null;
|
||||
const hostedTests = [
|
||||
maybeRead(["private", "hosted-policy", "src", "bin", "disasmer-hosted-service", "tests.rs"]),
|
||||
maybeRead(["private", "hosted-policy", "scripts", "hosted-deployment-smoke.js"]),
|
||||
maybeRead(["private", "hosted-policy", "scripts", "hosted-client-compat-smoke.js"]),
|
||||
].filter(Boolean).join("\n");
|
||||
|
||||
if (hostedService && hostedTests) {
|
||||
for (const [name, pattern] of [
|
||||
["hosted service turns malformed JSON into error responses", /decode_incoming_request[\s\S]*HostedServiceResponse::Error/],
|
||||
["tenant ids are validated", /fn tenant_id\(value: String\)[\s\S]*validate_identifier\("tenant", &value\)\?/],
|
||||
["node ids are validated", /fn node_id\(value: String\)[\s\S]*validate_identifier\("node", &value\)\?/],
|
||||
["process ids are validated", /fn process_id\(value: String\)[\s\S]*validate_identifier\("process", &value\)\?/],
|
||||
["identifiers reject empty and control/path characters", /fn validate_identifier[\s\S]*trim\(\)\.is_empty\(\)[\s\S]*ch\.is_control\(\) \|\| ch == '\/' \|\| ch == '\\\\'/],
|
||||
["OIDC text fields are bounded", /fn validate_text[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
||||
["tokens are bounded", /fn validate_token[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
||||
["control request bodies are bounded", /MAX_CONTROL_FRAME_BYTES[\s\S]*control request too large/],
|
||||
["identity protocol rejects unknown authority fields", /deny_unknown_fields/],
|
||||
]) {
|
||||
expect(hostedService, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["old client identity protocol is rejected", /hosted_login_protocol_rejects_client_identity_and_provider_configuration/],
|
||||
["raw operator action is rejected", /rawOperatorDenied[\s\S]*hosted_operator_request envelope/],
|
||||
["unsigned client identity is rejected", /const forged = await sendHostedControl[\s\S]*authenticated CLI session/],
|
||||
["cross-tenant process inspection is rejected", /crossTenantTaskEventsDenied[\s\S]*scope\|denied\|unauthorized/],
|
||||
]) {
|
||||
expect(hostedTests, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Hostile input contract smoke passed");
|
||||
300
scripts/node-attach-smoke.js
Executable file
300
scripts/node-attach-smoke.js
Executable file
|
|
@ -0,0 +1,300 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const identities = new Map();
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function nodeIdentity(node) {
|
||||
const existing = identities.get(node);
|
||||
if (existing) return existing;
|
||||
const { privateKey: privateKeyObject, publicKey } =
|
||||
crypto.generateKeyPairSync("ed25519");
|
||||
const privateDer = privateKeyObject.export({ format: "der", type: "pkcs8" });
|
||||
const publicDer = publicKey.export({
|
||||
format: "der",
|
||||
type: "spki",
|
||||
});
|
||||
const privateSeed = Buffer.from(privateDer).subarray(-32);
|
||||
const identity = {
|
||||
privateKey: `ed25519:${privateSeed.toString("base64")}`,
|
||||
publicKey: `ed25519:${Buffer.from(publicDer).subarray(-32).toString("base64")}`,
|
||||
privateKeyObject,
|
||||
};
|
||||
identities.set(node, identity);
|
||||
return identity;
|
||||
}
|
||||
|
||||
function nodeSignatureMessage(
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
issuedAtEpochSeconds
|
||||
) {
|
||||
const parts = [
|
||||
"disasmer-node-request-signature:v2",
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
String(issuedAtEpochSeconds),
|
||||
];
|
||||
return Buffer.concat(
|
||||
parts.flatMap((part) => [
|
||||
Buffer.from(`${Buffer.byteLength(part)}:`),
|
||||
Buffer.from(part),
|
||||
Buffer.from("\n"),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function signedNodeHeartbeat(node, identity) {
|
||||
const nonce = `node-attach-heartbeat-${process.pid}-${Date.now()}`;
|
||||
const issuedAt = Math.floor(Date.now() / 1000);
|
||||
const payloadDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(JSON.stringify({ node, type: "node_heartbeat" }))
|
||||
.digest("hex")}`;
|
||||
const signature = crypto.sign(
|
||||
null,
|
||||
nodeSignatureMessage(node, "node_heartbeat", payloadDigest, nonce, issuedAt),
|
||||
identity.privateKeyObject
|
||||
);
|
||||
return {
|
||||
nonce,
|
||||
issued_at_epoch_seconds: issuedAt,
|
||||
signature: `ed25519:${signature.toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function runAttach(addr, grant) {
|
||||
const identity = nodeIdentity("node-attach");
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"node",
|
||||
"attach",
|
||||
"--coordinator",
|
||||
`${addr.host}:${addr.port}`,
|
||||
"--tenant",
|
||||
"tenant",
|
||||
"--project-id",
|
||||
"project",
|
||||
"--node",
|
||||
"node-attach",
|
||||
"--public-key",
|
||||
identity.publicKey,
|
||||
"--enrollment-grant",
|
||||
grant,
|
||||
"--cap",
|
||||
"quic-direct",
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_NODE_PRIVATE_KEY: identity.privateKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`node attach failed with code ${code}\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(`node attach output was not JSON: ${stdout}\n${error.stack || error.message}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback",
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const grant = await send(addr, {
|
||||
type: "create_node_enrollment_grant",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
ttl_seconds: 900
|
||||
});
|
||||
assert.strictEqual(grant.type, "node_enrollment_grant_created");
|
||||
assert.strictEqual(grant.tenant, "tenant");
|
||||
assert.strictEqual(grant.project, "project");
|
||||
assert.match(grant.grant, /^node_grant_[A-Za-z0-9_-]+$/);
|
||||
assert.strictEqual(grant.scope, "node:attach");
|
||||
assert(grant.expires_at_epoch_seconds > Math.floor(Date.now() / 1000));
|
||||
assert(grant.expires_at_epoch_seconds <= Math.floor(Date.now() / 1000) + 900);
|
||||
|
||||
const report = await runAttach(addr, grant.grant);
|
||||
assert.strictEqual(report.plan.node, "node-attach");
|
||||
assert.strictEqual(report.plan.coordinator, `${addr.host}:${addr.port}`);
|
||||
assert.strictEqual(report.plan.enrollment.grant, grant.grant);
|
||||
assert.match(report.plan.enrollment.public_key_fingerprint, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.strictEqual(
|
||||
report.plan.enrollment.exchanges_short_lived_grant_for_long_lived_node_identity,
|
||||
true
|
||||
);
|
||||
assert.ok(report.plan.capabilities.arch.length > 0);
|
||||
assert.ok(report.plan.capabilities.source_providers.includes("filesystem"));
|
||||
assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect"));
|
||||
assert.strictEqual(report.plan.detection.auto_detected, true);
|
||||
assert.strictEqual(report.plan.detection.arch, report.plan.capabilities.arch);
|
||||
assert.deepStrictEqual(report.plan.detection.manual_capability_overrides, ["quic-direct"]);
|
||||
assert(
|
||||
report.plan.detection.recognized_capability_overrides.includes("QuicDirect")
|
||||
);
|
||||
assert.strictEqual(
|
||||
report.plan.detection.os_arch_capabilities_require_manual_flags,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(report.plan.detection.command_backend, "native-command");
|
||||
assert.strictEqual(report.plan.detection.command_backend_available, true);
|
||||
assert(
|
||||
report.plan.detection.source_provider_backends.some(
|
||||
(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);
|
||||
assert.strictEqual(report.coordinator_response.type, "node_enrollment_exchanged");
|
||||
assert.strictEqual(report.coordinator_response.node, "node-attach");
|
||||
assert.strictEqual(report.coordinator_response.credential.node, "node-attach");
|
||||
assert.strictEqual(report.coordinator_response.credential.scope, "node:attach");
|
||||
assert.strictEqual(report.coordinator_response.credential.credential_kind, "NodeCredential");
|
||||
assert.match(
|
||||
report.coordinator_response.credential.capability_policy_digest,
|
||||
/^sha256:[0-9a-f]{64}$/
|
||||
);
|
||||
assert.strictEqual(report.heartbeat_response.type, "node_heartbeat");
|
||||
assert.strictEqual(report.capability_response.type, "node_capabilities_recorded");
|
||||
|
||||
const heartbeat = await send(addr, {
|
||||
type: "node_heartbeat",
|
||||
node: "node-attach",
|
||||
node_signature: signedNodeHeartbeat("node-attach", nodeIdentity("node-attach")),
|
||||
});
|
||||
assert.strictEqual(heartbeat.type, "node_heartbeat");
|
||||
assert.strictEqual(heartbeat.node, "node-attach");
|
||||
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Node attach smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
164
scripts/node-lifecycle-contract-smoke.js
Executable file
164
scripts/node-lifecycle-contract-smoke.js
Executable file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing node lifecycle evidence: ${name}`);
|
||||
}
|
||||
|
||||
const nodeMain = read("crates/disasmer-node/src/daemon.rs");
|
||||
const nodeIdentity = read("crates/disasmer-node/src/node_identity.rs");
|
||||
const cliNode = read("crates/disasmer-cli/src/node.rs");
|
||||
const nodeTaskReports = read("crates/disasmer-node/src/task_reports.rs");
|
||||
const nodeDebugAgent = read("crates/disasmer-node/src/debug_agent.rs");
|
||||
const nodeLib = read("crates/disasmer-node/src/lib.rs");
|
||||
const sharedWasmtimeRuntime = `${read("crates/disasmer-wasm-runtime/src/lib.rs")}\n${read("crates/disasmer-wasm-runtime/src/task_host_linker.rs")}`;
|
||||
const nodeRuntimeSurface = `${nodeLib}\n${sharedWasmtimeRuntime}`;
|
||||
const nodeLifecycleSurface = `${nodeMain}\n${nodeIdentity}\n${nodeTaskReports}\n${nodeDebugAgent}`;
|
||||
const nodeAssignmentRunner = `${read("crates/disasmer-node/src/assignment_runner.rs")}\n${read("crates/disasmer-node/src/assignment_runner/control_watcher.rs")}\n${read("crates/disasmer-node/src/assignment_runner/process_runner.rs")}\n${read("crates/disasmer-node/src/assignment_runner/validation.rs")}`;
|
||||
const coordinatorCore = read("crates/disasmer-coordinator/src/lib.rs");
|
||||
const coordinatorService = `${read("crates/disasmer-coordinator/src/service.rs")}\n${read("crates/disasmer-coordinator/src/service/routing.rs")}`;
|
||||
const coordinatorServiceTests = read("crates/disasmer-coordinator/src/service/tests.rs");
|
||||
const coordinatorServiceSurface = `${coordinatorService}\n${coordinatorServiceTests}`;
|
||||
const wasmtimeAssignmentSmoke = read("scripts/wasmtime-assignment-smoke.js");
|
||||
const wasmtimeSmoke = read("scripts/wasmtime-node-smoke.js");
|
||||
const debugCore = read("crates/disasmer-core/src/debug.rs");
|
||||
const readme = read("README.md");
|
||||
|
||||
assert.strictEqual(
|
||||
(nodeMain.match(/CoordinatorSession::connect/g) || []).length,
|
||||
1,
|
||||
"node runtime should open one coordinator session in the local process-boundary runtime"
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["enrollment exchange over session", /"type": "exchange_node_enrollment_grant"/],
|
||||
["persisted node identity is reused locally", /"type": "node_identity_reused"/],
|
||||
["heartbeat over session", /"type": "node_heartbeat"/],
|
||||
["node-originated requests use signed envelope", /"type": "signed_node"/],
|
||||
["capability report over session", /"type": "report_node_capabilities"/],
|
||||
["task assignment polling over session", /"type": "poll_task_assignment"/],
|
||||
["process start over session", /"type": "start_process"/],
|
||||
["reconnect over session", /"type": "reconnect_node"/],
|
||||
["debug command polling over session", /"type": "poll_debug_command"/],
|
||||
["log event over session", /"type": "report_task_log"/],
|
||||
["VFS metadata over session", /"type": "report_vfs_metadata"/],
|
||||
["task control polling over session", /"type": "poll_task_control"/],
|
||||
["completion over session", /"type": "task_completed"/],
|
||||
["cancellation uses same session", /poll_task_cancellation\(session, args, &task, node_private_key\)/],
|
||||
["request count is reported", /session\.requests\(\)/],
|
||||
]) {
|
||||
expect(nodeLifecycleSurface, name, pattern);
|
||||
}
|
||||
|
||||
expect(cliNode, "user-authorized node attach over Client session", /"type": "attach_node"/);
|
||||
assert.doesNotMatch(
|
||||
nodeIdentity,
|
||||
/"type": "attach_node"/,
|
||||
"a persisted node must authenticate with its signed identity instead of replaying Client attach"
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["worker uses enrollment exchange", /workerReady\.node_status, "ready"/],
|
||||
["worker receives the task instance selected by the SDK runtime", /nodeRun\.task_assignment_response\.task,[\s\S]*sdkRun\.task_spec\.task_instance/],
|
||||
["worker verifies task ABI", /task_assignment_response\.task_spec\.dispatch\.abi/],
|
||||
["worker records task completion", /nodeRun\.coordinator_response\.type, "task_recorded"/],
|
||||
["worker exposes task result", /events\.events\[0\]\.result, \{ SmallJson: 42 \}/],
|
||||
["Wasm task observes cooperative cancellation", /task_definition: "cooperative_cancellation_probe"[\s\S]*type: "cancel_process"/],
|
||||
["cooperative cancellation returns under task control", /cancellationNodeRun\.terminal_state, "completed"/],
|
||||
["running command abort is exercised", /task_definition: "abort_probe"[\s\S]*type: "abort_process"/],
|
||||
["running command abort reaches terminal state", /abortedNodeRun\.terminal_state, "cancelled"/],
|
||||
["running native command receives a real Debug Epoch freeze", /waitForNativeSleep[\s\S]*type: "create_debug_epoch"[\s\S]*fully_frozen/],
|
||||
["native Linux process is observed stopped and resumed", /assert\.match\(procState\(nativeSleepPid\)[\s\S]*stopped[\s\S]*resume_debug_epoch[\s\S]*fully_resumed[\s\S]*assert\.doesNotMatch\(procState\(nativeSleepPid\)/],
|
||||
["parent and child Wasm tasks are frozen as one process", /task_definition: "debug_parent_probe"[\s\S]*debugChildInstance = "debug_parent_probe-1:child:1"[\s\S]*all-stop across active parent and child Wasm participants[\s\S]*affected_tasks\.length, 2[\s\S]*acknowledgements\.length, 2[\s\S]*\[debugChildInstance, "debug_parent_probe-1"\]/],
|
||||
["parent and child Wasm tasks both acknowledge resume", /multiParticipantResume[\s\S]*fully_resumed[\s\S]*acknowledgements\.length, 2/],
|
||||
["abort releases process slot", /afterAbort\.processes, \[\]/],
|
||||
]) {
|
||||
expect(wasmtimeAssignmentSmoke, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["cooperative cancellation and abort are distinct", /cancel_requested: false,[\s\S]*abort_requested: true/],
|
||||
["controlled runner polls abort while command runs", /fn abort_requested[\s\S]*poll_task_control/],
|
||||
["controlled runner creates a process group", /process\.process_group\(0\)/],
|
||||
["controlled runner freezes the native process group", /libc::kill\(process_group, libc::SIGSTOP\)/],
|
||||
["controlled runner resumes the native process group", /libc::kill\(process_group, libc::SIGCONT\)/],
|
||||
["controlled runner kills the process group", /libc::kill\(process_group, libc::SIGKILL\)/],
|
||||
["Wasm code can poll cooperative cancellation", /task_control_v1/],
|
||||
["matched Wasm probes remain at a quiescent boundary", /TaskHostOperation::DebugProbe[\s\S]*enter_quiescent_host_boundary[\s\S]*leave_quiescent_host_boundary/],
|
||||
["debug snapshots use the live Wasm task handle registry", /debug_handle_snapshot[\s\S]*task_handle_\{handle_id\}[\s\S]*state=active/],
|
||||
["native command status comes from the controlled runner", /set_command_status[\s\S]*frozen native command pid[\s\S]*native command exited with status/],
|
||||
]) {
|
||||
expect(`${coordinatorServiceSurface}\n${nodeLifecycleSurface}\n${sharedWasmtimeRuntime}\n${nodeAssignmentRunner}`, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["coordinator rejects stale process ownership", /fn node_reconnect_rejects_stale_process_epoch_after_restart\(\)/],
|
||||
["reconnect preserves enrolled node identity", /reconnect_node\(&NodeId::from\("node"\), None\)/],
|
||||
["stale process epoch is rejected", /CoordinatorError::StaleProcessEpoch/],
|
||||
]) {
|
||||
expect(coordinatorCore, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["coordinator delivers cancellation to connected node", /fn service_delivers_cancellation_to_connected_node_and_records_terminal_state\(\)/],
|
||||
["node polls task control", /CoordinatorRequest::PollTaskControl/],
|
||||
["cancelled terminal state is recorded", /TaskTerminalState::Cancelled/],
|
||||
]) {
|
||||
expect(coordinatorServiceSurface, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["native lifecycle test exists", /fn linux_task_lifecycle_supports_cancel_and_all_stop_freeze_resume\(\)/],
|
||||
["native freeze succeeds when supported", /lifecycle\.freeze_for_debug_epoch\(\)\.unwrap\(\)/],
|
||||
["native resume succeeds", /lifecycle\.resume_after_debug_epoch\(\)/],
|
||||
["native cancel reaches lifecycle", /lifecycle\.cancel\(\)/],
|
||||
["unsupported freeze errors", /BackendError::DebugFreezeUnsupported/],
|
||||
["wasmtime runtime exposes freeze resume probe", /pub fn freeze_resume_i32_export_probe/],
|
||||
["wasmtime runtime captures Wasm frame locals", /debug_i32_export_snapshot[\s\S]*local_values/],
|
||||
["wasmtime runtime creates Wasm debug participant", /kind: DebugParticipantKind::WasmTask/],
|
||||
["wasmtime debug participant carries local values", /local_values: snapshot\.local_values\.clone\(\)/],
|
||||
["wasmtime runtime resumes after freeze", /epoch\.continue_all\(\)/],
|
||||
]) {
|
||||
expect(nodeRuntimeSurface, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["wasmtime smoke runs debug freeze resume mode", /--debug-freeze-resume/],
|
||||
["wasmtime smoke verifies frozen state", /debugReport\.frozen_state, "Frozen"/],
|
||||
["wasmtime smoke verifies resumed state", /debugReport\.resumed_state, "Running"/],
|
||||
["wasmtime smoke verifies frame local values", /debugReport\.local_values[\s\S]*wasm_local_0/],
|
||||
["wasmtime smoke proves node runtime reached wasm task", /node_runtime_reached_wasm_task/],
|
||||
["wasmtime smoke proves node captured locals", /node_runtime_captured_wasm_locals/],
|
||||
]) {
|
||||
expect(wasmtimeSmoke, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["debug model freezes wasm and command participants", /fn breakpoint_creates_all_stop_debug_epoch_for_wasm_and_command_tasks\(\)/],
|
||||
["debug model rejects unsupported freeze", /fn debug_epoch_reports_freeze_failure_instead_of_claiming_all_stop\(\)/],
|
||||
["debug model resumes frozen participants", /fn continue_resumes_every_frozen_participant\(\)/],
|
||||
["debug model includes captured locals", /local_values/],
|
||||
["wasm participants are modeled", /DebugParticipantKind::WasmTask/],
|
||||
["controlled native command participants are modeled", /DebugParticipantKind::ControlledNativeCommand/],
|
||||
]) {
|
||||
expect(debugCore, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["docs describe long-lived coordinator and worker processes", /real-flagship-harness\.js[\s\S]*separate long-lived processes/],
|
||||
["docs distinguish cooperative cancellation from forced abort", /cooperative cancellation observed by task code and forced abort of uncooperative Wasm\/native work/],
|
||||
["docs describe failed freeze diagnostic", /Failed debug freezes/],
|
||||
]) {
|
||||
expect(readme, name, pattern);
|
||||
}
|
||||
|
||||
console.log("Node lifecycle contract smoke passed");
|
||||
177
scripts/node-signing.js
Normal file
177
scripts/node-signing.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
const crypto = require("crypto");
|
||||
const identities = new Map();
|
||||
|
||||
function nodeIdentity(identityPurpose, node) {
|
||||
const identityKey = `${identityPurpose}:${node}`;
|
||||
const existing = identities.get(identityKey);
|
||||
if (existing) return existing;
|
||||
const { privateKey: privateKeyObject, publicKey } =
|
||||
crypto.generateKeyPairSync("ed25519");
|
||||
const privateDer = privateKeyObject.export({ format: "der", type: "pkcs8" });
|
||||
const publicDer = publicKey.export({
|
||||
format: "der",
|
||||
type: "spki",
|
||||
});
|
||||
const privateSeed = Buffer.from(privateDer).subarray(-32);
|
||||
const identity = {
|
||||
privateKey: `ed25519:${privateSeed.toString("base64")}`,
|
||||
publicKey: `ed25519:${Buffer.from(publicDer).subarray(-32).toString("base64")}`,
|
||||
privateKeyObject,
|
||||
};
|
||||
identities.set(identityKey, identity);
|
||||
return identity;
|
||||
}
|
||||
|
||||
function nodeIdentityFromPrivateKey(privateKey) {
|
||||
if (typeof privateKey !== "string" || !privateKey.startsWith("ed25519:")) {
|
||||
throw new Error("node private key must use ed25519:<base64> encoding");
|
||||
}
|
||||
const seed = Buffer.from(privateKey.slice("ed25519:".length), "base64");
|
||||
if (seed.length !== 32) throw new Error("node private key must contain 32 bytes");
|
||||
const privateKeyObject = crypto.createPrivateKey({
|
||||
key: Buffer.concat([
|
||||
Buffer.from("302e020100300506032b657004220420", "hex"),
|
||||
seed,
|
||||
]),
|
||||
format: "der",
|
||||
type: "pkcs8",
|
||||
});
|
||||
const publicKeyObject = crypto.createPublicKey(privateKeyObject);
|
||||
const publicDer = publicKeyObject.export({ format: "der", type: "spki" });
|
||||
return {
|
||||
privateKey,
|
||||
publicKey: `ed25519:${Buffer.from(publicDer).subarray(-32).toString("base64")}`,
|
||||
privateKeyObject,
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalSignedRequest(value, topLevel = true) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => canonicalSignedRequest(entry, false));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(
|
||||
([key, entry]) =>
|
||||
entry !== null &&
|
||||
(!topLevel || !["agent_signature", "node_signature"].includes(key))
|
||||
)
|
||||
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.map(([key, entry]) => [key, canonicalSignedRequest(entry, false)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function withWireDefaults(request) {
|
||||
const value = { ...request };
|
||||
if (value.type === "report_node_capabilities") {
|
||||
value.dependency_cache_digests ??= [];
|
||||
} else if (value.type === "launch_task" || value.type === "launch_child_task") {
|
||||
value.wait_for_node ??= false;
|
||||
} else if (value.type === "start_process") {
|
||||
value.restart ??= false;
|
||||
} else if (value.type === "report_debug_state") {
|
||||
value.stack_frames ??= [];
|
||||
value.local_values ??= [];
|
||||
value.task_args ??= [];
|
||||
value.handles ??= [];
|
||||
value.recent_output ??= [];
|
||||
} else if (value.type === "report_task_log") {
|
||||
value.stdout_tail ??= "";
|
||||
value.stderr_tail ??= "";
|
||||
} else if (value.type === "task_completed") {
|
||||
value.stdout_tail ??= "";
|
||||
value.stderr_tail ??= "";
|
||||
value.stdout_truncated ??= false;
|
||||
value.stderr_truncated ??= false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function signedRequestPayloadDigest(request) {
|
||||
return `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(JSON.stringify(canonicalSignedRequest(withWireDefaults(request))))
|
||||
.digest("hex")}`;
|
||||
}
|
||||
|
||||
function nodeSignatureMessage(
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
issuedAtEpochSeconds
|
||||
) {
|
||||
const parts = [
|
||||
"disasmer-node-request-signature:v2",
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
String(issuedAtEpochSeconds),
|
||||
];
|
||||
return Buffer.concat(
|
||||
parts.flatMap((part) => [
|
||||
Buffer.from(`${Buffer.byteLength(part)}:`),
|
||||
Buffer.from(part),
|
||||
Buffer.from("\n"),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function signedNodeProof(node, identity, requestKind, request, options = {}) {
|
||||
const nonce =
|
||||
options.nonce ||
|
||||
`${requestKind}-${process.pid}-${Date.now()}-${crypto
|
||||
.randomBytes(8)
|
||||
.toString("hex")}`;
|
||||
const issuedAt =
|
||||
options.issuedAtEpochSeconds ?? Math.floor(Date.now() / 1000);
|
||||
const signature = crypto.sign(
|
||||
null,
|
||||
nodeSignatureMessage(
|
||||
node,
|
||||
requestKind,
|
||||
signedRequestPayloadDigest(request),
|
||||
nonce,
|
||||
issuedAt
|
||||
),
|
||||
identity.privateKeyObject
|
||||
);
|
||||
return {
|
||||
nonce,
|
||||
issued_at_epoch_seconds: issuedAt,
|
||||
signature: `ed25519:${signature.toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function signedNodeHeartbeat(node, identity, options = {}) {
|
||||
const request = { type: "node_heartbeat", node };
|
||||
return signedNodeProof(node, identity, "node_heartbeat", request, options);
|
||||
}
|
||||
|
||||
function signedNodeRequest(node, identity, requestKind, request, options = {}) {
|
||||
return {
|
||||
type: "signed_node",
|
||||
node,
|
||||
node_signature: signedNodeProof(
|
||||
node,
|
||||
identity,
|
||||
requestKind,
|
||||
request,
|
||||
options
|
||||
),
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nodeIdentity,
|
||||
nodeIdentityFromPrivateKey,
|
||||
signedNodeProof,
|
||||
signedRequestPayloadDigest,
|
||||
signedNodeHeartbeat,
|
||||
signedNodeRequest,
|
||||
};
|
||||
360
scripts/operator-panel-smoke.js
Executable file
360
scripts/operator-panel-smoke.js
Executable file
|
|
@ -0,0 +1,360 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const { nodeIdentity } = require("./node-signing");
|
||||
const {
|
||||
ensureRootlessPodman,
|
||||
repo,
|
||||
runFlagshipWorker,
|
||||
send,
|
||||
startFlagship,
|
||||
waitForTaskEvent,
|
||||
waitForJsonLine,
|
||||
} = require("./real-flagship-harness");
|
||||
|
||||
const panelNode = "panel-node";
|
||||
const panelNodeIdentity = nodeIdentity("operator-panel-smoke", panelNode);
|
||||
|
||||
function widget(panel, id) {
|
||||
const item = panel.widgets[id];
|
||||
assert(item, `missing panel widget ${id}`);
|
||||
return item;
|
||||
}
|
||||
|
||||
const delay = (milliseconds) =>
|
||||
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
|
||||
async function waitForBreakpointHit(addr, process) {
|
||||
for (let attempt = 0; attempt < 2400; attempt += 1) {
|
||||
const status = await send(addr, {
|
||||
type: "inspect_debug_breakpoints",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process,
|
||||
});
|
||||
assert.strictEqual(status.type, "debug_breakpoints", JSON.stringify(status));
|
||||
if (status.hit_epoch != null) return status;
|
||||
await delay(25);
|
||||
}
|
||||
throw new Error(`timed out waiting for package breakpoint in ${process}`);
|
||||
}
|
||||
|
||||
async function waitForDebugEpochFrozen(addr, process, epoch) {
|
||||
for (let attempt = 0; attempt < 2400; attempt += 1) {
|
||||
const status = await send(addr, {
|
||||
type: "inspect_debug_epoch",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process,
|
||||
epoch,
|
||||
});
|
||||
assert.strictEqual(status.type, "debug_epoch_status", JSON.stringify(status));
|
||||
if (status.failed) {
|
||||
throw new Error(status.failure_messages.join("; "));
|
||||
}
|
||||
if (status.fully_frozen) return status;
|
||||
await delay(25);
|
||||
}
|
||||
throw new Error(`timed out waiting for debug epoch ${epoch} to freeze`);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
ensureRootlessPodman();
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
let coordinatorStderr = "";
|
||||
let worker;
|
||||
coordinator.stderr.on("data", (chunk) => {
|
||||
coordinatorStderr += chunk.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
const projectCreated = await send(addr, {
|
||||
type: "create_project",
|
||||
tenant: "tenant",
|
||||
actor_user: "user",
|
||||
project: "project",
|
||||
name: "Operator panel smoke",
|
||||
});
|
||||
assert.strictEqual(projectCreated.type, "project_created");
|
||||
|
||||
worker = await runFlagshipWorker(addr, panelNode, panelNodeIdentity);
|
||||
const workerReady = await worker.ready;
|
||||
assert.strictEqual(workerReady.node_status, "ready");
|
||||
const workerCompletion = waitForJsonLine(worker.child);
|
||||
const flagship = startFlagship(addr);
|
||||
const configuredBreakpoints = await send(addr, {
|
||||
type: "set_debug_breakpoints",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process: flagship.process,
|
||||
probe_symbols: ["disasmer.probe.package_release"],
|
||||
});
|
||||
assert.strictEqual(configuredBreakpoints.type, "debug_breakpoints");
|
||||
const breakpointHit = await waitForBreakpointHit(addr, flagship.process);
|
||||
assert.strictEqual(
|
||||
breakpointHit.hit_probe_symbol,
|
||||
"disasmer.probe.package_release"
|
||||
);
|
||||
const frozenEpoch = await waitForDebugEpochFrozen(
|
||||
addr,
|
||||
flagship.process,
|
||||
breakpointHit.hit_epoch
|
||||
);
|
||||
assert(frozenEpoch.acknowledgements.length >= 2);
|
||||
const compileEvent = await waitForTaskEvent(
|
||||
addr,
|
||||
flagship.process,
|
||||
(event) => event.task_definition === "compile_linux",
|
||||
"compile_linux before the package breakpoint"
|
||||
);
|
||||
const report = await workerCompletion;
|
||||
assert.strictEqual(report.node_status, "completed");
|
||||
assert.strictEqual(report.coordinator_response.type, "task_recorded");
|
||||
const process = flagship.process;
|
||||
|
||||
const rendered = await send(addr, {
|
||||
type: "render_operator_panel",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
actor_user: "user",
|
||||
max_download_bytes: 1024 * 1024,
|
||||
stopped: false
|
||||
});
|
||||
assert.strictEqual(rendered.type, "operator_panel");
|
||||
const panel = rendered.panel;
|
||||
assert.strictEqual(panel.tenant, "tenant");
|
||||
assert.strictEqual(panel.project, "project");
|
||||
assert.strictEqual(panel.process, process);
|
||||
assert.strictEqual(panel.program_ui_events_enabled, true);
|
||||
|
||||
assert.deepStrictEqual(widget(panel, "process-status").kind, {
|
||||
Text: { value: "running" }
|
||||
});
|
||||
const taskProgress = widget(panel, "task-progress").kind.Progress;
|
||||
assert(taskProgress.current >= 2);
|
||||
assert.strictEqual(taskProgress.current, taskProgress.total);
|
||||
assert.match(
|
||||
widget(panel, "task-summary").kind.Text.value,
|
||||
new RegExp(`compile_linux \\[${compileEvent.task}\\]:Some\\(0\\):panel-node`)
|
||||
);
|
||||
assert.match(widget(panel, "recent-logs").kind.Text.value, /stdout=\d+ stderr=\d+/);
|
||||
const downloadWidget = widget(panel, "download-artifact").kind;
|
||||
const artifact = downloadWidget.ArtifactDownload.artifact;
|
||||
assert(
|
||||
artifact === compileEvent.artifact_path.slice("/vfs/artifacts/".length),
|
||||
"panel download must point at a real flagship artifact"
|
||||
);
|
||||
assert(!JSON.stringify(downloadWidget).includes("url_path"));
|
||||
assert(!JSON.stringify(downloadWidget).includes("scoped_token_digest"));
|
||||
assert.deepStrictEqual(widget(panel, "debug-process").kind, {
|
||||
Button: { action: "debug-process" }
|
||||
});
|
||||
assert.deepStrictEqual(widget(panel, "cancel-process").kind, {
|
||||
Button: { action: "cancel-process" }
|
||||
});
|
||||
assert.deepStrictEqual(widget(panel, "restart-selected-task").kind, {
|
||||
Button: { action: "restart-task" }
|
||||
});
|
||||
assert(panel.control_plane_actions.includes("DebugProcess"));
|
||||
assert(panel.control_plane_actions.includes("CancelProcess"));
|
||||
assert(
|
||||
panel.control_plane_actions.some(
|
||||
(action) =>
|
||||
action.RestartTask &&
|
||||
action.RestartTask === compileEvent.task
|
||||
),
|
||||
"panel restart must target the real flagship task instance"
|
||||
);
|
||||
assert(
|
||||
panel.control_plane_actions.some(
|
||||
(action) => action.DownloadArtifact === artifact
|
||||
)
|
||||
);
|
||||
assert(!JSON.stringify(panel).includes("<script"));
|
||||
assert(!JSON.stringify(panel).toLowerCase().includes("oauth"));
|
||||
|
||||
const panelLink = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact: downloadWidget.ArtifactDownload.artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60
|
||||
});
|
||||
assert.strictEqual(panelLink.type, "artifact_download_link");
|
||||
assert.strictEqual(panelLink.link.artifact, downloadWidget.ArtifactDownload.artifact);
|
||||
assert.match(panelLink.link.policy_context_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.deepStrictEqual(panelLink.link.source, { RetainedNode: "panel-node" });
|
||||
assert(panelLink.link.url_path.endsWith(`/artifacts/tenant/project/${process}/${artifact}`));
|
||||
|
||||
const apiTooLarge = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact: downloadWidget.ArtifactDownload.artifact,
|
||||
max_bytes: 1,
|
||||
ttl_seconds: 60
|
||||
});
|
||||
assert.strictEqual(apiTooLarge.type, "error");
|
||||
assert.match(apiTooLarge.message, /exceeds download limit/);
|
||||
|
||||
const panelTooLarge = await send(addr, {
|
||||
type: "render_operator_panel",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
actor_user: "user",
|
||||
max_download_bytes: 1,
|
||||
stopped: false
|
||||
});
|
||||
assert.strictEqual(panelTooLarge.type, "error");
|
||||
assert.match(panelTooLarge.message, /exceeds download limit/);
|
||||
|
||||
const accepted = await send(addr, {
|
||||
type: "submit_panel_event",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
widget_id: "debug-process",
|
||||
kind: "ButtonClicked",
|
||||
max_events: 1
|
||||
});
|
||||
assert.strictEqual(accepted.type, "panel_event_accepted");
|
||||
assert.strictEqual(accepted.used_events, 1);
|
||||
assert.strictEqual(accepted.max_events, 1);
|
||||
|
||||
const rateLimited = await send(addr, {
|
||||
type: "submit_panel_event",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
widget_id: "debug-process",
|
||||
kind: "ButtonClicked",
|
||||
max_events: 1
|
||||
});
|
||||
assert.strictEqual(rateLimited.type, "error");
|
||||
assert.match(rateLimited.message, /rate limit/i);
|
||||
|
||||
const stopped = await send(addr, {
|
||||
type: "render_operator_panel",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
actor_user: "user",
|
||||
max_download_bytes: 1024 * 1024,
|
||||
stopped: true
|
||||
});
|
||||
assert.strictEqual(stopped.type, "operator_panel");
|
||||
assert.strictEqual(stopped.panel.program_ui_events_enabled, false);
|
||||
assert.deepStrictEqual(widget(stopped.panel, "process-status").kind, {
|
||||
Text: { value: "stopped" }
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
widget(stopped.panel, "task-progress").kind,
|
||||
widget(panel, "task-progress").kind
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
widget(stopped.panel, "task-summary").kind,
|
||||
widget(panel, "task-summary").kind
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
widget(stopped.panel, "recent-logs").kind,
|
||||
widget(panel, "recent-logs").kind
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
widget(stopped.panel, "download-artifact").kind,
|
||||
widget(panel, "download-artifact").kind
|
||||
);
|
||||
assert(stopped.panel.control_plane_actions.includes("DebugProcess"));
|
||||
assert(
|
||||
stopped.panel.control_plane_actions.some(
|
||||
(action) => action.DownloadArtifact === artifact
|
||||
)
|
||||
);
|
||||
|
||||
const frozenEvent = await send(addr, {
|
||||
type: "submit_panel_event",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process,
|
||||
widget_id: "debug-process",
|
||||
kind: "ButtonClicked",
|
||||
max_events: 10
|
||||
});
|
||||
assert.strictEqual(frozenEvent.type, "error");
|
||||
assert.match(frozenEvent.message, /program UI events are disabled/i);
|
||||
|
||||
const crossTenant = await send(addr, {
|
||||
type: "render_operator_panel",
|
||||
tenant: "other",
|
||||
project: "project",
|
||||
process,
|
||||
actor_user: "user",
|
||||
max_download_bytes: 1024 * 1024,
|
||||
stopped: false
|
||||
});
|
||||
assert.strictEqual(crossTenant.type, "error");
|
||||
assert.match(
|
||||
crossTenant.message,
|
||||
/scope|tenant|project|requires an active virtual process/i
|
||||
);
|
||||
assert(!crossTenant.message.includes(process));
|
||||
|
||||
const resumed = await send(addr, {
|
||||
type: "resume_debug_epoch",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process,
|
||||
epoch: breakpointHit.hit_epoch,
|
||||
});
|
||||
assert.strictEqual(resumed.type, "debug_epoch");
|
||||
const cleanup = await send(addr, {
|
||||
type: "abort_process",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process,
|
||||
});
|
||||
assert.strictEqual(cleanup.type, "process_aborted");
|
||||
} catch (error) {
|
||||
if (coordinatorStderr) {
|
||||
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
worker?.child.kill("SIGTERM");
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Operator panel smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
89
scripts/phase3-ledger.js
Normal file
89
scripts/phase3-ledger.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const CRITERION_HEADING =
|
||||
/^## \*\*(Passed|Partial|Open):\*\* (P3-[A-Z]+-\d{3}):/gm;
|
||||
|
||||
function readPhase3Ledger(repo) {
|
||||
return fs.readFileSync(path.join(repo, "phase_3_acceptance_criteria.md"), "utf8");
|
||||
}
|
||||
|
||||
function criterionStatuses(source) {
|
||||
return [...source.matchAll(CRITERION_HEADING)].map((match) => ({
|
||||
status: match[1],
|
||||
id: match[2],
|
||||
}));
|
||||
}
|
||||
|
||||
function statusCounts(criteria) {
|
||||
return Object.fromEntries(
|
||||
["Passed", "Partial", "Open"].map((status) => [
|
||||
status,
|
||||
criteria.filter((criterion) => criterion.status === status).length,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function assertCompleteCriterionSet(criteria) {
|
||||
assert.strictEqual(criteria.length, 191, "Phase 3 ledger must contain 191 criteria");
|
||||
assert.strictEqual(
|
||||
new Set(criteria.map((criterion) => criterion.id)).size,
|
||||
191,
|
||||
"Phase 3 criterion ids must be unique"
|
||||
);
|
||||
}
|
||||
|
||||
function assertPreFinalLedger(source) {
|
||||
const criteria = criterionStatuses(source);
|
||||
assertCompleteCriterionSet(criteria);
|
||||
const counts = statusCounts(criteria);
|
||||
assert.deepStrictEqual(
|
||||
counts,
|
||||
{ Passed: 0, Partial: 181, Open: 10 },
|
||||
"the public-release E2E requires the independently reset 181 Partial / 10 Open ledger"
|
||||
);
|
||||
const expectedOpen = Array.from(
|
||||
{ length: 10 },
|
||||
(_, index) => `P3-GATE-${String(index + 1).padStart(3, "0")}`
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
criteria
|
||||
.filter((criterion) => criterion.status === "Open")
|
||||
.map((criterion) => criterion.id),
|
||||
expectedOpen,
|
||||
"only the ten final P3-GATE criteria may be Open in the reset ledger"
|
||||
);
|
||||
return counts;
|
||||
}
|
||||
|
||||
function assertFinalLedger(source) {
|
||||
const criteria = criterionStatuses(source);
|
||||
assertCompleteCriterionSet(criteria);
|
||||
const counts = statusCounts(criteria);
|
||||
assert.deepStrictEqual(
|
||||
counts,
|
||||
{ Passed: 191, Partial: 0, Open: 0 },
|
||||
"final release evidence requires all 191 Phase 3 criteria to be Passed"
|
||||
);
|
||||
return counts;
|
||||
}
|
||||
|
||||
function assertPreFinalOrFinalLedger(source) {
|
||||
const criteria = criterionStatuses(source);
|
||||
assertCompleteCriterionSet(criteria);
|
||||
const counts = statusCounts(criteria);
|
||||
if (counts.Open === 10) {
|
||||
return assertPreFinalLedger(source);
|
||||
}
|
||||
return assertFinalLedger(source);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assertFinalLedger,
|
||||
assertPreFinalLedger,
|
||||
assertPreFinalOrFinalLedger,
|
||||
criterionStatuses,
|
||||
readPhase3Ledger,
|
||||
statusCounts,
|
||||
};
|
||||
86
scripts/podman-backend-smoke.js
Executable file
86
scripts/podman-backend-smoke.js
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const path = require("path");
|
||||
const { configurePodmanTestEnvironment } = require("./podman-test-env");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const baseImage = "docker.io/library/alpine:3.20";
|
||||
|
||||
// Nix's standalone Podman package may not install the distribution-level
|
||||
// containers/image policy normally found under /etc. Use an isolated test HOME
|
||||
// without replacing a policy supplied by the host.
|
||||
configurePodmanTestEnvironment(repo);
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return cp.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: options.stdio || ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function incomplete(reason) {
|
||||
const error = new Error(`Linux Podman backend incomplete: ${reason}`);
|
||||
error.code = "DISASMER_PODMAN_INCOMPLETE";
|
||||
throw error;
|
||||
}
|
||||
|
||||
function ensurePodmanBaseImage() {
|
||||
try {
|
||||
run("podman", ["--version"]);
|
||||
} catch (error) {
|
||||
incomplete(`podman command is unavailable (${error.message})`);
|
||||
}
|
||||
|
||||
let rootless;
|
||||
try {
|
||||
rootless = run("podman", ["info", "--format", "{{.Host.Security.Rootless}}"]).trim();
|
||||
} catch (error) {
|
||||
incomplete(`podman info did not report rootless status (${error.message})`);
|
||||
}
|
||||
if (rootless !== "true") {
|
||||
incomplete(`podman is not running in rootless mode (reported ${JSON.stringify(rootless)})`);
|
||||
}
|
||||
|
||||
try {
|
||||
run("podman", ["image", "exists", baseImage]);
|
||||
} catch (_) {
|
||||
try {
|
||||
run("podman", ["pull", baseImage], { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
incomplete(`unable to make ${baseImage} available (${error.message})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ensurePodmanBaseImage();
|
||||
|
||||
const stdout = run("cargo", [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-podman-smoke"
|
||||
]);
|
||||
const report = JSON.parse(stdout.trim().split("\n").at(-1));
|
||||
|
||||
assert.strictEqual(report.podman_status, "completed");
|
||||
assert.strictEqual(report.status_code, 0);
|
||||
assert.strictEqual(report.stdout, "podman-ok:node-local source\n");
|
||||
assert.strictEqual(report.large_bytes_uploaded, false);
|
||||
assert.strictEqual(report.uses_full_repo_tarball, false);
|
||||
assert.strictEqual(report.coordinator_routed_file_reads, false);
|
||||
assert.strictEqual(report.staged_artifact.path, "/vfs/artifacts/podman-smoke.txt");
|
||||
|
||||
console.log("Podman backend smoke passed");
|
||||
} catch (error) {
|
||||
if (error.code === "DISASMER_PODMAN_INCOMPLETE") {
|
||||
console.error(error.message);
|
||||
process.exit(2);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
41
scripts/podman-test-env.js
Normal file
41
scripts/podman-test-env.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
function configurePodmanTestEnvironment(repo) {
|
||||
const originalHome = os.homedir();
|
||||
if (
|
||||
fs.existsSync(path.join(originalHome, ".config/containers/policy.json")) ||
|
||||
fs.existsSync("/etc/containers/policy.json")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isolatedHome = path.join(repo, "scripts/containers-home");
|
||||
const policy = path.join(isolatedHome, ".config/containers/policy.json");
|
||||
fs.mkdirSync(path.dirname(policy), { recursive: true });
|
||||
if (!fs.existsSync(policy)) {
|
||||
fs.writeFileSync(
|
||||
policy,
|
||||
`${JSON.stringify(
|
||||
{ default: [{ type: "insecureAcceptAnything" }] },
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
process.env.CARGO_HOME ||= path.join(originalHome, ".cargo");
|
||||
process.env.RUSTUP_HOME ||= path.join(originalHome, ".rustup");
|
||||
process.env.HOME = isolatedHome;
|
||||
process.env.XDG_DATA_HOME = path.join(
|
||||
os.tmpdir(),
|
||||
`disasmer-containers-data-${process.getuid?.() ?? "user"}`
|
||||
);
|
||||
process.env.XDG_CACHE_HOME = path.join(
|
||||
os.tmpdir(),
|
||||
`disasmer-containers-cache-${process.getuid?.() ?? "user"}`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { configurePodmanTestEnvironment };
|
||||
755
scripts/prepare-public-release-dryrun.js
Executable file
755
scripts/prepare-public-release-dryrun.js
Executable file
|
|
@ -0,0 +1,755 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const crypto = require("crypto");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const outputRoot = path.resolve(
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/public-release-dryrun")
|
||||
);
|
||||
const publicTree = path.join(outputRoot, "public-tree");
|
||||
const assetsDir = path.join(outputRoot, "assets");
|
||||
const stagingDir = path.join(outputRoot, "staging");
|
||||
const publicBuildTarget = path.join(repo, "target");
|
||||
const defaultHostedCoordinatorEndpoint = "https://disasmer.michelpaulissen.com";
|
||||
const forgejoHost = "git.michelpaulissen.com";
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const includeForgejoWorkflows =
|
||||
args.has("--include-forgejo-workflows") ||
|
||||
/^(1|true|yes)$/i.test(process.env.DISASMER_INCLUDE_FORGEJO_WORKFLOWS || "");
|
||||
const filteredTopLevel = ["private", "experiments", ".git", "target"];
|
||||
const filteredDirectoryNames = [".disasmer"];
|
||||
const archiveIgnoredPathFallbacks = [
|
||||
"target",
|
||||
".disasmer",
|
||||
"vscode-extension/node_modules",
|
||||
"scripts/containers-home",
|
||||
];
|
||||
const publicRepoBranch = process.env.DISASMER_PUBLIC_REPO_BRANCH || "main";
|
||||
const publicBinaries = [
|
||||
"disasmer",
|
||||
"disasmer-coordinator",
|
||||
"disasmer-node",
|
||||
"disasmer-debug-dap",
|
||||
];
|
||||
|
||||
function commandOutput(command, args, options = {}) {
|
||||
try {
|
||||
const execOptions = {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
...options,
|
||||
};
|
||||
execOptions.env = commandEnv(command, options.env);
|
||||
return cp
|
||||
.execFileSync(command, args, execOptions)
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const execOptions = {
|
||||
cwd: repo,
|
||||
stdio: "inherit",
|
||||
...options,
|
||||
};
|
||||
execOptions.env = commandEnv(command, options.env);
|
||||
cp.execFileSync(command, args, execOptions);
|
||||
}
|
||||
|
||||
function nonInteractiveGitEnv(extra = {}) {
|
||||
return {
|
||||
...process.env,
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false",
|
||||
SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false",
|
||||
GIT_SSH_COMMAND:
|
||||
process.env.GIT_SSH_COMMAND ||
|
||||
"ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0",
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function commandEnv(command, env) {
|
||||
if (command !== "git") {
|
||||
return env;
|
||||
}
|
||||
return nonInteractiveGitEnv(env);
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function filteredOutPatterns() {
|
||||
return [
|
||||
"private/**",
|
||||
"experiments/**",
|
||||
".git",
|
||||
"target",
|
||||
"git-ignored source paths",
|
||||
"root/*.md except README.md and SECURITY.md",
|
||||
"**/.disasmer/**",
|
||||
...(includeForgejoWorkflows ? [] : [".forgejo/**"]),
|
||||
];
|
||||
}
|
||||
|
||||
function gitIgnoredSourcePaths() {
|
||||
const output = commandOutput("git", [
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--ignored",
|
||||
"--exclude-standard",
|
||||
"--directory",
|
||||
"-z",
|
||||
]);
|
||||
if (output === null) {
|
||||
return archiveIgnoredPathFallbacks;
|
||||
}
|
||||
return [
|
||||
...new Set([
|
||||
...archiveIgnoredPathFallbacks,
|
||||
...output
|
||||
.split("\0")
|
||||
.filter(Boolean)
|
||||
.map((relativePath) =>
|
||||
relativePath.replaceAll("\\", "/").replace(/\/$/, "")
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function isGitIgnored(relativePath, ignoredSourcePaths) {
|
||||
const normalized = relativePath.replaceAll(path.sep, "/");
|
||||
return ignoredSourcePaths.some(
|
||||
(ignored) => normalized === ignored || normalized.startsWith(`${ignored}/`)
|
||||
);
|
||||
}
|
||||
|
||||
function isFilteredRootMarkdown(relativePath, entry) {
|
||||
return (
|
||||
!relativePath.includes(path.sep) &&
|
||||
(entry.isFile() || entry.isSymbolicLink()) &&
|
||||
path.extname(entry.name).toLowerCase() === ".md" &&
|
||||
!["README.md", "SECURITY.md"].includes(entry.name)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldFilter(entry, relativePath) {
|
||||
const parts = relativePath.split(path.sep).filter(Boolean);
|
||||
const topLevel = parts[0];
|
||||
if (filteredTopLevel.includes(topLevel)) return true;
|
||||
if (parts.some((part) => filteredDirectoryNames.includes(part))) return true;
|
||||
if (topLevel === ".forgejo" && !includeForgejoWorkflows) return true;
|
||||
if (isFilteredRootMarkdown(relativePath, entry)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function copyFilteredTree(src, dest, ignoredSourcePaths, relative = "") {
|
||||
ensureDir(dest);
|
||||
const entries = fs
|
||||
.readdirSync(src, { withFileTypes: true })
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
|
||||
if (
|
||||
shouldFilter(entry, childRelative) ||
|
||||
isGitIgnored(childRelative, ignoredSourcePaths)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const from = path.join(src, entry.name);
|
||||
const to = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyFilteredTree(from, to, ignoredSourcePaths, childRelative);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
fs.symlinkSync(fs.readlinkSync(from), to);
|
||||
} else if (entry.isFile()) {
|
||||
fs.copyFileSync(from, to);
|
||||
fs.chmodSync(to, fs.statSync(from).mode & 0o777);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertFilteredTree(ignoredSourcePaths) {
|
||||
for (const excluded of ["private", "experiments"]) {
|
||||
if (fs.existsSync(path.join(publicTree, excluded))) {
|
||||
throw new Error(`${excluded}/ leaked into the dry-run public tree`);
|
||||
}
|
||||
}
|
||||
for (const file of walkFiles(publicTree)) {
|
||||
if (file.split(path.sep).includes(".disasmer")) {
|
||||
throw new Error(`generated .disasmer view state leaked into the dry-run public tree: ${file}`);
|
||||
}
|
||||
}
|
||||
if (!includeForgejoWorkflows && fs.existsSync(path.join(publicTree, ".forgejo"))) {
|
||||
throw new Error(".forgejo/ leaked into the host-neutral dry-run public tree");
|
||||
}
|
||||
if (!fs.existsSync(path.join(publicTree, "README.md"))) {
|
||||
throw new Error("product README.md is missing from the dry-run public tree");
|
||||
}
|
||||
for (const ignored of ignoredSourcePaths) {
|
||||
if (fs.existsSync(path.join(publicTree, ...ignored.split("/")))) {
|
||||
throw new Error(`Git-ignored source path leaked into the dry-run public tree: ${ignored}`);
|
||||
}
|
||||
}
|
||||
for (const entry of fs.readdirSync(publicTree, { withFileTypes: true })) {
|
||||
if (isFilteredRootMarkdown(entry.name, entry)) {
|
||||
throw new Error(`internal root Markdown file leaked into the dry-run public tree: ${entry.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function walkFiles(root, relative = "") {
|
||||
const dir = path.join(root, relative);
|
||||
const entries = fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...walkFiles(root, childRelative));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(childRelative);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
files.push(childRelative);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function hashTree(root) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
for (const file of walkFiles(root)) {
|
||||
const absolute = path.join(root, file);
|
||||
const stat = fs.lstatSync(absolute);
|
||||
hash.update(file.replaceAll(path.sep, "/"));
|
||||
hash.update("\0");
|
||||
hash.update(String(stat.mode & 0o777));
|
||||
hash.update("\0");
|
||||
if (stat.isSymbolicLink()) {
|
||||
hash.update("symlink");
|
||||
hash.update("\0");
|
||||
hash.update(fs.readlinkSync(absolute));
|
||||
} else {
|
||||
hash.update(fs.readFileSync(absolute));
|
||||
}
|
||||
hash.update("\0");
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function sha256File(file) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
}
|
||||
|
||||
function tarGz(output, cwd, inputs) {
|
||||
run("tar", ["-czf", output, "-C", cwd, ...inputs]);
|
||||
}
|
||||
|
||||
function platformName() {
|
||||
return `${os.platform()}-${os.arch()}`;
|
||||
}
|
||||
|
||||
function binaryName(name) {
|
||||
return process.platform === "win32" ? `${name}.exe` : name;
|
||||
}
|
||||
|
||||
function buildPublicBinaries() {
|
||||
run("cargo", ["build", "--workspace", "--bins", "--release", "--jobs", "2"], {
|
||||
cwd: publicTree,
|
||||
env: { ...process.env, CARGO_TARGET_DIR: publicBuildTarget },
|
||||
});
|
||||
}
|
||||
|
||||
function stageBinaryAssets(releaseName) {
|
||||
const stageRoot = path.join(stagingDir, "binaries");
|
||||
const binDir = path.join(stageRoot, "bin");
|
||||
fs.rmSync(stageRoot, { recursive: true, force: true });
|
||||
ensureDir(binDir);
|
||||
|
||||
for (const binary of publicBinaries) {
|
||||
const fileName = binaryName(binary);
|
||||
const built = path.join(publicBuildTarget, "release", fileName);
|
||||
if (!fs.existsSync(built)) {
|
||||
throw new Error(`expected release binary ${built}`);
|
||||
}
|
||||
const staged = path.join(binDir, fileName);
|
||||
fs.copyFileSync(built, staged);
|
||||
fs.chmodSync(staged, 0o755);
|
||||
}
|
||||
|
||||
const archive = path.join(
|
||||
assetsDir,
|
||||
`disasmer-public-binaries-${releaseName}-${platformName()}.tar.gz`
|
||||
);
|
||||
tarGz(archive, stageRoot, ["."]);
|
||||
return archive;
|
||||
}
|
||||
|
||||
function publicBinaryDigests() {
|
||||
return Object.fromEntries(
|
||||
publicBinaries.map((binary) => {
|
||||
const fileName = binaryName(binary);
|
||||
const file = path.join(publicBuildTarget, "release", fileName);
|
||||
if (!fs.existsSync(file)) throw new Error(`missing built release binary ${file}`);
|
||||
return [fileName, `sha256:${sha256File(file)}`];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function stageEvidenceAsset(
|
||||
releaseName,
|
||||
sourceCommit,
|
||||
publicTreeIdentity,
|
||||
binaryDigests
|
||||
) {
|
||||
const stage = path.join(stagingDir, "evidence");
|
||||
fs.rmSync(stage, { recursive: true, force: true });
|
||||
ensureDir(stage);
|
||||
const evidenceRoot = path.join(repo, "target/acceptance");
|
||||
const included = [];
|
||||
for (const name of ["public-environment.json", "wasmtime-assignment.json"]) {
|
||||
const source = path.join(evidenceRoot, name);
|
||||
if (!fs.existsSync(source)) continue;
|
||||
fs.copyFileSync(source, path.join(stage, name));
|
||||
included.push(name);
|
||||
}
|
||||
const binding = {
|
||||
kind: "disasmer-release-evidence-binding",
|
||||
source_commit: sourceCommit,
|
||||
public_tree_identity: publicTreeIdentity,
|
||||
binary_digests: binaryDigests,
|
||||
configuration_generation:
|
||||
process.env.DISASMER_DEPLOYMENT_SYSTEM_GENERATION || null,
|
||||
included_evidence: included,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(stage, "EVIDENCE_BINDING.json"),
|
||||
`${JSON.stringify(binding, null, 2)}\n`
|
||||
);
|
||||
const archive = path.join(
|
||||
assetsDir,
|
||||
`disasmer-public-evidence-${releaseName}.tar.gz`
|
||||
);
|
||||
tarGz(archive, stage, ["."]);
|
||||
return archive;
|
||||
}
|
||||
|
||||
function stageSourceAsset(releaseName) {
|
||||
const archive = path.join(assetsDir, `disasmer-public-source-${releaseName}.tar.gz`);
|
||||
tarGz(archive, publicTree, ["."]);
|
||||
return archive;
|
||||
}
|
||||
|
||||
function stageExtensionAsset() {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(publicTree, "vscode-extension/package.json"), "utf8")
|
||||
);
|
||||
const archive = path.join(
|
||||
assetsDir,
|
||||
`${packageJson.name}-${packageJson.version}.vsix`
|
||||
);
|
||||
run(
|
||||
"npx",
|
||||
[
|
||||
"--yes",
|
||||
"@vscode/vsce",
|
||||
"package",
|
||||
"--allow-missing-repository",
|
||||
"--skip-license",
|
||||
"--out",
|
||||
archive,
|
||||
],
|
||||
{ cwd: path.join(publicTree, "vscode-extension") }
|
||||
);
|
||||
return archive;
|
||||
}
|
||||
|
||||
function resolverInstructions() {
|
||||
if (process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS) {
|
||||
return process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS.trim();
|
||||
}
|
||||
if (process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY) {
|
||||
return [
|
||||
"Add the controlled hosts entry supplied for this dry run:",
|
||||
"",
|
||||
"```",
|
||||
process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY.trim(),
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
if (process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP) {
|
||||
return [
|
||||
"Add this controlled hosts entry for the dry run:",
|
||||
"",
|
||||
"```",
|
||||
`${process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP} disasmer.michelpaulissen.com`,
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
return [
|
||||
"`disasmer.michelpaulissen.com` should resolve through public DNS. If it",
|
||||
"does not resolve yet, wait for DNS propagation or use the fallback hosts",
|
||||
"entry supplied with the invitation.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function writeGettingStartedAsset(releaseName, publicTreeIdentity, resolution) {
|
||||
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-${releaseName}.md`);
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
`# Disasmer Public Dry Run
|
||||
|
||||
This dry run is a real Disasmer deployment for selected external users. If
|
||||
public DNS has not propagated yet, use the fallback resolution instructions
|
||||
below.
|
||||
|
||||
The default hosted coordinator is the deployment at
|
||||
\`https://disasmer.michelpaulissen.com\`. The word \`public\` in this dry run
|
||||
refers to the public Forgejo repository, release downloads, selected-user
|
||||
network access, and public client protocol compatibility. It does not mean the
|
||||
server is the standalone open-source Core coordinator.
|
||||
|
||||
## DNS
|
||||
|
||||
${resolution}
|
||||
|
||||
## Install
|
||||
|
||||
1. Download the binary archive for your platform from the Forgejo Release at
|
||||
\`git.michelpaulissen.com\`.
|
||||
2. Verify it against \`SHA256SUMS\`.
|
||||
3. Extract the archive and put the \`bin/\` directory on your \`PATH\`.
|
||||
4. Download \`disasmer-vscode-*.vsix\` if you want the debugger and Disasmer
|
||||
side views, then install it:
|
||||
|
||||
\`\`\`bash
|
||||
code --install-extension disasmer-vscode-*.vsix
|
||||
\`\`\`
|
||||
|
||||
## Connect
|
||||
|
||||
Use the default hosted coordinator endpoint:
|
||||
|
||||
\`\`\`bash
|
||||
disasmer login --browser
|
||||
disasmer bundle inspect --project examples/launch-build-demo
|
||||
\`\`\`
|
||||
|
||||
\`disasmer login --browser\` opens the server-provided Authentik authorization
|
||||
URL and polls the hosted transaction. The provider callback terminates on the
|
||||
hosted service; provider codes and tokens do not pass through the CLI.
|
||||
|
||||
Enroll a node identity with the grant supplied by the invitation:
|
||||
|
||||
\`\`\`bash
|
||||
disasmer node attach --coordinator https://disasmer.michelpaulissen.com --enrollment-grant <grant> --public-key <public-key>
|
||||
\`\`\`
|
||||
|
||||
That command proves the identity/enrollment path and then exits. To actually run
|
||||
work for the flagship workflow, leave a worker process running in another
|
||||
terminal. Use a fresh enrollment grant, or skip the attach command above and use
|
||||
the worker command directly:
|
||||
|
||||
\`\`\`bash
|
||||
disasmer-node --coordinator https://disasmer.michelpaulissen.com --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready
|
||||
\`\`\`
|
||||
|
||||
Then run the flagship workflow from the filtered public repository while that
|
||||
worker is still running:
|
||||
|
||||
\`\`\`bash
|
||||
disasmer run --project examples/launch-build-demo build
|
||||
\`\`\`
|
||||
|
||||
The dry-run public tree identity is \`${publicTreeIdentity}\`.
|
||||
The release name is \`${releaseName}\`.
|
||||
`,
|
||||
"utf8"
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
function writeInviteAsset(releaseName, publicTreeIdentity, resolution) {
|
||||
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_INVITE-${releaseName}.md`);
|
||||
const publicRepo =
|
||||
process.env.DISASMER_PUBLIC_REPO_URL ||
|
||||
"https://git.michelpaulissen.com/<owner>/<public-repo>";
|
||||
const releaseUrl =
|
||||
process.env.DISASMER_FORGEJO_RELEASE_URL ||
|
||||
"the Forgejo Release attached to the public repository";
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
`# Disasmer Public Dry Run Invite
|
||||
|
||||
This invite is for selected external users, such as friends helping test the
|
||||
MVP release experience. The deployment is real and externally reachable, but it
|
||||
is intentionally not broadly advertised yet.
|
||||
|
||||
The hosted coordinator behind this invite is the deployment at
|
||||
\`https://disasmer.michelpaulissen.com\`. The public part is the Forgejo
|
||||
repository, release downloads, network-reachable dry run, and public client
|
||||
protocol used by the binaries; the hosted server is not the standalone Core
|
||||
coordinator.
|
||||
|
||||
If public DNS has not propagated yet, make sure \`disasmer.michelpaulissen.com\`
|
||||
resolves to the deployment host before running the CLI.
|
||||
|
||||
## Links
|
||||
|
||||
- Public repository: ${publicRepo}
|
||||
- Release downloads: ${releaseUrl}
|
||||
- Default hosted coordinator endpoint: ${defaultHostedCoordinatorEndpoint}
|
||||
- Public tree identity: ${publicTreeIdentity}
|
||||
- Release name: ${releaseName}
|
||||
|
||||
## DNS
|
||||
|
||||
${resolution}
|
||||
|
||||
## First run
|
||||
|
||||
1. Download the binary archive for your platform and \`SHA256SUMS\` from the
|
||||
Forgejo Release.
|
||||
2. Extract the archive and put \`bin/\` on your \`PATH\`.
|
||||
3. Optionally install \`disasmer-vscode-*.vsix\` with
|
||||
\`code --install-extension disasmer-vscode-*.vsix\`.
|
||||
4. Run \`disasmer login --browser\`; it opens the server-provided Authentik
|
||||
authorization URL and polls the hosted transaction for a scoped CLI session.
|
||||
5. Enroll a node identity with the enrollment grant supplied out of band.
|
||||
6. Start a long-lived worker with a fresh enrollment grant, or use the worker
|
||||
command directly instead of step 5:
|
||||
\`disasmer-node --coordinator https://disasmer.michelpaulissen.com --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready\`.
|
||||
7. Run \`disasmer run --project examples/launch-build-demo build\` from the
|
||||
public repository checkout while the worker process is still running.
|
||||
`,
|
||||
"utf8"
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
function writeSha256Sums(assets) {
|
||||
const sumsPath = path.join(assetsDir, "SHA256SUMS");
|
||||
const lines = assets
|
||||
.map((asset) => `${sha256File(asset)} ${path.basename(asset)}`)
|
||||
.sort()
|
||||
.join("\n");
|
||||
fs.writeFileSync(sumsPath, `${lines}\n`);
|
||||
return sumsPath;
|
||||
}
|
||||
|
||||
function boolEnv(name) {
|
||||
return /^(1|true|yes)$/i.test(process.env[name] || "");
|
||||
}
|
||||
|
||||
function writePublicTreeProvenance(sourceCommit, releaseName) {
|
||||
const provenance = {
|
||||
kind: "disasmer-filtered-public-tree",
|
||||
source_commit: sourceCommit,
|
||||
release_name: releaseName,
|
||||
filtered_out: filteredOutPatterns(),
|
||||
public_export: {
|
||||
host_neutral: !includeForgejoWorkflows,
|
||||
include_forgejo_workflows: includeForgejoWorkflows,
|
||||
},
|
||||
forgejo_host: forgejoHost,
|
||||
default_hosted_coordinator_endpoint: defaultHostedCoordinatorEndpoint,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(publicTree, "DISASMER_PUBLIC_TREE.json"),
|
||||
`${JSON.stringify(provenance, null, 2)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function publishPublicTree(releaseName, sourceCommit, publicTreeIdentity) {
|
||||
const remote =
|
||||
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
|
||||
process.env.DISASMER_PUBLIC_REPO_URL ||
|
||||
null;
|
||||
const enabled = boolEnv("DISASMER_PUBLISH_PUBLIC_TREE");
|
||||
const result = {
|
||||
enabled,
|
||||
remote,
|
||||
branch: publicRepoBranch,
|
||||
commit: null,
|
||||
pushed: false,
|
||||
};
|
||||
|
||||
if (!enabled) {
|
||||
return result;
|
||||
}
|
||||
if (!remote) {
|
||||
throw new Error(
|
||||
"DISASMER_PUBLISH_PUBLIC_TREE requires DISASMER_PUBLIC_REPO_REMOTE or DISASMER_PUBLIC_REPO_URL"
|
||||
);
|
||||
}
|
||||
if (!remote.includes(forgejoHost)) {
|
||||
throw new Error(`public repo remote must point at ${forgejoHost}: ${remote}`);
|
||||
}
|
||||
|
||||
run("git", ["init"], { cwd: publicTree });
|
||||
run("git", ["checkout", "-B", publicRepoBranch], { cwd: publicTree });
|
||||
run("git", ["config", "user.name", "Disasmer release dry run"], {
|
||||
cwd: publicTree,
|
||||
});
|
||||
run("git", ["config", "user.email", "release-dryrun@disasmer.invalid"], {
|
||||
cwd: publicTree,
|
||||
});
|
||||
run("git", ["add", "."], { cwd: publicTree });
|
||||
run(
|
||||
"git",
|
||||
[
|
||||
"commit",
|
||||
"-m",
|
||||
`Public dry run ${releaseName}`,
|
||||
"-m",
|
||||
`Source commit: ${sourceCommit}`,
|
||||
"-m",
|
||||
`Public tree identity: ${publicTreeIdentity}`,
|
||||
],
|
||||
{ cwd: publicTree }
|
||||
);
|
||||
result.commit = commandOutput("git", ["rev-parse", "HEAD"], { cwd: publicTree });
|
||||
run("git", ["remote", "add", "public", remote], { cwd: publicTree });
|
||||
const pushArgs = ["push", "public", `HEAD:${publicRepoBranch}`];
|
||||
if (boolEnv("DISASMER_PUBLIC_REPO_PUSH_FORCE_WITH_LEASE")) {
|
||||
run(
|
||||
"git",
|
||||
[
|
||||
"fetch",
|
||||
"public",
|
||||
`refs/heads/${publicRepoBranch}:refs/remotes/public/${publicRepoBranch}`,
|
||||
],
|
||||
{ cwd: publicTree }
|
||||
);
|
||||
pushArgs.splice(1, 0, "--force-with-lease");
|
||||
}
|
||||
run("git", pushArgs, { cwd: publicTree });
|
||||
result.pushed = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const sourceCommit =
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
||||
"unknown";
|
||||
const shortCommit = sourceCommit === "unknown" ? "unknown" : sourceCommit.slice(0, 12);
|
||||
const releaseName = process.env.DISASMER_PUBLIC_RELEASE_NAME || `dryrun-${shortCommit}`;
|
||||
const sourceStatus = commandOutput("git", ["status", "--short"]);
|
||||
const sourceTreeClean = sourceStatus === null ? null : sourceStatus === "";
|
||||
|
||||
fs.rmSync(outputRoot, { recursive: true, force: true });
|
||||
ensureDir(publicTree);
|
||||
ensureDir(assetsDir);
|
||||
ensureDir(stagingDir);
|
||||
|
||||
const ignoredSourcePaths = gitIgnoredSourcePaths();
|
||||
copyFilteredTree(repo, publicTree, ignoredSourcePaths);
|
||||
assertFilteredTree(ignoredSourcePaths);
|
||||
writePublicTreeProvenance(sourceCommit, releaseName);
|
||||
const publicTreeIdentity = hashTree(publicTree);
|
||||
const sourceArchive = stageSourceAsset(releaseName);
|
||||
|
||||
run("node", ["scripts/public-release-dryrun-contract-smoke.js"], {
|
||||
cwd: publicTree,
|
||||
});
|
||||
const publicTreePublish = publishPublicTree(
|
||||
releaseName,
|
||||
sourceCommit,
|
||||
publicTreeIdentity
|
||||
);
|
||||
buildPublicBinaries();
|
||||
const binaryArchive = stageBinaryAssets(releaseName);
|
||||
const binaryDigests = publicBinaryDigests();
|
||||
const evidenceArchive = stageEvidenceAsset(
|
||||
releaseName,
|
||||
sourceCommit,
|
||||
publicTreeIdentity,
|
||||
binaryDigests
|
||||
);
|
||||
const extensionArchive = stageExtensionAsset();
|
||||
const resolution = resolverInstructions();
|
||||
const gettingStarted = writeGettingStartedAsset(
|
||||
releaseName,
|
||||
publicTreeIdentity,
|
||||
resolution
|
||||
);
|
||||
const invite = writeInviteAsset(releaseName, publicTreeIdentity, resolution);
|
||||
const assets = [
|
||||
sourceArchive,
|
||||
binaryArchive,
|
||||
evidenceArchive,
|
||||
extensionArchive,
|
||||
gettingStarted,
|
||||
invite,
|
||||
];
|
||||
const sha256Sums = writeSha256Sums(assets);
|
||||
|
||||
const manifest = {
|
||||
kind: "disasmer-public-release-dryrun",
|
||||
release_name: releaseName,
|
||||
source_commit: sourceCommit,
|
||||
source_tree_clean: sourceTreeClean,
|
||||
public_tree_identity: publicTreeIdentity,
|
||||
public_tree: publicTree,
|
||||
filtered_out: filteredOutPatterns(),
|
||||
public_export: {
|
||||
host_neutral: !includeForgejoWorkflows,
|
||||
include_forgejo_workflows: includeForgejoWorkflows,
|
||||
},
|
||||
forgejo_host: forgejoHost,
|
||||
public_repo_url: process.env.DISASMER_PUBLIC_REPO_URL || publicTreePublish.remote,
|
||||
public_repo_remote: process.env.DISASMER_PUBLIC_REPO_REMOTE || null,
|
||||
public_tree_publish: publicTreePublish,
|
||||
forgejo_release_url: process.env.DISASMER_FORGEJO_RELEASE_URL || null,
|
||||
default_hosted_coordinator_endpoint: defaultHostedCoordinatorEndpoint,
|
||||
dns_publication_state:
|
||||
process.env.DISASMER_DNS_PUBLICATION_STATE || "published",
|
||||
resolver_override:
|
||||
process.env.DISASMER_RESOLVER_OVERRIDE || "none-required-public-dns",
|
||||
platform: platformName(),
|
||||
binary_digests: binaryDigests,
|
||||
configuration_generation:
|
||||
process.env.DISASMER_DEPLOYMENT_SYSTEM_GENERATION || null,
|
||||
tool_versions: {
|
||||
node: process.version,
|
||||
rustc: commandOutput("rustc", ["--version"]) || null,
|
||||
cargo: commandOutput("cargo", ["--version"]) || null,
|
||||
tar: commandOutput("tar", ["--version"]) || null,
|
||||
},
|
||||
commands: [
|
||||
"node scripts/public-release-dryrun-contract-smoke.js",
|
||||
...(publicTreePublish.enabled
|
||||
? [`git push public HEAD:${publicRepoBranch}`]
|
||||
: []),
|
||||
"cargo build --workspace --bins --release --jobs 2",
|
||||
],
|
||||
assets: [...assets, sha256Sums].map((asset) => ({
|
||||
file: asset,
|
||||
name: path.basename(asset),
|
||||
sha256: sha256File(asset),
|
||||
})),
|
||||
notes: [
|
||||
"Upload the assets to the Forgejo Release for the filtered public repository.",
|
||||
"The real service deployment and full e2e dry run remain separate acceptance evidence.",
|
||||
],
|
||||
};
|
||||
|
||||
const manifestPath = path.join(outputRoot, "public-release-manifest.json");
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ manifest: manifestPath, assets: assetsDir }, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
77
scripts/public-browser-login-contract-smoke.js
Normal file
77
scripts/public-browser-login-contract-smoke.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const infraRepo = path.resolve(repo, "..", "michelpaulissen.com");
|
||||
|
||||
function read(relativePath, base = repo) {
|
||||
return fs.readFileSync(path.join(base, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing public browser login evidence: ${name}`);
|
||||
}
|
||||
|
||||
const cliSource = [
|
||||
read("crates/disasmer-cli/src/main.rs"),
|
||||
read("crates/disasmer-cli/src/dispatch.rs"),
|
||||
read("crates/disasmer-cli/src/auth.rs"),
|
||||
].join("\n");
|
||||
const cliSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const phase2 = read("acceptance_criteria_phase2.md");
|
||||
const base = read("acceptance_criteria.md");
|
||||
|
||||
for (const [name, source] of [
|
||||
["base acceptance", base],
|
||||
["phase 2 acceptance", phase2],
|
||||
]) {
|
||||
expect(source, `${name} released binary browser criterion`, /Public released binaries[\s\S]*(?:human |server-owned )browser\/account flow/);
|
||||
}
|
||||
|
||||
expect(cliSource, "diagnostic plan flag", /plan: bool/);
|
||||
expect(cliSource, "interactive browser branch", /else if !args\.plan[\s\S]*execute_interactive_browser_login/);
|
||||
expect(cliSource, "browser command override", /DISASMER_BROWSER_OPEN_COMMAND/);
|
||||
expect(cliSource, "server-owned login start", /"type": "begin_oidc_browser_login"[\s\S]*requested_project/);
|
||||
expect(cliSource, "opaque login polling", /"type": "poll_oidc_browser_login"[\s\S]*transaction_id[\s\S]*polling_secret/);
|
||||
expect(cliSource, "server-owned state nonce and PKCE", /server_owns_state: true[\s\S]*server_owns_nonce: true[\s\S]*pkce_required: true/);
|
||||
expect(cliSource, "CLI receives no provider code or claims", /cli_receives_provider_authorization_code: false[\s\S]*cli_submits_identity_claims: false/);
|
||||
assert.doesNotMatch(cliSource, /TcpListener::bind|authorization_code.*issuer_url.*client_id/s);
|
||||
expect(cliSmoke, "smoke uses fake browser opener", /DISASMER_BROWSER_OPEN_COMMAND/);
|
||||
expect(cliSmoke, "smoke verifies server-owned browser transaction", /server-owned browser transaction/);
|
||||
expect(cliSmoke, "smoke rejects client OIDC authority", /assert\.deepStrictEqual\(request,[\s\S]*begin_oidc_browser_login[\s\S]*requested_project/);
|
||||
expect(cliSmoke, "smoke verifies coordinator completion", /scoped_cli_session_received/);
|
||||
expect(publicAcceptance, "public acceptance runs browser flow smoke", /node scripts\/cli-browser-login-flow-smoke\.js/);
|
||||
|
||||
if (fs.existsSync(infraRepo)) {
|
||||
const stack = read("modules/stack.nix", infraRepo);
|
||||
const hypervisor = read("hosts/hypervisor/default.nix", infraRepo);
|
||||
const oauth = read("modules/oauth-bootstrap.nix", infraRepo);
|
||||
const site = read("disasmer-site/index.html", infraRepo);
|
||||
|
||||
expect(stack, "Disasmer public host", /publicHost = "disasmer\.michelpaulissen\.com"/);
|
||||
expect(stack, "Disasmer loopback API port", /apiPort = 9080/);
|
||||
expect(hypervisor, "nginx Disasmer vhost", /\$\{stack\.hosts\.disasmer\.publicHost\}/);
|
||||
expect(hypervisor, "nginx Disasmer site root", /root = \.\.\/\.\.\/disasmer-site/);
|
||||
expect(hypervisor, "nginx control API proxy", /locations\."= \/api\/v1\/control"[\s\S]*proxyPass/);
|
||||
expect(hypervisor, "nginx hosted callback proxy", /locations\."= \/auth\/callback"[\s\S]*proxyPass/);
|
||||
assert.doesNotMatch(hypervisor, /locations\."= \/auth\/browser\/start"/);
|
||||
expect(hypervisor, "hosted service uses configured API port", /stack\.hosts\.disasmer\.apiPort/);
|
||||
expect(oauth, "Disasmer Authentik provider", /name: disasmer-provider/);
|
||||
expect(oauth, "Disasmer public OIDC client", /client_type: public/);
|
||||
expect(oauth, "Disasmer client id", /client_id: \${disasmerClientId}/);
|
||||
expect(oauth, "Disasmer hosted callback", /https:\/\/disasmer\.michelpaulissen\.com\/auth\/callback/);
|
||||
expect(oauth, "Disasmer Authentik application", /slug: disasmer/);
|
||||
expect(site, "site describes CLI login", /disasmer login --browser/);
|
||||
expect(site, "site describes hosted callback authority", /hosted service processes the callback/);
|
||||
expect(site, "site explains attached-node execution", /placed on a node you attach and explicitly authorize/);
|
||||
expect(site, "site labels pre-release status", /pre-release deployment under active verification/);
|
||||
assert.doesNotMatch(site, /local callback|provider codes and tokens are returned to the CLI/i);
|
||||
} else {
|
||||
console.warn("Skipping VPS config checks because ../michelpaulissen.com is not present");
|
||||
}
|
||||
|
||||
console.log("Public browser login contract smoke passed");
|
||||
69
scripts/public-local-demo-matrix-smoke.js
Executable file
69
scripts/public-local-demo-matrix-smoke.js
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing public local demo evidence: ${name}`);
|
||||
}
|
||||
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const cliInstall = read("scripts/cli-install-smoke.js");
|
||||
const flagship = read("scripts/flagship-demo-smoke.js");
|
||||
const cliLocalRun = read("scripts/cli-local-run-smoke.js");
|
||||
const nodeAttach = read("scripts/node-attach-smoke.js");
|
||||
const vscodeExtension = read("scripts/vscode-extension-smoke.js");
|
||||
const vscodeF5 = read("scripts/vscode-f5-smoke.js");
|
||||
const artifactDownload = read("scripts/artifact-download-smoke.js");
|
||||
const artifactExport = read("scripts/artifact-export-smoke.js");
|
||||
|
||||
for (const script of [publicAcceptance, publicSplit]) {
|
||||
for (const smoke of [
|
||||
"scripts/cli-install-smoke.js",
|
||||
"scripts/flagship-demo-smoke.js",
|
||||
"scripts/cli-local-run-smoke.js",
|
||||
"scripts/node-attach-smoke.js",
|
||||
"scripts/vscode-extension-smoke.js",
|
||||
"scripts/vscode-f5-smoke.js",
|
||||
"scripts/artifact-download-smoke.js",
|
||||
"scripts/artifact-export-smoke.js",
|
||||
]) {
|
||||
assert(script.includes(`node ${smoke}`), `public demo gate must run ${smoke}`);
|
||||
}
|
||||
}
|
||||
|
||||
expect(cliInstall, "CLI install from project path", /cargo[\s\S]*install[\s\S]*crates\/disasmer-cli/);
|
||||
expect(cliInstall, "CLI install smoke targets flagship project", /const project = path\.join\(repo, "examples\/launch-build-demo"\)/);
|
||||
expect(cliInstall, "installed CLI inspects flagship project", /installedBin[\s\S]*\["bundle", "inspect", "--project", project, "--json"\]/);
|
||||
|
||||
expect(flagship, "flagship project source is Rust workflow", /examples\/launch-build-demo[\s\S]*src\/build\.rs/);
|
||||
expect(flagship, "flagship source avoids local machine assumptions", /forbiddenSourceAssumptions/);
|
||||
expect(flagship, "flagship cargo test runs", /cargo[\s\S]*test[\s\S]*launch-build-demo/);
|
||||
|
||||
expect(cliLocalRun, "local run starts node process", /cli_process_started_node_process[\s\S]*true/);
|
||||
expect(cliLocalRun, "local run records real task events", /events\.events\.length >= 4[\s\S]*prepare_source[\s\S]*compile_linux[\s\S]*package_release/);
|
||||
expect(cliLocalRun, "local run records artifact metadata", /events\.events\.some\(\(event\) => event\.artifact_path\)/);
|
||||
|
||||
expect(nodeAttach, "Linux node attach creates enrollment grant", /create_node_enrollment_grant/);
|
||||
expect(nodeAttach, "Linux node attach uses enrollment exchange", /used_enrollment_exchange[\s\S]*true/);
|
||||
expect(nodeAttach, "attached Linux node proves signed enrollment", /used_enrollment_exchange[\s\S]*signedNodeHeartbeat/);
|
||||
|
||||
expect(vscodeExtension, "extension contributes debugger", /contributes\.debuggers[\s\S]*type === "disasmer"/);
|
||||
expect(vscodeF5, "F5 uses local-services backend", /runtimeBackend[\s\S]*local-services/);
|
||||
expect(vscodeF5, "F5 exposes virtual thread", /thread\.name\.includes\("build virtual process"\)[\s\S]*virtual thread/);
|
||||
expect(vscodeF5, "F5 does not fabricate a terminal event at the entry probe", /coordinator_task_events[\s\S]*value === 0[\s\S]*must not fabricate a terminal task event/);
|
||||
|
||||
expect(artifactDownload, "artifact download creates scoped link", /create_artifact_download_link/);
|
||||
expect(artifactDownload, "artifact download opens stream", /open_artifact_download_stream/);
|
||||
expect(artifactExport, "artifact export targets receiver node", /export_artifact_to_node[\s\S]*node-export-receiver/);
|
||||
expect(artifactExport, "artifact export rejects coordinator bulk relay", /coordinator_bulk_relay_allowed[\s\S]*false/);
|
||||
|
||||
console.log("Public local demo matrix smoke passed");
|
||||
185
scripts/public-private-boundary-smoke.js
Normal file
185
scripts/public-private-boundary-smoke.js
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function walk(relativePath, files = []) {
|
||||
const fullPath = path.join(repo, relativePath);
|
||||
if (!fs.existsSync(fullPath)) return files;
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
const base = path.basename(relativePath);
|
||||
if (["target", "node_modules", ".git", "containers-home"].includes(base)) {
|
||||
return files;
|
||||
}
|
||||
for (const entry of fs.readdirSync(fullPath)) {
|
||||
walk(path.join(relativePath, entry), files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
files.push(relativePath);
|
||||
return files;
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function copyPublicTree(sourceRoot, destinationRoot, relativePath = ".") {
|
||||
const skippedDirectories = new Set([
|
||||
".git",
|
||||
"target",
|
||||
"node_modules",
|
||||
"containers-home",
|
||||
"private",
|
||||
"experiments",
|
||||
]);
|
||||
const parts = relativePath.split(path.sep).filter(Boolean);
|
||||
if (parts.some((part) => skippedDirectories.has(part))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = path.join(sourceRoot, relativePath);
|
||||
const destination = path.join(destinationRoot, relativePath);
|
||||
const stat = fs.statSync(source);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
for (const entry of fs.readdirSync(source)) {
|
||||
copyPublicTree(sourceRoot, destinationRoot, path.join(relativePath, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.copyFileSync(source, destination);
|
||||
}
|
||||
|
||||
function assertPublicSplitTreeIsCoherent() {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-public-split-"));
|
||||
try {
|
||||
copyPublicTree(repo, tmp);
|
||||
|
||||
for (const forbidden of ["private", "experiments"]) {
|
||||
assert(
|
||||
!fs.existsSync(path.join(tmp, forbidden)),
|
||||
`${forbidden} must be absent from the public split tree`
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceToml = fs.readFileSync(path.join(tmp, "Cargo.toml"), "utf8");
|
||||
const membersBlock = workspaceToml.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
||||
assert(membersBlock, "public workspace must define members");
|
||||
const members = [...membersBlock[1].matchAll(/"([^"]+)"/g)].map(
|
||||
(match) => match[1]
|
||||
);
|
||||
assert(members.length > 0, "public workspace must list members");
|
||||
for (const member of members) {
|
||||
assert(
|
||||
!member.startsWith("private/") && !member.startsWith("experiments/"),
|
||||
`public workspace member must not point at filtered source: ${member}`
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(path.join(tmp, member, "Cargo.toml")),
|
||||
`public workspace member is missing after split: ${member}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const required of [
|
||||
"scripts/acceptance-public.sh",
|
||||
"scripts/verify-public-split.sh",
|
||||
"scripts/public-private-boundary-smoke.js",
|
||||
"scripts/public-local-demo-matrix-smoke.js",
|
||||
"vscode-extension/package.json",
|
||||
"examples/launch-build-demo/Cargo.toml",
|
||||
]) {
|
||||
assert(
|
||||
fs.existsSync(path.join(tmp, required)),
|
||||
`public split tree is missing ${required}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function publicSourceFiles() {
|
||||
return [
|
||||
...walk("crates"),
|
||||
...walk("examples"),
|
||||
...walk("scripts"),
|
||||
...walk("vscode-extension"),
|
||||
"Cargo.toml",
|
||||
].filter((file) => {
|
||||
if (file === "scripts/acceptance-private.sh") return false;
|
||||
if (file === "scripts/acceptance-evidence-contract-smoke.js") return false;
|
||||
if (file === "scripts/acceptance-environment-contract-smoke.js") return false;
|
||||
if (file === "scripts/docs-smoke.js") return false;
|
||||
if (file === "scripts/public-private-boundary-smoke.js") return false;
|
||||
if (file === "scripts/release-blocker-smoke.js") return false;
|
||||
return /\.(rs|toml|js|json|sh)$/.test(file);
|
||||
});
|
||||
}
|
||||
|
||||
const forbiddenPublicPatterns = [
|
||||
/\bdisasmer[_-]hosted[_-]policy\b/,
|
||||
/private\/hosted-policy/,
|
||||
/path\s*=\s*["'][^"']*private\//,
|
||||
/include!\s*\([^)]*private\//,
|
||||
/mod\s+private_hosted/,
|
||||
];
|
||||
|
||||
for (const file of publicSourceFiles()) {
|
||||
const content = read(file);
|
||||
for (const pattern of forbiddenPublicPatterns) {
|
||||
assert(
|
||||
!pattern.test(content),
|
||||
`public source ${file} must not reference private hosted code via ${pattern}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = read("Cargo.toml");
|
||||
assert(
|
||||
!/members\s*=\s*\[[\s\S]*private\//.test(workspace),
|
||||
"public workspace members must not include private hosted crates"
|
||||
);
|
||||
assertPublicSplitTreeIsCoherent();
|
||||
|
||||
const privateHostedRoot = path.join(repo, "private", "hosted-policy");
|
||||
if (fs.existsSync(privateHostedRoot)) {
|
||||
const privateCargo = read("private/hosted-policy/Cargo.toml");
|
||||
assert.match(privateCargo, /name\s*=\s*"disasmer-hosted-policy"/);
|
||||
assert.match(privateCargo, /disasmer-core\s*=\s*\{/);
|
||||
assert.match(privateCargo, /disasmer-coordinator\s*=\s*\{/);
|
||||
|
||||
const privateLib = read("private/hosted-policy/src/lib.rs");
|
||||
for (const required of [
|
||||
"AuthentikOidcConfig",
|
||||
"CommunityTierPolicy",
|
||||
"AdminControls",
|
||||
"preflight_zero_capability_hosted_wasm",
|
||||
"impl CapabilityPolicy for CommunityTierPolicy",
|
||||
"AuthContext",
|
||||
]) {
|
||||
assert(
|
||||
privateLib.includes(required),
|
||||
`private hosted policy is missing ${required}`
|
||||
);
|
||||
}
|
||||
|
||||
const privateFiles = walk("private").filter((file) => !file.includes("/target/"));
|
||||
const privateNodeRuntimeFiles = privateFiles.filter((file) =>
|
||||
/(^|\/)(disasmer-node|node-runtime)(\/|$)/.test(file)
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
privateNodeRuntimeFiles,
|
||||
[],
|
||||
"hosted mode must not carry a forked private node runtime"
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Public/private boundary smoke passed");
|
||||
321
scripts/public-release-dryrun-contract-smoke.js
Normal file
321
scripts/public-release-dryrun-contract-smoke.js
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const serviceEndpoint = "https://disasmer.michelpaulissen.com";
|
||||
const serviceHost = "disasmer.michelpaulissen.com";
|
||||
const forgejoHost = "git.michelpaulissen.com";
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(relativePath) {
|
||||
const file = path.join(repo, relativePath);
|
||||
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert(source !== null && source !== undefined, `missing source for ${name}`);
|
||||
assert.match(source, pattern, `missing public release dry-run evidence: ${name}`);
|
||||
}
|
||||
|
||||
const phase2 = maybeRead("acceptance_criteria_phase2.md");
|
||||
const readme = maybeRead("README.md");
|
||||
const cliSource = [
|
||||
read("crates/disasmer-cli/src/main.rs"),
|
||||
read("crates/disasmer-cli/src/config.rs"),
|
||||
read("crates/disasmer-cli/src/run.rs"),
|
||||
read("crates/disasmer-cli/src/client.rs"),
|
||||
read("crates/disasmer-cli/src/tests.rs"),
|
||||
].join("\n");
|
||||
const controlSource = read("crates/disasmer-control/src/lib.rs");
|
||||
const cliLoginSmoke = read("scripts/cli-login-smoke.js");
|
||||
const vscodePackage = JSON.parse(read("vscode-extension/package.json"));
|
||||
const vscodeExtension = read("vscode-extension/extension.js");
|
||||
const vscodeSmoke = read("scripts/vscode-extension-smoke.js");
|
||||
const prepScript = read("scripts/prepare-public-release-dryrun.js");
|
||||
const publishScript = read("scripts/publish-public-release-dryrun.js");
|
||||
const preflightScript = read("scripts/public-release-dryrun-preflight.js");
|
||||
const e2eScript = read("scripts/public-release-dryrun-e2e.js");
|
||||
const finalEvidenceScript = read("scripts/public-release-dryrun-final-evidence.js");
|
||||
const workflow = maybeRead(".forgejo/workflows/public-release-dryrun.yml");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
|
||||
if (readme) {
|
||||
expect(readme, "README", new RegExp(serviceHost.replaceAll(".", "\\.")));
|
||||
}
|
||||
|
||||
if (phase2) {
|
||||
const name = "phase 2 criteria";
|
||||
expect(phase2, name, new RegExp(serviceHost.replaceAll(".", "\\.")));
|
||||
expect(phase2, `${name} DNS publication state`, /DNS record|public DNS|dns[-_]publication/i);
|
||||
expect(phase2, `${name} resolver fallback`, /no resolver override|required resolver override|hosts entry|controlled resolution|fallback/i);
|
||||
expect(phase2, `${name} Forgejo host`, new RegExp(forgejoHost.replaceAll(".", "\\.")));
|
||||
expect(phase2, `${name} Forgejo Release`, /Forgejo Release/);
|
||||
expect(phase2, `${name} compiled assets`, /compiled\s+(release\s+)?assets|compiled release assets/);
|
||||
expect(phase2, `${name} filtered tree`, /private\/\*\*[\s\S]*experiments\/\*\*/);
|
||||
expect(phase2, `${name} root Markdown export filter`, /root `\*\.md`|root Markdown/);
|
||||
expect(phase2, `${name} host-neutral workflow filter`, /\.forgejo[\s\S]*--include-forgejo-workflows/);
|
||||
expect(phase2, `${name} GitHub release out of scope`, /GitHub[\s\S]*outside this dry run|public GitHub-release[\s\S]*out of scope/);
|
||||
}
|
||||
|
||||
expect(cliSource, "CLI default hosted coordinator constant", new RegExp(`DEFAULT_HOSTED_COORDINATOR_ENDPOINT: &str =[\\s\\S]*"${serviceEndpoint.replaceAll(".", "\\.")}"`));
|
||||
expect(cliSource, "login uses default hosted coordinator", /default_value_t = default_hosted_coordinator_endpoint\(\)/);
|
||||
expect(cliSource, "hosted run records coordinator endpoint", /hosted_coordinator_endpoint[\s\S]*Some\(default_hosted_coordinator_endpoint\(\)\)/);
|
||||
expect(controlSource, "hosted URL uses the shared HTTP control transport", /ControlTransport::Https[\s\S]*agent[\s\S]*\.post\(url\)/);
|
||||
expect(controlSource, "hosted control transport uses the canonical API path", /CONTROL_API_PATH: &str = "\/api\/v1\/control"/);
|
||||
expect(controlSource, "plain HTTP is restricted to loopback", /endpoint\.starts_with\("http:\/\/"\) && !endpoint_is_loopback\(endpoint\)/);
|
||||
expect(cliSource, "default hosted coordinator resolves to the HTTPS control API", /hosted_coordinator_remains_a_real_https_control_endpoint[\s\S]*https:\/\/disasmer\.michelpaulissen\.com\/api\/v1\/control/);
|
||||
assert.doesNotMatch(cliSource, /coord\.disasmer\.invalid/);
|
||||
|
||||
expect(cliLoginSmoke, "CLI login smoke covers default hosted coordinator", new RegExp(`defaultHostedCoordinatorEndpoint = "${serviceEndpoint.replaceAll(".", "\\.")}"`));
|
||||
assert.strictEqual(
|
||||
vscodePackage.contributes.debuggers[0].configurationAttributes.launch.properties.coordinatorEndpoint.default,
|
||||
undefined
|
||||
);
|
||||
expect(vscodeSmoke, "VS Code smoke covers session-inferred coordinator endpoint", /coordinatorEndpoint\.default, undefined/);
|
||||
|
||||
for (const binary of [
|
||||
"disasmer",
|
||||
"disasmer-coordinator",
|
||||
"disasmer-node",
|
||||
"disasmer-debug-dap",
|
||||
]) {
|
||||
expect(prepScript, `release prep includes ${binary}`, new RegExp(`"${binary}"`));
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["release prep filters private and experiments", /filteredTopLevel = \["private", "experiments", "\.git", "target"\]/],
|
||||
["release prep enumerates Git-ignored source paths", /function gitIgnoredSourcePaths\(\)[\s\S]*"ls-files"[\s\S]*"--ignored"[\s\S]*"--exclude-standard"[\s\S]*"--directory"/],
|
||||
["release prep has archive-safe ignored-path fallbacks without Git metadata", /archiveIgnoredPathFallbacks = \[[\s\S]*"target"[\s\S]*"\.disasmer"[\s\S]*"vscode-extension\/node_modules"[\s\S]*"scripts\/containers-home"[\s\S]*if \(output === null\) \{[\s\S]*return archiveIgnoredPathFallbacks/],
|
||||
["release prep filters Git-ignored source paths", /isGitIgnored\(childRelative, ignoredSourcePaths\)/],
|
||||
["release prep verifies Git-ignored paths are absent", /Git-ignored source path leaked into the dry-run public tree/],
|
||||
["release prep filters generated view state", /filteredDirectoryNames = \["\.disasmer"\]/],
|
||||
["release prep filters internal root Markdown", /isFilteredRootMarkdown[\s\S]*path\.extname\(entry\.name\)\.toLowerCase\(\) === "\.md"[\s\S]*README\.md[\s\S]*SECURITY\.md/],
|
||||
["release prep retains product README", /product README\.md is missing from the dry-run public tree/],
|
||||
["release prep filters .forgejo by default", /topLevel === "\.forgejo" && !includeForgejoWorkflows/],
|
||||
["release prep has Forgejo workflow opt-in argument", /--include-forgejo-workflows/],
|
||||
["release prep builds public release bins", /cargo"[\s\S]*"build"[\s\S]*"--workspace"[\s\S]*"--bins"[\s\S]*"--release"/],
|
||||
["release prep writes binary archive", /disasmer-public-binaries-\$\{releaseName\}-\$\{platformName\(\)\}\.tar\.gz/],
|
||||
["release prep writes source archive", /disasmer-public-source-\$\{releaseName\}\.tar\.gz/],
|
||||
["release prep writes extension VSIX", /\$\{packageJson\.name\}-\$\{packageJson\.version\}\.vsix[\s\S]*@vscode\/vsce[\s\S]*package/],
|
||||
["release prep packages extension non-interactively", /@vscode\/vsce[\s\S]*package[\s\S]*--skip-license/],
|
||||
["release prep writes selected-user guide", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\$\{releaseName\}\.md/],
|
||||
["release prep writes selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\$\{releaseName\}\.md/],
|
||||
["release prep accepts resolver instructions", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS/],
|
||||
["release prep accepts hosts entry", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY/],
|
||||
["release prep accepts deployment IP", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP/],
|
||||
["selected-user guide documents DNS or fallback", /public DNS[\s\S]*(fallback|hosts entry)|DNS record[\s\S]*controlled resolution/],
|
||||
["selected-user guide names hosted coordinator", /default hosted coordinator is the deployment/],
|
||||
["selected-user guide rejects standalone Core coordinator", /standalone open-source Core coordinator/],
|
||||
["selected-user invite documents friends dry run", /friends helping test[\s\S]*not broadly advertised/],
|
||||
["selected-user invite distinguishes Core", /hosted server is not the standalone Core\s+coordinator/],
|
||||
["selected-user guide documents default login", /disasmer login --browser/],
|
||||
["selected-user guide documents VSIX install", /code --install-extension disasmer-vscode-\*\.vsix/],
|
||||
["selected-user guide documents node attach", /disasmer node attach --coordinator https:\/\/disasmer\.michelpaulissen\.com/],
|
||||
["release prep packages generated evidence", /stageEvidenceAsset[\s\S]*EVIDENCE_BINDING\.json[\s\S]*disasmer-public-evidence-/],
|
||||
["release prep binds binary digests", /binary_digests: binaryDigests/],
|
||||
["release prep writes checksums", /SHA256SUMS/],
|
||||
["release prep writes manifest", /public-release-manifest\.json/],
|
||||
["release prep writes public tree provenance", /DISASMER_PUBLIC_TREE\.json/],
|
||||
["release prep records public tree identity", /public_tree_identity: publicTreeIdentity/],
|
||||
["release prep records DNS state", /dns_publication_state:/],
|
||||
["release prep records resolver override", /resolver_override:/],
|
||||
["release prep records Forgejo URLs", /public_repo_url:[\s\S]*public_repo_remote:[\s\S]*forgejo_release_url:/],
|
||||
["release prep requires publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE/],
|
||||
["release prep requires Forgejo remote", /remote\.includes\(forgejoHost\)/],
|
||||
["release prep can push filtered tree", /git"[\s\S]*"push"[\s\S]*"public"[\s\S]*`HEAD:\$\{publicRepoBranch\}`/],
|
||||
["release prep records publish result", /public_tree_publish: publicTreePublish/],
|
||||
["release prep publishes tree before building target", /const publicTreePublish = publishPublicTree[\s\S]*buildPublicBinaries\(\)/],
|
||||
]) {
|
||||
expect(prepScript, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["release publisher requires Forgejo token", /DISASMER_FORGEJO_TOKEN/],
|
||||
["release publisher infers Forgejo repo identity", /function resolveRepoIdentity\(manifest\)[\s\S]*parseForgejoRepoIdentity/],
|
||||
["release publisher uses manifest public repo URL", /manifest\.public_repo_url[\s\S]*manifest\.public_repo_remote/],
|
||||
["release publisher uses token auth", /Authorization: `token \$\{token\}`/],
|
||||
["release publisher lists releases", /\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases\?limit=100/],
|
||||
["release publisher creates release", /POST[\s\S]*\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases/],
|
||||
["release publisher reloads release details", /function loadRelease\(releaseId\)[\s\S]*\/releases\/\$\{releaseId\}/],
|
||||
["release publisher uploads assets", /releases\/\$\{release\.id\}\/assets\?name=\$\{encodeURIComponent\(asset\.name\)\}/],
|
||||
["release publisher uses attachment multipart field", /multipartFile\("attachment", asset\.file\)/],
|
||||
["release publisher skips already attached assets", /existingAssetByName\(release, asset\.name\)/],
|
||||
["release publisher rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
|
||||
["release publisher checks manifest kind", /manifest\.kind !== "disasmer-public-release-dryrun"/],
|
||||
["release publisher requires public tree push", /public tree must be pushed before publishing the Forgejo Release/],
|
||||
["release publisher accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/],
|
||||
["release publisher targets external public tree commit", /function publicTreeCommit\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_COMMIT/],
|
||||
["release publisher writes evidence report", /public-release-dryrun-forgejo-release\.json/],
|
||||
["release publisher records reused assets", /reused_assets:/],
|
||||
]) {
|
||||
expect(publishScript, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["preflight rejects stale release manifests", /manifest\.source_commit[\s\S]*currentSourceCommit/],
|
||||
["preflight disables interactive remote prompts", /function nonInteractiveEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*git", \["ls-remote"[\s\S]*timeout/],
|
||||
["preflight accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/],
|
||||
["preflight verifies public branch commit", /remoteMain[\s\S]*publicTreeCommit/],
|
||||
["preflight verifies local asset checksums", /parseSha256Sums[\s\S]*checksum mismatch/],
|
||||
["preflight records public tree push source", /public_tree_push_source/],
|
||||
["preflight records pending external gates", /external_gates:[\s\S]*forgejo_release_publication[\s\S]*public_release_e2e/],
|
||||
["preflight writes evidence report", /public-release-dryrun-preflight\.json/],
|
||||
]) {
|
||||
expect(preflightScript, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/],
|
||||
["e2e runner rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
|
||||
["e2e runner disables interactive git prompts", /function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/],
|
||||
["e2e runner accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/],
|
||||
["e2e runner downloads Forgejo Release assets", /downloadReleaseAssets/],
|
||||
["e2e runner verifies SHA256SUMS", /verifyChecksums/],
|
||||
["e2e runner clones public repo", /"git", \["clone", "--depth", "1"/],
|
||||
["e2e runner rejects private tree leaks", /private[\s\S]*experiments[\s\S]*target/],
|
||||
["e2e runner rejects internal root Markdown leaks", /internal root Markdown file leaked into public repo/],
|
||||
["e2e runner requires product README", /public checkout must include product README\.md/],
|
||||
["e2e runner rejects .forgejo leaks by default", /\.forgejo\/ leaked into public repo/],
|
||||
["e2e runner rejects generated view state", /generated \.disasmer view state leaked into public repo/],
|
||||
["e2e runner checks public tree identity", /hashTree\(checkout\)[\s\S]*manifest\.public_tree_identity/],
|
||||
["e2e runner extracts public binaries", /tar"[\s\S]*"-xzf"/],
|
||||
["e2e runner loads public coordinator binary", /executable\(installDir, "disasmer-coordinator"\)/],
|
||||
["e2e runner checks default hosted coordinator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/],
|
||||
["e2e runner requires an external browser driver", /DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND/],
|
||||
["e2e runner uses the CLI browser opener boundary", /DISASMER_BROWSER_OPEN_COMMAND: browserOpenCommand/],
|
||||
["e2e runner derives scope from the hosted session", /const tenant = loginSession\.tenant[\s\S]*const project = loginSession\.project[\s\S]*const user = loginSession\.user/],
|
||||
["e2e runner uses public CLI node attach", /"node"[\s\S]*"attach"[\s\S]*serviceEndpoint/],
|
||||
["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/],
|
||||
["e2e runner launches the released product through CLI", /"run"[\s\S]*"build"[\s\S]*runReport\.status[\s\S]*main_launched/],
|
||||
["standalone Core proof launches a real Wasm TaskSpec", /type: "launch_task"[\s\S]*task_spec:[\s\S]*kind: "coordinator_node_wasm"[\s\S]*bundle_digest: manifest\.bundle_digest/],
|
||||
["e2e runner verifies assignment polling", /worker_assignment_poll_verified/],
|
||||
["e2e runner validates standalone Core coordinator", /validateStandaloneCoreCoordinator/],
|
||||
["e2e runner uses canonical standalone Core wire envelopes", /function sendCore\(addr, message\)[\s\S]*coordinatorWireRequest\(message, "public-e2e-core"\)/],
|
||||
["e2e runner records public coordinator validation", /core_coordinator_validated/],
|
||||
["e2e runner validates released live DAP", /validateReleasedLiveDap/],
|
||||
["e2e runner verifies attach-mode Debug Epoch transition", /validateReleasedLiveDap[\s\S]*resumed through coordinator[\s\S]*requested freeze/],
|
||||
["e2e runner verifies attached coordinator task completion", /validateReleasedLiveDap[\s\S]*variable\.name === "state" && variable\.value === "Completed"[\s\S]*coordinator task event from node/],
|
||||
["e2e runner attaches live DAP to Client-authorized process", /attached_to_existing_client_process/],
|
||||
["e2e runner verifies task events", /type: "list_task_events"/],
|
||||
["e2e runner verifies download link", /type: "create_artifact_download_link"/],
|
||||
["e2e runner verifies VS Code debugger", /scripts\/vscode-f5-smoke\.js/],
|
||||
["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/],
|
||||
]) {
|
||||
expect(e2eScript, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["final evidence reads manifest", /public-release-manifest\.json/],
|
||||
["final evidence rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
|
||||
["final evidence verifies filtered export", /function assertFilteredOut\(manifest\)/],
|
||||
["final evidence reads Forgejo Release report", /public-release-dryrun-forgejo-release\.json/],
|
||||
["final evidence reads deployment manifest", /deployment-manifest\.json/],
|
||||
["final evidence reads service smoke report", /public-release-dryrun-service\.json/],
|
||||
["final evidence reads hosted Client compatibility report", /hosted-client-compat\.json/],
|
||||
["final evidence reads Core coordinator compatibility report", /core-coordinator-compat\.json/],
|
||||
["final evidence requires public e2e report", /public-release-dryrun-e2e\.json/],
|
||||
["final evidence verifies Forgejo host", /forgejoHost = "git\.michelpaulissen\.com"/],
|
||||
["final evidence verifies default hosted coordinator", /serviceEndpoint = "https:\/\/disasmer\.michelpaulissen\.com"/],
|
||||
["final evidence accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/],
|
||||
["final evidence records resolved public repo", /resolvedPublicRepoRemote[\s\S]*public_repository_url: resolvedPublicRepoRemote/],
|
||||
["final evidence requires private hosted coordinator", /coordinator_implementation[\s\S]*"hosted-policy-coordinator"/],
|
||||
["final evidence requires service private coordinator marker", /service\.coordinator_implementation[\s\S]*"hosted-policy-coordinator"/],
|
||||
["final evidence requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final evidence requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final evidence requires current Client compatibility source", /compat\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final evidence requires current Client compatibility release", /compat\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final evidence requires current public coordinator source", /coreCoordinator\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
["final evidence requires current public coordinator release", /coreCoordinator\.release_name[\s\S]*manifest\.release_name/],
|
||||
["final evidence requires server-generated service enrollment", /"server_generated_enrollment_grant"/],
|
||||
["final evidence requires service probe cleanup", /"process_abort"/],
|
||||
["final evidence requires process main launch", /e2e\.launch_task_response[\s\S]*"main_launched"/],
|
||||
["final evidence requires assignment polling", /worker_assignment_poll_verified/],
|
||||
["final evidence requires public coordinator validation", /core_coordinator_validated/],
|
||||
["final evidence requires standalone Core coordinator", /standalone-core-coordinator/],
|
||||
["final evidence requires released live DAP", /released_live_dap_verified/],
|
||||
["final evidence requires pushed public tree", /publicTreeAlreadyPushed\(manifest\)/],
|
||||
["final evidence requires release assets", /downloaded_release_assets/],
|
||||
["final evidence requires VS Code debugger", /vscode_debugger_verified/],
|
||||
["final evidence writes final report", /public-release-dryrun-final\.json/],
|
||||
]) {
|
||||
expect(finalEvidenceScript, name, pattern);
|
||||
}
|
||||
|
||||
if (workflow) {
|
||||
for (const [name, pattern] of [
|
||||
["manual Forgejo workflow", /workflow_dispatch:/],
|
||||
["Linux Forgejo asset job", /linux-assets:[\s\S]*runs-on: docker/],
|
||||
["Windows Forgejo asset job", /windows-assets:[\s\S]*runs-on: windows/],
|
||||
["Windows runner caveat", /intermittently online/],
|
||||
["workflow runs release prep", /node scripts\/prepare-public-release-dryrun\.js/],
|
||||
["workflow uploads assets", /actions\/upload-artifact@v4[\s\S]*target\/public-release-dryrun\/assets\/\*/],
|
||||
]) {
|
||||
expect(workflow, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
prepScript,
|
||||
"public tree prep disables interactive git prompts",
|
||||
/function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/
|
||||
);
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
assert(
|
||||
script.includes("node scripts/public-release-dryrun-contract-smoke.js"),
|
||||
`${scriptName} must run public-release-dryrun-contract-smoke.js`
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/prepare-public-release-dryrun.js"),
|
||||
`${scriptName} must run prepare-public-release-dryrun.js`
|
||||
);
|
||||
assert(
|
||||
script.includes("node scripts/publish-public-release-dryrun.js"),
|
||||
`${scriptName} must be able to publish the Forgejo Release`
|
||||
);
|
||||
assert(
|
||||
script.includes("DISASMER_FORGEJO_TOKEN"),
|
||||
`${scriptName} must gate Forgejo Release publishing on a token`
|
||||
);
|
||||
assert(
|
||||
script.includes('if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]'),
|
||||
`${scriptName} must not require owner/repo env when the manifest can infer the Forgejo repository`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["private acceptance", privateAcceptance],
|
||||
]) {
|
||||
if (scriptName === "public acceptance") {
|
||||
assert(
|
||||
script.includes("node scripts/public-release-dryrun-e2e.js"),
|
||||
"public acceptance must be able to run public-release-dryrun-e2e.js"
|
||||
);
|
||||
assert(
|
||||
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"),
|
||||
"public acceptance must gate public-release-dryrun-e2e.js"
|
||||
);
|
||||
}
|
||||
assert(
|
||||
script.includes("node scripts/public-release-dryrun-final-evidence.js"),
|
||||
`${scriptName} must be able to run public-release-dryrun-final-evidence.js`
|
||||
);
|
||||
assert(
|
||||
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"),
|
||||
`${scriptName} must gate final public release dry-run evidence`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Public release dry-run contract smoke passed");
|
||||
1422
scripts/public-release-dryrun-e2e.js
Executable file
1422
scripts/public-release-dryrun-e2e.js
Executable file
File diff suppressed because it is too large
Load diff
515
scripts/public-release-dryrun-final-evidence.js
Executable file
515
scripts/public-release-dryrun-final-evidence.js
Executable file
|
|
@ -0,0 +1,515 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
assertFinalLedger,
|
||||
readPhase3Ledger,
|
||||
} = require("./phase3-ledger");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const releaseRoot = path.resolve(
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/public-release-dryrun")
|
||||
);
|
||||
const acceptanceRoot = path.join(repo, "target/acceptance");
|
||||
const serviceEndpoint = "https://disasmer.michelpaulissen.com";
|
||||
const serviceHost = "disasmer.michelpaulissen.com";
|
||||
const serviceAddr = `${serviceHost}:443`;
|
||||
const forgejoHost = "git.michelpaulissen.com";
|
||||
|
||||
const inputs = {
|
||||
manifest: process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
|
||||
path.join(releaseRoot, "public-release-manifest.json"),
|
||||
forgejoRelease: process.env.DISASMER_PUBLIC_RELEASE_FORGEJO_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json"),
|
||||
deployment: process.env.DISASMER_PUBLIC_RELEASE_DEPLOYMENT_MANIFEST ||
|
||||
path.join(releaseRoot, "deployment/stage/deployment-manifest.json"),
|
||||
service: process.env.DISASMER_PUBLIC_RELEASE_SERVICE_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-service.json"),
|
||||
hostedClientCompat: process.env.DISASMER_HOSTED_CLIENT_COMPAT_REPORT ||
|
||||
path.join(acceptanceRoot, "hosted-client-compat.json"),
|
||||
coreCoordinatorCompat: process.env.DISASMER_CORE_COORDINATOR_COMPAT_REPORT ||
|
||||
path.join(acceptanceRoot, "core-coordinator-compat.json"),
|
||||
e2e: process.env.DISASMER_PUBLIC_RELEASE_E2E_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-e2e.json"),
|
||||
};
|
||||
|
||||
assertFinalLedger(readPhase3Ledger(repo));
|
||||
|
||||
function readJson(name, file) {
|
||||
assert(fs.existsSync(file), `missing ${name} evidence: ${file}`);
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function assertIncludes(value, expected, message) {
|
||||
assert(
|
||||
typeof value === "string" && value.includes(expected),
|
||||
`${message}: expected ${JSON.stringify(value)} to include ${expected}`
|
||||
);
|
||||
}
|
||||
|
||||
function assetNames(manifest) {
|
||||
assert(Array.isArray(manifest.assets), "manifest assets must be an array");
|
||||
return new Set(manifest.assets.map((asset) => asset.name));
|
||||
}
|
||||
|
||||
function uploadedAssetNames(report) {
|
||||
return new Set(
|
||||
[...(report.uploaded_assets || []), ...(report.reused_assets || [])].map(
|
||||
(asset) => asset.name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function assertEvidenceBooleans(report, fields) {
|
||||
for (const field of fields) {
|
||||
assert.strictEqual(report[field], true, `e2e report must set ${field}=true`);
|
||||
}
|
||||
}
|
||||
|
||||
function compactToolVersions(...reports) {
|
||||
const versions = {};
|
||||
for (const [name, report] of reports) {
|
||||
if (report && report.tool_versions) {
|
||||
versions[name] = report.tool_versions;
|
||||
}
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
function assertDnsState(value, name) {
|
||||
assert(
|
||||
["not-published", "published"].includes(value),
|
||||
`${name} has unexpected DNS publication state: ${value}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertFilteredOut(manifest) {
|
||||
assert(Array.isArray(manifest.filtered_out), "manifest filtered_out must be an array");
|
||||
for (const expected of [
|
||||
"private/**",
|
||||
"experiments/**",
|
||||
".git",
|
||||
"target",
|
||||
"root/*.md except README.md and SECURITY.md",
|
||||
"**/.disasmer/**",
|
||||
]) {
|
||||
assert(
|
||||
manifest.filtered_out.includes(expected),
|
||||
`manifest filtered_out must include ${expected}`
|
||||
);
|
||||
}
|
||||
const includeForgejoWorkflows =
|
||||
manifest.public_export && manifest.public_export.include_forgejo_workflows;
|
||||
if (!includeForgejoWorkflows) {
|
||||
assert(
|
||||
manifest.filtered_out.includes(".forgejo/**"),
|
||||
"manifest filtered_out must include .forgejo/** for the host-neutral export"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function publicTreeAlreadyPushed(manifest) {
|
||||
return (
|
||||
(manifest.public_tree_publish && manifest.public_tree_publish.pushed === true) ||
|
||||
process.env.DISASMER_PUBLIC_TREE_ALREADY_PUSHED === "1"
|
||||
);
|
||||
}
|
||||
|
||||
function publicRepoRemote(manifest) {
|
||||
return (
|
||||
manifest.public_repo_url ||
|
||||
manifest.public_repo_remote ||
|
||||
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
|
||||
process.env.DISASMER_PUBLIC_REPO_URL ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function commandOutput(command, args) {
|
||||
try {
|
||||
return require("child_process")
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedSourceCommit() {
|
||||
return (
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
const manifest = readJson("public release manifest", inputs.manifest);
|
||||
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
|
||||
assert.strictEqual(
|
||||
manifest.source_tree_clean,
|
||||
true,
|
||||
"final release evidence cannot be produced from a dirty source tree"
|
||||
);
|
||||
for (const binary of [
|
||||
"disasmer",
|
||||
"disasmer-coordinator",
|
||||
"disasmer-node",
|
||||
"disasmer-debug-dap",
|
||||
]) {
|
||||
const platformName = process.platform === "win32" ? `${binary}.exe` : binary;
|
||||
assert.match(
|
||||
manifest.binary_digests && manifest.binary_digests[platformName],
|
||||
/^sha256:[a-f0-9]{64}$/,
|
||||
`manifest omitted release binary digest for ${platformName}`
|
||||
);
|
||||
}
|
||||
assert.strictEqual(
|
||||
manifest.source_commit,
|
||||
expectedSourceCommit(),
|
||||
"final public release evidence must be generated from the current acceptance commit"
|
||||
);
|
||||
assert.strictEqual(manifest.default_hosted_coordinator_endpoint, serviceEndpoint);
|
||||
assert.strictEqual(manifest.forgejo_host, forgejoHost);
|
||||
assertDnsState(manifest.dns_publication_state, "manifest");
|
||||
assert(manifest.resolver_override, "manifest must record resolver override");
|
||||
assertFilteredOut(manifest);
|
||||
assert.strictEqual(
|
||||
publicTreeAlreadyPushed(manifest),
|
||||
true,
|
||||
"filtered public tree must be pushed to Forgejo"
|
||||
);
|
||||
const resolvedPublicRepoRemote = publicRepoRemote(manifest);
|
||||
assertIncludes(
|
||||
resolvedPublicRepoRemote,
|
||||
forgejoHost,
|
||||
"manifest public repository must point at Forgejo"
|
||||
);
|
||||
|
||||
const manifestAssets = assetNames(manifest);
|
||||
for (const pattern of [
|
||||
/^disasmer-public-source-/,
|
||||
/^disasmer-public-binaries-/,
|
||||
/^disasmer-public-evidence-/,
|
||||
/^disasmer-vscode-/,
|
||||
/^DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-/,
|
||||
/^DISASMER_PUBLIC_DRYRUN_INVITE-/,
|
||||
/^SHA256SUMS$/,
|
||||
]) {
|
||||
assert(
|
||||
[...manifestAssets].some((name) => pattern.test(name)),
|
||||
`manifest is missing asset matching ${pattern}`
|
||||
);
|
||||
}
|
||||
|
||||
const forgejoRelease = readJson("Forgejo Release report", inputs.forgejoRelease);
|
||||
assert.strictEqual(
|
||||
forgejoRelease.kind,
|
||||
"disasmer-public-release-dryrun-forgejo-release"
|
||||
);
|
||||
assertIncludes(forgejoRelease.forgejo_url, forgejoHost, "Forgejo Release host");
|
||||
assert.strictEqual(forgejoRelease.release_name, manifest.release_name);
|
||||
assert.strictEqual(
|
||||
forgejoRelease.default_hosted_coordinator_endpoint,
|
||||
manifest.default_hosted_coordinator_endpoint
|
||||
);
|
||||
assert.strictEqual(
|
||||
forgejoRelease.public_tree_identity,
|
||||
manifest.public_tree_identity
|
||||
);
|
||||
assert.strictEqual(forgejoRelease.source_commit, manifest.source_commit);
|
||||
const releaseAssets = uploadedAssetNames(forgejoRelease);
|
||||
for (const asset of manifestAssets) {
|
||||
assert(releaseAssets.has(asset), `Forgejo Release is missing asset ${asset}`);
|
||||
}
|
||||
|
||||
const deployment = readJson("deployment manifest", inputs.deployment);
|
||||
assert.strictEqual(deployment.kind, "disasmer-public-release-dryrun-deployment");
|
||||
assert.strictEqual(deployment.default_hosted_coordinator_endpoint, serviceEndpoint);
|
||||
assert.strictEqual(deployment.coordinator_implementation, "hosted-policy-coordinator");
|
||||
assert.match(
|
||||
deployment.public_release_meaning || "",
|
||||
/public repository[\s\S]*public release assets[\s\S]*public client protocol/
|
||||
);
|
||||
assert.strictEqual(deployment.service_host, serviceHost);
|
||||
assert.strictEqual(deployment.service_addr, serviceAddr);
|
||||
assertDnsState(deployment.dns_publication_state, "deployment");
|
||||
assert(deployment.resolver_override, "deployment manifest must record resolver override");
|
||||
assert(deployment.deployed_service_commit, "deployment must record deployed service commit");
|
||||
|
||||
const service = readJson("live service smoke report", inputs.service);
|
||||
assert.strictEqual(service.kind, "disasmer-public-release-dryrun-service");
|
||||
assert.strictEqual(service.source_commit, manifest.source_commit);
|
||||
assert.strictEqual(service.release_name, manifest.release_name);
|
||||
assert.strictEqual(service.endpoint, serviceEndpoint);
|
||||
assert.strictEqual(service.service_addr, serviceAddr);
|
||||
assert.strictEqual(service.coordinator_implementation, "hosted-policy-coordinator");
|
||||
assertDnsState(service.dns_publication_state, "service smoke");
|
||||
assert(service.resolver_override, "service smoke must record resolver override");
|
||||
assert.strictEqual(
|
||||
service.public_tree_identity || manifest.public_tree_identity,
|
||||
manifest.public_tree_identity
|
||||
);
|
||||
assert.strictEqual(
|
||||
service.deployed_service_commit || deployment.deployed_service_commit,
|
||||
deployment.deployed_service_commit
|
||||
);
|
||||
assert(service.deployment_config_commit, "service smoke must record deployment config commit");
|
||||
assert(
|
||||
service.deployment_system_generation,
|
||||
"service smoke must record the activated NixOS system generation"
|
||||
);
|
||||
assert(service.service_unit, "service smoke must record the active service unit fragment");
|
||||
for (const key of [
|
||||
"ping",
|
||||
"login",
|
||||
"projects",
|
||||
"forged_unsigned_project",
|
||||
"session_scope",
|
||||
"server_generated_enrollment_grant",
|
||||
"node_credential",
|
||||
"signed_node_heartbeat",
|
||||
"signed_node_capabilities",
|
||||
"scoped_node_listing",
|
||||
"process_start",
|
||||
"debug_attach",
|
||||
"process_abort",
|
||||
]) {
|
||||
assert(service.evidence && service.evidence[key], `service smoke missing ${key}`);
|
||||
}
|
||||
assert.strictEqual(service.evidence.forged_unsigned_project, "error");
|
||||
assert.strictEqual(service.evidence.session_scope, "derived_from_server_identity");
|
||||
if (service.acceptance_result) {
|
||||
assert.strictEqual(service.acceptance_result, "passed");
|
||||
}
|
||||
|
||||
const compat = readJson("hosted Client compatibility report", inputs.hostedClientCompat);
|
||||
assert.strictEqual(compat.kind, "disasmer-hosted-client-compatibility");
|
||||
assert.strictEqual(compat.source_commit, manifest.source_commit);
|
||||
assert.strictEqual(compat.release_name, manifest.release_name);
|
||||
assert.strictEqual(
|
||||
compat.public_cli_attach.coordinator_response,
|
||||
"node_enrollment_exchanged"
|
||||
);
|
||||
assert.strictEqual(compat.public_cli_attach.heartbeat_response, "node_heartbeat");
|
||||
assert.strictEqual(compat.server_owned_browser_login.response, "oidc_browser_session");
|
||||
assert(compat.server_owned_browser_login.coordinator_requests >= 2);
|
||||
assert.strictEqual(
|
||||
compat.server_owned_browser_login.provider_tokens_exposed_to_cli,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(compat.server_owned_browser_login.session_scope_derived, true);
|
||||
assert.strictEqual(compat.authority_denials.forged_unsigned_project, "error");
|
||||
assert.strictEqual(compat.authority_denials.cross_tenant_task_events, "error");
|
||||
assert.strictEqual(compat.session_lifecycle.logout_revoked, true);
|
||||
assert.strictEqual(compat.session_lifecycle.revoked_reuse, "error");
|
||||
assert.strictEqual(compat.session_lifecycle.expired_reuse, "error");
|
||||
|
||||
const coreCoordinator = readJson(
|
||||
"Core coordinator compatibility report",
|
||||
inputs.coreCoordinatorCompat
|
||||
);
|
||||
assert.strictEqual(coreCoordinator.kind, "disasmer-core-coordinator-compatibility");
|
||||
assert.strictEqual(coreCoordinator.source_commit, manifest.source_commit);
|
||||
assert.strictEqual(coreCoordinator.release_name, manifest.release_name);
|
||||
assert.strictEqual(
|
||||
coreCoordinator.coordinator_implementation,
|
||||
"standalone-core-coordinator"
|
||||
);
|
||||
assert.strictEqual(coreCoordinator.client_authority, "strict");
|
||||
assert.strictEqual(coreCoordinator.authenticated_session, true);
|
||||
assert.strictEqual(coreCoordinator.forged_body_authority_denied, "error");
|
||||
assert.strictEqual(coreCoordinator.wrong_session_denied, "error");
|
||||
assert.strictEqual(coreCoordinator.self_hosted_cli.connected, "connected");
|
||||
assert.strictEqual(coreCoordinator.self_hosted_cli.secret_read_from_stdin, true);
|
||||
assert.strictEqual(coreCoordinator.self_hosted_cli.secret_exposed_in_report, false);
|
||||
assert.strictEqual(coreCoordinator.self_hosted_cli.authenticated_status, "auth_status");
|
||||
assert.strictEqual(
|
||||
coreCoordinator.self_hosted_admin.nonce_bound_proof_succeeded,
|
||||
"admin_status"
|
||||
);
|
||||
assert.strictEqual(coreCoordinator.self_hosted_admin.replay_denied, "error");
|
||||
if (process.platform !== "win32") {
|
||||
assert.strictEqual(coreCoordinator.self_hosted_cli.session_file_mode, "600");
|
||||
}
|
||||
assert.strictEqual(coreCoordinator.task_placement, "task_placement");
|
||||
assert.strictEqual(coreCoordinator.task_completion, "task_recorded");
|
||||
assert(coreCoordinator.task_events >= 1, "Core coordinator compat must record task events");
|
||||
assert.strictEqual(coreCoordinator.artifact_export_plan, "artifact_export_plan");
|
||||
|
||||
const e2e = readJson("public repository e2e report", inputs.e2e);
|
||||
assert.strictEqual(e2e.kind, "disasmer-public-release-dryrun-e2e");
|
||||
assert.strictEqual(e2e.default_hosted_coordinator_endpoint, serviceEndpoint);
|
||||
assert.strictEqual(e2e.service_addr, serviceAddr);
|
||||
assert.strictEqual(e2e.launch_task_verified, true);
|
||||
assert.strictEqual(e2e.worker_assignment_poll_verified, true);
|
||||
assert.strictEqual(e2e.worker_assignment_poll_protocol, "poll_task_assignment");
|
||||
assert.strictEqual(e2e.launch_task_response, "main_launched");
|
||||
assert.strictEqual(e2e.worker_assignment_process, e2e.process);
|
||||
assert.strictEqual(e2e.core_coordinator_validated, true);
|
||||
assert.strictEqual(
|
||||
e2e.core_coordinator_implementation,
|
||||
"standalone-core-coordinator"
|
||||
);
|
||||
assert.strictEqual(e2e.core_coordinator_client_authority, "strict");
|
||||
assert.strictEqual(e2e.core_coordinator_authenticated_session, true);
|
||||
assert.strictEqual(e2e.core_coordinator_forged_body_authority_denied, "error");
|
||||
assert.strictEqual(e2e.core_coordinator_wrong_session_denied, "error");
|
||||
assert.strictEqual(e2e.core_coordinator_launch_task_response, "task_launched");
|
||||
assert.strictEqual(e2e.core_coordinator_assignment_response, "task_assignment");
|
||||
assert(e2e.core_coordinator_task_events >= 1, "e2e must record public coordinator task events");
|
||||
assert.strictEqual(e2e.released_live_dap_verified, true);
|
||||
assert.strictEqual(
|
||||
e2e.released_live_dap_coordinator_implementation,
|
||||
"hosted-policy-coordinator"
|
||||
);
|
||||
assert.strictEqual(e2e.released_live_dap_attached_to_client_process, true);
|
||||
assert.strictEqual(e2e.released_live_dap_authenticated_with_cli_session, true);
|
||||
assert(e2e.released_live_dap_task_events >= 1, "e2e must record released live-DAP task events");
|
||||
assert(
|
||||
e2e.evidence &&
|
||||
e2e.evidence.released_live_dap &&
|
||||
e2e.evidence.released_live_dap.attached_to_existing_client_process === true,
|
||||
"e2e must prove released DAP attaches to the existing Client-authorized process"
|
||||
);
|
||||
assert(
|
||||
e2e.evidence &&
|
||||
e2e.evidence.released_live_dap &&
|
||||
e2e.evidence.released_live_dap.variables_verified === true,
|
||||
"e2e must prove released live-DAP variables are inspectable"
|
||||
);
|
||||
if (e2e.dns_publication_state) {
|
||||
assertDnsState(e2e.dns_publication_state, "public repository e2e");
|
||||
}
|
||||
if (e2e.acceptance_result) {
|
||||
assert.strictEqual(e2e.acceptance_result, "passed");
|
||||
}
|
||||
assertIncludes(e2e.public_repository_url, forgejoHost, "e2e public repo URL");
|
||||
assert.strictEqual(e2e.release_name, manifest.release_name);
|
||||
assert.strictEqual(e2e.public_tree_identity, manifest.public_tree_identity);
|
||||
assert.strictEqual(e2e.source_commit, manifest.source_commit);
|
||||
assertEvidenceBooleans(e2e, [
|
||||
"downloaded_release_assets",
|
||||
"verified_checksums",
|
||||
"clean_public_checkout",
|
||||
"public_repo_build_or_install",
|
||||
"default_operator_selected",
|
||||
"browser_or_cli_login",
|
||||
"attached_user_node",
|
||||
"core_coordinator_validated",
|
||||
"released_live_dap_verified",
|
||||
"ran_flagship_workflow",
|
||||
"vscode_debugger_verified",
|
||||
"logs_verified",
|
||||
"artifact_metadata_verified",
|
||||
"artifact_download_or_export_verified",
|
||||
]);
|
||||
assert(Array.isArray(e2e.commands) && e2e.commands.length > 0);
|
||||
assert(Array.isArray(e2e.tool_versions) || typeof e2e.tool_versions === "object");
|
||||
|
||||
const finalReport = {
|
||||
kind: "disasmer-public-release-dryrun-final-evidence",
|
||||
release_name: manifest.release_name,
|
||||
source_commit: manifest.source_commit,
|
||||
public_tree_identity: manifest.public_tree_identity,
|
||||
deployed_service_commit: deployment.deployed_service_commit,
|
||||
default_hosted_coordinator_endpoint: serviceEndpoint,
|
||||
service_addr: serviceAddr,
|
||||
dns_publication_state: {
|
||||
manifest: manifest.dns_publication_state,
|
||||
deployment: deployment.dns_publication_state,
|
||||
service: service.dns_publication_state,
|
||||
public_repository_e2e: e2e.dns_publication_state || null,
|
||||
},
|
||||
resolver_override: {
|
||||
manifest: manifest.resolver_override,
|
||||
deployment: deployment.resolver_override,
|
||||
service: service.resolver_override,
|
||||
public_repository_e2e: e2e.resolver_override || null,
|
||||
},
|
||||
deployment_config: {
|
||||
repo: service.deployment_config_repo || null,
|
||||
commit: service.deployment_config_commit || null,
|
||||
system_generation: service.deployment_system_generation || null,
|
||||
service_unit: service.service_unit || null,
|
||||
},
|
||||
coordinator_validation: {
|
||||
private_hosted_default_operator: {
|
||||
coordinator_implementation: service.coordinator_implementation,
|
||||
service_addr: service.service_addr,
|
||||
launch_task: service.evidence.launch_task,
|
||||
assignment_poll: service.evidence.assignment_poll,
|
||||
released_live_dap: e2e.evidence.released_live_dap,
|
||||
},
|
||||
standalone_core_coordinator: {
|
||||
coordinator_implementation: coreCoordinator.coordinator_implementation,
|
||||
client_authority: coreCoordinator.client_authority,
|
||||
authenticated_session: coreCoordinator.authenticated_session,
|
||||
forged_body_authority_denied:
|
||||
coreCoordinator.forged_body_authority_denied,
|
||||
wrong_session_denied: coreCoordinator.wrong_session_denied,
|
||||
self_hosted_cli: coreCoordinator.self_hosted_cli,
|
||||
task_placement: coreCoordinator.task_placement,
|
||||
task_events: coreCoordinator.task_events,
|
||||
release_binary_e2e: {
|
||||
client_authority: e2e.core_coordinator_client_authority,
|
||||
authenticated_session: e2e.core_coordinator_authenticated_session,
|
||||
forged_body_authority_denied:
|
||||
e2e.core_coordinator_forged_body_authority_denied,
|
||||
wrong_session_denied: e2e.core_coordinator_wrong_session_denied,
|
||||
launch_task: e2e.core_coordinator_launch_task_response,
|
||||
assignment_poll: e2e.core_coordinator_assignment_response,
|
||||
task_events: e2e.core_coordinator_task_events,
|
||||
},
|
||||
},
|
||||
},
|
||||
forgejo_host: forgejoHost,
|
||||
public_repository_url: resolvedPublicRepoRemote,
|
||||
forgejo_release: {
|
||||
owner: forgejoRelease.owner,
|
||||
repo: forgejoRelease.repo,
|
||||
release_id: forgejoRelease.release_id,
|
||||
asset_count: releaseAssets.size,
|
||||
},
|
||||
tool_versions: compactToolVersions(
|
||||
["manifest", manifest],
|
||||
["service_smoke", service],
|
||||
["public_repository_e2e", e2e]
|
||||
),
|
||||
acceptance_results: {
|
||||
public_release_preparation: {
|
||||
commands: manifest.commands,
|
||||
result: "passed",
|
||||
},
|
||||
deployment_bundle: {
|
||||
commands: deployment.commands,
|
||||
result: "passed",
|
||||
},
|
||||
live_service_smoke: {
|
||||
command: service.acceptance_command || null,
|
||||
result: service.acceptance_result || "passed",
|
||||
evidence: service.evidence,
|
||||
},
|
||||
core_coordinator_compatibility: {
|
||||
result: "passed",
|
||||
evidence: coreCoordinator,
|
||||
},
|
||||
public_repository_e2e: {
|
||||
commands: e2e.commands,
|
||||
result: "passed",
|
||||
evidence: e2e.evidence,
|
||||
},
|
||||
},
|
||||
evidence_files: inputs,
|
||||
};
|
||||
|
||||
const output = path.join(acceptanceRoot, "public-release-dryrun-final.json");
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
fs.writeFileSync(output, `${JSON.stringify(finalReport, null, 2)}\n`);
|
||||
console.log(`Public release dry-run final evidence passed: ${output}`);
|
||||
292
scripts/public-release-dryrun-preflight.js
Executable file
292
scripts/public-release-dryrun-preflight.js
Executable file
|
|
@ -0,0 +1,292 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const releaseRoot = path.resolve(
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/public-release-dryrun")
|
||||
);
|
||||
const acceptanceRoot = path.join(repo, "target/acceptance");
|
||||
const manifestPath =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
|
||||
path.join(releaseRoot, "public-release-manifest.json");
|
||||
const reportPath =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_PREFLIGHT_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-preflight.json");
|
||||
|
||||
function commandOutput(command, args, options = {}) {
|
||||
try {
|
||||
return cp
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
...options,
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function nonInteractiveEnv(extra = {}) {
|
||||
return {
|
||||
...process.env,
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false",
|
||||
SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false",
|
||||
GIT_SSH_COMMAND:
|
||||
process.env.GIT_SSH_COMMAND ||
|
||||
"ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0",
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function expectedSourceCommit() {
|
||||
return (
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function sha256File(file) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
}
|
||||
|
||||
function parseSha256Sums(file) {
|
||||
const sums = new Map();
|
||||
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line.trim());
|
||||
assert(match, `malformed SHA256SUMS line: ${line}`);
|
||||
sums.set(path.basename(match[2]), match[1]);
|
||||
}
|
||||
return sums;
|
||||
}
|
||||
|
||||
function remoteHead(remote) {
|
||||
const output = commandOutput("git", ["ls-remote", remote, "HEAD", "refs/heads/main"], {
|
||||
env: nonInteractiveEnv(),
|
||||
timeout: Number(process.env.DISASMER_PUBLIC_REPO_REMOTE_TIMEOUT_MS || 30000),
|
||||
});
|
||||
if (!output) return null;
|
||||
const lines = output.split(/\r?\n/).filter(Boolean);
|
||||
const head = lines.find((line) => line.endsWith("\tHEAD")) || lines[0];
|
||||
return head && head.split(/\s+/)[0];
|
||||
}
|
||||
|
||||
function publicTreeAlreadyPushed(manifest) {
|
||||
return (
|
||||
(manifest.public_tree_publish && manifest.public_tree_publish.pushed === true) ||
|
||||
process.env.DISASMER_PUBLIC_TREE_ALREADY_PUSHED === "1"
|
||||
);
|
||||
}
|
||||
|
||||
function publicTreePushSource(manifest) {
|
||||
return manifest.public_tree_publish && manifest.public_tree_publish.pushed === true
|
||||
? "manifest"
|
||||
: "external-env";
|
||||
}
|
||||
|
||||
function publicRepoRemoteForManifest(manifest) {
|
||||
return (
|
||||
manifest.public_repo_url ||
|
||||
manifest.public_repo_remote ||
|
||||
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
|
||||
process.env.DISASMER_PUBLIC_REPO_URL ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function publicTreeCommitForManifest(manifest) {
|
||||
return (
|
||||
(manifest.public_tree_publish && manifest.public_tree_publish.commit) ||
|
||||
process.env.DISASMER_PUBLIC_TREE_COMMIT ||
|
||||
process.env.DISASMER_PUBLIC_RELEASE_TARGET ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function envState(name) {
|
||||
return process.env[name] ? "set" : "unset";
|
||||
}
|
||||
|
||||
function staleEvidence(file, currentSourceCommit) {
|
||||
if (!fs.existsSync(file)) {
|
||||
return { file, status: "missing", source_commit: null, release_name: null };
|
||||
}
|
||||
const evidence = readJson(file);
|
||||
if (!evidence.source_commit) {
|
||||
return {
|
||||
file,
|
||||
status: "unversioned",
|
||||
source_commit: null,
|
||||
release_name: evidence.release_name || null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
file,
|
||||
status: evidence.source_commit === currentSourceCommit ? "current" : "stale",
|
||||
source_commit: evidence.source_commit,
|
||||
release_name: evidence.release_name || null,
|
||||
};
|
||||
}
|
||||
|
||||
assert(fs.existsSync(manifestPath), `missing public release manifest: ${manifestPath}`);
|
||||
const manifest = readJson(manifestPath);
|
||||
const currentSourceCommit = expectedSourceCommit();
|
||||
const currentTreeStatus = commandOutput("git", ["status", "--short"]) || "";
|
||||
assert.strictEqual(
|
||||
currentTreeStatus,
|
||||
"",
|
||||
"public release preflight requires a clean source tree"
|
||||
);
|
||||
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
|
||||
assert.strictEqual(
|
||||
manifest.source_commit,
|
||||
currentSourceCommit,
|
||||
"public release manifest must be regenerated for the current acceptance commit"
|
||||
);
|
||||
assert.strictEqual(manifest.source_tree_clean, true, "public release prep must start clean");
|
||||
assert.strictEqual(
|
||||
publicTreeAlreadyPushed(manifest),
|
||||
true,
|
||||
"filtered public tree must be pushed to Forgejo before release publication"
|
||||
);
|
||||
|
||||
const publicRepoRemote = publicRepoRemoteForManifest(manifest);
|
||||
assert(publicRepoRemote, "manifest must record public repository URL or remote");
|
||||
const publicTreeCommit = publicTreeCommitForManifest(manifest);
|
||||
const remoteMain = remoteHead(publicRepoRemote);
|
||||
assert(remoteMain, "Forgejo public repository main branch must be readable");
|
||||
if (publicTreeCommit) {
|
||||
assert.strictEqual(
|
||||
remoteMain,
|
||||
publicTreeCommit,
|
||||
"Forgejo public repository main branch must match the prepared public tree commit"
|
||||
);
|
||||
}
|
||||
|
||||
assert(Array.isArray(manifest.assets) && manifest.assets.length > 0, "manifest assets missing");
|
||||
const checksumAsset = manifest.assets.find((asset) => asset.name === "SHA256SUMS");
|
||||
assert(checksumAsset, "manifest must include SHA256SUMS");
|
||||
assert(fs.existsSync(checksumAsset.file), `missing checksum asset: ${checksumAsset.file}`);
|
||||
const checksums = parseSha256Sums(checksumAsset.file);
|
||||
const assets = manifest.assets.map((asset) => {
|
||||
assert(fs.existsSync(asset.file), `missing release asset: ${asset.file}`);
|
||||
const actual = sha256File(asset.file);
|
||||
const expected = checksums.get(asset.name);
|
||||
if (asset.name !== "SHA256SUMS") {
|
||||
assert.strictEqual(actual, expected, `checksum mismatch for ${asset.name}`);
|
||||
}
|
||||
return {
|
||||
name: asset.name,
|
||||
file: asset.file,
|
||||
bytes: fs.statSync(asset.file).size,
|
||||
sha256: actual,
|
||||
};
|
||||
});
|
||||
|
||||
const evidence = [
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "public-release-dryrun-service.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "hosted-client-compat.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "core-coordinator-compat.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "public-release-dryrun-e2e.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
staleEvidence(
|
||||
path.join(acceptanceRoot, "public-release-dryrun-final.json"),
|
||||
currentSourceCommit
|
||||
),
|
||||
];
|
||||
|
||||
const report = {
|
||||
kind: "disasmer-public-release-dryrun-preflight",
|
||||
source_commit: currentSourceCommit,
|
||||
release_name: manifest.release_name,
|
||||
public_tree_commit: publicTreeCommit || remoteMain,
|
||||
public_tree_push_source: publicTreePushSource(manifest),
|
||||
public_repo_url: publicRepoRemote,
|
||||
public_repo_remote_head: remoteMain,
|
||||
source_tree_clean: currentTreeStatus === "",
|
||||
local_assets_ready: true,
|
||||
assets,
|
||||
evidence,
|
||||
external_gates: {
|
||||
forgejo_release_publication: {
|
||||
status: envState("DISASMER_FORGEJO_TOKEN") === "set" ? "ready" : "pending",
|
||||
env: {
|
||||
DISASMER_FORGEJO_TOKEN: envState("DISASMER_FORGEJO_TOKEN"),
|
||||
},
|
||||
},
|
||||
live_service_smoke: {
|
||||
status:
|
||||
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR") === "set" &&
|
||||
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND") === "set"
|
||||
? "ready"
|
||||
: "pending",
|
||||
env: {
|
||||
DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR: envState(
|
||||
"DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR"
|
||||
),
|
||||
DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND: envState(
|
||||
"DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND"
|
||||
),
|
||||
},
|
||||
},
|
||||
public_release_e2e: {
|
||||
status:
|
||||
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E") === "set" &&
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E === "1" &&
|
||||
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND") === "set"
|
||||
? "ready"
|
||||
: "pending",
|
||||
env: {
|
||||
DISASMER_PUBLIC_RELEASE_DRYRUN_E2E:
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E || "unset",
|
||||
DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND: envState(
|
||||
"DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND"
|
||||
),
|
||||
},
|
||||
},
|
||||
final_evidence: {
|
||||
status:
|
||||
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL") === "set" &&
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL === "1"
|
||||
? "ready"
|
||||
: "pending",
|
||||
env: {
|
||||
DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL || "unset",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ report: reportPath, release_name: report.release_name }, null, 2));
|
||||
101
scripts/public-story-contract-smoke.js
Normal file
101
scripts/public-story-contract-smoke.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing public-story evidence: ${name}`);
|
||||
}
|
||||
|
||||
const readme = read("README.md");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const cliLocalRunSmoke = read("scripts/cli-local-run-smoke.js");
|
||||
const dapSmoke = read("scripts/dap-smoke.js");
|
||||
const wasmtimeAssignmentSmoke = read("scripts/wasmtime-assignment-smoke.js");
|
||||
const debuggerEvidence = `${dapSmoke}\n${wasmtimeAssignmentSmoke}`;
|
||||
const flagshipDemoSmoke = read("scripts/flagship-demo-smoke.js");
|
||||
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
||||
const artifactExportSmoke = read("scripts/artifact-export-smoke.js");
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
assert(
|
||||
script.includes("node scripts/public-story-contract-smoke.js"),
|
||||
`${scriptName} must run public-story-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
[
|
||||
"README states public story",
|
||||
/one virtual process, many virtual threads\/tasks, ordinary debugger controls, attached user nodes, and explicit artifact handling/,
|
||||
],
|
||||
[
|
||||
"README states local-first bytes policy",
|
||||
/local source checkouts and large outputs stay node-local unless user code or policy explicitly moves bytes/,
|
||||
],
|
||||
[
|
||||
"README states normal debugger controls",
|
||||
/ordinary debugger\s+controls for breakpoints, continue, pause, and restart/,
|
||||
],
|
||||
]) {
|
||||
expect(readme, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["CLI local run starts a node process", /cli_process_started_node_process/],
|
||||
["CLI local run checks node is separate from CLI", /assert\.notStrictEqual\(report\.boundary\.spawned_node_process_id, cliPid\)/],
|
||||
["CLI local run checks node is separate from coordinator", /assert\.notStrictEqual\(report\.boundary\.spawned_node_process_id, coordinator\.pid\)/],
|
||||
["CLI local run records the real virtual task tree", /events\.events\.length >= 4[\s\S]*prepare_source[\s\S]*compile_linux[\s\S]*package_release/],
|
||||
["CLI local run records real artifact metadata", /events\.events\.some\(\(event\) => event\.artifact_path\)/],
|
||||
["CLI local-only run starts coordinator", /cli_process_started_coordinator_process[\s\S]*true/],
|
||||
["CLI local-only run hides coordinator address requirement", /\["run"[\s\S]*"--local"[\s\S]*"--project"[\s\S]*project/],
|
||||
]) {
|
||||
expect(cliLocalRunSmoke, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["DAP smoke uses local-services runtime", /runtimeBackend: "local-services"/],
|
||||
["DAP smoke exposes dynamic entry and child virtual threads", /mainThread[\s\S]*build virtual process[\s\S]*childThread[\s\S]*notStrictEqual\(childThread\.id, failThread\.id\)/],
|
||||
["DAP smoke binds the real entrypoint breakpoint", /stack\[0\]\.line, buildMainLine[\s\S]*stack\[0\]\.name, \/build_main::wasm\//],
|
||||
["DAP smoke binds the real child task breakpoint", /childStack\[0\]\.line, taskTrapLine[\s\S]*childStack\[0\]\.name, \/task_trap::wasm\//],
|
||||
["DAP smoke all-stops on breakpoint", /assert\.strictEqual\(stopped\.body\.allThreadsStopped, true\)/],
|
||||
["runtime smoke proves acknowledged pause all-stop", /fully_frozen[\s\S]*resume_debug_epoch[\s\S]*fully_resumed/],
|
||||
["DAP smoke refuses active-task restart without a clean boundary", /restartFailure[\s\S]*checkpoint boundary\|still active/],
|
||||
["DAP smoke supports rebuilt terminal main restart", /restartFrame[\s\S]*Restarted main from the rebuilt bundle/],
|
||||
["DAP smoke avoids native child debugger claims", /doesNotMatch\(stack\[0\]\.name, \/podman\|cmd\\\.exe\|powershell\|pid\|native child\/i\)/],
|
||||
["DAP smoke crosses coordinator-node boundary without fabricating terminal state", /runtime_backend[\s\S]*LocalServices[\s\S]*coordinator_task_events[\s\S]*0/],
|
||||
]) {
|
||||
expect(debuggerEvidence, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["flagship source does not need coordinator checkout", /coordinator_requires_checkout_access[\s\S]*false/],
|
||||
["flagship source bytes remain node-local", /local_source_bytes_remain_node_local[\s\S]*true/],
|
||||
["flagship avoids default source upload", /coordinator_receives_source_bytes_by_default[\s\S]*false/],
|
||||
["flagship avoids default repo tarball", /default_full_repo_tarball[\s\S]*false/],
|
||||
]) {
|
||||
expect(flagshipDemoSmoke, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, source, pattern] of [
|
||||
["artifact download creates scoped link", artifactDownloadSmoke, /create_artifact_download_link/],
|
||||
["artifact download records retained-node source", artifactDownloadSmoke, /assert\.deepStrictEqual\(link\.link\.source, \{ RetainedNode: "node-download" \}\)/],
|
||||
["artifact download opens scoped stream", artifactDownloadSmoke, /open_artifact_download_stream/],
|
||||
["artifact export targets attached receiver node", artifactExportSmoke, /export_artifact_to_node[\s\S]*node-export-receiver/],
|
||||
["artifact export disables coordinator bulk relay", artifactExportSmoke, /coordinator_bulk_relay_allowed[\s\S]*false/],
|
||||
]) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
|
||||
console.log("Public story contract smoke passed");
|
||||
349
scripts/publish-public-release-dryrun.js
Executable file
349
scripts/publish-public-release-dryrun.js
Executable file
|
|
@ -0,0 +1,349 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const cp = require("child_process");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const releaseRoot = path.resolve(
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/public-release-dryrun")
|
||||
);
|
||||
const manifestPath = path.join(releaseRoot, "public-release-manifest.json");
|
||||
const reportPath = path.join(
|
||||
repo,
|
||||
"target/acceptance/public-release-dryrun-forgejo-release.json"
|
||||
);
|
||||
const forgejoUrl = (
|
||||
process.env.DISASMER_FORGEJO_URL || "https://git.michelpaulissen.com"
|
||||
).replace(/\/+$/, "");
|
||||
const token = process.env.DISASMER_FORGEJO_TOKEN;
|
||||
let owner = process.env.DISASMER_PUBLIC_REPO_OWNER;
|
||||
let repoName = process.env.DISASMER_PUBLIC_REPO_NAME;
|
||||
|
||||
function requireEnv(name, value) {
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
}
|
||||
|
||||
function commandOutput(command, args) {
|
||||
try {
|
||||
return cp
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedSourceCommit() {
|
||||
return (
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function parseForgejoRepoIdentity(remote) {
|
||||
if (!remote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pathname = remote;
|
||||
try {
|
||||
pathname = new URL(remote).pathname;
|
||||
} catch (_) {
|
||||
const scpLike = /^[^@/]+@[^:]+:(.+)$/.exec(remote);
|
||||
if (scpLike) {
|
||||
pathname = scpLike[1];
|
||||
}
|
||||
}
|
||||
|
||||
const parts = pathname
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\.git$/, "")
|
||||
.split("/")
|
||||
.filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
owner: parts[parts.length - 2],
|
||||
repoName: parts[parts.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRepoIdentity(manifest) {
|
||||
if (owner && repoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inferred = parseForgejoRepoIdentity(
|
||||
manifest.public_repo_url ||
|
||||
manifest.public_repo_remote ||
|
||||
process.env.DISASMER_PUBLIC_REPO_REMOTE
|
||||
);
|
||||
owner = owner || (inferred && inferred.owner);
|
||||
repoName = repoName || (inferred && inferred.repoName);
|
||||
if (!owner || !repoName) {
|
||||
throw new Error(
|
||||
"DISASMER_PUBLIC_REPO_OWNER and DISASMER_PUBLIC_REPO_NAME are required when the manifest does not contain a parseable Forgejo repository URL"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function publicTreeAlreadyPushed(manifest) {
|
||||
return (
|
||||
(manifest.public_tree_publish && manifest.public_tree_publish.pushed === true) ||
|
||||
process.env.DISASMER_PUBLIC_TREE_ALREADY_PUSHED === "1"
|
||||
);
|
||||
}
|
||||
|
||||
function publicTreeCommit(manifest) {
|
||||
return (
|
||||
(manifest.public_tree_publish && manifest.public_tree_publish.commit) ||
|
||||
process.env.DISASMER_PUBLIC_TREE_COMMIT ||
|
||||
process.env.DISASMER_PUBLIC_RELEASE_TARGET ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function apiPath(pathname) {
|
||||
return `/api/v1${pathname}`;
|
||||
}
|
||||
|
||||
function request(method, pathname, { body, headers = {} } = {}) {
|
||||
const url = new URL(apiPath(pathname), forgejoUrl);
|
||||
const payload =
|
||||
body === undefined
|
||||
? null
|
||||
: Buffer.isBuffer(body)
|
||||
? body
|
||||
: Buffer.from(JSON.stringify(body));
|
||||
const requestHeaders = {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${token}`,
|
||||
...headers,
|
||||
};
|
||||
if (payload) {
|
||||
requestHeaders["Content-Length"] = payload.length;
|
||||
if (!requestHeaders["Content-Type"]) {
|
||||
requestHeaders["Content-Type"] = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(
|
||||
url,
|
||||
{
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
},
|
||||
(res) => {
|
||||
const chunks = [];
|
||||
res.on("data", (chunk) => chunks.push(chunk));
|
||||
res.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
let parsed = null;
|
||||
if (text.trim()) {
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch (_) {
|
||||
parsed = text;
|
||||
}
|
||||
}
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(
|
||||
new Error(
|
||||
`${method} ${url.pathname} failed with ${res.statusCode}: ${text}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve({ status: res.statusCode, body: parsed });
|
||||
});
|
||||
}
|
||||
);
|
||||
req.on("error", reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function multipartFile(fieldName, file) {
|
||||
const boundary = `disasmer-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const name = path.basename(file);
|
||||
const header = Buffer.from(
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="${fieldName}"; filename="${name}"\r\n` +
|
||||
"Content-Type: application/octet-stream\r\n\r\n"
|
||||
);
|
||||
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
||||
return {
|
||||
body: Buffer.concat([header, fs.readFileSync(file), footer]),
|
||||
contentType: `multipart/form-data; boundary=${boundary}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function existingRelease(tagName) {
|
||||
const releases = await request(
|
||||
"GET",
|
||||
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases?limit=100`
|
||||
);
|
||||
return (releases.body || []).find((release) => release.tag_name === tagName) || null;
|
||||
}
|
||||
|
||||
async function createOrReuseRelease(manifest) {
|
||||
const tagName = manifest.release_name;
|
||||
const found = await existingRelease(tagName);
|
||||
if (found) {
|
||||
return { release: found, created: false };
|
||||
}
|
||||
const targetCommitish =
|
||||
publicTreeCommit(manifest) ||
|
||||
"main";
|
||||
const body = [
|
||||
"Disasmer public release dry run.",
|
||||
"",
|
||||
`Default hosted coordinator endpoint: ${manifest.default_hosted_coordinator_endpoint}`,
|
||||
`DNS publication state: ${manifest.dns_publication_state}`,
|
||||
`Resolver override: ${manifest.resolver_override}`,
|
||||
`Public tree identity: ${manifest.public_tree_identity}`,
|
||||
`Source commit: ${manifest.source_commit}`,
|
||||
].join("\n");
|
||||
const created = await request(
|
||||
"POST",
|
||||
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases`,
|
||||
{
|
||||
body: {
|
||||
tag_name: tagName,
|
||||
target_commitish: targetCommitish,
|
||||
name: tagName,
|
||||
body,
|
||||
draft: false,
|
||||
prerelease: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
return { release: created.body, created: true };
|
||||
}
|
||||
|
||||
async function loadRelease(releaseId) {
|
||||
const response = await request(
|
||||
"GET",
|
||||
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases/${releaseId}`
|
||||
);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async function uploadAsset(release, asset) {
|
||||
const { body, contentType } = multipartFile("attachment", asset.file);
|
||||
const response = await request(
|
||||
"POST",
|
||||
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases/${release.id}/assets?name=${encodeURIComponent(asset.name)}`,
|
||||
{
|
||||
body,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
function existingAssetByName(release, name) {
|
||||
return Array.isArray(release.assets)
|
||||
? release.assets.find((asset) => asset.name === name) || null
|
||||
: null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
requireEnv("DISASMER_FORGEJO_TOKEN", token);
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new Error(`missing public release manifest: ${manifestPath}`);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
resolveRepoIdentity(manifest);
|
||||
if (manifest.kind !== "disasmer-public-release-dryrun") {
|
||||
throw new Error(`unexpected public release manifest kind: ${manifest.kind}`);
|
||||
}
|
||||
if (manifest.source_commit !== expectedSourceCommit()) {
|
||||
throw new Error(
|
||||
"public release manifest is stale; regenerate it for the current acceptance commit before publishing the Forgejo Release"
|
||||
);
|
||||
}
|
||||
if (
|
||||
!publicTreeAlreadyPushed(manifest)
|
||||
) {
|
||||
throw new Error(
|
||||
"public tree must be pushed before publishing the Forgejo Release; run prepare-public-release-dryrun.js with DISASMER_PUBLISH_PUBLIC_TREE=1"
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
||||
throw new Error("public release manifest has no assets");
|
||||
}
|
||||
|
||||
const { release: releaseResult, created } = await createOrReuseRelease(manifest);
|
||||
const release = await loadRelease(releaseResult.id);
|
||||
const uploaded = [];
|
||||
const reused = [];
|
||||
for (const asset of manifest.assets) {
|
||||
if (!fs.existsSync(asset.file)) {
|
||||
throw new Error(`missing release asset: ${asset.file}`);
|
||||
}
|
||||
const existing = existingAssetByName(release, asset.name);
|
||||
if (existing) {
|
||||
reused.push(existing);
|
||||
continue;
|
||||
}
|
||||
uploaded.push(await uploadAsset(release, asset));
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
const report = {
|
||||
kind: "disasmer-public-release-dryrun-forgejo-release",
|
||||
forgejo_url: forgejoUrl,
|
||||
owner,
|
||||
repo: repoName,
|
||||
release_id: release.id,
|
||||
release_name: release.name || manifest.release_name,
|
||||
tag_name: release.tag_name || manifest.release_name,
|
||||
release_created: created,
|
||||
default_hosted_coordinator_endpoint: manifest.default_hosted_coordinator_endpoint,
|
||||
public_tree_identity: manifest.public_tree_identity,
|
||||
public_tree_commit: publicTreeCommit(manifest),
|
||||
source_commit: manifest.source_commit,
|
||||
uploaded_assets: uploaded.map((asset) => ({
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
browser_download_url: asset.browser_download_url,
|
||||
})),
|
||||
reused_assets: reused.map((asset) => ({
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
browser_download_url: asset.browser_download_url,
|
||||
})),
|
||||
};
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ report: reportPath, uploaded: uploaded.length, reused: reused.length },
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
154
scripts/quic-smoke.js
Executable file
154
scripts/quic-smoke.js
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function rendezvousRequest(overrides = {}) {
|
||||
return {
|
||||
type: "request_rendezvous",
|
||||
scope: {
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process: "vp-quic",
|
||||
object: { Artifact: "quic-artifact" },
|
||||
authorization_subject: "node-a-to-node-b",
|
||||
},
|
||||
source: {
|
||||
node: "node-a",
|
||||
advertised_addr: "node-a.mesh.invalid:4433",
|
||||
public_key_fingerprint: "sha256:node-a-public-key",
|
||||
},
|
||||
destination: {
|
||||
node: "node-b",
|
||||
advertised_addr: "node-b.mesh.invalid:4433",
|
||||
public_key_fingerprint: "sha256:node-b-public-key",
|
||||
},
|
||||
direct_connectivity: true,
|
||||
failure_reason: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const output = cp.execFileSync(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-node", "--bin", "disasmer-quic-smoke"],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
const report = JSON.parse(output.trim().split("\n").at(-1));
|
||||
|
||||
assert.strictEqual(report.kind, "disasmer_quic_smoke");
|
||||
assert.strictEqual(report.transport, "NativeQuic");
|
||||
assert.strictEqual(report.rust_native_quic, true);
|
||||
assert.strictEqual(report.authenticated_direct_connection, true);
|
||||
assert.strictEqual(report.coordinator_assisted_rendezvous, true);
|
||||
assert.strictEqual(report.coordinator_bulk_relay_allowed, false);
|
||||
assert.strictEqual(report.source_node, "node-a");
|
||||
assert.strictEqual(report.destination_node, "node-b");
|
||||
assert.strictEqual(report.scope.tenant, "tenant");
|
||||
assert.strictEqual(report.scope.project, "project");
|
||||
assert.strictEqual(report.scope.process, "vp-quic");
|
||||
assert.deepStrictEqual(report.scope.object, { Artifact: "quic-artifact" });
|
||||
assert.ok(report.authorization_digest.startsWith("sha256:"));
|
||||
assert.ok(report.request_bytes > 0);
|
||||
assert.strictEqual(report.server_received_request_bytes, report.request_bytes);
|
||||
assert.ok(report.payload_bytes > 0);
|
||||
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback",
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
|
||||
const plan = await send(addr, rendezvousRequest());
|
||||
assert.strictEqual(plan.type, "rendezvous_plan");
|
||||
assert.strictEqual(plan.charged_rendezvous_attempts, 1);
|
||||
assert.strictEqual(plan.plan.transport, "NativeQuic");
|
||||
assert.strictEqual(plan.plan.scope.tenant, "tenant");
|
||||
assert.strictEqual(plan.plan.scope.project, "project");
|
||||
assert.strictEqual(plan.plan.source.node, "node-a");
|
||||
assert.strictEqual(plan.plan.destination.node, "node-b");
|
||||
assert.strictEqual(plan.plan.coordinator_assisted_rendezvous, true);
|
||||
assert.strictEqual(plan.plan.coordinator_bulk_relay_allowed, false);
|
||||
assert.ok(plan.plan.authorization_digest.startsWith("sha256:"));
|
||||
|
||||
const failed = await send(
|
||||
addr,
|
||||
rendezvousRequest({
|
||||
direct_connectivity: false,
|
||||
failure_reason: "nat traversal failed",
|
||||
})
|
||||
);
|
||||
assert.strictEqual(failed.type, "error");
|
||||
assert.match(failed.message, /nat traversal failed/);
|
||||
assert.match(failed.message, /coordinator bulk relay is disabled/);
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("QUIC smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
230
scripts/real-flagship-harness.js
Normal file
230
scripts/real-flagship-harness.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { configurePodmanTestEnvironment } = require("./podman-test-env");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
if (stderr.length > 4096) stderr = stderr.slice(-4096);
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
const detail = stderr.trim();
|
||||
reject(new Error(
|
||||
`process exited before JSON line with code ${code}${detail ? `: ${detail}` : ""}`
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function configureContainerPolicyHome() {
|
||||
configurePodmanTestEnvironment(repo);
|
||||
}
|
||||
|
||||
function commandWithPodman(program, args) {
|
||||
if (cp.spawnSync("podman", ["--version"], { stdio: "ignore" }).status === 0) {
|
||||
return { program, args };
|
||||
}
|
||||
if (cp.spawnSync("nix", ["--version"], { stdio: "ignore" }).status === 0) {
|
||||
return {
|
||||
program: "nix",
|
||||
args: ["shell", "nixpkgs#podman", "--command", program, ...args],
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
"real flagship smoke requires rootless Podman (or Nix to provide it)"
|
||||
);
|
||||
}
|
||||
|
||||
function ensureRootlessPodman() {
|
||||
configureContainerPolicyHome();
|
||||
const invocation = commandWithPodman("podman", [
|
||||
"info",
|
||||
"--format",
|
||||
"{{.Host.Security.Rootless}}",
|
||||
]);
|
||||
const rootless = cp.execFileSync(invocation.program, invocation.args, {
|
||||
cwd: repo,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
assert.strictEqual(rootless, "true", "flagship worker must use rootless Podman");
|
||||
}
|
||||
|
||||
async function runFlagshipWorker(addr, node, identity) {
|
||||
const enrollment = await send(addr, {
|
||||
type: "create_node_enrollment_grant",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
ttl_seconds: 60,
|
||||
});
|
||||
assert.strictEqual(enrollment.type, "node_enrollment_grant_created");
|
||||
const cargoArgs = [
|
||||
"run", "-q", "-p", "disasmer-node", "--bin", "disasmer-node", "--",
|
||||
"--coordinator", `${addr.host}:${addr.port}`,
|
||||
"--tenant", "tenant",
|
||||
"--project-id", "project",
|
||||
"--node", node,
|
||||
"--enrollment-grant", enrollment.grant,
|
||||
"--worker",
|
||||
"--emit-ready",
|
||||
"--project-root", project,
|
||||
"--assignment-poll-ms", "25",
|
||||
];
|
||||
const invocation = commandWithPodman("cargo", cargoArgs);
|
||||
const child = cp.spawn(invocation.program, invocation.args, {
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_NODE_PRIVATE_KEY: identity.privateKey,
|
||||
},
|
||||
});
|
||||
return { child, ready: waitForJsonLine(child) };
|
||||
}
|
||||
|
||||
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
|
||||
async function waitForTaskEvent(addr, process, predicate, description) {
|
||||
for (let attempt = 0; attempt < 2400; attempt += 1) {
|
||||
const response = await send(addr, {
|
||||
type: "list_task_events",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process,
|
||||
});
|
||||
assert.strictEqual(response.type, "task_events", JSON.stringify(response));
|
||||
const event = response.events.find(predicate);
|
||||
if (event) return event;
|
||||
await delay(25);
|
||||
}
|
||||
throw new Error(`timed out waiting for real Wasm task ${description}`);
|
||||
}
|
||||
|
||||
function startFlagship(addr) {
|
||||
const report = JSON.parse(
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--",
|
||||
"run", "build",
|
||||
"--coordinator", `disasmer+tcp://${addr.host}:${addr.port}`,
|
||||
"--project", project,
|
||||
"--json",
|
||||
],
|
||||
{ cwd: repo, env: process.env, encoding: "utf8" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(report.command, "run");
|
||||
assert.strictEqual(report.status, "main_launched", JSON.stringify(report));
|
||||
assert.strictEqual(report.entry, "build");
|
||||
assert.strictEqual(report.task_launch.type, "main_launched");
|
||||
assert.strictEqual(report.task_launch.task_instance, report.task_instance);
|
||||
assert.strictEqual(report.task_launch.task_definition, report.task_definition);
|
||||
assert.strictEqual(report.worker_placement_requested, true);
|
||||
assert.match(report.bundle_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.match(report.entry_export, /^disasmer_entry_v1_/);
|
||||
const virtualProcess = report.process;
|
||||
return { report, process: virtualProcess };
|
||||
}
|
||||
|
||||
async function launchFlagship(addr) {
|
||||
const { report, process: virtualProcess } = startFlagship(addr);
|
||||
const compileEvent = await waitForTaskEvent(
|
||||
addr,
|
||||
virtualProcess,
|
||||
(event) => event.task_definition === "compile_linux",
|
||||
"compile_linux"
|
||||
);
|
||||
const packageEvent = await waitForTaskEvent(
|
||||
addr,
|
||||
virtualProcess,
|
||||
(event) => event.task_definition === "package_release",
|
||||
"package_release"
|
||||
);
|
||||
const buildEvent = await waitForTaskEvent(
|
||||
addr,
|
||||
virtualProcess,
|
||||
(event) => event.task === report.task_instance && event.executor === "coordinator_main",
|
||||
"coordinator build main"
|
||||
);
|
||||
for (const event of [compileEvent, packageEvent, buildEvent]) {
|
||||
assert.strictEqual(event.terminal_state, "completed", JSON.stringify(event));
|
||||
}
|
||||
return {
|
||||
report,
|
||||
process: virtualProcess,
|
||||
compileEvent,
|
||||
packageEvent,
|
||||
buildEvent,
|
||||
};
|
||||
}
|
||||
|
||||
function flagshipNodeCapabilities() {
|
||||
return {
|
||||
os: "Linux",
|
||||
arch: process.arch,
|
||||
capabilities: [
|
||||
"Command",
|
||||
"Containers",
|
||||
"RootlessPodman",
|
||||
"SourceFilesystem",
|
||||
"VfsArtifacts",
|
||||
],
|
||||
environment_backends: ["Container"],
|
||||
source_providers: ["filesystem"],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureRootlessPodman,
|
||||
flagshipNodeCapabilities,
|
||||
launchFlagship,
|
||||
project,
|
||||
repo,
|
||||
runFlagshipWorker,
|
||||
send,
|
||||
startFlagship,
|
||||
waitForTaskEvent,
|
||||
waitForJsonLine,
|
||||
};
|
||||
189
scripts/release-blocker-smoke.js
Executable file
189
scripts/release-blocker-smoke.js
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(relativePath) {
|
||||
const fullPath = path.join(repo, relativePath);
|
||||
if (!fs.existsSync(fullPath)) return null;
|
||||
return fs.readFileSync(fullPath, "utf8");
|
||||
}
|
||||
|
||||
function section(source, heading) {
|
||||
const marker = `## ${heading}`;
|
||||
const start = source.indexOf(marker);
|
||||
assert(start >= 0, `missing section ${marker}`);
|
||||
const next = source.indexOf("\n## ", start + marker.length);
|
||||
return source.slice(start, next >= 0 ? next : source.length);
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing release-blocker evidence: ${name}`);
|
||||
}
|
||||
|
||||
const hiddenDemoBlockerPattern = new RegExp(
|
||||
[
|
||||
"flagship demo requires",
|
||||
["undocumented", "manual state"].join(" "),
|
||||
["hard-coded", "local paths"].join(" "),
|
||||
["demo-only", "credentials"].join(" "),
|
||||
["hidden", "setup"].join(" "),
|
||||
].join("[\\s\\S]*")
|
||||
);
|
||||
const hiddenDemoScanPattern = new RegExp(
|
||||
`demo_setup_pattern='${[
|
||||
["undocumented", "manual state"].join(" "),
|
||||
["hidden", "setup"].join(" "),
|
||||
["demo-only", "credentials?"].join(" "),
|
||||
["hard-coded", "local paths?"].join(" "),
|
||||
]
|
||||
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\\\|")}'`
|
||||
);
|
||||
|
||||
const phase2 = read("acceptance_criteria_phase2.md");
|
||||
const base = read("acceptance_criteria.md");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
||||
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
|
||||
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
|
||||
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
|
||||
const hostedClientCompatSmoke = maybeRead("private/hosted-policy/scripts/hosted-client-compat-smoke.js");
|
||||
const releaseSourceScan = read("scripts/release-source-scan.sh");
|
||||
const flagshipDemoSmoke = read("scripts/flagship-demo-smoke.js");
|
||||
|
||||
const releaseBlockers = section(phase2, "20. Release blockers");
|
||||
expect(
|
||||
releaseBlockers,
|
||||
"cross-tenant access is listed as a release blocker",
|
||||
/Cross-tenant access succeeds for projects, nodes, processes, logs, artifacts, downloads, debug state, panels, capabilities, source manifests, credentials, or metadata/
|
||||
);
|
||||
expect(
|
||||
releaseBlockers,
|
||||
"manual-state flagship blocker is listed",
|
||||
hiddenDemoBlockerPattern
|
||||
);
|
||||
|
||||
expect(
|
||||
section(base, "21. Authorization and tenant isolation"),
|
||||
"base criteria require tenant isolation failures to block release",
|
||||
/Tenant isolation failures are treated as release blockers/
|
||||
);
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
for (const smoke of [
|
||||
"scripts/artifact-download-smoke.js",
|
||||
"scripts/operator-panel-smoke.js",
|
||||
"scripts/source-preparation-smoke.js",
|
||||
"scripts/scheduler-placement-smoke.js",
|
||||
"scripts/flagship-demo-smoke.js",
|
||||
]) {
|
||||
assert(
|
||||
script.includes(`node ${smoke}`),
|
||||
`${scriptName} must run ${smoke} as part of tenant-isolation release blocking`
|
||||
);
|
||||
}
|
||||
assert(
|
||||
script.includes("scripts/release-source-scan.sh"),
|
||||
`${scriptName} must run release-source-scan.sh as part of release blocking`
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-client-compat-smoke.js"),
|
||||
"private acceptance must run hosted Client cross-tenant checks"
|
||||
);
|
||||
assert(
|
||||
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-deployment-smoke.js"),
|
||||
"private acceptance must run hosted deployment checks"
|
||||
);
|
||||
|
||||
const boundaryEvidence = [
|
||||
[
|
||||
"artifact download",
|
||||
artifactDownloadSmoke,
|
||||
[/const crossTenant = await send/, /const crossTenantOpen = await send/, /tenant mismatch/],
|
||||
],
|
||||
[
|
||||
"operator panel",
|
||||
operatorPanelSmoke,
|
||||
[/const crossTenant = await send/, /render_operator_panel/, /scope\|tenant\|project/],
|
||||
],
|
||||
[
|
||||
"source preparation",
|
||||
sourcePreparationSmoke,
|
||||
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
|
||||
],
|
||||
[
|
||||
"scheduler/node capability",
|
||||
schedulerSmoke,
|
||||
[/const crossTenantReport = await send/, /report_node_capabilities/, /tenant\\\/project scope/],
|
||||
],
|
||||
];
|
||||
|
||||
if (hostedClientCompatSmoke) {
|
||||
boundaryEvidence.push([
|
||||
"hosted Client compatibility",
|
||||
hostedClientCompatSmoke,
|
||||
[
|
||||
/const forged = await sendHostedControl/,
|
||||
/const crossTenantTaskEventsDenied = await sendHostedControl/,
|
||||
/scope\|denied\|unauthorized/,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
for (const [name, source, patterns] of boundaryEvidence) {
|
||||
for (const pattern of patterns) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["release source scan rejects manual demo state", hiddenDemoScanPattern],
|
||||
["release source scan rejects hidden local paths", /hidden_local_pattern='file:\/\/\|\/home\/\[.*\]_.-\]\+\/\|\/Users\/\[.*\]_.-\]\+\/\|C:\\\\Users\\\\\|https\?:\/\/\(localhost\|127\\\.0\\\.0\\\.1\)/],
|
||||
]) {
|
||||
expect(releaseSourceScan, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["public split excludes private modules", /--exclude='\.\/private'/],
|
||||
["public split excludes experiments", /--exclude='\.\/experiments'/],
|
||||
["public split tests copied workspace", /cargo test --workspace --manifest-path "\$tmp_dir\/Cargo\.toml"/],
|
||||
["public split builds copied workspace bins", /cargo build --workspace --bins --manifest-path "\$tmp_dir\/Cargo\.toml"/],
|
||||
["public split installs CLI from copied tree", /\(cd "\$tmp_dir" && node scripts\/cli-install-smoke\.js\)/],
|
||||
["public split installs VS Code extension from copied tree", /\(cd "\$tmp_dir" && node scripts\/vscode-extension-smoke\.js\)/],
|
||||
["public split attaches node from copied tree", /\(cd "\$tmp_dir" && node scripts\/node-attach-smoke\.js\)/],
|
||||
["public split runs real Wasm assignments from copied tree", /\(cd "\$tmp_dir" && node scripts\/wasmtime-assignment-smoke\.js\)/],
|
||||
["public split runs CLI local workflow from copied tree", /\(cd "\$tmp_dir" && node scripts\/cli-local-run-smoke\.js\)/],
|
||||
["public split runs artifact download from copied tree", /\(cd "\$tmp_dir" && node scripts\/artifact-download-smoke\.js\)/],
|
||||
["public split runs artifact export from copied tree", /\(cd "\$tmp_dir" && node scripts\/artifact-export-smoke\.js\)/],
|
||||
["public split runs DAP smoke from copied tree", /\(cd "\$tmp_dir" && node scripts\/dap-smoke\.js\)/],
|
||||
["public split runs flagship demo smoke from copied tree", /\(cd "\$tmp_dir" && node scripts\/flagship-demo-smoke\.js\)/],
|
||||
]) {
|
||||
expect(publicSplit, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["flagship demo rejects local machine assumptions", /forbiddenSourceAssumptions/],
|
||||
["flagship demo rejects coordinator checkout access", /coordinator_requires_checkout_access[\s\S]*false/],
|
||||
["flagship demo asserts local source bytes stay node-local", /local_source_bytes_remain_node_local[\s\S]*true/],
|
||||
["flagship demo asserts coordinator receives no source bytes by default", /coordinator_receives_source_bytes_by_default[\s\S]*false/],
|
||||
["flagship demo asserts no default full repo tarball", /default_full_repo_tarball[\s\S]*false/],
|
||||
]) {
|
||||
expect(flagshipDemoSmoke, name, pattern);
|
||||
}
|
||||
|
||||
console.log("Release blocker smoke passed");
|
||||
71
scripts/release-source-scan.sh
Executable file
71
scripts/release-source-scan.sh
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo"
|
||||
|
||||
release_paths=(
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
README.md
|
||||
crates
|
||||
examples
|
||||
private
|
||||
scripts
|
||||
vscode-extension
|
||||
)
|
||||
|
||||
existing_release_paths=()
|
||||
for path in "${release_paths[@]}"; do
|
||||
if [[ -e "$path" ]]; then
|
||||
existing_release_paths+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
prose_scan_paths=(
|
||||
README.md
|
||||
crates
|
||||
examples
|
||||
private
|
||||
scripts
|
||||
vscode-extension
|
||||
)
|
||||
|
||||
existing_prose_scan_paths=()
|
||||
for path in "${prose_scan_paths[@]}"; do
|
||||
if [[ -e "$path" ]]; then
|
||||
existing_prose_scan_paths+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
scan_globs=(
|
||||
--glob '!**/target/**'
|
||||
--glob '!**/node_modules/**'
|
||||
--glob '!scripts/release-source-scan.sh'
|
||||
)
|
||||
|
||||
placeholder_pattern='debugger-gate|experiments/debugger-gate|DISASMER-DEMO|device-code-placeholder|artifact://demo|vp-local-demo'
|
||||
if rg -n "${scan_globs[@]}" "$placeholder_pattern" "${existing_release_paths[@]}"; then
|
||||
echo "release source scan failed: stale experiment/demo placeholder reference found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
demo_setup_pattern='undocumented manual state|hidden setup|demo-only credentials?|hard-coded local paths?'
|
||||
if rg -n "${scan_globs[@]}" "$demo_setup_pattern" "${existing_prose_scan_paths[@]}"; then
|
||||
echo "release source scan failed: demo requires hidden setup or demo-only state" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
hidden_local_pattern='file://|/home/[[:alnum:]_.-]+/|/Users/[[:alnum:]_.-]+/|C:\\Users\\|https?://(localhost|127\.0\.0\.1)[^[:space:]]*/artifacts/'
|
||||
if rg -n "${scan_globs[@]}" "$hidden_local_pattern" "${existing_release_paths[@]}"; then
|
||||
echo "release source scan failed: hidden local path or local artifact URL found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
public_wording_pattern='reddit|hacker news|lobsters|launch forum|traffic source|free tier'
|
||||
if rg -n "${scan_globs[@]}" "$public_wording_pattern" "${existing_prose_scan_paths[@]}"; then
|
||||
echo "release source scan failed: public-facing launch-forum/free-tier wording found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release source scan passed"
|
||||
214
scripts/resource-metering-contract-smoke.js
Normal file
214
scripts/resource-metering-contract-smoke.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(segments) {
|
||||
const fullPath = path.join(repo, ...segments);
|
||||
if (!fs.existsSync(fullPath)) return null;
|
||||
return fs.readFileSync(fullPath, "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing resource metering evidence: ${name}`);
|
||||
}
|
||||
|
||||
function expectGate(script, gateName) {
|
||||
assert(
|
||||
script.includes("node scripts/resource-metering-contract-smoke.js"),
|
||||
`${gateName} must run resource-metering-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
const coreLimits = read("crates/disasmer-core/src/limits.rs");
|
||||
// Phase 3 keeps the protocol dispatch in service.rs and the metered operation
|
||||
// implementations in focused service modules. Read the complete relevant
|
||||
// boundary so this contract follows the refactor instead of one mega-file.
|
||||
const coordinatorService = [
|
||||
read("crates/disasmer-coordinator/src/service.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/routing.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/processes.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/process_launch.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/artifacts.rs"),
|
||||
].join("\n");
|
||||
const coordinatorQuota = read("crates/disasmer-coordinator/src/service/quota.rs");
|
||||
const coordinatorLogs = read("crates/disasmer-coordinator/src/service/logs.rs");
|
||||
const coordinatorDebug = read("crates/disasmer-coordinator/src/service/debug.rs");
|
||||
const coordinatorTests = read("crates/disasmer-coordinator/src/service/tests.rs");
|
||||
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
||||
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
|
||||
const quicSmoke = read("scripts/quic-smoke.js");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["API call limit kind", /\bApiCall,/],
|
||||
["spawn limit kind", /\bSpawn,/],
|
||||
["log bytes limit kind", /\bLogBytes,/],
|
||||
["metadata bytes limit kind", /\bMetadataBytes,/],
|
||||
["debug read bytes limit kind", /\bDebugReadBytes,/],
|
||||
["UI event limit kind", /\bUiEvent,/],
|
||||
["rendezvous attempt limit kind", /\bRendezvousAttempt,/],
|
||||
["artifact download bytes limit kind", /\bArtifactDownloadBytes,/],
|
||||
["hosted fuel limit kind", /\bHostedFuel,/],
|
||||
[
|
||||
"preflight can check without consuming",
|
||||
/pub fn can_charge\([\s\S]*used\.saturating_add\(amount\) > limit/,
|
||||
],
|
||||
[
|
||||
"charge goes through preflight",
|
||||
/pub fn charge\([\s\S]*self\.can_charge\(limits, kind, amount\)\?/,
|
||||
],
|
||||
]) {
|
||||
expect(coreLimits, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
[
|
||||
"rendezvous charges before transport planning",
|
||||
/handle_request_rendezvous[\s\S]*charge_rendezvous_attempt\([\s\S]*&scope\.tenant[\s\S]*&scope\.project[\s\S]*now_epoch_seconds[\s\S]*plan_authenticated_direct_bulk_transfer/,
|
||||
],
|
||||
[
|
||||
"artifact link creation preflights downloadable bytes before link creation",
|
||||
/handle_create_artifact_download_link[\s\S]*downloadable_size[\s\S]*can_charge_download\([\s\S]*&context\.tenant[\s\S]*&context\.project[\s\S]*downloadable_size[\s\S]*create_download_link/,
|
||||
],
|
||||
[
|
||||
"artifact delivery charges scoped bytes before advancing its offset",
|
||||
/handle_open_artifact_download_stream[\s\S]*stream_download_chunk\([\s\S]*charge_download\([\s\S]*&context\.tenant[\s\S]*&context\.project[\s\S]*streamed_bytes[\s\S]*delivered_offset = end/,
|
||||
],
|
||||
]) {
|
||||
expect(coordinatorService, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
[
|
||||
"quota keys include tenant/project resource kind and window",
|
||||
/struct ProjectQuotaScope[\s\S]*tenant: TenantId[\s\S]*project: ProjectId[\s\S]*struct MeterKey[\s\S]*kind: LimitKind[\s\S]*window: u64/,
|
||||
],
|
||||
[
|
||||
"quota module discards expired windows for an accessed scope and kind",
|
||||
/fn meter_mut[\s\S]*self\.meters\.retain[\s\S]*existing\.scope != key\.scope[\s\S]*existing\.kind != kind[\s\S]*existing\.window == key\.window/,
|
||||
],
|
||||
[
|
||||
"quota module charges rendezvous attempts through the scoped window meter",
|
||||
/fn charge_rendezvous_attempt[\s\S]*self\.charge\([\s\S]*tenant[\s\S]*project[\s\S]*LimitKind::RendezvousAttempt/,
|
||||
],
|
||||
[
|
||||
"quota module charges authenticated API calls through the scoped window meter",
|
||||
/fn charge_api_call[\s\S]*self\.charge\(tenant, project, LimitKind::ApiCall, 1, now_epoch_seconds\)/,
|
||||
],
|
||||
[
|
||||
"quota module preflights and charges log bytes through the scoped window meter",
|
||||
/fn can_charge_log_bytes[\s\S]*LimitKind::LogBytes[\s\S]*fn charge_log_bytes[\s\S]*LimitKind::LogBytes/,
|
||||
],
|
||||
[
|
||||
"quota module preflights artifact download bytes through the scoped meter",
|
||||
/fn can_charge_download[\s\S]*self\.can_charge\([\s\S]*tenant[\s\S]*project[\s\S]*LimitKind::ArtifactDownloadBytes/,
|
||||
],
|
||||
[
|
||||
"quota status reports current scoped window usage",
|
||||
/fn project_status[\s\S]*for kind in LimitKind::ALL[\s\S]*self\.used\(tenant, project, kind, now_epoch_seconds\)/,
|
||||
],
|
||||
]) {
|
||||
expect(coordinatorQuota, name, pattern);
|
||||
}
|
||||
|
||||
for (const [source, name, pattern] of [
|
||||
[
|
||||
coordinatorService,
|
||||
"authenticated API calls are charged after session authorization and before dispatch",
|
||||
/authenticate_cli_session[\s\S]*authorize_authenticated_user_operation[\s\S]*charge_api_call[\s\S]*match request/,
|
||||
],
|
||||
[
|
||||
coordinatorLogs,
|
||||
"signed node log ingestion preflights and charges bytes before accepting the report",
|
||||
/handle_report_task_log[\s\S]*authorize_node_for_process_or_termination[\s\S]*can_charge_log_bytes[\s\S]*charge_log_bytes[\s\S]*TaskLogRecorded/,
|
||||
],
|
||||
[
|
||||
coordinatorDebug,
|
||||
"debug reads charge the scoped debug-read budget before audit state is recorded",
|
||||
/record_debug_audit_event[\s\S]*charge_debug_read[\s\S]*DebugAuditEvent/,
|
||||
],
|
||||
[
|
||||
coordinatorTests,
|
||||
"tests prove API-call and log-byte quota enforcement and project isolation",
|
||||
/authenticated_api_calls_are_metered_per_tenant_and_project_before_dispatch[\s\S]*project-a[\s\S]*project-b[\s\S]*signed_node_log_ingestion_checks_scoped_quota_before_accepting_bytes[\s\S]*LogBytes/,
|
||||
],
|
||||
]) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, source, patterns] of [
|
||||
[
|
||||
"rendezvous smoke",
|
||||
quicSmoke,
|
||||
[/charged_rendezvous_attempts, 1/, /coordinator bulk relay is disabled/],
|
||||
],
|
||||
[
|
||||
"artifact download smoke",
|
||||
artifactDownloadSmoke,
|
||||
[
|
||||
/downloaded\.response\.charged_download_bytes,[\s\S]*packageEvent\.artifact_size_bytes/,
|
||||
/retaining_node_reverse_stream/,
|
||||
/revoked/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"operator panel smoke",
|
||||
operatorPanelSmoke,
|
||||
[
|
||||
/type: "submit_panel_event"/,
|
||||
/max_events: 1/,
|
||||
/used_events, 1/,
|
||||
/rate limit/i,
|
||||
/max_download_bytes: 1/,
|
||||
/exceeds download limit/,
|
||||
],
|
||||
],
|
||||
]) {
|
||||
for (const pattern of patterns) {
|
||||
expect(source, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
expectGate(publicAcceptance, "public acceptance");
|
||||
expectGate(publicSplit, "public split acceptance");
|
||||
expectGate(privateAcceptance, "private acceptance");
|
||||
|
||||
const privateHostedLibSource = maybeRead(["private", "hosted-policy", "src", "lib.rs"]);
|
||||
const privateHostedTests = maybeRead(["private", "hosted-policy", "src", "tests.rs"]);
|
||||
const privateHostedLib = privateHostedLibSource
|
||||
? [privateHostedLibSource, privateHostedTests].filter(Boolean).join("\n")
|
||||
: null;
|
||||
if (privateHostedLib) {
|
||||
for (const [name, pattern] of [
|
||||
[
|
||||
"private hosted configuration owns exact limits and quota windows",
|
||||
/community_tier_resource_limits[\s\S]*LimitKind::ApiCall[\s\S]*LimitKind::HostedFuel[\s\S]*community_tier_quota_configuration[\s\S]*CoordinatorQuotaConfiguration::new/,
|
||||
],
|
||||
[
|
||||
"hosted zero-capability Wasm budget preflight is atomic across every resource kind",
|
||||
/preflight_zero_capability_hosted_wasm\([\s\S]*let mut trial_meter = meter\.clone\(\)[\s\S]*LimitKind::HostedFuel[\s\S]*LimitKind::HostedMemoryBytes[\s\S]*LimitKind::HostedWallClockMs[\s\S]*LimitKind::HostedStateBytes[\s\S]*LimitKind::LogBytes[\s\S]*LimitKind::MetadataBytes[\s\S]*LimitKind::UiEvent[\s\S]*LimitKind::ApiCall[\s\S]*\*meter = trial_meter/,
|
||||
],
|
||||
[
|
||||
"hosted tests cover each zero-capability Wasm budget",
|
||||
/hosted_zero_capability_wasm_preflight_rejects_each_budget_over_limit\([\s\S]*LimitKind::HostedFuel[\s\S]*LimitKind::LogBytes[\s\S]*LimitKind::MetadataBytes[\s\S]*LimitKind::UiEvent[\s\S]*LimitKind::ApiCall/,
|
||||
],
|
||||
[
|
||||
"hosted tests prove tenant/project quota isolation",
|
||||
/hosted_resource_usage_is_isolated_by_tenant_and_project\([\s\S]*project-a[\s\S]*project-b[\s\S]*HostedFuel/,
|
||||
],
|
||||
]) {
|
||||
expect(privateHostedLib, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Resource metering contract smoke passed");
|
||||
407
scripts/scheduler-placement-smoke.js
Executable file
407
scripts/scheduler-placement-smoke.js
Executable file
|
|
@ -0,0 +1,407 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const digest = (value) =>
|
||||
`sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
||||
const environmentDigest = digest("scheduler-linux-container");
|
||||
const dependencyDigest = digest("scheduler-toolchain-dependencies");
|
||||
const sourceDigest = digest("scheduler-source-tree");
|
||||
|
||||
function buildFlagshipBundle() {
|
||||
const output = cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--",
|
||||
"build", "--project", "examples/launch-build-demo", "--json",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
const report = JSON.parse(output);
|
||||
const directory = path.resolve(repo, report.bundle_artifact.directory);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(directory, "manifest.json"), "utf8"));
|
||||
const entrypoints = JSON.parse(
|
||||
fs.readFileSync(path.join(directory, manifest.entrypoints), "utf8")
|
||||
);
|
||||
const entrypoint = entrypoints.find((candidate) => candidate.name === "build");
|
||||
assert(entrypoint, "flagship bundle omitted build entrypoint");
|
||||
const taskDescriptors = JSON.parse(
|
||||
fs.readFileSync(path.join(directory, manifest.task_descriptors), "utf8")
|
||||
);
|
||||
const prepareSource = taskDescriptors.find(
|
||||
(candidate) => candidate.name === "prepare_source"
|
||||
);
|
||||
assert(prepareSource, "flagship bundle omitted prepare_source task");
|
||||
return {
|
||||
digest: manifest.bundle_digest,
|
||||
taskExport: prepareSource.export,
|
||||
wasmModuleBase64: fs.readFileSync(path.join(directory, "module.wasm")).toString("base64"),
|
||||
};
|
||||
}
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function linuxCapabilities() {
|
||||
return {
|
||||
os: "Linux",
|
||||
arch: "x86_64",
|
||||
capabilities: [
|
||||
"Command",
|
||||
"Containers",
|
||||
"RootlessPodman",
|
||||
"SourceFilesystem",
|
||||
"VfsArtifacts"
|
||||
],
|
||||
environment_backends: ["Container"],
|
||||
source_providers: ["filesystem"]
|
||||
};
|
||||
}
|
||||
|
||||
function gitCapabilities() {
|
||||
const capabilities = linuxCapabilities();
|
||||
capabilities.capabilities = [...capabilities.capabilities, "SourceGit"].sort();
|
||||
capabilities.source_providers = [...capabilities.source_providers, "git"].sort();
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
async function attachNode(addr, node) {
|
||||
const identity = nodeIdentity("scheduler-placement-smoke", node);
|
||||
const attached = await send(addr, {
|
||||
type: "attach_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node,
|
||||
public_key: identity.publicKey
|
||||
});
|
||||
assert.strictEqual(attached.type, "node_attached");
|
||||
assert.strictEqual(attached.node, node);
|
||||
return identity;
|
||||
}
|
||||
|
||||
async function reportNode(addr, node, identity, locality) {
|
||||
const recorded = await send(addr, signedNodeRequest(node, identity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node,
|
||||
capabilities: locality.capabilities || linuxCapabilities(),
|
||||
cached_environment_digests: locality.cached_environment_digests,
|
||||
dependency_cache_digests: locality.dependency_cache_digests,
|
||||
source_snapshots: locality.source_snapshots,
|
||||
artifact_locations: locality.artifact_locations,
|
||||
direct_connectivity: locality.direct_connectivity !== false,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(
|
||||
recorded.type,
|
||||
"node_capabilities_recorded",
|
||||
JSON.stringify(recorded)
|
||||
);
|
||||
assert.strictEqual(recorded.node, node);
|
||||
return recorded;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const bundle = buildFlagshipBundle();
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
let coordinatorStderr = "";
|
||||
coordinator.stderr.on("data", (chunk) => {
|
||||
coordinatorStderr += chunk.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const coldNode = await attachNode(addr, "cold-node");
|
||||
const warmNode = await attachNode(addr, "warm-node");
|
||||
|
||||
const cold = await reportNode(addr, "cold-node", coldNode, {
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: false
|
||||
});
|
||||
assert.strictEqual(cold.node_descriptors, 1);
|
||||
|
||||
const warm = await reportNode(addr, "warm-node", warmNode, {
|
||||
cached_environment_digests: [environmentDigest],
|
||||
dependency_cache_digests: [dependencyDigest],
|
||||
source_snapshots: [sourceDigest],
|
||||
artifact_locations: ["toolchain-cache"]
|
||||
});
|
||||
assert.strictEqual(warm.node_descriptors, 2);
|
||||
const reportedNodes = new Set([cold.node, warm.node]);
|
||||
assert.strictEqual(reportedNodes.size, 2);
|
||||
assert(reportedNodes.has("cold-node"));
|
||||
assert(reportedNodes.has("warm-node"));
|
||||
|
||||
const inspected = await send(addr, {
|
||||
type: "list_node_descriptors",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator"
|
||||
});
|
||||
assert.strictEqual(inspected.type, "node_descriptors");
|
||||
assert.strictEqual(inspected.actor, "operator");
|
||||
assert.strictEqual(inspected.descriptors.length, 2);
|
||||
const warmDescriptor = inspected.descriptors.find(
|
||||
(descriptor) => descriptor.id === "warm-node"
|
||||
);
|
||||
assert(warmDescriptor, "warm node descriptor must be visible to inspector state");
|
||||
assert(warmDescriptor.capabilities.capabilities.includes("Command"));
|
||||
assert(warmDescriptor.capabilities.capabilities.includes("RootlessPodman"));
|
||||
assert(warmDescriptor.cached_environments.includes(environmentDigest));
|
||||
assert(warmDescriptor.dependency_caches.includes(dependencyDigest));
|
||||
assert(warmDescriptor.source_snapshots.includes(sourceDigest));
|
||||
assert(warmDescriptor.artifact_locations.includes("toolchain-cache"));
|
||||
|
||||
const crossScopeInspection = await send(addr, {
|
||||
type: "list_node_descriptors",
|
||||
tenant: "other-tenant",
|
||||
project: "project",
|
||||
actor_user: "operator"
|
||||
});
|
||||
assert.strictEqual(crossScopeInspection.type, "node_descriptors");
|
||||
assert.strictEqual(crossScopeInspection.descriptors.length, 0);
|
||||
|
||||
const crossTenantReport = await send(addr, signedNodeRequest("warm-node", warmNode, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "other-tenant",
|
||||
project: "project",
|
||||
node: "warm-node",
|
||||
capabilities: linuxCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(crossTenantReport.type, "error");
|
||||
assert.match(crossTenantReport.message, /tenant\/project scope/);
|
||||
|
||||
const placement = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: {
|
||||
os: "Linux",
|
||||
arch: null,
|
||||
capabilities: ["Containers", "RootlessPodman"]
|
||||
},
|
||||
environment_digest: environmentDigest,
|
||||
required_capabilities: ["Command"],
|
||||
dependency_cache: dependencyDigest,
|
||||
source_snapshot: sourceDigest,
|
||||
required_artifacts: ["toolchain-cache"],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(placement.type, "task_placement");
|
||||
assert.strictEqual(placement.placement.node, "warm-node");
|
||||
assert.ok(placement.placement.score > 0);
|
||||
assert.ok(placement.placement.reasons.includes("warm environment cache"));
|
||||
assert.ok(placement.placement.reasons.includes("warm dependency cache"));
|
||||
assert.ok(placement.placement.reasons.includes("source snapshot already local"));
|
||||
assert.ok(
|
||||
placement.placement.reasons.includes("1 required artifact(s) already local")
|
||||
);
|
||||
|
||||
const impossible = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["WindowsCommandDev"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: null,
|
||||
required_artifacts: [],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(impossible.type, "error");
|
||||
assert.match(impossible.message, /WindowsCommandDev/);
|
||||
|
||||
const started = await send(addr, {
|
||||
type: "start_process",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
process: "vp-wait-for-git"
|
||||
});
|
||||
assert.strictEqual(started.type, "process_started");
|
||||
|
||||
const queued = await send(addr, {
|
||||
type: "launch_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
task_spec: {
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process: "vp-wait-for-git",
|
||||
task_definition: "prepare_source",
|
||||
task_instance: "prepare_source-1",
|
||||
dispatch: {
|
||||
kind: "coordinator_node_wasm",
|
||||
export: bundle.taskExport,
|
||||
abi: "task_v1",
|
||||
},
|
||||
environment_id: null,
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["SourceFilesystem", "SourceGit"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: null,
|
||||
required_artifacts: [],
|
||||
args: [],
|
||||
vfs_epoch: started.epoch,
|
||||
bundle_digest: bundle.digest,
|
||||
},
|
||||
wait_for_node: true,
|
||||
artifact_path: "/vfs/artifacts/git-status.txt",
|
||||
wasm_module_base64: bundle.wasmModuleBase64,
|
||||
});
|
||||
assert.strictEqual(queued.type, "task_queued");
|
||||
assert.strictEqual(queued.process, "vp-wait-for-git");
|
||||
assert.strictEqual(queued.task, "prepare_source-1");
|
||||
assert.match(queued.reason, /SourceGit/);
|
||||
assert.strictEqual(queued.queued_tasks, 1);
|
||||
|
||||
const gitNode = await attachNode(addr, "git-node");
|
||||
const gitRecorded = await reportNode(addr, "git-node", gitNode, {
|
||||
capabilities: gitCapabilities(),
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: false
|
||||
});
|
||||
assert.strictEqual(gitRecorded.type, "node_capabilities_recorded");
|
||||
|
||||
const pendingAssignment = await send(addr, signedNodeRequest("git-node", gitNode, "poll_task_assignment", {
|
||||
type: "poll_task_assignment",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "git-node"
|
||||
}));
|
||||
assert.strictEqual(pendingAssignment.type, "task_assignment");
|
||||
assert(pendingAssignment.assignment, "late capable node should receive queued assignment");
|
||||
assert.strictEqual(pendingAssignment.assignment.process, "vp-wait-for-git");
|
||||
assert.strictEqual(pendingAssignment.assignment.task, "prepare_source-1");
|
||||
assert.strictEqual(pendingAssignment.assignment.node, "git-node");
|
||||
assert.strictEqual(
|
||||
pendingAssignment.assignment.task_spec.dispatch.export,
|
||||
bundle.taskExport
|
||||
);
|
||||
|
||||
const emptyAssignment = await send(addr, signedNodeRequest("git-node", gitNode, "poll_task_assignment", {
|
||||
type: "poll_task_assignment",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "git-node"
|
||||
}));
|
||||
assert.strictEqual(emptyAssignment.type, "task_assignment");
|
||||
assert.strictEqual(emptyAssignment.assignment, null);
|
||||
|
||||
await reportNode(addr, "warm-node", warmNode, {
|
||||
cached_environment_digests: [],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: false
|
||||
});
|
||||
const disconnectedTransfer = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["Command"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: sourceDigest,
|
||||
required_artifacts: ["toolchain-cache"],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(disconnectedTransfer.type, "error");
|
||||
assert.match(disconnectedTransfer.message, /source snapshot unavailable/);
|
||||
assert.match(disconnectedTransfer.message, /required artifact\(s\) unavailable/);
|
||||
assert.match(disconnectedTransfer.message, /direct connectivity unavailable/);
|
||||
} catch (error) {
|
||||
if (coordinatorStderr) {
|
||||
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Scheduler placement smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
49
scripts/sdk-spawn-runtime-smoke.js
Executable file
49
scripts/sdk-spawn-runtime-smoke.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const sdk = fs.readFileSync(path.join(repo, "crates/disasmer-sdk/src/lib.rs"), "utf8");
|
||||
const productRuntime = fs.readFileSync(
|
||||
path.join(repo, "crates/disasmer-sdk/src/sdk_runtime.rs"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.match(sdk, /pub struct RuntimeSpawnEvent/);
|
||||
assert.match(sdk, /pub debugger_visible: bool/);
|
||||
assert.match(sdk, /fn register_runtime_thread/);
|
||||
assert.match(sdk, /register_runtime_thread\(id, self\.name, self\.env\)/);
|
||||
assert.match(sdk, /pub fn debugger_visible\(&self\) -> bool/);
|
||||
assert.match(sdk, /ProductRuntimeConfig::from_env\(\)/);
|
||||
assert.match(sdk, /start_remote_task\(config, task_id/);
|
||||
assert.match(sdk, /start_guest_host_task/);
|
||||
assert.match(sdk, /join_remote_task\(remote\)/);
|
||||
assert.match(productRuntime, /let spec = TaskSpec \{/);
|
||||
assert.match(productRuntime, /CoordinatorNodeWasm/);
|
||||
assert.match(productRuntime, /task_start_v1/);
|
||||
assert.match(productRuntime, /task_join_v1/);
|
||||
assert.match(productRuntime, /command_run_v1/);
|
||||
assert.match(productRuntime, /"task_spec": spec/);
|
||||
assert.match(productRuntime, /remote_completion_observed/);
|
||||
assert.match(productRuntime, /TaskJoinState::Pending/);
|
||||
assert.doesNotMatch(
|
||||
productRuntime,
|
||||
/entry\s*\(/,
|
||||
"product runtime must not invoke the submitted local Rust closure"
|
||||
);
|
||||
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"test",
|
||||
"-p",
|
||||
"disasmer-sdk",
|
||||
"spawn_task_start_registers_debugger_visible_runtime_thread",
|
||||
],
|
||||
{ cwd: repo, stdio: "inherit" }
|
||||
);
|
||||
|
||||
console.log("SDK spawn runtime smoke passed");
|
||||
723
scripts/self-hosted-coordinator-smoke.js
Normal file
723
scripts/self-hosted-coordinator-smoke.js
Normal file
|
|
@ -0,0 +1,723 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const teamArtifactDigest = `sha256:${"a".repeat(64)}`;
|
||||
const teamEnvironmentDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update("env-team-linux")
|
||||
.digest("hex")}`;
|
||||
const teamDependencyCacheDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update("cargo-cache")
|
||||
.digest("hex")}`;
|
||||
const teamSourceDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update("source-team")
|
||||
.digest("hex")}`;
|
||||
const coordinatorTaskModule = Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
|
||||
const coordinatorTaskBundleDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(coordinatorTaskModule)
|
||||
.digest("hex")}`;
|
||||
const adminToken = "self-hosted-smoke-admin-token";
|
||||
const clientSessionSecret = "self-hosted-smoke-client-session-secret";
|
||||
const releaseManifestPath =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
|
||||
path.join(repo, "target/public-release-dryrun/public-release-manifest.json");
|
||||
|
||||
function sha256(value) {
|
||||
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
||||
}
|
||||
|
||||
function digestFromParts(parts) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
for (const value of parts) {
|
||||
const bytes = Buffer.from(String(value));
|
||||
const length = Buffer.alloc(8);
|
||||
length.writeBigUInt64BE(BigInt(bytes.length));
|
||||
hash.update(length);
|
||||
hash.update(bytes);
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function adminRequest(token, operation, tenant, actorUser, targetTenant, nonce) {
|
||||
const issuedAtEpochSeconds = Math.floor(Date.now() / 1000);
|
||||
return {
|
||||
type: operation,
|
||||
tenant,
|
||||
actor_user: actorUser,
|
||||
...(operation === "suspend_tenant" ? { target_tenant: targetTenant } : {}),
|
||||
admin_proof: digestFromParts([
|
||||
"disasmer-admin-request-proof:v1",
|
||||
sha256(token),
|
||||
operation,
|
||||
tenant,
|
||||
actorUser,
|
||||
targetTenant,
|
||||
nonce,
|
||||
issuedAtEpochSeconds,
|
||||
]),
|
||||
admin_nonce: nonce,
|
||||
issued_at_epoch_seconds: issuedAtEpochSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function authenticated(request, sessionSecret = clientSessionSecret) {
|
||||
return {
|
||||
type: "authenticated",
|
||||
session_secret: sessionSecret,
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
function linuxNodeCapabilities() {
|
||||
return {
|
||||
os: "Linux",
|
||||
arch: "x86_64",
|
||||
capabilities: ["Command", "RootlessPodman", "VfsArtifacts", "QuicDirect"],
|
||||
environment_backends: ["Container"],
|
||||
source_providers: ["filesystem", "git"]
|
||||
};
|
||||
}
|
||||
|
||||
function nodeIdentity(node) {
|
||||
void node;
|
||||
const { privateKey: privateKeyObject, publicKey } =
|
||||
crypto.generateKeyPairSync("ed25519");
|
||||
const publicDer = publicKey.export({
|
||||
format: "der",
|
||||
type: "spki",
|
||||
});
|
||||
return {
|
||||
publicKey: `ed25519:${Buffer.from(publicDer).subarray(-32).toString("base64")}`,
|
||||
privateKeyObject,
|
||||
};
|
||||
}
|
||||
|
||||
function signedRequestPayloadDigest(request) {
|
||||
const canonicalize = (value, topLevel = true) => {
|
||||
if (Array.isArray(value)) return value.map((entry) => canonicalize(entry, false));
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(
|
||||
([key, entry]) =>
|
||||
entry !== null &&
|
||||
(!topLevel || !["agent_signature", "node_signature"].includes(key))
|
||||
)
|
||||
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.map(([key, entry]) => [key, canonicalize(entry, false)])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update(JSON.stringify(canonicalize(request)))
|
||||
.digest("hex")}`;
|
||||
}
|
||||
|
||||
function nodeSignatureMessage(
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
issuedAtEpochSeconds
|
||||
) {
|
||||
const parts = [
|
||||
"disasmer-node-request-signature:v2",
|
||||
node,
|
||||
requestKind,
|
||||
payloadDigest,
|
||||
nonce,
|
||||
String(issuedAtEpochSeconds),
|
||||
];
|
||||
return Buffer.concat(
|
||||
parts.flatMap((part) => [
|
||||
Buffer.from(`${Buffer.byteLength(part)}:`),
|
||||
Buffer.from(part),
|
||||
Buffer.from("\n"),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function signedNodeHeartbeat(node, identity) {
|
||||
const request = { type: "node_heartbeat", node };
|
||||
const nonce = `self-hosted-heartbeat-${process.pid}-${Date.now()}`;
|
||||
const issuedAt = Math.floor(Date.now() / 1000);
|
||||
const signature = crypto.sign(
|
||||
null,
|
||||
nodeSignatureMessage(
|
||||
node,
|
||||
"node_heartbeat",
|
||||
signedRequestPayloadDigest(request),
|
||||
nonce,
|
||||
issuedAt
|
||||
),
|
||||
identity.privateKeyObject
|
||||
);
|
||||
return {
|
||||
nonce,
|
||||
issued_at_epoch_seconds: issuedAt,
|
||||
signature: `ed25519:${signature.toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function signedNodeRequest(node, identity, requestKind, request) {
|
||||
return {
|
||||
type: "signed_node",
|
||||
node,
|
||||
node_signature: signedNodeHeartbeatForKind(node, identity, requestKind, request),
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
function signedNodeHeartbeatForKind(node, identity, requestKind, request) {
|
||||
const nonce = `${requestKind}-${process.pid}-${Date.now()}`;
|
||||
const issuedAt = Math.floor(Date.now() / 1000);
|
||||
const signature = crypto.sign(
|
||||
null,
|
||||
nodeSignatureMessage(
|
||||
node,
|
||||
requestKind,
|
||||
signedRequestPayloadDigest(request),
|
||||
nonce,
|
||||
issuedAt
|
||||
),
|
||||
identity.privateKeyObject
|
||||
);
|
||||
return {
|
||||
nonce,
|
||||
issued_at_epoch_seconds: issuedAt,
|
||||
signature: `ed25519:${signature.toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function commandOutput(command, args) {
|
||||
try {
|
||||
return cp
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function runJson(command, args, options = {}) {
|
||||
return JSON.parse(
|
||||
cp.execFileSync(command, args, {
|
||||
cwd: options.cwd || repo,
|
||||
encoding: "utf8",
|
||||
input: options.input,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function expectedSourceCommit() {
|
||||
return (
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"])
|
||||
);
|
||||
}
|
||||
|
||||
function readReleaseManifest() {
|
||||
if (!fs.existsSync(releaseManifestPath)) return null;
|
||||
const manifest = JSON.parse(fs.readFileSync(releaseManifestPath, "utf8"));
|
||||
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
|
||||
const expectedCommit = expectedSourceCommit();
|
||||
if (expectedCommit) {
|
||||
assert.strictEqual(
|
||||
manifest.source_commit,
|
||||
expectedCommit,
|
||||
"self-hosted coordinator smoke must use the manifest for the current acceptance commit"
|
||||
);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function releaseIdentity() {
|
||||
const manifest = readReleaseManifest();
|
||||
return {
|
||||
sourceCommit: manifest ? manifest.source_commit : expectedSourceCommit(),
|
||||
releaseName:
|
||||
(manifest && manifest.release_name) ||
|
||||
process.env.DISASMER_PUBLIC_RELEASE_NAME ||
|
||||
null,
|
||||
};
|
||||
}
|
||||
|
||||
async function attachTrustedNode(addr, node, capabilities = linuxNodeCapabilities()) {
|
||||
const identity = nodeIdentity(node);
|
||||
const grant = await send(addr, authenticated({
|
||||
type: "create_node_enrollment_grant",
|
||||
ttl_seconds: 900,
|
||||
}));
|
||||
assert.strictEqual(grant.type, "node_enrollment_grant_created");
|
||||
|
||||
const attached = await send(addr, {
|
||||
type: "exchange_node_enrollment_grant",
|
||||
tenant: "team",
|
||||
project: "self-hosted",
|
||||
node,
|
||||
public_key: identity.publicKey,
|
||||
enrollment_grant: grant.grant,
|
||||
});
|
||||
assert.strictEqual(attached.type, "node_enrollment_exchanged");
|
||||
|
||||
const heartbeat = await send(addr, {
|
||||
type: "node_heartbeat",
|
||||
node,
|
||||
node_signature: signedNodeHeartbeat(node, identity)
|
||||
});
|
||||
assert.strictEqual(heartbeat.type, "node_heartbeat");
|
||||
|
||||
const reported = await send(addr, signedNodeRequest(node, identity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "team",
|
||||
project: "self-hosted",
|
||||
node,
|
||||
capabilities,
|
||||
cached_environment_digests: [teamEnvironmentDigest],
|
||||
dependency_cache_digests: [teamDependencyCacheDigest],
|
||||
source_snapshots: [teamSourceDigest],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(reported.type, "node_capabilities_recorded");
|
||||
return identity;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const release = releaseIdentity();
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0"
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_ADMIN_TOKEN: adminToken,
|
||||
DISASMER_SELF_HOSTED_SESSION_SECRET: clientSessionSecret,
|
||||
DISASMER_SELF_HOSTED_TENANT: "team",
|
||||
DISASMER_SELF_HOSTED_PROJECT: "self-hosted",
|
||||
DISASMER_SELF_HOSTED_USER: "developer",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual(ready.client_authority, "strict");
|
||||
assert.strictEqual(ready.self_hosted_session_bootstrapped, true);
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const forgedBodyAuthority = await send(addr, {
|
||||
type: "start_process",
|
||||
tenant: "victim-tenant",
|
||||
project: "victim-project",
|
||||
actor_user: "forged-user",
|
||||
process: "vp-forged",
|
||||
});
|
||||
assert.strictEqual(forgedBodyAuthority.type, "error");
|
||||
assert.match(forgedBodyAuthority.message, /body.*identity.*not authority/i);
|
||||
const wrongSession = await send(
|
||||
addr,
|
||||
authenticated({ type: "auth_status" }, "wrong-session-secret")
|
||||
);
|
||||
assert.strictEqual(wrongSession.type, "error");
|
||||
assert.match(wrongSession.message, /session.*not recognized|not recognized.*session/i);
|
||||
const authStatus = await send(addr, authenticated({ type: "auth_status" }));
|
||||
assert.strictEqual(authStatus.type, "auth_status");
|
||||
assert.strictEqual(authStatus.tenant, "team");
|
||||
assert.strictEqual(authStatus.project, "self-hosted");
|
||||
assert.strictEqual(authStatus.actor, "developer");
|
||||
|
||||
const missingAdminCredential = await send(
|
||||
addr,
|
||||
adminRequest("", "admin_status", "team", "forged-admin", "team", "admin-empty")
|
||||
);
|
||||
assert.strictEqual(missingAdminCredential.type, "error");
|
||||
assert.match(missingAdminCredential.message, /admin.*proof.*invalid/i);
|
||||
const wrongAdminCredential = await send(
|
||||
addr,
|
||||
adminRequest(
|
||||
"wrong-token",
|
||||
"admin_status",
|
||||
"team",
|
||||
"forged-admin",
|
||||
"team",
|
||||
"admin-wrong"
|
||||
)
|
||||
);
|
||||
assert.strictEqual(wrongAdminCredential.type, "error");
|
||||
assert.match(wrongAdminCredential.message, /admin.*proof.*invalid/i);
|
||||
const replayableAdminRequest = adminRequest(
|
||||
adminToken,
|
||||
"admin_status",
|
||||
"team",
|
||||
"self-hosted-admin",
|
||||
"team",
|
||||
"admin-replay"
|
||||
);
|
||||
const directAdminStatus = await send(addr, replayableAdminRequest);
|
||||
assert.strictEqual(directAdminStatus.type, "admin_status");
|
||||
const replayedAdminStatus = await send(addr, replayableAdminRequest);
|
||||
assert.strictEqual(replayedAdminStatus.type, "error");
|
||||
assert.match(replayedAdminStatus.message, /nonce was already used/i);
|
||||
const cargoTargetDir = path.resolve(
|
||||
process.env.CARGO_TARGET_DIR || path.join(repo, "target")
|
||||
);
|
||||
const cliBin = path.join(
|
||||
cargoTargetDir,
|
||||
"debug",
|
||||
process.platform === "win32" ? "disasmer.exe" : "disasmer"
|
||||
);
|
||||
const selfHostedCliProject = path.join(
|
||||
repo,
|
||||
"target/acceptance/self-hosted-cli-project"
|
||||
);
|
||||
fs.rmSync(selfHostedCliProject, { recursive: true, force: true });
|
||||
fs.mkdirSync(selfHostedCliProject, { recursive: true });
|
||||
const cliConnect = runJson(
|
||||
cliBin,
|
||||
[
|
||||
"auth",
|
||||
"connect-self-hosted",
|
||||
"--coordinator",
|
||||
ready.listen,
|
||||
"--tenant",
|
||||
"team",
|
||||
"--project-id",
|
||||
"self-hosted",
|
||||
"--user",
|
||||
"developer",
|
||||
"--session-secret-stdin",
|
||||
"--json",
|
||||
],
|
||||
{ cwd: selfHostedCliProject, input: `${clientSessionSecret}\n` }
|
||||
);
|
||||
assert.strictEqual(cliConnect.status, "connected");
|
||||
assert.strictEqual(cliConnect.session_secret_read_from_stdin, true);
|
||||
assert.strictEqual(cliConnect.session_secret_exposed_in_report, false);
|
||||
assert.strictEqual(cliConnect.coordinator_response.type, "auth_status");
|
||||
const sessionPath = path.join(
|
||||
selfHostedCliProject,
|
||||
".disasmer/session.json"
|
||||
);
|
||||
const storedSession = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
|
||||
assert.strictEqual(storedSession.kind, "self_hosted");
|
||||
assert.strictEqual(storedSession.session_secret, clientSessionSecret);
|
||||
assert.strictEqual(storedSession.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(storedSession.provider_tokens_sent_to_nodes, false);
|
||||
if (process.platform !== "win32") {
|
||||
assert.strictEqual(fs.statSync(sessionPath).mode & 0o777, 0o600);
|
||||
}
|
||||
const cliAuthStatus = runJson(
|
||||
cliBin,
|
||||
["auth", "status", "--json"],
|
||||
{ cwd: selfHostedCliProject }
|
||||
);
|
||||
assert.strictEqual(cliAuthStatus.active_coordinator, ready.listen);
|
||||
assert.strictEqual(
|
||||
cliAuthStatus.coordinator_account_status.used_cli_session_credential,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
cliAuthStatus.coordinator_account_status.coordinator_response_type,
|
||||
"auth_status"
|
||||
);
|
||||
const cliAdminStatus = runJson(cliBin, [
|
||||
"admin",
|
||||
"status",
|
||||
"--coordinator",
|
||||
ready.listen,
|
||||
"--tenant",
|
||||
"team",
|
||||
"--user",
|
||||
"self-hosted-admin",
|
||||
"--admin-token",
|
||||
adminToken,
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(cliAdminStatus.response.type, "admin_status");
|
||||
assert.strictEqual(cliAdminStatus.suspended, false);
|
||||
const cliAdminSuspend = runJson(cliBin, [
|
||||
"admin",
|
||||
"suspend-tenant",
|
||||
"--coordinator",
|
||||
ready.listen,
|
||||
"--tenant",
|
||||
"team",
|
||||
"--user",
|
||||
"self-hosted-admin",
|
||||
"--target-tenant",
|
||||
"admin-probe-tenant",
|
||||
"--admin-token",
|
||||
adminToken,
|
||||
"--yes",
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(cliAdminSuspend.response.type, "tenant_suspended");
|
||||
assert.strictEqual(cliAdminSuspend.suspended, true);
|
||||
const cliAdminProbeStatus = runJson(cliBin, [
|
||||
"admin",
|
||||
"status",
|
||||
"--coordinator",
|
||||
ready.listen,
|
||||
"--tenant",
|
||||
"admin-probe-tenant",
|
||||
"--user",
|
||||
"self-hosted-admin",
|
||||
"--admin-token",
|
||||
adminToken,
|
||||
"--json",
|
||||
]);
|
||||
assert.strictEqual(cliAdminProbeStatus.suspended, true);
|
||||
|
||||
const teamLinuxA = await attachTrustedNode(addr, "team-linux-a");
|
||||
const teamLinuxBCapabilities = linuxNodeCapabilities();
|
||||
teamLinuxBCapabilities.capabilities = teamLinuxBCapabilities.capabilities.filter(
|
||||
(capability) => capability !== "VfsArtifacts"
|
||||
);
|
||||
await attachTrustedNode(addr, "team-linux-b", teamLinuxBCapabilities);
|
||||
|
||||
const placement = await send(addr, authenticated({
|
||||
type: "schedule_task",
|
||||
environment: {
|
||||
os: "Linux",
|
||||
arch: "x86_64",
|
||||
capabilities: ["Command", "QuicDirect"]
|
||||
},
|
||||
environment_digest: teamEnvironmentDigest,
|
||||
required_capabilities: ["VfsArtifacts"],
|
||||
source_snapshot: teamSourceDigest,
|
||||
required_artifacts: [],
|
||||
prefer_node: "team-linux-a"
|
||||
}));
|
||||
assert.strictEqual(placement.type, "task_placement");
|
||||
assert.strictEqual(placement.placement.node, "team-linux-a");
|
||||
assert(placement.placement.reasons.includes("preferred node"));
|
||||
assert(placement.placement.reasons.includes("warm environment cache"));
|
||||
assert(placement.placement.reasons.includes("source snapshot already local"));
|
||||
|
||||
const started = await send(addr, authenticated({
|
||||
type: "start_process",
|
||||
process: "vp-team-build",
|
||||
restart: false,
|
||||
}));
|
||||
assert.strictEqual(started.type, "process_started");
|
||||
|
||||
const reconnected = await send(addr, signedNodeRequest("team-linux-a", teamLinuxA, "reconnect_node", {
|
||||
type: "reconnect_node",
|
||||
node: "team-linux-a",
|
||||
process: "vp-team-build",
|
||||
epoch: started.epoch
|
||||
}));
|
||||
assert.strictEqual(reconnected.type, "node_reconnected");
|
||||
|
||||
const launched = await send(addr, authenticated({
|
||||
type: "launch_task",
|
||||
task_spec: {
|
||||
tenant: "team",
|
||||
project: "self-hosted",
|
||||
process: "vp-team-build",
|
||||
task_definition: "compile-linux",
|
||||
task_instance: "compile-linux",
|
||||
dispatch: {
|
||||
kind: "coordinator_node_wasm",
|
||||
export: "compile_linux",
|
||||
abi: "task_v1",
|
||||
},
|
||||
environment_id: null,
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["VfsArtifacts"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: null,
|
||||
required_artifacts: [],
|
||||
args: [],
|
||||
vfs_epoch: started.epoch,
|
||||
bundle_digest: coordinatorTaskBundleDigest,
|
||||
},
|
||||
wait_for_node: false,
|
||||
artifact_path: "/vfs/artifacts/team-output.txt",
|
||||
wasm_module_base64: coordinatorTaskModule.toString("base64"),
|
||||
}));
|
||||
assert.strictEqual(launched.type, "task_launched", JSON.stringify(launched));
|
||||
assert.strictEqual(launched.placement.node, "team-linux-a");
|
||||
|
||||
const completed = await send(addr, signedNodeRequest("team-linux-a", teamLinuxA, "task_completed", {
|
||||
type: "task_completed",
|
||||
tenant: "team",
|
||||
project: "self-hosted",
|
||||
process: "vp-team-build",
|
||||
node: "team-linux-a",
|
||||
task: "compile-linux",
|
||||
status_code: 0,
|
||||
stdout_bytes: 18,
|
||||
stderr_bytes: 0,
|
||||
stdout_tail: "",
|
||||
stderr_tail: "",
|
||||
stdout_truncated: false,
|
||||
stderr_truncated: false,
|
||||
artifact_path: "/vfs/artifacts/team-output.txt",
|
||||
artifact_digest: teamArtifactDigest,
|
||||
artifact_size_bytes: 18
|
||||
}));
|
||||
assert.strictEqual(completed.type, "task_recorded", JSON.stringify(completed));
|
||||
|
||||
const events = await send(addr, authenticated({
|
||||
type: "list_task_events",
|
||||
process: "vp-team-build",
|
||||
}));
|
||||
assert.strictEqual(events.type, "task_events");
|
||||
assert.strictEqual(events.events.length, 1);
|
||||
assert.strictEqual(events.events[0].node, "team-linux-a");
|
||||
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/team-output.txt");
|
||||
|
||||
const link = await send(addr, authenticated({
|
||||
type: "create_artifact_download_link",
|
||||
artifact: "team-output.txt",
|
||||
max_bytes: 1024 * 1024,
|
||||
ttl_seconds: 60
|
||||
}));
|
||||
assert.strictEqual(link.type, "artifact_download_link");
|
||||
assert.deepStrictEqual(link.link.source, { RetainedNode: "team-linux-a" });
|
||||
|
||||
const exportPlan = await send(addr, authenticated({
|
||||
type: "export_artifact_to_node",
|
||||
artifact: "team-output.txt",
|
||||
receiver_node: "team-linux-b",
|
||||
direct_connectivity: true,
|
||||
failure_reason: ""
|
||||
}));
|
||||
assert.strictEqual(exportPlan.type, "artifact_export_plan");
|
||||
assert.strictEqual(exportPlan.source_node, "team-linux-a");
|
||||
assert.strictEqual(exportPlan.receiver_node, "team-linux-b");
|
||||
assert.strictEqual(exportPlan.plan.transport, "NativeQuic");
|
||||
assert.strictEqual(exportPlan.plan.coordinator_assisted_rendezvous, true);
|
||||
assert.strictEqual(exportPlan.plan.coordinator_bulk_relay_allowed, false);
|
||||
|
||||
const reportPath = path.join(repo, "target/acceptance/core-coordinator-compat.json");
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
reportPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
kind: "disasmer-core-coordinator-compatibility",
|
||||
source_commit: release.sourceCommit,
|
||||
release_name: release.releaseName,
|
||||
coordinator_implementation: "standalone-core-coordinator",
|
||||
coordinator_addr: ready.listen,
|
||||
client_authority: ready.client_authority,
|
||||
authenticated_session: authStatus.authenticated,
|
||||
forged_body_authority_denied: forgedBodyAuthority.type,
|
||||
wrong_session_denied: wrongSession.type,
|
||||
ping: "pong",
|
||||
nodes: ["team-linux-a", "team-linux-b"],
|
||||
task_placement: placement.type,
|
||||
process_started: started.type,
|
||||
task_completion: completed.type,
|
||||
task_events: events.events.length,
|
||||
artifact_download_link: link.type,
|
||||
artifact_export_plan: exportPlan.type,
|
||||
self_hosted_admin: {
|
||||
missing_credential_denied: missingAdminCredential.type,
|
||||
wrong_credential_denied: wrongAdminCredential.type,
|
||||
nonce_bound_proof_succeeded: directAdminStatus.type,
|
||||
replay_denied: replayedAdminStatus.type,
|
||||
cli_status: cliAdminStatus.response.type,
|
||||
cli_suspend: cliAdminSuspend.response.type,
|
||||
suspended_state_observed: cliAdminProbeStatus.suspended,
|
||||
},
|
||||
self_hosted_cli: {
|
||||
connected: cliConnect.status,
|
||||
secret_read_from_stdin: cliConnect.session_secret_read_from_stdin,
|
||||
secret_exposed_in_report: cliConnect.session_secret_exposed_in_report,
|
||||
session_file_mode:
|
||||
process.platform === "win32"
|
||||
? "windows-best-effort"
|
||||
: (fs.statSync(sessionPath).mode & 0o777).toString(8),
|
||||
authenticated_status:
|
||||
cliAuthStatus.coordinator_account_status.coordinator_response_type,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Self-hosted coordinator smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
218
scripts/source-preparation-smoke.js
Executable file
218
scripts/source-preparation-smoke.js
Executable file
|
|
@ -0,0 +1,218 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sourceCapableNode(sourceProviders = ["git"]) {
|
||||
return {
|
||||
os: "Linux",
|
||||
arch: "x86_64",
|
||||
capabilities: ["Command", "SourceFilesystem", "SourceGit"],
|
||||
environment_backends: [],
|
||||
source_providers: sourceProviders
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
let coordinatorStderr = "";
|
||||
coordinator.stderr.on("data", (chunk) => {
|
||||
coordinatorStderr += chunk.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const pending = await send(addr, {
|
||||
type: "request_source_preparation",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
provider: "Git"
|
||||
});
|
||||
assert.strictEqual(pending.type, "source_preparation");
|
||||
assert.strictEqual(pending.status.preparation.tenant, "tenant");
|
||||
assert.strictEqual(pending.status.preparation.project, "project");
|
||||
assert.strictEqual(pending.status.preparation.provider, "Git");
|
||||
assert.strictEqual(
|
||||
pending.status.preparation.coordinator_requires_checkout_access,
|
||||
false
|
||||
);
|
||||
assert.deepStrictEqual(pending.status.preparation.required_capabilities, [
|
||||
"SourceGit"
|
||||
]);
|
||||
assert.match(pending.status.disposition.Pending.reason, /waiting|node/i);
|
||||
|
||||
const nodeIdentities = new Map();
|
||||
for (const node of ["source-cold", "source-ready"]) {
|
||||
const identity = nodeIdentity("source-preparation-smoke", node);
|
||||
nodeIdentities.set(node, identity);
|
||||
const attached = await send(addr, {
|
||||
type: "attach_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node,
|
||||
public_key: identity.publicKey
|
||||
});
|
||||
assert.strictEqual(attached.type, "node_attached");
|
||||
}
|
||||
|
||||
const cold = await send(addr, signedNodeRequest("source-cold", nodeIdentities.get("source-cold"), "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "source-cold",
|
||||
capabilities: sourceCapableNode(),
|
||||
cached_environment_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(cold.type, "node_capabilities_recorded");
|
||||
|
||||
const readyReport = await send(addr, signedNodeRequest("source-ready", nodeIdentities.get("source-ready"), "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "source-ready",
|
||||
capabilities: sourceCapableNode(),
|
||||
cached_environment_digests: [],
|
||||
source_snapshots: [],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(readyReport.type, "node_capabilities_recorded");
|
||||
|
||||
const assigned = await send(addr, {
|
||||
type: "request_source_preparation",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
provider: "Git"
|
||||
});
|
||||
assert.strictEqual(assigned.type, "source_preparation");
|
||||
assert(["source-cold", "source-ready"].includes(assigned.status.disposition.Assigned.node));
|
||||
assert.strictEqual(
|
||||
assigned.status.preparation.coordinator_requires_checkout_access,
|
||||
false
|
||||
);
|
||||
|
||||
const crossTenantCompletion = await send(addr, signedNodeRequest("source-ready", nodeIdentities.get("source-ready"), "complete_source_preparation", {
|
||||
type: "complete_source_preparation",
|
||||
tenant: "other",
|
||||
project: "project",
|
||||
node: "source-ready",
|
||||
provider: "Git",
|
||||
source_snapshot: "sha256:source-prepared"
|
||||
}));
|
||||
assert.strictEqual(crossTenantCompletion.type, "error");
|
||||
assert.match(crossTenantCompletion.message, /tenant\/project scope/i);
|
||||
|
||||
const completed = await send(addr, signedNodeRequest("source-ready", nodeIdentities.get("source-ready"), "complete_source_preparation", {
|
||||
type: "complete_source_preparation",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: "source-ready",
|
||||
provider: "Git",
|
||||
source_snapshot: "sha256:source-prepared"
|
||||
}));
|
||||
assert.strictEqual(completed.type, "source_preparation_completed");
|
||||
assert.strictEqual(completed.node, "source-ready");
|
||||
assert.strictEqual(completed.provider, "Git");
|
||||
assert.strictEqual(completed.source_snapshot, "sha256:source-prepared");
|
||||
|
||||
const placement = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["SourceGit"],
|
||||
source_snapshot: "sha256:source-prepared",
|
||||
required_artifacts: [],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(placement.type, "task_placement");
|
||||
assert.strictEqual(placement.placement.node, "source-ready");
|
||||
assert(
|
||||
placement.placement.reasons.includes("source snapshot already local"),
|
||||
"completed source preparation must update node source locality"
|
||||
);
|
||||
} catch (error) {
|
||||
if (coordinatorStderr) {
|
||||
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Source preparation smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
197
scripts/tenant-isolation-contract-smoke.js
Normal file
197
scripts/tenant-isolation-contract-smoke.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function maybeRead(segments) {
|
||||
const fullPath = path.join(repo, ...segments);
|
||||
if (!fs.existsSync(fullPath)) return null;
|
||||
return fs.readFileSync(fullPath, "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing tenant-isolation evidence: ${name}`);
|
||||
}
|
||||
|
||||
function expectGate(script, gateName) {
|
||||
assert(
|
||||
script.includes("node scripts/tenant-isolation-contract-smoke.js"),
|
||||
`${gateName} must run tenant-isolation-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
const auth = read("crates/disasmer-core/src/auth.rs");
|
||||
const artifact = read("crates/disasmer-core/src/artifact.rs");
|
||||
const operatorPanel = read("crates/disasmer-core/src/operator_panel.rs");
|
||||
const source = read("crates/disasmer-core/src/source.rs");
|
||||
const coordinatorService = [
|
||||
read("crates/disasmer-coordinator/src/service.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/routing.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/nodes.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/keys.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/artifacts.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/processes.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/process_launch.rs"),
|
||||
read("crates/disasmer-coordinator/src/service/tests.rs"),
|
||||
].join("\n");
|
||||
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
|
||||
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
|
||||
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
|
||||
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
|
||||
const releaseBlockerSmoke = read("scripts/release-blocker-smoke.js");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
const privateAcceptance = read("scripts/acceptance-private.sh");
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["auth contexts carry tenant and project", /pub struct AuthContext[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
|
||||
["enrollment grants carry tenant and project", /pub struct EnrollmentGrant[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
|
||||
["node credentials carry tenant and project", /pub struct NodeCredential[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
|
||||
["same tenant/project denies tenant mismatch", /pub fn same_tenant_project[\s\S]*tenant mismatch/],
|
||||
["same tenant/project denies project mismatch", /pub fn same_tenant_project[\s\S]*project mismatch/],
|
||||
["auth unit denies cross-tenant access", /fn tenant_project_scope_denies_cross_tenant_access\(\)/],
|
||||
]) {
|
||||
expect(auth, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["artifact metadata carries tenant and project", /pub struct ArtifactMetadata[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
|
||||
["download links carry tenant project process and actor", /pub struct DownloadLink[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId[\s\S]*pub actor: Actor/],
|
||||
["downloads authorize against scoped metadata", /let authz = same_tenant_project\(context, &scope\)/],
|
||||
["artifact test denies cross-tenant download", /fn cross_tenant_download_is_denied_even_with_known_artifact_id\(\)/],
|
||||
["artifact test denies cross-project download", /fn cross_project_download_is_denied_even_with_known_artifact_id\(\)/],
|
||||
]) {
|
||||
expect(artifact, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["panel state carries tenant project and process", /pub struct PanelState[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId/],
|
||||
["panel events carry tenant project and process", /pub struct PanelEvent[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId/],
|
||||
["panel events reject scope mismatch", /PanelError::ScopeMismatch/],
|
||||
["panel scope validation compares tenant project process", /self\.tenant != event\.tenant[\s\S]*self\.project != event\.project[\s\S]*self\.process != event\.process/],
|
||||
]) {
|
||||
expect(operatorPanel, name, pattern);
|
||||
}
|
||||
|
||||
expect(
|
||||
source,
|
||||
"source preparation carries tenant and project",
|
||||
/pub struct SourcePreparation[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/
|
||||
);
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["project creation rejects foreign tenant reuse", /project id is outside the signed-in tenant scope/],
|
||||
["project selection rejects foreign tenant", /project is outside the signed-in tenant scope/],
|
||||
["project listing uses tenant-scoped context", /CoordinatorRequest::ListProjects[\s\S]*list_projects\(&context\)/],
|
||||
["node capability reports check enrollment tenant scope", /node capability report is outside the enrolled tenant\/project scope/],
|
||||
["operator panel stop state is tenant/project/process keyed", /type PanelStopKey = \(TenantId, ProjectId, ProcessId\)/],
|
||||
["download service tests tenant mismatch", /cross_tenant\.to_string\(\)\.contains\("tenant mismatch"\)/],
|
||||
["download service tests project mismatch", /cross_project\.to_string\(\)\.contains\("project mismatch"\)/],
|
||||
["node capability test rejects cross-scope report", /fn service_rejects_node_capability_report_outside_enrollment_scope\(\)/],
|
||||
["source preparation test rejects cross-scope completion", /fn service_rejects_source_preparation_completion_outside_node_scope\(\)/],
|
||||
]) {
|
||||
expect(coordinatorService, name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, sourceText, patterns] of [
|
||||
[
|
||||
"artifact download smoke",
|
||||
artifactDownloadSmoke,
|
||||
[
|
||||
/const crossTenant = await send/,
|
||||
/const crossProject = await send/,
|
||||
/const crossTenantOpen = await send/,
|
||||
/const crossProjectOpen = await send/,
|
||||
/tenant mismatch/,
|
||||
/project mismatch/,
|
||||
/token is invalid/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"operator panel smoke",
|
||||
operatorPanelSmoke,
|
||||
[/const crossTenant = await send/, /render_operator_panel/, /scope\|tenant\|project/],
|
||||
],
|
||||
[
|
||||
"scheduler and capability smoke",
|
||||
schedulerSmoke,
|
||||
[
|
||||
/const crossScopeInspection = await send/,
|
||||
/assert\.strictEqual\(crossScopeInspection\.descriptors\.length, 0\)/,
|
||||
/const crossTenantReport = await send/,
|
||||
/tenant\\\/project scope/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"source preparation smoke",
|
||||
sourcePreparationSmoke,
|
||||
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
|
||||
],
|
||||
]) {
|
||||
for (const pattern of patterns) {
|
||||
expect(sourceText, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
expect(releaseBlockerSmoke, "release blockers list tenant-isolation failure", /cross-tenant access is listed as a release blocker/);
|
||||
expectGate(publicAcceptance, "public acceptance");
|
||||
expectGate(publicSplit, "public split acceptance");
|
||||
expectGate(privateAcceptance, "private acceptance");
|
||||
|
||||
const hostedLibRoot = maybeRead(["private", "hosted-policy", "src", "lib.rs"]);
|
||||
const hostedServiceSource = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"src",
|
||||
"bin",
|
||||
"disasmer-hosted-service.rs",
|
||||
]);
|
||||
const hostedLib = hostedLibRoot;
|
||||
const hostedSmoke = maybeRead([
|
||||
"private",
|
||||
"hosted-policy",
|
||||
"scripts",
|
||||
"hosted-client-compat-smoke.js",
|
||||
]);
|
||||
|
||||
if (hostedLib && hostedServiceSource && hostedSmoke) {
|
||||
for (const [name, pattern] of [
|
||||
["hosted private layer is a compact identity broker", /pub struct HostedIdentityBroker[\s\S]*cli_session_ttl_seconds/],
|
||||
["hosted private layer has no parallel coordinator", /Runtime, persistence, nodes, processes,[\s\S]*remain owned by public CoordinatorService/],
|
||||
["hosted service delegates Client state to Core", /core_coordinator: CoordinatorService/],
|
||||
["hosted login creates project through Core", /issue_cli_session[\s\S]*AuthenticatedCoordinatorRequest::CreateProject/],
|
||||
]) {
|
||||
expect(`${hostedLib}\n${hostedServiceSource}`, name, pattern);
|
||||
}
|
||||
|
||||
for (const forbidden of [
|
||||
"HostedCommunityControlPlane",
|
||||
"HostedObservabilitySnapshot",
|
||||
"agent_public_keys: BTreeMap",
|
||||
"node_statuses: BTreeMap",
|
||||
"process_statuses: BTreeMap",
|
||||
"debug_sessions: BTreeMap",
|
||||
]) {
|
||||
assert(
|
||||
!hostedLib.includes(forbidden),
|
||||
`private hosted policy must not reimplement Core state: ${forbidden}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["client-supplied identity is denied", /const forged = await sendHostedControl[\s\S]*authenticated CLI session/],
|
||||
["second OIDC subject receives a different tenant", /assert\.notStrictEqual\(victimLogin\.session\.tenant, session\.tenant\)/],
|
||||
["cross-tenant process events are denied", /const crossTenantTaskEventsDenied = await sendHostedControl[\s\S]*vp-victim[\s\S]*scope\|denied\|unauthorized/],
|
||||
]) {
|
||||
expect(hostedSmoke, name, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Tenant isolation contract smoke passed");
|
||||
120
scripts/user-session-token-boundary-smoke.js
Executable file
120
scripts/user-session-token-boundary-smoke.js
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(path.join(repo, file), "utf8");
|
||||
}
|
||||
|
||||
function extractBalancedBlock(source, marker) {
|
||||
const start = source.indexOf(marker);
|
||||
assert(start >= 0, `missing marker ${marker}`);
|
||||
const open = source.indexOf("{", start);
|
||||
assert(open >= 0, `missing opening brace for ${marker}`);
|
||||
let depth = 0;
|
||||
for (let index = open; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === "{") depth += 1;
|
||||
if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(`missing closing brace for ${marker}`);
|
||||
}
|
||||
|
||||
function extractEnumVariant(source, variant) {
|
||||
const marker = ` ${variant} {`;
|
||||
return extractBalancedBlock(source, marker);
|
||||
}
|
||||
|
||||
const forbiddenUserSessionCredentials =
|
||||
/\b(BrowserSession|CliDeviceSession|BrowserLoginFlow|CliLoginFlow|authorization_url|verification_url|device_code|access_token|refresh_token|provider_token|provider_tokens|session_token|user_token|oauth_token)\b/i;
|
||||
|
||||
function assertNoUserSessionCredential(surface, text) {
|
||||
assert.doesNotMatch(
|
||||
text,
|
||||
forbiddenUserSessionCredentials,
|
||||
`${surface} must not carry user OAuth/browser/session credentials`
|
||||
);
|
||||
}
|
||||
|
||||
const coordinatorService = `${read("crates/disasmer-coordinator/src/service/protocol.rs")}\n${read("crates/disasmer-coordinator/src/service/protocol/responses.rs")}`;
|
||||
for (const variant of [
|
||||
"AdminStatus",
|
||||
"SuspendTenant",
|
||||
"AttachNode",
|
||||
"RegisterAgentPublicKey",
|
||||
"ListAgentPublicKeys",
|
||||
"RotateAgentPublicKey",
|
||||
"RevokeAgentPublicKey",
|
||||
"NodeHeartbeat",
|
||||
"ReportNodeCapabilities",
|
||||
"RevokeNodeCredential",
|
||||
"RequestRendezvous",
|
||||
"RequestSourcePreparation",
|
||||
"CompleteSourcePreparation",
|
||||
"StartProcess",
|
||||
"ReconnectNode",
|
||||
"CancelTask",
|
||||
"CancelProcess",
|
||||
"PollTaskControl",
|
||||
"RestartTask",
|
||||
"DebugAttach",
|
||||
"TaskCompleted",
|
||||
]) {
|
||||
assertNoUserSessionCredential(
|
||||
`CoordinatorRequest::${variant}`,
|
||||
extractEnumVariant(coordinatorService, variant)
|
||||
);
|
||||
}
|
||||
|
||||
const nodeRuntime = [
|
||||
read("crates/disasmer-node/src/lib.rs"),
|
||||
read("crates/disasmer-node/src/command_runner.rs"),
|
||||
].join("\n");
|
||||
for (const marker of [
|
||||
"pub struct LinuxCommandRunPlan",
|
||||
"pub struct LinuxCommandTaskOutput",
|
||||
"pub struct CapturedCommandLogs",
|
||||
"pub struct VirtualThreadCommand",
|
||||
"pub struct CommandOutput",
|
||||
]) {
|
||||
assertNoUserSessionCredential(marker, extractBalancedBlock(nodeRuntime, marker));
|
||||
}
|
||||
|
||||
const coreExecution = read("crates/disasmer-core/src/execution.rs");
|
||||
assertNoUserSessionCredential(
|
||||
"CommandInvocation",
|
||||
extractBalancedBlock(coreExecution, "pub struct CommandInvocation")
|
||||
);
|
||||
|
||||
const dapAdapter = read("crates/disasmer-dap/src/variables.rs");
|
||||
assertNoUserSessionCredential(
|
||||
"DAP variables response",
|
||||
extractBalancedBlock(dapAdapter, "fn variables_response")
|
||||
);
|
||||
|
||||
const panel = read("crates/disasmer-core/src/operator_panel.rs");
|
||||
assertNoUserSessionCredential(
|
||||
"PanelEvent",
|
||||
extractBalancedBlock(panel, "pub struct PanelEvent")
|
||||
);
|
||||
|
||||
const auth = read("crates/disasmer-core/src/auth.rs");
|
||||
assert.match(
|
||||
auth,
|
||||
/task_credentials_do_not_contain_user_session/,
|
||||
"core auth must keep the task credential user-session guard"
|
||||
);
|
||||
assert.match(
|
||||
auth,
|
||||
/CredentialKind::BrowserSession \| CredentialKind::CliDeviceSession/,
|
||||
"task credential guard must reject browser and CLI sessions"
|
||||
);
|
||||
|
||||
console.log("User session token boundary smoke passed");
|
||||
75
scripts/verify-public-split.sh
Executable file
75
scripts/verify-public-split.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source_commit="$(git -C "$repo_root" rev-parse HEAD)"
|
||||
export DISASMER_ACCEPTANCE_COMMIT="$source_commit"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
tar \
|
||||
--exclude='./.git' \
|
||||
--exclude='./target' \
|
||||
--exclude='./private' \
|
||||
--exclude='./experiments' \
|
||||
--exclude='./.disasmer' \
|
||||
--exclude='./vscode-extension/node_modules' \
|
||||
--exclude='./scripts/containers-home' \
|
||||
-C "$repo_root" \
|
||||
-cf - . | tar -C "$tmp_dir" -xf -
|
||||
|
||||
if find "$tmp_dir" -path "$tmp_dir/private" -print -quit | grep -q .; then
|
||||
echo "private directory leaked into public split" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if find "$tmp_dir" -path "$tmp_dir/experiments" -print -quit | grep -q .; then
|
||||
echo "experiments directory leaked into public split" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo test --workspace --manifest-path "$tmp_dir/Cargo.toml"
|
||||
cargo build --workspace --bins --manifest-path "$tmp_dir/Cargo.toml"
|
||||
(cd "$tmp_dir" && node scripts/acceptance-report-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/acceptance-doc-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/acceptance-environment-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/acceptance-evidence-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/code-size-guard.js)
|
||||
(cd "$tmp_dir" && node scripts/public-private-boundary-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/release-blocker-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/resource-metering-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/hostile-input-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/tenant-isolation-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/public-story-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/public-release-dryrun-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/self-hosted-coordinator-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/public-local-demo-matrix-smoke.js)
|
||||
(cd "$tmp_dir" && scripts/release-source-scan.sh)
|
||||
(cd "$tmp_dir" && node scripts/prepare-public-release-dryrun.js)
|
||||
if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]; then
|
||||
(cd "$tmp_dir" && node scripts/publish-public-release-dryrun.js)
|
||||
fi
|
||||
(cd "$tmp_dir" && node scripts/docs-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-output-mode-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-login-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-error-exit-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-browser-login-flow-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-install-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/user-session-token-boundary-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/sdk-spawn-runtime-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/node-lifecycle-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/vscode-extension-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/vscode-f5-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/node-attach-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/wasmtime-assignment-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/cli-local-run-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/artifact-download-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/artifact-export-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/operator-panel-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/source-preparation-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/scheduler-placement-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/windows-best-effort-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/windows-validation-contract-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/quic-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/dap-smoke.js)
|
||||
(cd "$tmp_dir" && node scripts/flagship-demo-smoke.js)
|
||||
232
scripts/vscode-extension-smoke.js
Executable file
232
scripts/vscode-extension-smoke.js
Executable file
|
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const assert = require("assert");
|
||||
|
||||
const extension = require("../vscode-extension/extension");
|
||||
const packageJson = require("../vscode-extension/package.json");
|
||||
const extensionSource = fs.readFileSync(
|
||||
path.join(__dirname, "../vscode-extension/extension.js"),
|
||||
"utf8"
|
||||
);
|
||||
const readme = fs.readFileSync(path.join(__dirname, "../README.md"), "utf8");
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
assert.strictEqual(packageJson.main, "./extension.js");
|
||||
assert(fs.existsSync(path.join(__dirname, "../vscode-extension", packageJson.main)));
|
||||
assert.deepStrictEqual(packageJson.dependencies || {}, {});
|
||||
assert.match(readme, /code --extensionDevelopmentPath "\$\(pwd\)\/vscode-extension"/);
|
||||
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-vscode-"));
|
||||
fs.mkdirSync(path.join(root, "envs/linux"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "envs/linux/Containerfile"), "FROM alpine\n");
|
||||
fs.mkdirSync(path.join(root, ".disasmer"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
extension.disasmerViewStatePath(root),
|
||||
JSON.stringify({
|
||||
nodes: [{ id: "node-linux", status: "online", capabilities: "Command RootlessPodman" }],
|
||||
processes: [{ id: "vp-build", status: "running", entry: "build" }],
|
||||
logs: [{ task: "compile-linux", message: "stdout=12 stderr=0", bytes: 12 }],
|
||||
artifacts: [{ path: "/vfs/artifacts/app.tar.zst", status: "retained", size: 12 }],
|
||||
inspector: [{ label: "debug", value: "attached" }]
|
||||
})
|
||||
);
|
||||
|
||||
const envs = extension.discoverEnvironmentNames(root);
|
||||
assert.deepStrictEqual(envs, ["linux"]);
|
||||
|
||||
const diagnostics = extension.diagnoseEnvReferences(
|
||||
'let _ = env!("linux"); let _ = env!("windows");',
|
||||
envs
|
||||
);
|
||||
assert.strictEqual(diagnostics.length, 1);
|
||||
assert.strictEqual(diagnostics[0].name, "windows");
|
||||
assert.match(diagnostics[0].message, /envs\/windows\/Containerfile/);
|
||||
|
||||
const inspectCommand = extension.bundleInspectCommand(root, "/repo");
|
||||
assert.strictEqual(inspectCommand.command, "cargo");
|
||||
assert.deepStrictEqual(inspectCommand.args.slice(0, 8), [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"bundle"
|
||||
]);
|
||||
assert(inspectCommand.args.includes("inspect"));
|
||||
assert(inspectCommand.args.includes("--project"));
|
||||
assert(inspectCommand.args.includes(root));
|
||||
assert(inspectCommand.args.includes("--json"));
|
||||
|
||||
const refreshed = extension.refreshBundleBeforeLaunch(root, "/repo", (command, args, options) => {
|
||||
assert.strictEqual(command, "cargo");
|
||||
assert(args.includes("bundle"));
|
||||
assert.strictEqual(options.cwd, "/repo");
|
||||
return {
|
||||
status: 0,
|
||||
stdout: JSON.stringify({ metadata: { identity: "sha256:abc" } }),
|
||||
stderr: ""
|
||||
};
|
||||
});
|
||||
assert.strictEqual(refreshed.metadata.identity, "sha256:abc");
|
||||
|
||||
const launch = extension.resolveDisasmerDebugConfiguration(
|
||||
{ uri: { fsPath: root } },
|
||||
{}
|
||||
);
|
||||
assert.deepStrictEqual(launch, {
|
||||
name: "Disasmer: Launch Virtual Process",
|
||||
type: "disasmer",
|
||||
request: "launch",
|
||||
entry: "build",
|
||||
project: root,
|
||||
runtimeBackend: "local-services"
|
||||
});
|
||||
assert.strictEqual(
|
||||
extension.disasmerProcessId("/workspace/app", "build"),
|
||||
"vp-e4bd6ef50539"
|
||||
);
|
||||
assert.strictEqual(
|
||||
extension.existingProcessRelationship(
|
||||
{ process: "vp-e4bd6ef50539", state: "running" },
|
||||
"vp-e4bd6ef50539"
|
||||
),
|
||||
"same_launch_target"
|
||||
);
|
||||
assert.strictEqual(
|
||||
extension.existingProcessRelationship(
|
||||
{ process: "vp-other", state: "running" },
|
||||
"vp-e4bd6ef50539"
|
||||
),
|
||||
"different_launch_target"
|
||||
);
|
||||
|
||||
const liveProcesses = extension.loadLiveProcesses(root, "/repo", (_command, args) => {
|
||||
assert.deepStrictEqual(args.slice(-3), ["process", "list", "--json"]);
|
||||
return {
|
||||
status: 0,
|
||||
stdout: JSON.stringify({
|
||||
coordinator: "https://disasmer.michelpaulissen.com",
|
||||
tenant: "tenant-live",
|
||||
project: "project-live",
|
||||
user: "user-live",
|
||||
processes: [{ process: "vp-live", state: "cancelling" }]
|
||||
}),
|
||||
stderr: ""
|
||||
};
|
||||
});
|
||||
assert.deepStrictEqual(liveProcesses.processes, [
|
||||
{ process: "vp-live", state: "cancelling" }
|
||||
]);
|
||||
assert.strictEqual(liveProcesses.project, "project-live");
|
||||
|
||||
const adapter = extension.debugAdapterExecutableSpec(root, repo);
|
||||
assert.strictEqual(adapter.command, "cargo");
|
||||
assert.deepStrictEqual(adapter.args, [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-dap",
|
||||
"--bin",
|
||||
"disasmer-debug-dap"
|
||||
]);
|
||||
assert.deepStrictEqual(adapter.options, { cwd: repo });
|
||||
|
||||
const releasedAdapterPath = path.join(root, process.platform === "win32" ? "disasmer-debug-dap.exe" : "disasmer-debug-dap");
|
||||
fs.writeFileSync(releasedAdapterPath, "");
|
||||
const releasedAdapter = extension.debugAdapterExecutableSpec(root, repo);
|
||||
assert.strictEqual(releasedAdapter.command, releasedAdapterPath);
|
||||
assert.deepStrictEqual(releasedAdapter.args, []);
|
||||
assert.deepStrictEqual(releasedAdapter.options, { cwd: root });
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
extension.refreshBundleBeforeLaunch(root, "/repo", () => ({
|
||||
status: 1,
|
||||
stdout: "",
|
||||
stderr: "missing environment linux"
|
||||
})),
|
||||
/missing environment linux/
|
||||
);
|
||||
|
||||
assert(
|
||||
packageJson.contributes.viewsContainers.activitybar.some(
|
||||
(container) => container.id === "disasmer" && container.title === "Disasmer"
|
||||
),
|
||||
"package.json must contribute a Disasmer activity-bar container"
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(path.join(__dirname, "../vscode-extension/resources/disasmer.svg")),
|
||||
"Disasmer activity-bar icon must exist"
|
||||
);
|
||||
const packageViewIds = packageJson.contributes.views.disasmer.map((view) => view.id).sort();
|
||||
const descriptorViewIds = extension.disasmerViewDescriptors().map((view) => view.id).sort();
|
||||
assert.deepStrictEqual(descriptorViewIds, [
|
||||
"disasmer.artifacts",
|
||||
"disasmer.inspector",
|
||||
"disasmer.logs",
|
||||
"disasmer.nodes",
|
||||
"disasmer.processes"
|
||||
]);
|
||||
assert.deepStrictEqual(packageViewIds, descriptorViewIds);
|
||||
for (const viewId of descriptorViewIds) {
|
||||
const items = extension.disasmerViewItems(
|
||||
extension.loadDisasmerViewState(root),
|
||||
viewId
|
||||
);
|
||||
assert(
|
||||
items.length > 0 && !items[0].label.startsWith("No "),
|
||||
`${viewId} should render state-backed items`
|
||||
);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
extension.disasmerViewItems(extension.loadDisasmerViewState(root), "disasmer.nodes")[0],
|
||||
{
|
||||
label: "node-linux",
|
||||
description: "online Command RootlessPodman"
|
||||
}
|
||||
);
|
||||
|
||||
assert(
|
||||
packageJson.contributes.debuggers.some((debuggerContribution) => debuggerContribution.type === "disasmer"),
|
||||
"package.json must contribute the disasmer debugger type"
|
||||
);
|
||||
for (const viewId of descriptorViewIds) {
|
||||
assert(
|
||||
packageJson.activationEvents.includes(`onView:${viewId}`),
|
||||
`${viewId} should activate the extension when opened`
|
||||
);
|
||||
}
|
||||
const launchProperties =
|
||||
packageJson.contributes.debuggers[0].configurationAttributes.launch.properties;
|
||||
assert.strictEqual(launchProperties.runtimeBackend.default, "local-services");
|
||||
assert(launchProperties.runtimeBackend.enum.includes("live-services"));
|
||||
assert.strictEqual(launchProperties.coordinatorEndpoint.default, undefined);
|
||||
assert.strictEqual(launchProperties.tenant, undefined);
|
||||
assert.strictEqual(launchProperties.projectId, undefined);
|
||||
assert.strictEqual(launchProperties.actorUser, undefined);
|
||||
assert(
|
||||
packageJson.contributes.debuggers[0].configurationAttributes.attach,
|
||||
"package.json must contribute an attach configuration"
|
||||
);
|
||||
for (const command of [
|
||||
"disasmer.refreshProcesses",
|
||||
"disasmer.process.attach",
|
||||
"disasmer.process.cancel",
|
||||
"disasmer.process.abort"
|
||||
]) {
|
||||
assert(
|
||||
packageJson.contributes.commands.some((entry) => entry.command === command),
|
||||
`${command} must be contributed`
|
||||
);
|
||||
}
|
||||
assert.match(extensionSource, /registerDebugAdapterDescriptorFactory\("disasmer"/);
|
||||
assert.match(extensionSource, /disasmer-debug-dap/);
|
||||
assert.match(extensionSource, /\.disasmer\/views\.json/);
|
||||
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
console.log("VS Code extension smoke passed");
|
||||
310
scripts/vscode-f5-smoke.js
Executable file
310
scripts/vscode-f5-smoke.js
Executable file
|
|
@ -0,0 +1,310 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const extension = require("../vscode-extension/extension");
|
||||
|
||||
class DapClient {
|
||||
constructor(spec) {
|
||||
this.child = cp.spawn(spec.command, spec.args, {
|
||||
cwd: spec.options && spec.options.cwd,
|
||||
env: spec.options && spec.options.env,
|
||||
detached: process.platform !== "win32"
|
||||
});
|
||||
this.seq = 1;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.messages = [];
|
||||
this.waiters = [];
|
||||
this.stderr = "";
|
||||
|
||||
this.child.stdout.on("data", (chunk) => {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
this.parse();
|
||||
});
|
||||
this.child.stderr.on("data", (chunk) => {
|
||||
this.stderr += chunk.toString();
|
||||
});
|
||||
this.child.on("exit", () => this.flushWaiters());
|
||||
}
|
||||
|
||||
send(command, args = {}) {
|
||||
const seq = this.seq++;
|
||||
const message = { seq, type: "request", command, arguments: args };
|
||||
const payload = Buffer.from(JSON.stringify(message));
|
||||
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
|
||||
this.child.stdin.write(payload);
|
||||
return seq;
|
||||
}
|
||||
|
||||
async response(seq, command) {
|
||||
const message = await this.waitFor(
|
||||
(item) =>
|
||||
item.type === "response" &&
|
||||
item.request_seq === seq &&
|
||||
item.command === command
|
||||
);
|
||||
if (!message.success) {
|
||||
throw new Error(
|
||||
`DAP ${command} failed: ${message.message || JSON.stringify(message)}\n${this.stderr}`
|
||||
);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
terminate() {
|
||||
if (this.child.exitCode !== null) return;
|
||||
if (process.platform === "win32") {
|
||||
this.child.kill("SIGKILL");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
process.kill(-this.child.pid, "SIGKILL");
|
||||
} catch (_) {
|
||||
this.child.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
|
||||
waitFor(predicate, timeoutMs = 240000) {
|
||||
const existing = this.messages.find(predicate);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.terminate();
|
||||
reject(new Error(`timed out waiting for DAP message\n${this.stderr}`));
|
||||
}, timeoutMs);
|
||||
this.waiters.push({ predicate, resolve, timer });
|
||||
});
|
||||
}
|
||||
|
||||
parse() {
|
||||
while (true) {
|
||||
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.buffer.slice(0, headerEnd).toString();
|
||||
const match = header.match(/Content-Length: (\d+)/i);
|
||||
if (!match) throw new Error(`bad DAP header: ${header}`);
|
||||
const length = Number(match[1]);
|
||||
const start = headerEnd + 4;
|
||||
const end = start + length;
|
||||
if (this.buffer.length < end) return;
|
||||
const payload = this.buffer.slice(start, end).toString();
|
||||
this.buffer = this.buffer.slice(end);
|
||||
this.messages.push(JSON.parse(payload));
|
||||
this.flushWaiters();
|
||||
}
|
||||
}
|
||||
|
||||
flushWaiters() {
|
||||
for (const waiter of [...this.waiters]) {
|
||||
const message = this.messages.find(waiter.predicate);
|
||||
if (!message) continue;
|
||||
clearTimeout(waiter.timer);
|
||||
this.waiters.splice(this.waiters.indexOf(waiter), 1);
|
||||
waiter.resolve(message);
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.child.exitCode !== null) return;
|
||||
const seq = this.send("disconnect");
|
||||
await this.response(seq, "disconnect");
|
||||
this.child.stdin.end();
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const project = path.join(repo, "examples/launch-build-demo");
|
||||
const buildSourceLines = fs
|
||||
.readFileSync(path.join(project, "src/build.rs"), "utf8")
|
||||
.split(/\r?\n/);
|
||||
const sourceLine = (needle) => {
|
||||
const index = buildSourceLines.findIndex((line) => line.includes(needle));
|
||||
assert(index >= 0, `flagship source must contain ${needle}`);
|
||||
return index + 1;
|
||||
};
|
||||
const buildMainLine = sourceLine("pub async fn build_main()");
|
||||
const launchConfig = extension.resolveDisasmerDebugConfiguration(
|
||||
{ uri: { fsPath: project } },
|
||||
{}
|
||||
);
|
||||
|
||||
assert.strictEqual(launchConfig.type, "disasmer");
|
||||
assert.strictEqual(launchConfig.request, "launch");
|
||||
assert.strictEqual(launchConfig.entry, "build");
|
||||
assert.strictEqual(launchConfig.project, project);
|
||||
assert.strictEqual(launchConfig.runtimeBackend, "local-services");
|
||||
|
||||
const inspection = extension.refreshBundleBeforeLaunch(project, repo);
|
||||
assert.match(inspection.metadata.identity, /^sha256:/);
|
||||
|
||||
// Keep the timed attach focused on the runtime boundary. A completely fresh
|
||||
// public checkout may otherwise spend most of that window compiling the
|
||||
// coordinator or node after the adapter has already started waiting.
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"build",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-node",
|
||||
],
|
||||
{ cwd: repo, stdio: "inherit" }
|
||||
);
|
||||
const executableSuffix = process.platform === "win32" ? ".exe" : "";
|
||||
const adapterSpec = extension.debugAdapterExecutableSpec(repo);
|
||||
adapterSpec.options = {
|
||||
...(adapterSpec.options || {}),
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_COORDINATOR_BIN: path.join(
|
||||
repo,
|
||||
"target",
|
||||
"debug",
|
||||
`disasmer-coordinator${executableSuffix}`
|
||||
),
|
||||
DISASMER_NODE_BIN: path.join(
|
||||
repo,
|
||||
"target",
|
||||
"debug",
|
||||
`disasmer-node${executableSuffix}`
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const client = new DapClient(adapterSpec);
|
||||
try {
|
||||
const initialize = client.send("initialize", {
|
||||
adapterID: "disasmer",
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true
|
||||
});
|
||||
await client.response(initialize, "initialize");
|
||||
|
||||
const launch = client.send("launch", launchConfig);
|
||||
await client.response(launch, "launch");
|
||||
await client.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
|
||||
const breakpoints = client.send("setBreakpoints", {
|
||||
source: { path: path.join(project, "src/build.rs") },
|
||||
breakpoints: [{ line: buildMainLine }]
|
||||
});
|
||||
const breakpointResponse = await client.response(breakpoints, "setBreakpoints");
|
||||
assert.strictEqual(breakpointResponse.body.breakpoints[0].verified, true);
|
||||
|
||||
const configurationDone = client.send("configurationDone");
|
||||
await client.response(configurationDone, "configurationDone");
|
||||
const stopped = await client.waitFor(
|
||||
(message) => message.type === "event" && message.event === "stopped"
|
||||
);
|
||||
assert.strictEqual(stopped.body.allThreadsStopped, true);
|
||||
assert.strictEqual(stopped.body.reason, "breakpoint");
|
||||
|
||||
const threadsRequest = client.send("threads");
|
||||
const threads = (await client.response(threadsRequest, "threads")).body.threads;
|
||||
const mainThread = threads.find((thread) => thread.name.includes("build virtual process"));
|
||||
assert(mainThread, "F5 launch must expose the real entrypoint as a virtual thread");
|
||||
|
||||
const stackRequest = client.send("stackTrace", {
|
||||
threadId: mainThread.id,
|
||||
startFrame: 0,
|
||||
levels: 1
|
||||
});
|
||||
const stack = (await client.response(stackRequest, "stackTrace")).body.stackFrames;
|
||||
assert.strictEqual(stack[0].line, buildMainLine);
|
||||
assert.strictEqual(stack[0].source.path, path.join(project, "src/build.rs"));
|
||||
assert.strictEqual(stack[0].source.sourceReference || 0, 0);
|
||||
|
||||
const sourceRequest = client.send("source", { source: stack[0].source });
|
||||
const source = (await client.response(sourceRequest, "source")).body;
|
||||
assert.match(source.content, /compile_linux/);
|
||||
|
||||
const scopesRequest = client.send("scopes", { frameId: stack[0].id });
|
||||
const scopes = (await client.response(scopesRequest, "scopes")).body.scopes;
|
||||
const localsScope = scopes.find((scope) => scope.name === "Source Locals");
|
||||
const argsScope = scopes.find((scope) => scope.name === "Task Args and Handles");
|
||||
const runtimeScope = scopes.find((scope) => scope.name === "Disasmer Runtime");
|
||||
const outputScope = scopes.find((scope) => scope.name === "Recent Output");
|
||||
assert(localsScope, "F5 launch must expose source locals scope");
|
||||
assert(argsScope, "F5 launch must expose task args and handles");
|
||||
assert(runtimeScope, "F5 launch must expose Disasmer runtime state");
|
||||
assert(outputScope, "F5 launch must expose recent output state");
|
||||
|
||||
const localsRequest = client.send("variables", {
|
||||
variablesReference: localsScope.variablesReference
|
||||
});
|
||||
const locals = (await client.response(localsRequest, "variables")).body.variables;
|
||||
assert(
|
||||
locals.some(
|
||||
(variable) =>
|
||||
variable.name === "unavailable-local-diagnostic" &&
|
||||
String(variable.value).includes("cannot be inspected")
|
||||
),
|
||||
"source locals scope must report unavailable real Rust locals explicitly"
|
||||
);
|
||||
|
||||
const runtimeRequest = client.send("variables", {
|
||||
variablesReference: runtimeScope.variablesReference
|
||||
});
|
||||
const runtime = (await client.response(runtimeRequest, "variables")).body.variables;
|
||||
assert(
|
||||
runtime.some(
|
||||
(variable) => variable.name === "runtime_backend" && variable.value === "LocalServices"
|
||||
),
|
||||
"extension-resolved F5 launch must use the real local-services backend"
|
||||
);
|
||||
assert(
|
||||
runtime.some(
|
||||
(variable) => variable.name === "coordinator_task_events" && variable.value === 0
|
||||
),
|
||||
"a task frozen at its entry probe must not fabricate a terminal task event"
|
||||
);
|
||||
assert(
|
||||
runtime.some(
|
||||
(variable) =>
|
||||
variable.name === "command_status" &&
|
||||
String(variable.value).includes(
|
||||
"frozen through local services at executing Wasm probe"
|
||||
)
|
||||
)
|
||||
);
|
||||
assert(
|
||||
runtime.some(
|
||||
(variable) => variable.name === "state" && variable.value === "Frozen"
|
||||
),
|
||||
"F5 must expose the node-acknowledged frozen participant state"
|
||||
);
|
||||
assert(runtime.some((variable) => variable.name === "command_spec"));
|
||||
assert(runtime.some((variable) => variable.name === "stdout_tail"));
|
||||
assert(runtime.some((variable) => variable.name === "stderr_tail"));
|
||||
|
||||
const outputRequest = client.send("variables", {
|
||||
variablesReference: outputScope.variablesReference
|
||||
});
|
||||
const output = (await client.response(outputRequest, "variables")).body.variables;
|
||||
assert(output.some((variable) => variable.name === "stdout_tail"));
|
||||
assert(output.some((variable) => variable.name === "stderr_tail"));
|
||||
|
||||
await client.close();
|
||||
} catch (error) {
|
||||
client.terminate();
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log("VS Code F5 smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
1313
scripts/wasmtime-assignment-smoke.js
Normal file
1313
scripts/wasmtime-assignment-smoke.js
Normal file
File diff suppressed because it is too large
Load diff
172
scripts/wasmtime-node-smoke.js
Normal file
172
scripts/wasmtime-node-smoke.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const wasmTarget = path.join(
|
||||
repo,
|
||||
"target",
|
||||
"wasm32-unknown-unknown",
|
||||
"release",
|
||||
"launch_build_demo.wasm"
|
||||
);
|
||||
|
||||
cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"build",
|
||||
"--release",
|
||||
"-p",
|
||||
"launch-build-demo",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: "z",
|
||||
CARGO_PROFILE_RELEASE_LTO: "thin",
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert(fs.existsSync(wasmTarget), `missing compiled Wasm module at ${wasmTarget}`);
|
||||
|
||||
const output = cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-wasmtime-smoke",
|
||||
"--",
|
||||
wasmTarget,
|
||||
"task_add_one",
|
||||
"41",
|
||||
"42",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
|
||||
const report = JSON.parse(output);
|
||||
assert.strictEqual(report.type, "wasmtime_task_smoke");
|
||||
assert.strictEqual(report.export, "task_add_one");
|
||||
assert.strictEqual(report.arg, 41);
|
||||
assert.strictEqual(report.result, 42);
|
||||
|
||||
const debugOutput = cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-wasmtime-smoke",
|
||||
"--",
|
||||
"--debug-freeze-resume",
|
||||
wasmTarget,
|
||||
"task_add_one",
|
||||
"41",
|
||||
"42",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
const debugReport = JSON.parse(debugOutput);
|
||||
assert.strictEqual(debugReport.type, "wasmtime_debug_freeze_resume_smoke");
|
||||
assert.strictEqual(debugReport.export, "task_add_one");
|
||||
assert.strictEqual(debugReport.task, "task_add_one");
|
||||
assert.strictEqual(debugReport.frozen_state, "Frozen");
|
||||
assert.strictEqual(debugReport.resumed_state, "Running");
|
||||
assert(debugReport.stack_frames.some((frame) => String(frame).includes("task_add_one")));
|
||||
assert(
|
||||
debugReport.local_values.some(
|
||||
([name, value]) => name === "wasm_local_0" && String(value).includes("41")
|
||||
),
|
||||
"Wasmtime debug snapshot must expose the real i32 argument as a frame local"
|
||||
);
|
||||
assert.strictEqual(debugReport.node_runtime_captured_wasm_locals, true);
|
||||
assert.strictEqual(debugReport.result, 42);
|
||||
assert.strictEqual(debugReport.node_runtime_reached_wasm_task, true);
|
||||
|
||||
const hostCommandOutput = cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-wasmtime-smoke",
|
||||
"--",
|
||||
"--host-command",
|
||||
wasmTarget,
|
||||
"compile_linux",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
const hostCommandReport = JSON.parse(hostCommandOutput);
|
||||
assert.strictEqual(hostCommandReport.type, "wasmtime_host_command_smoke");
|
||||
assert.strictEqual(hostCommandReport.task, "compile_linux");
|
||||
assert.match(hostCommandReport.export, /^disasmer_task_v1_[0-9a-f]{64}$/);
|
||||
assert.strictEqual(hostCommandReport.program, "cc");
|
||||
assert.deepStrictEqual(hostCommandReport.args, [
|
||||
"-Os",
|
||||
"-static",
|
||||
"-s",
|
||||
"fixture/hello-disasmer.c",
|
||||
"-o",
|
||||
"/disasmer/output/hello-disasmer",
|
||||
]);
|
||||
assert.strictEqual(hostCommandReport.working_directory, "/workspace");
|
||||
assert.deepStrictEqual(hostCommandReport.environment_variables, {
|
||||
SOURCE_DATE_EPOCH: "0",
|
||||
});
|
||||
assert.strictEqual(hostCommandReport.timeout_ms, 180000);
|
||||
assert.strictEqual(hostCommandReport.network, "disabled");
|
||||
assert.strictEqual(hostCommandReport.status_code, 0);
|
||||
assert.strictEqual(hostCommandReport.stdout, "");
|
||||
assert.strictEqual(hostCommandReport.artifact_name, "hello-disasmer");
|
||||
assert.strictEqual(hostCommandReport.artifact_size_bytes, "hello-disasmer".length);
|
||||
assert.match(hostCommandReport.artifact_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.strictEqual(hostCommandReport.node_host_import, "disasmer.command_run_v1");
|
||||
assert.strictEqual(hostCommandReport.artifact_host_import, "disasmer.vfs_operation_v1");
|
||||
assert.strictEqual(hostCommandReport.flagship_linux_build_task, true);
|
||||
assert.strictEqual(hostCommandReport.node_executed_host_command, true);
|
||||
assert.strictEqual(hostCommandReport.hosted_control_plane_ran_command, false);
|
||||
|
||||
const artifactOutput = cp.execFileSync(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-wasmtime-smoke",
|
||||
"--",
|
||||
"--task-artifact",
|
||||
wasmTarget,
|
||||
"package_release",
|
||||
],
|
||||
{ cwd: repo, encoding: "utf8" }
|
||||
);
|
||||
const artifactReport = JSON.parse(artifactOutput);
|
||||
assert.strictEqual(artifactReport.type, "wasmtime_task_artifact_smoke");
|
||||
assert.strictEqual(artifactReport.task, "package_release");
|
||||
assert.strictEqual(artifactReport.artifact_name, "release.tar");
|
||||
assert.strictEqual(artifactReport.artifact_size_bytes, "release.tar".length);
|
||||
assert.match(artifactReport.artifact_digest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.strictEqual(artifactReport.host_import, "disasmer.vfs_operation_v1");
|
||||
assert.strictEqual(artifactReport.host_issued_handle_returned, true);
|
||||
assert.ok(artifactReport.artifact.id.endsWith(artifactReport.artifact_digest.slice("sha256:".length)));
|
||||
|
||||
console.log("Wasmtime node smoke passed");
|
||||
105
scripts/website-inventory-contract-smoke.js
Normal file
105
scripts/website-inventory-contract-smoke.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const inventoryPath = path.join(repo, "website_mvp_inventory.md");
|
||||
if (!fs.existsSync(inventoryPath)) {
|
||||
if (fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json"))) {
|
||||
console.log(
|
||||
"Website inventory contract smoke skipped: website_mvp_inventory.md is filtered from this public tree"
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error("website_mvp_inventory.md is missing");
|
||||
}
|
||||
const source = fs.readFileSync(inventoryPath, "utf8");
|
||||
|
||||
function expect(name, pattern) {
|
||||
assert.match(source, pattern, `missing website inventory evidence: ${name}`);
|
||||
}
|
||||
|
||||
function criterionLines() {
|
||||
return source
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => /^- \[[ x]\] \*\*/.test(line));
|
||||
}
|
||||
|
||||
expect("header", /^# Disasmer MVP Website Acceptance Criteria/m);
|
||||
expect(
|
||||
"acceptance-style status",
|
||||
/\*\*Status:\*\* (?:private )?hosted website addendum to `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/
|
||||
);
|
||||
expect(
|
||||
"hosted signup exception",
|
||||
/except hosted Authentik account creation/
|
||||
);
|
||||
expect(
|
||||
"no duplicate work note",
|
||||
/does not automatically mean new product code, a new feature, or even actual implementation work is required/
|
||||
);
|
||||
expect(
|
||||
"prove existing behavior before implementation",
|
||||
/prefer proving, documenting, or tightening that existing behavior over duplicating capability or overengineering a parallel website mechanism/
|
||||
);
|
||||
expect(
|
||||
"barebones no CSS",
|
||||
/barebones functional HTML with no CSS/
|
||||
);
|
||||
expect(
|
||||
"future hosted business scope",
|
||||
/Billing is not part of the MVP[\s\S]*Paid checkout, paid-plan management, upgrade flows, hosted support tooling, a full hosted admin console, broad moderation workflows/
|
||||
);
|
||||
expect(
|
||||
"billing is not MVP website work",
|
||||
/Do not build billing\/upgrade flows into the minimal website for this MVP/
|
||||
);
|
||||
expect(
|
||||
"billing-only surfaces are postponed",
|
||||
/If a website route, API, database field, control, or acceptance item exists only for billing, paid plans, hosted business operations, broad admin\/moderation, team management, provider setup, secret management, durable account\/business-process management, or any similarly future hosted-business concern, postpone it instead of building it for this MVP\./
|
||||
);
|
||||
expect(
|
||||
"billing plan flags are placeholders",
|
||||
/Design-document references to billing, paid plans, or plan flags are future metadata placeholders; they do not require MVP website routes, database fields, UI controls, CLI commands, coordinator routes, or service logic/
|
||||
);
|
||||
assert.doesNotMatch(source, /community-tier/, "use `community tier`, not `community-tier`");
|
||||
|
||||
const lines = criterionLines();
|
||||
assert(lines.length > 0, "website inventory must contain status-prefixed checklist items");
|
||||
for (const line of lines) {
|
||||
assert.match(
|
||||
line,
|
||||
/^- \[[ x]\] \*\*(Passed|Partial|Open|Postponed)(?: \([^)]+\))?:\*\*/,
|
||||
`website inventory item lacks an explicit status prefix: ${line}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["CLI login command", /`disasmer login \[--browser\] \[--coordinator <url>\]`/],
|
||||
["CLI auth status command", /`disasmer auth status`/],
|
||||
["CLI key command", /`disasmer key add --public-key <pubkey>`/],
|
||||
["CLI project init command", /`disasmer project init`/],
|
||||
["CLI node enroll command", /`disasmer node enroll --coordinator <url> --ttl-seconds <seconds>`/],
|
||||
["CLI process status command", /`disasmer process status`/],
|
||||
["CLI task list command", /`disasmer task list`/],
|
||||
["CLI DAP command", /`disasmer dap`/],
|
||||
["CLI quota command", /`disasmer quota status`/],
|
||||
["CLI admin status command", /`disasmer admin status`/],
|
||||
]) {
|
||||
expect(name, pattern);
|
||||
}
|
||||
|
||||
for (const [name, pattern] of [
|
||||
["website default project is open", /- \[ \] \*\*Open:\*\* land in their default project\./],
|
||||
["website node attach is open", /- \[ \] \*\*Open:\*\* copy a node attach command\./],
|
||||
["website process view is open", /- \[ \] \*\*Open:\*\* see the single current virtual process\./],
|
||||
["website logs are open", /- \[ \] \*\*Open:\*\* see recent bounded logs\./],
|
||||
["website artifact download is open", /- \[ \] \*\*Open:\*\* securely download an available best-effort retained artifact\./],
|
||||
["website keys are partial through CLI", /- \[ \] \*\*Partial:\*\* manage\/revoke public keys or know the CLI command to do so\./],
|
||||
]) {
|
||||
expect(name, pattern);
|
||||
}
|
||||
|
||||
console.log("Website inventory contract smoke passed");
|
||||
302
scripts/windows-best-effort-smoke.js
Executable file
302
scripts/windows-best-effort-smoke.js
Executable file
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const digest = (value) =>
|
||||
`sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
||||
const environmentDigest = digest("windows-command-dev-environment");
|
||||
const sourceDigest = digest("windows-best-effort-source");
|
||||
const windowsArtifactBytes = Buffer.from("windows-output-data");
|
||||
const windowsArtifactDigest = digest(windowsArtifactBytes);
|
||||
const emptyWasm = Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
|
||||
const emptyWasmDigest = digest(emptyWasm);
|
||||
const nodeDaemon = fs.readFileSync(path.join(repo, "crates/disasmer-node/src/daemon.rs"), "utf8");
|
||||
const taskReports = fs.readFileSync(
|
||||
path.join(repo, "crates/disasmer-node/src/task_reports.rs"),
|
||||
"utf8"
|
||||
);
|
||||
const nodeLib = fs.readFileSync(path.join(repo, "crates/disasmer-node/src/lib.rs"), "utf8");
|
||||
const windowsDev = fs.readFileSync(
|
||||
path.join(repo, "crates/disasmer-node/src/windows_dev.rs"),
|
||||
"utf8"
|
||||
);
|
||||
const executionCore = fs.readFileSync(
|
||||
path.join(repo, "crates/disasmer-core/src/execution.rs"),
|
||||
"utf8"
|
||||
);
|
||||
const dapSource = [
|
||||
fs.readFileSync(path.join(repo, "crates/disasmer-dap/src/demo_backend.rs"), "utf8"),
|
||||
fs.readFileSync(path.join(repo, "crates/disasmer-dap/src/tests.rs"), "utf8"),
|
||||
].join("\n");
|
||||
const windowsNode = "windows-node";
|
||||
const windowsIdentity = nodeIdentity("windows-best-effort-smoke", windowsNode);
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function windowsCapabilities() {
|
||||
return {
|
||||
os: "Windows",
|
||||
arch: "x86_64",
|
||||
capabilities: ["Command", "VfsArtifacts", "WindowsCommandDev"],
|
||||
environment_backends: ["WindowsCommandDev"],
|
||||
source_providers: ["filesystem"]
|
||||
};
|
||||
}
|
||||
|
||||
function assertWindowsBackendBoundary() {
|
||||
assert.match(nodeDaemon, /CoordinatorSession::connect\(&args\.coordinator\)/);
|
||||
assert.match(nodeDaemon, /record_completed_task\(/);
|
||||
assert.match(taskReports, /"type": "task_completed"/);
|
||||
assert.doesNotMatch(nodeDaemon, /WindowsCommandDev|windows-command-dev|cfg\(windows\)/);
|
||||
|
||||
assert.match(nodeLib, /impl CommandBackend for LinuxRootlessPodmanBackend/);
|
||||
assert.match(nodeLib, /mod windows_dev/);
|
||||
assert.match(nodeLib, /pub use windows_dev::\{WindowsCommandDevBackend, WindowsSandboxStubBackend\}/);
|
||||
assert.match(windowsDev, /impl CommandBackend for WindowsCommandDevBackend/);
|
||||
assert.match(windowsDev, /impl CommandBackend for WindowsSandboxStubBackend/);
|
||||
assert.doesNotMatch(windowsDev, /LinuxRootlessPodmanBackend/);
|
||||
assert.match(executionCore, /\bLinuxRootlessPodman\b/);
|
||||
assert.match(executionCore, /\bWindowsCommandDev\b/);
|
||||
assert.match(executionCore, /\bStubbedWindowsSandbox\b/);
|
||||
|
||||
assert.match(dapSource, /thread\(WINDOWS_THREAD, "compile-windows", "compile windows"/);
|
||||
assert.match(dapSource, /fn launch_threads_include_windows_task_in_same_virtual_process\(\)/);
|
||||
assert.match(dapSource, /fn windows_thread_runtime_variables_share_virtual_process\(\)/);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
assertWindowsBackendBoundary();
|
||||
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
let coordinatorStderr = "";
|
||||
coordinator.stderr.on("data", (chunk) => {
|
||||
coordinatorStderr += chunk.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const attached = await send(addr, {
|
||||
type: "attach_node",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: windowsNode,
|
||||
public_key: windowsIdentity.publicKey
|
||||
});
|
||||
assert.strictEqual(attached.type, "node_attached");
|
||||
assert.strictEqual(attached.node, windowsNode);
|
||||
|
||||
const recorded = await send(addr, signedNodeRequest(windowsNode, windowsIdentity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: windowsNode,
|
||||
capabilities: windowsCapabilities(),
|
||||
cached_environment_digests: [environmentDigest],
|
||||
dependency_cache_digests: [],
|
||||
source_snapshots: [sourceDigest],
|
||||
artifact_locations: [],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(recorded.type, "node_capabilities_recorded");
|
||||
assert.strictEqual(recorded.node, windowsNode);
|
||||
|
||||
const placement = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: {
|
||||
os: "Windows",
|
||||
arch: null,
|
||||
capabilities: ["WindowsCommandDev"]
|
||||
},
|
||||
environment_digest: environmentDigest,
|
||||
required_capabilities: ["Command"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: sourceDigest,
|
||||
required_artifacts: [],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(placement.type, "task_placement");
|
||||
assert.strictEqual(placement.placement.node, windowsNode);
|
||||
assert.ok(placement.placement.reasons.includes("warm environment cache"));
|
||||
assert.ok(placement.placement.reasons.includes("source snapshot already local"));
|
||||
|
||||
const started = await send(addr, {
|
||||
type: "start_process",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process: "vp-windows"
|
||||
});
|
||||
assert.strictEqual(started.type, "process_started");
|
||||
assert.strictEqual(started.process, "vp-windows");
|
||||
|
||||
const reconnected = await send(addr, signedNodeRequest(windowsNode, windowsIdentity, "reconnect_node", {
|
||||
type: "reconnect_node",
|
||||
node: windowsNode,
|
||||
process: "vp-windows",
|
||||
epoch: started.epoch
|
||||
}));
|
||||
assert.strictEqual(reconnected.type, "node_reconnected");
|
||||
|
||||
const launched = await send(addr, {
|
||||
type: "launch_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
task_spec: {
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process: "vp-windows",
|
||||
task_definition: "windows-command-dev",
|
||||
task_instance: "windows-command-dev",
|
||||
dispatch: {
|
||||
kind: "coordinator_node_wasm",
|
||||
export: "windows_command_dev",
|
||||
abi: "task_v1",
|
||||
},
|
||||
environment_id: "windows",
|
||||
environment: {
|
||||
os: "Windows",
|
||||
arch: null,
|
||||
capabilities: ["WindowsCommandDev"],
|
||||
},
|
||||
environment_digest: environmentDigest,
|
||||
required_capabilities: ["Command", "WindowsCommandDev"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: sourceDigest,
|
||||
required_artifacts: [],
|
||||
args: [],
|
||||
vfs_epoch: started.epoch,
|
||||
bundle_digest: emptyWasmDigest,
|
||||
},
|
||||
wait_for_node: false,
|
||||
artifact_path: "/vfs/artifacts/windows-output.txt",
|
||||
wasm_module_base64: emptyWasm.toString("base64"),
|
||||
});
|
||||
assert.strictEqual(launched.type, "task_launched", JSON.stringify(launched));
|
||||
assert.strictEqual(launched.placement.node, windowsNode);
|
||||
|
||||
const recordedTask = await send(addr, signedNodeRequest(windowsNode, windowsIdentity, "task_completed", {
|
||||
type: "task_completed",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
process: "vp-windows",
|
||||
node: windowsNode,
|
||||
task: "windows-command-dev",
|
||||
status_code: 0,
|
||||
stdout_bytes: 18,
|
||||
stderr_bytes: 0,
|
||||
stdout_tail: "",
|
||||
stderr_tail: "",
|
||||
stdout_truncated: false,
|
||||
stderr_truncated: false,
|
||||
artifact_path: "/vfs/artifacts/windows-output.txt",
|
||||
artifact_digest: windowsArtifactDigest,
|
||||
artifact_size_bytes: windowsArtifactBytes.length
|
||||
}));
|
||||
assert.strictEqual(recordedTask.type, "task_recorded");
|
||||
assert.strictEqual(recordedTask.task, "windows-command-dev");
|
||||
|
||||
const events = await send(addr, {
|
||||
type: "list_task_events",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
process: "vp-windows"
|
||||
});
|
||||
assert.strictEqual(events.type, "task_events");
|
||||
assert.strictEqual(events.events.length, 1);
|
||||
assert.strictEqual(events.events[0].node, "windows-node");
|
||||
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/windows-output.txt");
|
||||
|
||||
const link = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact: "windows-output.txt",
|
||||
max_bytes: 1024
|
||||
});
|
||||
assert.strictEqual(link.type, "artifact_download_link");
|
||||
assert.strictEqual(link.link.process, "vp-windows");
|
||||
assert.deepStrictEqual(link.link.source, { RetainedNode: "windows-node" });
|
||||
} catch (error) {
|
||||
if (coordinatorStderr) {
|
||||
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
console.log("Windows best-effort smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
479
scripts/windows-runner-smoke.js
Executable file
479
scripts/windows-runner-smoke.js
Executable file
|
|
@ -0,0 +1,479 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
const { coordinatorWireRequest } = require("./coordinator-wire");
|
||||
const { nodeIdentity, signedNodeRequest } = require("./node-signing");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const forgejoWindowsNode = "forgejo-windows-node";
|
||||
const forgejoWindowsIdentity = nodeIdentity("windows-runner-smoke", forgejoWindowsNode);
|
||||
const windowsEnvironmentDigest = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update("disasmer/windows-command-dev/v1")
|
||||
.digest("hex")}`;
|
||||
const sourceSnapshot = `sha256:${crypto
|
||||
.createHash("sha256")
|
||||
.update("disasmer/windows-runner/source/v1")
|
||||
.digest("hex")}`;
|
||||
|
||||
class DapClient {
|
||||
constructor() {
|
||||
this.child = cp.spawn(
|
||||
"cargo",
|
||||
["run", "-q", "-p", "disasmer-dap", "--bin", "disasmer-debug-dap"],
|
||||
{ cwd: repo }
|
||||
);
|
||||
this.seq = 1;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.messages = [];
|
||||
this.waiters = [];
|
||||
this.stderr = "";
|
||||
|
||||
this.child.stdout.on("data", (chunk) => {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
this.parse();
|
||||
});
|
||||
this.child.stderr.on("data", (chunk) => {
|
||||
this.stderr += chunk.toString();
|
||||
});
|
||||
this.child.on("exit", () => this.flushWaiters());
|
||||
}
|
||||
|
||||
send(command, args = {}) {
|
||||
const seq = this.seq++;
|
||||
const message = { seq, type: "request", command, arguments: args };
|
||||
const payload = Buffer.from(JSON.stringify(coordinatorWireRequest(message)));
|
||||
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
|
||||
this.child.stdin.write(payload);
|
||||
return seq;
|
||||
}
|
||||
|
||||
async response(seq, command) {
|
||||
const message = await this.waitFor(
|
||||
(item) =>
|
||||
item.type === "response" &&
|
||||
item.request_seq === seq &&
|
||||
item.command === command
|
||||
);
|
||||
if (!message.success) {
|
||||
throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(coordinatorWireRequest(message))}`);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
waitFor(predicate, timeoutMs = 120000) {
|
||||
const existing = this.messages.find(predicate);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.child.kill("SIGKILL");
|
||||
reject(new Error(`timed out waiting for DAP message\n${this.stderr}`));
|
||||
}, timeoutMs);
|
||||
this.waiters.push({ predicate, resolve, timer });
|
||||
});
|
||||
}
|
||||
|
||||
parse() {
|
||||
while (true) {
|
||||
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.buffer.slice(0, headerEnd).toString();
|
||||
const match = header.match(/Content-Length: (\d+)/i);
|
||||
if (!match) throw new Error(`bad DAP header: ${header}`);
|
||||
const length = Number(match[1]);
|
||||
const start = headerEnd + 4;
|
||||
const end = start + length;
|
||||
if (this.buffer.length < end) return;
|
||||
const payload = this.buffer.slice(start, end).toString();
|
||||
this.buffer = this.buffer.slice(end);
|
||||
this.messages.push(JSON.parse(payload));
|
||||
this.flushWaiters();
|
||||
}
|
||||
}
|
||||
|
||||
flushWaiters() {
|
||||
for (const waiter of [...this.waiters]) {
|
||||
const message = this.messages.find(waiter.predicate);
|
||||
if (!message) continue;
|
||||
clearTimeout(waiter.timer);
|
||||
this.waiters.splice(this.waiters.indexOf(waiter), 1);
|
||||
waiter.resolve(message);
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.child.exitCode !== null) return;
|
||||
const seq = this.send("disconnect");
|
||||
await this.response(seq, "disconnect");
|
||||
this.child.stdin.end();
|
||||
}
|
||||
}
|
||||
|
||||
function waitForJsonLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
reject(new Error(`process exited before JSON line with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function runWindowsAttach(addr, grant) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"node",
|
||||
"attach",
|
||||
"--coordinator",
|
||||
`${addr.host}:${addr.port}`,
|
||||
"--tenant",
|
||||
"tenant",
|
||||
"--project-id",
|
||||
"project",
|
||||
"--node",
|
||||
forgejoWindowsNode,
|
||||
"--public-key",
|
||||
forgejoWindowsIdentity.publicKey,
|
||||
"--enrollment-grant",
|
||||
grant,
|
||||
"--cap",
|
||||
"windows-command-dev",
|
||||
"--json"
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_NODE_PRIVATE_KEY: forgejoWindowsIdentity.privateKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Windows node attach failed with code ${code}\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(`Windows node attach output was not JSON: ${stdout}\n${error.stack || error.message}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runWindowsNode(addr, grant) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-node",
|
||||
"--bin",
|
||||
"disasmer-node",
|
||||
"--",
|
||||
"--coordinator",
|
||||
`${addr.host}:${addr.port}`,
|
||||
"--tenant",
|
||||
"tenant",
|
||||
"--project-id",
|
||||
"project",
|
||||
"--node",
|
||||
forgejoWindowsNode,
|
||||
"--public-key",
|
||||
forgejoWindowsIdentity.publicKey,
|
||||
"--enrollment-grant",
|
||||
grant,
|
||||
"--process",
|
||||
"vp-forgejo-windows",
|
||||
"--task",
|
||||
"windows-command-dev",
|
||||
"--command",
|
||||
"cmd",
|
||||
"--arg",
|
||||
"/C",
|
||||
"--arg",
|
||||
"echo disasmer-windows-runner",
|
||||
"--artifact",
|
||||
"/vfs/artifacts/windows-runner-output.txt"
|
||||
],
|
||||
{
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
DISASMER_NODE_PRIVATE_KEY: forgejoWindowsIdentity.privateKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Windows node smoke failed with code ${code}\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1)));
|
||||
} catch (error) {
|
||||
reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function assertDebuggerShowsWindowsThread() {
|
||||
const client = new DapClient();
|
||||
try {
|
||||
const initialize = client.send("initialize", {
|
||||
adapterID: "disasmer",
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true
|
||||
});
|
||||
await client.response(initialize, "initialize");
|
||||
|
||||
const launch = client.send("launch", {
|
||||
entry: "build",
|
||||
project: path.join(repo, "examples/launch-build-demo"),
|
||||
runtimeBackend: "simulated"
|
||||
});
|
||||
await client.response(launch, "launch");
|
||||
await client.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
|
||||
const configurationDone = client.send("configurationDone");
|
||||
await client.response(configurationDone, "configurationDone");
|
||||
|
||||
const threadsRequest = client.send("threads");
|
||||
const threads = (await client.response(threadsRequest, "threads")).body.threads;
|
||||
assert(
|
||||
threads.some((thread) => thread.name.includes("compile windows")),
|
||||
"debugger must represent the Windows task as a virtual thread"
|
||||
);
|
||||
|
||||
await client.close();
|
||||
} catch (error) {
|
||||
client.child.kill("SIGKILL");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (process.platform !== "win32") {
|
||||
throw new Error(
|
||||
`Windows runner validation requires win32; current platform is ${process.platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const coordinator = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-coordinator",
|
||||
"--bin",
|
||||
"disasmer-coordinator",
|
||||
"--",
|
||||
"--listen",
|
||||
"127.0.0.1:0",
|
||||
"--allow-local-trusted-loopback"
|
||||
],
|
||||
{ cwd: repo }
|
||||
);
|
||||
let coordinatorStderr = "";
|
||||
coordinator.stderr.on("data", (chunk) => {
|
||||
coordinatorStderr += chunk.toString();
|
||||
});
|
||||
|
||||
try {
|
||||
const ready = await waitForJsonLine(coordinator);
|
||||
const [host, portText] = ready.listen.split(":");
|
||||
const addr = { host, port: Number(portText) };
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const attachGrant = await send(addr, {
|
||||
type: "create_node_enrollment_grant",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
ttl_seconds: 900
|
||||
});
|
||||
assert.strictEqual(attachGrant.type, "node_enrollment_grant_created");
|
||||
assert.strictEqual(attachGrant.scope, "node:attach");
|
||||
|
||||
const attach = await runWindowsAttach(addr, attachGrant.grant);
|
||||
assert.strictEqual(attach.plan.node, forgejoWindowsNode);
|
||||
assert.strictEqual(attach.boundary.cli_contacted_coordinator, true);
|
||||
assert.strictEqual(attach.boundary.used_enrollment_exchange, true);
|
||||
assert.strictEqual(attach.coordinator_response.type, "node_enrollment_exchanged");
|
||||
assert.strictEqual(attach.coordinator_response.credential.scope, "node:attach");
|
||||
assert(attach.plan.capabilities.capabilities.includes("WindowsCommandDev"));
|
||||
|
||||
const runtimeGrant = await send(addr, {
|
||||
type: "create_node_enrollment_grant",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "operator",
|
||||
ttl_seconds: 900
|
||||
});
|
||||
assert.strictEqual(runtimeGrant.type, "node_enrollment_grant_created");
|
||||
|
||||
const report = await runWindowsNode(addr, runtimeGrant.grant);
|
||||
assert.strictEqual(report.node_status, "completed");
|
||||
assert.strictEqual(report.virtual_thread, "windows-command-dev");
|
||||
assert.strictEqual(report.status_code, 0);
|
||||
assert.strictEqual(report.large_bytes_uploaded, false);
|
||||
assert.strictEqual(
|
||||
report.staged_artifact.path,
|
||||
"/vfs/artifacts/windows-runner-output.txt"
|
||||
);
|
||||
assert.strictEqual(report.registration_response.type, "node_enrollment_exchanged");
|
||||
assert.strictEqual(report.registration_response.credential.scope, "node:attach");
|
||||
assert.strictEqual(report.coordinator_response.type, "task_recorded");
|
||||
|
||||
const recorded = await send(addr, signedNodeRequest(forgejoWindowsNode, forgejoWindowsIdentity, "report_node_capabilities", {
|
||||
type: "report_node_capabilities",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
node: forgejoWindowsNode,
|
||||
capabilities: {
|
||||
os: "Windows",
|
||||
arch: "x86_64",
|
||||
capabilities: ["Command", "WindowsCommandDev", "VfsArtifacts"],
|
||||
environment_backends: ["WindowsCommandDev"],
|
||||
source_providers: ["filesystem"]
|
||||
},
|
||||
cached_environment_digests: [windowsEnvironmentDigest],
|
||||
source_snapshots: [sourceSnapshot],
|
||||
artifact_locations: ["windows-runner-output.txt"],
|
||||
direct_connectivity: true,
|
||||
online: true
|
||||
}));
|
||||
assert.strictEqual(recorded.type, "node_capabilities_recorded");
|
||||
|
||||
const placement = await send(addr, {
|
||||
type: "schedule_task",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
environment: {
|
||||
os: "Windows",
|
||||
arch: null,
|
||||
capabilities: ["WindowsCommandDev"]
|
||||
},
|
||||
environment_digest: windowsEnvironmentDigest,
|
||||
required_capabilities: ["Command"],
|
||||
source_snapshot: sourceSnapshot,
|
||||
required_artifacts: ["windows-runner-output.txt"],
|
||||
prefer_node: null
|
||||
});
|
||||
assert.strictEqual(placement.type, "task_placement");
|
||||
assert.strictEqual(placement.placement.node, "forgejo-windows-node");
|
||||
|
||||
const link = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_user: "user",
|
||||
artifact: "windows-runner-output.txt",
|
||||
max_bytes: 1024
|
||||
});
|
||||
assert.strictEqual(link.type, "artifact_download_link");
|
||||
assert.deepStrictEqual(link.link.source, {
|
||||
RetainedNode: "forgejo-windows-node"
|
||||
});
|
||||
} catch (error) {
|
||||
if (coordinatorStderr) {
|
||||
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
coordinator.kill("SIGTERM");
|
||||
}
|
||||
|
||||
await assertDebuggerShowsWindowsThread();
|
||||
|
||||
const outDir = path.join(repo, "target", "acceptance");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, "windows-runner.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
kind: "disasmer_windows_runner_validation",
|
||||
platform: process.platform,
|
||||
runner: process.env.FORGEJO_RUNNER_NAME || process.env.RUNNER_NAME || null,
|
||||
validated: true
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
|
||||
console.log("Windows runner smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
59
scripts/windows-validation-contract-smoke.js
Executable file
59
scripts/windows-validation-contract-smoke.js
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function expect(source, name, pattern) {
|
||||
assert.match(source, pattern, `missing Windows validation evidence: ${name}`);
|
||||
}
|
||||
|
||||
const workflow = read(".forgejo/workflows/windows-validation.yml");
|
||||
const runnerSmoke = read("scripts/windows-runner-smoke.js");
|
||||
const readme = read("README.md");
|
||||
const publicAcceptance = read("scripts/acceptance-public.sh");
|
||||
const publicSplit = read("scripts/verify-public-split.sh");
|
||||
|
||||
expect(workflow, "manual workflow dispatch", /workflow_dispatch/);
|
||||
expect(workflow, "intermittent Forgejo Windows runner note", /intermittently online/);
|
||||
expect(workflow, "Windows runner label", /runs-on:\s*windows/);
|
||||
expect(workflow, "Windows validation acceptance report env", /DISASMER_WINDOWS_VALIDATION:\s*forgejo-windows-runner/);
|
||||
expect(workflow, "acceptance report step", /node scripts\/acceptance-report\.js windows/);
|
||||
expect(workflow, "Windows runtime unit coverage", /cargo test -p disasmer-node windows_backend_is_labeled_user_attached_dev_execution/);
|
||||
expect(workflow, "Windows runner smoke step", /node scripts\/windows-runner-smoke\.js/);
|
||||
|
||||
expect(runnerSmoke, "real Windows platform guard", /process\.platform !== "win32"/);
|
||||
expect(runnerSmoke, "CLI node attach function", /function runWindowsAttach/);
|
||||
expect(runnerSmoke, "CLI node attach command", /"node"[\s\S]*"attach"[\s\S]*"windows-command-dev"/);
|
||||
expect(runnerSmoke, "server-generated enrollment grant creation", /create_node_enrollment_grant[\s\S]*attachGrant\.grant/);
|
||||
expect(runnerSmoke, "CLI enrollment exchange asserted", /used_enrollment_exchange[\s\S]*true/);
|
||||
expect(runnerSmoke, "server-generated runtime enrollment grant", /const runtimeGrant[\s\S]*create_node_enrollment_grant[\s\S]*runtimeGrant\.grant/);
|
||||
expect(runnerSmoke, "runtime uses enrollment grant", /"--enrollment-grant"[\s\S]*grant/);
|
||||
expect(runnerSmoke, "Windows command task runs", /"windows-command-dev"[\s\S]*"cmd"[\s\S]*"echo disasmer-windows-runner"/);
|
||||
expect(runnerSmoke, "artifact metadata is asserted", /windows-runner-output\.txt[\s\S]*artifact_download_link/);
|
||||
expect(runnerSmoke, "Windows placement is asserted", /schedule_task[\s\S]*WindowsCommandDev[\s\S]*forgejo-windows-node/);
|
||||
expect(runnerSmoke, "debugger shows Windows virtual thread", /compile windows/);
|
||||
expect(runnerSmoke, "validation result artifact", /disasmer_windows_runner_validation/);
|
||||
|
||||
expect(readme, "docs mention manual Windows workflow", /manual `Windows validation`\s+workflow/);
|
||||
expect(readme, "docs mention intermittent runner", /intermittent Windows runner/);
|
||||
expect(readme, "docs mention CLI attach in Windows validation", /runs `disasmer node attach`/);
|
||||
expect(readme, "docs keep Windows unvalidated when report is not-run", /windows_validation: "not-run"[\s\S]*best-effort and unvalidated/);
|
||||
|
||||
for (const [scriptName, script] of [
|
||||
["public acceptance", publicAcceptance],
|
||||
["public split", publicSplit],
|
||||
]) {
|
||||
assert(
|
||||
script.includes("node scripts/windows-validation-contract-smoke.js"),
|
||||
`${scriptName} must run windows-validation-contract-smoke.js`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Windows validation contract smoke passed");
|
||||
Loading…
Add table
Add a link
Reference in a new issue