Disciplines · Audits

Residual Audit — Agentic AI Studio (V1/features.md 4523–4942)

(apps/oshun/bff/src/agentic/runs-route.ts:148) runs a client-submitted tool plan through runGuardedToolPlan → dispatchGuardedToolCall → admitToolCall (libs/oshun/agentic-studio/src/runs/{orchestrator,dispatcher,executor}.ts), and

3sections17 minread

On this page

Date: 2026-06-11. Read-only static audit of the residual AFTER the 57-task 2026-06-10 ground-truth backlog landed (A5 server-side kill-switch authority, C9 envelope persistence + console bridge). Evidence = current code only; every delegation chain below was read to the bottom.

Verdict on the central question — "does a REAL executor now exist?"#

Yes, narrowly, and it is honest. POST /v1/agentic/runs/execute (apps/oshun/bff/src/agentic/runs-route.ts:148) runs a client-submitted tool plan through runGuardedToolPlandispatchGuardedToolCalladmitToolCall (libs/oshun/agentic-studio/src/runs/{orchestrator,dispatcher,executor}.ts), and the tools are real bounded mutations over real stores (apps/oshun/bff/src/agentic/studio-tool-catalog.ts wired in server.ts:900–934: workspace-state read/write, real enqueueGenerationJob, real outputGalleryAdminStore.applyBulkAction). Kill-switch admission genuinely prevents tool execution (dispatcher.ts:55–67 runs the tool IFF admitted). Nothing fabricates a tool result; unconfigured tools 503 fail-closed. No result-faking stub was found anywhere on the execution path — the adversarial grep over the lib + BFF agentic files returned zero actionable hits.

But the executor is an API-only island: no product UI calls it (only apps/oshun/web/e2e/agentic-pipeline-invocation.spec.ts), it is disconnected from the approval-gated customer lifecycle, budgets remain client-authority, and the executed envelope records no transcript. The findings below are that residual.


Severity: P0-SEC

Evidence:

  • apps/oshun/bff/src/agentic/runs-route.ts:146,148–151 — /v1/agentic/runs/execute is gated by createAuthPreHandler() only; no scope/role/tier check.
  • apps/oshun/bff/src/server.ts:917–930 — the injected retagOutputs dep calls outputGalleryAdminStore.applyBulkAction with authorization: { operatorId: input.operatorId, tier: 'operator-admin' } — the tier is fabricated server-side for whoever the caller is.
  • libs/isis/output-gallery/src/bulk-actions.ts:56–65 — re-tag requires curated-creator | aaa-creator | operator-admin; a plain customer has none.
  • Contrast: the real admin route apps/oshun/bff/src/routes/admin-isis-output-gallery-actions.ts:54–58,93,116 requires admin:*/admin:studio/admin:workspace:isis before passing the same tier: 'operator-admin'.

Spec promise: features.md ~4612–4615 (operator action authorization tests), ~4699 (per-call authorization checks under "Tool isolation").

What the code actually does: any authenticated user POSTs a one-step plan {toolId:'output-gallery.retag', ...} with payload outputIds/addTags/removeTags and mutates the admin output gallery as a synthetic operator-admin, bypassing the tier gate the dedicated admin route enforces. (generation.enqueue-job via the same route is no worse than the existing unauthenticated POST /v1/generation/jobs, generation/jobs-route.ts:204 — that hole belongs to the generation slice.)

Fix sketch: gate output-gallery.retag (or the whole execute route per-tool) on the caller's real scopes; derive the gallery tier from authContext.scopes instead of hardcoding operator-admin.


F2. Operator kill switches are evadable by request shaping — A5 fixed switch storage, not switch targeting#

Severity: P0-SEC

Evidence:

  • apps/oshun/bff/src/agentic/runs-route.ts:197–209 — the ExecutionTarget the switches match against is built from client-supplied fields: family: step.family ?? 'default', providerId: step.providerId ?? 'default', region: step.region ?? 'default', agentId: body.rootAgentId (free text, never validated against any registry).
  • runs-route.ts:176 — tenantId = authContext.tenantId ?? body.tenantId: when the principal carries no tenant claim, the body picks the tenant.
  • libs/oshun/agentic-studio/src/budgets/kill-switch.ts:57–69 — matching is plain string equality (s.scopeRef === target.family, etc.).

