Skip to content

Commit 461c267

Browse files
alsechemistclaude
andcommitted
feat(agent): 6-f.5 — unbounded persistence + any-unique-field block disambiguation (closes 6-f.5)
Operator web-test feedback on 6-f.4 (the duplicate custom-tracecat scenario), recorded as the 2026-07-06 ADR 0032 addendum. 1. No hard step caps (operator directive: "utilize the step count, but never limit it to any specific value"): the agent loop drops its fixed step ceiling and persists until the model answers. Remaining stops are real: the no-progress guard (unchanged, now primary), a NEW context-fit guard (AGENT_CONTEXT_FIT_THRESHOLD 0.8 x the provider's EFFECTIVE window — new effective_context_window(): Ollama = loaded num_ctx, failover chain = min across links), and a NEW optional AGENT_STEP_BREAKER (default off). EVERY forced stop synthesizes a best-effort answer from gathered evidence — stop_reason="budget_exhausted" and its canned apology are retired. max_safe_autonomous_steps becomes a soft take-stock checkpoint cadence. Web-research budget 12 -> 32. SYSTEM_PROMPT #7 PERSIST UNTIL SATISFIED. 2. Any-unique-field disambiguation: block_key matches the identity element first, then ANY leaf value uniquely selecting one live instance (hook_url/api_key) via one shared selector used by tool capture, build_candidate, the executor and the persistence proofs; B2 ops now on any unblocked section; identity-carry accepts identity OR leaf value; ambiguous-key refusals ENUMERATE each instance's discriminating fields so the model re-addresses instead of hallucinating. 908 backend tests / 0 skips (+12); mypy --strict 116 files; ruff; dashboard tsc + eslint. Docs: ADR 0032 addendum, roadmap 6-f.5 + 6-f.6 (deployment- aware config application, next), CHANGELOG, PROGRESS, memories (incl. the post-slice Nemotron model-switch instruction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eb13f9b commit 461c267

27 files changed

Lines changed: 1104 additions & 221 deletions

docs/10-build-roadmap.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,33 @@ write). See the ADR 0032 2026-07-05 addendum.
474474
directive, approval posture unchanged). GUI: block-op targets, upsert
475475
diffs, removal cards. Web-test: the `<integration>`/virustotal case
476476
end-to-end.
477+
- **6-f.5** ✅ — web-test feedback (2026-07-06): unbounded persistence +
478+
any-unique-field disambiguation. (1) NO hard step caps (operator
479+
directive: "utilize the step count, but never limit it to any specific
480+
value"): the loop persists until the model answers; the only other stops
481+
are the no-progress guard, the context-fit guard (0.8 × the provider's
482+
EFFECTIVE window — Ollama's loaded `num_ctx`, chain = min across links)
483+
and the optional `AGENT_STEP_BREAKER` (default off; Phase 6.10 GUI
484+
consumer) — and EVERY forced stop synthesizes a best-effort answer from
485+
the gathered evidence (`budget_exhausted` retired). The graded
486+
`max_safe_autonomous_steps` became a soft take-stock checkpoint cadence.
487+
Web budget 12 → 32. `SYSTEM_PROMPT` #7 PERSIST UNTIL SATISFIED (dynamic
488+
think/reason/verify/justify/validate on ANY request shape). (2) The
489+
duplicate-name fix (live tracecat scenario): `block_key` matches identity
490+
first, then ANY leaf value uniquely selecting one instance
491+
(`<hook_url>`/`<api_key>`); one shared selector across tool preview /
492+
`build_candidate` / executor / persistence proofs; B2 ops on ANY
493+
unblocked section; ambiguous-key refusals ENUMERATE each instance's
494+
discriminating fields so the model re-addresses instead of hallucinating.
495+
ADR 0032 addendum (2026-07-06).
496+
- **6-f.6** (next) — deployment-aware config application (operator
497+
directive): detect all-in-one vs distributed via `/cluster/status`;
498+
distributed applies per node via the per-node cluster configuration
499+
endpoints (verify empirically first — ossec.conf is NOT cluster-synced;
500+
today's `PUT /manager/configuration` writes the master only), per-node
501+
validation + rollback, one restart, node list surfaced to the approver.
502+
Indexer/dashboard config files are unreachable via the Server API →
503+
wolf-pack (Phase 12) scope, stated honestly.
477504

478505
**then** severity-tiered authority / four-eyes / crown-jewel tagging (policy
479506
hooks, B1 default = approval-for-all); auto-execution (Phase 13). The remaining

docs/CHANGELOG.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,84 @@ Copy this block and fill in at the start of each session entry:
4949

5050
---
5151

52+
## 2026-07-06 — 6-f.5: unbounded persistence + any-unique-field disambiguation (web-test feedback)
53+
54+
**Session type:** claude-code
55+
**Phase:** 6-f.5 (ADR 0032 addendum 2026-07-06; operator web-test feedback on 6-f.4)
56+
**Branch / commit:** main
57+
58+
### What we did
59+
- **No hard step caps** (operator directive: "utilize the step count, but never
60+
limit the step count to any specific value"). `agent/loop.py` replaced
61+
`for step in range(budget)` with unbounded persistence: the loop runs until
62+
the model answers; the only other stops are (a) the no-progress guard (two
63+
consecutive all-repeat steps — unchanged mechanism, now the primary guard),
64+
(b) a NEW context-fit guard (last call's `input_tokens`
65+
`AGENT_CONTEXT_FIT_THRESHOLD` (0.8) × the provider's EFFECTIVE context window
66+
— new `effective_context_window()`: Ollama returns its loaded `num_ctx`,
67+
`FailoverProvider` the min across links, others the descriptor window), and
68+
(c) a NEW optional operator circuit breaker `AGENT_STEP_BREAKER` (default
69+
0 = off; Phase 6.10 GUI consumer). EVERY forced stop now runs
70+
`_synthesized_stop` — a no-tools re-prompt composing the best answer from
71+
gathered evidence, honest about gaps — so `stop_reason="budget_exhausted"`
72+
and its canned apology are RETIRED (TS union keeps the member for legacy
73+
rows; the dashboard already has no-budget step phrases). The graded
74+
`max_safe_autonomous_steps` became a soft checkpoint cadence (take-stock
75+
nudge every N steps). Web-research per-request budget raised 12 → 32.
76+
- **Any-unique-field disambiguation** (the live duplicate `custom-tracecat`
77+
failure): `block_key` now matches the identity element first and falls back
78+
to ANY leaf value that uniquely selects one live instance (`<hook_url>`,
79+
`<api_key>`, …) — one shared `_identified_matches` selector behind
80+
`find_identified_blocks`, so tool capture, `build_candidate`, the executor
81+
and the persistence proofs all agree; B2 ops accepted on ANY unblocked
82+
section; validator identity-carry became `content_carries_key` (identity OR
83+
leaf value); ambiguous-key refusals ENUMERATE each instance's discriminating
84+
fields (`describe_instances`) + tell the model to re-call with one — the
85+
refusal now teaches the fix instead of starving the model into
86+
hallucination. True duplicates (no unique field) stay hand-fix.
87+
- Prompts: `SYSTEM_PROMPT` gained principle **#7 PERSIST UNTIL SATISFIED**
88+
(dynamic think/reason/verify/justify/validate on ANY request shape; act on
89+
tool guidance instead of giving up) + the #4 block_key teaching; GUIDED and
90+
WEB_RESEARCH suffixes lost their "keep the budget tight" phrasing for
91+
persistence phrasing.
92+
- Wired `agent_step_breaker` / `agent_context_fit_threshold` Settings into
93+
both `AgentLoop` constructions in `api/chat.py`.
94+
- Tests: 908 passed / 0 skips (+12): persists-past-the-grade (11 steps on a
95+
5-step grade + 2 checkpoint nudges pinned), breaker synthesis (no-tools +
96+
forced nudge pinned), context-fit stop, tracecat duplicate-name matrix
97+
(domain + tool level: enumerated refusal, hook_url-addressed preview/queue),
98+
validator any-section B2 + leaf-value carry, `effective_context_window`
99+
(Ollama num_ctx + chain floor), prompt tokens.
100+
- Gates: `make typecheck` Success (116 files), ruff clean, dashboard tsc +
101+
eslint clean (types.ts comment only).
102+
103+
### What we decided
104+
- Stops must be grounded in reality (no progress / physical context limit /
105+
explicit operator budget), never a hardcoded number — and every stop path
106+
produces a real best-effort answer. Recorded as an ADR 0032 addendum.
107+
- Deployment-aware config application = **6-f.6** (next): per-node apply via
108+
`/cluster/{node_id}/configuration` (verify empirically), honest wolf-pack
109+
boundary for indexer/dashboard files. Roadmap updated.
110+
111+
### What broke / what we discovered
112+
- The live 8-step wall was the failover chain's conservative floor (min across
113+
links → qwen3:8b's 8), not the cohere grade (15).
114+
- Wolf had actually TRIED `block_key=<hook_url>` in the web-test — the right
115+
instinct, rejected by name-only matching; the starved refusal then led it to
116+
hallucinate "identical hook_urls". The fix makes the refusal itself carry
117+
the disambiguation data.
118+
- Repo-wide `ruff format --check` drift (~47 files) pre-exists this slice; CI
119+
gates `ruff check` only. Formatted only the files this slice touched.
120+
121+
### What's next
122+
- Operator web-test of 6-f.5 (rerun the two tracecat scenarios) — needs the
123+
`manager:update_config` credential; a loop-capable chat model (qwen3:8b)
124+
recommended over `cohere/north-mini-code:free`.
125+
- 6-f.6 deployment-aware config application; then the Nemotron 3 Ultra/Super
126+
(free) model-switch evaluation the operator requested.
127+
128+
---
129+
52130
## 2026-07-05 — 6-f.4: config-authoring generalization + research-first agent posture
53131

54132
**Session type:** claude-code

docs/PROGRESS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
>
77
> For history of what changed when, see `CHANGELOG.md` (append-only).
88
9-
**Last updated:** 2026-07-05 by claude-code (**6-f.4 — config-authoring generalization + research-first agent posture; slice SHIPPED, operator web-test pending.** ADR 0032 B landed in full. **B3 free-form within rails:** `EDITABLE_SECTIONS` (7-entry allowlist) → `BLOCKED_SECTIONS` {auth, cluster, indexer, rule_test, ruleset}; any other well-formed section authorable (`is_valid_section_name`); `update_section` ADDS an absent single-instance section (`insert_section_block` before the final `</ossec_config>`). **B2 block-identity (the virustotal fix):** `IDENTITY_KEYS` {integration→`<name>`, localfile→`<location>`, command→`<name>`}; new forward ops `upsert_block`/`remove_block` + `block_key` addressing ONE instance of a repeated section; validator enforces the upsert content CARRIES the addressed identity (no address-X-write-Y) + refuses duplicate-key files; persistence proofs identity-scoped + reformatting-tolerant (`block_persisted`/`block_removed`); `restore_config` matches an optional `block_key` (undo THAT instance's change). **B1 confirm-diff = TWO-PHASE IN THE TOOL:** an unconfirmed `propose_config_change` call performs ALL author-time work (capability pre-flight → structural validation → live config read → `build_candidate` dry-run — the SAME shared transformation the executor runs) and returns `state="needs_confirmation"` + `current_content`, queueing NOTHING; only the `user_confirmed=true` re-call queues. KEY REFINEMENT (ADR addendum): author-time manager validation is impossible without a write (`GET /manager/configuration/validation` validates the ON-DISK file) → the author-time dry-run IS the transformation; manager validation stays at execute with auto-rollback (unchanged). Executor: per-op freshness (`_target_fresh` — an ADD whose target appeared since propose is stale), shared `build_candidate` perform, op-aware persist confirm, `operation`/`block_key` in results. Prompts: SYSTEM_PROMPT #4 = THE AUTHORING LOOP (research → preview → show diff → confirm → propose) + generalized ops; WEB_RESEARCH_SUFFIX += **RESEARCH-TO-ACT** (the [[web-research-as-universal-power]] directive — research chains into propose tools; approval posture unchanged). GUI actions page: block-op targets (`section <integration> 'virustotal'`), upsert diff (ADD shows "none — this adds a new block"), removal cards, op+key in result lines. GATES: **896 backend tests / 0 skips** (+30), mypy --strict (116 files), ruff+format, dashboard tsc+eslint. **LIVE self-validated (wiring):** qwen3:8b chained `query_runbook` → `propose_config_change(upsert_block, integration, block_key=virustotal)` UNPROMPTED; capability pre-flight correctly refused (org's scoped cred lacks `manager:update_config`) + honest relay — preview→confirm→queue phases unit-pinned, live in the operator web-test. NOTE: operator's live chat model is `cohere/north-mini-code:free` (emitted its tool call as prose — ADR 0031 free-model reality); probe ran on a temporary qwen3:8b flip, `.env` restored byte-identical + server restarted on the operator posture. NEXT: **operator web-test of 6-f.4** (virustotal end-to-end with the `manager:update_config` credential + qwen3:8b) → then the 6-f follow-ons (Brave/Tavily per-org, Phase 6.10 GUI consumers) or the next operator-chosen slice; Phase 6.13 grounding enrichment queued "later".)
9+
**Last updated:** 2026-07-06 by claude-code (**6-f.5 — unbounded persistence + any-unique-field disambiguation; slice SHIPPED (the 6-f.4 web-test feedback fixes).** From the operator's web-test (duplicate `custom-tracecat` + a generic request dying at 8 steps). **(1) NO hard step caps** (operator directive: "utilize the step count, but never limit the step count to any specific value"): `agent/loop.py` dropped `for step in range(budget)` — the loop persists until the model answers; the remaining stops are REAL: the no-progress guard (unchanged mechanism, now the primary guard), the NEW **context-fit guard** (last call's `input_tokens` ≥ `AGENT_CONTEXT_FIT_THRESHOLD` 0.8 × the provider's EFFECTIVE window — new `effective_context_window()`: Ollama = its loaded `num_ctx`, FailoverProvider = min across links; stops BEFORE Ollama's silent head-truncation), and the NEW optional `AGENT_STEP_BREAKER` circuit breaker (default 0 = off; Phase 6.10 GUI consumer). EVERY forced stop → `_synthesized_stop` (no-tools re-prompt composing the best answer from gathered evidence, honest about gaps) — **`stop_reason="budget_exhausted"` + the canned apology RETIRED** (TS union keeps the member for legacy persisted rows; dashboard needed no functional change — no-budget step phrases already existed). `max_safe_autonomous_steps` = a soft take-stock checkpoint cadence (`_CHECKPOINT_NUDGE` every N steps). Web-research budget 12 → 32. `SYSTEM_PROMPT` gained **#7 PERSIST UNTIL SATISFIED** (dynamic think/reason/verify/justify/validate on ANY request shape — generic/partial/full; act on tool guidance instead of giving up); GUIDED/WEB suffixes de-budgeted. Discovery: the live 8-step wall was the failover chain's conservative floor (min across links → qwen3:8b's 8), not cohere's grade (15). **(2) Any-unique-field disambiguation** (the duplicate-name fix): `block_key` matches the identity element FIRST and falls back to ANY leaf value uniquely selecting one live instance (`<hook_url>`/`<api_key>`); ONE shared `_identified_matches` selector behind `find_identified_blocks` → tool capture, `build_candidate`, executor and `block_persisted`/`block_removed` all agree; B2 ops now valid on ANY unblocked section (validator's IDENTITY_KEYS-membership requirement dropped); identity-carry → `content_carries_key` (identity OR carried leaf value; to change the addressing field itself, address by another unique field); ambiguous-key refusals **ENUMERATE each instance's discriminating fields** (`describe_instances` — values no other instance shares) + "re-call with one" — the refusal now TEACHES the fix (the web-test hallucination was caused by the starved refusal: Wolf had TRIED `block_key=hook_url`, and name-only matching rejected it). Truly indistinguishable duplicates stay hand-fix. ADR 0032 addendum (2026-07-06) records both. GATES: **908 backend tests / 0 skips** (+12), `make typecheck` Success (116 files), ruff clean, dashboard tsc+eslint clean. (Repo-wide `ruff format` drift ~47 files pre-exists; CI gates `ruff check` only — formatted only touched files.) NEXT: **operator web-test of 6-f.5** (rerun both tracecat scenarios; needs the `manager:update_config` credential + a loop-capable chat model — qwen3:8b) → **6-f.6 deployment-aware config application** (detect via `/cluster/status`; per-node `/cluster/{node_id}/configuration` VERIFIED EMPIRICALLY first — ossec.conf is NOT cluster-synced, today's PUT writes the master only; indexer/dashboard config = wolf-pack Phase 12 boundary, stated honestly) → then the operator-requested Nemotron 3 Ultra/Super (free) model-switch evaluation.)
10+
11+
_Prior:_ 2026-07-05 (**6-f.4 — config-authoring generalization + research-first agent posture; slice SHIPPED, operator web-test pending.** ADR 0032 B landed in full. **B3 free-form within rails:** `EDITABLE_SECTIONS` (7-entry allowlist) → `BLOCKED_SECTIONS` {auth, cluster, indexer, rule_test, ruleset}; any other well-formed section authorable (`is_valid_section_name`); `update_section` ADDS an absent single-instance section (`insert_section_block` before the final `</ossec_config>`). **B2 block-identity (the virustotal fix):** `IDENTITY_KEYS` {integration→`<name>`, localfile→`<location>`, command→`<name>`}; new forward ops `upsert_block`/`remove_block` + `block_key` addressing ONE instance of a repeated section; validator enforces the upsert content CARRIES the addressed identity (no address-X-write-Y) + refuses duplicate-key files; persistence proofs identity-scoped + reformatting-tolerant (`block_persisted`/`block_removed`); `restore_config` matches an optional `block_key` (undo THAT instance's change). **B1 confirm-diff = TWO-PHASE IN THE TOOL:** an unconfirmed `propose_config_change` call performs ALL author-time work (capability pre-flight → structural validation → live config read → `build_candidate` dry-run — the SAME shared transformation the executor runs) and returns `state="needs_confirmation"` + `current_content`, queueing NOTHING; only the `user_confirmed=true` re-call queues. KEY REFINEMENT (ADR addendum): author-time manager validation is impossible without a write (`GET /manager/configuration/validation` validates the ON-DISK file) → the author-time dry-run IS the transformation; manager validation stays at execute with auto-rollback (unchanged). Executor: per-op freshness (`_target_fresh` — an ADD whose target appeared since propose is stale), shared `build_candidate` perform, op-aware persist confirm, `operation`/`block_key` in results. Prompts: SYSTEM_PROMPT #4 = THE AUTHORING LOOP (research → preview → show diff → confirm → propose) + generalized ops; WEB_RESEARCH_SUFFIX += **RESEARCH-TO-ACT** (the [[web-research-as-universal-power]] directive — research chains into propose tools; approval posture unchanged). GUI actions page: block-op targets (`section <integration> 'virustotal'`), upsert diff (ADD shows "none — this adds a new block"), removal cards, op+key in result lines. GATES: **896 backend tests / 0 skips** (+30), mypy --strict (116 files), ruff+format, dashboard tsc+eslint. **LIVE self-validated (wiring):** qwen3:8b chained `query_runbook` → `propose_config_change(upsert_block, integration, block_key=virustotal)` UNPROMPTED; capability pre-flight correctly refused (org's scoped cred lacks `manager:update_config`) + honest relay — preview→confirm→queue phases unit-pinned, live in the operator web-test. NOTE: operator's live chat model is `cohere/north-mini-code:free` (emitted its tool call as prose — ADR 0031 free-model reality); probe ran on a temporary qwen3:8b flip, `.env` restored byte-identical + server restarted on the operator posture. NEXT: **operator web-test of 6-f.4** (virustotal end-to-end with the `manager:update_config` credential + qwen3:8b) → then the 6-f follow-ons (Brave/Tavily per-org, Phase 6.10 GUI consumers) or the next operator-chosen slice; Phase 6.13 grounding enrichment queued "later".)
1012

1113
_Prior:_ 2026-07-05 (**6-f.3 operator web-test PASSED + two operator directives documented; 6-f.4 OPENED.** Operator web-tested 6-f.3 on the live cluster (decoder/rule authoring + integration research): docs-first citations, official badges, clickable evidence links all confirmed — `WEB_SEARCH_ENABLED=1` stays on, feature accepted. DIRECTIVE 1 (**foundational — `web-research-as-universal-power` memory, ADR 0032 2026-07-05 addendum, roadmap 6-f intro**): web research is Wolf's UNIVERSAL power, its go-to tool for ALL operations — research-to-act like Claude: unknown procedure → research docs-first w/ valid sources+references → learn in-session → act via the capability-gated propose/approve/execute paths ("I want Wolf to be unstoppable"); provenance-backed learning (citations, untrusted envelope, grounding); authority model UNCHANGED (RBAC + approval gateway gate every write); extends wolf-unrestricted-full-power to the knowledge dimension; 6-f.4 gains the research-first agent-posture work. DIRECTIVE 2 (**roadmap Phase 6.13 — Grounding enrichment & verification depth, now COMMITTED**): the web-test showed correct docs-sourced answers still landing mostly Uncertain/Not-Verified (`grounding 0✓ 2⚠ 1✗`) → fully dedicated phase to enhance grounding across EVERY aspect — more Verified verdicts accurately (verification/justification/validation, never loosening the judge); scope: web-research evidence source-tier-aware for the judge · more evidence-supplying tools · per-claim evidence selection (ex-ADR-0026 `cited`) · judge/verdict quality + justification in chips · calibration harness; own ADR when opened; sequencing "later" per operator — 6-f.4 first. NEXT: **6-f.4 ACTIVE** — config-authoring generalization (B1 research→confirm-diff→dry-run→propose · B2 block-identity for repeated `<integration>`/`<localfile>`/`<command>` · B3 free-form within rails, cluster/auth/indexer/ruleset blocked) + the research-first agent posture; web-test = `<integration>`/virustotal end-to-end.)
1214

0 commit comments

Comments
 (0)