Skip to content

feat(terminal-demo): prove hardened renderer runtime#460

Merged
ss-o merged 8 commits into
mainfrom
feature-458
Jul 25, 2026
Merged

feat(terminal-demo): prove hardened renderer runtime#460
ss-o merged 8 commits into
mainfrom
feature-458

Conversation

@ss-o

@ss-o ss-o commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

  • add the pinned, non-root VHS/ttyd/Chromium renderer feasibility tool and immutable contributor toolchain
  • enforce the exact same-thread Landlock ABI 3 raw-exec boundary, inherited-descriptor cleanup, browser provenance check, and typed failure mapping
  • grant only the incremental WRITE_FILE right beneath a verified private devpts mount while retaining read-only /dev, the exact /dev/null exception, and the /dev/zero denial
  • attest the network-none runtime, process group, exact RLIMIT_NOFILE=256:256, normative 960x540 smoke presentation, and deterministic runtime environment
  • add immutable V1 resource limits, strict YAML-node manifest preflight, typed decoding, structural validation, and a synchronized JSON Schema
  • orchestrate manifest input checks only through the injected security-providing Reader, preserve sanitized typed failures, close every returned file, and never require or open output assets during structural loading

Security 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 repopath Reader implementation; Task 2 does not claim lexical symlink safety.

Verification

Task 1:

  • go test -race ./... -count=1
  • go vet ./...
  • contributor wrapper self-test and bash -n scripts/*.sh
  • process-group cleanup test, 10 consecutive runs
  • exact linux/amd64 Buildx test target
  • exact hardened Docker self-test: detected Landlock ABI: 9 and readme-terminal-demo self-test ok
  • independent final code/security review: Ready, no P0-P3 findings

Task 2, rerun from final head bd6234ac:

  • go test ./internal/failure ./internal/manifest -count=1
  • go test ./internal/manifest -run 'TestDecodeRejects|TestValidate' -count=1
  • go test ./... -count=1
  • go vet ./...
  • pinned gofmt -l clean and committed-range diff checks clean
  • fresh independent full-range re-review: Ready, all prior findings closed, no P0-P3 findings

go mod tidy -diff now 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

Copilot AI review requested due to automatic review settings July 24, 2026 22:10
@ss-o
ss-o marked this pull request as ready for review July 24, 2026 22:10
@ss-o
ss-o requested a review from a team as a code owner July 24, 2026 22:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tools/readme-terminal-demo/go.mod Outdated
Comment thread tools/readme-terminal-demo/manifest.schema.json
Comment thread tools/readme-terminal-demo/manifest.schema.json
Comment thread tools/readme-terminal-demo/internal/sandbox/exec_linux.go Outdated
Copilot AI review requested due to automatic review settings July 25, 2026 03:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 go directive in go.mod should use a language version (major.minor[.0]) rather than a specific patch toolchain version. go 1.26.5 is likely to be rejected by the Go toolchain; if you want to pin an exact compiler, use the toolchain directive (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.
Copilot AI review requested due to automatic review settings July 25, 2026 05:46
@ss-o

ss-o commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Pushed 56312bc addressing all 4 open Copilot findings:

  • go.mod (go 1.26.5): split into go 1.26 (language version, major.minor per spec) plus a separate toolchain go1.26.5 directive to keep the exact patch pin.
  • manifest.schema.json (alt-text pattern): extended the character-class exclusion from ASCII-only (\x00-\x1F, \x7F) to also cover the Unicode C1 control range (U+0080 through U+009F) and the line/paragraph separators U+2028 and U+2029, matching what internal/manifest/validate.go's unicode.IsControl plus its explicit separator check already reject. A validator that only reads this schema (without implementing the custom z-shell-alt-bytes format) would otherwise accept alt text the Go binary rejects.
  • manifest.schema.json (normalizedPath): same fix, adding the U+0080 through U+009F range so z-shell-path-bytes-unaware consumers get equivalent protection to unicode.IsControl in validateRepositoryPath. U+2028 and U+2029 were intentionally left out of the path pattern since the Go path validator doesn't reject those either (only alt text does).
  • exec_linux.go (validateExecPath): added a path.Clean(target) != target check alongside the existing checks, rejecting //, /./, /../ segments. Added a path-not-normalized case to TestPrepareExecVectors in launcher_internal_test.go.

Verified: go build ./..., go vet ./..., gofmt -l ., and trunk check are all clean. go test ./... passes for internal/manifest and internal/failure. Three tests fail in my sandbox but are pre-existing and environment-specific, not caused by this change, confirmed by running them against the unmodified origin/feature-458 tree where they fail identically: TestRawExecDeviceBoundary and the two Chromium-launcher tests in cmd/readme-terminal-demo all need real-root or capability operations (chown, raw Landlock exec boundary) this container doesn't have.

Separately, unrelated to these findings: go mod tidy -diff reports github.com/yuin/goldmark as an unused direct dependency on this branch already (nothing here imports it). Not touched, since it's outside the scope of the 4 findings, flagging in case it's worth a follow-up go mod tidy.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 81 changed files in this pull request and generated 2 comments.

Comment thread tools/readme-terminal-demo/internal/manifest/decode.go Outdated
Comment thread tools/readme-terminal-demo/cmd/readme-terminal-demo/main.go
…-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.
Copilot AI review requested due to automatic review settings July 25, 2026 07:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 81 changed files in this pull request and generated 2 comments.

Comment thread tools/readme-terminal-demo/internal/manifest/decode.go
Comment thread tools/readme-terminal-demo/Dockerfile
@ss-o
ss-o merged commit 6f6b825 into main Jul 25, 2026
5 checks passed
@ss-o
ss-o deleted the feature-458 branch July 25, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants