release 6.94.0: external-project delegation gap closed, nanny/evidence honesty end to end (sprint phase A)

The Slime Lab Escape saga (2026-08-10; 4 attempts, ~$215, the game built
twice) exposed that _WORKSPACE_ALLOWED_TOOLS predates delegation: tasks in
external project workspaces and their read-only children were resolved onto
the harness route and instructed to be nannies while delegate_start/wait/
cancel were filtered from their toolsets — every nanny burned metered API
tokens while the subscription paid $0, and a task contract demanded
send_photo while the same filter hid it.

- _WORKSPACE_ALLOWED_TOOLS += delegate_start/wait/cancel, switch_model,
  send_photo/send_video/send_file; subset invariant test pins both child
  profiles inside the envelope (registry.py additions only — no filter
  predicate changed).
- F3: enable_tools answers 'hidden by policy: <reason>' vs 'not found' via
  read-only registry.policy_hidden_reason(), drift-pinned to
  get_schema_by_name across six context variants; answers for
  contract-disabled names across ALL discovery sources.
- F4: nanny nudge reads durable delegate_custody.task_execution_evidence
  from the canonical (budget) custody root, so split-root subagents see
  their own delegated runs; PENDING != FAILED — started-but-unsettled runs
  emit NANNY_DELEGATED_RUN_PENDING (wait/cancel, do not finalize over an
  in-flight run) and NANNY_DELEGATED_RUN_FAILED is reserved for terminal
  non-successes.
- Q1A: capability preflight before the first paid LLM round
  (agent.preflight_delegate_visibility): a harness-dispatched child whose
  materialized toolset lacks any of the three delegate verbs either ends
  unrun through the typed zero-spend blocked outcome (explicit harness pin;
  delegate_tools_invisible, or delegate_visibility_unverified when toolset
  introspection itself failed) or falls back LOUDLY to native (auto; typed
  capability_delta, dispatch fields re-recorded).
- Substrate honesty: actual_substrate is a purely factual enum from custody
  evidence only (harness_used / harness_attempted / native_only) with raw
  attested counts beside it on every surface (durable result, public
  envelope, wait_tasks projection); unreadable evidence OMITS the claim
  (unknown is never native_only) and evidence_read_failed renders as typed
  unknown ("evidence unavailable"), never a false no-run receipt.
- F6 live toolset: promoted_task_toolset is built from the REAL registry
  availability resolution (workspace and non-workspace probes, live
  credential gates, typed unavailable_builtin_tools reasons), not the
  static allowlist union.
- Q2-B: dispatch note = decide delegation first, typed cost classes,
  delegate even small tasks on a healthy lane; no bare 'free'.
- Function-size gate honored: _prepare_task_context decomposed
  (_run_delegate_preflight / _capture_mutation_baseline).
- Docs + ABI mirrors synced; version carriers 6.94.0; README 6.94.0 row
  extended at landing with the amendment behaviors and the PR #176
  disclosure (community Linux .deb/.rpm packaging rides the same landing).

Squash of sprint/slime-landing 628de71e..7ecaa2bc (b49f8192 phase A initial,
6c03c24e audit amendments, 7ecaa2bc review-wave corrective batch) onto the
PR #176 merge; tree verified identical to 7ecaa2bc modulo the README row
amendment.

Adversarial review: initial wave on b49f8192 (SAFE TO PROCEED, accepted
findings applied) + full wave on 6c03c24e (fable triad + two gpt-5.6-sol
lanes + gemini; CRITICAL/HIGH/LOW accepted findings landed in 7ecaa2bc).
Plan: ~/.claude/plans/ouroboros-slime-saga-sprint.md (owner-approved).

Landing-gate scope fix: unreadable custody evidence emits no fabricated zero counts in the wait_tasks projection.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-08-11 01:01:05 +03:00
parent 628de71ec4
commit cccc758f6b
28 changed files with 1590 additions and 83 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
6.93.1
6.94.0

View file

