@bongos/core 1.19.709 → 1.19.711

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.bongos-core.json +74 -34
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +24 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +2 -0
  8. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +2 -0
  9. package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +56 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +506 -3
  12. package/docs/api-reference.md +14 -2
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/recipes/ops-gotchas.md +14 -0
  15. package/modules/agents/lib/answer-hold.js +91 -0
  16. package/modules/agents/lib/authoring.js +155 -0
  17. package/modules/agents/lib/fire-budget.js +109 -0
  18. package/modules/agents/lib/gate.js +64 -0
  19. package/modules/agents/routes/agents.js +494 -0
  20. package/modules/agents/spawn.js +12 -0
  21. package/modules/dev-box/app/src/vendor/bongos-client.cjs +16 -0
  22. package/modules/government/catalog.js +10 -0
  23. package/modules/government/migrations/government_013_agent_atoms.sql +66 -0
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/agent-invoke.js +5 -27
  27. package/scripts/gds/provision-repo.js +181 -0
  28. package/scripts/gds/provision-units.js +33 -5
  29. package/scripts/gds/provision.js +35 -3
  30. package/src/bongos/pool.js +14 -0
  31. package/src/module-api.js +1 -1
  32. package/tests/agents_authoring.mjs +306 -0
  33. package/tests/agents_routes.mjs +76 -15
  34. package/tests/agents_write_routes.mjs +461 -0
  35. package/tests/provision.mjs +227 -0
@@ -2446,5 +2446,232 @@ await ta('teardownInstance(standalone): no registered key / no token → teardow
2446
2446
  assert.ok(b.cmds.some((c) => /rm -f \S+\/\.git\/cloudbongos-deploy-key \S+\.pub/.test(c)), 'local key halves removed even with no credential');
2447
2447
  });
2448
2448
 
