@bongos/core 1.25.4 → 1.25.5

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.25.4",
6
- "core_contract": "1.25.4",
7
- "source_commit": "54f92d0726f9d8abd3e0e893a0122299963078fc",
5
+ "core_version": "1.25.5",
6
+ "core_contract": "1.25.5",
7
+ "source_commit": "095393e44481a16a8764084c02bc478f88127826",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-12T23:46:04.539Z",
9
+ "built_at": "2026-11-13T04:03:13.962Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 654,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3992,
14
+ "functional_verbatim": 3996,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4679,
20
- "tree_sha256": "37542ae5f076d40f03deb3171f2900d791d77910c8b0e008ac9ef0c94ac9a96a",
19
+ "file_count": 4683,
20
+ "tree_sha256": "7bee86259715879e702ebce474621fdcacaefdcbfec4f526604e05b59c9b3484",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -332,12 +332,12 @@
332
332
  {
333
333
  "path": "clients/bongos-client/README.md",
334
334
  "mode": "0000644",
335
- "sha256": "ed95879a40c6624dda1681930638ce528645de0de79003c4622e8edf97a9cfec"
335
+ "sha256": "f708a93778599fa4e5598d265766436184beb03b1ab2876aecd68c41eeab6358"
336
336
  },
337
337
  {
338
338
  "path": "clients/bongos-client/bongos-client.global.js",
339
339
  "mode": "0000644",
340
- "sha256": "534f04acb46cdf1a13ebbaf15af37488b93daba3eef39ab05c827ebeeebc551f"
340
+ "sha256": "01e1b81c1bd594dd0d2f044dfa2a366aa0b185dde4cd041001c551742e1b52d3"
341
341
  },
