feat(terminal-demo): prove hardened renderer runtime#460
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a hardened, reproducible “README terminal demo” renderer feasibility tool (Dockerized runtime + sandbox boundary) and a strict v1 manifest contract (typed YAML decoding + structural validation + synchronized JSON Schema) under tools/readme-terminal-demo/.
Changes:
- Introduces a Landlock ABI3-based restricted exec boundary (raw exec path, FD close-range cleanup, process-group cleanup) with extensive structural and behavioral tests.
- Adds strict manifest v1 decoding/validation (resource bounds, YAML preflight, typed decode, Reader-based input existence checks) plus JSON Schema parity fixtures.
- Adds reproducible, pinned toolchain/runtime assets (Dockerfile + dependency pins + release asset verification scripts + smoke tape).
Reviewed changes
Copilot reviewed 80 out of 81 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/readme-terminal-demo/.gitattributes | Git attributes to normalize whitespace handling for Go-related files. |
| tools/readme-terminal-demo/Dockerfile | Builds pinned runtime image (Chromium/VHS/ttyd/etc) and runs build-time self-checks. |
| tools/readme-terminal-demo/dependencies.env | Central pinned versions/digests/URLs/hashes for deterministic builds. |
| tools/readme-terminal-demo/go.mod | Defines module + dependencies for the renderer tool. |
| tools/readme-terminal-demo/go.sum | Records dependency checksums for reproducible builds. |
| tools/readme-terminal-demo/manifest.schema.json | JSON Schema for manifest v1, intended to stay in parity with typed validation. |
| tools/readme-terminal-demo/cmd/readme-terminal-demo/main.go | Renderer entrypoint: runtime self-test + hardened capture execution path. |
| tools/readme-terminal-demo/internal/failure/failure.go | Stable, sanitized structured failure contract (class/stage/rule + exit codes). |
| tools/readme-terminal-demo/internal/failure/failure_test.go | Tests for stable failure vocabulary and non-leakage formatting. |
| tools/readme-terminal-demo/internal/limits/limits.go | Immutable v1 resource limits for manifest/runtime behavior. |
| tools/readme-terminal-demo/internal/manifest/model.go | Manifest v1 typed model + Reader interface contract. |
| tools/readme-terminal-demo/internal/manifest/decode.go | Strict, bounded YAML decoding with preflight + typed decode + Reader-based Load. |
| tools/readme-terminal-demo/internal/manifest/validate.go | Structural validation for manifest v1 paths and alt-text constraints. |
| tools/readme-terminal-demo/internal/manifest/manifest_test.go | Extensive decode/validate/load tests + schema parity fixture suite. |
| tools/readme-terminal-demo/internal/sandbox/exec_linux.go | Raw exec terminal sequence + input/policy preflight helpers. |
| tools/readme-terminal-demo/internal/sandbox/fds_linux.go | FD cleanup primitives (close-range wrapper + production/test seam). |
| tools/readme-terminal-demo/internal/sandbox/landlock_linux.go | Landlock ABI3 policy preparation (including private devpts PTY allowance). |
| tools/readme-terminal-demo/internal/sandbox/landlock_syscall_linux.go | Production syscall wrappers used by the Landlock preflight. |
| tools/readme-terminal-demo/internal/sandbox/process_linux.go | Process-group runner with timeout + cleanup + non-reaping observation. |
| tools/readme-terminal-demo/internal/sandbox/launcher_structure_test.go | AST + escape-analysis tests to enforce “terminal sequence” invariants. |
| tools/readme-terminal-demo/internal/sandbox/sandbox_test.go | Behavioral tests for group cleanup, escaped pipe holder bounding, and device boundary. |
| tools/readme-terminal-demo/scripts/bootstrap-gh.sh | Pinned GitHub CLI bootstrap (download/copy, verify, extract, run). |
| tools/readme-terminal-demo/scripts/bootstrap-gh_test.sh | Tests pinned GH bootstrap execution and version output. |
| tools/readme-terminal-demo/scripts/in-go-image.sh | Contributor wrapper to run Go commands in pinned Docker toolchain. |
| tools/readme-terminal-demo/scripts/in-go-image_test.sh | Tests toolchain immutability and read-only source mount behavior. |
| tools/readme-terminal-demo/scripts/verify-gh-assets.sh | Verifies pinned GH CLI release bytes via checksums before extraction. |
| tools/readme-terminal-demo/scripts/verify-gh-assets_test.sh | Mutation tests proving GH asset verification fails when tampered. |
| tools/readme-terminal-demo/scripts/verify-release-assets.sh | Verifies pinned cosign/VHS bytes + Sigstore bundle verification. |
| tools/readme-terminal-demo/scripts/verify-release-assets_test.sh | Mutation tests proving release verification fails when tampered. |
| tools/readme-terminal-demo/testdata/smoke/readme.tape | Smoke VHS tape used to attest deterministic capture presentation. |
| tools/readme-terminal-demo/testdata/valid/manifest-minimal.yml | Minimal “valid” manifest fixture for schema/typed validation parity. |
| tools/readme-terminal-demo/testdata/valid/manifest-visible-zwj.yml | Valid manifest fixture proving visible alt text can include ZWJ. |
| tools/readme-terminal-demo/testdata/invalid/alt-bidi-format-only.yml | Invalid fixture: alt text is bidi format-only (no visible content). |
| tools/readme-terminal-demo/testdata/invalid/alt-bool.yml | Invalid fixture: alt text wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/alt-control-character.yml | Invalid fixture: alt text contains control character. |
| tools/readme-terminal-demo/testdata/invalid/alt-empty.yml | Invalid fixture: alt text empty string. |
| tools/readme-terminal-demo/testdata/invalid/alt-format-only.yml | Invalid fixture: alt text is format-only (no visible content). |
| tools/readme-terminal-demo/testdata/invalid/alt-multiline.yml | Invalid fixture: alt text is multiline. |
| tools/readme-terminal-demo/testdata/invalid/alt-null.yml | Invalid fixture: alt text wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/alt-number.yml | Invalid fixture: alt text wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/fixtures-bool.yml | Invalid fixture: fixtures path wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/fixtures-null.yml | Invalid fixture: fixtures path wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/fixtures-number.yml | Invalid fixture: fixtures path wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/fixtures-wrong-root.yml | Invalid fixture: fixtures path outside required .github/demos root. |
| tools/readme-terminal-demo/testdata/invalid/gif-nested-empty-stem.yml | Invalid fixture: GIF output path has empty filename stem. |
| tools/readme-terminal-demo/testdata/invalid/gif-wrong-root.yml | Invalid fixture: GIF output path outside required docs/assets root. |
| tools/readme-terminal-demo/testdata/invalid/missing-fixtures.yml | Invalid fixture: missing fixtures field. |
| tools/readme-terminal-demo/testdata/invalid/missing-output-gif.yml | Invalid fixture: missing outputs.gif field. |
| tools/readme-terminal-demo/testdata/invalid/missing-output-png.yml | Invalid fixture: missing outputs.png field. |
| tools/readme-terminal-demo/testdata/invalid/missing-outputs.yml | Invalid fixture: missing outputs object. |
| tools/readme-terminal-demo/testdata/invalid/missing-readme-alt.yml | Invalid fixture: missing readme.alt field. |
| tools/readme-terminal-demo/testdata/invalid/missing-readme-path.yml | Invalid fixture: missing readme.path field. |
| tools/readme-terminal-demo/testdata/invalid/missing-readme.yml | Invalid fixture: missing readme object. |
| tools/readme-terminal-demo/testdata/invalid/missing-scenario.yml | Invalid fixture: missing scenario field. |
| tools/readme-terminal-demo/testdata/invalid/missing-version.yml | Invalid fixture: missing version field. |
| tools/readme-terminal-demo/testdata/invalid/output-gif-bool.yml | Invalid fixture: outputs.gif wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/output-gif-null.yml | Invalid fixture: outputs.gif wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/output-gif-number.yml | Invalid fixture: outputs.gif wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/output-png-bool.yml | Invalid fixture: outputs.png wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/output-png-null.yml | Invalid fixture: outputs.png wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/output-png-number.yml | Invalid fixture: outputs.png wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/png-nested-empty-stem.yml | Invalid fixture: PNG output path has empty filename stem. |
| tools/readme-terminal-demo/testdata/invalid/png-wrong-extension.yml | Invalid fixture: outputs.png has wrong extension. |
| tools/readme-terminal-demo/testdata/invalid/readme-nested-empty-stem.yml | Invalid fixture: readme.path has empty filename stem. |
| tools/readme-terminal-demo/testdata/invalid/readme-path-bool.yml | Invalid fixture: readme.path wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/readme-path-null.yml | Invalid fixture: readme.path wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/readme-path-number.yml | Invalid fixture: readme.path wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/readme-wrong-extension.yml | Invalid fixture: readme.path not Markdown extension. |
| tools/readme-terminal-demo/testdata/invalid/readme-wrong-root.yml | Invalid fixture: readme.path outside allowed roots. |
| tools/readme-terminal-demo/testdata/invalid/scenario-absolute.yml | Invalid fixture: scenario path absolute. |
| tools/readme-terminal-demo/testdata/invalid/scenario-bool.yml | Invalid fixture: scenario wrong scalar type (bool). |
| tools/readme-terminal-demo/testdata/invalid/scenario-dot-dot.yml | Invalid fixture: scenario contains .. traversal. |
| tools/readme-terminal-demo/testdata/invalid/scenario-nested-empty-stem.yml | Invalid fixture: scenario path has empty filename stem. |
| tools/readme-terminal-demo/testdata/invalid/scenario-null.yml | Invalid fixture: scenario wrong scalar type (null). |
| tools/readme-terminal-demo/testdata/invalid/scenario-number.yml | Invalid fixture: scenario wrong scalar type (number). |
| tools/readme-terminal-demo/testdata/invalid/scenario-wrong-extension.yml | Invalid fixture: scenario has wrong extension. |
| tools/readme-terminal-demo/testdata/invalid/unknown-key.yml | Invalid fixture: unknown manifest key present. |
| tools/readme-terminal-demo/testdata/invalid/version-2.yml | Invalid fixture: unsupported manifest version. |
| tools/readme-terminal-demo/testdata/invalid/version-fractional.yml | Invalid fixture: fractional manifest version. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 80 out of 81 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tools/readme-terminal-demo/go.mod:3
- The
godirective ingo.modshould use a language version (major.minor[.0]) rather than a specific patch toolchain version.go 1.26.5is likely to be rejected by the Go toolchain; if you want to pin an exact compiler, use thetoolchaindirective (Go 1.21+) or rely on the pinned Docker image.
go 1.26.5
- go.mod: the language version directive must be major.minor; move the exact patch pin to a separate toolchain directive. - manifest.schema.json: the alt-text and path patterns only excluded ASCII controls, so a validator that does not implement the custom z-shell-alt-bytes/z-shell-path-bytes formats could accept C1 controls and U+2028/U+2029 that the Go validators already reject. Extend both patterns to match unicode.IsControl's actual coverage. - exec_linux.go: validateExecPath allowed non-normalized absolute paths (//, /./, /../). Reject anything path.Clean would change, matching the same check already used for manifest paths.
|
Pushed 56312bc addressing all 4 open Copilot findings:
Verified: Separately, unrelated to these findings: |
…-pipe-holder invalidManifest() always passed an empty Field to failure.E, unlike unsafeManifestPath which already threads one through. Give it a field parameter and populate it at every call site that has one in scope (path opens, readme.alt validation), leaving it empty only where an error genuinely spans the whole document (empty body, multiple YAML documents, etc). Added a Field assertion to the existing UTF-8 byte limit test to cover both the newly-fixed alt-text path and the already-correct scenario path. mapChildOutcome collapsed every non-nil childErr, including exec.ErrWaitDelay from an escaped pipe holder still holding stdio open past RunRestrictedChild's wait delay, into the same generic "restricted child cleanup failed" message. Distinguish it with its own internal message (Class/Stage/Rule stay the same, so the public failure contract is unchanged) and added a test asserting both the unwrapped message and the public Error() string.
Summary
WRITE_FILEright beneath a verified private devpts mount while retaining read-only/dev, the exact/dev/nullexception, and the/dev/zerodenialRLIMIT_NOFILE=256:256, normative 960x540 smoke presentation, and deterministic runtime environmentSecurity and contract boundaries
The decisive renderer invocation remains numeric non-root, no-network, read-only root, all capabilities dropped,
no-new-privileges, 128 PIDs, 1 GiB, 2 CPUs, exact private tmpfs mounts, no TTY, and no device or namespace injection. Landlock ABI 3 does not mediate device ioctls; this change adds no ioctl relaxation. The per-process FD limit is not a per-devpts-instance quota, so the residual short-lived allocation risk remains bounded by the combined FD/PID/memory/CPU/45-second/ephemeral-runner controls accepted in #458.The manifest decoder rejects unknown or duplicate keys, aliases/anchors, non-core tags, extra documents, excessive YAML resources, wrong scalar types, fractional versions, unsafe paths, schema divergence, and non-visible alt text before execution. Real filesystem containment and symlink safety remain delegated to the later
repopathReader implementation; Task 2 does not claim lexical symlink safety.Verification
Task 1:
go test -race ./... -count=1go vet ./...bash -n scripts/*.shlinux/amd64Buildx test targetdetected Landlock ABI: 9andreadme-terminal-demo self-test okTask 2, rerun from final head
bd6234ac:go test ./internal/failure ./internal/manifest -count=1go test ./internal/manifest -run 'TestDecodeRejects|TestValidate' -count=1go test ./... -count=1go vet ./...gofmt -lclean and committed-range diff checks cleango mod tidy -diffnow reports only the deliberately predeclared Goldmark dependency reserved by the approved plan for later README verification; Task 2 introduces no unintended module drift.Status
This remains a draft implementation PR. The reviewed hardened-runtime feasibility slice and strict manifest-contract slice are complete. Broader tape parsing, secure repository path opening, render/verify orchestration, workflow/image publication, pilots, merge, release, and pattern promotion remain in progress or maintainer-gated.
Refs #458