feat(test): provision erp-sandbox via Playwright (REST API + write-scoped ai_agent_sandbox user)
Extend the Deno + Playwright UI-automation POC to provision the erp-sandbox Dolibarr for the AI agent: - moduleSetup.ts: add enableApiModule(ctx) — toggles the REST API / Web services module on /admin/modules.php (kanban). Resilient: tries the fr_FR card label "API/Web services REST (serveur)" first, falls back to a /API.*REST|REST.*API/i title match if the exact label is absent. - userSetup.ts (new): createUser (returns the new numeric id), assignRights (clicks each addrights link on /user/perms.php, idempotent), generateApiKey (triggers Dolibarr's generate control on the user card and reads the value back). - provisionSandbox.ts (new entrypoint, main.ts untouched): login → enable API → create ai_agent_sandbox (non-admin) → grant write rights → generate API key, then write the key to test/.ai_agent_sandbox.key (gitignored) instead of printing it. - .gitignore (new), .env.example + README.md: sandbox vars, the deno run --allow-all provisionSandbox.ts command, and kubectl one-liners to pull DOLI_ADMIN_PASSWORD (secretkv) / DOLI_DB_PASSWORD (vso-db-credentials) from the erp-sandbox namespace. Why UI not SQL: API keys are encrypted with the instance's DOLI_INSTANCE_UNIQUE_ID, so the key must be generated by the sandbox itself, not INSERTed raw. deno check passes for provisionSandbox.ts and scripts/admin/userSetup.ts. NOT run end-to-end: the sandbox Dolibarr is not installed yet (empty DB / fresh install wizard), so the selectors are best-effort Dolibarr 22 conventions and must be confirmed on the first real run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
147
test/provisionSandbox.ts
Normal file
147
test/provisionSandbox.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import "load_dotenv";
|
||||
import { chromium } from "playwright";
|
||||
import path from "node:path";
|
||||
import login from "./scripts/login.ts";
|
||||
import moduleSetup from "./scripts/admin/moduleSetup.ts";
|
||||
import userSetup from "./scripts/admin/userSetup.ts";
|
||||
|
||||
/*
|
||||
provisionSandbox.ts — separate entrypoint (does NOT touch main.ts).
|
||||
|
||||
Provisions the erp-sandbox Dolibarr so the AI agent can write to it:
|
||||
1. enable the REST API / Web services module,
|
||||
2. create a write-scoped `ai_agent_sandbox` user (non-admin),
|
||||
3. grant it the write rights it needs,
|
||||
4. have Dolibarr generate its API key and emit it safely to
|
||||
test/.ai_agent_sandbox.key (gitignored).
|
||||
|
||||
Run:
|
||||
cd test && deno run --allow-all provisionSandbox.ts
|
||||
|
||||
CANNOT be run end-to-end yet: the sandbox Dolibarr is not installed/provisioned
|
||||
(empty DB, fresh install wizard). Selector correctness in moduleSetup.ts /
|
||||
userSetup.ts is therefore best-effort and must be verified on the first real
|
||||
run, AFTER the install wizard has been completed against the sandbox.
|
||||
|
||||
NEXT STEP (not implemented here): load the generated key into the dolibarr
|
||||
skill's sandbox config / Vault at kvv2/erp-sandbox/ai_agent. We intentionally
|
||||
do NOT write to Vault from this POC.
|
||||
*/
|
||||
|
||||
/*
|
||||
Write rights to grant `ai_agent_sandbox` (stable Dolibarr rights ids, verified
|
||||
against prod Dolibarr 22.0.4). Each module's read (lire) + create (creer):
|
||||
facture: lire=11, creer=12
|
||||
societe: lire=121, creer=122
|
||||
societe contact: lire=281, creer=282
|
||||
fournisseur: lire=1181, facture lire=1231, facture creer=1232
|
||||
produit: lire=31, creer=32
|
||||
*/
|
||||
const WRITE_IDS = [
|
||||
11, // facture lire
|
||||
12, // facture creer
|
||||
121, // societe lire
|
||||
122, // societe creer
|
||||
281, // societe contact lire
|
||||
282, // societe contact creer
|
||||
1181, // fournisseur lire
|
||||
1231, // fournisseur facture lire
|
||||
1232, // fournisseur facture creer
|
||||
31, // produit lire
|
||||
32, // produit creer
|
||||
];
|
||||
|
||||
const KEY_FILE = ".ai_agent_sandbox.key";
|
||||
|
||||
/*
|
||||
Initialisation — mirrors main.ts but targeted at the sandbox. The default
|
||||
address points at the sandbox; admin creds come from env (the same
|
||||
DOLI_ADMIN_LOGIN / DOLI_ADMIN_PASSWORD vars main.ts uses, populated from the
|
||||
erp-sandbox namespace secrets — see test/README.md "Provision the sandbox").
|
||||
*/
|
||||
const dolibarrAddress = Deno.env.get("DOLIBARR_ADDRESS") ||
|
||||
"https://erp-sandbox.arcodange.lab";
|
||||
const debug = true;
|
||||
const DBpassword = Deno.env.get("DOLI_DB_PASSWORD") || "undefined";
|
||||
const adminCredentials = {
|
||||
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "admin",
|
||||
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
||||
};
|
||||
|
||||
// The new user's login is fixed; its password comes from env or is generated.
|
||||
const AI_AGENT_LOGIN = "ai_agent_sandbox";
|
||||
const aiAgentPassword = Deno.env.get("AI_AGENT_SANDBOX_PASSWORD") ||
|
||||
generatePassword();
|
||||
|
||||
const rootFolderPath = Deno.env.get("ROOT_FOLDER") ||
|
||||
path.join(Deno.cwd(), "..");
|
||||
const imgFolderPath = Deno.env.get("IMG_FOLDER") ||
|
||||
path.join(rootFolderPath, "static/img");
|
||||
const configFolderPath = Deno.env.get("CONFIG_FOLDER") ||
|
||||
path.join(rootFolderPath, "static/config");
|
||||
|
||||
/* Generate a reasonably strong random password (used only if none provided). */
|
||||
function generatePassword(): string {
|
||||
const bytes = new Uint8Array(18);
|
||||
crypto.getRandomValues(bytes);
|
||||
// URL-safe base64 → no shell-hostile characters.
|
||||
return btoa(String.fromCharCode(...bytes))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
logger: {
|
||||
isEnabled: (_name, _severity) => debug,
|
||||
log: (name, severity, message, args) =>
|
||||
console.warn(`${severity}| ${name} :: ${message} __ ${args}`),
|
||||
},
|
||||
});
|
||||
const context = await browser.newContext({ locale: "fr-FR" });
|
||||
const page = await context.newPage();
|
||||
|
||||
const globalCtx = {
|
||||
dolibarrAddress,
|
||||
DBpassword,
|
||||
adminCredentials,
|
||||
|
||||
debug,
|
||||
rootFolderPath,
|
||||
imgFolderPath,
|
||||
configFolderPath,
|
||||
|
||||
browser,
|
||||
page,
|
||||
context,
|
||||
};
|
||||
|
||||
try {
|
||||
await login.doAdminLogin(globalCtx);
|
||||
console.log(`connected as ${await login.whoAmI(globalCtx)}`);
|
||||
|
||||
await moduleSetup.enableApiModule(globalCtx);
|
||||
console.log("REST API module enabled");
|
||||
|
||||
const userId = await userSetup.createUser(globalCtx, {
|
||||
login: AI_AGENT_LOGIN,
|
||||
password: aiAgentPassword,
|
||||
lastname: "AI Agent (sandbox)",
|
||||
admin: false,
|
||||
});
|
||||
console.log(`created user '${AI_AGENT_LOGIN}' (id=${userId})`);
|
||||
|
||||
await userSetup.assignRights(globalCtx, userId, WRITE_IDS);
|
||||
console.log(`granted ${WRITE_IDS.length} write rights to id=${userId}`);
|
||||
|
||||
const apiKey = await userSetup.generateApiKey(globalCtx, userId);
|
||||
|
||||
// Emit the key safely: write it to a gitignored file rather than printing it.
|
||||
await Deno.writeTextFile(KEY_FILE, apiKey + "\n");
|
||||
console.log(`AI_AGENT_SANDBOX API KEY WRITTEN TO test/${KEY_FILE}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user