@huynle/hub 0.7.3 → 0.8.0
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 +229 -0
- package/Dockerfile +7 -2
- package/README.md +153 -24
- 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/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6474 -3470
- 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/enrollment/enroll.ts +20 -8
- package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +95 -13
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +380 -9
- package/packages/cli/src/messaging.ts +3 -1
- package/packages/cli/src/setup/commands.ts +3 -2
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +403 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- 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 +96 -2
- package/packages/node/src/opencode-warm.ts +325 -0
- package/packages/node/src/run.ts +27 -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 +169 -6
- package/packages/protocol/src/launch.ts +22 -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 +3 -1
- package/packages/sdk/src/index.ts +5 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +548 -1
- 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 +215 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +17 -0
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +69 -2
- package/src/central/agent/delivery.ts +95 -15
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +5 -0
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +11 -3
- 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 +16 -2
- package/src/central/agent/launch-routes.ts +6 -1
- package/src/central/agent/launch-spec.ts +14 -1
- package/src/central/agent/message-routes.ts +2 -1
- package/src/central/agent/messages.ts +27 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/read-routes.ts +86 -19
- package/src/central/agent/routes.ts +176 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- 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 +773 -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 +339 -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 +89 -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 +130 -0
- package/src/central/concierge/proposals.ts +262 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +62 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +69 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +740 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +277 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +7 -1
- package/src/central/http.ts +633 -50
- 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 +43 -6
- 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 +772 -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 +888 -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 +97 -15
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +383 -18
- package/src/spoke/listing.ts +47 -4
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +176 -0
- package/src/spoke/spoke.ts +319 -20
- 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-BS0gVkfu.js +1 -0
- package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
- package/web/dist/assets/Library-CtxRqTjo.js +2 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
- package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
- package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/index-DaH5GUdV.js +31 -0
- package/web/dist/assets/index-WWuKfWxL.css +1 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/live-YyTjE47g.js +5 -0
- package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
- package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
- package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
- package/web/dist/assets/source-CvUHTc3O.js +1 -0
- package/web/dist/assets/ui-C9Igxzjm.js +1 -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
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// - Placement (placement.ts, under the user's fenced lease) moves it to provisioning on a node; the engine then
|
|
7
7
|
// runs one pipeline per launch: prepare on the node (requestNode) → launch create on the machine's service spoke
|
|
8
8
|
// (feature "launch": directory, launchID, agent, model) → roles (byUser only for the user's own launches; a refused
|
|
9
|
-
// role is noted, not fatal) → the name (taken meanwhile: failed) → the
|
|
9
|
+
// role is noted, not fatal) → the name (taken meanwhile: failed) → the spec's intent (CON-02, source launch; a
|
|
10
|
+
// failure is noted, not fatal) → the primer (Delivery.prime, so roles and name
|
|
10
11
|
// are in it) → the prompt (actions.prompt, resetHops false; marked prompted first, so at most once) → running.
|
|
11
12
|
// A stop between steps ends it stopped; the node going away ends it lost; anything else ends it failed.
|
|
12
13
|
// - openshell launches (design §6.4, decision lk6gn3u7): after prepare the engine mints the launch's hlt1 token
|
|
@@ -119,6 +120,10 @@ export interface LaunchEngineDeps {
|
|
|
119
120
|
readonly onLaunchChanged?: (launch: Launch) => void
|
|
120
121
|
/** Cluster mode: the leader's supervisor, for an engine that does not supervise (see EngineSupervisor). */
|
|
121
122
|
readonly supervisor?: EngineSupervisor
|
|
123
|
+
/** (HWA-15) Applies a launch's agentProfile before its prompt; returns notes for the reason, never throws. */
|
|
124
|
+
readonly profiles?: { applyAtLaunch(userID: string, sessionID: string, ref: { readonly profileID: string; readonly version: number }): Promise<string[]> }
|
|
125
|
+
/** (CON-02) Stores the spec's intent as the session's intent (source launch) at identify. */
|
|
126
|
+
readonly intents?: { storeLaunch(userID: string, sessionID: string, text: string, by?: string): Promise<void> }
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/** Node events of every pod (cluster mode; the same as local ones on a single hub). */
|
|
@@ -771,11 +776,20 @@ export class LaunchEngine {
|
|
|
771
776
|
}
|
|
772
777
|
}
|
|
773
778
|
if (spec.name !== undefined) await directory.setName(userID, sessionID, spec.name)
|
|
779
|
+
if (spec.intent !== undefined && this.deps.intents) {
|
|
780
|
+
try {
|
|
781
|
+
await this.deps.intents.storeLaunch(userID, sessionID, spec.intent, launch.by ?? "user")
|
|
782
|
+
} catch (cause) {
|
|
783
|
+
notes.push(`intent not stored: ${errorText(cause)}`)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
774
786
|
await delivery.prime(userID, sessionID)
|
|
787
|
+
if (spec.agentProfile) notes.push(...(this.deps.profiles ? await this.deps.profiles.applyAtLaunch(userID, sessionID, spec.agentProfile) : ["agentProfile not applied: this hub does not apply profiles"]))
|
|
775
788
|
checkpoint()
|
|
776
789
|
if (spec.prompt !== undefined) {
|
|
777
790
|
await this.updateWork(userID, launch.id, { prompted: true })
|
|
778
|
-
|
|
791
|
+
// (CON-10) The launch's prompt is the session's first prompt (its intent), even when the launch named it.
|
|
792
|
+
const prompted = await actions.prompt({ sessionID, text: spec.prompt, resetHops: false, intent: "first" })
|
|
779
793
|
if (prompted.outcome === "offline") throw new Error("The session went offline before its prompt")
|
|
780
794
|
if (prompted.outcome === "unsupported") throw new Error(unsupportedMessage(prompted))
|
|
781
795
|
if (prompted.mode === "steer") this.sawWorking(userID, launch.id)
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// → kill switch again → the engine. Apps and sessions see and stop only the launches they started (by); any other
|
|
14
14
|
// launch, like another user's, is 404 not_found. The engine applies the per-user launch caps (rate_limited).
|
|
15
15
|
// Every outcome once the body is valid is in the agent audit (verb launch, or launch.stop for DELETE).
|
|
16
|
+
import { boundedJson } from "./body"
|
|
16
17
|
import type { LaunchInfo, LaunchSpec as WireSpec } from "../../protocol"
|
|
17
18
|
import { Feed } from "../cluster/feed"
|
|
18
19
|
import type { ActingSwitch } from "./acting-switch"
|
|
@@ -120,7 +121,7 @@ const invalid = (message: string) => new AgentError("invalid", message)
|
|
|
120
121
|
const notFound = (id: string) => new AgentError("not_found", `No launch ${id}`)
|
|
121
122
|
|
|
122
123
|
async function bodyOf(request: Request): Promise<unknown> {
|
|
123
|
-
return
|
|
124
|
+
return boundedJson(request)
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/** ?state=a,b and ?label=k=v (repeated). */
|
|
@@ -197,6 +198,10 @@ export function launchRoutes(deps: LaunchRoutesDeps): AgentRoute[] {
|
|
|
197
198
|
async handle({ request, actor, user }) {
|
|
198
199
|
const raw = await bodyOf(request)
|
|
199
200
|
const spec = validateLaunchSpec(raw)
|
|
201
|
+
// HWA-15: naming a profile pins it; apps need profiles:assign, sessions never pin profiles.
|
|
202
|
+
if (spec.agentProfile && actor.kind !== "user" && !(actor.kind === "app" && actor.scopes.includes("profiles:assign"))) {
|
|
203
|
+
throw new AgentError("forbidden", "A launch's agentProfile needs scope profiles:assign; sessions cannot pin profiles")
|
|
204
|
+
}
|
|
200
205
|
const by = launchedBy(actor)
|
|
201
206
|
const held = spec.key === undefined ? undefined : (await launches.list(user.id)).find((l) => l.spec.key === spec.key)
|
|
202
207
|
let launchID: string | undefined
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// cleanup keep, no timeout.
|
|
11
11
|
// LaunchSpec is defined here until packages/protocol carries it (P4-T1); keep the two in step.
|
|
12
12
|
|
|
13
|
+
import { type LaunchAgentProfile, launchAgentProfileProblem } from "../../protocol"
|
|
13
14
|
import { AgentError } from "./errors"
|
|
15
|
+
import { intentText } from "./intents"
|
|
14
16
|
import { isReservedName, parseAddress, validName } from "./names"
|
|
15
17
|
import { canonicalProject, parseDuration } from "./policy"
|
|
16
18
|
|
|
@@ -66,9 +68,13 @@ export interface LaunchSpec {
|
|
|
66
68
|
readonly runtime: LaunchRuntime
|
|
67
69
|
readonly placement: LaunchPlacement
|
|
68
70
|
readonly prompt?: string
|
|
71
|
+
/** (CON-02) Stored as the session's intent (source launch) when it is identified. */
|
|
72
|
+
readonly intent?: string
|
|
69
73
|
/** Free-form app labels (never interpreted by the hub). */
|
|
70
74
|
readonly labels: Readonly<Record<string, string>>
|
|
71
75
|
readonly end: LaunchEnd
|
|
76
|
+
/** (HWA-15) The profile version pinned and applied before the prompt (explicit version; never latest). */
|
|
77
|
+
readonly agentProfile?: LaunchAgentProfile
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export const KEY_MAX = 200
|
|
@@ -83,7 +89,7 @@ export const MODEL_MAX = 200
|
|
|
83
89
|
export const LAUNCH_TIMEOUT_MIN_MS = 60_000
|
|
84
90
|
export const LAUNCH_TIMEOUT_MAX_MS = 30 * 86_400_000
|
|
85
91
|
|
|
86
|
-
const FIELDS = ["key", "name", "roles", "harness", "agent", "model", "workspace", "runtime", "placement", "prompt", "labels", "end"]
|
|
92
|
+
const FIELDS = ["key", "name", "roles", "harness", "agent", "model", "workspace", "runtime", "placement", "prompt", "intent", "labels", "end", "agentProfile"]
|
|
87
93
|
const WORKSPACE_FIELDS = ["repo", "mode", "branch", "base", "remote"]
|
|
88
94
|
const RUNTIME_FIELDS = ["kind", "profile"]
|
|
89
95
|
const PLACEMENT_FIELDS = ["labels", "machine", "mode"]
|
|
@@ -254,8 +260,13 @@ export function validateLaunchSpec(input: unknown): LaunchSpec {
|
|
|
254
260
|
if (typeof input.prompt !== "string" || input.prompt.length > PROMPT_MAX || PROMPT_CONTROL.test(input.prompt)) throw invalid(`prompt is text of up to ${PROMPT_MAX} characters`)
|
|
255
261
|
prompt = input.prompt
|
|
256
262
|
}
|
|
263
|
+
// (CON-02) What the session is for: untrusted, sanitised to one line, at most INTENT_MAX characters.
|
|
264
|
+
const intent = input.intent === undefined ? undefined : intentText(input.intent)
|
|
257
265
|
const labels = validLabels(input.labels, "labels")
|
|
258
266
|
const end = validEnd(input.end)
|
|
267
|
+
const profileProblem = input.agentProfile === undefined ? undefined : launchAgentProfileProblem(input.agentProfile)
|
|
268
|
+
if (profileProblem) throw invalid(profileProblem)
|
|
269
|
+
const agentProfile = input.agentProfile === undefined ? undefined : { profileID: (input.agentProfile as LaunchAgentProfile).profileID, version: (input.agentProfile as LaunchAgentProfile).version }
|
|
259
270
|
return {
|
|
260
271
|
...(key !== undefined ? { key } : {}),
|
|
261
272
|
...(name !== undefined ? { name } : {}),
|
|
@@ -267,8 +278,10 @@ export function validateLaunchSpec(input: unknown): LaunchSpec {
|
|
|
267
278
|
runtime,
|
|
268
279
|
placement,
|
|
269
280
|
...(prompt !== undefined ? { prompt } : {}),
|
|
281
|
+
...(intent !== undefined ? { intent } : {}),
|
|
270
282
|
labels,
|
|
271
283
|
end,
|
|
284
|
+
...(agentProfile ? { agentProfile } : {}),
|
|
272
285
|
}
|
|
273
286
|
}
|
|
274
287
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// user, who owns every party); others get not_found.
|
|
26
26
|
// - Messaging off (UserRecord.agent.messagingPaused): msg, ask, reply and decline answer paused; reading works.
|
|
27
27
|
// - Every stored message is published on the MessageFeed (GET /api/agent/watch 'message' events, waiters).
|
|
28
|
+
import { boundedJson } from "./body"
|
|
28
29
|
import { Feed } from "../cluster/feed"
|
|
29
30
|
import type { Store } from "../store"
|
|
30
31
|
import type { AgentActor } from "./actor"
|
|
@@ -70,7 +71,7 @@ const READ = { session: true, scope: "messages:read" } as const
|
|
|
70
71
|
type Body = Record<string, unknown>
|
|
71
72
|
|
|
72
73
|
async function bodyOf(request: Request): Promise<Body> {
|
|
73
|
-
const body = await request
|
|
74
|
+
const body = await boundedJson(request)
|
|
74
75
|
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentError("invalid", "Body must be a JSON object")
|
|
75
76
|
return body as Body
|
|
76
77
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// - Caps: each party (session, the user, an app) keeps its latest SESSION_MESSAGES; a message no party keeps
|
|
8
8
|
// is deleted. Per user, at most USER_MESSAGES messages / USER_BYTES; over either, the oldest go (down to
|
|
9
9
|
// 90%). Eviction keeps open asks while anything else can go.
|
|
10
|
+
// Deleting a message records the newest evicted `at` (retention(): the graph's `retention.truncated`, HWA-31).
|
|
10
11
|
// - Asks: open → answered (a reply) | declined | expired (at expiresAt, seen on read; expire() persists it).
|
|
11
12
|
// The first answer wins (version-checked write); later ones get `gone`.
|
|
12
13
|
// - Replies and declines come only from the recipient (anyone else: not_found, like a foreign id) and go to
|
|
@@ -39,8 +40,11 @@ export const INBOX_RECENT = 50
|
|
|
39
40
|
export type MessageKind = "msg" | "ask" | "reply" | "decline"
|
|
40
41
|
/** `sent` for msg/reply/decline; asks are open until answered, declined or expired. */
|
|
41
42
|
export type MessageState = "sent" | "open" | "answered" | "declined" | "expired"
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* How a message reached its recipient (design §3.3), recorded by the delivery engine. (CON-10) "dropped": it was
|
|
45
|
+
* pending for a session that ended for good (a retired Concierge session) and will never be delivered.
|
|
46
|
+
*/
|
|
47
|
+
export type Delivery = "steer" | "turn" | "queued" | "retained" | "pending" | "dropped"
|
|
44
48
|
|
|
45
49
|
export interface Message {
|
|
46
50
|
readonly id: string
|
|
@@ -117,11 +121,18 @@ export interface Usage {
|
|
|
117
121
|
readonly bytes: number
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/** What the store retains (the interaction graph's retention disclosure, HWA-31). */
|
|
125
|
+
export interface Retention extends Usage {
|
|
126
|
+
/** The newest `at` of a message the caps deleted; absent while nothing was evicted. */
|
|
127
|
+
readonly evictedAt?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
const MESSAGES = "messages"
|
|
121
131
|
const INDEX = "message-index"
|
|
122
132
|
const USAGE = "message-usage"
|
|
123
133
|
const MUTES = "message-mutes"
|
|
124
134
|
const USAGE_KEY = "total"
|
|
135
|
+
const EVICTED_KEY = "evicted"
|
|
125
136
|
/** Version-checked attempts to detach a contended message. */
|
|
126
137
|
const DETACH_ATTEMPTS = 16
|
|
127
138
|
|
|
@@ -261,6 +272,18 @@ export class Messages {
|
|
|
261
272
|
return (await this.state.get<Usage>(userID, USAGE, USAGE_KEY))?.value ?? { count: 0, bytes: 0 }
|
|
262
273
|
}
|
|
263
274
|
|
|
275
|
+
/** Every retained message of the user, oldest first, as of now: one scan, for whole-user projections. */
|
|
276
|
+
async all(userID: string): Promise<Message[]> {
|
|
277
|
+
const entries = await this.state.list<StoredMessage>(userID, MESSAGES)
|
|
278
|
+
return entries.map((entry) => this.view(entry.value.message)).sort((a, b) => a.at - b.at || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Usage and the newest evicted message's time (older interactions may be missing). */
|
|
282
|
+
async retention(userID: string): Promise<Retention> {
|
|
283
|
+
const [usage, evicted] = await Promise.all([this.usage(userID), this.state.get<number>(userID, USAGE, EVICTED_KEY)])
|
|
284
|
+
return { ...usage, ...(evicted ? { evictedAt: evicted.value } : {}) }
|
|
285
|
+
}
|
|
286
|
+
|
|
264
287
|
/** Records how the message was delivered; undefined when there is no such message. */
|
|
265
288
|
async setDelivery(userID: string, id: string, delivery: Delivery): Promise<Message | undefined> {
|
|
266
289
|
const at = this.now()
|
|
@@ -431,6 +454,8 @@ export class Messages {
|
|
|
431
454
|
if ((await this.state.put<StoredMessage>(userID, MESSAGES, id, { ...current.value, indexed }, { ifVersion: current.version })).ok) return
|
|
432
455
|
} else if ((await this.state.delete(userID, MESSAGES, id, { ifVersion: current.version })).ok) {
|
|
433
456
|
await this.addUsage(userID, -1, -current.value.bytes)
|
|
457
|
+
const at = current.value.message.at
|
|
458
|
+
await updateVersioned<number>(this.state, userID, USAGE, EVICTED_KEY, (marked) => (marked !== undefined && marked >= at ? undefined : at))
|
|
434
459
|
return
|
|
435
460
|
}
|
|
436
461
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { NAME_MAX, validName } from "../../../packages/sdk/src/address.ts"
|
|
6
6
|
|
|
7
|
-
export { NAME_MAX, parseAddress, validName, type Address } from "../../../packages/sdk/src/address.ts"
|
|
7
|
+
export { addressProblem, NAME_MAX, parseAddress, validName, type Address, type AddressProblem } from "../../../packages/sdk/src/address.ts"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* OpenCode's fallback titles: "New session" / "Child session" (display labels) and the historical
|
|
@@ -43,10 +43,20 @@ export function withSuffix(base: string, taken: (name: string) => boolean): stri
|
|
|
43
43
|
export const isReservedName = (name: string): boolean => name === "me"
|
|
44
44
|
|
|
45
45
|
/** Roles only the user may claim in phase 1 (grants including `roles` come in phase 3). */
|
|
46
|
-
const RESERVED_ROLES = ["orchestrator"]
|
|
46
|
+
const RESERVED_ROLES = ["orchestrator", "concierge"]
|
|
47
47
|
|
|
48
48
|
export const isReservedRole = (role: string): boolean => RESERVED_ROLES.some((prefix) => role.startsWith(prefix))
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* (CON-04) Roles only the hub itself claims and releases (reserved too): not the user, a session, an app under a
|
|
52
|
+
* `roles` grant or a launch spec. `concierge`: the Concierge service holds it for the user's current Concierge.
|
|
53
|
+
*/
|
|
54
|
+
const MANAGED_ROLES = ["concierge"]
|
|
55
|
+
|
|
56
|
+
export const isManagedRole = (role: string): boolean => MANAGED_ROLES.some((prefix) => role.startsWith(prefix))
|
|
57
|
+
|
|
58
|
+
export const MANAGED_ROLE_MESSAGE = (role: string) => `@@${role} is managed by the hub: use Settings → Concierge`
|
|
59
|
+
|
|
50
60
|
/** The longest derived name: a few words, short enough to type and read in an address (@send-hello-to-session). */
|
|
51
61
|
export const DERIVED_NAME_MAX = 24
|
|
52
62
|
/** At most this many words of the title make a derived name. */
|
|
@@ -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,53 @@ 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)
|
|
299
355
|
const states = params.get("state")?.split(",").filter(Boolean)
|
|
300
356
|
if (states?.some((state) => !STATES.has(state))) throw new AgentError("invalid", "state must be idle, working, blocked or error (comma-separated)")
|
|
301
357
|
const machine = params.get("machine") || undefined
|
|
@@ -310,6 +366,10 @@ function sessionFilter(deps: ReadDeps, userID: string, params: URLSearchParams):
|
|
|
310
366
|
if (project !== undefined && root !== project && name.toLowerCase() !== project.toLowerCase()) return false
|
|
311
367
|
if (states?.length && !states.includes(view.state)) return false
|
|
312
368
|
if (machineIDs && !machineIDs.has(row.machineID)) return false
|
|
369
|
+
if (wanted.length) {
|
|
370
|
+
const have = [...words(view.title ?? ""), ...words(view.name ?? "")]
|
|
371
|
+
if (!wanted.every((w) => have.some((h) => h.includes(w)))) return false
|
|
372
|
+
}
|
|
313
373
|
return labels.every(([key, value]) => view.labels[key] === value)
|
|
314
374
|
}
|
|
315
375
|
}
|
|
@@ -324,20 +384,25 @@ export function readRoutes(deps: ReadDeps): AgentRoute[] {
|
|
|
324
384
|
method: "GET",
|
|
325
385
|
path: `${AGENT_PREFIX}/status`,
|
|
326
386
|
allow: READ,
|
|
327
|
-
handle: async ({ user, actor }) => ok(snapshot(deps, user.id, actor, await labelsOf(deps, user.id))),
|
|
387
|
+
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
388
|
},
|
|
329
389
|
{
|
|
330
390
|
method: "GET",
|
|
331
391
|
path: `${AGENT_PREFIX}/sessions`,
|
|
332
392
|
allow: READ,
|
|
333
|
-
handle: async ({ user, url }) => {
|
|
393
|
+
handle: async ({ user, url, actor }) => {
|
|
334
394
|
const keep = sessionFilter(deps, user.id, url.searchParams)
|
|
395
|
+
const census = await censusOf(deps, user.id, actor)
|
|
335
396
|
const entries = directoryOf(deps.directory, user.id)
|
|
336
397
|
const labels = await labelsOf(deps, user.id)
|
|
337
|
-
const
|
|
398
|
+
const self = await conciergeSelf(deps.concierge, user.id, actor)
|
|
399
|
+
const listed = rows(deps.registry, user.id)
|
|
338
400
|
.map((row) => ({ row, view: view(row, entries.get(row.session.id), labels) }))
|
|
339
|
-
.filter(({ row, view }) => keep(row, view))
|
|
340
|
-
|
|
401
|
+
.filter(({ row, view }) => keep(row, view) && (!census || visible(census, row.session.id)) && !self.has(row.session.id))
|
|
402
|
+
// Only the listed sessions' intents are read (never the whole collection).
|
|
403
|
+
const intents = await intentsOf(deps, user.id, listed.map(({ row }) => row.session.id))
|
|
404
|
+
const concierge = await conciergeOf(deps, user.id)
|
|
405
|
+
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
406
|
return ok({ sessions })
|
|
342
407
|
},
|
|
343
408
|
},
|
|
@@ -345,13 +410,13 @@ export function readRoutes(deps: ReadDeps): AgentRoute[] {
|
|
|
345
410
|
method: "GET",
|
|
346
411
|
path: `${AGENT_PREFIX}/sessions/:addr`,
|
|
347
412
|
allow: READ,
|
|
348
|
-
handle: async ({ user, params }) => ok(await show(deps, user.id, params.addr
|
|
413
|
+
handle: async ({ user, params, actor }) => ok(await show(deps, user.id, params.addr!, await censusOf(deps, user.id, actor))),
|
|
349
414
|
},
|
|
350
415
|
{
|
|
351
416
|
method: "GET",
|
|
352
417
|
path: `${AGENT_PREFIX}/sessions/:addr/log`,
|
|
353
418
|
allow: READ,
|
|
354
|
-
handle: async ({ user, params, url }) => ok(await log(deps, user.id, params.addr!, url.searchParams.get("n"))),
|
|
419
|
+
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
420
|
},
|
|
356
421
|
...(deps.events ? [watchRoute(deps, deps.events)] : []),
|
|
357
422
|
]
|
|
@@ -363,9 +428,11 @@ function watchRoute(deps: ReadDeps, events: EventsRouteDeps): AgentRoute {
|
|
|
363
428
|
method: "GET",
|
|
364
429
|
path: `${AGENT_PREFIX}/watch`,
|
|
365
430
|
allow: READ,
|
|
366
|
-
handle: ({ user, url, request, actor }) => {
|
|
431
|
+
handle: async ({ user, url, request, actor }) => {
|
|
367
432
|
const addr = url.searchParams.get("session") || undefined
|
|
368
|
-
const
|
|
433
|
+
const census = addr ? await censusOf(deps, user.id, actor) : undefined
|
|
434
|
+
const resolved = addr ? (census && deps.discovery ? deps.discovery.resolve(census, addr) : deps.directory.resolve(user.id, addr)) : undefined
|
|
435
|
+
const sessions = resolved ? new Set(resolved.sessions.map((s) => s.sessionID)) : undefined
|
|
369
436
|
const blocked = ["1", "true"].includes(url.searchParams.get("blocked") ?? "")
|
|
370
437
|
const cursor = url.searchParams.get("cursor") || request.headers.get("last-event-id") || undefined
|
|
371
438
|
return watchStream({
|