@huynle/hub 0.6.1 → 0.7.2
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 +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// The directory on /api/agent (design §2.2) and its wiring to the hub: the Registry is the SessionSource
|
|
2
|
+
// (sessions reported now, per tenant), the console's Archive (viewPrefs.hiddenSessions) the archive source,
|
|
3
|
+
// and every catalog change syncs the affected users (records, lastSeen, derived names).
|
|
4
|
+
//
|
|
5
|
+
// Routes (user actors always; session tokens may read, and write for their OWN session only):
|
|
6
|
+
// GET /api/agent/who?project=&role=
|
|
7
|
+
// POST /api/agent/name {name, session?}
|
|
8
|
+
// POST /api/agent/roles/claim {role, exclusive?, project?, session?}
|
|
9
|
+
// POST /api/agent/roles/release {role, project?, session?}
|
|
10
|
+
// PUT /api/agent/sessions/:addr/archive {archived}
|
|
11
|
+
// `session` is an address (ses_…, ID prefix, @name, …) resolved for the caller; a session token defaults to
|
|
12
|
+
// its own session, a user or app must name one. Foreign and unknown sessions are both not_found.
|
|
13
|
+
//
|
|
14
|
+
// App tokens (lifecycle design, decision xwzersmm): naming and archiving need sessions:manage; a role claim or
|
|
15
|
+
// release also needs a grant allowing `roles` whose projects cover the (live) session's directory, in the acting
|
|
16
|
+
// order (kill switch → grant → hourly cap → kill switch), with every outcome in the agent audit (verb roles or
|
|
17
|
+
// roles.release). Reserved roles stay the user's to assign. Each change is in the admin audit and the event
|
|
18
|
+
// journal once (session.named, role.claimed, role.released, session.archived); a no-op is neither.
|
|
19
|
+
import type { ActingSwitch } from "./acting-switch"
|
|
20
|
+
import { auditOf } from "./acting"
|
|
21
|
+
import type { AgentAudit, AuditDecision, AuditOutcome } from "./audit"
|
|
22
|
+
import { partyFromActor } from "./envelope"
|
|
23
|
+
import { type AgentEvents, archiveEvent, type EventInput, namedEvent, pinnedEvent, roleEvent } from "./events"
|
|
24
|
+
import type { Grant, Grants } from "./grants"
|
|
25
|
+
import path from "node:path"
|
|
26
|
+
import type { CatalogMachine, Registry, RegistryMessage } from "../registry"
|
|
27
|
+
import type { Store } from "../store"
|
|
28
|
+
import { Directory, type ReportedSession, type SessionSource } from "./directory"
|
|
29
|
+
import { AgentError } from "./errors"
|
|
30
|
+
import { ArchiveInactive, parseInactiveFor } from "./archive-inactive"
|
|
31
|
+
import { slugify } from "./names"
|
|
32
|
+
import { AGENT_PREFIX, type AgentContext, type AgentRoute } from "./routes"
|
|
33
|
+
import type { CursorKeys } from "./event-cursor"
|
|
34
|
+
|
|
35
|
+
/** What the directory reads from the Registry. */
|
|
36
|
+
export interface DirectoryRegistry {
|
|
37
|
+
catalog(): CatalogMachine[]
|
|
38
|
+
sessionTenant(sessionID: string): string | undefined
|
|
39
|
+
subscribe(listener: (message: RegistryMessage, tenant?: string) => void): () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type CatalogSession = CatalogMachine["processes"][number]["sessions"][number]
|
|
43
|
+
|
|
44
|
+
/** The `.project` scope of a session: its project name (else folder name), as a slug. */
|
|
45
|
+
function projectOf(s: Pick<CatalogSession, "projectName" | "project" | "directory">): string | undefined {
|
|
46
|
+
const name = s.projectName || path.basename(s.project || s.directory || "")
|
|
47
|
+
return name ? (slugify(name) ?? name.toLowerCase()) : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Sessions the Registry reports for a user (subagents from the status trees too), and their Archive. */
|
|
51
|
+
export function registrySource(registry: Pick<DirectoryRegistry, "catalog" | "sessionTenant">, store: Pick<Store, "viewPrefs">): SessionSource {
|
|
52
|
+
return {
|
|
53
|
+
reported(userID) {
|
|
54
|
+
const out = new Map<string, ReportedSession>()
|
|
55
|
+
for (const machine of registry.catalog()) {
|
|
56
|
+
for (const process of machine.processes) {
|
|
57
|
+
for (const s of process.sessions) {
|
|
58
|
+
if (out.has(s.id) || registry.sessionTenant(s.id) !== userID) continue
|
|
59
|
+
const project = projectOf(s)
|
|
60
|
+
const scope = project ? { project } : {}
|
|
61
|
+
out.set(s.id, { sessionID: s.id, title: s.title, machineID: s.machineID, ...(s.parentID ? { parentID: s.parentID } : {}), ...scope, state: s.state })
|
|
62
|
+
for (const child of s.status) {
|
|
63
|
+
if (child.id === s.id || out.has(child.id)) continue
|
|
64
|
+
out.set(child.id, { sessionID: child.id, title: child.title, machineID: s.machineID, parentID: child.parentID ?? s.id, ...scope, state: child.state })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...out.values()]
|
|
70
|
+
},
|
|
71
|
+
archived: (userID) => new Set(store.viewPrefs(userID).hiddenSessions),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The hub's directory: fed by the Registry, synced for the affected users on every catalog change. */
|
|
76
|
+
export function hubDirectory(options: { registry: DirectoryRegistry; store: Pick<Store, "viewPrefs" | "updateAgentData" | "agentData" | "user">; now?: () => number }): Directory {
|
|
77
|
+
const { registry, store } = options
|
|
78
|
+
const directory = new Directory({ store, source: registrySource(registry, store), ...(options.now ? { now: options.now } : {}) })
|
|
79
|
+
/** Users with reported sessions at the last catalog change (they need a sync when theirs go away). */
|
|
80
|
+
let online = new Set<string>()
|
|
81
|
+
registry.subscribe((message) => {
|
|
82
|
+
if (message.type !== "catalog") return
|
|
83
|
+
const now = new Set<string>()
|
|
84
|
+
for (const machine of registry.catalog()) {
|
|
85
|
+
for (const process of machine.processes) {
|
|
86
|
+
for (const s of process.sessions) {
|
|
87
|
+
const tenant = registry.sessionTenant(s.id)
|
|
88
|
+
if (tenant !== undefined) now.add(tenant)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const userID of new Set([...online, ...now])) {
|
|
93
|
+
directory.sync(userID).catch((cause: unknown) => {
|
|
94
|
+
if (!(cause instanceof AgentError)) console.error("[hub] directory sync failed:", cause)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
online = now
|
|
98
|
+
})
|
|
99
|
+
return directory
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- routes ----------
|
|
103
|
+
|
|
104
|
+
type Body = Record<string, unknown>
|
|
105
|
+
|
|
106
|
+
async function readBody(request: Request): Promise<Body> {
|
|
107
|
+
const body = await request.json()
|
|
108
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentError("invalid", "Expected a JSON object")
|
|
109
|
+
return body as Body
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function text(body: Body, field: string, required: true): string
|
|
113
|
+
function text(body: Body, field: string, required?: false): string | undefined
|
|
114
|
+
function text(body: Body, field: string, required = false): string | undefined {
|
|
115
|
+
const value = body[field]
|
|
116
|
+
if (value === undefined && !required) return undefined
|
|
117
|
+
if (typeof value !== "string" || !value || value.length > 300) throw new AgentError("invalid", `${field} must be a string`)
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function flag(body: Body, field: string): boolean | undefined {
|
|
122
|
+
const value = body[field]
|
|
123
|
+
if (value !== undefined && typeof value !== "boolean") throw new AgentError("invalid", `${field} must be true or false`)
|
|
124
|
+
return value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The session a write targets: a session token acts on its own session only (forbidden otherwise); a user
|
|
129
|
+
* names any of their sessions by address.
|
|
130
|
+
*/
|
|
131
|
+
function target(directory: Directory, { actor, user }: AgentContext, addr: string | undefined): string {
|
|
132
|
+
if (actor.kind === "session") {
|
|
133
|
+
if (addr === undefined) return actor.sessionID
|
|
134
|
+
const sessionID = directory.resolveOne(user.id, addr).sessionID
|
|
135
|
+
if (sessionID !== actor.sessionID) throw new AgentError("forbidden", "A session token can only act for its own session")
|
|
136
|
+
return sessionID
|
|
137
|
+
}
|
|
138
|
+
if (addr === undefined) throw new AgentError("invalid", "session is required (ses_…, @name)")
|
|
139
|
+
return directory.resolveOne(user.id, addr).sessionID
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const NO_STORE = { "cache-control": "no-store" }
|
|
143
|
+
const ok = (body: unknown) => Response.json(body, { headers: NO_STORE })
|
|
144
|
+
|
|
145
|
+
export interface DirectoryRoutesDeps {
|
|
146
|
+
readonly directory: Directory
|
|
147
|
+
readonly store: Pick<Store, "setSessionArchived" | "setSessionPinned" | "archiveSessionIfUnpinned" | "viewPrefs" | "agentData" | "audit">
|
|
148
|
+
readonly events?: Pick<AgentEvents, "append">
|
|
149
|
+
/** App role writes: the live session (its directory, for the grant's projects). */
|
|
150
|
+
readonly registry?: Pick<Registry, "sessionTenant" | "session">
|
|
151
|
+
readonly grants?: Pick<Grants, "usable" | "consume">
|
|
152
|
+
readonly acting?: Pick<ActingSwitch, "ensureActing">
|
|
153
|
+
readonly audit?: Pick<AgentAudit, "record">
|
|
154
|
+
readonly previewKeys?: CursorKeys
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const MANAGE = { session: true, scope: "sessions:manage" } as const
|
|
158
|
+
|
|
159
|
+
export function directoryRoutes(deps: DirectoryRoutesDeps): AgentRoute[] {
|
|
160
|
+
const { directory, store, events } = deps
|
|
161
|
+
|
|
162
|
+
/** The admin audit and the journal, once per change. */
|
|
163
|
+
async function changed(context: AgentContext, action: string, sessionID: string, event: EventInput): Promise<void> {
|
|
164
|
+
await store.audit({ actor: context.user.id, action, target: sessionID, detail: context.actor.kind })
|
|
165
|
+
await events?.append(context.user.id, event).catch((cause) => console.error(`[hub] ${event.type} event failed: ${(cause as Error).message}`))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A role write: users and sessions as before; an app under a `roles` grant on the session's project, audited. */
|
|
169
|
+
async function roleWrite<T>(context: AgentContext, verb: "roles" | "roles.release", sessionID: string, run: () => Promise<T>): Promise<T> {
|
|
170
|
+
const { actor, user } = context
|
|
171
|
+
if (actor.kind !== "app") return run()
|
|
172
|
+
const { registry, grants, acting, audit } = deps
|
|
173
|
+
if (!registry || !grants || !acting || !audit) throw new AgentError("forbidden", "This hub does not let app tokens change roles")
|
|
174
|
+
let grant: Grant | undefined
|
|
175
|
+
const record = (decision: AuditDecision, outcome: AuditOutcome, reason?: string) =>
|
|
176
|
+
audit.record(user.id, { actor: partyFromActor(actor), verb, target: { sessionID }, ...(grant ? { grant: grant.id } : {}), decision, ...(reason ? { reason } : {}), outcome })
|
|
177
|
+
try {
|
|
178
|
+
acting.ensureActing(user.id)
|
|
179
|
+
const live = registry.sessionTenant(sessionID) === user.id ? registry.session(sessionID) : undefined
|
|
180
|
+
if (!live) throw new AgentError("offline", "No spoke reports this session now, so its project cannot be checked against a grant")
|
|
181
|
+
const usable = await grants.usable(actor, "roles", { directory: live.directory })
|
|
182
|
+
if (!usable.length) throw new AgentError("forbidden", "No grant lets this app change roles in this session's project")
|
|
183
|
+
grant = usable[0]!
|
|
184
|
+
await grants.consume(grant)
|
|
185
|
+
acting.ensureActing(user.id)
|
|
186
|
+
const result = await run()
|
|
187
|
+
await record("allow", "ok")
|
|
188
|
+
return result
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
const [decision, outcome] = auditOf(cause)
|
|
191
|
+
await record(decision, outcome, cause instanceof Error ? cause.message : undefined)
|
|
192
|
+
throw cause
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
...(deps.previewKeys ? [{
|
|
198
|
+
method: "POST" as const,
|
|
199
|
+
path: `${AGENT_PREFIX}/sessions/archive-inactive`,
|
|
200
|
+
allow: { scope: "sessions:manage" },
|
|
201
|
+
async handle(context: AgentContext) {
|
|
202
|
+
const inactive = new ArchiveInactive({ secret: deps.previewKeys!.active().secret })
|
|
203
|
+
const body = await readBody(context.request)
|
|
204
|
+
const prefs = store.viewPrefs(context.user.id)
|
|
205
|
+
const entries = (store.agentData(context.user.id).sessions ?? []).map((s) => ({ sessionID: s.sessionID, online: !!deps.registry?.session(s.sessionID), ...(s.lastSeen !== undefined ? { lastSeen: s.lastSeen } : {}), roles: [] }))
|
|
206
|
+
if (body.preview === true) {
|
|
207
|
+
const limit = body.limit
|
|
208
|
+
if (!Number.isInteger(limit) || Number(limit) < 1 || Number(limit) > 200) throw new AgentError("invalid", "limit must be a whole number from 1 to 200")
|
|
209
|
+
return ok(inactive.preview(context.user.id, entries, { pinned: new Set(prefs.pinnedSessions), archived: new Set(prefs.hiddenSessions), inactiveForMs: parseInactiveFor(body.inactiveFor), limit: Number(limit) }))
|
|
210
|
+
}
|
|
211
|
+
if (body.confirm !== true || typeof body.previewToken !== "string") throw new AgentError("invalid", "Use preview:true first, then confirm:true with its previewToken")
|
|
212
|
+
const open = new Set(entries.filter((s) => deps.registry?.session(s.sessionID)?.open).map((s) => s.sessionID))
|
|
213
|
+
const active = new Set(entries.filter((s) => { const v = deps.registry?.session(s.sessionID); return !!v && (v.busy || v.status.some((x) => x.state === "working" || x.state === "blocked")) }).map((s) => s.sessionID))
|
|
214
|
+
const checked = inactive.revalidate(context.user.id, body.previewToken, entries, { pinned: new Set(prefs.pinnedSessions), archived: new Set(prefs.hiddenSessions), open, active })
|
|
215
|
+
const changedIDs: string[] = []
|
|
216
|
+
const skipped = [...checked.skipped]
|
|
217
|
+
for (const sessionID of checked.candidates) {
|
|
218
|
+
const result = await store.archiveSessionIfUnpinned(context.user.id, sessionID)
|
|
219
|
+
if (result === "changed") { changedIDs.push(sessionID); await changed(context, "session.archive", sessionID, archiveEvent(sessionID, true)) }
|
|
220
|
+
else skipped.push({ sessionID, reason: result === "pinned" ? "pinned" : result === "not_found" ? "not_found" : "archived" })
|
|
221
|
+
}
|
|
222
|
+
if (changedIDs.length) await directory.sync(context.user.id)
|
|
223
|
+
return ok({ changed: changedIDs, skipped })
|
|
224
|
+
},
|
|
225
|
+
}] : []),
|
|
226
|
+
{
|
|
227
|
+
method: "PUT",
|
|
228
|
+
path: `${AGENT_PREFIX}/sessions/:addr/pin`,
|
|
229
|
+
allow: { scope: "sessions:manage" },
|
|
230
|
+
async handle(context) {
|
|
231
|
+
const body = await readBody(context.request)
|
|
232
|
+
if (flag(body, "pinned") === undefined) throw new AgentError("invalid", "pinned must be true or false")
|
|
233
|
+
const sessionID = target(directory, context, context.params.addr)
|
|
234
|
+
const pinned = body.pinned === true
|
|
235
|
+
const result = await store.setSessionPinned(context.user.id, sessionID, pinned)
|
|
236
|
+
if (!result) throw new AgentError("not_found", `No session ${context.params.addr}`)
|
|
237
|
+
if (result.changed) await changed(context, pinned ? "session.pin" : "session.unpin", sessionID, pinnedEvent(sessionID, pinned))
|
|
238
|
+
return ok({ sessionID, pinned, changed: result.changed })
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
method: "PUT",
|
|
243
|
+
path: `${AGENT_PREFIX}/sessions/:addr/archive`,
|
|
244
|
+
allow: { scope: "sessions:manage" },
|
|
245
|
+
async handle(context) {
|
|
246
|
+
const body = await readBody(context.request)
|
|
247
|
+
if (flag(body, "archived") === undefined) throw new AgentError("invalid", "archived must be true or false")
|
|
248
|
+
const sessionID = target(directory, context, context.params.addr)
|
|
249
|
+
const archived = body.archived === true
|
|
250
|
+
const result = await store.setSessionArchived(context.user.id, sessionID, archived)
|
|
251
|
+
if (!result) throw new AgentError("invalid", "The archive is full; unarchive a session first")
|
|
252
|
+
if (result.changed) {
|
|
253
|
+
await directory.sync(context.user.id)
|
|
254
|
+
await changed(context, archived ? "session.archive" : "session.unarchive", sessionID, archiveEvent(sessionID, archived))
|
|
255
|
+
}
|
|
256
|
+
return ok({ sessionID, archived, changed: result.changed })
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
method: "GET",
|
|
261
|
+
path: `${AGENT_PREFIX}/who`,
|
|
262
|
+
// Reading the directory: sessions, and app tokens holding sessions:read; writes stay users and sessions.
|
|
263
|
+
allow: { session: true, scope: "sessions:read" },
|
|
264
|
+
handle: ({ url, user }) => {
|
|
265
|
+
const project = url.searchParams.get("project") || undefined
|
|
266
|
+
const role = url.searchParams.get("role") || undefined
|
|
267
|
+
return ok({ sessions: directory.who(user.id, { ...(project ? { project } : {}), ...(role ? { role } : {}) }) })
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
method: "POST",
|
|
272
|
+
path: `${AGENT_PREFIX}/name`,
|
|
273
|
+
allow: MANAGE,
|
|
274
|
+
handle: async (context) => {
|
|
275
|
+
const body = await readBody(context.request)
|
|
276
|
+
const name = text(body, "name", true)
|
|
277
|
+
const sessionID = target(directory, context, text(body, "session"))
|
|
278
|
+
const before = directory.resolveOne(context.user.id, sessionID).name
|
|
279
|
+
const named = await directory.setName(context.user.id, sessionID, name)
|
|
280
|
+
if (named.name !== before) await changed(context, "session.name", sessionID, namedEvent(sessionID, named.name))
|
|
281
|
+
return ok(named)
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
method: "POST",
|
|
286
|
+
path: `${AGENT_PREFIX}/roles/claim`,
|
|
287
|
+
allow: MANAGE,
|
|
288
|
+
handle: async (context) => {
|
|
289
|
+
const body = await readBody(context.request)
|
|
290
|
+
const role = text(body, "role", true)
|
|
291
|
+
const exclusive = flag(body, "exclusive")
|
|
292
|
+
const project = text(body, "project")
|
|
293
|
+
const sessionID = target(directory, context, text(body, "session"))
|
|
294
|
+
const claim = await roleWrite(context, "roles", sessionID, async () => {
|
|
295
|
+
const before = directory.resolveOne(context.user.id, sessionID).roles
|
|
296
|
+
const claim = await directory.claimRole(context.user.id, sessionID, role, {
|
|
297
|
+
...(exclusive !== undefined ? { exclusive } : {}),
|
|
298
|
+
...(project !== undefined ? { project } : {}),
|
|
299
|
+
byUser: context.actor.kind === "user",
|
|
300
|
+
})
|
|
301
|
+
const prev = before.find((r) => r.role === claim.role && r.project === claim.project)
|
|
302
|
+
if (!prev || prev.generation !== claim.generation || !!prev.exclusive !== !!claim.exclusive) {
|
|
303
|
+
await changed(context, "role.claim", sessionID, roleEvent(sessionID, true, claim))
|
|
304
|
+
}
|
|
305
|
+
return claim
|
|
306
|
+
})
|
|
307
|
+
return ok({ sessionID, ...claim })
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
method: "POST",
|
|
312
|
+
path: `${AGENT_PREFIX}/roles/release`,
|
|
313
|
+
allow: MANAGE,
|
|
314
|
+
handle: async (context) => {
|
|
315
|
+
const body = await readBody(context.request)
|
|
316
|
+
const role = text(body, "role", true)
|
|
317
|
+
const project = text(body, "project")
|
|
318
|
+
const sessionID = target(directory, context, text(body, "session"))
|
|
319
|
+
const released = await roleWrite(context, "roles.release", sessionID, async () => {
|
|
320
|
+
const released = await directory.releaseRole(context.user.id, sessionID, role, project)
|
|
321
|
+
await changed(context, "role.release", sessionID, roleEvent(sessionID, false, released))
|
|
322
|
+
return released
|
|
323
|
+
})
|
|
324
|
+
return ok({ sessionID, ...released, released: true })
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
]
|
|
328
|
+
}
|