Spec promise: features.md ~4670–4673 ("Kill switches — per agent, per agent family, per tenant, per provider, per region") and ~4938 ("platform-wide tool kill propagates ≤ 5 s to all in-flight runs").

What the code actually does: an operator arming agent_family: isis (dev seed ks-agent-isis) stops only callers who volunteer family: 'isis' in their plan step; omit the field and the run sails through. Per-agent switches match a caller-chosen rootAgentId string; tenant switches are evadable by any principal without a tenant claim. Only global scope is server-authoritative end to end.

Fix sketch: resolve family/providerId/region server-side from the tool registry + agent registry entry for rootAgentId (reject unregistered agents); require a server-resolved tenant; treat client-supplied target attributes as untrusted.


F3. Any authenticated user can overwrite any stored AgentRun (including other users') via recordExecutedRun upsert#

Severity: P0-SEC

Evidence:

  • apps/oshun/bff/src/agentic/runs-route.ts:160–171,234–243 — runId is client-supplied free text; after execution the route unconditionally calls agentRunLifecycleStore.recordExecutedRun(...).
  • apps/oshun/bff/src/agentic/run-lifecycle-store.ts:147–155 — recordExecutedRun is a blind upsert by runId: "a re-execution of the same runId replaces the prior envelope." No ownership/tenant check against the existing record.
  • run-lifecycle-store.ts:169–175 — ownership is inputs.invokedByUserId; the new envelope's invoker is the overwriting caller (runs-route.ts buildRun:115), so the original owner's run vanishes from their GET /v1/agentic/runs history.

Spec promise: features.md ~4607–4608 ("Immutable per-run audit events").

What the code actually does: user B can POST execute with user A's runId (runIds are visible in the operator console and predictable in shape) and replace A's persisted envelope + approval history wholesale — destroying the audit record and also laundering an awaiting_approval run into a completed one.

Fix sketch: in recordExecutedRun, refuse (409) when an existing run's invokedByUserId/tenantId differs from the caller; better, have the server mint execution runIds and treat lifecycle runIds as approval-checked input (see F4).


F4. The approval-gated lifecycle and the executor are two disconnected machines — gates are not enforced at execution time, and approved runs never execute#

Severity: P0-STRUCT

Evidence:

  • apps/oshun/bff/src/agentic/run-lifecycle-store.ts:195–243 — approve() transitions awaiting_approval → 'queued' ("released to the executor" / "Released to Lilith's composer"). Grep finds no consumer of queued runs anywhere — no worker, no dispatch loop; status === 'queued' is read only for display mapping (routes/admin-agentic-operations.ts:57) and pause-ability (lib run-controls).
  • apps/oshun/bff/src/agentic/runs-route.ts:188–195 — the execute route builds a fresh envelope with status: 'running', never consulting the lifecycle store's approval state for that runId. Nothing requires a run to have passed its gate before tools fire.
  • apps/oshun/web/src/components/lilith/AreteReviewDraftRun.tsx:55 — customer copy for queued is "Released to Lilith's composer"; states running/completed (lines 56–59) are unreachable for lifecycle runs because nothing ever executes them.

Spec promise: features.md ~4590–4596 (operator-defined approval gates gating stages), ~4900–4905 (approvalRequirement pauses in awaiting_approval; "resolution gates resumption"), ~4860–4866 (arete.weekly_review_draft produces an actual draft).

What the code actually does: approval is a real, role-checked gate on a queue that goes nowhere, while actual execution runs on a parallel road with no gate at all. The flagship customer journey honestly dead-ends at "queued" forever (the component admits this in its own docstring, AreteReviewDraftRun.tsx:23–27 — honest, not a stub, but the product loop is open).

Fix sketch: make the executor consume the lifecycle store — approve() enqueues the run's plan into the guarded executor (or execute requires lifecycle.status ∈ {queued} for gated runs and stamps results back); delete the fresh-envelope path for runIds that exist in the store.


F5. Budgets and quotas have no server authority and meters are never debited — budget enforcement is effectively vacuous at runtime#

Severity: P1

Evidence:

  • apps/oshun/bff/src/agentic/runs-route.ts:87,223 — budgets: body.budgets ?? []: the client supplies every BudgetEnvelope, BudgetMeter, and requestUnits; omit the field and zero budgets apply. No server-side budget registry exists (A5's own DONE note concedes "no server budget registry exists yet"; no follow-up task was filed — this is the residual).
  • libs/oshun/agentic-studio/src/runs/orchestrator.ts:64–77 — the same static input.budgets (meters included) is re-checked at every step; nothing accumulates consumption between steps, so a 100-step plan each individually under the cap passes with unbounded cumulative cost.
  • consumeBudget has zero runtime callers in the agentic path (executor.ts:100 and dispatcher.ts:49 explicitly delegate it to "the caller"; runs-route.ts never calls it; repo-wide grep confirms only lilith/yemaya unrelated modules).
  • The admin store's per-tenant budgetUnits30d ledger (admin-agentic-operations-store.ts:355–383) is dev-seed display data — never resolved into the execute path and never updated by executed runs.

Spec promise: features.md ~4660–4677 ("Per-tenant, per-role, per-agent, per-run, and per-tool budgets and quotas", "Tests for budget enforcement").

What the code actually does: the budget engine (checkBudget verdicts, warn/grace, hard-stop kill) is real and the executor honors a verdict — but the only budgets it ever sees are ones the caller chooses to volunteer, checked against a meter the caller also supplies, and never debited.

Fix sketch: add a server budget registry (per tenant/agent/tool) resolved on every execute like resolveOperatorKillSwitches; thread consumeBudget after each executed call so meters accumulate within and across runs; persist meters.


F6. Executed runs persist with empty transcripts — toolCalls, evidence, cost ledger, and the termination audit event are all dropped#

Severity: P1

Evidence:

  • libs/oshun/agentic-studio/src/runs/dispatcher.ts:58–66 — on executed it returns admission.run (the unchanged input envelope) plus the result; nothing appends a toolCalls entry, evidence, or cost ledger line. executor.ts:167–172 likewise returns run: input.run untouched on admit.
  • apps/oshun/bff/src/agentic/runs-route.ts:234–243 — the persisted "executed envelope" therefore has toolCalls: [], evidenceTrail: [], costLedger: emptyCostLedger(), plan: [] always.
  • On termination, result.terminated.auditEvent (a real RunControlAuditEvent) is partially echoed in the HTTP response (runs-route.ts:248–254) and never storedrecordExecutedRun takes only the envelope.
  • Downstream: routes/admin-agentic-operations.ts:44,96–98 maps real runs with costUnits = 0 (band "low"), evidenceRefCount = 0, planNodeCount = 0.

Spec promise: features.md ~4563–4566 (AgentRun captures "tool calls, intermediate artifacts, evidence trails, cost ledger"), ~4607–4608 ("which tools fired"), ~4577 (replay with "full intermediate state and tool-call transcripts").

What the code actually does: C9 persisted the envelope, but the envelope was never populated — the operator console's "real runs" carry no record of what executed. Replay/time-travel is impossible by construction.

Fix sketch: in runGuardedToolPlan, append a toolCalls entry (toolId, startedAt, outcome, result ref) + cost ledger line per executed step and thread the enriched envelope forward; persist auditEvents alongside the envelope in the lifecycle store.


F7. Operator console renders every approved run as "rejected" — nonexistent field read#

Severity: P1

Evidence:

  • apps/oshun/bff/src/routes/admin-agentic-operations.ts:88–91 — latestApproval ? latestApproval.approved ? 'approved' : 'rejected' : ….
  • libs/oshun/agentic-studio/src/handoff/handoff-protocol.ts:315–322 — ApprovalDecision has verdict: 'approved' | 'rejected'; there is no approved property, so the access yields undefined → falsy → 'rejected'.
  • run-lifecycle-store.ts:234–241 pushes { verdict: 'approved', … } on approve.

Spec promise: features.md ~4599–4603 (dashboard approval-state filter fidelity).

What the code actually does: the only approval verdict the store can record is 'approved' (discard does not append a decision), yet the console maps every approved run's approvalState to rejected.

Fix sketch: latestApproval.verdict === 'approved' ? 'approved' : 'rejected'. Add a test asserting an approved lifecycle run surfaces approvalState: 'approved'.


F8. Gold sets and champion-challenger are state machines over display rows — no traffic routing, no evaluation, and in production nothing to operate on#

Severity: P1

Evidence:

  • apps/oshun/bff/src/admin/admin-agentic-operations-store.ts:115–195 — promoteGoldSet/rolloutChampionChallenger validate transitions + rationale and mutate the stored row. Nothing reads the result: repo-wide, trafficSharePct routes zero traffic, no shadow execution exists, fixtureCount/acceptanceRatePct are static numbers never produced by an evaluation harness.
  • The lib's real engines — libs/oshun/agentic-studio/src/feedback/{gold-sets, champion-challenger,drift-detection,feedback}.ts — have no importer outside the lib (module-consumption sweep, this audit).
  • admin-agentic-operations-store.ts:258–273 — production seed is empty and the routes expose no create verb (only promote/rollout on existing rows), so the prod console's gold-set and champion-challenger panels are permanently empty and the operator buttons can never have a target.

Spec promise: features.md ~4706–4720 (operator decisions promoted into versioned gold sets; champion-challenger with "parallel evaluation, statistical readiness gates"; promotion/freeze/rollback "with bound evaluation evidence").

What the code actually does: dev-seeded rows can be walked through a correct state machine; no runtime behavior anywhere changes as a consequence. Stored-config-never- enforced, and in prod, stored-config-never-exists.

Fix sketch: minimum honest V1: add create/ingest paths (operator decision → gold-set row; agent-version pair → experiment) and have the run dispatch path consult the active experiment's trafficSharePct for agent-version selection; otherwise label the panels read-only-registry and defer with a recorded V1.x note.


F9. The contract tool catalog (19 tools) has zero runtime implementations; the 4 live tools bypass it; no grant checks exist at dispatch#

Severity: P1

Evidence:

  • libs/contracts/src/agent/tools.ts:89–306 — V1_TOOL_IDS / V1_TOOL_CATALOG faithfully enumerate the spec catalog (web.fetch, sophia.ground, memory.read/write, handoff, approval.request, agent.terminate, …) with semver + dependency resolution. No runtime imports it outside contracts tests and a maya lib.
  • apps/oshun/bff/src/agentic/studio-tool-catalog.ts:18–23 — the live registry is 4 studio.*/generation.*/output-gallery.* ids, none present in the contract catalog; runs-route.ts:180–185 validates plan toolIds only against the injected registry, never the catalog.
  • Grant semantics: libs/oshun/agentic-studio/src/grants/resolver.ts + capabilities/tool-grants.ts (scoped grants, revocation cascade, queued-run recheck) have no runtime consumer; admitToolCall checks kill-switch/budget/ throttle but no grant — any authenticated caller may invoke any registered tool (see F1).

Spec promise: features.md ~4745–4800 (enumerated AgentToolCatalog "exported from libs/contracts/src/agent/tools.ts" — done — and per-tool grant records, revocation propagation, expiry; ~4884–4906 grant-scope tests).

What the code actually does: catalog and grant engines are real, tested, and unreachable; runtime authority is "tool exists in the injected map".

Fix sketch: validate plan steps against V1_TOOL_CATALOG (unknown → 400, known-but- unimplemented → 503); add a grant-resolution step to admitToolCall's input built from a server-side grant store keyed by (agentId, toolId, scope).


F10. Cross-domain pipelines: six named specs, registry, observability — no runtime registers, schedules, or executes any of them#

Severity: P1

Evidence:

  • libs/oshun/agentic-studio/src/pipelines/v1-pipelines.ts (259 lines: the six spec'd pipelines incl. ARETE_WEEKLY_REVIEW_DRAFT, VERITAS_STORY_DRAFTING) + pipeline-registry.ts + observability.ts — no importer outside the lib except libs/oshun/agent-pipelines, which is a 91-line pure re-export shim (src/pipelines/ index.ts:1–28, src/grants/resolver.ts:1–22) satisfying the spec's directory name with no added behavior.
  • No BFF route mentions pipelines beyond free-text pipelineId pass-through (routes/agentic-runs-lifecycle.ts:29 — any ≤200-char string accepted; nothing checks it against V1_PIPELINES); nextFireFor scheduling has no caller.

Spec promise: features.md ~4727–4742 (pipeline registry, fixture-bound rehearsal, tenant-scoped scheduling), ~4845–4878 (the six concrete pipelines with declared stages and gates).

What the code actually does: the only pipeline with any product presence is arete.weekly_review_draft as a string label on a lifecycle run that never executes (F4). Stage declarations, budget caps, gate declarations are inert data.

Fix sketch: at minimum validate submitted pipelineId against V1_PIPELINES and attach the declared approval gate + budget cap to the run; a pipeline-stage executor over the guarded tool plan is the real (larger) closure, likely V1.x — record it.


F11. No agent registry/catalog at runtime — agent identity is unvalidated free text everywhere#

Severity: P1

Evidence:

  • libs/oshun/agentic-studio/src/registry/{agent-registry,agent-catalog, agent-families}.ts — no importer outside the lib.
  • routes/agentic-runs-lifecycle.ts:27 + agentic/runs-route.ts:162 — rootAgentId is any non-empty string; no registry lookup, no lifecycle state, no version pin, no tool-grant inheritance, no cost class.
  • No BFF route or web/admin surface exposes a browsable agent catalog (repo-wide grep: no /v1/agents-style route; the web "agents" route is a prod-guarded fixture per C3).

Spec promise: features.md ~4538–4558 (canonical registry, browsable catalog, per-tenant views, version pins).

What the code actually does: every governance feature that should key off the registry (family kill-switch targeting F2, default grants F9, catalog UX) has nothing to key off.

Fix sketch: mount a read-only registry route over the lib's registry module with a small curated seed of the agents the product actually invokes (arete.weekly-review-composer, the studio executor agent), and make execute/submit reject unregistered ids.


F12. Throttles are never engaged on the runtime path#

Severity: P2

Evidence: libs/oshun/agentic-studio/src/runs/orchestrator.ts:72 hardcodes throttle: null for every step; runs-route.ts accepts no throttle config; the token bucket (budgets/throttle.ts acquireToken) and the executor's throttled outcome are reachable only from tests.

Spec promise: features.md ~4666–4669 (throttle/back-pressure on provider saturation).

What the code actually does: the throttled branch of the admission result is dead at runtime.

Fix sketch: accept a server-side per-tool/per-tenant throttle config (same pattern as resolveOperatorKillSwitches) and persist ThrottleState between requests.


F13. Spec pillars with engines but zero runtime: agent memory, anomaly quarantine, usage meters, modes, run controls/replay, dashboard query#

Severity: P2

Evidence (module-consumption sweep, no importer outside the lib for any of):

  • capabilities/agent-memory.ts (116 lines: inspect/export/redact/scoped-clear), capabilities/capability-audit.ts, capabilities/tool-isolation.ts
  • budgets/anomaly-detection.ts (quarantine), budgets/usage-meter.ts (customer-facing meters/grace/warnings)
  • modes/modes.ts (fast/balanced/deep/exhaustive/rehearsal — no UI affordance, no route parameter anywhere)
  • runs/run-controls.ts — pause/resume/cancel/branch/fork/retry-with-changes/ operator-step exist and are tested; only kill is invoked at runtime (via the executor). No operator route or console button drives any of them.
  • dashboard/{dashboard-query,operator-actions,run-detail-page,audit-events}.ts — the admin console (F14) uses none of them.

Spec promise: features.md ~4683–4700 (memory audit/export/redaction), ~4674–4676 (anomaly quarantine), ~4662–4665 (usage meters), ~4715–4726 (modes incl. rehearsal isolation), ~4571–4577 (pause/branch/replay).

What the code actually does: honest absence — no surface claims these exist. This is a scope gap, not a honesty gap; listed so the residual map is complete.

Fix sketch: not closable this turn as a batch; pick per-pillar. The cheapest real wins: wire pause/cancel operator actions onto the lifecycle store (run-controls is ready), and a mode field on submit that maps to budget envelopes once F5 lands.


F14. Operator console: none of the spec'd operator actions, filters, or per-run drill-down; plan-DAG and evidence panels can never show real-run data#

Severity: UX

Evidence:

  • apps/oshun/admin/src/components/AgenticOperationsPanel.tsx:280–344 — the run inventory is a read-only table: no pause/kill/escalate/override-gate/mark-reviewed/ reassign per-run or bulk actions (spec ~4604–4606), no filters by family/tenant/ band/severity/approval (spec ~4596–4599), no per-run page, no cross-run search or export (~4609–4611).
  • PlanDagSection/EvidenceTrailSection (lines 763–851) read snapshot.planSnapshots/snapshot.evidenceTrail, which only the dev seed populates (admin-agentic-operations-store.ts:481–553); recordExecutedRun writes neither (and the envelope is empty anyway, F6) — so in production these panels are permanently "No plan snapshots captured" / "No evidence entries" while real runs sit in the table above with cost 0 / evidence 0 (F6) and wrong approval badges (F7). Kill-switch arm/disarm is the one fully real operator control (and it now genuinely gates execution, modulo F2).

Spec promise: features.md ~4582–4612 (Operator Job Dashboard, Replay, and Audit).

Fix sketch: add per-run kill/pause buttons calling new lifecycle verbs (run-controls is ready, F13); client-side facet filters over the existing snapshot are an afternoon; bridge real plan/evidence rows when F6 populates the envelope.


F15. Customer/creator agent surface cohesion: one pipeline, one button, no pre-flight estimates, no progress, no output review; zero creator-side agentic actions#

Severity: UX

Evidence:

  • The entire customer/creator agentic product surface is apps/oshun/web/src/components/lilith/AreteReviewDraftRun.tsx (one hardcoded pipeline on /arete/review). No pre-flight cost/latency estimate, declared source-set/persona/tone (spec ~4634–4638); no in-progress run card with interrupt/redirect (~4639–4641 — moot while F4 holds, since nothing progresses); no output review/diff/regenerate (~4642–4646); no completion notifications surfaced (the store builds a real RunNotification on discard only, and nothing delivers it); no run-history surface beyond this one component's filtered list.
  • Creator-side: no authoring-workspace agentic actions exist anywhere (spec ~4628–4633); the studio "agents" pages (apps/oshun/web/src/app/studio/yemaya/*) are Yemaya bot-domain consoles, a different spec area, not Agentic-Studio invocation.

Spec promise: features.md ~4617–4651 (Customer- and Creator-Facing Agent Invocation).

Fix sketch: the honest near-term move is to keep the single surface but close its loop (F4) and add a completion notification + run-history read; declare the broader invocation matrix V1.x.


Explicit non-findings (checked, clean)#

  • No result-faking executor: the mandatory adversarial grep over libs/oshun/agentic-studio/src + apps/oshun/bff/src/agentic + the three routes returned zero actionable hits; tool results come from real stores; failures throw (StudioAgentToolError) rather than fabricate.
  • Fail-loud seams are correct: unconfigured tools → 503 agent_tools_not_configured; the lifecycle never auto-completes on a clock; AreteReviewDraftRun never fabricates a draft.
  • Dev seeds are dev-gated (admin-agentic-operations-store.ts:250–273, recorded in C9) — prod starts empty; not re-reported. The dev-mode mixing of seed rows with real runs in one table is a known dev aid.
  • A5's kill-switch storage authority is genuinely server-side (runs-route.ts:143–145, 215–217 read the durable admin store per request); F2 is about target attributes, a different hole.
  • admin/agentic-operations routes are properly admin-scope-gated (routes/admin-agentic-operations.ts:256–281) and mounted (app.ts:606,652); the admin Next proxies forward real sessions. No dead routes found.
  • Yemaya multi-agent orchestration (routes/admin-yemaya-multi-agent- orchestration.ts, studio workspaces) is the Yemaya messaging domain's slice, not features.md §Agentic — out of scope here.

Severity counts#

Severity Count Findings
P0-SEC 3 F1, F2, F3
P0-HONESTY 0
P0-STRUCT 1 F4
P1 7 F5, F6, F7, F8, F9, F10, F11
P2 2 F12, F13
UX 2 F14, F15
DEPLOY 0
Total 15