Skip to content
Closed
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
113 changes: 113 additions & 0 deletions .github/workflows/sandbox-log-redaction-quality-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Sandbox Log Redaction Quality CI

on:
pull_request:
branches: [main]
paths:
- ".github/workflows/sandbox-log-redaction-quality-ci.yml"
- "ARCHITECTURE.md"
- "CHANGELOG.md"
- "docs/doctoring/sandbox-log-redaction.md"
- "scripts/ci/redact_sensitive_log.py"
- "scripts/ci/sandboxed_verify.py"
- "scripts/ci/sandboxed_web_e2e.py"
- "tests/test_atomic_json_redaction.py"
- "tests/test_command_wrapper_redaction.py"
- "tests/test_opencode_security_boundaries.py"
- "tests/test_sandboxed_verify.py"
- "tests/test_sandboxed_web_e2e.py"
- "tests/test_sandboxed_log_redaction_regression.py"

permissions:
contents: read

concurrency:
group: sandbox-log-redaction-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
exact-head-redaction-contract:
name: Exact-head sandbox redaction contract
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout exact source revision
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

- name: Install exact hash-verified test dependencies
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_INPUT: "1"
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
cat >"${RUNNER_TEMP}/sandbox-redaction-quality-requirements.txt" <<'EOF'
coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f
iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
EOF
python -m pip install \
--only-binary=:all: \
--require-hashes \
-r "${RUNNER_TEMP}/sandbox-redaction-quality-requirements.txt"

- name: Verify fail-closed sandbox redaction contract
env:
STRIX_TEST_PROCESS_TIMEOUT_SECONDS: "3"
STRIX_TEST_FAKE_SLEEP_SECONDS: "5"
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
python -m coverage run --branch -m pytest \
tests/test_atomic_json_redaction.py \
tests/test_command_wrapper_redaction.py \
tests/test_opencode_security_boundaries.py \
tests/test_sandboxed_verify.py \
tests/test_sandboxed_web_e2e.py \
tests/test_sandboxed_log_redaction_regression.py \
-q
python -m coverage report \
--include='scripts/ci/redact_sensitive_log.py,scripts/ci/sandboxed_verify.py,scripts/ci/sandboxed_web_e2e.py' \
--fail-under=100
python - <<'PY'
import ast
from pathlib import Path

missing = []
for filename in (
"scripts/ci/redact_sensitive_log.py",
"scripts/ci/sandboxed_verify.py",
"scripts/ci/sandboxed_web_e2e.py",
):
tree = ast.parse(Path(filename).read_text(encoding="utf-8"), filename=filename)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
if not node.name.startswith("_") and ast.get_docstring(node) is None:
missing.append(f"{filename}:{node.lineno}:{node.name}")
if missing:
raise SystemExit("public docstrings missing: " + ", ".join(missing))
PY
python -m pytest tests -q
bash scripts/ci/test_strix_quick_gate.sh
python -m compileall -q \
scripts/ci/redact_sensitive_log.py \
scripts/ci/sandboxed_verify.py \
scripts/ci/sandboxed_web_e2e.py \
tests/test_atomic_json_redaction.py \
tests/test_command_wrapper_redaction.py \
tests/test_opencode_security_boundaries.py \
tests/test_sandboxed_verify.py \
tests/test_sandboxed_web_e2e.py \
tests/test_sandboxed_log_redaction_regression.py
git diff --exit-code
39 changes: 38 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,30 @@ sequenceDiagram
PR->>RW: pull_request_target on trusted base
RW->>OC: bounded evidence + NVIDIA NIM / OpenCode
OC->>SV: PoC command in isolated copy
SV-->>OC: redacted stdout/stderr + command metadata
SV-->>OC: layout-preserving redacted stdout/stderr + command metadata
OC-->>PR: APPROVE or request changes
MS->>PR: merge only on current-head approval + green checks
```

## Sandbox evidence redaction

```mermaid
flowchart TD
Cap["Captured stdout / stderr / service tail"]
Span["Bounded JSON span rewriter"]
Line["Line-oriented fallback"]
Pub["CI / review evidence"]

