From 6c1b433829ebde8e48627482afabd0b20f058132 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 23 May 2026 01:51:52 +0200 Subject: [PATCH 1/2] Add repository compute sandbox guard --- README.md | 2 + .../README.md | 49 ++ .../demo.js | 27 + .../index.js | 489 ++++++++++++++++++ .../package.json | 13 + .../reports/demo.json | 454 ++++++++++++++++ .../reports/demo.md | 43 ++ .../reports/demo.mp4 | Bin 0 -> 35632 bytes .../reports/demo.svg | 43 ++ .../sample-data.js | 151 ++++++ .../scripts/render-demo-video.js | 83 +++ .../test.js | 73 +++ 12 files changed, 1427 insertions(+) create mode 100644 repository-compute-sandbox-policy-guard/README.md create mode 100644 repository-compute-sandbox-policy-guard/demo.js create mode 100644 repository-compute-sandbox-policy-guard/index.js create mode 100644 repository-compute-sandbox-policy-guard/package.json create mode 100644 repository-compute-sandbox-policy-guard/reports/demo.json create mode 100644 repository-compute-sandbox-policy-guard/reports/demo.md create mode 100644 repository-compute-sandbox-policy-guard/reports/demo.mp4 create mode 100644 repository-compute-sandbox-policy-guard/reports/demo.svg create mode 100644 repository-compute-sandbox-policy-guard/sample-data.js create mode 100644 repository-compute-sandbox-policy-guard/scripts/render-demo-video.js create mode 100644 repository-compute-sandbox-policy-guard/test.js 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..4e08c7a8 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/index.js @@ -0,0 +1,489 @@ +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.every((host) => policy.doiResolverAllowlist.includes(host)); + const passed = allowedMode && doiOnly; + addCheck(pipeline.id, "NETWORK_EGRESS_POLICY", passed, mode); + + 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_${mount.path || "missing"}`, + 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..9c3e2157 --- /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": "doi-resolution-only" + }, + { + "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_/tmp/scibase-run/gpu-ensemble-replay", + "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": "none" + }, + { + "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_/tmp/scibase-run/analysis-primary", + "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": "open-internet" + }, + { + "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_/workspace", + "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 0000000000000000000000000000000000000000..e89249b291857880cae74af032485f78fd98bf29 GIT binary patch literal 35632 zcmYIuV|XS_(C!o4wr$(a#@=vaI~&{F*tTukwr$%wdB3A;X1edLx~saXx@Z3M0000& z6DK!2b4Oci000>9U;pzk>pL4VS=+HP0RR9<69;2s03d+O+DPB&hf@s>_WfPHA$Hn% zv?|%0M7u(`N^*7W%FND6NJnUB>tIaC^aCAOm>Ag!jrH}}IZgC`6q58m1O{0JF$p?W zLLoJgAEuG9;g3SZ*3Qk!*u;sDnURr&j+v2}c0pTRSInTN_7iLPLE6eM3GbLI-0LJ{CeFV*@K&LrXp;ZboiKLVX*3 zD>p}DK1Np-ZbnxoCKf_#V?HxuS3<|X20w^{(9Y5A$MmPwb1>p#qG$Xu`e_KQ&0UR+ z^#1e6^kbptpl@Sp%*Vt|XlUkOYpt*MW6DJ6u12n z%tFsdX#Za_e2nz$KhW+!$N$soyYjJf{RkYLjP3YX3C-<(R_SL){4C;+FMa!;4e;M` z0(=1g(+A_wKmgD8*M9j6>ZMK+UZuf4E2^Tmb;otZX>|Je(5FneFzU z`$*5z(Wm3@@kdqS$w(PDaT9f9HMX{l2JUyZo#b_MG(>3|!Pv7&uFwbZ-{WJXT{wyQ z8FrIB*huC4evGjtc|M55*hDfGU~n9!{_$9OjlJLhVn6!o}>MnXa@$~59q&?9|PA^~>0J;?~%udSOf zz#Vf2S{yOfr+h64omI6)j6>6wAhOpW$0jUyqeiql{vM^VxyTre>^0;8*i*SkxLYB6 zWQjuH8gJ}L?+@=rtPO@t4gsfkB&N&l&$WMx%`loR6FI)t^%+DMP2L!>Hhu1LP6G+c z(^tXZaQS8kKBS9NHhGyH$!#MbH7ucQc|aI-g$B zEeNdKr@IH~xv))(AJges^fM3Y4hCYI%XRH0*oCgZGxf+qlFc^P4p*on@JO(772&|{Zw_IgXxLq$+tu+q z9p@KBe~8fG4(JI_7@Co5W@OM$4R+;8wJlF2O~h=&m8r>Fl|%zL?`t6}O;#Iz^`}HJ zh?Y&|s4+F!44wyB**Ij<>t74h&Wh?&?dsFRIoa7gqFtZYYtH2VZgiHULDaB`e>pF< zRv+A_nr=7|Zv&j^sHSjDo{NYi%He`Uu%mHlV?m4VJUxshd;@r01qg2Lt`$wO!G_!@R znvR!mP*POgTQtatkKv6CuQ0|*o%cIZ=ZCA;k(KYY}E-yRKHo%Hy55yi9@41J+H@jo9TGrHN2~`iE8xCTAi}6yfiC zz#pmi%q8*J;1)C4S4$}2&_xOv3nxQ5kI|ER;>b34$*=K`F((G6uz>;ZGzQBguU9no zCz`Qrd!DEy>G`L^Rh3Yq*$cRdhd@O(Ga9j3i2=exD-Snf{(=IQ3ty_SXhJ8wDC z$c}t7xE}Z|R>ulOCWF5ydNGAit9HrxLjJLA`a8HLf{UqIcxgZz$K*z7i7?O_L?n_aghy1%$WHGHnpGtukpK#acQW@&wc7 zaBOm9cFU=&_z3nqRyQS;hjjgG#J~@q!K6p9SWba#+W(E+#+F03?aRkfG0F>7rryJ+ zrOZFUKLF*{uUbMryBoCc7GejKO;(+2IK7(03;i4Y0`tP}u3{O-_(s4HP>~ZOfwunB z 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 +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..d351d992 --- /dev/null +++ b/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js @@ -0,0 +1,83 @@ +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) { + return "font=Sans"; + } + 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..5e4bf0da --- /dev/null +++ b/repository-compute-sandbox-policy-guard/test.js @@ -0,0 +1,73 @@ +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", + ), +); + +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"); From a87addc3db04a1b17e3694aa178ecd30b15073df Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 23 May 2026 02:40:06 +0200 Subject: [PATCH 2/2] Address compute guard review comments --- .../index.js | 12 ++++++-- .../reports/demo.json | 12 ++++---- .../scripts/render-demo-video.js | 4 ++- .../test.js | 28 +++++++++++++++++++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/repository-compute-sandbox-policy-guard/index.js b/repository-compute-sandbox-policy-guard/index.js index 4e08c7a8..d6b73f81 100644 --- a/repository-compute-sandbox-policy-guard/index.js +++ b/repository-compute-sandbox-policy-guard/index.js @@ -254,9 +254,15 @@ function checkNetworkPolicy(pipeline, sandbox, policy, addCheck, addIssue) { const allowlist = [...(sandbox.egressAllowlist || [])].sort(); const doiOnly = mode !== "doi-resolution-only" || - allowlist.every((host) => policy.doiResolverAllowlist.includes(host)); + (allowlist.length > 0 && + allowlist.every((host) => policy.doiResolverAllowlist.includes(host))); const passed = allowedMode && doiOnly; - addCheck(pipeline.id, "NETWORK_EGRESS_POLICY", passed, mode); + addCheck( + pipeline.id, + "NETWORK_EGRESS_POLICY", + passed, + `mode=${mode}; allowlist=${allowlist.join(",") || "empty"}`, + ); if (!allowedMode || !doiOnly) { addIssue( @@ -356,7 +362,7 @@ function checkWritableMounts(pipeline, sandbox, policy, addCheck, addIssue) { const sizeOk = numberAtMost(mount.maxGb, policy.maxWritableGb); addCheck( pipeline.id, - `WRITABLE_MOUNT_${mount.path || "missing"}`, + "WRITABLE_MOUNT", allowedPath && sizeOk, `path=${mount.path}; maxGb=${mount.maxGb}`, ); diff --git a/repository-compute-sandbox-policy-guard/reports/demo.json b/repository-compute-sandbox-policy-guard/reports/demo.json index 9c3e2157..be1e72f9 100644 --- a/repository-compute-sandbox-policy-guard/reports/demo.json +++ b/repository-compute-sandbox-policy-guard/reports/demo.json @@ -35,7 +35,7 @@ "pipelineId": "gpu-ensemble-replay", "code": "NETWORK_EGRESS_POLICY", "passed": true, - "detail": "doi-resolution-only" + "detail": "mode=doi-resolution-only; allowlist=api.datacite.org,doi.org" }, { "pipelineId": "gpu-ensemble-replay", @@ -63,7 +63,7 @@ }, { "pipelineId": "gpu-ensemble-replay", - "code": "WRITABLE_MOUNT_/tmp/scibase-run/gpu-ensemble-replay", + "code": "WRITABLE_MOUNT", "passed": true, "detail": "path=/tmp/scibase-run/gpu-ensemble-replay; maxGb=2" }, @@ -143,7 +143,7 @@ "pipelineId": "analysis-primary", "code": "NETWORK_EGRESS_POLICY", "passed": true, - "detail": "none" + "detail": "mode=none; allowlist=empty" }, { "pipelineId": "analysis-primary", @@ -165,7 +165,7 @@ }, { "pipelineId": "analysis-primary", - "code": "WRITABLE_MOUNT_/tmp/scibase-run/analysis-primary", + "code": "WRITABLE_MOUNT", "passed": true, "detail": "path=/tmp/scibase-run/analysis-primary; maxGb=1.5" }, @@ -236,7 +236,7 @@ "pipelineId": "training-open-network", "code": "NETWORK_EGRESS_POLICY", "passed": false, - "detail": "open-internet" + "detail": "mode=open-internet; allowlist=example-data.invalid,pypi.org" }, { "pipelineId": "training-open-network", @@ -264,7 +264,7 @@ }, { "pipelineId": "training-open-network", - "code": "WRITABLE_MOUNT_/workspace", + "code": "WRITABLE_MOUNT", "passed": false, "detail": "path=/workspace; maxGb=12" }, diff --git a/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js b/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js index d351d992..d9c8265f 100644 --- a/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js +++ b/repository-compute-sandbox-policy-guard/scripts/render-demo-video.js @@ -77,7 +77,9 @@ function getFontArg() { ]; const font = candidates.find((candidate) => fs.existsSync(candidate)); if (!font) { - return "font=Sans"; + 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 index 5e4bf0da..0080ca78 100644 --- a/repository-compute-sandbox-policy-guard/test.js +++ b/repository-compute-sandbox-policy-guard/test.js @@ -35,6 +35,34 @@ assert.ok( (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");