@ -1,4 +1,4 @@
# Ouroboros v6.93.1 — Architecture & Reference
# Ouroboros v6.94.0 — Architecture & Reference
This file is NOT a changelog. Version history lives in README.md, git tags, and commit log.
@ -56,7 +56,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
├── event_bus.py ← Typed in-process event bus for skill subscriptions
├── evolution_checkpoints.py ← Append-only campaign/eval checkpoint ledger for evolution progress
├── improvement_backlog.py ← Durable advisory improvement backlog: recurrence-counted dedup (bump count/last_seen, never drop), priority+recurrence+recency ranking, close-on-commit (`close_backlog_items`), and size-triggered non-error-gated LLM grooming (`groom_backlog`); parser-safe locked writer; entries carry priority/kind (bug/improvement/capability_idea)
├── loop.py ← High-level LLM tool loop; one-shot no-op-attempt finalization nudge (declared expected_output + zero effects + no FINAL ANSWER); (v6.51.0) a one-shot ADVISORY red-verification finalization nudge (ordered before the receipt-absent nudge) when the latest host-attested verify receipt is unreconciled-RED (`outcomes.latest_unreconciled_failed_verification`) — re-check / explain / fix; (v6.52.2) a one-shot ADVISORY masked-verification nudge (ordered after the red nudge) when the latest PASSing verify check can launder its exit code (`outcomes.latest_unreconciled_masked_verification`) — re-ground without the masking pipe or explain; (v6.53.0) continuous explicit `FINAL ANSWER:` latching captures the latest typed candidate every round (tool-count-stamped, no prose mining) so review/nudge/forced-finalization paths do not erase a structured answer, and intrinsic no-deadline pacing asks for a salvageable current answer on long tasks; (v6.60.0) ALL marker prompting (P2 marker nudge, pacing salvage phrases, the context instruction) is gated on `task_contract.answer_protocol="final_answer_line"` via the `answer_protocol_active` SSOT — the latch/extractor stay unconditional; (v6.61.4) the protocol gate is SUFFICIENT for the P2 marker nudge — it no longer also requires a declared `expected_output` (a contract may carry the deliverable in `objective` while `expected_output` is empty; the latter therefore cannot suppress that salvage surface), and `extract_final_answer` structurally rejects the snake_case outcome-tier ledger identifiers (`best_effort`/`blocked_with_evidence`) as answers — internal enum vocabulary is never a deliverable (a reviewed run shipped `FINAL ANSWER: blocked_with_evidence` verbatim); `solved` stays extractable as an ordinary English word; (v6.90.0) a one-shot NANNY finalization nudge (ordered first): a child dispatched onto the delegated substrate (executor=harness) finalizing with ZERO `delegate_start` calls gets one structural reminder to delegate or state why not, and the forced-finalization paths carry the same fact as a NOTE inside their one final prompt instead of re-looping
├── loop.py ← High-level LLM tool loop; one-shot no-op-attempt finalization nudge (declared expected_output + zero effects + no FINAL ANSWER); (v6.51.0) a one-shot ADVISORY red-verification finalization nudge (ordered before the receipt-absent nudge) when the latest host-attested verify receipt is unreconciled-RED (`outcomes.latest_unreconciled_failed_verification`) — re-check / explain / fix; (v6.52.2) a one-shot ADVISORY masked-verification nudge (ordered after the red nudge) when the latest PASSing verify check can launder its exit code (`outcomes.latest_unreconciled_masked_verification`) — re-ground without the masking pipe or explain; (v6.53.0) continuous explicit `FINAL ANSWER:` latching captures the latest typed candidate every round (tool-count-stamped, no prose mining) so review/nudge/forced-finalization paths do not erase a structured answer, and intrinsic no-deadline pacing asks for a salvageable current answer on long tasks; (v6.60.0) ALL marker prompting (P2 marker nudge, pacing salvage phrases, the context instruction) is gated on `task_contract.answer_protocol="final_answer_line"` via the `answer_protocol_active` SSOT — the latch/extractor stay unconditional; (v6.61.4) the protocol gate is SUFFICIENT for the P2 marker nudge — it no longer also requires a declared `expected_output` (a contract may carry the deliverable in `objective` while `expected_output` is empty; the latter therefore cannot suppress that salvage surface), and `extract_final_answer` structurally rejects the snake_case outcome-tier ledger identifiers (`best_effort`/`blocked_with_evidence`) as answers — internal enum vocabulary is never a deliverable (a reviewed run shipped `FINAL ANSWER: blocked_with_evidence` verbatim); `solved` stays extractable as an ordinary English word; (v6.90.0) a one-shot NANNY finalization nudge (ordered first): a child dispatched onto the delegated substrate (executor=harness) finalizing with ZERO `delegate_start` calls gets one structural reminder to delegate or state why not, and the forced-finalization paths carry the same fact as a NOTE inside their one final prompt instead of re-looping; (2026-08-10 amendments) the nudge reads durable custody evidence from the CANONICAL (budget) root via `delegate_custody.custody_root` — the same root the writes land on, so split-root children are no longer blind — and branches PENDING ≠ FAILED: a started-but-unsettled run gets a "still pending — delegate_wait before finalizing" reminder (never a failure accusation, which would invite a duplicate concurrent run), `NANNY_DELEGATED_RUN_FAILED` is reserved for terminal non-success settles, and the nudge is suppressed entirely when the delegate verbs are policy-hidden from the child's toolset
├── loop_llm_call.py ← Single-round LLM call + usage accounting
├── task_pacing.py ← Task-pacing SSOT: deadline/cost milestones, finalization reserve, BudgetSnapshot, and acceptance-review launch/improvement rails. v6.64 reserves at least 200s for the first review and then `max(configured_floor, 1.5×EWMA)` from existing timing events (`alpha=0.5`); an explicit `max_improvement_passes` always binds, while Required+Blocking without one has no local count cap (deadline/global rails still apply). Legacy `until_deadline` and `stall_rounds_threshold` are accepted for one compatibility window with a deprecation event. v6.74.4 (figlet incident mitigation): workspace deliveries (`_workspace_delivery`, canonical `is_workspace_mode()` with an attribute fallback) get one shared commit-neutral tree sentence (`_TREE_FLUSH_SENTENCE` — commit-neutral because acting self_worktree subagents cannot commit and a moved HEAD fails patch capture closed) on the 10% deadline flush, the ~80% cost wrap-up, and a late FIRST cost milestone that would otherwise suppress the wrap-up; non-workspace texts stay byte-identical. Disclosed residual (mitigation, not closure): a forced tool-less exit crossed inside one long round with no pacing note or acceptance capsule in the terminal stretch can still ship an unverified last edit — the structural verification-freshness seam is an owner-pending follow-up.
├── vision_routing.py ← (v6.45) Send-time image routing SSOT: inline vision vs generic captions vs placeholders on a per-send message copy, controlled by `OUROBOROS_IMAGE_INPUT_MODE` and `OUROBOROS_MODEL_VISION`
@ -854,7 +854,7 @@ Finalization controls are typed owner-mailbox entries rather than injected owner
### Tool capability and execution
`tool_capabilities.py` is the SSOT for core, meta, parallel-safe, stateful-browser, untruncated, capped-result, and reviewed-mutative tool classes. `tool_policy.py` chooses the initial envelope; `ToolRegistry` remains the execution authority; `loop_tool_execution.py` owns timeouts, concurrency, live evidence, result handling, and mutative ceilings. A tool being registered or discoverable is not the same as being callable in the current task. Lazy capability discovery must return an explicit capability omission or `CAPABILITY_UNAVAILABLE` fact when the advertised surface cannot be enabled; it must not silently disappear. Review output and cognitive artifacts are not generic transport noise and remain outside ordinary result truncation.
`tool_capabilities.py` is the SSOT for core, meta, parallel-safe, stateful-browser, untruncated, capped-result, and reviewed-mutative tool classes. `tool_policy.py` chooses the initial envelope; `ToolRegistry` remains the execution authority; `loop_tool_execution.py` owns timeouts, concurrency, live evidence, result handling, and mutative ceilings. A tool being registered or discoverable is not the same as being callable in the current task. Lazy capability discovery must return an explicit capability omission or `CAPABILITY_UNAVAILABLE` fact when the advertised surface cannot be enabled; it must not silently disappear. `enable_tools`/discovery answer a REGISTERED tool filtered by policy with a typed "hidden by policy: <reason>" (`ToolRegistry.policy_hidden_reason` — same predicates and order as `get_schema_by_name`), never the same "Not found" as a nonexistent name (2026-08-10 saga); the contract-disabled check precedes the registration check, so a contract-disabled extension/MCP name also answers with its reason (deeper extension/MCP policy reasons — grants, network — remain a disclosed residual). The swarm-router promote turn's `promoted_task_toolset` (F6, `context._promoted_task_toolset`) is the LIVE registry projection — `available_tools()` evaluated for both target shapes (workspace-mode vs non-workspace) with credential gates live, plus typed `unavailable_builtin_tools` omissions; dynamic extension/MCP tools stay honestly unlisted. The workspace envelope (`_WORKSPACE_ALLOWED_TOOLS` in `tools/registry.py`) carries the delegate verbs, `switch_model`, and the `send_photo`/`send_video`/`send_file` family since 2026-08-10, and both delegated-child profiles are invariant-tested subsets of it (`tests/test_tool_capabilities.py`) so the workspace AND-intersection stays vacuous for children. Review output and cognitive artifacts are not generic transport noise and remain outside ordinary result truncation.
Outcome classification keeps policy refusal separate from execution failure. In particular, `user_files_path_blocked`, `cwd_blocked`, and `artifact_output_undeclared` are typed non-failure/policy-denial surfaces; a declared output that cannot be registered remains the genuine `artifact_output_error`. This prevents an expected authority boundary from falsely becoming the task's headline failure while preserving real artifact loss.
@ -1133,8 +1133,10 @@ The owned daemon remains session-scoped and stop remains own-only-if-self-starte
an attached or foreign process is never killed. This lifecycle changes only daemon
delivery; delegated-run custody below still follows the durable run receipts.
**Three nanny verbs** (`tools/delegate.py`), registered in BOTH child allowlists:
`delegate_start`, `delegate_wait`, `delegate_cancel`. There is deliberately no `hurry`:
**Three nanny verbs** (`tools/delegate.py`), registered in BOTH child allowlists AND in
the workspace envelope (`_WORKSPACE_ALLOWED_TOOLS`, 2026-08-10 — before that a task in an
external project workspace could not see the verbs, so a "nanny" instructed to delegate
physically could not): `delegate_start`, `delegate_wait`, `delegate_cancel`. There is deliberately no `hurry`:
Claudexor's only control verb is `cancel`, and cancelling a reviewer destroys the very
verdict the hurry wanted. `delegate_wait` is time-bounded and progress-aware — it
returns early on a terminal state or a containment fault, and otherwise HOLDS its
@ -1180,11 +1182,36 @@ and asks the daemon nothing at all when no route is configured or the request is
rather than falling through to the worker, because a fallback would bill the owner for
precisely the spend the pin was chosen to avoid. The `auto` rows carry a **visible
marker** into the child's own context (`agent.dispatch_executor_note`): a nanny is told
to delegate, and a child that fell back to metered tokens is told its route was
to decide its delegation plan FIRST — right after its objective/constraints — with typed
cost classes (a subscription-lane run has known-zero marginal cost when the route reports
its settled spend as $0; the child's own tokens are metered API money; never "free"
unqualified), and a child that fell back to metered tokens is told its route was
unavailable instead of discovering it by spending. `subagents.route_health` is the ONE
manifest reader, shared by the dispatcher and by `delegate_start`, so the two cannot
disagree about the same route.
A second, later check closes the class the dispatch table cannot see (Q1A, 2026-08-10
amendments): the route can be healthy while the child's MATERIALIZED toolset hides the
delegate verbs (workspace filters, contract `disabled_tools`, policy drift).
`agent.preflight_delegate_visibility` runs once at toolset materialization — after the
real `ToolContext` is set, before the first paid LLM round — and when a
harness-dispatched child cannot see ALL THREE delegate verbs (`delegate_start`,
`delegate_wait`, `delegate_cancel` — a child that can start but not wait is still
broken): an AUTO-resolved dispatch falls back LOUDLY to native (typed
`delegate_tools_invisible` capability delta, the same durable/live surfaces
re-recorded — events row, RUNNING record, supervisor mirror); an EXPLICITLY pinned
harness ends unrun through `agent.executor_blocked_outcome` with the distinct
`delegate_tools_invisible` reason code — the fix is tool policy, not waiting for the
route. A broken toolset introspection follows the same split: pinned fails CLOSED
(the probe cannot prove the pinned contract is executable), auto proceeds fail-open
with a `delegate_visibility_unverified` note on the capability delta. At the
completion seam the durable result then separates the PLAN from the FACT:
`actual_substrate` (`harness_used`/`harness_attempted`/`native_only`, from the custody
evidence alone) rides the envelope beside `effective_executor`, a harness dispatch that ended
`native_only` amends its `capability_delta` (`delegated_substrate_unused`), and the
`wait_tasks` batch projection carries the compact `execution_evidence` block — see the
§11.1 `ChatOutbound.execution_evidence` row.
**Read-only and mutating children share one nanny and one transport.** The only
difference is the run shape, and the shape has ONE owner —
`subagents.delegated_run_shape`, which answers a single question: is this an acting
@ -1980,7 +2007,7 @@ via `tests/test_contracts.py`.
| `api_v1` browser envelopes — inbound chat/command, outbound chat/media/log/extension/task/annotation frames, and HTTP health/state/task/evolution/settings shapes. `TaskCreateRequest` keeps optional project/workspace/memory/attachment, acceptance-claim, answer-protocol, resource-policy, disabled-tool, executor, teardown, deadline, and context metadata; `ExecutorRef` is host-owned. Nullable cost fields preserve unavailable versus `$0`. `gateway/contracts.py` is the active owner and `web/modules/api_types.js` its browser mirror; parity/AST tests pin emitted keys and task admission. | `ouroboros/gateway/contracts.py`, `web/modules/api_types.js` | `tests/test_contracts.py`, `tests/test_gateway_parity.py` |
| `ChatOutbound.cancelable` + `TaskCancelResponse.cascade` (v6.82.0) — additive-optional cancellation ABI: the host-attested `cancelable: true` progress-meta marker that gates the chat card's "Cancel run" action (a card's shape alone cannot distinguish a pooled root from an in-process direct-chat turn), plus the cancel endpoint's echoed `cascade` flag. Existing envelope semantics are unchanged; every field is optional. | `ouroboros/gateway/contracts.py`, `web/modules/api_types.js` | `tests/test_gateway_parity.py` pins both fields in both the Python and JavaScript mirrors; `tests/test_task_cancel_endpoint_v682.py` pins the response shapes; `tests/test_gateway_history.py` pins the marker's replay passthrough. |
| `ChatOutbound.executor_route` (phase 6) — OPAQUE harness id of the DISPATCH decision (where the subagent was routed — NOT a receipt that a harness executed; the receipt is `execution_evidence` below), stamped onto the live task metadata by `agent._record_executor_facts` from the ONE dispatch resolution (`subagents.resolve_subagent_dispatch`, whose executor axis is `subagents.dispatch_executor_resolution`) and projected by the canonical frame assembler `_subagent_progress_meta`; carried through history replay by the progress-meta allowlist. Empty/absent = the ordinary native path, and the UI draws NO chip (never a placeholder, never `api` noise on every bubble). The renderer is `log_events.executorChip` → a small icon+harness chip on the bubble and the subagent row (owner directive: a chip like Claudexor's, never a promotional badge), sticky per card so a later quiet frame cannot erase it. | `ouroboros/agent.py`, `ouroboros/gateway/history.py`, `ouroboros/gateway/contracts.py`, `web/modules/log_events.js`, `web/modules/chat.js` | `tests/test_claudexor_owned_daemon.py` pins the assembler + allowlist + both contract mirrors; `web/tests/review_truth.test.js` pins the chip renderer and the no-fact-no-chip rule. |
| `ChatOutbound.execution_evidence` (v6.89.x, additive-optional) — the completion-seam RECEIPT beside the dispatch decision: `{delegated_runs_started, delegated_runs_settled, subscription_cost_usd (None while undisclosed — never 0), subscription_cost_estimated, harness_models}` derived once from the durable delegate custody rows (`delegate_custody.task_execution_evidence`) in `subagents.envelope_from_task` at terminal statuses only, never overwriting `effective_executor`/`executor_route`; carried on the terminal subagent frame and through history replay by the progress-meta allowlist. `log_events.executorChip` renders LAYERED truth: before evidence — neutral "Dispatched to …"; with settled runs — the receipt with the subscription sum (`~` when estimated); with a route but no durable run record — "no durable record of a delegated run", never asserting native execution as fact. | `ouroboros/delegate_custody.py`, `ouroboros/subagents.py`, `supervisor/events.py`, `ouroboros/gateway/history.py`, `ouroboros/gateway/contracts.py`, `web/modules/log_events.js` | `tests/test_execution_evidence.py` pins aggregation (incl. undisclosed and estimated spend) and both reconciliation directions; `web/tests/review_truth.test.js` pins the layered chip. |
| `ChatOutbound.execution_evidence` (v6.89.x, additive-optional) — the completion-seam RECEIPT beside the dispatch decision: `{delegated_runs_started, delegated_runs_settled, delegated_runs_succeeded, delegated_run_failure_states (terminal-state axis, F4 2026-08-10: "tried and the run died" is distinguishable from "never tried"), evidence_read_failed (the custody log exists but could not be read — zero counts are then UNKNOWN, never a "no run" receipt), subscription_cost_usd (None while undisclosed — never 0), subscription_cost_estimated, harness_models}` derived once from the durable delegate custody rows (`delegate_custody.task_execution_evidence`) in `subagents.envelope_from_task` at terminal statuses only, never overwriting `effective_executor`/`executor_route`; carried on the terminal subagent frame and through history replay by the progress-meta allowlist. Beside it rides the additive-optional `actual_substrate` FACT — `harness_used` (>=1 delegated run succeeded) / `harness_attempted` (>=1 started, none succeeded) / `native_only` (none started) — derived from the custody evidence ONLY (never usage/rounds, where polling and thinking are indistinguishable) and always shipped beside the raw attested counts (in the envelope's `execution_evidence`, as top-level durable-result fields via `subagents.substrate_result_fields`, and in the `wait_tasks` batch projection's compact `execution_evidence` `{dispatch_executor, actual_substrate, delegated_runs_started, delegated_runs_succeeded}` — reduced to exactly `{dispatch_executor, evidence_read_failed}` when the custody log was unreadable — for harness-dispatched children). When the custody log was UNREADABLE (`evidence_read_failed`), the substrate claim is OMITTED everywhere — the zero counts are unknown, never classified as `native_only`, and no `capability_delta` amendment is made — and the `wait_tasks` projection omits the counts too, emitting only the typed marker (an unread log yields no numeric facts); the batch projection likewise omits the counts entirely when a stored result carries no envelope evidence at all (pre-6.94 records: absence means "no evidence yet", not a zero-run receipt). A harness-dispatched task that VERIFIABLY ended `native_only` additionally amends its `capability_delta` disclosure (`delegated_substrate_unused`). `log_events.executorChip` renders LAYERED truth: before evidence — neutral "Dispatched to …"; with settled runs — the receipt with the subscription sum (`~` when estimated); with a route but no durable run record — "no durable record of a delegated run", never asserting native execution as fact; with `evidence_read_failed` — "evidence unavailable", never "no run recorded". | `ouroboros/delegate_custody.py`, `ouroboros/subagents.py`, `supervisor/events.py`, `ouroboros/gateway/history.py`, `ouroboros/gateway/contracts.py`, `web/modules/log_events.js` | `tests/test_execution_evidence.py` pins aggregation (incl. undisclosed and estimated spend), substrate classification and both reconciliation directions; `web/tests/review_truth.test.js` pins the layered chip incl. the unreadable-evidence state; `tests/test_task_status_flow.py` pins the batch projection. |
| `TaskDetailResponse` + optional root-only `TaskCostBreakdown` — an open stored-result envelope plus a read-time, never-persisted physical-ledger projection. When available it contains every frozen field: `own_usd`, `children_usd`, `unattributed_usd`, `delegated_disclosed_usd`, `subscription_sessions`, `unknown_unmetered`, `non_final_rows`, `cost_final`, and `authority="physical_attempt_ledger"`; delegated is a filter, not a third sum. Non-root or unavailable/unattributable accounting omits the whole object rather than reporting `$0`. | `ouroboros/gateway/contracts.py`, `ouroboros/gateway/tasks.py`, `web/modules/api_types.js` | `tests/test_gateway_parity.py` pins type parity, exact keys, root-only emission, and full optionality. |
| Managed update gateway ABI — the empty preflight request, exact channel-bound `UpdateMergePlan`, pinned apply request (`strategy`, base/target SHAs, recovery confirmation), typed success/error variants, and `update_status_ready` WS notice that refreshes the boot-time cache in the UI. | `ouroboros/gateway/contracts.py`, `web/modules/api_types.js` | `tests/test_gateway_parity.py` pins every field and message type in both mirrors; `tests/test_update_apply_routing.py` drives pin, strategy, recovery-confirmation, and response routing. |
| `ChatOutbound.review_projection` (v6.65.0) — optional compact panel/actor truth for Chat and Logs: transport status, parse status, semantic verdict, task-acceptance `outcome_tier`, model/provider/role, coverage, quorum/enforcement impact, the complete redacted reason, a forensic `response_ref` (flat content hashes, no host paths — v6.70.0), and exact candidate/evidence/fence binding hashes; v6.74.0 adds additive optional keys — per-actor `dialogue_status`, per-panel `dialogue` ({status, votes}) and the `single_reviewer_no_diversity` label; raw reviewer output remains in private audit storage. | `ouroboros/gateway/contracts.py`, `ouroboros/review_substrate.py` | `tests/test_contracts.py` pins the field as optional frozen ABI; `tests/test_gateway_parity.py` pins the field in both Python and JavaScript contracts; `tests/test_review_substrate_v2.py` pins the bounded actor projection including `outcome_tier`; `web/tests/review_truth.test.js` pins the shared renderer. |

View file

@ -720,7 +720,7 @@ Before every commit, verify the following:
- [ ] No gratuitous abstract layers (Bible P7)
#### Structural Rules
- [ ] New Tool? `get_tools()` exports it using the `ToolEntry` pattern from `registry.py`, an explicit entry is added to `ouroboros/safety.py::TOOL_POLICY` (`POLICY_SKIP` for trusted built-ins, `POLICY_CHECK` for opaque or outward-facing ones), AND the intended visibility is declared in `ouroboros/tool_capabilities.py` (`CORE_TOOL_NAMES`, local-readonly/acting subagent allowlists, parallel/truncation sets as appropriate). If workspace tasks should see the tool, update the workspace allowlist in `tools/registry.py` too. Without the policy entry the tool falls through to `DEFAULT_POLICY = POLICY_CHECK` and pays a light-model LLM call per invocation, and without the capability/allowlist wiring a packaged/visible tool can still be unreachable to subagents or workspace tasks. **A tool that WRITES the repo working tree needs the GUARD surfaces too, not only the visibility ones:** add it to `_ROOT_ARG_REPO_WRITE_TOOLS` (the single set behind the acting-no-workspace fence, the protected-write gate and the acting root-enum narrowing) and make sure its target paths are canonicalized — via `_PATH_NORMALIZED_TOOLS` if it takes a top-level `path`, or via `canonical_repo_relative_path` + `_payload_write_paths` if its paths ride inside the payload. Visibility lists are all green while these are missing, so the gap does not surface as a failing test: `apply_patch`/`edit_batch` shipped a protected-path bypass that way (a guard reading `repo/BIBLE.md` while the write landed on `BIBLE.md`). Tests must exercise the REAL guard chain — a test that monkeypatches the resolver proves the mechanics, not the fence.
- [ ] New Tool? `get_tools()` exports it using the `ToolEntry` pattern from `registry.py`, an explicit entry is added to `ouroboros/safety.py::TOOL_POLICY` (`POLICY_SKIP` for trusted built-ins, `POLICY_CHECK` for opaque or outward-facing ones), AND the intended visibility is declared in `ouroboros/tool_capabilities.py` (`CORE_TOOL_NAMES`, local-readonly/acting subagent allowlists, parallel/truncation sets as appropriate). If workspace tasks should see the tool, update the workspace allowlist in `tools/registry.py` too; a tool added to a child profile MUST also be in that allowlist — both child profiles are invariant-tested subsets of it (`tests/test_tool_capabilities.py`, 2026-08-10 saga: profile-visible but workspace-hidden delegate verbs made delegation physically impossible exactly where children run). Without the policy entry the tool falls through to `DEFAULT_POLICY = POLICY_CHECK` and pays a light-model LLM call per invocation, and without the capability/allowlist wiring a packaged/visible tool can still be unreachable to subagents or workspace tasks. **A tool that WRITES the repo working tree needs the GUARD surfaces too, not only the visibility ones:** add it to `_ROOT_ARG_REPO_WRITE_TOOLS` (the single set behind the acting-no-workspace fence, the protected-write gate and the acting root-enum narrowing) and make sure its target paths are canonicalized — via `_PATH_NORMALIZED_TOOLS` if it takes a top-level `path`, or via `canonical_repo_relative_path` + `_payload_write_paths` if its paths ride inside the payload. Visibility lists are all green while these are missing, so the gap does not surface as a failing test: `apply_patch`/`edit_batch` shipped a protected-path bypass that way (a guard reading `repo/BIBLE.md` while the write landed on `BIBLE.md`). Tests must exercise the REAL guard chain — a test that monkeypatches the resolver proves the mechanics, not the fence.
- [ ] New Gateway (if extracted)? Contains no business logic, only transport.
- [ ] New memory/data files? Should they appear in LLM context (`context.py`)?

View file

@ -47,6 +47,7 @@ from ouroboros.contracts.task_constraint import normalize_task_constraint
from ouroboros.contracts.task_contract import attach_task_contract
from ouroboros.outcomes import infra_failed_axes
from ouroboros.subagents import (
CapabilityDelta,
SubagentExecutorResolution,
SUBAGENT_RESOLUTION_FIELDS,
SubagentDispatch,
@ -73,8 +74,13 @@ def dispatch_executor_note(decision: Optional[SubagentExecutorResolution]) -> st
route = decision.route.route_id if decision.route else ""
note = (
f"EXECUTOR: your parent scheduled you on the delegated substrate ({route}). "
"You are a NANNY: do your work with delegate_start / delegate_wait instead of "
"thinking on metered API tokens, and check what comes back rather than "
"You are a NANNY. Decide your delegation plan FIRST — right after reading "
"your objective and constraints, before any substantive work. Cost classes: "
"a subscription-lane run has known-zero marginal cost when the route reports "
"its settled spend as $0 (an estimated or undisclosed spend is estimated/unknown, "
"not zero); every token YOU think on is metered API money. "
"While the lane is healthy, delegate everything you can — even small tasks — "
"with delegate_start / delegate_wait, and verify what comes back rather than "
"believing it."
)
if decision.reset_at:
@ -113,6 +119,33 @@ def executor_blocked_outcome(decision: SubagentExecutorResolution) -> Tuple[str,
Deliberately NOT a fallback: the task ends unrun and typed, having spent nothing.
"""
if decision.reason in ("delegate_tools_invisible", "delegate_visibility_unverified"):
# Q1A preflight (2026-08-10 amendments): the route is healthy but the
# child's MATERIALIZED toolset does not carry the delegate verbs — or
# the toolset introspection itself failed, so visibility is UNKNOWN,
# not disproven (distinct reason: the terminal states exactly what is
# known). Either way the pin cannot be honored, and the fix is tool
# policy/contract, not waiting for the route to recover.
detail = (
"the delegate tools (delegate_start/delegate_wait/delegate_cancel) "
"are not visible in its materialized toolset"
if decision.reason == "delegate_tools_invisible"
else "the toolset introspection failed, so the delegate tools' "
"(delegate_start/delegate_wait/delegate_cancel) visibility could "
"not be verified"
)
text = (
"⚠️ EXECUTOR_UNAVAILABLE: this subagent was pinned to the delegated "
f"substrate (executor='harness'), but {detail}, so the pin cannot be "
"honored. The task was NOT run on metered API tokens. Fix the tool "
"policy / task contract that hides the delegate verbs, or schedule "
"again with executor='auto' to accept metered spend."
)
# Literal codes (not `decision.reason`) so the provenance drift guard
# keeps seeing every code the runtime can emit.
if decision.reason == "delegate_visibility_unverified":
return text, {"execution_status": "infra_failed", "reason_code": "delegate_visibility_unverified"}
return text, {"execution_status": "infra_failed", "reason_code": "delegate_tools_invisible"}
text = (
"⚠️ EXECUTOR_UNAVAILABLE: this subagent was pinned to the delegated substrate "
f"(executor='harness') and the route cannot run: {decision.reason}."
@ -313,6 +346,87 @@ def resolve_dispatch_axes(task: Dict[str, Any]) -> Optional[SubagentDispatch]:
return dispatch
# The dispatched harness contract needs the FULL verb set: a child that can
# start a run but not wait on or cancel it is still broken.
_DELEGATE_VERBS = ("delegate_start", "delegate_wait", "delegate_cancel")
def preflight_delegate_visibility(
tools: Any, task: Dict[str, Any], dispatch: Optional[SubagentDispatch],
) -> Tuple[Optional[SubagentDispatch], bool]:
"""Verify a harness dispatch can actually SEE its delegate verbs — after the
real toolset is materialized, BEFORE the first paid LLM round.
The dispatch resolution proves the ROUTE is healthy; it does not prove the
child's toolset carries the delegate verbs (workspace filters, contract
disabled_tools or future policy drift can hide them the Phase A allowlist
widening closed one instance of the class, not the class). The e9108a09c6574184
audit: nine children dispatched as nannies with the verbs invisible made zero
delegated runs and burned ~$29-54 of metered API while telemetry said harness.
One check at toolset materialization (owner decision Q1A): an AUTO-resolved
executor falls back LOUDLY to native the amended ``capability_delta``
(reason ``delegate_tools_invisible``, ``reduced=True``) and the corrected
dispatch fields are re-stamped onto the task record so telemetry does not
lie; an EXPLICIT ``harness`` pin becomes the typed blocked outcome that
terminalizes with zero spend (``executor_blocked_outcome``). A broken
introspection follows the same split: a pinned harness fails CLOSED (a probe
that cannot prove visibility cannot prove the pinned contract is executable),
an auto one proceeds fail-open with the probe failure disclosed as a
``capability_delta`` note. Returns the (possibly amended) dispatch and
whether it amended.
"""
if (
dispatch is None
or dispatch.executor_resolution is None
or dispatch.executor_resolution.executor != "harness"
):
return dispatch, False
import dataclasses
def _stamp(amended: SubagentDispatch) -> Tuple[SubagentDispatch, bool]:
# The same two writes resolve_dispatch_axes made: the record fields and
# the envelope rebuilt from them, so every downstream surface describes
# the amended resolution instead of the one the preflight just falsified.
task.update(amended.record_fields())
task["subagent_envelope"] = envelope_from_task(task, status=STATUS_RUNNING)
return amended, True
def _append_reason(delta: CapabilityDelta, note: str, **changes: Any) -> CapabilityDelta:
reasons = [part for part in (delta.reason, note) if part]
return dataclasses.replace(delta, reason="; ".join(reasons), **changes)
pinned = str(task.get("requested_executor") or "auto").strip().lower() == "harness"
reason = "delegate_tools_invisible"
try:
available = set(tools.available_tools())
if all(verb in available for verb in _DELEGATE_VERBS):
return dispatch, False
except Exception:
log.warning("delegate visibility preflight: introspection failed", exc_info=True)
if not pinned:
# Fail-open for auto, but never silently: the note rides the delta.
return _stamp(dataclasses.replace(
dispatch,
delta=_append_reason(dispatch.delta, "delegate_visibility_unverified")))
# Pinned + broken probe blocks with the honest reason: visibility is
# UNKNOWN, not disproven.
reason = "delegate_visibility_unverified"
executor = "blocked" if pinned else "native"
return _stamp(dataclasses.replace(
dispatch,
executor=executor,
route="",
delta=_append_reason(dispatch.delta, reason,
effective_executor=executor, reduced=True),
executor_resolution=dataclasses.replace(
dispatch.executor_resolution,
executor=executor, reason=reason, reset_at="",
),
))
def emit_dispatch_resolution(
event_queue: Any, task: Dict[str, Any], dispatch: Optional[SubagentDispatch],
) -> None:
@ -631,6 +745,50 @@ class OuroborosAgent:
except Exception:
log.debug("Failed to persist running task status", exc_info=True)
def _run_delegate_preflight(
self, drive_logs: Any, task: Dict[str, Any], dispatch: Optional[SubagentDispatch],
) -> Optional[SubagentDispatch]:
"""Q1A capability preflight (2026-08-10 amendments): the REAL toolset now
exists verify a harness dispatch can actually see its delegate verbs
before any paid LLM round. An amendment re-records the same durable and
live surfaces the original resolution wrote (events row, RUNNING record,
supervisor mirror), so all of them keep telling one story; a blocked pin
flows into the existing cap_info blocked terminal and spends nothing."""
dispatch, amended = preflight_delegate_visibility(self.tools, task, dispatch)
if amended:
_record_executor_resolution(drive_logs, task, dispatch)
self._persist_running_record(task)
emit_dispatch_resolution(self._event_queue, task, dispatch)
return dispatch
def _capture_mutation_baseline(self, task: Dict[str, Any], task_metadata: Dict[str, Any]) -> None:
"""Mutation-attribution baseline: snapshot the system repo's clean/dirty
state once, when a queued ROOT task starts. Evidence only a capture
failure never blocks the task; commit staging then simply has no
attributed candidate set to consume."""
if (
str(task.get("id") or "").strip()
and not bool(task.get("_is_direct_chat"))
and not bool(task.get("_ephemeral_turn"))
and str(task_metadata.get("delegation_role") or "").lower() != "subagent"
):
try:
from ouroboros.mutation_attribution import capture_mutation_baseline
capture_mutation_baseline(
pathlib.Path(
str(task.get("budget_drive_root") or "")
or self.env.budget_drive_root
or self.env.drive_root
),
str(task.get("id") or ""),
[{"surface_type": "system_repo", "host_root": str(self.env.repo_dir)}],
owner_kind="task_root",
owner_id=str(task.get("root_task_id") or task.get("id") or ""),
)
except Exception:
log.warning("mutation baseline capture failed for %s", task.get("id"), exc_info=True)
def _prepare_task_context(self, task: Dict[str, Any]) -> Tuple[ToolContext, List[Dict[str, Any]], Dict[str, Any]]:
"""Set up ToolContext, build messages, return (ctx, messages, cap_info)."""
drive_logs = self.env.drive_path("logs")
@ -812,32 +970,8 @@ class OuroborosAgent:
# per-model concurrency semaphore (ouroboros/model_concurrency.py), not by routing.
self.tools.set_context(ctx)
# Mutation-attribution baseline: snapshot the system repo's clean/dirty
# state once, when a queued ROOT task starts. Evidence only — a capture
# failure never blocks the task; commit staging then simply has no
# attributed candidate set to consume.
if (
str(task.get("id") or "").strip()
and not bool(task.get("_is_direct_chat"))
and not bool(task.get("_ephemeral_turn"))
and str(task_metadata.get("delegation_role") or "").lower() != "subagent"
):
try:
from ouroboros.mutation_attribution import capture_mutation_baseline
capture_mutation_baseline(
pathlib.Path(
str(task.get("budget_drive_root") or "")
or self.env.budget_drive_root
or self.env.drive_root
),
str(task.get("id") or ""),
[{"surface_type": "system_repo", "host_root": str(self.env.repo_dir)}],
owner_kind="task_root",
owner_id=str(task.get("root_task_id") or task.get("id") or ""),
)
except Exception:
log.warning("mutation baseline capture failed for %s", task.get("id"), exc_info=True)
dispatch = self._run_delegate_preflight(drive_logs, task, dispatch)
self._capture_mutation_baseline(task, task_metadata)
self._emit_typing_start()

View file

@ -32,7 +32,7 @@ from ouroboros.outcomes import (
normalize_outcome_axes,
)
from ouroboros.contracts.task_contract import build_task_contract
from ouroboros.subagents import envelope_from_task
from ouroboros.subagents import envelope_from_task, substrate_result_fields
from ouroboros.utils import utc_now_iso, append_jsonl, truncate_review_artifact as _truncate_with_notice
from ouroboros.post_task_checkpoint import (
POST_TASK_SYNTHESIS_INFLIGHT as _POST_TASK_SYNTHESIS_INFLIGHT,
@ -1070,8 +1070,7 @@ def _store_task_result(env: Any, task: Dict[str, Any], text: str,
swarm_efficiency = _build_swarm_efficiency(env, task)
subagent_envelope = task.get("subagent_envelope") if isinstance(task.get("subagent_envelope"), dict) else {}
if str(task.get("delegation_role") or "").lower() == "subagent":
subagent_envelope = envelope_from_task(
task, status=status, usage=usage, cost_usd=cost_fields.get("cost_usd"))
subagent_envelope = envelope_from_task(task, status=status, usage=usage, cost_usd=cost_fields.get("cost_usd"))
if cost_fields.get("cost_accounting_status") != "available":
subagent_envelope.update({
"cost_usd": None,
@ -1146,7 +1145,8 @@ def _store_task_result(env: Any, task: Dict[str, Any], text: str,
effective_executor=task.get("effective_executor"),
executor_route=task.get("executor_route"),
tool_profile=task.get("tool_profile"),
capability_delta=task.get("capability_delta"),
capability_delta=subagent_envelope.get("capability_delta") or task.get("capability_delta"), # Q1A: envelope copy carries the native_only amendment
**substrate_result_fields(subagent_envelope), # Q1A: substrate FACT + raw counts
reasoning_effort=task.get("reasoning_effort"),
task_group_id=task.get("task_group_id"),
task_group=task.get("task_group"),

View file

@ -410,6 +410,64 @@ def _runtime_budget_info(env: Any, task: Dict[str, Any]) -> Dict[str, Any]:
return budget_info
def _promoted_task_toolset(env: Any) -> Dict[str, Any]:
"""F6 (2026-08-10 amendments): the LIVE built-in toolset a promoted task will see.
The first cut projected the static ``_WORKSPACE_ALLOWED_TOOLS`` /
``CORE|META`` union it advertised credential-gated built-ins that real
availability removes (web_search without a backend) and omitted every valid
registered non-workspace built-in (get_github_issue, ...), so the router
could still author impossible or over-restricted contracts. This projection
asks the REGISTRY itself, through the same ``available_tools()`` resolution
a running task gets, once per intended target shape (workspace-mode task vs
non-workspace). Probe contexts carry a task_id so the credential-gate
predicates evaluate LIVE instead of taking the bare-registry structural
carve-out. Bounded structural list; no contract text is scanned (P5).
"""
from types import SimpleNamespace
from ouroboros.tools.registry import ToolRegistry, _builtin_tool_availability
registry = ToolRegistry(pathlib.Path(env.repo_dir), pathlib.Path(getattr(env, "drive_root", ".")))
def _probe(workspace: bool) -> Any:
return SimpleNamespace(
task_id="promote_toolset_probe", task_metadata={}, task_contract={},
task_constraint=None, is_workspace_mode=lambda: workspace,
is_ephemeral_turn=False,
)
registry.set_context(_probe(workspace=True))
workspace_tools = set(registry.available_tools())
probe = _probe(workspace=False)
registry.set_context(probe)
non_workspace_tools = set(registry.available_tools())
# Typed omissions: registered built-ins that live availability removes right
# now (credential gates). Named with their reason so the router can tell
# "does not exist" from "exists but currently unavailable".
unavailable = {}
for name in registry._entries:
available, reason, detail = _builtin_tool_availability(name, probe)
if not available:
unavailable[name] = f"{reason}: {detail}" if detail else reason
return {
"workspace_task_tools": sorted(workspace_tools),
"non_workspace_extra_tools": sorted(non_workspace_tools - workspace_tools),
**({"unavailable_builtin_tools": dict(sorted(unavailable.items()))} if unavailable else {}),
"rule": (
"LIVE built-in tool availability, evaluated by the real tool "
"registry at promote time. A task running in a project workspace "
"sees workspace_task_tools ONLY; other tasks additionally see "
"non_workspace_extra_tools. unavailable_builtin_tools exist but are "
"currently unusable (e.g. missing credentials) — do not demand "
"them. Dynamic extension/MCP tools are NOT listed (their "
"availability is unknowable at promote time). If an objective/"
"expected_output demands specific BUILT-IN tools, demand only names "
"listed here."
),
}
def build_runtime_section(env: Any, task: Dict[str, Any], *, ctx: Any = None) -> str:
try:
git_branch, git_sha = get_git_info(env.repo_dir)
@ -580,6 +638,17 @@ def build_runtime_section(env: Any, task: Dict[str, Any], *, ctx: Any = None) ->
"your judgment picks the target (or none -> answer inline / promote_chat_to_task). A "
"message in a project room defaults to that project unless it clearly says otherwise."
)
if _swarm_router:
# F6 (2026-08-10 saga): the router turn authors objectives/contracts for a
# task it will never run, and once wrote a HARD requirement on a tool the
# promoted task could not see (send_photo behind the workspace envelope).
# Give the turn the bounded LIVE tool-name envelope as a structural fact;
# the model still writes the contract itself (P5) — no contract text is
# ever scanned or gated.
try:
runtime_data["promoted_task_toolset"] = _promoted_task_toolset(env)
except Exception:
log.debug("Failed to build promoted-task toolset digest", exc_info=True)
if bool(task.get("_ephemeral_turn")) and not _swarm_router:
runtime_data["decision_turn_rule"] = _DECISION_TURN_OUTCOME_RULE
_main_manifest = (

View file

@ -347,9 +347,24 @@ def task_execution_evidence(drive_root: Any, task_id: str) -> Dict[str, Any]:
tid = str(task_id or "")
started: set = set()
settled: set = set()
succeeded: set = set()
failure_states: List[str] = []
models: List[str] = []
cost_total, cost_known, cost_estimated = 0.0, True, False
for row in _iter_rows(event_log_path(drive_root)):
# Scope finding (a5e59bdf gate): an UNREADABLE log must not collapse into
# the same zero-count result as a proven empty one — a reader would then
# accuse a nanny of "zero attempts" on evidence it never saw. A missing
# file IS a positively-established empty state (no row could exist);
# existing-but-unreadable is not, and _iter_rows swallows its own OSError.
evidence_read_failed = False
_log_path = event_log_path(drive_root)
try:
if _log_path.exists():
with _log_path.open("rb"):
pass
except OSError:
evidence_read_failed = True
for row in _iter_rows(_log_path):
if str(row.get("task_id") or "") != tid:
continue
run_id = str(row.get("run_id") or "")
@ -358,8 +373,21 @@ def task_execution_evidence(drive_root: Any, task_id: str) -> Dict[str, Any]:
kind = str(row.get("type") or "")
if kind == STARTED:
started.add(run_id)
elif kind == CLOSED_ABSENT and run_id not in settled:
# Closed-without-settlement is still TERMINAL: leaving it in the
# started-minus-settled gap would read as "still executing" to the
# pending/settled readers (nanny reminder) forever. No ledger row was
# written, so its spend is undisclosed, never zero.
settled.add(run_id)
failure_states.append("closed_absent")
cost_known = False
elif kind == SETTLED and run_id not in settled:
settled.add(run_id)
state = str(row.get("state") or "")
if state in SUCCEEDED_STATES:
succeeded.add(run_id)
elif state:
failure_states.append(state)
if row.get("spend_disclosed") and row.get("cost_usd") is not None:
try:
cost_total += float(row.get("cost_usd") or 0.0)
@ -380,6 +408,15 @@ def task_execution_evidence(drive_root: Any, task_id: str) -> Dict[str, Any]:
# A settled row whose started row fell out of the log is still a run that ran.
"delegated_runs_started": len(started | settled),
"delegated_runs_settled": len(settled),
# The terminal-state axis (F4, 2026-08-10 saga): a run that STARTED and
# FAILED is an ATTEMPTED route, not a refusal to delegate. Readers (the
# nanny nudge, the completion seam) must be able to tell "never tried"
# from "tried and the run died" without re-parsing the event log.
"delegated_runs_succeeded": len(succeeded),
"delegated_run_failure_states": sorted(set(failure_states)),
# True only when the canonical log EXISTS but could not be opened —
# zero counts are then "unknown", not "established" (additive key).
"evidence_read_failed": evidence_read_failed,
"subscription_cost_usd": round(cost_total, 6) if (settled and cost_known) else None,
# The settlement row's own estimated/final distinction, carried instead of
# dropped: an estimated sum must never render as an exact receipt.

View file

@ -112,10 +112,15 @@ class ChatOutbound(TypedDict):
# `capability_delta`, not by rewriting this field.
executor_route: NotRequired[str]
# The completion-seam EVIDENCE the route decision is reconciled against
# (subagents.envelope_from_task): delegated runs started/settled, disclosed
# subscription spend, engine-reported models. Terminal frames only; its
# absence means "no evidence yet", never "ran natively".
# (subagents.envelope_from_task): delegated runs started/settled/succeeded,
# terminal failure states, disclosed subscription spend (+estimated flag),
# engine-reported models. Terminal frames only; its absence means "no
# evidence yet", never "ran natively".
execution_evidence: NotRequired[Dict[str, Any]]
# The FACT beside the executor_route plan, from the same custody evidence:
# "harness_used" | "harness_attempted" | "native_only". Terminal frames only; absent =
# no substrate claim.
actual_substrate: NotRequired[str]
model: NotRequired[str]
task_group_id: NotRequired[str]
task_event: NotRequired[str]

View file

@ -95,6 +95,9 @@ _PROGRESS_META_FIELDS = (
# The completion-seam evidence block (delegated runs started/settled,
# subscription spend, harness models) — the chip's layered truth on replay.
"execution_evidence",
# The substrate FACT derived from that evidence
# (harness_used/harness_attempted/native_only).
"actual_substrate",
"task_group_id",
)

View file

@ -3187,7 +3187,7 @@ def _setup_dynamic_tools(tools_registry, tool_schemas, messages):
def _handle_enable_tools(ctx=None, tools: str = "", **kwargs):
names = [n.strip() for n in tools.split(",") if n.strip()]
enabled, not_found = [], []
enabled, hidden, not_found = [], [], []
for name in names:
schema = tools_registry.get_schema_by_name(name)
if schema and name not in active_tool_names:
@ -3198,13 +3198,27 @@ def _setup_dynamic_tools(tools_registry, tool_schemas, messages):
elif name in active_tool_names:
enabled.append(f"{name} (already active)")
else:
not_found.append(name)
# F3 (2026-08-10 saga): a policy-filtered tool is not "Not found" —
# answer with the typed reason so the agent stops guessing names.
reason = (
tools_registry.policy_hidden_reason(name)
if hasattr(tools_registry, "policy_hidden_reason") else None
)
if reason:
hidden.append(f"{name}{reason}")
else:
not_found.append(name)
parts = []
if enabled:
parts.append(
"✅ Tools are registered in the active capability envelope: "
+ ", ".join(enabled)
)
if hidden:
parts.append(
"🚫 Hidden by policy (the tool exists but this task cannot use it): "
+ "; ".join(hidden)
)
if not_found:
parts.append(f"❌ Not found: {', '.join(not_found)}")
return "\n".join(parts) if parts else "No tools specified."
@ -5748,6 +5762,103 @@ def _emit_round_progress(content: Any, msg: Dict[str, Any], emit_progress, llm_t
emit_progress(display_reasoning)
def _nanny_finalization_message(
tools: ToolRegistry, drive_root: pathlib.Path, task_id: str,
trace_attempted: bool = False,
) -> str:
"""The honest nanny reminder for a harness-dispatched child at finalization —
or '' when no reminder is deserved.
F4 (2026-08-10 saga): the old reminder accused children whose delegated runs
CRASHED of "choosing" not to delegate, and fired even when the delegate verbs
were policy-hidden from the task's toolset. Two structural facts fix both:
the task's own visible toolset, and the durable custody evidence
(delegate_custody.task_execution_evidence), which spans the WHOLE task
the per-execution llm_trace resets on every continuation. `trace_attempted`
carries the third fact: a delegate_start in THIS execution's trace. It must
not suppress the failure message (triad finding on e84475f2: the saga's own
shape delegate, run dies, finish by hand, finalize happens inside ONE
execution), only the accusation when custody has no rows yet (a pending or
uncustodied start is an attempt, not a choice).
"""
try:
if "delegate_start" not in set(tools.available_tools()):
return "" # the verbs are invisible here; "you chose not to" would be false
except Exception:
log.debug("nanny nudge: toolset visibility check failed", exc_info=True)
evidence: Dict[str, Any] = {}
try:
from ouroboros.delegate_custody import custody_root, task_execution_evidence
# Split-root fix (2026-08-10 amendments): custody WRITES land on the
# CANONICAL (budget) root, but this read used the loop's drive_root —
# the isolated CHILD drive for a split-root subagent, which carries no
# custody rows, leaving the nanny blind. Resolve the SAME root the
# writers use; the passed drive_root stays the fallback for contexts
# custody_root cannot resolve (e.g. unit-test stubs).
try:
evidence_root = custody_root(tools._ctx)
except Exception:
evidence_root = drive_root
evidence = task_execution_evidence(evidence_root, str(task_id or ""))
except Exception:
log.debug("nanny nudge: custody evidence read failed", exc_info=True)
if evidence.get("delegated_runs_succeeded"):
return "" # the route WAS used and worked (e.g. in an earlier execution)
started = int(evidence.get("delegated_runs_started") or 0)
if not started and (evidence.get("evidence_read_failed") or not evidence):
# Zero attempts is an ACCUSATION and needs positively-established
# evidence: an unreadable custody log (or a failed read above) proves
# nothing (scope finding on a5e59bdf).
return ""
if not started and trace_attempted:
# A start this execution's trace saw but custody has no row for: pending
# settlement or an uncustodied start. An attempt either way — neither
# accusation fits, and the wait/cancel path owns its own disclosure.
return ""
settled = int(evidence.get("delegated_runs_settled") or 0)
failure_states = [str(s) for s in (evidence.get("delegated_run_failure_states") or [])]
pending = max(0, started - settled)
if pending:
# PENDING ≠ FAILED (sol review on b49f8192): a STARTED row with no
# settlement may simply still be executing — calling it failed invites a
# duplicate concurrent run, and finalizing over it orphans the result.
# Takes precedence over the failed message: with a run in flight,
# "retry" is the wrong instruction even when an earlier sibling died
# (those failures still ride along as a fact).
failed_note = (
f" {len(failure_states)} earlier run(s) already ended: {', '.join(failure_states)}."
if failure_states else ""
)
return (
"⚠️ NANNY_DELEGATED_RUN_PENDING: you routed work onto the delegated "
f"substrate and {pending} delegated run(s) have started but not "
"settled — they may still be executing. Do not finalize over an "
"in-flight delegated run (its result would be orphaned) and do not "
"start a duplicate: wait for or check it (delegate_wait) before "
"finalizing, or cancel it (delegate_cancel) and say so." + failed_note
)
if started:
states = ", ".join(failure_states) or "settled without a recorded terminal state"
return (
"⚠️ NANNY_DELEGATED_RUN_FAILED: you DID route work onto the delegated "
f"substrate ({started} run(s) started), but none succeeded — your "
f"delegated run(s) ended: {states}. Do not finalize as if delegation "
"was never attempted: either retry it (delegate_start / delegate_wait) "
"or state in your final answer that the delegated run failed and why "
"the remaining work ran on metered API tokens."
)
return (
"⚠️ NANNY_DID_NOT_DELEGATE: this task was dispatched onto the delegated "
"substrate (executor=harness), but you are finalizing with ZERO "
"delegate_start calls — the work would end up billed to metered API "
"tokens the parent asked to avoid. Either delegate the remaining work "
"now (delegate_start / delegate_wait), or finalize with an explicit "
"statement of WHY delegation was not used (route refused, work shape "
"unsuited, deadline) so your parent sees the substrate decision."
)
def _maybe_inject_finalization_nudges(
tools: ToolRegistry, drive_root: Optional[pathlib.Path], task_id: str,
llm_trace: Dict[str, Any], content: Optional[str], messages: List[Dict[str, Any]],
@ -5759,31 +5870,33 @@ def _maybe_inject_finalization_nudges(
if drive_root is None:
return False
if (getattr(tools._ctx, "_nanny_route_dispatched", False)
and not getattr(tools._ctx, "_nanny_finalization_injected", False)
and not any(str(c.get("tool") or "") == "delegate_start"
for c in (llm_trace.get("tool_calls") or [])
if isinstance(c, dict))):
and not getattr(tools._ctx, "_nanny_finalization_injected", False)):
# Nanny postcondition (owner decision, 2026-08-07): a child dispatched onto
# the delegated substrate must not finalize as if that decision never
# existed. One structural fact (zero delegate_start calls in this task's
# trace), one re-loop; the child stays free to delegate now OR to finalize
# with a stated typed reason — never a hard gate on its judgment (P5).
tools._ctx._nanny_finalization_injected = True
_nanny_msg = (
"⚠️ NANNY_DID_NOT_DELEGATE: this task was dispatched onto the delegated "
"substrate (executor=harness), but you are finalizing with ZERO "
"delegate_start calls — the work would end up billed to metered API "
"tokens the parent asked to avoid. Either delegate the remaining work "
"now (delegate_start / delegate_wait), or finalize with an explicit "
"statement of WHY delegation was not used (route refused, work shape "
"unsuited, deadline) so your parent sees the substrate decision."
# existed. One structural fact, one re-loop; the child stays free to
# delegate now OR to finalize with a stated typed reason — never a hard
# gate on its judgment (P5). A delegate_start in THIS trace no longer
# short-circuits the whole nudge (triad finding on e84475f2): it rides
# into the message decision, where custody evidence distinguishes a
# failed run (truthful NANNY_DELEGATED_RUN_FAILED) from a pending or
# uncustodied attempt (no message). Suppression cases live in
# _nanny_finalization_message.
_trace_attempted = any(
str(c.get("tool") or "") == "delegate_start"
for c in (llm_trace.get("tool_calls") or [])
if isinstance(c, dict)
)
if content and content.strip():
messages.append({"role": "assistant", "content": content})
_append_or_merge_user_message(messages, f"[SYSTEM REMINDER]\n{_nanny_msg}")
emit_progress(_nanny_msg)
llm_trace["reasoning_notes"].append(_nanny_msg)
return True
tools._ctx._nanny_finalization_injected = True
_nanny_msg = _nanny_finalization_message(
tools, drive_root, task_id, trace_attempted=_trace_attempted,
)
if _nanny_msg:
if content and content.strip():
messages.append({"role": "assistant", "content": content})
_append_or_merge_user_message(messages, f"[SYSTEM REMINDER]\n{_nanny_msg}")
emit_progress(_nanny_msg)
llm_trace["reasoning_notes"].append(_nanny_msg)
return True
finalization_msg = _skill_finalization_message(drive_root, llm_trace)
if finalization_msg and not getattr(tools._ctx, "_skill_finalization_injected", False):
tools._ctx._skill_finalization_injected = True

View file

@ -1082,6 +1082,7 @@ def build_subagent_envelope(
usage: Dict[str, Any] | None = None,
cost_usd: float | None = None,
execution_evidence: Dict[str, Any] | None = None,
actual_substrate: str = "",
) -> Dict[str, Any]:
usage_data = dict(usage or {})
if cost_usd is None:
@ -1148,6 +1149,10 @@ def build_subagent_envelope(
# custody rows prove actually ran. Absent means "no evidence yet"
# (pre-completion), never "ran natively".
envelope["execution_evidence"] = dict(execution_evidence)
if actual_substrate:
# The FACT beside the plan (Q1A): harness_used / harness_attempted /
# native_only, always beside the execution_evidence counts above.
envelope["actual_substrate"] = str(actual_substrate)
return envelope
@ -1156,6 +1161,66 @@ def build_subagent_envelope(
# envelope carries no evidence — the neutral "dispatched" reading is the honest one.
_EVIDENCE_TERMINAL_STATUSES = frozenset({"completed", "failed", "cancelled", "interrupted"})
# The substrate FACT vocabulary (owner decision Q1A, 2026-08-10 amendments):
# `effective_executor`/`executor_route` stay the dispatch PLAN, `actual_substrate`
# is what the durable custody rows PROVE ran. Derived from custody evidence ONLY —
# never from usage/rounds, where delegate_wait polling and real native thinking
# are indistinguishable, so any boundary would be a guess. The raw attested
# counts always ride beside the enum on every surface that carries it.
SUBSTRATE_HARNESS_USED = "harness_used" # >=1 delegated run succeeded
SUBSTRATE_HARNESS_ATTEMPTED = "harness_attempted" # >=1 started, none succeeded
SUBSTRATE_NATIVE_ONLY = "native_only" # no delegated run ever started
def actual_substrate(evidence: Mapping[str, Any] | None) -> str:
"""Classify what ACTUALLY ran, from durable custody evidence alone (Q1A)."""
evidence = evidence if isinstance(evidence, Mapping) else {}
def _count(key: str) -> int:
try:
return int(evidence.get(key) or 0)
except (TypeError, ValueError):
return 0
if _count("delegated_runs_succeeded"):
return SUBSTRATE_HARNESS_USED
if _count("delegated_runs_started"):
return SUBSTRATE_HARNESS_ATTEMPTED
return SUBSTRATE_NATIVE_ONLY
def substrate_result_fields(envelope: Mapping[str, Any]) -> Dict[str, Any]:
"""Top-level durable-result mirror of the substrate FACT plus its raw counts.
The enum never travels without the attested counts it was derived from, so
a consumer of the durable result sees the full fact, not a classification.
"""
if not envelope.get("actual_substrate"):
return {}
ev = envelope.get("execution_evidence")
ev = ev if isinstance(ev, Mapping) else {}
return {
"actual_substrate": str(envelope["actual_substrate"]),
"delegated_runs_started": int(ev.get("delegated_runs_started") or 0),
"delegated_runs_succeeded": int(ev.get("delegated_runs_succeeded") or 0),
}
def _disclose_native_only_substrate(delta: Dict[str, Any]) -> Dict[str, Any]:
"""A harness dispatch that never started a delegated run is a REDUCED execution.
Surfaced through the EXISTING capability_delta disclosure (owner decision:
no new axis). Amends a COPY at the completion seam; the dispatch-time
author's dict on the live task stays untouched.
"""
amended = dict(delta or {})
reason = str(amended.get("reason") or "")
if "delegated_substrate_unused" not in reason:
amended["reason"] = "; ".join(
part for part in (reason, "delegated_substrate_unused") if part)
amended["reduced"] = True
return amended
def _execution_evidence_for_task(task: Mapping[str, Any], status: str) -> Dict[str, Any] | None:
"""Evidence for the completion envelope, or None when there is nothing to say.
@ -1203,6 +1268,17 @@ def envelope_from_task(
than a substituted default.
"""
usage = usage or {}
evidence = _execution_evidence_for_task(task, status)
# Unreadable custody log: the zero counts are UNKNOWN, not established
# facts — no substrate claim and no reduction amendment (the docs/JSDoc
# contract; omission keeps the enum vocabulary closed).
claimable = evidence is not None and not evidence.get("evidence_read_failed")
substrate = actual_substrate(evidence) if claimable else ""
capability_delta = task.get("capability_delta") if isinstance(task.get("capability_delta"), dict) else {}
if substrate == SUBSTRATE_NATIVE_ONLY and str(task.get("effective_executor") or "") == "harness":
# Q1A: a harness dispatch that ended native_only must not present as a
# clean un-reduced execution — the envelope carries the amended copy.
capability_delta = _disclose_native_only_substrate(capability_delta)
return build_subagent_envelope(
task_id=str(task.get("id") or ""),
parent_task_id=str(task.get("parent_task_id") or ""),
@ -1218,7 +1294,7 @@ def envelope_from_task(
effective_executor=str(task.get("effective_executor") or ""),
executor_route=str(task.get("executor_route") or ""),
tool_profile=str(task.get("tool_profile") or ""),
capability_delta=task.get("capability_delta") if isinstance(task.get("capability_delta"), dict) else {},
capability_delta=capability_delta,
status=status,
usage={
"prompt_tokens": int(usage.get("prompt_tokens") or 0),
@ -1226,5 +1302,6 @@ def envelope_from_task(
"rounds": int(usage.get("rounds") or 0),
},
cost_usd=cost_usd,
execution_evidence=_execution_evidence_for_task(task, status),
execution_evidence=evidence,
actual_substrate=substrate,
)

View file

@ -2488,6 +2488,38 @@ def _wait_for_tasks(
_delta = disclosable_capability_delta(data)
if _delta:
projected["capability_delta"] = _delta
# Delegation honesty (Q1A, 2026-08-10 amendments): whether a
# harness-dispatched child ACTUALLY delegated is a handoff fact the
# fan-out parent absorbs here — the e9108a09 incident hid nine
# native-only "harness" children behind this very projection.
# Compact counts only; the full evidence stays in the envelope.
_envelope = data.get("subagent_envelope") if isinstance(data.get("subagent_envelope"), dict) else {}
_evidence = _envelope.get("execution_evidence") if isinstance(_envelope.get("execution_evidence"), dict) else {}
if _evidence or str(data.get("effective_executor") or "") == "harness":
_ee: Dict[str, Any] = {
"dispatch_executor": str(data.get("effective_executor") or ""),
}
if _evidence.get("evidence_read_failed"):
# Unreadable custody log (v6.94.0 landing-gate scope fix):
# the counts are UNKNOWN — emitting them as 0 beside the
# marker fabricated a "no runs" receipt for a log that was
# never read. The compact projection carries ONLY the typed
# marker; counts AND the substrate claim are omitted, the
# same omission rule subagents.envelope_from_task applies.
_ee["evidence_read_failed"] = True
else:
if _evidence:
# Counts only when the envelope actually attested them:
# a result with no evidence recorded (pre-6.94) gets NO
# zero counts — absence means "no evidence yet", not
# "no runs".
_ee["delegated_runs_started"] = int(_evidence.get("delegated_runs_started") or 0)
_ee["delegated_runs_succeeded"] = int(_evidence.get("delegated_runs_succeeded") or 0)
# The substrate claim rides only when the envelope made one.
_substrate = str(data.get("actual_substrate") or _envelope.get("actual_substrate") or "")
if _substrate:
_ee["actual_substrate"] = _substrate
projected["execution_evidence"] = _ee
public_tasks[str(tid)] = projected
waited["tasks"] = public_tasks
waited["tasks_note"] = (

View file

@ -683,6 +683,19 @@ _WORKSPACE_ALLOWED_TOOLS = frozenset({
"ocr_pdf",
"youtube_transcript",
"extract_video_frames",
# Delegation-and-media gap (2026-08-10 saga): this allowlist predates the
# delegate verbs (v6.87.8) and the send_* family, so tasks in external
# project workspaces could neither delegate onto the already-paid substrate
# nor deliver media the task contract demanded. Both child profiles in
# tool_capabilities.py are strict subsets of this set (invariant-tested),
# so the workspace AND-intersection is vacuous for delegated children.
"delegate_start",
"delegate_wait",
"delegate_cancel",
"switch_model",
"send_photo",
"send_video",
"send_file",
"list_available_tools",
"enable_tools",
})
@ -1581,6 +1594,43 @@ class ToolRegistry:
def capability_omissions(self) -> List[Dict[str, Any]]:
return [dict(item) for item in self._capability_omissions]
def policy_hidden_reason(self, name: str) -> Optional[str]:
"""Why a REGISTERED built-in tool is invisible to THIS task, or None.
Read-only companion to get_schema_by_name (same predicates, same order):
it distinguishes "hidden by policy" from "does not exist" so discovery
answers can stop reporting a policy-filtered tool as nonexistent (F3,
2026-08-10 saga). None means visible OR unknown name callers that got
no schema and no reason may honestly say "not found".
"""
requested = str(name or "").strip()
if not requested:
return None
# BEFORE the registration check: the declarative contract policy applies
# across ALL discovery sources (get_schema_by_name checks it first for the
# same reason), so a contract-disabled extension/MCP name answers with its
# reason instead of "not found" (2026-08-10 amendments). Deeper extension/
# MCP policy reasons (grants, network) would need new plumbing — disclosed
# residual, not built.
if requested in _disabled_tools(self._ctx):
return "disabled by this task's contract (disabled_tools)"
if requested not in self._entries:
return None
available, reason, _detail = _builtin_tool_availability(requested, self._ctx)
if not available:
return f"unavailable ({reason})"
if getattr(self._ctx, "is_ephemeral_turn", False) and requested not in _EPHEMERAL_ALLOWED_TOOLS:
return "hidden on this ephemeral decision turn (allowlist)"
acting_subagent = self._is_acting_subagent()
workspace_mode = bool(getattr(self._ctx, "is_workspace_mode", lambda: False)()) and not acting_subagent
if workspace_mode and requested not in _WORKSPACE_ALLOWED_TOOLS:
return "hidden by the workspace tool envelope"
if self._is_local_readonly_subagent() and requested not in LOCAL_READONLY_SUBAGENT_TOOL_NAMES:
return "hidden by the read-only subagent profile"
if acting_subagent and requested not in ACTING_SUBAGENT_TOOL_NAMES:
return "hidden by the acting subagent profile"
return None
def get_schema_by_name(self, name: str) -> Optional[Dict[str, Any]]:
"""Return the full schema for a specific tool."""
requested = str(name or "").strip()

View file

@ -63,16 +63,25 @@ def _enable_tools(ctx: ToolContext, tools: str = "", **kwargs) -> str:
if not names:
return "No tools specified."
found = []
hidden = []
not_found = []
for name in names:
schema = _registry.get_schema_by_name(name)
if schema:
found.append(f"{name}: {schema['function'].get('description', '')[:100]}")
continue
# F3 (2026-08-10 saga): a registered tool filtered by policy used to be
# reported as nonexistent — indistinguishable from a typo'd name.
reason = _registry.policy_hidden_reason(name) if hasattr(_registry, "policy_hidden_reason") else None
if reason:
hidden.append(f"{name}{reason}")
else:
not_found.append(name)
parts = []
if found:
parts.append("✅ Tools are registered and already callable in the active envelope:\n" + "\n".join(f" - {s}" for s in found))
if hidden:
parts.append("🚫 Hidden by policy (the tool exists but this task cannot use it):\n" + "\n".join(f" - {s}" for s in hidden))
if not_found:
parts.append(f"❌ Not found: {', '.join(not_found)}")
return "\n".join(parts)

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ouroboros"
version = "6.93.1"
version = "6.94.0"
description = "Self-creating AI agent with constitution, background consciousness, and persistent identity"
readme = "README.md"
license = {text = "MIT"}

View file

@ -1464,6 +1464,9 @@ def _finish_task_done_dispatch(
_envelope = effective_result.get("subagent_envelope")
if isinstance(_envelope, dict) and isinstance(_envelope.get("execution_evidence"), dict):
progress_meta["execution_evidence"] = _envelope["execution_evidence"]
if isinstance(_envelope, dict) and _envelope.get("actual_substrate"):
# The FACT beside the plan (Q1A): harness_used / harness_attempted / native_only.
progress_meta["actual_substrate"] = str(_envelope["actual_substrate"])
if isinstance(task_done_event.get("outcome_axes"), dict):
progress_meta["outcome_axes"] = task_done_event["outcome_axes"]
if task_done_event.get("reason_code"):

View file

@ -270,6 +270,11 @@ _TRUNCATION_DECISIONS: dict[str, tuple[bool, str]] = {
# mirrors that tuple — changing the provenance of an already-reported result to
# describe a code that cannot appear in it.
"subagent_executor_unavailable": (False, "agent.py executor_blocked_outcome; a subagent terminal, never a trial's"),
# Q1A preflight (2026-08-10 amendments): an explicit harness pin whose child toolset
# hides the delegate verbs ends the CHILD unrun before any LLM spend — the same
# subagent-terminal class as subagent_executor_unavailable, never a trial's code.
"delegate_tools_invisible": (False, "agent.py executor_blocked_outcome / preflight_delegate_visibility; a subagent terminal, never a trial's"),
"delegate_visibility_unverified": (False, "agent.py preflight_delegate_visibility broken-introspection path; a subagent terminal, never a trial's"),
"task_exception": (False, "agent.py:777 the attempt ran and crashed; an honest failure"),
"capability_profile_mismatch": (False, "control_delegation.py:81 rejected delegate call"),
"delegation_constraint_block_surface": (False, "control_delegation.py:116 rejected call"),

View file

@ -0,0 +1,169 @@
"""Q1A capability preflight (2026-08-10 amendments): a harness dispatch whose
materialized toolset does not carry the delegate verbs must never pay for a
single LLM round under a dispatch record that says "harness".
The audited incident (task e9108a09c6574184, "slime games research PDF"): nine
children recorded effective_executor=harness / executor_route=codex, made ZERO
delegated runs and burned ~$29-54 of metered API, while their results said
execution=ok and capability_delta.reduced=false. The Phase A allowlist widening
closed one instance of the class; this preflight closes the class at the last
zero-cost moment toolset materialization.
"""
from types import SimpleNamespace
from ouroboros.agent import (
_blocked_executor_terminal,
executor_blocked_outcome,
preflight_delegate_visibility,
)
from ouroboros.subagents import (
CapabilityDelta,
SubagentDispatch,
SubagentExecutorResolution,
SubagentLaneResolution,
)
def _dispatch(requested_executor="auto", executor="harness"):
lane = SubagentLaneResolution(
requested_lane="auto", effective_lane="main", model="test-model",
resolved_from="main",
)
delta = CapabilityDelta(
requested_lane="auto", resolved_lane="main", effective_lane="main",
derived_effort="low", effective_effort="low",
requested_executor=requested_executor, effective_executor=executor,
reason="", reduced=False,
)
resolution = SubagentExecutorResolution(
requested=requested_executor, executor=executor, route=None,
reason="harness_ready" if executor == "harness" else "requested_native",
)
return SubagentDispatch(
lane=lane, effort="low", executor=executor,
route="claude" if executor == "harness" else "",
profile="local_readonly_subagent", delta=delta,
executor_resolution=resolution,
)
def _task(requested_executor="auto"):
return {
"id": "child-1",
"delegation_role": "subagent",
"requested_executor": requested_executor,
"effective_executor": "harness",
"executor_route": "claude",
}
def _tools(available):
return SimpleNamespace(available_tools=lambda: list(available))
def test_visible_verbs_leave_the_dispatch_untouched():
dispatch = _dispatch()
task = _task()
result, amended = preflight_delegate_visibility(
_tools(["delegate_start", "delegate_wait", "delegate_cancel", "read_file"]),
task, dispatch)
assert result is dispatch
assert amended is False
assert task["executor_route"] == "claude" # nothing re-stamped
def test_partial_verb_set_is_still_broken():
# A child that can start a run but not wait on it cannot honor the contract.
dispatch = _dispatch(requested_executor="auto")
task = _task(requested_executor="auto")
result, amended = preflight_delegate_visibility(
_tools(["delegate_start", "delegate_cancel", "read_file"]), task, dispatch)
assert amended is True
assert result.executor == "native"
assert "delegate_tools_invisible" in task["capability_delta"]["reason"]
def test_auto_dispatch_with_invisible_verbs_falls_back_loudly_to_native():
dispatch = _dispatch(requested_executor="auto")
task = _task(requested_executor="auto")
result, amended = preflight_delegate_visibility(
_tools(["read_file", "web_search"]), task, dispatch)
assert amended is True
assert result.executor == "native"
assert result.route == ""
# The typed capability_delta entry: the parent and the owner SEE the reduction.
assert task["capability_delta"]["reduced"] is True
assert "delegate_tools_invisible" in task["capability_delta"]["reason"]
assert task["capability_delta"]["effective_executor"] == "native"
# The recorded dispatch fields no longer lie.
assert task["effective_executor"] == "native"
assert task["executor_route"] == ""
assert task["subagent_envelope"]["effective_executor"] == "native"
assert task["subagent_envelope"]["executor_route"] == ""
def test_explicit_harness_pin_with_invisible_verbs_blocks_with_zero_spend():
dispatch = _dispatch(requested_executor="harness")
task = _task(requested_executor="harness")
result, amended = preflight_delegate_visibility(
_tools(["read_file"]), task, dispatch)
assert amended is True
assert result.blocked is True
assert task["effective_executor"] == "blocked"
# The existing blocked terminal carries the distinct typed reason.
text, usage = executor_blocked_outcome(result.executor_resolution)
assert usage["reason_code"] == "delegate_tools_invisible"
assert usage["execution_status"] == "infra_failed"
assert "not visible" in text
assert "NOT run on metered API tokens" in text
# And the cap_info seam _prepare_task_context feeds rebuilds the same outcome.
cap_info = {
"executor_blocked_reason": result.executor_resolution.reason,
"executor_blocked_requested": result.executor_resolution.requested,
"executor_blocked_reset_at": result.executor_resolution.reset_at,
}
terminal_text, terminal_usage, _trace = _blocked_executor_terminal(cap_info)
assert terminal_usage["reason_code"] == "delegate_tools_invisible"
assert "delegate_start" in terminal_text
def _broken_tools():
def _boom():
raise RuntimeError("registry exploded")
return SimpleNamespace(available_tools=_boom)
def test_broken_introspection_with_auto_executor_proceeds_disclosed():
# Fail-open for auto — but never silently: the probe failure rides the delta.
dispatch = _dispatch(requested_executor="auto")
task = _task(requested_executor="auto")
result, amended = preflight_delegate_visibility(_broken_tools(), task, dispatch)
assert amended is True
assert result.executor == "harness" # the dispatch itself is kept
assert task["effective_executor"] == "harness"
assert "delegate_visibility_unverified" in task["capability_delta"]["reason"]
def test_broken_introspection_with_pinned_harness_fails_closed():
# A probe that cannot prove visibility cannot prove the pinned contract is
# executable: the typed blocked outcome, zero spend — under the HONEST
# reason (visibility is unknown, not disproven).
dispatch = _dispatch(requested_executor="harness")
task = _task(requested_executor="harness")
result, amended = preflight_delegate_visibility(_broken_tools(), task, dispatch)
assert amended is True
assert result.blocked is True
assert task["effective_executor"] == "blocked"
text, usage = executor_blocked_outcome(result.executor_resolution)
assert usage["reason_code"] == "delegate_visibility_unverified"
assert "could not be verified" in text
assert "NOT run on metered API tokens" in text
def test_native_and_undispatched_children_are_not_probed():
probe_forbidden = SimpleNamespace(
available_tools=lambda: (_ for _ in ()).throw(AssertionError("must not probe")))
native = _dispatch(executor="native")
assert preflight_delegate_visibility(probe_forbidden, _task(), native) == (native, False)
assert preflight_delegate_visibility(probe_forbidden, _task(), None) == (None, False)

View file

@ -44,10 +44,10 @@ def _emit_started(drive, run_id="run-1", task_id="child-1", model=""):
def _emit_settled(drive, run_id="run-1", task_id="child-1", *,
cost_usd=0.0, spend_disclosed=True, model="claude-sonnet",
spend_estimated=False):
spend_estimated=False, state="succeeded"):
assert custody.emit(drive, custody.SETTLED, {
"run_id": run_id, "task_id": task_id, "route": "claude", "model": model,
"state": "succeeded", "cost_usd": cost_usd,
"state": state, "cost_usd": cost_usd,
"cost_final": spend_disclosed and not spend_estimated,
"spend_disclosed": spend_disclosed, "spend_estimated": spend_estimated,
})
@ -60,6 +60,9 @@ class TestCustodyAggregation:
assert evidence == {
"delegated_runs_started": 0,
"delegated_runs_settled": 0,
"delegated_runs_succeeded": 0,
"delegated_run_failure_states": [],
"evidence_read_failed": False,
"subscription_cost_usd": None,
"subscription_cost_estimated": False,
"harness_models": [],
@ -105,6 +108,28 @@ class TestCustodyAggregation:
assert evidence["delegated_runs_settled"] == 1
assert evidence["subscription_cost_usd"] is None
def test_failed_run_reads_as_attempted_route_not_zero_attempts(self, tmp_path):
# F4 (2026-08-10 saga): a run that STARTED and FAILED is an ATTEMPTED
# route. The terminal-state axis lets readers (the nanny nudge) tell
# "never tried" from "tried and the run died" without accusing the child.
drive = _drive(tmp_path)
_emit_started(drive, "run-1")
assert custody.emit(drive, custody.SETTLED, {
"run_id": "run-1", "task_id": "child-1", "route": "claude",
"model": "claude-opus-5", "state": "failed", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
evidence = custody.task_execution_evidence(drive, "child-1")
assert evidence["delegated_runs_started"] == 1
assert evidence["delegated_runs_succeeded"] == 0
assert evidence["delegated_run_failure_states"] == ["failed"]
# A succeeded run counts on the success axis and adds no failure state.
_emit_started(drive, "run-2")
_emit_settled(drive, "run-2")
evidence = custody.task_execution_evidence(drive, "child-1")
assert evidence["delegated_runs_succeeded"] == 1
assert evidence["delegated_run_failure_states"] == ["failed"]
def test_another_tasks_runs_do_not_leak_in(self, tmp_path):
drive = _drive(tmp_path)
_emit_started(drive, "run-9", task_id="other-task")
@ -126,6 +151,9 @@ class TestEnvelopeReconciliation:
assert envelope["execution_evidence"] == {
"delegated_runs_started": 0,
"delegated_runs_settled": 0,
"delegated_runs_succeeded": 0,
"delegated_run_failure_states": [],
"evidence_read_failed": False,
"subscription_cost_usd": None,
"subscription_cost_estimated": False,
"harness_models": [],
@ -172,6 +200,112 @@ class TestEnvelopeReconciliation:
assert envelope["executor_route"] == "claude"
class TestActualSubstrate:
"""Q1A (2026-08-10 amendments): the PLAN (`effective_executor`) and the FACT
(`actual_substrate`) are separate fields a harness-dispatched task that ran
everything on metered API must not read as a clean delegated execution."""
def test_vocabulary_is_purely_factual_from_custody_counts(self):
# Custody evidence ONLY — no usage/rounds axis, where polling and real
# thinking are indistinguishable and any boundary would be a guess.
from ouroboros.subagents import actual_substrate
assert actual_substrate(None) == "native_only"
assert actual_substrate({"delegated_runs_started": 0}) == "native_only"
# Started-but-failed is a FAILED ATTEMPT, not "never tried".
assert actual_substrate({"delegated_runs_started": 2,
"delegated_runs_succeeded": 0}) == "harness_attempted"
assert actual_substrate({"delegated_runs_started": 1,
"delegated_runs_succeeded": 1}) == "harness_used"
def test_attempted_run_classifies_attempted_in_the_envelope(self, tmp_path):
drive = _drive(tmp_path)
_emit_started(drive, "run-1")
_emit_settled(drive, "run-1", state="failed")
envelope = envelope_from_task(_subagent_task(drive), status="completed")
assert envelope["actual_substrate"] == "harness_attempted"
def test_envelope_carries_the_fact_beside_the_plan(self, tmp_path):
drive = _drive(tmp_path)
_emit_started(drive, "run-1")
_emit_settled(drive, "run-1")
envelope = envelope_from_task(_subagent_task(drive), status="completed",
usage={"rounds": 4})
assert envelope["effective_executor"] == "harness" # the plan, untouched
assert envelope["actual_substrate"] == "harness_used"
def test_native_only_harness_dispatch_discloses_a_reduced_delta(self, tmp_path):
# The e9108a09 shape: dispatched harness, zero delegated runs. The
# completion envelope must not present a clean un-reduced execution —
# the EXISTING capability_delta disclosure carries it (no new axis).
drive = _drive(tmp_path)
task = _subagent_task(drive, capability_delta={
"requested_executor": "auto", "effective_executor": "harness",
"reason": "", "reduced": False,
})
envelope = envelope_from_task(task, status="completed", usage={"rounds": 9})
assert envelope["actual_substrate"] == "native_only"
assert envelope["capability_delta"]["reduced"] is True
assert "delegated_substrate_unused" in envelope["capability_delta"]["reason"]
# The dispatch-time author's dict on the task stays untouched.
assert task["capability_delta"]["reduced"] is False
# And the batch-projection predicate now discloses it to the parent.
from ouroboros.tools.control import disclosable_capability_delta
assert disclosable_capability_delta({"capability_delta": envelope["capability_delta"]})
def test_durable_result_fields_carry_the_raw_counts_beside_the_enum(self, tmp_path):
from ouroboros.subagents import substrate_result_fields
drive = _drive(tmp_path)
_emit_started(drive, "run-1")
envelope = envelope_from_task(_subagent_task(drive), status="completed")
assert substrate_result_fields(envelope) == {
"actual_substrate": "harness_attempted",
"delegated_runs_started": 1,
"delegated_runs_succeeded": 0,
}
assert substrate_result_fields({}) == {} # no substrate claim, no fields
def test_unreadable_evidence_makes_no_substrate_claim_anywhere(self, tmp_path):
# 6c03c24e corrective wave (both sol lanes + fable): an unreadable
# canonical custody log returns zero counts with evidence_read_failed —
# those zeros are UNKNOWN, so the envelope must not classify them as
# native_only, must not add the delegated_substrate_unused reduction,
# and the durable result must carry no top-level substrate fields.
from ouroboros import delegate_custody as custody
from ouroboros.subagents import substrate_result_fields
drive = _drive(tmp_path)
log_path = custody.event_log_path(drive)
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.mkdir() # a directory where the file should be -> OSError
task = _subagent_task(drive, capability_delta={"reduced": False, "reason": ""})
envelope = envelope_from_task(task, status="completed")
assert envelope["execution_evidence"]["evidence_read_failed"] is True
assert "actual_substrate" not in envelope
assert envelope["capability_delta"]["reduced"] is False
assert "delegated_substrate_unused" not in str(envelope["capability_delta"].get("reason") or "")
assert substrate_result_fields(envelope) == {}
def test_delegated_success_does_not_amend_the_delta(self, tmp_path):
drive = _drive(tmp_path)
_emit_started(drive, "run-1")
_emit_settled(drive, "run-1")
task = _subagent_task(drive, capability_delta={"reduced": False, "reason": ""})
envelope = envelope_from_task(task, status="completed", usage={"rounds": 4})
assert envelope["capability_delta"]["reduced"] is False
def test_running_and_native_envelopes_carry_no_substrate_claim(self, tmp_path):
drive = _drive(tmp_path)
running = envelope_from_task(_subagent_task(drive), status="running")
assert "actual_substrate" not in running
native = envelope_from_task(
_subagent_task(drive, executor_route="", effective_executor="native"),
status="completed")
assert "actual_substrate" not in native
def test_terminal_frame_field_rides_the_history_replay_allowlist():
# The chip's layered truth must survive a reload: the terminal frame carries
# execution_evidence, and history replay filters progress meta by this list.
@ -196,3 +330,35 @@ def test_evidence_is_json_serializable(tmp_path):
_emit_settled(drive, "run-1")
envelope = envelope_from_task(_subagent_task(drive), status="failed")
json.dumps(envelope)
class TestEvidenceReadHonesty:
def test_unreadable_log_is_flagged_not_zero(self, tmp_path):
# Scope finding (a2a6253e gate lineage): an EXISTING but unreadable
# canonical log must not collapse into "zero attempts established" —
# a directory at the log path forces the open() OSError portably.
from ouroboros import delegate_custody as custody
log_path = custody.event_log_path(tmp_path)
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.mkdir() # a directory where the file should be
evidence = custody.task_execution_evidence(tmp_path, "t1")
assert evidence["evidence_read_failed"] is True
assert evidence["delegated_runs_started"] == 0
def test_nanny_never_accuses_on_unreadable_evidence(self, tmp_path):
from types import SimpleNamespace
from ouroboros import delegate_custody as custody
from ouroboros.loop import _maybe_inject_finalization_nudges
log_path = custody.event_log_path(tmp_path)
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.mkdir()
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
tools = SimpleNamespace(_ctx=ctx, available_tools=lambda: ["delegate_start"])
msgs: list = []
assert _maybe_inject_finalization_nudges(
tools, tmp_path, "t1",
{"reasoning_notes": [], "tool_calls": []}, "done", msgs, lambda *_: None,
) is False
assert not any("NANNY" in m.get("content", "") for m in msgs)

View file

@ -30,9 +30,246 @@ def test_harness_child_finalizing_without_delegation_gets_one_nudge():
assert _run(ctx, [], []) is False
def _tools(ctx_obj, available):
return SimpleNamespace(_ctx=ctx_obj, available_tools=lambda: list(available))
def _custody_drive(tmp_path):
(tmp_path / "logs").mkdir(parents=True, exist_ok=True)
return tmp_path
def _run_full(tools, drive, task_id, msgs, tool_calls):
return _maybe_inject_finalization_nudges(
tools, drive, task_id,
{"reasoning_notes": [], "tool_calls": tool_calls}, "done", msgs, lambda *_: None,
)
def test_no_nudge_when_delegate_verbs_are_policy_hidden(tmp_path):
# F4a (2026-08-10 saga): a child whose toolset does not carry the delegate
# verbs cannot "choose" to delegate — accusing it is false. No reminder.
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
msgs: list = []
assert _run_full(_tools(ctx, ["read_file", "web_search"]),
_custody_drive(tmp_path), "t", msgs, []) is False
assert not any("NANNY" in m.get("content", "") for m in msgs)
def test_failed_delegated_run_gets_the_truthful_reminder(tmp_path):
# F4b: a delegated run that STARTED but FAILED is an attempted route. The
# durable custody evidence (not the per-execution trace) proves it, and the
# reminder speaks the truth instead of accusing of zero attempts.
from ouroboros import delegate_custody as custody
drive = _custody_drive(tmp_path)
assert custody.emit(drive, custody.STARTED, {
"run_id": "run-1", "task_id": "child-1", "route": "claude", "max_seconds": 300,
})
assert custody.emit(drive, custody.SETTLED, {
"run_id": "run-1", "task_id": "child-1", "route": "claude",
"model": "claude-opus-5", "state": "failed", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
msgs: list = []
assert _run_full(_tools(ctx, ["delegate_start", "delegate_wait"]),
drive, "child-1", msgs, []) is True
joined = "\n".join(m.get("content", "") for m in msgs)
assert "NANNY_DELEGATED_RUN_FAILED" in joined
assert "failed" in joined
assert "NANNY_DID_NOT_DELEGATE" not in joined
def test_succeeded_delegated_run_suppresses_the_reminder(tmp_path):
# A run that succeeded in an EARLIER execution (continuation reset the trace)
# is a kept substrate decision — no reminder at all.
from ouroboros import delegate_custody as custody
drive = _custody_drive(tmp_path)
assert custody.emit(drive, custody.STARTED, {
"run_id": "run-1", "task_id": "child-1", "route": "claude", "max_seconds": 300,
})
assert custody.emit(drive, custody.SETTLED, {
"run_id": "run-1", "task_id": "child-1", "route": "claude",
"model": "claude-opus-5", "state": "succeeded", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
msgs: list = []
assert _run_full(_tools(ctx, ["delegate_start", "delegate_wait"]),
drive, "child-1", msgs, []) is False
assert not any("NANNY" in m.get("content", "") for m in msgs)
def test_failed_run_nudges_even_with_delegate_start_in_this_trace(tmp_path):
# Triad finding on e84475f2 (the saga's exact shape, all inside ONE
# execution): delegate → the run dies → finish by hand → finalize. The
# trace CONTAINS delegate_start, so the old outer guard skipped the nudge
# entirely and the failure was never spoken. Custody evidence must win.
from ouroboros import delegate_custody as custody
drive = _custody_drive(tmp_path)
assert custody.emit(drive, custody.STARTED, {
"run_id": "run-1", "task_id": "child-1", "route": "codex", "max_seconds": 300,
})
assert custody.emit(drive, custody.SETTLED, {
"run_id": "run-1", "task_id": "child-1", "route": "codex",
"model": "gpt-5.6-sol", "state": "failed", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
msgs: list = []
assert _run_full(_tools(ctx, ["delegate_start", "delegate_wait"]),
drive, "child-1", msgs,
[{"tool": "delegate_start", "args": {}}]) is True
joined = "\n".join(m.get("content", "") for m in msgs)
assert "NANNY_DELEGATED_RUN_FAILED" in joined
assert "NANNY_DID_NOT_DELEGATE" not in joined
def _split_root_ctx(parent, child):
return SimpleNamespace(
_nanny_route_dispatched=True, _nanny_finalization_injected=False,
task_metadata={"budget_drive_root": str(parent)}, drive_root=str(child),
)
def test_split_root_nanny_reads_custody_from_the_canonical_root(tmp_path):
# Split-root fix (2026-08-10 amendments): custody rows are WRITTEN to the
# canonical (budget) root — delegate_custody.custody_root — while a live
# subagent's loop passes its isolated CHILD drive as drive_root. The nanny
# read must resolve the same root as the writes: a succeeded run suppresses
# the nudge, a started-but-failed run yields the truthful failure message —
# both with the child drive passed exactly as production passes it.
from ouroboros import delegate_custody as custody
from ouroboros.loop import _nanny_finalization_message
parent, child = tmp_path / "parent", tmp_path / "child"
for root in (parent, child):
(root / "logs").mkdir(parents=True)
# (a) succeeded delegated run, rows on the CANONICAL root via the write path
ctx = _split_root_ctx(parent, child)
root = custody.custody_root(ctx)
assert root == parent.resolve()
assert custody.emit(root, custody.STARTED, {
"run_id": "run-ok", "task_id": "child-ok", "route": "claude", "max_seconds": 300,
})
assert custody.emit(root, custody.SETTLED, {
"run_id": "run-ok", "task_id": "child-ok", "route": "claude",
"model": "claude-opus-5", "state": "succeeded", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
tools = _tools(ctx, ["delegate_start", "delegate_wait"])
assert _nanny_finalization_message(tools, child, "child-ok") == ""
# (b) started-but-failed run: the truthful failure message, not blindness
assert custody.emit(root, custody.STARTED, {
"run_id": "run-dead", "task_id": "child-dead", "route": "codex", "max_seconds": 300,
})
assert custody.emit(root, custody.SETTLED, {
"run_id": "run-dead", "task_id": "child-dead", "route": "codex",
"model": "gpt-5.6-sol", "state": "failed", "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
message = _nanny_finalization_message(tools, child, "child-dead")
assert "NANNY_DELEGATED_RUN_FAILED" in message
assert "NANNY_DID_NOT_DELEGATE" not in message
def _emit_started(drive, run_id, task_id):
from ouroboros import delegate_custody as custody
assert custody.emit(drive, custody.STARTED, {
"run_id": run_id, "task_id": task_id, "route": "claude", "max_seconds": 300,
})
def _emit_settled(drive, run_id, task_id, state):
from ouroboros import delegate_custody as custody
assert custody.emit(drive, custody.SETTLED, {
"run_id": run_id, "task_id": task_id, "route": "claude",
"model": "claude-opus-5", "state": state, "cost_usd": 0.0,
"cost_final": True, "spend_disclosed": True, "spend_estimated": False,
})
def test_pending_run_gets_the_wait_reminder_not_a_failure_accusation(tmp_path):
# PENDING ≠ FAILED (sol review on b49f8192): a STARTED row with no settled
# receipt may simply still be executing. The old message called it failed and
# told the child to retry — a duplicate concurrent delegated run — while
# finalizing over it is exactly the orphan-result failure mode. The reminder
# points at delegate_wait and accuses nothing.
from ouroboros.loop import _nanny_finalization_message
drive = _custody_drive(tmp_path)
_emit_started(drive, "run-1", "child-1")
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
message = _nanny_finalization_message(
_tools(ctx, ["delegate_start", "delegate_wait"]), drive, "child-1")
assert "NANNY_DELEGATED_RUN_PENDING" in message
assert "delegate_wait" in message
assert "NANNY_DELEGATED_RUN_FAILED" not in message
assert "NANNY_DID_NOT_DELEGATE" not in message
def test_mixed_failed_and_pending_runs_prefer_the_pending_reminder(tmp_path):
# With one dead sibling AND one still in flight, "retry" is the wrong
# instruction: the pending reminder wins, the earlier failure rides along
# as a fact instead of being dropped.
from ouroboros.loop import _nanny_finalization_message
drive = _custody_drive(tmp_path)
_emit_started(drive, "run-dead", "child-1")
_emit_settled(drive, "run-dead", "child-1", "failed")
_emit_started(drive, "run-live", "child-1")
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
message = _nanny_finalization_message(
_tools(ctx, ["delegate_start", "delegate_wait"]), drive, "child-1")
assert "NANNY_DELEGATED_RUN_PENDING" in message
assert "failed" in message # the earlier death is still named
assert "NANNY_DELEGATED_RUN_FAILED" not in message
def test_all_failed_runs_keep_the_failure_message(tmp_path):
# All settled, none succeeded: the terminal non-success message stays.
from ouroboros.loop import _nanny_finalization_message
drive = _custody_drive(tmp_path)
for run_id in ("run-1", "run-2"):
_emit_started(drive, run_id, "child-1")
_emit_settled(drive, run_id, "child-1", "failed")
ctx = SimpleNamespace(_nanny_route_dispatched=True, _nanny_finalization_injected=False)
message = _nanny_finalization_message(
_tools(ctx, ["delegate_start", "delegate_wait"]), drive, "child-1")
assert "NANNY_DELEGATED_RUN_FAILED" in message
assert "NANNY_DELEGATED_RUN_PENDING" not in message
def test_closed_absent_run_counts_as_settled_not_pending(tmp_path):
# A run the daemon says it does not have closed custody terminally without a
# settlement row; the evidence must not read it as "still executing" forever.
from ouroboros import delegate_custody as custody
drive = _custody_drive(tmp_path)
_emit_started(drive, "run-1", "child-1")
assert custody.emit(drive, custody.CLOSED_ABSENT, {
"run_id": "run-1", "task_id": "child-1", "route": "claude",
"project_id": "", "reason": "reconcile_absent",
})
evidence = custody.task_execution_evidence(drive, "child-1")
assert evidence["delegated_runs_settled"] == 1
assert evidence["delegated_runs_succeeded"] == 0
assert "closed_absent" in evidence["delegated_run_failure_states"]
assert evidence["subscription_cost_usd"] is None # spend undisclosed, never zero
def test_a_delegating_nanny_and_a_native_child_are_not_nudged():
# A single delegate_start call in the trace IS the receipt — even a refused
# one proves the substrate decision was faced rather than ignored.
# A delegate_start in the trace with NO custody row yet (pending settlement
# or an uncustodied start) is an attempt, not a choice — no accusation, and
# the failure case is owned by custody evidence (see the test above).
delegating = SimpleNamespace(_nanny_route_dispatched=True,
_nanny_finalization_injected=False)
assert _run(delegating, [], [{"tool": "delegate_start", "args": {}}]) is False

View file

@ -0,0 +1,79 @@
"""F6 (2026-08-10 amendments): the promote/router turn sees the LIVE toolset.
The router turn authors objectives/contracts for a task it will never run. The
first F6 cut projected a static union that advertised credential-gated built-ins
real availability removes and omitted every registered non-workspace built-in
so the router could still author impossible or over-restricted contracts. These
tests pin the projection to the registry's REAL ``available_tools()`` resolution
for both target shapes (workspace-mode task vs non-workspace).
"""
import json
from types import SimpleNamespace
import pytest
def _env(tmp_path):
return SimpleNamespace(repo_dir=str(tmp_path / "repo"), drive_root=tmp_path)
def _toolset(tmp_path):
from ouroboros.context import build_runtime_section
task = {"id": "t1", "_ephemeral_turn": True, "metadata": {"force_plan": True}}
section = build_runtime_section(_env(tmp_path), task)
payload = json.loads(section.split("\n\n", 1)[1])
assert "promoted_task_toolset" in payload, "the swarm-router turn must carry F6"
return payload["promoted_task_toolset"]
@pytest.fixture()
def _github_token(monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
def test_credential_gated_tool_is_not_advertised_when_unavailable(tmp_path, monkeypatch):
# web_search is credential-gated behind live backends; with none available
# the router must not be able to demand it — it moves to the TYPED omission
# list instead of silently disappearing.
import ouroboros.tools.search as search
monkeypatch.setattr(search, "_available_web_search_backends", lambda: [])
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
toolset = _toolset(tmp_path)
assert "web_search" not in toolset["workspace_task_tools"]
assert "web_search" not in toolset["non_workspace_extra_tools"]
assert "missing_credential" in toolset["unavailable_builtin_tools"]["web_search"]
# GitHub built-ins without a token: same typed omission, not "does not exist".
assert "get_github_issue" not in toolset["non_workspace_extra_tools"]
assert "missing_credential" in toolset["unavailable_builtin_tools"]["get_github_issue"]
def test_live_toolset_classifies_core_workspace_and_non_workspace_tools(tmp_path, monkeypatch, _github_token):
import ouroboros.tools.search as search
monkeypatch.setattr(search, "_available_web_search_backends", lambda: ["ddgs"])
toolset = _toolset(tmp_path)
workspace = set(toolset["workspace_task_tools"])
extra = set(toolset["non_workspace_extra_tools"])
# A representative core/workspace tool rides the workspace list.
assert "read_file" in workspace
assert "delegate_start" in workspace
# A registered non-workspace built-in (invisible in the old static union)
# is now advertised where a non-workspace task would really see it.
assert "get_github_issue" in extra
assert "get_github_issue" not in workspace
# Workspace-only visibility: the two lists never overlap.
assert not (workspace & extra)
# With its credential present the gated tool is advertised normally.
assert "web_search" in workspace
assert "web_search" not in toolset.get("unavailable_builtin_tools", {})
def test_non_router_turns_do_not_pay_for_the_projection(tmp_path):
from ouroboros.context import build_runtime_section
section = build_runtime_section(_env(tmp_path), {"id": "t1", "type": "task"})
assert "promoted_task_toolset" not in section

View file

@ -701,6 +701,91 @@ def test_wait_for_tasks_returns_compact_structural_batch(tmp_path):
)
def test_wait_for_tasks_projects_execution_evidence_for_harness_children(tmp_path):
# Q1A (2026-08-10 amendments): the batch projection is the surface a fan-out
# parent absorbs its children through, and it used to hide whether a
# harness-dispatched child ever actually delegated (the e9108a09 shape:
# nine "harness" children, zero delegated runs, invisible in the batch).
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
from ouroboros.tools.control import _wait_for_tasks
write_task_result(
tmp_path, "harnesskid", STATUS_COMPLETED, result="done",
effective_executor="harness", executor_route="codex",
actual_substrate="native_only",
subagent_envelope={
"actual_substrate": "native_only",
"execution_evidence": {
"delegated_runs_started": 0, "delegated_runs_settled": 0,
"delegated_runs_succeeded": 0, "delegated_run_failure_states": [],
"evidence_read_failed": False, "subscription_cost_usd": None,
"subscription_cost_estimated": False, "harness_models": [],
},
},
)
write_task_result(tmp_path, "nativekid", STATUS_COMPLETED, result="done")
ctx = SimpleNamespace(drive_root=tmp_path)
payload = json.loads(_wait_for_tasks(ctx, ["harnesskid", "nativekid"], timeout_sec=0))
assert payload["tasks"]["harnesskid"]["execution_evidence"] == {
"dispatch_executor": "harness",
"actual_substrate": "native_only",
"delegated_runs_started": 0,
"delegated_runs_succeeded": 0,
}
# A native child with no custody evidence stays compact — no evidence block.
assert "execution_evidence" not in payload["tasks"]["nativekid"]
def test_wait_for_tasks_projection_marks_unreadable_evidence(tmp_path):
# v6.94.0 landing-gate scope fix: unreadable custody evidence means the
# counts are UNKNOWN — the projection carries ONLY dispatch_executor and
# the typed evidence_read_failed marker. Emitting the raw zeros beside the
# marker fabricated a "no runs" receipt for a log that was never read; the
# substrate claim is likewise dropped even when the stored record carries
# one (same omission rule subagents.envelope_from_task applies).
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
from ouroboros.tools.control import _wait_for_tasks
write_task_result(
tmp_path, "blindkid", STATUS_COMPLETED, result="done",
effective_executor="harness", executor_route="codex",
actual_substrate="native_only",
subagent_envelope={
"actual_substrate": "native_only",
"execution_evidence": {
"delegated_runs_started": 0, "delegated_runs_succeeded": 0,
"evidence_read_failed": True,
},
},
)
ctx = SimpleNamespace(drive_root=tmp_path)
payload = json.loads(_wait_for_tasks(ctx, ["blindkid"], timeout_sec=0))
assert payload["tasks"]["blindkid"]["execution_evidence"] == {
"dispatch_executor": "harness",
"evidence_read_failed": True,
}
def test_wait_for_tasks_projection_omits_counts_without_envelope_evidence(tmp_path):
# 6c03c24e corrective wave (LOW b): a stored harness child with NO envelope
# evidence at all (pre-6.94 records) must not read as a zero-run receipt —
# absence means "no evidence yet", so no counts and no substrate claim.
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
from ouroboros.tools.control import _wait_for_tasks
write_task_result(
tmp_path, "oldkid", STATUS_COMPLETED, result="done",
effective_executor="harness", executor_route="codex",
)
ctx = SimpleNamespace(drive_root=tmp_path)
payload = json.loads(_wait_for_tasks(ctx, ["oldkid"], timeout_sec=0))
assert payload["tasks"]["oldkid"]["execution_evidence"] == {
"dispatch_executor": "harness",
}
def test_wait_for_tasks_any_terminal_early_return_projects_pending_child(tmp_path):
from ouroboros.task_results import STATUS_COMPLETED, STATUS_SCHEDULED, write_task_result
from ouroboros.tools.control import _wait_for_tasks

View file

@ -68,6 +68,43 @@ def test_capabilities_sets_are_frozensets():
assert isinstance(obj, frozenset), f"{name} must be a frozenset"
def test_child_profiles_are_subsets_of_workspace_envelope():
"""Both delegated-child tool profiles must be subsets of _WORKSPACE_ALLOWED_TOOLS.
Guards against a future tool silently diverging between the lists: the
registry AND-intersects the workspace envelope with the child profiles, so a
profile tool missing from the envelope is invisible exactly where children
are spawned (the 2026-08-10 saga: delegate_start/wait/cancel and send_photo
were profile-visible but workspace-hidden, so "nanny" children instructed to
delegate physically could not). While these subsets hold, the intersection
is vacuous for children.
"""
from ouroboros.tool_capabilities import (
ACTING_SUBAGENT_TOOL_NAMES,
LOCAL_READONLY_SUBAGENT_TOOL_NAMES,
)
from ouroboros.tools.registry import _WORKSPACE_ALLOWED_TOOLS
assert LOCAL_READONLY_SUBAGENT_TOOL_NAMES <= _WORKSPACE_ALLOWED_TOOLS, (
f"read-only child tools missing from the workspace envelope: "
f"{sorted(LOCAL_READONLY_SUBAGENT_TOOL_NAMES - _WORKSPACE_ALLOWED_TOOLS)}"
)
assert ACTING_SUBAGENT_TOOL_NAMES <= _WORKSPACE_ALLOWED_TOOLS, (
f"acting child tools missing from the workspace envelope: "
f"{sorted(ACTING_SUBAGENT_TOOL_NAMES - _WORKSPACE_ALLOWED_TOOLS)}"
)
def test_workspace_envelope_carries_delegation_and_media_tools():
"""The 2026-08-10 additions themselves: workspace roots can delegate and send media."""
from ouroboros.tools.registry import _WORKSPACE_ALLOWED_TOOLS
assert {
"delegate_start", "delegate_wait", "delegate_cancel",
"switch_model", "send_photo", "send_video", "send_file",
} <= _WORKSPACE_ALLOWED_TOOLS
def test_frozen_registry_includes_pr_integration_tools(tmp_path, monkeypatch):
import sys
from ouroboros.tools.registry import ToolRegistry
@ -1482,6 +1519,140 @@ def test_discovery_uses_ssot_not_registry_core_names():
)
def test_enable_tools_distinguishes_policy_hidden_from_missing(tmp_path):
"""F3 (2026-08-10 saga): a registered tool filtered by policy must answer
'hidden by policy: <reason>', not the same 'Not found' as a typo'd name."""
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools import tool_discovery as td
from ouroboros.tools.registry import ToolContext, ToolRegistry
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
registry.set_context(
ToolContext(
repo_dir=tmp_path,
drive_root=tmp_path,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
td.set_registry(registry)
out = td._enable_tools(registry._ctx, tools="write_file, definitely_not_a_tool")
assert "Hidden by policy" in out
assert "write_file — hidden by the read-only subagent profile" in out
assert "❌ Not found: definitely_not_a_tool" in out
assert "write_file" not in out.split("Not found")[-1]
# Workspace envelope reason: a workspace task asking for a root-only tool.
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True, exist_ok=True)
ws_registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
ws_registry.set_context(
ToolContext(
repo_dir=system_repo, drive_root=data,
workspace_root=workspace, workspace_mode="external",
)
)
td.set_registry(ws_registry)
out = td._enable_tools(ws_registry._ctx, tools="commit_reviewed")
assert "commit_reviewed — hidden by the workspace tool envelope" in out
def test_policy_hidden_reason_pins_get_schema_by_name(tmp_path):
"""Drift pin (adversarial review of 9e59b05d, finding 1): policy_hidden_reason
promises "same predicates, same order" as get_schema_by_name. Enforce the
XOR invariant for every registered entry, in every context variant, a tool
is either visible (schema, no reason) or policy-hidden (no schema, reason).
A predicate added to one method but not the other breaks this immediately."""
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True, exist_ok=True)
def ctx_variants():
yield "plain", ToolContext(repo_dir=system_repo, drive_root=data)
yield "workspace", ToolContext(
repo_dir=system_repo, drive_root=data,
workspace_root=workspace, workspace_mode="external",
)
yield "readonly_child", ToolContext(
repo_dir=system_repo, drive_root=data,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
yield "acting_child", ToolContext(
repo_dir=system_repo, drive_root=data,
task_constraint=TaskConstraint(
mode="acting_subagent", allow_enable=False, surface="external_workspace",
),
)
ephemeral = ToolContext(repo_dir=system_repo, drive_root=data)
ephemeral.is_ephemeral_turn = True
yield "ephemeral", ephemeral
disabled = ToolContext(repo_dir=system_repo, drive_root=data)
disabled.task_contract = {"disabled_tools": ["write_file", "delegate_start"]}
yield "contract_disabled", disabled
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
for label, ctx in ctx_variants():
registry.set_context(ctx)
drift = []
for name in list(registry._entries):
schema = registry.get_schema_by_name(name)
reason = registry.policy_hidden_reason(name)
if (schema is None) != (reason is not None):
drift.append((name, schema is not None, reason))
assert not drift, f"policy_hidden_reason drifted from get_schema_by_name in ctx={label}: {drift}"
def test_policy_hidden_reason_covers_contract_disabled_unregistered_names(tmp_path):
"""ADDENDUM 4 (2026-08-10 amendments): the declarative contract policy applies
across ALL discovery sources, so a contract-disabled extension/MCP name (not
in ``_entries``) must answer with the disabled reason instead of "not found"
the contract check precedes the registration check, mirroring
get_schema_by_name's order. Unknown un-disabled names still answer None."""
from ouroboros.tools.registry import ToolContext, ToolRegistry
system_repo, data = tmp_path / "system", tmp_path / "data"
for path in (system_repo, data):
path.mkdir(parents=True, exist_ok=True)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
ctx = ToolContext(repo_dir=system_repo, drive_root=data)
ctx.task_contract = {"disabled_tools": ["someext_generate", "write_file"]}
registry.set_context(ctx)
assert "someext_generate" not in registry._entries # an extension-shaped name
assert registry.policy_hidden_reason("someext_generate") == (
"disabled by this task's contract (disabled_tools)"
)
assert registry.policy_hidden_reason("write_file") == (
"disabled by this task's contract (disabled_tools)"
)
assert registry.policy_hidden_reason("no_such_tool_anywhere") is None
assert registry.policy_hidden_reason("") is None
def test_enable_tools_hidden_label_is_shared_between_surfaces():
"""Drift pin (adversarial review of 9e59b05d, finding 5): the hidden-vs-missing
classification exists on TWO enable_tools surfaces (tool_discovery and the
loop's override). Pin both to policy_hidden_reason and the identical label so
the surfaces cannot silently diverge in honesty wording."""
import ouroboros.loop as loop_mod
import ouroboros.tools.tool_discovery as td
label = "🚫 Hidden by policy (the tool exists but this task cannot use it)"
loop_src = inspect.getsource(loop_mod)
td_src = inspect.getsource(td)
assert label in loop_src, "loop enable_tools override lost the shared hidden-by-policy label"
assert label in td_src, "tool_discovery lost the shared hidden-by-policy label"
assert loop_src.count("policy_hidden_reason(") >= 1
assert td_src.count("policy_hidden_reason(") >= 1
def test_discovery_path_consistent_with_policy():
"""list_available_tools must return the same non-core set as tool_policy.list_non_core_tools."""
from ouroboros.tools.registry import ToolRegistry

View file

@ -165,9 +165,19 @@
* path; no chip is drawn.
* @property {Object=} execution_evidence
* The completion-seam EVIDENCE the route decision is reconciled against:
* {delegated_runs_started, delegated_runs_settled, subscription_cost_usd,
* harness_models}. Terminal frames only; absent = "no evidence yet",
* never "ran natively".
* {delegated_runs_started, delegated_runs_settled, delegated_runs_succeeded,
* delegated_run_failure_states, evidence_read_failed, subscription_cost_usd,
* subscription_cost_estimated, harness_models}.
* Terminal frames only; absent = "no evidence yet", never "ran natively".
* `evidence_read_failed: true` = the custody log exists but could not be
* read zero counts are then UNKNOWN, never a "no run" receipt.
* @property {string=} actual_substrate
* The FACT beside the executor_route plan, derived from custody evidence
* ONLY (never usage/rounds): "harness_used" (>=1 delegated run succeeded) |
* "harness_attempted" (>=1 started, none succeeded) | "native_only" (none
* started). Always rides beside the raw execution_evidence counts. Terminal
* frames only; absent = no substrate claim (running, no evidence recorded,
* or unreadable evidence unknown is never classified).
* @property {string=} model
* @property {string=} task_group_id
* @property {string=} task_event
@ -703,4 +713,4 @@
* @property {?boolean} check_ok
*/
export const GATEWAY_CONTRACT_VERSION = '6.93.1';
export const GATEWAY_CONTRACT_VERSION = '6.94.0';

View file

@ -121,6 +121,17 @@ export function executorChip(evt) {
}
const started = Number(evidence.delegated_runs_started || 0);
const settled = Number(evidence.delegated_runs_settled || 0);
if (!started && evidence.evidence_read_failed) {
// The custody log EXISTS but could not be read: the zero counts above
// are UNKNOWN, not an established fact — rendering them as "no run
// recorded" would issue a receipt nothing verified (sol finding,
// b49f8192 wave).
return {
...base,
label: `${name} (evidence unavailable)`,
title: `The ${name} route was assigned, but the delegated-run evidence could not be read — whether a run happened is unknown, not "none"`,
};
}
if (!started) {
return {
...base,

View file

@ -1,6 +1,6 @@
{
"name": "ouroboros-web",
"version": "6.93.1",
"version": "6.94.0",
"private": true,
"type": "module",
"description": "Ouroboros browser UI package boundary",

View file

@ -248,6 +248,21 @@ test('the chip is layered truth: decision before evidence, receipt only from evi
});
assert.match(unused.label, /no run recorded/);
assert.match(unused.title, /no durable record of a delegated run/);
// Unreadable custody evidence is UNKNOWN, never a "no run" receipt: the
// zero counts ride evidence_read_failed and must not render as "recorded".
const unreadable = executorChip({
executor_route: 'claude',
execution_evidence: {
delegated_runs_started: 0, delegated_runs_settled: 0,
evidence_read_failed: true,
subscription_cost_usd: null, harness_models: [],
},
});
assert.match(unreadable.label, /evidence unavailable/);
assert.match(unreadable.title, /could not be read/);
assert.match(unreadable.title, /unknown/);
assert.doesNotMatch(unreadable.label, /no run recorded/);
// Zero custody rows prove neither non-execution nor the API path
// (started_uncustodied exists) — the title must assert NEITHER.
assert.doesNotMatch(unused.title, /natively|ran on the API/);