Skip to content

Network/read-only invariant enforcement suite #31

Description

@Saber5656

Title

Network/read-only invariant enforcement suite

Summary

Turn invariants I1–I3 into a permanent, CI-enforced test suite: the
import-boundary script hardened, whole-binary no-network proof, proxy-env
immunity, read-only tree hashing across all commands, and write-confinement
checks — the "trust but verify" layer over everything built in 01–29.

Context

Individual issues each test their slice; this issue adds the cross-cutting
proofs a security reviewer expects from a public repository (the repo is
already public), and pins them in CI so future changes cannot silently
regress the product's core promises (ADR-002/003).

Scope

  • internal/invariants/invariants_test.go (build-tag invariants, run in
    CI as a dedicated step), extensions to
    scripts/check_net_imports.sh, CI wiring, docs/INVARIANTS.md (short,
    linked from README by 31).

Detailed Requirements

  1. Import boundary (script hardening): also assert the full module graph —
    go list -deps ./cmd/worklog must not include any package importing
    net/net/http except via internal/llm; verify BurntSushi/toml
    itself imports neither (belt for the single dep).
  2. Runtime no-network proof (subprocess test): build the real binary
    (go build into t.TempDir) and run the full integration matrix from 25
    under a canary environment:
    • HTTP_PROXY/HTTPS_PROXY/ALL_PROXY pointed at a local canary
      listener → canary must observe zero connections across all runs
      (--no-llm and llm-on-with-loopback-fake alike).
    • llm endpoint configured to the fake loopback server → the ONLY
      network connection observed system-wide for the run is to that
      port: assert via a netstat-free approach — wrap the run with
      strace?? Not portable. Portable approach (implement exactly):
      the test asserts (a) canary untouched, (b) fake server touched
      iff llm-on, (c) a run with llm.endpoint set to the canary's
      ADDRESS-AS-HOSTNAME (http://127.0.0.1:<canary-port>/v1) DOES reach
      the canary (positive control proving the canary works), and (d) a
      run with a non-loopback endpoint produces llm_blocked_nonloopback
      and zero canary/external hits. This bounds the proof to "the only
      dialer is the loopback-enforced one", which the import boundary
      makes complete.
  3. Read-only proof, all commands: for each of daily, standup,
    sources, doctor (and version): hashTree(fixture HOME) unchanged;
    for daily --out, unchanged except exactly the target file; init:
    unchanged except exactly the config path. Any new file anywhere else in
    HOME = failure (catches stray state/cache/log writes forever).
    Environment pinning: the subprocess runs receive HOME,
    XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_STATE_HOME, XDG_DATA_HOME,
    and TMPDIR all pointed INSIDE the watched fixture tree, so writes to
    any conventional location are caught by the same hash sweep (the Go
    test framework's own temp usage stays in the test process, not the
    binary under test).
  4. Write-confinement negative: run daily --out targeting a path inside
    testdata sources with an existing file and no --force → refused;
    --save with unset directory → exit 2 (07's check) — re-asserted here
    end-to-end via the binary.
  5. Stateless proof: two consecutive identical runs produce byte-identical
    output (after hash normalization) and identical tree hashes — no hidden
    state accumulates.
  6. docs/INVARIANTS.md: one page stating I1–I5 in user language and
    naming the test/section that enforces each (auditor entry point).
  7. CI: new job step invariants (both OSes) running
    go test -tags invariants -run Invariants ./internal/invariants/ -v
    after the standard suite.

Acceptance Criteria

  • All proofs pass on macOS+Linux CI; suite runtime < 3 minutes.
  • Deliberate regression demos (in PR description, then reverted):
    (a) adding net/http import to internal/aggregate → script step
    fails; (b) adding an os.WriteFile("~/.cache/worklog", …) to daily →
    read-only proof fails; (c) pointing the default transport at
    ProxyFromEnvironment → canary test fails.
  • Positive control (2c) proves the canary observes real connections.
  • docs/INVARIANTS.md maps each invariant to its enforcement point.
  • Suite is deterministic (-count=3 clean).

Validation

CI run URL with the invariants step green on both OSes + the three red
demo logs in the PR.

Dependencies

02, 23, 25, 26, 27, 28, 29 — all hard: the suite's acceptance criteria
cover every subcommand, so it lands after the full command surface exists
(last issue before the wave-6 docs/release work).

Non-goals

Sandbox/seccomp enforcement (OS-level, v2 exploration), fuzzing campaigns
(per-parser fuzz targets already exist), pentesting.

Design References

  • docs/DESIGN.md §1.2, §12.2, §14
  • docs/decisions/ADR-002-network-boundary.md,
    ADR-003-read-only-sources-and-stateless-runs.md

Source of truth: docs/issues/30-network-invariant-enforcement.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 30 of 33).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions