Documentation
¶
Overview ¶
Package egress is the island outbound-network gate — the containment thesis applied to the one channel Dejima didn't yet observe or control: an island's traffic to the internet (the LLM API, git, package installs, and anything else the agent decides to reach). The daemon's existing gates (Port, capability, MCP, link) all cover an island reaching back INTO the host or other islands; this covers it reaching OUT.
The mechanism is a forward proxy the daemon runs and that islands are pointed at via HTTPS_PROXY. It attributes every connection to an island, applies a Policy, records the decision, and forwards allowed traffic. This is the hostname-level chokepoint — a firewall can later force all egress through it (so the policy can't be bypassed), but even alone it gives full visibility and cooperative control.
Phase 1 (this package) is observe-first: the default Policy is AllowAll, so nothing is blocked — the value is that the operator can finally SEE where each island connects. Phase 2 adds real allow/deny policy; the Policy seam is here so that lands without reworking the proxy.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ProxyEnv ¶
ProxyEnv returns the environment variables to inject into an island so its HTTP(S) clients route outbound through the egress proxy reachable at dial (e.g. "host.docker.internal:7280"). The island name is the proxy username, so attribution travels with every request — no source-IP registry or per-island listener. Phase 1 is observe-only, so the password is a fixed placeholder the proxy doesn't validate (token-checked attribution arrives with enforcement).
Both upper- and lower-case forms are set: curl/git/wget read the lowercase names, many language HTTP libs read the uppercase. NO_PROXY exempts loopback and the daemon host so the in-island autonomy/telemetry path (DEJIMA_HOST = host.docker.internal) and localhost don't route through — or loop back into — the proxy.
Types ¶
type AllowAll ¶
type AllowAll struct{}
AllowAll is the observe-first Phase-1 policy: log everything, block nothing.
type Event ¶
type Event struct {
Island string `json:"island"`
Host string `json:"host"` // destination hostname
Port string `json:"port,omitempty"` // destination port ("443", "80", …)
Method string `json:"method"` // "CONNECT" (HTTPS tunnel) or the HTTP verb
Decision Decision `json:"decision"`
Time time.Time `json:"time"`
}
Event is one recorded outbound connection attempt from an island. It carries the destination by HOST (not full URL/path) and never any payload — egress observability is "who did it talk to," not wiretapping its content. For HTTPS (a CONNECT tunnel) the host is all the proxy sees anyway.
type IslandPolicy ¶
type IslandPolicy struct {
Mode Mode `json:"mode"` // "" == observe
Allow []string `json:"allow,omitempty"` // permitted hosts (used in enforce mode)
Deny []string `json:"deny,omitempty"` // always-blocked hosts (even in observe mode)
}
IslandPolicy is one island's egress rules. Host matching is by exact hostname OR domain suffix — a rule "github.com" also covers "api.github.com" — so an operator allows a service with one entry, not one per subdomain/CDN node.
type Log ¶
type Log struct {
// contains filtered or unexported fields
}
Log is an in-memory, per-island ring of recent egress events. It bounds memory (a chatty island can't grow it unbounded) and is the source for the read API. It implements Recorder.
type Mode ¶
type Mode string
Mode is an island's egress posture.
const ( // ModeObserve (the default, incl. the zero value) allows all egress and just // records it — the Phase 1 behavior. A Deny list still blocks specific hosts. ModeObserve Mode = "observe" // ModeEnforce is deny-all: only hosts on the Allow list are permitted. This is // the containment posture (mirrors Port/MCP deny-all + operator grants), but // opt-in per island because an island legitimately needs some egress. ModeEnforce Mode = "enforce" )
type Policy ¶
type Policy interface {
// Allow reports whether island may open a connection to host. Implementations
// must be safe for concurrent use (the proxy calls it per connection).
Allow(island, host string) bool
}
Policy decides whether an island may reach a destination host. Phase 1 wires AllowAll; Phase 2 replaces it with an operator-grant-backed policy. Keeping it an interface is the whole point of shipping the proxy before the policy.
type PolicyError ¶
type PolicyError struct {
// contains filtered or unexported fields
}
PolicyError is a validation failure from Apply.
func (*PolicyError) Error ¶
func (e *PolicyError) Error() string
type PolicyPatch ¶
type PolicyPatch struct {
Mode Mode `json:"mode,omitempty"`
AddAllow []string `json:"add_allow,omitempty"`
RemoveAllow []string `json:"remove_allow,omitempty"`
AddDeny []string `json:"add_deny,omitempty"`
RemoveDeny []string `json:"remove_deny,omitempty"`
}
PolicyPatch is an incremental change applied to an island's policy (the body of PATCH .../egress/policy). Empty Mode leaves the mode unchanged; the add/ remove lists are applied as sets (idempotent — re-adding is a no-op).
type PolicyStore ¶
type PolicyStore struct {
// contains filtered or unexported fields
}
PolicyStore is a persistent, per-island egress policy. It implements Policy, so the proxy consults it per connection; the daemon's API mutates it. With no rules for an island it reports observe/allow-all, so enabling Phase 2 changes nothing until an operator sets a policy.
func OpenPolicy ¶
func OpenPolicy(path string) *PolicyStore
OpenPolicy loads the persisted egress policy from path (missing/corrupt → empty, i.e. observe-all for every island). Mutations persist back to path so policy survives a daemon restart.
func (*PolicyStore) Allow ¶
func (s *PolicyStore) Allow(island, host string) bool
Allow implements Policy: deny-list wins always; in enforce mode only allow- listed hosts pass; observe (and the zero value) permits everything.
func (*PolicyStore) Apply ¶
func (s *PolicyStore) Apply(island string, patch PolicyPatch) (IslandPolicy, error)
Apply mutates an island's policy with the patch and persists, returning the resulting policy. An unknown Mode (not observe/enforce/"") is rejected.
func (*PolicyStore) Get ¶
func (s *PolicyStore) Get(island string) IslandPolicy
Get returns a copy of an island's policy (zero value = observe, no rules).
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the island egress forward proxy: an http.Handler that serves both plain-HTTP proxy requests (absolute-URI form) and HTTPS CONNECT tunnels. For each connection it attributes the island (from Proxy-Authorization), asks the Policy, records an Event, and — if allowed — forwards the traffic. It is the daemon-run chokepoint islands are pointed at via HTTPS_PROXY.
It deliberately does NOT terminate TLS: HTTPS is a blind CONNECT tunnel, so the proxy sees the destination host but never the encrypted payload. Egress control here is "who can it talk to," not content inspection.
type Recorder ¶
type Recorder interface{ Record(Event) }
Recorder receives egress events. The daemon plugs in a Log (for the read API) and may also fan out to the ledger. A nil Recorder is a valid no-op via (*Log)(nil)-style guards at the call sites, but the proxy always holds a non-nil one (it defaults to a discard recorder).