mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 02:15:02 +00:00
Let Hub title workspaces created by its executions (#5302)
* fix(server): let Hub title created workspaces Agent creation can provision a workspace under hub.execute, but a separate title request required workspace.manage. Permit the same execution authority for title changes and their responses, and lock the Hub request contract with socket and permission tests. * test(server): keep Hub title repro free of conditional flow
This commit is contained in:
+5
-2
@@ -28,12 +28,15 @@ manage their own relationship or permissions.
|
||||
## Session grants and agent operations
|
||||
|
||||
Hub uses the same authenticated, resumable Session protocol as other clients. Its persisted
|
||||
`hub.execute` permission authorizes ordinary agent creation, messaging, cancellation, archival,
|
||||
agent/workspace observation, timeline subscriptions, and workspace recovery. This authority is
|
||||
`hub.execute` permission authorizes ordinary agent creation, workspace titling, messaging,
|
||||
cancellation, archival, agent/workspace observation, timeline subscriptions, and workspace recovery. This authority is
|
||||
daemon-wide; it is not limited to agents created by that Hub. Daemon configuration, terminals,
|
||||
browser control, and permission management still require their own permissions. See
|
||||
[permissions.md](permissions.md).
|
||||
|
||||
Creating an agent may create a directory workspace. The same `hub.execute` session can title that
|
||||
workspace through `workspace.title.set.request` without `workspace.manage`.
|
||||
|
||||
Clients using this contract check `server_info.features.hubAgentRpc` and
|
||||
`server_info.features.agentRequestReceipts` once. An older host must be upgraded; do not silently
|
||||
fall back to creating a fresh agent when continuation was requested.
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ A pairing invitation is neither. It is an expiring, single-use exchange that cre
|
||||
| `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` | Agent lifecycle, agent/workspace observation, and workspace recovery |
|
||||
| `hub.execute` | Agent lifecycle, workspace titling, observation, and recovery |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -78,11 +78,15 @@ describe("SessionAuthorization", () => {
|
||||
const authorization = new SessionAuthorization(["hub.execute"]);
|
||||
for (const type of [
|
||||
"create_agent_request",
|
||||
"send_agent_message_request",
|
||||
"workspace.title.set.request",
|
||||
"fetch_agents_request",
|
||||
"fetch_agent_request",
|
||||
"agent.timeline.set_subscription.request",
|
||||
"send_agent_message_request",
|
||||
"workspace.recovery.inspect.request",
|
||||
"workspace.recovery.restore.request",
|
||||
"archive_workspace_request",
|
||||
"cancel_agent_request",
|
||||
] as const) {
|
||||
expect(authorization.allowsInbound(inboundMessage(type))).toBe(true);
|
||||
}
|
||||
@@ -90,8 +94,17 @@ describe("SessionAuthorization", () => {
|
||||
"status",
|
||||
"agent_update",
|
||||
"agent_stream",
|
||||
"workspace_update",
|
||||
"rpc_error",
|
||||
"workspace.title.set.response",
|
||||
"fetch_agents_response",
|
||||
"fetch_agent_response",
|
||||
"agent.timeline.set_subscription.response",
|
||||
"send_agent_message_response",
|
||||
"workspace.recovery.inspect.response",
|
||||
"workspace.recovery.restore.response",
|
||||
"archive_workspace_response",
|
||||
"cancel_agent_response",
|
||||
] as const) {
|
||||
expect(authorization.allowsOutbound(outboundMessage(type))).toBe(true);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ const INBOUND_PERMISSION = {
|
||||
"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.title.set.request": ["workspace.manage", "hub.execute"],
|
||||
workspace_setup_status_request: "workspace.read",
|
||||
"workspace.setup.run.request": "workspace.write",
|
||||
write_project_config_request: "workspace.write",
|
||||
@@ -430,7 +430,7 @@ const OUTBOUND_PERMISSION = {
|
||||
"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.title.set.response": ["workspace.manage", "hub.execute"],
|
||||
workspace_setup_progress: "workspace.read",
|
||||
workspace_setup_status_response: "workspace.read",
|
||||
"workspace.setup.run.response": "workspace.write",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import type { SessionOutboundMessage } from "../messages.js";
|
||||
import { HubRelationshipHarness } from "./test-utils/relationship-harness.js";
|
||||
|
||||
let relationship: HubRelationshipHarness | null = null;
|
||||
@@ -16,6 +17,13 @@ async function launchRelationship(): Promise<HubRelationshipHarness> {
|
||||
return launched;
|
||||
}
|
||||
|
||||
function createdAgentWorkspaceId(message: SessionOutboundMessage): string {
|
||||
if (message.type !== "status" || message.payload.status !== "agent_created")
|
||||
throw new Error("Agent was not created");
|
||||
if (!message.payload.agent.workspaceId) throw new Error("Workspace was not created");
|
||||
return message.payload.agent.workspaceId;
|
||||
}
|
||||
|
||||
test("Hub retries one durable daemon execution across concurrency and reconstruction", async () => {
|
||||
const hub = await launchRelationship();
|
||||
|
||||
@@ -54,6 +62,46 @@ test("Hub execute can steer ordinary agents while unrelated administration stays
|
||||
expect(hub.serverInfoPermissions()).toEqual([["hub.execute"]]);
|
||||
});
|
||||
|
||||
test("Hub execute can title the workspace created with an agent", async () => {
|
||||
const hub = await launchRelationship();
|
||||
const created = await hub.requestOrdinary({
|
||||
type: "create_agent_request",
|
||||
requestId: "create-for-title",
|
||||
config: { provider: "codex", cwd: hub.repoRoot() },
|
||||
});
|
||||
expect(created).toMatchObject({
|
||||
type: "status",
|
||||
payload: { status: "agent_created", agent: { workspaceId: expect.any(String) } },
|
||||
});
|
||||
const workspaceId = createdAgentWorkspaceId(created);
|
||||
|
||||
expect(
|
||||
await hub.requestOrdinary({
|
||||
type: "workspace.title.set.request",
|
||||
requestId: "title-created-workspace",
|
||||
workspaceId,
|
||||
title: "Hub execution",
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: "workspace.title.set.response",
|
||||
payload: { workspaceId, accepted: true, title: "Hub execution", error: null },
|
||||
});
|
||||
|
||||
expect(
|
||||
await hub.requestOrdinary({
|
||||
type: "fetch_workspaces_request",
|
||||
requestId: "read-created-workspace",
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
entries: expect.arrayContaining([
|
||||
expect.objectContaining({ id: workspaceId, title: "Hub execution" }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("ordinary Hub create and message retries do not duplicate agents or prompts", async () => {
|
||||
const hub = await launchRelationship();
|
||||
const create = {
|
||||
|
||||
Reference in New Issue
Block a user