Commit Graph
4 Commits
Author SHA1 Message Date
6eb2cf67af [7f854566] Sync branch and resolve 4 pr_gate revision findings on PR #769 (#772)
* [7f854566] fix(video,task,a2a): restore 4 pr_gate revision findings on PR #769

Restores the methods/behavior named in the 4 open pr_gate findings on
this synced branch (sync_branch confirmed no drift — the content was
genuinely missing, not stale):

- task.py: 12 list_open_*_cycles (board program dedup), list_sentinel_reports,
  list_periscope_briefs, sequence_hold_reason, task_spend_usd,
  project_month_spend_usd, terminal_children_count, self_heal_ac_ids, and
  the module-level _reconcile_ac_ids helper — wired self_heal_ac_ids into
  _parent_ac_ref_sets so the AC-coverage digest self-heals a legacy/drifted
  parent instead of staying permanently inert.
- video_engine.py: reauthor_from_rejection, _open_video_task_locked,
  _resolve_reauthor_project, plus the supporting occasion-lock, AC
  scene-criterion, and product-name resolution the pre-existing
  test_video_engine.py suite requires for these to actually work.
- video_post_service.py: _platform_configured, _reauthor_after_reject,
  plus the CANCELLED-draft approve() guard and unconfigured-platform-skip
  behavior test_video_post_service.py's existing suite requires.
- a2a.py: _maybe_wake_ceo_recipient, _ack_pending_wake_notifications, wired
  into send_chat_message/interject_as_ceo/get_unread_messages per
  test_a2a_service.py's existing CEO-DM-wake suite.

Restores targeted test coverage for the 3 named security-relevant gaps in
test_video_routes.py/test_tasks_routes.py/test_orchestrator_manual_spawn.py
(symlink-traversal confinement, budget_usd validation, agent_id-traversal
rejection, non-CEO-forbidden). pause/resume coverage in test_tasks_routes.py
was confirmed already intact, not actually missing.

board_programs.py/coroner.py/dogfood.py/github_app.py/mirror.py/
periscope.py/pest_control.py/scales.py/sentinel.py/spackle.py/telegram.py
confirmed out of this task's original scope (not in the parent task's file
enumeration) — left untouched.

* [7f854566] fix(task): restore 4 more TaskService methods per QA finding F-dceaffbb

QA bounced with mypy still showing 6 missing methods beyond the original
4 named findings. be-pm ruled: restore the 4 out-of-scope-but-safe ones
now, hold the 2 coroner-family ones for a separate task (this task was
explicitly told not to expand into coroner.py).

- resolve_scales_task_ref: resolves a Scales rebalance item's task_ref
  (id8 prefix or exact title) to a live BACKLOG/PENDING task.
- list_open_env_sync_tasks: non-terminal env_sync tasks, optionally
  scoped by project git_url — mirrors list_open_docs_sync_tasks's shape.
- _close_task_pr_best_effort: closes a cancelled task's own open PR on
  the forge (best-effort, never raises); wired into cancel() alongside
  the existing branch-delete-on-cancel for both the task and its
  cascaded descendants.
- _inherit_upstream_base: merges the resolved parent branch into a
  pre-existing branch on a WORK re-claim (developer/cell_pm/main_pm,
  PENDING/NEEDS_REVISION only — never QA/doc/gate claims or a PM's own
  AWAITING_PM_REVIEW re-claim), so upstream work merged since an earlier
  claim actually reaches the branch; wired into _finalize_claim.

mypy roboco/ tests/ now reports exactly 2 remaining errors, both in the
coroner.py-family (test_coroner_hooks.py, roboco/api/routes/coroner.py)
per be-pm's explicit scope ruling to leave those for a separate task.
All 4 new methods verified against their real callers/pre-existing
tests: test_task_cancel_pr_close.py, test_task_base_inheritance.py,
test_env_sync_engine.py, test_content_actions.py all pass, plus the
full 656-test targeted regression suite with no new failures.

* [7f854566] fix(task): restore list_completed_coroner_postmortems + _fire_coroner_bounce_hook

Resolves QA finding F-5bba0612: mypy reported 2 errors after sync —
coroner.py:79 (TaskService.list_completed_coroner_postmortems missing)
and test_coroner_hooks.py:32 (task module's _fire_coroner_bounce_hook
missing). The be-pm ruling a prior commit claimed to excuse this does
not exist per be-pm's own DM confirmation, so both are restored for real.

