@rynx-ai/server 0.1.10 → 0.1.11-beta.10
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/dist/control-api.d.ts +17 -52
- package/dist/control-api.js +755 -341
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DgFZpTNw.js → highlighted-body-OFNGDK62-IdclXfSt.js} +1 -1
- package/dist/control-web/assets/index-5n6D-i-c.css +32 -0
- package/dist/control-web/assets/index-DhsO7WjD.js +709 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-BDikE3W1.js → mermaid-GHXKKRXX-BqMPrbIZ.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +4 -2
- package/dist/machine-session-service.d.ts +135 -27
- package/dist/machine-session-service.js +410 -58
- package/dist/remote-runtime-dispatcher.d.ts +12 -2
- package/dist/remote-runtime-dispatcher.js +56 -0
- package/dist/remote-runtime-session-projection.js +1 -0
- package/dist/server.d.ts +59 -29
- package/dist/server.js +158 -143
- package/dist/session-browser-service.d.ts +8 -0
- package/dist/session-browser-service.js +37 -20
- package/dist/session-portal.d.ts +42 -0
- package/dist/session-portal.js +746 -0
- package/dist/session-runtime-index.d.ts +7 -0
- package/dist/session-runtime-index.js +42 -6
- package/dist/session-terminal-host.d.ts +6 -0
- package/dist/session-terminal-host.js +56 -41
- package/dist/terminal-ws.js +8 -6
- package/package.json +7 -7
- package/dist/channel-manager.d.ts +0 -60
- package/dist/channel-manager.js +0 -106
- package/dist/control-web/assets/index-CnVtOmLv.css +0 -32
- package/dist/control-web/assets/index-Dlywgy58.js +0 -661
package/dist/control-api.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Control-plane HTTP surface: a JSON + SSE API (and the static console) for
|
|
3
|
-
* managing
|
|
4
|
-
* channel bound to an agent), plus declarative agents. Mounted by
|
|
3
|
+
* managing plugins, declarative agents, skills, and sessions. Mounted by
|
|
5
4
|
* {@link import("./server.js").createApp} only when control deps are injected.
|
|
6
5
|
*
|
|
7
|
-
* Channel and
|
|
8
|
-
*
|
|
9
|
-
* `/api/channels/:id/authorize`); credentials live in its `options`.
|
|
10
|
-
* - an **instance** binds a channel to an agent (`/api/instances`); it's what
|
|
11
|
-
* the daemon mounts and runs.
|
|
12
|
-
*
|
|
13
|
-
* The server is channel-agnostic: available channel *types* (with their declared
|
|
14
|
-
* config schema / authorize capability) come from {@link ControlPlaneDeps.listChannelTypes},
|
|
15
|
-
* which the daemon derives from loaded plugins. Plugins themselves are managed
|
|
16
|
-
* via the CLI, not here. The daemon owns the db-backed mutators (injected as
|
|
17
|
-
* {@link ControlPlaneDeps}); the server owns the {@link ChannelManager}.
|
|
6
|
+
* Channel configuration and instances are deliberately absent: each plugin
|
|
7
|
+
* owns that state and exposes its own management application.
|
|
18
8
|
*/
|
|
19
9
|
import { timingSafeEqual } from "node:crypto";
|
|
20
10
|
import Router from "@koa/router";
|
|
21
|
-
import {
|
|
22
|
-
import { DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, DAEMON_BROWSER_ARTIFACT_VERSION_PATH, DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonBrowserArtifactInstallInput, parseDaemonBrowserArtifactUpdateInput, parseDaemonChromeInspectionConfigureInput, parseDaemonCleanupSessionsInput, } from "@rynx-ai/protocol/control";
|
|
11
|
+
import { skillInstallRecipeSchema, } from "@rynx-ai/core";
|
|
12
|
+
import { DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, DAEMON_BROWSER_ARTIFACT_VERSION_PATH, DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_MAINTENANCE_LEASE_HEADER, DAEMON_MAINTENANCE_LEASE_RELEASE_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonBrowserArtifactInstallInput, parseDaemonBrowserArtifactUpdateInput, parseDaemonChromeInspectionConfigureInput, parseDaemonCleanupSessionsInput, } from "@rynx-ai/protocol/control";
|
|
23
13
|
import { DIRECT_RUNTIME_CONTROL_PATH, encodePairingCode, } from "@rynx-ai/protocol/direct-runtime";
|
|
24
|
-
import { PLUGIN_INSTALL_PREPARATIONS_PATH, parsePluginInstallCommitInput, parsePluginInstallPrepareInput, } from "@rynx-ai/protocol/plugin-management";
|
|
14
|
+
import { PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH, PLUGIN_CONSOLE_MARKETPLACES_PATH, PLUGIN_INSTALL_PREPARATIONS_PATH, PLUGIN_MARKETPLACES_PATH, parsePluginInstallCommitInput, parsePluginInstallPrepareInput, parsePluginMarketplaceAddInput, } from "@rynx-ai/protocol/plugin-management";
|
|
25
15
|
import { parseDaemonStatus } from "@rynx-ai/protocol/remote-runtime";
|
|
26
16
|
import { materializeRemoteRuntimeSessionEvent, materializeRemoteRuntimeSessionRuntimeSnapshot, parseRemoteRuntimeRpcRequest, REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES, SESSION_IMAGE_MEDIA_TYPES, SESSION_RESOURCE_MAX_IMAGE_BYTES, SESSION_RESOURCE_MAX_FILENAME_CHARS, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
27
17
|
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
28
18
|
import { parseRuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
29
|
-
import { ensureCodexResumeRollout } from "@rynx-ai/runtime";
|
|
30
19
|
import { encodeDaemonStatus } from "./remote-runtime.js";
|
|
31
20
|
import { SessionRuntimeIndex } from "./session-runtime-index.js";
|
|
32
21
|
import { authorizeRuntimeWebRequest, RUNTIME_WEB_CAPABILITY, RUNTIME_WEB_CAPABILITY_HEADER, } from "./runtime-web-auth.js";
|
|
@@ -40,6 +29,8 @@ const PLUGIN_CLI_TIMEOUT_MS = 120_000;
|
|
|
40
29
|
const PLUGIN_INSTALL_BODY_MAX_BYTES = 16 * 1024;
|
|
41
30
|
const PLUGIN_INSTALL_OPERATION_TIMEOUT_MS = 15 * 60_000;
|
|
42
31
|
const PLUGIN_INSTALL_ERROR_MAX_BYTES = 16 * 1024;
|
|
32
|
+
const PLUGIN_SETTINGS_UI_BODY_MAX_BYTES = 1024 * 1024;
|
|
33
|
+
const CANONICAL_PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}@[a-z0-9][a-z0-9-]{0,62}(?:\/[a-z0-9][a-z0-9._-]{0,99})?$/;
|
|
43
34
|
const BROWSER_ARTIFACT_BODY_MAX_BYTES = 4 * 1024;
|
|
44
35
|
const BROWSER_ARTIFACT_OPERATION_TIMEOUT_MS = 15 * 60_000;
|
|
45
36
|
const BROWSER_ARTIFACT_ERROR_MAX_BYTES = 16 * 1024;
|
|
@@ -74,7 +65,7 @@ const RUNTIME_BROWSER_RPC_METHODS = new Set([
|
|
|
74
65
|
"browser.page.reload",
|
|
75
66
|
]);
|
|
76
67
|
export function createControlRouter(opts) {
|
|
77
|
-
const {
|
|
68
|
+
const { deps, runtime, sessionBus, sessionLog, runnerManager, pluginRuntimeStatus, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeWebAccess, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, shutdownSignal, sessionRuntimeIndex = new SessionRuntimeIndex(), sessionLifecycle, runtimeLocalBrowser, providerClis, } = opts;
|
|
78
69
|
const router = new Router();
|
|
79
70
|
const mutateBrowserArtifact = async (ctx, operation) => {
|
|
80
71
|
const host = daemonManagement?.browserArtifacts;
|
|
@@ -137,8 +128,6 @@ export function createControlRouter(opts) {
|
|
|
137
128
|
invocation.dispose();
|
|
138
129
|
}
|
|
139
130
|
};
|
|
140
|
-
/** The instance (if any) bound to a channel — used to reload after a config change. */
|
|
141
|
-
const instanceForChannel = (channelId) => deps.listInstancesConfig().find((i) => i.channelId === channelId);
|
|
142
131
|
const listRuntimeTargets = async (ctx) => {
|
|
143
132
|
if (!runtimeTargetControl)
|
|
144
133
|
return notFound(ctx, "Runtime target control is not configured");
|
|
@@ -689,6 +678,37 @@ export function createControlRouter(opts) {
|
|
|
689
678
|
lease.release();
|
|
690
679
|
}
|
|
691
680
|
};
|
|
681
|
+
const runtimeSessionSnapshotPage = async (ctx) => {
|
|
682
|
+
const lease = await acquireRuntimeLease(ctx);
|
|
683
|
+
if (!lease)
|
|
684
|
+
return;
|
|
685
|
+
const sessionId = ctx.params.id;
|
|
686
|
+
const afterId = optionalRuntimeString(ctx.query.afterId);
|
|
687
|
+
if (ctx.query.afterId !== undefined && afterId === undefined) {
|
|
688
|
+
lease.release();
|
|
689
|
+
ctx.status = 400;
|
|
690
|
+
ctx.body = { error: "invalid_request" };
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const page = await lease.call("session.snapshot.get", {
|
|
695
|
+
sessionId,
|
|
696
|
+
...(afterId === undefined ? {} : { afterId }),
|
|
697
|
+
limit: REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE,
|
|
698
|
+
}, { timeoutMs: REMOTE_RUNTIME_SESSION_TIMEOUT_MS });
|
|
699
|
+
if (page.sessionId !== sessionId) {
|
|
700
|
+
throw runtimeProtocolError("Runtime Session snapshot identity changed");
|
|
701
|
+
}
|
|
702
|
+
ctx.set("Cache-Control", "no-store");
|
|
703
|
+
ctx.body = page;
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
runtimeTargetHostError(ctx, error);
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
lease.release();
|
|
710
|
+
}
|
|
711
|
+
};
|
|
692
712
|
const runtimeSessionInterrupt = async (ctx) => {
|
|
693
713
|
const outcome = await runtimeCall(ctx, "session.interrupt", { sessionId: ctx.params.id });
|
|
694
714
|
if (outcome.ok)
|
|
@@ -704,6 +724,27 @@ export function createControlRouter(opts) {
|
|
|
704
724
|
if (outcome.ok)
|
|
705
725
|
ctx.body = outcome.result;
|
|
706
726
|
};
|
|
727
|
+
const runtimeSessionExecutionSettings = async (ctx) => {
|
|
728
|
+
const outcome = await runtimeCall(ctx, "session.execution.get", {
|
|
729
|
+
sessionId: ctx.params.id,
|
|
730
|
+
});
|
|
731
|
+
if (outcome.ok)
|
|
732
|
+
ctx.body = outcome.result;
|
|
733
|
+
};
|
|
734
|
+
const runtimeSessionExecutionSettingsUpdate = async (ctx) => {
|
|
735
|
+
const body = await readRuntimeBody(ctx);
|
|
736
|
+
if (!body)
|
|
737
|
+
return;
|
|
738
|
+
if (!acceptRuntimeBodyKeys(ctx, body, ["model", "reasoningEffort"]))
|
|
739
|
+
return;
|
|
740
|
+
const outcome = await runtimeCall(ctx, "session.execution.update", {
|
|
741
|
+
sessionId: ctx.params.id,
|
|
742
|
+
model: body.model,
|
|
743
|
+
reasoningEffort: body.reasoningEffort,
|
|
744
|
+
});
|
|
745
|
+
if (outcome.ok)
|
|
746
|
+
ctx.body = outcome.result;
|
|
747
|
+
};
|
|
707
748
|
const runtimeSessionCreate = async (ctx) => {
|
|
708
749
|
const body = await readRuntimeBody(ctx);
|
|
709
750
|
if (!body)
|
|
@@ -712,6 +753,68 @@ export function createControlRouter(opts) {
|
|
|
712
753
|
if (outcome.ok)
|
|
713
754
|
ctx.body = outcome.result;
|
|
714
755
|
};
|
|
756
|
+
const runtimeSessionFork = async (ctx) => {
|
|
757
|
+
const body = await readRuntimeBody(ctx);
|
|
758
|
+
if (!body)
|
|
759
|
+
return;
|
|
760
|
+
if (!acceptRuntimeBodyKeys(ctx, body, ["operationId", "title"]))
|
|
761
|
+
return;
|
|
762
|
+
const outcome = await runtimeCall(ctx, "session.fork", {
|
|
763
|
+
sourceSessionId: ctx.params.id,
|
|
764
|
+
operationId: body.operationId,
|
|
765
|
+
...(typeof body.title === "string" ? { title: body.title } : {}),
|
|
766
|
+
});
|
|
767
|
+
if (outcome.ok)
|
|
768
|
+
ctx.body = outcome.result;
|
|
769
|
+
};
|
|
770
|
+
const runtimeProjects = async (ctx) => {
|
|
771
|
+
const outcome = await runtimeCall(ctx, "project.list", {});
|
|
772
|
+
if (outcome.ok)
|
|
773
|
+
ctx.body = outcome.result;
|
|
774
|
+
};
|
|
775
|
+
const runtimeProject = async (ctx) => {
|
|
776
|
+
const outcome = await runtimeCall(ctx, "project.get", {
|
|
777
|
+
projectId: ctx.params.projectId,
|
|
778
|
+
});
|
|
779
|
+
if (outcome.ok)
|
|
780
|
+
ctx.body = outcome.result;
|
|
781
|
+
};
|
|
782
|
+
const runtimeProjectCreate = async (ctx) => {
|
|
783
|
+
const body = await readRuntimeBody(ctx);
|
|
784
|
+
if (!body)
|
|
785
|
+
return;
|
|
786
|
+
if (!acceptRuntimeBodyKeys(ctx, body, ["name", "directories", "primary"]))
|
|
787
|
+
return;
|
|
788
|
+
const outcome = await runtimeCall(ctx, "project.create", {
|
|
789
|
+
name: body.name,
|
|
790
|
+
directories: body.directories,
|
|
791
|
+
primary: body.primary,
|
|
792
|
+
});
|
|
793
|
+
if (outcome.ok)
|
|
794
|
+
ctx.body = outcome.result;
|
|
795
|
+
};
|
|
796
|
+
const runtimeProjectUpdate = async (ctx) => {
|
|
797
|
+
const body = await readRuntimeBody(ctx);
|
|
798
|
+
if (!body)
|
|
799
|
+
return;
|
|
800
|
+
if (!acceptRuntimeBodyKeys(ctx, body, ["name", "directories", "primary"]))
|
|
801
|
+
return;
|
|
802
|
+
const outcome = await runtimeCall(ctx, "project.update", {
|
|
803
|
+
projectId: ctx.params.projectId,
|
|
804
|
+
name: body.name,
|
|
805
|
+
directories: body.directories,
|
|
806
|
+
primary: body.primary,
|
|
807
|
+
});
|
|
808
|
+
if (outcome.ok)
|
|
809
|
+
ctx.body = outcome.result;
|
|
810
|
+
};
|
|
811
|
+
const runtimeProjectDelete = async (ctx) => {
|
|
812
|
+
const outcome = await runtimeCall(ctx, "project.delete", {
|
|
813
|
+
projectId: ctx.params.projectId,
|
|
814
|
+
});
|
|
815
|
+
if (outcome.ok)
|
|
816
|
+
ctx.body = outcome.result;
|
|
817
|
+
};
|
|
715
818
|
const runtimeSessionMessage = async (ctx) => {
|
|
716
819
|
const body = await readRuntimeBody(ctx);
|
|
717
820
|
if (!body)
|
|
@@ -1623,10 +1726,19 @@ export function createControlRouter(opts) {
|
|
|
1623
1726
|
// The browser talks only to this loopback BFF; Local and Direct both enter the
|
|
1624
1727
|
// target daemon through the same typed application service.
|
|
1625
1728
|
router.get("/api/runtimes/:selector/sessions", publicRuntimeRead(runtimeSessions));
|
|
1729
|
+
router.get("/api/runtimes/:selector/projects", publicRuntimeRead(runtimeProjects));
|
|
1730
|
+
router.get("/api/runtimes/:selector/projects/:projectId", publicRuntimeRead(runtimeProject));
|
|
1731
|
+
router.post("/api/runtimes/:selector/projects", publicRuntimeMutation(runtimeProjectCreate));
|
|
1732
|
+
router.put("/api/runtimes/:selector/projects/:projectId", publicRuntimeMutation(runtimeProjectUpdate));
|
|
1733
|
+
router.delete("/api/runtimes/:selector/projects/:projectId", publicRuntimeMutation(runtimeProjectDelete));
|
|
1626
1734
|
router.get("/api/runtimes/:selector/sessions/agent-options", publicRuntimeRead(runtimeSessionAgentOptions));
|
|
1627
1735
|
router.get("/api/runtimes/:selector/sessions/launch-options", publicRuntimeRead(runtimeSessionLaunchOptions));
|
|
1628
1736
|
router.post("/api/runtimes/:selector/sessions", publicRuntimeMutation(runtimeSessionCreate));
|
|
1737
|
+
router.post("/api/runtimes/:selector/sessions/:id/fork", publicRuntimeMutation(runtimeSessionFork));
|
|
1738
|
+
router.get("/api/runtimes/:selector/sessions/:id/execution", publicRuntimeRead(runtimeSessionExecutionSettings));
|
|
1739
|
+
router.put("/api/runtimes/:selector/sessions/:id/execution", publicRuntimeMutation(runtimeSessionExecutionSettingsUpdate));
|
|
1629
1740
|
router.get("/api/runtimes/:selector/sessions/:id/snapshot", publicRuntimeRead(runtimeSessionSnapshot));
|
|
1741
|
+
router.get("/api/runtimes/:selector/sessions/:id/snapshot-page", publicRuntimeRead(runtimeSessionSnapshotPage));
|
|
1630
1742
|
router.get("/api/runtimes/:selector/sessions/:id/stream", publicRuntimeRead(runtimeSessionStream));
|
|
1631
1743
|
router.post("/api/runtimes/:selector/sessions/:id/interrupt", publicRuntimeMutation(runtimeSessionInterrupt));
|
|
1632
1744
|
router.post("/api/runtimes/:selector/sessions/:id/messages", publicRuntimeMutation(runtimeSessionMessage));
|
|
@@ -1801,7 +1913,6 @@ export function createControlRouter(opts) {
|
|
|
1801
1913
|
});
|
|
1802
1914
|
router.get("/api/meta", (ctx) => {
|
|
1803
1915
|
const meta = {
|
|
1804
|
-
channelTypes: deps.listChannelTypes().map((t) => t.type),
|
|
1805
1916
|
...(daemonManagement?.buildStatus
|
|
1806
1917
|
? { daemonBuild: daemonManagement.buildStatus() }
|
|
1807
1918
|
: {}),
|
|
@@ -1830,18 +1941,42 @@ export function createControlRouter(opts) {
|
|
|
1830
1941
|
return;
|
|
1831
1942
|
ctx.body = encodeDaemonStatus(remoteRuntime.status());
|
|
1832
1943
|
});
|
|
1833
|
-
router.post(DAEMON_SHUTDOWN_IF_IDLE_PATH, (ctx) => {
|
|
1944
|
+
router.post(DAEMON_SHUTDOWN_IF_IDLE_PATH, async (ctx) => {
|
|
1834
1945
|
if (!isConfiguredDaemonManagement(daemonManagement) ||
|
|
1835
1946
|
!daemonManagement.shutdownIfIdle) {
|
|
1836
1947
|
return notFound(ctx, "daemon shutdown management is not configured");
|
|
1837
1948
|
}
|
|
1838
1949
|
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
1839
1950
|
return;
|
|
1840
|
-
const
|
|
1951
|
+
const maintenanceLeaseToken = ctx.get(DAEMON_MAINTENANCE_LEASE_HEADER).trim() || undefined;
|
|
1952
|
+
let result;
|
|
1953
|
+
try {
|
|
1954
|
+
result = await daemonManagement.shutdownIfIdle(maintenanceLeaseToken);
|
|
1955
|
+
}
|
|
1956
|
+
catch (error) {
|
|
1957
|
+
ctx.set("Cache-Control", "no-store");
|
|
1958
|
+
ctx.status = 409;
|
|
1959
|
+
ctx.body = {
|
|
1960
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1961
|
+
};
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1841
1964
|
ctx.set("Cache-Control", "no-store");
|
|
1842
1965
|
ctx.status = result.outcome === "accepted" ? 202 : 409;
|
|
1843
1966
|
ctx.body = result;
|
|
1844
1967
|
});
|
|
1968
|
+
router.post(DAEMON_MAINTENANCE_LEASE_RELEASE_PATH, (ctx) => {
|
|
1969
|
+
if (!isConfiguredDaemonManagement(daemonManagement)
|
|
1970
|
+
|| !daemonManagement.releaseMaintenanceLease) {
|
|
1971
|
+
return notFound(ctx, "daemon maintenance lease management is not configured");
|
|
1972
|
+
}
|
|
1973
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
1974
|
+
return;
|
|
1975
|
+
const result = daemonManagement.releaseMaintenanceLease(ctx.get(DAEMON_MAINTENANCE_LEASE_HEADER).trim());
|
|
1976
|
+
ctx.set("Cache-Control", "no-store");
|
|
1977
|
+
ctx.status = result.outcome === "denied" ? 403 : 200;
|
|
1978
|
+
ctx.body = result;
|
|
1979
|
+
});
|
|
1845
1980
|
router.post(DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, async (ctx) => {
|
|
1846
1981
|
await mutateBrowserArtifact(ctx, "install");
|
|
1847
1982
|
});
|
|
@@ -2014,6 +2149,360 @@ export function createControlRouter(opts) {
|
|
|
2014
2149
|
ctx.set("Cache-Control", "no-store");
|
|
2015
2150
|
ctx.body = await daemonManagement.pluginManagement.listPlugins();
|
|
2016
2151
|
});
|
|
2152
|
+
// Public same-origin console view. It contains metadata only; package paths,
|
|
2153
|
+
// source URLs, package-private metadata, and credentials are not exposed.
|
|
2154
|
+
router.get("/api/plugins", async (ctx) => {
|
|
2155
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2156
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2157
|
+
}
|
|
2158
|
+
if (!requireConsoleReadRequest(ctx, runtimeWebAccess))
|
|
2159
|
+
return;
|
|
2160
|
+
ctx.set("Cache-Control", "no-store");
|
|
2161
|
+
ctx.body = await daemonManagement.pluginManagement.listPlugins();
|
|
2162
|
+
});
|
|
2163
|
+
router.get(PLUGIN_CONSOLE_MARKETPLACES_PATH, async (ctx) => {
|
|
2164
|
+
if (!daemonManagement?.pluginMarketplaces) {
|
|
2165
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2166
|
+
}
|
|
2167
|
+
if (!requireConsoleReadRequest(ctx, runtimeWebAccess))
|
|
2168
|
+
return;
|
|
2169
|
+
ctx.set("Cache-Control", "no-store");
|
|
2170
|
+
ctx.body = { marketplaces: await daemonManagement.pluginMarketplaces.list() };
|
|
2171
|
+
});
|
|
2172
|
+
router.post(PLUGIN_CONSOLE_MARKETPLACES_PATH, async (ctx) => {
|
|
2173
|
+
if (!daemonManagement?.pluginMarketplaces) {
|
|
2174
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2175
|
+
}
|
|
2176
|
+
if (!requireConsoleJsonRequest(ctx, "Market registration", runtimeWebAccess))
|
|
2177
|
+
return;
|
|
2178
|
+
let input;
|
|
2179
|
+
try {
|
|
2180
|
+
input = parsePluginMarketplaceAddInput(await readBoundedJson(ctx, PLUGIN_INSTALL_BODY_MAX_BYTES));
|
|
2181
|
+
}
|
|
2182
|
+
catch (error) {
|
|
2183
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2184
|
+
}
|
|
2185
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2186
|
+
try {
|
|
2187
|
+
ctx.status = 201;
|
|
2188
|
+
ctx.set("Cache-Control", "no-store");
|
|
2189
|
+
ctx.body = await daemonManagement.pluginMarketplaces.add(input, {
|
|
2190
|
+
signal: invocation.signal,
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
catch (error) {
|
|
2194
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2195
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2196
|
+
}
|
|
2197
|
+
finally {
|
|
2198
|
+
invocation.dispose();
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
router.post(`${PLUGIN_CONSOLE_MARKETPLACES_PATH}/:id/refresh`, async (ctx) => {
|
|
2202
|
+
if (!daemonManagement?.pluginMarketplaces) {
|
|
2203
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2204
|
+
}
|
|
2205
|
+
if (!requireConsoleJsonRequest(ctx, "Market refresh", runtimeWebAccess))
|
|
2206
|
+
return;
|
|
2207
|
+
try {
|
|
2208
|
+
await readBoundedJson(ctx, 1024);
|
|
2209
|
+
}
|
|
2210
|
+
catch (error) {
|
|
2211
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2212
|
+
}
|
|
2213
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2214
|
+
try {
|
|
2215
|
+
ctx.set("Cache-Control", "no-store");
|
|
2216
|
+
ctx.body = await daemonManagement.pluginMarketplaces.refresh(ctx.params.id, { signal: invocation.signal });
|
|
2217
|
+
}
|
|
2218
|
+
catch (error) {
|
|
2219
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2220
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2221
|
+
}
|
|
2222
|
+
finally {
|
|
2223
|
+
invocation.dispose();
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
router.delete(`${PLUGIN_CONSOLE_MARKETPLACES_PATH}/:id`, async (ctx) => {
|
|
2227
|
+
if (!daemonManagement?.pluginMarketplaces) {
|
|
2228
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2229
|
+
}
|
|
2230
|
+
if (!requireConsoleRequest(ctx, "Market removal", runtimeWebAccess))
|
|
2231
|
+
return;
|
|
2232
|
+
try {
|
|
2233
|
+
const removed = await daemonManagement.pluginMarketplaces.remove(ctx.params.id);
|
|
2234
|
+
if (!removed)
|
|
2235
|
+
return notFound(ctx, `marketplace "${ctx.params.id}" not found`);
|
|
2236
|
+
ctx.set("Cache-Control", "no-store");
|
|
2237
|
+
ctx.body = { removed: true };
|
|
2238
|
+
}
|
|
2239
|
+
catch (error) {
|
|
2240
|
+
ctx.status = 409;
|
|
2241
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
router.post(PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH, async (ctx) => {
|
|
2245
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2246
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2247
|
+
}
|
|
2248
|
+
if (!requireConsoleJsonRequest(ctx, "Plugin installation preparation", runtimeWebAccess))
|
|
2249
|
+
return;
|
|
2250
|
+
let input;
|
|
2251
|
+
try {
|
|
2252
|
+
input = parsePluginInstallPrepareInput(await readBoundedJson(ctx, PLUGIN_INSTALL_BODY_MAX_BYTES));
|
|
2253
|
+
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2256
|
+
}
|
|
2257
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2258
|
+
try {
|
|
2259
|
+
ctx.status = 201;
|
|
2260
|
+
ctx.set("Cache-Control", "no-store");
|
|
2261
|
+
ctx.body = await daemonManagement.pluginManagement.prepareInstallation(input, { signal: invocation.signal });
|
|
2262
|
+
}
|
|
2263
|
+
catch (error) {
|
|
2264
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2265
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2266
|
+
}
|
|
2267
|
+
finally {
|
|
2268
|
+
invocation.dispose();
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
router.post(`${PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH}/:token/commit`, async (ctx) => {
|
|
2272
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2273
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2274
|
+
}
|
|
2275
|
+
if (!requireConsoleJsonRequest(ctx, "Plugin installation commit", runtimeWebAccess))
|
|
2276
|
+
return;
|
|
2277
|
+
const token = ctx.params.token;
|
|
2278
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/.test(token)) {
|
|
2279
|
+
return bad(ctx, "invalid plugin preparation token");
|
|
2280
|
+
}
|
|
2281
|
+
let input;
|
|
2282
|
+
try {
|
|
2283
|
+
input = parsePluginInstallCommitInput(await readBoundedJson(ctx, PLUGIN_INSTALL_BODY_MAX_BYTES));
|
|
2284
|
+
}
|
|
2285
|
+
catch (error) {
|
|
2286
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2287
|
+
}
|
|
2288
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2289
|
+
try {
|
|
2290
|
+
ctx.set("Cache-Control", "no-store");
|
|
2291
|
+
ctx.body = await daemonManagement.pluginManagement.commitInstallation(token, input, { signal: invocation.signal });
|
|
2292
|
+
}
|
|
2293
|
+
catch (error) {
|
|
2294
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2295
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2296
|
+
}
|
|
2297
|
+
finally {
|
|
2298
|
+
invocation.dispose();
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
router.delete(`${PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH}/:token`, async (ctx) => {
|
|
2302
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2303
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2304
|
+
}
|
|
2305
|
+
if (!requireConsoleRequest(ctx, "Plugin installation cancellation", runtimeWebAccess))
|
|
2306
|
+
return;
|
|
2307
|
+
const token = ctx.params.token;
|
|
2308
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/.test(token)) {
|
|
2309
|
+
return bad(ctx, "invalid plugin preparation token");
|
|
2310
|
+
}
|
|
2311
|
+
ctx.set("Cache-Control", "no-store");
|
|
2312
|
+
ctx.body = {
|
|
2313
|
+
cancelled: await daemonManagement.pluginManagement.cancelPreparation(token),
|
|
2314
|
+
};
|
|
2315
|
+
});
|
|
2316
|
+
for (const [action, enabled] of [["enable", true], ["disable", false]]) {
|
|
2317
|
+
router.post(`/api/plugins/:pluginId/${action}`, async (ctx) => {
|
|
2318
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2319
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2320
|
+
}
|
|
2321
|
+
const pluginId = ctx.params.pluginId;
|
|
2322
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId))
|
|
2323
|
+
return bad(ctx, "invalid plugin identity");
|
|
2324
|
+
if (!requireConsoleJsonRequest(ctx, `Plugin ${action}`, runtimeWebAccess))
|
|
2325
|
+
return;
|
|
2326
|
+
try {
|
|
2327
|
+
await readBoundedJson(ctx, 1024);
|
|
2328
|
+
const plugin = await daemonManagement.pluginManagement.setPluginEnabled(pluginId, enabled);
|
|
2329
|
+
if (!plugin)
|
|
2330
|
+
return notFound(ctx, `plugin "${pluginId}" not found`);
|
|
2331
|
+
ctx.set("Cache-Control", "no-store");
|
|
2332
|
+
ctx.body = { plugin };
|
|
2333
|
+
}
|
|
2334
|
+
catch (error) {
|
|
2335
|
+
ctx.status = 409;
|
|
2336
|
+
ctx.body = {
|
|
2337
|
+
error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES),
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
router.get("/api/plugins/:pluginId/settings-ui", (ctx) => {
|
|
2343
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2344
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2345
|
+
}
|
|
2346
|
+
if (!requireConsoleReadRequest(ctx, runtimeWebAccess))
|
|
2347
|
+
return;
|
|
2348
|
+
const pluginId = ctx.params.pluginId;
|
|
2349
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId))
|
|
2350
|
+
return bad(ctx, "invalid plugin identity");
|
|
2351
|
+
try {
|
|
2352
|
+
const ui = daemonManagement.pluginManagement.settingsUi(pluginId);
|
|
2353
|
+
if (!ui)
|
|
2354
|
+
return notFound(ctx, `plugin "${pluginId}" has no settings UI`);
|
|
2355
|
+
const assetBasePath = `/api/plugins/${encodeURIComponent(pluginId)}/settings-ui-assets/` +
|
|
2356
|
+
`${ui.assetGeneration}/`;
|
|
2357
|
+
const assetBaseUrl = new URL(assetBasePath, ctx.href).href;
|
|
2358
|
+
ctx.set("Cache-Control", "no-store");
|
|
2359
|
+
ctx.body = {
|
|
2360
|
+
generation: ui.generation,
|
|
2361
|
+
html: withSettingsUiDocumentSecurity(ui.html, assetBaseUrl),
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
catch (error) {
|
|
2365
|
+
ctx.status = 409;
|
|
2366
|
+
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
router.get("/api/plugins/:pluginId/settings-ui-assets/:assetGeneration/*assetPath", (ctx) => {
|
|
2370
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2371
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2372
|
+
}
|
|
2373
|
+
if (!requireConsoleReadRequest(ctx, runtimeWebAccess))
|
|
2374
|
+
return;
|
|
2375
|
+
const pluginId = ctx.params.pluginId;
|
|
2376
|
+
const assetGeneration = ctx.params.assetGeneration;
|
|
2377
|
+
const assetPath = ctx.params.assetPath;
|
|
2378
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2379
|
+
return bad(ctx, "invalid plugin identity");
|
|
2380
|
+
}
|
|
2381
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(assetGeneration) || typeof assetPath !== "string") {
|
|
2382
|
+
return bad(ctx, "invalid plugin settings UI asset");
|
|
2383
|
+
}
|
|
2384
|
+
try {
|
|
2385
|
+
const asset = daemonManagement.pluginManagement.settingsUiAsset(pluginId, assetGeneration, assetPath);
|
|
2386
|
+
if (!asset)
|
|
2387
|
+
return notFound(ctx, "plugin settings UI asset not found");
|
|
2388
|
+
ctx.set("Access-Control-Allow-Origin", "*");
|
|
2389
|
+
ctx.set("Cache-Control", "private, max-age=31536000, immutable");
|
|
2390
|
+
ctx.set("Cross-Origin-Resource-Policy", "cross-origin");
|
|
2391
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
2392
|
+
ctx.set("Content-Type", asset.contentType);
|
|
2393
|
+
ctx.body = asset.data;
|
|
2394
|
+
}
|
|
2395
|
+
catch (error) {
|
|
2396
|
+
ctx.status = 409;
|
|
2397
|
+
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
router.post("/api/plugins/:pluginId/settings", async (ctx) => {
|
|
2401
|
+
if (!daemonManagement?.pluginManagement) {
|
|
2402
|
+
return notFound(ctx, "plugin management is not configured");
|
|
2403
|
+
}
|
|
2404
|
+
if (!requireConsoleJsonRequest(ctx, "Plugin settings call", runtimeWebAccess))
|
|
2405
|
+
return;
|
|
2406
|
+
const pluginId = ctx.params.pluginId;
|
|
2407
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId))
|
|
2408
|
+
return bad(ctx, "invalid plugin identity");
|
|
2409
|
+
let body;
|
|
2410
|
+
try {
|
|
2411
|
+
body = await readBoundedJson(ctx, PLUGIN_SETTINGS_UI_BODY_MAX_BYTES);
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2415
|
+
}
|
|
2416
|
+
const generation = asString(body.generation);
|
|
2417
|
+
const method = asString(body.method);
|
|
2418
|
+
if (!generation || !method || !/^[A-Za-z][A-Za-z0-9._:-]{0,127}$/.test(method)) {
|
|
2419
|
+
return bad(ctx, "plugin settings generation and method are required");
|
|
2420
|
+
}
|
|
2421
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2422
|
+
try {
|
|
2423
|
+
const result = await daemonManagement.pluginManagement.callSettings(pluginId, generation, method, body.params, { signal: invocation.signal });
|
|
2424
|
+
ctx.set("Cache-Control", "no-store");
|
|
2425
|
+
ctx.body = { result };
|
|
2426
|
+
}
|
|
2427
|
+
catch (error) {
|
|
2428
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2429
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2430
|
+
}
|
|
2431
|
+
finally {
|
|
2432
|
+
invocation.dispose();
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
router.get(PLUGIN_MARKETPLACES_PATH, async (ctx) => {
|
|
2436
|
+
if (!daemonManagement?.pluginMarketplaces ||
|
|
2437
|
+
!isConfiguredDaemonManagement(daemonManagement)) {
|
|
2438
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2439
|
+
}
|
|
2440
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2441
|
+
return;
|
|
2442
|
+
ctx.body = { marketplaces: await daemonManagement.pluginMarketplaces.list() };
|
|
2443
|
+
});
|
|
2444
|
+
router.post(PLUGIN_MARKETPLACES_PATH, async (ctx) => {
|
|
2445
|
+
if (!daemonManagement?.pluginMarketplaces ||
|
|
2446
|
+
!isConfiguredDaemonManagement(daemonManagement)) {
|
|
2447
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2448
|
+
}
|
|
2449
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2450
|
+
return;
|
|
2451
|
+
const body = await readBoundedJson(ctx, PLUGIN_INSTALL_BODY_MAX_BYTES);
|
|
2452
|
+
const source = asString(body.source);
|
|
2453
|
+
const alias = asString(body.alias);
|
|
2454
|
+
if (!source)
|
|
2455
|
+
return bad(ctx, "marketplace source is required");
|
|
2456
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2457
|
+
try {
|
|
2458
|
+
ctx.status = 201;
|
|
2459
|
+
ctx.body = await daemonManagement.pluginMarketplaces.add({ source, ...(alias ? { alias } : {}) }, { signal: invocation.signal });
|
|
2460
|
+
}
|
|
2461
|
+
catch (error) {
|
|
2462
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2463
|
+
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2464
|
+
}
|
|
2465
|
+
finally {
|
|
2466
|
+
invocation.dispose();
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
router.post(`${PLUGIN_MARKETPLACES_PATH}/:id/refresh`, async (ctx) => {
|
|
2470
|
+
if (!daemonManagement?.pluginMarketplaces ||
|
|
2471
|
+
!isConfiguredDaemonManagement(daemonManagement)) {
|
|
2472
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2473
|
+
}
|
|
2474
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2475
|
+
return;
|
|
2476
|
+
const invocation = pluginInstallInvocationAbortScope(ctx, shutdownSignal);
|
|
2477
|
+
try {
|
|
2478
|
+
ctx.body = await daemonManagement.pluginMarketplaces.refresh(ctx.params.id, { signal: invocation.signal });
|
|
2479
|
+
}
|
|
2480
|
+
catch (error) {
|
|
2481
|
+
ctx.status = invocation.signal.aborted ? 504 : 409;
|
|
2482
|
+
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2483
|
+
}
|
|
2484
|
+
finally {
|
|
2485
|
+
invocation.dispose();
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
router.delete(`${PLUGIN_MARKETPLACES_PATH}/:id`, async (ctx) => {
|
|
2489
|
+
if (!daemonManagement?.pluginMarketplaces ||
|
|
2490
|
+
!isConfiguredDaemonManagement(daemonManagement)) {
|
|
2491
|
+
return notFound(ctx, "plugin marketplace management is not configured");
|
|
2492
|
+
}
|
|
2493
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2494
|
+
return;
|
|
2495
|
+
try {
|
|
2496
|
+
const removed = await daemonManagement.pluginMarketplaces.remove(ctx.params.id);
|
|
2497
|
+
if (!removed)
|
|
2498
|
+
return notFound(ctx, `marketplace "${ctx.params.id}" not found`);
|
|
2499
|
+
ctx.body = { removed: true };
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
ctx.status = 409;
|
|
2503
|
+
ctx.body = { error: boundedUtf8(error instanceof Error ? error.message : String(error), PLUGIN_INSTALL_ERROR_MAX_BYTES) };
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2017
2506
|
router.post(PLUGIN_INSTALL_PREPARATIONS_PATH, async (ctx) => {
|
|
2018
2507
|
if (!isConfiguredDaemonManagement(daemonManagement) ||
|
|
2019
2508
|
!daemonManagement.pluginManagement) {
|
|
@@ -2140,8 +2629,8 @@ export function createControlRouter(opts) {
|
|
|
2140
2629
|
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2141
2630
|
return;
|
|
2142
2631
|
const pluginId = ctx.params.pluginId;
|
|
2143
|
-
if (
|
|
2144
|
-
return bad(ctx, "invalid plugin
|
|
2632
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2633
|
+
return bad(ctx, "invalid plugin identity");
|
|
2145
2634
|
}
|
|
2146
2635
|
try {
|
|
2147
2636
|
const uninstalled = await daemonManagement.pluginManagement.uninstallPlugin(pluginId);
|
|
@@ -2162,8 +2651,8 @@ export function createControlRouter(opts) {
|
|
|
2162
2651
|
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2163
2652
|
return;
|
|
2164
2653
|
const pluginId = ctx.params.pluginId;
|
|
2165
|
-
if (
|
|
2166
|
-
return bad(ctx, "invalid plugin
|
|
2654
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2655
|
+
return bad(ctx, "invalid plugin identity");
|
|
2167
2656
|
}
|
|
2168
2657
|
if (!ctx.is("application/json")) {
|
|
2169
2658
|
ctx.status = 415;
|
|
@@ -2214,8 +2703,8 @@ export function createControlRouter(opts) {
|
|
|
2214
2703
|
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2215
2704
|
return;
|
|
2216
2705
|
const pluginId = ctx.params.pluginId;
|
|
2217
|
-
if (
|
|
2218
|
-
bad(ctx, "invalid plugin
|
|
2706
|
+
if (!CANONICAL_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2707
|
+
bad(ctx, "invalid plugin identity");
|
|
2219
2708
|
return;
|
|
2220
2709
|
}
|
|
2221
2710
|
try {
|
|
@@ -2232,42 +2721,6 @@ export function createControlRouter(opts) {
|
|
|
2232
2721
|
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2233
2722
|
}
|
|
2234
2723
|
}
|
|
2235
|
-
router.post("/api/internal/plugin-runtimes/:pluginId/reload", async (ctx) => {
|
|
2236
|
-
if (!pluginRuntimeReload || !isConfiguredDaemonManagement(daemonManagement)) {
|
|
2237
|
-
return notFound(ctx, "plugin runtime management is not configured");
|
|
2238
|
-
}
|
|
2239
|
-
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2240
|
-
return;
|
|
2241
|
-
const pluginId = ctx.params.pluginId;
|
|
2242
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(pluginId)) {
|
|
2243
|
-
return bad(ctx, "invalid plugin id");
|
|
2244
|
-
}
|
|
2245
|
-
try {
|
|
2246
|
-
ctx.body = await pluginRuntimeReload(pluginId);
|
|
2247
|
-
}
|
|
2248
|
-
catch (error) {
|
|
2249
|
-
ctx.status = 409;
|
|
2250
|
-
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2251
|
-
}
|
|
2252
|
-
});
|
|
2253
|
-
router.post("/api/internal/plugin-runtimes/:pluginId/ensure", async (ctx) => {
|
|
2254
|
-
if (!pluginRuntimeEnsure || !isConfiguredDaemonManagement(daemonManagement)) {
|
|
2255
|
-
return notFound(ctx, "plugin runtime management is not configured");
|
|
2256
|
-
}
|
|
2257
|
-
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2258
|
-
return;
|
|
2259
|
-
const pluginId = ctx.params.pluginId;
|
|
2260
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(pluginId)) {
|
|
2261
|
-
return bad(ctx, "invalid plugin id");
|
|
2262
|
-
}
|
|
2263
|
-
try {
|
|
2264
|
-
ctx.body = await pluginRuntimeEnsure(pluginId);
|
|
2265
|
-
}
|
|
2266
|
-
catch (error) {
|
|
2267
|
-
ctx.status = 409;
|
|
2268
|
-
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2269
|
-
}
|
|
2270
|
-
});
|
|
2271
2724
|
router.get("/control/link", (ctx) => {
|
|
2272
2725
|
const token = asString(ctx.query.token);
|
|
2273
2726
|
if (!token || !deps.consumeControlLink)
|
|
@@ -2277,153 +2730,18 @@ export function createControlRouter(opts) {
|
|
|
2277
2730
|
return notFound(ctx, "control link expired or already used");
|
|
2278
2731
|
ctx.redirect(`/sessions/${encodeURIComponent(link.sessionId)}`);
|
|
2279
2732
|
});
|
|
2280
|
-
router.get("/api/channel-types", (ctx) => {
|
|
2281
|
-
ctx.body = { channelTypes: deps.listChannelTypes() };
|
|
2282
|
-
});
|
|
2283
|
-
// ── channels ───────────────────────────────────────────────────────────
|
|
2284
|
-
router.get("/api/channels", (ctx) => {
|
|
2285
|
-
ctx.body = {
|
|
2286
|
-
channels: deps.listChannels().map((c) => ({
|
|
2287
|
-
id: c.id,
|
|
2288
|
-
name: c.name,
|
|
2289
|
-
type: c.type,
|
|
2290
|
-
options: redact(deps, c.type, c.options),
|
|
2291
|
-
})),
|
|
2292
|
-
};
|
|
2293
|
-
});
|
|
2294
|
-
router.post("/api/channels", async (ctx) => {
|
|
2295
|
-
const body = await readJson(ctx);
|
|
2296
|
-
const name = asString(body.name);
|
|
2297
|
-
const type = asString(body.type);
|
|
2298
|
-
if (!name)
|
|
2299
|
-
return bad(ctx, "missing channel name");
|
|
2300
|
-
if (!type)
|
|
2301
|
-
return bad(ctx, "missing channel type");
|
|
2302
|
-
let id;
|
|
2303
|
-
try {
|
|
2304
|
-
({ id } = deps.createChannel({ name, type, options: asRecord(body.options) }));
|
|
2305
|
-
}
|
|
2306
|
-
catch (error) {
|
|
2307
|
-
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2308
|
-
}
|
|
2309
|
-
ctx.body = { ok: true, id };
|
|
2310
|
-
});
|
|
2311
|
-
router.put("/api/channels/:id", async (ctx) => {
|
|
2312
|
-
const id = ctx.params.id;
|
|
2313
|
-
const body = await readJson(ctx);
|
|
2314
|
-
try {
|
|
2315
|
-
deps.setChannel(id, {
|
|
2316
|
-
...(body.name !== undefined ? { name: asString(body.name) } : {}),
|
|
2317
|
-
...(body.options !== undefined ? { options: asRecord(body.options) } : {}),
|
|
2318
|
-
});
|
|
2319
|
-
}
|
|
2320
|
-
catch (error) {
|
|
2321
|
-
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2322
|
-
}
|
|
2323
|
-
const bound = instanceForChannel(id);
|
|
2324
|
-
if (bound)
|
|
2325
|
-
await manager.reloadInstance(bound.id);
|
|
2326
|
-
ctx.body = { ok: true };
|
|
2327
|
-
});
|
|
2328
|
-
router.delete("/api/channels/:id", async (ctx) => {
|
|
2329
|
-
const id = ctx.params.id;
|
|
2330
|
-
const bound = instanceForChannel(id);
|
|
2331
|
-
if (bound)
|
|
2332
|
-
await manager.stopInstance(bound.id); // its row cascades on channel delete
|
|
2333
|
-
deps.removeChannel(id);
|
|
2334
|
-
ctx.body = { ok: true };
|
|
2335
|
-
});
|
|
2336
|
-
// Authorize (config mode 2): run the channel type's flow over SSE, persist the
|
|
2337
|
-
// resolved options on the channel, reload the bound instance (if any).
|
|
2338
|
-
router.post("/api/channels/:id/authorize", async (ctx) => {
|
|
2339
|
-
const id = ctx.params.id;
|
|
2340
|
-
const channel = deps.listChannels().find((c) => c.id === id);
|
|
2341
|
-
if (!channel)
|
|
2342
|
-
return notFound(ctx, "channel not found");
|
|
2343
|
-
await streamEvents(ctx, async (send, signal) => {
|
|
2344
|
-
send("status", { text: `authorizing ${channel.type}…` });
|
|
2345
|
-
const bound = instanceForChannel(id);
|
|
2346
|
-
const options = await deps.authorizeChannel(channel.type, {
|
|
2347
|
-
...(bound ? { instanceId: bound.id } : {}),
|
|
2348
|
-
config: channel.options ?? {},
|
|
2349
|
-
}, (m) => send(m.kind, m), signal);
|
|
2350
|
-
deps.setChannel(id, { options });
|
|
2351
|
-
if (bound)
|
|
2352
|
-
await manager.reloadInstance(bound.id);
|
|
2353
|
-
send("done", { id });
|
|
2354
|
-
}, shutdownSignal);
|
|
2355
|
-
});
|
|
2356
|
-
// ── instances ──────────────────────────────────────────────────────────
|
|
2357
|
-
router.get("/api/instances", (ctx) => {
|
|
2358
|
-
const statuses = new Map(manager.list().map((status) => [status.instanceId, status]));
|
|
2359
|
-
const instances = deps.listInstancesConfig().map((inst) => {
|
|
2360
|
-
const status = statuses.get(inst.id);
|
|
2361
|
-
return {
|
|
2362
|
-
id: inst.id,
|
|
2363
|
-
channelId: inst.channelId,
|
|
2364
|
-
channelName: inst.channelName,
|
|
2365
|
-
type: inst.type,
|
|
2366
|
-
agent: inst.agent,
|
|
2367
|
-
enabled: inst.enabled,
|
|
2368
|
-
running: status?.running ?? false,
|
|
2369
|
-
connected: status?.connected ?? null,
|
|
2370
|
-
lastError: status?.lastError ?? null,
|
|
2371
|
-
lastEventAt: status?.lastEventAt ?? null,
|
|
2372
|
-
};
|
|
2373
|
-
});
|
|
2374
|
-
ctx.body = { instances };
|
|
2375
|
-
});
|
|
2376
|
-
router.post("/api/instances", async (ctx) => {
|
|
2377
|
-
const body = await readJson(ctx);
|
|
2378
|
-
const channelId = asString(body.channelId);
|
|
2379
|
-
if (!channelId)
|
|
2380
|
-
return bad(ctx, "missing channelId");
|
|
2381
|
-
let id;
|
|
2382
|
-
try {
|
|
2383
|
-
({ id } = deps.createInstance({ channelId, agent: asString(body.agent) }));
|
|
2384
|
-
}
|
|
2385
|
-
catch (error) {
|
|
2386
|
-
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2387
|
-
}
|
|
2388
|
-
await manager.reloadInstance(id);
|
|
2389
|
-
ctx.body = { ok: true, id };
|
|
2390
|
-
});
|
|
2391
|
-
router.put("/api/instances/:id", async (ctx) => {
|
|
2392
|
-
const id = ctx.params.id;
|
|
2393
|
-
const body = await readJson(ctx);
|
|
2394
|
-
deps.setInstance(id, {
|
|
2395
|
-
...(body.agent !== undefined ? { agent: asString(body.agent) } : {}),
|
|
2396
|
-
...(body.enabled !== undefined ? { enabled: Boolean(body.enabled) } : {}),
|
|
2397
|
-
});
|
|
2398
|
-
await manager.reloadInstance(id);
|
|
2399
|
-
ctx.body = { ok: true };
|
|
2400
|
-
});
|
|
2401
|
-
router.post("/api/instances/:id/enable", async (ctx) => {
|
|
2402
|
-
const id = ctx.params.id;
|
|
2403
|
-
deps.setInstance(id, { enabled: true });
|
|
2404
|
-
await manager.startInstance(id);
|
|
2405
|
-
ctx.body = { ok: true };
|
|
2406
|
-
});
|
|
2407
|
-
router.post("/api/instances/:id/disable", async (ctx) => {
|
|
2408
|
-
const id = ctx.params.id;
|
|
2409
|
-
deps.setInstance(id, { enabled: false });
|
|
2410
|
-
await manager.stopInstance(id);
|
|
2411
|
-
ctx.body = { ok: true };
|
|
2412
|
-
});
|
|
2413
|
-
router.post("/api/instances/:id/restart", async (ctx) => {
|
|
2414
|
-
await manager.reloadInstance(ctx.params.id);
|
|
2415
|
-
ctx.body = { ok: true };
|
|
2416
|
-
});
|
|
2417
|
-
router.delete("/api/instances/:id", async (ctx) => {
|
|
2418
|
-
const id = ctx.params.id;
|
|
2419
|
-
await manager.stopInstance(id);
|
|
2420
|
-
deps.removeInstance(id);
|
|
2421
|
-
ctx.body = { ok: true };
|
|
2422
|
-
});
|
|
2423
2733
|
// ── agents ─────────────────────────────────────────────────────────────
|
|
2424
2734
|
router.get("/api/agents", async (ctx) => {
|
|
2425
2735
|
ctx.body = { agents: await deps.listAgents() };
|
|
2426
2736
|
});
|
|
2737
|
+
router.get("/api/agents/runtime-options", async (ctx) => {
|
|
2738
|
+
if (!deps.listAgentRuntimeOptions)
|
|
2739
|
+
return notFound(ctx, "local runtime discovery is unavailable");
|
|
2740
|
+
ctx.set("Cache-Control", "no-store");
|
|
2741
|
+
ctx.body = {
|
|
2742
|
+
runtimes: await deps.listAgentRuntimeOptions({ refresh: asString(ctx.query.refresh) === "1" }),
|
|
2743
|
+
};
|
|
2744
|
+
});
|
|
2427
2745
|
router.get("/api/agents/:id", async (ctx) => {
|
|
2428
2746
|
const spec = await deps.getAgent(ctx.params.id);
|
|
2429
2747
|
if (!spec)
|
|
@@ -2436,6 +2754,11 @@ export function createControlRouter(opts) {
|
|
|
2436
2754
|
if (!id)
|
|
2437
2755
|
return bad(ctx, "missing agent id");
|
|
2438
2756
|
try {
|
|
2757
|
+
if (await deps.getAgent(id)) {
|
|
2758
|
+
ctx.status = 409;
|
|
2759
|
+
ctx.body = { error: "agent id already exists" };
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2439
2762
|
deps.writeAgent(id, body);
|
|
2440
2763
|
}
|
|
2441
2764
|
catch (error) {
|
|
@@ -2443,6 +2766,19 @@ export function createControlRouter(opts) {
|
|
|
2443
2766
|
}
|
|
2444
2767
|
ctx.body = { ok: true };
|
|
2445
2768
|
});
|
|
2769
|
+
router.put("/api/agents/:id", async (ctx) => {
|
|
2770
|
+
const id = ctx.params.id;
|
|
2771
|
+
const body = await readJson(ctx);
|
|
2772
|
+
if (!await deps.getAgent(id))
|
|
2773
|
+
return notFound(ctx, "agent not found");
|
|
2774
|
+
try {
|
|
2775
|
+
deps.writeAgent(id, { ...body, id });
|
|
2776
|
+
}
|
|
2777
|
+
catch (error) {
|
|
2778
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2779
|
+
}
|
|
2780
|
+
ctx.body = { ok: true };
|
|
2781
|
+
});
|
|
2446
2782
|
router.delete("/api/agents/:id", (ctx) => {
|
|
2447
2783
|
ctx.body = { ok: deps.removeAgent(ctx.params.id) };
|
|
2448
2784
|
});
|
|
@@ -2482,6 +2818,64 @@ export function createControlRouter(opts) {
|
|
|
2482
2818
|
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2483
2819
|
}
|
|
2484
2820
|
});
|
|
2821
|
+
router.post("/api/skills/local-scan", async (ctx) => {
|
|
2822
|
+
if (!deps.scanLocalSkills)
|
|
2823
|
+
return notFound(ctx, "local Skill discovery is unavailable");
|
|
2824
|
+
const body = await readJson(ctx);
|
|
2825
|
+
const runtime = parseProviderCliId(body.runtime);
|
|
2826
|
+
if (!runtime)
|
|
2827
|
+
return bad(ctx, "runtime must be codex, traex, or claude");
|
|
2828
|
+
try {
|
|
2829
|
+
ctx.body = { candidates: await deps.scanLocalSkills(runtime) };
|
|
2830
|
+
}
|
|
2831
|
+
catch (error) {
|
|
2832
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
router.post("/api/skills/local-import", async (ctx) => {
|
|
2836
|
+
if (!deps.importLocalSkills)
|
|
2837
|
+
return notFound(ctx, "local Skill import is unavailable");
|
|
2838
|
+
const body = await readJson(ctx);
|
|
2839
|
+
const runtime = parseProviderCliId(body.runtime);
|
|
2840
|
+
const rawKeys = Array.isArray(body.keys) ? body.keys : [];
|
|
2841
|
+
const keys = rawKeys.length > 0
|
|
2842
|
+
? rawKeys.filter((value) => typeof value === "string" && value.length > 0)
|
|
2843
|
+
: [];
|
|
2844
|
+
if (!runtime)
|
|
2845
|
+
return bad(ctx, "runtime must be codex, traex, or claude");
|
|
2846
|
+
if (keys.length === 0 || keys.length !== rawKeys.length || keys.length > 100) {
|
|
2847
|
+
return bad(ctx, "keys must contain 1-100 non-empty candidate keys");
|
|
2848
|
+
}
|
|
2849
|
+
try {
|
|
2850
|
+
ctx.body = { results: await deps.importLocalSkills(runtime, keys) };
|
|
2851
|
+
}
|
|
2852
|
+
catch (error) {
|
|
2853
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
router.post("/api/skills/import-archive", async (ctx) => {
|
|
2857
|
+
if (!deps.importSkillArchive)
|
|
2858
|
+
return notFound(ctx, "Skill archive import is unavailable");
|
|
2859
|
+
let body;
|
|
2860
|
+
try {
|
|
2861
|
+
body = await readJson(ctx, 29 * 1024 * 1024);
|
|
2862
|
+
}
|
|
2863
|
+
catch (error) {
|
|
2864
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2865
|
+
}
|
|
2866
|
+
const filename = asString(body.filename);
|
|
2867
|
+
const contentBase64 = asString(body.contentBase64);
|
|
2868
|
+
if (!filename || !contentBase64)
|
|
2869
|
+
return bad(ctx, "filename and contentBase64 are required");
|
|
2870
|
+
if (contentBase64.length > 28 * 1024 * 1024)
|
|
2871
|
+
return bad(ctx, "archive payload is too large");
|
|
2872
|
+
try {
|
|
2873
|
+
ctx.body = { results: await deps.importSkillArchive(filename, contentBase64) };
|
|
2874
|
+
}
|
|
2875
|
+
catch (error) {
|
|
2876
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2485
2879
|
router.delete("/api/skills/:name", async (ctx) => {
|
|
2486
2880
|
try {
|
|
2487
2881
|
ctx.body = { ok: await deps.removeSkill(ctx.params.name) };
|
|
@@ -2802,8 +3196,7 @@ export function createControlRouter(opts) {
|
|
|
2802
3196
|
const updatedAt = log ? new Date(log.updatedAt).toISOString() : (meta?.updatedAt ?? createdAt);
|
|
2803
3197
|
return {
|
|
2804
3198
|
id,
|
|
2805
|
-
provider: meta?.provider,
|
|
2806
|
-
agent: meta?.agent,
|
|
3199
|
+
provider: meta?.execution?.provider,
|
|
2807
3200
|
title: meta?.title,
|
|
2808
3201
|
status: sessionRuntimeIndex.snapshot(id).status,
|
|
2809
3202
|
createdAt,
|
|
@@ -2815,134 +3208,90 @@ export function createControlRouter(opts) {
|
|
|
2815
3208
|
ctx.body = { sessions };
|
|
2816
3209
|
});
|
|
2817
3210
|
router.post("/api/sessions", async (ctx) => {
|
|
2818
|
-
if (!
|
|
2819
|
-
return bad(ctx, "session
|
|
3211
|
+
if (!localMachineSessions) {
|
|
3212
|
+
return bad(ctx, "session creation is unavailable on this server");
|
|
3213
|
+
}
|
|
2820
3214
|
const body = await readJson(ctx);
|
|
2821
3215
|
const agent = asString(body.agent);
|
|
2822
|
-
|
|
3216
|
+
const provider = asString(body.provider);
|
|
3217
|
+
const permissionPreset = parseSessionPermissionPreset(body.permissionPreset);
|
|
2823
3218
|
if (body.config != null) {
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3219
|
+
return bad(ctx, "inline Agent configs are no longer supported; save an Agent first");
|
|
3220
|
+
}
|
|
3221
|
+
if (Boolean(agent) === Boolean(provider)) {
|
|
3222
|
+
return bad(ctx, "provide exactly one Agent or Provider");
|
|
3223
|
+
}
|
|
3224
|
+
if (body.permissionPreset !== undefined && !permissionPreset) {
|
|
3225
|
+
return bad(ctx, "permissionPreset must be default, read-only, workspace-write, or full-access");
|
|
3226
|
+
}
|
|
3227
|
+
try {
|
|
3228
|
+
ctx.body = await localMachineSessions.create({
|
|
3229
|
+
...(agent ? { agent } : { provider: provider }),
|
|
3230
|
+
...(asString(body.model) ? { model: asString(body.model) } : {}),
|
|
3231
|
+
...(asString(body.reasoningEffort)
|
|
3232
|
+
? { reasoningEffort: asString(body.reasoningEffort) }
|
|
3233
|
+
: {}),
|
|
3234
|
+
...(permissionPreset ? { permissionPreset } : {}),
|
|
3235
|
+
...(asString(body.title) ? { title: asString(body.title) } : {}),
|
|
3236
|
+
...(asString(body.projectId) ? { projectId: asString(body.projectId) } : {}),
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
catch (error) {
|
|
3240
|
+
return bad(ctx, error instanceof Error ? error.message : String(error));
|
|
3241
|
+
}
|
|
2844
3242
|
});
|
|
2845
3243
|
router.post("/api/sessions/:id/messages", async (ctx) => {
|
|
2846
3244
|
const id = ctx.params.id;
|
|
2847
|
-
if (!runtime)
|
|
2848
|
-
return bad(ctx, "session runs are not available on this server");
|
|
2849
3245
|
const body = await readJson(ctx);
|
|
2850
3246
|
const message = asString(body.message);
|
|
2851
3247
|
if (!message)
|
|
2852
3248
|
return bad(ctx, "missing message");
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
// binding recorded in the shared store (its original runtime/model/cwd). The
|
|
2857
|
-
// turn lands in the same canonical log + LLM session, so the channel sees it
|
|
2858
|
-
// in history — but no card is pushed (channels don't subscribe to the bus).
|
|
2859
|
-
const consoleMeta = meta?.source === "console" ? meta : undefined;
|
|
2860
|
-
const bound = consoleMeta ? null : ((await sessionStore?.get(id)) ?? null);
|
|
2861
|
-
if (!meta && !bound)
|
|
2862
|
-
return notFound(ctx, "session not found");
|
|
2863
|
-
// First message names a console session (reference implementation-style: the message,
|
|
2864
|
-
// truncated). Channel sessions keep their own title elsewhere — skip.
|
|
2865
|
-
if (consoleMeta && !consoleMeta.title)
|
|
2866
|
-
deps.setSessionTitle(id, synthesizeSessionTitle(message));
|
|
2867
|
-
// codex-native live mode (single writer): bring up the session's persistent
|
|
2868
|
-
// forwarder + TUI, then INJECT this turn into the shared app-server thread
|
|
2869
|
-
// (turn/start | turn/steer) and return immediately. The forwarder mirrors the
|
|
2870
|
-
// output onto the bus; the web renders it from the persistent `/stream`. No
|
|
2871
|
-
// per-`/messages` normalize/persist here — that would double-write the turn.
|
|
2872
|
-
if (runnerManager?.ensureLiveSession && runnerManager.injectMessage) {
|
|
2873
|
-
// Resolve the session's runtime from its agent spec (as the run path does),
|
|
2874
|
-
// so live co-drive picks the right backend PER AGENT — the live path
|
|
2875
|
-
// bypasses ConversationRuntime's own resolution, so without this it would
|
|
2876
|
-
// fall back to the global default and mis-route (e.g. a claude agent onto codex).
|
|
2877
|
-
const liveRuntime = await runtime.resolveRuntime({
|
|
2878
|
-
agentName: consoleMeta?.agent,
|
|
2879
|
-
agentSpec: consoleMeta?.config,
|
|
2880
|
-
provider: meta?.provider ?? bound?.runtime,
|
|
2881
|
-
});
|
|
2882
|
-
// codex RESUME with a missing local rollout (fork / cross-machine): synthesize
|
|
2883
|
-
// one from the session log so the app-server's thread/resume finds it. No-op
|
|
2884
|
-
// when the rollout already exists (the normal single-machine case). The bound
|
|
2885
|
-
// thread record is fetched directly — `bound` above is null for console
|
|
2886
|
-
// sessions, but a console session still resumes a stored codex thread.
|
|
2887
|
-
if ((liveRuntime === "codex" || liveRuntime === "traex") && sessionLog) {
|
|
2888
|
-
const codexRecord = await sessionStore?.get(id);
|
|
2889
|
-
if (codexRecord?.codexSessionId) {
|
|
2890
|
-
ensureCodexResumeRollout({
|
|
2891
|
-
// `id` (the rynx localThreadId) resolves the per-session CODEX_HOME —
|
|
2892
|
-
// the SAME id the runner child gets via `RYNX_RUNNER_SESSION`, so the
|
|
2893
|
-
// synthesized rollout lands where that session's app-server reads.
|
|
2894
|
-
sessionId: id,
|
|
2895
|
-
runtime: liveRuntime,
|
|
2896
|
-
threadId: codexRecord.codexSessionId,
|
|
2897
|
-
cwd: codexRecord.cwd ?? process.cwd(),
|
|
2898
|
-
items: await sessionLog.snapshot(id),
|
|
2899
|
-
});
|
|
2900
|
-
}
|
|
3249
|
+
if (localMachineSessions) {
|
|
3250
|
+
try {
|
|
3251
|
+
ctx.body = await localMachineSessions.sendMessage(id, message);
|
|
2901
3252
|
}
|
|
2902
|
-
|
|
2903
|
-
...(bound?.cwd
|
|
2904
|
-
? { cwd: bound.cwd }
|
|
2905
|
-
: consoleMeta?.config?.osEnv?.cwd
|
|
2906
|
-
? { cwd: consoleMeta.config.osEnv.cwd }
|
|
2907
|
-
: {}),
|
|
2908
|
-
runtime: liveRuntime,
|
|
2909
|
-
...(consoleMeta?.reasoningEffort ? { reasoningEffort: consoleMeta.reasoningEffort } : {}),
|
|
2910
|
-
// Carry the agent identity so the live launch applies the agent spec's
|
|
2911
|
-
// model / skills / instructions (not just the runtime). A preset agent
|
|
2912
|
-
// rides its id; an inline-config console session rides its spec.
|
|
2913
|
-
...(consoleMeta?.agent ? { agentName: consoleMeta.agent } : {}),
|
|
2914
|
-
...(consoleMeta?.config ? { agentSpec: consoleMeta.config } : {}),
|
|
2915
|
-
});
|
|
2916
|
-
if (!live) {
|
|
2917
|
-
// A live-runtime (codex/claude-native) session whose forwarder couldn't come
|
|
2918
|
-
// up. reference implementation reports this as a failure and NEVER falls through to a second
|
|
2919
|
-
// output path — falling through would run+persist the turn while the forwarder
|
|
2920
|
-
// also mirrors it (the double-write). Report + stop.
|
|
2921
|
-
const detail = runnerManager.lastLiveSessionError?.(id);
|
|
3253
|
+
catch (error) {
|
|
2922
3254
|
ctx.status = 503;
|
|
2923
|
-
ctx.body = {
|
|
2924
|
-
error: `live session unavailable (${liveRuntime})`,
|
|
2925
|
-
...(detail ? { detail } : {}),
|
|
2926
|
-
};
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
const outcome = await runnerManager.injectMessage(id, message);
|
|
2930
|
-
if (outcome === "injected") {
|
|
2931
|
-
ctx.body = { ok: true, injected: true };
|
|
2932
|
-
return;
|
|
3255
|
+
ctx.body = { error: error instanceof Error ? error.message : String(error) };
|
|
2933
3256
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
const meta = deps.getSessionMeta(id);
|
|
3260
|
+
if (!meta)
|
|
3261
|
+
return notFound(ctx, "session not found");
|
|
3262
|
+
if (!meta.title)
|
|
3263
|
+
deps.setSessionTitle(id, synthesizeSessionTitle(message));
|
|
3264
|
+
if (!runnerManager?.ensureLiveSession || !runnerManager.injectMessage) {
|
|
2937
3265
|
ctx.status = 503;
|
|
2938
|
-
ctx.body = { error:
|
|
3266
|
+
ctx.body = { error: "session runs require a live runner" };
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const live = await runnerManager.ensureLiveSession(id, {
|
|
3270
|
+
workspace: structuredClone(meta.workspace),
|
|
3271
|
+
execution: structuredClone(meta.execution),
|
|
3272
|
+
});
|
|
3273
|
+
if (!live) {
|
|
3274
|
+
ctx.status = 503;
|
|
3275
|
+
ctx.body = {
|
|
3276
|
+
error: `live session unavailable (${meta.execution.provider})`,
|
|
3277
|
+
...(runnerManager.lastLiveSessionError?.(id)
|
|
3278
|
+
? { detail: runnerManager.lastLiveSessionError(id) }
|
|
3279
|
+
: {}),
|
|
3280
|
+
};
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
const outcome = await runnerManager.injectMessage(id, message);
|
|
3284
|
+
if (outcome === "injected") {
|
|
3285
|
+
ctx.body = { ok: true, injected: true };
|
|
2939
3286
|
return;
|
|
2940
3287
|
}
|
|
2941
|
-
// Native-only: a turn can only run on a live runner (codex app-server / claude
|
|
2942
|
-
// TUI). With no runner there is nothing to inject into, and there is no non-live
|
|
2943
|
-
// programmatic fallback (that path was removed) — report + stop.
|
|
2944
3288
|
ctx.status = 503;
|
|
2945
|
-
ctx.body = {
|
|
3289
|
+
ctx.body = {
|
|
3290
|
+
error: `live injection ${outcome}`,
|
|
3291
|
+
...(runnerManager.lastLiveSessionError?.(id)
|
|
3292
|
+
? { detail: runnerManager.lastLiveSessionError(id) }
|
|
3293
|
+
: {}),
|
|
3294
|
+
};
|
|
2946
3295
|
});
|
|
2947
3296
|
// Stop the session's active turn (the web Stop button). For a live native
|
|
2948
3297
|
// session this interrupts the running turn at its source — codex app-server
|
|
@@ -3162,11 +3511,28 @@ export function synthesizeSessionTitle(message, limit = 60) {
|
|
|
3162
3511
|
return collapsed;
|
|
3163
3512
|
return `${collapsed.slice(0, limit - 1).trimEnd()}…`;
|
|
3164
3513
|
}
|
|
3165
|
-
async function readJson(ctx) {
|
|
3514
|
+
async function readJson(ctx, maxBytes) {
|
|
3166
3515
|
const raw = await new Promise((resolve, reject) => {
|
|
3167
3516
|
let data = "";
|
|
3168
|
-
|
|
3169
|
-
|
|
3517
|
+
let bytes = 0;
|
|
3518
|
+
let exceeded = false;
|
|
3519
|
+
ctx.req.on("data", (chunk) => {
|
|
3520
|
+
if (exceeded)
|
|
3521
|
+
return;
|
|
3522
|
+
bytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
3523
|
+
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
3524
|
+
exceeded = true;
|
|
3525
|
+
data = "";
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
data += chunk;
|
|
3529
|
+
});
|
|
3530
|
+
ctx.req.on("end", () => {
|
|
3531
|
+
if (exceeded)
|
|
3532
|
+
reject(new Error("request body is too large"));
|
|
3533
|
+
else
|
|
3534
|
+
resolve(data);
|
|
3535
|
+
});
|
|
3170
3536
|
ctx.req.on("error", reject);
|
|
3171
3537
|
});
|
|
3172
3538
|
if (!raw.trim())
|
|
@@ -3179,23 +3545,6 @@ async function readJson(ctx) {
|
|
|
3179
3545
|
return {};
|
|
3180
3546
|
}
|
|
3181
3547
|
}
|
|
3182
|
-
/**
|
|
3183
|
-
* Mask secret-ish option values so the page never echoes credentials back. A
|
|
3184
|
-
* value is masked when its key is a `type: "secret"` field in the channel type's
|
|
3185
|
-
* declared schema, or matches the secret-name heuristic as a fallback.
|
|
3186
|
-
*/
|
|
3187
|
-
function redact(deps, type, options) {
|
|
3188
|
-
if (!options)
|
|
3189
|
-
return undefined;
|
|
3190
|
-
const schema = deps.listChannelTypes().find((t) => t.type === type)?.configSchema;
|
|
3191
|
-
const secretKeys = new Set((schema ?? []).filter((f) => f.type === "secret").map((f) => f.key));
|
|
3192
|
-
const out = {};
|
|
3193
|
-
for (const [key, value] of Object.entries(options)) {
|
|
3194
|
-
const secret = secretKeys.has(key) || /secret|token|key/i.test(key);
|
|
3195
|
-
out[key] = secret && value ? "***" : value;
|
|
3196
|
-
}
|
|
3197
|
-
return out;
|
|
3198
|
-
}
|
|
3199
3548
|
function asString(value) {
|
|
3200
3549
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
3201
3550
|
}
|
|
@@ -3204,6 +3553,14 @@ function parseProviderCliId(value) {
|
|
|
3204
3553
|
? value
|
|
3205
3554
|
: undefined;
|
|
3206
3555
|
}
|
|
3556
|
+
function parseSessionPermissionPreset(value) {
|
|
3557
|
+
return value === "default" ||
|
|
3558
|
+
value === "read-only" ||
|
|
3559
|
+
value === "workspace-write" ||
|
|
3560
|
+
value === "full-access"
|
|
3561
|
+
? value
|
|
3562
|
+
: undefined;
|
|
3563
|
+
}
|
|
3207
3564
|
function parseProviderCliOperation(value) {
|
|
3208
3565
|
return value === "install" || value === "update" ? value : undefined;
|
|
3209
3566
|
}
|
|
@@ -3359,6 +3716,63 @@ function namedError(message, name) {
|
|
|
3359
3716
|
error.name = name;
|
|
3360
3717
|
return error;
|
|
3361
3718
|
}
|
|
3719
|
+
function withSettingsUiDocumentSecurity(html, assetBaseUrl) {
|
|
3720
|
+
const source = assetBaseUrl.replace(/["&<>]/g, encodeURIComponent);
|
|
3721
|
+
const csp = [
|
|
3722
|
+
"default-src 'none'",
|
|
3723
|
+
`script-src 'unsafe-inline' ${source}`,
|
|
3724
|
+
`style-src 'unsafe-inline' ${source}`,
|
|
3725
|
+
`img-src data: ${source}`,
|
|
3726
|
+
`font-src data: ${source}`,
|
|
3727
|
+
`media-src ${source}`,
|
|
3728
|
+
"connect-src 'none'",
|
|
3729
|
+
"worker-src 'none'",
|
|
3730
|
+
"object-src 'none'",
|
|
3731
|
+
"frame-src 'none'",
|
|
3732
|
+
"form-action 'none'",
|
|
3733
|
+
"navigate-to 'none'",
|
|
3734
|
+
`base-uri ${source}`,
|
|
3735
|
+
].join("; ");
|
|
3736
|
+
const prefix = `<meta http-equiv="Content-Security-Policy" content="${csp}">` +
|
|
3737
|
+
`<base href="${source}">`;
|
|
3738
|
+
return /<head(?:\s[^>]*)?>/i.test(html)
|
|
3739
|
+
? html.replace(/<head(?:\s[^>]*)?>/i, (head) => `${head}${prefix}`)
|
|
3740
|
+
: `${prefix}${html}`;
|
|
3741
|
+
}
|
|
3742
|
+
function requireConsoleReadRequest(ctx, access) {
|
|
3743
|
+
const failure = authorizeRuntimeWebRequest(ctx.req, { access });
|
|
3744
|
+
if (!failure)
|
|
3745
|
+
return true;
|
|
3746
|
+
ctx.status = failure.status;
|
|
3747
|
+
ctx.body = { error: failure.error };
|
|
3748
|
+
return false;
|
|
3749
|
+
}
|
|
3750
|
+
function requireConsoleRequest(ctx, operation, access) {
|
|
3751
|
+
const failure = authorizeRuntimeWebRequest(ctx.req, {
|
|
3752
|
+
mutation: true,
|
|
3753
|
+
capability: ctx.get(RUNTIME_WEB_CAPABILITY_HEADER),
|
|
3754
|
+
access,
|
|
3755
|
+
});
|
|
3756
|
+
if (failure) {
|
|
3757
|
+
ctx.status = failure.status;
|
|
3758
|
+
ctx.body = { error: failure.error };
|
|
3759
|
+
return false;
|
|
3760
|
+
}
|
|
3761
|
+
if (ctx.get("x-rynx-console") === "1")
|
|
3762
|
+
return true;
|
|
3763
|
+
ctx.status = 403;
|
|
3764
|
+
ctx.body = { error: `${operation} requires the Rynx console` };
|
|
3765
|
+
return false;
|
|
3766
|
+
}
|
|
3767
|
+
function requireConsoleJsonRequest(ctx, operation, access) {
|
|
3768
|
+
if (!requireConsoleRequest(ctx, operation, access))
|
|
3769
|
+
return false;
|
|
3770
|
+
if (ctx.is("application/json"))
|
|
3771
|
+
return true;
|
|
3772
|
+
ctx.status = 415;
|
|
3773
|
+
ctx.body = { error: `${operation} requires application/json` };
|
|
3774
|
+
return false;
|
|
3775
|
+
}
|
|
3362
3776
|
function authorizeLoopbackManagementRequest(ctx, expectedToken) {
|
|
3363
3777
|
if (!isLoopbackRequest(ctx)) {
|
|
3364
3778
|
ctx.status = 403;
|