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
- 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).
- 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.
- 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).
- 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.
- Stateless proof: two consecutive identical runs produce byte-identical
output (after hash normalization) and identical tree hashes — no hidden
state accumulates.
docs/INVARIANTS.md: one page stating I1–I5 in user language and
naming the test/section that enforces each (auditor entry point).
- CI: new job step
invariants (both OSes) running
go test -tags invariants -run Invariants ./internal/invariants/ -v
after the standard suite.
Acceptance Criteria
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).
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-taginvariants, run inCI as a dedicated step), extensions to
scripts/check_net_imports.sh, CI wiring,docs/INVARIANTS.md(short,linked from README by 31).
Detailed Requirements
go list -deps ./cmd/worklogmust not include any package importingnet/net/httpexcept viainternal/llm; verifyBurntSushi/tomlitself imports neither (belt for the single dep).
(
go buildinto t.TempDir) and run the full integration matrix from 25under a canary environment:
HTTP_PROXY/HTTPS_PROXY/ALL_PROXYpointed at a local canarylistener → canary must observe zero connections across all runs
(
--no-llmand llm-on-with-loopback-fake alike).network connection observed system-wide for the run is to that
port: assert via a
netstat-free approach — wrap the run withstrace?? Not portable. Portable approach (implement exactly):the test asserts (a) canary untouched, (b) fake server touched
iff llm-on, (c) a run with
llm.endpointset to the canary'sADDRESS-AS-HOSTNAME (
http://127.0.0.1:<canary-port>/v1) DOES reachthe canary (positive control proving the canary works), and (d) a
run with a non-loopback endpoint produces
llm_blocked_nonloopbackand zero canary/external hits. This bounds the proof to "the only
dialer is the loopback-enforced one", which the import boundary
makes complete.
daily,standup,sources,doctor(andversion): 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
TMPDIRall pointed INSIDE the watched fixture tree, so writes toany 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).
daily --outtargeting a path insidetestdatasources with an existing file and no--force→ refused;--savewith unset directory → exit 2 (07's check) — re-asserted hereend-to-end via the binary.
output (after hash normalization) and identical tree hashes — no hidden
state accumulates.
docs/INVARIANTS.md: one page stating I1–I5 in user language andnaming the test/section that enforces each (auditor entry point).
invariants(both OSes) runninggo test -tags invariants -run Invariants ./internal/invariants/ -vafter the standard suite.
Acceptance Criteria
(a) adding
net/httpimport to internal/aggregate → script stepfails; (b) adding an
os.WriteFile("~/.cache/worklog", …)to daily →read-only proof fails; (c) pointing the default transport at
ProxyFromEnvironment→ canary test fails.docs/INVARIANTS.mdmaps each invariant to its enforcement point.-count=3clean).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, §14docs/decisions/ADR-002-network-boundary.md,ADR-003-read-only-sources-and-stateless-runs.mdSource of truth:
docs/issues/30-network-invariant-enforcement.md(PR #1, branchdocs/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).