@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,385 @@
|
|
|
1
|
+
// Messaging on /api/agent (design §3, §8; decisions jswt9p22): thin routes over the delivery engine (./delivery).
|
|
2
|
+
//
|
|
3
|
+
// Routes (user actors always; session tokens; app tokens by scope):
|
|
4
|
+
// POST /api/agent/msg {to, text, wake?, urgent?, all?} messages:send → 201 {message, delivery?} | {sent: [...]} (all)
|
|
5
|
+
// POST /api/agent/ask {to, text, wake?, urgent?, all?, expiresIn?, wait?} messages:send → 201 {message, delivery?, answer?}
|
|
6
|
+
// POST /api/agent/reply {replyTo, text, wake?} messages:send → 201 {message, delivery?, original}
|
|
7
|
+
// POST /api/agent/decline {replyTo, why} messages:send → 201 {message, delivery?, original}
|
|
8
|
+
// GET /api/agent/inbox messages:read → {asks, waiting, messages, muted}
|
|
9
|
+
// GET /api/agent/messages/:id?wait= messages:read → {message, answer?}
|
|
10
|
+
// POST /api/agent/mute {session} / POST /api/agent/unmute {session} messages:send → {muted}
|
|
11
|
+
// GET /api/agent/messaging / PUT /api/agent/messaging {enabled} user actors only: the per-user switch
|
|
12
|
+
// GET /api/agent/decisions?state= / GET /api/agent/decisions/:id messages:read → {decisions} / {decision}
|
|
13
|
+
//
|
|
14
|
+
// - @me (design §12.1): msg @me is stored for the user; ask @me opens a Decision (./decisions) with the extra
|
|
15
|
+
// fields {options: [{id, label, safe?}], recommendation: {option, why?}, urgency, tier, deadline (seconds),
|
|
16
|
+
// onTimeout, about: [addresses]} → 201 {message, decision, answer?}. Those fields on an ask to anyone else are
|
|
17
|
+
// invalid. Only the user answers a Decision, on the console (/api/decisions/:id/answer): its ask cannot be
|
|
18
|
+
// replied to or declined here. Decisions read for their asker (and the user); others get not_found.
|
|
19
|
+
//
|
|
20
|
+
// - The sender is only ever the authenticated actor; any from/sender in a body is ignored. A session cannot
|
|
21
|
+
// message itself (invalid). Replies and declines come only from the message's recipient; anyone else, and any
|
|
22
|
+
// unknown or foreign id, gets not_found. Non-session senders (the user, apps) have a stored-only inbox.
|
|
23
|
+
// - expiresIn and wait are seconds. wait (0–WAIT_MAX_S) long-polls an ask until it is answered, declined or
|
|
24
|
+
// expired; longer waits loop on GET /messages/:id?wait=. A message reads for its sender and recipient (and the
|
|
25
|
+
// user, who owns every party); others get not_found.
|
|
26
|
+
// - Messaging off (UserRecord.agent.messagingPaused): msg, ask, reply and decline answer paused; reading works.
|
|
27
|
+
// - Every stored message is published on the MessageFeed (GET /api/agent/watch 'message' events, waiters).
|
|
28
|
+
import { Feed } from "../cluster/feed"
|
|
29
|
+
import type { Store } from "../store"
|
|
30
|
+
import type { AgentActor } from "./actor"
|
|
31
|
+
import { type AskInput, DECISION_STATES, type DecisionState, type Decisions, type Tier, type Urgency } from "./decisions"
|
|
32
|
+
import type { Delivery, SendOptions, Sent } from "./delivery"
|
|
33
|
+
import type { Directory } from "./directory"
|
|
34
|
+
import { partyFromActor, partyKey } from "./envelope"
|
|
35
|
+
import { AgentError } from "./errors"
|
|
36
|
+
import { TEXT_MAX, WHY_MAX, type Message } from "./messages"
|
|
37
|
+
import { AGENT_PREFIX, type AgentRoute } from "./routes"
|
|
38
|
+
|
|
39
|
+
/** The longest wait of one request (seconds); clients loop for longer ones. */
|
|
40
|
+
export const WAIT_MAX_S = 60
|
|
41
|
+
/** How often a waiting request looks at the message besides the feed (expiry, other hub processes). */
|
|
42
|
+
export const WAIT_POLL_MS = 1_000
|
|
43
|
+
|
|
44
|
+
/** Message events' Bus topic (cluster mode). */
|
|
45
|
+
export const MESSAGES_TOPIC = "feed.messages"
|
|
46
|
+
|
|
47
|
+
/** Stored messages of every user, as they are sent (shared with the other pods once attached; see ../cluster/feed). */
|
|
48
|
+
export class MessageFeed extends Feed<Message> {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(MESSAGES_TOPIC, (cause) => console.error("[hub] message listener failed:", cause))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MessageDeps {
|
|
55
|
+
readonly delivery: Delivery
|
|
56
|
+
readonly directory: Pick<Directory, "resolveOne">
|
|
57
|
+
readonly store: Pick<Store, "agentData" | "updateAgentData" | "audit">
|
|
58
|
+
readonly feed: MessageFeed
|
|
59
|
+
/** Decisions (ask @me); without them an ask to @me is a plain stored ask. */
|
|
60
|
+
readonly decisions?: Decisions
|
|
61
|
+
/** Default WAIT_POLL_MS. */
|
|
62
|
+
readonly pollMs?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const NO_STORE = { "cache-control": "no-store" }
|
|
66
|
+
const ok = (body: unknown, status = 200) => Response.json(body, { status, headers: NO_STORE })
|
|
67
|
+
const SEND = { session: true, scope: "messages:send" } as const
|
|
68
|
+
const READ = { session: true, scope: "messages:read" } as const
|
|
69
|
+
|
|
70
|
+
type Body = Record<string, unknown>
|
|
71
|
+
|
|
72
|
+
async function bodyOf(request: Request): Promise<Body> {
|
|
73
|
+
const body = await request.json()
|
|
74
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentError("invalid", "Body must be a JSON object")
|
|
75
|
+
return body as Body
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function string(body: Body, key: string, max?: number): string {
|
|
79
|
+
const value = body[key]
|
|
80
|
+
if (typeof value !== "string" || !value.trim()) throw new AgentError("invalid", `${key} must be a non-empty string`)
|
|
81
|
+
if (max !== undefined && value.length > max) throw new AgentError("invalid", `${key} is longer than ${max} characters`)
|
|
82
|
+
return value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function flag(body: Body, key: string): boolean | undefined {
|
|
86
|
+
const value = body[key]
|
|
87
|
+
if (value === undefined) return undefined
|
|
88
|
+
if (typeof value !== "boolean") throw new AgentError("invalid", `${key} must be true or false`)
|
|
89
|
+
return value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function seconds(value: unknown, key: string, max = Number.POSITIVE_INFINITY): number | undefined {
|
|
93
|
+
if (value === undefined) return undefined
|
|
94
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > max) {
|
|
95
|
+
const range = Number.isFinite(max) ? ` from 0 to ${max}` : ""
|
|
96
|
+
throw new AgentError("invalid", `${key} must be a number of seconds${range}`, key === "wait" ? { hint: `Loop on GET ${AGENT_PREFIX}/messages/:id?wait=${WAIT_MAX_S} for longer waits` } : {})
|
|
97
|
+
}
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The wait query parameter (seconds), or undefined. */
|
|
102
|
+
function waitQuery(raw: string | null): number | undefined {
|
|
103
|
+
if (raw === null || raw === "") return undefined
|
|
104
|
+
return seconds(/^\d+(\.\d+)?$/.test(raw) ? Number(raw) : Number.NaN, "wait", WAIT_MAX_S)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sendOptions = (body: Body): SendOptions => {
|
|
108
|
+
const wake = flag(body, "wake")
|
|
109
|
+
const urgent = flag(body, "urgent")
|
|
110
|
+
return { ...(wake === undefined ? {} : { wake }), ...(urgent === undefined ? {} : { urgent }) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The user's address (also the malformed `@me.project`, which the delivery engine refuses). */
|
|
114
|
+
const isMe = (to: string) => to === "@me" || to.startsWith("@me.")
|
|
115
|
+
|
|
116
|
+
const DECISION_FIELDS = ["options", "recommendation", "urgency", "tier", "deadline", "onTimeout", "about"] as const
|
|
117
|
+
|
|
118
|
+
/** The state query parameter of a Decision listing, or undefined. */
|
|
119
|
+
export function decisionStateQuery(raw: string | null): DecisionState | undefined {
|
|
120
|
+
if (raw === null || raw === "") return undefined
|
|
121
|
+
if (!DECISION_STATES.includes(raw as DecisionState)) throw new AgentError("invalid", `state is one of ${DECISION_STATES.join(", ")}`)
|
|
122
|
+
return raw as DecisionState
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The Decision fields of an ask body (shapes only; ./decisions validates the rest); about stays addresses. */
|
|
126
|
+
function decisionFields(body: Body): Omit<AskInput, "question" | "about" | "expiresInMs"> & { about?: string[] } {
|
|
127
|
+
const out: Record<string, unknown> = {}
|
|
128
|
+
if (body.options !== undefined) {
|
|
129
|
+
if (!Array.isArray(body.options)) throw new AgentError("invalid", "options must be a list of {id, label, safe?}")
|
|
130
|
+
out.options = body.options.map((option: unknown) => {
|
|
131
|
+
if (typeof option !== "object" || option === null) throw new AgentError("invalid", "options must be a list of {id, label, safe?}")
|
|
132
|
+
const { id, label, safe } = option as Record<string, unknown>
|
|
133
|
+
if (safe !== undefined && typeof safe !== "boolean") throw new AgentError("invalid", "safe must be true or false")
|
|
134
|
+
return { id, label, ...(safe === true ? { safe } : {}) }
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
if (body.recommendation !== undefined) {
|
|
138
|
+
const r = body.recommendation as Record<string, unknown> | null
|
|
139
|
+
if (typeof r !== "object" || r === null || Array.isArray(r)) throw new AgentError("invalid", "recommendation must be {option, why?}")
|
|
140
|
+
out.recommendation = { option: r.option, ...(r.why === undefined ? {} : { why: r.why }) }
|
|
141
|
+
}
|
|
142
|
+
for (const key of ["urgency", "tier", "onTimeout"] as const) {
|
|
143
|
+
if (body[key] === undefined) continue
|
|
144
|
+
if (typeof body[key] !== "string") throw new AgentError("invalid", `${key} must be a string`)
|
|
145
|
+
out[key] = body[key]
|
|
146
|
+
}
|
|
147
|
+
const deadline = seconds(body.deadline, "deadline")
|
|
148
|
+
if (deadline !== undefined) out.deadlineMs = Math.round(deadline * 1000)
|
|
149
|
+
if (body.about !== undefined) {
|
|
150
|
+
if (!Array.isArray(body.about) || body.about.some((a) => typeof a !== "string")) throw new AgentError("invalid", "about must be a list of addresses")
|
|
151
|
+
out.about = body.about as string[]
|
|
152
|
+
}
|
|
153
|
+
return out as Omit<AskInput, "question" | "about" | "expiresInMs"> & { urgency?: Urgency; tier?: Tier; about?: string[] }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sentView = ({ message, delivery }: Sent) => ({ message, ...(delivery ? { delivery } : {}) })
|
|
157
|
+
|
|
158
|
+
export function messageRoutes(deps: MessageDeps): AgentRoute[] {
|
|
159
|
+
const { delivery, feed } = deps
|
|
160
|
+
const pollMs = deps.pollMs ?? WAIT_POLL_MS
|
|
161
|
+
|
|
162
|
+
const paused = (userID: string) => deps.store.agentData(userID).messagingPaused === true
|
|
163
|
+
const ensureOn = (userID: string) => {
|
|
164
|
+
if (paused(userID)) throw new AgentError("paused", "Messaging is off for this user", { hint: "Ask the user to turn messaging back on (it is their switch: PUT /api/agent/messaging {\"enabled\": true}; no console switch yet)" })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The message, when the actor may read it (its sender or recipient, or the user). */
|
|
168
|
+
const readable = async (userID: string, actor: AgentActor, id: string): Promise<Message> => {
|
|
169
|
+
const message = await delivery.messages.get(userID, id)
|
|
170
|
+
const me = partyKey(partyFromActor(actor))
|
|
171
|
+
if (!message || (actor.kind !== "user" && partyKey(message.from) !== me && partyKey(message.recipient) !== me)) {
|
|
172
|
+
throw new AgentError("not_found", "No such message")
|
|
173
|
+
}
|
|
174
|
+
return message
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Resolves on the feed's answer to `id`, after `ms`, or on abort. */
|
|
178
|
+
const nudge = (userID: string, id: string, ms: number, signal: AbortSignal) =>
|
|
179
|
+
new Promise<void>((resolve) => {
|
|
180
|
+
const done = () => {
|
|
181
|
+
clearTimeout(timer)
|
|
182
|
+
unsubscribe()
|
|
183
|
+
signal.removeEventListener("abort", done)
|
|
184
|
+
resolve()
|
|
185
|
+
}
|
|
186
|
+
const timer = setTimeout(done, ms)
|
|
187
|
+
const unsubscribe = feed.subscribe((user, message) => {
|
|
188
|
+
if (user === userID && message.replyTo === id) done()
|
|
189
|
+
})
|
|
190
|
+
signal.addEventListener("abort", done, { once: true })
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
/** The message and its answer, after waiting up to `waitS` seconds for an open ask to close. */
|
|
194
|
+
const settled = async (userID: string, message: Message, waitS: number | undefined, signal: AbortSignal) => {
|
|
195
|
+
const deadline = Date.now() + (waitS ?? 0) * 1000
|
|
196
|
+
let current = message
|
|
197
|
+
while (current.state === "open" && !signal.aborted) {
|
|
198
|
+
const left = deadline - Date.now()
|
|
199
|
+
if (left <= 0) break
|
|
200
|
+
await nudge(userID, current.id, Math.min(pollMs, left), signal)
|
|
201
|
+
current = (await delivery.messages.get(userID, current.id)) ?? current
|
|
202
|
+
}
|
|
203
|
+
const answer = current.answer ? await delivery.messages.get(userID, current.answer.id) : undefined
|
|
204
|
+
return { message: current, ...(answer ? { answer } : {}) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A Decision's ask is answered only on the console (the user is its only possible replier). */
|
|
208
|
+
const notADecision = async (userID: string, actor: AgentActor, replyTo: string) => {
|
|
209
|
+
if (actor.kind !== "user" || !deps.decisions) return
|
|
210
|
+
const decision = await deps.decisions.byMessage(userID, replyTo)
|
|
211
|
+
if (decision) throw new AgentError("invalid", `${replyTo} carries decision ${decision.id}`, { hint: "Answer it on the console's Home page (its decision card)" })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Decisions the actor may read: the user all, others those they asked. */
|
|
215
|
+
const decisionsFor = (userID: string, actor: AgentActor, state?: DecisionState) =>
|
|
216
|
+
deps.decisions!.list(userID, { ...(state ? { state } : {}), ...(actor.kind === "user" ? {} : { from: partyFromActor(actor) }) })
|
|
217
|
+
|
|
218
|
+
/** Sends to one session the address names, or (all) to every one; published on the feed. */
|
|
219
|
+
async function send(userID: string, actor: AgentActor, to: string, kind: "msg" | "ask", text: string, options: SendOptions, all: boolean | undefined): Promise<Sent[]> {
|
|
220
|
+
ensureOn(userID)
|
|
221
|
+
const sent = all ? await delivery.sendAll(userID, actor, to, kind, text, options) : [await delivery.send(userID, actor, to, kind, text, options)]
|
|
222
|
+
for (const { message } of sent) feed.publish(userID, message)
|
|
223
|
+
return sent
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
method: "POST",
|
|
229
|
+
path: `${AGENT_PREFIX}/msg`,
|
|
230
|
+
allow: SEND,
|
|
231
|
+
async handle({ request, user, actor }) {
|
|
232
|
+
const body = await bodyOf(request)
|
|
233
|
+
const to = string(body, "to")
|
|
234
|
+
const text = string(body, "text", TEXT_MAX)
|
|
235
|
+
const all = flag(body, "all")
|
|
236
|
+
const sent = await send(user.id, actor, to, "msg", text, sendOptions(body), all)
|
|
237
|
+
return ok(all ? { sent: sent.map(sentView) } : sentView(sent[0]!), 201)
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
method: "POST",
|
|
242
|
+
path: `${AGENT_PREFIX}/ask`,
|
|
243
|
+
allow: SEND,
|
|
244
|
+
async handle({ request, user, actor }) {
|
|
245
|
+
const body = await bodyOf(request)
|
|
246
|
+
const to = string(body, "to")
|
|
247
|
+
const text = string(body, "text", TEXT_MAX)
|
|
248
|
+
const all = flag(body, "all")
|
|
249
|
+
const expiresIn = seconds(body.expiresIn, "expiresIn")
|
|
250
|
+
const wait = seconds(body.wait, "wait", WAIT_MAX_S)
|
|
251
|
+
if (all && wait !== undefined) throw new AgentError("invalid", "wait cannot be combined with all")
|
|
252
|
+
const options: SendOptions = { ...sendOptions(body), ...(expiresIn === undefined ? {} : { expiresInMs: Math.round(expiresIn * 1000) }) }
|
|
253
|
+
const decisionAsked = DECISION_FIELDS.some((key) => body[key] !== undefined)
|
|
254
|
+
if (isMe(to) && deps.decisions) {
|
|
255
|
+
if (all) throw new AgentError("invalid", "all cannot be combined with @me")
|
|
256
|
+
ensureOn(user.id)
|
|
257
|
+
const { about, ...fields } = decisionFields(body)
|
|
258
|
+
const sessions = about?.map((addr) => deps.directory.resolveOne(user.id, addr).sessionID)
|
|
259
|
+
const { message, decision } = await deps.decisions.ask(user.id, actor, {
|
|
260
|
+
question: text,
|
|
261
|
+
...fields,
|
|
262
|
+
...(sessions ? { about: sessions } : {}),
|
|
263
|
+
...(options.expiresInMs === undefined ? {} : { expiresInMs: options.expiresInMs }),
|
|
264
|
+
})
|
|
265
|
+
feed.publish(user.id, message)
|
|
266
|
+
const done = await settled(user.id, message, wait, request.signal)
|
|
267
|
+
return ok({ ...done, decision: (await deps.decisions.get(user.id, decision.id)) ?? decision }, 201)
|
|
268
|
+
}
|
|
269
|
+
if (decisionAsked) throw new AgentError("invalid", "options, recommendation, urgency, tier, deadline, onTimeout and about are for asks to @me")
|
|
270
|
+
const sent = await send(user.id, actor, to, "ask", text, options, all)
|
|
271
|
+
if (all) return ok({ sent: sent.map(sentView) }, 201)
|
|
272
|
+
const { message, delivery: how } = sent[0]!
|
|
273
|
+
return ok({ ...sentView({ message, ...(how ? { delivery: how } : {}) }), ...(await settled(user.id, message, wait, request.signal)) }, 201)
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
method: "POST",
|
|
278
|
+
path: `${AGENT_PREFIX}/reply`,
|
|
279
|
+
allow: SEND,
|
|
280
|
+
async handle({ request, user, actor }) {
|
|
281
|
+
const body = await bodyOf(request)
|
|
282
|
+
const replyTo = string(body, "replyTo")
|
|
283
|
+
const text = string(body, "text", TEXT_MAX)
|
|
284
|
+
const wake = flag(body, "wake")
|
|
285
|
+
ensureOn(user.id)
|
|
286
|
+
await notADecision(user.id, actor, replyTo)
|
|
287
|
+
const replied = await delivery.reply(user.id, actor, replyTo, text, wake === undefined ? {} : { wake })
|
|
288
|
+
feed.publish(user.id, replied.message)
|
|
289
|
+
return ok({ ...sentView(replied), original: replied.original }, 201)
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
method: "POST",
|
|
294
|
+
path: `${AGENT_PREFIX}/decline`,
|
|
295
|
+
allow: SEND,
|
|
296
|
+
async handle({ request, user, actor }) {
|
|
297
|
+
const body = await bodyOf(request)
|
|
298
|
+
const replyTo = string(body, "replyTo")
|
|
299
|
+
const why = string(body, "why", WHY_MAX)
|
|
300
|
+
ensureOn(user.id)
|
|
301
|
+
await notADecision(user.id, actor, replyTo)
|
|
302
|
+
const declined = await delivery.decline(user.id, actor, replyTo, why)
|
|
303
|
+
feed.publish(user.id, declined.message)
|
|
304
|
+
return ok({ ...sentView(declined), original: declined.original }, 201)
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
method: "GET",
|
|
309
|
+
path: `${AGENT_PREFIX}/inbox`,
|
|
310
|
+
allow: READ,
|
|
311
|
+
async handle({ user, actor }) {
|
|
312
|
+
const party = partyFromActor(actor)
|
|
313
|
+
const inbox = await delivery.messages.inbox(user.id, party)
|
|
314
|
+
return ok({ ...inbox, muted: await delivery.messages.muted(user.id, party) })
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
method: "GET",
|
|
319
|
+
path: `${AGENT_PREFIX}/messages/:id`,
|
|
320
|
+
allow: READ,
|
|
321
|
+
async handle({ user, actor, params, url, request }) {
|
|
322
|
+
const wait = waitQuery(url.searchParams.get("wait"))
|
|
323
|
+
const message = await readable(user.id, actor, params.id!)
|
|
324
|
+
return ok(await settled(user.id, message, wait, request.signal))
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
...(deps.decisions
|
|
328
|
+
? [
|
|
329
|
+
{
|
|
330
|
+
method: "GET",
|
|
331
|
+
path: `${AGENT_PREFIX}/decisions`,
|
|
332
|
+
allow: READ,
|
|
333
|
+
handle: async ({ user, actor, url }) => ok({ decisions: await decisionsFor(user.id, actor, decisionStateQuery(url.searchParams.get("state"))) }),
|
|
334
|
+
} satisfies AgentRoute,
|
|
335
|
+
{
|
|
336
|
+
method: "GET",
|
|
337
|
+
path: `${AGENT_PREFIX}/decisions/:id`,
|
|
338
|
+
allow: READ,
|
|
339
|
+
async handle({ user, actor, params }) {
|
|
340
|
+
const decision = await deps.decisions!.get(user.id, params.id!)
|
|
341
|
+
if (!decision || (actor.kind !== "user" && partyKey(decision.from) !== partyKey(partyFromActor(actor)))) throw new AgentError("not_found", "No such decision")
|
|
342
|
+
return ok({ decision })
|
|
343
|
+
},
|
|
344
|
+
} satisfies AgentRoute,
|
|
345
|
+
]
|
|
346
|
+
: []),
|
|
347
|
+
...(["mute", "unmute"] as const).map(
|
|
348
|
+
(verb): AgentRoute => ({
|
|
349
|
+
method: "POST",
|
|
350
|
+
path: `${AGENT_PREFIX}/${verb}`,
|
|
351
|
+
allow: SEND,
|
|
352
|
+
async handle({ request, user, actor }) {
|
|
353
|
+
const body = await bodyOf(request)
|
|
354
|
+
const entry = deps.directory.resolveOne(user.id, string(body, "session"))
|
|
355
|
+
if (actor.kind === "session" && entry.sessionID === actor.sessionID) throw new AgentError("invalid", "A session cannot mute itself")
|
|
356
|
+
const party = partyFromActor(actor)
|
|
357
|
+
if (verb === "mute") await delivery.messages.mute(user.id, party, entry.sessionID)
|
|
358
|
+
else await delivery.messages.unmute(user.id, party, entry.sessionID)
|
|
359
|
+
return ok({ muted: await delivery.messages.muted(user.id, party) })
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
),
|
|
363
|
+
{
|
|
364
|
+
method: "GET",
|
|
365
|
+
path: `${AGENT_PREFIX}/messaging`,
|
|
366
|
+
allow: {},
|
|
367
|
+
handle: ({ user }) => ok({ enabled: !paused(user.id) }),
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
method: "PUT",
|
|
371
|
+
path: `${AGENT_PREFIX}/messaging`,
|
|
372
|
+
allow: {},
|
|
373
|
+
async handle({ request, user }) {
|
|
374
|
+
const enabled = flag(await bodyOf(request), "enabled")
|
|
375
|
+
if (enabled === undefined) throw new AgentError("invalid", "enabled must be true or false")
|
|
376
|
+
await deps.store.updateAgentData(user.id, (data) => {
|
|
377
|
+
if (enabled) delete data.messagingPaused
|
|
378
|
+
else data.messagingPaused = true
|
|
379
|
+
})
|
|
380
|
+
await deps.store.audit({ actor: user.id, action: enabled ? "messaging.resume" : "messaging.pause" })
|
|
381
|
+
return ok({ enabled })
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
]
|
|
385
|
+
}
|