Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ On a release, the maintainer moves the accumulated `[Unreleased]` entries into a

## [Unreleased]

- **frontdoor-parity** — Centralize the native runtime capabilities shared by run/watch/resume/swarm, chat/TUI, and serve (advisor budget, repo orientation, proactive context window, live code intelligence, memory/lessons, and trusted steering) behind one configurator, with a constructor-level parity test across all three real builders so a one-door omission cannot ship inert again. _Owns:_ `cmd/nilcore/{native_runtime.go,native_runtime_test.go,chat.go,main.go}`, `CHANGELOG.md`. _(architecture hardening)_

- **security-release-hardening** — Pin the build/release floor to patched Go 1.25.12 and `x/sys` 0.44.0, add a pinned reachable-vulnerability CI gate, make release publication refuse missing/corrupt platform assets or checksum records, and add real four-target release + installer E2Es proving valid install plus fail-closed partial-release, missing-checksum, and tampered-binary paths; refresh the public toolchain and state snapshot. _Owns:_ `go.mod`, `go.sum`, `.github/workflows/{ci,release}.yml`, `scripts/{install,verify-release-assets}.sh`, `test/{install,release-assets}-e2e.sh`, `README.md`, `STATE.md`, `docs/PREREQUISITES.md`. _(security / release hardening)_

