@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.41
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 +6043 -6016
- package/bundled-plugins/plugins/lark/dist/settings.js +6003 -5985
- 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-http.d.ts +9 -0
- package/dist/chrome-for-testing-http.js +58 -0
- package/dist/chrome-for-testing-release-resolver.js +13 -6
- package/dist/chrome-for-testing-runtime.js +4 -3
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +99 -17
- 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 +42 -17
- package/dist/db.d.ts +2 -2
- package/dist/db.js +157 -21
- package/dist/headless-browser-host.js +397 -41
- package/dist/lifecycle.d.ts +2 -1
- package/dist/lifecycle.js +4 -1
- 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 +48 -9
- package/dist/plugin-host-rpc.js +593 -81
- 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/pm2.d.ts +2 -0
- package/dist/pm2.js +6 -0
- 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 +25 -42
- 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 +85 -30
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +46 -22
- 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 +55 -0
- package/dist/session-portal-grant-store.js +160 -0
- package/dist/session-resource-store.d.ts +24 -6
- package/dist/session-resource-store.js +387 -69
- package/dist/session-timeline-projection-store.d.ts +24 -0
- package/dist/session-timeline-projection-store.js +53 -0
- 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/stdio-epipe-guard.d.ts +1 -1
- package/dist/stdio-epipe-guard.js +1 -1
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +17 -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
|
@@ -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,6 @@
|
|
|
1
|
+
import { SessionLogAppendError } from "@rynx-ai/core";
|
|
1
2
|
import { db } from "./db.js";
|
|
3
|
+
import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
|
|
2
4
|
function toItem(row) {
|
|
3
5
|
return {
|
|
4
6
|
id: row.id,
|
|
@@ -8,6 +10,9 @@ function toItem(row) {
|
|
|
8
10
|
status: row.status,
|
|
9
11
|
createdAt: row.created_at,
|
|
10
12
|
...(row.created_by ? { createdBy: row.created_by } : {}),
|
|
13
|
+
...(row.parent_tool_call_id
|
|
14
|
+
? { parentToolCallId: row.parent_tool_call_id }
|
|
15
|
+
: {}),
|
|
11
16
|
type: row.type,
|
|
12
17
|
data: JSON.parse(row.data),
|
|
13
18
|
};
|
|
@@ -18,10 +23,12 @@ export class SqliteSessionLogStore {
|
|
|
18
23
|
return [];
|
|
19
24
|
const conn = db();
|
|
20
25
|
const insert = conn.prepare(`INSERT INTO session_items
|
|
21
|
-
(id, session_id, position, response_id, type, status, data, created_by,
|
|
22
|
-
|
|
26
|
+
(id, session_id, position, response_id, type, status, data, created_by,
|
|
27
|
+
parent_tool_call_id, created_at)
|
|
28
|
+
VALUES (@id, @session_id, @position, @response_id, @type, @status, @data,
|
|
29
|
+
@created_by, @parent_tool_call_id, @created_at)`);
|
|
23
30
|
const maxStmt = conn.prepare("SELECT MAX(position) AS max FROM session_items WHERE session_id = ?");
|
|
24
|
-
const tx =
|
|
31
|
+
const tx = transaction(conn, (batch) => {
|
|
25
32
|
const { max } = maxStmt.get(sessionId);
|
|
26
33
|
let next = (max ?? -1) + 1;
|
|
27
34
|
return batch.map((item) => {
|
|
@@ -35,41 +42,54 @@ export class SqliteSessionLogStore {
|
|
|
35
42
|
status: stored.status,
|
|
36
43
|
data: JSON.stringify(stored.data),
|
|
37
44
|
created_by: stored.createdBy ?? null,
|
|
45
|
+
parent_tool_call_id: stored.parentToolCallId ?? null,
|
|
38
46
|
created_at: stored.createdAt,
|
|
39
47
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
const resourceIds = stored.type === "message" && stored.data.role === "user"
|
|
49
|
+
? stored.data.content.flatMap((part) => (part.type === "input_image" || part.type === "input_file") &&
|
|
50
|
+
"resourceId" in part && part.resourceId
|
|
51
|
+
? [part.resourceId]
|
|
52
|
+
: [])
|
|
53
|
+
: stored.type === "function_call_output" &&
|
|
54
|
+
stored.data.generatedImage &&
|
|
55
|
+
"resourceId" in stored.data.generatedImage &&
|
|
56
|
+
stored.data.generatedImage.resourceId
|
|
57
|
+
? [stored.data.generatedImage.resourceId]
|
|
58
|
+
: [];
|
|
59
|
+
if (resourceIds.length > 0) {
|
|
43
60
|
for (const resourceId of resourceIds) {
|
|
44
61
|
const resource = conn.prepare(`SELECT state, message_item_id
|
|
45
62
|
FROM session_resources
|
|
46
63
|
WHERE id = ? AND session_id = ?`).get(resourceId, sessionId);
|
|
47
|
-
if (!resource
|
|
48
|
-
resource
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
if (!resource) {
|
|
65
|
+
throw new SessionLogAppendError(`Session resource ${resourceId} does not exist`, 422, "session_resource_missing");
|
|
66
|
+
}
|
|
67
|
+
if (resource.state === "uploading") {
|
|
68
|
+
throw new SessionLogAppendError(`Session resource ${resourceId} is still uploading`, 409, "session_resource_not_ready");
|
|
69
|
+
}
|
|
70
|
+
if (resource.message_item_id && resource.message_item_id !== stored.id) {
|
|
71
|
+
throw new SessionLogAppendError(`Session resource ${resourceId} is already committed to another item`, 422, "session_resource_already_committed");
|
|
51
72
|
}
|
|
52
73
|
conn.prepare(`UPDATE session_resources
|
|
53
74
|
SET state = 'committed', message_item_id = ?,
|
|
54
75
|
committed_at = COALESCE(committed_at, ?)
|
|
55
76
|
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
77
|
}
|
|
68
78
|
}
|
|
69
79
|
return stored;
|
|
70
80
|
});
|
|
71
81
|
});
|
|
72
|
-
|
|
82
|
+
try {
|
|
83
|
+
return tx(items);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof SessionLogAppendError)
|
|
87
|
+
throw error;
|
|
88
|
+
if (hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT)) {
|
|
89
|
+
throw new SessionLogAppendError(error instanceof Error ? error.message : "Session log constraint rejected the item", 422, "session_log_constraint", { cause: error });
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
94
|
async list(sessionId, opts = {}) {
|
|
75
95
|
const conn = db();
|
|
@@ -92,6 +112,10 @@ export class SqliteSessionLogStore {
|
|
|
92
112
|
.all(sessionId, afterPosition, limit);
|
|
93
113
|
return rows.map(toItem);
|
|
94
114
|
}
|
|
115
|
+
async get(sessionId, itemId) {
|
|
116
|
+
const row = db().prepare("SELECT * FROM session_items WHERE session_id = ? AND id = ?").get(sessionId, itemId);
|
|
117
|
+
return row ? toItem(row) : undefined;
|
|
118
|
+
}
|
|
95
119
|
async snapshot(sessionId) {
|
|
96
120
|
const rows = db()
|
|
97
121
|
.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;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { normalizeSessionTitle, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
15
15
|
import { db } from "./db.js";
|
|
16
|
+
import { transaction } from "./sqlite.js";
|
|
16
17
|
const COLS = "id, workspace, execution, title, forked_from_session_id, source, created_at, updated_at";
|
|
17
18
|
export function listSessionMetas() {
|
|
18
19
|
return db()
|
|
@@ -46,10 +47,15 @@ export function createSessionMeta(meta) {
|
|
|
46
47
|
export function setSessionTitle(id, title) {
|
|
47
48
|
db()
|
|
48
49
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
|
|
49
|
-
.run(normalizeSessionTitle(title), new Date().toISOString(), id);
|
|
50
|
+
.run(normalizeSessionTitle(title) ?? null, new Date().toISOString(), id);
|
|
51
|
+
}
|
|
52
|
+
export function setSessionExecution(id, execution) {
|
|
53
|
+
db()
|
|
54
|
+
.prepare("UPDATE sessions SET execution = ?, updated_at = ? WHERE id = ?")
|
|
55
|
+
.run(JSON.stringify(resolvedExecutionSnapshotSchema.parse(execution)), new Date().toISOString(), id);
|
|
50
56
|
}
|
|
51
57
|
export function removeSessionMeta(id) {
|
|
52
|
-
db()
|
|
58
|
+
transaction(db(), () => {
|
|
53
59
|
const now = new Date().toISOString();
|
|
54
60
|
db()
|
|
55
61
|
.prepare(`UPDATE plugin_session_launch_operations
|
|
@@ -70,8 +76,25 @@ export const sessionRegistry = {
|
|
|
70
76
|
get: getSessionMeta,
|
|
71
77
|
list: listSessionMetas,
|
|
72
78
|
setTitle: setSessionTitle,
|
|
79
|
+
setExecution: setSessionExecution,
|
|
73
80
|
remove: removeSessionMeta,
|
|
74
81
|
};
|
|
82
|
+
/** Generic plugin-principal ownership, including descendants forked from a
|
|
83
|
+
* plugin-created Session. Portal authorization calls this on every request so
|
|
84
|
+
* a grant cannot cross into console or another plugin's Sessions. */
|
|
85
|
+
export function sessionBelongsToPlugin(pluginId, sessionId) {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
let current = sessionRegistry.get(sessionId);
|
|
88
|
+
while (current && !seen.has(current.id)) {
|
|
89
|
+
if (current.source === `plugin:${pluginId}`)
|
|
90
|
+
return true;
|
|
91
|
+
seen.add(current.id);
|
|
92
|
+
current = current.forkedFromSessionId
|
|
93
|
+
? sessionRegistry.get(current.forkedFromSessionId)
|
|
94
|
+
: undefined;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
75
98
|
function rowToMeta(row) {
|
|
76
99
|
return {
|
|
77
100
|
id: row.id,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type SessionPortalScope = "plugin_sessions" | "single_session";
|
|
2
|
+
export type SessionPortalAccess = "read_only" | "read_write";
|
|
3
|
+
export interface SessionPortalAuthorization {
|
|
4
|
+
portalId: string;
|
|
5
|
+
pluginId: string;
|
|
6
|
+
scope: SessionPortalScope;
|
|
7
|
+
access: SessionPortalAccess;
|
|
8
|
+
embedOrigin?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
}
|
|
12
|
+
/** Issue a browser handoff into the native Session workbench. The Host knows
|
|
13
|
+
* only the generic Session scope and access level; product/user policy stays in
|
|
14
|
+
* the plugin that asks for the ticket. */
|
|
15
|
+
export declare function issueSessionPortalTicket(input: {
|
|
16
|
+
pluginId: string;
|
|
17
|
+
scope: SessionPortalScope;
|
|
18
|
+
access: SessionPortalAccess;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
embedOrigin?: string;
|
|
21
|
+
grantHandle?: string;
|
|
22
|
+
now?: Date;
|
|
23
|
+
}): {
|
|
24
|
+
path: string;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
grantHandle: string;
|
|
27
|
+
};
|
|
28
|
+
/** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
|
|
29
|
+
export declare function redeemSessionPortalTicket(ticket: string, now?: Date, embedOrigin?: string): (SessionPortalAuthorization & {
|
|
30
|
+
secret: string;
|
|
31
|
+
}) | undefined;
|
|
32
|
+
/** Read the public, still-active half of a grant without accepting its secret.
|
|
33
|
+
* Used only to bind an embedded SPA bootstrap and transport lifecycle to the
|
|
34
|
+
* exact grant that was issued; it does not authorize an API call. */
|
|
35
|
+
export declare function inspectSessionPortalGrant(portalId: string, now?: Date): SessionPortalAuthorization | undefined;
|
|
36
|
+
export declare function authorizeSessionPortalGrant(portalId: string, secret: string, now?: Date): SessionPortalAuthorization | undefined;
|
|
37
|
+
/** Revoke one issued Portal link, whether its ticket is still unused or it has
|
|
38
|
+
* already become a browser grant. The opaque handle carries no product meaning. */
|
|
39
|
+
export declare function revokeSessionPortalGrant(input: {
|
|
40
|
+
pluginId: string;
|
|
41
|
+
grantHandle: string;
|
|
42
|
+
now?: Date;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
revoked: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
/** Subscribe to committed grant revocations. The notification is emitted only
|
|
47
|
+
* after SQLite commits, so a listener may synchronously tear down every
|
|
48
|
+
* transport admitted under the revoked portal id. */
|
|
49
|
+
export declare function onSessionPortalGrantRevoked(listener: (portalIds: readonly string[]) => void | Promise<void>): () => void;
|
|
50
|
+
/** Embedded writable grants are process-generation capabilities. A daemon
|
|
51
|
+
* restart has no surviving transport to drain and Plugin recovery may fail, so
|
|
52
|
+
* fail closed before the Portal listener is reopened; ordinary non-embedded
|
|
53
|
+
* Portal links retain their historical fixed-lifetime behavior. */
|
|
54
|
+
export declare function revokeEmbeddedWritableSessionPortalGrants(now?: Date): void;
|
|
55
|
+
export declare function pruneSessionPortalAuthorization(now?: Date): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { db } from "./db.js";
|
|
3
|
+
import { transaction } from "./sqlite.js";
|
|
4
|
+
const TICKET_TTL_MS = 5 * 60 * 1_000;
|
|
5
|
+
const GRANT_TTL_MS = 30 * 60 * 1_000;
|
|
6
|
+
const revocationListeners = new Set();
|
|
7
|
+
/** Issue a browser handoff into the native Session workbench. The Host knows
|
|
8
|
+
* only the generic Session scope and access level; product/user policy stays in
|
|
9
|
+
* the plugin that asks for the ticket. */
|
|
10
|
+
export function issueSessionPortalTicket(input) {
|
|
11
|
+
if ((input.scope === "single_session") !== Boolean(input.sessionId)) {
|
|
12
|
+
throw new Error("single_session Portal tickets require exactly one Session id");
|
|
13
|
+
}
|
|
14
|
+
const now = input.now ?? new Date();
|
|
15
|
+
const expiresAt = new Date(now.getTime() + TICKET_TTL_MS).toISOString();
|
|
16
|
+
const ticket = randomToken();
|
|
17
|
+
const grantHandle = input.grantHandle ?? randomUUID();
|
|
18
|
+
pruneSessionPortalAuthorization(now);
|
|
19
|
+
db().prepare(`INSERT INTO session_portal_tickets
|
|
20
|
+
(ticket_hash, plugin_id, scope, access, session_id, created_at, expires_at, used_at,
|
|
21
|
+
grant_handle, embed_origin)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(hash(ticket), input.pluginId, input.scope, input.access, input.sessionId ?? null, now.toISOString(), expiresAt, grantHandle, input.embedOrigin ?? null);
|
|
23
|
+
const redeem = new URL("/session-portal/redeem", "http://session-portal.invalid");
|
|
24
|
+
if (input.embedOrigin)
|
|
25
|
+
redeem.searchParams.set("embed_origin", input.embedOrigin);
|
|
26
|
+
return {
|
|
27
|
+
path: `${redeem.pathname}${redeem.search}#ticket=${encodeURIComponent(ticket)}`,
|
|
28
|
+
expiresAt,
|
|
29
|
+
grantHandle,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
|
|
33
|
+
export function redeemSessionPortalTicket(ticket, now = new Date(), embedOrigin) {
|
|
34
|
+
const connection = db();
|
|
35
|
+
return transaction(connection, () => {
|
|
36
|
+
const ticketHash = hash(ticket);
|
|
37
|
+
const row = connection.prepare(`SELECT plugin_id, scope, access, session_id, expires_at, used_at,
|
|
38
|
+
grant_handle, embed_origin
|
|
39
|
+
FROM session_portal_tickets
|
|
40
|
+
WHERE ticket_hash = ?`).get(ticketHash);
|
|
41
|
+
if (!row ||
|
|
42
|
+
row.used_at !== null ||
|
|
43
|
+
row.expires_at <= now.toISOString() ||
|
|
44
|
+
(row.embed_origin ?? "") !== (embedOrigin ?? ""))
|
|
45
|
+
return undefined;
|
|
46
|
+
const consumed = connection.prepare(`UPDATE session_portal_tickets SET used_at = ?
|
|
47
|
+
WHERE ticket_hash = ? AND used_at IS NULL AND expires_at > ?`).run(now.toISOString(), ticketHash, now.toISOString());
|
|
48
|
+
if (consumed.changes !== 1)
|
|
49
|
+
return undefined;
|
|
50
|
+
const portalId = randomUUID();
|
|
51
|
+
const secret = randomToken();
|
|
52
|
+
const expiresAt = new Date(now.getTime() + GRANT_TTL_MS).toISOString();
|
|
53
|
+
connection.prepare(`INSERT INTO session_portal_grants
|
|
54
|
+
(portal_id, secret_hash, plugin_id, scope, access, session_id, created_at, expires_at,
|
|
55
|
+
revoked_at, grant_handle, embed_origin)
|
|
56
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(portalId, hash(secret), row.plugin_id, row.scope, row.access, row.session_id, now.toISOString(), expiresAt, row.grant_handle, row.embed_origin);
|
|
57
|
+
return {
|
|
58
|
+
portalId,
|
|
59
|
+
secret,
|
|
60
|
+
pluginId: row.plugin_id,
|
|
61
|
+
scope: row.scope,
|
|
62
|
+
access: row.access,
|
|
63
|
+
...(row.embed_origin ? { embedOrigin: row.embed_origin } : {}),
|
|
64
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
65
|
+
expiresAt,
|
|
66
|
+
};
|
|
67
|
+
})();
|
|
68
|
+
}
|
|
69
|
+
/** Read the public, still-active half of a grant without accepting its secret.
|
|
70
|
+
* Used only to bind an embedded SPA bootstrap and transport lifecycle to the
|
|
71
|
+
* exact grant that was issued; it does not authorize an API call. */
|
|
72
|
+
export function inspectSessionPortalGrant(portalId, now = new Date()) {
|
|
73
|
+
const row = db().prepare(`SELECT portal_id, secret_hash, plugin_id, scope, access, session_id, expires_at, revoked_at,
|
|
74
|
+
grant_handle, embed_origin
|
|
75
|
+
FROM session_portal_grants
|
|
76
|
+
WHERE portal_id = ?`).get(portalId);
|
|
77
|
+
if (!row || row.revoked_at !== null || row.expires_at <= now.toISOString())
|
|
78
|
+
return undefined;
|
|
79
|
+
return publicGrant(row);
|
|
80
|
+
}
|
|
81
|
+
export function authorizeSessionPortalGrant(portalId, secret, now = new Date()) {
|
|
82
|
+
const row = db().prepare(`SELECT portal_id, secret_hash, plugin_id, scope, access, session_id, expires_at, revoked_at,
|
|
83
|
+
grant_handle, embed_origin
|
|
84
|
+
FROM session_portal_grants
|
|
85
|
+
WHERE portal_id = ?`).get(portalId);
|
|
86
|
+
if (!row || row.revoked_at !== null || row.expires_at <= now.toISOString())
|
|
87
|
+
return undefined;
|
|
88
|
+
const actual = Buffer.from(hash(secret), "hex");
|
|
89
|
+
const expected = Buffer.from(row.secret_hash, "hex");
|
|
90
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
91
|
+
return undefined;
|
|
92
|
+
return publicGrant(row);
|
|
93
|
+
}
|
|
94
|
+
/** Revoke one issued Portal link, whether its ticket is still unused or it has
|
|
95
|
+
* already become a browser grant. The opaque handle carries no product meaning. */
|
|
96
|
+
export async function revokeSessionPortalGrant(input) {
|
|
97
|
+
const now = input.now ?? new Date();
|
|
98
|
+
const connection = db();
|
|
99
|
+
const result = transaction(connection, () => {
|
|
100
|
+
const portalIds = connection.prepare(`SELECT portal_id FROM session_portal_grants
|
|
101
|
+
WHERE plugin_id = ? AND grant_handle = ? AND revoked_at IS NULL`).all(input.pluginId, input.grantHandle);
|
|
102
|
+
const tickets = connection.prepare(`UPDATE session_portal_tickets SET used_at = ?
|
|
103
|
+
WHERE plugin_id = ? AND grant_handle = ? AND used_at IS NULL`).run(now.toISOString(), input.pluginId, input.grantHandle);
|
|
104
|
+
const grants = connection.prepare(`UPDATE session_portal_grants SET revoked_at = ?
|
|
105
|
+
WHERE plugin_id = ? AND grant_handle = ? AND revoked_at IS NULL`).run(now.toISOString(), input.pluginId, input.grantHandle);
|
|
106
|
+
return {
|
|
107
|
+
revoked: tickets.changes > 0 || grants.changes > 0,
|
|
108
|
+
portalIds: portalIds.map((row) => row.portal_id),
|
|
109
|
+
};
|
|
110
|
+
})();
|
|
111
|
+
if (result.portalIds.length > 0) {
|
|
112
|
+
// Revocation is a barrier, not merely a database flag. Host RPC returns
|
|
113
|
+
// only after every Portal transport admitted under the committed grant has
|
|
114
|
+
// been aborted and drained.
|
|
115
|
+
await Promise.all([...revocationListeners].map((listener) => listener(result.portalIds)));
|
|
116
|
+
}
|
|
117
|
+
return { revoked: result.revoked };
|
|
118
|
+
}
|
|
119
|
+
/** Subscribe to committed grant revocations. The notification is emitted only
|
|
120
|
+
* after SQLite commits, so a listener may synchronously tear down every
|
|
121
|
+
* transport admitted under the revoked portal id. */
|
|
122
|
+
export function onSessionPortalGrantRevoked(listener) {
|
|
123
|
+
revocationListeners.add(listener);
|
|
124
|
+
return () => revocationListeners.delete(listener);
|
|
125
|
+
}
|
|
126
|
+
/** Embedded writable grants are process-generation capabilities. A daemon
|
|
127
|
+
* restart has no surviving transport to drain and Plugin recovery may fail, so
|
|
128
|
+
* fail closed before the Portal listener is reopened; ordinary non-embedded
|
|
129
|
+
* Portal links retain their historical fixed-lifetime behavior. */
|
|
130
|
+
export function revokeEmbeddedWritableSessionPortalGrants(now = new Date()) {
|
|
131
|
+
const connection = db();
|
|
132
|
+
transaction(connection, () => {
|
|
133
|
+
connection.prepare(`UPDATE session_portal_tickets SET used_at=?
|
|
134
|
+
WHERE access='read_write' AND embed_origin IS NOT NULL AND used_at IS NULL`).run(now.toISOString());
|
|
135
|
+
connection.prepare(`UPDATE session_portal_grants SET revoked_at=?
|
|
136
|
+
WHERE access='read_write' AND embed_origin IS NOT NULL AND revoked_at IS NULL`).run(now.toISOString());
|
|
137
|
+
})();
|
|
138
|
+
}
|
|
139
|
+
export function pruneSessionPortalAuthorization(now = new Date()) {
|
|
140
|
+
const connection = db();
|
|
141
|
+
connection.prepare("DELETE FROM session_portal_tickets WHERE expires_at <= ? OR used_at IS NOT NULL").run(now.toISOString());
|
|
142
|
+
connection.prepare("DELETE FROM session_portal_grants WHERE expires_at <= ? OR revoked_at IS NOT NULL").run(now.toISOString());
|
|
143
|
+
}
|
|
144
|
+
function randomToken() {
|
|
145
|
+
return randomBytes(32).toString("base64url");
|
|
146
|
+
}
|
|
147
|
+
function hash(value) {
|
|
148
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
149
|
+
}
|
|
150
|
+
function publicGrant(row) {
|
|
151
|
+
return {
|
|
152
|
+
portalId: row.portal_id,
|
|
153
|
+
pluginId: row.plugin_id,
|
|
154
|
+
scope: row.scope,
|
|
155
|
+
access: row.access,
|
|
156
|
+
...(row.embed_origin ? { embedOrigin: row.embed_origin } : {}),
|
|
157
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
158
|
+
expiresAt: row.expires_at,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type UserContentPart } from "@rynx-ai/core";
|
|
1
|
+
import { type SessionImageFileReference, type UserContentPart } from "@rynx-ai/core";
|
|
2
2
|
import { type RemoteRuntimeSessionResourceDeleteParams, type RemoteRuntimeSessionResourceDeleteResult, type RemoteRuntimeSessionResourceGetParams, type RemoteRuntimeSessionResourceGetResult, type RemoteRuntimeSessionResourcePolicyResult, type RemoteRuntimeSessionResourceReadParams, type RemoteRuntimeSessionResourceReadResult, type RemoteRuntimeSessionResourceUploadBeginParams, type RemoteRuntimeSessionResourceUploadBeginResult, type RemoteRuntimeSessionResourceUploadChunkParams, type RemoteRuntimeSessionResourceUploadChunkResult, type RemoteRuntimeSessionResourceUploadCommitParams, type RemoteRuntimeSessionResourceUploadCommitResult } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
3
|
-
import { type MachineSessionPreparedInput, type MachineSessionResourcePort } from "@rynx-ai/server";
|
|
3
|
+
import { type MachineSessionPreparedInput, type MachineSessionResourcePort, type MachineSessionRunExecutionSelection, type MachineSessionRunExecutionSnapshot } from "@rynx-ai/server";
|
|
4
4
|
export declare class SqliteSessionResourceStore implements MachineSessionResourcePort {
|
|
5
5
|
private readonly root;
|
|
6
6
|
constructor(root?: string);
|
|
@@ -9,18 +9,36 @@ export declare class SqliteSessionResourceStore implements MachineSessionResourc
|
|
|
9
9
|
writeUploadChunk(input: RemoteRuntimeSessionResourceUploadChunkParams): Promise<RemoteRuntimeSessionResourceUploadChunkResult>;
|
|
10
10
|
commitUpload(input: RemoteRuntimeSessionResourceUploadCommitParams): Promise<RemoteRuntimeSessionResourceUploadCommitResult>;
|
|
11
11
|
getResource(input: RemoteRuntimeSessionResourceGetParams): Promise<RemoteRuntimeSessionResourceGetResult>;
|
|
12
|
+
resolveCommittedLocalPath(sessionId: string, resourceId: string): string;
|
|
12
13
|
readResource(input: RemoteRuntimeSessionResourceReadParams): Promise<RemoteRuntimeSessionResourceReadResult>;
|
|
13
14
|
deleteResource(input: RemoteRuntimeSessionResourceDeleteParams): Promise<RemoteRuntimeSessionResourceDeleteResult>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
materializeGeneratedImage(input: {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
sourceId: string;
|
|
18
|
+
result: string;
|
|
19
|
+
}): Promise<SessionImageFileReference>;
|
|
20
|
+
prepareInput(sessionId: string, content: UserContentPart[], clientMessageId?: string, operationExecution?: {
|
|
21
|
+
selection: MachineSessionRunExecutionSelection;
|
|
22
|
+
snapshot: MachineSessionRunExecutionSnapshot;
|
|
23
|
+
}): Promise<MachineSessionPreparedInput>;
|
|
24
|
+
markMessageInjecting(sessionId: string, clientMessageId: string, identity?: {
|
|
25
|
+
responseId: string;
|
|
26
|
+
messageItemId: string;
|
|
27
|
+
}): void;
|
|
28
|
+
markMessageInjected(sessionId: string, clientMessageId: string, identity?: {
|
|
29
|
+
responseId: string;
|
|
30
|
+
}): void;
|
|
31
|
+
markMessageMirrored(sessionId: string, clientMessageId: string, identity: {
|
|
32
|
+
responseId: string;
|
|
33
|
+
messageItemId: string;
|
|
34
|
+
}): void;
|
|
17
35
|
markMessageOutcomeUnknown(sessionId: string, clientMessageId: string, reason: string): void;
|
|
18
36
|
markMessageFailedNotStarted(sessionId: string, clientMessageId: string, reason: string): void;
|
|
19
37
|
deleteSession(sessionId: string): void;
|
|
20
38
|
private prepareMessageOperation;
|
|
21
39
|
private bindOperationResources;
|
|
22
40
|
private transitionOperation;
|
|
23
|
-
private
|
|
41
|
+
private finalizeStoredFile;
|
|
24
42
|
private reconcileUploadOffset;
|
|
25
43
|
private rowByUpload;
|
|
26
44
|
private requireUpload;
|