Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/sealed-public-execution-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Sealed Public Execution Contract

on:
pull_request:
paths:
- 'scripts/sealed_public_execution.py'
- 'tests/test_sealed_public_execution.py'
- '.github/workflows/sealed-public-execution.yml'
- '.github/workflows/sealed-public-execution-contract.yml'
- 'docs/sealed-public-execution.md'
workflow_dispatch:

permissions:
contents: read

jobs:
contract:
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false

- name: Run dependency-free contract tests
run: python3 -m unittest -v tests/test_sealed_public_execution.py

- name: Install age for round-trip test
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age >/dev/null

- name: Exercise execute-seal-decrypt round trip
shell: bash
run: |
set -euo pipefail
mkdir -p /tmp/sealed-selftest/capsule
cat > /tmp/sealed-selftest/capsule/run.sh <<'SH'
echo "captured stdout"
echo "captured stderr" >&2
printf 'round-trip-ok\n' > "$SEALED_RESULT_DIR/out.txt"
SH
tar -C /tmp/sealed-selftest/capsule -czf /tmp/sealed-selftest/capsule.tar.gz run.sh
capsule_sha=$(sha256sum /tmp/sealed-selftest/capsule.tar.gz | awk '{print $1}')
capsule_b64=$(base64 -w0 /tmp/sealed-selftest/capsule.tar.gz)

age-keygen -o /tmp/sealed-selftest/identity.txt >/dev/null 2>&1
recipient=$(awk '/# public key:/ {print $4}' /tmp/sealed-selftest/identity.txt)
test -n "$recipient"

python3 scripts/sealed_public_execution.py \
--assignment-id selftest-001 \
--capsule-b64 "$capsule_b64" \
--capsule-sha256 "$capsule_sha" \
--recipient "$recipient" \
--timeout-seconds 30 \
--out-dir /tmp/sealed-selftest/sealed

age --decrypt \
--identity /tmp/sealed-selftest/identity.txt \
--output /tmp/sealed-selftest/result.tar.gz \
/tmp/sealed-selftest/sealed/result.age
mkdir /tmp/sealed-selftest/result
tar -C /tmp/sealed-selftest/result -xzf /tmp/sealed-selftest/result.tar.gz

grep -qx 'captured stdout' /tmp/sealed-selftest/result/stdout.txt
grep -qx 'captured stderr' /tmp/sealed-selftest/result/stderr.txt
grep -qx 'round-trip-ok' /tmp/sealed-selftest/result/files/out.txt
python3 - <<'PY'
import json
from pathlib import Path
receipt = json.loads(Path('/tmp/sealed-selftest/sealed/receipt.json').read_text())
assert receipt['assignment_id'] == 'selftest-001'
assert receipt['status'] == 'completed'
assert len(receipt['sealed_sha256']) == 64
PY
88 changes: 88 additions & 0 deletions .github/workflows/sealed-public-execution.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Sealed Public Execution
run-name: Sealed execution ${{ inputs.assignment_id }}

on:
workflow_dispatch:
inputs:
assignment_id:
description: "Opaque assignment correlation ID"
required: true
type: string
capsule_b64:
description: "Base64 gzip-tar execution capsule; public-safe projection only"
required: true
type: string
capsule_sha256:
description: "Lowercase SHA-256 of decoded capsule bytes"
required: true
type: string
recipient:
description: "Ephemeral age X25519 public recipient (age1...)"
required: true
type: string
timeout_seconds:
description: "Task timeout, max 7200 seconds"
required: false
default: "3600"
type: string

permissions:
contents: read

jobs:
execute:
runs-on: ubuntu-latest
timeout-minutes: 125
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false

- name: Install result sealer
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age >/dev/null

- name: Execute public-safe capsule and seal evidence
id: execute
shell: bash
env:
ASSIGNMENT_ID: ${{ inputs.assignment_id }}
CAPSULE_B64: ${{ inputs.capsule_b64 }}
CAPSULE_SHA256: ${{ inputs.capsule_sha256 }}
AGE_RECIPIENT: ${{ inputs.recipient }}
TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }}
run: |
set +e
python3 scripts/sealed_public_execution.py \
--assignment-id "$ASSIGNMENT_ID" \
--capsule-b64 "$CAPSULE_B64" \
--capsule-sha256 "$CAPSULE_SHA256" \
--recipient "$AGE_RECIPIENT" \
--timeout-seconds "$TIMEOUT_SECONDS" \
--out-dir .sealed
rc=$?
echo "task_rc=$rc" >> "$GITHUB_OUTPUT"
exit 0