2449
+
2450
+ console.log('\nper-instance unix account + password-authed PG role (task 1003369, audit B3):');
2451
+
2452
+ t('instanceUser: prefixed, never the bare slug — a project called `postgres` cannot claim that account', () => {
2453
+ assert.equal(P.instanceUser(coTenant), 'bongos-mercury');
2454
+ assert.equal(P.instanceUser({ slug: 'postgres' }), 'bongos-postgres');
2455
+ assert.notEqual(P.instanceUser({ slug: 'lars' }), 'lars');
2456
+ });
2457
+
2458
+ t('instanceUser: a 40-char slug folds to a LEGAL account name — <=32 chars, no trailing/double dash', () => {
2459
+ const slug = 'a'.repeat(39) + 'b'; // the isValidSlug maximum
2460
+ const u = P.instanceUser({ slug });
2461
+ assert.ok(u.length <= 32, `account name must fit utmp's 32 chars, got ${u.length} (${u})`);
2462
+ assert.match(u, /^[a-z][a-z0-9-]*[a-z0-9]$/, 'must satisfy useradd NAME_REGEX');
2463
+ assert.ok(!u.includes('--'), 'no double dash');
2464
+ });
2465
+
2466
+ t('instanceUser: two slugs sharing a long prefix get DIFFERENT accounts — a fold collision would hand each the other\'s identity', () => {
2467
+ const a = 'production-instance-alpha-one'; // 29 chars, identical for 26 of them
2468
+ const b = 'production-instance-alpha-two';
2469
+ assert.notEqual(P.instanceUser({ slug: a }), P.instanceUser({ slug: b }));
2470
+ });
2471
+
2472
+ t('instanceUser: an invalid slug THROWS rather than deriving an account name from it', () => {
2473
+ assert.throws(() => P.instanceUser({ slug: '../../etc/passwd' }), /invalid slug/);
2474
+ assert.throws(() => P.instanceUser({ slug: 'Has Spaces' }), /invalid slug/);
2475
+ assert.throws(() => P.instanceUser({ slug: '' }), /invalid slug/);
2476
+ });
2477
+
2478
+ t('instanceStateDir is NOT under /home — ProtectHome=read-only would make the first-boot secret write EROFS', () => {
2479
+ const dir = P.instanceStateDir(coTenant);
2480
+ assert.equal(dir, '/var/lib/bongos-mercury');
2481
+ assert.ok(!dir.startsWith('/home'), 'the writable state dir must survive ProtectHome=read-only');
2482
+ });
2483
+
2484
+ t('generateDbPassword: base64url only, so it needs no escaping in the env file, the SQL literal or a URL', () => {
2485
+ for (let i = 0; i < 20; i++) {
2486
+ const pw = P.generateDbPassword();
2487
+ assert.match(pw, /^[A-Za-z0-9_-]+$/, `not base64url: ${pw}`);
2488
+ assert.ok(pw.length >= 40, `too short to be 32 bytes: ${pw.length}`);
2489
+ }
2490
+ assert.notEqual(P.generateDbPassword(), P.generateDbPassword(), 'must not be deterministic');
2491
+ });
2492
+
2493
+ t('ensureInstanceUserCmd: idempotent SYSTEM account, nologin, state dir re-asserted every run', () => {
2494
+ const c = P.ensureInstanceUserCmd(coTenant, { privileged: true });
2495
+ assert.match(c, /id -u bongos-mercury >\/dev\/null 2>&1 \|\|/, 'an existing account short-circuits');
2496
+ assert.match(c, /useradd --system/, 'a system account, not a login user');
2497
+ assert.match(c, /--shell \/usr\/sbin\/nologin/, 'nothing should ever log in as an instance');
2498
+ assert.match(c, /install -d -o bongos-mercury -g bongos-mercury -m 0700 \/var\/lib\/bongos-mercury/);
2499
+ assert.ok(!P.ensureInstanceUserCmd(coTenant).includes('sudo '), 'unprivileged renders without sudo');
2500
+ });
2501
+
2502
+ t('grantInstanceRepoReadCmd: adds the instance account to the app-user group so the unit can exec its WorkingDirectory', () => {
2503
+ assert.equal(P.grantInstanceRepoReadCmd(coTenant, { privileged: true }), 'sudo usermod -aG lars bongos-mercury');
2504
+ });
2505
+
2506
+ t('dbRoleCmd: LOGIN + password, upserted — CREATE on a fresh box, ALTER on a re-provision (the rotation path)', () => {
2507
+ const c = P.dbRoleCmd(coTenant, { password: 'pw_ABC-123', privileged: true });
2508
+ assert.match(c, /sudo -u postgres psql -v ON_ERROR_STOP=1 -d postgres/, 'role DDL runs on `postgres`, not the instance DB');
2509
+ assert.match(c, /SELECT 1 FROM pg_roles WHERE rolname = \$r\$bongos-mercury\$r\$/);
2510
+ assert.match(c, /CREATE ROLE %I WITH LOGIN PASSWORD %L/);
2511
+ assert.match(c, /ALTER ROLE %I WITH LOGIN PASSWORD %L/);
2512
+ assert.ok(c.includes('$p$pw_ABC-123$p$'), 'the password rides a dollar-quoted literal');
2513
+ });
2514
+
2515
+ t('dbRoleCmd: the role is NOT a superuser and gets no CREATEDB/CREATEROLE — a compromised instance reaches its own data and no further', () => {
2516
+ const c = P.dbRoleCmd(coTenant, { password: P.generateDbPassword(), privileged: true });
2517
+ assert.ok(!/SUPERUSER/i.test(c), 'never a superuser');
2518
+ assert.ok(!/CREATEDB/i.test(c), 'no CREATEDB');
2519
+ assert.ok(!/CREATEROLE/i.test(c), 'no CREATEROLE');
2520
+ assert.ok(!/BYPASSRLS/i.test(c), 'no BYPASSRLS');
2521
+ });
2522
+
2523
+ t('dbRoleCmd: REVOKEs CONNECT from PUBLIC — the half that isolates, since PG grants it to PUBLIC by default', () => {
2524
+ const c = P.dbRoleCmd(coTenant, { password: 'pw', privileged: true });
2525
+ assert.match(c, /REVOKE CONNECT ON DATABASE %I FROM PUBLIC/, 'without this a sibling peer-auths straight in');
2526
+ assert.match(c, /ALTER DATABASE %I OWNER TO %I/, 'the role owns its own DB');
2527
+ });
2528
+
2529
+ t('dbRoleCmd: refuses a password that is not base64url — no caller can smuggle a quote into the SQL', () => {
2530
+ assert.throws(() => P.dbRoleCmd(coTenant, { password: "x'; DROP DATABASE mercury; --" }), /base64url/);
2531
+ assert.throws(() => P.dbRoleCmd(coTenant, { password: '' }), /base64url/);
2532
+ assert.throws(() => P.dbRoleCmd(coTenant, {}), /base64url/);
2533
+ });
2534
+
2535
+ t('dbRoleGrantsCmd: runs on the INSTANCE db and grants forward too — the migrate runs as postgres, so it owns the tables', () => {
2536
+ const c = P.dbRoleGrantsCmd(coTenant, { privileged: true });
2537
+ assert.match(c, /-d mercury/, 'schema grants are per-database');
2538
+ assert.match(c, /GRANT ALL ON ALL TABLES IN SCHEMA public TO %I/, 'the tables the migrate already created');
2539
+ assert.match(c, /ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO %I/, 'and the ones the NEXT migrate will');
2540
+ assert.match(c, /GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO %I/);
2541
+ });
2542
+
2543
+ t('BOTH role commands render with NO double quote — they have to survive a YAML double-quoted cloud-init scalar', () => {
2544
+ const dq = String.fromCharCode(34);
2545
+ for (const c of [P.dbRoleCmd(coTenant, { password: 'pw_ABC-123' }), P.dbRoleGrantsCmd(coTenant)]) {
2546
+ assert.ok(!c.includes(dq), `a double-quoted identifier silently truncates the cloud-init line: ${c}`);
2547
+ }
2548
+ });
2549
+
2550
+ t('serviceUnit: runs as the instance\'s OWN account, never the shared app user (the /proc/<pid>/environ vector)', () => {
2551
+ const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
2552
+ assert.match(u, /^User=bongos-mercury$/m);
2553
+ assert.match(u, /^Group=bongos-mercury$/m);
2554
+ assert.ok(!/^User=lars$/m.test(u), 'a shared User= is the defect this closes');
2555
+ assert.ok(!/^Group=lars$/m.test(u));
2556
+ });
2557
+
2558
+ t('serviceUnit: two instances on one box get two DIFFERENT accounts — co-tenant is the default shape', () => {
2559
+ const a = P.serviceUnit(coTenant, { etcBase: '/etc' });
2560
+ const b = P.serviceUnit({ ...coTenant, slug: 'venus', db_name: 'venus' }, { etcBase: '/etc' });
2561
+ assert.match(a, /^User=bongos-mercury$/m);
2562
+ assert.match(b, /^User=bongos-venus$/m);
2563
+ });
2564
+
2565
+ t('serviceUnit: carries the hardening block, with HOME + ReadWritePaths pointing at the state dir', () => {
2566
+ const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
2567
+ assert.match(u, /^ProtectSystem=strict$/m);
2568
+ assert.match(u, /^ProtectHome=read-only$/m);
2569
+ assert.match(u, /^PrivateTmp=true$/m);
2570
+ assert.match(u, /^NoNewPrivileges=true$/m);
2571
+ assert.match(u, /^Environment=HOME=\/var\/lib\/bongos-mercury$/m, 'the config-home write must not land under the read-only /home');
2572
+ assert.match(u, /^ReadWritePaths=\/var\/lib\/bongos-mercury /m, 'the state dir is writable');
2573
+ });
2574
+
2575
+ t('serviceUnit: ReadWritePaths does NOT make the checkout the service runs writable by it', () => {
2576
+ const inst = { ...coTenant, hosting_shape: 'standalone' };
2577
+ const u = P.serviceUnit(inst, { etcBase: '/etc' });
2578
+ const root = P.standaloneRoot(inst);
2579
+ const rw = u.split('\n').find((l) => l.startsWith('ReadWritePaths='));
2580
+ assert.ok(rw.includes(`-${root}/config`), 'only config/, and optional so a shape without one still mounts');
2581
+ assert.ok(!rw.split(' ').includes(root), 'the code the service runs stays read-only to it');
2582
+ });
2583
+
2584
+ t('webEnvBody: carries PGUSER + PGPASSWORD for the instance role when the runner minted one', () => {
2585
+ const b = P.webEnvBody(coTenant, { dbPassword: 'pw_ABC-123' });
2586
+ assert.match(b, /^PGUSER=bongos-mercury$/m);
2587
+ assert.match(b, /^PGPASSWORD=pw_ABC-123$/m);
2588
+ assert.match(b, /^PGDATABASE=mercury$/m);
2589
+ });
2590
+
2591
+ t('webEnvBody: NO password ⇒ no PG credential lines — an instance provisioned before this keeps its peer auth', () => {
2592
+ const b = P.webEnvBody(coTenant);
2593
+ assert.doesNotMatch(b, /^PGUSER=/m);
2594
+ assert.doesNotMatch(b, /^PGPASSWORD=/m);
2595
+ assert.match(b, /^PGDATABASE=mercury$/m, 'the DB name is unchanged either way');
2596
+ });
2597
+
2598
+ t('dedicatedUserData: the droplet runs the SAME account + role steps, in the same order around the migrate', () => {
2599
+ const ud = P.dedicatedUserData(coTenant);
2600
+ const at = (needle) => ud.indexOf(needle);
2601
+ assert.ok(at('useradd --system') > -1, 'the droplet creates the account too');
2602
+ assert.ok(at('usermod -aG lars') > -1);
2603
+ assert.ok(at('useradd --system') < at('createdb mercury'), 'account before the DB');
2604
+ assert.ok(at('migrate.sh') < at('CREATE ROLE %I'), 'the role grants must follow the migrate that created the tables');
2605
+ assert.ok(at('CREATE ROLE %I') < at('GRANT ALL ON ALL TABLES'), 'role before its grants');
2606
+ });
2607
+
2608
+ t('dedicatedUserData: NEVER bakes a password into cloud-init — DO retains user-data and serves it back', () => {
2609
+ const ud = P.dedicatedUserData(coTenant);
2610
+ // The dollar-quoted password literal dbRoleCmd uses. Its absence is the assertion.
2611
+ assert.ok(!ud.includes('$p$'), 'a credential in user-data is a permanent copy we cannot lock down');
2612
+ assert.match(ud, /CREATE ROLE %I WITH LOGIN\$f\$/, 'the role is still created, password-less (single-tenant droplet)');
2613
+ assert.match(ud, /REVOKE CONNECT ON DATABASE %I FROM PUBLIC/, 'and PUBLIC still loses CONNECT');
2614
+ });
2615
+
2616
+ t('dbRoleCmd(peerOnly): omits the PASSWORD clause entirely, and REFUSES a password', () => {
2617
+ const c = P.dbRoleCmd(coTenant, { peerOnly: true });
2618
+ assert.ok(!/PASSWORD/.test(c), 'no PASSWORD clause at all — not an empty one');
2619
+ assert.match(c, /CREATE ROLE %I WITH LOGIN\$f\$/);
2620
+ assert.match(c, /ALTER DATABASE %I OWNER TO %I/, 'ownership and the revoke are unchanged');
2621
+ assert.throws(() => P.dbRoleCmd(coTenant, { peerOnly: true, password: 'pw_ABC' }), /peerOnly takes no password/);
2622
+ });
2623
+
2624
+ t('dbRoleCmdShown: the label the runner logs names every effect and carries NO password', () => {
2625
+ const pw = P.generateDbPassword();
2626
+ const real = P.dbRoleCmd(coTenant, { password: pw });
2627
+ const shown = P.dbRoleCmdShown(coTenant);
2628
+ assert.ok(real.includes(pw), 'precondition: the real command does embed the secret');
2629
+ assert.ok(!shown.includes(pw), 'the logged label must not');
2630
+ assert.ok(!shown.includes('$p$'), 'nor the dollar-quoted literal around it');
2631
+ for (const effect of ['bongos-mercury', 'mercury', 'revoke PUBLIC connect']) {
2632
+ assert.ok(shown.includes(effect), `the label must still name: ${effect}`);
2633
+ }
2634
+ });
2635
+
2636
+
2637
+ await ta('provisionInstance: EVERY secret-bearing command opts into `shown`, so no password reaches the runner log', async () => {
2638
+ // The generator tests prove the label is clean; this proves the CALL SITE uses it.
2639
+ // Stated as an invariant rather than one assertion about one line: any future command
2640
+ // that embeds a dollar-quoted secret is caught by the same test, which is the failure
2641
+ // mode worth pinning — the leak was not a typo, it was a call site that never opted in.
2642
+ const { deps } = recorder();
2643
+ const seen = [];
2644
+ deps.exec = (cmd, opts = {}) => { seen.push({ cmd, opts }); return { ok: true, stdout: '' }; };
2645
+ await P.provisionInstance(coTenant, deps);
2646
+
2647
+ const secretBearing = seen.filter((c) => c.cmd.includes('$p$'));
2648
+ assert.ok(secretBearing.length >= 1, 'precondition: the run really does issue a password-bearing command');
2649
+ for (const { cmd, opts } of secretBearing) {
2650
+ assert.ok(opts.shown, `a command embedding a secret must pass \`shown\`: ${cmd.slice(0, 60)}…`);
2651
+ assert.ok(!opts.shown.includes('$p$'), 'the label must not carry the dollar-quoted secret');
2652
+ // The password itself, pulled back out of the real command, must not be in the label.
2653
+ const pw = (cmd.match(/\$p\$([^$]+)\$p\$/) || [])[1];
2654
+ assert.ok(pw && pw.length > 20, 'precondition: recovered the generated password from the command');
2655
+ assert.ok(!opts.shown.includes(pw), 'the label must not carry the password');
2656
+ }
2657
+ });
2658
+
2659
+ await ta('provisionInstance: the password IS written to web.env, which makeWriteFile logs by path not body', async () => {
2660
+ const { deps } = recorder();
2661
+ const bodies = [];
2662
+ deps.writeFile = (path, body, opts) => { bodies.push({ path, body, opts }); return { ok: true }; };
2663
+ const seen = [];
2664
+ deps.exec = (cmd, opts = {}) => { seen.push({ cmd, opts }); return { ok: true, stdout: '' }; };
2665
+ await P.provisionInstance(coTenant, deps);
2666
+
2667
+ const env = bodies.find((b) => /web\.env$/.test(b.path));
2668
+ assert.ok(env, 'wrote a web.env');
2669
+ assert.equal(env.opts && env.opts.mode, 0o600, 'web.env stays chmod 600');
2670
+ const pw = (seen.map((c) => c.cmd).join('\n').match(/\$p\$([^$]+)\$p\$/) || [])[1];
2671
+ assert.ok(pw, 'recovered the run\'s generated password');
2672
+ assert.ok(env.body.includes(`PGPASSWORD=${pw}`), 'the SAME password the role was set to reaches web.env — one mint per run');
2673
+ assert.ok(env.body.includes('PGUSER=bongos-mercury'));
2674
+ });
2675
+
2449
2676
  console.log(`\nprovision: ${passed} passed, ${failed} failed`);
2450
2677
  process.exit(failed ? 1 : 0);