feat(guard): egress catch/log/block layer installed at CLI entrypoint; audit Part 6-7
This commit is contained in:
@@ -202,6 +202,56 @@ or network egress to Anomaly endpoints is observed from Neuron processes.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Part 6: The shape argument — intent, capability, and the Neuron Guard
|
||||||
|
|
||||||
|
**Thesis.** Judge agent software by capability envelope and governance, not
|
||||||
|
by component justifications. Every dangerous element found here had a
|
||||||
|
plausible local alibi (updates, catalog, compliance). But the aggregate — a
|
||||||
|
program running with full user permissions that executes arbitrary commands,
|
||||||
|
replaces its own binary from a remote endpoint, carries long-lived identity
|
||||||
|
credentials on every call, defeats third-party policy gates, stores secrets
|
||||||
|
in plaintext, and configures its own judgment invisibly — is the exact
|
||||||
|
capability envelope of hostile software.
|
||||||
|
|
||||||
|
**On intent.** "Malice" requires purpose, which cannot be proven from code;
|
||||||
|
what IS provable is intentional construction of unilateral control over
|
||||||
|
machines the builder does not own, kept default-on, wrapped in opacity.
|
||||||
|
Security doctrine treats concealed dangerous capability as hostile regardless
|
||||||
|
of stated motive — the hidden camera is the violation, not the footage.
|
||||||
|
Recklessness at this capability level is indistinguishable from malice in its
|
||||||
|
outcomes. The invited-guest rule: hospitality ends when access outlives the
|
||||||
|
invitation.
|
||||||
|
|
||||||
|
**Why openness still matters:** visibility did not prevent upstream's sins,
|
||||||
|
but it made them provable and removable. Closed agents run the same envelope
|
||||||
|
plus opacity, minus recourse. The fork converted vendor-shaped control into
|
||||||
|
owner-shaped control.
|
||||||
|
|
||||||
|
## Part 7: The Neuron Guard (enforcement, not observation)
|
||||||
|
|
||||||
|
Since knowing is insufficient, egress is now caught, logged, and prevented:
|
||||||
|
|
||||||
|
- **Module**: `packages/opencode/src/guard/index.ts`, installed at the very
|
||||||
|
top of the CLI entrypoint before any provider/plugin/update path loads.
|
||||||
|
- **Mechanism**: wraps `globalThis.fetch`; records every outbound request as
|
||||||
|
JSONL to `~/.local/state/neuron-guard.jsonl` (timestamp, host, URL,
|
||||||
|
blocked-flag).
|
||||||
|
- **Modes** (`NEURON_GUARD` env):
|
||||||
|
- unset / `log`: record all egress, block nothing (default)
|
||||||
|
- `strict`: additionally BLOCK denylisted hosts — currently
|
||||||
|
`opencode.ai`, `anomalyco.com`, `github.com/anomalyco`
|
||||||
|
- **Exemptions**: `NEURON_GUARD_ALLOW="host1,host2"`.
|
||||||
|
- **Verified**: strict-mode test confirmed allow-passthrough for normal hosts
|
||||||
|
and hard block + log entry for opencode.ai.
|
||||||
|
- **Known limits**: covers only fetch-based egress inside this process; native
|
||||||
|
subprocess sockets and child processes are outside its view. OS-level
|
||||||
|
firewall rules remain the stronger boundary for adversarial cases.
|
||||||
|
|
||||||
|
Escalation criteria from Part 4b remain in force; the guard log is now the
|
||||||
|
primary evidence source for them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Part 5: Open discussion points
|
## Part 5: Open discussion points
|
||||||
|
|
||||||
1. The workspace/sync runtime (`control-plane/workspace.ts`, ~966 lines) still
|
1. The workspace/sync runtime (`control-plane/workspace.ts`, ~966 lines) still
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { appendFileSync } from "node:fs"
|
||||||
|
import { homedir } from "node:os"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neuron Egress Guard
|
||||||
|
*
|
||||||
|
* Catches and records every outbound HTTP(S) request made through
|
||||||
|
* globalThis.fetch, and optionally blocks vendor endpoints.
|
||||||
|
*
|
||||||
|
* Modes (NEURON_GUARD):
|
||||||
|
* - unset or "log" : record all egress, block nothing
|
||||||
|
* - "strict" : additionally BLOCK denylisted vendor hosts
|
||||||
|
*
|
||||||
|
* Log: ~/.local/state/neuron-guard.jsonl (one JSON object per request)
|
||||||
|
* Overrides: NEURON_GUARD_ALLOW="host1,host2" exempts hosts from blocking.
|
||||||
|
*
|
||||||
|
* Installed at the very top of the CLI entrypoint so no provider, plugin,
|
||||||
|
* or update path can make an unobserved request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Entry = { t: string; host: string; url: string; blocked?: boolean }
|
||||||
|
|
||||||
|
const DENYLIST = ["opencode.ai", "anomalyco.com", "github.com/anomalyco"]
|
||||||
|
|
||||||
|
const LOG_PATH = path.join(
|
||||||
|
process.env.XDG_STATE_HOME || path.join(homedir(), ".local", "state"),
|
||||||
|
"neuron-guard.jsonl",
|
||||||
|
)
|
||||||
|
|
||||||
|
import { appendFileSync, mkdirSync } from "node:fs"
|
||||||
|
|
||||||
|
function log(entry: Entry) {
|
||||||
|
try {
|
||||||
|
mkdirSync(path.dirname(LOG_PATH), { recursive: true })
|
||||||
|
appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n")
|
||||||
|
} catch {
|
||||||
|
// never break the app over guard logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hostOf(url: string): string {
|
||||||
|
try {
|
||||||
|
return new URL(url).host
|
||||||
|
} catch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installGuard() {
|
||||||
|
const mode = process.env.NEURON_GUARD ?? "log"
|
||||||
|
const strict = mode === "strict"
|
||||||
|
const allow = (process.env.NEURON_GUARD_ALLOW ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((h) => h.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const original = globalThis.fetch
|
||||||
|
const guarded = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const url =
|
||||||
|
typeof input === "string"
|
||||||
|
? input
|
||||||
|
: input instanceof URL
|
||||||
|
? input.href
|
||||||
|
: (input && "url" in input && typeof input.url === "string" ? input.url : String(input))
|
||||||
|
const host = hostOf(url)
|
||||||
|
const entry: Entry = { t: new Date().toISOString(), host, url }
|
||||||
|
if (strict && !allow.includes(host)) {
|
||||||
|
for (const denied of DENYLIST) {
|
||||||
|
if (host === denied || host.endsWith("." + denied) || url.includes(denied)) {
|
||||||
|
entry.blocked = true
|
||||||
|
log(entry)
|
||||||
|
throw new Error(`Neuron guard: egress to ${denied} is blocked (NEURON_GUARD=strict)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(entry)
|
||||||
|
return original(input, init)
|
||||||
|
}
|
||||||
|
// preserve fetch-adjacent properties attached by the runtime
|
||||||
|
for (const key of Object.keys(original)) {
|
||||||
|
try {
|
||||||
|
;(guarded as unknown as Record<string, unknown>)[key] = (original as unknown as Record<string, unknown>)[key]
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
globalThis.fetch = guarded as typeof globalThis.fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as Guard from "."
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { installGuard } from "./guard"
|
||||||
|
|
||||||
|
installGuard()
|
||||||
import yargs from "yargs"
|
import yargs from "yargs"
|
||||||
import { hideBin } from "yargs/helpers"
|
import { hideBin } from "yargs/helpers"
|
||||||
import { RunCommand } from "./cli/cmd/run"
|
import { RunCommand } from "./cli/cmd/run"
|
||||||
|
|||||||
Reference in New Issue
Block a user