pmx-canvas 0.1.28 → 0.1.30
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 +193 -0
- package/Readme.md +20 -10
- package/dist/canvas/global.css +13 -0
- package/dist/canvas/index.js +80 -163
- package/dist/canvas/surface-theme.css +142 -0
- package/dist/json-render/index.js +103 -103
- package/dist/types/client/nodes/HtmlNode.d.ts +0 -7
- package/dist/types/client/nodes/ax-node-actions.d.ts +18 -0
- package/dist/types/client/nodes/surface-url.d.ts +22 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +17 -0
- package/dist/types/json-render/renderer/index.d.ts +2 -0
- package/dist/types/json-render/schema.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +47 -0
- package/dist/types/server/ax-interaction.d.ts +210 -0
- package/dist/types/server/ax-state.d.ts +67 -1
- package/dist/types/server/canvas-db.d.ts +4 -0
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +47 -2
- package/dist/types/server/html-surface.d.ts +40 -0
- package/dist/types/server/index.d.ts +56 -2
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/placement.d.ts +1 -1
- package/dist/types/shared/surface.d.ts +19 -0
- package/docs/cli.md +30 -0
- package/docs/http-api.md +55 -0
- package/docs/mcp.md +40 -2
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-004-pmx-ax-primitives.md +623 -394
- package/docs/sdk.md +20 -0
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +107 -9
- package/src/cli/agent.ts +190 -0
- package/src/client/canvas/CanvasNode.tsx +8 -4
- package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -0
- package/src/client/nodes/ContextNode.tsx +17 -0
- package/src/client/nodes/ExtAppFrame.tsx +40 -3
- package/src/client/nodes/FileNode.tsx +26 -0
- package/src/client/nodes/HtmlNode.tsx +60 -188
- package/src/client/nodes/LedgerNode.tsx +39 -5
- package/src/client/nodes/McpAppNode.tsx +47 -2
- package/src/client/nodes/StatusNode.tsx +20 -0
- package/src/client/nodes/ax-node-actions.ts +39 -0
- package/src/client/nodes/surface-url.ts +48 -0
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/intent-bridge.ts +33 -0
- package/src/client/theme/global.css +13 -0
- package/src/client/theme/surface-theme.css +142 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/json-render/schema.ts +4 -0
- package/src/json-render/server.ts +31 -1
- package/src/mcp/canvas-access.ts +212 -1
- package/src/mcp/server.ts +238 -5
- package/src/server/ax-context.ts +3 -0
- package/src/server/ax-interaction.ts +549 -0
- package/src/server/ax-state.ts +188 -2
- package/src/server/canvas-db.ts +20 -0
- package/src/server/canvas-operations.ts +11 -0
- package/src/server/canvas-serialization.ts +9 -0
- package/src/server/canvas-state.ts +177 -16
- package/src/server/html-surface.ts +170 -0
- package/src/server/index.ts +105 -1
- package/src/server/mutation-history.ts +5 -0
- package/src/server/placement.ts +5 -1
- package/src/server/server.ts +305 -0
- package/src/shared/surface.ts +38 -0
package/src/server/server.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
ListToolsResult,
|
|
49
49
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
50
50
|
import { type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
51
|
+
import { buildHtmlSurfaceDocument, HTML_SURFACE_SANDBOX, normalizeSurfaceTheme } from './html-surface.js';
|
|
51
52
|
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
52
53
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
53
54
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
@@ -77,8 +78,10 @@ import {
|
|
|
77
78
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
78
79
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
79
80
|
import { buildCanvasAxContext } from './ax-context.js';
|
|
81
|
+
import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
80
82
|
import { isAxEventKind, isAxEvidenceKind } from './ax-state.js';
|
|
81
83
|
import type {
|
|
84
|
+
PmxAxPolicy,
|
|
82
85
|
PmxAxReviewAnchorType,
|
|
83
86
|
PmxAxReviewKind,
|
|
84
87
|
PmxAxReviewRegion,
|
|
@@ -1376,6 +1379,108 @@ function handleFrameDocument(pathname: string): Response {
|
|
|
1376
1379
|
});
|
|
1377
1380
|
}
|
|
1378
1381
|
|
|
1382
|
+
// ── Node surfaces ("Open as site") ─────────────────────────────
|
|
1383
|
+
//
|
|
1384
|
+
// One stable, node-addressable URL — /api/canvas/surface/:nodeId — that serves
|
|
1385
|
+
// (or redirects to) the exact same rendered surface a node shows in the canvas.
|
|
1386
|
+
// The in-canvas html iframe points at this URL too, so there is one render path.
|
|
1387
|
+
//
|
|
1388
|
+
// Served document types (html / ext-app) carry the same opaque-origin posture as
|
|
1389
|
+
// frame documents: `Content-Security-Policy: sandbox <tokens>` (no
|
|
1390
|
+
// allow-same-origin) means author scripts cannot reach the PMX origin even when
|
|
1391
|
+
// the page is opened top-level in a browser tab. Other types redirect to the
|
|
1392
|
+
// route that already renders them standalone.
|
|
1393
|
+
|
|
1394
|
+
function surfaceHtmlResponse(html: string, sandbox: string): Response {
|
|
1395
|
+
return new Response(html, {
|
|
1396
|
+
headers: {
|
|
1397
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1398
|
+
'Cache-Control': 'no-store',
|
|
1399
|
+
'Content-Security-Policy': `sandbox ${sandbox}`,
|
|
1400
|
+
'Referrer-Policy': 'no-referrer',
|
|
1401
|
+
'X-Content-Type-Options': 'nosniff',
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function surfaceRedirect(target: string): Response {
|
|
1407
|
+
return new Response(null, { status: 302, headers: { Location: target, 'Cache-Control': 'no-store' } });
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Permit only absolute http(s) URLs and root-relative same-origin paths. Blocks
|
|
1411
|
+
// `javascript:`/`data:` and protocol-relative `//host` open-redirects.
|
|
1412
|
+
function isSafeSurfaceRedirect(target: string): boolean {
|
|
1413
|
+
if (/^https?:\/\//i.test(target)) return true;
|
|
1414
|
+
if (/^\/(?!\/)/.test(target)) return true;
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function handleNodeSurface(pathname: string, url: URL): Response {
|
|
1419
|
+
const nodeId = decodeURIComponent(pathname.slice('/api/canvas/surface/'.length));
|
|
1420
|
+
if (!nodeId) return responseText('Missing node id', 400);
|
|
1421
|
+
const node = canvasState.getNode(nodeId);
|
|
1422
|
+
if (!node) return responseText('Node not found', 404);
|
|
1423
|
+
|
|
1424
|
+
const theme = normalizeSurfaceTheme(url.searchParams.get('theme'));
|
|
1425
|
+
|
|
1426
|
+
if (node.type === 'html') {
|
|
1427
|
+
const html = typeof node.data.html === 'string'
|
|
1428
|
+
? node.data.html
|
|
1429
|
+
: typeof node.data.content === 'string'
|
|
1430
|
+
? node.data.content
|
|
1431
|
+
: '';
|
|
1432
|
+
if (!html) return responseText('HTML node has no content', 404);
|
|
1433
|
+
const present = url.searchParams.get('present') === '1';
|
|
1434
|
+
const axCaps = resolveNodeAxCapabilities(node);
|
|
1435
|
+
const doc = buildHtmlSurfaceDocument(html, {
|
|
1436
|
+
theme,
|
|
1437
|
+
themeToken: url.searchParams.get('themeToken') ?? undefined,
|
|
1438
|
+
presentation: present,
|
|
1439
|
+
presentationExitToken: url.searchParams.get('presentToken') ?? undefined,
|
|
1440
|
+
axBridge: axCaps.enabled && axCaps.allowed.length > 0,
|
|
1441
|
+
axToken: url.searchParams.get('axToken') ?? undefined,
|
|
1442
|
+
nodeId: node.id,
|
|
1443
|
+
});
|
|
1444
|
+
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (node.type === 'json-render' || node.type === 'graph') {
|
|
1448
|
+
const params = new URLSearchParams({ nodeId, theme });
|
|
1449
|
+
const display = url.searchParams.get('display');
|
|
1450
|
+
if (display === 'expanded') params.set('display', 'expanded');
|
|
1451
|
+
return surfaceRedirect(`/api/canvas/json-render/view?${params.toString()}`);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (node.type === 'mcp-app') {
|
|
1455
|
+
// Bundled web artifact — same standalone page the canvas iframe already loads.
|
|
1456
|
+
if (node.data.viewerType === 'web-artifact' && typeof node.data.path === 'string' && node.data.path) {
|
|
1457
|
+
return surfaceRedirect(`/artifact?path=${encodeURIComponent(node.data.path)}`);
|
|
1458
|
+
}
|
|
1459
|
+
// Hosted ext-app — serve the same prepared HTML the in-canvas frame receives.
|
|
1460
|
+
// The app's host bridge has no peer in a standalone tab, so interactive
|
|
1461
|
+
// tool-calls won't function there; the UI still renders. Served TOP-LEVEL with
|
|
1462
|
+
// a tighter sandbox than the in-canvas iframe (no allow-popups-to-escape-sandbox)
|
|
1463
|
+
// since this is untrusted third-party HTML opened as its own page.
|
|
1464
|
+
if (node.data.mode === 'ext-app' && typeof node.data.html === 'string' && node.data.html) {
|
|
1465
|
+
return surfaceHtmlResponse(node.data.html, HTML_SURFACE_SANDBOX);
|
|
1466
|
+
}
|
|
1467
|
+
// URL-backed viewer — hand off to its own origin.
|
|
1468
|
+
if (typeof node.data.url === 'string' && isSafeSurfaceRedirect(node.data.url)) {
|
|
1469
|
+
return surfaceRedirect(node.data.url);
|
|
1470
|
+
}
|
|
1471
|
+
return responseText('MCP app node has no openable surface', 404);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (node.type === 'webpage') {
|
|
1475
|
+
if (typeof node.data.url === 'string' && isSafeSurfaceRedirect(node.data.url)) {
|
|
1476
|
+
return surfaceRedirect(node.data.url);
|
|
1477
|
+
}
|
|
1478
|
+
return responseText('Webpage node has no url', 404);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return responseText('Node type cannot be opened as a site', 404);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1379
1484
|
async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
1380
1485
|
const body = await readJson(req);
|
|
1381
1486
|
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
@@ -1542,6 +1647,9 @@ function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
|
1542
1647
|
ok: true,
|
|
1543
1648
|
node: serialized,
|
|
1544
1649
|
...serialized,
|
|
1650
|
+
// `nodeId` aliases `id` so HTTP/CLI node-create responses match the MCP
|
|
1651
|
+
// createdNodePayload — agents using either key (or a cached schema) work.
|
|
1652
|
+
nodeId: node.id,
|
|
1545
1653
|
};
|
|
1546
1654
|
}
|
|
1547
1655
|
|
|
@@ -2423,12 +2531,14 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
2423
2531
|
const devtoolsEnabled =
|
|
2424
2532
|
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
|
|
2425
2533
|
url.searchParams.get('devtools') === '1';
|
|
2534
|
+
const axToken = url.searchParams.get('axToken');
|
|
2426
2535
|
const html = await buildJsonRenderViewerHtml({
|
|
2427
2536
|
title,
|
|
2428
2537
|
spec,
|
|
2429
2538
|
...(theme ? { theme } : {}),
|
|
2430
2539
|
...(url.searchParams.get('display') === 'expanded' ? { display: 'expanded' as const } : {}),
|
|
2431
2540
|
...(devtoolsEnabled ? { devtools: true } : {}),
|
|
2541
|
+
...(axToken ? { nodeId, axToken } : {}),
|
|
2432
2542
|
});
|
|
2433
2543
|
return new Response(html, {
|
|
2434
2544
|
headers: {
|
|
@@ -3713,6 +3823,136 @@ function handleGetAxContext(): Response {
|
|
|
3713
3823
|
return responseJson(buildCanvasAxContext());
|
|
3714
3824
|
}
|
|
3715
3825
|
|
|
3826
|
+
async function handleAxInteraction(req: Request): Promise<Response> {
|
|
3827
|
+
const body = await readJson(req);
|
|
3828
|
+
const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
|
|
3829
|
+
for (const e of events) {
|
|
3830
|
+
broadcastWorkbenchEvent(e.event, {
|
|
3831
|
+
...e.payload,
|
|
3832
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3833
|
+
timestamp: new Date().toISOString(),
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
return responseJson(result, result.ok ? 200 : result.status);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
function handleAxDeliveryPending(url: URL): Response {
|
|
3840
|
+
const consumer = url.searchParams.get('consumer') ?? undefined;
|
|
3841
|
+
const limitRaw = Number(url.searchParams.get('limit') ?? '');
|
|
3842
|
+
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
|
|
3843
|
+
const pending = canvasState.getPendingSteering({
|
|
3844
|
+
...(consumer ? { consumer } : {}),
|
|
3845
|
+
...(limit ? { limit } : {}),
|
|
3846
|
+
});
|
|
3847
|
+
return responseJson({ ok: true, pending });
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
function handleAxDeliveryMark(id: string): Response {
|
|
3851
|
+
const delivered = canvasState.markSteeringDelivered(id);
|
|
3852
|
+
if (delivered) {
|
|
3853
|
+
broadcastWorkbenchEvent('ax-event-created', {
|
|
3854
|
+
steeringDelivered: id,
|
|
3855
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3856
|
+
timestamp: new Date().toISOString(),
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
return responseJson({ ok: true, delivered });
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
function handleAxElicitationList(): Response {
|
|
3863
|
+
return responseJson({ ok: true, elicitations: canvasState.getElicitations() });
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
async function handleAxElicitationRequest(req: Request): Promise<Response> {
|
|
3867
|
+
const body = await readJson(req);
|
|
3868
|
+
if (typeof body.prompt !== 'string' || !body.prompt.trim()) {
|
|
3869
|
+
return responseJson({ ok: false, error: 'elicitation requires a prompt.' }, 400);
|
|
3870
|
+
}
|
|
3871
|
+
const elicitation = canvasState.requestElicitation(
|
|
3872
|
+
{
|
|
3873
|
+
prompt: body.prompt,
|
|
3874
|
+
...(Array.isArray(body.fields) ? { fields: body.fields.filter((f: unknown): f is string => typeof f === 'string') } : {}),
|
|
3875
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
3876
|
+
},
|
|
3877
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3878
|
+
);
|
|
3879
|
+
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3880
|
+
return responseJson({ ok: true, elicitation });
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
async function handleAxElicitationRespond(req: Request, id: string): Promise<Response> {
|
|
3884
|
+
const body = await readJson(req);
|
|
3885
|
+
const response = isRecord(body.response) ? body.response : {};
|
|
3886
|
+
const elicitation = canvasState.respondElicitation(id, response, { source: normalizeAxSource(body.source, 'api') });
|
|
3887
|
+
if (!elicitation) return responseJson({ ok: false, error: 'elicitation not found or already answered.' }, 404);
|
|
3888
|
+
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3889
|
+
return responseJson({ ok: true, elicitation });
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function handleAxModeList(): Response {
|
|
3893
|
+
return responseJson({ ok: true, modeRequests: canvasState.getModeRequests() });
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
async function handleAxModeRequest(req: Request): Promise<Response> {
|
|
3897
|
+
const body = await readJson(req);
|
|
3898
|
+
if (body.mode !== 'plan' && body.mode !== 'execute' && body.mode !== 'autonomous') {
|
|
3899
|
+
return responseJson({ ok: false, error: 'mode request requires mode plan|execute|autonomous.' }, 400);
|
|
3900
|
+
}
|
|
3901
|
+
const modeRequest = canvasState.requestMode(
|
|
3902
|
+
{
|
|
3903
|
+
mode: body.mode,
|
|
3904
|
+
...(typeof body.reason === 'string' ? { reason: body.reason } : {}),
|
|
3905
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
3906
|
+
},
|
|
3907
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3908
|
+
);
|
|
3909
|
+
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3910
|
+
return responseJson({ ok: true, modeRequest });
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
async function handleAxModeResolve(req: Request, id: string): Promise<Response> {
|
|
3914
|
+
const body = await readJson(req);
|
|
3915
|
+
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
3916
|
+
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
3917
|
+
}
|
|
3918
|
+
const modeRequest = canvasState.resolveModeRequest(id, body.decision, {
|
|
3919
|
+
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
3920
|
+
source: normalizeAxSource(body.source, 'api'),
|
|
3921
|
+
});
|
|
3922
|
+
if (!modeRequest) return responseJson({ ok: false, error: 'mode request not found or already resolved.' }, 404);
|
|
3923
|
+
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3924
|
+
return responseJson({ ok: true, modeRequest });
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
function handleAxCommandList(): Response {
|
|
3928
|
+
return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
async function handleAxCommandInvoke(req: Request): Promise<Response> {
|
|
3932
|
+
const body = await readJson(req);
|
|
3933
|
+
if (typeof body.name !== 'string') {
|
|
3934
|
+
return responseJson({ ok: false, error: 'command requires a name.' }, 400);
|
|
3935
|
+
}
|
|
3936
|
+
const event = canvasState.invokeCommand(body.name, isRecord(body.args) ? body.args : null, { source: normalizeAxSource(body.source, 'api') });
|
|
3937
|
+
if (!event) return responseJson({ ok: false, error: `Unknown command "${body.name}".` }, 400);
|
|
3938
|
+
broadcastWorkbenchEvent('ax-event-created', { event, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3939
|
+
return responseJson({ ok: true, event });
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function handleAxPolicyGet(): Response {
|
|
3943
|
+
return responseJson({ ok: true, policy: canvasState.getPolicy() });
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
async function handleAxPolicySet(req: Request): Promise<Response> {
|
|
3947
|
+
const body = await readJson(req);
|
|
3948
|
+
const patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> } = {};
|
|
3949
|
+
if (isRecord(body.tools)) patch.tools = body.tools as Partial<PmxAxPolicy['tools']>;
|
|
3950
|
+
if (isRecord(body.prompt)) patch.prompt = body.prompt as Partial<PmxAxPolicy['prompt']>;
|
|
3951
|
+
const policy = canvasState.setPolicy(patch, { source: normalizeAxSource(body.source, 'api') });
|
|
3952
|
+
broadcastWorkbenchEvent('ax-state-changed', { policy, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
3953
|
+
return responseJson({ ok: true, policy });
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3716
3956
|
async function handleAxFocusUpdate(req: Request): Promise<Response> {
|
|
3717
3957
|
const body = await readJson(req);
|
|
3718
3958
|
const nodeIds = normalizeAxNodeIds(body.nodeIds);
|
|
@@ -4760,6 +5000,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4760
5000
|
return handleFrameDocument(url.pathname);
|
|
4761
5001
|
}
|
|
4762
5002
|
|
|
5003
|
+
if (url.pathname.startsWith('/api/canvas/surface/') && req.method === 'GET') {
|
|
5004
|
+
return handleNodeSurface(url.pathname, url);
|
|
5005
|
+
}
|
|
5006
|
+
|
|
4763
5007
|
if (url.pathname === '/' || url.pathname === '/workbench' || url.pathname === '/artifact') {
|
|
4764
5008
|
return new Response(canvasSpaHtml(), {
|
|
4765
5009
|
headers: {
|
|
@@ -5074,6 +5318,67 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5074
5318
|
return handleAxHostCapabilityReport(req);
|
|
5075
5319
|
}
|
|
5076
5320
|
|
|
5321
|
+
if (url.pathname === '/api/canvas/ax/interaction' && req.method === 'POST') {
|
|
5322
|
+
return handleAxInteraction(req);
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5325
|
+
if (url.pathname === '/api/canvas/ax/delivery/pending' && req.method === 'GET') {
|
|
5326
|
+
return handleAxDeliveryPending(url);
|
|
5327
|
+
}
|
|
5328
|
+
|
|
5329
|
+
if (url.pathname.startsWith('/api/canvas/ax/delivery/') && url.pathname.endsWith('/mark') && req.method === 'POST') {
|
|
5330
|
+
const deliveryId = decodeURIComponent(
|
|
5331
|
+
url.pathname.slice('/api/canvas/ax/delivery/'.length, -'/mark'.length),
|
|
5332
|
+
);
|
|
5333
|
+
return handleAxDeliveryMark(deliveryId);
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5336
|
+
if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'GET') {
|
|
5337
|
+
return handleAxElicitationList();
|
|
5338
|
+
}
|
|
5339
|
+
|
|
5340
|
+
if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'POST') {
|
|
5341
|
+
return handleAxElicitationRequest(req);
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
if (url.pathname.startsWith('/api/canvas/ax/elicitation/') && url.pathname.endsWith('/respond') && req.method === 'POST') {
|
|
5345
|
+
const elicitationId = decodeURIComponent(
|
|
5346
|
+
url.pathname.slice('/api/canvas/ax/elicitation/'.length, -'/respond'.length),
|
|
5347
|
+
);
|
|
5348
|
+
return handleAxElicitationRespond(req, elicitationId);
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
|
|
5352
|
+
return handleAxModeList();
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5355
|
+
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'POST') {
|
|
5356
|
+
return handleAxModeRequest(req);
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
if (url.pathname.startsWith('/api/canvas/ax/mode/') && url.pathname.endsWith('/resolve') && req.method === 'POST') {
|
|
5360
|
+
const modeId = decodeURIComponent(
|
|
5361
|
+
url.pathname.slice('/api/canvas/ax/mode/'.length, -'/resolve'.length),
|
|
5362
|
+
);
|
|
5363
|
+
return handleAxModeResolve(req, modeId);
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
|
|
5367
|
+
return handleAxCommandList();
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
if (url.pathname === '/api/canvas/ax/command' && req.method === 'POST') {
|
|
5371
|
+
return handleAxCommandInvoke(req);
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
if (url.pathname === '/api/canvas/ax/policy' && req.method === 'GET') {
|
|
5375
|
+
return handleAxPolicyGet();
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
if (url.pathname === '/api/canvas/ax/policy' && req.method === 'POST') {
|
|
5379
|
+
return handleAxPolicySet(req);
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5077
5382
|
// Spatial context API
|
|
5078
5383
|
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
5079
5384
|
const layout = canvasState.getLayout();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-cutting helpers for node "surfaces" — the standalone documents served at
|
|
3
|
+
* /api/canvas/surface/:nodeId. Shared by the server route, the server-side node
|
|
4
|
+
* serialization, and the client so the openability rule lives in exactly one
|
|
5
|
+
* place. The authoritative dispatch (serve vs redirect) lives in the server's
|
|
6
|
+
* handleNodeSurface; this predicate mirrors which node types it can open.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether a node has an "Open as site" surface. Coarse by design (presence of the
|
|
11
|
+
* minimal field) — the route itself decides what to actually serve.
|
|
12
|
+
*/
|
|
13
|
+
export function canOpenNodeAsSurface(type: string, data: Record<string, unknown>): boolean {
|
|
14
|
+
switch (type) {
|
|
15
|
+
case 'html':
|
|
16
|
+
return (typeof data.html === 'string' && data.html.length > 0)
|
|
17
|
+
|| (typeof data.content === 'string' && data.content.length > 0);
|
|
18
|
+
case 'json-render':
|
|
19
|
+
case 'graph':
|
|
20
|
+
return true;
|
|
21
|
+
case 'mcp-app':
|
|
22
|
+
return (data.viewerType === 'web-artifact' && typeof data.path === 'string' && data.path.length > 0)
|
|
23
|
+
|| (data.mode === 'ext-app' && typeof data.html === 'string' && data.html.length > 0)
|
|
24
|
+
|| (typeof data.url === 'string' && data.url.length > 0);
|
|
25
|
+
case 'webpage':
|
|
26
|
+
return typeof data.url === 'string' && data.url.length > 0;
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CSP sandbox tokens for a hosted ext-app surface, used both as the in-canvas
|
|
34
|
+
* iframe `sandbox` attribute and as the `Content-Security-Policy: sandbox` value
|
|
35
|
+
* when the surface route serves an ext-app standalone. All tokens are in the
|
|
36
|
+
* server's safe-sandbox allowlist; notably NOT allow-same-origin.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_EXT_APP_SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox';
|