diff --git a/README.md b/README.md index d338cf68..ab1e61a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # deepevents.ai deepevents.ai main codebase + +- [Repository compute sandbox policy guard](repository-compute-sandbox-policy-guard/README.md) diff --git a/repository-compute-sandbox-policy-guard/README.md b/repository-compute-sandbox-policy-guard/README.md new file mode 100644 index 00000000..3d0c4f46 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/README.md @@ -0,0 +1,49 @@ +# Repository Compute Sandbox Policy Guard + +This module is a self-contained Project Repository & Version Control slice for SCIBASE.AI issue #10. It evaluates whether a tagged scientific repository release/export candidate is safe to execute in a reproducibility sandbox before merge, DOI publication, or export bundle distribution. + +The guard uses synthetic data only. It does not scan real repositories, call external services, use credentials, or process patient/research participant data. + +## Scope + +- Require digest-pinned sandbox container images. +- Block open or unreviewed network egress during reproducibility replay. +- Enforce CPU, memory, runtime, GPU, and deterministic-seed policy. +- Restrict writable mounts to controlled scratch paths with size limits. +- Require sha256 checkpoints for input manifests, lockfiles, expected artifacts, component manifests, and export bundles. +- Emit deterministic remediation actions for protected merge, export, and DOI publication gates. + +## Requirement Mapping + +| Issue #10 area | Implementation | +| --- | --- | +| Computation-aware reproducibility | CPU, memory, runtime, GPU, deterministic seed, and container replay checks | +| Container support | Digest-pinned image validation for each pipeline | +| Execution sandboxes | Network egress and writable mount policy checks | +| Hash-based integrity | Component, input, lockfile, artifact, and export bundle sha256 checkpoints | +| Programmatic access & export | Release actions block unsafe export bundles and DOI publication | + +## Files + +- `index.js` - dependency-free evaluator, report renderer, Markdown renderer, and SVG renderer. +- `sample-data.js` - synthetic ready, blocked, and needs-review repository candidates. +- `test.js` - Node assertion coverage for the gate decisions and report renderers. +- `demo.js` - writes deterministic JSON, Markdown, and SVG reviewer artifacts. +- `scripts/render-demo-video.js` - optional ffmpeg-based MP4 renderer for the reviewer packet. +- `reports/` - generated reviewer artifacts. + +## Validation + +```bash +npm run check +npm test +npm run demo +``` + +Optional video render when ffmpeg is available: + +```bash +npm run demo:video +``` + +The included demo artifacts are deterministic and based only on the synthetic fixtures in `sample-data.js`. diff --git a/repository-compute-sandbox-policy-guard/demo.js b/repository-compute-sandbox-policy-guard/demo.js new file mode 100644 index 00000000..ae65f076 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/demo.js @@ -0,0 +1,27 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + createPolicyReport, + renderMarkdown, + renderSvg, +} = require("./index"); +const { sampleCandidates } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const report = createPolicyReport(sampleCandidates); +const jsonPath = path.join(reportsDir, "demo.json"); +const markdownPath = path.join(reportsDir, "demo.md"); +const svgPath = path.join(reportsDir, "demo.svg"); + +fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(markdownPath, renderMarkdown(report)); +fs.writeFileSync(svgPath, renderSvg(report)); + +console.log(`Wrote ${path.relative(process.cwd(), jsonPath)}`); +console.log(`Wrote ${path.relative(process.cwd(), markdownPath)}`); +console.log(`Wrote ${path.relative(process.cwd(), svgPath)}`); +console.log( + `Ready=${report.totals.ready} NeedsReview=${report.totals.needsReview} Blocked=${report.totals.blocked}`, +); diff --git a/repository-compute-sandbox-policy-guard/index.js b/repository-compute-sandbox-policy-guard/index.js new file mode 100644 index 00000000..d6b73f81 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/index.js @@ -0,0 +1,495 @@ +const DEFAULT_POLICY = Object.freeze({ + generatedAt: "2026-05-22T00:00:00.000Z", + maxCpuCores: 16, + maxMemoryGb: 64, + maxRuntimeMinutes: 240, + maxWritableGb: 2, + allowedEgressModes: ["none", "doi-resolution-only"], + doiResolverAllowlist: ["api.datacite.org", "doi.org"], + permittedWritablePrefixes: ["/tmp/scibase-run/"], + requiredHashPrefix: "sha256:", +}); + +const REQUIREMENT_MAP = Object.freeze({ + pinned_image: "Computation-aware reproducibility: container support", + network_policy: "Execution sandboxes: secure runtime validation", + compute_budget: "Computation-aware reproducibility: controlled execution", + writable_mounts: "Execution sandboxes: limited mutable state", + artifact_hashes: "File and metadata versioning: hash-based integrity", + export_gate: "Programmatic access and export: release bundle guard", +}); + +function evaluateRepositoryComputePolicy(candidate, policy = DEFAULT_POLICY) { + assertCandidate(candidate); + + const checks = []; + const issues = []; + const addCheck = (pipelineId, code, passed, detail) => { + checks.push({ pipelineId, code, passed, detail }); + }; + const addIssue = ( + severity, + pipelineId, + code, + message, + requirement, + remediation, + ) => { + issues.push({ + severity, + pipelineId, + code, + message, + requirement, + remediation, + }); + }; + + if (!isHash(candidate.componentManifestHash, policy)) { + addIssue( + "error", + "repository", + "COMPONENT_MANIFEST_HASH_MISSING", + "The repository component manifest is missing a deterministic hash.", + REQUIREMENT_MAP.artifact_hashes, + "Generate a sha256 component manifest before release or export.", + ); + } + + if (!isHash(candidate.exportBundleHash, policy)) { + addIssue( + "error", + "repository", + "EXPORT_BUNDLE_HASH_MISSING", + "The export bundle is missing a deterministic hash.", + REQUIREMENT_MAP.export_gate, + "Build the release bundle and attach its sha256 hash before DOI publication.", + ); + } + + const pipelines = [...candidate.pipelines].sort((a, b) => + a.id.localeCompare(b.id), + ); + + for (const pipeline of pipelines) { + const sandbox = pipeline.sandbox || {}; + const compute = pipeline.compute || {}; + + checkPinnedImage(pipeline, sandbox, policy, addCheck, addIssue); + checkNetworkPolicy(pipeline, sandbox, policy, addCheck, addIssue); + checkComputeBudget(pipeline, compute, policy, addCheck, addIssue); + checkWritableMounts(pipeline, sandbox, policy, addCheck, addIssue); + checkPipelineHashes(pipeline, policy, addCheck, addIssue); + } + + const blockingIssues = issues.filter((issue) => issue.severity === "error"); + const warnings = issues.filter((issue) => issue.severity === "warning"); + const status = + blockingIssues.length > 0 + ? "blocked" + : warnings.length > 0 + ? "needs_review" + : "ready"; + + return { + generatedAt: policy.generatedAt, + repositoryId: candidate.repositoryId, + tag: candidate.tag, + doiDraft: candidate.doiDraft, + status, + summary: { + pipelines: pipelines.length, + checks: checks.length, + passedChecks: checks.filter((check) => check.passed).length, + blockingIssues: blockingIssues.length, + warnings: warnings.length, + }, + checks, + issues: issues.sort(compareIssues), + releaseActions: buildReleaseActions(status, issues), + requirementCoverage: REQUIREMENT_MAP, + }; +} + +function createPolicyReport(candidates, policy = DEFAULT_POLICY) { + const evaluations = candidates + .map((candidate) => evaluateRepositoryComputePolicy(candidate, policy)) + .sort((a, b) => a.repositoryId.localeCompare(b.repositoryId)); + + return { + generatedAt: policy.generatedAt, + guard: "repository-compute-sandbox-policy-guard", + issue: "SCIBASE-AI/SCIBASE.AI#10", + totals: { + candidates: evaluations.length, + ready: evaluations.filter((item) => item.status === "ready").length, + needsReview: evaluations.filter((item) => item.status === "needs_review") + .length, + blocked: evaluations.filter((item) => item.status === "blocked").length, + blockingIssues: evaluations.reduce( + (sum, item) => sum + item.summary.blockingIssues, + 0, + ), + warnings: evaluations.reduce((sum, item) => sum + item.summary.warnings, 0), + }, + evaluations, + }; +} + +function renderMarkdown(report) { + const rows = report.evaluations + .map( + (item) => + `| ${item.repositoryId} | ${item.tag} | ${item.status} | ${item.summary.blockingIssues} | ${item.summary.warnings} |`, + ) + .join("\n"); + + const issueList = report.evaluations + .flatMap((item) => + item.issues.map( + (issue) => + `- ${item.repositoryId} ${issue.pipelineId} ${issue.code}: ${issue.remediation}`, + ), + ) + .join("\n"); + + return `# Repository Compute Sandbox Policy Guard + +Synthetic reviewer packet for ${report.issue}. + +## Summary + +| Candidate | Tag | Status | Blocking issues | Warnings | +| --- | --- | --- | ---: | ---: | +${rows} + +## Gate Totals + +- Ready candidates: ${report.totals.ready} +- Needs review: ${report.totals.needsReview} +- Blocked candidates: ${report.totals.blocked} +- Blocking issues: ${report.totals.blockingIssues} +- Warnings: ${report.totals.warnings} + +## Deterministic Remediation Queue + +${issueList || "- No remediation required."} + +## Requirement Mapping + +- Pinned sandbox images: ${REQUIREMENT_MAP.pinned_image} +- Network egress policy: ${REQUIREMENT_MAP.network_policy} +- CPU, memory, runtime, and GPU budgets: ${REQUIREMENT_MAP.compute_budget} +- Limited writable mounts: ${REQUIREMENT_MAP.writable_mounts} +- Artifact hash checkpoints: ${REQUIREMENT_MAP.artifact_hashes} +- DOI and export blocking actions: ${REQUIREMENT_MAP.export_gate} +`; +} + +function renderSvg(report) { + const width = 960; + const height = 540; + const cardWidth = 280; + const colors = { + ready: "#157f57", + needs_review: "#b7791f", + blocked: "#b42318", + }; + const cards = report.evaluations + .map((item, index) => { + const x = 40 + index * (cardWidth + 20); + const color = colors[item.status]; + return ` + + + ${escapeXml(item.repositoryId)} + ${escapeXml(item.tag)} + ${escapeXml(item.status)} + Checks: ${item.summary.passedChecks}/${item.summary.checks} + Blocks: ${item.summary.blockingIssues} +`; + }) + .join("\n"); + + return ` +Repository compute sandbox policy guard +Deterministic status report for SCIBASE repository release candidates. + +Repository Compute Sandbox Policy Guard +Pinned containers, network policy, budgets, writable mounts, and artifact hashes. + + + Ready ${report.totals.ready} + + Needs review ${report.totals.needsReview} + + Blocked ${report.totals.blocked} + +${cards} +Issue mapping: Project Repository and Version Control, computation-aware reproducibility, execution sandboxes, export bundles. + +`; +} + +function checkPinnedImage(pipeline, sandbox, policy, addCheck, addIssue) { + const image = sandbox.image || ""; + const pinned = image.includes("@sha256:") && !image.endsWith(":latest"); + addCheck(pipeline.id, "PINNED_SANDBOX_IMAGE", pinned, image || "missing image"); + + if (!pinned) { + addIssue( + "error", + pipeline.id, + "SANDBOX_IMAGE_NOT_PINNED", + "Sandbox image must be digest-pinned and must not use latest tags.", + REQUIREMENT_MAP.pinned_image, + "Pin the execution image with an immutable sha256 digest.", + ); + } +} + +function checkNetworkPolicy(pipeline, sandbox, policy, addCheck, addIssue) { + const mode = sandbox.networkEgress || "unspecified"; + const allowedMode = policy.allowedEgressModes.includes(mode); + const allowlist = [...(sandbox.egressAllowlist || [])].sort(); + const doiOnly = + mode !== "doi-resolution-only" || + (allowlist.length > 0 && + allowlist.every((host) => policy.doiResolverAllowlist.includes(host))); + const passed = allowedMode && doiOnly; + addCheck( + pipeline.id, + "NETWORK_EGRESS_POLICY", + passed, + `mode=${mode}; allowlist=${allowlist.join(",") || "empty"}`, + ); + + if (!allowedMode || !doiOnly) { + addIssue( + "error", + pipeline.id, + "NETWORK_EGRESS_TOO_BROAD", + "Reproducibility execution cannot use open or unreviewed network egress.", + REQUIREMENT_MAP.network_policy, + "Set egress to none, or restrict DOI lookup to the approved resolver allowlist.", + ); + } +} + +function checkComputeBudget(pipeline, compute, policy, addCheck, addIssue) { + const cpuOk = numberAtMost(compute.cpuCores, policy.maxCpuCores); + const memoryOk = numberAtMost(compute.memoryGb, policy.maxMemoryGb); + const runtimeOk = numberAtMost( + compute.runtimeMinutes, + policy.maxRuntimeMinutes, + ); + const seeded = Boolean(compute.deterministicSeed); + + addCheck( + pipeline.id, + "CPU_MEMORY_RUNTIME_BUDGET", + cpuOk && memoryOk && runtimeOk, + `cpu=${compute.cpuCores}; memoryGb=${compute.memoryGb}; runtimeMinutes=${compute.runtimeMinutes}`, + ); + addCheck(pipeline.id, "DETERMINISTIC_SEED", seeded, compute.deterministicSeed); + + if (!cpuOk || !memoryOk || !runtimeOk) { + addIssue( + "error", + pipeline.id, + "COMPUTE_BUDGET_EXCEEDED", + "The pipeline exceeds the approved CPU, memory, or runtime budget.", + REQUIREMENT_MAP.compute_budget, + `Keep CPU <= ${policy.maxCpuCores}, memory <= ${policy.maxMemoryGb} GB, and runtime <= ${policy.maxRuntimeMinutes} minutes.`, + ); + } + + if (!seeded) { + addIssue( + "error", + pipeline.id, + "DETERMINISTIC_SEED_MISSING", + "The pipeline does not declare a deterministic seed.", + REQUIREMENT_MAP.compute_budget, + "Declare the seed used for replay so results can be regenerated byte-for-byte.", + ); + } + + if ((compute.gpuCount || 0) > 0) { + const justified = Boolean(compute.gpuJustification) && seeded; + addCheck( + pipeline.id, + "GPU_REVIEW_PACKET", + justified, + compute.gpuJustification || "missing GPU justification", + ); + addIssue( + justified ? "warning" : "error", + pipeline.id, + "GPU_REVIEW_REQUIRED", + "GPU execution requires reviewer signoff because hardware kernels can affect determinism.", + REQUIREMENT_MAP.compute_budget, + "Attach deterministic kernel notes and reviewer signoff before final publication.", + ); + } +} + +function checkWritableMounts(pipeline, sandbox, policy, addCheck, addIssue) { + const mounts = sandbox.writableMounts || []; + const inputsReadOnly = sandbox.readOnlyInputs === true; + addCheck( + pipeline.id, + "READ_ONLY_INPUTS", + inputsReadOnly, + inputsReadOnly ? "inputs are read-only" : "inputs may be mutable", + ); + + if (!inputsReadOnly) { + addIssue( + "error", + pipeline.id, + "INPUTS_NOT_READ_ONLY", + "Repository inputs must be mounted read-only during replay.", + REQUIREMENT_MAP.writable_mounts, + "Mount repository inputs read-only and write outputs only to the controlled scratch path.", + ); + } + + for (const mount of mounts) { + const allowedPath = policy.permittedWritablePrefixes.some((prefix) => + String(mount.path || "").startsWith(prefix), + ); + const sizeOk = numberAtMost(mount.maxGb, policy.maxWritableGb); + addCheck( + pipeline.id, + "WRITABLE_MOUNT", + allowedPath && sizeOk, + `path=${mount.path}; maxGb=${mount.maxGb}`, + ); + + if (!allowedPath || !sizeOk) { + addIssue( + "error", + pipeline.id, + "WRITABLE_MOUNT_NOT_CONSTRAINED", + "Writable mount is outside the approved scratch prefix or exceeds size limits.", + REQUIREMENT_MAP.writable_mounts, + `Use ${policy.permittedWritablePrefixes.join(", ")} with max ${policy.maxWritableGb} GB.`, + ); + } + } +} + +function checkPipelineHashes(pipeline, policy, addCheck, addIssue) { + const hashFields = [ + ["INPUT_MANIFEST_HASH", pipeline.inputManifestHash], + ["LOCKFILE_HASH", pipeline.lockfileHash], + ]; + for (const [code, value] of hashFields) { + const passed = isHash(value, policy); + addCheck(pipeline.id, code, passed, value || "missing"); + if (!passed) { + addIssue( + "error", + pipeline.id, + `${code}_MISSING`, + `${code} is missing a sha256 checkpoint.`, + REQUIREMENT_MAP.artifact_hashes, + `Generate ${code.toLowerCase()} before accepting this release candidate.`, + ); + } + } + + const artifacts = pipeline.expectedArtifacts || []; + for (const artifact of artifacts) { + const passed = isHash(artifact.hash, policy); + addCheck( + pipeline.id, + `ARTIFACT_HASH_${artifact.name}`, + passed, + artifact.hash || "missing", + ); + if (!passed) { + addIssue( + "error", + pipeline.id, + "EXPECTED_ARTIFACT_HASH_MISSING", + `Expected artifact ${artifact.name} is missing a sha256 checkpoint.`, + REQUIREMENT_MAP.artifact_hashes, + "Run the pipeline once in the sandbox and record the artifact hash before export.", + ); + } + } +} + +function buildReleaseActions(status, issues) { + const blocking = issues.filter((issue) => issue.severity === "error"); + const warnings = issues.filter((issue) => issue.severity === "warning"); + return { + allowProtectedMerge: blocking.length === 0, + allowExportBundle: blocking.length === 0, + allowDoiPublication: blocking.length === 0 && warnings.length === 0, + requiresReviewerSignoff: warnings.length > 0, + remediationChecklist: issues.map((issue) => ({ + code: issue.code, + pipelineId: issue.pipelineId, + action: issue.remediation, + })), + status, + }; +} + +function assertCandidate(candidate) { + if (!candidate || typeof candidate !== "object") { + throw new TypeError("candidate must be an object"); + } + for (const field of ["repositoryId", "tag", "pipelines"]) { + if (!candidate[field]) { + throw new TypeError(`candidate.${field} is required`); + } + } + if (!Array.isArray(candidate.pipelines) || candidate.pipelines.length === 0) { + throw new TypeError("candidate.pipelines must contain at least one pipeline"); + } +} + +function numberAtMost(value, max) { + return typeof value === "number" && Number.isFinite(value) && value <= max; +} + +function isHash(value, policy) { + return ( + typeof value === "string" && + value.startsWith(policy.requiredHashPrefix) && + value.length >= policy.requiredHashPrefix.length + 16 + ); +} + +function compareIssues(a, b) { + return ( + severityRank(a.severity) - severityRank(b.severity) || + a.pipelineId.localeCompare(b.pipelineId) || + a.code.localeCompare(b.code) + ); +} + +function severityRank(severity) { + return severity === "error" ? 0 : 1; +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +module.exports = { + DEFAULT_POLICY, + REQUIREMENT_MAP, + createPolicyReport, + evaluateRepositoryComputePolicy, + renderMarkdown, + renderSvg, +}; diff --git a/repository-compute-sandbox-policy-guard/package.json b/repository-compute-sandbox-policy-guard/package.json new file mode 100644 index 00000000..e2a904c1 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "repository-compute-sandbox-policy-guard", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "description": "Synthetic compute sandbox policy guard for SCIBASE project repository releases.", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js && node --check scripts/render-demo-video.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node scripts/render-demo-video.js" + } +} diff --git a/repository-compute-sandbox-policy-guard/reports/demo.json b/repository-compute-sandbox-policy-guard/reports/demo.json new file mode 100644 index 00000000..be1e72f9 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/reports/demo.json @@ -0,0 +1,454 @@ +{ + "generatedAt": "2026-05-22T00:00:00.000Z", + "guard": "repository-compute-sandbox-policy-guard", + "issue": "SCIBASE-AI/SCIBASE.AI#10", + "totals": { + "candidates": 3, + "ready": 1, + "needsReview": 1, + "blocked": 1, + "blockingIssues": 11, + "warnings": 1 + }, + "evaluations": [ + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "repositoryId": "climate-lab/regional-ensemble", + "tag": "preprint-v1.4", + "doiDraft": "10.5555/scibase.regional-ensemble.v1", + "status": "needs_review", + "summary": { + "pipelines": 1, + "checks": 10, + "passedChecks": 10, + "blockingIssues": 0, + "warnings": 1 + }, + "checks": [ + { + "pipelineId": "gpu-ensemble-replay", + "code": "PINNED_SANDBOX_IMAGE", + "passed": true, + "detail": "ghcr.io/scibase/repro-cuda@sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "NETWORK_EGRESS_POLICY", + "passed": true, + "detail": "mode=doi-resolution-only; allowlist=api.datacite.org,doi.org" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "CPU_MEMORY_RUNTIME_BUDGET", + "passed": true, + "detail": "cpu=12; memoryGb=48; runtimeMinutes=210" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "DETERMINISTIC_SEED", + "passed": true, + "detail": "regional-ensemble-v1.4" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "GPU_REVIEW_PACKET", + "passed": true, + "detail": "Replay published CUDA ensemble with deterministic kernels and fixed seeds." + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "READ_ONLY_INPUTS", + "passed": true, + "detail": "inputs are read-only" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "WRITABLE_MOUNT", + "passed": true, + "detail": "path=/tmp/scibase-run/gpu-ensemble-replay; maxGb=2" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "INPUT_MANIFEST_HASH", + "passed": true, + "detail": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "LOCKFILE_HASH", + "passed": true, + "detail": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "pipelineId": "gpu-ensemble-replay", + "code": "ARTIFACT_HASH_results/ensemble/replay-summary.json", + "passed": true, + "detail": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "issues": [ + { + "severity": "warning", + "pipelineId": "gpu-ensemble-replay", + "code": "GPU_REVIEW_REQUIRED", + "message": "GPU execution requires reviewer signoff because hardware kernels can affect determinism.", + "requirement": "Computation-aware reproducibility: controlled execution", + "remediation": "Attach deterministic kernel notes and reviewer signoff before final publication." + } + ], + "releaseActions": { + "allowProtectedMerge": true, + "allowExportBundle": true, + "allowDoiPublication": false, + "requiresReviewerSignoff": true, + "remediationChecklist": [ + { + "code": "GPU_REVIEW_REQUIRED", + "pipelineId": "gpu-ensemble-replay", + "action": "Attach deterministic kernel notes and reviewer signoff before final publication." + } + ], + "status": "needs_review" + }, + "requirementCoverage": { + "pinned_image": "Computation-aware reproducibility: container support", + "network_policy": "Execution sandboxes: secure runtime validation", + "compute_budget": "Computation-aware reproducibility: controlled execution", + "writable_mounts": "Execution sandboxes: limited mutable state", + "artifact_hashes": "File and metadata versioning: hash-based integrity", + "export_gate": "Programmatic access and export: release bundle guard" + } + }, + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "repositoryId": "neuro-lab/sleep-spindle-atlas", + "tag": "preprint-v2.1", + "doiDraft": "10.5555/scibase.sleep-spindle-atlas.v2", + "status": "ready", + "summary": { + "pipelines": 1, + "checks": 10, + "passedChecks": 10, + "blockingIssues": 0, + "warnings": 0 + }, + "checks": [ + { + "pipelineId": "analysis-primary", + "code": "PINNED_SANDBOX_IMAGE", + "passed": true, + "detail": "ghcr.io/scibase/repro-python@sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pipelineId": "analysis-primary", + "code": "NETWORK_EGRESS_POLICY", + "passed": true, + "detail": "mode=none; allowlist=empty" + }, + { + "pipelineId": "analysis-primary", + "code": "CPU_MEMORY_RUNTIME_BUDGET", + "passed": true, + "detail": "cpu=8; memoryGb=32; runtimeMinutes=115" + }, + { + "pipelineId": "analysis-primary", + "code": "DETERMINISTIC_SEED", + "passed": true, + "detail": "sleep-spindle-atlas-v2" + }, + { + "pipelineId": "analysis-primary", + "code": "READ_ONLY_INPUTS", + "passed": true, + "detail": "inputs are read-only" + }, + { + "pipelineId": "analysis-primary", + "code": "WRITABLE_MOUNT", + "passed": true, + "detail": "path=/tmp/scibase-run/analysis-primary; maxGb=1.5" + }, + { + "pipelineId": "analysis-primary", + "code": "INPUT_MANIFEST_HASH", + "passed": true, + "detail": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + { + "pipelineId": "analysis-primary", + "code": "LOCKFILE_HASH", + "passed": true, + "detail": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + { + "pipelineId": "analysis-primary", + "code": "ARTIFACT_HASH_results/tables/spindle-summary.csv", + "passed": true, + "detail": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + { + "pipelineId": "analysis-primary", + "code": "ARTIFACT_HASH_results/figures/cohort-comparison.svg", + "passed": true, + "detail": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "issues": [], + "releaseActions": { + "allowProtectedMerge": true, + "allowExportBundle": true, + "allowDoiPublication": true, + "requiresReviewerSignoff": false, + "remediationChecklist": [], + "status": "ready" + }, + "requirementCoverage": { + "pinned_image": "Computation-aware reproducibility: container support", + "network_policy": "Execution sandboxes: secure runtime validation", + "compute_budget": "Computation-aware reproducibility: controlled execution", + "writable_mounts": "Execution sandboxes: limited mutable state", + "artifact_hashes": "File and metadata versioning: hash-based integrity", + "export_gate": "Programmatic access and export: release bundle guard" + } + }, + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "repositoryId": "oncology-lab/cell-response-map", + "tag": "release-unsafe-candidate", + "doiDraft": "10.5555/scibase.cell-response-map.unsafe", + "status": "blocked", + "summary": { + "pipelines": 1, + "checks": 10, + "passedChecks": 0, + "blockingIssues": 11, + "warnings": 0 + }, + "checks": [ + { + "pipelineId": "training-open-network", + "code": "PINNED_SANDBOX_IMAGE", + "passed": false, + "detail": "python:latest" + }, + { + "pipelineId": "training-open-network", + "code": "NETWORK_EGRESS_POLICY", + "passed": false, + "detail": "mode=open-internet; allowlist=example-data.invalid,pypi.org" + }, + { + "pipelineId": "training-open-network", + "code": "CPU_MEMORY_RUNTIME_BUDGET", + "passed": false, + "detail": "cpu=24; memoryGb=96; runtimeMinutes=360" + }, + { + "pipelineId": "training-open-network", + "code": "DETERMINISTIC_SEED", + "passed": false, + "detail": "" + }, + { + "pipelineId": "training-open-network", + "code": "GPU_REVIEW_PACKET", + "passed": false, + "detail": "missing GPU justification" + }, + { + "pipelineId": "training-open-network", + "code": "READ_ONLY_INPUTS", + "passed": false, + "detail": "inputs may be mutable" + }, + { + "pipelineId": "training-open-network", + "code": "WRITABLE_MOUNT", + "passed": false, + "detail": "path=/workspace; maxGb=12" + }, + { + "pipelineId": "training-open-network", + "code": "INPUT_MANIFEST_HASH", + "passed": false, + "detail": "missing" + }, + { + "pipelineId": "training-open-network", + "code": "LOCKFILE_HASH", + "passed": false, + "detail": "missing" + }, + { + "pipelineId": "training-open-network", + "code": "ARTIFACT_HASH_results/models/cell-response.pt", + "passed": false, + "detail": "pending" + } + ], + "issues": [ + { + "severity": "error", + "pipelineId": "repository", + "code": "EXPORT_BUNDLE_HASH_MISSING", + "message": "The export bundle is missing a deterministic hash.", + "requirement": "Programmatic access and export: release bundle guard", + "remediation": "Build the release bundle and attach its sha256 hash before DOI publication." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "COMPUTE_BUDGET_EXCEEDED", + "message": "The pipeline exceeds the approved CPU, memory, or runtime budget.", + "requirement": "Computation-aware reproducibility: controlled execution", + "remediation": "Keep CPU <= 16, memory <= 64 GB, and runtime <= 240 minutes." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "DETERMINISTIC_SEED_MISSING", + "message": "The pipeline does not declare a deterministic seed.", + "requirement": "Computation-aware reproducibility: controlled execution", + "remediation": "Declare the seed used for replay so results can be regenerated byte-for-byte." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "EXPECTED_ARTIFACT_HASH_MISSING", + "message": "Expected artifact results/models/cell-response.pt is missing a sha256 checkpoint.", + "requirement": "File and metadata versioning: hash-based integrity", + "remediation": "Run the pipeline once in the sandbox and record the artifact hash before export." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "GPU_REVIEW_REQUIRED", + "message": "GPU execution requires reviewer signoff because hardware kernels can affect determinism.", + "requirement": "Computation-aware reproducibility: controlled execution", + "remediation": "Attach deterministic kernel notes and reviewer signoff before final publication." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "INPUT_MANIFEST_HASH_MISSING", + "message": "INPUT_MANIFEST_HASH is missing a sha256 checkpoint.", + "requirement": "File and metadata versioning: hash-based integrity", + "remediation": "Generate input_manifest_hash before accepting this release candidate." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "INPUTS_NOT_READ_ONLY", + "message": "Repository inputs must be mounted read-only during replay.", + "requirement": "Execution sandboxes: limited mutable state", + "remediation": "Mount repository inputs read-only and write outputs only to the controlled scratch path." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "LOCKFILE_HASH_MISSING", + "message": "LOCKFILE_HASH is missing a sha256 checkpoint.", + "requirement": "File and metadata versioning: hash-based integrity", + "remediation": "Generate lockfile_hash before accepting this release candidate." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "NETWORK_EGRESS_TOO_BROAD", + "message": "Reproducibility execution cannot use open or unreviewed network egress.", + "requirement": "Execution sandboxes: secure runtime validation", + "remediation": "Set egress to none, or restrict DOI lookup to the approved resolver allowlist." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "SANDBOX_IMAGE_NOT_PINNED", + "message": "Sandbox image must be digest-pinned and must not use latest tags.", + "requirement": "Computation-aware reproducibility: container support", + "remediation": "Pin the execution image with an immutable sha256 digest." + }, + { + "severity": "error", + "pipelineId": "training-open-network", + "code": "WRITABLE_MOUNT_NOT_CONSTRAINED", + "message": "Writable mount is outside the approved scratch prefix or exceeds size limits.", + "requirement": "Execution sandboxes: limited mutable state", + "remediation": "Use /tmp/scibase-run/ with max 2 GB." + } + ], + "releaseActions": { + "allowProtectedMerge": false, + "allowExportBundle": false, + "allowDoiPublication": false, + "requiresReviewerSignoff": false, + "remediationChecklist": [ + { + "code": "EXPORT_BUNDLE_HASH_MISSING", + "pipelineId": "repository", + "action": "Build the release bundle and attach its sha256 hash before DOI publication." + }, + { + "code": "COMPUTE_BUDGET_EXCEEDED", + "pipelineId": "training-open-network", + "action": "Keep CPU <= 16, memory <= 64 GB, and runtime <= 240 minutes." + }, + { + "code": "DETERMINISTIC_SEED_MISSING", + "pipelineId": "training-open-network", + "action": "Declare the seed used for replay so results can be regenerated byte-for-byte." + }, + { + "code": "EXPECTED_ARTIFACT_HASH_MISSING", + "pipelineId": "training-open-network", + "action": "Run the pipeline once in the sandbox and record the artifact hash before export." + }, + { + "code": "GPU_REVIEW_REQUIRED", + "pipelineId": "training-open-network", + "action": "Attach deterministic kernel notes and reviewer signoff before final publication." + }, + { + "code": "INPUT_MANIFEST_HASH_MISSING", + "pipelineId": "training-open-network", + "action": "Generate input_manifest_hash before accepting this release candidate." + }, + { + "code": "INPUTS_NOT_READ_ONLY", + "pipelineId": "training-open-network", + "action": "Mount repository inputs read-only and write outputs only to the controlled scratch path." + }, + { + "code": "LOCKFILE_HASH_MISSING", + "pipelineId": "training-open-network", + "action": "Generate lockfile_hash before accepting this release candidate." + }, + { + "code": "NETWORK_EGRESS_TOO_BROAD", + "pipelineId": "training-open-network", + "action": "Set egress to none, or restrict DOI lookup to the approved resolver allowlist." + }, + { + "code": "SANDBOX_IMAGE_NOT_PINNED", + "pipelineId": "training-open-network", + "action": "Pin the execution image with an immutable sha256 digest." + }, + { + "code": "WRITABLE_MOUNT_NOT_CONSTRAINED", + "pipelineId": "training-open-network", + "action": "Use /tmp/scibase-run/ with max 2 GB." + } + ], + "status": "blocked" + }, + "requirementCoverage": { + "pinned_image": "Computation-aware reproducibility: container support", + "network_policy": "Execution sandboxes: secure runtime validation", + "compute_budget": "Computation-aware reproducibility: controlled execution", + "writable_mounts": "Execution sandboxes: limited mutable state", + "artifact_hashes": "File and metadata versioning: hash-based integrity", + "export_gate": "Programmatic access and export: release bundle guard" + } + } + ] +} diff --git a/repository-compute-sandbox-policy-guard/reports/demo.md b/repository-compute-sandbox-policy-guard/reports/demo.md new file mode 100644 index 00000000..085d95f1 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/reports/demo.md @@ -0,0 +1,43 @@ +# Repository Compute Sandbox Policy Guard + +Synthetic reviewer packet for SCIBASE-AI/SCIBASE.AI#10. + +## Summary + +| Candidate | Tag | Status | Blocking issues | Warnings | +| --- | --- | --- | ---: | ---: | +| climate-lab/regional-ensemble | preprint-v1.4 | needs_review | 0 | 1 | +| neuro-lab/sleep-spindle-atlas | preprint-v2.1 | ready | 0 | 0 | +| oncology-lab/cell-response-map | release-unsafe-candidate | blocked | 11 | 0 | + +## Gate Totals + +- Ready candidates: 1 +- Needs review: 1 +- Blocked candidates: 1 +- Blocking issues: 11 +- Warnings: 1 + +## Deterministic Remediation Queue + +- climate-lab/regional-ensemble gpu-ensemble-replay GPU_REVIEW_REQUIRED: Attach deterministic kernel notes and reviewer signoff before final publication. +- oncology-lab/cell-response-map repository EXPORT_BUNDLE_HASH_MISSING: Build the release bundle and attach its sha256 hash before DOI publication. +- oncology-lab/cell-response-map training-open-network COMPUTE_BUDGET_EXCEEDED: Keep CPU <= 16, memory <= 64 GB, and runtime <= 240 minutes. +- oncology-lab/cell-response-map training-open-network DETERMINISTIC_SEED_MISSING: Declare the seed used for replay so results can be regenerated byte-for-byte. +- oncology-lab/cell-response-map training-open-network EXPECTED_ARTIFACT_HASH_MISSING: Run the pipeline once in the sandbox and record the artifact hash before export. +- oncology-lab/cell-response-map training-open-network GPU_REVIEW_REQUIRED: Attach deterministic kernel notes and reviewer signoff before final publication. +- oncology-lab/cell-response-map training-open-network INPUT_MANIFEST_HASH_MISSING: Generate input_manifest_hash before accepting this release candidate. +- oncology-lab/cell-response-map training-open-network INPUTS_NOT_READ_ONLY: Mount repository inputs read-only and write outputs only to the controlled scratch path. +- oncology-lab/cell-response-map training-open-network LOCKFILE_HASH_MISSING: Generate lockfile_hash before accepting this release candidate. +- oncology-lab/cell-response-map training-open-network NETWORK_EGRESS_TOO_BROAD: Set egress to none, or restrict DOI lookup to the approved resolver allowlist. +- oncology-lab/cell-response-map training-open-network SANDBOX_IMAGE_NOT_PINNED: Pin the execution image with an immutable sha256 digest. +- oncology-lab/cell-response-map training-open-network WRITABLE_MOUNT_NOT_CONSTRAINED: Use /tmp/scibase-run/ with max 2 GB. + +## Requirement Mapping + +- Pinned sandbox images: Computation-aware reproducibility: container support +- Network egress policy: Execution sandboxes: secure runtime validation +- CPU, memory, runtime, and GPU budgets: Computation-aware reproducibility: controlled execution +- Limited writable mounts: Execution sandboxes: limited mutable state +- Artifact hash checkpoints: File and metadata versioning: hash-based integrity +- DOI and export blocking actions: Programmatic access and export: release bundle guard diff --git a/repository-compute-sandbox-policy-guard/reports/demo.mp4 b/repository-compute-sandbox-policy-guard/reports/demo.mp4 new file mode 100644 index 00000000..e89249b2 Binary files /dev/null and b/repository-compute-sandbox-policy-guard/reports/demo.mp4 differ diff --git a/repository-compute-sandbox-policy-guard/reports/demo.svg b/repository-compute-sandbox-policy-guard/reports/demo.svg new file mode 100644 index 00000000..db154a9a --- /dev/null +++ b/repository-compute-sandbox-policy-guard/reports/demo.svg @@ -0,0 +1,43 @@ + +Repository compute sandbox policy guard +Deterministic status report for SCIBASE repository release candidates. + +Repository Compute Sandbox Policy Guard +Pinned containers, network policy, budgets, writable mounts, and artifact hashes. + + + Ready 1 + + Needs review 1 + + Blocked 1 + + + + + climate-lab/regional-ensemble + preprint-v1.4 + needs_review + Checks: 10/10 + Blocks: 0 + + + + + neuro-lab/sleep-spindle-atlas + preprint-v2.1 + ready + Checks: 10/10 + Blocks: 0 + + + + + oncology-lab/cell-response-map + release-unsafe-candidate + blocked + Checks: 0/10 + Blocks: 11 + +Issue mapping: Project Repository and Version Control, computation-aware reproducibility, execution sandboxes, export bundles. + diff --git a/repository-compute-sandbox-policy-guard/sample-data.js b/repository-compute-sandbox-policy-guard/sample-data.js new file mode 100644 index 00000000..4619dcab --- /dev/null +++ b/repository-compute-sandbox-policy-guard/sample-data.js @@ -0,0 +1,151 @@ +const HASH_A = + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const HASH_B = + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const HASH_C = + "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const HASH_D = + "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const HASH_E = + "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; +const HASH_F = + "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + +const sampleCandidates = [ + { + repositoryId: "neuro-lab/sleep-spindle-atlas", + tag: "preprint-v2.1", + doiDraft: "10.5555/scibase.sleep-spindle-atlas.v2", + componentManifestHash: HASH_A, + exportBundleHash: HASH_B, + reviewers: ["orcid:0000-0002-1825-0097", "orcid:0000-0003-1555-4212"], + pipelines: [ + { + id: "analysis-primary", + component: "code/run_analysis.ipynb", + inputManifestHash: HASH_C, + lockfileHash: HASH_D, + sandbox: { + image: + "ghcr.io/scibase/repro-python@sha256:1111111111111111111111111111111111111111111111111111111111111111", + networkEgress: "none", + egressAllowlist: [], + readOnlyInputs: true, + writableMounts: [ + { + path: "/tmp/scibase-run/analysis-primary", + maxGb: 1.5, + }, + ], + }, + compute: { + cpuCores: 8, + memoryGb: 32, + runtimeMinutes: 115, + gpuCount: 0, + deterministicSeed: "sleep-spindle-atlas-v2", + }, + expectedArtifacts: [ + { + name: "results/tables/spindle-summary.csv", + hash: HASH_E, + }, + { + name: "results/figures/cohort-comparison.svg", + hash: HASH_F, + }, + ], + }, + ], + }, + { + repositoryId: "oncology-lab/cell-response-map", + tag: "release-unsafe-candidate", + doiDraft: "10.5555/scibase.cell-response-map.unsafe", + componentManifestHash: HASH_B, + exportBundleHash: "", + reviewers: ["orcid:0000-0001-0000-0000"], + pipelines: [ + { + id: "training-open-network", + component: "notebooks/train-response-model.ipynb", + inputManifestHash: "", + lockfileHash: "", + sandbox: { + image: "python:latest", + networkEgress: "open-internet", + egressAllowlist: ["pypi.org", "example-data.invalid"], + readOnlyInputs: false, + writableMounts: [ + { + path: "/workspace", + maxGb: 12, + }, + ], + }, + compute: { + cpuCores: 24, + memoryGb: 96, + runtimeMinutes: 360, + gpuCount: 1, + deterministicSeed: "", + }, + expectedArtifacts: [ + { + name: "results/models/cell-response.pt", + hash: "pending", + }, + ], + }, + ], + }, + { + repositoryId: "climate-lab/regional-ensemble", + tag: "preprint-v1.4", + doiDraft: "10.5555/scibase.regional-ensemble.v1", + componentManifestHash: HASH_C, + exportBundleHash: HASH_D, + reviewers: ["orcid:0000-0002-4444-8888"], + pipelines: [ + { + id: "gpu-ensemble-replay", + component: "code/replay_ensemble.py", + inputManifestHash: HASH_E, + lockfileHash: HASH_F, + sandbox: { + image: + "ghcr.io/scibase/repro-cuda@sha256:2222222222222222222222222222222222222222222222222222222222222222", + networkEgress: "doi-resolution-only", + egressAllowlist: ["doi.org", "api.datacite.org"], + egressJustification: "Resolve DOI metadata during citation export only.", + readOnlyInputs: true, + writableMounts: [ + { + path: "/tmp/scibase-run/gpu-ensemble-replay", + maxGb: 2, + }, + ], + }, + compute: { + cpuCores: 12, + memoryGb: 48, + runtimeMinutes: 210, + gpuCount: 1, + gpuJustification: + "Replay published CUDA ensemble with deterministic kernels and fixed seeds.", + deterministicSeed: "regional-ensemble-v1.4", + }, + expectedArtifacts: [ + { + name: "results/ensemble/replay-summary.json", + hash: HASH_A, + }, + ], + }, + ], + }, +]; + +module.exports = { + sampleCandidates, +}; diff --git a/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js b/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..d9c8265f --- /dev/null +++ b/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js @@ -0,0 +1,85 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const reportsDir = path.join(__dirname, "..", "reports"); +const reportPath = path.join(reportsDir, "demo.json"); +const outputPath = path.join(reportsDir, "demo.mp4"); + +if (!fs.existsSync(reportPath)) { + throw new Error("Run npm run demo before rendering the video."); +} + +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")); +const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg"; +const title = "SCIBASE compute sandbox policy guard"; +const subtitle = `ready ${report.totals.ready} review ${report.totals.needsReview} blocked ${report.totals.blocked}`; +const detail = `blocking issues ${report.totals.blockingIssues} warnings ${report.totals.warnings}`; +const fontArg = getFontArg(); +const filters = [ + "drawbox=x=0:y=0:w=iw:h=ih:color=0x0b1020@1:t=fill", + "drawbox=x=72:y=86:w=1136:h=548:color=0xffffff@0.94:t=fill", + "drawbox=x=72:y=86:w=1136:h=18:color=0x157f57@1:t=fill", + drawText(title, 112, 160, 42, "0x111827", fontArg), + drawText("Project Repository and Version Control", 112, 220, 24, "0x374151", fontArg), + drawText(subtitle, 112, 305, 34, "0x111827", fontArg), + drawText(detail, 112, 365, 26, "0xb42318", fontArg), + drawText( + "Pins container images, constrains network egress, budgets compute, and blocks unsafe export.", + 112, + 450, + 22, + "0x374151", + fontArg, + ), + drawText("Synthetic data only. No external services or private repositories.", 112, 500, 22, "0x374151", fontArg), +].join(","); + +const result = spawnSync( + ffmpeg, + [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0x0b1020:s=1280x720:d=4:r=25", + "-vf", + filters, + "-pix_fmt", + "yuv420p", + outputPath, + ], + { stdio: "inherit" }, +); + +if (result.status !== 0) { + throw new Error(`ffmpeg exited with status ${result.status}`); +} + +console.log(`Wrote ${path.relative(process.cwd(), outputPath)}`); + +function drawText(text, x, y, size, color, fontArg) { + return `drawtext=${fontArg}:text='${escapeDrawtext(text)}':x=${x}:y=${y}:fontsize=${size}:fontcolor=${color}`; +} + +function escapeDrawtext(value) { + return String(value) + .replaceAll("\\", "\\\\") + .replaceAll(":", "\\:") + .replaceAll("'", "\\'"); +} + +function getFontArg() { + const candidates = [ + "C:/Windows/Fonts/arial.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", + ]; + const font = candidates.find((candidate) => fs.existsSync(candidate)); + if (!font) { + throw new Error( + `No supported TrueType font file found. Checked: ${candidates.join(", ")}`, + ); + } + return `fontfile='${font.replaceAll(":", "\\:")}'`; +} diff --git a/repository-compute-sandbox-policy-guard/test.js b/repository-compute-sandbox-policy-guard/test.js new file mode 100644 index 00000000..0080ca78 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/test.js @@ -0,0 +1,101 @@ +const assert = require("node:assert/strict"); +const { + createPolicyReport, + evaluateRepositoryComputePolicy, + renderMarkdown, + renderSvg, +} = require("./index"); +const { sampleCandidates } = require("./sample-data"); + +const ready = evaluateRepositoryComputePolicy(sampleCandidates[0]); +assert.equal(ready.status, "ready"); +assert.equal(ready.releaseActions.allowProtectedMerge, true); +assert.equal(ready.releaseActions.allowDoiPublication, true); +assert.equal(ready.summary.blockingIssues, 0); + +const blocked = evaluateRepositoryComputePolicy(sampleCandidates[1]); +assert.equal(blocked.status, "blocked"); +assert.equal(blocked.releaseActions.allowExportBundle, false); +assert.ok( + blocked.issues.some((issue) => issue.code === "SANDBOX_IMAGE_NOT_PINNED"), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "NETWORK_EGRESS_TOO_BROAD"), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "COMPUTE_BUDGET_EXCEEDED"), +); +assert.ok( + blocked.issues.some( + (issue) => issue.code === "WRITABLE_MOUNT_NOT_CONSTRAINED", + ), +); +assert.ok( + blocked.issues.some( + (issue) => issue.code === "EXPECTED_ARTIFACT_HASH_MISSING", + ), +); +assert.ok( + blocked.checks + .filter((check) => check.code === "WRITABLE_MOUNT") + .every((check) => check.detail.includes("path=")), +); +assert.ok( + !blocked.checks.some((check) => check.code.includes("/workspace")), +); + +const missingDoiAllowlist = evaluateRepositoryComputePolicy({ + ...sampleCandidates[0], + pipelines: [ + { + ...sampleCandidates[0].pipelines[0], + sandbox: { + ...sampleCandidates[0].pipelines[0].sandbox, + networkEgress: "doi-resolution-only", + egressAllowlist: [], + }, + }, + ], +}); +assert.equal(missingDoiAllowlist.status, "blocked"); +assert.ok( + missingDoiAllowlist.issues.some( + (issue) => issue.code === "NETWORK_EGRESS_TOO_BROAD", + ), +); + +const needsReview = evaluateRepositoryComputePolicy(sampleCandidates[2]); +assert.equal(needsReview.status, "needs_review"); +assert.equal(needsReview.releaseActions.allowProtectedMerge, true); +assert.equal(needsReview.releaseActions.allowDoiPublication, false); +assert.equal(needsReview.releaseActions.requiresReviewerSignoff, true); +assert.ok( + needsReview.issues.some((issue) => issue.code === "GPU_REVIEW_REQUIRED"), +); + +const report = createPolicyReport(sampleCandidates); +assert.deepEqual(report.totals, { + candidates: 3, + ready: 1, + needsReview: 1, + blocked: 1, + blockingIssues: 11, + warnings: 1, +}); + +const markdown = renderMarkdown(report); +assert.match(markdown, /Repository Compute Sandbox Policy Guard/); +assert.match(markdown, /NETWORK_EGRESS_TOO_BROAD/); +assert.match(markdown, /DOI and export blocking actions/); + +const svg = renderSvg(report); +assert.match(svg, / evaluateRepositoryComputePolicy({ repositoryId: "missing/pipelines" }), + /candidate.tag is required/, +); + +console.log("repository-compute-sandbox-policy-guard tests passed");