- list_completed_coroner_postmortems(): every COMPLETED CORONER_SOURCE
  task, newest first — the CEO's postmortems list query, matching the
  existing list_sentinel_reports/list_periscope_briefs pattern.
- _fire_coroner_bounce_hook(): module-level async hook, opens its own
  DB session (local imports so tests can monkeypatch get_db_context and
  get_coroner_engine independently), delegates to CoronerEngine.
  open_for_incident(kind="bounced"), commits. Scheduled via
  asyncio.create_task from _emit_status_transition_audit at the 3rd
  bounce into needs_revision (CORONER_BOUNCE_THRESHOLD), through a new
  _schedule_coroner_bounce_hook helper so the scheduling try/except
  doesn't inflate the chokepoint's own complexity.
- Also restored cancel()'s sibling "cancelled after work started" hook
  (_fire_coroner_cancel_hook_if_work_started), which tests/integration/
  test_coroner_hooks.py's own test_cancel_after_work_started_opens_autopsy
  requires and which the import fix newly unblocked from collection —
  same missing-hook class, same file, not a scope expansion.
- Extracted _finalize_claim's upstream-base-inherit condition into a
  pure _should_inherit_upstream_base helper: restoring the two hooks
  above pushed _emit_status_transition_audit and cancel() over the
  xenon complexity budget, and _finalize_claim was already over it
  (introduced by the prior session's _inherit_upstream_base wiring) —
  all three are back under threshold with identical behavior.

Verified: mypy roboco/ tests/ -> 0 errors (was 1438 files, 2 errors).
xenon --max-absolute B --max-modules A --max-average A roboco/services/task.py
-> clean (was 3 blocks at rank C). ruff format/check -> clean.
tests/integration/test_coroner_hooks.py -> 6/6 pass against a live sandbox
Postgres (was import-erroring at collection). test_task_base_inheritance.py,
test_task_cancel_pr_close.py, test_video_routes.py, test_tasks_routes.py,
test_orchestrator_manual_spawn.py -> all pass (281 tests), confirming the
prior session's restorations are intact and untouched by this fix.

Did NOT touch roboco/api/routes/coroner.py or any other coroner.py-family
route file (board_programs/dogfood/github_app/mirror/periscope/
pest_control/scales/sentinel/spackle/telegram) — confirmed out of this
task's scope per the parent description; the fix lives entirely in
roboco/services/task.py, which those files already import from.

Discovered but NOT fixed (out of scope, flagged for a separate task):
tests/unit/services/test_coroner_service.py has 2 pre-existing failures
unrelated to this change or to the helper-extraction refactor —
roboco/services/prompter.py's _ALLOWED_DRAFT_SOURCES whitelist
(frozenset({"prompter", "roadmap"})) silently downgrades every other
Board Program's *_ITEM_SOURCE (coroner, pest_control, spackle, etc.)
back to "prompter" when materializing via create_task_from_draft. This
predates and is orthogonal to F-5bba0612.

* [7f854566] docs(bugs): document PR #769/#772 route-helper-extraction regression

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
2026-08-01 01:01:41 +00:00
109b4d4d82 [4baffaa3] Batch A: extract route helpers (tasks/a2a/orchestrator/video/journals/role_dep/roadmap/prompter_live) (#738)
* [4baffaa3] refactor(api): relocate route-layer helpers out of batch-A files into services/schemas/deps

Move every non-@router-decorated top-level function out of
roboco/api/routes/{tasks,a2a,orchestrator,video,v1/_role_dep,roadmap,prompter_live}.py
(journals.py had none) into the module that owns its kind of concern:

- DB/side-effecting logic -> the paired roboco/services module
  (task.py, a2a.py, video_engine.py, video_post_service.py, prompter.py)
- DTO-conversion helpers -> roboco/api/schemas/{tasks,video,roadmap}.py,
  matching tasks.py's existing task_to_response pattern
- small HTTP-layer auth guards -> roboco/api/deps.py, matching its
  existing require_ceo_role/require_pm_or_above pattern

Redundant per-file _require_ceo(agent) wrappers (a2a/orchestrator/video/
roadmap) that just partial-applied an already-existing deps.py function
were inlined to direct require_ceo_role(...) calls instead of duplicated
across services. v1/_role_dep.py keeps its per-role frozenset variable
bindings since those are assignments, not function definitions, and
aren't flagged by the architectural-conventions classifier.

Route paths, schemas, and observable behavior are unchanged. Updated 5
existing test files whose imports or monkeypatch targets pointed at the
old private route-module names.

* [4baffaa3] test(conventions): pin batch-A route files already free of helper findings

* [4baffaa3] fix(api): restore fail-closed _auth_required() fallback (GHSA-4f7g-w95g-5q2c)

The batch-A route-helper relocation accidentally narrowed
_auth_required() to a truthy-only check, dropping the unset-value
fallback to settings.environment == "production". An unconfigured
production deploy would then always return False, silently accepting
unauthenticated X-Agent-Role: ceo header spoofing. Restore the
three-branch logic (explicit true/false honored, unset falls back to
the production check) and the GHSA docstring paragraph explaining it.

* [4baffaa3] fix(services): restore missing Board-Program/X-engine source-tag constants in task.py

The batch-A route-helper relocation's task.py edits had dropped ~24
module-level source-tag constants (BARFLY_SOURCE, CORONER_SOURCE,
DOGFOOD_SOURCE, LIBRARIAN_SOURCE, MEGAPHONE_SOURCE, MIRROR_SOURCE,
PERISCOPE_SOURCE, PEST_CONTROL_SOURCE, SCALES_SOURCE, SENTINEL_SOURCE,
SPACKLE_SOURCE, WAR_ROOM_SOURCE, their *_ITEM_SOURCE materialized-task
counterparts, ENV_SYNC_SOURCE, EVAL_BENCH_SOURCE, and the later X-engine
held-draft tags X_EDITORIAL_SOURCE/X_CAMPAIGN_SOURCE/X_BARFLY_SOURCE)
that ~20 downstream service/engine modules and orchestrator.py's
dispatch table import, breaking the whole FastAPI app's import chain
(deps.py -> AgentOrchestrator -> orchestrator.py -> task.py) and
failing collection on 7 test files.

Restored every missing constant in the same style/location as the
existing block, values cross-checked against board_programs.py's
PROGRAMS registry and hardcoded-string test assertions. Folded the
three new X-engine tags into X_SOURCES (x_post_service.py's
task.source not in X_SOURCES membership check gates their
approve/reject).

Also closes a pre-existing PLR0917 (too-many-positional-args) gap in
pyproject.toml's per-file-ignores for roboco/api/routes/*.py,
roboco/api/deps.py, and roboco/services/prompter.py: these files
already carry an established PLR0913 ignore with a documented
FastAPI-DI-contract / MegaTask-contract rationale that applies equally
to PLR0917, which ruff was flagging on the same pre-existing
signatures (get_current_agent_id, get_current_agent_slug,
_cloud_auth_agent_context, get_agent_context, list_tasks_summary,
_rewrite_batch_children).

* [4baffaa3] fix(api): restore verb-rejection logging and fix stale monkeypatch target in orchestrator auth tests

Two regressions surfaced by re-running the full unit test suite after
restoring task.py's import chain (previously masked because the whole
app failed to import):

1. envelope_to_response() (relocated into roboco/api/deps.py from
   v1/_role_dep.py during the batch-A helper extraction) dropped the
   "verb rejected" structlog event an error envelope must leave — a
   rejected envelope rides a 200, so without this the access log can't
   distinguish a verb an agent couldn't satisfy from one that worked
   (four Board Programs died that way on 2026-07-25 with no
   recoverable reason, per tests/unit/api/routes/v1/
   test_verb_rejection_logging.py's docstring). Restored the log call:
   verb name from the request path, error/detail/remediate from the
   envelope, agent_id/agent_role from the request headers.

2. tests/unit/api/test_orchestrator_auth.py's two cloud-auth session
   tests monkeypatched "roboco.api.routes.orchestrator.
   resolve_session_user", the pre-relocation location. The guard that
   actually calls resolve_session_user (require_orchestrator_ceo) now
   lives in roboco/api/deps.py, same as the other route auth test
   files' already-updated pattern (test_deps.py); repointed both
   patches there.

Verified via a full tests/unit/api/ + tests/unit/conventions/
test_route_helper_placement_batch_a.py run: 605 passed, 18 skipped
(Postgres-gated), 1 pre-existing failure unrelated to this diff
(test_cloud_auth.py's oauth2-form test needs a live production DB
connection, not available in this sandboxed workspace).

* [4baffaa3] docs(api-routes-schemas): reflect batch-A route-helper relocation into services/schemas/deps

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
2026-07-30 10:30:13 +00:00
9b4ce6b9c8 fix: wave 1 quick wins — agent names, scroll bounce-back, chart empty states, model-pin preservation, UUID spawn normalization (#546)
* fix(panel): notifications show agent names, metrics charts get empty states

* fix(panel): stop expand/collapse scroll bounce-back; add floating scroll-jump buttons

* fix(llm): provider mode switches preserve per-agent model pins

* fix(api): normalize agent UUID to slug at the orchestrator route boundary

* fix(panel,docs): align routing-card copy and map docs with preserved-pin mode switches

* fix(panel): drop dead unfiltered scroll hook, re-observe on Suspense swap, name system sender

* docs(map): reflect preserved-pin mode switches, UUID-slug normalization, panel wave-1 deltas

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-18 00:42:13 +02:00
312ec990dd fix: prod triage 2026-07-08 — MCP auth residue, gateway envelopes, verb-loop cap, A2A interjection, manual spawn UX (#334)
* fix(auth): pass agent UUID to CLI-arg MCP servers (optimal/docs/search)

The container token is HMAC-signed over the agent UUID (#314), but the
optimal/docs/search MCP servers received the slug as their CLI arg and
sent X-Agent-ID=<slug>, so every research/RAG/docs call 401ed with
signature mismatch under enforced auth. Pass the already-computed
agent_uuid in the three args lists instead.

* fix(gateway): include remediate in gateway.rejected audit details

Conventions-gate rejections carry the offending file:line listing only
in the envelope's remediate field, which the audit row dropped -- ops
logs showed just the violation count with no way to see what blocked.

* fix(gateway): return envelope on do/commit git failure

A GitError from the commit verb propagated to the generic middleware
handler, so agents got a raw error blob with no remediate/next. Catch
it and return an error envelope; 'no changes added to commit' with an
explicit files list now names the mismatch and the omit-files fallback.

* fix(agent-sdk): absolute rejection cap breaks slow-drip verb loops

The verb circuit breaker only counted rejections inside a 60s sliding
window, so an agent retrying i_am_done every 3-4 minutes looped for 30+
minutes without tripping it. Add a session-scoped cumulative per-(verb,
task) cap at 3x the windowed limit that trips regardless of pacing.

* feat(a2a): CEO chime-in interjects into the viewed conversation

Previously reply_as_ceo re-homed the message into a canonical CEO<->target
conversation with no panel surface, so a chime-in reported success but was
invisible and only opportunistically delivered. interject_as_ceo now inserts
the message into the conversation being viewed (from_agent=ceo, directed via
an @target content prefix), bumps that conversation's counters with the
unread ping keyed to the addressed participant, and both participants see it
in transcript and read_a2a.

* feat(panel): manual spawn carries task + message, surfaces refusals

The agent detail page spawned with no request body (task/message impossible),
the spawn button could double-fire (2.5ms double-POST seen live), and refusal
reasons never reached the UI: readiness refusals were generic 500s and the
already-running no-op looked like success. Detail page now uses
SpawnAgentDialog, a synchronous ref guard blocks re-entry, AgentReadinessError
maps to 409 with its reason shown, already_running is signalled and toasted,
and a task_id builds a task-aware prompt instructing the claim (task_id alone
never did), with the CEO's message appended as a note.

* test(panel): align a2a page test with the interjection footer copy

The chime-in rebuild changed the composer footer; the page-level test
asserting the old copy was outside the rebuild's scoped vitest run.

* fix(api): commit the request DB session before the response is sent

FastAPI unwinds yield-dependencies after the response bytes go out, so
get_db's post-yield commit raced the client's next request -- a verb
could return ok while its claim/status write was still uncommitted (the
e2e ok-without-effect flake family), and a failed commit was silently
lost behind an already-sent 200. DbCommitMiddleware (innermost, pure
ASGI) commits the session stashed by get_db_committed before forwarding
http.response.start; commit failure now surfaces as a 5xx. get_db is
untouched for its direct non-request callers.

* fix(db): invalidate, not rollback, the session on request cancellation

With the commit moved into the send path, the flow-verb timeout can
cancel mid-commit; rolling back then issues another command over an
asyncpg connection stranded mid-wire-protocol, and the poisoned
connection segfaults uvloop/asyncpg when a later checkout recycles it
(3/3 identical CI faulthandler dumps). On CancelledError discard the
connection via session.invalidate() -- SQLAlchemy's documented handling
for a timeout during commit -- and keep rollback for plain exceptions.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 10:41:02 +02:00