clearotron 0.3.1-beta.1 → 0.3.1-beta.2
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/.env.example +17 -0
- package/INSTALL.md +1 -1
- package/README.md +6 -1
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/bin/connect.mjs +2 -2
- package/bin/onboard.mjs +96 -11
- package/bin/start.mjs +129 -23
- package/build-info.json +2 -2
- package/docs/architecture/05-config-governance.md +14 -0
- package/driver/CHANGELOG.md +44 -0
- package/driver/citation-census.json +2 -1
- package/driver/common-law-receipts.mjs +78 -1
- package/driver/contract-vocabulary.mjs +6 -6
- package/driver/engine/anthropic-agent.mjs +75 -0
- package/driver/form-neighbourhood.mjs +44 -2
- package/driver/matter-frame-record.mjs +68 -1
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +27 -27
- package/driver/portal-config-view.mjs +6 -0
- package/driver/portal-mcp-client.mjs +54 -9
- package/driver/portal-service.mjs +69 -6
- package/driver/profile-page.html +23 -1
- package/driver/profiles.mjs +5 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +22 -0
- package/driver/scope-ledger.mjs +33 -0
- package/driver/skills/prelim-common-law/SKILL.md +3 -1
- package/driver/skills/prelim-search/synthesis-rules.md +3 -1
- package/driver/stages.mjs +10 -1
- package/driver/suite-census.json +74 -56
- package/mcp-server/CHANGELOG.md +12 -0
- package/mcp-server/http-server.mjs +27 -4
- package/mcp-server/lib/cf-access.mjs +16 -2
- package/mcp-server/lib/http-handler.mjs +37 -1
- package/mcp-server/lib/ops.mjs +22 -9
- package/mcp-server/lib/options.mjs +1 -1
- package/mcp-server/lib/plan.mjs +47 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +29 -4
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-D5WAoLZI.css} +20 -6
- package/portal-ui/dist/assets/{index-DLa7PlKY.js → index-D8ITW-aD.js} +2131 -743
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +6 -6
- package/providers/_shared/term-shape.mjs +6 -0
- package/providers/_shared/territory-codes.mjs +43 -0
- package/providers/clarivate/src/capabilities.js +13 -1
- package/providers/clarivate/src/core.js +21 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/citation-line-check.mjs +67 -21
- package/scripts/e2e.mjs +4 -2
- package/scripts/import-cycle-check.mjs +34 -3
- package/scripts/live-surface-check.mjs +2 -2
- package/scripts/mint-names-in-force.mjs +21 -2
- package/scripts/purge-runs.mjs +114 -3
- package/scripts/revisit-render-check.mjs +77 -44
- package/scripts/test-run.mjs +7 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/connect-clients.mjs +95 -16
- package/shared/invocation.mjs +25 -0
- package/shared/mcp-challenge.mjs +32 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/notice-owed.mjs +62 -0
- package/shared/scope.mjs +66 -5
- package/shared/stdio-connect.mjs +92 -24
- package/shared/trigger-lane.mjs +19 -0
- package/shared/wsl.mjs +15 -0
- package/skills/clearotron-account/SKILL.md +27 -0
- package/skills/clearotron-ops/COURIER.md +8 -1
- package/skills/clearotron-ops/SKILL.md +18 -2
package/driver/suite-census.json
CHANGED
|
@@ -172,8 +172,8 @@
|
|
|
172
172
|
"todos": 0
|
|
173
173
|
},
|
|
174
174
|
"a-connect-line-says-where-it-runs.test.mjs": {
|
|
175
|
-
"tests":
|
|
176
|
-
"asserts":
|
|
175
|
+
"tests": 7,
|
|
176
|
+
"asserts": 25,
|
|
177
177
|
"skips": 0,
|
|
178
178
|
"todos": 0
|
|
179
179
|
},
|
|
@@ -400,8 +400,8 @@
|
|
|
400
400
|
"todos": 0
|
|
401
401
|
},
|
|
402
402
|
"a-key-the-demo-prints-signs-for-that-demo.test.mjs": {
|
|
403
|
-
"tests":
|
|
404
|
-
"asserts":
|
|
403
|
+
"tests": 8,
|
|
404
|
+
"asserts": 40,
|
|
405
405
|
"skips": 0,
|
|
406
406
|
"todos": 0
|
|
407
407
|
},
|
|
@@ -621,6 +621,12 @@
|
|
|
621
621
|
"skips": 0,
|
|
622
622
|
"todos": 0
|
|
623
623
|
},
|
|
624
|
+
"a-refusal-marks-the-field-in-whichever-view-is-open.test.mjs": {
|
|
625
|
+
"tests": 4,
|
|
626
|
+
"asserts": 10,
|
|
627
|
+
"skips": 0,
|
|
628
|
+
"todos": 0
|
|
629
|
+
},
|
|
624
630
|
"a-refusal-means-nothing-without-its-control.test.mjs": {
|
|
625
631
|
"tests": 8,
|
|
626
632
|
"asserts": 38,
|
|
@@ -1216,8 +1222,8 @@
|
|
|
1216
1222
|
"todos": 0
|
|
1217
1223
|
},
|
|
1218
1224
|
"bundle-rebuild-and-ports.test.mjs": {
|
|
1219
|
-
"tests":
|
|
1220
|
-
"asserts":
|
|
1225
|
+
"tests": 10,
|
|
1226
|
+
"asserts": 24,
|
|
1221
1227
|
"skips": 0,
|
|
1222
1228
|
"todos": 0
|
|
1223
1229
|
},
|
|
@@ -1288,8 +1294,8 @@
|
|
|
1288
1294
|
"todos": 0
|
|
1289
1295
|
},
|
|
1290
1296
|
"channel-fallback-silence.test.mjs": {
|
|
1291
|
-
"tests":
|
|
1292
|
-
"asserts":
|
|
1297
|
+
"tests": 9,
|
|
1298
|
+
"asserts": 28,
|
|
1293
1299
|
"skips": 0,
|
|
1294
1300
|
"todos": 0
|
|
1295
1301
|
},
|
|
@@ -1337,7 +1343,7 @@
|
|
|
1337
1343
|
},
|
|
1338
1344
|
"client-door.test.mjs": {
|
|
1339
1345
|
"tests": 38,
|
|
1340
|
-
"asserts":
|
|
1346
|
+
"asserts": 163,
|
|
1341
1347
|
"skips": 1,
|
|
1342
1348
|
"todos": 0
|
|
1343
1349
|
},
|
|
@@ -1432,8 +1438,8 @@
|
|
|
1432
1438
|
"todos": 0
|
|
1433
1439
|
},
|
|
1434
1440
|
"connect-clients-are-data.test.mjs": {
|
|
1435
|
-
"tests":
|
|
1436
|
-
"asserts":
|
|
1441
|
+
"tests": 17,
|
|
1442
|
+
"asserts": 87,
|
|
1437
1443
|
"skips": 0,
|
|
1438
1444
|
"todos": 0
|
|
1439
1445
|
},
|
|
@@ -1870,8 +1876,8 @@
|
|
|
1870
1876
|
"todos": 0
|
|
1871
1877
|
},
|
|
1872
1878
|
"doctor-reads-the-units-not-the-shell.test.mjs": {
|
|
1873
|
-
"tests":
|
|
1874
|
-
"asserts":
|
|
1879
|
+
"tests": 37,
|
|
1880
|
+
"asserts": 111,
|
|
1875
1881
|
"skips": 1,
|
|
1876
1882
|
"todos": 0
|
|
1877
1883
|
},
|
|
@@ -2092,8 +2098,8 @@
|
|
|
2092
2098
|
"todos": 0
|
|
2093
2099
|
},
|
|
2094
2100
|
"engine.anthropic.test.mjs": {
|
|
2095
|
-
"tests":
|
|
2096
|
-
"asserts":
|
|
2101
|
+
"tests": 65,
|
|
2102
|
+
"asserts": 238,
|
|
2097
2103
|
"skips": 0,
|
|
2098
2104
|
"todos": 0
|
|
2099
2105
|
},
|
|
@@ -2368,8 +2374,8 @@
|
|
|
2368
2374
|
"todos": 0
|
|
2369
2375
|
},
|
|
2370
2376
|
"form-neighbourhood.test.mjs": {
|
|
2371
|
-
"tests":
|
|
2372
|
-
"asserts":
|
|
2377
|
+
"tests": 20,
|
|
2378
|
+
"asserts": 89,
|
|
2373
2379
|
"skips": 0,
|
|
2374
2380
|
"todos": 0
|
|
2375
2381
|
},
|
|
@@ -2704,8 +2710,8 @@
|
|
|
2704
2710
|
"todos": 0
|
|
2705
2711
|
},
|
|
2706
2712
|
"matter-frame-record.test.mjs": {
|
|
2707
|
-
"tests":
|
|
2708
|
-
"asserts":
|
|
2713
|
+
"tests": 20,
|
|
2714
|
+
"asserts": 71,
|
|
2709
2715
|
"skips": 0,
|
|
2710
2716
|
"todos": 0
|
|
2711
2717
|
},
|
|
@@ -2818,8 +2824,8 @@
|
|
|
2818
2824
|
"todos": 0
|
|
2819
2825
|
},
|
|
2820
2826
|
"nothing-a-command-awaits-imports-it-back.test.mjs": {
|
|
2821
|
-
"tests":
|
|
2822
|
-
"asserts":
|
|
2827
|
+
"tests": 18,
|
|
2828
|
+
"asserts": 56,
|
|
2823
2829
|
"skips": 0,
|
|
2824
2830
|
"todos": 0
|
|
2825
2831
|
},
|
|
@@ -3058,8 +3064,8 @@
|
|
|
3058
3064
|
"todos": 0
|
|
3059
3065
|
},
|
|
3060
3066
|
"portal-access.test.mjs": {
|
|
3061
|
-
"tests":
|
|
3062
|
-
"asserts":
|
|
3067
|
+
"tests": 16,
|
|
3068
|
+
"asserts": 59,
|
|
3063
3069
|
"skips": 0,
|
|
3064
3070
|
"todos": 0
|
|
3065
3071
|
},
|
|
@@ -3250,8 +3256,8 @@
|
|
|
3250
3256
|
"todos": 0
|
|
3251
3257
|
},
|
|
3252
3258
|
"profiles.test.mjs": {
|
|
3253
|
-
"tests":
|
|
3254
|
-
"asserts":
|
|
3259
|
+
"tests": 48,
|
|
3260
|
+
"asserts": 263,
|
|
3255
3261
|
"skips": 0,
|
|
3256
3262
|
"todos": 0
|
|
3257
3263
|
},
|
|
@@ -3310,8 +3316,8 @@
|
|
|
3310
3316
|
"todos": 0
|
|
3311
3317
|
},
|
|
3312
3318
|
"purge-runs.test.mjs": {
|
|
3313
|
-
"tests":
|
|
3314
|
-
"asserts":
|
|
3319
|
+
"tests": 25,
|
|
3320
|
+
"asserts": 71,
|
|
3315
3321
|
"skips": 0,
|
|
3316
3322
|
"todos": 0
|
|
3317
3323
|
},
|
|
@@ -3520,8 +3526,8 @@
|
|
|
3520
3526
|
"todos": 0
|
|
3521
3527
|
},
|
|
3522
3528
|
"register-capabilities.test.mjs": {
|
|
3523
|
-
"tests":
|
|
3524
|
-
"asserts":
|
|
3529
|
+
"tests": 34,
|
|
3530
|
+
"asserts": 268,
|
|
3525
3531
|
"skips": 0,
|
|
3526
3532
|
"todos": 0
|
|
3527
3533
|
},
|
|
@@ -4348,8 +4354,8 @@
|
|
|
4348
4354
|
"todos": 0
|
|
4349
4355
|
},
|
|
4350
4356
|
"symbol-citation-survives-a-move.test.mjs": {
|
|
4351
|
-
"tests":
|
|
4352
|
-
"asserts":
|
|
4357
|
+
"tests": 9,
|
|
4358
|
+
"asserts": 25,
|
|
4353
4359
|
"skips": 0,
|
|
4354
4360
|
"todos": 0
|
|
4355
4361
|
},
|
|
@@ -4426,8 +4432,8 @@
|
|
|
4426
4432
|
"todos": 0
|
|
4427
4433
|
},
|
|
4428
4434
|
"test-run-tmpdir.test.mjs": {
|
|
4429
|
-
"tests":
|
|
4430
|
-
"asserts":
|
|
4435
|
+
"tests": 12,
|
|
4436
|
+
"asserts": 27,
|
|
4431
4437
|
"skips": 0,
|
|
4432
4438
|
"todos": 0
|
|
4433
4439
|
},
|
|
@@ -4475,7 +4481,7 @@
|
|
|
4475
4481
|
},
|
|
4476
4482
|
"the-boot-line-says-which-door-it-found.test.mjs": {
|
|
4477
4483
|
"tests": 5,
|
|
4478
|
-
"asserts":
|
|
4484
|
+
"asserts": 26,
|
|
4479
4485
|
"skips": 0,
|
|
4480
4486
|
"todos": 0
|
|
4481
4487
|
},
|
|
@@ -4570,8 +4576,8 @@
|
|
|
4570
4576
|
"todos": 0
|
|
4571
4577
|
},
|
|
4572
4578
|
"the-demo-offers-no-background-form.test.mjs": {
|
|
4573
|
-
"tests":
|
|
4574
|
-
"asserts":
|
|
4579
|
+
"tests": 5,
|
|
4580
|
+
"asserts": 19,
|
|
4575
4581
|
"skips": 0,
|
|
4576
4582
|
"todos": 0
|
|
4577
4583
|
},
|
|
@@ -4756,8 +4762,8 @@
|
|
|
4756
4762
|
"todos": 0
|
|
4757
4763
|
},
|
|
4758
4764
|
"the-minters-read-the-published-tree.test.mjs": {
|
|
4759
|
-
"tests":
|
|
4760
|
-
"asserts":
|
|
4765
|
+
"tests": 4,
|
|
4766
|
+
"asserts": 12,
|
|
4761
4767
|
"skips": 0,
|
|
4762
4768
|
"todos": 0
|
|
4763
4769
|
},
|
|
@@ -4774,14 +4780,14 @@
|
|
|
4774
4780
|
"todos": 0
|
|
4775
4781
|
},
|
|
4776
4782
|
"the-notice-goes-to-whoever-asked.test.mjs": {
|
|
4777
|
-
"tests":
|
|
4778
|
-
"asserts":
|
|
4783
|
+
"tests": 10,
|
|
4784
|
+
"asserts": 28,
|
|
4779
4785
|
"skips": 1,
|
|
4780
4786
|
"todos": 0
|
|
4781
4787
|
},
|
|
4782
4788
|
"the-open-line-says-what-to-do-when-the-page-is-not-ours.test.mjs": {
|
|
4783
|
-
"tests":
|
|
4784
|
-
"asserts":
|
|
4789
|
+
"tests": 6,
|
|
4790
|
+
"asserts": 24,
|
|
4785
4791
|
"skips": 0,
|
|
4786
4792
|
"todos": 0
|
|
4787
4793
|
},
|
|
@@ -5037,6 +5043,12 @@
|
|
|
5037
5043
|
"skips": 0,
|
|
5038
5044
|
"todos": 0
|
|
5039
5045
|
},
|
|
5046
|
+
"the-trigger-reaches-a-local-socket.test.mjs": {
|
|
5047
|
+
"tests": 4,
|
|
5048
|
+
"asserts": 24,
|
|
5049
|
+
"skips": 0,
|
|
5050
|
+
"todos": 0
|
|
5051
|
+
},
|
|
5040
5052
|
"the-two-env-files-are-named-where-they-appear.test.mjs": {
|
|
5041
5053
|
"tests": 5,
|
|
5042
5054
|
"asserts": 12,
|
|
@@ -5493,8 +5505,8 @@
|
|
|
5493
5505
|
"todos": 0
|
|
5494
5506
|
},
|
|
5495
5507
|
"cf-access.test.mjs": {
|
|
5496
|
-
"tests":
|
|
5497
|
-
"asserts":
|
|
5508
|
+
"tests": 20,
|
|
5509
|
+
"asserts": 34,
|
|
5498
5510
|
"skips": 0,
|
|
5499
5511
|
"todos": 0
|
|
5500
5512
|
},
|
|
@@ -5511,8 +5523,8 @@
|
|
|
5511
5523
|
"todos": 0
|
|
5512
5524
|
},
|
|
5513
5525
|
"connector-guidance.test.mjs": {
|
|
5514
|
-
"tests":
|
|
5515
|
-
"asserts":
|
|
5526
|
+
"tests": 13,
|
|
5527
|
+
"asserts": 70,
|
|
5516
5528
|
"skips": 0,
|
|
5517
5529
|
"todos": 0
|
|
5518
5530
|
},
|
|
@@ -5619,8 +5631,8 @@
|
|
|
5619
5631
|
"todos": 0
|
|
5620
5632
|
},
|
|
5621
5633
|
"ops.test.mjs": {
|
|
5622
|
-
"tests":
|
|
5623
|
-
"asserts":
|
|
5634
|
+
"tests": 39,
|
|
5635
|
+
"asserts": 168,
|
|
5624
5636
|
"skips": 0,
|
|
5625
5637
|
"todos": 0
|
|
5626
5638
|
},
|
|
@@ -5637,8 +5649,8 @@
|
|
|
5637
5649
|
"todos": 0
|
|
5638
5650
|
},
|
|
5639
5651
|
"plan.test.mjs": {
|
|
5640
|
-
"tests":
|
|
5641
|
-
"asserts":
|
|
5652
|
+
"tests": 28,
|
|
5653
|
+
"asserts": 125,
|
|
5642
5654
|
"skips": 0,
|
|
5643
5655
|
"todos": 0
|
|
5644
5656
|
},
|
|
@@ -5852,8 +5864,8 @@
|
|
|
5852
5864
|
"todos": 0
|
|
5853
5865
|
},
|
|
5854
5866
|
"home.test.ts": {
|
|
5855
|
-
"tests":
|
|
5856
|
-
"asserts":
|
|
5867
|
+
"tests": 51,
|
|
5868
|
+
"asserts": 169,
|
|
5857
5869
|
"skips": 0,
|
|
5858
5870
|
"todos": 0
|
|
5859
5871
|
},
|
|
@@ -6078,8 +6090,8 @@
|
|
|
6078
6090
|
"todos": 0
|
|
6079
6091
|
},
|
|
6080
6092
|
"providers/_shared/test/count-kernel.test.mjs": {
|
|
6081
|
-
"tests":
|
|
6082
|
-
"asserts":
|
|
6093
|
+
"tests": 10,
|
|
6094
|
+
"asserts": 45,
|
|
6083
6095
|
"skips": 0,
|
|
6084
6096
|
"todos": 0
|
|
6085
6097
|
},
|
|
@@ -6150,8 +6162,14 @@
|
|
|
6150
6162
|
"todos": 0
|
|
6151
6163
|
},
|
|
6152
6164
|
"providers/_shared/test/territory-codes.test.mjs": {
|
|
6153
|
-
"tests":
|
|
6154
|
-
"asserts":
|
|
6165
|
+
"tests": 11,
|
|
6166
|
+
"asserts": 38,
|
|
6167
|
+
"skips": 0,
|
|
6168
|
+
"todos": 0
|
|
6169
|
+
},
|
|
6170
|
+
"providers/clarivate/test/phrase-value.test.mjs": {
|
|
6171
|
+
"tests": 6,
|
|
6172
|
+
"asserts": 23,
|
|
6155
6173
|
"skips": 0,
|
|
6156
6174
|
"todos": 0
|
|
6157
6175
|
},
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# trademark-artifacts-mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.1-beta.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f96c089: Fixed: A clearance for a new client can be ordered through an assistant connector without setting up a company first.
|
|
8
|
+
- 240673c: Fixed: Sending an access key to the engine's network address now gets a refusal that says so. It reports that the address takes an identity from the sign-in proxy and never a key.
|
|
9
|
+
|
|
10
|
+
Fixed: That refusal also names the local door where a key is accepted. Before, it reported only a missing sign-in assertion, which sent operators to the wrong configuration.
|
|
11
|
+
|
|
12
|
+
For operators: A program on the same machine can now reach the engine's local key door without setting a host name for it. The local door no longer applies a browser protection that only a network address needs.
|
|
13
|
+
- f96c089: New: The free preview of a search lists, for each territory ordered, which registers legally bind it.
|
|
14
|
+
|
|
3
15
|
## 0.3.1-beta.1
|
|
4
16
|
|
|
5
17
|
No changes in this release.
|
|
@@ -128,14 +128,31 @@ const LOOPBACK = new Set(["127.0.0.1", "::1"]);
|
|
|
128
128
|
// transport is imported lazily here (it pulls @hono/node-server/undici) so nothing else has to.
|
|
129
129
|
// `owner` = the authed email that created the session; stored on the entry so the handler can refuse a
|
|
130
130
|
// different identity attaching to it (session owner-binding).
|
|
131
|
-
|
|
131
|
+
// ── DNS-REBINDING PROTECTION BELONGS TO THE NETWORK DOOR, AND ONLY TO IT ────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// `networkDoor` is false for the key socket. Both doors share this factory, and the allowed-hosts list
|
|
134
|
+
// it arms is read from the TCP door's own environment — so a socket caller was being held to a Host
|
|
135
|
+
// header naming the TCP door's address, on a transport that has no host, no port and no DNS.
|
|
136
|
+
//
|
|
137
|
+
// key-socket.mjs already states in terms that this refusal does not apply to it: "the allowed-hosts
|
|
138
|
+
// refusal arms DNS-rebinding protection, which is an attack on a browser resolving a HOSTNAME. A socket
|
|
139
|
+
// has no hostname and no DNS." That was true of the door it builds and false of the sessions that door
|
|
140
|
+
// creates, because the exclusion lived in a comment beside one handler while the protection lived in
|
|
141
|
+
// the factory both handlers call. Measured on a real key over a real socket: the call succeeds ONLY if
|
|
142
|
+
// the caller sends a Host naming the TCP listener, which is a requirement nothing documents and no
|
|
143
|
+
// integrator would guess.
|
|
144
|
+
//
|
|
145
|
+
// The network door is UNCHANGED — it passes nothing and keeps the default — so this narrows one
|
|
146
|
+
// transport rather than relaxing a protection.
|
|
147
|
+
async function createSession(sessions, scope = { kind: "internal", runId: null }, owner = null,
|
|
148
|
+
{ networkDoor = true } = {}) {
|
|
132
149
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
133
150
|
// HTTP is never the trusted-local surface: local:false (so what-if is never exposed remotely, even to ops).
|
|
134
151
|
const server = makeServer({ scope, local: false });
|
|
135
152
|
const transport = new StreamableHTTPServerTransport({
|
|
136
153
|
sessionIdGenerator: () => randomUUID(),
|
|
137
|
-
enableDnsRebindingProtection: ALLOWED_HOSTS.length > 0,
|
|
138
|
-
allowedHosts: ALLOWED_HOSTS.length ? ALLOWED_HOSTS : undefined,
|
|
154
|
+
enableDnsRebindingProtection: networkDoor && ALLOWED_HOSTS.length > 0,
|
|
155
|
+
allowedHosts: networkDoor && ALLOWED_HOSTS.length ? ALLOWED_HOSTS : undefined,
|
|
139
156
|
onsessioninitialized: (id) => sessions.set(id, { server, transport, lastSeen: Date.now(), email: owner, sub: scope?.sub ?? null, kind: scope?.kind ?? null }),
|
|
140
157
|
});
|
|
141
158
|
transport.onclose = () => { if (transport.sessionId) sessions.delete(transport.sessionId); };
|
|
@@ -303,6 +320,9 @@ if (isMain) {
|
|
|
303
320
|
// non-firm identity a mis-scoped edge might admit. Tenants gated purely by MCP_ALLOWED_EMAILS leave this
|
|
304
321
|
// list empty, so tokenless internal read-all stays off for them (fail-closed) unless they set the domain list.
|
|
305
322
|
const handler = makeHttpHandler({ verify, limiter, opsLimiter, sessions, createSession, ns: NS, sessionMax: SESSION_MAX,
|
|
323
|
+
// The network door tells a caller who sends a key WHERE the key door is, when this deployment has
|
|
324
|
+
// one. Refusing without saying that is what sent the last operator to the proxy's configuration.
|
|
325
|
+
keyDoorPath: KEY_SOCKET || null,
|
|
306
326
|
authHeader: AUTH_HEADER, devMode: !TOKEN_ONLY && AUTH_DISABLED && DEV, tokenOnly: TOKEN_ONLY, firmDomains: ALLOWED_DOMAINS, log });
|
|
307
327
|
// Three states now, and the line must name the one it is in. "AUTH OFF (dev)" was printed by anything
|
|
308
328
|
// with a null `verify`, which from here on includes the key door — a line claiming the opposite of
|
|
@@ -337,7 +357,10 @@ if (isMain) {
|
|
|
337
357
|
if (refusal) { log(`FATAL: ${refusal}`); process.exit(1); }
|
|
338
358
|
const keyHandler = makeHttpHandler({
|
|
339
359
|
verify: null, tokenOnly: true, devMode: false,
|
|
340
|
-
limiter, opsLimiter, sessions: new Map(),
|
|
360
|
+
limiter, opsLimiter, sessions: new Map(), ns: NS, sessionMax: SESSION_MAX,
|
|
361
|
+
// The socket's sessions are not network sessions — see createSession. Passed here rather than
|
|
362
|
+
// read inside it, so the factory keeps one rule and the caller names which door it is.
|
|
363
|
+
createSession: (sessions, scope, owner) => createSession(sessions, scope, owner, { networkDoor: false }),
|
|
341
364
|
authHeader: AUTH_HEADER, firmDomains: ALLOWED_DOMAINS, log,
|
|
342
365
|
});
|
|
343
366
|
openKeyDoor({ handler: keyHandler, path: KEY_SOCKET, log })
|
|
@@ -34,7 +34,7 @@ export class AuthError extends Error {
|
|
|
34
34
|
* applied IN ADDITION to the domain gate (either empty list = that gate off). Building a verifier with
|
|
35
35
|
* NO identity gate at all requires an explicit allowAnyDomain:true (fail-closed footgun guard).
|
|
36
36
|
*/
|
|
37
|
-
export function makeAccessVerifier({ team, aud, allowedDomains = [], allowedEmails = [], allowAnyDomain = false, identityMode = "intersection", issuer, jwksUrl, emailClaim = "email", jwks } = {}) {
|
|
37
|
+
export function makeAccessVerifier({ team, aud, allowedDomains = [], allowedEmails = [], allowedNow = null, allowAnyDomain = false, identityMode = "intersection", issuer, jwksUrl, emailClaim = "email", jwks } = {}) {
|
|
38
38
|
const iss = issuer || (team ? `https://${team}.cloudflareaccess.com` : "");
|
|
39
39
|
// `!aud` alone does not close this: an EMPTY ARRAY is truthy, so a parser that returned `[]` for an
|
|
40
40
|
// unset variable would satisfy this guard and the four call sites' `!AUD` at once. MEASURED, because
|
|
@@ -94,7 +94,21 @@ export function makeAccessVerifier({ team, aud, allowedDomains = [], allowedEmai
|
|
|
94
94
|
if (identityMode === "union") {
|
|
95
95
|
const okDomain = domains.length > 0 && domains.includes(domain);
|
|
96
96
|
const okEmail = emails.length > 0 && emails.includes(email);
|
|
97
|
-
|
|
97
|
+
// ── AND THE PEOPLE THE DEPLOYMENT HAS ADDED SINCE THIS VERIFIER WAS BUILT ──────────────────────
|
|
98
|
+
//
|
|
99
|
+
// The two lists above are read once, at construction, out of the environment. The People page
|
|
100
|
+
// writes a person into the deployment's own guest list and the write succeeds — and this check
|
|
101
|
+
// never saw it, so the person was refused at the door with a 403 naming their address, and no
|
|
102
|
+
// restart helped because the values do not come from the file they were added to. Somebody added
|
|
103
|
+
// on the page could not sign in, which is the page's whole promise.
|
|
104
|
+
//
|
|
105
|
+
// ASKED AT VERIFY TIME, not frozen: `allowedNow` is a callback the caller supplies, reading the
|
|
106
|
+
// guest list as it is now. It can only ADD — the env lists stay exactly as they were, so a
|
|
107
|
+
// deployment that names nobody in a file is not widened by this and the fail-closed guards above
|
|
108
|
+
// still decide whether a verifier may be built at all.
|
|
109
|
+
const okAdded = !okDomain && !okEmail && typeof allowedNow === "function"
|
|
110
|
+
&& (() => { try { return allowedNow().includes(email); } catch { return false; } })();
|
|
111
|
+
if (!okDomain && !okEmail && !okAdded) throw new AuthError(403, `email not permitted: ${email}`);
|
|
98
112
|
} else {
|
|
99
113
|
if (domains.length && !domains.includes(domain)) throw new AuthError(403, `email domain not permitted: ${email}`);
|
|
100
114
|
if (emails.length && !emails.includes(email)) throw new AuthError(403, `email not on the allowlist: ${email}`);
|
|
@@ -69,7 +69,7 @@ export function evictOldest(sessions) {
|
|
|
69
69
|
* presents another identity's mcp-session-id is refused (403) — a leaked/guessed session id must never
|
|
70
70
|
* let one CF-authed person attach to another's session (which may carry an ops-scoped inner token).
|
|
71
71
|
*/
|
|
72
|
-
export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions, createSession, ns = "trademark-artifacts", sessionMax = 500, maxBody = 4 * 1024 * 1024, authHeader = "cf-access-jwt-assertion", firmDomains = [], clientSurface = false, devMode = false, tokenOnly = false, log = () => {} }) {
|
|
72
|
+
export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions, createSession, ns = "trademark-artifacts", sessionMax = 500, maxBody = 4 * 1024 * 1024, authHeader = "cf-access-jwt-assertion", firmDomains = [], clientSurface = false, devMode = false, tokenOnly = false, keyDoorPath = null, log = () => {} }) {
|
|
73
73
|
if (!verify && !devMode && !tokenOnly) throw new Error("makeHttpHandler: verify is required unless devMode:true or tokenOnly:true (fail-closed; refusing to build an unauthenticated handler)");
|
|
74
74
|
// The two verify-less modes mean OPPOSITE things and must never be combined: devMode trusts the local
|
|
75
75
|
// operator and hands out a synthetic identity, tokenOnly trusts NOBODY without a valid key. Together,
|
|
@@ -99,6 +99,42 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
99
99
|
user = await verify(hdr(req.headers[authHeader]));
|
|
100
100
|
} catch (e) {
|
|
101
101
|
const status = e instanceof AuthError ? e.status : 401;
|
|
102
|
+
// ── A KEY PRESENTED HERE IS NOT A MISSING JWT, AND THE OLD SENTENCE SAID IT WAS ─────────
|
|
103
|
+
//
|
|
104
|
+
// This door takes a proxy identity and has no path that reads an access key — the refusal is
|
|
105
|
+
// structural rather than a check. But the sentence a caller got was "missing auth-proxy JWT",
|
|
106
|
+
// which is a statement about an ABSENT assertion and says nothing about the key that was
|
|
107
|
+
// actually sent. Measured against a real key on all three carriers: an operator reads it as a
|
|
108
|
+
// misconfigured proxy and goes looking there. That is the outage this issue was raised from
|
|
109
|
+
// wearing a different hat — a true sentence that is not the one the reader needs.
|
|
110
|
+
//
|
|
111
|
+
// DETECTED ON THE UNAMBIGUOUS CARRIERS ONLY. `?token=` and `x-trademark-token` carry nothing
|
|
112
|
+
// but a trademark key. `Authorization` is NOT read here even though a key can arrive in it,
|
|
113
|
+
// because on a proxy-fronted door that header belongs to whatever the proxy or agent is doing
|
|
114
|
+
// and reading it as a key would put this sentence in front of callers who never sent one. So
|
|
115
|
+
// a key in `Authorization` still gets the old message: narrower than the finding, and wrong
|
|
116
|
+
// in the direction that costs nothing.
|
|
117
|
+
const keySent = e instanceof AuthError && e.status === 401
|
|
118
|
+
&& Boolean(url.searchParams.get("token") || hdr(req.headers["x-trademark-token"]));
|
|
119
|
+
if (keySent) {
|
|
120
|
+
log("auth reject 401: an access key was presented on the proxy door, which has no key path");
|
|
121
|
+
// THE SENTENCE IS ALSO READ BY A MACHINE, and the first draft of it broke that. `doorCredential`
|
|
122
|
+
// in shared/trigger-lane.mjs classifies a door by what its refusal asks for, and the portal's
|
|
123
|
+
// boot probe uses that to report which lane it has. A sentence saying "an access key has no
|
|
124
|
+
// door here" contains the words "access key", so it classified as a KEY door — and the portal
|
|
125
|
+
// would have announced that the network door takes its key, against a door that refuses it.
|
|
126
|
+
// That is this issue's own false-reassurance finding, reintroduced by the fix for its
|
|
127
|
+
// neighbour. Naming the JWT puts it in the proxy branch, which is checked first and is the
|
|
128
|
+
// true answer: this door wants a proxy identity.
|
|
129
|
+
// THE PATH IS NOT IN THE SENTENCE, and the first draft put it there. This door faces the
|
|
130
|
+
// internet and answers before anyone has authenticated, so naming the socket's filesystem
|
|
131
|
+
// path hands an unauthenticated stranger a piece of the deployment's layout. They cannot
|
|
132
|
+
// reach it — the protection is the filesystem — so it is disclosure rather than exposure,
|
|
133
|
+
// and it buys nothing: an operator needs to know a local socket is where to look, and
|
|
134
|
+
// already has the path in the boot line and the unit file. A stranger gets nothing usable.
|
|
135
|
+
return send(res, 401, { error: "this listener takes an auth-proxy JWT and never an access key — a key has no door here"
|
|
136
|
+
+ (keyDoorPath ? ". A key is taken on this deployment's local socket; the engine's boot line names it" : ". This deployment has no key door configured") });
|
|
137
|
+
}
|
|
102
138
|
log(`auth reject ${status}: ${e.message}`);
|
|
103
139
|
return send(res, status, { error: e.message });
|
|
104
140
|
}
|
package/mcp-server/lib/ops.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { validateJob } from "../../driver/enqueue-schema.mjs";
|
|
|
17
17
|
import { doorGates } from "../../driver/door-gates.mjs"; // the resolved-product checks every door runs
|
|
18
18
|
import { probeQueueWatch, unwatchedQueueWarning } from "../../driver/queue-watch-probe.mjs"; //
|
|
19
19
|
import { resolveRun, runAccountKey } from "./runs.mjs";
|
|
20
|
-
import { assertAccountAccess, attributionOf } from "../../shared/scope.mjs";
|
|
20
|
+
import { assertAccountAccess, attributionOf, grantsGeneric, GENERIC_NOT_GRANTED } from "../../shared/scope.mjs";
|
|
21
21
|
import { requestCancel, isCancelled } from "../../driver/cancel.mjs"; // one marker shape, shared with the engine
|
|
22
22
|
import { readEngineChild, engineChildIsLive, endEngineChild } from "../../driver/engine/child-record.mjs"; //
|
|
23
23
|
import { killEscalateMs } from "../../driver/engine/common.mjs"; // the watchdogs' grace, reused by an immediate stop
|
|
@@ -224,20 +224,33 @@ export const START_RUN_JOB_FIELDS = Object.freeze({
|
|
|
224
224
|
}),
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
// The accounts-scoped rule shared by start_run and plan_run:
|
|
228
|
-
// keys on profileKey, but the runner's forwarderDomain→matchDomains fallback could
|
|
229
|
-
// customer for an untagged job —
|
|
230
|
-
// too so a scoped caller cannot use the free preview to probe another
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
// The accounts-scoped rule shared by start_run and plan_run: the job must NAME the account it runs
|
|
228
|
+
// under. The grant gate keys on profileKey, but the runner's forwarderDomain→matchDomains fallback could
|
|
229
|
+
// resolve a FOREIGN customer for an untagged job — so an untagged job is what the door refuses to build.
|
|
230
|
+
// plan_run goes through it too, so a scoped caller cannot use the free preview to probe another
|
|
231
|
+
// customer's configuration.
|
|
232
|
+
//
|
|
233
|
+
// NAMING IT IS NOT THE SAME AS THE CALLER TYPING IT, and reading the two as one refused the ordinary
|
|
234
|
+
// case this door exists to serve. A clearance for a NEW client has no account yet; the connector's own
|
|
235
|
+
// instructions say to omit profileKey then, and this threw on exactly that, so an assistant following
|
|
236
|
+
// its briefing hit a wall it could not explain. Where the session's access covers Generic, the neutral
|
|
237
|
+
// profile a job with no profileKey runs under anyway, the door now writes that key into the job itself:
|
|
238
|
+
// the bypass stays closed because the job is tagged, and the caller gets the run they asked for.
|
|
239
|
+
//
|
|
240
|
+
// Where it does not, the refusal says what to do next instead of only what is forbidden.
|
|
241
|
+
export function resolveScopedProfileKey(args, scope, verb) {
|
|
242
|
+
if (!Array.isArray(scope?.accounts)) return args;
|
|
243
|
+
if (args.profileKey && String(args.profileKey).trim()) return args;
|
|
244
|
+
if (grantsGeneric(scope)) return { ...args, profileKey: "generic" };
|
|
245
|
+
throw new Error(`${verb}: this session holds [${scope.accounts.join(", ")}] and the request names no account. `
|
|
246
|
+
+ `${GENERIC_NOT_GRANTED} A job that names no customer is never resolved from the requester's email domain.`);
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
// start_run — enqueue a clearotron job the runner will drain. Returns the queue id + slug; the runId/codename is
|
|
237
250
|
// assigned by the runner on claim, so the caller polls list_runs(slug) / run_changes for it.
|
|
238
251
|
export function startRun(args = {}, { scope } = {}) {
|
|
239
252
|
const agent = args.agent ? String(args.agent) : "";
|
|
240
|
-
|
|
253
|
+
args = resolveScopedProfileKey(args, scope, "start_run");
|
|
241
254
|
const qdir = queueDir(agent);
|
|
242
255
|
// ONE builder shared with plan_run, so the preview a caller confirms is the job that actually runs.
|
|
243
256
|
let job;
|
|
@@ -331,7 +331,7 @@ export function describeOptions(args = {}, { scope, now = Date.now() } = {}) {
|
|
|
331
331
|
const account = accountFor(key, { scope, now });
|
|
332
332
|
const granted = scope?.accounts;
|
|
333
333
|
// The profileKey discovery answer: several accounts held, none named. Listing the KEYS (and, where the
|
|
334
|
-
// roster reads, their names) is what turns "
|
|
334
|
+
// roster reads, their names) is what turns "this session holds [...] and the request names no account"
|
|
335
335
|
// from a dead end into a question the assistant can put to its user.
|
|
336
336
|
const accountsGranted = !account && Array.isArray(granted) && granted.length
|
|
337
337
|
? granted.map((k) => {
|
package/mcp-server/lib/plan.mjs
CHANGED
|
@@ -34,8 +34,10 @@ import { gateResolvedRequest } from "../../driver/door-gates.mjs"; // the reso
|
|
|
34
34
|
import { quoteForJob } from "../../driver/run-quote.mjs";
|
|
35
35
|
import { accountUsage, DEFAULT_CLIENT_DAILY_RUNS } from "../../driver/usage-ledger.mjs";
|
|
36
36
|
import { config } from "./driver.mjs";
|
|
37
|
-
import { buildJob,
|
|
37
|
+
import { buildJob, resolveScopedProfileKey } from "./ops.mjs";
|
|
38
38
|
import { BRAND } from "../../shared/brand.mjs"; // — the operator name in the allowance note, from the tenant seam
|
|
39
|
+
import { bindingLayersFor } from "../../driver/binding-layers.mjs"; // — what BINDS a territory; never what a provider returns
|
|
40
|
+
import { normalizeTerritory } from "../../providers/_shared/territory-codes.mjs";
|
|
39
41
|
|
|
40
42
|
// The verbatim legal caveat every plan carries. NOT paraphrasable: it states what a common-law-first
|
|
41
43
|
// screen can and cannot tell you, and softening it would misrepresent the product to the person deciding
|
|
@@ -141,13 +143,52 @@ function allowanceFor(profile, { scope, now = Date.now() } = {}) {
|
|
|
141
143
|
};
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* WHICH REGISTERS ALREADY BIND EACH ORDERED TERRITORY, for the preview to say out loud.
|
|
148
|
+
*
|
|
149
|
+
* A requester asked to clear a mark in one EU member state and was sold the member state AND the EU as
|
|
150
|
+
* two territories, on the reasoning that the EU-wide right had to be listed to be covered. It does not:
|
|
151
|
+
* an EU trade mark blocks use in the member state without appearing in its national register, and the
|
|
152
|
+
* engine has searched on that basis since the ruling `binding-layers.mjs` quotes in its header. Naming
|
|
153
|
+
* two territories instead resolves a different product — the one that carries no case-law reading and
|
|
154
|
+
* no automatic native-language investigation — so the composition bought less coverage for more money,
|
|
155
|
+
* and nothing on this surface said so.
|
|
156
|
+
*
|
|
157
|
+
* WHAT BINDS A TERRITORY, NOT WHAT A SEARCH RETURNS. `binding-layers.mjs` separates those two facts
|
|
158
|
+
* deliberately: the first is a property of the territory and always true, the second is a vendor fact
|
|
159
|
+
* that is often unestablished. This reports the first only. `search.registerReach` next to it is the
|
|
160
|
+
* second, and the two must never be blurred into one sentence.
|
|
161
|
+
*
|
|
162
|
+
* A territory this build cannot resolve to a register says so rather than being dropped from the list:
|
|
163
|
+
* a silent omission reads as "nothing binds there", which is never true.
|
|
164
|
+
*/
|
|
165
|
+
function bindingLayersForOrder(jurisdictions) {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const j of jurisdictions ?? []) {
|
|
168
|
+
const code = normalizeTerritory(j);
|
|
169
|
+
if (!code) {
|
|
170
|
+
out.push({ territory: j, code: null, binds: null,
|
|
171
|
+
note: `"${j}" could not be matched to a register this build knows, so what binds it is not stated here` });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
out.push({ territory: j, code, binds: bindingLayersFor(code).map((b) => ({ layer: b.layer, office: b.office, why: b.why })) });
|
|
176
|
+
} catch {
|
|
177
|
+
out.push({ territory: j, code, binds: null, note: `what binds "${j}" could not be read` });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
/**
|
|
145
184
|
* plan_run — resolve a prospective search and describe it. Spends nothing, writes nothing.
|
|
146
185
|
*
|
|
147
186
|
* Takes the SAME args as start_run. Confirming is a separate, explicit start_run call with the same args.
|
|
148
187
|
*/
|
|
149
188
|
export function planRun(args = {}, { scope, now = Date.now() } = {}) {
|
|
150
|
-
|
|
189
|
+
// Resolved, not merely checked — and BEFORE buildJob, so the preview describes the job start_run
|
|
190
|
+
// would build, including the account it would run under when the request named none.
|
|
191
|
+
args = resolveScopedProfileKey(args, scope, "plan_run");
|
|
151
192
|
// The EXACT job start_run would build — not a lookalike assembled here, which could drift from it.
|
|
152
193
|
const job = buildJob(args, { scope });
|
|
153
194
|
|
|
@@ -352,6 +393,10 @@ export function planRun(args = {}, { scope, now = Date.now() } = {}) {
|
|
|
352
393
|
scope: {
|
|
353
394
|
jurisdictions: eff.jurisdictions,
|
|
354
395
|
jurisdictionsFrom: eff.jurisdictionsFrom,
|
|
396
|
+
// WHAT EACH ORDERED TERRITORY ALREADY BINDS — so nobody adds a region to the list to "include"
|
|
397
|
+
// a right that the country already carries, and pays for it with the product that change
|
|
398
|
+
// resolves to. This is the territory's own property, never a claim about what the search returns.
|
|
399
|
+
bindingLayers: bindingLayersForOrder(eff.jurisdictions),
|
|
355
400
|
platforms: eff.platforms,
|
|
356
401
|
platformsAdded: eff.platformsAdded,
|
|
357
402
|
platformsFrom: eff.platformsFrom,
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.1-beta.
|
|
3
|
+
"version": "0.3.1-beta.2",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|