- name: Upload sealed result mailbox
if: always() && hashFiles('.sealed/result.age', '.sealed/receipt.json') != ''
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: sealed-${{ inputs.assignment_id }}
path: |
.sealed/result.age
.sealed/receipt.json
if-no-files-found: error
retention-days: 1
compression-level: 0

- name: Preserve task verdict
if: always()
shell: bash
env:
TASK_RC: ${{ steps.execute.outputs.task_rc }}
run: |
test -n "$TASK_RC" || exit 1
exit "$TASK_RC"
75 changes: 75 additions & 0 deletions docs/sealed-public-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Sealed public execution

Status: experimental public-safe execution adapter.

## Purpose

Use standard GitHub-hosted Actions capacity in this public repository for work that a trusted/private authority has already determined is safe to project into a public runner, while keeping substantive result evidence out of Git history and returning it to the trusted side as short-lived ciphertext.

This adapter is an execution/materialization mechanism only. It does not own project intent, acceptance truth, research state, task prioritization, or declassification decisions.

## Boundary

Trusted/private side responsibilities:

1. resolve the originating project-native authority;
2. determine whether the work may execute on a public runner;
3. derive the least-sufficient public-safe capsule;
4. generate a per-assignment age X25519 keypair and retain the private identity outside GitHub;
5. dispatch the public workflow with only the opaque assignment ID, capsule, capsule digest, public recipient, and bounded timeout;
6. retrieve the encrypted Actions artifact, verify the receipt/ciphertext digest, decrypt privately, and reconcile useful evidence back to the originating authority;
7. delete the public artifact/run when it no longer has diagnostic value.

Public runner responsibilities:

1. verify the capsule digest and strict archive bounds;
2. execute only the capsule's top-level `run.sh` with no private credentials;
3. capture task stdout/stderr into the private result bundle rather than Actions logs;
4. collect files written beneath `SEALED_RESULT_DIR`;
5. package and encrypt the result to the supplied age recipient;
6. upload only `result.age` plus a minimal `receipt.json` as a one-day Actions artifact;
7. preserve task success/failure in the workflow verdict.

## Capsule contract

The workflow accepts a gzip-compressed tar archive encoded as base64. Current hard bounds are intentionally small:

- base64 input: at most 60,000 characters;
- archive entries: at most 256;
- unpacked content: at most 16 MiB;
- no symlinks, hardlinks, devices, or path traversal;
- a regular top-level `run.sh` is required;
- task timeout is at most 7,200 seconds.

At runtime the worker sets:

- `SEALED_ASSIGNMENT_ID` — opaque correlation identity;
- `SEALED_RESULT_DIR` — directory for substantive result files.

The capsule itself is public-observable execution material. Do not put information in it that the originating authority has not approved for public-runner exposure.

## Result contract

The public artifact contains only:

- `result.age` — age-encrypted gzip tar containing execution metadata, captured stdout/stderr, and result files;
- `receipt.json` — opaque assignment ID, completed/failed status, ciphertext SHA-256/size, public worker revision, and Actions run correlation.

Retention is set to one day. Private reconciliation should normally delete the artifact sooner after successful pickup/decryption.

The artifact is transport, not durable project authority. Durable conclusions, accepted evidence, negative results, or follow-on decisions belong back in the originating private/project-native authority.

## Security / value-preservation invariants

- No private credential is required by the execution workflow.
- No private repository checkout occurs in this lane.
- The private decryption identity never enters GitHub.
- No plaintext result artifact is uploaded.
- Task stdout/stderr are not intentionally emitted to Actions logs.
- Public runner visibility of the projected capsule is accepted by the originating authority before dispatch.
- The public repository must not accumulate experiment interpretation, private hypotheses, result corpora, or project-specific research history merely because it provided compute.
- A second project does not need to adopt this adapter unless its own authority chooses to use it.

## Non-goals

This increment does not add a scheduler, task database, queue, generic provider registry, automatic public/private classifier, result archive, or new portfolio authority. It also does not make `agent-dispatch` mandatory for interactive/local execution paths.
Loading
Loading