@sigrun/sigrun 0.1.6 → 0.2.0
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/CHANGELOG.md +104 -0
- package/README.md +352 -36
- package/dist/blocks/schema.js +9 -2
- package/dist/blocks/validate.js +57 -8
- package/dist/cli.js +26 -2
- package/dist/config.js +67 -1
- package/dist/extension.d.ts +189 -15
- package/dist/harness/ambient.js +39 -185
- package/dist/harness/blocks/entries.js +47 -0
- package/dist/harness/blocks/state.js +58 -0
- package/dist/harness/blocks/tools.js +274 -0
- package/dist/harness/boot.js +253 -20
- package/dist/harness/builtins.js +15 -11
- package/dist/harness/event-bus.js +105 -10
- package/dist/harness/identity/accounts.js +186 -0
- package/dist/harness/identity/groups.js +14 -0
- package/dist/harness/identity/index.js +13 -6
- package/dist/harness/identity/json-store.js +15 -4
- package/dist/harness/identity/setup-code-check.js +9 -0
- package/dist/harness/identity/tokens.js +28 -5
- package/dist/harness/identity/users.js +111 -12
- package/dist/harness/interactions.js +292 -136
- package/dist/harness/loader.js +271 -17
- package/dist/harness/packages.js +1 -1
- package/dist/harness/reserved-sections.js +21 -0
- package/dist/harness/seam-host.js +80 -6
- package/dist/harness/settings.js +63 -2
- package/dist/harness/stores.js +241 -4
- package/dist/harness/user-services.js +391 -0
- package/dist/harness/user-tools.js +187 -0
- package/dist/harness/workspace.js +61 -57
- package/dist/jobs/fire.js +10 -16
- package/dist/sandbox/availability.js +162 -0
- package/dist/sandbox/command-observer.js +59 -0
- package/dist/sandbox/grants.js +159 -0
- package/dist/sandbox/helper.js +29 -0
- package/dist/sandbox/isolation.js +29 -0
- package/dist/sandbox/launch.js +20 -0
- package/dist/sandbox/os-account.js +49 -0
- package/dist/sandbox/policy.js +253 -0
- package/dist/sandbox/pool.js +548 -0
- package/dist/sandbox/protocol.js +167 -0
- package/dist/sandbox/sandboxed-env.js +245 -0
- package/dist/sandbox/unavailable-env.js +49 -0
- package/dist/sandbox/worker.js +281 -0
- package/dist/serve.js +82 -7
- package/dist/server/auth/access-principal.js +16 -0
- package/dist/server/auth/pipeline.js +47 -33
- package/dist/server/blocks/answers.js +94 -0
- package/dist/server/blocks/chat-surfaces.js +393 -0
- package/dist/server/blocks/notes.js +216 -0
- package/dist/server/blocks/summaries.js +328 -0
- package/dist/server/commands.js +5 -0
- package/dist/server/content-parts-lazy.js +13 -0
- package/dist/server/conversation-handler.js +2 -5
- package/dist/server/conversation-ops.js +138 -37
- package/dist/server/helpers.js +18 -6
- package/dist/server/host-bridge.js +40 -20
- package/dist/server/images/cache.js +205 -0
- package/dist/server/images/convert.js +13 -0
- package/dist/server/images/fetch.js +302 -0
- package/dist/server/images/ranges.js +42 -0
- package/dist/server/images/rewrite.js +94 -0
- package/dist/server/images/sign.js +84 -0
- package/dist/server/images/sniff.js +62 -0
- package/dist/server/index.js +144 -4
- package/dist/server/login/flows.js +126 -0
- package/dist/server/login/http.js +31 -0
- package/dist/server/login/resolve.js +133 -0
- package/dist/server/login/setup-claim.js +55 -0
- package/dist/server/login/usernames.js +15 -0
- package/dist/server/ownership.js +13 -0
- package/dist/server/routes/admin.js +58 -0
- package/dist/server/routes/auth.js +88 -2
- package/dist/server/routes/conversations.js +3 -6
- package/dist/server/routes/events.js +28 -3
- package/dist/server/routes/extensions.js +37 -5
- package/dist/server/routes/images.js +267 -0
- package/dist/server/routes/interactions.js +57 -68
- package/dist/server/routes/jobs.js +2 -8
- package/dist/server/routes/login.js +223 -0
- package/dist/server/routes/me.js +90 -0
- package/dist/server/routes/meta.js +15 -0
- package/dist/server/routes/onboarding.js +94 -84
- package/dist/server/routes/settings-sections.js +190 -0
- package/dist/server/routes/settings.js +16 -4
- package/dist/server/routes/submissions.js +1 -1
- package/dist/server/routes/surfaces.js +175 -23
- package/dist/server/routes/users.js +116 -20
- package/dist/server/schema-fixture.js +213 -0
- package/dist/server/session-meta.js +3 -0
- package/dist/server/shutdown.js +11 -4
- package/dist/server/static.js +18 -0
- package/dist/server/surfaces/dedup.js +9 -2
- package/dist/server/surfaces/service.js +181 -13
- package/dist/server/surfaces/settings-surfaces.js +597 -0
- package/dist/server/turns/submit-turn.js +26 -0
- package/dist/server/views.js +16 -0
- package/dist/settings/core-sections.js +532 -0
- package/dist/settings/pending.js +181 -0
- package/dist/settings/registry.js +453 -0
- package/dist/skills/index.js +6 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +6 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
- package/node_modules/@sigrun/shared/dist/access.js +11 -4
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
- package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
- package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
- package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
- package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
- package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
- package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
- package/node_modules/@sigrun/shared/dist/themes.js +2 -3
- package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/turns.js +53 -3
- package/node_modules/@sigrun/shared/dist/views.d.ts +175 -12
- package/node_modules/@sigrun/shared/dist/views.js +16 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +11 -6
- package/webui/dist/assets/index-CxB969qp.js +55 -0
- package/webui/dist/assets/index-yqkL6BSS.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-CiYInlLx.css +0 -1
- package/webui/dist/assets/index-JDfOQV9z.js +0 -52
|
@@ -444,6 +444,65 @@ export const VIEW_SCHEMA = {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
},
|
|
447
|
+
{
|
|
448
|
+
"kind": "object",
|
|
449
|
+
"fields": {
|
|
450
|
+
"name": {
|
|
451
|
+
"kind": "string"
|
|
452
|
+
},
|
|
453
|
+
"description": {
|
|
454
|
+
"kind": "string"
|
|
455
|
+
},
|
|
456
|
+
"parameters": {
|
|
457
|
+
"kind": "object",
|
|
458
|
+
"fields": {
|
|
459
|
+
"type": {
|
|
460
|
+
"kind": "string"
|
|
461
|
+
},
|
|
462
|
+
"required": {
|
|
463
|
+
"kind": "array",
|
|
464
|
+
"variants": [
|
|
465
|
+
{
|
|
466
|
+
"kind": "string"
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
"properties": {
|
|
471
|
+
"kind": "object",
|
|
472
|
+
"fields": {
|
|
473
|
+
"title": {
|
|
474
|
+
"kind": "object",
|
|
475
|
+
"fields": {
|
|
476
|
+
"type": {
|
|
477
|
+
"kind": "string"
|
|
478
|
+
},
|
|
479
|
+
"description": {
|
|
480
|
+
"kind": "string"
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
"tree": {
|
|
485
|
+
"kind": "object",
|
|
486
|
+
"fields": {
|
|
487
|
+
"description": {
|
|
488
|
+
"kind": "string"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
"data": {
|
|
493
|
+
"kind": "object",
|
|
494
|
+
"fields": {
|
|
495
|
+
"description": {
|
|
496
|
+
"kind": "string"
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
},
|
|
447
506
|
{
|
|
448
507
|
"kind": "object",
|
|
449
508
|
"fields": {
|
|
@@ -541,6 +600,137 @@ export const VIEW_SCHEMA = {
|
|
|
541
600
|
}
|
|
542
601
|
}
|
|
543
602
|
},
|
|
603
|
+
{
|
|
604
|
+
"kind": "object",
|
|
605
|
+
"fields": {
|
|
606
|
+
"name": {
|
|
607
|
+
"kind": "string"
|
|
608
|
+
},
|
|
609
|
+
"description": {
|
|
610
|
+
"kind": "string"
|
|
611
|
+
},
|
|
612
|
+
"parameters": {
|
|
613
|
+
"kind": "object",
|
|
614
|
+
"fields": {
|
|
615
|
+
"type": {
|
|
616
|
+
"kind": "string"
|
|
617
|
+
},
|
|
618
|
+
"required": {
|
|
619
|
+
"kind": "array",
|
|
620
|
+
"variants": [
|
|
621
|
+
{
|
|
622
|
+
"kind": "string"
|
|
623
|
+
}
|
|
624
|
+
]
|
|
625
|
+
},
|
|
626
|
+
"properties": {
|
|
627
|
+
"kind": "object",
|
|
628
|
+
"fields": {
|
|
629
|
+
"entryId": {
|
|
630
|
+
"kind": "object",
|
|
631
|
+
"fields": {
|
|
632
|
+
"type": {
|
|
633
|
+
"kind": "string"
|
|
634
|
+
},
|
|
635
|
+
"description": {
|
|
636
|
+
"kind": "string"
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
"ops": {
|
|
641
|
+
"kind": "object",
|
|
642
|
+
"fields": {
|
|
643
|
+
"type": {
|
|
644
|
+
"kind": "string"
|
|
645
|
+
},
|
|
646
|
+
"items": {
|
|
647
|
+
"kind": "object",
|
|
648
|
+
"fields": {}
|
|
649
|
+
},
|
|
650
|
+
"description": {
|
|
651
|
+
"kind": "string"
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"kind": "object",
|
|
663
|
+
"fields": {
|
|
664
|
+
"name": {
|
|
665
|
+
"kind": "string"
|
|
666
|
+
},
|
|
667
|
+
"description": {
|
|
668
|
+
"kind": "string"
|
|
669
|
+
},
|
|
670
|
+
"parameters": {
|
|
671
|
+
"kind": "object",
|
|
672
|
+
"fields": {
|
|
673
|
+
"type": {
|
|
674
|
+
"kind": "string"
|
|
675
|
+
},
|
|
676
|
+
"required": {
|
|
677
|
+
"kind": "array",
|
|
678
|
+
"variants": [
|
|
679
|
+
{
|
|
680
|
+
"kind": "string"
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
"properties": {
|
|
685
|
+
"kind": "object",
|
|
686
|
+
"fields": {
|
|
687
|
+
"to": {
|
|
688
|
+
"kind": "object",
|
|
689
|
+
"fields": {
|
|
690
|
+
"type": {
|
|
691
|
+
"kind": "string"
|
|
692
|
+
},
|
|
693
|
+
"description": {
|
|
694
|
+
"kind": "string"
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
"text": {
|
|
699
|
+
"kind": "object",
|
|
700
|
+
"fields": {
|
|
701
|
+
"type": {
|
|
702
|
+
"kind": "string"
|
|
703
|
+
},
|
|
704
|
+
"description": {
|
|
705
|
+
"kind": "string"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
"files": {
|
|
710
|
+
"kind": "object",
|
|
711
|
+
"fields": {
|
|
712
|
+
"type": {
|
|
713
|
+
"kind": "string"
|
|
714
|
+
},
|
|
715
|
+
"items": {
|
|
716
|
+
"kind": "object",
|
|
717
|
+
"fields": {
|
|
718
|
+
"type": {
|
|
719
|
+
"kind": "string"
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
"description": {
|
|
724
|
+
"kind": "string"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
},
|
|
544
734
|
{
|
|
545
735
|
"kind": "object",
|
|
546
736
|
"fields": {
|
|
@@ -791,6 +981,29 @@ export const VIEW_SCHEMA = {
|
|
|
791
981
|
}
|
|
792
982
|
}
|
|
793
983
|
}
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"kind": "object",
|
|
987
|
+
"fields": {
|
|
988
|
+
"name": {
|
|
989
|
+
"kind": "string"
|
|
990
|
+
},
|
|
991
|
+
"description": {
|
|
992
|
+
"kind": "string"
|
|
993
|
+
},
|
|
994
|
+
"parameters": {
|
|
995
|
+
"kind": "object",
|
|
996
|
+
"fields": {
|
|
997
|
+
"type": {
|
|
998
|
+
"kind": "string"
|
|
999
|
+
},
|
|
1000
|
+
"properties": {
|
|
1001
|
+
"kind": "object",
|
|
1002
|
+
"fields": {}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
794
1007
|
}
|
|
795
1008
|
]
|
|
796
1009
|
},
|
|
@@ -219,6 +219,9 @@ export function applySessionMetaPatch(draft, patch) {
|
|
|
219
219
|
draft.sharing = {
|
|
220
220
|
scope: "people",
|
|
221
221
|
users: [...patch.sharing.users],
|
|
222
|
+
...(patch.sharing.groups === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { groups: [...patch.sharing.groups] }),
|
|
222
225
|
level: patch.sharing.level,
|
|
223
226
|
};
|
|
224
227
|
}
|
package/dist/server/shutdown.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The ordered shutdown (gateway C2's extraction of serve.ts's inline
|
|
3
|
-
* `shutdown`):
|
|
3
|
+
* `shutdown`): eight steps, each in its own try/catch so a failure never
|
|
4
4
|
* skips the remaining steps, with every failure collected into one
|
|
5
5
|
* `AggregateError("sigrun shutdown failed")` after ALL steps ran (the
|
|
6
6
|
* pre-extraction semantics, byte-for-byte).
|
|
@@ -20,15 +20,16 @@
|
|
|
20
20
|
* settles, then deliver `harnessShutdown` — handlers may commit);
|
|
21
21
|
* 5. `gateway.stop` — the engine stops only once every settle has been
|
|
22
22
|
* delivered (its adapters' outbound side saw the final answers);
|
|
23
|
-
* 6. `
|
|
23
|
+
* 6. `stopSandboxWorkers` — all per-user workers and their commands stop;
|
|
24
|
+
* 7. `stopProviderRefresh` — the interval scheduler + the bounded
|
|
24
25
|
* boot-refresh join (no catalog fetch outlives the shutdown);
|
|
25
|
-
*
|
|
26
|
+
* 8. `closeHarness` — the seal (joins tasks, seals storage).
|
|
26
27
|
*
|
|
27
28
|
* Does NOT exit the process — the caller owns exit policy (the deploy
|
|
28
29
|
* entry exits 0/1; the CLI's `run` returns to its exit code).
|
|
29
30
|
*/
|
|
30
31
|
/**
|
|
31
|
-
* Build the ordered shutdown over the
|
|
32
|
+
* Build the ordered shutdown over the eight steps. Failures from ANY step
|
|
32
33
|
* are collected (the remaining steps still run) and thrown together as
|
|
33
34
|
* `AggregateError(failures, "sigrun shutdown failed")` at the end; a clean
|
|
34
35
|
* run resolves.
|
|
@@ -66,6 +67,12 @@ export function createShutdown(steps) {
|
|
|
66
67
|
catch (error) {
|
|
67
68
|
failures.push(error);
|
|
68
69
|
}
|
|
70
|
+
try {
|
|
71
|
+
await steps.stopSandboxWorkers();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
failures.push(error);
|
|
75
|
+
}
|
|
69
76
|
try {
|
|
70
77
|
await steps.stopProviderRefresh();
|
|
71
78
|
}
|
package/dist/server/static.js
CHANGED
|
@@ -147,6 +147,17 @@ export async function serveWebuiBundle(app, bundle) {
|
|
|
147
147
|
await app.register(fastifyStatic, {
|
|
148
148
|
root: bundle.dir,
|
|
149
149
|
prefix: "/",
|
|
150
|
+
// U7 Task 3 (spec §11.2): the webui's HTML responses carry the
|
|
151
|
+
// one-directive CSP — remote https image sources are already
|
|
152
|
+
// rewritten to the /v1/image proxy at serve time, so img-src
|
|
153
|
+
// stays closed to the server itself plus data: URLs. A single
|
|
154
|
+
// img-src directive restricts images and nothing else: scripts
|
|
155
|
+
// and styles are unaffected. Static ASSETS keep no CSP.
|
|
156
|
+
setHeaders(res, path) {
|
|
157
|
+
if (path.endsWith(".html")) {
|
|
158
|
+
res.header("content-security-policy", "img-src 'self' data:");
|
|
159
|
+
}
|
|
160
|
+
},
|
|
150
161
|
});
|
|
151
162
|
app.setNotFoundHandler(async (request, reply) => {
|
|
152
163
|
const path = (request.raw.url ?? "/").split("?")[0] ?? "/";
|
|
@@ -156,6 +167,13 @@ export async function serveWebuiBundle(app, bundle) {
|
|
|
156
167
|
return await reply
|
|
157
168
|
.code(200)
|
|
158
169
|
.type("text/html; charset=utf-8")
|
|
170
|
+
// U7 Task 3 (spec §11.2): the webui's HTML responses carry
|
|
171
|
+
// the one-directive CSP — remote https image sources are
|
|
172
|
+
// already rewritten to the /v1/image proxy at serve time, so
|
|
173
|
+
// img-src stays closed to the server itself plus data: URLs.
|
|
174
|
+
// A single img-src directive restricts images and nothing
|
|
175
|
+
// else: scripts and styles are unaffected.
|
|
176
|
+
.header("content-security-policy", "img-src 'self' data:")
|
|
159
177
|
.send(bundle.indexHtml);
|
|
160
178
|
}
|
|
161
179
|
// Fastify's default 404 body shape, restated so the API error surface
|
|
@@ -35,7 +35,7 @@ export class ActionDedup {
|
|
|
35
35
|
* duplicate, `undefined` when never seen, `null` when the actionId
|
|
36
36
|
* belongs to a different instance (treated as fresh by the caller).
|
|
37
37
|
*/
|
|
38
|
-
check(actionId, composite) {
|
|
38
|
+
check(actionId, composite, fingerprint) {
|
|
39
39
|
const entry = this.entries.get(actionId);
|
|
40
40
|
if (entry === undefined)
|
|
41
41
|
return undefined;
|
|
@@ -45,12 +45,19 @@ export class ActionDedup {
|
|
|
45
45
|
}
|
|
46
46
|
if (entry.composite !== composite)
|
|
47
47
|
return null;
|
|
48
|
+
// A fingerprint mismatch is NOT a retry (the class docstring's
|
|
49
|
+
// rule): the id was reused by another client, so the caller runs
|
|
50
|
+
// the handler. The new result OVERWRITES the record (the latest
|
|
51
|
+
// invocation owns the id from here).
|
|
52
|
+
if (entry.fingerprint !== fingerprint)
|
|
53
|
+
return undefined;
|
|
48
54
|
return { result: entry.result };
|
|
49
55
|
}
|
|
50
56
|
/** Record the first result of one actionId for one instance. */
|
|
51
|
-
record(actionId, composite, result) {
|
|
57
|
+
record(actionId, composite, fingerprint, result) {
|
|
52
58
|
this.entries.set(actionId, {
|
|
53
59
|
composite,
|
|
60
|
+
fingerprint,
|
|
54
61
|
result,
|
|
55
62
|
expiresAt: this.clock() + DEDUP_WINDOW_MS,
|
|
56
63
|
});
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
* maps it to `500 surface_failed` (bodies carry `errors` only when
|
|
25
25
|
* `server.dev`). The extension stays loaded.
|
|
26
26
|
*/
|
|
27
|
+
import { rewriteImages } from "../images/rewrite.js";
|
|
27
28
|
import { validateTree } from "../../blocks/validate.js";
|
|
28
29
|
import { AccessCache } from "./access-cache.js";
|
|
29
30
|
import { ActionDedup } from "./dedup.js";
|
|
30
31
|
import { eventKey, instanceKey as defaultInstanceKey } from "./instance.js";
|
|
31
32
|
import { sniffMime } from "./mime.js";
|
|
32
33
|
import { ActionRateLimit } from "./rate-limit.js";
|
|
34
|
+
import { NamedUserRequiredError } from "./settings-surfaces.js";
|
|
33
35
|
import { WatcherRegistry } from "./watchers.js";
|
|
34
36
|
/** The shared-data op-log window (spec §7.3): the last 100 updates. */
|
|
35
37
|
export const SHARED_OPS_WINDOW = 100;
|
|
@@ -40,6 +42,21 @@ const TIMED_OUT_RESULT = {
|
|
|
40
42
|
ok: false,
|
|
41
43
|
error: { message: "timed out" },
|
|
42
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* The reserved chat "slug" (spec §8.2): a NUL byte can never appear in
|
|
47
|
+
* an extension slug, so chat composites never collide with one.
|
|
48
|
+
*/
|
|
49
|
+
export const CHAT_SURFACE_SLUG = "\u0000chat";
|
|
50
|
+
/** Decompose either address form into the service's table keys. */
|
|
51
|
+
function partsOf(address) {
|
|
52
|
+
return "conversationId" in address
|
|
53
|
+
? {
|
|
54
|
+
slug: CHAT_SURFACE_SLUG,
|
|
55
|
+
id: String(address.conversationId),
|
|
56
|
+
key: address.entryId,
|
|
57
|
+
}
|
|
58
|
+
: { slug: address.slug, id: address.id, key: address.key };
|
|
59
|
+
}
|
|
43
60
|
/** Errors `act` reports without calling the handler. */
|
|
44
61
|
export class ActionRateLimitedError extends Error {
|
|
45
62
|
constructor() {
|
|
@@ -119,12 +136,25 @@ export class SurfaceService {
|
|
|
119
136
|
maxUploadBytes;
|
|
120
137
|
watchers;
|
|
121
138
|
publishChanged;
|
|
139
|
+
/** The image-signer (U7 Task 1, spec §11.2): every tree/data response passes through the rewriter before leaving the server. */
|
|
140
|
+
imageSign;
|
|
141
|
+
extra = [];
|
|
122
142
|
constructor(bindings, options) {
|
|
123
143
|
this.bindings = bindings;
|
|
144
|
+
this.extra.push(...(options?.settingsSurfaces ?? []));
|
|
124
145
|
this.cache = new AccessCache();
|
|
125
146
|
this.maxUploadBytes = options?.maxUploadBytes ?? Number.MAX_SAFE_INTEGER;
|
|
126
147
|
this.watchers = options?.watchers ?? new WatcherRegistry();
|
|
127
148
|
this.publishChanged = options?.publishChanged;
|
|
149
|
+
this.imageSign = options?.imageSign;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Attach the settings surfaces post-construction (their save
|
|
153
|
+
* handlers close over THIS service for the optimistic-concurrency
|
|
154
|
+
* rev read, so they can only be built once the service exists).
|
|
155
|
+
*/
|
|
156
|
+
attachSettingsSurfaces(extra) {
|
|
157
|
+
this.extra.push(...extra);
|
|
128
158
|
}
|
|
129
159
|
/** The watcher registry (the events route reads it per event). */
|
|
130
160
|
get watcherRegistry() {
|
|
@@ -139,12 +169,19 @@ export class SurfaceService {
|
|
|
139
169
|
if (def !== undefined)
|
|
140
170
|
return { slug, def };
|
|
141
171
|
}
|
|
172
|
+
for (const binding of this.extra) {
|
|
173
|
+
if (binding.extension !== slug)
|
|
174
|
+
continue;
|
|
175
|
+
const def = binding.defs.find((entry) => entry.id === id);
|
|
176
|
+
if (def !== undefined)
|
|
177
|
+
return { slug, def };
|
|
178
|
+
}
|
|
142
179
|
return undefined;
|
|
143
180
|
}
|
|
144
181
|
/** The instance key: the surface's own `instanceKey` or the default. */
|
|
145
|
-
keyFor(def, params) {
|
|
182
|
+
keyFor(def, params, principal) {
|
|
146
183
|
if (def.instanceKey !== undefined)
|
|
147
|
-
return def.instanceKey(params);
|
|
184
|
+
return def.instanceKey(params, principal);
|
|
148
185
|
return defaultInstanceKey(params);
|
|
149
186
|
}
|
|
150
187
|
/** The per-instance state, created on first touch (revs start at 0). */
|
|
@@ -166,7 +203,7 @@ export class SurfaceService {
|
|
|
166
203
|
const located = this.locate(slug, id);
|
|
167
204
|
if (located === undefined)
|
|
168
205
|
return undefined;
|
|
169
|
-
const key = this.keyFor(located.def, params);
|
|
206
|
+
const key = this.keyFor(located.def, params, principal);
|
|
170
207
|
const result = await this.callRender(located, key, params, principal);
|
|
171
208
|
const validation = validateTree(result.tree, {
|
|
172
209
|
placement: located.def.placement,
|
|
@@ -197,8 +234,11 @@ export class SurfaceService {
|
|
|
197
234
|
}
|
|
198
235
|
}
|
|
199
236
|
return {
|
|
200
|
-
|
|
201
|
-
|
|
237
|
+
// SAFETY: the rewriter is a structural JSON walk returning the same
|
|
238
|
+
// shape; `Tree` (interfaces) has no implicit index signature, so the
|
|
239
|
+
// generic JsonValue round-trip needs the bridge cast.
|
|
240
|
+
tree: this.rewriteForWire(validation.tree),
|
|
241
|
+
data: this.rewriteForWire(result.data),
|
|
202
242
|
rev: state.rev,
|
|
203
243
|
treeRev: state.treeRev,
|
|
204
244
|
access: result.access,
|
|
@@ -215,7 +255,7 @@ export class SurfaceService {
|
|
|
215
255
|
const located = this.locate(slug, id);
|
|
216
256
|
if (located === undefined)
|
|
217
257
|
return undefined;
|
|
218
|
-
const key = this.keyFor(located.def, params);
|
|
258
|
+
const key = this.keyFor(located.def, params, principal);
|
|
219
259
|
const state = this.stateFor(slug, id, key);
|
|
220
260
|
const access = await this.accessFor(slug, id, key, params, principal);
|
|
221
261
|
if (access === "none")
|
|
@@ -231,7 +271,18 @@ export class SurfaceService {
|
|
|
231
271
|
const ops = state.ops
|
|
232
272
|
.filter((entry) => entry.rev > since)
|
|
233
273
|
.flatMap((entry) => entry.ops);
|
|
234
|
-
|
|
274
|
+
// Ops answers are data responses too (spec §7.3): the client folds
|
|
275
|
+
// them into its local copy, so they must carry the SAME rewritten
|
|
276
|
+
// sources the full data answer would — else fold(data, ops) drifts
|
|
277
|
+
// from the server's data.
|
|
278
|
+
return {
|
|
279
|
+
rev: state.rev,
|
|
280
|
+
treeRev: state.treeRev,
|
|
281
|
+
// SAFETY: ops are wire JSON (DataOp values); the rewriter returns
|
|
282
|
+
// the same shape, and `unknown[]`'s lack of an index signature is
|
|
283
|
+
// the only reason for the bridge cast.
|
|
284
|
+
ops: this.rewriteForWire(ops),
|
|
285
|
+
};
|
|
235
286
|
}
|
|
236
287
|
let data;
|
|
237
288
|
let access2;
|
|
@@ -274,7 +325,20 @@ export class SurfaceService {
|
|
|
274
325
|
if (located.def.sharedData !== true) {
|
|
275
326
|
state.rev += 1;
|
|
276
327
|
}
|
|
277
|
-
return { rev: state.rev, treeRev: state.treeRev, data };
|
|
328
|
+
return { rev: state.rev, treeRev: state.treeRev, data: this.rewriteForWire(data) };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The wire-side image rewrite (U7 Task 1, spec §11.1/§11.2): every
|
|
332
|
+
* tree and data (and ops) response passes through `rewriteImages`
|
|
333
|
+
* before leaving the server, replacing https image sources with
|
|
334
|
+
* signed proxy URLs. Absent signer → verbatim pass-through. This is
|
|
335
|
+
* the hook point U5's chat surfaces reuse when they land: they call
|
|
336
|
+
* the same `rewriteImages` over their trees/data.
|
|
337
|
+
*/
|
|
338
|
+
rewriteForWire(value) {
|
|
339
|
+
return this.imageSign === undefined
|
|
340
|
+
? value
|
|
341
|
+
: rewriteImages(value, this.imageSign);
|
|
278
342
|
}
|
|
279
343
|
/**
|
|
280
344
|
* One principal's access to one instance (spec §6.5): the cached
|
|
@@ -292,8 +356,8 @@ export class SurfaceService {
|
|
|
292
356
|
return result.access;
|
|
293
357
|
}
|
|
294
358
|
/** The composite instance key (`slugidkey`) for one def + params. */
|
|
295
|
-
compositeFor(slug, def, params) {
|
|
296
|
-
return `${slug}${def.id}${this.keyFor(def, params)}`;
|
|
359
|
+
compositeFor(slug, def, params, principal) {
|
|
360
|
+
return `${slug}${def.id}${this.keyFor(def, params, principal)}`;
|
|
297
361
|
}
|
|
298
362
|
/**
|
|
299
363
|
* The effective upload cap for one field (spec §7.1): the field's
|
|
@@ -344,18 +408,48 @@ export class SurfaceService {
|
|
|
344
408
|
throw new ActionRateLimitedError();
|
|
345
409
|
}
|
|
346
410
|
const composite = `${slug}${id}${key}`;
|
|
411
|
+
// The request fingerprint (action + params + payload)
|
|
412
|
+
// disambiguates a RETRY (same body — replay the stored result)
|
|
413
|
+
// from an actionId REUSE (a second client whose channel restarted
|
|
414
|
+
// its id sequence — two tabs of one webui share the module-level
|
|
415
|
+
// counter; U4 Task 4).
|
|
416
|
+
const fingerprint = JSON.stringify({ action, params, payload });
|
|
347
417
|
if (actionId !== undefined) {
|
|
348
|
-
const hit = this.dedup.check(actionId, composite);
|
|
418
|
+
const hit = this.dedup.check(actionId, composite, fingerprint);
|
|
349
419
|
if (hit !== undefined && hit !== null)
|
|
350
420
|
return hit.result;
|
|
351
421
|
}
|
|
352
|
-
|
|
422
|
+
let result;
|
|
423
|
+
try {
|
|
424
|
+
result = await this.invokeWithTimeout(() => handler({ principal, params, key, signal }, payload));
|
|
425
|
+
}
|
|
426
|
+
catch (cause) {
|
|
427
|
+
if (cause instanceof NamedUserRequiredError)
|
|
428
|
+
throw cause;
|
|
429
|
+
throw new SurfaceFailedError(`surface ${slug}/${id} action "${action}" threw: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
430
|
+
}
|
|
353
431
|
const wrapped = this.wrapResult(slug, id, key, result);
|
|
354
432
|
if (actionId !== undefined) {
|
|
355
|
-
this.dedup.record(actionId, composite, wrapped);
|
|
433
|
+
this.dedup.record(actionId, composite, fingerprint, wrapped);
|
|
356
434
|
}
|
|
357
435
|
return wrapped;
|
|
358
436
|
}
|
|
437
|
+
/** The instance's current rev (the save action's optimistic check). */
|
|
438
|
+
currentRev(slug, id, key) {
|
|
439
|
+
return this.stateFor(slug, id, key).rev;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The instance key for one surface + params, or `undefined` when the
|
|
443
|
+
* surface is unknown (the routes' located-check — covers extension
|
|
444
|
+
* views/panels AND settings surfaces, one source). A throwing
|
|
445
|
+
* `instanceKey` propagates (the extension-fault 500 path).
|
|
446
|
+
*/
|
|
447
|
+
instanceKeyFor(slug, id, params, principal) {
|
|
448
|
+
const located = this.locate(slug, id);
|
|
449
|
+
if (located === undefined)
|
|
450
|
+
return undefined;
|
|
451
|
+
return this.keyFor(located.def, params, principal);
|
|
452
|
+
}
|
|
359
453
|
/**
|
|
360
454
|
* Deliver one upload (spec §7.1/§7.2): the bytes were capped at the
|
|
361
455
|
* route (field `maxBytes` clamped by `server.maxUploadBytes`); the
|
|
@@ -476,6 +570,78 @@ export class SurfaceService {
|
|
|
476
570
|
});
|
|
477
571
|
return state.rev;
|
|
478
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Record one chat-block change (spec §8.2: chat data is shared by
|
|
575
|
+
* everyone who can see the conversation, so ops are kept like
|
|
576
|
+
* `sharedData` surfaces). Only the chat surface ROUTES call this —
|
|
577
|
+
* the core handler itself publishes the Task-1 `surface.changed`
|
|
578
|
+
* seam event and never touches this table, and an `update_blocks`
|
|
579
|
+
* agent edit likewise publishes ONLY the seam event (never an
|
|
580
|
+
* announceChatRev), so after an agent edit the in-memory rev no
|
|
581
|
+
* longer continues the recorded sequence and the rev-match guard
|
|
582
|
+
* below forces the client's next fetch to a full-data answer.
|
|
583
|
+
* `rev` is the DURABLE family's revision — the chat block's source
|
|
584
|
+
* of truth — so the table merely mirrors it: a rev that does not
|
|
585
|
+
* continue the recorded sequence (or a restart resetting memory)
|
|
586
|
+
* breaks the replay window's contiguity, and the window RESETS
|
|
587
|
+
* rather than ever answering a fetch across a missing rev.
|
|
588
|
+
*/
|
|
589
|
+
announceChatRev(address, rev, ops) {
|
|
590
|
+
const { slug, id, key } = partsOf(address);
|
|
591
|
+
const state = this.stateFor(slug, id, key);
|
|
592
|
+
if (ops !== undefined && rev === state.rev + 1) {
|
|
593
|
+
state.rev = rev;
|
|
594
|
+
state.ops.push({ rev, ops: [...ops.ops] });
|
|
595
|
+
while (state.ops.length > SHARED_OPS_WINDOW)
|
|
596
|
+
state.ops.shift();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// An ops-less announcement, or a rev that skips: the window is no
|
|
600
|
+
// longer contiguous from any `since` — drop it; the next contiguous
|
|
601
|
+
// recorded action rebuilds it.
|
|
602
|
+
if (rev > state.rev)
|
|
603
|
+
state.rev = rev;
|
|
604
|
+
state.ops = [];
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The ops since one rev for a chat block (the `data?since=` answer):
|
|
608
|
+
* the kept ops when the window still covers `since` AND the in-memory
|
|
609
|
+
* rev matches the durable one the caller read (a stale table serves
|
|
610
|
+
* nothing — the caller answers the full data). `undefined` otherwise.
|
|
611
|
+
*/
|
|
612
|
+
chatOpsSince(address, since, currentRev) {
|
|
613
|
+
const { slug, id, key } = partsOf(address);
|
|
614
|
+
const state = this.instances.get(`${slug}\u0001${id}\u0001${key}`);
|
|
615
|
+
if (state === undefined)
|
|
616
|
+
return undefined;
|
|
617
|
+
if (state.rev !== currentRev)
|
|
618
|
+
return undefined;
|
|
619
|
+
if (since < state.rev - state.ops.length || since > state.rev) {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
rev: state.rev,
|
|
624
|
+
ops: state.ops
|
|
625
|
+
.filter((entry) => entry.rev > since)
|
|
626
|
+
.flatMap((entry) => entry.ops),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/** One chat action's rate-limit token (the ext route's own gate). */
|
|
630
|
+
chatTakeToken(address, user) {
|
|
631
|
+
const { slug, id } = partsOf(address);
|
|
632
|
+
return this.rateLimit.take(slug, id, user);
|
|
633
|
+
}
|
|
634
|
+
/** The dedup lookup for one chat action (same table as ext actions). */
|
|
635
|
+
chatDedupCheck(actionId, address, fingerprint) {
|
|
636
|
+
const { slug, id, key } = partsOf(address);
|
|
637
|
+
const hit = this.dedup.check(actionId, `${slug}\u0001${id}\u0001${key}`, fingerprint);
|
|
638
|
+
return hit === undefined || hit === null ? hit : hit.result;
|
|
639
|
+
}
|
|
640
|
+
/** Record one chat action's first result (the dedup entry). */
|
|
641
|
+
chatDedupRecord(actionId, address, fingerprint, result) {
|
|
642
|
+
const { slug, id, key } = partsOf(address);
|
|
643
|
+
this.dedup.record(actionId, `${slug}\u0001${id}\u0001${key}`, fingerprint, result);
|
|
644
|
+
}
|
|
479
645
|
/** Call the surface's `render`, mapping throws to SurfaceFailedError. */
|
|
480
646
|
async callRender(located, key, params, principal) {
|
|
481
647
|
try {
|
|
@@ -487,6 +653,8 @@ export class SurfaceService {
|
|
|
487
653
|
});
|
|
488
654
|
}
|
|
489
655
|
catch (cause) {
|
|
656
|
+
if (cause instanceof NamedUserRequiredError)
|
|
657
|
+
throw cause;
|
|
490
658
|
throw new SurfaceFailedError(`surface ${located.slug}/${located.def.id} render() threw: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
491
659
|
}
|
|
492
660
|
}
|