@rynx-ai/daemon 0.1.11-beta.5 → 0.2.0-beta.2

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.
@@ -5,18 +5,6 @@
5
5
  "displayName": "Lark",
6
6
  "description": "Lark and Feishu channel integration managed by the plugin.",
7
7
  "apiRange": "^1.0.0",
8
- "capabilities": [
9
- "conversation.run",
10
- "agent.catalog.read",
11
- "runtime.catalog.read",
12
- "session.cancel",
13
- "session.fork",
14
- "session.goal.read",
15
- "session.goal.write",
16
- "session.meta.read",
17
- "session.meta.title.write",
18
- "session.provision"
19
- ],
20
8
  "runtime": {
21
9
  "entry": "./dist/runtime.js"
22
10
  },
@@ -13,8 +13,8 @@ interface HostSessionLaunchMeta {
13
13
  export declare const LARK_RUN_EVENT_QUEUE_MAX_ITEMS = 256;
14
14
  export declare const LARK_RUN_EVENT_QUEUE_MAX_BYTES: number;
15
15
  /**
16
- * ConversationRuntime-shaped adapter backed exclusively by capability-checked
17
- * Host RPC. The plugin never receives the daemon's runtime, abort controller,
16
+ * ConversationRuntime-shaped adapter backed exclusively by the typed Host API.
17
+ * The plugin never receives the daemon's runtime, abort controller,
18
18
  * steer controller, or session bus.
19
19
  */
20
20
  export declare class HostConversationRuntime implements LarkConversationRuntime {
@@ -6,8 +6,8 @@ import { FileLarkActiveSessionStore, } from "./lark-session-store.js";
6
6
  export const LARK_RUN_EVENT_QUEUE_MAX_ITEMS = 256;
7
7
  export const LARK_RUN_EVENT_QUEUE_MAX_BYTES = 8 * 1024 * 1024;
8
8
  /**
9
- * ConversationRuntime-shaped adapter backed exclusively by capability-checked
10
- * Host RPC. The plugin never receives the daemon's runtime, abort controller,
9
+ * ConversationRuntime-shaped adapter backed exclusively by the typed Host API.
10
+ * The plugin never receives the daemon's runtime, abort controller,
11
11
  * steer controller, or session bus.
12
12
  */
13
13
  export class HostConversationRuntime {
@@ -37,7 +37,7 @@ export class HostConversationRuntime {
37
37
  const interrupt = () => {
38
38
  if (!runId || terminal)
39
39
  return;
40
- void this.host.call("session.interrupt", { runId }).catch(() => undefined);
40
+ void this.host.sessions.interrupt({ runId }).catch(() => undefined);
41
41
  };
42
42
  const accept = (event, payload) => {
43
43
  if (!runId) {
@@ -86,7 +86,7 @@ export class HostConversationRuntime {
86
86
  unsubscribeEvents();
87
87
  let started;
88
88
  try {
89
- started = await this.host.call("session.run", {
89
+ started = await this.host.sessions.run({
90
90
  sessionId: input.threadId,
91
91
  message: input.message,
92
92
  });
@@ -109,7 +109,7 @@ export class HostConversationRuntime {
109
109
  const unsteer = input.steer?.subscribe((text) => {
110
110
  if (!runId || terminal)
111
111
  return;
112
- void this.host.call("session.steer", { runId, text }).catch(() => undefined);
112
+ void this.host.sessions.steer({ runId, text }).catch(() => undefined);
113
113
  });
114
114
  input.signal?.addEventListener("abort", interrupt, { once: true });
115
115
  const generator = (async function* () {
@@ -141,16 +141,16 @@ export class HostAgentCapabilities {
141
141
  this.host = host;
142
142
  }
143
143
  listModels(runtime) {
144
- return this.host.call("session.models", runtime ? { runtime } : {});
144
+ return this.host.sessions.listModels(runtime ? { runtime } : undefined);
145
145
  }
146
146
  getGoal(sessionId) {
147
- return this.host.call("session.goal.get", { sessionId });
147
+ return this.host.sessions.goals.get({ sessionId });
148
148
  }
149
149
  setGoal(sessionId, objective) {
150
- return this.host.call("session.goal.set", { sessionId, objective });
150
+ return this.host.sessions.goals.set({ sessionId, objective });
151
151
  }
152
152
  clearGoal(sessionId) {
153
- return this.host.call("session.goal.clear", { sessionId });
153
+ return this.host.sessions.goals.clear({ sessionId });
154
154
  }
155
155
  }
156
156
  /** Narrow agent catalog + first-turn execution resolver backed by Host RPC. */
@@ -160,13 +160,13 @@ export class HostAgentCatalog {
160
160
  this.host = host;
161
161
  }
162
162
  list() {
163
- return this.host.call("agent.catalog.list", {});
163
+ return this.host.agents.list();
164
164
  }
165
165
  get(id) {
166
- return this.host.call("agent.catalog.get", { id });
166
+ return this.host.agents.get({ id });
167
167
  }
168
168
  resolveExecution(input) {
169
- return this.host.call("agent.execution.resolve", input);
169
+ return this.host.agents.resolveExecution(input);
170
170
  }
171
171
  }
172
172
  /** Daemon-owned session allocator. operationId must identify one allocation attempt durably. */
@@ -179,7 +179,7 @@ export class HostSessionProvisioner {
179
179
  this.instanceId = instanceId;
180
180
  }
181
181
  async provision(operationId, session = {}) {
182
- const result = await this.host.call("session.ensure", {
182
+ const result = await this.host.sessions.ensure({
183
183
  scopeId: this.instanceId,
184
184
  operationId,
185
185
  session,
@@ -203,7 +203,7 @@ export class HostSessionProvisioner {
203
203
  }
204
204
  async existingExecution(sessionId) {
205
205
  try {
206
- const session = await this.host.call("session.meta.get", { sessionId });
206
+ const session = await this.host.sessions.get({ sessionId });
207
207
  return {
208
208
  runtime: session.execution.provider,
209
209
  model: session.execution.model,
@@ -225,7 +225,7 @@ export class HostSessionProvisioner {
225
225
  return stableOperationId("fork", this.instanceId, fromSessionId, newSessionKey);
226
226
  }
227
227
  async fork(operationId, sourceSessionId, title) {
228
- const result = await this.host.call("session.fork", {
228
+ const result = await this.host.sessions.fork({
229
229
  sourceSessionId,
230
230
  operationId,
231
231
  ...(title ? { title } : {}),
@@ -235,7 +235,7 @@ export class HostSessionProvisioner {
235
235
  return result.sessionId;
236
236
  }
237
237
  async setTitle(sessionId, title) {
238
- await this.host.call("session.meta.setTitle", { sessionId, title });
238
+ await this.host.sessions.setTitle({ sessionId, title });
239
239
  }
240
240
  }
241
241
  /**
@@ -126096,6 +126096,11 @@ var envSchema = external_exports.object({
126096
126096
  /** The generic control API is local-first and unauthenticated by default. */
126097
126097
  HOST: external_exports.string().default("127.0.0.1"),
126098
126098
  PORT: external_exports.coerce.number().int().positive().default(3e3),
126099
+ /** Optional, grant-gated native Session Portal listener. It is deliberately separate
126100
+ * from the generic Control Web listener and remains disabled unless a port is
126101
+ * configured explicitly. */
126102
+ RYNX_SESSION_PORTAL_HOST: external_exports.string().default("127.0.0.1"),
126103
+ RYNX_SESSION_PORTAL_PORT: external_exports.coerce.number().int().positive().optional(),
126099
126104
  LOG_LEVEL: external_exports.string().default("info"),
126100
126105
  /**
126101
126106
  * Default agent runtime for conversations that haven't picked one via the
@@ -126880,7 +126885,7 @@ var HostConversationRuntime = class {
126880
126885
  };
126881
126886
  const interrupt = () => {
126882
126887
  if (!runId || terminal) return;
126883
- void this.host.call("session.interrupt", { runId }).catch(() => void 0);
126888
+ void this.host.sessions.interrupt({ runId }).catch(() => void 0);
126884
126889
  };
126885
126890
  const accept = (event, payload) => {
126886
126891
  if (!runId) {
@@ -126926,7 +126931,7 @@ var HostConversationRuntime = class {
126926
126931
  if (earlyOverflow) unsubscribeEvents();
126927
126932
  let started;
126928
126933
  try {
126929
- started = await this.host.call("session.run", {
126934
+ started = await this.host.sessions.run({
126930
126935
  sessionId: input.threadId,
126931
126936
  message: input.message
126932
126937
  });
@@ -126945,7 +126950,7 @@ var HostConversationRuntime = class {
126945
126950
  }
126946
126951
  const unsteer = input.steer?.subscribe((text) => {
126947
126952
  if (!runId || terminal) return;
126948
- void this.host.call("session.steer", { runId, text }).catch(() => void 0);
126953
+ void this.host.sessions.steer({ runId, text }).catch(() => void 0);
126949
126954
  });
126950
126955
  input.signal?.addEventListener("abort", interrupt, { once: true });
126951
126956
  const generator = (async function* () {
@@ -126973,16 +126978,16 @@ var HostAgentCapabilities = class {
126973
126978
  }
126974
126979
  host;
126975
126980
  listModels(runtime) {
126976
- return this.host.call("session.models", runtime ? { runtime } : {});
126981
+ return this.host.sessions.listModels(runtime ? { runtime } : void 0);
126977
126982
  }
126978
126983
  getGoal(sessionId) {
126979
- return this.host.call("session.goal.get", { sessionId });
126984
+ return this.host.sessions.goals.get({ sessionId });
126980
126985
  }
126981
126986
  setGoal(sessionId, objective) {
126982
- return this.host.call("session.goal.set", { sessionId, objective });
126987
+ return this.host.sessions.goals.set({ sessionId, objective });
126983
126988
  }
126984
126989
  clearGoal(sessionId) {
126985
- return this.host.call("session.goal.clear", { sessionId });
126990
+ return this.host.sessions.goals.clear({ sessionId });
126986
126991
  }
126987
126992
  };
126988
126993
  var HostAgentCatalog = class {
@@ -126991,13 +126996,13 @@ var HostAgentCatalog = class {
126991
126996
  }
126992
126997
  host;
126993
126998
  list() {
126994
- return this.host.call("agent.catalog.list", {});
126999
+ return this.host.agents.list();
126995
127000
  }
126996
127001
  get(id) {
126997
- return this.host.call("agent.catalog.get", { id });
127002
+ return this.host.agents.get({ id });
126998
127003
  }
126999
127004
  resolveExecution(input) {
127000
- return this.host.call("agent.execution.resolve", input);
127005
+ return this.host.agents.resolveExecution(input);
127001
127006
  }
127002
127007
  };
127003
127008
  var HostSessionProvisioner = class {
@@ -127009,7 +127014,7 @@ var HostSessionProvisioner = class {
127009
127014
  instanceId;
127010
127015
  resolvedExecutions = /* @__PURE__ */ new Map();
127011
127016
  async provision(operationId, session = {}) {
127012
- const result = await this.host.call("session.ensure", {
127017
+ const result = await this.host.sessions.ensure({
127013
127018
  scopeId: this.instanceId,
127014
127019
  operationId,
127015
127020
  session
@@ -127030,7 +127035,7 @@ var HostSessionProvisioner = class {
127030
127035
  }
127031
127036
  async existingExecution(sessionId) {
127032
127037
  try {
127033
- const session = await this.host.call("session.meta.get", { sessionId });
127038
+ const session = await this.host.sessions.get({ sessionId });
127034
127039
  return {
127035
127040
  runtime: session.execution.provider,
127036
127041
  model: session.execution.model,
@@ -127048,7 +127053,7 @@ var HostSessionProvisioner = class {
127048
127053
  return stableOperationId("fork", this.instanceId, fromSessionId, newSessionKey);
127049
127054
  }
127050
127055
  async fork(operationId, sourceSessionId, title) {
127051
- const result = await this.host.call("session.fork", {
127056
+ const result = await this.host.sessions.fork({
127052
127057
  sourceSessionId,
127053
127058
  operationId,
127054
127059
  ...title ? { title } : {}
@@ -127057,7 +127062,7 @@ var HostSessionProvisioner = class {
127057
127062
  return result.sessionId;
127058
127063
  }
127059
127064
  async setTitle(sessionId, title) {
127060
- await this.host.call("session.meta.setTitle", { sessionId, title });
127065
+ await this.host.sessions.setTitle({ sessionId, title });
127061
127066
  }
127062
127067
  };
127063
127068
  var HostProvisionedLarkSessionStore = class extends FileLarkActiveSessionStore {
@@ -126032,6 +126032,11 @@ var envSchema = external_exports.object({
126032
126032
  /** The generic control API is local-first and unauthenticated by default. */
126033
126033
  HOST: external_exports.string().default("127.0.0.1"),
126034
126034
  PORT: external_exports.coerce.number().int().positive().default(3e3),
126035
+ /** Optional, grant-gated native Session Portal listener. It is deliberately separate
126036
+ * from the generic Control Web listener and remains disabled unless a port is
126037
+ * configured explicitly. */
126038
+ RYNX_SESSION_PORTAL_HOST: external_exports.string().default("127.0.0.1"),
126039
+ RYNX_SESSION_PORTAL_PORT: external_exports.coerce.number().int().positive().optional(),
126035
126040
  LOG_LEVEL: external_exports.string().default("info"),
126036
126041
  /**
126037
126042
  * Default agent runtime for conversations that haven't picked one via the
@@ -126363,13 +126368,13 @@ var HostAgentCatalog = class {
126363
126368
  }
126364
126369
  host;
126365
126370
  list() {
126366
- return this.host.call("agent.catalog.list", {});
126371
+ return this.host.agents.list();
126367
126372
  }
126368
126373
  get(id) {
126369
- return this.host.call("agent.catalog.get", { id });
126374
+ return this.host.agents.get({ id });
126370
126375
  }
126371
126376
  resolveExecution(input) {
126372
- return this.host.call("agent.execution.resolve", input);
126377
+ return this.host.agents.resolveExecution(input);
126373
126378
  }
126374
126379
  };
126375
126380
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.5",
3
+ "version": "0.2.0-beta.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -18,6 +18,7 @@ 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
23
  import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
23
24
  import { createBrowserArtifactManagementService } from "./browser-artifact-management.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";
@@ -797,6 +798,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
797
798
  registry: desktopBrowserHosts,
798
799
  daemonInstanceId,
799
800
  },
801
+ sessionPortalAuthorization: {
802
+ redeem: redeemSessionPortalTicket,
803
+ authorize: authorizeSessionPortalGrant,
804
+ ownsSession: sessionBelongsToPlugin,
805
+ },
800
806
  });
801
807
  baseShutdown = server.shutdown.bind(server);
802
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, capabilities, and immutable packages.
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
- migratePluginCapabilities(conn);
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 PluginCapability, type PluginHostEvent, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
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 capability-gated Host RPC surface.
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
  }
@@ -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 capability-gated Host RPC surface.
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
- capabilities: [...this.capabilities],
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 ? {
@@ -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, capabilities, runtime_state, runtime_error
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, @capabilities, 'stopped', NULL
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",
@@ -22,3 +22,7 @@ export declare function setSessionExecution(id: string, execution: MachineSessio
22
22
  export declare function removeSessionMeta(id: string): void;
23
23
  /** {@link SessionRegistry} over the same `sessions` table. */
24
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;
@@ -78,6 +78,22 @@ export const sessionRegistry = {
78
78
  setExecution: setSessionExecution,
79
79
  remove: removeSessionMeta,
80
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
+ }
81
97
  function rowToMeta(row) {
82
98
  return {
83
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.11-beta.5",
3
+ "version": "0.2.0-beta.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -47,17 +47,17 @@
47
47
  "pm2": "^6.0.0",
48
48
  "tar": "^7.5.19",
49
49
  "ws": "^8.21.0",
50
- "@rynx-ai/core": "0.1.11-beta.5",
51
- "@rynx-ai/emulator": "0.1.11-beta.5",
52
- "@rynx-ai/plugin-runner": "0.1.11-beta.5",
53
- "@rynx-ai/plugin-sdk": "0.1.11-beta.5",
54
- "@rynx-ai/protocol": "0.1.11-beta.5",
55
- "@rynx-ai/remote-runtime-client": "0.1.11-beta.5",
56
- "@rynx-ai/server": "0.1.11-beta.5"
50
+ "@rynx-ai/core": "0.2.0-beta.2",
51
+ "@rynx-ai/emulator": "0.2.0-beta.2",
52
+ "@rynx-ai/plugin-sdk": "0.2.0-beta.2",
53
+ "@rynx-ai/plugin-runner": "0.2.0-beta.2",
54
+ "@rynx-ai/remote-runtime-client": "0.2.0-beta.2",
55
+ "@rynx-ai/protocol": "0.2.0-beta.2",
56
+ "@rynx-ai/server": "0.2.0-beta.2"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/ws": "^8.18.1",
60
- "@rynx-ai/plugin-channel-lark": "0.1.11-beta.5"
60
+ "@rynx-ai/plugin-channel-lark": "0.2.0-beta.2"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",