feat(write-skill): chronology guard + explicit production opt-in

Two guards, both from real incidents in the same session.

1. invoice-create.sh — chronology (CGI art. 289). Dolibarr assigns the number at
   validation, in creation order, so issuing a document dated BEFORE the last one
   already issued gives a higher number to an earlier date. The July plan walked
   straight into it: the M3 deferred part is due 2026-10-23 and must be issued at
   D-60 (24/08) to stay under the L.441-10 I ceiling, while the M4 fixed part is
   dated 23/08 — issue them in the wrong order and the numbering breaks. The
   guard reads the last issued document of the same kind and refuses an earlier
   date, with ARCO_ALLOW_BACKDATE as a loud, documented override.
   Verified: refuses a 01/07 invoice against FAC008 (23/07), accepts 23/08.

2. test/scripts/guard.ts — production opt-in. The sandbox-only guard had no way
   to express a deliberate production run, so any prod work meant bypassing it
   entirely (which is how guards die). Production now requires BOTH
   ARCO_ALLOW_PRODUCTION=<exact host> and
   ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD, and prints a banner. Nothing
   reaches prod by inheriting an ambient variable.
   Also fixes a misleading "(sandbox verified)" log that printed even on prod.

grantAgentRight.ts joins the repo (it was never committed) and gains --revoke,
so a temporarily elevated right can be handed back — used today to attach a
payment in production and revoked immediately after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
This commit is contained in:
2026-07-26 01:26:12 +02:00
co-authored by Claude Opus 5
parent b1984aa337
commit dbe4b36c62
5 changed files with 138 additions and 3 deletions
+16 -1
View File
@@ -37,12 +37,27 @@ export function assertSandbox(address?: string): string {
}
if (!ALLOWED_HOST_PATTERN.test(host)) {
// Production opt-in: deliberate, loud, and per-run. The operator must name
// the exact host AND type the confirmation phrase, so nothing reaches prod
// by inheriting an ambient variable — the same posture the promote flow
// takes with DOLIBARR_PROD_WRITE_KEY / ARCO_PROMOTE_CONFIRM.
const allowProd = Deno.env.get("ARCO_ALLOW_PRODUCTION") ?? "";
const confirm = Deno.env.get("ARCO_PROD_CONFIRM") ?? "";
if (allowProd === host && confirm === "I-UNDERSTAND-THIS-WRITES-PROD") {
console.warn(
`\n*** PRODUCTION TARGET: ${host} — explicit opt-in accepted. ***\n` +
" Every write below hits the real ledger and cannot be undone.\n",
);
return target;
}
throw new UnsafeTargetError(
`REFUSED: '${host}' is not the sandbox.\n` +
"UI admin scripts may only drive erp-sandbox.*; production is changed " +
"by the operator through the human-gated path, never by a script.\n" +
"Override the ambient env explicitly, e.g.\n" +
" DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab deno run ...",
" DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab deno run ...\n" +
"To target production on purpose, set BOTH:\n" +
` ARCO_ALLOW_PRODUCTION=${host} ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD`,
);
}
return target;