Files
erp/.claude/skills/dolibarr-sandbox-write/scripts/dol-write.sh
Gabriel Radureau d2e8b3a3a4 feat(skills): dolibarr-sandbox-write — host-guarded write skill (V9)
The write-capable companion to the read-only dolibarr* skills, scoped to the
erp-sandbox. Lets an AI agent rehearse bookkeeping writes against a copy of prod
(ADR-0003) before a human promotes the reviewed change to prod.

- scripts/dol-write.sh: write wrapper that REFUSES any host that is not
  erp-sandbox.arcodange.lab (the structural prod-safety guarantee) using the
  ai_agent_sandbox key from a gitignored .env.
- scripts/thirdparty-create.sh: create client/supplier fiches; codes auto-assign
  via the elephant mask (code="-1").
- scripts/invoice-create.sh: customer (/invoices) or supplier (/supplierinvoices)
  invoices with product/service lines + ref_supplier, optional validate.
- scripts/payment-record.sh: record a règlement (VIR/CB/CHQ/LIQ); customer pays
  full + marks paid, supplier needs an amount.
- SKILL.md (safety model + workflows + the human-gated promote flow), .env.example,
  example input.

Proven end-to-end live against the sandbox: client -> invoice (service+product
lines, HT 1100 / TTC 1320) -> validate -> payment (paid); supplier -> supplier
invoice (ref_supplier carried) -> validate. Host guard verified to refuse a prod
URL before sending.

Avoirs (credit notes) and bin/arcodange CLI wiring are planned follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-29 20:49:31 +02:00

83 lines
3.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Host-guarded WRITE wrapper for the Arcodange Dolibarr SANDBOX API.
#
# THE GUARANTEE (ADR-0003): this wrapper REFUSES to run against anything that is
# not the erp-sandbox host. It is the structural reason an AI agent driving this
# skill can never mutate production — prod is a different host, and the guard
# below rejects it before any request is sent.
#
# Usage:
# dol-write.sh <METHOD> <path> [json-body|@file]
# dol-write.sh POST /thirdparties '{"name":"Acme","client":"1"}'
# dol-write.sh POST /invoices @invoice.json
# dol-write.sh PUT /thirdparties/42 '{"phone":"+33..."}'
# dol-write.sh GET /thirdparties?limit=5 # reads are allowed too
#
# Reads DOLIBARR_SANDBOX_URL + DOLIBARR_SANDBOX_API_KEY from the sibling .env
# (.claude/skills/dolibarr-sandbox-write/.env), mode 600, gitignored.
# Prints the response body to stdout; exits non-zero on HTTP >= 400.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/../.env"
if [[ ! -f "${ENV_FILE}" ]]; then
echo "dol-write.sh: missing ${ENV_FILE}" >&2
echo " Create it with DOLIBARR_SANDBOX_URL + DOLIBARR_SANDBOX_API_KEY. See README.md." >&2
exit 2
fi
# shellcheck disable=SC1090
set -a; source "${ENV_FILE}"; set +a
: "${DOLIBARR_SANDBOX_URL:?dol-write.sh: DOLIBARR_SANDBOX_URL not set in .env}"
: "${DOLIBARR_SANDBOX_API_KEY:?dol-write.sh: DOLIBARR_SANDBOX_API_KEY not set in .env}"
# ---------------------------------------------------------------------------
# HOST GUARD — the structural safety invariant. Only the sandbox host passes.
# Override the allowed pattern only via DOLIBARR_SANDBOX_HOST_RE in .env if the
# sandbox FQDN ever changes; never widen it to include a prod host.
# ---------------------------------------------------------------------------
ALLOWED_RE="${DOLIBARR_SANDBOX_HOST_RE:-^https://erp-sandbox\.arcodange\.lab(/|$)}"
if [[ ! "${DOLIBARR_SANDBOX_URL}" =~ ${ALLOWED_RE} ]]; then
echo "dol-write.sh: REFUSING to write — DOLIBARR_SANDBOX_URL='${DOLIBARR_SANDBOX_URL}'" >&2
echo " is not the erp-sandbox host (allowed: ${ALLOWED_RE})." >&2
echo " This skill only writes the sandbox. Promotion to prod is a separate, human-gated step." >&2
exit 3
fi
if [[ $# -lt 2 ]]; then
echo "dol-write.sh: usage: dol-write.sh <METHOD> <path> [json-body|@file]" >&2
exit 2
fi
METHOD="$1"; API_PATH="$2"; BODY="${3-}"
case "${METHOD}" in
GET|POST|PUT|DELETE|PATCH) ;;
*) echo "dol-write.sh: unsupported method '${METHOD}'" >&2; exit 2 ;;
esac
CURL_ARGS=(
-sS -X "${METHOD}"
-H "DOLAPIKEY: ${DOLIBARR_SANDBOX_API_KEY}"
-H "Accept: application/json"
--max-time 30
)
if [[ -n "${BODY}" ]]; then
# curl --data supports '@file' to read a JSON body from a file.
CURL_ARGS+=( -H "Content-Type: application/json" --data "${BODY}" )
fi
BODY_FILE="$(mktemp -t dolwrite.XXXXXX)"
trap 'rm -f "${BODY_FILE}"' EXIT
HTTP_CODE=$(curl "${CURL_ARGS[@]}" \
-o "${BODY_FILE}" -w "%{http_code}" \
"${DOLIBARR_SANDBOX_URL}/api/index.php${API_PATH}")
cat "${BODY_FILE}"
if [[ "${HTTP_CODE}" -ge 400 ]]; then
echo "" >&2
echo "dol-write.sh: HTTP ${HTTP_CODE} on ${METHOD} ${API_PATH}" >&2
exit 1
fi