Approval Bus (command service — owns the queue)
The command side: owns the proposal queue. propose_* only queue; a cleared human approves in the card; only then does the kind's applier execute the vendor write. Every transition is recorded on the shared store.
Overview
src/approvals/ (engine, store, RBAC, write policy). The core invariant (ADR §5): no executable action is ever exposed as an agent-callable tool. The agent-callable propose_* tools only queue and render; execution lives exclusively behind the human-only Commit ApprovalCommit ApprovalCommandv0.1.0The ONLY execution path. A human click in the approval card approves (→ the write runs) or rejects a pending proposal. A...Ownervisionary-engineeringSchemaMapView docs (app-only visibility + per-card widget token + session-bound RBAC).
Lifecycle: pending → approved → applied / failed, or pending → rejected. The pending→decided transition is claimed atomically (row transition; lost race = 409) so a non-idempotent vendor write runs at most once per proposal.
Dispatch: on approve, the proposal’s kind selects its applier service — VendorBillApplier — AP intake (workflow #1)VendorBillApplier — AP intake (workflow #1)Servicev0.1.0Applies an approved vendor bill by PO linkage: PO-linked bills import into Fishbowl (auto-pushes to QBO); non-PO bills p...Publishesqbo-create-bill, fishbowl-import-billSubscribesproposal-approvedOwnervisionary-engineeringMapRepoView docs · BillDateCorrectionApplier (workflow #2)BillDateCorrectionApplier (workflow #2)Servicev0.1.0Applies an approved bill-date correction: re-fetches the bill for a fresh SyncToken, then sparse-updates the one date fi...Publishesaccounting-read, qbo-update-billSubscribesproposal-approvedOwnervisionary-engineeringMapRepoView docs · InvoiceGenerationApplier (workflow #3)InvoiceGenerationApplier (workflow #3)Servicev0.1.0Applies an approved customer invoice: creates it in QBO with one SalesItemLineDetail per approved line.Publishesqbo-create-invoiceSubscribesproposal-approvedOwnervisionary-engineeringMapRepoView docs · OrderEntryApplier (workflow #8)OrderEntryApplier (workflow #8)Servicev0.1.0Applies an approved sales order: imports a draft SO (status Estimate) via Fishbowl's CSV import path.Publishesfishbowl-import-sales-orderSubscribesproposal-approvedOwnervisionary-engineeringMapRepoView docs · ArCollectionsApplier (workflow #4)ArCollectionsApplier (workflow #4)Servicev0.1.0Applies an approved AR collections email: sends via Microsoft Graph sendMail. The approval IS the send gate; the human c...Publishesgraph-send-mailSubscribesproposal-approvedOwnervisionary-engineeringMapRepoView docs — which performs the one vendor write; the result/error is recorded back on the proposal (Mutation AppliedMutation AppliedEventv0.1.0The approved mutation executed against its vendor system; the applier's result (e.g.Ownervisionary-engineeringMapView docs / Mutation FailedMutation FailedEventv0.1.0The applier threw (vendor down, validation, auth) — recorded as status `failed` with the error, surfaced to the card as ...Ownervisionary-engineeringMapView docs).
Both CQRS sides of the store meet here: commands write it; the read side (List Pending ApprovalsList Pending ApprovalsQueryv0.1.0The approval inbox: every proposed mutation still awaiting a human decision.Ownervisionary-engineeringMapView docs) reads the same rows — Jesh’s shared-database CQRS, deliberately not two datastores.
Write policy: WRITE_MODE=disabled (beta) refuses approves at the choke point; gated refuses to boot against live write connectors until applier idempotency (U6) ships.
RBAC: static role→kind grants (admin/controller/ap-clerk/ar-clerk/sales/viewer) keyed off the verified Entra subject (MCP_SUBJECT_ROLE_MAP); the M4 admin portal replaces the map behind the same canApprove seam.