@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21
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/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +20 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/index.html +2 -2
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +22 -11
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +194 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +37 -6
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +87 -4
- package/dist/control-link-store.js +4 -3
- package/dist/daemon-owner.js +4 -7
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +158 -45
- package/dist/db.d.ts +2 -2
- package/dist/db.js +55 -20
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -87
- package/dist/lifecycle.d.ts +13 -0
- package/dist/lifecycle.js +19 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/migrations/cleanup-legacy-sessions.js +4 -3
- package/dist/plugin-cli.d.ts +10 -0
- package/dist/plugin-cli.js +19 -2
- package/dist/plugin-host-rpc.d.ts +35 -6
- package/dist/plugin-host-rpc.js +222 -59
- package/dist/plugin-installer.js +62 -2
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-marketplace.js +2 -1
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.d.ts +5 -0
- package/dist/plugin-supervisor.js +218 -33
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +116 -19
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-target-store.d.ts +2 -2
- package/dist/remote-runtime-target-store.js +6 -6
- package/dist/session-emulator-binding-store.js +4 -6
- package/dist/session-fork-store.js +24 -8
- package/dist/session-launch-operations.d.ts +10 -1
- package/dist/session-launch-operations.js +21 -11
- package/dist/session-log-store.js +2 -1
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +25 -2
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +85 -0
- package/dist/session-resource-store.js +3 -2
- package/dist/setup-service.d.ts +21 -0
- package/dist/setup-service.js +39 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -10
- package/dist/sqlite.d.ts +22 -0
- package/dist/sqlite.js +57 -0
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +37 -21
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -550
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -386
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/pm2.js
CHANGED
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { mkdirSync, realpathSync } from "node:fs";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { userInfo } from "node:os";
|
|
14
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
15
15
|
import { loadConfig, rynxHome } from "@rynx-ai/core";
|
|
16
16
|
import { daemonEntryRestartRequired } from "./daemon-build-status.js";
|
|
17
|
+
import { resolveDaemonEntry } from "./entry-path.js";
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
/** pm2 app name — single resident daemon per machine. */
|
|
19
20
|
const APP_NAME = "rynx";
|
|
20
21
|
const PM2_COMMAND_TIMEOUT_MS = 5_000;
|
|
22
|
+
const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
|
|
23
|
+
const LOGIN_SHELL_PATH_MAX_BYTES = 256 * 1024;
|
|
24
|
+
const REFRESH_LOGIN_SHELL_PATH_ENV = "RYNX_REFRESH_LOGIN_SHELL_PATH";
|
|
21
25
|
const DAEMON_PACKAGE_VERSION = require("../package.json").version;
|
|
22
26
|
/**
|
|
23
27
|
* Build a dialable control-console URL from the daemon's bind host/port. Bind-all
|
|
@@ -49,10 +53,6 @@ function pm2Bin() {
|
|
|
49
53
|
// pm2 ships its CLI at bin/pm2; run it with our own node.
|
|
50
54
|
return require.resolve("pm2/bin/pm2");
|
|
51
55
|
}
|
|
52
|
-
/** `index-daemon.js` sits next to this file in `dist/`. */
|
|
53
|
-
function daemonEntryPath() {
|
|
54
|
-
return join(dirname(fileURLToPath(import.meta.url)), "index-daemon.js");
|
|
55
|
-
}
|
|
56
56
|
function logsDir() {
|
|
57
57
|
const dir = join(rynxHome(), "logs");
|
|
58
58
|
mkdirSync(dir, { recursive: true });
|
|
@@ -71,11 +71,58 @@ function nodeRuntimeEnv() {
|
|
|
71
71
|
: {}),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* A detached plugin updater intentionally receives a reduced environment and
|
|
76
|
+
* therefore has no SHELL. Starting the replacement daemon with that reduced
|
|
77
|
+
* PATH would make user-installed runtimes (for example an fnm-managed Codex)
|
|
78
|
+
* disappear after every upgrade. Recover the user's login-shell PATH only at
|
|
79
|
+
* this boundary; normal interactive starts keep their exact caller environment.
|
|
80
|
+
*/
|
|
81
|
+
function daemonStartEnv() {
|
|
82
|
+
const env = nodeRuntimeEnv();
|
|
83
|
+
const refreshLoginPath = env[REFRESH_LOGIN_SHELL_PATH_ENV] === "1";
|
|
84
|
+
delete env[REFRESH_LOGIN_SHELL_PATH_ENV];
|
|
85
|
+
if (!refreshLoginPath)
|
|
86
|
+
return env;
|
|
87
|
+
const shell = userInfo().shell?.trim();
|
|
88
|
+
if (!shell) {
|
|
89
|
+
throw new Error("cannot refresh daemon PATH: the user login shell is unavailable");
|
|
90
|
+
}
|
|
91
|
+
const result = spawnSync(shell, ["-lic", `printf '\\036%s\\037' "$PATH"`], {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
// Do not feed an fnm/nvm "multishell" directory inherited from the old
|
|
94
|
+
// daemon back into the next login shell. Version managers prepend a new
|
|
95
|
+
// directory on every shell startup, so reusing the old PATH would grow
|
|
96
|
+
// it indefinitely across upgrades.
|
|
97
|
+
env: { ...env, SHELL: shell, PATH: loginShellBaselinePath() },
|
|
98
|
+
timeout: LOGIN_SHELL_PATH_TIMEOUT_MS,
|
|
99
|
+
killSignal: "SIGTERM",
|
|
100
|
+
maxBuffer: LOGIN_SHELL_PATH_MAX_BYTES,
|
|
101
|
+
});
|
|
102
|
+
if (result.error) {
|
|
103
|
+
throw new Error(`cannot refresh daemon PATH: ${result.error.message}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.status !== 0) {
|
|
106
|
+
throw new Error(`cannot refresh daemon PATH: login shell exited ${result.status ?? "without status"}`);
|
|
107
|
+
}
|
|
108
|
+
const path = extractFramedPath(result.stdout);
|
|
109
|
+
if (!path) {
|
|
110
|
+
throw new Error("cannot refresh daemon PATH: login shell returned no framed PATH");
|
|
111
|
+
}
|
|
112
|
+
const npmPrefix = env.NPM_CONFIG_PREFIX?.trim();
|
|
113
|
+
const npmBin = npmPrefix && isAbsolute(npmPrefix)
|
|
114
|
+
? join(npmPrefix, "bin")
|
|
115
|
+
: "";
|
|
116
|
+
const refreshedPath = npmBin === ""
|
|
117
|
+
? path
|
|
118
|
+
: [npmBin, ...path.split(delimiter).filter((entry) => entry !== npmBin)].join(delimiter);
|
|
119
|
+
return { ...env, PATH: refreshedPath, SHELL: shell };
|
|
120
|
+
}
|
|
74
121
|
/** Run a pm2 command inheriting stdio; returns its exit code. */
|
|
75
|
-
function runPm2(args, quiet = false) {
|
|
122
|
+
function runPm2(args, quiet = false, env = nodeRuntimeEnv()) {
|
|
76
123
|
const res = spawnSync(process.execPath, [pm2Bin(), ...args], {
|
|
77
124
|
stdio: quiet ? "ignore" : "inherit",
|
|
78
|
-
env
|
|
125
|
+
env,
|
|
79
126
|
timeout: PM2_COMMAND_TIMEOUT_MS,
|
|
80
127
|
killSignal: "SIGTERM",
|
|
81
128
|
});
|
|
@@ -117,7 +164,7 @@ function pm2ErrorMessage(error) {
|
|
|
117
164
|
* entry from scratch (delete + start) so config flags are always fresh; the
|
|
118
165
|
* `force === false` path short-circuits when it's already online.
|
|
119
166
|
*/
|
|
120
|
-
export function startDaemon({ force, quiet = false }) {
|
|
167
|
+
export function startDaemon({ force, quiet = false, launchEnvironment, }) {
|
|
121
168
|
let existing;
|
|
122
169
|
try {
|
|
123
170
|
existing = describe();
|
|
@@ -139,6 +186,20 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
139
186
|
if (staleBuild && !quiet) {
|
|
140
187
|
console.log("A newer daemon build is present on disk; restarting Rynx.");
|
|
141
188
|
}
|
|
189
|
+
let startEnv;
|
|
190
|
+
try {
|
|
191
|
+
// Resolve the replacement environment before deleting an existing app.
|
|
192
|
+
// A broken/hung login shell must leave the known-good daemon untouched.
|
|
193
|
+
startEnv = {
|
|
194
|
+
...daemonStartEnv(),
|
|
195
|
+
...launchEnvironment,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (!quiet)
|
|
200
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
142
203
|
const dir = logsDir();
|
|
143
204
|
if (existing) {
|
|
144
205
|
const removed = runPm2(["delete", APP_NAME], quiet);
|
|
@@ -147,7 +208,7 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
147
208
|
}
|
|
148
209
|
const status = runPm2([
|
|
149
210
|
"start",
|
|
150
|
-
|
|
211
|
+
resolveDaemonEntry(),
|
|
151
212
|
"--name",
|
|
152
213
|
APP_NAME,
|
|
153
214
|
// App and CLI share one daemon-owned state/workspace root.
|
|
@@ -163,7 +224,7 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
163
224
|
join(dir, "err.log"),
|
|
164
225
|
"--merge-logs",
|
|
165
226
|
"--time",
|
|
166
|
-
], quiet);
|
|
227
|
+
], quiet, startEnv);
|
|
167
228
|
if (status === 0 && !quiet) {
|
|
168
229
|
console.log(`rynx ${force ? "restarted" : "started"} (pm2 app "${APP_NAME}", logs: ${dir}).`);
|
|
169
230
|
console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
|
|
@@ -173,16 +234,35 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
173
234
|
export function isPm2DaemonOnline() {
|
|
174
235
|
return describe()?.pm2_env?.status === "online";
|
|
175
236
|
}
|
|
176
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Stop the daemon and terminate its dedicated pm2 supervisor.
|
|
239
|
+
*
|
|
240
|
+
* PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
|
|
241
|
+
* applications. Leaving it alive across an npm replacement is unsafe: the
|
|
242
|
+
* daemon process container path is resolved inside the old pm2 package. Once
|
|
243
|
+
* npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
|
|
244
|
+
* subsequent `rynx start` enters a restart loop before our code can run.
|
|
245
|
+
*/
|
|
177
246
|
export function stopDaemon() {
|
|
178
|
-
|
|
247
|
+
const existing = describe();
|
|
248
|
+
if (!existing) {
|
|
249
|
+
// Even an empty process list can belong to a resident PM2 daemon whose
|
|
250
|
+
// module path is about to be replaced. `pm2 kill` is idempotent and is
|
|
251
|
+
// the only reliable package-upgrade boundary.
|
|
252
|
+
const killed = runPm2(["kill"], true);
|
|
253
|
+
if (killed !== 0)
|
|
254
|
+
return killed;
|
|
179
255
|
console.log("rynx is not running.");
|
|
180
256
|
return 0;
|
|
181
257
|
}
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
258
|
+
const removed = runPm2(["delete", APP_NAME]);
|
|
259
|
+
if (removed !== 0)
|
|
260
|
+
return removed;
|
|
261
|
+
const killed = runPm2(["kill"]);
|
|
262
|
+
if (killed !== 0)
|
|
263
|
+
return killed;
|
|
264
|
+
console.log("rynx stopped.");
|
|
265
|
+
return 0;
|
|
186
266
|
}
|
|
187
267
|
/** Print a one-line status; exit 0 only when online. */
|
|
188
268
|
export function statusDaemon() {
|
|
@@ -209,7 +289,7 @@ export function streamLogs() {
|
|
|
209
289
|
}
|
|
210
290
|
function residentBuildRestartRequired(app) {
|
|
211
291
|
const runningEntry = app.pm2_env?.pm_exec_path;
|
|
212
|
-
if (runningEntry && !sameEntryPath(runningEntry,
|
|
292
|
+
if (runningEntry && !sameEntryPath(runningEntry, resolveDaemonEntry()))
|
|
213
293
|
return true;
|
|
214
294
|
const runningVersion = app.pm2_env?.version;
|
|
215
295
|
if (runningVersion &&
|
|
@@ -220,7 +300,7 @@ function residentBuildRestartRequired(app) {
|
|
|
220
300
|
const startedAt = app.pm2_env?.pm_uptime;
|
|
221
301
|
return typeof startedAt === "number" &&
|
|
222
302
|
Number.isFinite(startedAt) &&
|
|
223
|
-
daemonEntryRestartRequired(
|
|
303
|
+
daemonEntryRestartRequired(resolveDaemonEntry(), startedAt);
|
|
224
304
|
}
|
|
225
305
|
function sameEntryPath(left, right) {
|
|
226
306
|
try {
|
|
@@ -230,3 +310,20 @@ function sameEntryPath(left, right) {
|
|
|
230
310
|
return resolve(left) === resolve(right);
|
|
231
311
|
}
|
|
232
312
|
}
|
|
313
|
+
function extractFramedPath(stdout) {
|
|
314
|
+
if (!stdout)
|
|
315
|
+
return undefined;
|
|
316
|
+
const start = stdout.lastIndexOf("\x1e");
|
|
317
|
+
if (start < 0)
|
|
318
|
+
return undefined;
|
|
319
|
+
const end = stdout.indexOf("\x1f", start + 1);
|
|
320
|
+
if (end < 0)
|
|
321
|
+
return undefined;
|
|
322
|
+
const value = stdout.slice(start + 1, end);
|
|
323
|
+
return value.trim() ? value : undefined;
|
|
324
|
+
}
|
|
325
|
+
function loginShellBaselinePath() {
|
|
326
|
+
return process.platform === "darwin"
|
|
327
|
+
? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
328
|
+
: "/usr/local/bin:/usr/bin:/bin";
|
|
329
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface SupervisorProcess {
|
|
2
|
+
connected?: boolean;
|
|
3
|
+
disconnect?: () => void;
|
|
4
|
+
}
|
|
5
|
+
/** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
|
|
6
|
+
export declare function disconnectSupervisorIpc(target?: SupervisorProcess): void;
|
|
7
|
+
/**
|
|
8
|
+
* Run the daemon in the current process. Process ownership and executable
|
|
9
|
+
* selection belong to the caller (CLI standalone lifecycle or App lifecycle).
|
|
10
|
+
*/
|
|
11
|
+
export declare function runDaemonProcess(): Promise<void>;
|
|
12
|
+
export {};
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { startRynxDaemonServer } from "./daemon-server.js";
|
|
2
|
+
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
3
|
+
import { installStdioEpipeGuard } from "./stdio-epipe-guard.js";
|
|
4
|
+
/** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
|
|
5
|
+
export function disconnectSupervisorIpc(target = process) {
|
|
6
|
+
if (target.connected && typeof target.disconnect === "function") {
|
|
7
|
+
target.disconnect();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run the daemon in the current process. Process ownership and executable
|
|
12
|
+
* selection belong to the caller (CLI standalone lifecycle or App lifecycle).
|
|
13
|
+
*/
|
|
14
|
+
export async function runDaemonProcess() {
|
|
15
|
+
installStdioEpipeGuard();
|
|
16
|
+
const followsApp = process.env.RYNX_DAEMON_LIFECYCLE === "follow-app";
|
|
17
|
+
let parentDisconnected = followsApp && !process.connected;
|
|
18
|
+
const recordParentDisconnect = () => {
|
|
19
|
+
parentDisconnected = true;
|
|
20
|
+
};
|
|
21
|
+
if (followsApp)
|
|
22
|
+
process.once("disconnect", recordParentDisconnect);
|
|
23
|
+
const preparePlugins = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const bundled = await ensureBundledOfficialPlugins({ reclaimHostLeases: true });
|
|
26
|
+
if (bundled.status === "ready") {
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
level: "info",
|
|
29
|
+
type: "bundled_plugins_ready",
|
|
30
|
+
installed: bundled.installed,
|
|
31
|
+
updated: bundled.updated,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(JSON.stringify({
|
|
37
|
+
level: "error",
|
|
38
|
+
type: "bundled_plugins_failed",
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
let server;
|
|
44
|
+
try {
|
|
45
|
+
server = await startRynxDaemonServer({
|
|
46
|
+
preparePlugins,
|
|
47
|
+
onShutdownComplete: () => disconnectSupervisorIpc(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
process.off("disconnect", recordParentDisconnect);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
let shutdownPromise;
|
|
55
|
+
const shutdown = (signal) => {
|
|
56
|
+
if (shutdownPromise)
|
|
57
|
+
return shutdownPromise;
|
|
58
|
+
process.off("SIGINT", onSigint);
|
|
59
|
+
process.off("SIGTERM", onSigterm);
|
|
60
|
+
process.off("disconnect", onDisconnect);
|
|
61
|
+
shutdownPromise = (async () => {
|
|
62
|
+
console.log(JSON.stringify({ level: "info", type: "shutdown", signal }));
|
|
63
|
+
await server.shutdown();
|
|
64
|
+
})();
|
|
65
|
+
shutdownPromise.catch((error) => {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
console.error(`Shutdown failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
68
|
+
});
|
|
69
|
+
return shutdownPromise;
|
|
70
|
+
};
|
|
71
|
+
const onSigint = () => {
|
|
72
|
+
void shutdown("SIGINT");
|
|
73
|
+
};
|
|
74
|
+
const onSigterm = () => {
|
|
75
|
+
void shutdown("SIGTERM");
|
|
76
|
+
};
|
|
77
|
+
const onDisconnect = () => {
|
|
78
|
+
if (process.env.RYNX_DAEMON_LIFECYCLE === "follow-app") {
|
|
79
|
+
void shutdown("APP_DISCONNECT");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
process.once("SIGINT", onSigint);
|
|
83
|
+
process.once("SIGTERM", onSigterm);
|
|
84
|
+
if (followsApp) {
|
|
85
|
+
process.once("disconnect", onDisconnect);
|
|
86
|
+
process.off("disconnect", recordParentDisconnect);
|
|
87
|
+
if (parentDisconnected || !process.connected) {
|
|
88
|
+
void shutdown("APP_DISCONNECT");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type DatabaseSync } from "node:sqlite";
|
|
2
2
|
declare const DEFAULT_AUTHORITIES: readonly ["daemon.full"];
|
|
3
3
|
export type RuntimeAuthority = (typeof DEFAULT_AUTHORITIES)[number];
|
|
4
4
|
export type RemoteRuntimeAccessErrorCode = "invalid_claim" | "claim_expired" | "claim_used" | "grant_not_found" | "grant_revoked" | "grant_identity_mismatch" | "conflict" | "corrupt_state";
|
|
@@ -46,7 +46,7 @@ type Clock = () => Date;
|
|
|
46
46
|
export declare class RemoteRuntimeAccessStore {
|
|
47
47
|
private readonly connection;
|
|
48
48
|
private readonly clock;
|
|
49
|
-
constructor(connection?:
|
|
49
|
+
constructor(connection?: DatabaseSync, clock?: Clock);
|
|
50
50
|
createPairingClaim(input: CreatePairingClaimInput): RuntimePairingClaim;
|
|
51
51
|
redeemPairingClaim(input: RedeemPairingClaimInput): RuntimeClientGrant;
|
|
52
52
|
resolveActiveGrant(input: ResolveActiveGrantInput): RuntimeClientGrant;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, createPublicKey, randomBytes, timingSafeEqual, } from "node:crypto";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
|
|
3
4
|
const CLAIM_SECRET_HASH_DOMAIN = "rynx-runtime-pairing-claim-secret-v1\0";
|
|
4
5
|
const DEFAULT_AUTHORITIES = Object.freeze(["daemon.full"]);
|
|
5
6
|
const MIN_TTL_MS = 1_000;
|
|
@@ -35,7 +36,7 @@ export class RemoteRuntimeAccessStore {
|
|
|
35
36
|
const created = this.now();
|
|
36
37
|
const createdAt = created.toISOString();
|
|
37
38
|
const expiresAt = new Date(created.getTime() + ttlMs).toISOString();
|
|
38
|
-
const create = this.connection
|
|
39
|
+
const create = transaction(this.connection, () => {
|
|
39
40
|
this.deleteRetainedExpiredClaims(created);
|
|
40
41
|
this.connection
|
|
41
42
|
.prepare(`INSERT INTO runtime_pairing_claims
|
|
@@ -60,7 +61,7 @@ export class RemoteRuntimeAccessStore {
|
|
|
60
61
|
const clientId = validId(input.clientId, "clientId", "invalid_claim");
|
|
61
62
|
const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "invalid_claim");
|
|
62
63
|
const requestedLabel = optionalLabel(input.label, "invalid_claim");
|
|
63
|
-
const redeem = this.connection
|
|
64
|
+
const redeem = transaction(this.connection, () => {
|
|
64
65
|
const now = this.now().toISOString();
|
|
65
66
|
const claim = this.connection
|
|
66
67
|
.prepare(`SELECT claim_id, secret_hash, authorities, client_label, created_at, expires_at,
|
|
@@ -124,7 +125,7 @@ export class RemoteRuntimeAccessStore {
|
|
|
124
125
|
const grantId = validId(input.grantId, "grantId", "grant_not_found");
|
|
125
126
|
const clientId = validId(input.clientId, "clientId", "grant_identity_mismatch");
|
|
126
127
|
const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "grant_identity_mismatch");
|
|
127
|
-
const resolve = this.connection
|
|
128
|
+
const resolve = transaction(this.connection, () => {
|
|
128
129
|
const row = this.connection
|
|
129
130
|
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
130
131
|
created_at, revoked_at, last_used_at
|
|
@@ -180,7 +181,7 @@ export class RemoteRuntimeAccessStore {
|
|
|
180
181
|
revokeClientGrant(grantIdInput) {
|
|
181
182
|
const grantId = validId(grantIdInput, "grantId", "grant_not_found");
|
|
182
183
|
const revokedAt = this.now().toISOString();
|
|
183
|
-
const revoke = this.connection
|
|
184
|
+
const revoke = transaction(this.connection, () => {
|
|
184
185
|
const revoked = this.connection
|
|
185
186
|
.prepare(`UPDATE runtime_client_grants
|
|
186
187
|
SET revoked_at = ?
|
|
@@ -382,11 +383,7 @@ function grantFromRow(row) {
|
|
|
382
383
|
};
|
|
383
384
|
}
|
|
384
385
|
function isSqliteConstraint(error) {
|
|
385
|
-
return (
|
|
386
|
-
error !== null &&
|
|
387
|
-
"code" in error &&
|
|
388
|
-
typeof error.code === "string" &&
|
|
389
|
-
error.code.startsWith("SQLITE_CONSTRAINT"));
|
|
386
|
+
return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
|
|
390
387
|
}
|
|
391
388
|
function accessError(code, message, cause) {
|
|
392
389
|
return new RemoteRuntimeAccessError(code, message, cause === undefined ? undefined : { cause });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type DatabaseSync } from "node:sqlite";
|
|
2
2
|
export interface RemoteRuntimeTarget {
|
|
3
3
|
daemonId: string;
|
|
4
4
|
displayName: string;
|
|
@@ -40,7 +40,7 @@ export interface CommitRemoteRuntimePendingEnrollmentOptions {
|
|
|
40
40
|
export declare class RemoteRuntimeTargetStore {
|
|
41
41
|
private readonly connection;
|
|
42
42
|
private readonly clock;
|
|
43
|
-
constructor(connection?:
|
|
43
|
+
constructor(connection?: DatabaseSync, clock?: () => Date);
|
|
44
44
|
add(input: AddRemoteRuntimeTargetInput): RemoteRuntimeTarget;
|
|
45
45
|
addPendingEnrollment(input: AddRemoteRuntimePendingEnrollmentInput): RemoteRuntimePendingEnrollment;
|
|
46
46
|
getPendingEnrollment(claimIdInput: string): RemoteRuntimePendingEnrollment | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createPublicKey } from "node:crypto";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
|
|
3
4
|
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
|
|
4
5
|
const CREDENTIAL_REF_PATTERN = /^credential_[A-Za-z0-9_-]{24}$/;
|
|
5
6
|
const MAX_DISPLAY_NAME_CHARS = 128;
|
|
@@ -13,7 +14,7 @@ export class RemoteRuntimeTargetStore {
|
|
|
13
14
|
add(input) {
|
|
14
15
|
const normalized = normalizeTargetInput(input);
|
|
15
16
|
const now = validNow(this.clock()).toISOString();
|
|
16
|
-
const insert = this.connection
|
|
17
|
+
const insert = transaction(this.connection, () => {
|
|
17
18
|
this.connection.prepare(`INSERT INTO runtime_targets
|
|
18
19
|
(daemon_id, display_name, identity_public_key, credential_ref, created_at, updated_at)
|
|
19
20
|
VALUES (?, ?, ?, ?, ?, ?)`).run(normalized.daemonId, normalized.displayName, normalized.identityPublicKey, normalized.credentialRef, now, now);
|
|
@@ -67,7 +68,7 @@ export class RemoteRuntimeTargetStore {
|
|
|
67
68
|
}
|
|
68
69
|
removePendingEnrollment(claimIdInput) {
|
|
69
70
|
const claimId = validId(claimIdInput, "claimId");
|
|
70
|
-
const remove = this.connection
|
|
71
|
+
const remove = transaction(this.connection, () => {
|
|
71
72
|
const pending = this.getPendingEnrollment(claimId);
|
|
72
73
|
if (!pending)
|
|
73
74
|
return undefined;
|
|
@@ -82,7 +83,7 @@ export class RemoteRuntimeTargetStore {
|
|
|
82
83
|
const claimId = validId(claimIdInput, "claimId");
|
|
83
84
|
const normalized = normalizeTargetInput(input);
|
|
84
85
|
const now = validNow(this.clock()).toISOString();
|
|
85
|
-
const commit = this.connection
|
|
86
|
+
const commit = transaction(this.connection, () => {
|
|
86
87
|
const pending = this.getPendingEnrollment(claimId);
|
|
87
88
|
if (!pending)
|
|
88
89
|
throw new Error("pending Runtime enrollment was not found");
|
|
@@ -134,7 +135,7 @@ export class RemoteRuntimeTargetStore {
|
|
|
134
135
|
}
|
|
135
136
|
remove(daemonIdInput) {
|
|
136
137
|
const daemonId = validId(daemonIdInput, "daemonId");
|
|
137
|
-
const remove = this.connection
|
|
138
|
+
const remove = transaction(this.connection, () => {
|
|
138
139
|
const target = this.get(daemonId);
|
|
139
140
|
if (!target)
|
|
140
141
|
return undefined;
|
|
@@ -321,6 +322,5 @@ function validNow(value) {
|
|
|
321
322
|
return value;
|
|
322
323
|
}
|
|
323
324
|
function isSqliteConstraint(error) {
|
|
324
|
-
return
|
|
325
|
-
typeof error.code === "string" && error.code.startsWith("SQLITE_CONSTRAINT");
|
|
325
|
+
return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
|
|
326
326
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SessionEmulatorBindingConflictError, } from "@rynx-ai/server";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
|
|
3
4
|
export class SqliteSessionEmulatorBindingStore {
|
|
4
5
|
get(sessionId) {
|
|
5
6
|
const row = db().prepare(`SELECT session_id, binding_id, backend, device_id, device_name, attached_at
|
|
@@ -13,7 +14,7 @@ export class SqliteSessionEmulatorBindingStore {
|
|
|
13
14
|
}
|
|
14
15
|
bind(record) {
|
|
15
16
|
const connection = db();
|
|
16
|
-
const replace =
|
|
17
|
+
const replace = transaction(connection, () => {
|
|
17
18
|
connection.prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?")
|
|
18
19
|
.run(record.sessionId);
|
|
19
20
|
connection.prepare(`INSERT INTO session_emulator_bindings
|
|
@@ -40,7 +41,7 @@ export class SqliteSessionEmulatorBindingStore {
|
|
|
40
41
|
return db().prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?").run(sessionId).changes > 0;
|
|
41
42
|
}
|
|
42
43
|
releaseDevice(backend, deviceId) {
|
|
43
|
-
return db().prepare("DELETE FROM session_emulator_bindings WHERE backend = ? AND device_id = ?").run(backend, deviceId).changes;
|
|
44
|
+
return Number(db().prepare("DELETE FROM session_emulator_bindings WHERE backend = ? AND device_id = ?").run(backend, deviceId).changes);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
function bindingRecord(row) {
|
|
@@ -54,8 +55,5 @@ function bindingRecord(row) {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
function isSqliteConstraint(error) {
|
|
57
|
-
|
|
58
|
-
return false;
|
|
59
|
-
const code = error.code;
|
|
60
|
-
return typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT");
|
|
58
|
+
return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
|
|
61
59
|
}
|
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { newSessionId, newSessionItemId, normalizeSessionTitle, resolvedExecutionSnapshotSchema, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
5
5
|
import { MachineSessionServiceFailure, } from "@rynx-ai/server";
|
|
6
6
|
import { db } from "./db.js";
|
|
7
|
+
import { scalar, transaction } from "./sqlite.js";
|
|
7
8
|
const FORK_COLS = [
|
|
8
9
|
"operation_id",
|
|
9
10
|
"request_hash",
|
|
@@ -39,9 +40,8 @@ export class SqliteSessionForkStore {
|
|
|
39
40
|
}
|
|
40
41
|
const now = new Date().toISOString();
|
|
41
42
|
let targetSessionId = newSessionId();
|
|
42
|
-
while (db().prepare("SELECT 1 FROM sessions WHERE id = ?")
|
|
43
|
-
db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")
|
|
44
|
-
.pluck().get(targetSessionId)) {
|
|
43
|
+
while (scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?")).get(targetSessionId) ||
|
|
44
|
+
scalar(db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")).get(targetSessionId)) {
|
|
45
45
|
targetSessionId = newSessionId();
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
@@ -130,7 +130,7 @@ export class SqliteSessionForkStore {
|
|
|
130
130
|
};
|
|
131
131
|
});
|
|
132
132
|
const conn = db();
|
|
133
|
-
|
|
133
|
+
transaction(conn, () => {
|
|
134
134
|
if (input.operationId) {
|
|
135
135
|
const current = this.read(input.operationId);
|
|
136
136
|
if (current?.state === "completed")
|
|
@@ -160,8 +160,24 @@ export class SqliteSessionForkStore {
|
|
|
160
160
|
@byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
|
|
161
161
|
@message_item_id, @created_at, @committed_at
|
|
162
162
|
)`);
|
|
163
|
-
for (const resource of resources)
|
|
164
|
-
insertResource.run(
|
|
163
|
+
for (const resource of resources) {
|
|
164
|
+
insertResource.run({
|
|
165
|
+
id: resource.id,
|
|
166
|
+
session_id: resource.session_id,
|
|
167
|
+
client_upload_id: resource.client_upload_id,
|
|
168
|
+
upload_id: resource.upload_id,
|
|
169
|
+
filename: resource.filename,
|
|
170
|
+
media_type: resource.media_type,
|
|
171
|
+
byte_length: resource.byte_length,
|
|
172
|
+
uploaded_bytes: resource.uploaded_bytes,
|
|
173
|
+
sha256: resource.sha256,
|
|
174
|
+
width: resource.width,
|
|
175
|
+
height: resource.height,
|
|
176
|
+
message_item_id: resource.message_item_id,
|
|
177
|
+
created_at: resource.created_at,
|
|
178
|
+
committed_at: resource.committed_at,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
165
181
|
const insertItem = conn.prepare(`INSERT INTO session_items (
|
|
166
182
|
id, session_id, position, response_id, type, status,
|
|
167
183
|
data, created_by, created_at
|
|
@@ -203,9 +219,9 @@ export class SqliteSessionForkStore {
|
|
|
203
219
|
ORDER BY created_at`).all().map(toOperation);
|
|
204
220
|
}
|
|
205
221
|
hasReservedSource(sessionId) {
|
|
206
|
-
return Boolean(db().prepare(`SELECT 1
|
|
222
|
+
return Boolean(scalar(db().prepare(`SELECT 1
|
|
207
223
|
FROM session_fork_operations
|
|
208
|
-
WHERE source_session_id = ? AND state = 'reserved'`)
|
|
224
|
+
WHERE source_session_id = ? AND state = 'reserved'`)).get(sessionId));
|
|
209
225
|
}
|
|
210
226
|
markTargetDeleted(sessionId) {
|
|
211
227
|
const now = new Date().toISOString();
|
|
@@ -9,13 +9,22 @@ export interface EnsurePluginSessionLaunchInput {
|
|
|
9
9
|
operationId: string;
|
|
10
10
|
/** SHA-256 of the host-normalized, execution-affecting launch request. */
|
|
11
11
|
requestHash: string;
|
|
12
|
+
/** SHA-256 before mutable filesystem selectors are canonicalized. */
|
|
13
|
+
rawRequestHash?: string;
|
|
12
14
|
session: PluginSessionLaunchMeta;
|
|
13
15
|
}
|
|
14
16
|
export interface EnsurePluginSessionLaunchResult {
|
|
15
17
|
sessionId: string;
|
|
16
18
|
disposition: "created" | "replayed";
|
|
17
19
|
}
|
|
18
|
-
export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"
|
|
20
|
+
export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"> & {
|
|
21
|
+
/**
|
|
22
|
+
* A caller doing a pre-canonicalization lookup may treat a hash mismatch as
|
|
23
|
+
* a cache miss, then retry with the canonical hash. The canonical lookup
|
|
24
|
+
* must keep the default strict conflict behavior.
|
|
25
|
+
*/
|
|
26
|
+
allowRequestHashMismatch?: boolean;
|
|
27
|
+
};
|
|
19
28
|
export declare class SessionLaunchOperationConflictError extends Error {
|
|
20
29
|
readonly pluginId: string;
|
|
21
30
|
readonly scopeId: string;
|