provisionSandbox.ts granted `[12, 122, 262, 32, 111, 251]`: opaque numeric ids,
one fixed scope, one agent per environment. Three failures in the 2026-07/08
sessions came straight from that:
1. Production `ai_agent` — documented as READ-ONLY in AGENTS.md, ADR-0003 and
every SKILL.md — actually holds 46 rights against 9 declared, including
create/modify on customer AND supplier invoices, thirdparties, contacts,
thirdparty payment details, proposals, plus THREE delete rights (proposals,
events, and submit/delete documents in the GED).
2. Writing a payment, then a product, to production each required granting a
right by hand and revoking it after. A privilege granted ad hoc under time
pressure is worse than one nobody holds.
3. A sandbox refresh wiped the agent's proposal rights, because they had been
granted manually and lived nowhere in code.
- scopes.ts declares three scopes with their purpose and allowed environments:
`read` (both envs), `sandbox-write` (sandbox only), `prod-write` (production
only, narrow: invoices + payments, what the gated promote apply actually
does). resolveScope() refuses a scope on an environment it does not belong to.
No scope grants DELETE — the ledger is append-only, deletion stays human.
- provisionAiUser.ts creates or aligns one user per (environment × scope), emits
its API key to a gitignored 600 file, and has an --audit mode that diffs what a
user HOLDS against what its scope DECLARES. Production writes require the
guard.ts opt-in.
- The audit reads permission labels LIVE off /user/perms.php rather than trusting
a catalogue in code: ids are stable per Dolibarr version, not across them, and
an audit that cannot name what it found is not actionable.
findUserId is implemented locally rather than imported: the trunk's userSetup.ts
has one, but it is uncommitted WIP and a provisioning script must not depend on
someone's working tree.
Tooling only — no production rights were changed. The migration (create the
scoped users, repoint the promote pipeline, then strip the over-grants from
`ai_agent`) rotates credentials used by every read skill and is the operator's
call.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh