@huynle/hub 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +430 -1
- package/Dockerfile +7 -2
- package/README.md +230 -32
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/paths.ts +3 -3
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6871 -3460
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
- package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
- package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +96 -14
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +2 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/launches.ts +9 -3
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +385 -10
- package/packages/cli/src/messaging.ts +7 -3
- package/packages/cli/src/profiles-mcp.ts +301 -0
- package/packages/cli/src/setup/commands.ts +23 -4
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/spoke-services.ts +1 -1
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +460 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/paths.ts +3 -3
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +104 -2
- package/packages/node/src/opencode-warm.ts +344 -0
- package/packages/node/src/run.ts +28 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +175 -6
- package/packages/protocol/src/launch.ts +36 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +4 -1
- package/packages/sdk/src/index.ts +6 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +568 -1
- package/packages/sdk/src/wake.ts +20 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +218 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +18 -1
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +125 -2
- package/src/central/agent/delivery.ts +194 -40
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +7 -0
- package/src/central/agent/escalation.ts +10 -2
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +18 -4
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +189 -32
- package/src/central/agent/launch-routes.ts +26 -10
- package/src/central/agent/launch-spec.ts +40 -3
- package/src/central/agent/launch-store.ts +55 -2
- package/src/central/agent/message-limits.ts +89 -11
- package/src/central/agent/message-routes.ts +10 -4
- package/src/central/agent/messages.ts +28 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/placement.ts +93 -18
- package/src/central/agent/read-routes.ts +89 -19
- package/src/central/agent/routes.ts +178 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent/webhooks.ts +17 -2
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +776 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +350 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +86 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +131 -0
- package/src/central/concierge/proposals.ts +268 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +122 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +125 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +767 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +279 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +28 -6
- package/src/central/http.ts +660 -54
- package/src/central/main.ts +1 -1
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +69 -7
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +786 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +910 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +126 -23
- package/src/migrate.ts +109 -3
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/env-inject.ts +8 -1
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +404 -25
- package/src/spoke/launchers.ts +6 -4
- package/src/spoke/listing.ts +65 -10
- package/src/spoke/localhub.ts +14 -4
- package/src/spoke/locations.ts +40 -0
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +256 -0
- package/src/spoke/spoke.ts +330 -23
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
- package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
- package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
- package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
- package/web/dist/assets/index-BabrNDB3.css +1 -0
- package/web/dist/assets/index-lIOfSYck.js +34 -0
- package/web/dist/assets/live-BDq0AABg.js +5 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
- package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
- package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
- package/web/dist/assets/source-CDU04kYw.js +1 -0
- package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- package/web/dist/assets/index-FUJbj_Du.js +0 -20
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// Workspace routes on /api/agent (HWA-06; contract: Workspaces in docs/plans/2026-10-30-hub-workspaces-artifacts-
|
|
2
|
+
// contracts.md and its clarifications from HWA-03/04/05/14):
|
|
3
|
+
//
|
|
4
|
+
// POST /workspaces {title, description?, tags?, metadata?} Idempotency-Key manage
|
|
5
|
+
// GET /workspaces?state=active|archived&limit&cursor read
|
|
6
|
+
// GET /workspaces/:id → {workspace, bindings} read
|
|
7
|
+
// PATCH /workspaces/:id {expectedRevision, title?, description?, tags?, metadata?} manage
|
|
8
|
+
// POST /workspaces/:id/archive {expectedRevision} → {workspace, departed} manage
|
|
9
|
+
// POST /workspaces/:id/restore {expectedRevision} manage
|
|
10
|
+
// GET /workspaces/:id/bindings read
|
|
11
|
+
// POST /workspaces/:id/bindings {machineID, repo, label?} Idempotency-Key manage
|
|
12
|
+
// DELETE /workspaces/:id/bindings?bindingID=&expectedRevision= manage
|
|
13
|
+
// GET /workspaces/:id/members?includeLeft=1 read + S
|
|
14
|
+
// POST /workspaces/:id/members {sessionID, role} Idempotency-Key manage + S
|
|
15
|
+
// PATCH /workspaces/:id/members/:memberID {role, expectedRevision} manage + S
|
|
16
|
+
// DELETE /workspaces/:id/members/:memberID?expectedRevision= manage + S
|
|
17
|
+
// GET /workspaces/:id/assignments?memberID&status&limit&cursor read + S
|
|
18
|
+
// GET /workspaces/:id/assignments/:assignmentID read + S
|
|
19
|
+
// POST /workspaces/:id/assignments {memberID, title, status?, criteria?, ref?} Idempotency-Key manage
|
|
20
|
+
// PATCH /workspaces/:id/assignments/:assignmentID {expectedRevision, status | memberID} manage
|
|
21
|
+
// GET /workspaces/:id/supervision → {workspaceID, revision, links} read
|
|
22
|
+
// PUT /workspaces/:id/supervision {memberID, supervisorMemberID|null, order?, expectedRevision} manage
|
|
23
|
+
//
|
|
24
|
+
// Who: users, always (their own records; another user's answer not_found). App tokens by scope workspaces:read /
|
|
25
|
+
// workspaces:manage; routes that show or name sessions (S: members with presence, joins, assignments with
|
|
26
|
+
// presence) also need sessions:read. Session tokens are refused: membership grants nothing, and no session reads
|
|
27
|
+
// or changes workspaces through its own token in v1.
|
|
28
|
+
// Every mutation of an existing record requires expectedRevision (supervision: the graph aggregate's revision);
|
|
29
|
+
// a stale one is 409 conflict with currentRevision. Creates accept Idempotency-Key: workspaces and assignments
|
|
30
|
+
// keep the key in their stores, bindings and members through the shared route helper (their natural keys,
|
|
31
|
+
// machine + repo and session + role, already make repeats return the existing record).
|
|
32
|
+
// Every change is in the admin audit (IDs only) and the journal (compact, text-free events). Reads are bounded:
|
|
33
|
+
// lists page (≤ LIST_LIMIT_MAX), and members, bindings and supervision links are capped per workspace.
|
|
34
|
+
// Not here: the board (HWA-10), checkpoints (HWA-11), master replacement (HWA-12), side chats (HWA-13,
|
|
35
|
+
// sidechats-routes.ts).
|
|
36
|
+
import { isExpectedRevision, WORKSPACE_SCOPES } from "../../protocol"
|
|
37
|
+
import type { AgentContext, AgentRoute } from "../agent/routes"
|
|
38
|
+
import type { AgentEvents, EventInput } from "../agent/events"
|
|
39
|
+
import { AgentError } from "../agent/errors"
|
|
40
|
+
import { idempotencyKeyOf, RouteIdempotency, storeAnswer } from "../agent/idempotency"
|
|
41
|
+
import type { Store } from "../store"
|
|
42
|
+
import { assignmentChangedEvent, bindingEvent, memberJoinedEvent, memberRoleEvent, supervisionSetEvent, workspaceArchivedEvent, workspaceCreatedEvent, workspacePatchedEvent } from "./events"
|
|
43
|
+
import { WorkspaceLifecycle } from "./lifecycle"
|
|
44
|
+
import type { MembershipStore } from "./members"
|
|
45
|
+
import type { WorkspaceStore } from "./store"
|
|
46
|
+
import type { SupervisionStore } from "./supervision"
|
|
47
|
+
import { AGENT_JSON_BODY_MAX, boundedText } from "../agent/body"
|
|
48
|
+
|
|
49
|
+
const PREFIX = "/api/agent/workspaces"
|
|
50
|
+
/** The shared route helper's records for bindings and members (store-level keys live in workspace-idempotency). */
|
|
51
|
+
export const WORKSPACE_ROUTE_IDEMPOTENCY = "workspace-route-idempotency"
|
|
52
|
+
const NO_STORE = { "cache-control": "no-store" }
|
|
53
|
+
const [READ_SCOPE, MANAGE_SCOPE] = WORKSPACE_SCOPES
|
|
54
|
+
const READ = { scope: READ_SCOPE } as const
|
|
55
|
+
const MANAGE = { scope: MANAGE_SCOPE } as const
|
|
56
|
+
|
|
57
|
+
export interface WorkspaceRoutesDeps {
|
|
58
|
+
readonly workspaces: WorkspaceStore
|
|
59
|
+
readonly members: MembershipStore
|
|
60
|
+
readonly supervision: SupervisionStore
|
|
61
|
+
/** Departures with their supervision and events; one over the stores and `events` when absent. */
|
|
62
|
+
readonly lifecycle?: WorkspaceLifecycle
|
|
63
|
+
readonly events?: Pick<AgentEvents, "append">
|
|
64
|
+
readonly audit?: Pick<Store, "audit">
|
|
65
|
+
/** Idempotent binding and member creates; one over the workspaces' StateStore when absent. */
|
|
66
|
+
readonly idempotency?: RouteIdempotency
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ok = (body: unknown, status = 200) => Response.json(body, { status, headers: NO_STORE })
|
|
70
|
+
|
|
71
|
+
async function bodyOf(request: Request): Promise<Record<string, unknown>> {
|
|
72
|
+
const text = await boundedText(request, AGENT_JSON_BODY_MAX)
|
|
73
|
+
const raw = JSON.parse(text === "" ? "{}" : text) as unknown
|
|
74
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new AgentError("invalid", "Body must be a JSON object")
|
|
75
|
+
return raw as Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function only(body: Record<string, unknown>, allowed: readonly string[]): void {
|
|
79
|
+
for (const key of Object.keys(body)) if (!allowed.includes(key)) throw new AgentError("invalid", `unknown field ${/^[A-Za-z0-9_-]{1,40}$/.test(key) ? key : "(unprintable)"}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The required expectedRevision of a mutation (body field or query parameter). */
|
|
83
|
+
function expected(value: unknown): number {
|
|
84
|
+
const n = typeof value === "string" && /^[0-9]{1,15}$/.test(value) ? Number(value) : value
|
|
85
|
+
if (n === undefined || n === null || n === "") throw new AgentError("invalid", "expectedRevision is required: the revision you read", { hint: "Read the record and send its revision" })
|
|
86
|
+
if (!isExpectedRevision(n)) throw new AgentError("invalid", "expectedRevision must be the revision you read")
|
|
87
|
+
return n as number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function intParam(url: URL, name: string): number | undefined {
|
|
91
|
+
const raw = url.searchParams.get(name)
|
|
92
|
+
return raw === null ? undefined : /^-?[0-9]{1,12}$/.test(raw) ? Number(raw) : Number.NaN
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const text = (url: URL, name: string) => url.searchParams.get(name) || undefined
|
|
96
|
+
|
|
97
|
+
/** Apps on routes that show or name sessions also need sessions:read. */
|
|
98
|
+
function sessionsVisible(context: AgentContext): void {
|
|
99
|
+
const { actor } = context
|
|
100
|
+
if (actor.kind === "app" && !actor.scopes.includes("sessions:read")) throw new AgentError("forbidden", "Member and assignment routes also need scope sessions:read")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function workspaceRoutes(deps: WorkspaceRoutesDeps): AgentRoute[] {
|
|
104
|
+
const { workspaces, members, supervision } = deps
|
|
105
|
+
const lifecycle = deps.lifecycle ?? new WorkspaceLifecycle({ workspaces, members, supervision, ...(deps.events ? { events: deps.events } : {}) })
|
|
106
|
+
const idempotency = deps.idempotency ?? new RouteIdempotency({ state: workspaces.state, collection: WORKSPACE_ROUTE_IDEMPOTENCY })
|
|
107
|
+
|
|
108
|
+
async function changed(context: AgentContext, action: string, target: string, ...events: EventInput[]): Promise<void> {
|
|
109
|
+
await deps.audit?.audit({ actor: context.user.id, action, target, detail: context.actor.kind })
|
|
110
|
+
await lifecycle.emit(context.user.id, ...events)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A create through the shared helper (it runs `perform` directly when no key is sent). */
|
|
114
|
+
const keyed = (context: AgentContext, body: Record<string, unknown>, perform: () => Promise<{ status: number; body: unknown }>): Promise<Response> =>
|
|
115
|
+
idempotency.run(context.user.id, `POST ${context.url.pathname}`, idempotencyKeyOf(context.request) ?? null, body, perform)
|
|
116
|
+
|
|
117
|
+
const id = (context: AgentContext) => context.params.id!
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
// ---------- workspaces ----------
|
|
121
|
+
{
|
|
122
|
+
method: "POST",
|
|
123
|
+
path: PREFIX,
|
|
124
|
+
allow: MANAGE,
|
|
125
|
+
async handle(context) {
|
|
126
|
+
const body = await bodyOf(context.request)
|
|
127
|
+
const key = idempotencyKeyOf(context.request)
|
|
128
|
+
const { workspace, created } = await workspaces.create(context.user.id, body, key === undefined ? {} : { idempotencyKey: key })
|
|
129
|
+
if (created) await changed(context, "workspace.create", workspace.id, workspaceCreatedEvent(workspace))
|
|
130
|
+
return storeAnswer(created, { workspace })
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
method: "GET",
|
|
135
|
+
path: PREFIX,
|
|
136
|
+
allow: READ,
|
|
137
|
+
async handle({ url, user }) {
|
|
138
|
+
const limit = intParam(url, "limit")
|
|
139
|
+
const state = text(url, "state")
|
|
140
|
+
const cursor = text(url, "cursor")
|
|
141
|
+
return ok(await workspaces.list(user.id, { ...(limit !== undefined ? { limit } : {}), ...(state ? { state } : {}), ...(cursor ? { cursor } : {}) }))
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
method: "GET",
|
|
146
|
+
path: `${PREFIX}/:id`,
|
|
147
|
+
allow: READ,
|
|
148
|
+
async handle(context) {
|
|
149
|
+
const workspace = await workspaces.require(context.user.id, id(context))
|
|
150
|
+
return ok({ workspace, bindings: await workspaces.bindings(context.user.id, workspace.id) })
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
path: `${PREFIX}/:id`,
|
|
156
|
+
allow: MANAGE,
|
|
157
|
+
async handle(context) {
|
|
158
|
+
const { expectedRevision, ...patch } = await bodyOf(context.request)
|
|
159
|
+
const revision = expected(expectedRevision)
|
|
160
|
+
const workspace = await workspaces.update(context.user.id, id(context), patch, revision)
|
|
161
|
+
if (workspace.revision !== revision) await changed(context, "workspace.update", workspace.id, workspacePatchedEvent(workspace))
|
|
162
|
+
return ok({ workspace })
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
method: "POST",
|
|
167
|
+
path: `${PREFIX}/:id/archive`,
|
|
168
|
+
allow: MANAGE,
|
|
169
|
+
async handle(context) {
|
|
170
|
+
const body = await bodyOf(context.request)
|
|
171
|
+
only(body, ["expectedRevision"])
|
|
172
|
+
const result = await lifecycle.archive(context.user.id, id(context), expected(body.expectedRevision))
|
|
173
|
+
await deps.audit?.audit({ actor: context.user.id, action: "workspace.archive", target: result.workspace.id, detail: context.actor.kind })
|
|
174
|
+
return ok(result)
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
method: "POST",
|
|
179
|
+
path: `${PREFIX}/:id/restore`,
|
|
180
|
+
allow: MANAGE,
|
|
181
|
+
async handle(context) {
|
|
182
|
+
const body = await bodyOf(context.request)
|
|
183
|
+
only(body, ["expectedRevision"])
|
|
184
|
+
const revision = expected(body.expectedRevision)
|
|
185
|
+
const workspace = await workspaces.restore(context.user.id, id(context), revision)
|
|
186
|
+
if (workspace.revision !== revision) await changed(context, "workspace.restore", workspace.id, workspaceArchivedEvent(workspace, 0))
|
|
187
|
+
return ok({ workspace })
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// ---------- host repository bindings ----------
|
|
192
|
+
{
|
|
193
|
+
method: "GET",
|
|
194
|
+
path: `${PREFIX}/:id/bindings`,
|
|
195
|
+
allow: READ,
|
|
196
|
+
handle: async (context) => ok({ bindings: await workspaces.bindings(context.user.id, id(context)) }),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
method: "POST",
|
|
200
|
+
path: `${PREFIX}/:id/bindings`,
|
|
201
|
+
allow: MANAGE,
|
|
202
|
+
async handle(context) {
|
|
203
|
+
const body = await bodyOf(context.request)
|
|
204
|
+
only(body, ["machineID", "repo", "label"])
|
|
205
|
+
return keyed(context, body, async () => {
|
|
206
|
+
const { binding, created } = await workspaces.bind(context.user.id, id(context), body)
|
|
207
|
+
if (created) await changed(context, "workspace.bind", id(context), bindingEvent(id(context), binding, "bound"))
|
|
208
|
+
return { status: created ? 201 : 200, body: { binding, created } }
|
|
209
|
+
})
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
method: "DELETE",
|
|
214
|
+
path: `${PREFIX}/:id/bindings`,
|
|
215
|
+
allow: MANAGE,
|
|
216
|
+
async handle(context) {
|
|
217
|
+
const bindingID = text(context.url, "bindingID")
|
|
218
|
+
if (!bindingID) throw new AgentError("invalid", "bindingID is required")
|
|
219
|
+
const binding = await workspaces.unbind(context.user.id, id(context), bindingID, expected(context.url.searchParams.get("expectedRevision") ?? undefined))
|
|
220
|
+
await changed(context, "workspace.unbind", id(context), bindingEvent(id(context), binding, "unbound"))
|
|
221
|
+
return new Response(null, { status: 204, headers: NO_STORE })
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// ---------- members ----------
|
|
226
|
+
{
|
|
227
|
+
method: "GET",
|
|
228
|
+
path: `${PREFIX}/:id/members`,
|
|
229
|
+
allow: READ,
|
|
230
|
+
async handle(context) {
|
|
231
|
+
sessionsVisible(context)
|
|
232
|
+
const includeLeft = ["1", "true"].includes(context.url.searchParams.get("includeLeft") ?? "")
|
|
233
|
+
return ok({ members: await members.members(context.user.id, id(context), { includeLeft }) })
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
method: "POST",
|
|
238
|
+
path: `${PREFIX}/:id/members`,
|
|
239
|
+
allow: MANAGE,
|
|
240
|
+
async handle(context) {
|
|
241
|
+
sessionsVisible(context)
|
|
242
|
+
const body = await bodyOf(context.request)
|
|
243
|
+
only(body, ["sessionID", "role"])
|
|
244
|
+
return keyed(context, body, async () => {
|
|
245
|
+
const { member, created } = await members.join(context.user.id, id(context), body)
|
|
246
|
+
if (created) await changed(context, "workspace.member.join", member.id, memberJoinedEvent(member))
|
|
247
|
+
return { status: created ? 201 : 200, body: { member, created } }
|
|
248
|
+
})
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
method: "PATCH",
|
|
253
|
+
path: `${PREFIX}/:id/members/:member`,
|
|
254
|
+
allow: MANAGE,
|
|
255
|
+
async handle(context) {
|
|
256
|
+
sessionsVisible(context)
|
|
257
|
+
const body = await bodyOf(context.request)
|
|
258
|
+
only(body, ["role", "expectedRevision"])
|
|
259
|
+
const revision = expected(body.expectedRevision)
|
|
260
|
+
const member = await members.setRole(context.user.id, id(context), context.params.member!, body.role, revision)
|
|
261
|
+
if (member.revision !== revision) await changed(context, "workspace.member.role", member.id, memberRoleEvent(member))
|
|
262
|
+
return ok({ member })
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
method: "DELETE",
|
|
267
|
+
path: `${PREFIX}/:id/members/:member`,
|
|
268
|
+
allow: MANAGE,
|
|
269
|
+
async handle(context) {
|
|
270
|
+
sessionsVisible(context)
|
|
271
|
+
const revision = expected(context.url.searchParams.get("expectedRevision") ?? undefined)
|
|
272
|
+
const departure = await lifecycle.leave(context.user.id, id(context), context.params.member!, revision)
|
|
273
|
+
await deps.audit?.audit({ actor: context.user.id, action: "workspace.member.leave", target: departure.member.id, detail: context.actor.kind })
|
|
274
|
+
return ok(departure)
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// ---------- assignments ----------
|
|
279
|
+
{
|
|
280
|
+
method: "GET",
|
|
281
|
+
path: `${PREFIX}/:id/assignments`,
|
|
282
|
+
allow: READ,
|
|
283
|
+
async handle(context) {
|
|
284
|
+
sessionsVisible(context)
|
|
285
|
+
const { url } = context
|
|
286
|
+
const limit = intParam(url, "limit")
|
|
287
|
+
const query = { ...(limit !== undefined ? { limit } : {}), ...Object.fromEntries(["memberID", "status", "cursor"].flatMap((k) => (text(url, k) ? [[k, text(url, k)!]] : []))) }
|
|
288
|
+
return ok(await members.assignments(context.user.id, id(context), query))
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
method: "GET",
|
|
293
|
+
path: `${PREFIX}/:id/assignments/:assignment`,
|
|
294
|
+
allow: READ,
|
|
295
|
+
async handle(context) {
|
|
296
|
+
sessionsVisible(context)
|
|
297
|
+
return ok({ assignment: await members.assignment(context.user.id, id(context), context.params.assignment!) })
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
method: "POST",
|
|
302
|
+
path: `${PREFIX}/:id/assignments`,
|
|
303
|
+
allow: MANAGE,
|
|
304
|
+
async handle(context) {
|
|
305
|
+
const body = await bodyOf(context.request)
|
|
306
|
+
const key = idempotencyKeyOf(context.request)
|
|
307
|
+
const { assignment, created } = await members.assign(context.user.id, id(context), body, key === undefined ? {} : { idempotencyKey: key })
|
|
308
|
+
if (created) await changed(context, "workspace.assignment.create", assignment.id, assignmentChangedEvent(assignment, "created"))
|
|
309
|
+
return storeAnswer(created, { assignment })
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
method: "PATCH",
|
|
314
|
+
path: `${PREFIX}/:id/assignments/:assignment`,
|
|
315
|
+
allow: MANAGE,
|
|
316
|
+
async handle(context) {
|
|
317
|
+
const body = await bodyOf(context.request)
|
|
318
|
+
only(body, ["status", "memberID", "expectedRevision"])
|
|
319
|
+
const revision = expected(body.expectedRevision)
|
|
320
|
+
if ((body.status === undefined) === (body.memberID === undefined)) throw new AgentError("invalid", "Send exactly one of status or memberID")
|
|
321
|
+
const assignmentID = context.params.assignment!
|
|
322
|
+
const change = body.status !== undefined ? "status" : "reassigned"
|
|
323
|
+
const assignment =
|
|
324
|
+
change === "status"
|
|
325
|
+
? await members.setStatus(context.user.id, id(context), assignmentID, body.status, revision)
|
|
326
|
+
: await members.reassign(context.user.id, id(context), assignmentID, body.memberID, revision)
|
|
327
|
+
if (assignment.revision !== revision) await changed(context, `workspace.assignment.${change}`, assignment.id, assignmentChangedEvent(assignment, change))
|
|
328
|
+
return ok({ assignment })
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// ---------- supervision ----------
|
|
333
|
+
{
|
|
334
|
+
method: "GET",
|
|
335
|
+
path: `${PREFIX}/:id/supervision`,
|
|
336
|
+
allow: READ,
|
|
337
|
+
handle: async (context) => ok(await supervision.graph(context.user.id, id(context))),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
method: "PUT",
|
|
341
|
+
path: `${PREFIX}/:id/supervision`,
|
|
342
|
+
allow: MANAGE,
|
|
343
|
+
async handle(context) {
|
|
344
|
+
const body = await bodyOf(context.request)
|
|
345
|
+
only(body, ["memberID", "supervisorMemberID", "order", "expectedRevision"])
|
|
346
|
+
expected(body.expectedRevision)
|
|
347
|
+
const result = await supervision.set(context.user.id, id(context), body)
|
|
348
|
+
if (result.change !== "unchanged" || result.pruned > 0)
|
|
349
|
+
await changed(context, "workspace.supervision", id(context), supervisionSetEvent(body.memberID as string, result, result.link))
|
|
350
|
+
return ok(result)
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
]
|
|
354
|
+
}
|