clay-server 4.0.0-beta.9 → 4.0.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/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The mandatory declarative floor element of a Capsule's Display set.
|
|
2
|
+
//
|
|
3
|
+
// A Capsule is Logic, Skills, and Display. Display carries no packaging duty
|
|
4
|
+
// towards an agent (no Mate ever reads it), but it does carry the human's
|
|
5
|
+
// sovereignty: every Capsule must ship one element the host can always render,
|
|
6
|
+
// so the Capsule stays fully operable by hand with no AI in the path. That
|
|
7
|
+
// element is the validated declarative tree, and it is the floor.
|
|
8
|
+
//
|
|
9
|
+
// Two gates follow from that, and they are gates rather than aspirations:
|
|
10
|
+
// 1. Registration refuses a Capsule whose Display set lacks the floor.
|
|
11
|
+
// 2. Skills go dark when the floor does. A Capsule whose floor is
|
|
12
|
+
// unavailable leaves the catalog and the control surface a Mate sees, at
|
|
13
|
+
// the same moment the human loses it.
|
|
14
|
+
//
|
|
15
|
+
// The only question either gate asks is the one tool-ui-spec answers: does the
|
|
16
|
+
// Capsule have a declarative tree, and does that tree validate? A tree that is
|
|
17
|
+
// merely sparse is still a floor. Nothing here reads a stored verdict, because
|
|
18
|
+
// a stored verdict is a claim rather than the Display itself; the gate always
|
|
19
|
+
// validates the actual tree.
|
|
20
|
+
|
|
21
|
+
var toolUiSpec = require("./tool-ui-spec");
|
|
22
|
+
|
|
23
|
+
var FLOOR_ELEMENT = "declarative";
|
|
24
|
+
|
|
25
|
+
// Fails closed: a scan error, a missing ID, a missing tree, or a tree that
|
|
26
|
+
// does not validate all mean the same thing to a Mate, which is no Capsule.
|
|
27
|
+
function hasUsableFloor(manifest, uiTree) {
|
|
28
|
+
if (!manifest || manifest.error || !manifest.id) return false;
|
|
29
|
+
var tree = uiTree === undefined ? manifest.uiTree : uiTree;
|
|
30
|
+
if (tree === undefined || tree === null) return false;
|
|
31
|
+
try {
|
|
32
|
+
toolUiSpec.validateUiTreeForManifest(tree, manifest);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertFloor(uiTree, manifest) {
|
|
40
|
+
if (uiTree === undefined || uiTree === null) {
|
|
41
|
+
throw new Error("Capsule Display must include the declarative floor element.");
|
|
42
|
+
}
|
|
43
|
+
toolUiSpec.validateUiTreeForManifest(uiTree, manifest);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
FLOOR_ELEMENT: FLOOR_ELEMENT,
|
|
49
|
+
hasUsableFloor: hasUsableFloor,
|
|
50
|
+
assertFloor: assertFloor,
|
|
51
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Isolated host for a Capsule's opt-in rich Display element.
|
|
2
|
+
//
|
|
3
|
+
// The rich element is additive Display: arbitrary rendering (canvas, WebGL)
|
|
4
|
+
// with zero authority. It runs in an iframe that is sandboxed with
|
|
5
|
+
// allow-scripts only and served from this dedicated listener, a distinct
|
|
6
|
+
// origin from the app, so nothing of Clay's DOM, cookies, or storage is
|
|
7
|
+
// reachable. Its one outward edge is postMessage to its embedder, and the
|
|
8
|
+
// only thing it can ask for is the same validated act a human click requests.
|
|
9
|
+
//
|
|
10
|
+
// The frame document carries its own CSP, strictly tighter than the app
|
|
11
|
+
// policy: default-src 'none' with a per-response script nonce. There is no
|
|
12
|
+
// connect-src at all, so the frame cannot fetch, beacon, or open sockets; a
|
|
13
|
+
// rich element ships self-contained. The nonce authorizes exactly two
|
|
14
|
+
// scripts: the inline bridge below and the Capsule's own display.js.
|
|
15
|
+
//
|
|
16
|
+
// This origin serves no cookies and knows no sessions. A frame URL is gated
|
|
17
|
+
// by a short-lived token issued over the user's authenticated WebSocket, and
|
|
18
|
+
// each token admits one shell fetch and one display.js fetch. Whatever state
|
|
19
|
+
// the host later pushes into the frame is the entire exfiltration surface,
|
|
20
|
+
// chosen per element, exactly like a snapshot projection is chosen per caller.
|
|
21
|
+
|
|
22
|
+
var crypto = require("crypto");
|
|
23
|
+
var fs = require("fs");
|
|
24
|
+
var http = require("http");
|
|
25
|
+
var https = require("https");
|
|
26
|
+
var path = require("path");
|
|
27
|
+
var toolsRegistry = require("./tools-registry");
|
|
28
|
+
|
|
29
|
+
var TOKEN_TTL_MS = 60000;
|
|
30
|
+
var DISPLAY_FILE = "display.js";
|
|
31
|
+
var MAX_DISPLAY_BYTES = 512 * 1024;
|
|
32
|
+
|
|
33
|
+
function createCapsuleFrameServer(opts) {
|
|
34
|
+
opts = opts || {};
|
|
35
|
+
var tlsOptions = opts.tlsOptions || null;
|
|
36
|
+
var tokens = Object.create(null);
|
|
37
|
+
var server = null;
|
|
38
|
+
var listening = null;
|
|
39
|
+
|
|
40
|
+
function pruneTokens(now) {
|
|
41
|
+
var keys = Object.keys(tokens);
|
|
42
|
+
for (var i = 0; i < keys.length; i++) {
|
|
43
|
+
if (tokens[keys[i]].expiresAt <= now) delete tokens[keys[i]];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function displayPathFor(ctx, toolId) {
|
|
48
|
+
toolsRegistry.validateToolId(toolId);
|
|
49
|
+
return path.join(toolsRegistry.resolveToolsRoot(ctx), toolId, DISPLAY_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasRichDisplay(ctx, toolId) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.existsSync(displayPathFor(ctx, toolId));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureListening() {
|
|
61
|
+
if (listening) return listening;
|
|
62
|
+
server = tlsOptions ? https.createServer(tlsOptions, handleRequest) : http.createServer(handleRequest);
|
|
63
|
+
listening = new Promise(function (resolve, reject) {
|
|
64
|
+
server.once("error", reject);
|
|
65
|
+
server.listen(opts.port || 0, function () {
|
|
66
|
+
resolve(server.address().port);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return listening;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Issues the one-time frame URL for a Capsule that ships a rich element.
|
|
73
|
+
// Callable only from the authenticated WebSocket side, which is what binds
|
|
74
|
+
// the anonymous frame origin to a specific user's tools root.
|
|
75
|
+
function issueFrameUrl(ctx, toolId) {
|
|
76
|
+
if (!hasRichDisplay(ctx, toolId)) {
|
|
77
|
+
return Promise.reject(new Error("This Capsule ships no rich Display element."));
|
|
78
|
+
}
|
|
79
|
+
return ensureListening().then(function (port) {
|
|
80
|
+
pruneTokens(Date.now());
|
|
81
|
+
var token = crypto.randomBytes(24).toString("hex");
|
|
82
|
+
tokens[token] = {
|
|
83
|
+
ctx: { userId: ctx.userId, multiUser: ctx.multiUser, linuxUser: ctx.linuxUser },
|
|
84
|
+
toolId: toolId,
|
|
85
|
+
expiresAt: Date.now() + TOKEN_TTL_MS,
|
|
86
|
+
shell: true,
|
|
87
|
+
display: true,
|
|
88
|
+
};
|
|
89
|
+
return { port: port, path: "/capsule/?t=" + token, secure: !!tlsOptions };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function consumeToken(token, use) {
|
|
94
|
+
if (typeof token !== "string" || !token) return null;
|
|
95
|
+
var entry = tokens[token];
|
|
96
|
+
if (!entry) return null;
|
|
97
|
+
if (entry.expiresAt <= Date.now()) {
|
|
98
|
+
delete tokens[token];
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!entry[use]) return null;
|
|
102
|
+
entry[use] = false;
|
|
103
|
+
if (!entry.shell && !entry.display) delete tokens[token];
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deny(res, code) {
|
|
108
|
+
res.writeHead(code, { "Content-Type": "text/plain; charset=utf-8", "X-Content-Type-Options": "nosniff" });
|
|
109
|
+
res.end(code === 404 ? "Not found" : "Forbidden");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The in-frame half of the bridge. Its only capabilities are receiving
|
|
113
|
+
// state/event pushes from the embedder and asking the embedder for an act.
|
|
114
|
+
// The embedder routes that request into the same pipeline a floor button
|
|
115
|
+
// uses; nothing here can mutate anything directly.
|
|
116
|
+
function bridgeSource() {
|
|
117
|
+
return [
|
|
118
|
+
"(function () {",
|
|
119
|
+
" \"use strict\";",
|
|
120
|
+
" var api = {",
|
|
121
|
+
" onState: null,",
|
|
122
|
+
" onEvent: null,",
|
|
123
|
+
" act: function (actionId, args) {",
|
|
124
|
+
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"act\", actionId: String(actionId), args: args || {} }, \"*\");",
|
|
125
|
+
" },",
|
|
126
|
+
" };",
|
|
127
|
+
" window.ClayCapsule = api;",
|
|
128
|
+
" window.addEventListener(\"message\", function (event) {",
|
|
129
|
+
" if (event.source !== window.parent) return;",
|
|
130
|
+
" var data = event.data;",
|
|
131
|
+
" if (!data || data.clayCapsuleFrame !== 1) return;",
|
|
132
|
+
" if (data.type === \"state\" && typeof api.onState === \"function\") api.onState(data.state || {});",
|
|
133
|
+
" else if (data.type === \"event\" && typeof api.onEvent === \"function\") api.onEvent(data.event || null);",
|
|
134
|
+
" });",
|
|
135
|
+
" // The host owns the iframe box, so the frame reports its natural",
|
|
136
|
+
" // content height and the host resizes the box to fit. Without this",
|
|
137
|
+
" // the box stays fixed and a widening layout clips the element.",
|
|
138
|
+
" var reportedHeight = 0;",
|
|
139
|
+
" var reportQueued = false;",
|
|
140
|
+
" function reportSize() {",
|
|
141
|
+
" reportQueued = false;",
|
|
142
|
+
" var root = document.documentElement;",
|
|
143
|
+
" var body = document.body;",
|
|
144
|
+
" var height = Math.ceil(Math.max(root ? root.scrollHeight : 0, body ? body.scrollHeight : 0));",
|
|
145
|
+
" if (!height || height === reportedHeight) return;",
|
|
146
|
+
" reportedHeight = height;",
|
|
147
|
+
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"size\", height: height }, \"*\");",
|
|
148
|
+
" }",
|
|
149
|
+
" function queueReport() {",
|
|
150
|
+
" if (reportQueued) return;",
|
|
151
|
+
" reportQueued = true;",
|
|
152
|
+
" if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(reportSize);",
|
|
153
|
+
" else setTimeout(reportSize, 50);",
|
|
154
|
+
" }",
|
|
155
|
+
" if (typeof ResizeObserver === \"function\") {",
|
|
156
|
+
" new ResizeObserver(queueReport).observe(document.documentElement);",
|
|
157
|
+
" if (document.body) new ResizeObserver(queueReport).observe(document.body);",
|
|
158
|
+
" }",
|
|
159
|
+
" window.addEventListener(\"resize\", queueReport);",
|
|
160
|
+
" window.addEventListener(\"load\", function () {",
|
|
161
|
+
" window.parent.postMessage({ clayCapsuleFrame: 1, type: \"ready\" }, \"*\");",
|
|
162
|
+
" queueReport();",
|
|
163
|
+
" });",
|
|
164
|
+
"})();",
|
|
165
|
+
].join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shellHtml(nonce, token) {
|
|
169
|
+
return [
|
|
170
|
+
"<!doctype html>",
|
|
171
|
+
"<html>",
|
|
172
|
+
"<head>",
|
|
173
|
+
"<meta charset=\"utf-8\">",
|
|
174
|
+
"<title>Capsule Display</title>",
|
|
175
|
+
"</head>",
|
|
176
|
+
"<body>",
|
|
177
|
+
"<script nonce=\"" + nonce + "\">" + bridgeSource() + "</script>",
|
|
178
|
+
"<script nonce=\"" + nonce + "\" src=\"/capsule/display.js?t=" + token + "\"></script>",
|
|
179
|
+
"</body>",
|
|
180
|
+
"</html>",
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleRequest(req, res) {
|
|
185
|
+
if (req.method !== "GET") return deny(res, 404);
|
|
186
|
+
var parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = new URL(req.url, "http://frame.invalid");
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return deny(res, 404);
|
|
191
|
+
}
|
|
192
|
+
var token = parsed.searchParams.get("t");
|
|
193
|
+
if (parsed.pathname === "/capsule/" || parsed.pathname === "/capsule") {
|
|
194
|
+
var shellEntry = consumeToken(token, "shell");
|
|
195
|
+
if (!shellEntry) return deny(res, 403);
|
|
196
|
+
var nonce = crypto.randomBytes(16).toString("base64");
|
|
197
|
+
res.writeHead(200, {
|
|
198
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
199
|
+
// No connect-src, no default fallback: the frame cannot reach the
|
|
200
|
+
// network at all, and only the two nonced scripts may run. This is
|
|
201
|
+
// deliberately tighter than the app policy in every direction.
|
|
202
|
+
"Content-Security-Policy": "default-src 'none'; script-src 'nonce-" + nonce + "'; base-uri 'none'; form-action 'none'",
|
|
203
|
+
"X-Content-Type-Options": "nosniff",
|
|
204
|
+
"Referrer-Policy": "no-referrer",
|
|
205
|
+
"Cache-Control": "no-store",
|
|
206
|
+
});
|
|
207
|
+
res.end(shellHtml(nonce, token));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (parsed.pathname === "/capsule/display.js") {
|
|
211
|
+
var displayEntry = consumeToken(token, "display");
|
|
212
|
+
if (!displayEntry) return deny(res, 403);
|
|
213
|
+
var source;
|
|
214
|
+
try {
|
|
215
|
+
var filePath = displayPathFor(displayEntry.ctx, displayEntry.toolId);
|
|
216
|
+
if (fs.statSync(filePath).size > MAX_DISPLAY_BYTES) return deny(res, 403);
|
|
217
|
+
source = fs.readFileSync(filePath, "utf8");
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return deny(res, 404);
|
|
220
|
+
}
|
|
221
|
+
res.writeHead(200, {
|
|
222
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
223
|
+
"X-Content-Type-Options": "nosniff",
|
|
224
|
+
"Referrer-Policy": "no-referrer",
|
|
225
|
+
"Cache-Control": "no-store",
|
|
226
|
+
});
|
|
227
|
+
res.end(source);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
return deny(res, 404);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function close() {
|
|
234
|
+
if (!server) return Promise.resolve();
|
|
235
|
+
var closing = server;
|
|
236
|
+
server = null;
|
|
237
|
+
listening = null;
|
|
238
|
+
return new Promise(function (resolve) {
|
|
239
|
+
closing.close(function () { resolve(); });
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
issueFrameUrl: issueFrameUrl,
|
|
245
|
+
hasRichDisplay: hasRichDisplay,
|
|
246
|
+
ensureListening: ensureListening,
|
|
247
|
+
close: close,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
DISPLAY_FILE: DISPLAY_FILE,
|
|
253
|
+
TOKEN_TTL_MS: TOKEN_TTL_MS,
|
|
254
|
+
createCapsuleFrameServer: createCapsuleFrameServer,
|
|
255
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Bridges Capsule engagement events to the opponent Mate's session.
|
|
2
|
+
//
|
|
3
|
+
// This module knows no game. A Capsule's Logic declares on its causal event
|
|
4
|
+
// when the Mate should be engaged (`event.engage = {kind}`): "turn" asks the
|
|
5
|
+
// Mate to read the state and act, "start" asks it to acknowledge a fresh
|
|
6
|
+
// game without acting. The bridge only carries that declaration: it finds the
|
|
7
|
+
// opponent Mate's project and delivers a prompt through it, so the Capsule
|
|
8
|
+
// round-trips for every game alike: the human plays on the Display, the Mate
|
|
9
|
+
// wakes up, reads the state over clay_tool_snapshot, and acts through the
|
|
10
|
+
// same clay_tool_act pipeline. The prompt carries words only; it grants no
|
|
11
|
+
// authority the Mate does not already have, and a lost prompt costs a
|
|
12
|
+
// reminder, never the game. Only human-caused events engage, whatever Logic
|
|
13
|
+
// declares, so a Mate can never wake itself.
|
|
14
|
+
//
|
|
15
|
+
// The opponent is the Mate that last acted on this Capsule for this user.
|
|
16
|
+
// Before any Mate has acted, the built-in host Mate (Clay) takes the seat, so
|
|
17
|
+
// a fresh game is playable with zero setup.
|
|
18
|
+
|
|
19
|
+
function attachCapsuleMateTurn(deps) {
|
|
20
|
+
var users = deps.users;
|
|
21
|
+
var findMateProject = deps.findMateProject || null;
|
|
22
|
+
var broadcastToUser = deps.broadcastToUser || null;
|
|
23
|
+
var opponents = Object.create(null);
|
|
24
|
+
var nudgedSeqs = Object.create(null);
|
|
25
|
+
|
|
26
|
+
function keyFor(userId, toolId) {
|
|
27
|
+
return String(userId) + "\u0000" + String(toolId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Called when a Mate acts on a Capsule: that Mate holds the opponent seat
|
|
31
|
+
// for this user's game from then on.
|
|
32
|
+
function rememberOpponent(userId, toolId, mateId) {
|
|
33
|
+
if (typeof mateId === "string" && mateId) opponents[keyFor(userId, toolId)] = mateId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function turnPrompt(manifest) {
|
|
37
|
+
return "Capsule turn: it is your seat's turn in the \"" + manifest.name + "\" Capsule (toolId \"" + manifest.id + "\"). "
|
|
38
|
+
+ "Read the game with clay_tool_snapshot, decide your move, and take your turn with clay_tool_act. "
|
|
39
|
+
+ "Keep playing until the turn passes away from your seat, and say your reasoning in one or two short sentences as you play. "
|
|
40
|
+
+ "If an act is refused, read the snapshot again instead of retrying blindly.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startPrompt(manifest) {
|
|
44
|
+
return "Capsule game: the user just started a new game of \"" + manifest.name + "\" (toolId \"" + manifest.id + "\") against you. "
|
|
45
|
+
+ "The user moves first. Reply with one short line of table talk, and do not call clay_tool_act now: "
|
|
46
|
+
+ "you will be woken in this session whenever it is your turn.";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fire-and-forget from the act pipeline: delivers a Logic-declared
|
|
50
|
+
// engagement exactly once per event, and only for human-caused events.
|
|
51
|
+
function maybeNudge(userId, manifest, actor, event) {
|
|
52
|
+
if (!findMateProject || !manifest || !event) return Promise.resolve(null);
|
|
53
|
+
if (actor !== "human") return Promise.resolve(null);
|
|
54
|
+
var kind = event.engage && (event.engage.kind === "turn" || event.engage.kind === "start") ? event.engage.kind : null;
|
|
55
|
+
if (!kind) return Promise.resolve(null);
|
|
56
|
+
var key = keyFor(userId, manifest.id);
|
|
57
|
+
if (nudgedSeqs[key] !== undefined && event.seq <= nudgedSeqs[key]) return Promise.resolve(null);
|
|
58
|
+
nudgedSeqs[key] = event.seq;
|
|
59
|
+
return Promise.resolve().then(function () {
|
|
60
|
+
var mateId = opponents[key] || null;
|
|
61
|
+
// The mates registry keys single-user data off a null userId, exactly
|
|
62
|
+
// like the home chat surface does.
|
|
63
|
+
var mateUserId = users.isMultiUser() ? userId : null;
|
|
64
|
+
var found = findMateProject(mateUserId, mateId, true);
|
|
65
|
+
if (!found && mateId) found = findMateProject(mateUserId, null, true);
|
|
66
|
+
if (!found || !found.ctx || typeof found.ctx.deliverCapsuleTurn !== "function") {
|
|
67
|
+
console.error("[capsules] No Mate project could take the '" + manifest.id + "' turn; is a Mate installed?");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
var principal = { userId: users.isMultiUser() ? userId : null };
|
|
71
|
+
var mateId = found.mate && found.mate.id ? found.mate.id : null;
|
|
72
|
+
return found.ctx.deliverCapsuleTurn(principal, {
|
|
73
|
+
toolId: manifest.id,
|
|
74
|
+
toolName: manifest.name,
|
|
75
|
+
kind: kind,
|
|
76
|
+
text: kind === "turn" ? turnPrompt(manifest) : startPrompt(manifest),
|
|
77
|
+
}).then(function (delivered) {
|
|
78
|
+
// An explicit game start navigates the user's home board into the
|
|
79
|
+
// Mate's game session, so starting a game visibly opens the table.
|
|
80
|
+
if (broadcastToUser && delivered && delivered.reference && mateId) {
|
|
81
|
+
broadcastToUser(userId, {
|
|
82
|
+
type: "capsule_game_session",
|
|
83
|
+
toolId: manifest.id,
|
|
84
|
+
toolName: manifest.name,
|
|
85
|
+
mateId: mateId,
|
|
86
|
+
sessionId: delivered.reference,
|
|
87
|
+
kind: kind,
|
|
88
|
+
created: !!delivered.created,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return delivered;
|
|
92
|
+
});
|
|
93
|
+
}).catch(function (error) {
|
|
94
|
+
console.error("[capsules] Could not deliver the Mate's turn:", error && error.message ? error.message : error);
|
|
95
|
+
return null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
rememberOpponent: rememberOpponent,
|
|
101
|
+
maybeNudge: maybeNudge,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { attachCapsuleMateTurn: attachCapsuleMateTurn };
|