Commit Graph
8 Commits
Author SHA1 Message Date
KRATOS b0a280eaf6 audit: fix four high-severity concurrency/correctness bugs (#261)
Surfaced by a full-tree audit; all are latent (the suite passed) and live
in race windows / error edges the tests don't exercise. Each ships a
regression test.

- swarm: liveAgents() now counts queued specs, so AdoptOrphans can no longer
  re-dispatch (double-execute) a task whose member is merely waiting for a
  free slot over the concurrency cap.
- cron/hooks/oauth: stale-lock reclaim is now atomic (rename aside, then
  verify-and-restore if it turns out fresh) instead of a blind Remove that
  let two racers both reclaim and hold the lock; also falls through to the
  bounded wait instead of hot-spinning when a reclaim never wins.
- lsp: a dead language-server session is evicted and restarted instead of
  being returned forever, so one server crash no longer permanently breaks
  diagnostics (and spuriously fails self-correct every iteration).
- config: an API-key env var no longer overwrites a same-named
  compatible-transport provider's kind when no base URL is supplied.
2026-06-20 10:38:53 +05:30
KRATOS 1c30f4c117 audit fixes: cron lock, accounting race, O(n²) hot paths, /style wiring (#260)
* audit fixes: cron lock, accounting race, O(n^2) hot paths, /style wiring

Fixes five verified findings from a code audit. Two related findings were already
addressed upstream and three need larger/separate work (noted below).

H-1 cron store: cross-process lock for the read-modify-write.
Two schedulers seeing a job due raced the metadata read-modify-write; fireJob
even documented it ("full atomicity needs file locking"). Adds a per-job O_EXCL
lock (mirroring internal/oauth's pattern, stale-reclaim included) and a Mutate
primitive doing the re-read+modify+write atomically. Update/Remove lock too. The
lock is held only for the millisecond RMW, never across a job's exec.

M-5 specialist accounting: close the check-then-append race.
specialistEventExists (unlocked ReadEvents) + a later AppendEvent let two
finishers (TaskOutput poll vs onExit, even cross-process) both pass the check and
write duplicate stop/usage events. Adds sessions.Store.AppendEventUnlessExists
(check + append atomic under the session lock) and routes the stop and usage
paths through it.

M-2 hooks audit: O(1) sequence via tail read.
append re-read and scanned the whole audit log every call to find the max
sequence (O(n^2) over a session). Reads only the file tail now; multi-process
safe (the log is a shared global file), unlike an in-memory counter which would
collide sequences across processes.

M-4 transcript rehydration: O(n^2) -> O(n).
The three resume/rewind/compact rehydration loops called appendTranscriptRow per
event, each re-scanning every existing row for the keyed-row dedup. A bulk
appendTranscriptRowsDedup builds the key set once; semantics are identical.

M-6 /style: actually shape responses.
The TUI /style command stored a style but never threaded it anywhere. Adds
agent.Options.ResponseStyle, a system-prompt directive for concise/explanatory/
review, and wires m.responseStyle through the run. "balanced" is a no-op (prompt
byte-identical to before).

Already addressed upstream (no change):
- M-1 forked-session double-count: Store.Fork already skips EventUsage events,
  with a comment naming the exact double-count it prevents.
- M-3 lineage O(N^2): Tree already does a single store.List() scan and indexes
  children in memory; no path calls List per node.

Deferred (need larger/separate changes):
- M-7 escalate_model in the TUI needs a ModelSwitcher that builds a provider
  mid-run; registering the tool alone yields a tool that errors on call.
- L-1 MCP SSE reconnect is a stream-lifecycle change (low-likelihood after the
  8 MiB per-event cap).
- L-2 dead-code removal belongs in a dedicated cleanup sweep.

Tests: per-fix unit tests including concurrency (cron Mutate, sessions
AppendEventUnlessExists) under -race; full go test ./... green.

* fix(windows): treat ERROR_ACCESS_DENIED as lock contention (cron + oauth)

The Windows smoke run failed TestMutateIsAtomicUnderConcurrency with
"...lock: Access is denied" (FireCount 19/20). On Windows a concurrent holder's
os.Remove leaves the lock file in a delete-pending state, so an O_EXCL create
races it with ERROR_ACCESS_DENIED (os.ErrPermission) rather than ErrExist — the
retry loop only treated ErrExist as contention and hard-failed.

Treat os.ErrPermission as contention (retry) in the new cron lock and in the
oauth lock it mirrors (oauth's own concurrent test exposed the same pre-existing
bug under the added load). Unix is unaffected — it returns ErrExist.

* fix: address review — cross-process atomic audit append + AppendRun guard

Addresses the review feedback on this PR.

P1 (blocking): AuditStore.append is now cross-process atomic.
The O(1) tail read observes another process's prior appends, but the read-then-
append was not atomic across processes — two could both read sequence N and write
N+1. Wraps lastSequence()+append in a per-file O_EXCL lock (hooks/lock.go, same
stale-reclaim + Windows ErrPermission handling as cron/oauth); store.mu stays as
the in-process fast path. Adds TestAuditStoreSequenceConcurrent (4 stores x 12
goroutines on one file -> unique dense sequences), which the prior sequential
test could not catch. Walked back the comment that oversold the tail read.

P2: AppendRun no longer resurrects a removed job.
fireJob's AppendRun ran after Mutate released the per-job lock; a Remove in that
window let AppendRun's MkdirAll recreate the dir with an orphaned runs.jsonl.
AppendRun now takes the per-job lock and bails if metadata.json is gone. Adds
TestAppendRunDoesNotResurrectRemovedJob.

P2: lastSequence ReadAt tolerates a short final read.
ReadAt may return io.EOF with n < len(buf) if the file shrank between Stat and
the read; treat EOF as non-fatal and use buf[:n] (matches lastEventSequence).

P3: oauth stale-reclaim no-op removed.
The double os.ReadFile "stale == data" guard compared the file to itself read
twice in a row (always true); staleness is decided by the mtime check, so the
guard is dropped (behavior-preserving).

Tests under -race; cron/hooks/oauth green. A shared filelock helper would DRY the
three near-identical locks — left as a follow-up to avoid churning the
already-green cron/oauth packages here.
2026-06-19 21:11:34 +05:30
KRATOS b1ab01f53f Hooks management command + build & supply-chain hardening (#193)
* Bump toolchain to go1.26.4 to clear reachable stdlib CVEs

go.mod pinned toolchain go1.24.13, whose bundled standard library has 9
govulncheck-reachable CVEs (crypto/tls KeyUpdate DoS, net/http2 loop, crypto/x509 x3,
net, net/url, os, net/textproto). CI and release build via go-version-file: go.mod, so
shipped binaries inherited them.

Bumping the toolchain directive to go1.26.4 clears all nine — govulncheck ./... now
reports 0 reachable (minimum that clears them is go1.25.11; go1.26.4 matches the team's
local Go). The go directive stays at 1.24.2 (minimum language version). Verified:
go build/vet/gofmt clean, full go test ./... green.

* Add govulncheck CI gate and SHA-pin GitHub Actions

- ci.yml: new 'security' job runs govulncheck as a hard gate (fails on a reachable
  vulnerability; passes now that the toolchain is bumped) plus an advisory deadcode step
  that surfaces dormant code without blocking.
- Pin every GitHub Action to a full commit SHA (checkout, upload-artifact, github-script)
  with a version comment, across all four workflows — previously only setup-go was pinned,
  leaving the mutable v4/v7 tags as a supply-chain risk.

* Add hooks management command (add/remove/enable/disable)

Wires the previously-dormant hooks.ConfigStore (10 unreachable funcs -> 0) into the CLI:

  zero hooks add <id> --event <event> --command <cmd> [--name --description --matcher --arg --user --json]
  zero hooks remove|enable|disable <id> [--user --json]

Writes the project hook config by default (<cwd>/.zero/hooks.json) or the user config with
--user, reusing the store's locking + atomic writes + validation (normalizeDefinition). New
hooks are enabled; state is managed via enable/disable. JSON output is secret-scrubbed via
redaction. Mirrors the existing 'zero mcp add/remove/enable/disable' shape. Covered by
add/remove/toggle round-trip, validation, unknown-event, and JSON-redaction tests.

* Fix govulncheck CI job: pin GOTOOLCHAIN to the go.mod toolchain

go run govulncheck@latest selected the toolchain from govulncheck's own go.mod
(downgrading to go1.25.11), which then could not load our go1.26-requiring packages
(fips140only_go1.26.go), failing the security job. Resolve GOTOOLCHAIN from go.mod's
toolchain line so govulncheck and deadcode both run under go1.26.4.

* Address review: harden CI checkout, pin tool versions, validate hook event

CodeRabbit findings on #193:
- ci.yml: persist-credentials: false on all checkout steps (smoke/performance/security)
  — these jobs run repo code and never push, so don't keep the token in git config.
- ci.yml: pin [email protected] and [email protected].0 instead of @latest, so the security
  gate is deterministic and not subject to supply-chain drift.
- hooks: add exported IsValidEvent/KnownEvents (single source); parseEvent reuses it, and
  'zero hooks add' now rejects an invalid --event with a usage error at parse time instead
  of falling through to the Upsert app-error crash path.
2026-06-14 11:56:29 +05:30
KRATOS 06ea4f004b Edit safety: file read/write tracking, external-modification detection, post-apply hooks (#172)
* Edit safety: file version tracking, conflict detection, hook wiring

Phase 1 — FileTracker (internal/tools/file_tracker.go): per-session
map[absPath]{sha256, size, mtime}; CheckConflict flags drift only against
a known baseline, so first-touch writes are unaffected. nil = no-op.

Phase 2 — external-modification detection: read_file records the
whole-file baseline; edit_file and write_file --overwrite refuse to
clobber a file whose on-disk content drifted from it and tell the model
to re-read first, then re-baseline to what they wrote. apply_patch
forgets the files it rewrote (git apply has its own context guard).
Threaded via RunOptions/agent.Options FileTracker, built once per session
in the exec, spec, and TUI paths.

Phase 3 — post-apply validation via hooks: the dormant hooks.Dispatcher
is wired into executeToolCall. beforeTool hooks can veto a call
(DenialHookBlocked); afterTool hook output is scrubbed and surfaced to
the model. DispatchOutcome now collects hook stdout/stderr. Per-session
dispatcher built from merged user+project hooks.json; fails open and runs
nothing until a hook is configured.

* edit-safety: address review — fail closed on read, redact hook reason

- write_file overwrite: if a tracked file cannot be re-read to verify it,
  fail CLOSED (refuse the overwrite) instead of silently proceeding — the
  read error previously skipped the conflict check and could clobber an
  externally modified or replaced file.
- blockedByHookResult: scrub outcome.Reason through redaction before placing
  it in model-visible ToolResult.Output; this intercepted path bypassed the
  registry's normal output-redaction boundary.
- TestHashContentIsStableAndDistinguishing: compare separately stored hashes
  so the stability check is not a same-expression comparison (SA4000).

* edit-safety: flag ToolResult.Redacted when hook output is scrubbed

appendHookFeedback now reports whether scrubbing changed the afterTool
feedback, and blockedByHookResult reports whether it scrubbed the reason;
both set ToolResult.Redacted so the hook-intercepted paths match the
registry's redaction contract. Adds tests asserting a secret-bearing hook
reason/feedback is scrubbed and flips Redacted.
2026-06-11 20:34:35 +05:30
gnanam1990 0c37093974 audit: address PR review findings (security, correctness, tests)
Fixes from the code review on the critical/high audit branch:

- tools/apply_patch: parseHunkCounts now reads only the range section between
  the opening and closing "@@". A crafted section heading like
  "@@ -1,1 +1,1 @@ +1,999999" could otherwise overwrite the line count, wedge
  the parser in hunk mode, and swallow later file headers so they escaped
  validatePatchPaths / recheckPatchWriteTargets — a workspace-confinement bypass.
- providerhealth: the SSRF-hardened dial now tries every validated address in
  order instead of pinning addrs[0], so a dual-stack/multi-record provider isn't
  failed by a single dead record. Extracted dialValidatedAddrs for coverage.
- hooks/dispatch: a beforeTool hook killed by its deadline now fails CLOSED
  (vetoes the tool) instead of fail-open. A launch failure (missing binary)
  still fails open so a misconfigured hook can't wedge every tool call.
- cron: Store.Get returns a distinct ErrJobNotFound sentinel; cron_run only
  treats that as "removed during run" and still records/persists on a transient
  read error instead of silently dropping the run.
- providers/anthropic: closeOpen finalizes thinking buffers still open at stream
  end (SSE ended after thinking_delta/signature_delta but before
  content_block_stop), so they survive in the done event's ReasoningBlocks for
  replay. Finalized in index order.
- providers/gemini: corrected the stale thinkingConfig comment (no IncludeThoughts
  field; thought summaries are suppressed by skipping part.Thought parts).

Tests: regression coverage for each fix, plus the requested nitpicks —
EOF-terminated MCP line, recipe-prompt propagation assertion, checked
file.Close() in session store tests, and a shortened bash process-group timeout
test via the bashWaitDelay hook.

Note: declined the suggestion to replay a "signature" on redacted_thinking
blocks — Anthropic's redacted_thinking carries only an opaque "data" field; the
signature is for regular thinking blocks, and the stream never populates one for
redacted blocks.
2026-06-11 15:02:09 +05:30
gnanam1990 28b32297f6 hooks: add a Dispatcher that runs and audits configured hooks
The package had all the primitives (Select, ConfigStore, AuditStore) but no
caller ever ran a hook. Add a Dispatcher that selects the hooks for a lifecycle
event, runs each command (no shell) with the event payload on stdin under a
timeout, and records started/completed audit events. A beforeTool hook that
exits non-zero vetoes the tool and stops the chain; other events are advisory.
A hook that cannot be launched is recorded as an error but never blocks. The
command runner is injectable for testing.
2026-06-11 15:02:09 +05:30
Anandan 561fe0b7ad Add specialist lifecycle metadata (#106) 2026-06-07 11:06:30 +05:30
KRATOSandKRATOS f43db6f600 [codex] add Go extension backend commands (#61)
* feat(go): add plugin manifest backend

* feat(go): add extension policy stores

* feat(go): expose extension backend commands

* fix(go): reject rooted plugin paths cross-platform

* fix(go): address extension backend review

* fix(go): harden extension backend persistence

* fix(go): preserve extension usage and hook enabled semantics

* fix(go): use process-released MCP permission locks

* fix(plugins): close symlink missing-leaf escapes

* fix(plugins): fail closed on manifest realpath errors

* fix(tests): keep plugin realpath regression portable

* fix: enable CodeRabbit verdict workflow

---------

Co-authored-by: KRATOS <[email protected]>
2026-06-05 12:11:36 +05:30