Cap --> Span
Span -->|"complete JSON span"| Pub
Span -->|"no complete span"| Line
Line --> Pub
```

Operators reading a pretty-printed job log should still see the original
layout, duplicate keys, and scalar categories. Only credential leaves are
replaced. See [`docs/doctoring/sandbox-log-redaction.md`](docs/doctoring/sandbox-log-redaction.md).

## Trust boundaries

- Required review workflows execute **base-branch** scripts. A PR that edits
Expand All @@ -98,6 +117,22 @@ sequenceDiagram
- Logs and review receipts redact credential shapes (tokens, bearer values,
known provider prefixes). They do not mask operational PII that the
control plane must process.
- Raw JSON evidence is rewritten as source spans before any line split.
Duplicate member names keep order and count because RFC 8259 §4 treats
receiver behavior as unpredictable, while ECMA-404 / ISO/IEC 21778 leave
uniqueness to the processor (Bray, 2017; Ecma International, 2017;
International Organization for Standardization, 2017). A dictionary
collapse would drop the first secret of a duplicate `token` pair. A
failed opener is scored only until the next plausible start, so
`##[group]` and prose `[timeout]` cannot erase a later complete object.
Downloaded Actions job logs prefix every line with an RFC 3339 UTC
runner timestamp (`YYYY-MM-DDTHH:MM:SS.nnnnnnnZ `). The span parser
skips that line metadata the same way it skips JSON whitespace, so a
pretty-printed password object remains one span. A `[` opens an array
only when the next significant token can start a JSON value
(`true` / `false` / `null` / number / string / container / `]`), so
line-start `[INFO]` diagnostics stay visible (Klyne & Newman, 2002;
Bray, 2017).
- LLM and scheduled agents bind `NVIDIA_NIM_API_KEY` (env may be
`NVIDIA_API_KEY`). They never use `COPILOT_GITHUB_TOKEN`. Existing
review-agent key schemes stay unchanged.
Expand All @@ -120,6 +155,8 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for
— Project #1 operation.
- [`PR_GOVERNANCE_AUDIT.md`](PR_GOVERNANCE_AUDIT.md) — live review/merge
contract.
- [`docs/doctoring/sandbox-log-redaction.md`](docs/doctoring/sandbox-log-redaction.md)
— atomic JSON evidence redaction, RFC 8259 / ECMA-404 / ISO/IEC 21778.
- [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md)
— current increment's repair-worker decision and APA 7th citations.
- [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md)
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Semantic Versioning where the repository publishes a release.

### Security

- Preserve raw JSON layout atomically during sandbox log redaction so command wrappers cannot leak secrets through pretty-printed dumps.

- Reject `.github/` and `scripts/ci/` from review-thread-derived autofix path authority so an untrusted inline reviewer cannot authorize the write-capable repair agent to modify workflows, CODEOWNERS, actions, scheduler code, or CI helpers that govern its own control plane.
- Require the model-write snapshot and exact-path allowlist to remain outside the pull-request worktree, checking both absolute and resolved locations so repository-local controls and outside-looking symlinks resolving into the repository fail closed before they can authorize or verify model changes.
- Snapshot the complete pre-model worktree for ordinary and conflict repair and reject every model-caused created, deleted, modified, mode-changed, retargeted, ignored, dangling, directory-backed, external-link, metadata-race, or out-of-scope path before staging or push.
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ repeatable compile command.
- **Review output must go through the Python normalizer** (`scripts/ci/opencode_review_normalize_output.py`)
— it escapes `<`, `>`, `&` when embedding JSON in HTML comments to prevent Markdown-comment
breakout. Do not reintroduce bash fast-path extraction.
- **Downloaded Actions job logs keep per-line RFC 3339 runner timestamps.** `redact_sensitive_log`
skips those prefixes inside JSON spans and opens `[` only for a real JSON value, so `##[group]`
and `[INFO]` diagnostics are not fail-closed to `[REDACTED]`. See
`docs/doctoring/sandbox-log-redaction.md`.
- **Cloudflare changes are dry-run by default**; nothing is deleted unless `prune = true` is set
explicitly. PRs never see the Cloudflare API token.
- **Org-wide binding conventions** (permissive licenses only — verify SPDX before adding anything;
Expand Down
Loading
Loading