From eb753813a198958dcf75a08d65b71b77c60ffdd7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 3 Sep 2026 00:08:30 +0200 Subject: [PATCH] feat(hub): expose provider catalogs to executions (#4242) Hub execution credentials need provider snapshot access so remote workflow editors can present the daemon's native models, modes, and thinking options without gaining broader daemon read authority. --- .../src/server/authorization/index.test.ts | 15 +++++++++++++ .../server/src/server/authorization/index.ts | 7 +++++-- .../authorization/operation-permissions.ts | 21 +++++++++---------- public-docs/hub/triggers/index.md | 8 +++++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/authorization/index.test.ts b/packages/server/src/server/authorization/index.test.ts index 6f5b2b717d..18e7feca2b 100644 --- a/packages/server/src/server/authorization/index.test.ts +++ b/packages/server/src/server/authorization/index.test.ts @@ -48,6 +48,21 @@ describe("SessionAuthorization", () => { true, ); expect(authorization.allowsOutbound(outboundMessage("hub.execution.agent.update"))).toBe(true); + expect(authorization.allowsInbound(inboundMessage("get_providers_snapshot_request"))).toBe( + true, + ); + expect(authorization.allowsInbound(inboundMessage("refresh_providers_snapshot_request"))).toBe( + true, + ); + expect(authorization.allowsOutbound(outboundMessage("get_providers_snapshot_response"))).toBe( + true, + ); + expect(authorization.allowsOutbound(outboundMessage("providers_snapshot_update"))).toBe(true); + expect( + authorization.allowsOutbound(outboundMessage("refresh_providers_snapshot_response")), + ).toBe(true); + expect(authorization.allowsInbound(inboundMessage("get_daemon_config_request"))).toBe(false); + expect(authorization.allowsInbound(inboundMessage("provider_diagnostic_request"))).toBe(false); expect(authorization.allowsInbound(inboundMessage("ping"))).toBe(false); expect( authorization.allowsInbound(inboundMessage("hub.management.daemon.get_status.request")), diff --git a/packages/server/src/server/authorization/index.ts b/packages/server/src/server/authorization/index.ts index 7cddda6194..38414f33f9 100644 --- a/packages/server/src/server/authorization/index.ts +++ b/packages/server/src/server/authorization/index.ts @@ -1,6 +1,7 @@ import type { SessionInboundMessage, SessionOutboundMessage } from "../messages.js"; import { DAEMON_PERMISSIONS, type DaemonPermission } from "@getpaseo/protocol/messages"; import { + type PermissionRequirement, requiredPermissionForInbound, requiredPermissionForOutbound, } from "./operation-permissions.js"; @@ -48,8 +49,10 @@ export class SessionAuthorization { return this.permissions.has(permission); } - private allows(permission: DaemonPermission | null): boolean { - return permission === null || this.permissions.has(permission); + private allows(requirement: PermissionRequirement): boolean { + if (requirement === null) return true; + if (typeof requirement === "string") return this.permissions.has(requirement); + return requirement.some((permission) => this.permissions.has(permission)); } } diff --git a/packages/server/src/server/authorization/operation-permissions.ts b/packages/server/src/server/authorization/operation-permissions.ts index ae5ab24544..95995bc65e 100644 --- a/packages/server/src/server/authorization/operation-permissions.ts +++ b/packages/server/src/server/authorization/operation-permissions.ts @@ -3,6 +3,7 @@ import type { DaemonPermission } from "./index.js"; type InboundOperation = SessionInboundMessage["type"]; type OutboundOperation = SessionOutboundMessage["type"]; +export type PermissionRequirement = DaemonPermission | readonly DaemonPermission[] | null; const INBOUND_PERMISSION = { abort_request: "workspace.write", @@ -89,7 +90,7 @@ const INBOUND_PERMISSION = { "fs.file.unsubscribe.request": "workspace.read", "fs.file.write.request": "workspace.write", get_daemon_config_request: "daemon.read", - get_providers_snapshot_request: "daemon.read", + get_providers_snapshot_request: ["daemon.read", "hub.execute"], github_search_request: "workspace.read", "hub.execution.agent.create.request": "hub.execute", "hub.execution.agent.validate.request": "hub.execute", @@ -145,7 +146,7 @@ const INBOUND_PERMISSION = { "push.unregister.request": "workspace.read", read_project_config_request: "workspace.read", refresh_agent_request: "workspace.write", - refresh_providers_snapshot_request: "daemon.read", + refresh_providers_snapshot_request: ["daemon.read", "hub.execute"], register_push_token: "workspace.read", restart_server_request: "daemon.manage", resume_agent_request: "workspace.write", @@ -200,7 +201,7 @@ const INBOUND_PERMISSION = { workspace_setup_status_request: "workspace.read", "workspace.setup.run.request": "workspace.write", write_project_config_request: "workspace.write", -} as const satisfies Record; +} as const satisfies Record; const OUTBOUND_PERMISSION = { activity_log: "workspace.read", @@ -299,7 +300,7 @@ const OUTBOUND_PERMISSION = { "fs.file.update": "workspace.read", "fs.file.write.response": "workspace.write", get_daemon_config_response: "daemon.read", - get_providers_snapshot_response: "daemon.read", + get_providers_snapshot_response: ["daemon.read", "hub.execute"], github_search_response: "workspace.read", "hub.execution.agent.create.response": "hub.execute", "hub.execution.agent.stream": "hub.execute", @@ -353,11 +354,11 @@ const OUTBOUND_PERMISSION = { project_icon_response: "workspace.read", "provider.usage.list.response": "daemon.read", provider_diagnostic_response: "daemon.read", - providers_snapshot_update: "daemon.read", + providers_snapshot_update: ["daemon.read", "hub.execute"], pull_request_timeline_response: "workspace.read", "push.unregister.response": "workspace.read", read_project_config_response: "workspace.read", - refresh_providers_snapshot_response: "daemon.read", + refresh_providers_snapshot_response: ["daemon.read", "hub.execute"], rpc_error: null, "schedule/create/response": "automation.manage", "schedule/delete/response": "automation.manage", @@ -413,14 +414,12 @@ const OUTBOUND_PERMISSION = { "workspace.setup.run.response": "workspace.write", workspace_update: "workspace.read", write_project_config_response: "workspace.write", -} as const satisfies Record; +} as const satisfies Record; -export function requiredPermissionForInbound(operation: InboundOperation): DaemonPermission | null { +export function requiredPermissionForInbound(operation: InboundOperation): PermissionRequirement { return INBOUND_PERMISSION[operation]; } -export function requiredPermissionForOutbound( - operation: OutboundOperation, -): DaemonPermission | null { +export function requiredPermissionForOutbound(operation: OutboundOperation): PermissionRequirement { return OUTBOUND_PERMISSION[operation]; } diff --git a/public-docs/hub/triggers/index.md b/public-docs/hub/triggers/index.md index 0ae72c1ee0..eb34349b3f 100644 --- a/public-docs/hub/triggers/index.md +++ b/public-docs/hub/triggers/index.md @@ -32,6 +32,14 @@ steps: Field-by-field detail is in the [configuration reference](/docs/hub/configuration/hub-yml). +## Choose the agent in Hub + +When you create or edit a trigger in the Hub dashboard, choose the daemon and enter its working directory first. Hub then asks that daemon for its available providers, models, execution modes, and thinking options. The suggested model and mode are the daemon's defaults. + +Changing the daemon or working directory reloads the choices. If an existing trigger names a model, mode, or thinking option that the daemon no longer reports, Hub marks that value unavailable without replacing it. You can keep the authored value, choose a current value, or switch to YAML editing. + +If the daemon is offline or needs a newer Paseo version, the agent selectors show an error and a retry action. The rest of the trigger and its YAML remain editable. + ## Events | `on` | Fires when |