safety + repl: opts.category for Norris + probe (Phase 7 commit #4)

Closes the last two broker call sites that flow through safety.lua.
Together with commits #1-#3, all 7 broker call sites in aish now
attribute usage to the cost accumulator under the right category.

Changes:

  safety.lua:

  - llm_probe (the YES/NO destructive checker) — broker.chat call
    gains opts.category = "probe". Captures (text, usage) via
    (reply, second) and, when opts.on_usage is provided AND the
    call succeeded, routes second through opts.on_usage(model,
    category, payload). N4 signature chain: opts already flowed
    through llm_second_opinion -> M.is_destructive from #52's
    work; opts.on_usage rides along naturally with no further
    signature change.

  - M.norris_step (Norris main broker round-trip):
      * opts to broker.chat_stream gains category = "norris"
      * probe_opts (passed to is_destructive inside the loop)
        gains on_usage = helpers.on_usage so the LLM probe's
        cost lands under "probe" too
      * on_delta wrapper adds elseif kind == "usage" branch that
        calls helpers.on_usage(payload.model, payload.category,
        payload). Coexists cleanly with the existing text (rehydrator)
        and tool_call branches.

  repl.lua:

  - Norris helpers table gains on_usage = _record_usage. The R5
    central chokepoint (commit #3) does the warn-threshold check
    AND ctx:add_usage atomically.

  - :safety check meta's probe_opts always carries on_usage now
    (independently of whether secrets_session is set). secrets-aware
    scrub_msgs/rehydrate added conditionally as before.

E2E verified against live broker (safety.llm_model = "cloud"):
  - :safety check ls -la /tmp -> 2 cloud probe calls
  - "[aish] session cost $0.000128 has crossed warn_at_dollars=$0.000100"
  - probe category visible in accumulator (would appear in :cost detail
    once commit #5 ships the meta).

Regression: test_safety 87/87, test_router_model 31/31, repl loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 23:01:21 +00:00
parent 8adebd52cc
commit b30212af0f
2 changed files with 46 additions and 14 deletions
+15 -8
View File
@@ -1286,6 +1286,11 @@ function M.run(config)
and secrets.streaming_rehydrator(secrets_session)
or nil
end,
-- Phase 7: hand the central usage chokepoint to safety.lua.
-- safety.norris_step routes the Norris main broker's usage
-- here under category="norris"; safety.is_destructive's LLM
-- probe routes via opts.on_usage under category="probe".
on_usage = _record_usage,
}
local step_n = 1
@@ -1726,15 +1731,17 @@ function M.run(config)
-- :safety check --no-llm <cmd> if added in v2.
-- Issue #52: thread secrets scrub/rehydrate so the probe
-- model sees placeholders for any secrets in `cmd`.
local probe_opts
-- Phase 7: also thread on_usage so the probe's cost
-- lands in the accumulator under category="probe".
local probe_opts = { on_usage = _record_usage }
if secrets_session then
probe_opts = {
scrub_msgs = function(msgs, mode_cfg)
return scrub_messages(msgs,
secrets_mode_for(mode_cfg or active_cfg))
end,
rehydrate = function(t) return secrets_session:rehydrate(t) end,
}
probe_opts.scrub_msgs = function(msgs, mode_cfg)
return scrub_messages(msgs,
secrets_mode_for(mode_cfg or active_cfg))
end
probe_opts.rehydrate = function(t)
return secrets_session:rehydrate(t)
end
end
local hit, reason = safety.is_destructive(cmd, config, probe_opts)
if hit then