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: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ include/naab/ All headers
- **Separation of duties**: per-agent `network_allowed` (bool, same pattern as `shell_allowed`) and `allowed_actions` matrix (`["SHELL_EXEC", "NET_CONNECT", "FS_READ", "FS_WRITE", "AGENT_SEND", "TOOL_EXEC"]`). Enforced in `checkNetworkAllowed()`, `checkShellAllowed()`, `checkFilesystemAllowed()`, and `agentSend()`. `TOOL_EXEC` controls tool execution in agent tool loops. Ratchet enforcement prevents mid-run loosening of action matrix.
- **Output admissibility**: Post-CDD gate on response coherence — symmetric to `checkAdmission()` (pre-send). Evaluates after CDD scoring, before response dict construction. Three actions: `block` (enforce() throws, response not returned — HARD/SOFT = GovernanceHardError, DETECT = catchable), `quarantine` (response returned with `admissibility.admissible = false`), `attest` (quarantine + Ed25519 signed attestation in response + telemetry). PulseVerdict IMPAIRED forces inadmissible regardless of score. Config: `circuit_breaker.output_admissibility.enabled`, `threshold` (default 0.70), `action` (default "quarantine"), `level` (for "block" only, clamped to minimum DETECT), `inadmissible_history` ("commit" default / "exclude" — whether quarantined/attested responses enter handle history), `gate_tool_calls` (bool, default false — coherence/pulse gate before each tool execution in the tool loop), `max_quarantine_streak` (default 5, 0 = disabled — consecutive quarantined responses before GovernanceHardError terminates the agent; prevents the quarantine+commit degradation loop), `require_corroboration` (default 0 = disabled/historical; N = a quarantined turn only ADVANCES the streak when N DISTINCT penalising signals fired that turn). Coherence is a weighted sum, so one noisy signal firing repeatedly accumulates to a kill alone — on replayed traces `semantic_stability` firing on a compliant agent's varied phrasing killed the run at turn 8, and corroboration=2 removed that false kill while every genuine failure mode still died on the same turn. Counted from `DriftState.last_turn_penalties` (one slot per signal: distinct by construction, absorbed firings excluded, detection-only `coherence_velocity` excluded free since it never penalises). Do NOT use `signals_fired_this_turn` — `repeated_failures` and `intent_contradictions` increment it inside per-item loops, so one signal could corroborate itself. Gates the streak ADVANCE only: admissibility, history disposition, attestation and `OUTPUT_INADMISSIBLE` are unchanged, and a declined advance emits `QUARANTINE_UNCORROBORATED`. Applied identically at both enforcement sites (`agentSend` and `agentCommit`). Ratchet: enabling it or raising N yields fewer kills = loosening. Ratchet-enforced (enabled→disabled, threshold down, action rank down, exclude→commit, gate_tool_calls true→false, streak limit removed/raised = violations). Telemetry: `OUTPUT_ADMISSIBILITY_EVAL` (pass/fail), `OUTPUT_INADMISSIBLE` (fail only, includes `history_committed`). Dashboard: `OA Gate:` line.
- **Split commit** (see `docs/transition-admissibility.md`): `agent.send()` separates accounting commits (tracker turns/tokens + `recordAutonomousAction()` exposure — committed BEFORE the post-receive gates, so blocked attempts still count toward the next admission projection) from conversation-state commits (history append happens AFTER CDD + OA gate — blocked turns, including catchable DETECT blocks, never enter handle history).
- **Propose/commit** (selective adjudication): `agent.propose(handle, msg [, n])` generates up to `propose_candidates_max` (per-agent config, 0 = disabled, increase = ratchet violation) candidates with NO state commit — no history append, no turn increment, no CDD mutation, no tool execution (tool defs never sent). Candidate 0 uses the configured temperature; candidates 1+ step it up (+0.15 each, capped at 1.5) for sampling diversity — a local-copy change only, never committed to config or history. Each candidate carries an `admissibility` section (read-only score vs current DriftState snapshot) and an anti-forge `__proposal` HMAC nonce; authoritative content lives server-side in `s_pending_proposals`. `agent.commit(handle, proposal)` runs the post-receive pipeline exactly once (BSD AGENT_RESPONSE, CDD, full enforce-capable OA gate, split commit of history + turn). Proposals are single-use and invalidated by any subsequent send/propose/commit on the handle. Lease-expired state refuses propose (fail-closed — re-authorize via `agent.send()`, which renews the lease on a passed challenge). The gate is the **standing lease, not the engine-global governance level**: level is scrutiny, not authorization, and nothing the handle does lowers it (a passed challenge renews the lease and recovers coherence; de-escalation needs `deescalate_sustained` calm pressure samples), so gating on level made the error message's own remedy unsatisfiable — live run 12 refused propose for a handle holding 19 turns of lease after 47 passed challenges. Lease expiry covers BOTH `standing_lease_turns` and `standing_lease_seconds` via the shared `leaseExpiredLocked()` helper (propose previously checked only the turn-based half; the level clause was masking that). Agents with **no lease configured** keep the level test — there is no authorization state to consult, so loosening them would be a pure weakening; configure a lease if propose must work under elevated scrutiny. CRITICAL denies regardless of lease. Note `agent.commit()` has no step-up/lease gate of its own — propose's gate is the only authorization check on the path (commit still runs CDD + the full enforce-capable OA gate). Test: `test_propose_commit.sh` Group F. `orchestra.select_admissible(candidates [, spec])` is the pure ranking helper (spec reuses enforce_convergence pattern/required_fields). Telemetry: `AGENT_PROPOSE`, `AGENT_PROPOSAL_COMMIT`.
- **Propose/commit** (selective adjudication): `agent.propose(handle, msg [, n])` generates up to `propose_candidates_max` (per-agent config, 0 = disabled, increase = ratchet violation) candidates with NO state commit — no history append, no turn increment, no CDD mutation, no tool execution (tool defs never sent). Candidate 0 uses the configured temperature; candidates 1+ step it up (+0.15 each, capped at 1.5) for sampling diversity — a local-copy change only, never committed to config or history. Each candidate carries an `admissibility` section (read-only score vs current DriftState snapshot) and an anti-forge `__proposal` HMAC nonce; authoritative content lives server-side in `s_pending_proposals`. `agent.commit(handle, proposal)` runs the post-receive pipeline exactly once (BSD AGENT_RESPONSE, CDD, full enforce-capable OA gate, split commit of history + turn). Proposals are single-use and invalidated by any subsequent send/propose/commit on the handle. Lease-expired state refuses propose (fail-closed — re-authorize via `agent.send()`, which renews the lease on a passed challenge). The gate is the **standing lease, not the engine-global governance level**: level is scrutiny, not authorization, and nothing the handle does lowers it (a passed challenge renews the lease and recovers coherence; de-escalation needs `deescalate_sustained` calm pressure samples), so gating on level made the error message's own remedy unsatisfiable — live run 12 refused propose for a handle holding 19 turns of lease after 47 passed challenges. Lease expiry covers BOTH `standing_lease_turns` and `standing_lease_seconds` via the shared `leaseExpiredLocked()` helper (propose previously checked only the turn-based half; the level clause was masking that). Agents with **no lease configured** keep the level test — there is no authorization state to consult, so loosening them would be a pure weakening; configure a lease if propose must work under elevated scrutiny. CRITICAL denies regardless of lease. `agent.commit()` re-checks the lease via the same `leaseExpiredLocked()` helper, in its existing `s_agent_mutex` block: authority is time-varying and a proposal is not, so the deliberation gap propose/commit exists to create is exactly where a wall-clock lease can lapse. Only the wall-clock half can fire there — turns cannot advance between propose and commit, so a turn-based lease that passed propose is still valid at commit by construction. The refusal mirrors the two existing shapes: a message carrying the `step-up` substring when `step_up_enabled` (renewable via `agent.send()`), the hard "lease expired" wording when not. Because the re-authorizing send *invalidates outstanding proposals*, a caller must redo propose→select→commit as a unit rather than retry the commit — `examples/living-script_extended` shows the pattern, pairing a pre-emptive `lease_remaining_seconds` check with a one-shot retry. Commit still omits `hard_stopped` and `recordAutonomousAction` deliberately: both guard *spend*, and commit makes no API call (the candidate was paid for at propose time). Commit also deliberately does NOT call `reloadIfChanged()` — an accepted reload runs `onAgentConfigChanged()`, which would re-baseline `mandate_keywords` and score an already-generated candidate (S11/S20) against a mandate it never received, and the atomic `rules_ptr_` swap would dangle `config`. Tests: `test_propose_commit.sh` Groups F (propose) and G (commit). `orchestra.select_admissible(candidates [, spec])` is the pure ranking helper (spec reuses enforce_convergence pattern/required_fields). Telemetry: `AGENT_PROPOSE`, `AGENT_PROPOSAL_COMMIT`.
- **Governance level de-escalation hysteresis**: escalation is immediate once sustained-pressure thresholds are met, but stepping DOWN requires `circuit_breaker.deescalate_sustained` (default 2) consecutive turns where the computed target level sits below the current level, and moves one level at a time (engine-global `deescalate_calm_turns_` counter; pulse floors still apply). Calm turns are counted only from the handle whose pressure raised/held the level (`deescalate_pressure_handle_`) — a calm sibling agent's turns neither advance nor reset the counter, so interleaved multi-agent traffic can't drain scrutiny a degraded agent earned. Previously a single calm composite sample dropped straight to NORMAL — scrutiny vanished exactly when a decaying agent briefly looked calm. Ratchet: lowering `deescalate_sustained` mid-run is a violation.
- **Campaign findings**: `docs/governance-campaign-findings.md` maps each governance defect found in living-script runs 7-23 to its mechanism, its regression test, and whether the fix has ever fired against a live API (confirmed / inert-verified / stub-only / n/a). Also records observations deliberately NOT changed (S9's frozen baseline, S17's warm-up baseline and adaptive-baseline gating, CDD's blindness to erosion from the response stream) and four proposals withdrawn on evidence — read it before "fixing" any of those.
- **Governance Pulse** (`src/runtime/governance_engine.cpp`): PulseVerdict (HEALTHY/DEGRADED/IMPAIRED) with hysteresis, two-phase mutex, stepped recovery, BSD emission on transitions, `PULSE_TRANSITION` telemetry on every verdict change (from/to verdict, degradation_reasons, streak counters, epoch) — the BSD events alone surface only when a pattern matches them, so a degrade-then-recover between two `governance.health()` samples was previously visible only as an unexplained two-step jump in `governance_epoch`. Dashboard line. `governance.health()` stdlib returns `{verdict, coherence, governance_epoch, mode, zone, active, consecutive_passes, observation_count, integrity_verified, ...}` — it does **not** expose `governance_level`; read the level from `GOVERNANCE_LEVEL_CHANGE` telemetry (`from_level`/`to_level`) or `CDD_TURN`'s `level` field instead. Config: `governance_health` section. Six degradation signals, each named in `pulse.degradation_reasons` (exposed by `governance.health()`, in the `GOVERNANCE_HEALTH_QUERY` telemetry event, on the dashboard's `degraded by:` line, and in the PULSE_DEGRADED/PULSE_IMPAIRED BSD event text): `bsd_disconnected`, `cdd_disconnected`, `low_entropy`, `telemetry_stalled`, `semantic_inert`, `uniform_passes`. The reason **latches** — reaching DEGRADED resets the clean-turn streak, so the next evaluation finds nothing firing while the verdict is still DEGRADED; the field keeps the cause of the current verdict and clears on recovery to HEALTHY. The verdict sawtooths (DEGRADED resets the streak → recovery two turns later → climbs again), so a single terminal sample says nothing about whether a tripwire ever tripped — sample per turn or read the transitions. `pulse.consecutive_passes` counts **consecutive agent turns with no enforcement**, ticked once per pulse evaluation in `computePulseVerdict()` and reset by `enforce()` (via `blocked_since_last_pulse`) and on epoch boundaries. It is deliberately NOT incremented in `recordPass()`: every `recordPass` site is a static-source, capability, plugin or runtime-pin check — nothing on the agent-behaviour path records passes at all — so counting them there measured how much generated source had been scanned. `agent_governance_active_` did not contain that (it is a one-way latch set at the first `agent.create()`), so post-agent `codegen.run()` and polyglot blocks fed ~5 uniform passes each into the counter and any codegen-heavy run saturated `consecutive_passes_suspicion` on volume alone. Test: `tests/governance_v4/test_pulse_uniformity.sh`. Note the pulse is evaluated only from `checkContextDrift()` — with CDD disabled (or BSD disabled, which starves CDD of AGENT_RESPONSE events) it never runs.
Expand Down
Loading
Loading