mirror of
https://github.com/razzant/ouroboros.git
synced 2026-08-26 03:21:42 +00:00
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:
parent
628de71ec4
commit
cccc758f6b
28 changed files with 1590 additions and 83 deletions
File diff suppressed because one or more lines are too long
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
6.93.1
|
||||
6.94.0
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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`)?
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"] = (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
169
tests/test_delegate_preflight.py
Normal file
169
tests/test_delegate_preflight.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
79
tests/test_promoted_task_toolset.py
Normal file
79
tests/test_promoted_task_toolset.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue