Old internal-tool-selector attached `file_approval` to any agent with
`reportsTo === null`, and attached both tools through the same
management-keyword predicate chain. That let a VP or director with
org-management authority walk away with `file_approval`, contradicting
the C4 commit narrative that scoped `approve_ceo_strategy` as CEO-only.
Introduce `canFileCeoStrategy(db, companyId, agentId)` as a dedicated
predicate requiring both a CEO-equivalent role/title AND
`reportsTo === null`. Using either half alone is too loose: role alone
lets a demoted CEO with a virtual superior still file strategy,
`reportsTo` alone lets a VP with no manager slip through. The two
conditions together pin the predicate to a genuine top-level CEO.
- `hire_agent` continues to gate on `canRequestHire` (managers hiring
ICs is a legitimate flow and the original "CEO-only" framing in the
C4 commit message was narrower than the production reality).
- `file_approval` now gates on `canFileCeoStrategy`, orthogonal to hire
authority.
- New unit test covers 4 edges: genuine CEO, CEO title with virtual
superior, VP without manager, and plain IC.
Migrate all remaining pass-through call sites (social, skills, budget,
entityCrud, documents, knowledge, automation, wakeup, backup, threads,
settings, snapshot, connectors, workspaces, activity) to their owning
domain modules. `AppDatabase` now carries only three methods:
- createHireRequest (multi-module composite: agents + approvalsMod)
- rollbackConfigRevision (multi-module composite: documents + entityCrud)
- queryAll (raw-SQL escape hatch, @internal)
The vault/env-binding infrastructure previously passed argument-by-
argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at
module construction, letting callers invoke `db.entityCrud.saveSecret({...})`
without carrying vault plumbing.
Structural types that narrowed AppDatabase to a bag of flat methods
(`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in
budget-enforcement / budget-incidents / task-quality-gates /
failure-escalation / task-quality-gates-sweep) are reshaped to the new
namespace. `connectorPolicyDepsFromDb` replaces the old pattern of
handing the whole AppDatabase to `validateConnectorModelSelection`.
A new ESLint rule in eslint.config.mjs blocks new top-level methods on
AppDatabase so the pass-through layer cannot silently grow back.
`AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost
the 270-line trailing pass-through block.
Rewrite ~525 call sites from `db.<method>()` to `db.<module>.<method>()`
for tasks and agents domains (including `getAgent`/`findAgent`/`getTask`
which live on the `entityCrud` field). Delete the corresponding
pass-through methods on AppDatabaseBase. Structural types that narrowed
`db` to a bag of flat methods (CapabilityMatchDb, EscalationDeps,
SweepStaleReviewsDeps) are reshaped to match the new namespace. Internal
callback references inside database.ts/database-base.ts now invoke the
owning module directly (e.g. `this.agents.saveAgent`) rather than the
deleted pass-through.
The codemod lives at scripts/codemod/remove-appdatabase-passthrough.mjs
and filters by owning module + optional explicit method list. It is
idempotent and does not re-rewrite already-namespaced calls.
safeGetAgent was catching all errors and returning null, causing every
permission check (canControlAgent / canManageOrganization / canRequestHire)
to silently deny when the database threw. Only swallow the 'Agent not found'
case and let real DB errors propagate up to registerHandle's sanitizer.