@abot-ai/socket 0.0.0-stage → 0.2.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -2
  4. package/bin/abot-socket.mjs +15 -0
  5. package/config.example.json +14 -0
  6. package/docs/APP-REVIEW.md +184 -0
  7. package/docs/DEPLOYMENT.md +124 -0
  8. package/docs/INSTALLATION.md +177 -0
  9. package/docs/MIGRATION.md +84 -0
  10. package/docs/PROTOCOL.md +371 -0
  11. package/docs/SECURITY.md +116 -0
  12. package/docs/SELF-HOSTING.md +161 -0
  13. package/package.json +65 -4
  14. package/src/auth/auth-database.mjs +105 -0
  15. package/src/auth/auth-limits.mjs +17 -0
  16. package/src/auth/auth-pairing.mjs +95 -0
  17. package/src/auth/auth-sessions.mjs +119 -0
  18. package/src/auth/auth-storage-path.mjs +70 -0
  19. package/src/auth/auth.mjs +45 -0
  20. package/src/auth/runtime-credentials.mjs +39 -0
  21. package/src/cli/cli-arguments.mjs +23 -0
  22. package/src/cli/cli-entry.mjs +84 -0
  23. package/src/cli/cli-environment.mjs +31 -0
  24. package/src/cli/cli-help.mjs +34 -0
  25. package/src/cli/cli-output.mjs +35 -0
  26. package/src/cli/cli.mjs +58 -0
  27. package/src/cli/client-capabilities.mjs +14 -0
  28. package/src/cli/doctor.mjs +106 -0
  29. package/src/config/config-validation.mjs +41 -0
  30. package/src/config/config.mjs +57 -0
  31. package/src/deployment/deployment-command.mjs +42 -0
  32. package/src/deployment/deployment-input.mjs +76 -0
  33. package/src/deployment/deployment-plan.mjs +62 -0
  34. package/src/deployment/deployment-templates.mjs +109 -0
  35. package/src/main.mjs +26 -0
  36. package/src/notifications/notification-delivery-credentials.mjs +48 -0
  37. package/src/notifications/notification-delivery-fcm.mjs +100 -0
  38. package/src/notifications/notification-delivery-gateway.mjs +36 -0
  39. package/src/notifications/notification-delivery-http.mjs +53 -0
  40. package/src/notifications/notification-delivery-payload.mjs +45 -0
  41. package/src/notifications/notification-delivery.mjs +43 -0
  42. package/src/notifications/notification-gateway-command.mjs +48 -0
  43. package/src/notifications/notification-gateway-store.mjs +159 -0
  44. package/src/notifications/notification-gateway.mjs +181 -0
  45. package/src/notifications/notification-http.mjs +39 -0
  46. package/src/notifications/notification-input.mjs +82 -0
  47. package/src/notifications/notification-outbox.mjs +60 -0
  48. package/src/notifications/notification-preview-crypto.mjs +42 -0
  49. package/src/notifications/notification-preview-schema.mjs +7 -0
  50. package/src/notifications/notification-preview-store.mjs +63 -0
  51. package/src/notifications/notification-preview-text.mjs +20 -0
  52. package/src/notifications/notification-projection.mjs +20 -0
  53. package/src/notifications/notification-schema.mjs +22 -0
  54. package/src/notifications/notification-service.mjs +147 -0
  55. package/src/notifications/notification-store.mjs +132 -0
  56. package/src/pairing/pairing-qr-payload.mjs +32 -0
  57. package/src/pairing/pairing-qr.mjs +42 -0
  58. package/src/protocol/approval-protocol.mjs +39 -0
  59. package/src/protocol/protocol-validation.mjs +137 -0
  60. package/src/protocol/protocol.mjs +120 -0
  61. package/src/protocol/spark-chat-protocol.mjs +51 -0
  62. package/src/relay/relay-approvals.mjs +86 -0
  63. package/src/relay/relay-client-admission.mjs +40 -0
  64. package/src/relay/relay-correlations.mjs +101 -0
  65. package/src/relay/relay-model-catalog.mjs +69 -0
  66. package/src/relay/relay-request-adoption.mjs +97 -0
  67. package/src/relay/relay-requests.mjs +211 -0
  68. package/src/relay/relay-runtime-registration.mjs +49 -0
  69. package/src/relay/relay-session-mutations.mjs +122 -0
  70. package/src/relay/relay.mjs +222 -0
  71. package/src/relay/request-ownership-schema.mjs +18 -0
  72. package/src/relay/request-ownership-store.mjs +115 -0
  73. package/src/review/review-demo-isolation.mjs +132 -0
  74. package/src/review/review-demo-setup.mjs +97 -0
  75. package/src/review/review-portal-access.mjs +64 -0
  76. package/src/review/review-portal-form.mjs +24 -0
  77. package/src/review/review-portal-page.mjs +68 -0
  78. package/src/review/review-portal-server.mjs +105 -0
  79. package/src/setup/setup-files.mjs +37 -0
  80. package/src/setup/setup-questions.mjs +62 -0
  81. package/src/setup/setup-wizard.mjs +50 -0
  82. package/src/setup/setup.mjs +42 -0
  83. package/src/transport/http-api.mjs +32 -0
  84. package/src/transport/server.mjs +175 -0
  85. package/src/transport/transport-limits.mjs +85 -0
