-
Notifications
You must be signed in to change notification settings - Fork 0
Phase B remainder: daemon scoping, limits identity, staging tests, attestation, CI image #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| name: Plugin image | ||
|
|
||
| # Builds the Phase B2 plugin sandbox image and runs the live container | ||
| # backend test against the digest-pinned reference — so the container path | ||
| # is exercised in CI, not only on maintainer machines. | ||
| # | ||
| # No registry publication here: the image is pushed to a job-local registry | ||
| # and its digest is consumed in the same job. Publishing a pinned image for | ||
| # hosted fleets (GHCR) is a separate commercial decision; see | ||
| # docs/ISOLATION_MODEL.md. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - "docker/plugin.Dockerfile" | ||
| - "src/or_audit/eval/plugins.py" | ||
| - "src/or_audit/eval/plugin_host.py" | ||
| - "src/or_audit/eval/contracts.py" | ||
| - "tests/test_eval_plugin_host.py" | ||
| - ".github/workflows/plugin-image.yml" | ||
| pull_request: | ||
| paths: | ||
| - "docker/plugin.Dockerfile" | ||
| - "src/or_audit/eval/plugins.py" | ||
| - "src/or_audit/eval/plugin_host.py" | ||
| - "src/or_audit/eval/contracts.py" | ||
| - "tests/test_eval_plugin_host.py" | ||
| - ".github/workflows/plugin-image.yml" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: plugin-image-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build-and-test: | ||
| name: Build sandbox image, test live backend | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | ||
| - name: Start job-local registry | ||
| run: docker run -d -p 5000:5000 --name ci-registry registry:2 | ||
| - name: Build and push sandbox image | ||
| run: | | ||
| docker build --load -f docker/plugin.Dockerfile -t localhost:5000/surgeval-plugin:ci . | ||
| docker push localhost:5000/surgeval-plugin:ci | ||
| - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 | ||
| with: | ||
| enable-cache: true | ||
| - name: Install | ||
| run: uv sync --all-extras | ||
| - name: Resolve digest-pinned reference | ||
| id: ref | ||
| run: | | ||
| DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' localhost:5000/surgeval-plugin:ci) | ||
| case "$DIGEST" in | ||
| localhost:5000/surgeval-plugin@sha256:*) ;; | ||
| *) echo "Unexpected RepoDigest: $DIGEST" >&2; exit 1 ;; | ||
| esac | ||
| echo "value=$DIGEST" >> "$GITHUB_OUTPUT" | ||
| echo "Sandbox image: $DIGEST" >> "$GITHUB_STEP_SUMMARY" | ||
| - name: Live container backend test | ||
| env: | ||
| SURGEVAL_TEST_PLUGIN_IMAGE: ${{ steps.ref.outputs.value }} | ||
| run: uv run pytest tests/test_eval_plugin_host.py -q |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """Executor attestation for hosted evaluation runs (Phase B5, OSS side). | ||
|
|
||
| Local job heads are unkeyed digests: tamper-evident, re-stampable. Cross-lab | ||
| trust needs the executor to stamp what it observed with a key the submitter | ||
| does not hold. This module defines the stamp and its verification; minting | ||
| happens in the hosted executor (private cloud tree), which holds the | ||
| operator secret. Nothing here mints without a secret, and nothing verifies | ||
| without the same one. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import hmac | ||
| import json | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
|
|
||
| def _canonical(payload: dict[str, Any]) -> bytes: | ||
| return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") | ||
|
|
||
|
|
||
| class ExecutorAttestation(BaseModel): | ||
| """HMAC stamp over observed execution provenance.""" | ||
|
|
||
| model_config = ConfigDict(frozen=True, extra="forbid") | ||
|
|
||
| executor_id: str = Field(min_length=1, max_length=128) | ||
| artifact_head: str = Field(min_length=64, max_length=64, pattern="^[0-9a-f]{64}$") | ||
| backend: str = Field(min_length=1, max_length=32) | ||
| world_pin: str = Field(min_length=1, max_length=128) | ||
| nonce: str = Field(min_length=8, max_length=128) | ||
| mac: str = Field(min_length=64, max_length=64, pattern="^[0-9a-f]{64}$") | ||
|
|
||
| def payload(self) -> dict[str, Any]: | ||
| return { | ||
| "executor_id": self.executor_id, | ||
| "artifact_head": self.artifact_head, | ||
| "backend": self.backend, | ||
| "world_pin": self.world_pin, | ||
| "nonce": self.nonce, | ||
| } | ||
|
|
||
|
|
||
| def attest( | ||
| *, | ||
| executor_id: str, | ||
| artifact_head: str, | ||
| backend: str, | ||
| world_pin: str, | ||
| nonce: str, | ||
| secret: bytes, | ||
| ) -> ExecutorAttestation: | ||
| """Mint an attestation. Called by the hosted executor, never by evaluated code.""" | ||
| if not secret: | ||
| raise ValueError("attestation needs a non-empty operator secret") | ||
| stamp = ExecutorAttestation( | ||
| executor_id=executor_id, | ||
| artifact_head=artifact_head, | ||
| backend=backend, | ||
| world_pin=world_pin, | ||
| nonce=nonce, | ||
| mac="0" * 64, | ||
| ) | ||
| mac = hmac.new(secret, _canonical(stamp.payload()), hashlib.sha256).hexdigest() | ||
| return stamp.model_copy(update={"mac": mac}) | ||
|
|
||
|
|
||
| def verify(attestation: ExecutorAttestation, *, secret: bytes) -> bool: | ||
| """Check the stamp against the operator secret (constant-time).""" | ||
| if not secret: | ||
| return False | ||
| expected = hmac.new(secret, _canonical(attestation.payload()), hashlib.sha256).hexdigest() | ||
| return hmac.compare_digest(expected, attestation.mac) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """Executor attestation contract: minting needs the secret, forgery fails.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from or_audit.eval.attestation import attest, verify | ||
|
|
||
|
|
||
| def test_attest_verify_roundtrip() -> None: | ||
| stamp = attest( | ||
| executor_id="machine0-eu-1", | ||
| artifact_head="a" * 64, | ||
| backend="real", | ||
| world_pin="b" * 40, | ||
| nonce="nonce-1234", | ||
| secret=b"operator-secret", | ||
| ) | ||
| assert verify(stamp, secret=b"operator-secret") is True | ||
| assert verify(stamp, secret=b"wrong-secret") is False | ||
| assert verify(stamp, secret=b"") is False | ||
|
|
||
|
|
||
| def test_tampered_stamp_fails_verification() -> None: | ||
| stamp = attest( | ||
| executor_id="machine0-eu-1", | ||
| artifact_head="a" * 64, | ||
| backend="real", | ||
| world_pin="b" * 40, | ||
| nonce="nonce-1234", | ||
| secret=b"operator-secret", | ||
| ) | ||
| forged = stamp.model_copy(update={"backend": "synthetic-stub"}) | ||
| assert verify(forged, secret=b"operator-secret") is False | ||
| upgraded = stamp.model_copy(update={"artifact_head": "c" * 64}) | ||
| assert verify(upgraded, secret=b"operator-secret") is False | ||
|
|
||
|
|
||
| def test_minting_needs_a_secret() -> None: | ||
| with pytest.raises(ValueError, match="non-empty operator secret"): | ||
| attest( | ||
| executor_id="e", | ||
| artifact_head="a" * 64, | ||
| backend="real", | ||
| world_pin="b" * 40, | ||
| nonce="nonce-1234", | ||
| secret=b"", | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] [security] Validate container limits from untrusted packages
RuntimeDescriptoris loaded fromagent.toml, so untrusted submissions can now setcontainer_memory/container_cpus/container_pids_limitto values that disable or greatly expand resource caps (for examplecontainer_pids_limit="-1"), enabling host DoS when the container runtime is used; enforce these limits via trusted policy (clamp/validate to safe ranges, or require they match executor-configured limits) before passing them intodocker run.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in this branch: digest resolved via docker inspect RepoDigests with @sha256: validation; container limits clamped executor-side (64m..16g, 0.1..16, 16..4096) with refusal tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already implemented in this branch: _clamp_container_limits is invoked in _container_command (plugins.py) with executor-side bounds and refusal tests. No change made.