Documentation
¶
Overview ¶
Package canary implements graphi's hermetic egress-denied canary and zero-telemetry CI gate (SW-008).
The package provides two independent mechanisms behind one acceptance contract:
Runtime egress canary — exercises the graphi surface stack and asserts that ZERO non-loopback network dials are attempted. The assertion is made at dial-attempt time via an injected interceptor (not libpcap), so even a blocked attempt is caught. On Linux it can additionally run inside a loopback-only network namespace; when isolation is unavailable the canary HARD-FAILS rather than silently passing.
Static zero-telemetry gate — scans the default CGo-free build graph for telemetry/analytics SDK imports and non-allowlisted outbound dial constructors, failing CI with the offending symbol + import path.
Both mechanisms build with CGO_ENABLED=0 and produce machine-readable JSON artifacts. They live OUTSIDE the surfaces/engine production code (this is a CI/test concern, not a runtime surface concern).
Layering: internal/canary imports only the standard library, the graphi query operation vocabulary (for surface-union derivation), and go/packages for the static gate. It must not import surfaces/* or engine/* production runtime code.
Package canary — static zero-telemetry gate (SW-008 Slice 3).
The gate scans the DEFAULT CGo-free build graph (CGO_ENABLED=0, default build tags, module root) for two classes of trust-breaking code:
- Telemetry/analytics SDK imports — a curated denylist of import paths whose presence in the default graph fails CI with the offending import.
- Non-allowlisted outbound dial constructors — source-level AST scan for net.Dial / net.DialUDP / http.Client that would reach non-loopback destinations, against an explicit allowlist. The loopback-only surfaces (the daemon Unix socket, local HTTP/SSE) are allowlisted.
It invokes the Go toolchain via `go list`/`go vet`-style inspection (no golang.org/x/tools dependency, keeping the build graph lean) and is itself CGo-free. The graphi-broad CGo flavor is excluded: the gate only scans the default build graph, which is the artifact the local-first contract applies to.
Package canary — netns isolation harness.
On Linux, Run sets up a loopback-only network namespace and executes the isolated function inside it, then tears it down. On non-Linux (or when the runner lacks the required capabilities), IsAvailable returns false and Run HARD-FAILS — a misconfigured environment must never silently mask egress (SW-008 AC: "the job hard-fails rather than silently passing").
This file declares the portable interface; platform implementations live in netns_linux.go (real isolation) and netns_other.go (hard-fail stub).
Package canary — Linux loopback-only network-namespace isolator.
Run creates a fresh network namespace, brings up loopback (so in-process local servers / IPC on 127.0.0.0/8 and ::1 keep working), and leaves every other interface DOWN — so non-loopback dials cannot reach the wire. The calling thread must have CAP_SYS_ADMIN (root or appropriate caps); if not, IsAvailable() reports false and the canary hard-fails rather than silently passing.
This is pure-Go + golang.org/x/sys/unix (already an indirect dependency via modernc/sqlite) — no CGo, no libpcap, fully consistent with the CGo-free default build.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MarshalArtifact ¶
MarshalArtifact serializes a canary artifact to stable JSON for the CI evidence artifact.
Types ¶
type Artifact ¶
type Artifact struct {
Verdict string `json:"verdict"` // "pass" | "fail" | "no-isolation"
CoveredTools []string `json:"covered_tools"`
DialAttempts []DialAttempt `json:"dial_attempts"`
Violations []DialAttempt `json:"violations"` // non-loopback subset
Isolation string `json:"isolation"` // isolator kind / "unavailable"
StartedAt time.Time `json:"started_at"`
DurationMS int64 `json:"duration_ms"`
FailReason string `json:"fail_reason,omitempty"`
}
Artifact is the machine-readable canary result (SW-008 AC: "emits a machine-readable artifact (covered-tool list + packet-capture summary) proving the whole surface was exercised under denial"). Stable JSON schema for CI consumers and audit.
func Run ¶
Run executes the hermetic egress canary:
- Preflight — require that isolation is actually available; if not, HARD-FAIL (no-isolation verdict) rather than silently passing (SW-008 AC + S2).
- Drive — exercise every tool/command in the union inside isolation, recording any dial attempts.
- Verdict — pass iff zero non-loopback dial attempts were observed; on any violation, fail naming the offending tool + destination.
It returns the Artifact and a non-nil error only for HARD-FAIL conditions (no isolation available). A "fail" verdict (violations observed) is returned as an Artifact with Verdict="fail" AND a wrapping error so CI gates naturally fail; callers wanting the structured detail should read the Artifact.
type DialAttempt ¶
type DialAttempt struct {
// Tool identifies the surface tool/command that triggered the dial.
Tool string `json:"tool"`
// Network is the dial network ("tcp", "udp", …).
Network string `json:"network"`
// Address is the dial destination (host:port or host).
Address string `json:"address"`
}
DialAttempt captures a single outbound dial attempt observed during a canary run. The assertion is on ATTEMPT (the destination the code tried to reach), not on packets that made it onto the wire — so even a dial that would be blocked by netns is caught (SW-008 refinement finding D2/S1).
func (DialAttempt) IsLoopback ¶
func (d DialAttempt) IsLoopback() bool
IsLoopback reports whether the dial destination is loopback (127.0.0.0/8 or ::1). Only loopback is permitted by the canary; in-process local servers and the daemon's Unix socket / local HTTP+SSE are the legitimate loopback users (SW-008 refinement finding S3).
type DialRecorder ¶
type DialRecorder struct {
// contains filtered or unexported fields
}
DialRecorder collects dial attempts observed during a canary run. The default recorder is passive (records what a driver reports). Tests and the in-process driver hook real dial attempts into it via Record.
func NewDialRecorder ¶
func NewDialRecorder() *DialRecorder
NewDialRecorder returns an empty recorder.
func (*DialRecorder) All ¶
func (r *DialRecorder) All() []DialAttempt
All returns a copy of all recorded attempts.
func (*DialRecorder) NonLoopback ¶
func (r *DialRecorder) NonLoopback() []DialAttempt
NonLoopback returns only the attempts that are NOT loopback — the violations the canary must fail on.
func (*DialRecorder) Record ¶
func (r *DialRecorder) Record(a DialAttempt)
Record appends an observed dial attempt.
type GateConfig ¶
type GateConfig struct {
ModuleDir string
// GraphCommand returns the default-graph dependency list. Defaults to
// `go list -deps -test=false ./...` under CGO_ENABLED=0; injectable for
// hermetic tests.
GraphCommand func(dir string) ([]string, error)
}
GateConfig configures the static gate. ModuleDir defaults to the graphi module root (detected via `go env GOMOD` when empty).
type GateResult ¶
type GateResult struct {
Verdict string `json:"verdict"` // "pass" | "fail"
Findings []TelemetryFinding `json:"findings"`
}
GateResult is the static-gate verdict.
func RunGate ¶
func RunGate(cfg GateConfig) (GateResult, error)
RunGate executes the static zero-telemetry gate over the default graph.
type IsolationError ¶
type IsolationError struct {
Reason string
}
IsolationError reports that runner-level network isolation is unavailable. The canary treats this as fatal: it MUST NOT proceed without isolation.
func (*IsolationError) Error ¶
func (e *IsolationError) Error() string
type Isolator ¶
type Isolator interface {
// IsAvailable reports whether this runner can actually deny non-loopback
// egress. A false result means the canary must hard-fail.
IsAvailable() bool
// Run executes fn under loopback-only isolation. It MUST be callable only
// when IsAvailable() is true; callers gate on IsAvailable first.
Run(fn func() error) error
}
Isolator abstracts the runner's ability to provide loopback-only network isolation. The production implementation is the Linux netns harness; tests inject a fake to exercise the preflight pass/fail branches without root.
func DefaultIsolator ¶
func DefaultIsolator() Isolator
DefaultIsolator returns the best available isolator for the current platform (the Linux netns harness, or the hard-failing stub elsewhere). Exported so other packages — e.g. internal/audit's privacy-audit live exercise — can run a representative operation under the same loopback-only isolation as the canary.
type RunConfig ¶
type RunConfig struct {
Isolator Isolator
Driver SurfaceDriver
Union SurfaceUnion
}
RunConfig parameterizes a canary run. Fields are injectable so the preflight, isolation, and verdict logic are all unit-testable without netns.
type SurfaceDriver ¶
type SurfaceDriver interface {
// Drive runs every tool/command in the union once, recording any dial
// attempts. It returns an error only if the surface itself fails to run;
// dial attempts are reported via the recorder, not as an error.
Drive(ctx context.Context, union SurfaceUnion, rec *DialRecorder) error
}
SurfaceDriver exercises a slice of the graphi surface and reports any dial attempts it observes to the recorder. The default driver runs the REAL in-process surface functions over an in-memory store, so the canary exercises genuine graphi code paths (query dispatch, search, CLI runner) rather than stubs. A test may inject a fake driver to assert verdict logic in isolation.
func DefaultDriver ¶
func DefaultDriver(out io.Writer) SurfaceDriver
DefaultDriver returns the canonical in-process surface driver over a fresh in-memory store, exercising the real graphi surfaces (query / search / CLI). It is the representative operation used by both the canary and the privacy-audit live exercise. out receives surface stdout; pass io.Discard unless debugging.
func NewInProcessDriver ¶
func NewInProcessDriver(store graphstore.Graphstore, out io.Writer) SurfaceDriver
NewInProcessDriver builds a driver over the given store (use an in-memory store for the hermetic canary). out receives surface stdout; pass io.Discard unless debugging.
type SurfaceUnion ¶
type SurfaceUnion struct {
// CLICommands are the graphi subcommands (cmd/graphi/main.go dispatch).
CLICommands []string `json:"cli_commands"`
// QueryOperations are the structural query operations (engine/query.Operations).
QueryOperations []string `json:"query_operations"`
// SearchTool is the search capability name advertised over MCP/CLI.
SearchTool string `json:"search_tool"`
}
SurfaceUnion is the canonical set of graphi commands/tools the canary must exercise. It is derived programmatically (see NewSurfaceUnion) so the canary cannot silently miss a new tool as graphi grows — fulfilling the "drive every tool/command at least once" acceptance criterion without a hand-maintained list.
func NewSurfaceUnion ¶
func NewSurfaceUnion() SurfaceUnion
NewSurfaceUnion derives the canonical surface union programmatically: CLI subcommands + the engine's canonical query operation list + search. Adding a new query operation to engine/query.Operations automatically extends the canary coverage — no hand-maintained list to drift (SW-008 AC + refinement A4).
func (SurfaceUnion) CoveredTools ¶
func (su SurfaceUnion) CoveredTools() []string
CoveredTools returns the flattened list of covered tool/command identifiers, stable-sorted, for the machine-readable canary artifact.
type TelemetryFinding ¶
type TelemetryFinding struct {
Kind string `json:"kind"` // "telemetry-import" | "outbound-dial"
Import string `json:"import"` // offending import path (telemetry) or package containing the call
Symbol string `json:"symbol"` // offending symbol/call (outbound-dial) or ""
File string `json:"file"` // source file, when known
Reason string `json:"reason"`
}
TelemetryFinding names an import path that the gate has rejected.