@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +24 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6018
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
- 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.js +4 -1
- package/dist/chrome-for-testing-runtime.js +4 -3
- package/dist/chrome-for-testing-store.js +29 -7
- 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.js +25 -14
- package/dist/db.d.ts +2 -2
- package/dist/db.js +133 -21
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +3 -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 +31 -9
- package/dist/plugin-host-rpc.js +428 -80
- package/dist/plugin-installer.js +3 -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.js +0 -4
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-connection-manager.js +1 -1
- 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.d.ts +1 -0
- package/dist/session-fork-store.js +82 -28
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +19 -16
- 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.d.ts +20 -6
- package/dist/session-resource-store.js +364 -68
- package/dist/setup-service.d.ts +1 -0
- package/dist/setup-service.js +1 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +317 -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 +15 -12
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/plugin-installer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { MARKETPLACE_ID_PATTERN, pluginCliInvocationLeasePid, readPluginRuntimeLeases, removePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
|
|
5
5
|
import { rynxHome } from "@rynx-ai/core";
|
|
6
6
|
import { db } from "./db.js";
|
|
7
|
+
import { transaction } from "./sqlite.js";
|
|
7
8
|
import { isBuiltInPluginMarketplace } from "./plugin-marketplace.js";
|
|
8
9
|
import { detectPluginSource, inspectPluginPackage, materializePluginPackage, } from "./plugin-package.js";
|
|
9
10
|
import { canonicalPluginId, getPlugin, removePlugin, upsertPlugin, } from "./plugin-store.js";
|
|
@@ -117,7 +118,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
|
|
|
117
118
|
promotedByThisTransaction = true;
|
|
118
119
|
}
|
|
119
120
|
promotedPath = realpathSync(target);
|
|
120
|
-
db()
|
|
121
|
+
transaction(db(), () => {
|
|
121
122
|
const current = getPlugin(canonicalId);
|
|
122
123
|
if (!samePluginGeneration(existing, current)) {
|
|
123
124
|
throw new Error(`plugin "${canonicalId}" changed concurrently; retry`);
|
|
@@ -172,7 +173,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
|
|
|
172
173
|
}
|
|
173
174
|
export async function uninstallPlugin(identity, onProgress = () => { }, options = {}) {
|
|
174
175
|
let record;
|
|
175
|
-
const removed = db()
|
|
176
|
+
const removed = transaction(db(), () => {
|
|
176
177
|
record = getPlugin(identity);
|
|
177
178
|
if (!record)
|
|
178
179
|
return false;
|
|
@@ -295,7 +295,6 @@ function toManagementItem(record, settingsAvailable = false, updateState = "unkn
|
|
|
295
295
|
enabled: record.enabled,
|
|
296
296
|
runtimeState: record.manifest.runtime ? record.runtimeState : "absent",
|
|
297
297
|
...(record.runtimeError ? { runtimeError: record.runtimeError } : {}),
|
|
298
|
-
capabilities: [...record.manifest.capabilities],
|
|
299
298
|
hasSettingsUi: settingsAvailable,
|
|
300
299
|
updatedAt: record.updatedAt,
|
|
301
300
|
};
|
|
@@ -322,7 +321,6 @@ function toInstallPreparation(token, expiresAtMs, prepared) {
|
|
|
322
321
|
subdirectory: prepared.requestedSource.subdir ?? ".",
|
|
323
322
|
},
|
|
324
323
|
digest: prepared.integrity,
|
|
325
|
-
capabilities: [...prepared.manifest.capabilities],
|
|
326
324
|
hasRuntime: Boolean(prepared.manifest.runtime),
|
|
327
325
|
hasSettingsUi: Boolean(prepared.manifest.settings),
|
|
328
326
|
...(prepared.manifest.runtime ? {
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
|
3
3
|
import { MARKETPLACE_ID_PATTERN, parsePluginMarketplace, } from "@rynx-ai/plugin-sdk";
|
|
4
4
|
import { rynxHome } from "@rynx-ai/core";
|
|
5
5
|
import { db } from "./db.js";
|
|
6
|
+
import { transaction } from "./sqlite.js";
|
|
6
7
|
import { detectPluginSource, materializePluginPackage, parsePluginSourceSpec, pluginSourceFingerprint, } from "./plugin-package.js";
|
|
7
8
|
export function listPluginMarketplaces() {
|
|
8
9
|
return db().prepare("SELECT * FROM plugin_marketplaces ORDER BY marketplace_id").all().map(toRecord);
|
|
@@ -27,7 +28,7 @@ export async function addPluginMarketplace(spec, options = {}) {
|
|
|
27
28
|
throw new Error(`built-in marketplace "${marketplaceId}" is managed by Rynx`);
|
|
28
29
|
}
|
|
29
30
|
const now = new Date().toISOString();
|
|
30
|
-
db()
|
|
31
|
+
transaction(db(), () => {
|
|
31
32
|
const binding = db().prepare(`SELECT source_fingerprint AS sourceFingerprint
|
|
32
33
|
FROM plugin_marketplace_bindings
|
|
33
34
|
WHERE marketplace_id = ?`).get(marketplaceId);
|
package/dist/plugin-store.js
CHANGED
|
@@ -57,11 +57,11 @@ export function upsertPlugin(rec) {
|
|
|
57
57
|
db().prepare(`INSERT INTO plugin_installations (
|
|
58
58
|
canonical_id, marketplace_id, plugin_id, source_type, requested_source,
|
|
59
59
|
resolved_revision, source_fingerprint, version, enabled, installed_at,
|
|
60
|
-
updated_at, install_path, manifest, integrity,
|
|
60
|
+
updated_at, install_path, manifest, integrity, runtime_state, runtime_error
|
|
61
61
|
) VALUES (
|
|
62
62
|
@canonicalId, @marketplaceId, @pluginId, @source, @requestedSource,
|
|
63
63
|
@resolvedRevision, @sourceFingerprint, @version, @enabled, @now,
|
|
64
|
-
@now, @installPath, @manifest, @integrity,
|
|
64
|
+
@now, @installPath, @manifest, @integrity, 'stopped', NULL
|
|
65
65
|
)
|
|
66
66
|
ON CONFLICT(canonical_id) DO UPDATE SET
|
|
67
67
|
source_type = excluded.source_type,
|
|
@@ -74,7 +74,6 @@ export function upsertPlugin(rec) {
|
|
|
74
74
|
install_path = excluded.install_path,
|
|
75
75
|
manifest = excluded.manifest,
|
|
76
76
|
integrity = excluded.integrity,
|
|
77
|
-
capabilities = excluded.capabilities,
|
|
78
77
|
runtime_state = 'stopped',
|
|
79
78
|
runtime_error = NULL`).run({
|
|
80
79
|
canonicalId: rec.canonicalId,
|
|
@@ -90,7 +89,6 @@ export function upsertPlugin(rec) {
|
|
|
90
89
|
installPath: rec.installPath,
|
|
91
90
|
manifest: JSON.stringify(rec.manifest),
|
|
92
91
|
integrity: rec.integrity,
|
|
93
|
-
capabilities: JSON.stringify(rec.manifest.capabilities),
|
|
94
92
|
});
|
|
95
93
|
}
|
|
96
94
|
export function setPluginEnabled(identity, enabled) {
|
|
@@ -294,7 +294,6 @@ export class PluginSupervisor {
|
|
|
294
294
|
daemonInstanceId: this.daemonInstanceId,
|
|
295
295
|
productVersion: this.productVersion,
|
|
296
296
|
cliVersion: this.cliVersion,
|
|
297
|
-
capabilities: state.record.manifest.capabilities,
|
|
298
297
|
services: this.services,
|
|
299
298
|
...(state.record.pluginId === "workspace"
|
|
300
299
|
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
@@ -324,7 +323,6 @@ export class PluginSupervisor {
|
|
|
324
323
|
role: "runtime",
|
|
325
324
|
dataDir,
|
|
326
325
|
cacheDir,
|
|
327
|
-
capabilities: state.record.manifest.capabilities,
|
|
328
326
|
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
329
327
|
onStderr: (line) => this.log({
|
|
330
328
|
level: "info",
|
|
@@ -411,7 +409,6 @@ export class PluginSupervisor {
|
|
|
411
409
|
daemonInstanceId: this.daemonInstanceId,
|
|
412
410
|
productVersion: this.productVersion,
|
|
413
411
|
cliVersion: this.cliVersion,
|
|
414
|
-
capabilities: state.record.manifest.capabilities,
|
|
415
412
|
services: this.services,
|
|
416
413
|
...(state.record.pluginId === "workspace"
|
|
417
414
|
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
@@ -440,7 +437,6 @@ export class PluginSupervisor {
|
|
|
440
437
|
role: "settings",
|
|
441
438
|
dataDir,
|
|
442
439
|
cacheDir,
|
|
443
|
-
capabilities: state.record.manifest.capabilities,
|
|
444
440
|
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
445
441
|
onStderr: (line) => this.log({
|
|
446
442
|
level: "info",
|
|
@@ -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 });
|
|
@@ -1465,7 +1465,7 @@ function nonNegativeInteger(value, fallback, field) {
|
|
|
1465
1465
|
return value;
|
|
1466
1466
|
}
|
|
1467
1467
|
function parseCallTimeout(value) {
|
|
1468
|
-
if (value === undefined)
|
|
1468
|
+
if (value === undefined || value === null)
|
|
1469
1469
|
return undefined;
|
|
1470
1470
|
if (!Number.isSafeInteger(value) ||
|
|
1471
1471
|
value < MIN_CALL_TIMEOUT_MS ||
|
|
@@ -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
|
}
|
|
@@ -30,6 +30,7 @@ export declare class SqliteSessionForkStore implements MachineSessionForkStorePo
|
|
|
30
30
|
items: SessionItem[];
|
|
31
31
|
}): void;
|
|
32
32
|
private publishTarget;
|
|
33
|
+
private publishTargetUnchecked;
|
|
33
34
|
markError(operationId: string, error: string): void;
|
|
34
35
|
listReserved(): MachineSessionForkOperationRecord[];
|
|
35
36
|
hasReservedSource(sessionId: string): boolean;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, } from "node:fs";
|
|
3
|
+
import { extname, 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 {
|
|
@@ -94,6 +94,19 @@ export class SqliteSessionForkStore {
|
|
|
94
94
|
this.publishTarget(input);
|
|
95
95
|
}
|
|
96
96
|
publishTarget(input) {
|
|
97
|
+
try {
|
|
98
|
+
this.publishTargetUnchecked(input);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const published = scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?"))
|
|
102
|
+
.get(input.target.id);
|
|
103
|
+
if (!published) {
|
|
104
|
+
rmSync(this.sessionDirectory(input.target.id), { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
publishTargetUnchecked(input) {
|
|
97
110
|
const itemIds = new Map();
|
|
98
111
|
for (const item of input.items)
|
|
99
112
|
itemIds.set(item.id, newSessionItemId(item.type));
|
|
@@ -130,7 +143,7 @@ export class SqliteSessionForkStore {
|
|
|
130
143
|
};
|
|
131
144
|
});
|
|
132
145
|
const conn = db();
|
|
133
|
-
|
|
146
|
+
transaction(conn, () => {
|
|
134
147
|
if (input.operationId) {
|
|
135
148
|
const current = this.read(input.operationId);
|
|
136
149
|
if (current?.state === "completed")
|
|
@@ -152,16 +165,33 @@ export class SqliteSessionForkStore {
|
|
|
152
165
|
source, created_at, updated_at
|
|
153
166
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.target.id, JSON.stringify(sessionWorkspaceSnapshotSchema.parse(input.target.workspace)), JSON.stringify(resolvedExecutionSnapshotSchema.parse(input.target.execution)), normalizeSessionTitle(input.target.title) ?? null, input.target.forkedFromSessionId ?? null, input.target.source, input.target.createdAt, input.target.updatedAt ?? input.target.createdAt);
|
|
154
167
|
const insertResource = conn.prepare(`INSERT INTO session_resources (
|
|
155
|
-
id, session_id, client_upload_id, upload_id, filename, media_type,
|
|
168
|
+
id, session_id, client_upload_id, upload_id, kind, filename, media_type,
|
|
156
169
|
byte_length, uploaded_bytes, sha256, width, height, state,
|
|
157
170
|
message_item_id, created_at, committed_at
|
|
158
171
|
) VALUES (
|
|
159
|
-
@id, @session_id, @client_upload_id, @upload_id, @filename, @media_type,
|
|
172
|
+
@id, @session_id, @client_upload_id, @upload_id, @kind, @filename, @media_type,
|
|
160
173
|
@byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
|
|
161
174
|
@message_item_id, @created_at, @committed_at
|
|
162
175
|
)`);
|
|
163
|
-
for (const resource of resources)
|
|
164
|
-
insertResource.run(
|
|
176
|
+
for (const resource of resources) {
|
|
177
|
+
insertResource.run({
|
|
178
|
+
id: resource.id,
|
|
179
|
+
session_id: resource.session_id,
|
|
180
|
+
client_upload_id: resource.client_upload_id,
|
|
181
|
+
upload_id: resource.upload_id,
|
|
182
|
+
kind: resource.kind,
|
|
183
|
+
filename: resource.filename,
|
|
184
|
+
media_type: resource.media_type,
|
|
185
|
+
byte_length: resource.byte_length,
|
|
186
|
+
uploaded_bytes: resource.uploaded_bytes,
|
|
187
|
+
sha256: resource.sha256,
|
|
188
|
+
width: resource.width,
|
|
189
|
+
height: resource.height,
|
|
190
|
+
message_item_id: resource.message_item_id,
|
|
191
|
+
created_at: resource.created_at,
|
|
192
|
+
committed_at: resource.committed_at,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
165
195
|
const insertItem = conn.prepare(`INSERT INTO session_items (
|
|
166
196
|
id, session_id, position, response_id, type, status,
|
|
167
197
|
data, created_by, created_at
|
|
@@ -203,9 +233,9 @@ export class SqliteSessionForkStore {
|
|
|
203
233
|
ORDER BY created_at`).all().map(toOperation);
|
|
204
234
|
}
|
|
205
235
|
hasReservedSource(sessionId) {
|
|
206
|
-
return Boolean(db().prepare(`SELECT 1
|
|
236
|
+
return Boolean(scalar(db().prepare(`SELECT 1
|
|
207
237
|
FROM session_fork_operations
|
|
208
|
-
WHERE source_session_id = ? AND state = 'reserved'`)
|
|
238
|
+
WHERE source_session_id = ? AND state = 'reserved'`)).get(sessionId));
|
|
209
239
|
}
|
|
210
240
|
markTargetDeleted(sessionId) {
|
|
211
241
|
const now = new Date().toISOString();
|
|
@@ -234,7 +264,7 @@ export class SqliteSessionForkStore {
|
|
|
234
264
|
return join(this.resourceRoot, key);
|
|
235
265
|
}
|
|
236
266
|
resourcePath(row) {
|
|
237
|
-
return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row
|
|
267
|
+
return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row)}`);
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
function toOperation(row) {
|
|
@@ -252,22 +282,40 @@ function toOperation(row) {
|
|
|
252
282
|
function referencedResourceIds(items) {
|
|
253
283
|
const ids = new Set();
|
|
254
284
|
for (const item of items) {
|
|
255
|
-
if (item.type
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
285
|
+
if (item.type === "message" && item.data.role === "user") {
|
|
286
|
+
for (const part of item.data.content) {
|
|
287
|
+
if ((part.type === "input_image" || part.type === "input_file") &&
|
|
288
|
+
typeof part.resourceId === "string")
|
|
289
|
+
ids.add(part.resourceId);
|
|
290
|
+
}
|
|
260
291
|
}
|
|
292
|
+
if (item.type === "function_call_output" &&
|
|
293
|
+
item.data.generatedImage &&
|
|
294
|
+
typeof item.data.generatedImage.resourceId === "string")
|
|
295
|
+
ids.add(item.data.generatedImage.resourceId);
|
|
261
296
|
}
|
|
262
297
|
return [...ids];
|
|
263
298
|
}
|
|
264
299
|
function remapItemResources(item, resources) {
|
|
265
300
|
const cloned = structuredClone(item);
|
|
266
|
-
if (cloned.type
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
301
|
+
if (cloned.type === "message" && cloned.data.role === "user") {
|
|
302
|
+
cloned.data.content = cloned.data.content.map((part) => {
|
|
303
|
+
if ((part.type === "input_image" || part.type === "input_file") &&
|
|
304
|
+
typeof part.resourceId === "string") {
|
|
305
|
+
return { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId };
|
|
306
|
+
}
|
|
307
|
+
return part;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (cloned.type === "function_call_output" &&
|
|
311
|
+
cloned.data.generatedImage &&
|
|
312
|
+
typeof cloned.data.generatedImage.resourceId === "string") {
|
|
313
|
+
const resourceId = cloned.data.generatedImage.resourceId;
|
|
314
|
+
cloned.data.generatedImage = {
|
|
315
|
+
...cloned.data.generatedImage,
|
|
316
|
+
resourceId: resources.get(resourceId) ?? resourceId,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
271
319
|
return cloned;
|
|
272
320
|
}
|
|
273
321
|
function deterministicId(prefix, targetSessionId, sourceId) {
|
|
@@ -276,13 +324,19 @@ function deterministicId(prefix, targetSessionId, sourceId) {
|
|
|
276
324
|
.digest("hex");
|
|
277
325
|
return `${prefix}_${digest.slice(0, 40)}`;
|
|
278
326
|
}
|
|
279
|
-
function extension(
|
|
280
|
-
|
|
281
|
-
|
|
327
|
+
function extension(row) {
|
|
328
|
+
if (row.kind === "image") {
|
|
329
|
+
if (row.media_type === "image/png")
|
|
282
330
|
return "png";
|
|
283
|
-
|
|
331
|
+
if (row.media_type === "image/jpeg")
|
|
284
332
|
return "jpg";
|
|
285
|
-
|
|
333
|
+
if (row.media_type === "image/webp")
|
|
286
334
|
return "webp";
|
|
335
|
+
if (row.media_type === "image/gif")
|
|
336
|
+
return "gif";
|
|
337
|
+
if (row.media_type === "image/svg+xml")
|
|
338
|
+
return "svg";
|
|
287
339
|
}
|
|
340
|
+
const value = extname(row.filename).slice(1).toLowerCase();
|
|
341
|
+
return /^[a-z0-9][a-z0-9._-]{0,15}$/.test(value) ? value : "bin";
|
|
288
342
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { newSessionId } from "@rynx-ai/core";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { scalar, transaction } from "./sqlite.js";
|
|
3
4
|
import { createSessionMeta } from "./session-meta-store.js";
|
|
4
5
|
const MAX_ID_LENGTH = 512;
|
|
5
6
|
const SHA256_PATTERN = /^[a-fA-F0-9]{64}$/;
|
|
@@ -58,7 +59,7 @@ export function replayPluginSessionLaunch(input) {
|
|
|
58
59
|
throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
|
|
59
60
|
}
|
|
60
61
|
if (!sessionExists(existing.session_id)) {
|
|
61
|
-
db()
|
|
62
|
+
transaction(db(), () => {
|
|
62
63
|
tombstoneOperations(existing.session_id, new Date().toISOString());
|
|
63
64
|
}).immediate();
|
|
64
65
|
throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
|
|
@@ -81,7 +82,7 @@ export function ensurePluginSessionLaunch(input) {
|
|
|
81
82
|
const rawRequestHash = input.rawRequestHash
|
|
82
83
|
? requiredRequestHash(input.rawRequestHash)
|
|
83
84
|
: requestHash;
|
|
84
|
-
const ensure = db()
|
|
85
|
+
const ensure = transaction(db(), () => {
|
|
85
86
|
const existing = readOperation(pluginId, scopeId, operationId);
|
|
86
87
|
if (existing) {
|
|
87
88
|
if (existing.request_hash !== requestHash &&
|
|
@@ -139,14 +140,14 @@ function readOperation(pluginId, scopeId, operationId) {
|
|
|
139
140
|
.get(pluginId, scopeId, operationId);
|
|
140
141
|
}
|
|
141
142
|
function sessionExists(sessionId) {
|
|
142
|
-
return Boolean(db().prepare("SELECT 1 FROM sessions WHERE id = ?")
|
|
143
|
+
return Boolean(scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?")).get(sessionId));
|
|
143
144
|
}
|
|
144
145
|
function tombstoneOperations(sessionId, now) {
|
|
145
|
-
return db()
|
|
146
|
+
return Number(db()
|
|
146
147
|
.prepare(`UPDATE plugin_session_launch_operations
|
|
147
148
|
SET state = 'deleted', updated_at = ?, deleted_at = ?
|
|
148
149
|
WHERE session_id = ? AND state = 'active'`)
|
|
149
|
-
.run(now, now, sessionId).changes;
|
|
150
|
+
.run(now, now, sessionId).changes);
|
|
150
151
|
}
|
|
151
152
|
function requiredId(value, label) {
|
|
152
153
|
const normalized = value?.trim();
|
|
@@ -14,6 +14,7 @@ export declare class SqliteSessionLogStore implements SessionLogStore {
|
|
|
14
14
|
afterId?: string;
|
|
15
15
|
limit?: number;
|
|
16
16
|
}): Promise<SessionItem[]>;
|
|
17
|
+
get(sessionId: string, itemId: string): Promise<SessionItem | undefined>;
|
|
17
18
|
snapshot(sessionId: string): Promise<SessionItem[]>;
|
|
18
19
|
listSessions(opts?: {
|
|
19
20
|
limit?: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
|
+
import { transaction } from "./sqlite.js";
|
|
2
3
|
function toItem(row) {
|
|
3
4
|
return {
|
|
4
5
|
id: row.id,
|
|
@@ -21,7 +22,7 @@ export class SqliteSessionLogStore {
|
|
|
21
22
|
(id, session_id, position, response_id, type, status, data, created_by, created_at)
|
|
22
23
|
VALUES (@id, @session_id, @position, @response_id, @type, @status, @data, @created_by, @created_at)`);
|
|
23
24
|
const maxStmt = conn.prepare("SELECT MAX(position) AS max FROM session_items WHERE session_id = ?");
|
|
24
|
-
const tx =
|
|
25
|
+
const tx = transaction(conn, (batch) => {
|
|
25
26
|
const { max } = maxStmt.get(sessionId);
|
|
26
27
|
let next = (max ?? -1) + 1;
|
|
27
28
|
return batch.map((item) => {
|
|
@@ -37,9 +38,18 @@ export class SqliteSessionLogStore {
|
|
|
37
38
|
created_by: stored.createdBy ?? null,
|
|
38
39
|
created_at: stored.createdAt,
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const resourceIds = stored.type === "message" && stored.data.role === "user"
|
|
42
|
+
? stored.data.content.flatMap((part) => (part.type === "input_image" || part.type === "input_file") &&
|
|
43
|
+
"resourceId" in part && part.resourceId
|
|
44
|
+
? [part.resourceId]
|
|
45
|
+
: [])
|
|
46
|
+
: stored.type === "function_call_output" &&
|
|
47
|
+
stored.data.generatedImage &&
|
|
48
|
+
"resourceId" in stored.data.generatedImage &&
|
|
49
|
+
stored.data.generatedImage.resourceId
|
|
50
|
+
? [stored.data.generatedImage.resourceId]
|
|
51
|
+
: [];
|
|
52
|
+
if (resourceIds.length > 0) {
|
|
43
53
|
for (const resourceId of resourceIds) {
|
|
44
54
|
const resource = conn.prepare(`SELECT state, message_item_id
|
|
45
55
|
FROM session_resources
|
|
@@ -47,23 +57,12 @@ export class SqliteSessionLogStore {
|
|
|
47
57
|
if (!resource ||
|
|
48
58
|
resource.state === "uploading" ||
|
|
49
59
|
(resource.message_item_id && resource.message_item_id !== stored.id)) {
|
|
50
|
-
throw new Error(`Session
|
|
60
|
+
throw new Error(`Session resource ${resourceId} is not committable`);
|
|
51
61
|
}
|
|
52
62
|
conn.prepare(`UPDATE session_resources
|
|
53
63
|
SET state = 'committed', message_item_id = ?,
|
|
54
64
|
committed_at = COALESCE(committed_at, ?)
|
|
55
65
|
WHERE id = ? AND session_id = ?`).run(stored.id, stored.createdAt, resourceId, sessionId);
|
|
56
|
-
const operation = conn.prepare(`SELECT client_message_id
|
|
57
|
-
FROM session_message_operation_resources
|
|
58
|
-
WHERE session_id = ? AND resource_id = ?`).get(sessionId, resourceId);
|
|
59
|
-
if (operation)
|
|
60
|
-
operationIds.add(operation.client_message_id);
|
|
61
|
-
}
|
|
62
|
-
for (const clientMessageId of operationIds) {
|
|
63
|
-
conn.prepare(`UPDATE session_message_operations
|
|
64
|
-
SET state = 'mirrored', error = NULL, updated_at = ?
|
|
65
|
-
WHERE session_id = ? AND client_message_id = ?
|
|
66
|
-
AND state IN ('injecting', 'injected', 'outcome_unknown')`).run(stored.createdAt, sessionId, clientMessageId);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
return stored;
|
|
@@ -92,6 +91,10 @@ export class SqliteSessionLogStore {
|
|
|
92
91
|
.all(sessionId, afterPosition, limit);
|
|
93
92
|
return rows.map(toItem);
|
|
94
93
|
}
|
|
94
|
+
async get(sessionId, itemId) {
|
|
95
|
+
const row = db().prepare("SELECT * FROM session_items WHERE session_id = ? AND id = ?").get(sessionId, itemId);
|
|
96
|
+
return row ? toItem(row) : undefined;
|
|
97
|
+
}
|
|
95
98
|
async snapshot(sessionId) {
|
|
96
99
|
const rows = db()
|
|
97
100
|
.prepare("SELECT * FROM session_items WHERE session_id = ? ORDER BY position ASC")
|
|
@@ -18,6 +18,11 @@ export declare function listSessionMetas(): StoredSessionMeta[];
|
|
|
18
18
|
export declare function getSessionMeta(id: string): StoredSessionMeta | undefined;
|
|
19
19
|
export declare function createSessionMeta(meta: StoredSessionMeta): void;
|
|
20
20
|
export declare function setSessionTitle(id: string, title: string): void;
|
|
21
|
+
export declare function setSessionExecution(id: string, execution: MachineSessionRecord["execution"]): void;
|
|
21
22
|
export declare function removeSessionMeta(id: string): void;
|
|
22
23
|
/** {@link SessionRegistry} over the same `sessions` table. */
|
|
23
24
|
export declare const sessionRegistry: SessionRegistry;
|
|
25
|
+
/** Generic plugin-principal ownership, including descendants forked from a
|
|
26
|
+
* plugin-created Session. Portal authorization calls this on every request so
|
|
27
|
+
* a grant cannot cross into console or another plugin's Sessions. */
|
|
28
|
+
export declare function sessionBelongsToPlugin(pluginId: string, sessionId: string): boolean;
|