diff --git a/export-control-data-transfer-guard/package.json b/export-control-data-transfer-guard/package.json new file mode 100644 index 00000000..2a373528 --- /dev/null +++ b/export-control-data-transfer-guard/package.json @@ -0,0 +1,11 @@ +{ + "name": "export-control-data-transfer-guard", + "version": "1.0.0", + "description": "Deterministic export-control and restricted-data transfer readiness guard for scientific bounty challenges.", + "type": "module", + "scripts": { + "demo": "node scripts/demo.js", + "test": "node --test test/*.test.js" + }, + "license": "MIT" +} diff --git a/export-control-data-transfer-guard/readme.md b/export-control-data-transfer-guard/readme.md new file mode 100644 index 00000000..a407742e --- /dev/null +++ b/export-control-data-transfer-guard/readme.md @@ -0,0 +1,58 @@ +# Export-Control Data Transfer Guard + +This module adds an export-control and restricted-data transfer readiness guard for the Scientific Bounty System in issue #18. + +The slice focuses on a gap that appears before a global scientific challenge is published: whether the sponsor can safely open challenge materials, data rooms, participation, and prize payouts across jurisdictions. + +## Why This Matters + +Scientific bounty challenges can involve dual-use research, controlled biological material, genomic data, clinical trial data, geolocation data, or cross-border teams. A platform should not release these materials simply because a challenge has a prize and rubric. It needs a deterministic hold/revise/release check before: + +- public challenge publication, +- private data-room access, +- solver workspace provisioning, +- reviewer access, +- payout release, +- IP handoff after payment. + +## Implemented Scope + +- Detects sensitive challenge topics such as dual-use, controlled biological agent, encryption research, satellite imagery, advanced semiconductor, and autonomous weapons. +- Detects high-risk data types such as human-subject data, genomic data, clinical trial data, geolocation precision, and critical infrastructure data. +- Blocks restricted participant jurisdictions until legal review clears eligibility. +- Requires export classification for sensitive topics. +- Requires a data-use agreement for restricted data. +- Requires auditable cross-border data-room access logs. +- Requires an NDA workflow when a challenge says an NDA is needed. +- Requires payout sanctions or payout-eligibility screening before reward release. +- Generates a deterministic reviewer transfer manifest. + +## Decision Model + +- `hold`: at least one blocker exists, so publication or data-room access should not open. +- `revise`: no blockers, but missing audit controls should be remediated before launch. +- `release`: no blockers or warnings. + +## Local Validation + +```bash +npm test +npm run demo +``` + +## Files + +- `src/index.js`: no-dependency evaluator and manifest builder. +- `test/index.test.js`: node:test coverage for hold, revise, release, and manifest generation. +- `scripts/demo.js`: emits blocked and releasable sample manifests. +- `reports/export-control-transfer-report.md`: reviewer-facing summary. +- `reports/export-control-transfer-manifest.json`: deterministic sample manifest output. + +## Out of Scope + +- Live legal advice. +- Payment processing. +- Wallets, Stripe, tax systems, or sanctions APIs. +- Private challenge data, participant PII, or credentials. +- Automated publication to external portals. + diff --git a/export-control-data-transfer-guard/reports/export-control-transfer-demo.mp4 b/export-control-data-transfer-guard/reports/export-control-transfer-demo.mp4 new file mode 100644 index 00000000..32b372c6 Binary files /dev/null and b/export-control-data-transfer-guard/reports/export-control-transfer-demo.mp4 differ diff --git a/export-control-data-transfer-guard/reports/export-control-transfer-manifest.json b/export-control-data-transfer-guard/reports/export-control-transfer-manifest.json new file mode 100644 index 00000000..7ff6186d --- /dev/null +++ b/export-control-data-transfer-guard/reports/export-control-transfer-manifest.json @@ -0,0 +1,98 @@ +{ + "blocked": { + "challengeId": "bio-agent-open-prize", + "generatedAt": "2026-06-05T12:40:00.000Z", + "decision": "hold", + "dataRoom": { + "crossBorder": true, + "dataTypes": [ + "genomic-data", + "clinical-trial-data" + ], + "accessLogRequired": true + }, + "eligibility": { + "participantCountries": [ + "US", + "DE", + "IR" + ], + "restrictedJurisdictionPresent": true, + "payoutScreeningRequired": true + }, + "controls": { + "exportClassification": "", + "dataUseAgreement": false, + "dataRoomAccessLog": false, + "ndaWorkflow": false, + "payoutSanctionsScreening": false + }, + "findings": [ + { + "severity": "blocker", + "code": "missing-export-classification", + "message": "Sensitive challenge topics require export classification before release: controlled-biological-agent.", + "remediation": "Hold publication until a responsible reviewer records export-control classification or confirms the challenge is not controlled." + }, + { + "severity": "blocker", + "code": "restricted-participant-jurisdiction", + "message": "Participant country list includes restricted jurisdictions: IR.", + "remediation": "Do not open the challenge to these participants until legal review approves eligibility and payout routing." + }, + { + "severity": "blocker", + "code": "missing-data-use-agreement", + "message": "Restricted or privacy-sensitive data types require a data-use agreement: genomic-data, clinical-trial-data.", + "remediation": "Require a signed data-use agreement before granting access to challenge data or submission workspaces." + }, + { + "severity": "warning", + "code": "missing-transfer-audit-log", + "message": "Cross-border participation or shared data rooms require an auditable access log.", + "remediation": "Enable immutable data-room access logging before releasing controlled datasets." + }, + { + "severity": "warning", + "code": "missing-nda-workflow", + "message": "Challenge requires NDA handling, but no NDA workflow is attached.", + "remediation": "Attach NDA routing, acceptance timestamps, and revocation steps before submissions open." + }, + { + "severity": "warning", + "code": "missing-payout-screening", + "message": "Prize payout is configured without sanctions or payout-eligibility screening.", + "remediation": "Screen recipients before reward release and record the screening decision with the payout manifest." + } + ] + }, + "releasable": { + "challengeId": "open-climate-forecast", + "generatedAt": "2026-06-05T12:40:00.000Z", + "decision": "release", + "dataRoom": { + "crossBorder": false, + "dataTypes": [ + "public-weather-data" + ], + "accessLogRequired": false + }, + "eligibility": { + "participantCountries": [ + "US", + "CA", + "GB" + ], + "restrictedJurisdictionPresent": false, + "payoutScreeningRequired": true + }, + "controls": { + "exportClassification": "public-ear99-confirmed", + "dataUseAgreement": true, + "dataRoomAccessLog": true, + "ndaWorkflow": true, + "payoutSanctionsScreening": true + }, + "findings": [] + } +} diff --git a/export-control-data-transfer-guard/reports/export-control-transfer-report.md b/export-control-data-transfer-guard/reports/export-control-transfer-report.md new file mode 100644 index 00000000..9409e822 --- /dev/null +++ b/export-control-data-transfer-guard/reports/export-control-transfer-report.md @@ -0,0 +1,51 @@ +# Export-Control Data Transfer Guard Report + +Generated: 2026-06-05T12:40:00.000Z + +## Reviewer Summary + +This guard prevents a Scientific Bounty System challenge from opening data-room access, solver workspaces, or reward release before export-control and restricted-data transfer risks are cleared. + +## Blocked Sample + +Challenge: `bio-agent-open-prize` + +Decision: `hold` + +Blockers: + +- Missing export classification for `controlled-biological-agent`. +- Restricted participant jurisdiction present: `IR`. +- Missing data-use agreement for `genomic-data` and `clinical-trial-data`. + +Warnings: + +- Missing cross-border transfer audit log. +- Missing NDA workflow. +- Missing payout sanctions or payout-eligibility screening. + +Required action: + +Resolve all blocker findings before challenge publication or data-room access. Add audit controls before opening private workspaces or payout routing. + +## Releasable Sample + +Challenge: `open-climate-forecast` + +Decision: `release` + +Reason: + +The challenge uses public weather data, has no restricted topic hits, records export classification, includes a data-use agreement, enables access logging, and screens payout eligibility. + +## Issue #18 Fit + +This maps to the Scientific Bounty System requirements by adding a pre-publication trust gate for: + +- challenge posting readiness, +- secure submission workspaces, +- private data-room access, +- arbitration evidence, +- prize payout routing, +- IP handoff after payment. + diff --git a/export-control-data-transfer-guard/reports/summary.svg b/export-control-data-transfer-guard/reports/summary.svg new file mode 100644 index 00000000..46999e37 --- /dev/null +++ b/export-control-data-transfer-guard/reports/summary.svg @@ -0,0 +1,25 @@ + + Export-control transfer guard summary + A three stage hold revise release flow for scientific bounty challenge data transfer readiness. + + Export-Control Data Transfer Guard + Pre-publication risk check for global scientific bounty challenges + + + HOLD + Controlled topics, restricted + jurisdictions, missing DUAs + + + + REVISE + No blockers, but audit or + NDA controls are missing + + + + RELEASE + Classification, DUA, logging, + NDA, and payout checks pass + + diff --git a/export-control-data-transfer-guard/scripts/demo.js b/export-control-data-transfer-guard/scripts/demo.js new file mode 100644 index 00000000..d69f86b0 --- /dev/null +++ b/export-control-data-transfer-guard/scripts/demo.js @@ -0,0 +1,13 @@ +import { + buildTransferManifest, + evaluateExportControlReadiness, + sampleChallenges, +} from '../src/index.js'; + +const blockedEvaluation = evaluateExportControlReadiness(sampleChallenges.blocked); +const releasableEvaluation = evaluateExportControlReadiness(sampleChallenges.releasable); + +console.log(JSON.stringify({ + blocked: buildTransferManifest(sampleChallenges.blocked, blockedEvaluation), + releasable: buildTransferManifest(sampleChallenges.releasable, releasableEvaluation), +}, null, 2)); diff --git a/export-control-data-transfer-guard/src/index.js b/export-control-data-transfer-guard/src/index.js new file mode 100644 index 00000000..d34b418f --- /dev/null +++ b/export-control-data-transfer-guard/src/index.js @@ -0,0 +1,171 @@ +const RESTRICTED_COUNTRIES = new Set(['CU', 'IR', 'KP', 'SY', 'RU', 'BY']); +const SENSITIVE_TOPICS = new Set([ + 'dual-use', + 'controlled-biological-agent', + 'encryption-research', + 'satellite-imagery', + 'advanced-semiconductor', + 'autonomous-weapons', +]); +const HIGH_RISK_DATA_TYPES = new Set([ + 'human-subject-data', + 'genomic-data', + 'clinical-trial-data', + 'geolocation-precision', + 'critical-infrastructure', +]); + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function normalizedSet(values) { + return new Set(list(values).map((value) => String(value).trim()).filter(Boolean)); +} + +function includesAny(values, reference) { + return [...normalizedSet(values)].filter((value) => reference.has(value)); +} + +function addFinding(findings, severity, code, message, remediation) { + findings.push({ severity, code, message, remediation }); +} + +export function evaluateExportControlReadiness(challenge) { + const findings = []; + const topicHits = includesAny(challenge.topics, SENSITIVE_TOPICS); + const dataHits = includesAny(challenge.dataTypes, HIGH_RISK_DATA_TYPES); + const participantCountries = normalizedSet(challenge.participantCountries); + const restrictedParticipants = [...participantCountries].filter((country) => RESTRICTED_COUNTRIES.has(country)); + const hasCrossBorderTransfer = participantCountries.size > 1 || Boolean(challenge.crossBorderDataRoom); + const controls = challenge.controls || {}; + + if (topicHits.length > 0 && !controls.exportClassification) { + addFinding( + findings, + 'blocker', + 'missing-export-classification', + `Sensitive challenge topics require export classification before release: ${topicHits.join(', ')}.`, + 'Hold publication until a responsible reviewer records export-control classification or confirms the challenge is not controlled.' + ); + } + + if (restrictedParticipants.length > 0) { + addFinding( + findings, + 'blocker', + 'restricted-participant-jurisdiction', + `Participant country list includes restricted jurisdictions: ${restrictedParticipants.join(', ')}.`, + 'Do not open the challenge to these participants until legal review approves eligibility and payout routing.' + ); + } + + if (dataHits.length > 0 && !controls.dataUseAgreement) { + addFinding( + findings, + 'blocker', + 'missing-data-use-agreement', + `Restricted or privacy-sensitive data types require a data-use agreement: ${dataHits.join(', ')}.`, + 'Require a signed data-use agreement before granting access to challenge data or submission workspaces.' + ); + } + + if (hasCrossBorderTransfer && !controls.dataRoomAccessLog) { + addFinding( + findings, + 'warning', + 'missing-transfer-audit-log', + 'Cross-border participation or shared data rooms require an auditable access log.', + 'Enable immutable data-room access logging before releasing controlled datasets.' + ); + } + + if (challenge.requiresNda && !controls.ndaWorkflow) { + addFinding( + findings, + 'warning', + 'missing-nda-workflow', + 'Challenge requires NDA handling, but no NDA workflow is attached.', + 'Attach NDA routing, acceptance timestamps, and revocation steps before submissions open.' + ); + } + + if (challenge.prizeAmount > 0 && !controls.payoutSanctionsScreening) { + addFinding( + findings, + 'warning', + 'missing-payout-screening', + 'Prize payout is configured without sanctions or payout-eligibility screening.', + 'Screen recipients before reward release and record the screening decision with the payout manifest.' + ); + } + + const blockers = findings.filter((finding) => finding.severity === 'blocker'); + const warnings = findings.filter((finding) => finding.severity === 'warning'); + + return { + challengeId: challenge.id, + decision: blockers.length > 0 ? 'hold' : warnings.length > 0 ? 'revise' : 'release', + blockers: blockers.length, + warnings: warnings.length, + findings, + releaseConditions: blockers.length > 0 + ? ['Resolve all blocker findings before challenge publication or data-room access.'] + : warnings.map((finding) => finding.remediation), + }; +} + +export function buildTransferManifest(challenge, evaluation) { + return { + challengeId: challenge.id, + generatedAt: '2026-06-05T12:40:00.000Z', + decision: evaluation.decision, + dataRoom: { + crossBorder: Boolean(challenge.crossBorderDataRoom), + dataTypes: list(challenge.dataTypes), + accessLogRequired: evaluation.findings.some((finding) => finding.code === 'missing-transfer-audit-log'), + }, + eligibility: { + participantCountries: list(challenge.participantCountries), + restrictedJurisdictionPresent: evaluation.findings.some((finding) => finding.code === 'restricted-participant-jurisdiction'), + payoutScreeningRequired: Boolean(challenge.prizeAmount), + }, + controls: challenge.controls || {}, + findings: evaluation.findings, + }; +} + +export const sampleChallenges = { + blocked: { + id: 'bio-agent-open-prize', + topics: ['controlled-biological-agent', 'ml-modeling'], + dataTypes: ['genomic-data', 'clinical-trial-data'], + participantCountries: ['US', 'DE', 'IR'], + crossBorderDataRoom: true, + requiresNda: true, + prizeAmount: 250000, + controls: { + exportClassification: '', + dataUseAgreement: false, + dataRoomAccessLog: false, + ndaWorkflow: false, + payoutSanctionsScreening: false, + }, + }, + releasable: { + id: 'open-climate-forecast', + topics: ['climate-modeling'], + dataTypes: ['public-weather-data'], + participantCountries: ['US', 'CA', 'GB'], + crossBorderDataRoom: false, + requiresNda: false, + prizeAmount: 10000, + controls: { + exportClassification: 'public-ear99-confirmed', + dataUseAgreement: true, + dataRoomAccessLog: true, + ndaWorkflow: true, + payoutSanctionsScreening: true, + }, + }, +}; diff --git a/export-control-data-transfer-guard/test/index.test.js b/export-control-data-transfer-guard/test/index.test.js new file mode 100644 index 00000000..9e20a85b --- /dev/null +++ b/export-control-data-transfer-guard/test/index.test.js @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + buildTransferManifest, + evaluateExportControlReadiness, + sampleChallenges, +} from '../src/index.js'; + +test('holds challenges with controlled topics, sensitive data, and restricted jurisdictions', () => { + const result = evaluateExportControlReadiness(sampleChallenges.blocked); + + assert.equal(result.decision, 'hold'); + assert.equal(result.blockers, 3); + assert.ok(result.findings.some((finding) => finding.code === 'missing-export-classification')); + assert.ok(result.findings.some((finding) => finding.code === 'missing-data-use-agreement')); + assert.ok(result.findings.some((finding) => finding.code === 'restricted-participant-jurisdiction')); +}); + +test('returns revise when blockers are resolved but audit controls are incomplete', () => { + const challenge = { + ...sampleChallenges.blocked, + participantCountries: ['US', 'DE'], + controls: { + exportClassification: 'requires-controlled-data-room', + dataUseAgreement: true, + dataRoomAccessLog: false, + ndaWorkflow: true, + payoutSanctionsScreening: true, + }, + }; + + const result = evaluateExportControlReadiness(challenge); + + assert.equal(result.decision, 'revise'); + assert.equal(result.blockers, 0); + assert.equal(result.warnings, 1); + assert.equal(result.findings[0].code, 'missing-transfer-audit-log'); +}); + +test('releases public challenges when all controls are present', () => { + const result = evaluateExportControlReadiness(sampleChallenges.releasable); + + assert.equal(result.decision, 'release'); + assert.equal(result.blockers, 0); + assert.equal(result.warnings, 0); + assert.deepEqual(result.findings, []); +}); + +test('builds a deterministic transfer manifest for reviewer audit', () => { + const evaluation = evaluateExportControlReadiness(sampleChallenges.blocked); + const manifest = buildTransferManifest(sampleChallenges.blocked, evaluation); + + assert.equal(manifest.generatedAt, '2026-06-05T12:40:00.000Z'); + assert.equal(manifest.challengeId, 'bio-agent-open-prize'); + assert.equal(manifest.decision, 'hold'); + assert.equal(manifest.dataRoom.accessLogRequired, true); + assert.equal(manifest.eligibility.restrictedJurisdictionPresent, true); + assert.equal(manifest.eligibility.payoutScreeningRequired, true); +});