@bongos/core 1.19.887 → 1.19.889
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.
- package/.bongos-core.json +46 -21
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/gds-db-backup.md +30 -2
- package/migrations/core_249_session_token_hash.sql +103 -0
- package/modules/agents/lib/validate.js +49 -2
- package/modules/agents/migrations/agents_005_cron_trigger.sql +78 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/db-backup-offsite.sh +105 -9
- package/scripts/gds/run-unit-tests.js +8 -0
- package/src/bongos/db-kernel.js +42 -6
- package/src/bongos/db.js +9 -2
- package/src/module-api.js +1 -1
- package/tests/agents_cron_trigger.mjs +133 -0
- package/tests/agents_validate.mjs +13 -5
- package/tests/auth_session_expiry_db.mjs +26 -21
- package/tests/government_live_proof.mjs +8 -2
- package/tests/own_session_revoke.mjs +13 -1
- package/tests/session_token_hash.mjs +139 -0
- package/tests/session_token_hash_db.mjs +193 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.889",
|
|
6
|
+
"core_contract": "1.19.889",
|
|
7
|
+
"source_commit": "b7871cd73a1ce5f73988d99ef63101a6a2d23050",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-19T21:23:27.620Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 497,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2269,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2792,
|
|
20
|
+
"tree_sha256": "b7b8bc9e53808070c5d46a211481ceef0dea2c901bc79e2b0dc0871e60c362b1",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2607,7 +2607,7 @@
|
|
|
2607
2607
|
{
|
|
2608
2608
|
"path": "docs/module-api-changelog.md",
|
|
2609
2609
|
"mode": "0000644",
|
|
2610
|
-
"sha256": "
|
|
2610
|
+
"sha256": "01a90a40951ae88d4c36c5fabbaf3debc0e02bcaa9fa8a7b70e247aa5d61d236"
|
|
2611
2611
|
},
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/modules-contract.md",
|
|
@@ -2742,7 +2742,7 @@
|
|
|
2742
2742
|
{
|
|
2743
2743
|
"path": "docs/recipes/gds-db-backup.md",
|
|
2744
2744
|
"mode": "0000644",
|
|
2745
|
-
"sha256": "
|
|
2745
|
+
"sha256": "d3e628bc627cb29874e977bf2e93c6557dc72a210506c22515a983f4f513d627"
|
|
2746
2746
|
},
|
|
2747
2747
|
{
|
|
2748
2748
|
"path": "docs/recipes/instance-deploy.md",
|
|
@@ -3699,6 +3699,11 @@
|
|
|
3699
3699
|
"mode": "0000644",
|
|
3700
3700
|
"sha256": "4f35f87050a532f26b7c9cd0a9bc6f298ab5c0e15eb1c36ddcdd4d261201291d"
|
|
3701
3701
|
},
|
|
3702
|
+
{
|
|
3703
|
+
"path": "migrations/core_249_session_token_hash.sql",
|
|
3704
|
+
"mode": "0000644",
|
|
3705
|
+
"sha256": "ae3e405c7e59fbc13e23ae2a4ba945176f2369a150c997d9b8134e49991f273b"
|
|
3706
|
+
},
|
|
3702
3707
|
{
|
|
3703
3708
|
"path": "modules/agents/CLAUDE.md",
|
|
3704
3709
|
"mode": "0000644",
|
|
@@ -3732,7 +3737,7 @@
|
|
|
3732
3737
|
{
|
|
3733
3738
|
"path": "modules/agents/lib/validate.js",
|
|
3734
3739
|
"mode": "0000644",
|
|
3735
|
-
"sha256": "
|
|
3740
|
+
"sha256": "e9b3c905539a323d74a55f98455204bd46daca2daf49c476324e85684ba7971f"
|
|
3736
3741
|
},
|
|
3737
3742
|
{
|
|
3738
3743
|
"path": "modules/agents/migrations/agents_001_definitions.sql",
|
|
@@ -3754,6 +3759,11 @@
|
|
|
3754
3759
|
"mode": "0000644",
|
|
3755
3760
|
"sha256": "31a5ac35536cecd264bc989907f4cf094e050db0b8de85e7e3e7831cf7c25a9d"
|
|
3756
3761
|
},
|
|
3762
|
+
{
|
|
3763
|
+
"path": "modules/agents/migrations/agents_005_cron_trigger.sql",
|
|
3764
|
+
"mode": "0000644",
|
|
3765
|
+
"sha256": "5d422998b1d53ee015f4a494ccede1adcb1eb90bd501bd28c1d88f9840b87367"
|
|
3766
|
+
},
|
|
3757
3767
|
{
|
|
3758
3768
|
"path": "modules/agents/module.json",
|
|
3759
3769
|
"mode": "0000644",
|
|
@@ -7692,12 +7702,12 @@
|
|
|
7692
7702
|
{
|
|
7693
7703
|
"path": "package-lock.json",
|
|
7694
7704
|
"mode": "0000644",
|
|
7695
|
-
"sha256": "
|
|
7705
|
+
"sha256": "d1348f68cd79ad102a381e3d7f90b784d9df605e859269ede84ad859cc831deb"
|
|
7696
7706
|
},
|
|
7697
7707
|
{
|
|
7698
7708
|
"path": "package.json",
|
|
7699
7709
|
"mode": "0000644",
|
|
7700
|
-
"sha256": "
|
|
7710
|
+
"sha256": "36e895c05e2aed3f97ce34ae3b3f73b0492fff7185077519068bc69dda6a8fd6"
|
|
7701
7711
|
},
|
|
7702
7712
|
{
|
|
7703
7713
|
"path": "public-docs/index.html",
|
|
@@ -8127,7 +8137,7 @@
|
|
|
8127
8137
|
{
|
|
8128
8138
|
"path": "scripts/gds/db-backup-offsite.sh",
|
|
8129
8139
|
"mode": "0000644",
|
|
8130
|
-
"sha256": "
|
|
8140
|
+
"sha256": "97f52f6aff95e1c81e069c1d4228eecb514af6068c810441465b5f5b7ee0f2ce"
|
|
8131
8141
|
},
|
|
8132
8142
|
{
|
|
8133
8143
|
"path": "scripts/gds/demote.js",
|
|
@@ -8802,7 +8812,7 @@
|
|
|
8802
8812
|
{
|
|
8803
8813
|
"path": "scripts/gds/run-unit-tests.js",
|
|
8804
8814
|
"mode": "0000644",
|
|
8805
|
-
"sha256": "
|
|
8815
|
+
"sha256": "a40a90ba6255820130aa6a94ddc314b643177e4a153750dc6290550a75e1a4e8"
|
|
8806
8816
|
},
|
|
8807
8817
|
{
|
|
8808
8818
|
"path": "scripts/gds/runner-drift.js",
|
|
@@ -9442,12 +9452,12 @@
|
|
|
9442
9452
|
{
|
|
9443
9453
|
"path": "src/bongos/db-kernel.js",
|
|
9444
9454
|
"mode": "0000644",
|
|
9445
|
-
"sha256": "
|
|
9455
|
+
"sha256": "dd16328ac569aa8c6af09b2700957634e2c6d368d291b015a74a8dc452dc340a"
|
|
9446
9456
|
},
|
|
9447
9457
|
{
|
|
9448
9458
|
"path": "src/bongos/db.js",
|
|
9449
9459
|
"mode": "0000644",
|
|
9450
|
-
"sha256": "
|
|
9460
|
+
"sha256": "def84475a0470dafb894e7ca3f96f9db27cea78964f90ac3e81ed748fe9efa48"
|
|
9451
9461
|
},
|
|
9452
9462
|
{
|
|
9453
9463
|
"path": "src/bongos/github-repos.js",
|
|
@@ -9702,7 +9712,7 @@
|
|
|
9702
9712
|
{
|
|
9703
9713
|
"path": "src/module-api.js",
|
|
9704
9714
|
"mode": "0000644",
|
|
9705
|
-
"sha256": "
|
|
9715
|
+
"sha256": "541b09991f4858b3255f250ef7e52e86b0d561ec69d058e74bc218fe1a104932"
|
|
9706
9716
|
},
|
|
9707
9717
|
{
|
|
9708
9718
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9844,6 +9854,11 @@
|
|
|
9844
9854
|
"mode": "0000644",
|
|
9845
9855
|
"sha256": "eea8f9e0bdf2c84fabc81e16c30e4026a7a336c1e705d614e68b805957b9b74a"
|
|
9846
9856
|
},
|
|
9857
|
+
{
|
|
9858
|
+
"path": "tests/agents_cron_trigger.mjs",
|
|
9859
|
+
"mode": "0000644",
|
|
9860
|
+
"sha256": "be78fa37657214c39a4880d2e1b44a1647c8b509d63262ca9a4a334431d7bdde"
|
|
9861
|
+
},
|
|
9847
9862
|
{
|
|
9848
9863
|
"path": "tests/agents_event_router.mjs",
|
|
9849
9864
|
"mode": "0000644",
|
|
@@ -9877,7 +9892,7 @@
|
|
|
9877
9892
|
{
|
|
9878
9893
|
"path": "tests/agents_validate.mjs",
|
|
9879
9894
|
"mode": "0000644",
|
|
9880
|
-
"sha256": "
|
|
9895
|
+
"sha256": "164059d69fabbd5dd70a29b29c524b5359f0346b71091e73a5e4fafa5dbc0804"
|
|
9881
9896
|
},
|
|
9882
9897
|
{
|
|
9883
9898
|
"path": "tests/agents_write_routes.mjs",
|
|
@@ -10082,7 +10097,7 @@
|
|
|
10082
10097
|
{
|
|
10083
10098
|
"path": "tests/auth_session_expiry_db.mjs",
|
|
10084
10099
|
"mode": "0000644",
|
|
10085
|
-
"sha256": "
|
|
10100
|
+
"sha256": "89730864ff5aa36271582c7e2717bf434bc84cbc20b615026005a1ef662ff31c"
|
|
10086
10101
|
},
|
|
10087
10102
|
{
|
|
10088
10103
|
"path": "tests/auth_session_ttl.mjs",
|
|
@@ -11347,7 +11362,7 @@
|
|
|
11347
11362
|
{
|
|
11348
11363
|
"path": "tests/government_live_proof.mjs",
|
|
11349
11364
|
"mode": "0000644",
|
|
11350
|
-
"sha256": "
|
|
11365
|
+
"sha256": "ca29425b30e726e6a7e939b008f0e5ca71877fa88fdcbfe1bf6356482f47c1e8"
|
|
11351
11366
|
},
|
|
11352
11367
|
{
|
|
11353
11368
|
"path": "tests/government_live_proof_shape.mjs",
|
|
@@ -12362,7 +12377,7 @@
|
|
|
12362
12377
|
{
|
|
12363
12378
|
"path": "tests/own_session_revoke.mjs",
|
|
12364
12379
|
"mode": "0000644",
|
|
12365
|
-
"sha256": "
|
|
12380
|
+
"sha256": "ff24d3753a52a64f426e0602bc2411aad13c1426c2bc0774b7618a1d20cebfad"
|
|
12366
12381
|
},
|
|
12367
12382
|
{
|
|
12368
12383
|
"path": "tests/package_core.mjs",
|
|
@@ -13224,6 +13239,16 @@
|
|
|
13224
13239
|
"mode": "0000644",
|
|
13225
13240
|
"sha256": "a630ac72981f230e8aa6609152765c13e8928ac329cb0ff3fe615371f31208d2"
|
|
13226
13241
|
},
|
|
13242
|
+
{
|
|
13243
|
+
"path": "tests/session_token_hash.mjs",
|
|
13244
|
+
"mode": "0000644",
|
|
13245
|
+
"sha256": "ff4cea59acb15ce383d9346793206865f0aababc5e2f4144c957bb93d45f62b4"
|
|
13246
|
+
},
|
|
13247
|
+
{
|
|
13248
|
+
"path": "tests/session_token_hash_db.mjs",
|
|
13249
|
+
"mode": "0000644",
|
|
13250
|
+
"sha256": "4a757c27f11cde314649e46ab24e51600d24c2f503f1aa7f055cdb4fa60ea6ce"
|
|
13251
|
+
},
|
|
13227
13252
|
{
|
|
13228
13253
|
"path": "tests/session_type.mjs",
|
|
13229
13254
|
"mode": "0000644",
|
|
@@ -2248,5 +2248,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2248
2248
|
landed since 1.19.885 with no explicit bump. run 35467975408. (task 1002620)
|
|
2249
2249
|
1.19.887 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2250
2250
|
landed since 1.19.886 with no explicit bump. run 35468511941. (task 1002620)
|
|
2251
|
+
1.19.888 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2252
|
+
landed since 1.19.887 with no explicit bump. run 35468948342. (task 1002620)
|
|
2253
|
+
1.19.889 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2254
|
+
landed since 1.19.888 with no explicit bump. run 35470239834. (task 1002620)
|
|
2251
2255
|
---------------------------------------------------------------------------
|
|
2252
2256
|
```
|
|
@@ -46,8 +46,33 @@ gds-db-offsite.timer ──→ gds-db-offsite.service ──→ db-backup-offsit
|
|
|
46
46
|
- **It does not take its own `pg_dump`.** It uploads the newest `/var/backups/gds/<db>-*.sql.gz` that the local nightly already produced. Runs at 05:00 UTC — one hour after the 04:00 local dump — so the copy is same-day-fresh.
|
|
47
47
|
- **Bucket layout:** objects land under the `gds-offsite/` key prefix in the Space.
|
|
48
48
|
- **Retention:** 4 weekly copies (`GDS_OFFSITE_KEEP=4` — ~1 month), pruned oldest-first under that prefix. Sub-1 MB each.
|
|
49
|
-
- **Credentials** live at `/etc/example/spaces.env` (chmod 600, owned by `lars`), never in the repo (ADR 0022). Required vars: `SPACES_KEY`, `SPACES_SECRET`, `SPACES_BUCKET`, `SPACES_REGION`, `SPACES_ENDPOINT`. systemd injects them via `EnvironmentFile`; a manual run sources the same file.
|
|
50
|
-
- **Dependency:** `python3` + the `boto3` module on the droplet. The script fails loudly if
|
|
49
|
+
- **Credentials** live at `/etc/example/spaces.env` (chmod 600, owned by `lars`), never in the repo (ADR 0022). Required vars: `SPACES_KEY`, `SPACES_SECRET`, `SPACES_BUCKET`, `SPACES_REGION`, `SPACES_ENDPOINT`, `GDS_OFFSITE_AGE_RECIPIENT`. systemd injects them via `EnvironmentFile`; a manual run sources the same file.
|
|
50
|
+
- **Dependency:** `age`, plus `python3` + the `boto3` module on the droplet. The script fails loudly if any is missing.
|
|
51
|
+
- **The uploaded object is encrypted** (`<db>-<date>.sql.gz.age`) and the retention count applies to those. Any legacy plaintext `.sql.gz` object still under the prefix is **deleted on the next run**, outside the retention count — see below.
|
|
52
|
+
|
|
53
|
+
#### Offsite dumps are encrypted (task [#1003372](https://cloudbongos.com/builders#/task/1003372), audit A1)
|
|
54
|
+
|
|
55
|
+
Until this task the weekly job uploaded the raw `pg_dump`. A dump is not merely "a backup": it is every secret the platform holds, in one file, on storage the platform does not own — OAuth credentials, the GitHub App private key, and (before `core_249`) every builder's live session bearer in plaintext. A breach of the Space, a mis-set bucket ACL or a leaked read-only S3 key was a total compromise with no trace in any log we keep.
|
|
56
|
+
|
|
57
|
+
The dump is now encrypted with [`age`](https://github.com/FiloSottile/age) **to a public key**, so the droplet can produce backups it cannot itself read. That asymmetry is the point: an attacker who owns the droplet already has the live database, but they do not also get the archive of every previous state, and they cannot decrypt what is already in the Space. **Keep the private key off the server.**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# ONE-TIME, on a machine that is NOT the server:
|
|
61
|
+
age-keygen -o bongos-backup.key # store safe + OFFLINE — this is the only way back
|
|
62
|
+
# it prints: Public key: age1xxxxxxxx...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Add the **public** key to `/etc/example/spaces.env` as `GDS_OFFSITE_AGE_RECIPIENT=age1...`.
|
|
66
|
+
|
|
67
|
+
> **Without a recipient the job refuses to run**, and does not fall back to a plaintext upload — that fallback is the vulnerability, and a silent one, since a backup that "worked" looks identical either way. A failing timer is visible in `systemctl status gds-db-offsite.timer`; an unencrypted dump in a bucket is visible to nobody until it matters. **If offsite backups stop after this deploy, this is why** — set the recipient.
|
|
68
|
+
>
|
|
69
|
+
> **The first successful run also PURGES every unencrypted `.sql.gz` already in the Space**, unconditionally and outside the retention count. Encrypting from here on does nothing about the copies already up there, and retaining four of them "for safety" would retain the finding. Local nightly dumps on the droplet are untouched, so this removes copies, never the last copy.
|
|
70
|
+
|
|
71
|
+
To restore an offsite object:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
age -d -i bongos-backup.key <db>-<date>.sql.gz.age | gunzip | psql <db>
|
|
75
|
+
```
|
|
51
76
|
|
|
52
77
|
> **History:** an earlier task ([#7](https://example.com/builders#/task/7)) shipped a `scripts/backup.sh` + `/etc/cron.d/otb-backup` daily-to-Spaces cron that was forgotten and never worked (empty credentials, no Space, no log). [#415](https://example.com/builders#/task/415) retired it and replaced it with the systemd unit above. If you find any trace of `otb-backup.cron`, `scripts/backup.sh`, or `~/.config/otb/spaces.env` on a host, it's dead — remove it.
|
|
53
78
|
>
|
|
@@ -65,6 +90,9 @@ SPACES_SECRET=...
|
|
|
65
90
|
SPACES_BUCKET=otb-gds-backups
|
|
66
91
|
SPACES_REGION=nyc3
|
|
67
92
|
SPACES_ENDPOINT=https://nyc3.digitaloceanspaces.com
|
|
93
|
+
# The age PUBLIC key from `age-keygen`, generated OFF this box (see above).
|
|
94
|
+
# Without it the job refuses to run rather than upload plaintext.
|
|
95
|
+
GDS_OFFSITE_AGE_RECIPIENT=age1...
|
|
68
96
|
ENV
|
|
69
97
|
sudo chmod 600 /etc/example/spaces.env
|
|
70
98
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
-- core_249 — builder_sessions stores a HASH of the bearer, never the bearer
|
|
2
|
+
-- (task 1003372, audit ref A1 from the 2026-08-29 security audit, goal 1000084).
|
|
3
|
+
--
|
|
4
|
+
-- forward-unsafe: drops builder_sessions.token and makes token_hash NOT NULL.
|
|
5
|
+
-- Both are the point of the migration, not incidental to it. The whole finding is
|
|
6
|
+
-- that the plaintext bearer is READABLE AT REST, so a version of this that kept the
|
|
7
|
+
-- column would not fix anything; and the NOT NULL is what stops a future writer
|
|
8
|
+
-- inserting a session no lookup can ever match. Neither is reversible, and neither
|
|
9
|
+
-- should be: rolling back to code that reads `token` finds it gone, but a variant
|
|
10
|
+
-- that merely emptied the column would leave that code finding NULL and signing
|
|
11
|
+
-- everyone out just the same — so the unsafety is in the rollback direction only,
|
|
12
|
+
-- and is identical either way. Forward, nobody is signed out: the backfill below
|
|
13
|
+
-- derives every existing row's hash from the plaintext before the column goes.
|
|
14
|
+
--
|
|
15
|
+
-- WHAT WAS WRONG. `token` held the live bearer in plaintext, NOT NULL UNIQUE and
|
|
16
|
+
-- INDEXED. Every row was a directly replayable credential for the builder it
|
|
17
|
+
-- belonged to — Archons included — so ANY read of this table was a complete
|
|
18
|
+
-- takeover of every account holding a session. That is the exact credential the
|
|
19
|
+
-- whole ADR 0016 trust boundary rests on: the server re-resolves authority per
|
|
20
|
+
-- request from a token, so a token IS the authority. `token_hash` had zero
|
|
21
|
+
-- occurrences repo-wide before this migration.
|
|
22
|
+
--
|
|
23
|
+
-- A read of the table is not hypothetical. The offsite dump was uploaded
|
|
24
|
+
-- unencrypted (same task, `scripts/gds/db-backup-offsite.sh`), and a co-tenant
|
|
25
|
+
-- Postgres role reached this table across tenants until task 1003369 split the
|
|
26
|
+
-- unix accounts. Hashing at rest is what makes those reads worthless rather than
|
|
27
|
+
-- catastrophic.
|
|
28
|
+
--
|
|
29
|
+
-- WHY A BACKFILL AND NOT A FORCED RE-AUTH. The hash is computable from the
|
|
30
|
+
-- plaintext we already hold, so every live session survives the change: the row
|
|
31
|
+
-- keeps working and its holder notices nothing. The alternative — ship the code
|
|
32
|
+
-- and let sessions re-mint — would sign out every builder and every dev box on
|
|
33
|
+
-- deploy day to reach a state this statement reaches for free. Migration 035 and
|
|
34
|
+
-- core_243 both reasoned this way in their own direction: do not put a policy
|
|
35
|
+
-- change on the backs of the people holding live sessions when it lands.
|
|
36
|
+
--
|
|
37
|
+
-- WHY sha256 WITH NO EXTENSION. Postgres 16 (docs/architecture.md) carries
|
|
38
|
+
-- `sha256(bytea)` as a built-in, so the backfill needs no pgcrypto — one less
|
|
39
|
+
-- extension to have installed on a box before a migration will run. It is the
|
|
40
|
+
-- same digest `db-kernel.js` computes in Node, so a row backfilled here and a row
|
|
41
|
+
-- minted by the code resolve identically.
|
|
42
|
+
--
|
|
43
|
+
-- WHY A FAST HASH IS THE RIGHT ONE. A session token is 256 bits of CSPRNG, not a
|
|
44
|
+
-- password: there is no dictionary to run and nothing to slow an attacker down,
|
|
45
|
+
-- so bcrypt/argon would buy nothing and cost a KDF on every authenticated
|
|
46
|
+
-- request. The same reasoning is written out at
|
|
47
|
+
-- `modules/platform-identity/platform-identity.js` (`sha256hex`), which hashes
|
|
48
|
+
-- its client secrets and authorization codes for exactly this reason.
|
|
49
|
+
--
|
|
50
|
+
-- WHY THE PLAINTEXT COLUMN IS DROPPED, NOT NULLED. Nulling it would leave a dead
|
|
51
|
+
-- column that still has to be kept empty by every future writer, and it buys no
|
|
52
|
+
-- rollback safety: rolling back to code that reads `token` finds it NULL and
|
|
53
|
+
-- signs everyone out either way. Dropping it is the honest end state, and the
|
|
54
|
+
-- unique index moves to `token_hash` so the uniqueness guarantee is unbroken.
|
|
55
|
+
--
|
|
56
|
+
-- WHAT THIS DOES NOT CHANGE. Expiry is untouched. ADR 0294 (CLI sessions do not
|
|
57
|
+
-- expire) and ADR 0206 (no absolute ceiling) both stand exactly as written; this
|
|
58
|
+
-- migration does not read, write or reason about `expires_at`. The audit also
|
|
59
|
+
-- asked for refresh-on-use to stop and an absolute lifetime to be added — both are
|
|
60
|
+
-- decisions those two ADRs have already taken the other way, with the owner as
|
|
61
|
+
-- decider, AFTER this audit was filed. Changing them is not this task's to do.
|
|
62
|
+
--
|
|
63
|
+
-- IDEMPOTENT. The whole body is guarded on the plaintext column still existing,
|
|
64
|
+
-- so a second run is a no-op rather than an error about a column that is gone.
|
|
65
|
+
|
|
66
|
+
BEGIN;
|
|
67
|
+
|
|
68
|
+
DO $$
|
|
69
|
+
BEGIN
|
|
70
|
+
IF EXISTS (
|
|
71
|
+
SELECT 1 FROM information_schema.columns
|
|
72
|
+
WHERE table_name = 'builder_sessions' AND column_name = 'token'
|
|
73
|
+
) THEN
|
|
74
|
+
|
|
75
|
+
ALTER TABLE builder_sessions ADD COLUMN IF NOT EXISTS token_hash text;
|
|
76
|
+
|
|
77
|
+
-- Every live row keeps working: the hash is derived from the bearer already
|
|
78
|
+
-- stored, so no holder is signed out by this deploy.
|
|
79
|
+
UPDATE builder_sessions
|
|
80
|
+
SET token_hash = encode(sha256(token::bytea), 'hex')
|
|
81
|
+
WHERE token_hash IS NULL;
|
|
82
|
+
|
|
83
|
+
-- The invariant the lookup depends on. Enforced only after the backfill has
|
|
84
|
+
-- given every existing row a value.
|
|
85
|
+
ALTER TABLE builder_sessions ALTER COLUMN token_hash SET NOT NULL;
|
|
86
|
+
|
|
87
|
+
-- Carries over `token`'s UNIQUE: two sessions may not share a bearer, and the
|
|
88
|
+
-- hash is what the lookup now keys on, so it needs the same index the
|
|
89
|
+
-- plaintext had for the same reason.
|
|
90
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_builder_sessions_token_hash
|
|
91
|
+
ON builder_sessions (token_hash);
|
|
92
|
+
|
|
93
|
+
DROP INDEX IF EXISTS idx_builder_sessions_token;
|
|
94
|
+
ALTER TABLE builder_sessions DROP COLUMN token;
|
|
95
|
+
|
|
96
|
+
END IF;
|
|
97
|
+
END
|
|
98
|
+
$$;
|
|
99
|
+
|
|
100
|
+
COMMENT ON COLUMN builder_sessions.token_hash IS
|
|
101
|
+
'SHA-256 hex of the bearer token. The plaintext is NEVER stored (core_249, audit A1): it exists only in the response that mints it and in the client that holds it. lookupSession hashes the presented bearer and keys on this column, so a read of this table yields nothing replayable.';
|
|
102
|
+
|
|
103
|
+
COMMIT;
|
|
@@ -48,8 +48,31 @@
|
|
|
48
48
|
const EVENT_CATALOG = Object.freeze(['task.shipped', 'idea.graded.full']);
|
|
49
49
|
|
|
50
50
|
// Mirrors agents_definitions_trigger_type_chk. 'event' consumes the catalog above;
|
|
51
|
-
// 'on-demand' is the historian shape (a builder asks, the agent answers)
|
|
52
|
-
|
|
51
|
+
// 'on-demand' is the historian shape (a builder asks, the agent answers); 'cron'
|
|
52
|
+
// fires on a cadence and consumes CADENCES below (task 1004008, agents_005).
|
|
53
|
+
const TRIGGER_TYPES = Object.freeze(['event', 'on-demand', 'cron']);
|
|
54
|
+
|
|
55
|
+
// The cadence vocabulary a 'cron' trigger may declare, in trigger_spec.cadence.
|
|
56
|
+
//
|
|
57
|
+
// DUPLICATED FROM scripts/gds/routine-schedule.js ON PURPOSE, for the reason
|
|
58
|
+
// RANK_ORDER below is duplicated: a module may require only src/module-api.js
|
|
59
|
+
// (ADR 0083), so it cannot reach into scripts/. The two copies are kept honest by
|
|
60
|
+
// tests/agents_cron_trigger.mjs, which reads routine-schedule.js's own CADENCES +
|
|
61
|
+
// isUniformDivisor and asserts this file agrees — a drifting copy fails there
|
|
62
|
+
// rather than at some future tick.
|
|
63
|
+
//
|
|
64
|
+
// N MUST DIVIDE 60 for the sub-hourly form. A cron step restarts each hour, so
|
|
65
|
+
// only a divisor yields a uniform period: `*/15` fires :00 :15 :30 :45 always,
|
|
66
|
+
// but `*/7` fires :00…:56 then waits 4 — a "7min" cadence that is not one.
|
|
67
|
+
const CADENCES = Object.freeze(['hourly', 'daily', 'weekly', 'monthly']);
|
|
68
|
+
const SUBHOURLY_RE = /^([1-9]\d*)min$/;
|
|
69
|
+
function isCadence(name) {
|
|
70
|
+
if (CADENCES.includes(name)) return true;
|
|
71
|
+
const m = SUBHOURLY_RE.exec(String(name == null ? '' : name));
|
|
72
|
+
if (!m) return false;
|
|
73
|
+
const n = Number(m[1]);
|
|
74
|
+
return Number.isInteger(n) && n > 0 && n < 60 && 60 % n === 0;
|
|
75
|
+
}
|
|
53
76
|
|
|
54
77
|
// Mirrors agents_definitions_model_tier_chk. No 'main' tier by design — an agent
|
|
55
78
|
// is a subagent, never the main session.
|
|
@@ -222,6 +245,29 @@ function validateAgentDefinition(obj, opts = {}) {
|
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
|
|
248
|
+
// --- the cadence, fail-closed, the same shape as the event catalog above ----
|
|
249
|
+
// agents_005's CHECK pins that a cron definition HAS a non-empty cadence; the
|
|
250
|
+
// vocabulary is pinned here, because the sub-hourly rule is a predicate and a
|
|
251
|
+
// CHECK spelling it out would be a second copy free to drift.
|
|
252
|
+
//
|
|
253
|
+
// The reach is deliberately the EVENT BLOCK'S reach, no more: an UNWIRED
|
|
254
|
+
// cadence is refused wherever it appears, including on a non-cron definition
|
|
255
|
+
// where it is dead config. A VALID cadence on a non-cron definition is ACCEPTED,
|
|
256
|
+
// exactly as a wired `event:` on an on-demand definition is — harmless, unread,
|
|
257
|
+
// and not this validator's call to police. Saying otherwise in a comment would
|
|
258
|
+
// describe a guard that is not here.
|
|
259
|
+
if (spec) {
|
|
260
|
+
const cadence = safeGet(spec, 'cadence');
|
|
261
|
+
const hasCadenceKey = cadence !== undefined && cadence !== null;
|
|
262
|
+
if (triggerType === 'cron' && !isNonEmptyString(cadence)) {
|
|
263
|
+
E('trigger_cadence_missing', 'trigger_spec.cadence',
|
|
264
|
+
`trigger_spec.cadence: required for a 'cron' trigger — a definition with no cadence can never fire. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60.`);
|
|
265
|
+
} else if (hasCadenceKey && !isCadence(cadence)) {
|
|
266
|
+
E('trigger_cadence_unwired', 'trigger_spec.cadence',
|
|
267
|
+
`trigger_spec.cadence: ${JSON.stringify(cadence)} is not a cadence. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60 (so the declared period and the realized one are the same claim).`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
225
271
|
// --- model tier -----------------------------------------------------------
|
|
226
272
|
const rawTier = safeGet(obj, 'model_tier');
|
|
227
273
|
const modelTier = rawTier === undefined || rawTier === null ? 'default' : rawTier;
|
|
@@ -355,6 +401,7 @@ function validateAgentDefinition(obj, opts = {}) {
|
|
|
355
401
|
module.exports = {
|
|
356
402
|
validateAgentDefinition,
|
|
357
403
|
EVENT_CATALOG,
|
|
404
|
+
CADENCES, isCadence,
|
|
358
405
|
TRIGGER_TYPES,
|
|
359
406
|
MODEL_TIERS,
|
|
360
407
|
RANK_ORDER,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- agents_005_cron_trigger.sql — make 'cron' a trigger type that can exist (task 1004008).
|
|
2
|
+
--
|
|
3
|
+
-- Tasks 1002498 (the cron-router poller) and 1002501 (the cron roster) both ask
|
|
4
|
+
-- for a timer whose each tick SELECTS DUE CRON AGENTS. There are none and there
|
|
5
|
+
-- can be none: agents_001 pinned trigger_type to ('event','on-demand') on BOTH
|
|
6
|
+
-- agents_definitions and agents_runs, and lib/validate.js mirrors it. Build the
|
|
7
|
+
-- poller against that and you get a timer that is correct, tested, and selects
|
|
8
|
+
-- from an empty set by construction — green tests over an unreachable branch,
|
|
9
|
+
-- which is a false claim of coverage rather than coverage. This migration is the
|
|
10
|
+
-- enabling half; the poller is 1002498's and is deliberately NOT built here.
|
|
11
|
+
--
|
|
12
|
+
-- WIDENING A FAIL-CLOSED ENUM IS THE POINT, NOT A WORKAROUND. agents_001's header
|
|
13
|
+
-- calls these enums fail-closed on purpose: "an unwired trigger name is refused,
|
|
14
|
+
-- never silently stored". That rule is kept — 'cron' stops being unwired in the
|
|
15
|
+
-- same change that makes it storable, because lib/validate.js is widened and
|
|
16
|
+
-- constrained alongside. What is still true after this: a trigger name outside
|
|
17
|
+
-- the three is refused at both the validator and the constraint.
|
|
18
|
+
--
|
|
19
|
+
-- BOTH TABLES, AND WHY THE LEDGER MATTERS AS MUCH AS THE DEFINITION. agents_001
|
|
20
|
+
-- constrains trigger_type on agents_runs too. Widen only the definition and a
|
|
21
|
+
-- cron agent could be stored and enabled, and then fail at the moment it fires,
|
|
22
|
+
-- when the ledger insert violates its own CHECK — the worst shape available: a
|
|
23
|
+
-- definition that looks armed and cannot record a single run.
|
|
24
|
+
--
|
|
25
|
+
-- A CRON DEFINITION MUST SAY WHEN. agents_001 already refuses an event definition
|
|
26
|
+
-- with no event name ("a definition that can never fire"). A cron definition with
|
|
27
|
+
-- no cadence is the same defect wearing a different key, so it gets the same
|
|
28
|
+
-- treatment rather than being left for the poller to interpret at tick time.
|
|
29
|
+
-- The cadence VOCABULARY is validated in lib/validate.js, not here: the named set
|
|
30
|
+
-- ('hourly','daily','weekly','monthly') plus the open-ended sub-hourly form
|
|
31
|
+
-- ('15min') is a predicate, and a CHECK that tried to spell the sub-hourly rule —
|
|
32
|
+
-- N must divide 60 — would be a second, drifting copy of it. The constraint
|
|
33
|
+
-- pins the SHAPE (a non-empty cadence key is present); the validator pins the
|
|
34
|
+
-- VALUE. That is the same division agents_001 draws for `event`.
|
|
35
|
+
--
|
|
36
|
+
-- Module migrations are ADDITIVE and NAMESPACED (ADR 0083 §Decision #5): every
|
|
37
|
+
-- object is agents_-prefixed, there is no down-migration, and the filename is
|
|
38
|
+
-- agents_-prefixed (ADR 0108 §5). Idempotent: safe to re-run. No existing row
|
|
39
|
+
-- changes — widening a CHECK can only make rows MORE legal, so nothing that
|
|
40
|
+
-- validates today stops validating.
|
|
41
|
+
|
|
42
|
+
BEGIN;
|
|
43
|
+
|
|
44
|
+
DO $$
|
|
45
|
+
BEGIN
|
|
46
|
+
-- agents_definitions: the three WIRED trigger types.
|
|
47
|
+
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_type_chk') THEN
|
|
48
|
+
ALTER TABLE agents_definitions DROP CONSTRAINT agents_definitions_trigger_type_chk;
|
|
49
|
+
END IF;
|
|
50
|
+
ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_type_chk
|
|
51
|
+
CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
|
|
52
|
+
|
|
53
|
+
-- agents_runs: the same three, so a cron fire can be RECORDED and not only run.
|
|
54
|
+
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_runs_trigger_type_chk') THEN
|
|
55
|
+
ALTER TABLE agents_runs DROP CONSTRAINT agents_runs_trigger_type_chk;
|
|
56
|
+
END IF;
|
|
57
|
+
ALTER TABLE agents_runs ADD CONSTRAINT agents_runs_trigger_type_chk
|
|
58
|
+
CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
|
|
59
|
+
|
|
60
|
+
-- The twin of agents_definitions_trigger_spec_event_chk: a cron definition
|
|
61
|
+
-- carries a non-empty cadence, or it is a definition that can never fire.
|
|
62
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_spec_cadence_chk') THEN
|
|
63
|
+
ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_spec_cadence_chk
|
|
64
|
+
CHECK (trigger_type <> 'cron'
|
|
65
|
+
OR (trigger_spec ? 'cadence' AND length(trigger_spec ->> 'cadence') > 0));
|
|
66
|
+
END IF;
|
|
67
|
+
END $$;
|
|
68
|
+
|
|
69
|
+
-- agents_001 indexes (trigger_type, trigger_spec->>'event') for the event
|
|
70
|
+
-- router's "who wants this event" read. The cron router's read is a different
|
|
71
|
+
-- question — "which cron agents are due" — and it is answered by scanning the
|
|
72
|
+
-- cron rows, which are few. Adding a cadence index now would be indexing for a
|
|
73
|
+
-- query no committed code makes; 1002498 adds one if its poller needs it.
|
|
74
|
+
|
|
75
|
+
COMMENT ON CONSTRAINT agents_definitions_trigger_type_chk ON agents_definitions IS
|
|
76
|
+
'The WIRED trigger vocabulary (task 1004008 added cron). Mirrored by TRIGGER_TYPES in modules/agents/lib/validate.js — widen both together or the two disagree about what can exist.';
|
|
77
|
+
|
|
78
|
+
COMMIT;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.889",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.889",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.889",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|