- **docs(state-of-the-project): reconcile every document with the code as it actually is.** A ground-truth pack was extracted from the tree (CLI surface, the full env-var inventory incl. the prefix-constructed `NILCORE_{CODEX,CLAUDE}_{MODEL,EFFORT}`, package inventory + metrics, emitted event kinds, invariant enforcement sites, shipped-vs-gated status), then all 30 docs were audited against it and each repair was **independently refuted** by a second reader; 15 surviving defects were re-fixed. Headline corrections: `router.Classify(goal)` returns `run|build|swarm` — **not** `|decompose` (`Decompose` is a fourth `Preset`, opt-in only, excluded from `All()`), a claim three contract files asserted wrongly; `docs/ARCHITECTURE.md` overstated the container egress control as SSRF-proof/IP-layer when `AllowEgressVia` only sets `--network bridge` + `HTTP(S)_PROXY` and no packet filter exists — the proxy is the only *sanctioned* way out, not a hard network wall; metrics refreshed everywhere (120 packages, 375 source + 406 test files, ~89.8K non-test LOC); the six `NILCORE_OPENROUTER_*` / `NILCORE_COMPAT_*` vars documented only in suffix shorthand are now spelled out; `docs/REFERENCE.md` gained an explicit upgrade note for the two settings whose MEANING changed at `573a4df` (`NILCORE_SELFIMPROVE_AUTOAPPROVE=1` was a no-op and now really merges; swarm shards now reach their preset's declared hosts). Roadmap/plan docs keep their plans but now carry accurate SHIPPED / PARTIALLY SHIPPED / NOT-BUILT status and no longer name pruned symbols as live. `CHANGELOG.md` history untouched (append-only). Markdown only — no `.go`, `Makefile` or CI file changed. _Owns:_ `*.md`, `docs/**`. _(Phase 16 — documentation)_
Expand Down
36 changes: 4 additions & 32 deletions cmd/nilcore/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import (
"syscall"
"time"

"nilcore/internal/advisor"
"nilcore/internal/agent"
"nilcore/internal/backend"
"nilcore/internal/blastbudget"
Expand Down Expand Up @@ -1352,14 +1351,10 @@ func chatNativeBackend(d chatDeps, prov model.Provider, adv advisorCfg, box sand
MaxSteps: *d.flags.common.maxSteps,
Seed: in.Seed,
}
// Repo orientation + window awareness (upgrade program), exactly as buildBackend
// (run/watch) and the serve backend already wire: without these the interactive
// chat/TUI drive starts BLIND (no repo map ⇒ ls/cat structure discovery on every
// first step) and never proactively compacts (nil CtxWindow ⇒ only the one-shot
// overflow-400 recovery). box.Workdir() is the same per-task worktree the backend
// is built against; both are nil-safe seams.
n.RepoContext = func(context.Context) string { return repoMap(box.Workdir(), repoMapBudget) }
n.CtxWindow = meter.CtxWindow
configureNativeRuntime(n, nativeRuntimeConfig{
Provider: prov, Advisor: adv, Box: box, Memory: d.mem,
Project: d.baseRepo, SteeringDir: d.baseRepo,
})
if in.Inbox != nil {
n.Inbox = in.Inbox
}
Expand All @@ -1374,29 +1369,6 @@ func chatNativeBackend(d chatDeps, prov model.Provider, adv advisorCfg, box sand
if in.AskUser != nil {
n.AskUser = in.AskUser
}
if adv.prov != nil {
// A fresh advisor per drive so its per-drive consult ceiling is honored,
// exactly as the run path's buildBackend does. Metered against the conversation
// budget wall (§6/§7) — a raw adv.prov would let strong-model consults escape it.
n.Advisor = advisor.New(meteredAdvisor(prov, adv.prov), adv.maxCalls)
n.EscalateAfter = adv.escalateAfter
}
// Live incremental code-intelligence (P3-T16), opt-in via NILCORE_LIVE_INDEX:
// the conversational loop gets the same worktree-aware `live` tool the run path
// has — previously only `buildBackend` (run/watch/propose-edit) wired it, so the
// advertised front door silently lacked it. Off by default (nil seam).
if envOptIn("NILCORE_LIVE_INDEX") {
n.LiveSession = liveSession(d.mem, d.baseRepo)
}
// Cross-project memory + distilled lessons: the conversational front door reads the
// same merged view the run path does. Previously only buildBackend wired this, so
// the door people actually use never saw the agent's own distilled scars.
attachMemoryContext(n, d.mem, d.baseRepo)
// Operator steering (P10-T01): an authoritative project steering file
// (NILCORE.md / AGENTS.md) committed at the repo root is loaded ONCE at launch
// from the operator's own repo — front-door origin, never tool/inbox text — and
// prepended as TRUSTED instructions (the I7 exception). nil/empty ⇒ byte-identical.
attachSteering(n, d.baseRepo)
return n
}

Expand Down
62 changes: 11 additions & 51 deletions cmd/nilcore/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import (
"syscall"
"time"

"nilcore/internal/advisor"
"nilcore/internal/agent"
"nilcore/internal/backend"
"nilcore/internal/blastbudget"
Expand Down Expand Up @@ -1779,6 +1778,10 @@ func serveNativeBackend(d serveDeps, prov model.Provider, adv advisorCfg, box sa
MaxSteps: *d.flags.maxSteps,
Seed: in.Seed,
}
configureNativeRuntime(n, nativeRuntimeConfig{
Provider: prov, Advisor: adv, Box: box, Memory: d.mem,
Project: d.baseRepo, SteeringDir: d.baseRepo,
})
if in.Inbox != nil {
n.Inbox = in.Inbox
}
Expand All @@ -1794,26 +1797,6 @@ func serveNativeBackend(d serveDeps, prov model.Provider, adv advisorCfg, box sa
if in.AskUser != nil {
n.AskUser = in.AskUser
}
// Same orientation + window seams as buildBackend's native case: serve/chat
// drives start with the map and compact before overflow, exactly like run.
n.RepoContext = func(context.Context) string { return repoMap(box.Workdir(), repoMapBudget) }
n.CtxWindow = meter.CtxWindow
if adv.prov != nil {
// Meter the advisor against the same conversation/thread budget wall as the
// main provider (§6/§7) — a raw adv.prov would let strong-model consults escape
// the ceiling.
n.Advisor = advisor.New(meteredAdvisor(prov, adv.prov), adv.maxCalls)
n.EscalateAfter = adv.escalateAfter
}
// Live incremental code-intelligence (P3-T16), opt-in via NILCORE_LIVE_INDEX —
// the serve loop gets the same `live` tool as the run/chat paths.
if envOptIn("NILCORE_LIVE_INDEX") {
n.LiveSession = liveSession(d.mem, d.baseRepo)
}
// Cross-project memory + distilled lessons, and the operator's steering file —
// both reach serve drives now, exactly as they reach run/watch and chat.
attachMemoryContext(n, d.mem, d.baseRepo)
attachSteering(n, d.baseRepo)
// Self-timer (serve-only): the `sleep` tool arms a durable wake for this thread.
// nil ⇒ no `sleep` tool advertised (byte-identical) — e.g. no checkpointer wired.
n.Wake = wakeArm
Expand Down Expand Up @@ -2274,12 +2257,6 @@ func envFactory(c commonFlags, prov model.Provider, cred func(string) string, ad
box := attachBlast(selectSandbox(*c.sandboxPref, *c.runtime, *c.image, dir), blast)
v := orchestratorVerifier(box, *c.checkCmd, log, *c.logPath)
be := buildBackend(*c.backendName, prov, cred, adv, box, v, log, *c.maxSteps, mem, project, cfg)
// Operator steering (P10-T01): a committed NILCORE.md / AGENTS.md is present in
// the worktree checkout; load it once and prepend as trusted instructions on
// the native backend. nil/empty ⇒ byte-identical; only the native loop reads it.
if n, ok := be.(*backend.Native); ok {
attachSteering(n, dir)
}
return agent.Env{Backend: be, Verifier: v, Box: box}
}
}
Expand Down Expand Up @@ -2307,11 +2284,6 @@ func multiEnvFactory(c commonFlags, b boot, log *eventlog.Log, mem *memory.Memor
box := attachBlast(selectSandbox(*c.sandboxPref, *c.runtime, *c.image, dir), blast)
v := orchestratorVerifier(box, *c.checkCmd, log, *c.logPath)
be := buildBackend(name, prov, b.cred, adv, box, v, log, *c.maxSteps, mem, project, b.cfg)
// Operator steering parity with envFactory: load committed NILCORE.md/AGENTS.md
// once for the native backend (nil/empty ⇒ byte-identical; only native reads it).
if n, ok := be.(*backend.Native); ok {
attachSteering(n, dir)
}
return agent.Env{Backend: be, Verifier: v, Box: box}
}
}
Expand Down Expand Up @@ -2441,26 +2413,14 @@ func buildBackend(name string, prov model.Provider, cred func(string) string, ad
CommandGuard: policy.DefaultCommandPolicy().Check,
MaxSteps: maxSteps,
}
// A fresh advisor per task so its per-task consult ceiling is honored. Metered
// against the main provider's wall (§6/§7) so strong-model consults can't escape it.
if adv.prov != nil {
n.Advisor = advisor.New(meteredAdvisor(prov, adv.prov), adv.maxCalls)
n.EscalateAfter = adv.escalateAfter
}
attachMemoryContext(n, mem, project)
// Repo orientation + window awareness (upgrade program): the map spares the
// first steps of every drive from ls/cat structure discovery, and the window
// resolver lets the loop compact BEFORE a context overflow instead of dying
// on the 400. Both nil-safe seams; box.Workdir() is the per-task worktree.
n.RepoContext = func(context.Context) string { return repoMap(box.Workdir(), repoMapBudget) }
n.CtxWindow = meter.CtxWindow
// Live incremental code-intelligence (P3-T16), opt-in via NILCORE_LIVE_INDEX:
// the loop gets a worktree-aware `live` tool whose graph re-indexes edits
// incrementally and fuses project memory. Off by default (nil ⇒ byte-identical;
// no full per-run index cost unless requested).
if envOptIn("NILCORE_LIVE_INDEX") {
n.LiveSession = liveSession(mem, project)
steeringDir := project
if box != nil && box.Workdir() != "" {
steeringDir = box.Workdir()
}
configureNativeRuntime(n, nativeRuntimeConfig{
Provider: prov, Advisor: adv, Box: box, Memory: mem,
Project: project, SteeringDir: steeringDir,
})
return n
}
}
Expand Down
69 changes: 69 additions & 0 deletions cmd/nilcore/native_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

// The native loop is constructed by three production front-door families:
// run/watch/propose-edit (buildBackend), terminal chat/TUI (chatNativeBackend), and
// channel serve (serveNativeBackend). These shared seams used to be wired independently,
// which repeatedly let a capability ship on one door while remaining inert on another.
// Keep the front-door-specific tools, inbox, emitter, ask, and wake seams at their call
// sites; configure every cross-cutting runtime capability exactly once here.

import (
"context"

"nilcore/internal/advisor"
"nilcore/internal/backend"
"nilcore/internal/memory"
"nilcore/internal/meter"
"nilcore/internal/model"
"nilcore/internal/sandbox"
)

type nativeRuntimeConfig struct {
Provider model.Provider
Advisor advisorCfg
Box sandbox.Sandbox
Memory *memory.Memory
Project string
SteeringDir string
}

// configureNativeRuntime applies the cross-cutting seams every native front door must
// share. Optional features remain nil/off byte-identically: no advisor means no advisor,
// nil memory means no memory context, NILCORE_LIVE_INDEX off means no live session, and
// an absent steering file means no trusted steering context.
func configureNativeRuntime(n *backend.Native, cfg nativeRuntimeConfig) {
if n == nil {
return
}

// Repo orientation and proactive context compaction are baseline runtime features.
// The sandbox worktree is the model's actual view; fall back to the project root only
// for a defensive nil-box construction in tests or future read-only hosts.
workdir := cfg.Project
if cfg.Box != nil && cfg.Box.Workdir() != "" {
workdir = cfg.Box.Workdir()
}
if workdir != "" {
n.RepoContext = func(context.Context) string { return repoMap(workdir, repoMapBudget) }
}
n.CtxWindow = meter.CtxWindow

// A fresh advisor per drive preserves the per-drive call ceiling. If the main
// provider is metered, meteredAdvisor shares its ledger so strong-model spend cannot
// escape the one budget wall.
if cfg.Advisor.prov != nil {
n.Advisor = advisor.New(meteredAdvisor(cfg.Provider, cfg.Advisor.prov), cfg.Advisor.maxCalls)
n.EscalateAfter = cfg.Advisor.escalateAfter
}

// Live code intelligence is opt-in and worktree-aware; memory and steering are the
// same shared context sources on every door. SteeringDir stays explicit because chat
// loads from the principal's repo while run loads from its disposable worktree.
if envOptIn("NILCORE_LIVE_INDEX") && cfg.Project != "" {
n.LiveSession = liveSession(cfg.Memory, cfg.Project)
}
attachMemoryContext(n, cfg.Memory, cfg.Project)
if cfg.SteeringDir != "" {
attachSteering(n, cfg.SteeringDir)
}
}
90 changes: 90 additions & 0 deletions cmd/nilcore/native_runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"

"nilcore/internal/backend"
"nilcore/internal/memory"
"nilcore/internal/onboard"
"nilcore/internal/sandbox"
"nilcore/internal/session"
"nilcore/internal/verify"
)