342
342
  {
343
343
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -347,17 +347,17 @@
347
347
  {
348
348
  "path": "clients/bongos-client/index.cjs",
349
349
  "mode": "0000644",
350
- "sha256": "d4a8ca745ce3c3aa7ee83e3b6dad2071d9bb31d82104002c0f8d62785a24fd8f"
350
+ "sha256": "1e8e8ff332ccdd9fcf9f5b0847b6a7938903bf9564abc9a1e3c1070682afd1c7"
351
351
  },
352
352
  {
353
353
  "path": "clients/bongos-client/index.d.ts",
354
354
  "mode": "0000644",
355
- "sha256": "e7ce03b5c6d9c370c9fa0380911a83c17945c3eb9535252b4d396da5675149cf"
355
+ "sha256": "9fa2ee83d1d6b068f727c32f510dd9fdf1bd98d6caa1a401e9b263fd5af5eea9"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/index.mjs",
359
359
  "mode": "0000644",
360
- "sha256": "2f6c7c1de4f5fe6abb98aec6993ef7fa212f42dcf513b33f82213578765cc952"
360
+ "sha256": "6e9d62b3dcf0e8e760afa2aab9afcd0470ae4a1f649918486f8839ba12fd18c8"
361
361
  },
362
362
  {
363
363
  "path": "clients/bongos-client/package.json",
@@ -2497,12 +2497,12 @@
2497
2497
  {
2498
2498
  "path": "docs/api-reference.md",
2499
2499
  "mode": "0000644",
2500
- "sha256": "63bf723ba632d53f26bc2850623f0ce85d40fabcb65aadd693808663c5822ded"
2500
+ "sha256": "2ad27bf8ccbe4b3349d7f45ac2756bb314478eac2563239dd708cc82b7871e6a"
2501
2501
  },
2502
2502
  {
2503
2503
  "path": "docs/api/openapi.json",
2504
2504
  "mode": "0000644",
2505
- "sha256": "4b1c9bdd62f88e916be092ecffe6b70c90ac08fc709afa372ce992c88a2070f6"
2505
+ "sha256": "98b41ef963ce5a1009851a8d2cea6e421b02edb46c2f50a9cb809c2d1670bf6b"
2506
2506
  },
2507
2507
  {
2508
2508
  "path": "docs/architecture.md",
@@ -3087,7 +3087,7 @@
3087
3087
  {
3088
3088
  "path": "docs/module-api-changelog.md",
3089
3089
  "mode": "0000644",
3090
- "sha256": "14a5fc426f87175460ed546e7e421cfb6d1d7fc067b7ec5fe1cc16bbaa8b4651"
3090
+ "sha256": "6c8db5f3d03887690f13f153942bcd1a8485e51473bd6e8a8584fc9de13c46f5"
3091
3091
  },
3092
3092
  {
3093
3093
  "path": "docs/modules-contract.md",
@@ -10139,10 +10139,15 @@
10139
10139
  "mode": "0000644",
10140
10140
  "sha256": "ff20f9e15d437638b4122392f21283598681c0564bb50788f6fb7ec770722e35"
10141
10141
  },
10142
+ {
10143
+ "path": "modules/provisioning/migrations/provisioning_037_update_subscribed.sql",
10144
+ "mode": "0000644",
10145
+ "sha256": "bb1f63759204c9c8022bda6dc2619dc9f12d6e512c1c129a30ba8c6e9ec41ab6"
10146
+ },
10142
10147
  {
10143
10148
  "path": "modules/provisioning/module.json",
10144
10149
  "mode": "0000644",
10145
- "sha256": "a1ac12bb92dc303a819878b15f620f59c1edd4a4a3317eacc965117a9a8ce679"
10150
+ "sha256": "cf3310beddd37559b57a4fa996430d73e6957b297bfa675ed46148b926dd4150"
10146
10151
  },
10147
10152
  {
10148
10153
  "path": "modules/provisioning/onboard-plan.js",
@@ -10272,7 +10277,7 @@
10272
10277
  {
10273
10278
  "path": "modules/provisioning/routes/provisioning.js",
10274
10279
  "mode": "0000644",
10275
- "sha256": "c72d11bef0a5fd0f8bbf070dbc28152caac1b1babbdfc9de101172c4fc99d33d"
10280
+ "sha256": "955f854e4b90892aae4bfc58f258f96f72de06af35389362f61b204cef80bfaf"
10276
10281
  },
10277
10282
  {
10278
10283
  "path": "modules/provisioning/routes/render-standup.js",
@@ -10284,6 +10289,11 @@
10284
10289
  "mode": "0000644",
10285
10290
  "sha256": "70a66e07e1bec13759b7b41b76ef009931c674867a6fa5270aff7bf3f6d31ecc"
10286
10291
  },
10292
+ {
10293
+ "path": "modules/provisioning/routes/update-subscription.js",
10294
+ "mode": "0000644",
10295
+ "sha256": "6595fcab0d5de5a6281aae1ec3db376aa5cbdf8406e3dfbb4d750a7ff0be2a64"
10296
+ },
10287
10297
  {
10288
10298
  "path": "modules/provisioning/screening.js",
10289
10299
  "mode": "0000644",
@@ -12227,12 +12237,12 @@
12227
12237
  {
12228
12238
  "path": "package-lock.json",
12229
12239
  "mode": "0000644",
12230
- "sha256": "64aca3f420ab1c244e6e0da1c255f7c7a41c49f9370cda2ecebaa8a47d006da3"
12240
+ "sha256": "78d48441e049a15bee43f6853bc005719770cdb7144bb4b40404f06b87cbf65a"
12231
12241
  },
12232
12242
  {
12233
12243
  "path": "package.json",
12234
12244
  "mode": "0000644",
12235
- "sha256": "3b1217c94f2a73ef137b3cef6a150ffaded568479178ad1f0a8f4b57c0d1983f"
12245
+ "sha256": "cdec3c6aaf13f30b623f43c601a6ca5765a39d9408f35c0901335ea6d55ff3e6"
12236
12246
  },
12237
12247
  {
12238
12248
  "path": "public-docs/index.html",
@@ -12252,7 +12262,7 @@
12252
12262
  {
12253
12263
  "path": "release-notes.json",
12254
12264
  "mode": "0000644",
12255
- "sha256": "c29dbb324350fdc524b08c15fba8be85bdd78fb3a8759812f8335d83b3803ce1"
12265
+ "sha256": "678e09f8d3f0e83ad0c1b9e02421db99231530fb7b5d951e603e4257c1fa14e0"
12256
12266
  },
12257
12267
  {
12258
12268
  "path": "scripts/bongos/CLAUDE.md",
@@ -13877,12 +13887,12 @@
13877
13887
  {
13878
13888
  "path": "scripts/bongos/update-channel.js",
13879
13889
  "mode": "0000644",
13880
- "sha256": "f23e3667cc373215997c412e731e6e3252d9f9b85fa6f6b46024ba5d9dfb1bf9"
13890
+ "sha256": "b36cf627b9dd862f171822156dcb6d9623ba2d61a609125c75b38ab8dd6536d4"
13881
13891
  },
13882
13892
  {
13883
13893
  "path": "scripts/bongos/update-sweep.js",
13884
13894
  "mode": "0000644",
13885
- "sha256": "0b980f1b667ee73e52229bec63b40b8b50725c96178452df2508622724f50e8e"
13895
+ "sha256": "6aa3e527c2cb49ab147d8156a49bda9084131a708122b28eba797b090836048b"
13886
13896
  },
13887
13897
  {
13888
13898
  "path": "scripts/bongos/upgrade-artist-gate.js",
@@ -15707,7 +15717,7 @@
15707
15717
  {
15708
15718
  "path": "src/bongos/api-errors.js",
15709
15719
  "mode": "0000644",
15710
- "sha256": "031bc92f93daba9fd332449f69832261af8d9fbb83026cbe700a504531f5a61a"
15720
+ "sha256": "b3527d2014da3f141a6351f7c1daa2aec1ef43935b779a99d1d2a7faa21521ae"
15711
15721
  },
15712
15722
  {
15713
15723
  "path": "src/bongos/api-path-404.js",
@@ -15802,7 +15812,7 @@
15802
15812
  {
15803
15813
  "path": "src/bongos/db.js",
15804
15814
  "mode": "0000644",
15805
- "sha256": "5eb2da3ce4e2b2340cea3eb8b391c28415ea848ae8016504c100a3e8a90f445e"
15815
+ "sha256": "db2eefddf1d69a5ae5a43f1c6a5235f951ee1e23cca9ff4ce45910f1c7880a61"
15806
15816
  },
15807
15817
  {
15808
15818
  "path": "src/bongos/device-approval.js",
@@ -16012,7 +16022,7 @@
16012
16022
  {
16013
16023
  "path": "src/bongos/route-rank-check.js",
16014
16024
  "mode": "0000644",
16015
- "sha256": "c25614561abc45b7ab1f9e014e9b1ff7ee027d4928618a50d2f3d13d0b7a3773"
16025
+ "sha256": "374034d7015bccdb28729f328e752773219f8b703f89affe154c04fdb130865d"
16016
16026
  },
16017
16027
  {
16018
16028
  "path": "src/bongos/routes.js",
@@ -16057,7 +16067,7 @@
16057
16067
  {
16058
16068
  "path": "src/bongos/routes/builders.js",
16059
16069
  "mode": "0000644",
16060
- "sha256": "775ab4581124e9ccb0d4c195ade3147e65e301ca5c055fdeb3bf8fc68aa8b856"
16070
+ "sha256": "9fccaac46052db95614c66396719b7b710e5480e5b1381ba1677fb768239576e"
16061
16071
  },
16062
16072
  {
16063
16073
  "path": "src/bongos/routes/cli-commands.js",
@@ -16262,7 +16272,7 @@
16262
16272
  {
16263
16273
  "path": "src/module-api.js",
16264
16274
  "mode": "0000644",
16265
- "sha256": "bc5de69383b4a204e65b36fe8bf271bea18abdba915771abec35da1096614a91"
16275
+ "sha256": "020b1e30dd86433757c2939761aa01abe097e1e5aa6f1043d0a0050138b4d06c"
16266
16276
  },
16267
16277
  {
16268
16278
  "path": "src/module-loader/catalog.js",
@@ -17109,6 +17119,11 @@
17109
17119
  "mode": "0000644",
17110
17120
  "sha256": "215f53db8e7fd45d0f229ff7e67d8b1d16ec80b25e8d9f7070dfe65898b6cb8a"
17111
17121
  },
17122
+ {
17123
+ "path": "tests/builder_card_requirement.mjs",
17124
+ "mode": "0000644",
17125
+ "sha256": "612308a0650bc6bb7eedbd3340bb863f5893a907bc62ae4a8b2f49829820af71"
17126
+ },
17112
17127
  {
17113
17128
  "path": "tests/builder_needs.mjs",
17114
17129
  "mode": "0000644",
@@ -21714,6 +21729,11 @@
21714
21729
  "mode": "0000644",
21715
21730
  "sha256": "31f7a766772450f2f79a43126154f9840d9a22972f0fcc9399f71dfbf95c43a2"
21716
21731
  },
21732
+ {
21733
+ "path": "tests/provisioning_update_subscription_route.mjs",
21734
+ "mode": "0000644",
21735
+ "sha256": "5ffe5b0271b8a256da33617e30b20baf4a037523fc6e23298b6826fdb8fc3208"
21736
+ },
21717
21737
  {
21718
21738
  "path": "tests/provisioning_wizard_create_xenos.mjs",
21719
21739
  "mode": "0000644",
@@ -23117,7 +23137,7 @@
23117
23137
  {
23118
23138
  "path": "tests/update_channel_db.mjs",
23119
23139
  "mode": "0000644",
23120
- "sha256": "2f4e16a3bffef6bf1d3dccc04a325c694760faf19c70e3678d58c21c17a851a7"
23140
+ "sha256": "ee73e3a43fc0b9133b72a032b1ffc88f5be2a8f4abcb39333a58e4a2c47aaedd"
23121
23141
  },
23122
23142
  {
23123
23143
  "path": "tests/update_pill.mjs",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 699 operations across 85 resource groups
8
+ - 701 operations across 85 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -293,6 +293,8 @@ function createClient(opts = {}) {
293
293
  patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
294
294
  // PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
295
295
  patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
296
+ // PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
297
+ patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
296
298
  // GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
297
299
  getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
298
300
  // GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
@@ -1279,6 +1281,8 @@ function createClient(opts = {}) {
1279
1281
  postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
1280
1282
  // POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
1281
1283
  postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
1284
+ // POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
1285
+ postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
1282
1286
  // GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
1283
1287
  getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
1284
1288
  // GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
@@ -292,6 +292,8 @@ function createClient(opts = {}) {
292
292
  patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
293
293
  // PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
294
294
  patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
295
+ // PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
296
+ patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
295
297
  // GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
296
298
  getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
297
299
  // GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
@@ -1278,6 +1280,8 @@ function createClient(opts = {}) {
1278
1280
  postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
1279
1281
  // POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
1280
1282
  postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
1283
+ // POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
1284
+ postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
1281
1285
  // GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
1282
1286
  getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
1283
1287
  // GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
@@ -231,6 +231,8 @@ export interface PatchBuildersIdBudgetRequest { monthly_budget_usd?: unknown }
231
231
  export interface PatchBuildersIdBudgetResponse { builder: unknown }
232
232
  export interface PatchBuildersIdCapabilitiesRequest { server: boolean }
233
233
  export interface PatchBuildersIdCapabilitiesResponse { builder: unknown }
234
+ export interface PatchBuildersIdCardRequirementRequest { require_card: boolean }
235
+ export interface PatchBuildersIdCardRequirementResponse { builder: unknown }
234
236
  export interface PatchBuildersIdRankRequest { rank: string }
235
237
  export interface PatchBuildersIdRankResponse { builder: unknown; previous_rank: unknown; direction: unknown; credit_delta: unknown; broadcast: unknown }
236
238
  export interface PatchBuildersIdStatusRequest { status: "active" | "inactive"; reason?: string }
@@ -602,6 +604,8 @@ export interface PostProvisioningInstancesIdRenderResponse { ok: boolean; queued
602
604
  export interface PostProvisioningInstancesIdRepoPrivateResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
603
605
  export interface PostProvisioningInstancesIdRestartResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
604
606
  export interface PostProvisioningInstancesIdTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
607
+ export interface PostProvisioningInstancesIdUpdateSubscriptionRequest { subscribed: boolean }
608
+ export interface PostProvisioningInstancesIdUpdateSubscriptionResponse { ok: boolean; update_subscribed: unknown; previous: unknown; changed: unknown }
605
609
  export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; no_address?: boolean; tier?: string; onboard_mode?: string; type?: string; description?: string; detail?: Record<string, unknown>; modules?: string[]; terms_acknowledged?: boolean; demo?: Record<string, unknown>; ship_mode?: "auto" | "manual"; genesis?: boolean }
606
610
  export interface PostProvisioningPhysicsCheckRequest { description?: string; detail?: Record<string, unknown> }
607
611
  export interface PostProvisioningPhysicsCheckResponse { stop: boolean; message: unknown; soft_limits: unknown }
@@ -960,6 +964,8 @@ export interface BongosClient {
960
964
  patchBuildersIdBudget(args?: RequestArgs & { body?: PatchBuildersIdBudgetRequest }): Promise<PatchBuildersIdBudgetResponse>;
961
965
  /** PATCH /builders/{id}/capabilities — rank: archon */
962
966
  patchBuildersIdCapabilities(args: RequestArgs & { body: PatchBuildersIdCapabilitiesRequest }): Promise<PatchBuildersIdCapabilitiesResponse>;
967
+ /** PATCH /builders/{id}/card-requirement — rank: archon */
968
+ patchBuildersIdCardRequirement(args: RequestArgs & { body: PatchBuildersIdCardRequirementRequest }): Promise<PatchBuildersIdCardRequirementResponse>;
963
969
  /** GET /builders/{id}/onboarding — rank: public */
964
970
  getBuildersIdOnboarding(args?: RequestArgs): Promise<GetBuildersIdOnboardingResponse>;
965
971
  /** GET /builders/{id}/profile — rank: public */
@@ -1946,6 +1952,8 @@ export interface BongosClient {
1946
1952
  postProvisioningInstancesIdTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdTeardownResponse>;
1947
1953
  /** POST /provisioning/instances/{id}/update-channel — rank: archon */
1948
1954
  postProvisioningInstancesIdUpdateChannel(args?: RequestArgs): Promise<ApiResponse>;
1955
+ /** POST /provisioning/instances/{id}/update-subscription — rank: metic+archon */
1956
+ postProvisioningInstancesIdUpdateSubscription(args: RequestArgs & { body: PostProvisioningInstancesIdUpdateSubscriptionRequest }): Promise<PostProvisioningInstancesIdUpdateSubscriptionResponse>;
1949
1957
  /** GET /provisioning/logos/{file} — rank: public */
1950
1958
  getProvisioningLogosFile(args?: RequestArgs): Promise<ApiResponse>;
1951
1959
  /** GET /provisioning/onboard-plan — rank: public */
@@ -289,6 +289,8 @@ export function createClient(opts = {}) {
289
289
  patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
290
290
  // PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
291
291
  patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
292
+ // PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
293
+ patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
292
294
  // GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
293
295
  getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
294
296
  // GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
@@ -1275,6 +1277,8 @@ export function createClient(opts = {}) {
1275
1277
  postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
1276
1278
  // POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
1277
1279
  postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
1280
+ // POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
1281
+ postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
1278
1282
  // GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
1279
1283
  getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
1280
1284
  // GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
@@ -4324,6 +4324,72 @@
4324
4324
  ]
4325
4325
  }
4326
4326
  },
4327
+ "/builders/{id}/card-requirement": {
4328
+ "patch": {
4329
+ "operationId": "patch_builders_id_card_requirement",
4330
+ "tags": [
4331
+ "builders"
4332
+ ],
4333
+ "summary": "PATCH /builders/:id/card-requirement",
4334
+ "description": "PATCH /builders/:id/card-requirement — an Archon switches the hardware-card requirement on or off for one builder (task 1004748, ADR 0382 D4). Body: { require_card: boolean }. Default is off for everyone. Switching ON refuses 409 no_card_registered unless the builder has a registered card, so the switch cannot lock anyone out. You cannot switch OFF your own requirement (403 cannot_disable_own_card_requirement) — a stolen session must not be able to drop it; another Archon does it. Every change writes an audit row naming the target. rank: archon. Gate: requirePermission('builder.rank.set') (the non-delegable atom that guards the step-up's own target route) then requireCardStepUp; read fresh per request.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `builder.rank.set` (all required).",
4335
+ "x-rank": "archon",
4336
+ "x-source": "src/bongos/routes/builders.js",
4337
+ "x-permissions": [
4338
+ "builder.rank.set"
4339
+ ],
4340
+ "parameters": [
4341
+ {
4342
+ "name": "id",
4343
+ "in": "path",
4344
+ "required": true,
4345
+ "schema": {
4346
+ "type": "string"
4347
+ },
4348
+ "description": "Path parameter `id`."
4349
+ }
4350
+ ],
4351
+ "requestBody": {
4352
+ "required": true,
4353
+ "content": {
4354
+ "application/json": {
4355
+ "schema": {
4356
+ "$ref": "#/components/schemas/PatchBuildersIdCardRequirementRequest"
4357
+ }
4358
+ }
4359
+ },
4360
+ "x-validated": true
4361
+ },
4362
+ "responses": {
4363
+ "200": {
4364
+ "description": "Success.",
4365
+ "content": {
4366
+ "application/json": {
4367
+ "schema": {
4368
+ "$ref": "#/components/schemas/PatchBuildersIdCardRequirementResponse"
4369
+ }
4370
+ }
4371
+ }
4372
+ },
4373
+ "400": {
4374
+ "$ref": "#/components/responses/ValidationFailed"
4375
+ },
4376
+ "401": {
4377
+ "$ref": "#/components/responses/Unauthorized"
4378
+ },
4379
+ "403": {
4380
+ "$ref": "#/components/responses/Forbidden"
4381
+ },
4382
+ "404": {
4383
+ "$ref": "#/components/responses/NotFound"
4384
+ }
4385
+ },
4386
+ "security": [
4387
+ {
4388
+ "builderSession": []
4389
+ }
4390
+ ]
4391
+ }
4392
+ },
4327
4393
  "/builders/{id}/onboarding": {
4328
4394
  "get": {
4329
4395
  "operationId": "get_builders_id_onboarding",
@@ -24804,6 +24870,72 @@
24804
24870
  ]
24805
24871
  }
24806
24872
  },
24873
+ "/provisioning/instances/{id}/update-subscription": {
24874
+ "post": {
24875
+ "operationId": "post_provisioning_instances_id_update_subscription",
24876
+ "tags": [
24877
+ "provisioning"
24878
+ ],
24879
+ "summary": "POST /provisioning/instances/:id/update-subscription",
24880
+ "description": "Body: { subscribed: boolean }. Gate: the 'provisioning.fleet.manage' permission — the fleet's own.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `provisioning.fleet.manage` (all required).",
24881
+ "x-rank": "metic+archon",
24882
+ "x-source": "modules/provisioning/routes/update-subscription.js",
24883
+ "x-permissions": [
24884
+ "provisioning.fleet.manage"
24885
+ ],
24886
+ "parameters": [
24887
+ {
24888
+ "name": "id",
24889
+ "in": "path",
24890
+ "required": true,
24891
+ "schema": {
24892
+ "type": "string"
24893
+ },
24894
+ "description": "Path parameter `id`."
24895
+ }
24896
+ ],
24897
+ "requestBody": {
24898
+ "required": true,
24899
+ "content": {
24900
+ "application/json": {
24901
+ "schema": {
24902
+ "$ref": "#/components/schemas/PostProvisioningInstancesIdUpdateSubscriptionRequest"
24903
+ }
24904
+ }
24905
+ },
24906
+ "x-validated": true
24907
+ },
24908
+ "responses": {
24909
+ "200": {
24910
+ "description": "Success.",
24911
+ "content": {
24912
+ "application/json": {
24913
+ "schema": {
24914
+ "$ref": "#/components/schemas/PostProvisioningInstancesIdUpdateSubscriptionResponse"
24915
+ }
24916
+ }
24917
+ }
24918
+ },
24919
+ "400": {
24920
+ "$ref": "#/components/responses/ValidationFailed"
24921
+ },
24922
+ "401": {
24923
+ "$ref": "#/components/responses/Unauthorized"
24924
+ },
24925
+ "403": {
24926
+ "$ref": "#/components/responses/Forbidden"
24927
+ },
24928
+ "404": {
24929
+ "$ref": "#/components/responses/NotFound"
24930
+ }
24931
+ },
24932
+ "security": [
24933
+ {
24934
+ "builderSession": []
24935
+ }
24936
+ ]
24937
+ }
24938
+ },
24807
24939
  "/provisioning/logos/{file}": {
24808
24940
  "get": {
24809
24941
  "operationId": "get_provisioning_logos_file",
@@ -35357,6 +35489,27 @@
35357
35489
  "builder"
35358
35490
  ]
35359
35491
  },
35492
+ "PatchBuildersIdCardRequirementRequest": {
35493
+ "type": "object",
35494
+ "properties": {
35495
+ "require_card": {
35496
+ "type": "boolean"
35497
+ }
35498
+ },
35499
+ "required": [
35500
+ "require_card"
35501
+ ],
35502
+ "additionalProperties": false
35503
+ },
35504
+ "PatchBuildersIdCardRequirementResponse": {
35505
+ "type": "object",
35506
+ "properties": {
35507
+ "builder": {}
35508
+ },
35509
+ "required": [
35510
+ "builder"
35511
+ ]
35512
+ },
35360
35513
  "PatchBuildersIdRankRequest": {
35361
35514
  "type": "object",
35362
35515
  "properties": {
@@ -41304,6 +41457,35 @@
41304
41457
  "message"
41305
41458
  ]
41306
41459
  },
41460
+ "PostProvisioningInstancesIdUpdateSubscriptionRequest": {
41461
+ "type": "object",
41462
+ "properties": {
41463
+ "subscribed": {
41464
+ "type": "boolean"
41465
+ }
41466
+ },
41467
+ "required": [
41468
+ "subscribed"
41469
+ ],
41470
+ "additionalProperties": false
41471
+ },
41472
+ "PostProvisioningInstancesIdUpdateSubscriptionResponse": {
41473
+ "type": "object",
41474
+ "properties": {
41475
+ "ok": {
41476
+ "type": "boolean"
41477
+ },
41478
+ "update_subscribed": {},
41479
+ "previous": {},
41480
+ "changed": {}
41481
+ },
41482
+ "required": [
41483
+ "ok",
41484
+ "update_subscribed",
41485
+ "previous",
41486
+ "changed"
41487
+ ]
41488
+ },
41307
41489
  "PostProvisioningInstancesRequest": {
41308
41490
  "type": "object",
41309
41491
  "properties": {
@@ -43737,9 +43919,9 @@
43737
43919
  "description": "A required dependency/feature is not configured or is temporarily down."
43738
43920
  }
43739
43921
  },
43740
- "x-endpoint-count": 699,
43741
- "x-schema-count": 734,
43922
+ "x-endpoint-count": 701,
43923
+ "x-schema-count": 738,
43742
43924
  "x-undocumented-bodies": 12,
43743
- "x-response-schemas": 464,
43925
+ "x-response-schemas": 466,
43744
43926
  "x-generated-by": "scripts/bongos/gen-api-docs.js"
43745
43927
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 699 endpoints across 117 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 701 endpoints across 118 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -161,7 +161,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
161
161
  |---|---|---|---|---|
162
162
  | GET | `/api/bongos/bug-attachments/:name` | `any-builder` | — | GET /api/bongos/bug-attachments/:name — stream one stored image. |
163
163
 
164
- ## `builders` (14)
164
+ ## `builders` (15)
165
165
 
166
166
  | Method | Path | Rank | Body | Description |
167
167
  |---|---|---|---|---|
@@ -171,6 +171,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
171
171
  | GET | `/api/bongos/builders/:id/activity` | `any-builder` | — | GET /builders/:id/activity — the profile's PER-CRAFT activity log (task 1003281, goal 1000072, criterion legibility-activity-by-role). |
172
172
  | PATCH | `/api/bongos/builders/:id/budget` | `metic+archon` | `monthly_budget_usd` | PATCH /builders/:id/budget — set or clear a builder's monthly spend ceiling (V3.R84 task 302, criterion C3). |
173
173
  | PATCH | `/api/bongos/builders/:id/capabilities` | `archon` | `server` | PATCH /builders/:id/capabilities — the owner gives or takes a builder's server access (task 1004950, ADR 0395). |
174
+ | PATCH | `/api/bongos/builders/:id/card-requirement` | `archon` | `require_card` | PATCH /builders/:id/card-requirement — an Archon switches the hardware-card requirement on or off for one builder (task 1004748, ADR 0382… |
174
175
  | GET | `/api/bongos/builders/:id/onboarding` | `public` | — | GET /builders/:id/onboarding — V3.R75 (task 294) onboarding checklist read. |
175
176
  | GET | `/api/bongos/builders/:id/profile` | `public` | — | GET /builders/:id/profile — the viewable builder profile page's read (task 1467, goal 25 "bx-identity"). |
176
177
  | PATCH | `/api/bongos/builders/:id/rank` | `archon` | `rank` | sole chokepoint that mutates builders.rank; ADR 0016 §enforcement. |
@@ -826,7 +827,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
826
827
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | own-or-metic in effect: where to invite a builder into a project, asked by a `project.curate` holder OR the project's OWN OWNER (task 100… |
827
828
  | POST | `/api/bongos/projects/invite-guild` | `metic+archon` | `client_id`, `guild` | own-or-metic in effect, the SAME gate as POST /projects/invite above and for the same reasons (a non-inviter gets that route's unchanged … |
828
829
 
829
- ## `provisioning` (46)
830
+ ## `provisioning` (47)
830
831
 
831
832
  | Method | Path | Rank | Body | Description |
832
833
  |---|---|---|---|---|
@@ -868,6 +869,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
868
869
  | PATCH | `/api/bongos/provisioning/instances/:id/settings` | `any-builder` | validated | PATCH /provisioning/instances/:id/settings — change a project's settings. |
869
870
  | POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
870
871
  | POST | `/api/bongos/provisioning/instances/:id/update-channel` | `archon` | — | POST /provisioning/instances/:id/update-channel — set the project's update rule (task 1004445). |
872
+ | POST | `/api/bongos/provisioning/instances/:id/update-subscription` | `metic+archon` | `subscribed` | Body: { subscribed: boolean }. |
871
873
  | GET | `/api/bongos/provisioning/logos/:file` | `public` | — | a logo is shown on a public hall, and its address is its own sha256, so it names no project and cannot be guessed. |
872
874
  | GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criteri… |
873
875
  | POST | `/api/bongos/provisioning/physics-check` | `any-builder` | `description`, `detail` | POST /provisioning/physics-check — the planet-physics questions' read BEFORE anything exists (task 1004418, BV2.PS07). |
@@ -3450,5 +3450,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3450
3450
  landed since 1.25.2 with no explicit bump. run 43328266045. (task 1002620)
3451
3451
  1.25.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3452
3452
  landed since 1.25.3 with no explicit bump. run 43331422033. (task 1002620)
3453
+ 1.25.5 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3454
+ landed since 1.25.4 with no explicit bump. run 43362791111. (task 1002620)
3453
3455
  ---------------------------------------------------------------------------
3454
3456
  ```
@@ -0,0 +1,21 @@
1
+ -- provisioning_037_update_subscribed.sql — whether an instance takes unattended patch upgrades,
2
+ -- stored on the instance row (task 1004150, ADR 0325 decision 5).
3
+ --
4
+ -- WHY. Every other fleet fact lives in provisioning_instances; "which instances the nightly sweep
5
+ -- moves on its own" lived only in config/update-subscriptions.json. That left GET /provisioning/fleet
6
+ -- unable to answer the one question a fleet view exists for, and let a restored control-plane backup
7
+ -- bring back every instance row with an empty roster — the sweep then reported failed=0 while
8
+ -- upgrading nothing. On the row, the subscription and its update_channel (provisioning_029) travel
9
+ -- and restore together.
10
+ --
11
+ -- DEFAULT false: an instance is armed only by an explicit act, as the shipped-empty roster was
12
+ -- (ADR 0136). The roster file stays honoured as an override, so an operator's current arming is
13
+ -- not silently dropped.
14
+ --
15
+ -- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
16
+
17
+ BEGIN;
18
+
19
+ ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS update_subscribed boolean NOT NULL DEFAULT false;
20
+
21
+ COMMIT;
@@ -20,6 +20,7 @@
20
20
  "core-upgrade",
21
21
  "env-manifest",
22
22
  "repo-private",
23
+ "update-subscription",
23
24
  "render-standup",
24
25
  "look",
25
26
  "demo",
@@ -164,6 +164,10 @@ function ownerInstance(row) {
164
164
  function fleetInstance(row) {
165
165
  return {
166
166
  owner: { id: row.owner_builder_id, github_login: row.github_login, display_name: row.display_name, rank: row.rank },
167
+ // Fleet-only (task 1004150): which rows take unattended upgrades, and under what rule. Kept off
168
+ // publicInstance, which feeds owner reads — who auto-upgrades is an operator's question.
169
+ update_channel: row.update_channel || 'patch',
170
+ update_subscribed: row.update_subscribed === true,
167
171
  ...ownerInstance(row),
168
172
  };
169
173
  }
@@ -0,0 +1,51 @@
1
+ // modules/provisioning/routes/update-subscription.js — POST /provisioning/instances/:id/update-subscription
2
+ // (task 1004150, ADR 0325 decision 5).
3
+ //
4
+ // WHY. Whether the nightly sweep moves an instance on its own was only a line in
5
+ // config/update-subscriptions.json, so the fleet roster could not report it and a restored
6
+ // backup brought back every row with an empty roster. The flag lives on the row now
7
+ // (provisioning_037); this is how an operator arms or disarms it. It changes a rule, never the
8
+ // core: nothing is queued.
9
+ //
10
+ // Its own routes file (declared in module.json) because routes/provisioning.js sits at the
11
+ // size ratchet — the routes/repo-private.js precedent.
12
+ 'use strict';
13
+
14
+ const express = require('express');
15
+ const api = require('../../../src/module-api');
16
+ const { pool, validateOrRespond, parseId } = api;
17
+ const provisioning = require('../provisioning');
18
+ const { failFrom } = require('../public-refusal');
19
+
20
+ const log = api.logger('provisioning');
21
+
22
+ module.exports = function buildUpdateSubscriptionRouter() {
23
+ const router = express.Router();
24
+
25
+ // Body: { subscribed: boolean }. Gate: the 'provisioning.fleet.manage' permission — the fleet's own.
26
+ router.post('/provisioning/instances/:id/update-subscription', api.requireBuilder, api.requirePermission('provisioning.fleet.manage'), async (req, res) => {
27
+ const id = parseId(req, res);
28
+ if (!id) return;
29
+ if (validateOrRespond(req, res, { subscribed: { type: 'boolean', required: true } }, { strict: true })) return;
30
+ try {
31
+ const inst = await provisioning.getInstanceById(pool, id);
32
+ if (!inst) return res.fail('instance_not_found', 404);
33
+ const subscribed = req.body.subscribed === true;
34
+ const was = inst.update_subscribed === true;
35
+ if (was !== subscribed) {
36
+ await pool.query('UPDATE provisioning_instances SET update_subscribed = $2, updated_at = now() WHERE id = $1', [id, subscribed]);
37
+ await provisioning.recordEvent(pool, {
38
+ instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'update-subscription-changed',
39
+ detail: `unattended patch upgrades ${subscribed ? 'armed' : 'disarmed'} by ${req.builder.github_login || req.builder.id}`,
40
+ actor: `api:fleet:${req.builder.id}`,
41
+ }).catch(() => {});
42
+ }
43
+ res.json({ ok: true, update_subscribed: subscribed, previous: was, changed: was !== subscribed });
44
+ } catch (err) {
45
+ log.error('[provisioning] POST /provisioning/instances/:id/update-subscription', err);
46
+ failFrom(res, err, 'update_subscription_failed');
47
+ }
48
+ });
49
+
50
+ return router;
51
+ };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.4",
3
+ "version": "1.25.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.4",
9
+ "version": "1.25.5",
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.25.4",
3
+ "version": "1.25.5",
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",
@@ -11718,5 +11718,15 @@
11718
11718
  "id": "1005249",
11719
11719
  "text": "Your details now sit on your character at the bottom left of the game: hover to see them, click to open your full profile without leaving the scene. The old profile card at the top right is gone."
11720
11720
  }
11721
+ ],
11722
+ "1.25.5": [
11723
+ {
11724
+ "id": "1004748",
11725
+ "text": "Archon can now switch on a card requirement for a builder who has registered a card, with every change recorded."
11726
+ },
11727
+ {
11728
+ "id": "1004150",
11729
+ "text": "The fleet view can now show which projects get automatic patch upgrades, and the nightly upgrade run says so out loud when it has nothing to do."
11730
+ }
11721
11731
  ]
11722
11732
  }
@@ -333,13 +333,14 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
333
333
  const CHANNEL_DB_ENV = 'UPDATE_CHANNEL_DB';
334
334
  const CHANNEL_SOURCE_DEPLOY = 'deploy';
335
335
  const CHANNEL_SOURCE_ROSTER = 'roster';
336
- const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
336
+ const DB_CHANNELS_SQL = 'SELECT slug, update_channel, update_subscribed FROM provisioning_instances';
337
337
  const DB_CHANNELS_TIMEOUT_MS = 15000;
338
338
 
339
339
  // PURE. psql's stderr reduced to a class that is safe to log.
340
340
  function psqlFailureClass(stderr) {
341
341
  const s = String(stderr || '');
342
342
  if (/relation .*provisioning_instances.* does not exist/i.test(s)) return 'no provisioning_instances table (provisioning module off?)';
343
+ if (/column .*update_subscribed.* does not exist/i.test(s)) return 'no update_subscribed column (migration provisioning_037 not applied)';
343
344
  if (/column .*update_channel.* does not exist/i.test(s)) return 'no update_channel column (migration provisioning_029 not applied)';
344
345
  if (/authentication failed|role .* does not exist|permission denied/i.test(s)) return 'the sweep\'s database login was refused';
345
346
  if (/could not connect|connection refused|no such file or directory|could not translate host/i.test(s)) return 'database unreachable';
@@ -366,11 +367,32 @@ function readDbChannels({ env = process.env, run = spawnSync } = {}) {
366
367
  }
367
368
  if (r.status !== 0) return { ok: false, reason: `psql exit ${r.status}: ${psqlFailureClass(r.stderr)}` };
368
369
  const channels = new Map();
370
+ const subscribed = new Map();
369
371
  for (const line of String(r.stdout || '').split('\n')) {
370
- const [slug, ch] = line.split('\t');
371
- if (slug && ch) channels.set(slug.trim(), ch.trim());
372
+ const [slug, ch, sub] = line.split('\t');
373
+ if (!slug || !ch) continue;
374
+ channels.set(slug.trim(), ch.trim());
375
+ subscribed.set(slug.trim(), String(sub || '').trim() === 't');
372
376
  }
373
- return { ok: true, channels };
377
+ return { ok: true, channels, subscribed };
378
+ }
379
+
380
+ // PURE. Merge the two places a subscription can be written: the project row
381
+ // (provisioning_instances.update_subscribed, task 1004150) and the roster file. The file is an
382
+ // OVERRIDE — an entry in it is subscribed whatever the row says, so an operator's current arming
383
+ // is not dropped — but a row subscribed with NO file entry can't be upgraded (the file carries the
384
+ // `dir`), and it is returned in `unreachable` rather than vanishing, because "subscribed on the
385
+ // fleet view, never touched by the sweep" is the silent failure this column exists to end.
386
+ function applyDbSubscriptions(instances, db) {
387
+ const subscribed = db && db.ok && db.subscribed instanceof Map ? db.subscribed : null;
388
+ const slugs = new Set(instances.map((i) => i.slug));
389
+ const out = instances.map((inst) => {
390
+ if (!subscribed) return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER };
391
+ if (subscribed.get(inst.slug) === true) return { ...inst, subscribedSource: CHANNEL_SOURCE_DEPLOY };
392
+ return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER, subscribedNote: 'subscribed by the roster file only (the project row says not subscribed)' };
393
+ });
394
+ const unreachable = subscribed ? [...subscribed].filter(([slug, on]) => on && !slugs.has(slug)).map(([slug]) => slug) : [];
395
+ return { instances: out, unreachable };
374
396
  }
375
397
 
376
398
  // PURE. Give each roster entry the channel it is actually under, and say where it came from:
@@ -496,6 +518,7 @@ module.exports = {
496
518
  readDbChannels,
497
519
  CHANNEL_DB_ENV,
498
520
  applyDbChannels,
521
+ applyDbSubscriptions,
499
522
  channelLine,
500
523
  psqlFailureClass,
501
524
  CHANNEL_SOURCE_DEPLOY,
@@ -593,16 +593,29 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
593
593
  // CONTROL PLANE's database — this process's own env, never an entry's `env`, which points at that
594
594
  // instance's database. A core too old to carry the reader keeps the roster, as before.
595
595
  const db = typeof channel.readDbChannels === 'function' ? channel.readDbChannels({ env: process.env }) : null;
596
- const instances = db ? channel.applyDbChannels(loaded.instances, db) : loaded.instances;
597
- if (db && !db.ok && instances.length) console.log(` • update rules: read from the roster — ${db.reason}`);
596
+ const withChannels = db ? channel.applyDbChannels(loaded.instances, db) : loaded.instances;
597
+ if (db && !db.ok && withChannels.length) console.log(` • update rules: read from the roster — ${db.reason}`);
598
+ // Who is subscribed is also on the project row (task 1004150). The roster file stays an override.
599
+ const merged = db && typeof channel.applyDbSubscriptions === 'function'
600
+ ? channel.applyDbSubscriptions(withChannels, db) : { instances: withChannels, unreachable: [] };
601
+ const instances = merged.instances;
598
602
 
599
603
  const roster = only ? instances.filter((i) => i.slug === only || i.dir === only) : instances;
600
604
  const mode = apply ? 'apply' : 'dry-run';
601
605
  console.log(`core-update-subscription (${mode}) — ${roster.length} subscribed instance(s)${present ? '' : ' [no roster file — nothing subscribed]'}`);
602
- for (const inst of roster) if (inst.channelNote) console.log(channel.channelLine(inst));
606
+ for (const inst of roster) {
607
+ if (inst.channelNote) console.log(channel.channelLine(inst));
608
+ if (inst.subscribedNote) console.log(` • ${inst.slug}: ${inst.subscribedNote}`);
609
+ }
603
610
  if (only && roster.length === 0) console.error(` ! --only ${only} matched no subscribed instance`);
604
611
 
605
612
  let upgraded = 0, skipped = 0, failed = 0, upToDate = 0, healed = 0;
613
+ // Subscribed on the project row but absent from this roster: there is no `dir` to upgrade, so say
614
+ // so and count it a skip. Silence here is how a restored backup reported failed=0 on a dead sweep.
615
+ for (const slug of only ? [] : merged.unreachable) {
616
+ console.error(` ! ${slug}: subscribed on its project row, but this roster has no entry with a dir for it — not upgraded`);
617
+ skipped++;
618
+ }
606
619
  const refusalsFile = refusalsPath();
607
620
  const refusals = readRefusals(refusalsFile);
608
621
 
@@ -817,6 +830,7 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
817
830
  upgraded++;
818
831
  }
819
832
 
833
+ if (!only && roster.length === 0 && skipped === 0) console.log('\ncore-update-subscription: NOTHING SUBSCRIBED — no instance was considered, so no upgrade was attempted (this is not a clean run).');
820
834
  console.log(`\ncore-update-subscription: upgraded=${upgraded} up-to-date=${upToDate} skipped=${skipped} failed=${failed}${healed ? ` healed=${healed}` : ''} (${mode})`);
821
835
  return failed > 0 ? 1 : 0;
822
836
  }
@@ -89,6 +89,8 @@ const OVERRIDES = Object.freeze({
89
89
  bfg_write_disabled: 'forbidden',
90
90
  target_is_system_principal: 'forbidden',
91
91
  cannot_change_own_tester: 'forbidden',
92
+ cannot_disable_own_card_requirement: 'forbidden',
93
+ no_card_registered: 'conflict',
92
94
  self_vote_forbidden: 'forbidden',
93
95
  self_dep: 'conflict',
94
96
  newcomer_must_ship_first: 'forbidden',
package/src/bongos/db.js CHANGED
@@ -724,6 +724,48 @@ async function setBuilderTester({ builderId, isTester }) {
724
724
  });
725
725
  }
726
726
 
727
+ // setRequireCard — an Archon switches the hardware-card requirement on or off for one
728
+ // builder (task 1004748, ADR 0382 D3/D4). Switching ON needs at least one registered
729
+ // card, so nobody is locked out by the switch. The audit row is written in the SAME
730
+ // transaction as the flag, so a change cannot happen without its entry; unlike the
731
+ // generic request log it names the builder it was done TO.
732
+ async function setRequireCard({ builderId, required, actorBuilderId, ip = null, userAgent = null }) {
733
+ return withTx(async (client) => {
734
+ const { rows: found } = await client.query(
735
+ `SELECT id, system_role, require_card FROM builders WHERE id = $1 FOR UPDATE`,
736
+ [builderId]
737
+ );
738
+ const before = found[0];
739
+ if (!before) return null;
740
+ if (before.system_role) throw { code: 'TARGET_IS_SYSTEM_PRINCIPAL' };
741
+ if (required) {
742
+ const { rows: cards } = await client.query(
743
+ `SELECT 1 FROM passkey_credentials WHERE builder_id = $1 LIMIT 1 FOR SHARE`,
744
+ [builderId]
745
+ );
746
+ if (!cards[0]) throw { code: 'NO_CARD_REGISTERED' };
747
+ }
748
+ const { rows } = await client.query(
749
+ `UPDATE builders SET require_card = $2 WHERE id = $1
750
+ RETURNING id, github_login, display_name, rank, status, require_card`,
751
+ [builderId, !!required]
752
+ );
753
+ await client.query(
754
+ `INSERT INTO audit_log
755
+ (builder_id, route, method, request_body_redacted, response_status, ip, user_agent, surface)
756
+ VALUES ($1, $2, 'PATCH', $3, 200, $4, $5, NULL)`,
757
+ [
758
+ actorBuilderId,
759
+ `${require('./api-prefix').API_PREFIX}/builders/:id/card-requirement`,
760
+ { target_builder_id: String(builderId), require_card: !!required, was: before.require_card === true },
761
+ ip,
762
+ userAgent,
763
+ ]
764
+ );
765
+ return rows[0];
766
+ });
767
+ }
768
+
727
769
  async function reactivateBuilder({ builderId, actorBuilderId }) {
728
770
  return withTx(async (client) => {
729
771
  const { rows } = await client.query(
@@ -1183,6 +1225,7 @@ module.exports = {
1183
1225
  RANKS,
1184
1226
  setBuilderRank,
1185
1227
  setBuilderTester,
1228
+ setRequireCard,
1186
1229
  getLatestRankChangeForBuilder,
1187
1230
  deactivateBuilder,
1188
1231
  reactivateBuilder,
@@ -194,6 +194,7 @@ const EXPECTED_RANKS = {
194
194
  // builder authority — rank + lifecycle status mutation
195
195
  'PATCH /builders/:id/rank': 'perm:builder.rank.set',
196
196
  'PATCH /builders/:id/status': 'perm:builder.status.set',
197
+ 'PATCH /builders/:id/card-requirement': 'perm:builder.rank.set',
197
198
  // task 1004561: the Treasury desk sets a builder's monthly limit through it
198
199
  'PATCH /builders/:id/budget': 'perm:builder.budget.set',
199
200
  // session revocation (kill another builder's / all sessions)
@@ -421,6 +421,43 @@ module.exports = function buildBuildersRouter() {
421
421
  }
422
422
  });
423
423
 
424
+ // PATCH /builders/:id/card-requirement — an Archon switches the hardware-card
425
+ // requirement on or off for one builder (task 1004748, ADR 0382 D4). Body:
426
+ // { require_card: boolean }. Default is off for everyone. Switching ON refuses 409
427
+ // no_card_registered unless the builder has a registered card, so the switch cannot
428
+ // lock anyone out. You cannot switch OFF your own requirement (403
429
+ // cannot_disable_own_card_requirement) — a stolen session must not be able to drop
430
+ // it; another Archon does it. Every change writes an audit row naming the target.
431
+ // rank: archon. Gate: requirePermission('builder.rank.set') (the non-delegable atom that
432
+ // guards the step-up's own target route) then requireCardStepUp; read fresh per request.
433
+ router.patch('/builders/:id/card-requirement', auth.requireBuilder, auth.requirePermission('builder.rank.set'), requireCardStepUp, async (req, res) => {
434
+ if (validateOrRespond(req, res, { require_card: { required: true, type: 'boolean' } })) return;
435
+ const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
436
+ if (targetId === null) return;
437
+ const required = req.body.require_card;
438
+ if (!required && String(targetId) === String(req.builder.id)) {
439
+ return res.fail('cannot_disable_own_card_requirement', 403);
440
+ }
441
+ try {
442
+ const builder = await db.setRequireCard({
443
+ builderId: targetId,
444
+ required,
445
+ actorBuilderId: req.builder.id,
446
+ ip: req.ip || null,
447
+ userAgent: req.headers['user-agent'] || null,
448
+ });
449
+ if (!builder) return res.fail('builder_not_found', 404);
450
+ res.json({ builder });
451
+ } catch (err) {
452
+ if (err && err.code === 'TARGET_IS_SYSTEM_PRINCIPAL') return res.fail('target_is_system_principal', 403);
453
+ if (err && err.code === 'NO_CARD_REGISTERED') {
454
+ return res.fail('no_card_registered', 409, { message: 'That builder has no registered card yet; they must register one before the requirement can be switched on.' });
455
+ }
456
+ log.error('[gds] PATCH /builders/:id/card-requirement', err);
457
+ res.fail('card_requirement_change_failed', { status: 500, message: 'internal error' });
458
+ }
459
+ });
460
+
424
461
  // PATCH /builders/:id/capabilities — the owner gives or takes a builder's server
425
462
  // access (task 1004950, ADR 0395). Body: { server: boolean }. A task that needs
426
463
  // the server is shown to and claimable by a builder only while this is on.
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.4'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.5'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,75 @@
1
+ // tests/builder_card_requirement.mjs
2
+ //
3
+ // Task 1004748 (ADR 0382 D3/D4) — an Archon switches the hardware-card requirement on
4
+ // per builder. DB-free: the shared pool's client is a recorder.
5
+ //
6
+ // Run: node tests/builder_card_requirement.mjs
7
+
8
+ import { strict as assert } from 'node:assert';
9
+ import { createRequire } from 'node:module';
10
+ import { makeRunner } from './helpers.mjs';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const { pool } = require('../src/bongos/pool');
14
+ const { test, summary } = makeRunner();
15
+
16
+ let builderRow; let hasCard; const sent = [];
17
+ pool.connect = async () => ({
18
+ query: async (sql, params = []) => {
19
+ const q = String(sql);
20
+ sent.push({ sql: q, params });
21
+ if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(q)) return { rows: [] };
22
+ if (/FROM builders WHERE id/.test(q)) return { rows: builderRow ? [builderRow] : [] };
23
+ if (/FROM passkey_credentials/.test(q)) return { rows: hasCard ? [{}] : [] };
24
+ if (/UPDATE builders SET require_card/.test(q)) return { rows: [{ id: params[0], require_card: params[1] }] };
25
+ return { rows: [] };
26
+ },
27
+ release() {},
28
+ });
29
+
30
+ const { setRequireCard } = require('../src/bongos/db');
31
+ const reset = () => { sent.length = 0; };
32
+
33
+ await test('switching on refuses a builder with no registered card, writes nothing', async () => {
34
+ reset(); builderRow = { id: '7', system_role: null, require_card: false }; hasCard = false;
35
+ await assert.rejects(setRequireCard({ builderId: '7', required: true, actorBuilderId: '1' }), { code: 'NO_CARD_REGISTERED' });
36
+ assert.equal(sent.some((s) => /UPDATE builders|INSERT INTO audit_log/.test(s.sql)), false);
37
+ });
38
+
39
+ await test('switching on with a card sets the flag and audits the target in the same transaction', async () => {
40
+ reset(); hasCard = true;
41
+ const r = await setRequireCard({ builderId: '7', required: true, actorBuilderId: '1' });
42
+ assert.equal(r.require_card, true);
43
+ const audit = sent.find((s) => /INSERT INTO audit_log/.test(s.sql));
44
+ assert.ok(audit);
45
+ assert.equal(audit.params[0], '1');
46
+ assert.deepEqual(audit.params[2], { target_builder_id: '7', require_card: true, was: false });
47
+ });
48
+
49
+ await test('switching off needs no card and is audited', async () => {
50
+ reset(); hasCard = false; builderRow = { id: '7', system_role: null, require_card: true };
51
+ const r = await setRequireCard({ builderId: '7', required: false, actorBuilderId: '1' });
52
+ assert.equal(r.require_card, false);
53
+ assert.deepEqual(sent.find((s) => /INSERT INTO audit_log/.test(s.sql)).params[2].was, true);
54
+ });
55
+
56
+ await test('unknown builder is null; a system principal is refused', async () => {
57
+ reset(); builderRow = null;
58
+ assert.equal(await setRequireCard({ builderId: '9', required: false, actorBuilderId: '1' }), null);
59
+ builderRow = { id: '9', system_role: 'runner', require_card: false };
60
+ await assert.rejects(setRequireCard({ builderId: '9', required: false, actorBuilderId: '1' }), { code: 'TARGET_IS_SYSTEM_PRINCIPAL' });
61
+ });
62
+
63
+ await test('route: rank atom + step-up gate, self-disable refused, 409 mapped, errors classified', async () => {
64
+ const fs = await import('node:fs');
65
+ const src = fs.readFileSync(new URL('../src/bongos/routes/builders.js', import.meta.url), 'utf8');
66
+ const line = src.split(/\r?\n/).find((l) => l.includes("router.patch('/builders/:id/card-requirement'"));
67
+ assert.match(line, /requirePermission\('builder\.rank\.set'\), requireCardStepUp/);
68
+ assert.match(src, /cannot_disable_own_card_requirement', 403/);
69
+ assert.match(src, /NO_CARD_REGISTERED[\s\S]{0,80}no_card_registered', 409/);
70
+ const { classify } = require('../src/bongos/api-errors');
71
+ assert.equal(classify('no_card_registered'), 'conflict');
72
+ assert.equal(classify('cannot_disable_own_card_requirement'), 'forbidden');
73
+ });
74
+
75
+ summary();
@@ -0,0 +1,141 @@
1
+ // tests/provisioning_update_subscription_route.mjs
2
+ //
3
+ // Task 1004150 (ADR 0325 decision 5): POST /provisioning/instances/:id/update-subscription — the
4
+ // operator's switch for "does the nightly sweep move this instance on its own".
5
+ //
6
+ // The load-bearing assertions:
7
+ // • the fleet permission gate is what stands in front of it (an ungated route could arm any
8
+ // instance for unattended upgrades)
9
+ // • the body is strict and boolean: a string, a missing field or an extra field is a 400 and
10
+ // nothing is written
11
+ // • a real change writes the column and one audit event; a no-op writes and records nothing
12
+ // • an unknown id is a 404
13
+ //
14
+ // DB-free: the pool and data access are stubbed on the cached module objects over a real mounted
15
+ // router (the provisioning_repo_private_route.mjs pattern).
16
+ //
17
+ // Run: node tests/provisioning_update_subscription_route.mjs
18
+
19
+ import { strict as assert } from 'node:assert';
20
+ import { createRequire } from 'node:module';
21
+ import http from 'node:http';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const provisioning = require('../modules/provisioning/provisioning.js');
25
+ const api = require('../src/module-api.js');
26
+
27
+ let passed = 0, failed = 0;
28
+ async function ta(name, fn) {
29
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
30
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
31
+ }
32
+
33
+ let instance = null;
34
+ let writes = [];
35
+ let events = [];
36
+ let permissionAsked = null;
37
+ let allowed = true;
38
+ provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
39
+ provisioning.recordEvent = async (_db, e) => { events.push(e); };
40
+ api.pool.query = async (sql, params) => { writes.push({ sql, params }); return { rows: [] }; };
41
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 5, github_login: 'op', rank: 'metic' }; next(); };
42
+ api.requirePermission = (atom) => {
43
+ permissionAsked = atom;
44
+ return (req, res, next) => (allowed ? next() : res.status(403).json({ error: { code: 'forbidden' } }));
45
+ };
46
+ api.logger = () => ({ error() {}, warn() {}, info() {}, debug() {} });
47
+
48
+ const express = require('express');
49
+ const buildRouter = require('../modules/provisioning/routes/update-subscription.js');
50
+ const app = express();
51
+ app.use(express.json());
52
+ app.use((_req, res, next) => {
53
+ res.fail = (code, opts) => {
54
+ const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
55
+ res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}) } });
56
+ };
57
+ next();
58
+ });
59
+ app.use('/api/bongos', buildRouter());
60
+
61
+ const server = app.listen(0);
62
+ await new Promise((r) => server.once('listening', r));
63
+ const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
64
+
65
+ function post(path, body) {
66
+ return new Promise((resolve, reject) => {
67
+ const payload = JSON.stringify(body);
68
+ const req = http.request(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
69
+ let d = '';
70
+ res.on('data', (c) => { d += c; });
71
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
72
+ });
73
+ req.on('error', reject);
74
+ req.write(payload);
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ function reset(patch = {}) {
80
+ instance = { owner_builder_id: 7, slug: 'spike', update_subscribed: false, ...patch };
81
+ writes = []; events = []; allowed = true;
82
+ }
83
+ const URL_18 = '/provisioning/instances/18/update-subscription';
84
+
85
+ console.log('POST /provisioning/instances/:id/update-subscription:');
86
+
87
+ await ta('is gated on the fleet permission, and a caller without it writes nothing', async () => {
88
+ reset(); allowed = false;
89
+ const r = await post(URL_18, { subscribed: true });
90
+ assert.equal(permissionAsked, 'provisioning.fleet.manage');
91
+ assert.equal(r.status, 403, r.raw);
92
+ assert.equal(writes.length, 0);
93
+ });
94
+
95
+ await ta('arming writes the column once and records one audit event', async () => {
96
+ reset();
97
+ const r = await post(URL_18, { subscribed: true });
98
+ assert.equal(r.status, 200, r.raw);
99
+ assert.deepEqual(r.body, { ok: true, update_subscribed: true, previous: false, changed: true });
100
+ assert.equal(writes.length, 1);
101
+ assert.match(writes[0].sql, /SET update_subscribed = \$2/);
102
+ assert.deepEqual(writes[0].params, [18, true]);
103
+ assert.equal(events.length, 1);
104
+ assert.equal(events[0].event, 'update-subscription-changed');
105
+ });
106
+
107
+ await ta('a no-op (already in that state) writes nothing and records nothing', async () => {
108
+ reset({ update_subscribed: true });
109
+ const r = await post(URL_18, { subscribed: true });
110
+ assert.equal(r.status, 200, r.raw);
111
+ assert.equal(r.body.changed, false);
112
+ assert.equal(writes.length, 0);
113
+ assert.equal(events.length, 0);
114
+ });
115
+
116
+ await ta('disarming is symmetric', async () => {
117
+ reset({ update_subscribed: true });
118
+ const r = await post(URL_18, { subscribed: false });
119
+ assert.equal(r.body.update_subscribed, false);
120
+ assert.deepEqual(writes[0].params, [18, false]);
121
+ });
122
+
123
+ await ta('a non-boolean, a missing field and an extra field are all 400 with nothing written', async () => {
124
+ for (const body of [{ subscribed: 'yes' }, {}, { subscribed: true, channel: 'minor' }]) {
125
+ reset();
126
+ const r = await post(URL_18, body);
127
+ assert.equal(r.status, 400, `${JSON.stringify(body)} → ${r.raw}`);
128
+ assert.equal(writes.length, 0);
129
+ }
130
+ });
131
+
132
+ await ta('an unknown id is 404', async () => {
133
+ reset(); instance = null;
134
+ const r = await post(URL_18, { subscribed: true });
135
+ assert.equal(r.status, 404, r.raw);
136
+ assert.equal(r.body.error.code, 'instance_not_found');
137
+ });
138
+
139
+ server.close();
140
+ console.log(`\n${passed} passed, ${failed} failed`);
141
+ process.exit(failed ? 1 : 0);
@@ -129,3 +129,27 @@ test('update-sweep.js reads the DB from its own env, never an entry env (which n
129
129
  // A core too old to carry the reader must keep the roster rather than crash the sweep.
130
130
  assert.match(src, /typeof channel\.readDbChannels === 'function'/);
131
131
  });
132
+
133
+ // ---- task 1004150: the subscription roster lives on the project row too ----
134
+
135
+ const subDb = (subs) => ({ ok: true, channels: new Map(), subscribed: new Map(Object.entries(subs)) });
136
+
137
+ test('subscribed on the row: reported as deploy-sourced; a file-only entry is kept as an override', () => {
138
+ const { instances, unreachable } = uc.applyDbSubscriptions([entry(), entry({ slug: 'charter' })], subDb({ cloudbongos: true, charter: false }));
139
+ assert.equal(instances[0].subscribedSource, uc.CHANNEL_SOURCE_DEPLOY);
140
+ assert.equal(instances[1].subscribedSource, uc.CHANNEL_SOURCE_ROSTER);
141
+ assert.match(instances[1].subscribedNote, /roster file only/);
142
+ assert.deepEqual(unreachable, []);
143
+ });
144
+
145
+ test('subscribed on the row with no roster entry: surfaced as unreachable, never dropped', () => {
146
+ const { instances, unreachable } = uc.applyDbSubscriptions([], subDb({ mercury: true, venus: false }));
147
+ assert.equal(instances.length, 0);
148
+ assert.deepEqual(unreachable, ['mercury']);
149
+ });
150
+
151
+ test('database unreachable: the roster alone decides and nothing is reported unreachable', () => {
152
+ const { instances, unreachable } = uc.applyDbSubscriptions([entry()], { ok: false, reason: 'psql not found' });
153
+ assert.equal(instances.length, 1);
154
+ assert.deepEqual(unreachable, []);
155
+ });