@@ -0,0 +1,177 @@
1
+ # Installation and first connection
2
+
3
+ ABot Socket is MIT-licensed and distributed as `@abot-ai/socket` on npm.
4
+ This guide covers package installation, server preparation and the first
5
+ runtime/app connection.
6
+
7
+ ## Install the command
8
+
9
+ Use Node.js 24 or later and a persistent npm installation owned by an unprivileged
10
+ account. Linux with systemd is the server deployment target; macOS can run the
11
+ foreground service and local development checks. The authentication store uses
12
+ POSIX ownership/permissions and does not support Windows server installation.
13
+
14
+ Install the command from the official npm registry:
15
+
16
+ ```sh
17
+ npm install --global @abot-ai/socket@0.2.0
18
+ abot-socket --version
19
+ abot-socket setup
20
+ ```
21
+
22
+ Prefer an explicit version for server upgrades. An installed package uses the
23
+ `abot-socket` command; it does not require a Git checkout or `npm ci`.
24
+ For an existing installation, retain its configuration and follow the
25
+ [state backup and schema upgrade guide](MIGRATION.md#durable-request-ownership-schema-upgrade)
26
+ before replacing the package. `setup` prepares a new configuration; it does not
27
+ migrate or overwrite an existing one.
28
+
29
+ Alternatively, prepare a tarball from the public source:
30
+
31
+ ```sh
32
+ git clone https://github.com/Dorshitrit/abot-socket.git
33
+ cd abot-socket
34
+ npm ci --omit=optional --ignore-scripts
35
+ npm pack --ignore-scripts --json > /tmp/abot-socket-pack.json
36
+ npm run check-package -- /tmp/abot-socket-pack.json
37
+ ```
38
+
39
+ Then install that tarball (or a verified local tarball you already have):
40
+
41
+ ```sh
42
+ npm install --global ./abot-ai-socket-0.2.0.tgz
43
+ abot-socket --version
44
+ abot-socket setup
45
+ ```
46
+
47
+ Contributor commands are separate in [Self-hosting](SELF-HOSTING.md#source-checkout-development).
48
+
49
+ Do not use an ephemeral npx installation for a persistent system service. npm
50
+ installation has no lifecycle install hooks and does not run the setup wizard,
51
+ request certificates, contact your runtime or change OS service settings.
52
+
53
+ ## Follow setup
54
+
55
+ The interactive wizard asks for:
56
+
57
+ 1. A new private installation directory; the default is `~/.abot-socket`.
58
+ 2. Your canonical HTTPS origin, for example `https://socket.example.net`.
59
+ 3. The runtime identity and a private loopback listener port (default 8788).
60
+ 4. On Linux as a non-root user: nginx, Apache or an existing HTTPS setup. Proxy
61
+ templates require paths to an existing full-chain certificate and private key.
62
+ 5. Confirmation before writing any installation files.
63
+
64
+ Setup creates a new directory with mode `0700` and private files with mode `0600`:
65
+
66
+ | Output | Purpose |
67
+ | --- | --- |
68
+ | `config.json` | HTTPS origin, listener port, state location and runtime grants containing token hashes. |
69
+ | `runtime-connection.json` | Bridge URL, runtime identity and secret; transfer only to the runtime computer. |
70
+ | `state/auth.sqlite3` | Pairing state, device credentials, notification metadata and pending delivery state. |
71
+ | `deployment/` | Reviewable Linux service/proxy plan, when selected in the interactive wizard. |
72
+
73
+ Setup defaults to Ask permission and port `8788`. Keep the installation outside
74
+ the package/source directory so package updates cannot replace its state.
75
+ No services are installed/started and certificate contents are never read.
76
+ Pairing details are displayed in the terminal, not stored as a plaintext file.
77
+ They expire after five minutes, so request a fresh bundle when HTTPS is ready.
78
+
79
+ The optional endpoint check at the end makes one bounded public HTTPS request only
80
+ if you explicitly choose it. A successful check proves relay health, not a working
81
+ runtime or model. Cancellation before file creation leaves no installation behind.
82
+
83
+ For unattended local file preparation, use explicit arguments instead of prompts:
84
+
85
+ ```sh
86
+ abot-socket setup --output /srv/abot-private \
87
+ --origin https://socket.example.net --runtime-id workstation --port 8788
88
+ ```
89
+
90
+ The parent directory must exist, and the output directory must not exist. Run as
91
+ the same unprivileged account that will operate the relay. The unattended command
92
+ creates the configuration/state only; use `deployment` separately for service files.
93
+ If state initialization fails after files were written, preserve the installation,
94
+ correct the private-state path/permissions and use `pair` to retry. Setup never
95
+ overwrites existing configuration or state.
96
+
97
+ ## Configure HTTPS and start the relay
98
+
99
+ Follow [Deployment](DEPLOYMENT.md) to review and explicitly apply the generated
100
+ systemd and proxy files. Supply DNS, a valid certificate and the required proxy
101
+ software yourself; neither npm nor setup obtains or installs them. Existing
102
+ services must be checked before applying the plan.
103
+
104
+ For foreground operation instead of a system service:
105
+
106
+ ```sh
107
+ abot-socket start
108
+ ```
109
+
110
+ Commands use `~/.abot-socket/config.json` by default. For a custom location:
111
+
112
+ ```sh
113
+ abot-socket start --config /srv/abot-private/config.json
114
+ abot-socket check-config --config /srv/abot-private/config.json
115
+ ```
116
+
117
+ `--config` overrides environment settings. Otherwise `ABOT_SOCKET_CONFIG` and its
118
+ legacy `SOCKET_V2_CONFIG` alias retain their existing precedence/conflict checks.
119
+ `setup` takes `--output` instead; it never overwrites an existing configuration.
120
+
121
+ ## Connect runtime, then pair the app
122
+
123
+ Privately transfer `runtime-connection.json` to the intended runtime computer.
124
+ Configure its existing bridge with the generated `agentBridgeUrl`, `runtimeId`
125
+ and `runtimeToken`, using that runtime version's documented configuration names.
126
+ The file is a credential handoff, not a replacement runtime configuration or a
127
+ second bridge process. This is separate from app pairing; the runtime secret
128
+ must never go into the app.
129
+ No runtime source changes are required by this installer.
130
+
131
+ Recoverable approvals and continuation of computer-created requests require a
132
+ compatible app and a runtime advertising `request_approvals_v1`. SPARK companion
133
+ chat requires `spark_chat_v1` and a configured SPARK model. The app must check
134
+ the selected runtime's capabilities and readiness, not just the Socket's global
135
+ flags. Earlier clients and runtimes retain their supported legacy chat path;
136
+ upgrading the Socket alone does not enable these runtime features.
137
+
138
+ ```sh
139
+ abot-socket doctor
140
+ abot-socket pair
141
+ ```
142
+
143
+ For custom configuration locations, add `--config` to those commands as above.
144
+ Doctor verifies TLS and the `/health` response, refuses redirects and has a five-second
145
+ total timeout. Its output always includes `runtimeVerified: false`. It never sends
146
+ credentials or generates a device session. The app must confirm that the selected
147
+ runtime is online and can complete a chat request.
148
+
149
+ In a normal terminal, `abot-socket pair` also displays a private QR for a scanner-capable
150
+ ABot App. Scan inside the app, check the displayed server address, and confirm.
151
+ The QR contains the same HTTPS URL, temporary pairing key and six-digit code; it
152
+ expires after five minutes and can be used once. Anyone with the QR can use that
153
+ pending pairing, so do not publish it or send it to a public QR service.
154
+
155
+ Generate a fresh QR **after** the service and HTTPS are ready; setup does not start
156
+ the server, and its initial pairing may expire while deployment is being completed.
157
+ The QR is printed locally, not saved to disk or served at a public endpoint.
158
+ Piped output keeps the existing text/JSON format. `TERM=dumb` or a narrow terminal
159
+ uses manual details instead; widen the terminal and run `pair` again if needed.
160
+ When running a remote command through SSH, allocate a terminal to display the QR.
161
+
162
+ Manual entry remains available for app versions without a scanner or when camera
163
+ access is unavailable: enter the new code, HTTPS URL and temporary pairing key. Afterwards,
164
+ `abot-socket devices` lists devices and `abot-socket revoke <device-id>` revokes one.
165
+ Both operate on the local private state without restarting the service.
166
+
167
+ Push delivery is optional and off by default. Ordinary chat and the notification
168
+ inbox work without it. See [push delivery configuration](SELF-HOSTING.md#optional-push-delivery)
169
+ for the difference between the Store application's gateway and a self-built app's
170
+ own Firebase sender.
171
+
172
+ ## Updates
173
+
174
+ Keep private configuration/state outside the npm package directory. Updating the
175
+ package must not replace that state. Regenerate the service plan if Node or the
176
+ package path changes, and restart the service explicitly during the approved update.
177
+ No automatic update, live migration or service restart is implemented here.
@@ -0,0 +1,84 @@
1
+ # Moving to pairing
2
+
3
+ Installing the package does not change existing services or applications. Use a
4
+ separately approved cutover after real-device verification.
5
+
6
+ The chat envelopes, runtime bridge and runtime credential grants remain compatible.
7
+ Authentication changes: the new client uses local-operator pairing instead of
8
+ email. Old in-memory sessions cannot be imported. Existing devices must pair to
9
+ the new service, receiving separate credentials bound to its HTTPS origin.
10
+
11
+ 1. Back up the current private configuration and deployed version.
12
+ 2. Prepare a separate candidate endpoint and private state with a unique test
13
+ runtime identity. Do not attach two active runtimes using the same ID.
14
+ 3. Issue pairing locally; verify pairing, restart persistence, runtime discovery,
15
+ history, chat, steering, approvals and revocation through the actual app.
16
+ 4. Perform the approved service/client cutover while retaining a rollback path.
17
+
18
+ The new configuration has `publicOrigin`, `port`, `stateDirectory`, and
19
+ `runtimeGrants`. Remove old `allowedEmail` and `mail` settings; they are rejected.
20
+ `ABOT_SOCKET_CONFIG` accepts `SOCKET_V2_CONFIG` as a path alias. No mail-token
21
+ variable is used. The existing runtime adapter needs configuration only, not
22
+ source changes. Setup generates new runtime credentials rather than rotating or
23
+ migrating an existing installation automatically.
24
+
25
+ After cutover, retain the private state directory through upgrades. Device
26
+ sessions persist there. Socket connections and transient subscriptions still
27
+ disappear on restart; durable request ownership is described below. Interrupted
28
+ requests must be reconciled against runtime state before retry.
29
+
30
+ ## Durable request ownership schema upgrade
31
+
32
+ Schema v4 adds a routing-ownership table to the existing private database.
33
+ Schemas v1, v2 and v3 upgrade transactionally, preserving owner identity, paired
34
+ devices, pending pairing, notification metadata, preview keys and delivery queues.
35
+ No new dependency or additional state file is required.
36
+
37
+ New durable requests record their runtime, session, request, owner and
38
+ initiating device identifiers, plus routing status and timestamps. A paired owner
39
+ device can also recover an unowned computer-created request: after a fresh
40
+ response from the authorized runtime verifies the exact session and request, the
41
+ Socket records that ownership with the adopting device. Existing ownership is
42
+ never reassigned to a different owner or session. There is no bulk import, and
43
+ historical events cannot establish ownership.
44
+
45
+ This table contains no conversation content, approval presentation, decision or
46
+ continuation. The runtime remains authoritative for pending approvals and decision
47
+ receipts. Recovery requires a compatible client and `request_approvals_v1` on the
48
+ selected runtime; legacy requests are not automatically converted during upgrade.
49
+
50
+ Ownership is persisted before forwarding a new request. Open or uncertain records
51
+ do not expire or get evicted; a full 512-record ledger rejects new admissions.
52
+ Only confirmed terminal records expire after 12 hours. Confirmed whole-session
53
+ deletion retires that session's records. Revoking the originating
54
+ device does not erase ownership: another paired device of the same owner can
55
+ still recover the request, but revoked credentials cannot connect or receive push.
56
+ An interrupted send is never automatically resent from this ledger.
57
+
58
+ Stop the service and preserve the private state directory and matching code before
59
+ upgrading. Older binaries reject schema v4. Rollback requires restoring the
60
+ matching pre-upgrade database snapshot and code while stopped; do not delete state
61
+ to make an old binary start. Reconcile device revocations and deliveries made
62
+ since that snapshot before resuming service. This source change alone does not
63
+ upgrade or restart a deployed service.
64
+
65
+
66
+ ## Notification preview schema upgrade
67
+
68
+ Schema v3 adds a device-scoped preview-key table and nullable encrypted-preview
69
+ column on the existing outbox. Both schema v1 and v2 upgrade transactionally;
70
+ paired-device tokens, pending pairing, owner identity, notification metadata and
71
+ generic delivery queues are preserved. Preview remains off until that device
72
+ explicitly provisions a key after push registration. No runtime changes are needed.
73
+
74
+ Stop the service and back up the private state directory and matching deployed
75
+ code before upgrading. Older binaries reject schema v3. Rollback requires restoring
76
+ the matching pre-upgrade database snapshot together with old code while stopped;
77
+ do not point an old binary at the migrated state or delete the database to make it
78
+ start. Restoring a snapshot also restores its older device/revocation/delivery state,
79
+ so reconcile changes made since that snapshot before resuming service.
80
+
81
+ For gateway delivery, upgrade the publisher gateway before clients enable previews.
82
+ Keep previews off until compatible client support is installed. Old clients and
83
+ opted-out devices retain the original generic push behavior. This source change
84
+ alone does not deploy or enable preview delivery on an existing installation.
@@ -0,0 +1,371 @@
1
+ # Protocol and compatibility
2
+
3
+ This candidate retains the Socket v2 chat transport contract and replaces email
4
+ authentication with operator-issued pairing. It does not change
5
+ runtime source or expose the complete runtime Web UI API. The `/health` value
6
+ `version: 2` identifies the protocol family, not a package release number.
7
+
8
+ ## HTTP
9
+
10
+ Use the configured HTTPS origin. JSON body limits depend on the route:
11
+
12
+ | Route | Maximum JSON request body |
13
+ | --- | --- |
14
+ | `POST /v2/pairing/redeem`, `POST /v2/notifications/read`, `POST /v2/notifications/presence` | 4 KiB (4096 bytes). |
15
+ | `PUT /v2/notifications/push` | 8 KiB (8192 bytes). |
16
+ | `PUT /v2/notifications/push/preview` | 512 bytes. |
17
+
18
+ These are application limits; a reverse proxy can impose a smaller limit. The
19
+ generated direct-proxy plans currently cap request bodies at 4 KiB. The separate
20
+ notification gateway accepts JSON bodies up to 8 KiB and needs its own proxy
21
+ configuration. Field validation still applies within each body limit.
22
+
23
+ Responses are `no-store`. Host must match the configured origin, and a supplied
24
+ browser Origin must match it exactly. The local proxy requirements apply to all
25
+ routes, including `/health`.
26
+
27
+ | Method and path | Request / response |
28
+ | --- | --- |
29
+ | `GET /health` | `{ok:true, version:2}`; no account or runtime details. |
30
+ | `POST /v2/pairing/redeem` | `{pairingId, code, deviceName}` → `{token, expiresAt, session:{id,subject,deviceName}}`. |
31
+ | `GET /v2/auth/sessions` | Bearer session token → `{sessions:[{id,deviceName,createdAt,expiresAt,current}]}`. |
32
+ | `DELETE /v2/auth/sessions/:id` | Bearer session token → `{revoked}`. |
33
+ | `POST /v2/auth/logout` | Bearer session token → `{ok:true}`. |
34
+ | `GET /v2/runtimes` | Bearer session token → `{runtimes:[...]}` with IDs, online state, model catalog, allowed modes, and capabilities. |
35
+
36
+ `pairingId` is a random 32-character base64url temporary key; `code` is exactly
37
+ six decimal digits, including leading zeros. Both are required. A bundle is
38
+ valid for five minutes and five failed attempts; consume and device issuance
39
+ share a database transaction. A retry after a lost success response cannot reuse
40
+ it: the operator must create a fresh bundle. Neither secret belongs in a URL.
41
+ The ordinary Socket API has no HTTP endpoint for generating pairing bundles.
42
+ The optional, separately enabled [app-review portal](APP-REVIEW.md) exposes an
43
+ authenticated `POST /review/pair` on its own listener/origin, bound to a dedicated
44
+ review installation. It is not part of the ordinary Socket API. Legacy email
45
+ endpoints are no longer supported.
46
+
47
+ ### Pairing QR, version 1
48
+
49
+ The operator CLI can encode the existing bundle as raw UTF-8 JSON in a QR:
50
+
51
+ ```json
52
+ {"type":"abot-pairing","version":1,"serverUrl":"https://socket.example.test","pairingId":"0123456789abcdefghijklmnopqrstuv","code":"000123","expiresAt":1800000300000}
53
+ ```
54
+
55
+ This example is illustrative, not a usable pairing. `expiresAt` is Unix time in
56
+ milliseconds; the server remains authoritative for validity. The code is a string
57
+ to preserve leading zeros. The QR contains only these six fields, never a runtime
58
+ token, permanent device token, configuration file, or secret-bearing URL/deep link.
59
+
60
+ Clients validate the type/version and field shapes, accept only a canonical HTTPS
61
+ origin, display the destination for confirmation, and then use the existing
62
+ `POST /v2/pairing/redeem`. Scanning alone must not contact the destination. Suppress
63
+ duplicate camera detections while confirmation or redemption is in progress.
64
+ The existing expiry, attempt limits and one-use transaction apply unchanged;
65
+ creating a new bundle replaces only the pending pairing, not existing devices.
66
+ The QR is a bearer enrollment secret, so it must not be retained in logs or sent
67
+ to external QR-generation services.
68
+
69
+ An expired, unknown, consumed or wrong pairing returns `invalid_code` / 401.
70
+ General HTTP limits and source/global code-verification limits return `rate_limited` / 429.
71
+ Verification quotas apply only after matching an unexpired, unlocked pending pairing
72
+ key; malformed, unknown, expired or already-locked pairing requests do not spend them.
73
+ A full device list
74
+ returns `device_limit` / 429; free a slot locally. Malformed device names return
75
+ `invalid_request` / 400 without consuming an otherwise valid pairing.
76
+
77
+ ## Native client WebSocket
78
+
79
+ Connect to `/v2/realtime` using `Authorization: Bearer <device-session-token>` in
80
+ the HTTP upgrade. Credentials are not accepted in URL query parameters. Native
81
+ clients should store the token in the platform's secure credential store.
82
+ Browser WebSocket APIs cannot set this header; browser support needs its own
83
+ designed authentication path.
84
+
85
+ On connection, the relay sends `authenticated` with the session expiry and
86
+ capability flags. Client commands use `{id, runtimeId, body}`. IDs must be unique
87
+ per connection. Supported body types are:
88
+
89
+ - `run_request`, `steer_request`, `tool_approval_response`;
90
+ - `request.approvals.request`, `request.approval.decide.request` for an owned request on a capable runtime;
91
+ - `session.list.request`, `session.snapshot.request`, `request.events.request`;
92
+ - `spark.chat.open.request` on a SPARK-capable runtime;
93
+ - `session.delete.request`, `session.messages.clear.request`, `session.message.delete.request`;
94
+ - `attachment.upload.request`, `attachment.fetch.request`;
95
+ - relay-local `subscribe_request` for a known request belonging to this owner.
96
+
97
+ The relay returns `relay.ack`, `relay.error`, `relay.response`, or `relay.event`
98
+ with the appropriate command ID, runtime ID, and body. An acknowledgement means
99
+ dispatched or queued, not completed. Runtime correlations belong to the relay;
100
+ the client cannot choose them. Unknown command fields and unsupported operations
101
+ are rejected rather than forwarded as a generic admin API.
102
+
103
+ Tool permission values are `ask`, `full_access`, and `full_plus`, constrained by
104
+ the runtime grant. Setup grants only `ask`. The model catalog and supported
105
+ reasoning controls come from the connected runtime, not a hardcoded model list.
106
+
107
+ ### SPARK companion chat
108
+
109
+ The Socket advertises `authenticated.capabilities.sparkChat`. Each runtime in
110
+ `/v2/runtimes` reports this capability only when online and advertising
111
+ `spark_chat_v1`. Clients must check the per-runtime capability; a runtime feature
112
+ alone does not establish support in an older Socket.
113
+
114
+ Send `{type:'spark.chat.open.request'}` inside the usual authenticated command
115
+ envelope. The client supplies no session, model, permission or correlation fields.
116
+ The correlated `spark.chat.open.response` contains `ok`, the canonical
117
+ `sessionId:'spark-companion'`, `modelProfileId` (or null), `toolPermissionMode` and
118
+ `readiness` (`ready`, `model_required`, or `model_unavailable`). Failure responses
119
+ contain a bounded public error code. Other runtime fields are not forwarded.
120
+
121
+ Opening creates or reuses the marked companion without starting a model request.
122
+ Load history and recover waiting approvals through the existing APIs. Send chat
123
+ through `run_request` with the returned session and permission mode; the runtime
124
+ selects its configured SPARK model. Opening can report a mode above the relay's
125
+ grant, but does not authorize it: the existing grant check rejects that send.
126
+ The runtime also rejects a mode changed since open with
127
+ `spark_chat_permission_mismatch`; refresh state and let the user retry, never
128
+ silently increase permissions or automatically resubmit an uncertain request.
129
+
130
+ Unsupported runtimes reject SPARK open/run with `spark_chat_unsupported`.
131
+ Correlations remain bound to the requesting client and exact runtime connection,
132
+ are consumed once, and expire after 30 seconds. This contract does not expose
133
+ SPARK settings writes, collection, jobs or autonomous activity.
134
+
135
+ ### Session mutations
136
+
137
+ Each runtime in `/v2/runtimes` and the WebSocket `authenticated` frame includes
138
+ `capabilities.sessionMutation: true` for these three relay operations. Clients must treat an
139
+ absent/false flag as unsupported and require explicit user confirmation.
140
+
141
+ | Request body | Response body (inside `relay.response`) |
142
+ | --- | --- |
143
+ | `{type:'session.delete.request', sessionId}` | `{type:'session.delete.response', ok, sessionId, deleted, deletedMessages, deletedRequests}` |
144
+ | `{type:'session.messages.clear.request', sessionId}` | `{type:'session.messages.clear.response', ok, sessionId, cleared, deletedMessages, deletedRequests, updatedAt}` |
145
+ | `{type:'session.message.delete.request', sessionId, messageId}` | `{type:'session.message.delete.response', ok, sessionId, messageId, deleted, updatedAt, deletedRequestId?, deletedRequestEvents?}` |
146
+
147
+ Use canonical message IDs from a fresh runtime snapshot, not local UI IDs.
148
+ Numeric message IDs are normalized to strings. Only `ok:true` together with
149
+ `deleted:true` / `cleared:true` confirms a change; reload canonical history after
150
+ the result. The runtime's single-message delete removes only that message row,
151
+ plus its associated request replay/context and memory checkpoints; sibling
152
+ message rows remain. Session deletion/clearing follows the existing runtime's
153
+ history and attachment cleanup semantics.
154
+
155
+ The relay rejects observed active/uncertain requests and in-flight uploads with
156
+ `session_busy`. Before forwarding a mutation it reads a complete snapshot with
157
+ `includeRequests:true`; active requests prevent deletion, and an incomplete or
158
+ unrecognized snapshot returns `session_state_unavailable`. No snapshot contents
159
+ are added to the mutation response. A pending mutation blocks another mutation,
160
+ new run or upload in that session (`session_mutation_pending`). It retains this
161
+ guard after client disconnect until the outcome or the shared 30-second timeout.
162
+ A client disconnected during the read check cannot begin deletion.
163
+
164
+ This check is not an atomic lock over independent Web UI clients. A request
165
+ started directly at the runtime after the check can race with deletion; the
166
+ runtime fences deleted generations and that request may be cancelled. Wait for
167
+ work on all clients to finish. `outcome_unknown` never authorizes an automatic
168
+ retry: reload authoritative history first. Missing sessions report
169
+ `session_not_found`; runtime failures remain failures.
170
+
171
+ ## Runtime bridge
172
+
173
+ Connect to `/agent-bridge` with the separate runtime Bearer token, then send the
174
+ existing `{type:'register', agentId, features?, modelCatalog?}` message. The
175
+ registered ID must match the credential's grant. A second connection cannot
176
+ replace an already connected runtime with the same ID. Client device credentials
177
+ cannot authenticate this role.
178
+
179
+ ## Recovery and capability limits
180
+
181
+ - A timeout or disconnect can return `outcome_unknown`; do not retry commands
182
+ automatically. Read canonical runtime history/replay to reconcile the result;
183
+ the durable decision retry rules below apply only to that decision, never a run.
184
+ - Live `eventSequence` and replay `seqNo` are separate counters. `afterSeq` uses
185
+ the persisted replay counter only.
186
+ - Legacy `tool_approval_response` requires a live observed pending approval for
187
+ the correct owner, runtime and request. Positive approval expires after five
188
+ minutes; an explicit decline can remain possible once on that same active
189
+ runtime connection and request. Historical replay never creates authority.
190
+ - `cancel` remains false. Restart, cron, configuration, and generic administration
191
+ are not exposed.
192
+ - The relay keeps bounded routing state and persistent durable-request ownership
193
+ metadata. It is not an offline queue, conversation store, or exactly-once
194
+ execution service.
195
+
196
+ ### Durable approval recovery
197
+
198
+ The WebSocket `authenticated.capabilities.durableApprovals` flag describes
199
+ whether the Socket has the persistent ownership store required by this API.
200
+ Each `/v2/runtimes` entry reports the flag separately: it is true only while that
201
+ runtime is online and advertises `request_approvals_v1`. Clients must check the
202
+ chosen runtime, not infer its support from the global flag. Older runtimes remain
203
+ usable through the legacy path.
204
+
205
+ To opt in, a new `run_request` includes `approvalMode:'durable_v1'`. Omitting it
206
+ retains legacy behavior. The relay persists its owner, originating device,
207
+ runtime, session and request identity before forwarding the run. It does not
208
+ persist the prompt, approval payload, decision or runtime continuation. An
209
+ unavailable capability rejects the opt-in instead of silently downgrading it.
210
+
211
+ | Request body | Response body inside `relay.response` |
212
+ | --- | --- |
213
+ | `{type:'request.approvals.request', sessionId, requestId}` | `{type:'request.approvals.response', correlationId, sessionId, requestId, ok:true, approvals:[{request,wait?}], lifecycle, finalState}` |
214
+ | `{type:'request.approval.decide.request', sessionId, requestId, approvalId, approved, reason?, generation?, waitId?, revision?, commandId?}` | `{type:'request.approval.decide.response', correlationId, sessionId, requestId, ok:true, result:{accepted,duplicate?,reason?}}` |
215
+
216
+ Runtime failures use `ok:false,error` with the same scope. The relay assigns the
217
+ private correlation; clients correlate the outer command `id`. Only a response
218
+ from the current runtime connection with matching request, session and correlation
219
+ is accepted. One approval query/decision per request may be in flight at a time.
220
+
221
+ Recovery requires a durable ownership record admitted by this Socket. When an
222
+ authorized paired device queries approvals for a request created directly on
223
+ its runtime, the relay can establish that record after a fresh, privately
224
+ correlated snapshot from the current runtime connection confirms the exact
225
+ session and request. Client-provided history or a replayed
226
+ `tool.approval.required` event cannot establish ownership. An existing record
227
+ for another owner or session is never reassigned. A decision alone cannot
228
+ establish ownership; query the current approvals first.
229
+ After a Socket or runtime restart, an authenticated device belonging to the same
230
+ installation owner may recover the request; revoking the originating device does
231
+ not transfer its credential or notification inbox. Recovery also restores live
232
+ delivery for that request. It does not rerun it or reconstruct missed events.
233
+
234
+ The runtime remains authoritative for pending approvals and their lifecycle.
235
+ Durable waits do not inherit the relay's legacy five-minute approval timeout.
236
+ For a returned `wait`, send its exact `generation`, `waitId` and `revision` together
237
+ with a stable client-generated `commandId`. All four fields are required together;
238
+ omit all four only for a legacy approval returned by the runtime. Do not substitute
239
+ new wait identities when retrying an uncertain decision. Reuse the original
240
+ decision and `commandId` so the runtime can recognize its durable receipt, then
241
+ reload canonical state. Accepted duplicate decisions must not execute the action
242
+ again. A declined or stale decision remains a refusal, not an invitation to retry
243
+ against a newer approval.
244
+
245
+ Ownership storage is capped at 512 records. Open records are not age-evicted;
246
+ confirmed terminal records are retained for 12 hours, or removed after confirmed
247
+ deletion of their complete session. Capacity exhaustion rejects new admission.
248
+
249
+ Use the new pairing-capable client. Email-only personal builds cannot enroll
250
+ through this authentication contract. Clients and runtime adapters that omit the
251
+ durable approval opt-in retain legacy behavior.
252
+
253
+ ## Notifications
254
+
255
+ `capabilities.notifications: true` advertises the authenticated notification API.
256
+ Notifications are owned by the **paired device that initiated the request**, or
257
+ by the **device that first adopts an unowned computer-created request** through
258
+ the verified recovery flow above. Ownership does not follow the current
259
+ WebSocket connection: later recovery by another paired device does not transfer
260
+ the notification inbox. Other paired devices cannot list or mark that device's
261
+ records. There are no new unsolicited WebSocket frame types; clients poll the
262
+ inbox while foregrounded and reconcile on resume or push tap.
263
+
264
+ | Method and path | Request / response |
265
+ | --- | --- |
266
+ | `GET /v2/notifications?limit=50&before=<cursor>` | Device inbox, descending time; limit 1–100; opaque optional cursor. |
267
+ | `POST /v2/notifications/read` | `{ids:[...]}` (maximum 100) → `{ok:true}`. Unknown or foreign IDs have no effect. |
268
+ | `POST /v2/notifications/presence` | `{foreground:boolean,runtimeId?,sessionId?}` → `{ok:true}`; a session requires its runtime. |
269
+ | `PUT /v2/notifications/push` | `{provider:'fcm',token}` or `{provider:'gateway',endpointId,sendToken}` → `{ok:true}`. |
270
+ | `DELETE /v2/notifications/push` | Remove this device's push registration, preview key and queued deliveries → `{ok:true}`. |
271
+ | `PUT /v2/notifications/push/preview` | Exact `{keyId,key}` → `{ok:true}`; opt in after registering push. Replaces the device's key. |
272
+ | `DELETE /v2/notifications/push/preview` | Remove this device's preview key and queued ciphertext, retaining generic deliveries → `{ok:true}`. |
273
+
274
+ The inbox response is `{version:1,items,nextCursor,unreadCount,unreadByRuntime,
275
+ unreadBySession,delivery,registration,preview}`. Each item is
276
+ `{id,runtimeId,sessionId,requestId,kind,createdAt,readAt,expiresAt}`; timestamps
277
+ are epoch milliseconds, and `readAt` is null until read. `kind` is `completed`,
278
+ `failed` or `approval_required`. `unreadByRuntime` maps runtime IDs to counts;
279
+ `unreadBySession` maps runtime IDs to session-ID/count maps. `delivery` is
280
+ `{mode:'disabled'|'fcm'|'gateway',configured:boolean}`, and `registration` is
281
+ `{enabled:boolean}`. `preview` is `{supported:true,enabled:boolean,keyId:string|null}`.
282
+ Older servers omit `preview`; clients treat that as unsupported and off. Keys
283
+ and registration credentials are never returned by this API.
284
+
285
+ Foreground presence lasts 45 seconds. Renew while active; `foreground:false`
286
+ clears it. Presence is advisory: the phone may be suspended before its background
287
+ update arrives, so presence alone never marks records read or suppresses delivery.
288
+ Only an explicit authenticated read acknowledgement consumes records and cancels
289
+ queued deliveries. Clients acknowledge fetched IDs after showing the conversation.
290
+ Counts share the same ledger; they are not runtime transcript read receipts.
291
+ Push taps resolve the notification ID through this authenticated inbox
292
+ before opening a conversation; a push payload grants no execution or approval authority.
293
+
294
+ Only validated live `completed`, `failed` and `tool.approval.required` frames for
295
+ known relay requests create records. Streaming/progress events and replay do not.
296
+ Granted/rejected/terminal events cancel pending approval deliveries. Approval
297
+ notifications expire after five minutes; other records after at most seven days
298
+ or device credential expiry. Storage is capped at 500 records per device.
299
+ Expiration, logout and revocation remove registrations and pending deliveries.
300
+ The outbox retries at most five times, with bounded backoff. Duplicate source
301
+ events are deduplicated, but provider delivery is not an exactly-once guarantee.
302
+
303
+ The inbox and queued deliveries survive relay restarts. Live subscriptions must
304
+ be restored explicitly; durable ownership permits the scoped recovery described
305
+ above. This is not a complete runtime-wide event subscription or an offline
306
+ execution queue. Work started outside the relay has no automatic notification
307
+ subscription. After an existing computer request is verified and adopted, later
308
+ validated live events for that request can create notifications for its adopting
309
+ device; recovery does not backfill missed notifications. Never interpret a missing
310
+ notification as a failed request or auto-retry it.
311
+
312
+ Push is disabled unless private operator configuration includes
313
+ `notifications.delivery`. A self-built client uses
314
+ `{type:'fcm',serviceAccountFile:'./private-sender.json'}`; the path is resolved
315
+ relative to the private socket configuration, and sender credentials stay on
316
+ that operator's server. The client must belong to the same configured Firebase
317
+ project. Official builds can use `{type:'gateway',gatewayOrigin:'https://<trusted-host>'}`
318
+ with a separately enrolled endpoint. Clients cannot choose a gateway URL or send
319
+ provider credentials through the registration API. A missing or mismatched
320
+ configured transport returns `409 push_unavailable`; ordinary chat and the inbox
321
+ continue to work with delivery disabled.
322
+
323
+
324
+ ### Optional encrypted answer previews
325
+
326
+ Previews are off by default and enabled per paired device, only after an active
327
+ push registration exists. The app generates a random 32-byte AES-256-GCM key and
328
+ 16-byte key ID, sends both over authenticated HTTPS to its own Socket, and enables
329
+ local display only after acknowledgement. Both fields use canonical unpadded
330
+ base64url. No email, additional identity provider, or runtime change is involved.
331
+ Provider-token rotation preserves the preview setting. Key replacement clears
332
+ pending ciphertext. Disabling previews clears the key and ciphertext while leaving
333
+ the generic queue intact; unregistering push removes the entire delivery queue.
334
+ In-flight sends are aborted where possible, but provider-accepted notifications
335
+ cannot be recalled. The client must discard its key immediately on disable/logout.
336
+
337
+ Only a nonempty final `completed.output` is eligible. The Socket takes a bounded
338
+ prefix, removes unsafe controls and bidi override characters, and truncates on a
339
+ Unicode scalar boundary to at most 1024 UTF-8 bytes, including an ellipsis when
340
+ needed. It is an excerpt, not secret redaction. Approval, failure and progress
341
+ content is never included. No plaintext preview is stored in the inbox or outbox.
342
+ The inbox remains metadata-only; encrypted content exists only in the pending
343
+ outbox and is removed on delivery, read acknowledgement, revocation or expiry.
344
+
345
+ The exact encrypted envelope is `{v:1,keyId,nonce,ciphertext,expiresAt}`. Nonce is
346
+ 12 random bytes. Ciphertext contains encrypted UTF-8 followed by the 16-byte GCM
347
+ authentication tag (at most 1040 bytes), using canonical unpadded base64url.
348
+ Authenticated additional data is UTF-8, with literal newline separators:
349
+
350
+ ```text
351
+ ABot push preview v1
352
+ <keyId>
353
+ <notificationId>
354
+ <expiresAt decimal epoch milliseconds>
355
+ ```
356
+
357
+ The expiry equals notification expiry, capped by the device credential and seven
358
+ days. The device rejects an unknown key, altered ID/expiry/content, expired data
359
+ or malformed envelope and retains a generic alert. The gateway receives neither
360
+ the key nor plaintext. Its `/v1/deliver` accepts an optional strictly validated
361
+ `preview` field alongside the existing fixed delivery fields. Upgrade the gateway
362
+ before enabling previews; older gateways do not accept this field.
363
+
364
+ Encrypted FCM sends have no top-level `notification` or `android.notification`.
365
+ Their string-valued data is `notificationId`, `abotPreview` (envelope JSON),
366
+ `abotKind` (`completed`), `abotExpiresAt` and `abotBadge`. Android renders the
367
+ notification locally. APNs retains a generic alert and sets `mutable-content:1`
368
+ for the app's notification-service extension. Generic sends keep their existing
369
+ payload. Serialized provider envelopes are bounded to 4096 UTF-8 bytes. Foreground
370
+ display and navigation continue to use the authenticated inbox; push cannot grant
371
+ access or approval authority.