@bongos/core 1.19.646 → 1.19.647
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 +30 -25
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +137 -3
- package/docs/api-reference.md +4 -2
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_237_box_widen_paths.sql +40 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/dev-box/box-access.js +128 -4
- package/modules/dev-box/boxes.js +21 -0
- package/modules/dev-box/routes/box.js +108 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-sync.js +84 -1
- package/src/module-api.js +1 -1
- package/tests/box_access.mjs +155 -0
- package/tests/box_sync_scope_report.mjs +45 -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.647",
|
|
6
|
+
"core_contract": "1.19.647",
|
|
7
|
+
"source_commit": "8fa0f8d28d7d61f52297185e5c669d70a60480de",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-10T18:
|
|
9
|
+
"built_at": "2026-09-10T18:40:35.458Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 470,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2107,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2601,
|
|
20
|
+
"tree_sha256": "7cebbe4108e45c224a38f2068e1e7a1d00552500b0b6f6d7f5c111d45f63a037",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -362,12 +362,12 @@
|
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/README.md",
|
|
364
364
|
"mode": "0000644",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "03379a98721207feab9a9b4c03862051ee6d52a29a0bc467ed73ae1d36bf49cd"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "ec75333abb0ee49b89320a86e73b56f2f3872c13c596b1e67f36e705b8ba1210"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -377,17 +377,17 @@
|
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/index.cjs",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "bce3dcdbe37505e0fa526e02b7b379ca9ab378f6c34b41345621d55170d9f19d"
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/index.d.ts",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "a6b15120b937f2a73d47ef180956068560eb3ac3efe5cf58f3a021b544ff9bc0"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.mjs",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "3fbcb50de89350e95b86180e828a16b897be32a1f8b4cdf78cc060e1c5afff86"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1892,12 +1892,12 @@
|
|
|
1892
1892
|
{
|
|
1893
1893
|
"path": "docs/api-reference.md",
|
|
1894
1894
|
"mode": "0000644",
|
|
1895
|
-
"sha256": "
|
|
1895
|
+
"sha256": "0fa07d5f61ab66a43e0c51e51cfac8223afcb1aa12ea7342a226cb1832c0be62"
|
|
1896
1896
|
},
|
|
1897
1897
|
{
|
|
1898
1898
|
"path": "docs/api/openapi.json",
|
|
1899
1899
|
"mode": "0000644",
|
|
1900
|
-
"sha256": "
|
|
1900
|
+
"sha256": "02c082bff4c5bebbf13cefeeb46a6b9cdff430cc32b65dd4e78a72248c6fd74f"
|
|
1901
1901
|
},
|
|
1902
1902
|
{
|
|
1903
1903
|
"path": "docs/architecture.md",
|
|
@@ -2772,7 +2772,7 @@
|
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/module-api-changelog.md",
|
|
2774
2774
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2775
|
+
"sha256": "0f1d4f5ea01f97e24ee83708c35267828f6d7fb2c64a47db9b7b94c43205951a"
|
|
2776
2776
|
},
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/modules-contract.md",
|
|
@@ -3814,6 +3814,11 @@
|
|
|
3814
3814
|
"mode": "0000644",
|
|
3815
3815
|
"sha256": "06b53d66e710dfbf602011459ec8d69adce8e281632927daea0735f8078b2f86"
|
|
3816
3816
|
},
|
|
3817
|
+
{
|
|
3818
|
+
"path": "migrations/core_237_box_widen_paths.sql",
|
|
3819
|
+
"mode": "0000644",
|
|
3820
|
+
"sha256": "2cda282b43ea71dd82ddf4e5ece90f11ad4013cbee0820f0a33391f202feff9c"
|
|
3821
|
+
},
|
|
3817
3822
|
{
|
|
3818
3823
|
"path": "modules/agents/lib/validate.js",
|
|
3819
3824
|
"mode": "0000644",
|
|
@@ -4212,7 +4217,7 @@
|
|
|
4212
4217
|
{
|
|
4213
4218
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
4214
4219
|
"mode": "0000644",
|
|
4215
|
-
"sha256": "
|
|
4220
|
+
"sha256": "bce3dcdbe37505e0fa526e02b7b379ca9ab378f6c34b41345621d55170d9f19d"
|
|
4216
4221
|
},
|
|
4217
4222
|
{
|
|
4218
4223
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -4222,7 +4227,7 @@
|
|
|
4222
4227
|
{
|
|
4223
4228
|
"path": "modules/dev-box/box-access.js",
|
|
4224
4229
|
"mode": "0000644",
|
|
4225
|
-
"sha256": "
|
|
4230
|
+
"sha256": "7bc7c746f2518cb465773a205c8e9fe7efc30852c857001255be92ad8e340bca"
|
|
4226
4231
|
},
|
|
4227
4232
|
{
|
|
4228
4233
|
"path": "modules/dev-box/box-credential.js",
|
|
@@ -4237,7 +4242,7 @@
|
|
|
4237
4242
|
{
|
|
4238
4243
|
"path": "modules/dev-box/boxes.js",
|
|
4239
4244
|
"mode": "0000644",
|
|
4240
|
-
"sha256": "
|
|
4245
|
+
"sha256": "667993d81db36e1ddd1307013044f3d40b6baca3c6f5271b77332d10fa290063"
|
|
4241
4246
|
},
|
|
4242
4247
|
{
|
|
4243
4248
|
"path": "modules/dev-box/db.js",
|
|
@@ -4252,7 +4257,7 @@
|
|
|
4252
4257
|
{
|
|
4253
4258
|
"path": "modules/dev-box/routes/box.js",
|
|
4254
4259
|
"mode": "0000644",
|
|
4255
|
-
"sha256": "
|
|
4260
|
+
"sha256": "b61996de3fa3dbdc9b8fb639fd53aba5c1c2567350dc602b39ebbbd3a93164ac"
|
|
4256
4261
|
},
|
|
4257
4262
|
{
|
|
4258
4263
|
"path": "modules/discord/CLAUDE.md",
|
|
@@ -7722,12 +7727,12 @@
|
|
|
7722
7727
|
{
|
|
7723
7728
|
"path": "package-lock.json",
|
|
7724
7729
|
"mode": "0000644",
|
|
7725
|
-
"sha256": "
|
|
7730
|
+
"sha256": "b271a576a07b28caf13c7d64f1ba8407e3a4d5e53d13accf0da4ad461f94382e"
|
|
7726
7731
|
},
|
|
7727
7732
|
{
|
|
7728
7733
|
"path": "package.json",
|
|
7729
7734
|
"mode": "0000644",
|
|
7730
|
-
"sha256": "
|
|
7735
|
+
"sha256": "e6e4d441aab1b961398eb377d6ac1e4cb9f072efcfa26bd5f51b878b116a8a3e"
|
|
7731
7736
|
},
|
|
7732
7737
|
{
|
|
7733
7738
|
"path": "public-docs/index.html",
|
|
@@ -7917,7 +7922,7 @@
|
|
|
7917
7922
|
{
|
|
7918
7923
|
"path": "scripts/gds/box-sync.js",
|
|
7919
7924
|
"mode": "0000644",
|
|
7920
|
-
"sha256": "
|
|
7925
|
+
"sha256": "33a79ddd2049419fe7df09cd7b5fe3da9163728a36bc8fe1351d89dcf2463a11"
|
|
7921
7926
|
},
|
|
7922
7927
|
{
|
|
7923
7928
|
"path": "scripts/gds/box.js",
|
|
@@ -9482,7 +9487,7 @@
|
|
|
9482
9487
|
{
|
|
9483
9488
|
"path": "src/module-api.js",
|
|
9484
9489
|
"mode": "0000644",
|
|
9485
|
-
"sha256": "
|
|
9490
|
+
"sha256": "5394603956c3ec34e8377f82a7f493d7c029f0db1d99b236a0e024fd619fe869"
|
|
9486
9491
|
},
|
|
9487
9492
|
{
|
|
9488
9493
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9862,7 +9867,7 @@
|
|
|
9862
9867
|
{
|
|
9863
9868
|
"path": "tests/box_access.mjs",
|
|
9864
9869
|
"mode": "0000644",
|
|
9865
|
-
"sha256": "
|
|
9870
|
+
"sha256": "911a764f72976298daa17855acb836d0ada3215b480961ae8607e1fc3634f882"
|
|
9866
9871
|
},
|
|
9867
9872
|
{
|
|
9868
9873
|
"path": "tests/box_auth_check.mjs",
|
|
@@ -9932,7 +9937,7 @@
|
|
|
9932
9937
|
{
|
|
9933
9938
|
"path": "tests/box_sync_scope_report.mjs",
|
|
9934
9939
|
"mode": "0000644",
|
|
9935
|
-
"sha256": "
|
|
9940
|
+
"sha256": "4b1df674e2e53ce9d521bab3c43c2b3959dee7daa51b97dfa9b23c20e2a41f94"
|
|
9936
9941
|
},
|
|
9937
9942
|
{
|
|
9938
9943
|
"path": "tests/box_task_scope.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
|
-
-
|
|
8
|
+
- 362 operations across 55 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -205,6 +205,10 @@ function createClient(opts = {}) {
|
|
|
205
205
|
postBoxTerminal: (args) => request("POST", "/box/terminal", { hasBody: true }, args),
|
|
206
206
|
// POST /box/version — rank: any-builder — POST /box/version
|
|
207
207
|
postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
|
|
208
|
+
// GET /box/widen — rank: any-builder — GET /box/widen
|
|
209
|
+
getBoxWiden: (args) => request("GET", "/box/widen", { hasBody: false }, args),
|
|
210
|
+
// POST /box/widen — rank: any-builder — POST /box/widen
|
|
211
|
+
postBoxWiden: (args) => request("POST", "/box/widen", { hasBody: true }, args),
|
|
208
212
|
},
|
|
209
213
|
"boxes": {
|
|
210
214
|
// GET /boxes — rank: metic+archon — GET /boxes
|
|
@@ -204,6 +204,10 @@ function createClient(opts = {}) {
|
|
|
204
204
|
postBoxTerminal: (args) => request("POST", "/box/terminal", { hasBody: true }, args),
|
|
205
205
|
// POST /box/version — rank: any-builder — POST /box/version
|
|
206
206
|
postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
|
|
207
|
+
// GET /box/widen — rank: any-builder — GET /box/widen
|
|
208
|
+
getBoxWiden: (args) => request("GET", "/box/widen", { hasBody: false }, args),
|
|
209
|
+
// POST /box/widen — rank: any-builder — POST /box/widen
|
|
210
|
+
postBoxWiden: (args) => request("POST", "/box/widen", { hasBody: true }, args),
|
|
207
211
|
},
|
|
208
212
|
"boxes": {
|
|
209
213
|
// GET /boxes — rank: metic+archon — GET /boxes
|
|
@@ -29,6 +29,7 @@ export interface GetBoxMeResponse { box: unknown; open_intent: unknown; cost: un
|
|
|
29
29
|
export interface GetBoxSourceAccessResponse { scope: unknown; mode: unknown; sparse_paths: unknown; repo_url: unknown; credential: unknown; configured: unknown; separate_starter_repo: unknown; expires_at: unknown; ttl_seconds: unknown; guidance: unknown }
|
|
30
30
|
export interface GetBoxSshKeyResponse { keys: unknown }
|
|
31
31
|
export interface GetBoxTerminalResponse { state: unknown; url: unknown; credential: unknown; hint: unknown; warming_up?: boolean; terminal_at?: unknown }
|
|
32
|
+
export interface GetBoxWidenResponse { widen_paths: unknown; admitted: unknown; ignored: unknown; max: unknown }
|
|
32
33
|
export interface GetBoxesResponse { boxes: unknown }
|
|
33
34
|
export interface GetBuildersDirectoryResponse { builders: unknown }
|
|
34
35
|
export interface GetBuildersIdActiveClaimsResponse { builder_id: unknown; claims: unknown; stale_hours: unknown; stale_hours_source: unknown }
|
|
@@ -222,6 +223,8 @@ export interface PostBoxTerminalRequest { url: string; credential?: string }
|
|
|
222
223
|
export interface PostBoxTerminalResponse { ok: boolean; state: unknown }
|
|
223
224
|
export interface PostBoxVersionRequest { sha: string; committed_at?: string }
|
|
224
225
|
export interface PostBoxVersionResponse { ok: boolean; state: unknown }
|
|
226
|
+
export interface PostBoxWidenRequest { add?: unknown[]; remove?: unknown[]; clear?: boolean }
|
|
227
|
+
export interface PostBoxWidenResponse { ok: boolean; widen_paths: unknown; admitted: unknown; ignored: unknown; max: unknown }
|
|
225
228
|
export interface PostBoxesBuilderIdCloseResponse { ok: boolean; queued: unknown; action: unknown; state: unknown; message: unknown }
|
|
226
229
|
export interface PostClaimsBatchReleaseRequest { claim_ids: unknown[]; force?: boolean }
|
|
227
230
|
export interface PostClaimsBatchReleaseResponse { released: unknown; failures: unknown }
|
|
@@ -536,6 +539,10 @@ export interface BongosClient {
|
|
|
536
539
|
postBoxTerminal(args: RequestArgs & { body: PostBoxTerminalRequest }): Promise<PostBoxTerminalResponse>;
|
|
537
540
|
/** POST /box/version — rank: any-builder */
|
|
538
541
|
postBoxVersion(args: RequestArgs & { body: PostBoxVersionRequest }): Promise<PostBoxVersionResponse>;
|
|
542
|
+
/** GET /box/widen — rank: any-builder */
|
|
543
|
+
getBoxWiden(args?: RequestArgs): Promise<GetBoxWidenResponse>;
|
|
544
|
+
/** POST /box/widen — rank: any-builder */
|
|
545
|
+
postBoxWiden(args?: RequestArgs & { body?: PostBoxWidenRequest }): Promise<PostBoxWidenResponse>;
|
|
539
546
|
};
|
|
540
547
|
"boxes": {
|
|
541
548
|
/** GET /boxes — rank: metic+archon */
|
|
@@ -201,6 +201,10 @@ export function createClient(opts = {}) {
|
|
|
201
201
|
postBoxTerminal: (args) => request("POST", "/box/terminal", { hasBody: true }, args),
|
|
202
202
|
// POST /box/version — rank: any-builder — POST /box/version
|
|
203
203
|
postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
|
|
204
|
+
// GET /box/widen — rank: any-builder — GET /box/widen
|
|
205
|
+
getBoxWiden: (args) => request("GET", "/box/widen", { hasBody: false }, args),
|
|
206
|
+
// POST /box/widen — rank: any-builder — POST /box/widen
|
|
207
|
+
postBoxWiden: (args) => request("POST", "/box/widen", { hasBody: true }, args),
|
|
204
208
|
},
|
|
205
209
|
"boxes": {
|
|
206
210
|
// GET /boxes — rank: metic+archon — GET /boxes
|
package/docs/api/openapi.json
CHANGED
|
@@ -2275,6 +2275,91 @@
|
|
|
2275
2275
|
]
|
|
2276
2276
|
}
|
|
2277
2277
|
},
|
|
2278
|
+
"/box/widen": {
|
|
2279
|
+
"get": {
|
|
2280
|
+
"operationId": "get_box_widen",
|
|
2281
|
+
"tags": [
|
|
2282
|
+
"box"
|
|
2283
|
+
],
|
|
2284
|
+
"summary": "GET /box/widen",
|
|
2285
|
+
"description": "GET /box/widen — the caller's own recorded widen set (task 1003087). rank: any authenticated builder, own resource. Reports what is STORED plus what is currently ADMITTED, because those differ whenever a request sits outside the builder's rank scope — showing only the stored list would let a Xenos believe a widen took effect that the fetch quietly drops.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
2286
|
+
"x-rank": "any-builder",
|
|
2287
|
+
"x-source": "modules/dev-box/routes/box.js",
|
|
2288
|
+
"responses": {
|
|
2289
|
+
"200": {
|
|
2290
|
+
"description": "Success.",
|
|
2291
|
+
"content": {
|
|
2292
|
+
"application/json": {
|
|
2293
|
+
"schema": {
|
|
2294
|
+
"$ref": "#/components/schemas/GetBoxWidenResponse"
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
"400": {
|
|
2300
|
+
"$ref": "#/components/responses/BadRequest"
|
|
2301
|
+
},
|
|
2302
|
+
"401": {
|
|
2303
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
2304
|
+
},
|
|
2305
|
+
"403": {
|
|
2306
|
+
"$ref": "#/components/responses/Forbidden"
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
"security": [
|
|
2310
|
+
{
|
|
2311
|
+
"builderSession": []
|
|
2312
|
+
}
|
|
2313
|
+
]
|
|
2314
|
+
},
|
|
2315
|
+
"post": {
|
|
2316
|
+
"operationId": "post_box_widen",
|
|
2317
|
+
"tags": [
|
|
2318
|
+
"box"
|
|
2319
|
+
],
|
|
2320
|
+
"summary": "POST /box/widen",
|
|
2321
|
+
"description": "POST /box/widen — record extra sparse-checkout directories that must SURVIVE the */10 source-fetch (task 1003087, idea 1000793). rank: any authenticated builder, OWN box (builder_id-scoped in SQL, so no id from the body can point this at someone else's box). Deliberately NOT allowBoxScope. The on-box crons read source-access with a box-scoped session (ADR 0053); this is a WRITE that changes what the box pulls forever after, so it takes the builder's own session — the same posture that keeps a stolen box token from re-scoping the checkout it was stolen from. add/remove are applied to the stored set and the result is re-normalized, so the endpoint is idempotent: widening twice is not an error, and neither is removing something that was never there. A path outside the caller's rank scope is ACCEPTED into storage but reported in `ignored` rather than refused. That is the honest shape: rank can change, and a builder promoted to Metic should find the widen they asked for as a Xenos simply start working, instead of having been silently discarded months earlier. What it can never do is take effect early — admissibleWidenPaths is re-evaluated against the LIVE rank on every single fetch.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
2322
|
+
"x-rank": "any-builder",
|
|
2323
|
+
"x-source": "modules/dev-box/routes/box.js",
|
|
2324
|
+
"requestBody": {
|
|
2325
|
+
"required": false,
|
|
2326
|
+
"content": {
|
|
2327
|
+
"application/json": {
|
|
2328
|
+
"schema": {
|
|
2329
|
+
"$ref": "#/components/schemas/PostBoxWidenRequest"
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
},
|
|
2333
|
+
"x-validated": true
|
|
2334
|
+
},
|
|
2335
|
+
"responses": {
|
|
2336
|
+
"200": {
|
|
2337
|
+
"description": "Success.",
|
|
2338
|
+
"content": {
|
|
2339
|
+
"application/json": {
|
|
2340
|
+
"schema": {
|
|
2341
|
+
"$ref": "#/components/schemas/PostBoxWidenResponse"
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
},
|
|
2346
|
+
"400": {
|
|
2347
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
2348
|
+
},
|
|
2349
|
+
"401": {
|
|
2350
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
2351
|
+
},
|
|
2352
|
+
"403": {
|
|
2353
|
+
"$ref": "#/components/responses/Forbidden"
|
|
2354
|
+
}
|
|
2355
|
+
},
|
|
2356
|
+
"security": [
|
|
2357
|
+
{
|
|
2358
|
+
"builderSession": []
|
|
2359
|
+
}
|
|
2360
|
+
]
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2278
2363
|
"/boxes": {
|
|
2279
2364
|
"get": {
|
|
2280
2365
|
"operationId": "get_boxes",
|
|
@@ -16801,6 +16886,21 @@
|
|
|
16801
16886
|
"hint"
|
|
16802
16887
|
]
|
|
16803
16888
|
},
|
|
16889
|
+
"GetBoxWidenResponse": {
|
|
16890
|
+
"type": "object",
|
|
16891
|
+
"properties": {
|
|
16892
|
+
"widen_paths": {},
|
|
16893
|
+
"admitted": {},
|
|
16894
|
+
"ignored": {},
|
|
16895
|
+
"max": {}
|
|
16896
|
+
},
|
|
16897
|
+
"required": [
|
|
16898
|
+
"widen_paths",
|
|
16899
|
+
"admitted",
|
|
16900
|
+
"ignored",
|
|
16901
|
+
"max"
|
|
16902
|
+
]
|
|
16903
|
+
},
|
|
16804
16904
|
"GetBoxesResponse": {
|
|
16805
16905
|
"type": "object",
|
|
16806
16906
|
"properties": {
|
|
@@ -19429,6 +19529,40 @@
|
|
|
19429
19529
|
"state"
|
|
19430
19530
|
]
|
|
19431
19531
|
},
|
|
19532
|
+
"PostBoxWidenRequest": {
|
|
19533
|
+
"type": "object",
|
|
19534
|
+
"properties": {
|
|
19535
|
+
"add": {
|
|
19536
|
+
"type": "array"
|
|
19537
|
+
},
|
|
19538
|
+
"remove": {
|
|
19539
|
+
"type": "array"
|
|
19540
|
+
},
|
|
19541
|
+
"clear": {
|
|
19542
|
+
"type": "boolean"
|
|
19543
|
+
}
|
|
19544
|
+
},
|
|
19545
|
+
"additionalProperties": false
|
|
19546
|
+
},
|
|
19547
|
+
"PostBoxWidenResponse": {
|
|
19548
|
+
"type": "object",
|
|
19549
|
+
"properties": {
|
|
19550
|
+
"ok": {
|
|
19551
|
+
"type": "boolean"
|
|
19552
|
+
},
|
|
19553
|
+
"widen_paths": {},
|
|
19554
|
+
"admitted": {},
|
|
19555
|
+
"ignored": {},
|
|
19556
|
+
"max": {}
|
|
19557
|
+
},
|
|
19558
|
+
"required": [
|
|
19559
|
+
"ok",
|
|
19560
|
+
"widen_paths",
|
|
19561
|
+
"admitted",
|
|
19562
|
+
"ignored",
|
|
19563
|
+
"max"
|
|
19564
|
+
]
|
|
19565
|
+
},
|
|
19432
19566
|
"PostBoxesBuilderIdCloseResponse": {
|
|
19433
19567
|
"type": "object",
|
|
19434
19568
|
"properties": {
|
|
@@ -22806,9 +22940,9 @@
|
|
|
22806
22940
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
22807
22941
|
}
|
|
22808
22942
|
},
|
|
22809
|
-
"x-endpoint-count":
|
|
22810
|
-
"x-schema-count":
|
|
22943
|
+
"x-endpoint-count": 362,
|
|
22944
|
+
"x-schema-count": 401,
|
|
22811
22945
|
"x-undocumented-bodies": 9,
|
|
22812
|
-
"x-response-schemas":
|
|
22946
|
+
"x-response-schemas": 270,
|
|
22813
22947
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
22814
22948
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 362 endpoints across 58 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/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -81,7 +81,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
81
81
|
| POST | `/api/bongos/blockers/:id/link` | `metic+archon` | `task_id` | Link an existing blocker to a task. |
|
|
82
82
|
| POST | `/api/bongos/blockers/:id/resolve` | `metic+archon` | `resolution_note` | Resolve. |
|
|
83
83
|
|
|
84
|
-
## `box` (
|
|
84
|
+
## `box` (20)
|
|
85
85
|
|
|
86
86
|
| Method | Path | Rank | Body | Description |
|
|
87
87
|
|---|---|---|---|---|
|
|
@@ -103,6 +103,8 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
103
103
|
| GET | `/api/bongos/box/terminal` | `any-builder` | — | GET /box/terminal — the caller reads their box's web-terminal URL + credential (the Chromebook on-ramp, ADR 0038: ttyd behind a Cloudflar… |
|
|
104
104
|
| POST | `/api/bongos/box/terminal` | `any-builder` | `url`, `credential` | POST /box/terminal — the box publishes its current web-terminal URL + the per-box basic-auth credential. |
|
|
105
105
|
| POST | `/api/bongos/box/version` | `any-builder` | `sha`, `committed_at` | POST /box/version — the box reports its /workspace HEAD commit so the server can flag a stale box (idea 332 / task 1316, ADR 0072). |
|
|
106
|
+
| GET | `/api/bongos/box/widen` | `any-builder` | — | GET /box/widen — the caller's own recorded widen set (task 1003087). |
|
|
107
|
+
| POST | `/api/bongos/box/widen` | `any-builder` | `add`, `remove`, `clear` | POST /box/widen — record extra sparse-checkout directories that must SURVIVE the */10 source-fetch (task 1003087, idea 1000793). |
|
|
106
108
|
|
|
107
109
|
## `boxes` (4)
|
|
108
110
|
|
|
@@ -1741,5 +1741,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1741
1741
|
landed since 1.19.644 with no explicit bump. run 34511775606. (task 1002620)
|
|
1742
1742
|
1.19.646 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1743
1743
|
landed since 1.19.645 with no explicit bump. run 34512629504. (task 1002620)
|
|
1744
|
+
1.19.647 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1745
|
+
landed since 1.19.646 with no explicit bump. run 34515733973. (task 1002620)
|
|
1744
1746
|
---------------------------------------------------------------------------
|
|
1745
1747
|
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- core_237_box_widen_paths.sql — let a builder's own sparse-checkout widening
|
|
2
|
+
-- survive the */10 source-fetch tick (task 1003087, goal 1000095, idea 1000793).
|
|
3
|
+
--
|
|
4
|
+
-- THE BUG THIS EXISTS FOR. The box's source-fetch cron re-applies the
|
|
5
|
+
-- SERVER-COMPUTED sparse set every ~10 minutes. A builder who runs
|
|
6
|
+
-- `git sparse-checkout add <path>` to see code their task scope did not pull gets
|
|
7
|
+
-- it deleted out from under them on the next tick — including WHILE A COMMAND IS
|
|
8
|
+
-- RUNNING. A live test suite died mid-run with MODULE_NOT_FOUND that read exactly
|
|
9
|
+
-- like a code bug. It also breaks gen-repo-map, whose output is TREE-DEPENDENT:
|
|
10
|
+
-- a sparse tree regenerates different nested CLAUDE.md symbol blocks than CI's
|
|
11
|
+
-- full tree, so a box-committed regen reds CI's freshness gate (learning 1000192;
|
|
12
|
+
-- it stranded PR #254 until the tree was widened).
|
|
13
|
+
--
|
|
14
|
+
-- WHY THE FIX LIVES IN THE DATABASE AND NOT IN THE FETCH SCRIPT. The obvious fix
|
|
15
|
+
-- is "make box-source-fetch.sh union the builder's additions". It cannot be done
|
|
16
|
+
-- there from this repo: the fetch script is INSTANCE-owned, not shipped by the
|
|
17
|
+
-- neutral core (ADR 0150). More importantly it would not work — the cron asks the
|
|
18
|
+
-- SERVER for the set on every tick, so anything the script remembers locally is
|
|
19
|
+
-- one `git sparse-checkout set` away from being overwritten anyway. Recording the
|
|
20
|
+
-- widening server-side is what makes it tick-surviving: the union is computed in
|
|
21
|
+
-- GET /box/source-access, so EVERY fetch — cron included — already returns it,
|
|
22
|
+
-- and no instance-owned file has to change.
|
|
23
|
+
--
|
|
24
|
+
-- ONE COLUMN, NOT A TABLE. A widen set is small (capped in box-access.js), has no
|
|
25
|
+
-- attributes of its own, and is meaningless apart from its box — the same shape
|
|
26
|
+
-- as the existing array-ish per-box state on this table. A join table would buy
|
|
27
|
+
-- nothing and cost a query on the hot source-fetch path.
|
|
28
|
+
--
|
|
29
|
+
-- NOT AN AUTHORITY GRANT. This column widens what is MATERIALIZED on the box, not
|
|
30
|
+
-- what the builder may author. The wall stays where ADR 0148 put it — goal-scope
|
|
31
|
+
-- -check against the module globs, not what happens to be on disk — and
|
|
32
|
+
-- box-access.js admits a widen path only when the builder's RANK scope already
|
|
33
|
+
-- reaches it, so a Xenos cannot widen past the starter surface. Empty default, so
|
|
34
|
+
-- every existing box is unchanged until its owner asks for something.
|
|
35
|
+
|
|
36
|
+
ALTER TABLE builder_boxes
|
|
37
|
+
ADD COLUMN IF NOT EXISTS widen_paths text[] NOT NULL DEFAULT '{}';
|
|
38
|
+
|
|
39
|
+
COMMENT ON COLUMN builder_boxes.widen_paths IS
|
|
40
|
+
'Builder-requested extra sparse-checkout directories, unioned into the server-computed set on every source fetch so they survive the */10 cron (task 1003087). Admitted only within the builder''s rank scope — see modules/dev-box/box-access.js admissibleWidenPaths.';
|
|
@@ -204,6 +204,10 @@ function createClient(opts = {}) {
|
|
|
204
204
|
postBoxTerminal: (args) => request("POST", "/box/terminal", { hasBody: true }, args),
|
|
205
205
|
// POST /box/version — rank: any-builder — POST /box/version
|
|
206
206
|
postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
|
|
207
|
+
// GET /box/widen — rank: any-builder — GET /box/widen
|
|
208
|
+
getBoxWiden: (args) => request("GET", "/box/widen", { hasBody: false }, args),
|
|
209
|
+
// POST /box/widen — rank: any-builder — POST /box/widen
|
|
210
|
+
postBoxWiden: (args) => request("POST", "/box/widen", { hasBody: true }, args),
|
|
207
211
|
},
|
|
208
212
|
"boxes": {
|
|
209
213
|
// GET /boxes — rank: metic+archon — GET /boxes
|
|
@@ -254,6 +254,117 @@ function sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) {
|
|
|
254
254
|
return [...base, ...extra];
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// PURE: builder-requested WIDENING (task 1003087 / idea 1000793)
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
// A builder sometimes needs a directory their task scope did not pull — to run
|
|
262
|
+
// the full suite, to read a caller, to regenerate a TREE-DEPENDENT artifact. The
|
|
263
|
+
// obvious move, `git sparse-checkout add <path>`, does not survive: the */10
|
|
264
|
+
// source-fetch cron re-applies the SERVER-computed set and deletes the addition,
|
|
265
|
+
// including WHILE A COMMAND IS RUNNING (a live suite died mid-run with
|
|
266
|
+
// MODULE_NOT_FOUND that read exactly like a code bug; a box-committed
|
|
267
|
+
// gen-repo-map regen off a narrow tree reds CI's freshness gate — learning
|
|
268
|
+
// 1000192, which stranded PR #254).
|
|
269
|
+
//
|
|
270
|
+
// So the widening is recorded SERVER-side and unioned in HERE, which is what
|
|
271
|
+
// makes it tick-surviving: the cron asks this endpoint for the set on every tick,
|
|
272
|
+
// so it gets the union too. Nothing in the instance-owned fetch script changes —
|
|
273
|
+
// which matters, because the neutral core does not ship it (ADR 0150).
|
|
274
|
+
|
|
275
|
+
// Bounds. Small on purpose: this is an escape hatch for a handful of dirs, not a
|
|
276
|
+
// second scope system. Over the cap the extras are dropped, not an error — a
|
|
277
|
+
// truncated widen still fetches, and failing the source pull over it would be a
|
|
278
|
+
// far worse trade than materializing less.
|
|
279
|
+
const MAX_WIDEN_PATHS = 24;
|
|
280
|
+
const MAX_WIDEN_PATH_LEN = 200;
|
|
281
|
+
|
|
282
|
+
// withinAny(p, roots) — is p one of roots, or underneath one? Cone mode
|
|
283
|
+
// materializes a directory and everything below it, so "already covered" and
|
|
284
|
+
// "inside the allowed surface" are the same ancestor walk.
|
|
285
|
+
function withinAny(p, roots) {
|
|
286
|
+
let q = p;
|
|
287
|
+
for (;;) {
|
|
288
|
+
if (roots.includes(q)) return true;
|
|
289
|
+
const cut = q.lastIndexOf('/');
|
|
290
|
+
if (cut < 0) return false;
|
|
291
|
+
q = q.slice(0, cut);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// normalizeWidenPath — repo-relative POSIX directory, or null when it is not one.
|
|
296
|
+
//
|
|
297
|
+
// This is a path that reaches `git sparse-checkout set` on a box, so it is
|
|
298
|
+
// validated as INPUT rather than trusted: absolute paths, Windows drive letters,
|
|
299
|
+
// '..' traversal and empty segments are all refused rather than normalized away,
|
|
300
|
+
// because "cleaned up into something that still works" is how a traversal becomes
|
|
301
|
+
// a shrug. Backslashes are folded to '/' first so a Windows-typed path is judged
|
|
302
|
+
// on what it means, not on its separator.
|
|
303
|
+
function normalizeWidenPath(raw) {
|
|
304
|
+
if (typeof raw !== 'string') return null;
|
|
305
|
+
// BACKSLASH by char code, not as a literal: this file is edited through
|
|
306
|
+
// tooling that has silently collapsed a doubled backslash before, and a
|
|
307
|
+
// separator fold that quietly stops folding is invisible until a Windows
|
|
308
|
+
// path slips through as one long segment.
|
|
309
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
310
|
+
let p = raw.trim().split(BACKSLASH).join('/');
|
|
311
|
+
while (p.startsWith('./')) p = p.slice(2);
|
|
312
|
+
p = p.replace(/\/+/g, '/').replace(/\/+$/, '');
|
|
313
|
+
if (!p || p === '.' || p.length > MAX_WIDEN_PATH_LEN) return null;
|
|
314
|
+
if (p.startsWith('/') || /^[A-Za-z]:/.test(p)) return null;
|
|
315
|
+
const segs = p.split('/');
|
|
316
|
+
if (!segs.every((s) => s && s !== '.' && s !== '..' && /^[A-Za-z0-9._-]+$/.test(s))) return null;
|
|
317
|
+
return p;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// admissibleWidenPaths — the widen requests this builder's RANK actually permits,
|
|
321
|
+
// normalized, deduped, capped and sorted.
|
|
322
|
+
//
|
|
323
|
+
// THIS IS THE SECURITY BOUNDARY OF THE FEATURE, so it is stated plainly: widening
|
|
324
|
+
// changes what is MATERIALIZED on a box, and rank decides how much of the repo a
|
|
325
|
+
// box may hold at all (ADR 0031 section 9.2 — 'full' for Metic+, the 'starter'
|
|
326
|
+
// surface below that). Honoring an arbitrary widen would hand a Xenos the whole
|
|
327
|
+
// tree through a convenience flag, which is a rank escalation dressed as
|
|
328
|
+
// ergonomics. So:
|
|
329
|
+
// - a builder whose RANK scope is 'full' may widen anywhere (they are already
|
|
330
|
+
// entitled to the entire repo; the widen only decides what gets pulled);
|
|
331
|
+
// - anyone else may widen only INSIDE the starter surface. That is not a null
|
|
332
|
+
// grant: a task-scoped box is BASE union its modules, which is NARROWER than
|
|
333
|
+
// starter, so this is exactly the room to climb back to what their rank
|
|
334
|
+
// already allowed — and no further.
|
|
335
|
+
// The authoring wall is untouched either way. What a builder may CHANGE is still
|
|
336
|
+
// goal-scope-check against the module globs, never what happens to be on disk
|
|
337
|
+
// (ADR 0148).
|
|
338
|
+
function admissibleWidenPaths(widenPaths, rank) {
|
|
339
|
+
const unrestricted = boxScopeForRank(rank) === 'full';
|
|
340
|
+
const out = [];
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
for (const raw of Array.isArray(widenPaths) ? widenPaths : []) {
|
|
343
|
+
const p = normalizeWidenPath(raw);
|
|
344
|
+
if (!p || seen.has(p)) continue;
|
|
345
|
+
if (!unrestricted && !withinAny(p, STARTER_SPARSE_PATHS)) continue;
|
|
346
|
+
seen.add(p);
|
|
347
|
+
out.push(p);
|
|
348
|
+
if (out.length >= MAX_WIDEN_PATHS) break;
|
|
349
|
+
}
|
|
350
|
+
return out.sort();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// withWidenPaths — union admitted widen dirs into a computed sparse set, skipping
|
|
354
|
+
// any the set already covers (cone mode pulls a dir and everything under it, so
|
|
355
|
+
// adding 'scripts/gds' when 'scripts' is present is noise in the git command and
|
|
356
|
+
// noise in the box's scope readout). A null set is the 'full' clone — there is
|
|
357
|
+
// nothing to widen into, so it passes through untouched.
|
|
358
|
+
function withWidenPaths(sparsePaths, admitted) {
|
|
359
|
+
if (!Array.isArray(sparsePaths) || !admitted || admitted.length === 0) return sparsePaths;
|
|
360
|
+
const out = [...sparsePaths];
|
|
361
|
+
for (const dir of admitted) {
|
|
362
|
+
if (withinAny(dir, out)) continue;
|
|
363
|
+
out.push(dir);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
257
368
|
// ---------------------------------------------------------------------------
|
|
258
369
|
// PURE: the per-fetch access decision (consumed by GET /box/source-access)
|
|
259
370
|
// ---------------------------------------------------------------------------
|
|
@@ -295,7 +406,7 @@ function sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) {
|
|
|
295
406
|
// { allowed:false, reason:'BUILDER_INACTIVE' }
|
|
296
407
|
// { allowed:false, reason:'INSUFFICIENT_RANK', required, actual }
|
|
297
408
|
// { allowed:true, scope, spec }
|
|
298
|
-
function decideSourceAccess({ rank, status, floor = DEFAULT_PROVISION_FLOOR, scopeKeys, hasActiveClaims, moduleScopeMap } = {}) {
|
|
409
|
+
function decideSourceAccess({ rank, status, floor = DEFAULT_PROVISION_FLOOR, scopeKeys, hasActiveClaims, moduleScopeMap, widenPaths } = {}) {
|
|
299
410
|
if (status && status !== 'active') {
|
|
300
411
|
return { allowed: false, reason: 'BUILDER_INACTIVE' };
|
|
301
412
|
}
|
|
@@ -307,23 +418,32 @@ function decideSourceAccess({ rank, status, floor = DEFAULT_PROVISION_FLOOR, sco
|
|
|
307
418
|
actual: rank ?? null,
|
|
308
419
|
};
|
|
309
420
|
}
|
|
421
|
+
// The builder's own widening, narrowed to what their rank already reaches
|
|
422
|
+
// (task 1003087). Computed once here so every sparse branch below unions the
|
|
423
|
+
// SAME set — a widen that applied to one scope and not another would be a
|
|
424
|
+
// surprise that only surfaces when a claim changes.
|
|
425
|
+
const widen = admissibleWidenPaths(widenPaths, rank);
|
|
426
|
+
|
|
310
427
|
// Claim-driven task scope (present → the box asks for exactly its work's code).
|
|
311
428
|
if (Array.isArray(scopeKeys)) {
|
|
312
429
|
if (scopeKeys.length > 0) {
|
|
313
|
-
const sparsePaths = sparsePathsForScopeKeys(scopeKeys, moduleScopeMap);
|
|
430
|
+
const sparsePaths = withWidenPaths(sparsePathsForScopeKeys(scopeKeys, moduleScopeMap), widen);
|
|
314
431
|
return { allowed: true, scope: 'task', spec: { scope: 'task', mode: 'sparse', sparsePaths } };
|
|
315
432
|
}
|
|
316
433
|
// Empty. A builder mid-claim whose goals declare no wall gets the rank scope,
|
|
317
434
|
// NOT a stripped box — the planning gap is not theirs to pay for.
|
|
318
435
|
if (!hasActiveClaims) {
|
|
319
|
-
const sparsePaths = sparsePathsForScopeKeys([], moduleScopeMap);
|
|
436
|
+
const sparsePaths = withWidenPaths(sparsePathsForScopeKeys([], moduleScopeMap), widen);
|
|
320
437
|
return { allowed: true, scope: 'base', spec: { scope: 'base', mode: 'sparse', sparsePaths } };
|
|
321
438
|
}
|
|
322
439
|
}
|
|
323
440
|
// No claim signal (no lifecycle port on this instance), or claims with an
|
|
324
441
|
// undeclared goal wall: the pre-ADR-0148 rank fallback.
|
|
325
442
|
const scope = boxScopeForRank(rank);
|
|
326
|
-
|
|
443
|
+
const spec = cloneSpecForScope(scope);
|
|
444
|
+
// 'full' carries sparsePaths null (the whole repo) — withWidenPaths passes it
|
|
445
|
+
// through, because there is no sparse set to widen INTO.
|
|
446
|
+
return { allowed: true, scope, spec: { ...spec, sparsePaths: withWidenPaths(spec.sparsePaths, widen) } };
|
|
327
447
|
}
|
|
328
448
|
|
|
329
449
|
// scopeMetadata — PURE. The credential-FREE projection of a source-access
|
|
@@ -470,6 +590,10 @@ module.exports = {
|
|
|
470
590
|
boxCanHoldSource,
|
|
471
591
|
cloneSpecForScope,
|
|
472
592
|
sparsePathsForScopeKeys, // ADR 0148 — BASE ∪ the claims' module dirs (cone-mode)
|
|
593
|
+
MAX_WIDEN_PATHS, // task 1003087 — the builder-widening cap
|
|
594
|
+
normalizeWidenPath, // task 1003087 — repo-relative dir, or null
|
|
595
|
+
admissibleWidenPaths, // task 1003087 — the widen requests this RANK permits
|
|
596
|
+
withWidenPaths, // task 1003087 — union a widen set into a sparse set
|
|
473
597
|
scopeMetadata, // BV1.R100 — credential-free projection for GET /box/scope
|
|
474
598
|
// pure — decisions
|
|
475
599
|
decideSourceAccess,
|
package/modules/dev-box/boxes.js
CHANGED
|
@@ -397,6 +397,26 @@ function decideEnsureAction({ boxState, rank, status, blocked = false, floor = D
|
|
|
397
397
|
// DB helpers (async). Each takes the pg pool (or a client mid-transaction).
|
|
398
398
|
// ---------------------------------------------------------------------------
|
|
399
399
|
|
|
400
|
+
// setBoxWidenPaths — replace a box row's builder-requested widen set (task
|
|
401
|
+
// 1003087). Whole-set replace rather than add/remove SQL: the set is tiny and
|
|
402
|
+
// capped, the caller has already normalized and rank-filtered it, and a
|
|
403
|
+
// read-modify-write in one statement keeps two concurrent widens from
|
|
404
|
+
// interleaving into a half-applied list.
|
|
405
|
+
//
|
|
406
|
+
// Returns the stored array, or null when the builder has no box row — the
|
|
407
|
+
// caller turns that into a 404 rather than silently creating one, because a
|
|
408
|
+
// widen with no box to apply it to is a request that has not happened yet.
|
|
409
|
+
async function setBoxWidenPaths(db, builderId, paths) {
|
|
410
|
+
const { rows } = await db.query(
|
|
411
|
+
`UPDATE builder_boxes
|
|
412
|
+
SET widen_paths = $2::text[], updated_at = now()
|
|
413
|
+
WHERE builder_id = $1
|
|
414
|
+
RETURNING widen_paths`,
|
|
415
|
+
[builderId, Array.isArray(paths) ? paths : []]
|
|
416
|
+
);
|
|
417
|
+
return rows[0] ? rows[0].widen_paths : null;
|
|
418
|
+
}
|
|
419
|
+
|
|
400
420
|
async function getBoxByBuilderId(db, builderId) {
|
|
401
421
|
const { rows } = await db.query(
|
|
402
422
|
`SELECT * FROM builder_boxes WHERE builder_id = $1`,
|
|
@@ -891,6 +911,7 @@ module.exports = {
|
|
|
891
911
|
decideEnsureAction,
|
|
892
912
|
// db
|
|
893
913
|
getBoxByBuilderId,
|
|
914
|
+
setBoxWidenPaths,
|
|
894
915
|
ensureBoxRow,
|
|
895
916
|
listBoxes,
|
|
896
917
|
recordEvent,
|
|
@@ -480,8 +480,16 @@ module.exports = function buildBoxRouter() {
|
|
|
480
480
|
hasActiveClaims = undefined;
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
|
-
|
|
483
|
+
// Read the box BEFORE deciding: its widen_paths are an INPUT to the sparse
|
|
484
|
+
// set now (task 1003087). A builder-requested directory is unioned in here,
|
|
485
|
+
// which is what makes it survive the */10 cron — the cron re-asks THIS
|
|
486
|
+
// endpoint every tick, so the union comes back with it instead of being
|
|
487
|
+
// deleted by the next `git sparse-checkout set`.
|
|
484
488
|
const box = await boxes.getBoxByBuilderId(pool, req.builder.id);
|
|
489
|
+
const decision = boxAccess.decideSourceAccess({
|
|
490
|
+
rank, status, scopeKeys, hasActiveClaims, moduleScopeMap: api.moduleScopeMap,
|
|
491
|
+
widenPaths: box ? box.widen_paths : null,
|
|
492
|
+
});
|
|
485
493
|
const auditDeny = (reason, extra) =>
|
|
486
494
|
box && boxAccess.recordSourceEvent(pool, {
|
|
487
495
|
box, builderId: req.builder.id, event: 'source_deny',
|
|
@@ -598,7 +606,13 @@ module.exports = function buildBoxRouter() {
|
|
|
598
606
|
hasActiveClaims = undefined;
|
|
599
607
|
}
|
|
600
608
|
}
|
|
601
|
-
|
|
609
|
+
// Same widen input as the real fetch — this readout exists precisely so it
|
|
610
|
+
// cannot drift from what the box will actually hold.
|
|
611
|
+
const scopeBox = await boxes.getBoxByBuilderId(pool, req.builder.id);
|
|
612
|
+
const decision = boxAccess.decideSourceAccess({
|
|
613
|
+
rank, status, scopeKeys, hasActiveClaims, moduleScopeMap: api.moduleScopeMap,
|
|
614
|
+
widenPaths: scopeBox ? scopeBox.widen_paths : null,
|
|
615
|
+
});
|
|
602
616
|
if (!decision.allowed) {
|
|
603
617
|
return res.fail('SOURCE_ACCESS_REVOKED', 403, {
|
|
604
618
|
reason: decision.reason,
|
|
@@ -753,6 +767,98 @@ module.exports = function buildBoxRouter() {
|
|
|
753
767
|
}
|
|
754
768
|
});
|
|
755
769
|
|
|
770
|
+
// GET /box/widen — the caller's own recorded widen set (task 1003087).
|
|
771
|
+
// rank: any authenticated builder, own resource. Reports what is STORED plus
|
|
772
|
+
// what is currently ADMITTED, because those differ whenever a request sits
|
|
773
|
+
// outside the builder's rank scope — showing only the stored list would let a
|
|
774
|
+
// Xenos believe a widen took effect that the fetch quietly drops.
|
|
775
|
+
router.get('/box/widen', auth.requireBuilder, async (req, res) => {
|
|
776
|
+
try {
|
|
777
|
+
const builder = await gdsDb.getBuilderById(req.builder.id);
|
|
778
|
+
const box = await boxes.getBoxByBuilderId(pool, req.builder.id);
|
|
779
|
+
if (!box) return res.fail('no_box', 404, { message: 'no dev box for this builder' });
|
|
780
|
+
const stored = Array.isArray(box.widen_paths) ? box.widen_paths : [];
|
|
781
|
+
const admitted = boxAccess.admissibleWidenPaths(stored, builder ? builder.rank : null);
|
|
782
|
+
res.json({
|
|
783
|
+
widen_paths: stored,
|
|
784
|
+
admitted,
|
|
785
|
+
ignored: stored.filter((p) => !admitted.includes(boxAccess.normalizeWidenPath(p))),
|
|
786
|
+
max: boxAccess.MAX_WIDEN_PATHS,
|
|
787
|
+
});
|
|
788
|
+
} catch (err) {
|
|
789
|
+
log.error('[gds] GET /box/widen', err);
|
|
790
|
+
res.fail('widen_read_failed', { status: 500, message: 'internal error' });
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// POST /box/widen — record extra sparse-checkout directories that must SURVIVE
|
|
795
|
+
// the */10 source-fetch (task 1003087, idea 1000793). rank: any authenticated
|
|
796
|
+
// builder, OWN box (builder_id-scoped in SQL, so no id from the body can point
|
|
797
|
+
// this at someone else's box).
|
|
798
|
+
//
|
|
799
|
+
// Deliberately NOT allowBoxScope. The on-box crons read source-access with a
|
|
800
|
+
// box-scoped session (ADR 0053); this is a WRITE that changes what the box
|
|
801
|
+
// pulls forever after, so it takes the builder's own session — the same posture
|
|
802
|
+
// that keeps a stolen box token from re-scoping the checkout it was stolen from.
|
|
803
|
+
//
|
|
804
|
+
// add/remove are applied to the stored set and the result is re-normalized, so
|
|
805
|
+
// the endpoint is idempotent: widening twice is not an error, and neither is
|
|
806
|
+
// removing something that was never there.
|
|
807
|
+
//
|
|
808
|
+
// A path outside the caller's rank scope is ACCEPTED into storage but reported
|
|
809
|
+
// in `ignored` rather than refused. That is the honest shape: rank can change,
|
|
810
|
+
// and a builder promoted to Metic should find the widen they asked for as a
|
|
811
|
+
// Xenos simply start working, instead of having been silently discarded months
|
|
812
|
+
// earlier. What it can never do is take effect early — admissibleWidenPaths is
|
|
813
|
+
// re-evaluated against the LIVE rank on every single fetch.
|
|
814
|
+
router.post('/box/widen', auth.requireBuilder, async (req, res) => {
|
|
815
|
+
if (validateOrRespond(req, res, {
|
|
816
|
+
add: { type: 'array' },
|
|
817
|
+
remove: { type: 'array' },
|
|
818
|
+
clear: { type: 'boolean' },
|
|
819
|
+
})) return;
|
|
820
|
+
try {
|
|
821
|
+
const box = await boxes.getBoxByBuilderId(pool, req.builder.id);
|
|
822
|
+
if (!box) return res.fail('no_box', 404, { message: 'no dev box for this builder' });
|
|
823
|
+
const builder = await gdsDb.getBuilderById(req.builder.id);
|
|
824
|
+
|
|
825
|
+
const current = Array.isArray(box.widen_paths) ? box.widen_paths : [];
|
|
826
|
+
const next = new Set(req.body && req.body.clear === true ? [] : current);
|
|
827
|
+
|
|
828
|
+
const rejected = [];
|
|
829
|
+
for (const raw of Array.isArray(req.body && req.body.add) ? req.body.add : []) {
|
|
830
|
+
const p = boxAccess.normalizeWidenPath(raw);
|
|
831
|
+
if (!p) { rejected.push(String(raw).slice(0, 120)); continue; }
|
|
832
|
+
next.add(p);
|
|
833
|
+
}
|
|
834
|
+
for (const raw of Array.isArray(req.body && req.body.remove) ? req.body.remove : []) {
|
|
835
|
+
const p = boxAccess.normalizeWidenPath(raw);
|
|
836
|
+
if (p) next.delete(p);
|
|
837
|
+
}
|
|
838
|
+
if (rejected.length) {
|
|
839
|
+
return res.fail('bad_widen_path', 400, {
|
|
840
|
+
rejected,
|
|
841
|
+
message: 'a widen path must be a repo-relative directory: no absolute paths, no "..", no drive letters',
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const stored = [...next].sort().slice(0, boxAccess.MAX_WIDEN_PATHS);
|
|
846
|
+
const saved = await boxes.setBoxWidenPaths(pool, req.builder.id, stored);
|
|
847
|
+
if (saved === null) return res.fail('no_box', 404, { message: 'no dev box for this builder' });
|
|
848
|
+
const admitted = boxAccess.admissibleWidenPaths(saved, builder ? builder.rank : null);
|
|
849
|
+
res.json({
|
|
850
|
+
ok: true,
|
|
851
|
+
widen_paths: saved,
|
|
852
|
+
admitted,
|
|
853
|
+
ignored: saved.filter((p) => !admitted.includes(p)),
|
|
854
|
+
max: boxAccess.MAX_WIDEN_PATHS,
|
|
855
|
+
});
|
|
856
|
+
} catch (err) {
|
|
857
|
+
log.error('[gds] POST /box/widen', err);
|
|
858
|
+
res.fail('widen_failed', { status: 500, message: 'internal error' });
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
756
862
|
// GET /box/authorized-keys — the box pulls its builder's authorized_keys.
|
|
757
863
|
// rank: any authenticated builder (own resource) — but the ALLOW/DENY is decided
|
|
758
864
|
// INSIDE from the caller's LIVE rank+status (boxAccess.decideSourceAccess), NOT a
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.647",
|
|
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.647",
|
|
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.647",
|
|
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",
|
package/scripts/gds/box-sync.js
CHANGED
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Usage: node scripts/gds/box-sync.js # re-fetch now
|
|
17
17
|
// node scripts/gds/box-sync.js --dry-run # print what it WOULD run
|
|
18
|
+
// node scripts/gds/box-sync.js --widen <dir> # keep <dir> across ticks
|
|
19
|
+
// node scripts/gds/box-sync.js --unwiden <dir> # stop keeping it
|
|
20
|
+
// node scripts/gds/box-sync.js --widen-list # what is kept, and what is ignored
|
|
21
|
+
//
|
|
22
|
+
// WHY --widen EXISTS (task 1003087 / idea 1000793). `git sparse-checkout add`
|
|
23
|
+
// does not survive: the */10 cron re-applies the SERVER-computed set and deletes
|
|
24
|
+
// the addition, sometimes MID-COMMAND — a live test suite died with
|
|
25
|
+
// MODULE_NOT_FOUND that read exactly like a code bug. --widen records the
|
|
26
|
+
// directory server-side instead, so every fetch (the cron included) returns it
|
|
27
|
+
// in the set and it stops being deleted. It is capped, and it can never widen
|
|
28
|
+
// past what your RANK already reaches — a request outside it is stored but
|
|
29
|
+
// reported as ignored rather than silently applied.
|
|
18
30
|
|
|
19
31
|
const fs = require('node:fs');
|
|
20
32
|
const path = require('node:path');
|
|
@@ -36,7 +48,78 @@ function resolveFetchScript(opts = {}) {
|
|
|
36
48
|
try { return fs.existsSync(p) ? p : null; } catch { return null; }
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
// valueAfter(argv, flag) — the argument following a flag, or null. Kept tiny and
|
|
52
|
+
// pure so the arg handling is testable without spawning anything.
|
|
53
|
+
function valueAfter(argv, flag) {
|
|
54
|
+
const i = argv.indexOf(flag);
|
|
55
|
+
if (i < 0) return null;
|
|
56
|
+
const v = argv[i + 1];
|
|
57
|
+
return typeof v === 'string' && v && !v.startsWith('--') ? v : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// formatWidenReport — PURE. The lines to print for a /box/widen body. Reports
|
|
61
|
+
// IGNORED entries loudly: a widen the server accepted into storage but will not
|
|
62
|
+
// apply (because it is outside your rank scope) looks identical to a working one
|
|
63
|
+
// unless it is said out loud, and a silently-inert widen is exactly the class of
|
|
64
|
+
// bug this whole task is about.
|
|
65
|
+
function formatWidenReport(body) {
|
|
66
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
67
|
+
return ['widen: could not read your widen set.'];
|
|
68
|
+
}
|
|
69
|
+
const kept = Array.isArray(body.widen_paths) ? body.widen_paths : [];
|
|
70
|
+
const ignored = Array.isArray(body.ignored) ? body.ignored : [];
|
|
71
|
+
const out = [];
|
|
72
|
+
out.push(kept.length
|
|
73
|
+
? `widen: keeping ${kept.length} path(s) across ticks: ${kept.join(', ')}`
|
|
74
|
+
: 'widen: nothing kept — the fetch set is whatever your claim scope says.');
|
|
75
|
+
if (ignored.length) {
|
|
76
|
+
out.push(`widen: IGNORED (outside your rank scope, stored but not applied): ${ignored.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The --widen family. Talks to the control plane, so it works off a box too —
|
|
82
|
+
// unlike the fetch itself, the widen set is server state you can inspect and
|
|
83
|
+
// edit from anywhere.
|
|
84
|
+
async function runWiden(argv) {
|
|
85
|
+
const { apiCall } = require('./cli-lib');
|
|
86
|
+
const add = valueAfter(argv, '--widen');
|
|
87
|
+
const remove = valueAfter(argv, '--unwiden');
|
|
88
|
+
const clear = argv.includes('--widen-clear');
|
|
89
|
+
const listOnly = !add && !remove && !clear;
|
|
90
|
+
|
|
91
|
+
const r = listOnly
|
|
92
|
+
? await apiCall('GET', '/api/gds/box/widen')
|
|
93
|
+
: await apiCall('POST', '/api/gds/box/widen', {
|
|
94
|
+
...(add ? { add: [add] } : {}),
|
|
95
|
+
...(remove ? { remove: [remove] } : {}),
|
|
96
|
+
...(clear ? { clear: true } : {}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!r || (r.status !== 200 && r.status !== 201)) {
|
|
100
|
+
const code = r && r.data && r.data.error ? r.data.error : `HTTP ${r ? r.status : '?'}`;
|
|
101
|
+
if (code === 'no_box') {
|
|
102
|
+
console.error('widen: you have no dev box — there is nothing to widen. Provision one first.');
|
|
103
|
+
} else if (code === 'bad_widen_path') {
|
|
104
|
+
const bad = r.data && r.data.rejected ? r.data.rejected.join(', ') : '';
|
|
105
|
+
console.error(`widen: refused ${bad} — give a repo-relative DIRECTORY (no leading /, no "..").`);
|
|
106
|
+
} else {
|
|
107
|
+
console.error(`widen: failed (${code}).`);
|
|
108
|
+
}
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
for (const line of formatWidenReport(r.data)) console.log(line);
|
|
112
|
+
if (!listOnly) console.log('widen: run `box-sync` (no flags) to apply it to the checkout now.');
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
39
116
|
function main(argv = process.argv.slice(2)) {
|
|
117
|
+
// The widen family is server state, not a fetch — handled before the on-a-box
|
|
118
|
+
// check, so it works from a laptop too.
|
|
119
|
+
if (argv.some((a) => a === '--widen' || a === '--unwiden' || a === '--widen-list' || a === '--widen-clear')) {
|
|
120
|
+
runWiden(argv).then((code) => process.exit(code), () => process.exit(1));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
40
123
|
const dryRun = argv.includes('--dry-run') || argv.includes('-n');
|
|
41
124
|
const script = resolveFetchScript();
|
|
42
125
|
if (!script) {
|
|
@@ -130,4 +213,4 @@ async function reportScope() {
|
|
|
130
213
|
|
|
131
214
|
if (require.main === module) main();
|
|
132
215
|
|
|
133
|
-
module.exports = { resolveFetchScript, FETCH_SCRIPT, formatScopeReport };
|
|
216
|
+
module.exports = { resolveFetchScript, FETCH_SCRIPT, formatScopeReport, formatWidenReport, valueAfter };
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.647'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/box_access.mjs
CHANGED
|
@@ -414,5 +414,160 @@ t('1003227 config/ is a cone-mode DIRECTORY entry (a file path aborts the whole
|
|
|
414
414
|
}
|
|
415
415
|
});
|
|
416
416
|
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// task 1003087 / idea 1000793 — builder widening that SURVIVES the */10 tick
|
|
419
|
+
//
|
|
420
|
+
// The bug: the cron re-applies the server-computed sparse set, so a builder's
|
|
421
|
+
// own `git sparse-checkout add` is deleted — sometimes mid-command (a live suite
|
|
422
|
+
// died with MODULE_NOT_FOUND that read exactly like a code bug). The widen set is
|
|
423
|
+
// therefore recorded server-side and unioned in HERE, on the same path the cron
|
|
424
|
+
// asks every tick. These pin the two halves that matter: it actually widens, and
|
|
425
|
+
// it cannot be used to climb past the rank scope.
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
console.log('\nnormalizeWidenPath — a repo-relative directory, or nothing:');
|
|
429
|
+
|
|
430
|
+
t('accepts a plain repo-relative dir and strips the noise around it', () => {
|
|
431
|
+
assert.equal(access.normalizeWidenPath('docs'), 'docs');
|
|
432
|
+
assert.equal(access.normalizeWidenPath(' docs '), 'docs');
|
|
433
|
+
assert.equal(access.normalizeWidenPath('./docs/'), 'docs');
|
|
434
|
+
assert.equal(access.normalizeWidenPath('docs//adr'), 'docs/adr');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
t('folds a Windows separator rather than judging the path on it', () => {
|
|
438
|
+
const BS = String.fromCharCode(92);
|
|
439
|
+
assert.equal(access.normalizeWidenPath(`modules${BS}game`), 'modules/game');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
t('REFUSES traversal, absolute paths and drive letters outright', () => {
|
|
443
|
+
// Refused, never "cleaned up into something that still works" — this string
|
|
444
|
+
// reaches `git sparse-checkout set` on a real box.
|
|
445
|
+
for (const bad of ['../etc', 'a/../../b', '/etc/passwd', 'C:/Windows', '..', '.', '']) {
|
|
446
|
+
assert.equal(access.normalizeWidenPath(bad), null, `${JSON.stringify(bad)} must be refused`);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
t('refuses a non-string, an over-long path, and odd characters', () => {
|
|
451
|
+
assert.equal(access.normalizeWidenPath(null), null);
|
|
452
|
+
assert.equal(access.normalizeWidenPath(42), null);
|
|
453
|
+
assert.equal(access.normalizeWidenPath({}), null);
|
|
454
|
+
assert.equal(access.normalizeWidenPath('a'.repeat(500)), null);
|
|
455
|
+
assert.equal(access.normalizeWidenPath('modules/ga me'), null);
|
|
456
|
+
assert.equal(access.normalizeWidenPath('modules/rm -rf'), null);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
console.log('\nadmissibleWidenPaths — rank is the ceiling, and it is the whole point:');
|
|
460
|
+
|
|
461
|
+
t('a Metic (full rank scope) may widen anywhere', () => {
|
|
462
|
+
assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'metic'), ['modules/game']);
|
|
463
|
+
assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'archon'), ['migrations']);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
t('a Xenos may NOT widen past the starter surface — that would be rank escalation', () => {
|
|
467
|
+
// The security property of this feature stated as a test: if an arbitrary widen
|
|
468
|
+
// were honored, a convenience flag would hand a starter-scoped builder the whole
|
|
469
|
+
// repo, which is the rank gate defeated rather than a checkout preference.
|
|
470
|
+
assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'xenos'), []);
|
|
471
|
+
assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'xenos'), []);
|
|
472
|
+
assert.deepEqual(access.admissibleWidenPaths(['modules/lifecycle'], 'thetes'), []);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
t('a Xenos MAY widen INSIDE starter — a task box is narrower than their rank allows', () => {
|
|
476
|
+
// Not a null grant: task scope is BASE union modules, which is narrower than
|
|
477
|
+
// starter, so this is the room to climb back to what rank already permitted.
|
|
478
|
+
assert.deepEqual(access.admissibleWidenPaths(['docs/adr'], 'xenos'), ['docs/adr']);
|
|
479
|
+
assert.deepEqual(access.admissibleWidenPaths(['src/world'], 'xenos'), ['src/world']);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
t('normalizes, dedupes and sorts, and drops the invalid without failing the rest', () => {
|
|
483
|
+
const got = access.admissibleWidenPaths(['./docs/', 'docs', '../etc', 'art'], 'metic');
|
|
484
|
+
assert.deepEqual(got, ['art', 'docs'], 'one invalid entry must not discard the valid ones');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
t('is capped, so a widen set can never become a second scope system', () => {
|
|
488
|
+
const many = Array.from({ length: access.MAX_WIDEN_PATHS + 20 }, (_, i) => `docs/d${i}`);
|
|
489
|
+
assert.equal(access.admissibleWidenPaths(many, 'metic').length, access.MAX_WIDEN_PATHS);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
t('a missing / non-array widen set is simply no widening', () => {
|
|
493
|
+
for (const v of [undefined, null, 'docs', 42, {}]) {
|
|
494
|
+
assert.deepEqual(access.admissibleWidenPaths(v, 'metic'), []);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
console.log('\nwithWidenPaths — union into the computed set, without redundancy:');
|
|
499
|
+
|
|
500
|
+
t('adds a dir the set does not already cover', () => {
|
|
501
|
+
assert.deepEqual(access.withWidenPaths(['src/bongos'], ['docs']), ['src/bongos', 'docs']);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
t('skips a dir an ANCESTOR already covers — cone mode pulls the whole subtree', () => {
|
|
505
|
+
assert.deepEqual(access.withWidenPaths(['scripts'], ['scripts/gds']), ['scripts'],
|
|
506
|
+
'adding scripts/gds under scripts is noise in the git command and in the scope readout');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
t('passes a null sparse set (the full clone) straight through', () => {
|
|
510
|
+
assert.equal(access.withWidenPaths(null, ['docs']), null,
|
|
511
|
+
'a full clone already holds everything — there is nothing to widen INTO');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
t('an empty widen set returns the computed set untouched', () => {
|
|
515
|
+
const base = ['src/bongos', 'scripts'];
|
|
516
|
+
assert.deepEqual(access.withWidenPaths(base, []), base);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
console.log('\ndecideSourceAccess — the widen reaches the spec the box actually fetches:');
|
|
520
|
+
|
|
521
|
+
t('a task-scoped box gets its widen unioned into the sparse set', () => {
|
|
522
|
+
const d = access.decideSourceAccess({
|
|
523
|
+
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false,
|
|
524
|
+
moduleScopeMap: {}, widenPaths: ['modules/game'],
|
|
525
|
+
});
|
|
526
|
+
assert.equal(d.allowed, true);
|
|
527
|
+
assert.ok(d.spec.sparsePaths.includes('modules/game'),
|
|
528
|
+
'the widen must reach the SPEC — this is the value the cron writes into sparse-checkout');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
t('an out-of-scope widen never reaches the spec for a starter-scoped builder', () => {
|
|
532
|
+
const d = access.decideSourceAccess({
|
|
533
|
+
rank: 'xenos', status: 'active', scopeKeys: [], hasActiveClaims: false,
|
|
534
|
+
moduleScopeMap: {}, widenPaths: ['modules/game'],
|
|
535
|
+
});
|
|
536
|
+
assert.ok(!d.spec.sparsePaths.includes('modules/game'));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
t('no widenPaths leaves every scope byte-identical to before the feature', () => {
|
|
540
|
+
const withArg = access.decideSourceAccess({
|
|
541
|
+
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {}, widenPaths: [],
|
|
542
|
+
});
|
|
543
|
+
const without = access.decideSourceAccess({
|
|
544
|
+
rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
|
|
545
|
+
});
|
|
546
|
+
assert.deepEqual(withArg.spec, without.spec, 'a box that asked for nothing must fetch exactly what it always did');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
t('a full-clone builder is unaffected — sparsePaths stays null', () => {
|
|
550
|
+
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', widenPaths: ['docs'] });
|
|
551
|
+
assert.equal(d.spec.sparsePaths, null);
|
|
552
|
+
assert.equal(d.spec.mode, 'full');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
t('widening cannot resurrect access for an inactive or below-floor builder', () => {
|
|
556
|
+
// The widen set is read from the box row, so it must not become a way around
|
|
557
|
+
// the ALLOW/DENY gate that runs before any scope is computed.
|
|
558
|
+
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', widenPaths: ['docs'] });
|
|
559
|
+
assert.equal(inactive.allowed, false);
|
|
560
|
+
assert.equal(inactive.reason, 'BUILDER_INACTIVE');
|
|
561
|
+
assert.equal(inactive.spec, undefined);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
t('every admitted widen path is still a DIRECTORY — cone mode aborts on a file', () => {
|
|
565
|
+
for (const p of access.admissibleWidenPaths(['docs/adr', 'src/world'], 'metic')) {
|
|
566
|
+
assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
|
|
567
|
+
const last = p.split('/').pop();
|
|
568
|
+
assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
417
572
|
console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
|
|
418
573
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -18,6 +18,8 @@ import { createRequire } from 'node:module';
|
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const { formatScopeReport } = require('../scripts/gds/box-sync.js');
|
|
21
|
+
// task 1003087 — the widen read-out lives in the same module.
|
|
22
|
+
const sync = require('../scripts/gds/box-sync.js');
|
|
21
23
|
|
|
22
24
|
let passed = 0;
|
|
23
25
|
let failed = 0;
|
|
@@ -77,5 +79,48 @@ t('an unknown scope value is reported verbatim rather than swallowed', () => {
|
|
|
77
79
|
assert.match(joined({ scope: 'something-new', sparse_paths: [] }), /something-new/);
|
|
78
80
|
});
|
|
79
81
|
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// task 1003087 — the --widen read-out
|
|
84
|
+
//
|
|
85
|
+
// The whole feature exists because a widen that silently stops applying is
|
|
86
|
+
// indistinguishable from one that works. So the report must say when the server
|
|
87
|
+
// stored a path it will NOT apply (outside the caller's rank scope) — otherwise
|
|
88
|
+
// this fix reintroduces its own bug one layer up.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const widenLines = (body) => sync.formatWidenReport(body).join('\n');
|
|
92
|
+
|
|
93
|
+
t('an empty widen set says so plainly rather than printing nothing', () => {
|
|
94
|
+
assert.match(widenLines({ widen_paths: [], ignored: [] }), /nothing kept/i);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
t('a kept set names the paths and the count', () => {
|
|
98
|
+
const out = widenLines({ widen_paths: ['docs', 'modules/game'], ignored: [] });
|
|
99
|
+
assert.match(out, /2 path/);
|
|
100
|
+
assert.match(out, /docs/);
|
|
101
|
+
assert.match(out, /modules\/game/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
t('an IGNORED path is called out loudly, not folded into the kept list', () => {
|
|
105
|
+
const out = widenLines({ widen_paths: ['docs', 'modules/game'], ignored: ['modules/game'] });
|
|
106
|
+
assert.match(out, /IGNORED/);
|
|
107
|
+
assert.match(out, /rank scope/i,
|
|
108
|
+
'the reason must be named — "stored but not applied" with no why is the silent failure again');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
t('a garbage / missing body degrades to one honest line rather than throwing', () => {
|
|
112
|
+
for (const bad of [null, undefined, 'nope', 42, []]) {
|
|
113
|
+
assert.match(widenLines(bad), /could not read/i);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
t('valueAfter reads the argument after a flag, and refuses the next flag as a value', () => {
|
|
118
|
+
assert.equal(sync.valueAfter(['--widen', 'docs'], '--widen'), 'docs');
|
|
119
|
+
assert.equal(sync.valueAfter(['--widen', '--dry-run'], '--widen'), null,
|
|
120
|
+
'consuming the next FLAG as a path would silently widen to something nobody typed');
|
|
121
|
+
assert.equal(sync.valueAfter(['--widen'], '--widen'), null);
|
|
122
|
+
assert.equal(sync.valueAfter([], '--widen'), null);
|
|
123
|
+
});
|
|
124
|
+
|
|
80
125
|
console.log(`\nbox_sync_scope_report.mjs: ${passed} passed, ${failed} failed`);
|
|
81
126
|
process.exit(failed === 0 ? 0 : 1);
|