Security

A Seed executes code that it wrote itself, and runs shell commands that a model chose. The design assumes autonomy will grow, so trust boundaries are structural rather than advisory.

Sandboxing

A Seed runs in one container, an unprivileged container running as your user with all capabilities dropped, no-new-privileges, a read-only root filesystem, and memory, CPU and process limits (SEED_MEMORY, SEED_CPUS, SEED_PIDS_LIMIT). Inside it, every command an evolution runs, and the live organism, is isolated again with bubblewrap (kernel/sandbox/bwrap.go). Each sandbox gets:

Nested bubblewrap needs to create namespaces and mount filesystems. Docker's default seccomp profile allows those calls only with CAP_SYS_ADMIN, which a Seed never gets. A Seed therefore uses a tailored seccomp profile (cmd/seed/seccomp.json): Docker's default profile with only clone, unshare, setns, mount, umount2 and pivot_root allowed unconditionally. AppArmor's docker-default profile and /proc masking also forbid these, so those two are relaxed. There is still no privileged mode and no capability.

Network.

Build caches. The kernel is built with its own Go caches (.seed/kernel-cache), which no sandbox can see. Sandboxes have a separate writable cache (.seed/cache), so sandboxed code cannot poison the next kernel build.

The runtime image is built from Dockerfile without a build context, so it cannot copy repository files or secrets.

Permissions

Every tool call is classified before it runs (tools.Registry.Execute):

level examples default
safe read files, list, search, logs, schema, git status/diff, HTTP to the sandbox allow
review write organism/knowledge/skills, run commands in the sandbox, migrations on the scratch DB, dependency installs allow
dangerous writing or deleting any protected (kernel) path ask

Decisions are allow, ask or deny per level in seed.yaml. ask creates an approval and moves the evolution to needs_input. The control plane shows it, and the evolution blocks until the owner decides. Plan and apply approvals can also be required (require_plan_approval, require_apply_approval).

The kernel boundary

The boundary is an allowlist: only organism/, knowledge/ and skills/ evolve freely (kernel.evolvable). Everything else is the kernel. That includes kernel/, cmd/, control/, seed.yaml, go.mod and every root file, as well as new files such as go.work or vendor/, which would change what go build compiles on the host. The boundary is guarded in four layers:

  1. File tools classify writes outside the evolvable directories as dangerous, which requires approval by default.
  2. Sandboxes mount the workspace read-only except for the evolvable directories, so shell commands cannot touch the kernel or create root files.
  3. At commit time, the kernel diffs the worktree and refuses any protected change that was not approved during that evolution ("kernel boundary violated").
  4. seed run builds the kernel with GOWORK=off GOFLAGS=-mod=readonly.

Sandboxed code can create symlinks inside evolvable directories. Every host-side read or write of repository content goes through Go's os.Root (kernel/fsx, and the file tools), including knowledge, skills, migrations, the logo and extensions. A link such as knowledge/x.md → ~/.ssh/id_rsa therefore fails instead of leaking a host file into a prompt.

Kernel evolution is therefore possible but deliberate. After an approved kernel change is applied, the kernel restarts into its own new source.

Data

Control plane

The control plane shares an origin with the organism (/_seed and /). The organism's JavaScript is code the Seed wrote, so the kernel does not trust it:

Outbound access

The live organism runs in its own network namespace with no network. Its only way out is the kernel's proxy, reached through a socket and exposed inside as HTTPS_PROXY (Go and Node honor it; NODE_USE_ENV_PROXY=1 is set for Node's fetch):

Operating on live data

The chat can read live data with query_data or a GET call_api. Reads are enforced by the database, not by the prompt:

Changing live data (change_data, or a non-GET call_api) is classified dangerous, so the owner approves each change before it runs. Dangerous requests are shown to the owner in full, never truncated, each field rendered readably. Requests too large to review (over 16 KB) are refused.

Known limitations (v0.1)

Edit this page on GitHub