// TestNativeFrontDoorsShareRuntimeCapabilities is the constructor-level parity gate.
// It drives the real run, chat/TUI, and serve builders and asserts every one receives
// the same cross-cutting runtime seams. Before configureNativeRuntime existed, omitting
// one of these assignments on a single front door compiled and its isolated tests stayed
// green; this test makes that class of built-but-inert regression discriminating.
func TestNativeFrontDoorsShareRuntimeCapabilities(t *testing.T) {
t.Setenv("NILCORE_LIVE_INDEX", "0")
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "NILCORE.md"), []byte("shared steering marker\n"), 0o644); err != nil {
t.Fatal(err)
}

steps := 8
prov := &fakeProvider{id: "main"}
advProv := &fakeProvider{id: "advisor"}
adv := advisorCfg{prov: advProv, maxCalls: 2, escalateAfter: 3}
mem := memory.New(nil) // construction-only: the callback is asserted, never invoked
box := sandbox.NewContainer("podman", "img", dir)

run, ok := buildBackend("native", prov, func(string) string { return "" }, adv, box,
verify.Pass{}, nil, steps, mem, dir, onboard.Config{}).(*backend.Native)
if !ok {
t.Fatal("buildBackend(native) did not return *backend.Native")
}

chat := chatNativeBackend(chatDeps{
flags: newChatFlags(dir), provider: prov, baseRepo: dir, mem: mem,
}, prov, adv, box, verify.Pass{}, session.NativeRun{Mode: session.ModeExecute})

