@bongos/core 1.19.698 → 1.19.700
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 +33 -23
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +127 -3
- package/docs/api-reference.md +8 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/agents/lib/validate.js +6 -0
- package/modules/agents/module.json +1 -1
- package/modules/agents/routes/agents.js +177 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/serve-internal.js +48 -7
- package/src/module-api.js +1 -1
- package/tests/agents_routes.mjs +194 -0
- package/tests/currency_label.mjs +83 -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.700",
|
|
6
|
+
"core_contract": "1.19.700",
|
|
7
|
+
"source_commit": "7af21bffa8227c67a1c257f64a8fd73f2c1760dc",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-13T03:16:24.200Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 477,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2128,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2629,
|
|
20
|
+
"tree_sha256": "3e7d4889874e8dafb6bc2f7d30722f62c5495396f2760d7a3d06985a79b6adb8",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -367,12 +367,12 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/README.md",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "cfa105831443d1543f03b217a1673b12ba54e40198d768b57bac1c10459ee35c"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "a418c0eace4c6335d4bd131ce33032b716bdd33f1b7b080c03b76db4259aa069"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -382,17 +382,17 @@
|
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/index.cjs",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "c36be893037b5c271ca116d9a26d66defcde55f1b06a1ea5b6a736b1997be0d4"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.d.ts",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "4fc17fc7a5a9ecdaa4539d99f6f30dde4a600043b24affd4677018c95fb94b17"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.mjs",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "83c4a4fc8f4adccb95f15c1943cc40eb6c29d8027c58269271313bb37dbad46c"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1922,12 +1922,12 @@
|
|
|
1922
1922
|
{
|
|
1923
1923
|
"path": "docs/api-reference.md",
|
|
1924
1924
|
"mode": "0000644",
|
|
1925
|
-
"sha256": "
|
|
1925
|
+
"sha256": "2e05ec4efeab7341561a2230e880f6f34d55ada2d542e53b3dcf87f1efa2062c"
|
|
1926
1926
|
},
|
|
1927
1927
|
{
|
|
1928
1928
|
"path": "docs/api/openapi.json",
|
|
1929
1929
|
"mode": "0000644",
|
|
1930
|
-
"sha256": "
|
|
1930
|
+
"sha256": "7263a9c74173ef0ca46a444310156f5d901ad4bb191fbd181f690387171d755c"
|
|
1931
1931
|
},
|
|
1932
1932
|
{
|
|
1933
1933
|
"path": "docs/architecture.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "ba12cd298d7b45de5f7764920115a0419101b4ba4f172ef3ff95ef8798c57508"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -3862,7 +3862,7 @@
|
|
|
3862
3862
|
{
|
|
3863
3863
|
"path": "modules/agents/lib/validate.js",
|
|
3864
3864
|
"mode": "0000644",
|
|
3865
|
-
"sha256": "
|
|
3865
|
+
"sha256": "5cff70fc4e280c49d33dfb8f60ddb01181decd1645625b0bb5e7c2eb97335b81"
|
|
3866
3866
|
},
|
|
3867
3867
|
{
|
|
3868
3868
|
"path": "modules/agents/migrations/agents_001_definitions.sql",
|
|
@@ -3872,7 +3872,12 @@
|
|
|
3872
3872
|
{
|
|
3873
3873
|
"path": "modules/agents/module.json",
|
|
3874
3874
|
"mode": "0000644",
|
|
3875
|
-
"sha256": "
|
|
3875
|
+
"sha256": "af487dc7d134b45f2c1616b1a06cb017106529510e5ebc9d15d7dbb9f72d5fe5"
|
|
3876
|
+
},
|
|
3877
|
+
{
|
|
3878
|
+
"path": "modules/agents/routes/agents.js",
|
|
3879
|
+
"mode": "0000644",
|
|
3880
|
+
"sha256": "31fbc1118ac1f315babb9ba3d0ab4ae634bdce3a6e83cb79defdad73288b2e00"
|
|
3876
3881
|
},
|
|
3877
3882
|
{
|
|
3878
3883
|
"path": "modules/agents/spawn.js",
|
|
@@ -4257,7 +4262,7 @@
|
|
|
4257
4262
|
{
|
|
4258
4263
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
4259
4264
|
"mode": "0000644",
|
|
4260
|
-
"sha256": "
|
|
4265
|
+
"sha256": "c36be893037b5c271ca116d9a26d66defcde55f1b06a1ea5b6a736b1997be0d4"
|
|
4261
4266
|
},
|
|
4262
4267
|
{
|
|
4263
4268
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -7772,12 +7777,12 @@
|
|
|
7772
7777
|
{
|
|
7773
7778
|
"path": "package-lock.json",
|
|
7774
7779
|
"mode": "0000644",
|
|
7775
|
-
"sha256": "
|
|
7780
|
+
"sha256": "4cab339364e2c40b9adb593184fe0c78b8775c11203d6551d019c12de6675e5d"
|
|
7776
7781
|
},
|
|
7777
7782
|
{
|
|
7778
7783
|
"path": "package.json",
|
|
7779
7784
|
"mode": "0000644",
|
|
7780
|
-
"sha256": "
|
|
7785
|
+
"sha256": "ed864f4879e51f4bc1adf7222e46b6fb9446fa93d59ea3b17bc26dbde19f18e2"
|
|
7781
7786
|
},
|
|
7782
7787
|
{
|
|
7783
7788
|
"path": "public-docs/index.html",
|
|
@@ -9512,7 +9517,7 @@
|
|
|
9512
9517
|
{
|
|
9513
9518
|
"path": "src/bongos/serve-internal.js",
|
|
9514
9519
|
"mode": "0000644",
|
|
9515
|
-
"sha256": "
|
|
9520
|
+
"sha256": "61b13299ffdd7f036d5bc3d364135bd8ca4d3470223001b8ff6cc45d858d6107"
|
|
9516
9521
|
},
|
|
9517
9522
|
{
|
|
9518
9523
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -9542,7 +9547,7 @@
|
|
|
9542
9547
|
{
|
|
9543
9548
|
"path": "src/module-api.js",
|
|
9544
9549
|
"mode": "0000644",
|
|
9545
|
-
"sha256": "
|
|
9550
|
+
"sha256": "46a02032df3b4b36ac48c77f8f373ee44735d00dca00ec483100ffd0f7b9062e"
|
|
9546
9551
|
},
|
|
9547
9552
|
{
|
|
9548
9553
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9644,6 +9649,11 @@
|
|
|
9644
9649
|
"mode": "0000644",
|
|
9645
9650
|
"sha256": "bdc9d3678e12dee3f3252785ca82e06071421762c7146cbc1591d3fc2ec72180"
|
|
9646
9651
|
},
|
|
9652
|
+
{
|
|
9653
|
+
"path": "tests/agents_routes.mjs",
|
|
9654
|
+
"mode": "0000644",
|
|
9655
|
+
"sha256": "4e07055707e77bffe2b60750273d16e92a6b213dbbc999e8ed7e0cc742915f7c"
|
|
9656
|
+
},
|
|
9647
9657
|
{
|
|
9648
9658
|
"path": "tests/agents_spawn.mjs",
|
|
9649
9659
|
"mode": "0000644",
|
|
@@ -10327,7 +10337,7 @@
|
|
|
10327
10337
|
{
|
|
10328
10338
|
"path": "tests/currency_label.mjs",
|
|
10329
10339
|
"mode": "0000644",
|
|
10330
|
-
"sha256": "
|
|
10340
|
+
"sha256": "20994467d64d35647433c320265e360a520391bff974b1a39a918ef66ad7f172"
|
|
10331
10341
|
},
|
|
10332
10342
|
{
|
|
10333
10343
|
"path": "tests/cutover_codemod.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
|
+
- 365 operations across 56 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -100,6 +100,12 @@ function createClient(opts = {}) {
|
|
|
100
100
|
// GET /achievements — rank: any-builder — GET /achievements
|
|
101
101
|
getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
|
|
102
102
|
},
|
|
103
|
+
"agents": {
|
|
104
|
+
// GET /agents — rank: any-builder — GET /agents
|
|
105
|
+
getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
|
|
106
|
+
// GET /agents/{name} — rank: any-builder — GET /agents/:name
|
|
107
|
+
getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
|
|
108
|
+
},
|
|
103
109
|
"analytics": {
|
|
104
110
|
// GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
|
|
105
111
|
getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
|
|
@@ -99,6 +99,12 @@ function createClient(opts = {}) {
|
|
|
99
99
|
// GET /achievements — rank: any-builder — GET /achievements
|
|
100
100
|
getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
|
|
101
101
|
},
|
|
102
|
+
"agents": {
|
|
103
|
+
// GET /agents — rank: any-builder — GET /agents
|
|
104
|
+
getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
|
|
105
|
+
// GET /agents/{name} — rank: any-builder — GET /agents/:name
|
|
106
|
+
getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
|
|
107
|
+
},
|
|
102
108
|
"analytics": {
|
|
103
109
|
// GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
|
|
104
110
|
getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
|
|
@@ -19,6 +19,8 @@ export interface ErrorEnvelope { error: ApiErrorObject }
|
|
|
19
19
|
export interface GetAccessRequestsResponse { requests: unknown }
|
|
20
20
|
export interface GetAccessRequestsStatusResponse { github_login: unknown; status: unknown; admitted: unknown }
|
|
21
21
|
export interface GetAchievementsResponse { achievements: unknown }
|
|
22
|
+
export interface GetAgentsNameResponse { ok: boolean; agent: unknown }
|
|
23
|
+
export interface GetAgentsResponse { ok: boolean; agents: unknown; counts: unknown; vocabulary: unknown }
|
|
22
24
|
export interface GetAuditLogResponse { rows: unknown; page: unknown }
|
|
23
25
|
export interface GetAuthAppPairInfoResponse { pair_code: unknown; device_name: unknown; platform: unknown; status: unknown }
|
|
24
26
|
export interface GetAuthWebAdmissionStatusResponse { admitted: boolean }
|
|
@@ -434,6 +436,12 @@ export interface BongosClient {
|
|
|
434
436
|
/** GET /achievements — rank: any-builder */
|
|
435
437
|
getAchievements(args?: RequestArgs): Promise<GetAchievementsResponse>;
|
|
436
438
|
};
|
|
439
|
+
"agents": {
|
|
440
|
+
/** GET /agents — rank: any-builder */
|
|
441
|
+
getAgents(args?: RequestArgs): Promise<GetAgentsResponse>;
|
|
442
|
+
/** GET /agents/{name} — rank: any-builder */
|
|
443
|
+
getAgentsName(args?: RequestArgs): Promise<GetAgentsNameResponse>;
|
|
444
|
+
};
|
|
437
445
|
"analytics": {
|
|
438
446
|
/** GET /analytics/builder/{id} — rank: any-builder */
|
|
439
447
|
getAnalyticsBuilderId(args?: RequestArgs): Promise<ApiResponse>;
|
|
@@ -96,6 +96,12 @@ export function createClient(opts = {}) {
|
|
|
96
96
|
// GET /achievements — rank: any-builder — GET /achievements
|
|
97
97
|
getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
|
|
98
98
|
},
|
|
99
|
+
"agents": {
|
|
100
|
+
// GET /agents — rank: any-builder — GET /agents
|
|
101
|
+
getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
|
|
102
|
+
// GET /agents/{name} — rank: any-builder — GET /agents/:name
|
|
103
|
+
getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
|
|
104
|
+
},
|
|
99
105
|
"analytics": {
|
|
100
106
|
// GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
|
|
101
107
|
getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
|
package/docs/api/openapi.json
CHANGED
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"name": "achievements",
|
|
30
30
|
"description": "Endpoints under `/api/bongos/achievements`."
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"name": "agents",
|
|
34
|
+
"description": "Endpoints under `/api/bongos/agents`."
|
|
35
|
+
},
|
|
32
36
|
{
|
|
33
37
|
"name": "analytics",
|
|
34
38
|
"description": "Endpoints under `/api/bongos/analytics`."
|
|
@@ -505,6 +509,96 @@
|
|
|
505
509
|
]
|
|
506
510
|
}
|
|
507
511
|
},
|
|
512
|
+
"/agents": {
|
|
513
|
+
"get": {
|
|
514
|
+
"operationId": "get_agents",
|
|
515
|
+
"tags": [
|
|
516
|
+
"agents"
|
|
517
|
+
],
|
|
518
|
+
"summary": "GET /agents",
|
|
519
|
+
"description": "GET /agents — the whole registry. Unpaginated on purpose: agents are instance CONFIGURATION reconciled from committed files, so the row count is bounded by what a human wrote, not by traffic. If that ever stops being true, this is the line that needs a cursor — not a silent LIMIT, which would make the registry quietly incomplete.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
520
|
+
"x-rank": "any-builder",
|
|
521
|
+
"x-source": "modules/agents/routes/agents.js",
|
|
522
|
+
"responses": {
|
|
523
|
+
"200": {
|
|
524
|
+
"description": "Success.",
|
|
525
|
+
"content": {
|
|
526
|
+
"application/json": {
|
|
527
|
+
"schema": {
|
|
528
|
+
"$ref": "#/components/schemas/GetAgentsResponse"
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
"400": {
|
|
534
|
+
"$ref": "#/components/responses/BadRequest"
|
|
535
|
+
},
|
|
536
|
+
"401": {
|
|
537
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
538
|
+
},
|
|
539
|
+
"403": {
|
|
540
|
+
"$ref": "#/components/responses/Forbidden"
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
"security": [
|
|
544
|
+
{
|
|
545
|
+
"builderSession": []
|
|
546
|
+
}
|
|
547
|
+
]
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
"/agents/{name}": {
|
|
551
|
+
"get": {
|
|
552
|
+
"operationId": "get_agents_name",
|
|
553
|
+
"tags": [
|
|
554
|
+
"agents"
|
|
555
|
+
],
|
|
556
|
+
"summary": "GET /agents/:name",
|
|
557
|
+
"description": "GET /agents/:name — one definition, persona included. The name is validated against the SAME regex the schema CHECKs, before it reaches the query. The query is parameterised regardless, so this is not the injection wall — it is the difference between a 404 and a 400 for a caller who sent something that could never have been a name.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
558
|
+
"x-rank": "any-builder",
|
|
559
|
+
"x-source": "modules/agents/routes/agents.js",
|
|
560
|
+
"parameters": [
|
|
561
|
+
{
|
|
562
|
+
"name": "name",
|
|
563
|
+
"in": "path",
|
|
564
|
+
"required": true,
|
|
565
|
+
"schema": {
|
|
566
|
+
"type": "string"
|
|
567
|
+
},
|
|
568
|
+
"description": "Path parameter `name`."
|
|
569
|
+
}
|
|
570
|
+
],
|
|
571
|
+
"responses": {
|
|
572
|
+
"200": {
|
|
573
|
+
"description": "Success.",
|
|
574
|
+
"content": {
|
|
575
|
+
"application/json": {
|
|
576
|
+
"schema": {
|
|
577
|
+
"$ref": "#/components/schemas/GetAgentsNameResponse"
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
"400": {
|
|
583
|
+
"$ref": "#/components/responses/BadRequest"
|
|
584
|
+
},
|
|
585
|
+
"401": {
|
|
586
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
587
|
+
},
|
|
588
|
+
"403": {
|
|
589
|
+
"$ref": "#/components/responses/Forbidden"
|
|
590
|
+
},
|
|
591
|
+
"404": {
|
|
592
|
+
"$ref": "#/components/responses/NotFound"
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
"security": [
|
|
596
|
+
{
|
|
597
|
+
"builderSession": []
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
},
|
|
508
602
|
"/analytics/builder/{id}": {
|
|
509
603
|
"get": {
|
|
510
604
|
"operationId": "get_analytics_builder_id",
|
|
@@ -16776,6 +16870,36 @@
|
|
|
16776
16870
|
"achievements"
|
|
16777
16871
|
]
|
|
16778
16872
|
},
|
|
16873
|
+
"GetAgentsNameResponse": {
|
|
16874
|
+
"type": "object",
|
|
16875
|
+
"properties": {
|
|
16876
|
+
"ok": {
|
|
16877
|
+
"type": "boolean"
|
|
16878
|
+
},
|
|
16879
|
+
"agent": {}
|
|
16880
|
+
},
|
|
16881
|
+
"required": [
|
|
16882
|
+
"ok",
|
|
16883
|
+
"agent"
|
|
16884
|
+
]
|
|
16885
|
+
},
|
|
16886
|
+
"GetAgentsResponse": {
|
|
16887
|
+
"type": "object",
|
|
16888
|
+
"properties": {
|
|
16889
|
+
"ok": {
|
|
16890
|
+
"type": "boolean"
|
|
16891
|
+
},
|
|
16892
|
+
"agents": {},
|
|
16893
|
+
"counts": {},
|
|
16894
|
+
"vocabulary": {}
|
|
16895
|
+
},
|
|
16896
|
+
"required": [
|
|
16897
|
+
"ok",
|
|
16898
|
+
"agents",
|
|
16899
|
+
"counts",
|
|
16900
|
+
"vocabulary"
|
|
16901
|
+
]
|
|
16902
|
+
},
|
|
16779
16903
|
"GetAuditLogResponse": {
|
|
16780
16904
|
"type": "object",
|
|
16781
16905
|
"properties": {
|
|
@@ -22992,9 +23116,9 @@
|
|
|
22992
23116
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
22993
23117
|
}
|
|
22994
23118
|
},
|
|
22995
|
-
"x-endpoint-count":
|
|
22996
|
-
"x-schema-count":
|
|
23119
|
+
"x-endpoint-count": 365,
|
|
23120
|
+
"x-schema-count": 403,
|
|
22997
23121
|
"x-undocumented-bodies": 9,
|
|
22998
|
-
"x-response-schemas":
|
|
23122
|
+
"x-response-schemas": 272,
|
|
22999
23123
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
23000
23124
|
}
|
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. 365 endpoints across 59 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`.
|
|
@@ -23,6 +23,13 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
23
23
|
|---|---|---|---|---|
|
|
24
24
|
| GET | `/api/bongos/achievements` | `any-builder` | — | All seeded achievements — lets /builders render locked + unlocked side by side. |
|
|
25
25
|
|
|
26
|
+
## `agents` (2)
|
|
27
|
+
|
|
28
|
+
| Method | Path | Rank | Body | Description |
|
|
29
|
+
|---|---|---|---|---|
|
|
30
|
+
| GET | `/api/bongos/agents` | `any-builder` | — | GET /agents — the whole registry. |
|
|
31
|
+
| GET | `/api/bongos/agents/:name` | `any-builder` | — | GET /agents/:name — one definition, persona included. |
|
|
32
|
+
|
|
26
33
|
## `analytics` (1)
|
|
27
34
|
|
|
28
35
|
| Method | Path | Rank | Body | Description |
|
|
@@ -1855,5 +1855,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1855
1855
|
landed since 1.19.696 with no explicit bump. run 34733301156. (task 1002620)
|
|
1856
1856
|
1.19.698 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1857
1857
|
landed since 1.19.697 with no explicit bump. run 34733917938. (task 1002620)
|
|
1858
|
+
1.19.699 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1859
|
+
landed since 1.19.698 with no explicit bump. run 34734848584. (task 1002620)
|
|
1860
|
+
1.19.700 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1861
|
+
landed since 1.19.699 with no explicit bump. run 34735048203. (task 1002620)
|
|
1858
1862
|
---------------------------------------------------------------------------
|
|
1859
1863
|
```
|
|
@@ -303,4 +303,10 @@ module.exports = {
|
|
|
303
303
|
MODEL_TIERS,
|
|
304
304
|
RANK_ORDER,
|
|
305
305
|
SCOPE_RANK_FLOOR,
|
|
306
|
+
// Exported for the read routes (task 1002488), which reject an impossible
|
|
307
|
+
// :name with a 400 before it reaches the query. Shared rather than re-spelled:
|
|
308
|
+
// this pair already mirrors the agents_definitions_name_fmt_chk CHECK, and a
|
|
309
|
+
// second copy in the route layer is one more place for the three to drift.
|
|
310
|
+
NAME_RE,
|
|
311
|
+
NAME_MAX,
|
|
306
312
|
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// modules/agents/routes/agents.js — the agent registry's READ surface (task
|
|
2
|
+
// 1002488, BONGOS-V1 goal 1000038 Phase 1).
|
|
3
|
+
//
|
|
4
|
+
// WHY. agents-sync reconciles .claude/agents/*.md into agents_definitions at
|
|
5
|
+
// deploy, and the spawn path fires from that table — so the table is the only
|
|
6
|
+
// honest answer to "what agents does this instance actually have, and which of
|
|
7
|
+
// them are armed?". Until now nothing could ask. A builder wondering why an
|
|
8
|
+
// agent did or did not fire had to read the deploy log or the DB directly, and
|
|
9
|
+
// an instance author had no way to see that the definition they committed was
|
|
10
|
+
// flagged for a scope violation and therefore silently disarmed.
|
|
11
|
+
//
|
|
12
|
+
// GET /agents — the registry, newest-synced first.
|
|
13
|
+
// GET /agents/:name — one definition, by its stable name.
|
|
14
|
+
//
|
|
15
|
+
// SELF-GATED, and that is the whole gate: `requireBuilder` and nothing else.
|
|
16
|
+
// These are reads of the instance's own configuration by the people who build
|
|
17
|
+
// it, the same audience and posture as GET /tasks. There is deliberately NO rank
|
|
18
|
+
// floor — a xenos who can see WHICH agents are armed is better placed to
|
|
19
|
+
// understand the review they just received, and none of the authority-bearing
|
|
20
|
+
// fields is writable here (authoring is a separate task, with its own wall).
|
|
21
|
+
//
|
|
22
|
+
// WHAT IS NOT SERVED, ON PURPOSE:
|
|
23
|
+
//
|
|
24
|
+
// `persona` is omitted from the LIST and served only on the detail route. It
|
|
25
|
+
// is capped at 32KB per row by the schema, so a 40-agent registry would be a
|
|
26
|
+
// 1.2MB response to render a table that shows none of it. This is a size
|
|
27
|
+
// decision, not a secrecy one — the detail route hands it over to anyone who
|
|
28
|
+
// asks by name.
|
|
29
|
+
//
|
|
30
|
+
// `author_builder_id` is never served. The FACT that matters downstream is
|
|
31
|
+
// `author_rank` — the rank the definition was stamped with at sync, which is
|
|
32
|
+
// what the spawn gate consults — and serving the id would turn a registry read
|
|
33
|
+
// into a people-directory read, which has its own gate (GET /scouting).
|
|
34
|
+
//
|
|
35
|
+
// Same idioms as modules/copy-desk/routes/copy-desk.js: doorway-only requires,
|
|
36
|
+
// factory export mounted from module.json `contributes.routes`, res.fail codes.
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const express = require('express');
|
|
41
|
+
const api = require('../../../src/module-api');
|
|
42
|
+
const validate = require('../lib/validate');
|
|
43
|
+
|
|
44
|
+
const log = api.logger('agents');
|
|
45
|
+
|
|
46
|
+
// The columns the serializer needs. Spelled out rather than `SELECT *` so that a
|
|
47
|
+
// column added later (an author's email, a raw credential) cannot reach a
|
|
48
|
+
// response by default — the read surface widens only when someone edits this
|
|
49
|
+
// list and reads the comment above about what is deliberately not served.
|
|
50
|
+
const COLUMNS = `
|
|
51
|
+
id, name, title, trigger_type, trigger_spec, model_tier,
|
|
52
|
+
scope_modules, scope_paths, scope_violation,
|
|
53
|
+
source, provenance, author_rank,
|
|
54
|
+
source_path, last_synced_at, enabled, created_at, updated_at
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// PURE. Row → wire shape, so the projection is unit-testable without a database.
|
|
58
|
+
//
|
|
59
|
+
// `model_tier` is stored as the TIER ('default' | 'routine'), never a model id:
|
|
60
|
+
// the id a tier resolves to is a deployment fact that changes under the row, and
|
|
61
|
+
// a registry that answered with a stale model name would be worse than one that
|
|
62
|
+
// answers with the tier and lets the caller resolve it. The task asks for the
|
|
63
|
+
// "resolved model tier", and the tier IS the resolved value at this layer.
|
|
64
|
+
//
|
|
65
|
+
// `armed` is derived rather than aliased from `enabled`, because two schema
|
|
66
|
+
// CHECKs make "enabled" narrower than it reads: a row carrying a
|
|
67
|
+
// `scope_violation` is forced to enabled=false, and an enabled row must carry an
|
|
68
|
+
// author_rank. Surfacing the derived boolean beside the reason means a caller
|
|
69
|
+
// asking "why did this not fire?" gets the answer in the same object.
|
|
70
|
+
function serializeDefinition(row, { includePersona = false } = {}) {
|
|
71
|
+
if (!row) return null;
|
|
72
|
+
const out = {
|
|
73
|
+
name: row.name,
|
|
74
|
+
title: row.title || null,
|
|
75
|
+
trigger: {
|
|
76
|
+
type: row.trigger_type,
|
|
77
|
+
// trigger_spec is jsonb and NOT NULL DEFAULT '{}', but a hand-written row
|
|
78
|
+
// could still be any object; the event name is lifted out because it is
|
|
79
|
+
// the only part of the spec every event agent must carry (schema CHECK).
|
|
80
|
+
spec: row.trigger_spec && typeof row.trigger_spec === 'object' ? row.trigger_spec : {},
|
|
81
|
+
event: row.trigger_type === 'event' && row.trigger_spec ? (row.trigger_spec.event || null) : null,
|
|
82
|
+
},
|
|
83
|
+
model_tier: row.model_tier,
|
|
84
|
+
provenance: row.provenance, // 'built-in' | 'instance'
|
|
85
|
+
source: row.source, // 'file' | 'db'
|
|
86
|
+
// A file-sourced row's path is its identity in the repo; a db-authored row
|
|
87
|
+
// has none, and null says so rather than an empty string that sorts oddly.
|
|
88
|
+
source_path: row.source_path || null,
|
|
89
|
+
author_rank: row.author_rank || null,
|
|
90
|
+
scope: {
|
|
91
|
+
modules: Array.isArray(row.scope_modules) ? row.scope_modules : [],
|
|
92
|
+
paths: Array.isArray(row.scope_paths) ? row.scope_paths : [],
|
|
93
|
+
violation: row.scope_violation || null,
|
|
94
|
+
},
|
|
95
|
+
armed: row.enabled === true,
|
|
96
|
+
// Why an agent is not armed, when the answer is knowable. A scope violation
|
|
97
|
+
// is the one disarm the registry can explain; anything else is simply off.
|
|
98
|
+
disarmed_reason: row.enabled === true ? null : (row.scope_violation ? 'scope_violation' : null),
|
|
99
|
+
last_synced_at: row.last_synced_at || null,
|
|
100
|
+
created_at: row.created_at,
|
|
101
|
+
updated_at: row.updated_at,
|
|
102
|
+
};
|
|
103
|
+
if (includePersona) out.persona = row.persona;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = function agentsRoutes() {
|
|
108
|
+
const router = express.Router();
|
|
109
|
+
|
|
110
|
+
// GET /agents — the whole registry. Unpaginated on purpose: agents are
|
|
111
|
+
// instance CONFIGURATION reconciled from committed files, so the row count is
|
|
112
|
+
// bounded by what a human wrote, not by traffic. If that ever stops being
|
|
113
|
+
// true, this is the line that needs a cursor — not a silent LIMIT, which
|
|
114
|
+
// would make the registry quietly incomplete.
|
|
115
|
+
router.get('/agents', api.requireBuilder, async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const { rows } = await api.pool.query(
|
|
118
|
+
`SELECT ${COLUMNS} FROM agents_definitions
|
|
119
|
+
ORDER BY enabled DESC, name ASC`
|
|
120
|
+
);
|
|
121
|
+
res.json({
|
|
122
|
+
ok: true,
|
|
123
|
+
agents: rows.map((r) => serializeDefinition(r)),
|
|
124
|
+
// The counts a reader actually wants first, computed here so three
|
|
125
|
+
// callers do not each reduce the array their own way.
|
|
126
|
+
counts: {
|
|
127
|
+
total: rows.length,
|
|
128
|
+
armed: rows.filter((r) => r.enabled === true).length,
|
|
129
|
+
flagged: rows.filter((r) => r.scope_violation).length,
|
|
130
|
+
},
|
|
131
|
+
// The vocabulary, from the validator that enforces it — so a UI building
|
|
132
|
+
// a filter cannot spell a trigger type the registry will never contain.
|
|
133
|
+
// Same rule as the task-kind filter (task 1003453): serve the enum, do
|
|
134
|
+
// not let the client keep a copy.
|
|
135
|
+
vocabulary: {
|
|
136
|
+
trigger_types: [...validate.TRIGGER_TYPES],
|
|
137
|
+
model_tiers: [...validate.MODEL_TIERS],
|
|
138
|
+
events: [...validate.EVENT_CATALOG],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.error({ err }, 'GET /agents failed');
|
|
143
|
+
if (!res.headersSent) res.fail('agents_list_failed', 500);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// GET /agents/:name — one definition, persona included.
|
|
148
|
+
//
|
|
149
|
+
// The name is validated against the SAME regex the schema CHECKs, before it
|
|
150
|
+
// reaches the query. The query is parameterised regardless, so this is not the
|
|
151
|
+
// injection wall — it is the difference between a 404 and a 400 for a caller
|
|
152
|
+
// who sent something that could never have been a name.
|
|
153
|
+
router.get('/agents/:name', api.requireBuilder, async (req, res) => {
|
|
154
|
+
const name = String(req.params.name || '');
|
|
155
|
+
if (!validate.NAME_RE.test(name) || name.length > validate.NAME_MAX) {
|
|
156
|
+
return res.fail('invalid_agent_name', 400);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const { rows } = await api.pool.query(
|
|
160
|
+
`SELECT ${COLUMNS}, persona FROM agents_definitions WHERE name = $1`,
|
|
161
|
+
[name]
|
|
162
|
+
);
|
|
163
|
+
if (!rows.length) return res.fail('agent_not_found', 404);
|
|
164
|
+
return res.json({ ok: true, agent: serializeDefinition(rows[0], { includePersona: true }) });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
log.error({ err, name }, 'GET /agents/:name failed');
|
|
167
|
+
if (!res.headersSent) return res.fail('agent_read_failed', 500);
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return router;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Exported for the unit tests, which drive the projection with fabricated rows
|
|
176
|
+
// rather than standing up Postgres.
|
|
177
|
+
module.exports.serializeDefinition = serializeDefinition;
|
|
@@ -99,6 +99,12 @@ function createClient(opts = {}) {
|
|
|
99
99
|
// GET /achievements — rank: any-builder — GET /achievements
|
|
100
100
|
getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
|
|
101
101
|
},
|
|
102
|
+
"agents": {
|
|
103
|
+
// GET /agents — rank: any-builder — GET /agents
|
|
104
|
+
getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
|
|
105
|
+
// GET /agents/{name} — rank: any-builder — GET /agents/:name
|
|
106
|
+
getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
|
|
107
|
+
},
|
|
102
108
|
"analytics": {
|
|
103
109
|
// GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
|
|
104
110
|
getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.700",
|
|
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.700",
|
|
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.700",
|
|
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",
|
|
@@ -107,25 +107,66 @@ function themeMarkup(theme) {
|
|
|
107
107
|
if (!theme) return '';
|
|
108
108
|
const decls = [];
|
|
109
109
|
for (const [key, cssVar] of Object.entries(COLOR_VARS)) {
|
|
110
|
-
const v = theme.colors?.[key];
|
|
110
|
+
const v = cssSafe(theme.colors?.[key]);
|
|
111
111
|
if (v) decls.push(`${cssVar}:${v};`);
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
113
|
+
const display = cssSafe(theme.fonts?.display);
|
|
114
|
+
if (display) decls.push(`--font-display:${display};`);
|
|
115
|
+
const body = cssSafe(theme.fonts?.body);
|
|
116
|
+
if (body) decls.push(`--font-body:${body};`);
|
|
115
117
|
let out = '';
|
|
116
118
|
for (const href of theme.fonts?.links || []) {
|
|
117
|
-
out += `<link rel="stylesheet" href="${href}">`;
|
|
119
|
+
out += `<link rel="stylesheet" href="${esc(href)}">`;
|
|
118
120
|
}
|
|
119
121
|
// `backdrop` paints the page background (e.g. the Cloud Bongos sunrise gradient,
|
|
120
122
|
// ADR 0064 / R74) over the flat var(--bg); omitted packs (OTB) keep --bg.
|
|
121
123
|
const css = [];
|
|
122
124
|
if (decls.length) css.push(`:root{${decls.join('')}}`);
|
|
123
|
-
|
|
125
|
+
const backdrop = cssSafe(theme.backdrop);
|
|
126
|
+
if (backdrop) css.push(`body{background:${backdrop};}`);
|
|
124
127
|
if (css.length) out += `<style>${css.join('')}</style>`;
|
|
125
|
-
|
|
128
|
+
// esc() is lossless in an attribute: the HTML parser decodes the entities back
|
|
129
|
+
// before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
|
|
130
|
+
// <svg …>` favicon resolves to exactly the same bytes it does today.
|
|
131
|
+
if (theme.favicon) out += `<link rel="icon" href="${esc(theme.favicon)}">`;
|
|
126
132
|
return out;
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
// A pack-supplied value on its way into <style> TEXT. esc() is the wrong tool
|
|
136
|
+
// here and would corrupt the output: CSS is not entity-decoded, so a font stack
|
|
137
|
+
// like `"Cinzel", serif` would reach the browser as `"Cinzel", serif`
|
|
138
|
+
// and simply not parse. There is no encoding that both survives CSS and
|
|
139
|
+
// neutralizes markup, so this fails closed instead. The only way out of a <style>
|
|
140
|
+
// element is a literal `</style`, every route to it begins with `<`, and no
|
|
141
|
+
// colour, font stack, or backdrop has a legitimate use for one — so a value
|
|
142
|
+
// carrying `<` is dropped whole rather than emitted half-escaped. Dropping one
|
|
143
|
+
// declaration loses that pack's colour and falls back to style.css's default;
|
|
144
|
+
// emitting it loses the page.
|
|
145
|
+
function cssSafe(v) {
|
|
146
|
+
const s = String(v == null ? '' : v);
|
|
147
|
+
return s.includes('<') ? '' : s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Serialize a pack value for injection INSIDE a <script> element. JSON.stringify
|
|
151
|
+
// alone is not enough: its output is valid JSON, but the browser reads a script
|
|
152
|
+
// body as raw text FIRST, so a `</script>` anywhere in the pack ends the element
|
|
153
|
+
// early and everything after it lands in the document as markup (the latent
|
|
154
|
+
// self-XSS from an operator-authored pack — idea 348, Hacker dispatch on task
|
|
155
|
+
// 1374). Escaping `<` alone closes it: `</script`, `<script`, and `<!--` all need
|
|
156
|
+
// one. `>` and `&` need no escaping here precisely because a classic script body
|
|
157
|
+
// is NOT entity-decoded — there is nothing for them to start. U+2028/U+2029 are
|
|
158
|
+
// escaped as well: legal in string literals since ES2019, so not a break-out on
|
|
159
|
+
// any browser we serve, but free to escape and still fatal to an older parser.
|
|
160
|
+
// `\u003c` is the same character to the JS parser and never one to the HTML
|
|
161
|
+
// parser, so the value the page actually reads is unchanged. Same fix, same
|
|
162
|
+
// reasoning as modules/provisioning/routes/callback-page.js.
|
|
163
|
+
function jsonForScript(value) {
|
|
164
|
+
return JSON.stringify(value)
|
|
165
|
+
.replace(/</g, '\\u003c')
|
|
166
|
+
.replace(/\u2028/g, '\\u2028')
|
|
167
|
+
.replace(/\u2029/g, '\\u2029');
|
|
168
|
+
}
|
|
169
|
+
|
|
129
170
|
// Inject the client-safe branding pack into a served HTML document (R56 task 1195
|
|
130
171
|
// + R57 task 1196, ADR 0062 §3):
|
|
131
172
|
// * `window.__BRANDING__` — a synchronous global so every hall/status script
|
|
@@ -145,7 +186,7 @@ function injectBrandingGlobal(html, brand = clientBranding(), mods = clientModul
|
|
|
145
186
|
// UI sections for off modules synchronously (no fetch, no race). R60 wires the
|
|
146
187
|
// actual section-hiding; this exposes the data.
|
|
147
188
|
const stamped = applyBrandTokens(html, brand);
|
|
148
|
-
const tag = `${clientBundleScriptTag()}<script>window.__BRANDING__=${
|
|
189
|
+
const tag = `${clientBundleScriptTag()}<script>window.__BRANDING__=${jsonForScript(brand)};window.__MODULES__=${jsonForScript(mods)};</script>${themeMarkup(brand.theme)}`;
|
|
149
190
|
return stamped.includes('</head>') ? stamped.replace('</head>', `${tag}</head>`) : tag + stamped;
|
|
150
191
|
}
|
|
151
192
|
|
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.700'; // 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');
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// tests/agents_routes.mjs — the agent registry's read surface (task 1002488,
|
|
2
|
+
// goal 1000038 Phase 1).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS LIVES IN tests/ AND NOT modules/agents/tests/. The `agents` module is
|
|
5
|
+
// `default: false`, and run-unit-tests.js discovers a module's own tests only
|
|
6
|
+
// when that module is ENABLED ("it isn't mounted, so its tests don't run
|
|
7
|
+
// either"). On this instance agents is disabled, so a module-local file here
|
|
8
|
+
// would be silently skipped forever — coverage in name only. tests/ always runs.
|
|
9
|
+
// Same call task 1002487 made for tests/agents_validate.mjs; this follows it.
|
|
10
|
+
//
|
|
11
|
+
// No database, no fixtures, no HTTP. The projection is a pure function and the
|
|
12
|
+
// gate is a property of the mounted router, so both are checked directly.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/agents_routes.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const routes = require('../modules/agents/routes/agents.js');
|
|
21
|
+
const validate = require('../modules/agents/lib/validate.js');
|
|
22
|
+
const api = require('../src/module-api.js');
|
|
23
|
+
|
|
24
|
+
const { serializeDefinition } = routes;
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
function t(name, fn) {
|
|
29
|
+
try { fn(); console.log(` PASS ${name}`); passed += 1; }
|
|
30
|
+
catch (err) { console.log(` FAIL ${name}\n ${err.message}`); failed += 1; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A full row as agents-sync would have written it — every column the serializer
|
|
34
|
+
// reads, including the two it must never emit.
|
|
35
|
+
function row(over = {}) {
|
|
36
|
+
return {
|
|
37
|
+
id: 7,
|
|
38
|
+
name: 'historian',
|
|
39
|
+
title: 'The Historian',
|
|
40
|
+
persona: 'PERSONA BODY',
|
|
41
|
+
trigger_type: 'event',
|
|
42
|
+
trigger_spec: { event: 'task.shipped' },
|
|
43
|
+
model_tier: 'routine',
|
|
44
|
+
scope_modules: ['lifecycle'],
|
|
45
|
+
scope_paths: [],
|
|
46
|
+
scope_violation: null,
|
|
47
|
+
source: 'file',
|
|
48
|
+
provenance: 'built-in',
|
|
49
|
+
author_rank: 'archon',
|
|
50
|
+
author_builder_id: 4242,
|
|
51
|
+
source_path: '.claude/agents/historian.md',
|
|
52
|
+
last_synced_at: '2026-09-01T00:00:00.000Z',
|
|
53
|
+
enabled: true,
|
|
54
|
+
created_at: '2026-08-01T00:00:00.000Z',
|
|
55
|
+
updated_at: '2026-09-01T00:00:00.000Z',
|
|
56
|
+
...over,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('\nthe projection — what a registry read may and may not carry:');
|
|
61
|
+
|
|
62
|
+
t('the LIST omits persona; the DETAIL route serves it', () => {
|
|
63
|
+
assert.ok(!('persona' in serializeDefinition(row())), 'a 32KB body per row must not ride the list');
|
|
64
|
+
assert.equal(serializeDefinition(row(), { includePersona: true }).persona, 'PERSONA BODY');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
t('author_builder_id is NEVER served, on either route', () => {
|
|
68
|
+
// The rank is the fact the spawn gate consults; the id would turn a registry
|
|
69
|
+
// read into a people-directory read, which has its own gate (GET /scouting).
|
|
70
|
+
for (const opts of [{}, { includePersona: true }]) {
|
|
71
|
+
const json = JSON.stringify(serializeDefinition(row(), opts));
|
|
72
|
+
assert.doesNotMatch(json, /4242/, 'the author id leaked into the response');
|
|
73
|
+
assert.doesNotMatch(json, /author_builder_id/, 'the author id key leaked into the response');
|
|
74
|
+
}
|
|
75
|
+
assert.equal(serializeDefinition(row()).author_rank, 'archon', 'the rank IS served — it is the gate-relevant fact');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
t('model_tier is served as the TIER, never a resolved model id', () => {
|
|
79
|
+
// The id a tier resolves to is a deployment fact that changes under the row.
|
|
80
|
+
// A registry answering with a stale model name is worse than one answering
|
|
81
|
+
// with the tier, so this asserts the absence of any model-shaped string.
|
|
82
|
+
const o = serializeDefinition(row());
|
|
83
|
+
assert.equal(o.model_tier, 'routine');
|
|
84
|
+
assert.ok(validate.MODEL_TIERS.includes(o.model_tier));
|
|
85
|
+
assert.doesNotMatch(JSON.stringify(o), /claude-|sonnet|opus|haiku/i, 'a concrete model id must not appear');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
console.log('\n`armed` is DERIVED, because two CHECKs make `enabled` narrower than it reads:');
|
|
89
|
+
|
|
90
|
+
t('a scope-violating row reads as disarmed AND says why', () => {
|
|
91
|
+
// agents_definitions_flagged_not_armed_chk forces enabled=false when a
|
|
92
|
+
// violation is recorded. A caller asking "why did this not fire?" gets the
|
|
93
|
+
// answer in the same object rather than having to know that CHECK exists.
|
|
94
|
+
const o = serializeDefinition(row({ enabled: false, scope_violation: 'scope_paths exceeds module' }));
|
|
95
|
+
assert.equal(o.armed, false);
|
|
96
|
+
assert.equal(o.disarmed_reason, 'scope_violation');
|
|
97
|
+
assert.equal(o.scope.violation, 'scope_paths exceeds module');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
t('an ordinary disabled row is disarmed with no invented reason', () => {
|
|
101
|
+
const o = serializeDefinition(row({ enabled: false }));
|
|
102
|
+
assert.equal(o.armed, false);
|
|
103
|
+
assert.equal(o.disarmed_reason, null, 'absence of a reason must not be reported as a violation');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
t('armed is a strict boolean, not a truthy passthrough', () => {
|
|
107
|
+
assert.equal(serializeDefinition(row({ enabled: true })).armed, true);
|
|
108
|
+
// A driver returning 't'/1 for a boolean column must not read as armed by
|
|
109
|
+
// accident — the fail-safe direction for "is this thing allowed to fire" is
|
|
110
|
+
// false, so the check is `=== true`.
|
|
111
|
+
assert.equal(serializeDefinition(row({ enabled: 1 })).armed, false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('\nshape tolerance — a hand-written db row is still a row:');
|
|
115
|
+
|
|
116
|
+
t('a null/odd trigger_spec degrades to {} instead of throwing', () => {
|
|
117
|
+
assert.deepEqual(serializeDefinition(row({ trigger_spec: null })).trigger.spec, {});
|
|
118
|
+
assert.deepEqual(serializeDefinition(row({ trigger_spec: 'nope' })).trigger.spec, {});
|
|
119
|
+
assert.equal(serializeDefinition(row({ trigger_type: 'on-demand' })).trigger.event, null,
|
|
120
|
+
'an on-demand agent has no event, even when a spec carries one');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
t('null arrays degrade to [], and a missing row serializes to null', () => {
|
|
124
|
+
const o = serializeDefinition(row({ scope_modules: null, scope_paths: null }));
|
|
125
|
+
assert.deepEqual(o.scope.modules, []);
|
|
126
|
+
assert.deepEqual(o.scope.paths, []);
|
|
127
|
+
assert.equal(serializeDefinition(null), null);
|
|
128
|
+
assert.equal(serializeDefinition(undefined), null);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log('\nthe gate — self-gated means requireBuilder, on EVERY route:');
|
|
132
|
+
|
|
133
|
+
t('both routes mount requireBuilder, and neither is left ungated', () => {
|
|
134
|
+
const router = routes();
|
|
135
|
+
const mounted = router.stack.filter((l) => l.route);
|
|
136
|
+
assert.equal(mounted.length, 2, 'exactly two routes — a third needs its own gate and its own row here');
|
|
137
|
+
const paths = mounted.map((l) => l.route.path).sort();
|
|
138
|
+
assert.deepEqual(paths, ['/agents', '/agents/:name']);
|
|
139
|
+
for (const layer of mounted) {
|
|
140
|
+
const handlers = layer.route.stack.map((s) => s.handle);
|
|
141
|
+
assert.ok(handlers.includes(api.requireBuilder),
|
|
142
|
+
`${layer.route.path} does not mount requireBuilder — a registry read must not be anonymous`);
|
|
143
|
+
assert.ok(handlers.length >= 2, `${layer.route.path} has no handler behind its gate`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
t('every mounted route is a GET — this surface writes nothing', () => {
|
|
148
|
+
// The read half is a separate task from authoring on purpose. A write verb
|
|
149
|
+
// appearing here would carry no author-rank clamp and no scope wall.
|
|
150
|
+
for (const layer of routes().stack.filter((l) => l.route)) {
|
|
151
|
+
assert.deepEqual(Object.keys(layer.route.methods), ['get'],
|
|
152
|
+
`${layer.route.path} exposes a non-GET verb on the read surface`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log('\nthe vocabulary is SERVED, not copied into the client:');
|
|
157
|
+
|
|
158
|
+
t('the route file spells no enum of its own', () => {
|
|
159
|
+
// Task 1003453's lesson, applied before the bug rather than after: a filter
|
|
160
|
+
// built from a client-side copy goes stale against the validator and the DB
|
|
161
|
+
// CHECK. The list response carries the enums, and they come from validate.js.
|
|
162
|
+
const src = require('node:fs').readFileSync(new URL('../modules/agents/routes/agents.js', import.meta.url), 'utf8');
|
|
163
|
+
assert.doesNotMatch(src, /\[\s*'event'\s*,\s*'on-demand'\s*\]/, 'trigger types re-spelled in the route');
|
|
164
|
+
assert.doesNotMatch(src, /\[\s*'default'\s*,\s*'routine'\s*\]/, 'model tiers re-spelled in the route');
|
|
165
|
+
assert.match(src, /validate\.TRIGGER_TYPES/);
|
|
166
|
+
assert.match(src, /validate\.MODEL_TIERS/);
|
|
167
|
+
assert.match(src, /validate\.EVENT_CATALOG/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
t('the name guard reuses the validator regex, not a third copy of it', () => {
|
|
171
|
+
// The pattern already exists twice — in validate.js and in the schema's
|
|
172
|
+
// agents_definitions_name_fmt_chk. The route makes a third place to drift.
|
|
173
|
+
assert.ok(validate.NAME_RE instanceof RegExp, 'NAME_RE is exported for the route to share');
|
|
174
|
+
assert.equal(typeof validate.NAME_MAX, 'number');
|
|
175
|
+
assert.ok(validate.NAME_RE.test('historian'));
|
|
176
|
+
assert.equal(validate.NAME_RE.test('Historian'), false, 'the CHECK is lowercase-anchored');
|
|
177
|
+
assert.equal(validate.NAME_RE.test('9lives'), false, 'must start with a letter');
|
|
178
|
+
const src = require('node:fs').readFileSync(new URL('../modules/agents/routes/agents.js', import.meta.url), 'utf8');
|
|
179
|
+
assert.doesNotMatch(src, /\/\^\[a-z\]\[a-z0-9-\]\*\$\//, 'the name regex was re-spelled in the route');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log('\nthe module declares the routes it now ships:');
|
|
183
|
+
|
|
184
|
+
t('module.json contributes the agents route file', () => {
|
|
185
|
+
const manifest = require('../modules/agents/module.json');
|
|
186
|
+
assert.ok(Array.isArray(manifest.contributes.routes), 'contributes.routes is declared');
|
|
187
|
+
assert.ok(manifest.contributes.routes.includes('agents'),
|
|
188
|
+
'the loader mounts modules/agents/routes/<name>.js from this list — without it the file is dead');
|
|
189
|
+
assert.equal(manifest.contributes.migrations, true, 'the existing migrations contribution survives');
|
|
190
|
+
assert.equal(manifest.default, false, 'shipped is not enabled — an instance opts in (ADR 0083)');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
console.log(`\nagents_routes: ${passed} passed, ${failed} failed`);
|
|
194
|
+
process.exit(failed ? 1 : 0);
|
package/tests/currency_label.mjs
CHANGED
|
@@ -121,6 +121,89 @@ test('injectBrandingGlobal degrades safely when there is no </head>', () => {
|
|
|
121
121
|
assert.ok(out.includes('<div>no head</div>'));
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// --- task 1001434 (idea 348): everything the pack contributes is escaped for the
|
|
125
|
+
// context it lands in. The pack is operator-authored, so this is not a defence
|
|
126
|
+
// against a hostile visitor — it is a defence against one stray character in a
|
|
127
|
+
// config file silently turning every served page's <head> into markup.
|
|
128
|
+
|
|
129
|
+
// A pack string carrying `</script>` is the whole bug: the browser finds the end
|
|
130
|
+
// of the script element by scanning raw text, before any JS parsing happens.
|
|
131
|
+
const HOSTILE = {
|
|
132
|
+
identity: { productName: '</script><img src=x onerror=alert(1)>', worldName: 'w' },
|
|
133
|
+
copy: { tagline: 'a < b && c > d' },
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function brandingJson(out) {
|
|
137
|
+
const m = /window\.__BRANDING__=(.*?);window\.__MODULES__=/s.exec(out);
|
|
138
|
+
assert.ok(m, 'the injected branding assignment must be findable');
|
|
139
|
+
return m[1];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test('1001434: a `</script>` in the pack cannot close the injected script element', () => {
|
|
143
|
+
const out = injectBrandingGlobal('<head></head>', clientBranding(HOSTILE), {});
|
|
144
|
+
assert.ok(/<script>window\.__BRANDING__=(.*?)<\/script>/s.exec(out), 'the injected block is well-formed');
|
|
145
|
+
// Two closers are correct and expected: the client-bundle <script src=…></script>
|
|
146
|
+
// that leads the tag, and our own terminator. A third would BE the bug — the
|
|
147
|
+
// pack's `</script>` ending the element early. Unfixed, this counts 3.
|
|
148
|
+
assert.equal(out.split('</script>').length - 1, 2, 'no early close');
|
|
149
|
+
assert.ok(!out.includes('<img src=x'), 'the payload never reaches the document as a tag');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('1001434: every `<` in the injected JSON is \\u003c, and the VALUE is unchanged', () => {
|
|
153
|
+
const out = injectBrandingGlobal('<head></head>', clientBranding(HOSTILE), {});
|
|
154
|
+
const json = brandingJson(out);
|
|
155
|
+
assert.ok(!json.includes('<'), 'no raw < survives into the script body');
|
|
156
|
+
assert.ok(json.includes('\\u003c'), 'it is escaped, not stripped');
|
|
157
|
+
// Escaping must be lossless: < is the same character to the JSON parser,
|
|
158
|
+
// so the page reads exactly what the operator configured.
|
|
159
|
+
const parsed = JSON.parse(json);
|
|
160
|
+
assert.equal(parsed.identity.productName, '</script><img src=x onerror=alert(1)>');
|
|
161
|
+
assert.equal(parsed.copy.tagline, 'a < b && c > d', '> and & are left alone — a script body is not entity-decoded');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('1001434: __MODULES__ is escaped too — it rides the same script tag', () => {
|
|
165
|
+
const out = injectBrandingGlobal('<head></head>', clientBranding({}), { enabled: { '</script>': true } });
|
|
166
|
+
assert.equal(out.split('</script>').length - 1, 2, 'the bundle tag and our terminator, and nothing else');
|
|
167
|
+
const m = /window\.__MODULES__=(.*?);<\/script>/s.exec(out);
|
|
168
|
+
assert.ok(m && !m[1].includes('<'));
|
|
169
|
+
assert.deepEqual(JSON.parse(m[1]), { enabled: { '</script>': true } });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('1001434: a theme value carrying `<` is DROPPED rather than breaking out of <style>', () => {
|
|
173
|
+
const m = themeMarkup({
|
|
174
|
+
colors: { bg: '#fff', ink: '</style><script>alert(1)</script>' },
|
|
175
|
+
backdrop: '</style>x',
|
|
176
|
+
fonts: { display: '"Cinzel", serif' },
|
|
177
|
+
});
|
|
178
|
+
assert.ok(!m.includes('<script>'), 'no escape from the style element');
|
|
179
|
+
assert.equal(m.split('</style>').length - 1, 1, 'exactly one style terminator — our own');
|
|
180
|
+
assert.ok(m.includes('--bg:#fff;'), 'the clean declarations still ship');
|
|
181
|
+
assert.ok(!m.includes('--ink:'), 'the poisoned one is dropped whole, not half-escaped');
|
|
182
|
+
assert.ok(!m.includes('body{background:'), 'a poisoned backdrop is dropped too');
|
|
183
|
+
// CSS is not entity-decoded, so esc() here would be a bug of its own: the font
|
|
184
|
+
// stack has to keep its real quotes.
|
|
185
|
+
assert.ok(m.includes('--font-display:"Cinzel", serif;'), 'quotes in a font stack survive intact');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('1001434: a pack cannot break out of the favicon or font-link href attribute', () => {
|
|
189
|
+
const m = themeMarkup({ favicon: 'x"><script>alert(1)</script>', fonts: { links: ['y"><script>alert(2)</script>'] } });
|
|
190
|
+
assert.ok(!m.includes('<script>'), 'neither href can escape its attribute');
|
|
191
|
+
assert.ok(m.includes('"'), 'the quote is entity-escaped, which the parser undoes on read');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('1001434: escaping the favicon is lossless — the neutral inline-SVG data URI still resolves', () => {
|
|
195
|
+
const favicon = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><circle r='8'/></svg>";
|
|
196
|
+
const m = themeMarkup({ favicon });
|
|
197
|
+
const href = /<link rel="icon" href="([^"]*)">/.exec(m);
|
|
198
|
+
assert.ok(href, 'the attribute is still well-formed');
|
|
199
|
+
// Decode the way an HTML parser does; the operator's bytes must come back.
|
|
200
|
+
const decoded = href[1]
|
|
201
|
+
.replace(/</g, '<').replace(/>/g, '>')
|
|
202
|
+
.replace(/"/g, '"').replace(/'/g, "'")
|
|
203
|
+
.replace(/&/g, '&');
|
|
204
|
+
assert.equal(decoded, favicon, 'the browser reads exactly the URI the pack configured');
|
|
205
|
+
});
|
|
206
|
+
|
|
124
207
|
// --- BV1.R75: module hall-widget script injection (ADR 0062 §4) ---
|
|
125
208
|
|
|
126
209
|
const HALL = '<body><script src="/builders/builders.js?v=2026-06-27-r75" defer></script></body>';
|