mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
feat(server): establish semantic session permissions (#3981)
Keep trusted-client and Hub behavior stable while replacing RPC-name grants with a semantic authorization boundary. This establishes the operation inventory needed for future principal, pairing, and resource-scoped access work.
This commit is contained in:
@@ -46,6 +46,7 @@ At the start of non-trivial work, list `docs/` and skim anything relevant to the
|
||||
| [docs/rpc-namespacing.md](docs/rpc-namespacing.md) | WebSocket RPC naming convention — dotted namespaces and `.request`/`.response` pairs |
|
||||
| [docs/protocol-compatibility.md](docs/protocol-compatibility.md) | Why app/daemon versions drift, protocol vs feature contract, capability gating, COMPAT tagging |
|
||||
| [docs/protocol-validation.md](docs/protocol-validation.md) | zod-aot generated inbound WebSocket validation, patched compiler regressions, schema-purity rules |
|
||||
| [docs/permissions.md](docs/permissions.md) | Semantic daemon permissions, principals, credentials, pairing invitations, and Hub authority |
|
||||
| [docs/terminal-performance.md](docs/terminal-performance.md) | Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage |
|
||||
| [docs/agent-stream-performance.md](docs/agent-stream-performance.md) | Assistant text pipeline — coalescing window, paced reveal, why arrival lumps are smoothed at render |
|
||||
| [docs/file-observation.md](docs/file-observation.md) | Recursive watcher ownership, Linux constraints, teardown invariants, and Parcel comparison |
|
||||
|
||||
+4
-4
@@ -24,10 +24,10 @@ receive only `hub.execution.*` authority, so execution credentials cannot manage
|
||||
## Session grants and execution ownership
|
||||
|
||||
Trusted clients and the Hub use the same `Session` implementation. The connection boundary supplies
|
||||
grants: trusted clients receive `*`, while an enrolled Hub connection receives its persisted
|
||||
`hub.execution.*` grant. One matcher handles exact RPC names and trailing namespace wildcards for
|
||||
both inbound requests and outbound messages. A denied request returns the ordinary `rpc_error`
|
||||
shape.
|
||||
semantic permissions: trusted clients receive the owner permission set, while an enrolled Hub
|
||||
connection receives `hub.execute`. The current enrollment and persistence format still carries
|
||||
`hub.execution.*`; one compatibility boundary translates it before the session is created. A denied
|
||||
request returns the ordinary `rpc_error` shape. See [permissions.md](permissions.md).
|
||||
|
||||
The Hub connection still has a narrow lifecycle boundary: it has no trusted-client hello/resume,
|
||||
browser, binary, retained-session, or broadcast state. Its outbound execution events include only
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Daemon permissions
|
||||
|
||||
The daemon authorizes principals with semantic permissions. RPC names and protocol namespaces are not authority.
|
||||
|
||||
## Model
|
||||
|
||||
```text
|
||||
principal -> grants
|
||||
|
|
||||
+-- authenticated by a device or service credential
|
||||
`-- opens a session with equal or narrower authority
|
||||
```
|
||||
|
||||
A principal is the durable identity the daemon authorizes. A credential proves that a device or service represents it. Keep them separate so you can rotate credentials, attach more than one device, and revoke a Hub user without inventing daemon user accounts.
|
||||
|
||||
A pairing invitation is neither. It is an expiring, single-use exchange that creates a principal and credential with the permissions selected by its issuer.
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Authority |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| `daemon.read` | Daemon status, diagnostics, configuration, and provider information |
|
||||
| `daemon.manage` | Restart, update, configuration changes, providers, skills, and plugins |
|
||||
| `tunnel.manage` | Relay, Hub, service tunnel, and public endpoint relationships |
|
||||
| `access.manage` | Pairing invitations, principals, credentials, grants, and revocation |
|
||||
| `workspace.read` | Projects, workspaces, agents, timelines, files, diffs, and terminal output |
|
||||
| `workspace.write` | Prompts, agent control, files, terminals, git operations, and scripts |
|
||||
| `workspace.manage` | Create, rename, archive, and remove projects and workspaces |
|
||||
| `automation.manage` | Schedules, heartbeats, and loops |
|
||||
| `hub.execute` | The Hub-owned execution lifecycle |
|
||||
|
||||
Agents and terminals use workspace authority. Both can execute code and mutate the workspace, so separate write permissions would claim an isolation boundary the daemon cannot enforce.
|
||||
|
||||
Owner, operator, and viewer are UI presets expanded into explicit permissions. Do not persist them as roles. Adding a permission must not silently widen an existing principal.
|
||||
|
||||
Permissions are additive allows. Missing authority denies the operation. Do not add deny precedence.
|
||||
|
||||
## Resources
|
||||
|
||||
Permissions are daemon-wide today. Future grants may select workspaces or agents, but operation classification remains inside the authorization module:
|
||||
|
||||
```ts
|
||||
type Grant = {
|
||||
permission: Permission;
|
||||
resource: { kind: "daemon" } | { kind: "workspace"; ids: string[] };
|
||||
};
|
||||
```
|
||||
|
||||
A delegating principal can grant only authority it already possesses. A session may attenuate its principal's grants but cannot widen them.
|
||||
|
||||
Workspace-scoped grants require every resource-bearing operation and outbound observation to enforce the same workspace boundary. File preview currently accepts any daemon-readable regular file, so it must gain resource enforcement before workspace-specific access ships.
|
||||
|
||||
## Hub
|
||||
|
||||
The Hub authenticates as a service principal. Its locally selected grants decide whether it may execute agents, manage the daemon, manage tunnels, or manage access.
|
||||
|
||||
Hub user and role identifiers remain opaque external subjects. The Hub may create and revoke linked daemon principals when granted `access.manage`; the daemon does not interpret accounts, organizations, or roles.
|
||||
|
||||
The current Hub enrollment protocol still exchanges `hub.execution.*`. The daemon translates that legacy value to `hub.execute` once at the connection boundary. Remove the translation when Hub enrollment exchanges semantic permissions.
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
SessionInboundMessageSchema,
|
||||
SessionOutboundMessageSchema,
|
||||
type SessionInboundMessage,
|
||||
type SessionOutboundMessage,
|
||||
} from "../messages.js";
|
||||
import {
|
||||
DAEMON_PERMISSIONS,
|
||||
OWNER_PERMISSIONS,
|
||||
SessionAuthorization,
|
||||
permissionsForLegacyHubScopes,
|
||||
} from "./index.js";
|
||||
|
||||
function inboundOperationTypes(): SessionInboundMessage["type"][] {
|
||||
return SessionInboundMessageSchema.options.map((option) => option.shape.type.value);
|
||||
}
|
||||
|
||||
function outboundOperationTypes(): SessionOutboundMessage["type"][] {
|
||||
return SessionOutboundMessageSchema.options.map((option) => option.shape.type.value);
|
||||
}
|
||||
|
||||
function inboundMessage(type: SessionInboundMessage["type"]): SessionInboundMessage {
|
||||
return { type } as SessionInboundMessage;
|
||||
}
|
||||
|
||||
function outboundMessage(type: SessionOutboundMessage["type"]): SessionOutboundMessage {
|
||||
return { type } as SessionOutboundMessage;
|
||||
}
|
||||
|
||||
describe("SessionAuthorization", () => {
|
||||
test("owner authority covers every session operation", () => {
|
||||
const authorization = new SessionAuthorization(OWNER_PERMISSIONS);
|
||||
|
||||
expect(
|
||||
inboundOperationTypes().every((type) => authorization.allowsInbound(inboundMessage(type))),
|
||||
).toBe(true);
|
||||
expect(
|
||||
outboundOperationTypes().every((type) => authorization.allowsOutbound(outboundMessage(type))),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("semantic permissions authorize operations instead of RPC namespaces", () => {
|
||||
const authorization = new SessionAuthorization(["hub.execute"]);
|
||||
|
||||
expect(authorization.allowsInbound(inboundMessage("hub.execution.agent.create.request"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(authorization.allowsOutbound(outboundMessage("hub.execution.agent.update"))).toBe(true);
|
||||
expect(authorization.allowsInbound(inboundMessage("ping"))).toBe(false);
|
||||
expect(
|
||||
authorization.allowsInbound(inboundMessage("hub.management.daemon.get_status.request")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("correlated authorization errors can always be emitted", () => {
|
||||
const authorization = new SessionAuthorization([]);
|
||||
|
||||
expect(authorization.allowsOutbound(outboundMessage("rpc_error"))).toBe(true);
|
||||
});
|
||||
|
||||
test("legacy Hub authority is translated at one compatibility boundary", () => {
|
||||
expect(permissionsForLegacyHubScopes(["hub.execution.*"])).toEqual(["hub.execute"]);
|
||||
expect(permissionsForLegacyHubScopes(["*"])).toEqual([]);
|
||||
});
|
||||
|
||||
test("permission names are semantic", () => {
|
||||
expect(
|
||||
DAEMON_PERMISSIONS.every(
|
||||
(permission) => !permission.includes("*") && !permission.includes("request"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { SessionInboundMessage, SessionOutboundMessage } from "../messages.js";
|
||||
import {
|
||||
requiredPermissionForInbound,
|
||||
requiredPermissionForOutbound,
|
||||
} from "./operation-permissions.js";
|
||||
|
||||
export const DAEMON_PERMISSIONS = [
|
||||
"daemon.read",
|
||||
"daemon.manage",
|
||||
"tunnel.manage",
|
||||
"access.manage",
|
||||
"workspace.read",
|
||||
"workspace.write",
|
||||
"workspace.manage",
|
||||
"automation.manage",
|
||||
"hub.execute",
|
||||
] as const;
|
||||
|
||||
export type DaemonPermission = (typeof DAEMON_PERMISSIONS)[number];
|
||||
|
||||
export const OWNER_PERMISSIONS: readonly DaemonPermission[] = DAEMON_PERMISSIONS;
|
||||
|
||||
export class SessionAuthorization {
|
||||
private permissions: ReadonlySet<DaemonPermission>;
|
||||
|
||||
constructor(permissions: readonly DaemonPermission[]) {
|
||||
this.permissions = new Set(permissions);
|
||||
}
|
||||
|
||||
allowsInbound(message: SessionInboundMessage): boolean {
|
||||
return this.allows(requiredPermissionForInbound(message.type));
|
||||
}
|
||||
|
||||
allowsOutbound(message: SessionOutboundMessage): boolean {
|
||||
return this.allows(requiredPermissionForOutbound(message.type));
|
||||
}
|
||||
|
||||
replacePermissions(permissions: readonly DaemonPermission[]): void {
|
||||
this.permissions = new Set(permissions);
|
||||
}
|
||||
|
||||
private allows(permission: DaemonPermission | null): boolean {
|
||||
return permission === null || this.permissions.has(permission);
|
||||
}
|
||||
}
|
||||
|
||||
const LEGACY_HUB_EXECUTION_SCOPE = "hub.execution.*";
|
||||
|
||||
export function permissionsForLegacyHubScopes(
|
||||
scopes: readonly string[],
|
||||
): readonly DaemonPermission[] {
|
||||
// COMPAT(semanticHubPermissions): added in v0.7, remove after Hub enrollment uses permissions.
|
||||
return scopes.includes(LEGACY_HUB_EXECUTION_SCOPE) ? ["hub.execute"] : [];
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import type { SessionInboundMessage, SessionOutboundMessage } from "../messages.js";
|
||||
import type { DaemonPermission } from "./index.js";
|
||||
|
||||
type InboundOperation = SessionInboundMessage["type"];
|
||||
type OutboundOperation = SessionOutboundMessage["type"];
|
||||
|
||||
const INBOUND_PERMISSION = {
|
||||
abort_request: "workspace.write",
|
||||
"agent.config.apply.request": "workspace.write",
|
||||
"agent.detach.request": "workspace.write",
|
||||
"agent.fork_context.request": "workspace.read",
|
||||
"agent.provider_subagents.list.request": "workspace.read",
|
||||
"agent.provider_subagents.timeline.get.request": "workspace.read",
|
||||
"agent.rewind.request": "workspace.write",
|
||||
"agent.skills.get_status.request": "daemon.read",
|
||||
"agent.skills.import_legacy_selection.request": "daemon.manage",
|
||||
"agent.skills.reconcile.request": "daemon.manage",
|
||||
"agent.skills.save_selection.request": "daemon.manage",
|
||||
"agent.skills.uninstall.request": "daemon.manage",
|
||||
"agent.timeline.list_prompts.request": "workspace.read",
|
||||
"agent.timeline.set_subscription.request": "workspace.read",
|
||||
agent_permission_response: "workspace.write",
|
||||
archive_agent_request: "workspace.write",
|
||||
archive_workspace_request: "workspace.manage",
|
||||
audio_played: "workspace.write",
|
||||
branch_suggestions_request: "workspace.read",
|
||||
"browser.automation.execute.response": "workspace.write",
|
||||
cancel_agent_request: "workspace.write",
|
||||
capture_terminal_request: "workspace.read",
|
||||
"chat/create": "workspace.write",
|
||||
"chat/delete": "workspace.write",
|
||||
"chat/inspect": "workspace.read",
|
||||
"chat/list": "workspace.read",
|
||||
"chat/post": "workspace.write",
|
||||
"chat/read": "workspace.read",
|
||||
"chat/wait": "workspace.read",
|
||||
"checkout.commits.file_diff.request": "workspace.read",
|
||||
"checkout.commits.list.request": "workspace.read",
|
||||
"checkout.discard_changes.request": "workspace.write",
|
||||
"checkout.forge.get_check_details.request": "workspace.read",
|
||||
"checkout.forge.set_auto_merge.request": "workspace.write",
|
||||
"checkout.github.get_check_details.request": "workspace.read",
|
||||
"checkout.github.set_auto_merge.request": "workspace.write",
|
||||
"checkout.refresh.request": "workspace.read",
|
||||
"checkout.rename_branch.request": "workspace.write",
|
||||
checkout_commit_request: "workspace.write",
|
||||
checkout_merge_from_base_request: "workspace.write",
|
||||
checkout_merge_request: "workspace.write",
|
||||
checkout_pr_create_request: "workspace.write",
|
||||
checkout_pr_merge_request: "workspace.write",
|
||||
checkout_pr_status_request: "workspace.read",
|
||||
checkout_pull_request: "workspace.write",
|
||||
checkout_push_request: "workspace.write",
|
||||
checkout_status_request: "workspace.read",
|
||||
checkout_switch_branch_request: "workspace.write",
|
||||
clear_agent_attention: "workspace.write",
|
||||
client_heartbeat: "workspace.read",
|
||||
close_items_request: "workspace.manage",
|
||||
create_agent_request: "workspace.write",
|
||||
create_paseo_worktree_request: "workspace.manage",
|
||||
create_terminal_request: "workspace.write",
|
||||
"daemon.config.reload.request": "daemon.manage",
|
||||
"daemon.get_pairing_offer.request": "access.manage",
|
||||
"daemon.get_status.request": "daemon.read",
|
||||
"daemon.update.request": "daemon.manage",
|
||||
delete_agent_request: "workspace.write",
|
||||
"diagnostics.request": "daemon.read",
|
||||
dictation_stream_cancel: "workspace.write",
|
||||
dictation_stream_chunk: "workspace.write",
|
||||
dictation_stream_finish: "workspace.write",
|
||||
dictation_stream_start: "workspace.write",
|
||||
directory_suggestions_request: "workspace.read",
|
||||
fetch_agent_history_request: "workspace.read",
|
||||
fetch_agent_request: "workspace.read",
|
||||
fetch_agent_timeline_request: "workspace.read",
|
||||
fetch_agents_request: "workspace.read",
|
||||
fetch_recent_provider_sessions_request: "workspace.read",
|
||||
fetch_workspaces_request: "workspace.read",
|
||||
"file.upload.request": "workspace.write",
|
||||
file_download_token_request: "workspace.read",
|
||||
file_explorer_request: "workspace.read",
|
||||
"forge.search.request": "workspace.read",
|
||||
"fs.entry.create.request": "workspace.write",
|
||||
"fs.entry.delete.request": "workspace.write",
|
||||
"fs.entry.duplicate.request": "workspace.write",
|
||||
"fs.entry.rename.request": "workspace.write",
|
||||
"fs.file.subscribe.request": "workspace.read",
|
||||
"fs.file.unsubscribe.request": "workspace.read",
|
||||
"fs.file.write.request": "workspace.write",
|
||||
get_daemon_config_request: "daemon.read",
|
||||
get_providers_snapshot_request: "daemon.read",
|
||||
github_search_request: "workspace.read",
|
||||
"hub.execution.agent.create.request": "hub.execute",
|
||||
"hub.execution.agent.validate.request": "hub.execute",
|
||||
"hub.execution.control.request": "hub.execute",
|
||||
"hub.management.daemon.connect.request": "tunnel.manage",
|
||||
"hub.management.daemon.disconnect.request": "tunnel.manage",
|
||||
"hub.management.daemon.get_status.request": "tunnel.manage",
|
||||
import_agent_request: "workspace.write",
|
||||
kill_terminal_request: "workspace.write",
|
||||
list_available_editors_request: "workspace.read",
|
||||
list_available_providers_request: "daemon.read",
|
||||
list_commands_request: "daemon.read",
|
||||
list_provider_features_request: "daemon.read",
|
||||
list_provider_models_request: "daemon.read",
|
||||
list_provider_modes_request: "daemon.read",
|
||||
list_terminals_request: "workspace.read",
|
||||
"loop/inspect": "automation.manage",
|
||||
"loop/list": "automation.manage",
|
||||
"loop/logs": "automation.manage",
|
||||
"loop/run": "automation.manage",
|
||||
"loop/stop": "automation.manage",
|
||||
open_in_editor_request: "workspace.write",
|
||||
open_project_request: "workspace.manage",
|
||||
paseo_worktree_archive_request: "workspace.manage",
|
||||
paseo_worktree_list_request: "workspace.read",
|
||||
ping: "daemon.read",
|
||||
"plugin.catalog.get.request": "daemon.manage",
|
||||
"plugin.directory.inspect.request": "daemon.manage",
|
||||
"plugin.directory.install.request": "daemon.manage",
|
||||
"plugin.disable.request": "daemon.manage",
|
||||
"plugin.enable.request": "daemon.manage",
|
||||
"plugin.list.request": "daemon.manage",
|
||||
"plugin.logs.get.request": "daemon.manage",
|
||||
"plugin.reload.request": "daemon.manage",
|
||||
"plugin.remove.request": "daemon.manage",
|
||||
"plugin.rpc.invoke.request": "daemon.manage",
|
||||
"plugin.source.install.request": "daemon.manage",
|
||||
"plugin.source.status.request": "daemon.manage",
|
||||
"plugin.source.update.request": "daemon.manage",
|
||||
"project.add.request": "workspace.manage",
|
||||
"project.create_directory.request": "workspace.manage",
|
||||
"project.github.clone.request": "workspace.manage",
|
||||
"project.icon.get.request": "workspace.read",
|
||||
"project.icon.set.request": "workspace.manage",
|
||||
"project.list.request": "workspace.read",
|
||||
"project.remove.request": "workspace.manage",
|
||||
"project.rename.request": "workspace.manage",
|
||||
project_icon_request: "workspace.read",
|
||||
"provider.usage.list.request": "daemon.read",
|
||||
provider_diagnostic_request: "daemon.read",
|
||||
pull_request_timeline_request: "workspace.read",
|
||||
"push.unregister.request": "workspace.read",
|
||||
read_project_config_request: "workspace.read",
|
||||
refresh_agent_request: "workspace.write",
|
||||
refresh_providers_snapshot_request: "daemon.read",
|
||||
register_push_token: "workspace.read",
|
||||
restart_server_request: "daemon.manage",
|
||||
resume_agent_request: "workspace.write",
|
||||
"schedule/create": "automation.manage",
|
||||
"schedule/delete": "automation.manage",
|
||||
"schedule/inspect": "automation.manage",
|
||||
"schedule/list": "automation.manage",
|
||||
"schedule/logs": "automation.manage",
|
||||
"schedule/pause": "automation.manage",
|
||||
"schedule/resume": "automation.manage",
|
||||
"schedule/run-once": "automation.manage",
|
||||
"schedule/update": "automation.manage",
|
||||
send_agent_message_request: "workspace.write",
|
||||
set_agent_feature_request: "workspace.write",
|
||||
set_agent_mode_request: "workspace.write",
|
||||
set_agent_model_request: "workspace.write",
|
||||
set_agent_thinking_request: "workspace.write",
|
||||
set_daemon_config_request: "daemon.manage",
|
||||
set_voice_mode: "workspace.write",
|
||||
shutdown_server_request: "daemon.manage",
|
||||
start_workspace_script_request: "workspace.write",
|
||||
stash_list_request: "workspace.read",
|
||||
stash_pop_request: "workspace.write",
|
||||
stash_save_request: "workspace.write",
|
||||
subscribe_checkout_diff_request: "workspace.read",
|
||||
subscribe_terminal_request: "workspace.read",
|
||||
subscribe_terminals_request: "workspace.read",
|
||||
"terminal.rename.request": "workspace.write",
|
||||
terminal_input: "workspace.write",
|
||||
unsubscribe_checkout_diff_request: "workspace.read",
|
||||
unsubscribe_terminal_request: "workspace.read",
|
||||
unsubscribe_terminals_request: "workspace.read",
|
||||
update_agent_request: "workspace.write",
|
||||
validate_branch_request: "workspace.read",
|
||||
voice_audio_chunk: "workspace.write",
|
||||
wait_for_finish_request: "workspace.read",
|
||||
"workspace.clear_attention.request": "workspace.write",
|
||||
"workspace.create.request": "workspace.manage",
|
||||
"workspace.github.search_repositories.request": "workspace.read",
|
||||
"workspace.label.assignment.set.request": "workspace.manage",
|
||||
"workspace.label.delete.inspect.request": "workspace.read",
|
||||
"workspace.label.delete.request": "workspace.manage",
|
||||
"workspace.label.list.request": "workspace.read",
|
||||
"workspace.label.update.request": "workspace.manage",
|
||||
"workspace.pin.set.request": "workspace.manage",
|
||||
"workspace.recovery.inspect.request": "workspace.read",
|
||||
"workspace.recovery.restore.request": "workspace.manage",
|
||||
"workspace.script.list.request": "workspace.read",
|
||||
"workspace.script.start.request": "workspace.write",
|
||||
"workspace.script.stop.request": "workspace.write",
|
||||
"workspace.title.set.request": "workspace.manage",
|
||||
workspace_setup_status_request: "workspace.read",
|
||||
write_project_config_request: "workspace.write",
|
||||
} as const satisfies Record<InboundOperation, DaemonPermission | null>;
|
||||
|
||||
const OUTBOUND_PERMISSION = {
|
||||
activity_log: "workspace.read",
|
||||
"agent.config.apply.response": "workspace.write",
|
||||
"agent.detach.response": "workspace.write",
|
||||
"agent.fork_context.response": "workspace.read",
|
||||
"agent.provider_subagents.list.response": "workspace.read",
|
||||
"agent.provider_subagents.timeline.get.response": "workspace.read",
|
||||
"agent.provider_subagents.update": "workspace.read",
|
||||
"agent.rewind.response": "workspace.write",
|
||||
"agent.skills.get_status.response": "daemon.read",
|
||||
"agent.skills.import_legacy_selection.response": "daemon.manage",
|
||||
"agent.skills.reconcile.response": "daemon.manage",
|
||||
"agent.skills.save_selection.response": "daemon.manage",
|
||||
"agent.skills.uninstall.response": "daemon.manage",
|
||||
"agent.timeline.list_prompts.response": "workspace.read",
|
||||
"agent.timeline.replacement": "workspace.read",
|
||||
"agent.timeline.set_subscription.response": "workspace.read",
|
||||
agent_archived: "workspace.read",
|
||||
agent_attention_required: "workspace.read",
|
||||
agent_deleted: "workspace.read",
|
||||
agent_permission_request: "workspace.read",
|
||||
agent_permission_resolved: "workspace.write",
|
||||
agent_status: "workspace.read",
|
||||
agent_stream: "workspace.read",
|
||||
agent_update: "workspace.read",
|
||||
archive_workspace_response: "workspace.manage",
|
||||
artifact: "workspace.read",
|
||||
assistant_chunk: "workspace.read",
|
||||
audio_output: "workspace.write",
|
||||
branch_suggestions_response: "workspace.read",
|
||||
"browser.automation.execute.request": "workspace.write",
|
||||
cancel_agent_response: "workspace.write",
|
||||
capture_terminal_response: "workspace.read",
|
||||
"chat/create/response": "workspace.write",
|
||||
"chat/delete/response": "workspace.write",
|
||||
"chat/inspect/response": "workspace.read",
|
||||
"chat/list/response": "workspace.read",
|
||||
"chat/post/response": "workspace.write",
|
||||
"chat/read/response": "workspace.read",
|
||||
"chat/wait/response": "workspace.read",
|
||||
"checkout.commits.file_diff.response": "workspace.read",
|
||||
"checkout.commits.list.response": "workspace.read",
|
||||
"checkout.discard_changes.response": "workspace.write",
|
||||
"checkout.forge.get_check_details.response": "workspace.read",
|
||||
"checkout.forge.set_auto_merge.response": "workspace.write",
|
||||
"checkout.github.get_check_details.response": "workspace.read",
|
||||
"checkout.github.set_auto_merge.response": "workspace.write",
|
||||
"checkout.refresh.response": "workspace.read",
|
||||
"checkout.rename_branch.response": "workspace.write",
|
||||
checkout_commit_response: "workspace.write",
|
||||
checkout_diff_update: "workspace.read",
|
||||
checkout_merge_from_base_response: "workspace.write",
|
||||
checkout_merge_response: "workspace.write",
|
||||
checkout_pr_create_response: "workspace.write",
|
||||
checkout_pr_merge_response: "workspace.write",
|
||||
checkout_pr_status_response: "workspace.read",
|
||||
checkout_pull_response: "workspace.write",
|
||||
checkout_push_response: "workspace.write",
|
||||
checkout_status_response: "workspace.read",
|
||||
checkout_status_update: "workspace.read",
|
||||
checkout_switch_branch_response: "workspace.write",
|
||||
clear_agent_attention_response: "workspace.write",
|
||||
close_items_response: "workspace.manage",
|
||||
create_paseo_worktree_response: "workspace.manage",
|
||||
create_terminal_response: "workspace.write",
|
||||
"daemon.config.reload.response": "daemon.manage",
|
||||
"daemon.get_pairing_offer.response": "access.manage",
|
||||
"daemon.get_status.response": "daemon.read",
|
||||
"daemon.update.progress": "daemon.manage",
|
||||
"daemon.update.response": "daemon.manage",
|
||||
"diagnostics.response": "daemon.read",
|
||||
dictation_stream_ack: "workspace.write",
|
||||
dictation_stream_error: "workspace.write",
|
||||
dictation_stream_final: "workspace.write",
|
||||
dictation_stream_finish_accepted: "workspace.write",
|
||||
dictation_stream_partial: "workspace.write",
|
||||
directory_suggestions_response: "workspace.read",
|
||||
fetch_agent_history_response: "workspace.read",
|
||||
fetch_agent_response: "workspace.read",
|
||||
fetch_agent_timeline_response: "workspace.read",
|
||||
fetch_agents_response: "workspace.read",
|
||||
fetch_recent_provider_sessions_response: "workspace.read",
|
||||
fetch_workspaces_response: "workspace.read",
|
||||
"file.upload.response": "workspace.write",
|
||||
file_download_token_response: "workspace.read",
|
||||
file_explorer_response: "workspace.read",
|
||||
"forge.search.response": "workspace.read",
|
||||
"fs.entry.create.response": "workspace.write",
|
||||
"fs.entry.delete.response": "workspace.write",
|
||||
"fs.entry.duplicate.response": "workspace.write",
|
||||
"fs.entry.rename.response": "workspace.write",
|
||||
"fs.file.subscribe.response": "workspace.read",
|
||||
"fs.file.unsubscribe.response": "workspace.read",
|
||||
"fs.file.update": "workspace.read",
|
||||
"fs.file.write.response": "workspace.write",
|
||||
get_daemon_config_response: "daemon.read",
|
||||
get_providers_snapshot_response: "daemon.read",
|
||||
github_search_response: "workspace.read",
|
||||
"hub.execution.agent.create.response": "hub.execute",
|
||||
"hub.execution.agent.stream": "hub.execute",
|
||||
"hub.execution.agent.update": "hub.execute",
|
||||
"hub.execution.agent.validate.response": "hub.execute",
|
||||
"hub.execution.control.response": "hub.execute",
|
||||
"hub.management.daemon.connect.response": "tunnel.manage",
|
||||
"hub.management.daemon.disconnect.response": "tunnel.manage",
|
||||
"hub.management.daemon.get_status.response": "tunnel.manage",
|
||||
kill_terminal_response: "workspace.write",
|
||||
list_available_editors_response: "workspace.read",
|
||||
list_available_providers_response: "daemon.read",
|
||||
list_commands_response: "daemon.read",
|
||||
list_provider_features_response: "daemon.read",
|
||||
list_provider_models_response: "daemon.read",
|
||||
list_provider_modes_response: "daemon.read",
|
||||
list_terminals_response: "workspace.read",
|
||||
"loop/inspect/response": "automation.manage",
|
||||
"loop/list/response": "automation.manage",
|
||||
"loop/logs/response": "automation.manage",
|
||||
"loop/run/response": "automation.manage",
|
||||
"loop/stop/response": "automation.manage",
|
||||
open_in_editor_response: "workspace.read",
|
||||
open_project_response: "workspace.manage",
|
||||
paseo_worktree_archive_response: "workspace.manage",
|
||||
paseo_worktree_list_response: "workspace.read",
|
||||
"plugin.catalog.get.response": "daemon.manage",
|
||||
"plugin.directory.inspect.response": "daemon.manage",
|
||||
"plugin.directory.install.response": "daemon.manage",
|
||||
"plugin.disable.response": "daemon.manage",
|
||||
"plugin.enable.response": "daemon.manage",
|
||||
"plugin.list.response": "daemon.manage",
|
||||
"plugin.logs.get.response": "daemon.manage",
|
||||
"plugin.reload.response": "daemon.manage",
|
||||
"plugin.remove.response": "daemon.manage",
|
||||
"plugin.rpc.invoke.response": "daemon.manage",
|
||||
"plugin.source.install.response": "daemon.manage",
|
||||
"plugin.source.status.response": "daemon.manage",
|
||||
"plugin.source.update.response": "daemon.manage",
|
||||
pong: "daemon.read",
|
||||
"project.add.response": "workspace.manage",
|
||||
"project.create_directory.response": "workspace.manage",
|
||||
"project.github.clone.response": "workspace.manage",
|
||||
"project.icon.get.response": "workspace.read",
|
||||
"project.icon.set.response": "workspace.manage",
|
||||
"project.list.response": "workspace.read",
|
||||
"project.remove.response": "workspace.manage",
|
||||
"project.rename.response": "workspace.manage",
|
||||
"project.update": "workspace.read",
|
||||
project_icon_response: "workspace.read",
|
||||
"provider.usage.list.response": "daemon.read",
|
||||
provider_diagnostic_response: "daemon.read",
|
||||
providers_snapshot_update: "daemon.read",
|
||||
pull_request_timeline_response: "workspace.read",
|
||||
"push.unregister.response": "workspace.read",
|
||||
read_project_config_response: "workspace.read",
|
||||
refresh_providers_snapshot_response: "daemon.read",
|
||||
rpc_error: null,
|
||||
"schedule/create/response": "automation.manage",
|
||||
"schedule/delete/response": "automation.manage",
|
||||
"schedule/inspect/response": "automation.manage",
|
||||
"schedule/list/response": "automation.manage",
|
||||
"schedule/logs/response": "automation.manage",
|
||||
"schedule/pause/response": "automation.manage",
|
||||
"schedule/resume/response": "automation.manage",
|
||||
"schedule/run-once/response": "automation.manage",
|
||||
"schedule/update/response": "automation.manage",
|
||||
script_status_update: "workspace.read",
|
||||
send_agent_message_response: "workspace.write",
|
||||
set_agent_feature_response: "workspace.write",
|
||||
set_agent_mode_response: "workspace.write",
|
||||
set_agent_model_response: "workspace.write",
|
||||
set_agent_thinking_response: "workspace.write",
|
||||
set_daemon_config_response: "daemon.manage",
|
||||
set_voice_mode_response: "workspace.write",
|
||||
start_workspace_script_response: "workspace.write",
|
||||
stash_list_response: "workspace.read",
|
||||
stash_pop_response: "workspace.read",
|
||||
stash_save_response: "workspace.read",
|
||||
status: "daemon.read",
|
||||
subscribe_checkout_diff_response: "workspace.read",
|
||||
subscribe_terminal_response: "workspace.read",
|
||||
"terminal.rename.response": "workspace.write",
|
||||
terminal_attention_required: "workspace.read",
|
||||
terminal_stream_exit: "workspace.read",
|
||||
terminals_changed: "workspace.read",
|
||||
transcription_result: "workspace.write",
|
||||
update_agent_response: "workspace.write",
|
||||
validate_branch_response: "workspace.read",
|
||||
voice_input_state: "workspace.write",
|
||||
wait_for_finish_response: "workspace.read",
|
||||
"workspace.clear_attention.response": "workspace.write",
|
||||
"workspace.create.response": "workspace.manage",
|
||||
"workspace.github.search_repositories.response": "workspace.read",
|
||||
"workspace.label.assignment.set.response": "workspace.manage",
|
||||
"workspace.label.delete.inspect.response": "workspace.read",
|
||||
"workspace.label.delete.response": "workspace.manage",
|
||||
"workspace.label.list.response": "workspace.read",
|
||||
"workspace.label.update": "workspace.read",
|
||||
"workspace.label.update.response": "workspace.manage",
|
||||
"workspace.pin.set.response": "workspace.manage",
|
||||
"workspace.recovery.inspect.response": "workspace.read",
|
||||
"workspace.recovery.restore.response": "workspace.manage",
|
||||
"workspace.script.list.response": "workspace.read",
|
||||
"workspace.script.start.response": "workspace.write",
|
||||
"workspace.script.stop.response": "workspace.write",
|
||||
"workspace.title.set.response": "workspace.manage",
|
||||
workspace_setup_progress: "workspace.read",
|
||||
workspace_setup_status_response: "workspace.read",
|
||||
workspace_update: "workspace.read",
|
||||
write_project_config_response: "workspace.write",
|
||||
} as const satisfies Record<OutboundOperation, DaemonPermission | null>;
|
||||
|
||||
export function requiredPermissionForInbound(operation: InboundOperation): DaemonPermission | null {
|
||||
return INBOUND_PERMISSION[operation];
|
||||
}
|
||||
|
||||
export function requiredPermissionForOutbound(
|
||||
operation: OutboundOperation,
|
||||
): DaemonPermission | null {
|
||||
return OUTBOUND_PERMISSION[operation];
|
||||
}
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
FileTransferOpcode,
|
||||
type FileTransferFrame,
|
||||
} from "@getpaseo/protocol/binary-frames/index";
|
||||
import { isSessionRpcAllowed, Session } from "./session.js";
|
||||
import { Session } from "./session.js";
|
||||
import { OWNER_PERMISSIONS, type DaemonPermission } from "./authorization/index.js";
|
||||
import { DownloadTokenStore } from "./file-download/token-store.js";
|
||||
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
|
||||
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
||||
@@ -281,7 +282,7 @@ vi.mock("./worktree-bootstrap.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
interface SessionForTestOptions {
|
||||
scopes?: readonly string[];
|
||||
permissions?: readonly DaemonPermission[];
|
||||
agentManager?: { [K in keyof SessionOptions["agentManager"]]?: unknown };
|
||||
agentStorage?: { [K in keyof SessionOptions["agentStorage"]]?: unknown };
|
||||
github?: Partial<ForgeService & GitHubService>;
|
||||
@@ -426,7 +427,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session {
|
||||
serverId: options.serverId,
|
||||
daemonVersion: options.daemonVersion,
|
||||
daemonRuntimeConfig: options.daemonRuntimeConfig,
|
||||
scopes: options.scopes ?? ["*"],
|
||||
permissions: options.permissions ?? OWNER_PERMISSIONS,
|
||||
};
|
||||
return new Session(sessionOptions);
|
||||
}
|
||||
@@ -721,7 +722,7 @@ describe("workspace label editing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session authorization scopes", () => {
|
||||
describe("session authorization permissions", () => {
|
||||
test("routes named-agent validation through the session source", async () => {
|
||||
const messages: SessionOutboundMessage[] = [];
|
||||
const providers = createProviderSnapshotManagerStub();
|
||||
@@ -760,10 +761,10 @@ describe("session authorization scopes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects an RPC outside an exact grant with the generic RPC error", async () => {
|
||||
test("rejects an operation without its semantic permission", async () => {
|
||||
const messages: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForTest({
|
||||
scopes: ["hub.execution.agent.create.request"],
|
||||
permissions: ["hub.execute"],
|
||||
messages,
|
||||
});
|
||||
|
||||
@@ -782,32 +783,16 @@ describe("session authorization scopes", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["*", "ping"],
|
||||
["hub.execution.*", "hub.execution.agent.create.request"],
|
||||
["hub.execution.agent.create.request", "hub.execution.agent.create.request"],
|
||||
])("scope %s authorizes %s", (scope, requestType) => {
|
||||
expect(isSessionRpcAllowed([scope], requestType)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["hub.execution.*", "hub.management.daemon.get_status.request"],
|
||||
["hub.execution.agent.create.request", "hub.execution.agent.update"],
|
||||
["hub.execution.*", "hub.executions.agent.create.request"],
|
||||
])("scope %s rejects %s", (scope, requestType) => {
|
||||
expect(isSessionRpcAllowed([scope], requestType)).toBe(false);
|
||||
});
|
||||
|
||||
test("replaces a session's scopes without reconstructing the session", async () => {
|
||||
test("replaces a session's permissions without reconstructing the session", async () => {
|
||||
const messages: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForTest({ scopes: ["hub.execution.*"], messages });
|
||||
const session = createSessionForTest({ permissions: ["hub.execute"], messages });
|
||||
|
||||
await session.handleMessage({
|
||||
type: "ping",
|
||||
requestId: "before-scope-change",
|
||||
clientSentAt: 1,
|
||||
});
|
||||
session.setScopes(["*"]);
|
||||
session.setPermissions(["daemon.read"]);
|
||||
await session.handleMessage({ type: "ping", requestId: "after-scope-change", clientSentAt: 2 });
|
||||
|
||||
expect(messages).toEqual([
|
||||
|
||||
@@ -244,6 +244,7 @@ import {
|
||||
} from "./worktree-session.js";
|
||||
import { archiveByScope, type ActiveWorkspaceRef } from "./workspace-archive-service.js";
|
||||
import { WorkspaceSetupRuntime } from "./workspace-setup-runtime.js";
|
||||
import { SessionAuthorization, type DaemonPermission } from "./authorization/index.js";
|
||||
|
||||
function resolveWorkspaceSetupRuntime(
|
||||
runtime: WorkspaceSetupRuntime | undefined,
|
||||
@@ -434,7 +435,7 @@ type AgentMcpTransportFactory = () => Promise<unknown>;
|
||||
|
||||
export interface SessionOptions {
|
||||
clientId: string;
|
||||
scopes: readonly string[];
|
||||
permissions: readonly DaemonPermission[];
|
||||
appVersion?: string | null;
|
||||
clientCapabilities?: Record<string, unknown> | null;
|
||||
onMessage: (msg: SessionOutboundMessage) => void;
|
||||
@@ -568,18 +569,6 @@ function parseClientCapabilities(
|
||||
return new Set(result);
|
||||
}
|
||||
|
||||
export function isSessionRpcAllowed(scopes: readonly string[], rpcName: string): boolean {
|
||||
return scopes.some((scope) => {
|
||||
if (scope === "*" || scope === rpcName) {
|
||||
return true;
|
||||
}
|
||||
if (!scope.endsWith(".*")) {
|
||||
return false;
|
||||
}
|
||||
return rpcName.startsWith(scope.slice(0, -1));
|
||||
});
|
||||
}
|
||||
|
||||
function sessionRequestId(message: SessionInboundMessage): string | null {
|
||||
if ("requestId" in message && typeof message.requestId === "string") {
|
||||
return message.requestId;
|
||||
@@ -652,7 +641,7 @@ function workspaceLabelErrorCode(error: unknown): string {
|
||||
|
||||
export class Session {
|
||||
private readonly clientId: string;
|
||||
private scopes: readonly string[];
|
||||
private readonly authorization: SessionAuthorization;
|
||||
private appVersion: string | null;
|
||||
private clientCapabilities: ReadonlySet<ClientCapability>;
|
||||
private readonly sessionId: string;
|
||||
@@ -753,7 +742,7 @@ export class Session {
|
||||
constructor(options: SessionOptions) {
|
||||
const {
|
||||
clientId,
|
||||
scopes,
|
||||
permissions,
|
||||
appVersion,
|
||||
clientCapabilities,
|
||||
onMessage,
|
||||
@@ -808,7 +797,7 @@ export class Session {
|
||||
getWebSocketRuntimeMetrics,
|
||||
} = options;
|
||||
this.clientId = clientId;
|
||||
this.scopes = [...scopes];
|
||||
this.authorization = new SessionAuthorization(permissions);
|
||||
this.appVersion = appVersion ?? null;
|
||||
this.clientCapabilities = parseClientCapabilities(clientCapabilities);
|
||||
this.sessionId = uuidv4();
|
||||
@@ -1864,7 +1853,7 @@ export class Session {
|
||||
},
|
||||
"agent.session.inbound",
|
||||
);
|
||||
if (!isSessionRpcAllowed(this.scopes, msg.type)) {
|
||||
if (!this.authorization.allowsInbound(msg)) {
|
||||
const requestId = sessionRequestId(msg);
|
||||
if (requestId) {
|
||||
this.emit({
|
||||
@@ -1918,8 +1907,8 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
public setScopes(scopes: readonly string[]): void {
|
||||
this.scopes = [...scopes];
|
||||
public setPermissions(permissions: readonly DaemonPermission[]): void {
|
||||
this.authorization.replacePermissions(permissions);
|
||||
}
|
||||
|
||||
private async dispatchInboundMessage(msg: SessionInboundMessage, source?: object): Promise<void> {
|
||||
@@ -7515,7 +7504,7 @@ export class Session {
|
||||
* Emit a message to the client
|
||||
*/
|
||||
private emit(msg: SessionOutboundMessage): void {
|
||||
if (msg.type !== "rpc_error" && !isSessionRpcAllowed(this.scopes, msg.type)) {
|
||||
if (!this.authorization.allowsOutbound(msg)) {
|
||||
return;
|
||||
}
|
||||
// JSON.stringify(msg) is only computed when trace is enabled — it runs for
|
||||
|
||||
@@ -4,6 +4,7 @@ import type pino from "pino";
|
||||
import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js";
|
||||
import { createServiceProxySubsystem, type ServiceProxySubsystem } from "./service-proxy.js";
|
||||
import { Session, type SessionOptions } from "./session.js";
|
||||
import { OWNER_PERMISSIONS } from "./authorization/index.js";
|
||||
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
|
||||
import { createTestLogger } from "../test-utils/test-logger.js";
|
||||
@@ -191,7 +192,7 @@ function createSessionForWorkspaceGitWatchTests(options?: {
|
||||
|
||||
const session = new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message as { type: string; payload: unknown }),
|
||||
logger: createStub<pino.Logger>(logger),
|
||||
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
|
||||
|
||||
@@ -9,6 +9,7 @@ import path from "node:path";
|
||||
import { expect, test, vi } from "vitest";
|
||||
|
||||
import { Session, type SessionOptions } from "./session.js";
|
||||
import { OWNER_PERMISSIONS } from "./authorization/index.js";
|
||||
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
|
||||
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
|
||||
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||
@@ -88,7 +89,7 @@ function createHarness(input: {
|
||||
|
||||
const session = new Session({
|
||||
clientId: "test",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
appVersion: null,
|
||||
onMessage: (m) => emitted.push(m),
|
||||
logger: createStub<SessionOptions["logger"]>(logger),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
|
||||
import { createTestLogger } from "../test-utils/test-logger.js";
|
||||
import { Session } from "./session.js";
|
||||
import type { SessionOptions } from "./session.js";
|
||||
import { OWNER_PERMISSIONS } from "./authorization/index.js";
|
||||
import type { AgentUpdatesService } from "./session/agent-updates/agent-updates-service.js";
|
||||
import type { AgentSnapshotPayload, SessionOutboundMessage } from "@getpaseo/protocol/messages";
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
@@ -633,7 +634,7 @@ function createSessionForWorkspaceTests(
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
appVersion: options.appVersion ?? null,
|
||||
onMessage: options.onMessage ?? vi.fn(),
|
||||
onWorkspaceRecovered: options.onWorkspaceRecovered,
|
||||
@@ -990,7 +991,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
serverId: "test-server",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
appVersion: null,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(logger),
|
||||
@@ -1142,7 +1143,7 @@ test("create_agent_request launches from an exact subdirectory in a created work
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
appVersion: null,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(logger),
|
||||
@@ -1279,7 +1280,7 @@ test("create_agent_request does not title an existing workspace from the agent p
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
appVersion: null,
|
||||
onMessage: vi.fn(),
|
||||
logger: asSessionLogger(logger),
|
||||
@@ -1548,7 +1549,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients"
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(logger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
@@ -1913,7 +1914,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(sessionLogger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
@@ -2081,7 +2082,7 @@ test("close_items_request archives stored agents that are not currently loaded",
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(sessionLogger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
@@ -2240,7 +2241,7 @@ test("close_items_request continues after an archive failure", async () => {
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(sessionLogger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
@@ -3501,7 +3502,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage: (message) => emitted.push(message),
|
||||
logger: asSessionLogger(logger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { Session } from "./session.js";
|
||||
import type { SessionOptions } from "./session.js";
|
||||
import { OWNER_PERMISSIONS } from "./authorization/index.js";
|
||||
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
||||
import { asInternals, createStub } from "./test-utils/class-mocks.js";
|
||||
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
|
||||
@@ -99,7 +100,7 @@ describe("snapshot mutation ownership boundary", () => {
|
||||
const session = asInternals<SessionInternals>(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
onMessage,
|
||||
logger: createStub<SessionOptions["logger"]>(logger),
|
||||
downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}),
|
||||
|
||||
@@ -98,6 +98,11 @@ import {
|
||||
import type { BrowserToolsBroker } from "./browser-tools/broker.js";
|
||||
import type { DaemonRuntimeConfig } from "./session/daemon/daemon-session.js";
|
||||
import { DirectorySyncService } from "./directory-sync/index.js";
|
||||
import {
|
||||
OWNER_PERMISSIONS,
|
||||
permissionsForLegacyHubScopes,
|
||||
type DaemonPermission,
|
||||
} from "./authorization/index.js";
|
||||
import type { WorkspaceLabelService } from "./workspace-labels/index.js";
|
||||
import {
|
||||
APPLICATION_SOCKET_LEASE_CHECK_INTERVAL_MS,
|
||||
@@ -479,7 +484,7 @@ interface SocketSessionOptions {
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
clientCapabilities: Record<string, unknown> | null;
|
||||
scopes: readonly string[];
|
||||
permissions: readonly DaemonPermission[];
|
||||
connectionLogger: pino.Logger;
|
||||
onMessage: (message: SessionOutboundMessage) => void;
|
||||
onMessageToSource?: (source: object, message: SessionOutboundMessage) => void;
|
||||
@@ -1017,7 +1022,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
clientId: `hub:${options.daemonId}`,
|
||||
appVersion: null,
|
||||
clientCapabilities: null,
|
||||
scopes: options.scopes,
|
||||
permissions: permissionsForLegacyHubScopes(options.scopes),
|
||||
connectionLogger,
|
||||
onMessage: (message) => this.sendToClient(ws, wrapSessionMessage(message)),
|
||||
hubExecutionAgents: options.agents,
|
||||
@@ -1339,7 +1344,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
clientId,
|
||||
appVersion,
|
||||
clientCapabilities,
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
connectionLogger,
|
||||
onMessage: (msg) => {
|
||||
if (!connection) {
|
||||
@@ -1409,7 +1414,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
clientId: options.clientId,
|
||||
appVersion: options.appVersion,
|
||||
clientCapabilities: options.clientCapabilities,
|
||||
scopes: options.scopes,
|
||||
permissions: options.permissions,
|
||||
onMessage: options.onMessage,
|
||||
onMessageToSource: options.onMessageToSource,
|
||||
onBinaryMessage: options.onBinaryMessage,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type SessionOutboundMessage,
|
||||
} from "@getpaseo/protocol/messages";
|
||||
import { Session, type SessionOptions } from "./session.js";
|
||||
import { OWNER_PERMISSIONS } from "./authorization/index.js";
|
||||
import { DirectorySyncService } from "./directory-sync/index.js";
|
||||
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
|
||||
import type { AgentTimelineRow } from "./agent/agent-manager.js";
|
||||
@@ -206,7 +207,7 @@ function createSessionForWireCompatTest(options?: {
|
||||
|
||||
const session = new Session({
|
||||
clientId: "wire-compat-client",
|
||||
scopes: ["*"],
|
||||
permissions: OWNER_PERMISSIONS,
|
||||
clientCapabilities: options?.clientCapabilities ?? null,
|
||||
onMessage: (message) => messages.push(message),
|
||||
logger: pino({ level: "silent" }),
|
||||
|
||||
Reference in New Issue
Block a user