serve := serveNativeBackend(serveDeps{
flags: commonFlags{maxSteps: &steps}, provider: prov, baseRepo: dir, mem: mem,
}, prov, adv, box, verify.Pass{}, session.NativeRun{Mode: session.ModeExecute}, nil)

for name, n := range map[string]*backend.Native{"run": run, "chat/tui": chat, "serve": serve} {
t.Run(name, func(t *testing.T) {
if n.RepoContext == nil {
t.Fatal("RepoContext is not wired")
}
if n.CtxWindow == nil {
t.Fatal("CtxWindow is not wired")
}
if n.Advisor == nil || n.EscalateAfter != adv.escalateAfter {
t.Fatalf("advisor parity lost: advisor=%v escalate_after=%d", n.Advisor, n.EscalateAfter)
}
if n.MemoryContext == nil {
t.Fatal("MemoryContext is not wired")
}
if n.SteeringContext == nil || !strings.Contains(n.SteeringContext(), "shared steering marker") {
t.Fatal("SteeringContext is not wired from the principal repo")
}
if n.LiveSession != nil {
t.Fatal("NILCORE_LIVE_INDEX=0 must keep LiveSession disabled")
}
})
}

// The opt-in live-index seam must also reach every constructor through the same
// configurator. This is a second, positive control; merely asserting the default-off
// path would not catch a regression that dropped LiveSession from the helper itself.
t.Setenv("NILCORE_LIVE_INDEX", "1")
liveRun := buildBackend("native", prov, func(string) string { return "" }, adv, box,
verify.Pass{}, nil, steps, mem, dir, onboard.Config{}).(*backend.Native)
liveChat := chatNativeBackend(chatDeps{
flags: newChatFlags(dir), provider: prov, baseRepo: dir, mem: mem,
}, prov, adv, box, verify.Pass{}, session.NativeRun{Mode: session.ModeExecute})
liveServe := serveNativeBackend(serveDeps{
flags: commonFlags{maxSteps: &steps}, provider: prov, baseRepo: dir, mem: mem,
}, prov, adv, box, verify.Pass{}, session.NativeRun{Mode: session.ModeExecute}, nil)
for name, n := range map[string]*backend.Native{"run": liveRun, "chat/tui": liveChat, "serve": liveServe} {
if n.LiveSession == nil {
t.Errorf("%s: NILCORE_LIVE_INDEX=1 did not wire LiveSession", name)
}
}
}
Loading