@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
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Routes (user actors always; session tokens and app tokens holding sessions:read):
|
|
5
5
|
// GET /api/agent/status
|
|
6
|
-
// GET /api/agent/sessions?project=&state=&machine=&label=k=v
|
|
6
|
+
// GET /api/agent/sessions?project=&state=&machine=&label=k=v&q= (CON-11) q: every word in the title or @name
|
|
7
7
|
// GET /api/agent/sessions/:addr show: the catalog view plus the spoke's summary (when it serves one)
|
|
8
8
|
// GET /api/agent/sessions/:addr/log?n=20 the newest timeline rows (spoke feature "timeline")
|
|
9
9
|
// GET /api/agent/watch?blocked=1&session=<addr> SSE of compact events (watch.ts)
|
|
10
10
|
// show and log call spokes; a spoke without the feature answers unsupported {feature, harness}.
|
|
11
11
|
// A session a launch started carries that launch's labels (the latest launch's, when several did); others have none.
|
|
12
|
+
// (CON-02) /sessions and /sessions/:addr entries carry `intent {text, source, revision}` when one is stored (untrusted
|
|
13
|
+
// text; the title fallback is only on GET /sessions/:addr/intent, intent-routes.ts).
|
|
12
14
|
import path from "node:path"
|
|
13
15
|
import { featureForOp, type ServerRequest, type SpokeFeature } from "../../protocol"
|
|
14
16
|
import type { CatalogMachine, CatalogSession, LiveStatus, NowActivity, RegistryMessage, SessionView } from "../registry"
|
|
@@ -21,9 +23,14 @@ import { AgentError } from "./errors"
|
|
|
21
23
|
import { AGENT_PREFIX, type AgentRoute } from "./routes"
|
|
22
24
|
import { watchStream } from "./watch"
|
|
23
25
|
import type { EventsRouteDeps } from "./events-routes"
|
|
26
|
+
import { type Census, type TeamDiscovery, visible } from "../teams/discovery"
|
|
27
|
+
import type { SessionIntent } from "../../protocol"
|
|
28
|
+
import { intentField } from "./intents"
|
|
24
29
|
|
|
25
30
|
/** Labels per session ID. */
|
|
26
31
|
export type SessionLabels = ReadonlyMap<string, Readonly<Record<string, string>>>
|
|
32
|
+
/** (CON-02) Stored intents per session ID. */
|
|
33
|
+
export type SessionIntentMap = ReadonlyMap<string, SessionIntent>
|
|
27
34
|
|
|
28
35
|
/** What the read routes need from the Registry. */
|
|
29
36
|
export interface ReadRegistry {
|
|
@@ -32,6 +39,8 @@ export interface ReadRegistry {
|
|
|
32
39
|
session(sessionID: string): SessionView | undefined
|
|
33
40
|
owner(sessionID: string): string | undefined
|
|
34
41
|
supports(connID: string, feature: SpokeFeature): boolean
|
|
42
|
+
/** (RP-08) A connection's harness (Registry implements it). */
|
|
43
|
+
harnessOf?(connID: string): string | undefined
|
|
35
44
|
subscribe(listener: (message: RegistryMessage, tenant?: string) => void): () => void
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -47,6 +56,8 @@ export interface ReadDeps {
|
|
|
47
56
|
readonly spokes?: SpokeRequester
|
|
48
57
|
/** The labels of the user's launched sessions (none without it). */
|
|
49
58
|
readonly labels?: (userID: string) => Promise<SessionLabels>
|
|
59
|
+
/** (CON-02) The stored intents of these sessions: GET /sessions and /sessions/:addr carry `intent` when present. */
|
|
60
|
+
readonly intents?: (userID: string, sessionIDs: readonly string[]) => Promise<SessionIntentMap>
|
|
50
61
|
/** The journal GET /api/agent/watch streams (no watch route without it). */
|
|
51
62
|
readonly events?: EventsRouteDeps
|
|
52
63
|
/** Watch stream heartbeat (default WATCH_HEARTBEAT_MS). */
|
|
@@ -54,6 +65,10 @@ export interface ReadDeps {
|
|
|
54
65
|
/** Aborted when the hub drains: watch streams end with a `retry:` hint. */
|
|
55
66
|
readonly draining?: AbortSignal
|
|
56
67
|
readonly now?: () => number
|
|
68
|
+
/** Team-aware discovery (HWA-40): team members see their teams; invisible sessions read as unknown. */
|
|
69
|
+
readonly discovery?: TeamDiscovery
|
|
70
|
+
/** (CON-04) The user's Concierge sessions (current and hidden): their entries carry `concierge: true`. */
|
|
71
|
+
readonly concierge?: (userID: string) => Promise<readonly string[]>
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/** One session in agent answers (status, sessions). */
|
|
@@ -168,8 +183,9 @@ function you(actor: AgentActor, entries: Map<string, DirectoryEntry>) {
|
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
/** The status snapshot of one user (design §4): machines → projects → sessions, and what needs attention. */
|
|
171
|
-
export function snapshot(deps: ReadDeps, userID: string, actor: AgentActor, labels: SessionLabels = NO_LABELS) {
|
|
172
|
-
const list = rows(deps.registry, userID)
|
|
186
|
+
export function snapshot(deps: ReadDeps, userID: string, actor: AgentActor, labels: SessionLabels = NO_LABELS, census?: Census, exclude: ReadonlySet<string> = NONE) {
|
|
187
|
+
const list = rows(deps.registry, userID).filter((r) => (!census || visible(census, r.session.id)) && !exclude.has(r.session.id))
|
|
188
|
+
const teams = census && deps.discovery ? deps.discovery.groups(census, list.map((r) => r.session.id)) : []
|
|
173
189
|
const entries = directoryOf(deps.directory, userID)
|
|
174
190
|
const online = new Map(deps.registry.catalog().map((m) => [m.id, m]))
|
|
175
191
|
const machines = deps.store
|
|
@@ -193,12 +209,12 @@ export function snapshot(deps: ReadDeps, userID: string, actor: AgentActor, labe
|
|
|
193
209
|
projects: [...projects.values()],
|
|
194
210
|
}
|
|
195
211
|
})
|
|
196
|
-
return { at: (deps.now ?? Date.now)(), you: you(actor, entries), machines, attention: attention(list, entries) }
|
|
212
|
+
return { at: (deps.now ?? Date.now)(), you: you(actor, entries), machines, attention: attention(list, entries), ...(teams.length ? { teams } : {}) }
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
/** The session an address names for the user, if the user's spokes report it now (else offline). */
|
|
200
|
-
function located(deps: ReadDeps, userID: string, addr: string) {
|
|
201
|
-
const entry = deps.directory.resolveOne(userID, addr)
|
|
216
|
+
function located(deps: ReadDeps, userID: string, addr: string, census?: Census) {
|
|
217
|
+
const entry = census && deps.discovery ? deps.discovery.resolveOne(census, addr) : deps.directory.resolveOne(userID, addr)
|
|
202
218
|
const live = deps.registry.sessionTenant(entry.sessionID) === userID ? deps.registry.session(entry.sessionID) : undefined
|
|
203
219
|
const list = rows(deps.registry, userID)
|
|
204
220
|
const row = list.find((r) => r.session.id === entry.sessionID) ?? (live && list.find((r) => r.session.id === live.status[0]?.id))
|
|
@@ -226,9 +242,13 @@ async function ask(deps: ReadDeps, connID: string, req: ServerRequest): Promise<
|
|
|
226
242
|
}
|
|
227
243
|
|
|
228
244
|
/** GET /sessions/:addr: the session's catalog view with full blockers, and its summary from the spoke. */
|
|
229
|
-
async function show(deps: ReadDeps, userID: string, addr: string) {
|
|
230
|
-
const { entry, live, row } = located(deps, userID, addr)
|
|
245
|
+
async function show(deps: ReadDeps, userID: string, addr: string, census?: Census) {
|
|
246
|
+
const { entry, live, row } = located(deps, userID, addr, census)
|
|
247
|
+
const memberships = census && deps.discovery ? deps.discovery.memberships(census, entry.sessionID) : []
|
|
248
|
+
const teams = memberships.length ? { teams: memberships } : {}
|
|
231
249
|
const labels = { ...(await labelsOf(deps, userID)).get(entry.sessionID) }
|
|
250
|
+
const intent = intentField((await intentsOf(deps, userID, [entry.sessionID])).get(entry.sessionID))
|
|
251
|
+
const marker = (await conciergeOf(deps, userID)).has(entry.sessionID) ? { concierge: true } : {}
|
|
232
252
|
const name = entry.name ? { name: `@${entry.name}` } : {}
|
|
233
253
|
const roles = entry.roles.map(roleLabel)
|
|
234
254
|
if (!live) {
|
|
@@ -245,6 +265,9 @@ async function show(deps: ReadDeps, userID: string, addr: string) {
|
|
|
245
265
|
...(entry.lastSeen !== undefined ? { lastSeen: entry.lastSeen } : {}),
|
|
246
266
|
blockers: [],
|
|
247
267
|
labels,
|
|
268
|
+
...intent,
|
|
269
|
+
...marker,
|
|
270
|
+
...teams,
|
|
248
271
|
}
|
|
249
272
|
}
|
|
250
273
|
const harness = row?.harness ?? "unknown"
|
|
@@ -266,6 +289,9 @@ async function show(deps: ReadDeps, userID: string, addr: string) {
|
|
|
266
289
|
blockers: live.blockers.map(({ sessionTitle: _, ...b }) => b),
|
|
267
290
|
children: live.status.filter((e) => e.parentID === live.id).length,
|
|
268
291
|
labels,
|
|
292
|
+
...intent,
|
|
293
|
+
...marker,
|
|
294
|
+
...teams,
|
|
269
295
|
}
|
|
270
296
|
try {
|
|
271
297
|
const connID = spokeFor(deps, live.id, "summary", harness)
|
|
@@ -279,23 +305,55 @@ async function show(deps: ReadDeps, userID: string, addr: string) {
|
|
|
279
305
|
}
|
|
280
306
|
|
|
281
307
|
/** GET /sessions/:addr/log: the newest `n` timeline rows, newest first. */
|
|
282
|
-
async function log(deps: ReadDeps, userID: string, addr: string, raw: string | null) {
|
|
308
|
+
async function log(deps: ReadDeps, userID: string, addr: string, raw: string | null, census?: Census) {
|
|
283
309
|
const n = raw === null ? 20 : Number(raw)
|
|
284
310
|
if (!Number.isInteger(n) || n < 1 || n > 200) throw new AgentError("invalid", "n must be 1–200")
|
|
285
|
-
const { entry, live, row } = located(deps, userID, addr)
|
|
311
|
+
const { entry, live, row } = located(deps, userID, addr, census)
|
|
286
312
|
if (!live) throw new AgentError("offline", "No spoke reports this session now")
|
|
287
313
|
const connID = spokeFor(deps, entry.sessionID, "timeline", row?.harness ?? "unknown")
|
|
288
314
|
const page = (await ask(deps, connID, { op: "timeline", sessionID: entry.sessionID, limit: n, order: "desc" })) as { items?: unknown[]; older?: string }
|
|
289
315
|
return { sessionID: entry.sessionID, rows: page.items ?? [], ...(page.older ? { older: page.older } : {}) }
|
|
290
316
|
}
|
|
291
317
|
|
|
318
|
+
const censusOf = (deps: ReadDeps, userID: string, actor: AgentActor): Promise<Census | undefined> => deps.discovery?.census(userID, actor) ?? Promise.resolve(undefined)
|
|
319
|
+
|
|
292
320
|
const labelsOf = (deps: ReadDeps, userID: string): Promise<SessionLabels> => deps.labels?.(userID) ?? Promise.resolve(NO_LABELS)
|
|
293
321
|
|
|
322
|
+
const NO_INTENTS: SessionIntentMap = new Map()
|
|
323
|
+
/** (CON-04) The user's Concierge sessions (none without the dep). */
|
|
324
|
+
async function conciergeOf(deps: ReadDeps, userID: string): Promise<ReadonlySet<string>> {
|
|
325
|
+
return new Set(deps.concierge ? await deps.concierge(userID) : [])
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const NONE: ReadonlySet<string> = new Set()
|
|
329
|
+
/**
|
|
330
|
+
* (CON-09a) What a Concierge session's own reads leave out: every Concierge session (current and previous), so it
|
|
331
|
+
* never reports itself as one of the user's sessions ("Greeting is working"). Anyone else: nothing.
|
|
332
|
+
*/
|
|
333
|
+
export async function conciergeSelf(concierge: ((userID: string) => Promise<readonly string[]>) | undefined, userID: string, actor: AgentActor): Promise<ReadonlySet<string>> {
|
|
334
|
+
if (actor.kind !== "session" || !concierge) return NONE
|
|
335
|
+
const all = new Set(await concierge(userID))
|
|
336
|
+
return all.has(actor.sessionID) ? all : NONE
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const intentsOf = (deps: ReadDeps, userID: string, sessionIDs: readonly string[]): Promise<SessionIntentMap> =>
|
|
340
|
+
deps.intents && sessionIDs.length ? deps.intents(userID, sessionIDs) : Promise.resolve(NO_INTENTS)
|
|
341
|
+
|
|
294
342
|
const STATES = new Set(["idle", "working", "blocked", "error"])
|
|
295
343
|
|
|
296
344
|
/** The /sessions filters: project (name or root), state (comma list), machine (ID or name), label k=v. */
|
|
345
|
+
/** (CON-11) The longest `q` (characters). */
|
|
346
|
+
const QUERY_MAX = 200
|
|
347
|
+
/** Search words: lower case, letters and digits only (an @, dashes and punctuation separate words). */
|
|
348
|
+
const words = (text: string) => text.toLowerCase().normalize("NFKD").replace(/\p{M}/gu, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean)
|
|
349
|
+
|
|
297
350
|
function sessionFilter(deps: ReadDeps, userID: string, params: URLSearchParams): (row: Row, view: AgentSessionView) => boolean {
|
|
298
351
|
const project = params.get("project") || undefined
|
|
352
|
+
const q = params.get("q") ?? ""
|
|
353
|
+
if (q.length > QUERY_MAX) throw new AgentError("invalid", `q is at most ${QUERY_MAX} characters`)
|
|
354
|
+
const wanted = words(q)
|
|
355
|
+
// (FX-07) A q with no letters or digits ("@", "«»", "?") names no word: it matches nothing (never every session).
|
|
356
|
+
const nothing = q.trim() !== "" && wanted.length === 0
|
|
299
357
|
const states = params.get("state")?.split(",").filter(Boolean)
|
|
300
358
|
if (states?.some((state) => !STATES.has(state))) throw new AgentError("invalid", "state must be idle, working, blocked or error (comma-separated)")
|
|
301
359
|
const machine = params.get("machine") || undefined
|
|
@@ -306,10 +364,15 @@ function sessionFilter(deps: ReadDeps, userID: string, params: URLSearchParams):
|
|
|
306
364
|
return [label.slice(0, at), label.slice(at + 1)] as const
|
|
307
365
|
})
|
|
308
366
|
return (row, view) => {
|
|
367
|
+
if (nothing) return false
|
|
309
368
|
const { root, name } = projectOf(row.session)
|
|
310
369
|
if (project !== undefined && root !== project && name.toLowerCase() !== project.toLowerCase()) return false
|
|
311
370
|
if (states?.length && !states.includes(view.state)) return false
|
|
312
371
|
if (machineIDs && !machineIDs.has(row.machineID)) return false
|
|
372
|
+
if (wanted.length) {
|
|
373
|
+
const have = [...words(view.title ?? ""), ...words(view.name ?? "")]
|
|
374
|
+
if (!wanted.every((w) => have.some((h) => h.includes(w)))) return false
|
|
375
|
+
}
|
|
313
376
|
return labels.every(([key, value]) => view.labels[key] === value)
|
|
314
377
|
}
|
|
315
378
|
}
|
|
@@ -324,20 +387,25 @@ export function readRoutes(deps: ReadDeps): AgentRoute[] {
|
|
|
324
387
|
method: "GET",
|
|
325
388
|
path: `${AGENT_PREFIX}/status`,
|
|
326
389
|
allow: READ,
|
|
327
|
-
handle: async ({ user, actor }) => ok(snapshot(deps, user.id, actor, await labelsOf(deps, user.id))),
|
|
390
|
+
handle: async ({ user, actor }) => ok(snapshot(deps, user.id, actor, await labelsOf(deps, user.id), await censusOf(deps, user.id, actor), await conciergeSelf(deps.concierge, user.id, actor))),
|
|
328
391
|
},
|
|
329
392
|
{
|
|
330
393
|
method: "GET",
|
|
331
394
|
path: `${AGENT_PREFIX}/sessions`,
|
|
332
395
|
allow: READ,
|
|
333
|
-
handle: async ({ user, url }) => {
|
|
396
|
+
handle: async ({ user, url, actor }) => {
|
|
334
397
|
const keep = sessionFilter(deps, user.id, url.searchParams)
|
|
398
|
+
const census = await censusOf(deps, user.id, actor)
|
|
335
399
|
const entries = directoryOf(deps.directory, user.id)
|
|
336
400
|
const labels = await labelsOf(deps, user.id)
|
|
337
|
-
const
|
|
401
|
+
const self = await conciergeSelf(deps.concierge, user.id, actor)
|
|
402
|
+
const listed = rows(deps.registry, user.id)
|
|
338
403
|
.map((row) => ({ row, view: view(row, entries.get(row.session.id), labels) }))
|
|
339
|
-
.filter(({ row, view }) => keep(row, view))
|
|
340
|
-
|
|
404
|
+
.filter(({ row, view }) => keep(row, view) && (!census || visible(census, row.session.id)) && !self.has(row.session.id))
|
|
405
|
+
// Only the listed sessions' intents are read (never the whole collection).
|
|
406
|
+
const intents = await intentsOf(deps, user.id, listed.map(({ row }) => row.session.id))
|
|
407
|
+
const concierge = await conciergeOf(deps, user.id)
|
|
408
|
+
const sessions = listed.map(({ row, view }) => ({ ...view, machineID: row.machineID, project: projectOf(row.session).name, ...intentField(intents.get(row.session.id)), ...(concierge.has(row.session.id) ? { concierge: true } : {}) }))
|
|
341
409
|
return ok({ sessions })
|
|
342
410
|
},
|
|
343
411
|
},
|
|
@@ -345,13 +413,13 @@ export function readRoutes(deps: ReadDeps): AgentRoute[] {
|
|
|
345
413
|
method: "GET",
|
|
346
414
|
path: `${AGENT_PREFIX}/sessions/:addr`,
|
|
347
415
|
allow: READ,
|
|
348
|
-
handle: async ({ user, params }) => ok(await show(deps, user.id, params.addr
|
|
416
|
+
handle: async ({ user, params, actor }) => ok(await show(deps, user.id, params.addr!, await censusOf(deps, user.id, actor))),
|
|
349
417
|
},
|
|
350
418
|
{
|
|
351
419
|
method: "GET",
|
|
352
420
|
path: `${AGENT_PREFIX}/sessions/:addr/log`,
|
|
353
421
|
allow: READ,
|
|
354
|
-
handle: async ({ user, params, url }) => ok(await log(deps, user.id, params.addr!, url.searchParams.get("n"))),
|
|
422
|
+
handle: async ({ user, params, url, actor }) => ok(await log(deps, user.id, params.addr!, url.searchParams.get("n"), await censusOf(deps, user.id, actor))),
|
|
355
423
|
},
|
|
356
424
|
...(deps.events ? [watchRoute(deps, deps.events)] : []),
|
|
357
425
|
]
|
|
@@ -363,9 +431,11 @@ function watchRoute(deps: ReadDeps, events: EventsRouteDeps): AgentRoute {
|
|
|
363
431
|
method: "GET",
|
|
364
432
|
path: `${AGENT_PREFIX}/watch`,
|
|
365
433
|
allow: READ,
|
|
366
|
-
handle: ({ user, url, request, actor }) => {
|
|
434
|
+
handle: async ({ user, url, request, actor }) => {
|
|
367
435
|
const addr = url.searchParams.get("session") || undefined
|
|
368
|
-
const
|
|
436
|
+
const census = addr ? await censusOf(deps, user.id, actor) : undefined
|
|
437
|
+
const resolved = addr ? (census && deps.discovery ? deps.discovery.resolve(census, addr) : deps.directory.resolve(user.id, addr)) : undefined
|
|
438
|
+
const sessions = resolved ? new Set(resolved.sessions.map((s) => s.sessionID)) : undefined
|
|
369
439
|
const blocked = ["1", "true"].includes(url.searchParams.get("blocked") ?? "")
|
|
370
440
|
const cursor = url.searchParams.get("cursor") || request.headers.get("last-event-id") || undefined
|
|
371
441
|
return watchStream({
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Path parameters address sessions as `:addr` (resolved per caller), never `:session`.
|
|
9
9
|
import type { AgentEvents } from "./events"
|
|
10
10
|
import type { CursorKeys } from "./event-cursor"
|
|
11
|
-
import { eventsRoutes } from "./events-routes"
|
|
11
|
+
import { eventsRoutes, type EventsRouteDeps } from "./events-routes"
|
|
12
12
|
import type { DeviceRecord, Store, UserRecord } from "../store"
|
|
13
13
|
import type { Directory } from "./directory"
|
|
14
14
|
import { directoryRoutes } from "./directory-routes"
|
|
@@ -30,6 +30,36 @@ import { actingRoutes } from "./acting-routes"
|
|
|
30
30
|
import type { Actions } from "../actions"
|
|
31
31
|
import { launchRoutes, type RouteLaunches } from "./launch-routes"
|
|
32
32
|
import { type RouteWebhooks, webhookRoutes } from "./webhook-routes"
|
|
33
|
+
import { pinAuthorizer } from "../profiles/apply"
|
|
34
|
+
import { profileRoutes } from "../profiles/routes"
|
|
35
|
+
import { ProfileUsage } from "../profiles/usage"
|
|
36
|
+
import { usagePorts } from "../profiles/usage-ports"
|
|
37
|
+
import type { ProfileStore } from "../profiles/store"
|
|
38
|
+
import { workspaceRoutes, type WorkspaceRoutesDeps } from "../workspaces/routes"
|
|
39
|
+
import { boardRoutes, registryBoardSessions } from "../workspaces/board"
|
|
40
|
+
import { type CheckpointStore, checkpointRoutes } from "../workspaces/checkpoints"
|
|
41
|
+
import { masterRoutes } from "../workspaces/master"
|
|
42
|
+
import { routePorts, sideChatRoutes } from "../workspaces/sidechats-routes"
|
|
43
|
+
import { type ArtifactRouteDeps, artifactRoutes } from "../artifacts/routes"
|
|
44
|
+
import { artifactJobRoutes, jobPorts } from "../artifacts/jobs-routes"
|
|
45
|
+
import { type ArtifactHostRouteDeps, artifactHostRoutes } from "../artifacts/host"
|
|
46
|
+
import { type GraphRouteDeps, graphRoutes } from "./interaction-graph-routes"
|
|
47
|
+
import type { TeamIntakeService } from "../teams/intake"
|
|
48
|
+
import { intakeRoutes } from "../teams/intake-routes"
|
|
49
|
+
import { eventVisibility, type TeamDiscovery } from "../teams/discovery"
|
|
50
|
+
import { teamRoutes, type TeamRoutesDeps } from "../teams/routes"
|
|
51
|
+
import type { StateStore } from "../backend/types"
|
|
52
|
+
import type { ActivityTracker } from "../concierge/activity"
|
|
53
|
+
import { boardRoutes as conciergeBoardRoutes } from "../concierge/board-routes"
|
|
54
|
+
import type { ResultTracker } from "../concierge/board-results"
|
|
55
|
+
import { type ConciergeIdentity, watchlistRoutes } from "../concierge/watchlist-routes"
|
|
56
|
+
import { conciergeRoutes, type MachineModel } from "../concierge/routes"
|
|
57
|
+
import { guardConciergeRoutes } from "../concierge/guard"
|
|
58
|
+
import { isConciergeLaunch } from "../concierge/supervisor"
|
|
59
|
+
import type { ConciergeService } from "../concierge/service"
|
|
60
|
+
import type { SessionIntent } from "../../protocol"
|
|
61
|
+
import type { SessionIntents } from "./intents"
|
|
62
|
+
import { intentRoutes } from "./intent-routes"
|
|
33
63
|
|
|
34
64
|
export const AGENT_PREFIX = "/api/agent"
|
|
35
65
|
|
|
@@ -90,6 +120,22 @@ export interface AgentDeps {
|
|
|
90
120
|
readonly launches?: RouteLaunches
|
|
91
121
|
/** Webhooks (P4-T10): the /api/agent/webhooks routes when given. */
|
|
92
122
|
readonly webhooks?: RouteWebhooks
|
|
123
|
+
/** Profiles (HWA-14): the /api/agent/profiles and /sessions/:addr/profile routes when given with the directory. */
|
|
124
|
+
readonly profiles?: ProfileStore
|
|
125
|
+
/** Workspaces (HWA-06): the /api/agent/workspaces routes when given. */
|
|
126
|
+
readonly workspaces?: Pick<WorkspaceRoutesDeps, "workspaces" | "members" | "supervision" | "lifecycle">
|
|
127
|
+
/** Supervisor checkpoints (HWA-11): POST/GET /api/agent/workspaces/:id/checkpoints when given. */
|
|
128
|
+
readonly workspaceCheckpoints?: CheckpointStore
|
|
129
|
+
/** Team intake (HWA-36): /api/agent/teams/:id/intake when given. */
|
|
130
|
+
readonly teamIntake?: TeamIntakeService
|
|
131
|
+
/** Teams (HWA-37): /api/agent/teams definitions, staffing and status when given (the hub's single instances). */
|
|
132
|
+
readonly teams?: Pick<TeamRoutesDeps, "teams" | "seats" | "staffing">
|
|
133
|
+
/** Persistent artifacts (HWA-21): /api/agent/artifacts; without a store the routes answer 422 unsupported. */
|
|
134
|
+
readonly artifacts?: ArtifactRouteDeps
|
|
135
|
+
/** Isolated artifact host (HWA-26): mint/revoke capability URLs for html/app revisions when given. */
|
|
136
|
+
readonly artifactHost?: ArtifactHostRouteDeps
|
|
137
|
+
/** Interaction graph (HWA-31) team overlay; the graph routes are added with the directory, registry and delivery. */
|
|
138
|
+
readonly graph?: Pick<GraphRouteDeps, "teams">
|
|
93
139
|
/** The canonical agent event journal: archive changes are appended to it. */
|
|
94
140
|
readonly agentEvents?: Pick<AgentEvents, "append" | "latest" | "since" | "appended">
|
|
95
141
|
/** Signing keys of GET /api/agent/events cursors; the events route is served when given with agentEvents. */
|
|
@@ -105,6 +151,34 @@ export interface AgentDeps {
|
|
|
105
151
|
/** Aborted when the hub drains: watch streams end with a `retry:` hint. */
|
|
106
152
|
readonly draining?: AbortSignal
|
|
107
153
|
readonly now?: () => number
|
|
154
|
+
/** Team-aware discovery (HWA-40): who and the read routes scope team members to their teams. */
|
|
155
|
+
readonly teamDiscovery?: TeamDiscovery
|
|
156
|
+
/**
|
|
157
|
+
* Concierge (CON-01): the /api/agent/concierge/watchlist routes when given with the directory and registry.
|
|
158
|
+
* `identity` is the user's current Concierge session (CON-04); without it no session token is admitted.
|
|
159
|
+
*/
|
|
160
|
+
readonly concierge?: {
|
|
161
|
+
readonly state: StateStore
|
|
162
|
+
readonly activity: Pick<ActivityTracker, "list">
|
|
163
|
+
readonly identity?: ConciergeIdentity
|
|
164
|
+
readonly results?: Pick<ResultTracker, "many">
|
|
165
|
+
/** (CON-04) Sessions only the user acts on (the Concierge's): the act verbs refuse agents on them. */
|
|
166
|
+
readonly protects?: (userID: string, sessionID: string) => Promise<boolean>
|
|
167
|
+
/** (CON-04) The Concierge service: /api/agent/concierge settings, enable/disable/restart and notes. */
|
|
168
|
+
readonly service?: ConciergeService
|
|
169
|
+
/** (FX-04) A machine's OpenCode model list (routes.ts machineModels): GET …/concierge/machines/:id/models. */
|
|
170
|
+
readonly models?: (machineID: string) => Promise<MachineModel[]>
|
|
171
|
+
/** (CON-06 and later) More Concierge routes (the tiers slot). */
|
|
172
|
+
readonly routes?: () => AgentRoute[]
|
|
173
|
+
/** (CON-06) The Decision armed now by the current Concierge (the board's `armed` flag). */
|
|
174
|
+
readonly armed?: (userID: string) => Promise<string | undefined>
|
|
175
|
+
/** (CON-10) The user's time zone (Concierge settings): the rendered board's time is local. */
|
|
176
|
+
readonly timeZone?: (userID: string) => Promise<string | undefined>
|
|
177
|
+
/** (CON-13) The board's inGrant (board-routes.ts BoardRouteDeps.coverage). */
|
|
178
|
+
readonly coverage?: (userID: string, conciergeSessionID: string, sessionID: string, requestID: string) => Promise<{ readonly inGrant: boolean; readonly auto: boolean }>
|
|
179
|
+
}
|
|
180
|
+
/** (CON-02) Session intents: GET/PUT /api/agent/sessions/:addr/intent with the directory; reads carry `intent`. */
|
|
181
|
+
readonly intents?: SessionIntents
|
|
108
182
|
}
|
|
109
183
|
|
|
110
184
|
/**
|
|
@@ -125,11 +199,12 @@ const userView = (user: UserRecord) => ({
|
|
|
125
199
|
/** The built-in /api/agent routes. */
|
|
126
200
|
export function agentRoutes(deps: AgentDeps): AgentRoute[] {
|
|
127
201
|
const feed = deps.feed ?? new MessageFeed()
|
|
128
|
-
|
|
129
|
-
...(deps.directory ? directoryRoutes({ directory: deps.directory, store: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.registry ? { registry: deps.registry } : {}), ...(deps.grants ? { grants: deps.grants } : {}), ...(deps.acting ? { acting: deps.acting } : {}), ...(deps.audit ? { audit: deps.audit } : {}), ...(deps.eventCursorKeys ? { previewKeys: deps.eventCursorKeys } : {}) }) : []),
|
|
130
|
-
...(deps.directory && deps.
|
|
202
|
+
const routes: AgentRoute[] = [
|
|
203
|
+
...(deps.directory ? directoryRoutes({ directory: deps.directory, store: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.registry ? { registry: deps.registry } : {}), ...(deps.grants ? { grants: deps.grants } : {}), ...(deps.acting ? { acting: deps.acting } : {}), ...(deps.audit ? { audit: deps.audit } : {}), ...(deps.eventCursorKeys ? { previewKeys: deps.eventCursorKeys } : {}), ...(deps.teamDiscovery ? { discovery: deps.teamDiscovery } : {}), ...intentReads(deps), ...conciergeReads(deps) }) : []),
|
|
204
|
+
...(deps.directory && deps.intents ? intentRoutes({ intents: deps.intents, directory: deps.directory, store: deps.store, ...(deps.registry ? { registry: deps.registry } : {}), ...(deps.teamDiscovery ? { discovery: deps.teamDiscovery } : {}) }) : []),
|
|
205
|
+
...(deps.directory && deps.agentEvents && deps.eventCursorKeys ? eventsRoutes({ events: deps.agentEvents, keys: deps.eventCursorKeys, directory: deps.directory, ...eventScope(deps) }) : []),
|
|
131
206
|
...(deps.directory && deps.delivery ? messageRoutes({ delivery: deps.delivery, directory: deps.directory, store: deps.store, feed, ...(deps.decisions ? { decisions: deps.decisions } : {}) }) : []),
|
|
132
|
-
...(deps.directory && deps.registry ? readRoutes({ store: deps.store, directory: deps.directory, registry: deps.registry, ...(deps.spokes ? { spokes: deps.spokes } : {}), ...launchReads(deps), ...(deps.agentEvents && deps.eventCursorKeys ? { events: { events: deps.agentEvents, keys: deps.eventCursorKeys, directory: deps.directory } } : {}), ...(deps.heartbeatMs !== undefined ? { heartbeatMs: deps.heartbeatMs } : {}), ...(deps.now ? { now: deps.now } : {}), ...(deps.draining ? { draining: deps.draining } : {}) }) : []),
|
|
207
|
+
...(deps.directory && deps.registry ? readRoutes({ store: deps.store, directory: deps.directory, registry: deps.registry, ...(deps.spokes ? { spokes: deps.spokes } : {}), ...launchReads(deps), ...(deps.agentEvents && deps.eventCursorKeys ? { events: { events: deps.agentEvents, keys: deps.eventCursorKeys, directory: deps.directory, ...eventScope(deps) } } : {}), ...(deps.heartbeatMs !== undefined ? { heartbeatMs: deps.heartbeatMs } : {}), ...(deps.now ? { now: deps.now } : {}), ...(deps.draining ? { draining: deps.draining } : {}), ...(deps.teamDiscovery ? { discovery: deps.teamDiscovery } : {}), ...intentReads(deps), ...conciergeReads(deps) }) : []),
|
|
133
208
|
{
|
|
134
209
|
method: "GET",
|
|
135
210
|
path: `${AGENT_PREFIX}/whoami`,
|
|
@@ -145,7 +220,83 @@ export function agentRoutes(deps: AgentDeps): AgentRoute[] {
|
|
|
145
220
|
? launchRoutes({ launches: deps.launches, grants: deps.grants, acting: deps.acting, audit: deps.audit, ...(deps.directory ? { directory: deps.directory } : {}) })
|
|
146
221
|
: []),
|
|
147
222
|
...(deps.webhooks ? webhookRoutes(deps.webhooks, deps.store) : []),
|
|
223
|
+
...(deps.profiles && deps.directory
|
|
224
|
+
? profileRoutes({
|
|
225
|
+
profiles: deps.profiles,
|
|
226
|
+
directory: deps.directory,
|
|
227
|
+
audit: deps.store,
|
|
228
|
+
...(deps.agentEvents ? { events: deps.agentEvents } : {}),
|
|
229
|
+
// HWA-15: an app's pin also needs a usable prompt grant on the session.
|
|
230
|
+
...(deps.grants && deps.registry ? { authorizePin: pinAuthorizer({ registry: deps.registry, grants: deps.grants, ...(deps.acting ? { acting: deps.acting } : {}) }) } : {}),
|
|
231
|
+
// RP-08: where a profile is used (pins, team roles, launches, workspaces).
|
|
232
|
+
usage: new ProfileUsage({
|
|
233
|
+
profiles: deps.profiles,
|
|
234
|
+
ports: usagePorts({
|
|
235
|
+
directory: deps.directory,
|
|
236
|
+
...(deps.registry ? { registry: deps.registry } : {}),
|
|
237
|
+
...(deps.teams ? { teams: { teams: deps.teams.teams, seats: deps.teams.seats } } : {}),
|
|
238
|
+
...(deps.launches ? { launches: deps.launches } : {}),
|
|
239
|
+
...(deps.workspaces ? { workspaces: { workspaces: deps.workspaces.workspaces, members: deps.workspaces.members } } : {}),
|
|
240
|
+
}),
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
: []),
|
|
244
|
+
...(deps.workspaces ? workspaceRoutes({ ...deps.workspaces, audit: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}) }) : []),
|
|
245
|
+
...(deps.workspaces ? boardRoutes({ ...deps.workspaces, ...(deps.directory && deps.registry ? { sessions: registryBoardSessions(deps.directory, deps.registry) } : {}), ...(deps.decisions ? { decisions: deps.decisions } : {}), ...(deps.artifacts?.artifacts ? { artifacts: deps.artifacts.artifacts } : {}), ...(deps.now ? { now: deps.now } : {}) }) : []),
|
|
246
|
+
...(deps.workspaceCheckpoints ? checkpointRoutes({ checkpoints: deps.workspaceCheckpoints, audit: deps.store }) : []),
|
|
247
|
+
...(deps.workspaces ? masterRoutes({ ...deps.workspaces, audit: deps.store, ...(deps.workspaceCheckpoints ? { checkpoints: deps.workspaceCheckpoints } : {}), ...(deps.launches ? { launches: deps.launches } : {}), ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.now ? { now: deps.now } : {}) }) : []),
|
|
248
|
+
...artifactRoutes(deps.artifacts ?? {}),
|
|
249
|
+
...(deps.artifactHost ? artifactHostRoutes(deps.artifactHost) : []),
|
|
250
|
+
...(deps.directory && deps.registry && deps.delivery ? graphRoutes({ store: deps.store, registry: deps.registry, directory: deps.directory, messages: deps.delivery.messages, ...(deps.decisions ? { decisions: deps.decisions } : {}), ...(deps.launches ? { launches: deps.launches } : {}), ...(deps.workspaces ? { workspaces: deps.workspaces } : {}), ...(deps.audit ? { audit: deps.audit } : {}), ...deps.graph, ...(deps.now ? { now: deps.now } : {}), ...conciergeReads(deps), ...conciergeCurrent(deps) }) : []),
|
|
251
|
+
...(deps.teamIntake ? intakeRoutes({ intake: deps.teamIntake, audit: deps.store }) : []),
|
|
252
|
+
...(deps.teams ? teamRoutes({ ...deps.teams, ...(deps.teamIntake ? { intake: deps.teamIntake } : {}), audit: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}) }) : []),
|
|
253
|
+
...(deps.concierge && deps.directory && deps.registry
|
|
254
|
+
? watchlistRoutes({ ...deps.concierge, directory: deps.directory, store: deps.store, registry: deps.registry, ...(deps.decisions ? { decisions: deps.decisions } : {}), ...launchReads(deps), ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.now ? { now: deps.now } : {}) })
|
|
255
|
+
: []),
|
|
256
|
+
// CON-03: the situation board and the Concierge's Decision reads (users and the current Concierge only).
|
|
257
|
+
...(deps.concierge && deps.directory && deps.registry
|
|
258
|
+
? conciergeBoardRoutes({ ...deps.concierge, directory: deps.directory, store: deps.store, registry: deps.registry, ...(deps.decisions ? { decisions: deps.decisions } : {}), ...(deps.intents ? { intents: deps.intents } : {}), ...launchReads(deps), ...(deps.now ? { now: deps.now } : {}) })
|
|
259
|
+
: []),
|
|
260
|
+
// CON-04: settings, enable/disable/restart and notes; CON-06's routes through the tiers slot.
|
|
261
|
+
...(deps.concierge?.service && deps.concierge.identity ? conciergeRoutes({ service: deps.concierge.service, identity: deps.concierge.identity, ...(deps.concierge.models ? { models: deps.concierge.models } : {}) }) : []),
|
|
262
|
+
...(deps.concierge?.routes ? deps.concierge.routes() : []),
|
|
148
263
|
]
|
|
264
|
+
// HWA-13: side chats read, launch and message through the routes above, as the caller.
|
|
265
|
+
const { registry } = deps
|
|
266
|
+
const sourceUpdatedAt = registry ? (userID: string, id: string) => (registry.sessionTenant(id) === userID ? registry.session(id)?.updated : undefined) : undefined
|
|
267
|
+
// HWA-24: artifact AI jobs start their handler through the launch and message routes above, as the requester.
|
|
268
|
+
const a = deps.artifacts
|
|
269
|
+
routes.push(...artifactJobRoutes({ ...(a?.artifacts ? { artifacts: a.artifacts } : {}), ...(a?.state ? { state: a.state } : {}), ports: jobPorts(routes), ...(deps.launches ? { launches: deps.launches } : {}), audit: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.now ? { now: deps.now } : {}) }))
|
|
270
|
+
const all = deps.workspaces ? [...routes, ...sideChatRoutes({ workspaces: deps.workspaces.workspaces, ports: routePorts(routes), ...(deps.launches ? { launches: deps.launches } : {}), ...(sourceUpdatedAt ? { sourceUpdatedAt } : {}), audit: deps.store, ...(deps.agentEvents ? { events: deps.agentEvents } : {}), ...(deps.now ? { now: deps.now } : {}) })] : routes
|
|
271
|
+
// CON-04: manage paths never touch a Concierge session (concierge/guard.ts).
|
|
272
|
+
const identity = deps.concierge?.identity
|
|
273
|
+
const directory = deps.directory
|
|
274
|
+
const launches = deps.launches
|
|
275
|
+
return identity && directory
|
|
276
|
+
? guardConciergeRoutes(all, {
|
|
277
|
+
identity,
|
|
278
|
+
resolve: (userID, addr) => directory.resolve(userID, addr).sessions.map((s) => s.sessionID),
|
|
279
|
+
...(launches
|
|
280
|
+
? {
|
|
281
|
+
launch: async (userID: string, id: string) => {
|
|
282
|
+
const l = await launches.view(userID, id)
|
|
283
|
+
return l ? { ...(l.sessionID !== undefined ? { sessionID: l.sessionID } : {}), concierge: isConciergeLaunch(l) } : undefined
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
287
|
+
})
|
|
288
|
+
: all
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** HWA-28/40: unfiltered events and watch read by a team-scoped session leave out what discovery hides from it. */
|
|
292
|
+
function eventScope(deps: AgentDeps): Pick<EventsRouteDeps, "scope" | "resolve"> {
|
|
293
|
+
const discovery = deps.teamDiscovery
|
|
294
|
+
return discovery
|
|
295
|
+
? {
|
|
296
|
+
scope: async (userID, actor) => eventVisibility(await discovery.census(userID, actor)),
|
|
297
|
+
resolve: async (userID, actor, addr) => discovery.resolve(await discovery.census(userID, actor), addr).sessions.map((s) => s.sessionID),
|
|
298
|
+
}
|
|
299
|
+
: {}
|
|
149
300
|
}
|
|
150
301
|
|
|
151
302
|
/** What the read routes show of launches: launched sessions' labels (the latest launch wins). */
|
|
@@ -165,6 +316,24 @@ function launchReads(deps: AgentDeps): Pick<ReadDeps, "labels"> {
|
|
|
165
316
|
}
|
|
166
317
|
}
|
|
167
318
|
|
|
319
|
+
/** (CON-04) Which sessions reads mark `concierge: true` (the user's current and hidden Concierge sessions). */
|
|
320
|
+
function conciergeReads(deps: AgentDeps): { concierge?: (userID: string) => Promise<readonly string[]> } {
|
|
321
|
+
const identity = deps.concierge?.identity
|
|
322
|
+
return identity ? { concierge: async (userID: string) => (await identity.current(userID)).hiddenSessionIDs } : {}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** (CON-09a) The user's current Concierge session (Watchtower labels it @@concierge). */
|
|
326
|
+
function conciergeCurrent(deps: AgentDeps): { conciergeCurrent?: (userID: string) => Promise<string | undefined> } {
|
|
327
|
+
const identity = deps.concierge?.identity
|
|
328
|
+
return identity ? { conciergeCurrent: async (userID: string) => (await identity.current(userID)).sessionID } : {}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** (CON-02) What reads show of intents: the stored ones of the listed sessions, by session. */
|
|
332
|
+
function intentReads(deps: AgentDeps): { intents?: (userID: string, sessionIDs: readonly string[]) => Promise<Map<string, SessionIntent>> } {
|
|
333
|
+
const { intents } = deps
|
|
334
|
+
return intents ? { intents: (userID: string, sessionIDs: readonly string[]) => intents.many(userID, sessionIDs) } : {}
|
|
335
|
+
}
|
|
336
|
+
|
|
168
337
|
/** The act verbs, when every part they need is given. */
|
|
169
338
|
function actVerbRoutes(deps: AgentDeps): AgentRoute[] {
|
|
170
339
|
const { actions, acting, grants, audit, registry, directory, spokes } = deps
|
|
@@ -180,6 +349,7 @@ function actVerbRoutes(deps: AgentDeps): AgentRoute[] {
|
|
|
180
349
|
audit,
|
|
181
350
|
...(deps.escalate ? { escalate: deps.escalate } : {}),
|
|
182
351
|
...(deps.maxPromptChars !== undefined ? { maxPromptChars: deps.maxPromptChars } : {}),
|
|
352
|
+
...(deps.concierge?.protects ? { protect: deps.concierge.protects, conciergeActor: deps.concierge.protects } : {}),
|
|
183
353
|
}),
|
|
184
354
|
)
|
|
185
355
|
}
|
|
@@ -192,6 +362,8 @@ export interface AgentApiOptions {
|
|
|
192
362
|
/** Records activity of a resolved caller (device and user lastSeen). */
|
|
193
363
|
touch?(user: UserRecord, device?: DeviceRecord): void
|
|
194
364
|
readonly routes: readonly AgentRoute[]
|
|
365
|
+
/** Runs for every resolved caller before the route class check (CON-04: the Concierge's route allow-list). */
|
|
366
|
+
gate?(actor: AgentActor, route: AgentRoute): Promise<void>
|
|
195
367
|
}
|
|
196
368
|
|
|
197
369
|
export interface AgentApi {
|
|
@@ -250,6 +422,7 @@ export function agentApi(options: AgentApiOptions): AgentApi {
|
|
|
250
422
|
options.touch?.(resolved.user, resolved.device)
|
|
251
423
|
const found = find(method, url.pathname)
|
|
252
424
|
if (!found) throw new AgentError("not_found", "Not found")
|
|
425
|
+
if (options.gate) await options.gate(resolved.actor, found.route)
|
|
253
426
|
if (!agentAllows(found.route.allow, resolved.actor)) throw new AgentError("forbidden", "This credential cannot use this route")
|
|
254
427
|
const { actor, credential, user, device } = resolved
|
|
255
428
|
return await found.route.handle({ request, url, params: found.params, actor, credential, user, ...(device ? { device } : {}) })
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// scope of the only events so far, launch.*), which see and delete only the registrations they made and get only
|
|
10
10
|
// the events of launches they started. Sessions cannot register webhooks. Another user's registration, or (apps)
|
|
11
11
|
// one the token did not make, is 404 not_found.
|
|
12
|
+
import { boundedJson } from "./body"
|
|
12
13
|
import type { Store } from "../store"
|
|
13
14
|
import type { AgentActor } from "./actor"
|
|
14
15
|
import { AgentError } from "./errors"
|
|
@@ -37,7 +38,7 @@ const requireEventScopes = (actor: AgentActor, events: unknown) => {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async function bodyOf(request: Request): Promise<Record<string, unknown>> {
|
|
40
|
-
const raw =
|
|
41
|
+
const raw = (await boundedJson(request)) as unknown
|
|
41
42
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new AgentError("invalid", "Body must be a JSON object")
|
|
42
43
|
return raw as Record<string, unknown>
|
|
43
44
|
}
|
|
@@ -39,6 +39,18 @@ export const CANONICAL_WEBHOOK_EVENT_TYPES: readonly string[] = [
|
|
|
39
39
|
"blocker.opened", "blocker.closed", "machine.changed", "role.claimed", "role.released",
|
|
40
40
|
"message.changed", "launch.changed",
|
|
41
41
|
]
|
|
42
|
+
/** A launch.changed event for a queued launch's new wait kind (not a state step). */
|
|
43
|
+
export const isWaitChange = (event: Pick<AgentEvent, "type" | "data">): boolean => event.type === "launch.changed" && event.data.wait !== undefined
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The legacy `launch.<state>` type a canonical launch.changed event also matches; none for a queued launch's
|
|
47
|
+
* change of wait kind (FX-02), so launch.queued still fires once per launch, as before.
|
|
48
|
+
*/
|
|
49
|
+
export function legacyLaunchType(event: Pick<AgentEvent, "type" | "data">): string | undefined {
|
|
50
|
+
if (event.type !== "launch.changed" || typeof event.data.state !== "string" || isWaitChange(event)) return undefined
|
|
51
|
+
return `launch.${event.data.state}`
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/** Includes persisted pre-canonical launch registrations for rolling rollback compatibility. */
|
|
43
55
|
export const WEBHOOK_EVENT_TYPES: readonly string[] = [...CANONICAL_WEBHOOK_EVENT_TYPES, ...LAUNCH_STATES.map((state) => `launch.${state}`)]
|
|
44
56
|
export const DEFAULT_WEBHOOK_EVENTS: readonly string[] = ["launch.*"]
|
|
@@ -336,11 +348,14 @@ export class Webhooks {
|
|
|
336
348
|
}
|
|
337
349
|
|
|
338
350
|
publishCanonical(userID: string, event: AgentEvent, _family: EventFamily, audience?: EventAudience): Promise<number> {
|
|
339
|
-
|
|
351
|
+
// (FX-02) A queued launch's new wait kind is journaled (agent events, watch) but sent to no webhook: webhooks keep
|
|
352
|
+
// one delivery per launch state, as before.
|
|
353
|
+
if (isWaitChange(event)) return Promise.resolve(0)
|
|
354
|
+
const legacy = legacyLaunchType(event)
|
|
340
355
|
return this.publish(userID, canonicalWebhookEvent(this.deps.source, userID, event), {
|
|
341
356
|
...(audience?.by ? { by: audience.by } : {}),
|
|
342
357
|
...(audience?.parties ? { owners: audience.parties.filter((party) => party.startsWith("app:")) } : {}),
|
|
343
|
-
...(
|
|
358
|
+
...(legacy ? { matchTypes: [event.type, legacy] } : {}),
|
|
344
359
|
})
|
|
345
360
|
}
|
|
346
361
|
|
|
@@ -38,6 +38,8 @@ export interface AgentSession {
|
|
|
38
38
|
/** Project name (lowercased) used by `.project` scopes. */
|
|
39
39
|
project?: string
|
|
40
40
|
title?: string
|
|
41
|
+
/** The last harness kind a spoke reported it under (offline sessions keep their glyph, RF-02). */
|
|
42
|
+
harness?: string
|
|
41
43
|
firstSeen?: number
|
|
42
44
|
/** Last time a spoke reported it (ms; persisted at minute granularity while it stays online). */
|
|
43
45
|
lastSeen?: number
|
|
@@ -70,6 +72,8 @@ export interface AgentData {
|
|
|
70
72
|
generations?: Record<string, number>
|
|
71
73
|
/** The user's agent kill switch (agent/acting-switch.ts): every agent action answers paused. */
|
|
72
74
|
actingPaused?: true
|
|
75
|
+
/** (CON-09a) Sessions unnamed before the directory knew them: their record is never derived a name (≤ 500). */
|
|
76
|
+
unnamed?: string[]
|
|
73
77
|
[field: string]: unknown
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Who may do what with an artifact (HWA-20; contract: Cross-cutting rules, Artifacts). Checked on every access by
|
|
2
|
+
// the artifact store. Tenancy comes first: the store only ever reads the caller's own user's records, so another
|
|
3
|
+
// user's artifact is simply missing. Within a user:
|
|
4
|
+
// - The user (console, app token) owns every artifact: read, revise, manage.
|
|
5
|
+
// - A session reads an artifact only when its visibility is "workspace" and the session is a *current* member of
|
|
6
|
+
// that workspace (re-checked on every read, so leaving a workspace ends access). It never reads a private one,
|
|
7
|
+
// even one it created: createdBySessionID is provenance and grants nothing.
|
|
8
|
+
// - A session may create artifacts (they belong to its user). Membership grants no write: revising, metadata,
|
|
9
|
+
// visibility, archive and delete stay with the user unless an explicit grant-aware policy is passed in (the
|
|
10
|
+
// routes, HWA-21, may wire the existing grants through `authorize`).
|
|
11
|
+
// Role labels and workspace roles never confer authority here.
|
|
12
|
+
import type { Artifact } from "../../protocol"
|
|
13
|
+
|
|
14
|
+
export type ArtifactPrincipal = { readonly kind: "user" } | { readonly kind: "session"; readonly sessionID: string }
|
|
15
|
+
|
|
16
|
+
/** "manage": metadata, visibility, archive/unarchive and delete. */
|
|
17
|
+
export type ArtifactAction = "read" | "create" | "revise" | "manage"
|
|
18
|
+
|
|
19
|
+
/** The workspace facts access needs (HWA-03/04 provide them; tests use fixtures). */
|
|
20
|
+
export interface ArtifactWorkspaces {
|
|
21
|
+
exists(userID: string, workspaceID: string): Promise<boolean>
|
|
22
|
+
isCurrentMember(userID: string, workspaceID: string, sessionID: string): Promise<boolean>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AccessRequest {
|
|
26
|
+
readonly userID: string
|
|
27
|
+
readonly principal: ArtifactPrincipal
|
|
28
|
+
readonly action: ArtifactAction
|
|
29
|
+
/** Absent for create. */
|
|
30
|
+
readonly artifact?: Artifact
|
|
31
|
+
/** Absent when this hub has no workspaces: then no session reads through a workspace. */
|
|
32
|
+
readonly workspaces?: ArtifactWorkspaces
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ArtifactAuthorizer = (request: AccessRequest) => Promise<boolean>
|
|
36
|
+
|
|
37
|
+
/** The contract's default policy (see the header). */
|
|
38
|
+
export const defaultArtifactPolicy: ArtifactAuthorizer = async ({ userID, principal, action, artifact, workspaces }) => {
|
|
39
|
+
if (principal.kind === "user") return true
|
|
40
|
+
if (action === "create") return true
|
|
41
|
+
if (action !== "read" || !artifact) return false
|
|
42
|
+
if (artifact.visibility !== "workspace" || artifact.workspaceID === undefined || !workspaces) return false
|
|
43
|
+
return workspaces.isCurrentMember(userID, artifact.workspaceID, principal.sessionID)
|
|
44
|
+
}
|