@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.6
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/host-adapters.d.ts +2 -2
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +16 -16
- package/bundled-plugins/plugins/lark/dist/runtime.js +24 -14
- package/bundled-plugins/plugins/lark/dist/settings.js +13 -3
- 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/control-deps.d.ts +6 -2
- package/dist/control-deps.js +86 -3
- package/dist/daemon-server.js +13 -4
- package/dist/db.js +49 -15
- package/dist/plugin-host-rpc.d.ts +8 -4
- package/dist/plugin-host-rpc.js +32 -33
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.js +0 -4
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +22 -0
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +84 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -7
- package/package.json +9 -9
- 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/daemon-server.js
CHANGED
|
@@ -18,8 +18,9 @@ import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMA
|
|
|
18
18
|
import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
19
19
|
import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
|
|
20
20
|
import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, listRuntimeModels, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
|
|
21
|
+
import { authorizeSessionPortalGrant, redeemSessionPortalTicket, } from "./session-portal-grant-store.js";
|
|
21
22
|
import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
|
|
22
|
-
import { buildControlDeps } from "./control-deps.js";
|
|
23
|
+
import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
|
|
23
24
|
import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
|
|
24
25
|
import { createDaemonBuildStatus } from "./daemon-build-status.js";
|
|
25
26
|
import { MaintenanceLeaseStore } from "./maintenance-lease.js";
|
|
@@ -49,7 +50,7 @@ import { SqliteSessionResourceStore } from "./session-resource-store.js";
|
|
|
49
50
|
import { createSessionFallbackWorkspace, discardSessionFallbackWorkspace, } from "./session-fallback-workspace.js";
|
|
50
51
|
import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
|
|
51
52
|
import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
|
|
52
|
-
import { sessionRegistry } from "./session-meta-store.js";
|
|
53
|
+
import { sessionBelongsToPlugin, sessionRegistry } from "./session-meta-store.js";
|
|
53
54
|
import { sessionPendingMessageStore } from "./session-pending-message-store.js";
|
|
54
55
|
import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
|
|
55
56
|
import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
|
|
@@ -280,7 +281,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
280
281
|
const directConfig = directHostingSupported
|
|
281
282
|
? await loadDirectConfig()
|
|
282
283
|
: undefined;
|
|
283
|
-
const control = buildControlDeps();
|
|
284
|
+
const control = buildControlDeps(appConfig);
|
|
284
285
|
const installationIdentity = await loadOrCreateDaemonInstallationIdentity();
|
|
285
286
|
const directEncryptionIdentity = directHostingSupported
|
|
286
287
|
? await loadOrCreateDaemonDirectRuntimeEncryptionIdentity()
|
|
@@ -304,6 +305,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
304
305
|
"semantic.session.interrupt.v1",
|
|
305
306
|
"semantic.session.agent-options.v1",
|
|
306
307
|
"semantic.session.launch-options.v2",
|
|
308
|
+
"semantic.session.execution-settings.v1",
|
|
307
309
|
"semantic.session.create.v1",
|
|
308
310
|
"semantic.session.fork.v1",
|
|
309
311
|
"semantic.session.message.v1",
|
|
@@ -556,14 +558,16 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
556
558
|
forks: sessionForks,
|
|
557
559
|
execution: {
|
|
558
560
|
runtime: sessionRuntimeServices.conversationRuntime,
|
|
559
|
-
resolve: ({ agent, provider, model, reasoningEffort, cwd }) => resolveSessionExecutionSnapshot({
|
|
561
|
+
resolve: ({ agent, provider, model, reasoningEffort, permissionPreset, cwd }) => resolveSessionExecutionSnapshot({
|
|
560
562
|
config: appConfig,
|
|
561
563
|
...(agent ? { agentName: agent } : {}),
|
|
562
564
|
...(provider ? { provider } : {}),
|
|
563
565
|
...(model ? { model } : {}),
|
|
564
566
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
567
|
+
...(permissionPreset ? { permissionPreset } : {}),
|
|
565
568
|
cwd,
|
|
566
569
|
}),
|
|
570
|
+
listModels: (provider) => listControlAgentModels(appConfig, provider),
|
|
567
571
|
runner: sessionRuntimeServices.runnerManager,
|
|
568
572
|
},
|
|
569
573
|
workspace: {
|
|
@@ -794,6 +798,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
794
798
|
registry: desktopBrowserHosts,
|
|
795
799
|
daemonInstanceId,
|
|
796
800
|
},
|
|
801
|
+
sessionPortalAuthorization: {
|
|
802
|
+
redeem: redeemSessionPortalTicket,
|
|
803
|
+
authorize: authorizeSessionPortalGrant,
|
|
804
|
+
ownsSession: sessionBelongsToPlugin,
|
|
805
|
+
},
|
|
797
806
|
});
|
|
798
807
|
baseShutdown = server.shutdown.bind(server);
|
|
799
808
|
if (directConfig && directEncryptionIdentity) {
|
package/dist/db.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `~/.rynx/rynx.db` — the daemon's SQLite store. Concerns kept apart:
|
|
3
|
-
* - `plugin_installations` — source-qualified manifests
|
|
3
|
+
* - `plugin_installations` — source-qualified manifests and immutable packages.
|
|
4
4
|
* - `plugin_marketplaces` — catalog registrations; catalog data is never executable.
|
|
5
5
|
* - `plugin_session_launch_operations` — durable idempotency/tombstones.
|
|
6
6
|
* - `control_links` — generic, one-use links to the local session console.
|
|
7
|
+
* - `session_portal_tickets` / `session_portal_grants` — hashed, scoped,
|
|
8
|
+
* browser Session Portal authorization.
|
|
7
9
|
* - `runtime_pairing_claims` / `runtime_client_grants` — one-use enrollment
|
|
8
10
|
* claims and the durable client identities they issue for direct runtimes.
|
|
9
11
|
* - `runtime_targets` / `runtime_direct_routes` / `runtime_pending_enrollments`
|
|
@@ -86,7 +88,6 @@ function migrate(conn) {
|
|
|
86
88
|
install_path TEXT NOT NULL,
|
|
87
89
|
manifest TEXT NOT NULL,
|
|
88
90
|
integrity TEXT NOT NULL,
|
|
89
|
-
capabilities TEXT NOT NULL,
|
|
90
91
|
runtime_state TEXT,
|
|
91
92
|
runtime_error TEXT,
|
|
92
93
|
UNIQUE(marketplace_id, plugin_id)
|
|
@@ -245,6 +246,39 @@ function migrate(conn) {
|
|
|
245
246
|
);
|
|
246
247
|
CREATE INDEX IF NOT EXISTS idx_control_links_expires
|
|
247
248
|
ON control_links(expires_at);
|
|
249
|
+
CREATE TABLE IF NOT EXISTS session_portal_tickets (
|
|
250
|
+
ticket_hash TEXT PRIMARY KEY,
|
|
251
|
+
plugin_id TEXT NOT NULL,
|
|
252
|
+
scope TEXT NOT NULL CHECK(scope IN ('plugin_sessions', 'single_session')),
|
|
253
|
+
access TEXT NOT NULL CHECK(access IN ('read_only', 'read_write')),
|
|
254
|
+
session_id TEXT,
|
|
255
|
+
created_at TEXT NOT NULL,
|
|
256
|
+
expires_at TEXT NOT NULL,
|
|
257
|
+
used_at TEXT,
|
|
258
|
+
CHECK(
|
|
259
|
+
(scope = 'plugin_sessions' AND session_id IS NULL)
|
|
260
|
+
OR (scope = 'single_session' AND session_id IS NOT NULL)
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_session_portal_tickets_expires
|
|
264
|
+
ON session_portal_tickets(expires_at);
|
|
265
|
+
CREATE TABLE IF NOT EXISTS session_portal_grants (
|
|
266
|
+
portal_id TEXT PRIMARY KEY,
|
|
267
|
+
secret_hash TEXT NOT NULL,
|
|
268
|
+
plugin_id TEXT NOT NULL,
|
|
269
|
+
scope TEXT NOT NULL CHECK(scope IN ('plugin_sessions', 'single_session')),
|
|
270
|
+
access TEXT NOT NULL CHECK(access IN ('read_only', 'read_write')),
|
|
271
|
+
session_id TEXT,
|
|
272
|
+
created_at TEXT NOT NULL,
|
|
273
|
+
expires_at TEXT NOT NULL,
|
|
274
|
+
revoked_at TEXT,
|
|
275
|
+
CHECK(
|
|
276
|
+
(scope = 'plugin_sessions' AND session_id IS NULL)
|
|
277
|
+
OR (scope = 'single_session' AND session_id IS NOT NULL)
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_session_portal_grants_expires
|
|
281
|
+
ON session_portal_grants(expires_at);
|
|
248
282
|
CREATE TABLE IF NOT EXISTS runtime_pairing_claims (
|
|
249
283
|
claim_id TEXT PRIMARY KEY,
|
|
250
284
|
secret_hash TEXT NOT NULL,
|
|
@@ -308,7 +342,7 @@ function migrate(conn) {
|
|
|
308
342
|
CREATE INDEX IF NOT EXISTS idx_runtime_pending_enrollments_expires
|
|
309
343
|
ON runtime_pending_enrollments(expires_at);
|
|
310
344
|
`);
|
|
311
|
-
|
|
345
|
+
migratePluginCapabilityDeclarations(conn);
|
|
312
346
|
migrateSessionSnapshots(conn);
|
|
313
347
|
// Breaking plugin-runtime cutover: remove obsolete core-owned state.
|
|
314
348
|
conn.exec(`
|
|
@@ -319,6 +353,18 @@ function migrate(conn) {
|
|
|
319
353
|
addColumnIfMissing(conn, "sessions", "forked_from_session_id", "TEXT");
|
|
320
354
|
addColumnIfMissing(conn, "plugin_session_launch_operations", "raw_request_hash", "TEXT");
|
|
321
355
|
}
|
|
356
|
+
/** Remove the inert permission-declaration columns left by older installations. */
|
|
357
|
+
function migratePluginCapabilityDeclarations(conn) {
|
|
358
|
+
const columns = conn
|
|
359
|
+
.prepare("PRAGMA table_info(plugin_installations)")
|
|
360
|
+
.all();
|
|
361
|
+
const names = new Set(columns.map((column) => column.name));
|
|
362
|
+
for (const legacyColumn of ["capabilities", "grants"]) {
|
|
363
|
+
if (names.has(legacyColumn)) {
|
|
364
|
+
conn.exec(`ALTER TABLE plugin_installations DROP COLUMN ${legacyColumn}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
322
368
|
/**
|
|
323
369
|
* Breaking Session cutover: the previous rows contain mutable Agent/Provider
|
|
324
370
|
* references and no immutable workspace/execution snapshots, so they cannot be
|
|
@@ -358,18 +404,6 @@ function migrateSessionSnapshots(conn) {
|
|
|
358
404
|
`);
|
|
359
405
|
}).immediate();
|
|
360
406
|
}
|
|
361
|
-
function migratePluginCapabilities(conn) {
|
|
362
|
-
const columns = conn
|
|
363
|
-
.prepare("PRAGMA table_info(plugin_installations)")
|
|
364
|
-
.all();
|
|
365
|
-
if (columns.some((column) => column.name === "capabilities"))
|
|
366
|
-
return;
|
|
367
|
-
if (columns.some((column) => column.name === "grants")) {
|
|
368
|
-
conn.exec("ALTER TABLE plugin_installations RENAME COLUMN grants TO capabilities");
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
conn.exec("ALTER TABLE plugin_installations ADD COLUMN capabilities TEXT NOT NULL DEFAULT '[]'");
|
|
372
|
-
}
|
|
373
407
|
/** Idempotently add a column to an existing table (no-op if already present). */
|
|
374
408
|
function addColumnIfMissing(conn, table, column, ddl) {
|
|
375
409
|
const cols = conn.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AdmissionReservation, type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
2
|
-
import { type PluginAgentSummary, type
|
|
2
|
+
import { type PluginAgentSummary, type PluginHostEvent, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
|
|
3
3
|
import type { SessionInteractionResolution } from "@rynx-ai/protocol";
|
|
4
4
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
5
5
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
@@ -60,6 +60,11 @@ export interface PluginHostServices {
|
|
|
60
60
|
}>;
|
|
61
61
|
admissionOpen?: () => boolean;
|
|
62
62
|
admissionReserve?: () => AdmissionReservation | undefined;
|
|
63
|
+
sessionPortal?: {
|
|
64
|
+
scheme: "http" | "https";
|
|
65
|
+
bindHost: string;
|
|
66
|
+
port: number;
|
|
67
|
+
};
|
|
63
68
|
}
|
|
64
69
|
export interface PluginHostRpcOptions {
|
|
65
70
|
/** Manifest id returned by host.info. */
|
|
@@ -69,7 +74,6 @@ export interface PluginHostRpcOptions {
|
|
|
69
74
|
daemonInstanceId: string;
|
|
70
75
|
productVersion: string;
|
|
71
76
|
cliVersion: string;
|
|
72
|
-
capabilities: readonly PluginCapability[];
|
|
73
77
|
services: PluginHostServices;
|
|
74
78
|
/** Canonical plugin-owned root permitted for the Workspace plugin's isolated
|
|
75
79
|
* Session directory. Omitted for every other plugin. */
|
|
@@ -80,7 +84,7 @@ export interface PluginHostRpcOptions {
|
|
|
80
84
|
disposeForceAfterMs?: number;
|
|
81
85
|
}
|
|
82
86
|
/**
|
|
83
|
-
* One authenticated plugin's
|
|
87
|
+
* One authenticated plugin's Host RPC surface.
|
|
84
88
|
*
|
|
85
89
|
* This adapter is deliberately the only place that maps public string methods
|
|
86
90
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -92,7 +96,6 @@ export declare class PluginHostRpc {
|
|
|
92
96
|
private readonly daemonInstanceId;
|
|
93
97
|
private readonly productVersion;
|
|
94
98
|
private readonly cliVersion;
|
|
95
|
-
private readonly capabilities;
|
|
96
99
|
private readonly services;
|
|
97
100
|
private readonly managedWorkspaceRoot;
|
|
98
101
|
private readonly emit;
|
|
@@ -131,6 +134,7 @@ export declare class PluginHostRpc {
|
|
|
131
134
|
private setSessionTitle;
|
|
132
135
|
private resolveAgentExecution;
|
|
133
136
|
private issueControlLink;
|
|
137
|
+
private issueSessionPortalLink;
|
|
134
138
|
private ownedSession;
|
|
135
139
|
private isOwned;
|
|
136
140
|
}
|
package/dist/plugin-host-rpc.js
CHANGED
|
@@ -6,6 +6,7 @@ import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
|
|
|
6
6
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
7
7
|
import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
8
8
|
import { createControlLink } from "./control-link-store.js";
|
|
9
|
+
import { issueSessionPortalTicket } from "./session-portal-grant-store.js";
|
|
9
10
|
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
10
11
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
11
12
|
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
@@ -13,31 +14,8 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
|
13
14
|
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
14
15
|
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
15
16
|
const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
16
|
-
const METHOD_CAPABILITY = {
|
|
17
|
-
"installation.proof.sign": "installation.proof",
|
|
18
|
-
"agent.catalog.list": "agent.catalog.read",
|
|
19
|
-
"agent.catalog.get": "agent.catalog.read",
|
|
20
|
-
"agent.execution.resolve": "agent.catalog.read",
|
|
21
|
-
"session.ensure": "session.provision",
|
|
22
|
-
"session.run": "conversation.run",
|
|
23
|
-
"session.steer": "conversation.run",
|
|
24
|
-
"session.interrupt": "session.cancel",
|
|
25
|
-
"session.interaction.resolve": "conversation.run",
|
|
26
|
-
"session.models": "runtime.catalog.read",
|
|
27
|
-
"session.binding.get": "session.binding.read",
|
|
28
|
-
"session.binding.list": "session.binding.read",
|
|
29
|
-
"session.binding.find": "session.binding.read",
|
|
30
|
-
"session.goal.get": "session.goal.read",
|
|
31
|
-
"session.goal.set": "session.goal.write",
|
|
32
|
-
"session.goal.clear": "session.goal.write",
|
|
33
|
-
"session.fork": "session.fork",
|
|
34
|
-
"session.meta.get": "session.meta.read",
|
|
35
|
-
"session.meta.list": "session.meta.read",
|
|
36
|
-
"session.meta.setTitle": "session.meta.title.write",
|
|
37
|
-
"control.link.issue": "control-link.issue",
|
|
38
|
-
};
|
|
39
17
|
/**
|
|
40
|
-
* One authenticated plugin's
|
|
18
|
+
* One authenticated plugin's Host RPC surface.
|
|
41
19
|
*
|
|
42
20
|
* This adapter is deliberately the only place that maps public string methods
|
|
43
21
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -49,7 +27,6 @@ export class PluginHostRpc {
|
|
|
49
27
|
daemonInstanceId;
|
|
50
28
|
productVersion;
|
|
51
29
|
cliVersion;
|
|
52
|
-
capabilities;
|
|
53
30
|
services;
|
|
54
31
|
managedWorkspaceRoot;
|
|
55
32
|
emit;
|
|
@@ -65,7 +42,6 @@ export class PluginHostRpc {
|
|
|
65
42
|
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
66
43
|
this.productVersion = requiredString(options.productVersion, "productVersion", 128);
|
|
67
44
|
this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
|
|
68
|
-
this.capabilities = new Set(options.capabilities);
|
|
69
45
|
this.services = options.services;
|
|
70
46
|
this.managedWorkspaceRoot = options.managedWorkspaceRoot
|
|
71
47
|
? path.resolve(options.managedWorkspaceRoot)
|
|
@@ -89,15 +65,11 @@ export class PluginHostRpc {
|
|
|
89
65
|
publicKey: this.services.installationProof.publicKey,
|
|
90
66
|
},
|
|
91
67
|
protocolVersion: 1,
|
|
92
|
-
|
|
68
|
+
...(this.services.sessionPortal
|
|
69
|
+
? { sessionPortal: { ...this.services.sessionPortal } }
|
|
70
|
+
: {}),
|
|
93
71
|
};
|
|
94
72
|
}
|
|
95
|
-
const capability = METHOD_CAPABILITY[method];
|
|
96
|
-
if (!capability)
|
|
97
|
-
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
98
|
-
if (!this.capabilities.has(capability)) {
|
|
99
|
-
throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
|
|
100
|
-
}
|
|
101
73
|
try {
|
|
102
74
|
switch (method) {
|
|
103
75
|
case "installation.proof.sign": {
|
|
@@ -151,6 +123,16 @@ export class PluginHostRpc {
|
|
|
151
123
|
return this.setSessionTitle(params);
|
|
152
124
|
case "control.link.issue":
|
|
153
125
|
return this.issueControlLink(params);
|
|
126
|
+
case "session.portal.plugin.issue": {
|
|
127
|
+
const access = requiredSessionPortalAccess(params);
|
|
128
|
+
return this.issueSessionPortalLink("plugin_sessions", access);
|
|
129
|
+
}
|
|
130
|
+
case "session.portal.session.issue": {
|
|
131
|
+
const input = record(params);
|
|
132
|
+
const sessionId = requiredSessionId(input);
|
|
133
|
+
this.ownedSession(sessionId);
|
|
134
|
+
return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId);
|
|
135
|
+
}
|
|
154
136
|
default:
|
|
155
137
|
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
156
138
|
}
|
|
@@ -652,6 +634,17 @@ export class PluginHostRpc {
|
|
|
652
634
|
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
653
635
|
});
|
|
654
636
|
}
|
|
637
|
+
issueSessionPortalLink(scope, access, sessionId) {
|
|
638
|
+
if (!this.services.sessionPortal) {
|
|
639
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
|
|
640
|
+
}
|
|
641
|
+
return issueSessionPortalTicket({
|
|
642
|
+
pluginId: this.principalId,
|
|
643
|
+
scope,
|
|
644
|
+
access,
|
|
645
|
+
...(sessionId ? { sessionId } : {}),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
655
648
|
ownedSession(sessionId) {
|
|
656
649
|
const session = this.services.sessionRegistry.get(sessionId);
|
|
657
650
|
if (!session || !this.isOwned(session)) {
|
|
@@ -768,6 +761,12 @@ function sessionExecutionSnapshot(value) {
|
|
|
768
761
|
function requiredSessionId(params) {
|
|
769
762
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
770
763
|
}
|
|
764
|
+
function requiredSessionPortalAccess(params) {
|
|
765
|
+
const access = record(params).access;
|
|
766
|
+
if (access === "read_only" || access === "read_write")
|
|
767
|
+
return access;
|
|
768
|
+
throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
|
|
769
|
+
}
|
|
771
770
|
function sessionInteractionResolution(value) {
|
|
772
771
|
const input = record(value);
|
|
773
772
|
const keys = Object.keys(input);
|
|
@@ -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 ? {
|
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",
|
|
@@ -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;
|
|
@@ -48,6 +48,11 @@ export function setSessionTitle(id, title) {
|
|
|
48
48
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
|
|
49
49
|
.run(normalizeSessionTitle(title), new Date().toISOString(), id);
|
|
50
50
|
}
|
|
51
|
+
export function setSessionExecution(id, execution) {
|
|
52
|
+
db()
|
|
53
|
+
.prepare("UPDATE sessions SET execution = ?, updated_at = ? WHERE id = ?")
|
|
54
|
+
.run(JSON.stringify(resolvedExecutionSnapshotSchema.parse(execution)), new Date().toISOString(), id);
|
|
55
|
+
}
|
|
51
56
|
export function removeSessionMeta(id) {
|
|
52
57
|
db().transaction(() => {
|
|
53
58
|
const now = new Date().toISOString();
|
|
@@ -70,8 +75,25 @@ export const sessionRegistry = {
|
|
|
70
75
|
get: getSessionMeta,
|
|
71
76
|
list: listSessionMetas,
|
|
72
77
|
setTitle: setSessionTitle,
|
|
78
|
+
setExecution: setSessionExecution,
|
|
73
79
|
remove: removeSessionMeta,
|
|
74
80
|
};
|
|
81
|
+
/** Generic plugin-principal ownership, including descendants forked from a
|
|
82
|
+
* plugin-created Session. Portal authorization calls this on every request so
|
|
83
|
+
* a grant cannot cross into console or another plugin's Sessions. */
|
|
84
|
+
export function sessionBelongsToPlugin(pluginId, sessionId) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
let current = sessionRegistry.get(sessionId);
|
|
87
|
+
while (current && !seen.has(current.id)) {
|
|
88
|
+
if (current.source === `plugin:${pluginId}`)
|
|
89
|
+
return true;
|
|
90
|
+
seen.add(current.id);
|
|
91
|
+
current = current.forkedFromSessionId
|
|
92
|
+
? sessionRegistry.get(current.forkedFromSessionId)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
75
97
|
function rowToMeta(row) {
|
|
76
98
|
return {
|
|
77
99
|
id: row.id,
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
sessionId?: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
}
|
|
11
|
+
/** Issue a browser handoff into the native Session workbench. The Host knows
|
|
12
|
+
* only the generic Session scope and access level; product/user policy stays in
|
|
13
|
+
* the plugin that asks for the ticket. */
|
|
14
|
+
export declare function issueSessionPortalTicket(input: {
|
|
15
|
+
pluginId: string;
|
|
16
|
+
scope: SessionPortalScope;
|
|
17
|
+
access: SessionPortalAccess;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
now?: Date;
|
|
20
|
+
}): {
|
|
21
|
+
path: string;
|
|
22
|
+
expiresAt: string;
|
|
23
|
+
};
|
|
24
|
+
/** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
|
|
25
|
+
export declare function redeemSessionPortalTicket(ticket: string, now?: Date): (SessionPortalAuthorization & {
|
|
26
|
+
secret: string;
|
|
27
|
+
}) | undefined;
|
|
28
|
+
export declare function authorizeSessionPortalGrant(portalId: string, secret: string, now?: Date): SessionPortalAuthorization | undefined;
|
|
29
|
+
export declare function pruneSessionPortalAuthorization(now?: Date): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { db } from "./db.js";
|
|
3
|
+
const TICKET_TTL_MS = 5 * 60 * 1_000;
|
|
4
|
+
const GRANT_TTL_MS = 30 * 60 * 1_000;
|
|
5
|
+
/** Issue a browser handoff into the native Session workbench. The Host knows
|
|
6
|
+
* only the generic Session scope and access level; product/user policy stays in
|
|
7
|
+
* the plugin that asks for the ticket. */
|
|
8
|
+
export function issueSessionPortalTicket(input) {
|
|
9
|
+
if ((input.scope === "single_session") !== Boolean(input.sessionId)) {
|
|
10
|
+
throw new Error("single_session Portal tickets require exactly one Session id");
|
|
11
|
+
}
|
|
12
|
+
const now = input.now ?? new Date();
|
|
13
|
+
const expiresAt = new Date(now.getTime() + TICKET_TTL_MS).toISOString();
|
|
14
|
+
const ticket = randomToken();
|
|
15
|
+
pruneSessionPortalAuthorization(now);
|
|
16
|
+
db().prepare(`INSERT INTO session_portal_tickets
|
|
17
|
+
(ticket_hash, plugin_id, scope, access, session_id, created_at, expires_at, used_at)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`).run(hash(ticket), input.pluginId, input.scope, input.access, input.sessionId ?? null, now.toISOString(), expiresAt);
|
|
19
|
+
return {
|
|
20
|
+
path: `/session-portal/redeem#ticket=${encodeURIComponent(ticket)}`,
|
|
21
|
+
expiresAt,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
|
|
25
|
+
export function redeemSessionPortalTicket(ticket, now = new Date()) {
|
|
26
|
+
const connection = db();
|
|
27
|
+
return connection.transaction(() => {
|
|
28
|
+
const ticketHash = hash(ticket);
|
|
29
|
+
const row = connection.prepare(`SELECT plugin_id, scope, access, session_id, expires_at, used_at
|
|
30
|
+
FROM session_portal_tickets
|
|
31
|
+
WHERE ticket_hash = ?`).get(ticketHash);
|
|
32
|
+
if (!row || row.used_at !== null || row.expires_at <= now.toISOString())
|
|
33
|
+
return undefined;
|
|
34
|
+
const consumed = connection.prepare(`UPDATE session_portal_tickets SET used_at = ?
|
|
35
|
+
WHERE ticket_hash = ? AND used_at IS NULL AND expires_at > ?`).run(now.toISOString(), ticketHash, now.toISOString());
|
|
36
|
+
if (consumed.changes !== 1)
|
|
37
|
+
return undefined;
|
|
38
|
+
const portalId = randomUUID();
|
|
39
|
+
const secret = randomToken();
|
|
40
|
+
const expiresAt = new Date(now.getTime() + GRANT_TTL_MS).toISOString();
|
|
41
|
+
connection.prepare(`INSERT INTO session_portal_grants
|
|
42
|
+
(portal_id, secret_hash, plugin_id, scope, access, session_id, created_at, expires_at, revoked_at)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(portalId, hash(secret), row.plugin_id, row.scope, row.access, row.session_id, now.toISOString(), expiresAt);
|
|
44
|
+
return {
|
|
45
|
+
portalId,
|
|
46
|
+
secret,
|
|
47
|
+
pluginId: row.plugin_id,
|
|
48
|
+
scope: row.scope,
|
|
49
|
+
access: row.access,
|
|
50
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
51
|
+
expiresAt,
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
}
|
|
55
|
+
export function authorizeSessionPortalGrant(portalId, secret, now = new Date()) {
|
|
56
|
+
const row = db().prepare(`SELECT portal_id, secret_hash, plugin_id, scope, access, session_id, expires_at, revoked_at
|
|
57
|
+
FROM session_portal_grants
|
|
58
|
+
WHERE portal_id = ?`).get(portalId);
|
|
59
|
+
if (!row || row.revoked_at !== null || row.expires_at <= now.toISOString())
|
|
60
|
+
return undefined;
|
|
61
|
+
const actual = Buffer.from(hash(secret), "hex");
|
|
62
|
+
const expected = Buffer.from(row.secret_hash, "hex");
|
|
63
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
64
|
+
return undefined;
|
|
65
|
+
return {
|
|
66
|
+
portalId: row.portal_id,
|
|
67
|
+
pluginId: row.plugin_id,
|
|
68
|
+
scope: row.scope,
|
|
69
|
+
access: row.access,
|
|
70
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
71
|
+
expiresAt: row.expires_at,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function pruneSessionPortalAuthorization(now = new Date()) {
|
|
75
|
+
const connection = db();
|
|
76
|
+
connection.prepare("DELETE FROM session_portal_tickets WHERE expires_at <= ? OR used_at IS NOT NULL").run(now.toISOString());
|
|
77
|
+
connection.prepare("DELETE FROM session_portal_grants WHERE expires_at <= ? OR revoked_at IS NOT NULL").run(now.toISOString());
|
|
78
|
+
}
|
|
79
|
+
function randomToken() {
|
|
80
|
+
return randomBytes(32).toString("base64url");
|
|
81
|
+
}
|
|
82
|
+
function hash(value) {
|
|
83
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
84
|
+
}
|
package/dist/skills-catalog.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
-
import type { SkillDetail } from "@rynx-ai/protocol/control";
|
|
1
|
+
import { type LocalSkillRuntime, type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
+
import type { ControlLocalSkillCandidate, ControlLocalSkillImportResult, SkillDetail } from "@rynx-ai/protocol/control";
|
|
3
3
|
export declare function listCatalogSkills(): Promise<SkillMeta[]>;
|
|
4
4
|
/** Install a skill (or a repo's skills) into the catalog per a full install
|
|
5
5
|
* recipe; delegates to the core catalog layer (which also writes each skill's
|
|
6
6
|
* `.rynx-skill.json` install-info file — a catalog-only artifact). Returns
|
|
7
7
|
* the refreshed catalog. */
|
|
8
8
|
export declare function installCatalogSkill(recipe: SkillInstallRecipe): Promise<SkillMeta[]>;
|
|
9
|
+
export declare function scanLocalCatalogSkillCandidates(runtime: LocalSkillRuntime): Promise<ControlLocalSkillCandidate[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Register complete local runtime Skill bundles into the owner catalog. Every
|
|
12
|
+
* content hash gets a durable immutable artifact; the mutable catalog entry's
|
|
13
|
+
* receipt points at that artifact so old Agent specs keep their exact bytes
|
|
14
|
+
* after a later catalog update.
|
|
15
|
+
*/
|
|
16
|
+
export declare function importLocalCatalogSkills(runtime: LocalSkillRuntime, keys: string[]): Promise<ControlLocalSkillImportResult[]>;
|
|
17
|
+
export declare function importCatalogSkillsFromArchive(filename: string, contentBase64: string): Promise<ControlLocalSkillImportResult[]>;
|
|
9
18
|
/** Detail for one catalog skill: its metadata + the file tree under its dir. */
|
|
10
19
|
export declare function getCatalogSkill(name: string): Promise<SkillDetail | null>;
|
|
11
20
|
/** Read one text file inside a catalog skill. Path-confined to the skill dir,
|