@abot-ai/socket 0.1.0 → 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-11-14
4
+
5
+ ### Added
6
+
7
+ - Recover pending tool approvals after client reconnects and Socket or runtime
8
+ restarts, using persistent request ownership and runtime-authoritative waits.
9
+ - Continue and approve requests started on the computer from a paired client,
10
+ after the current runtime confirms the exact session and request. Existing
11
+ ownership is never reassigned to another owner or session.
12
+ - Open and chat with the runtime's SPARK companion through compatible clients,
13
+ using its configured model and permissions. This does not expose SPARK
14
+ management, settings writes or proactive activity.
15
+
16
+ ### Fixed
17
+
18
+ - Restore scoped live request delivery when recovering an owned request, without
19
+ rerunning it or reconstructing events missed during disconnection.
20
+
21
+ ### Upgrade notes
22
+
23
+ - New features require a compatible client and the selected runtime's
24
+ `request_approvals_v1` or `spark_chat_v1` capability. Legacy chat and live-only
25
+ approval behavior remain available when the new capabilities are not used.
26
+ - Private SQLite state upgrades transactionally from schema v1, v2 or v3 to v4.
27
+ Existing pairing, device credentials, notification metadata, preview keys and
28
+ delivery queues are preserved. Stop the service and back up its private state
29
+ and matching code before upgrading; rollback requires both the pre-upgrade
30
+ state and old code. See [Migration](docs/MIGRATION.md).
31
+ - Node.js 24 or later remains required. Dependencies and the HTTP/WebSocket
32
+ protocol envelope are unchanged.
33
+
34
+ ## 0.1.0 - 2026-11-11
35
+
36
+ - Initial MIT-licensed npm release with authenticated runtime relay, QR/manual
37
+ pairing, device revocation, notification delivery and optional encrypted
38
+ previews, and CLI setup/deployment guidance.
package/README.md CHANGED
@@ -6,6 +6,7 @@ computers. The relay does not require an ABot-hosted account or backend.
6
6
 
7
7
  The source is available under the [MIT license](LICENSE). Install the versioned
8
8
  `@abot-ai/socket` package from npm, or build a local tarball from source.
9
+ See the [changelog](CHANGELOG.md) for release notes and upgrade requirements.
9
10
 
10
11
  ## Install and prepare
11
12
 
@@ -14,7 +15,7 @@ guide for installing the command, preparing a server, connecting a runtime and
14
15
  pairing the app by QR or manual entry.
15
16
 
16
17
  ```sh
17
- npm install --global @abot-ai/socket@0.1.0
18
+ npm install --global @abot-ai/socket@0.2.0
18
19
  abot-socket setup
19
20
  ```
20
21
 
@@ -49,27 +50,39 @@ ABot App scans the private pairing QR and asks you to confirm its server address
49
50
  Manual code, HTTPS URL and temporary-key entry remains available. The app receives
50
51
  its own device credential; never copy the runtime credential into the app.
51
52
 
52
- The relay supports chat, steering, observed tool approvals, runtime history,
53
- model selection and attachments through the existing bridge. It does not execute
54
- models or store conversation history. Runtime configuration uses the existing
55
- bridge adapter; runtime source changes are not part of this project.
53
+ The relay supports chat, steering, tool approvals, runtime history, model selection
54
+ and attachments through the runtime Bridge. Compatible clients can recover pending
55
+ approvals across reconnects and restarts, continue requests created on their
56
+ computer, and open the runtime's SPARK companion chat. SPARK model settings and
57
+ permissions remain runtime-owned; management and proactive activity are not exposed.
58
+ These features require the selected runtime to advertise `request_approvals_v1`
59
+ or `spark_chat_v1` respectively; older runtimes retain the legacy chat path.
60
+ The relay does not execute models or store conversation history. Configure the
61
+ runtime's existing Bridge; this installer does not modify runtime source.
56
62
 
57
63
  Device sessions last up to 30 days without sliding renewal and survive restarts
58
64
  in a private SQLite database. Operators can list and revoke devices locally.
59
65
  The device-scoped notification inbox records completion, failure and observed
60
- approval requests initiated through this relay. Push delivery is off by default;
66
+ approval events for routed requests. Notifications belong to the initiating device
67
+ or the device that first recovers a computer-created request through the verified
68
+ ownership flow; later recovery by another device does not transfer that inbox.
69
+ Push delivery is off by default;
61
70
  operators may configure their own FCM sender or an explicitly trusted ABot push
62
71
  gateway. Notifications are generic by default. An optional per-device encrypted
63
72
  preview can show a bounded final-answer excerpt; the gateway/provider never gets
64
73
  the preview key or plaintext. The app remains usable without push.
65
- Cancellation, durable approval restoration and recovery of events missed while
66
- the relay/runtime is disconnected are not implemented. The relay host is trusted and can read routed content;
67
- transport encryption is not end-to-end encryption.
68
-
69
- Notification metadata and pending deliveries share the private SQLite state.
70
- Upgrading migrates schema v1 or v2 to v3 without replacing paired devices. Back up the
71
- private state before deploying: an older binary cannot open the new schema.
72
- See [notification protocol and delivery](docs/PROTOCOL.md#notifications).
74
+ Cancellation and reconstruction of events missed while disconnected are not
75
+ implemented. Approval recovery queries current runtime state and restores scoped
76
+ live delivery; it never automatically resends an interrupted request. The relay
77
+ host is trusted and can read routed content; transport encryption is not end-to-end
78
+ encryption.
79
+
80
+ Notification metadata, pending deliveries and durable request ownership share the
81
+ private SQLite state. Upgrading migrates schema v1, v2 or v3 to v4 without replacing
82
+ paired devices. Back up the private state and matching code while stopped before
83
+ upgrading. Older binaries cannot open v4; rollback requires the matching state
84
+ backup and old code together. See [migration](docs/MIGRATION.md#durable-request-ownership-schema-upgrade)
85
+ and [notification protocol and delivery](docs/PROTOCOL.md#notifications).
73
86
 
74
87
  ## Source layout
75
88
 
@@ -14,13 +14,17 @@ POSIX ownership/permissions and does not support Windows server installation.
14
14
  Install the command from the official npm registry:
15
15
 
16
16
  ```sh
17
- npm install --global @abot-ai/socket@0.1.0
17
+ npm install --global @abot-ai/socket@0.2.0
18
18
  abot-socket --version
19
19
  abot-socket setup
20
20
  ```
21
21
 
22
22
  Prefer an explicit version for server upgrades. An installed package uses the
23
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.
24
28
 
25
29
  Alternatively, prepare a tarball from the public source:
26
30
 
@@ -35,7 +39,7 @@ npm run check-package -- /tmp/abot-socket-pack.json
35
39
  Then install that tarball (or a verified local tarball you already have):
36
40
 
37
41
  ```sh
38
- npm install --global ./abot-ai-socket-0.1.0.tgz
42
+ npm install --global ./abot-ai-socket-0.2.0.tgz
39
43
  abot-socket --version
40
44
  abot-socket setup
41
45
  ```
@@ -124,6 +128,13 @@ second bridge process. This is separate from app pairing; the runtime secret
124
128
  must never go into the app.
125
129
  No runtime source changes are required by this installer.
126
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
+
127
138
  ```sh
128
139
  abot-socket doctor
129
140
  abot-socket pair
package/docs/MIGRATION.md CHANGED
@@ -23,8 +23,44 @@ source changes. Setup generates new runtime credentials rather than rotating or
23
23
  migrating an existing installation automatically.
24
24
 
25
25
  After cutover, retain the private state directory through upgrades. Device
26
- sessions persist there; transient relay routes still disappear on restart.
27
- Interrupted requests must be reconciled against runtime history before retry.
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.
28
64
 
29
65
 
30
66
  ## Notification preview schema upgrade
package/docs/PROTOCOL.md CHANGED
@@ -87,7 +87,9 @@ capability flags. Client commands use `{id, runtimeId, body}`. IDs must be uniqu
87
87
  per connection. Supported body types are:
88
88
 
89
89
  - `run_request`, `steer_request`, `tool_approval_response`;
90
+ - `request.approvals.request`, `request.approval.decide.request` for an owned request on a capable runtime;
90
91
  - `session.list.request`, `session.snapshot.request`, `request.events.request`;
92
+ - `spark.chat.open.request` on a SPARK-capable runtime;
91
93
  - `session.delete.request`, `session.messages.clear.request`, `session.message.delete.request`;
92
94
  - `attachment.upload.request`, `attachment.fetch.request`;
93
95
  - relay-local `subscribe_request` for a known request belonging to this owner.
@@ -102,6 +104,34 @@ Tool permission values are `ask`, `full_access`, and `full_plus`, constrained by
102
104
  the runtime grant. Setup grants only `ask`. The model catalog and supported
103
105
  reasoning controls come from the connected runtime, not a hardcoded model list.
104
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
+
105
135
  ### Session mutations
106
136
 
107
137
  Each runtime in `/v2/runtimes` and the WebSocket `authenticated` frame includes
@@ -149,28 +179,87 @@ cannot authenticate this role.
149
179
  ## Recovery and capability limits
150
180
 
151
181
  - A timeout or disconnect can return `outcome_unknown`; do not retry commands
152
- automatically. Read canonical runtime history/replay to reconcile the result.
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.
153
184
  - Live `eventSequence` and replay `seqNo` are separate counters. `afterSeq` uses
154
185
  the persisted replay counter only.
155
- - Approval authority requires an observed pending approval for the correct
156
- runtime and request. Historical replay alone does not restore that authority.
157
- Positive approval expires after five minutes; an explicit decline can remain
158
- possible once on that same active runtime connection and request.
159
- - `cancel` and `durableApprovals` are advertised as false. Restart, cron,
160
- configuration, and generic administration are not exposed.
161
- - The relay keeps bounded temporary routing state. It is not an offline queue,
162
- durable conversation store, or exactly-once execution service.
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.
163
248
 
164
249
  Use the new pairing-capable client. Email-only personal builds cannot enroll
165
- through this authentication contract. Existing runtime adapters are unchanged.
250
+ through this authentication contract. Clients and runtime adapters that omit the
251
+ durable approval opt-in retain legacy behavior.
166
252
 
167
253
  ## Notifications
168
254
 
169
255
  `capabilities.notifications: true` advertises the authenticated notification API.
170
- Notifications are owned by the **paired device that initiated the request**, not
171
- by its current WebSocket connection. Other paired devices cannot list or mark
172
- that device's records. There are no new unsolicited WebSocket frame types;
173
- clients poll the inbox while foregrounded and reconcile on resume or push tap.
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.
174
263
 
175
264
  | Method and path | Request / response |
176
265
  | --- | --- |
@@ -211,10 +300,14 @@ Expiration, logout and revocation remove registrations and pending deliveries.
211
300
  The outbox retries at most five times, with bounded backoff. Duplicate source
212
301
  events are deduplicated, but provider delivery is not an exactly-once guarantee.
213
302
 
214
- The inbox and queued deliveries survive relay restarts. In-flight request routing
215
- does not: this is not a complete runtime-wide event subscription or an offline
216
- execution queue. Web UI, schedules and autonomous SPARK events are outside this
217
- phase. Never interpret a missing notification as a failed request or auto-retry it.
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.
218
311
 
219
312
  Push is disabled unless private operator configuration includes
220
313
  `notifications.delivery`. A self-built client uses
package/docs/SECURITY.md CHANGED
@@ -60,10 +60,28 @@ and rejects unsupported operations. These controls do not make a compromised
60
60
  authorized device harmless: it can act within its owner's configured grants.
61
61
  Revoking that device session removes its relay access.
62
62
 
63
+ Paired owner devices can continue requests started directly on an authorized
64
+ runtime. Before admitting an unowned request for approval recovery, the relay
65
+ verifies its exact session/request identity through a fresh response from that
66
+ runtime connection. It never replaces an existing ownership binding for a
67
+ different owner or session. The runtime still validates every approval decision
68
+ against its current wait; a session snapshot is not approval authority.
69
+
63
70
  The relay does not automatically retry uncertain operations, restore approval
64
71
  authority from historical events, or provide exactly-once execution. After a
65
72
  disconnect, reconcile against runtime history before resubmission.
66
73
 
74
+ ## SPARK chat boundary
75
+
76
+ SPARK chat uses the same paired-device runtime access and tool-permission ceiling
77
+ as ordinary chat. Opening it exposes only the canonical session, selected model,
78
+ permission mode and model readiness, never the full preferences or internal
79
+ diagnostics. Runtime support is required before dispatch. The runtime remains
80
+ responsible for its marked companion and rereads its policy at every new send;
81
+ the relay never caches that policy as permission to execute. Existing approval
82
+ ownership and recovery checks remain in force. SPARK configuration and autonomous
83
+ activity are outside this API.
84
+
67
85
  ## Push preview boundary
68
86
 
69
87
  Answer previews are opt-in per paired device and off by default. The already
@@ -131,8 +131,15 @@ Back up the private configuration, runtime credential and state directory using
131
131
  a consistent filesystem/database snapshot or with the service stopped. Losing
132
132
  state requires pairing devices again. Restoring an old backup may restore old
133
133
  revocation state; reapply necessary revocations. Conversation history remains in
134
- each runtime. Temporary relay routes do not survive restart, so reconcile
135
- interrupted requests with runtime history rather than automatically resending.
134
+ each runtime. Temporary relay routes do not survive restart. Durable request
135
+ ownership does: a compatible app can query current approvals on a runtime
136
+ advertising `request_approvals_v1` and restore live delivery for that request.
137
+ This does not reconstruct missed events or resend interrupted work; reconcile
138
+ against current runtime state before retrying.
139
+
140
+ Version 0.2.0 uses state schema v4. See the
141
+ [upgrade and rollback procedure](MIGRATION.md#durable-request-ownership-schema-upgrade)
142
+ before upgrading; older binaries require the matching pre-upgrade state backup.
136
143
 
137
144
  The package does not provision a public TLS endpoint or install a service.
138
145
  Real-device, proxy and runtime verification remains separate from automated tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abot-ai/socket",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -50,6 +50,7 @@
50
50
  "src/setup/*.mjs",
51
51
  "src/transport/*.mjs",
52
52
  "README.md",
53
+ "CHANGELOG.md",
53
54
  "LICENSE",
54
55
  "config.example.json",
55
56
  "docs/SELF-HOSTING.md",
@@ -3,9 +3,10 @@ import { DatabaseSync } from 'node:sqlite';
3
3
  import { assertSameDatabaseFile, prepareAuthenticationDatabase } from './auth-storage-path.mjs';
4
4
  import { NOTIFICATION_SCHEMA } from '../notifications/notification-schema.mjs';
5
5
  import { NOTIFICATION_PREVIEW_SCHEMA } from '../notifications/notification-preview-schema.mjs';
6
+ import { REQUEST_OWNERSHIP_SCHEMA } from '../relay/request-ownership-schema.mjs';
6
7
 
7
8
  const APPLICATION_ID = 0x41425341;
8
- const SCHEMA_VERSION = 3;
9
+ const SCHEMA_VERSION = 4;
9
10
 
10
11
  const SCHEMA = `
11
12
  CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT;
@@ -81,9 +82,10 @@ export class AuthDatabase {
81
82
  const applicationId = this.prepare('PRAGMA application_id').get().application_id;
82
83
  const version = this.prepare('PRAGMA user_version').get().user_version;
83
84
  if (applicationId === APPLICATION_ID && version === SCHEMA_VERSION) return;
84
- if (applicationId === APPLICATION_ID && [1, 2].includes(version)) {
85
+ if (applicationId === APPLICATION_ID && [1, 2, 3].includes(version)) {
85
86
  if (version === 1) this.#connection.exec(NOTIFICATION_SCHEMA);
86
- this.#connection.exec(NOTIFICATION_PREVIEW_SCHEMA);
87
+ if (version <= 2) this.#connection.exec(NOTIFICATION_PREVIEW_SCHEMA);
88
+ this.#connection.exec(REQUEST_OWNERSHIP_SCHEMA);
87
89
  this.#connection.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
88
90
  return;
89
91
  }
@@ -94,6 +96,7 @@ export class AuthDatabase {
94
96
  this.#connection.exec(SCHEMA);
95
97
  this.#connection.exec(NOTIFICATION_SCHEMA);
96
98
  this.#connection.exec(NOTIFICATION_PREVIEW_SCHEMA);
99
+ this.#connection.exec(REQUEST_OWNERSHIP_SCHEMA);
97
100
  const insert = this.prepare('INSERT INTO metadata(key, value) VALUES (?, ?)');
98
101
  insert.run('owner_id', randomUUID());
99
102
  insert.run('token_salt', randomBytes(32).toString('hex'));
@@ -1,4 +1,14 @@
1
+ import { supportsSparkChat } from '../protocol/spark-chat-protocol.mjs';
2
+
1
3
  /** Socket-owned API support; runtime identity/features remain separately scoped. */
2
- export function clientCapabilities() {
3
- return { cancel: false, durableApprovals: false, sessionMutation: true, notifications: true };
4
+ export function clientCapabilities({ ownership, runtime } = {}) {
5
+ return { cancel: false, durableApprovals: supportsDurableApprovals(ownership, runtime),
6
+ sessionMutation: true, notifications: true, sparkChat: supportsSparkChat(runtime) };
7
+ }
8
+
9
+ function supportsDurableApprovals(ownership, runtime) {
10
+ if (!ownership) return false;
11
+ if (runtime === undefined) return true;
12
+ if (runtime.online !== true) return false;
13
+ return Array.isArray(runtime.features) && runtime.features.includes('request_approvals_v1');
4
14
  }
@@ -0,0 +1,39 @@
1
+ import { MAX_TEXT_BYTES, approvalId, boolean, fail, id, integer, optional, string } from './protocol-validation.mjs';
2
+
3
+ export const APPROVAL_FIELDS = Object.freeze({
4
+ 'request.approvals.request': ['sessionId', 'requestId'],
5
+ 'request.approval.decide.request': ['sessionId', 'requestId', 'approvalId', 'approved', 'reason',
6
+ 'generation', 'waitId', 'revision', 'commandId'],
7
+ });
8
+
9
+ export const APPROVAL_RESPONSES = Object.freeze({
10
+ 'request.approvals.request': 'request.approvals.response',
11
+ 'request.approval.decide.request': 'request.approval.decide.response',
12
+ });
13
+
14
+ const WAIT_FIELDS = ['generation', 'waitId', 'revision', 'commandId'];
15
+
16
+ export function isApprovalCommand(type) {
17
+ return typeof type === 'string' && Object.hasOwn(APPROVAL_FIELDS, type);
18
+ }
19
+
20
+ export function durableApprovalMode(value) {
21
+ if (value !== 'durable_v1') fail();
22
+ return value;
23
+ }
24
+
25
+ /** Validate transport input only; current approval state and decisions belong to the runtime. */
26
+ export function normalizeApprovalCommand(value) {
27
+ const result = { type: value.type, sessionId: id(value.sessionId), requestId: id(value.requestId) };
28
+ if (value.type === 'request.approvals.request') return result;
29
+ result.approvalId = approvalId(value.approvalId);
30
+ result.approved = boolean(value.approved);
31
+ optional(value, result, 'reason', reason => string(reason, MAX_TEXT_BYTES, { controls: true }));
32
+ if (!WAIT_FIELDS.some(key => Object.hasOwn(value, key))) return result;
33
+ if (!WAIT_FIELDS.every(key => Object.hasOwn(value, key))) fail();
34
+ result.generation = id(value.generation);
35
+ result.waitId = id(value.waitId);
36
+ result.revision = integer(value.revision);
37
+ result.commandId = id(value.commandId);
38
+ return result;
39
+ }
@@ -3,11 +3,16 @@ import {
3
3
  MAX_MESSAGE_BYTES, MAX_TEXT_BYTES, fail, record, string, id, approvalId, integer,
4
4
  boolean, optional, attachment, attachments, modelPreference, reasoningPreference, upload,
5
5
  } from './protocol-validation.mjs';
6
+ import { APPROVAL_FIELDS, APPROVAL_RESPONSES, durableApprovalMode,
7
+ isApprovalCommand, normalizeApprovalCommand } from './approval-protocol.mjs';
8
+ import { SPARK_OPEN_REQUEST, SPARK_OPEN_RESPONSE } from './spark-chat-protocol.mjs';
6
9
 
7
10
  export { MAX_MESSAGE_BYTES, MAX_ATTACHMENT_BYTES, MAX_TEXT_BYTES } from './protocol-validation.mjs';
8
11
 
9
12
  const FIELDS = Object.freeze({
10
- run_request: ['requestId', 'sessionId', 'input', 'text', 'agentMode', 'modelPreference', 'reasoningPreference', 'toolPermissionMode', 'attachments'],
13
+ [SPARK_OPEN_REQUEST]: [],
14
+ run_request: ['requestId', 'sessionId', 'input', 'text', 'agentMode', 'modelPreference', 'reasoningPreference', 'toolPermissionMode', 'attachments', 'approvalMode'],
15
+ ...APPROVAL_FIELDS,
11
16
  steer_request: ['requestId', 'steerId', 'text'],
12
17
  tool_approval_response: ['approvalId', 'approved', 'reason'],
13
18
  'request.events.request': ['requestId', 'afterSeq'],
@@ -21,6 +26,8 @@ const FIELDS = Object.freeze({
21
26
  subscribe_request: ['requestId'],
22
27
  });
23
28
  const RESPONSES = Object.freeze({
29
+ [SPARK_OPEN_REQUEST]: SPARK_OPEN_RESPONSE,
30
+ ...APPROVAL_RESPONSES,
24
31
  'request.events.request': 'request.events.response',
25
32
  'session.list.request': 'session.list.response',
26
33
  'session.snapshot.request': 'session.snapshot.response',
@@ -48,6 +55,7 @@ function run(value, allowedModes) {
48
55
  });
49
56
  optional(value, result, 'modelPreference', modelPreference);
50
57
  optional(value, result, 'reasoningPreference', reasoningPreference);
58
+ optional(value, result, 'approvalMode', durableApprovalMode);
51
59
  const mode = Object.hasOwn(value, 'toolPermissionMode') ? value.toolPermissionMode : 'ask';
52
60
  if (!MODES.includes(mode)) fail();
53
61
  if (!Array.isArray(allowedModes) || !allowedModes.includes(mode)) fail('forbidden');
@@ -57,8 +65,10 @@ function run(value, allowedModes) {
57
65
  }
58
66
 
59
67
  function normalize(value, allowedModes) {
68
+ if (isApprovalCommand(value.type)) return normalizeApprovalCommand(value);
60
69
  const result = { type: value.type };
61
70
  switch (value.type) {
71
+ case SPARK_OPEN_REQUEST: return result;
62
72
  case 'run_request': return run(value, allowedModes);
63
73
  case 'subscribe_request': return { ...result, requestId: id(value.requestId) };
64
74
  case 'steer_request': return { ...result, requestId: id(value.requestId), steerId: id(value.steerId), text: text(value.text) };
@@ -0,0 +1,51 @@
1
+ import { id, string, fail } from './protocol-validation.mjs';
2
+
3
+ export const SPARK_SESSION_ID = 'spark-companion';
4
+ export const SPARK_CHAT_FEATURE = 'spark_chat_v1';
5
+ export const SPARK_OPEN_REQUEST = 'spark.chat.open.request';
6
+ export const SPARK_OPEN_RESPONSE = 'spark.chat.open.response';
7
+
8
+ const MODES = new Set(['ask', 'full_access', 'full_plus']);
9
+ const READINESS = new Set(['ready', 'model_required', 'model_unavailable']);
10
+ const ERRORS = new Set(['invalid_spark_chat_request', 'spark_chat_session_unavailable',
11
+ 'spark_chat_model_required', 'spark_chat_model_unavailable', 'spark_chat_permission_mismatch',
12
+ 'spark_chat_unsupported', 'spark_chat_unavailable']);
13
+
14
+ export function supportsSparkChat(runtime) {
15
+ if (runtime === undefined) return true;
16
+ if (runtime.online !== true) return false;
17
+ return Array.isArray(runtime.features) && runtime.features.includes(SPARK_CHAT_FEATURE);
18
+ }
19
+
20
+ function isSparkChatCommand(command) {
21
+ if (command.type === SPARK_OPEN_REQUEST) return true;
22
+ return command.type === 'run_request' && command.sessionId === SPARK_SESSION_ID;
23
+ }
24
+
25
+ /** The existing authenticated runtime grant remains the authority ceiling. */
26
+ export function assertSparkChatSupported(command, runtime) {
27
+ if (!isSparkChatCommand(command)) return;
28
+ if (!runtime.features.includes(SPARK_CHAT_FEATURE)) fail('spark_chat_unsupported');
29
+ }
30
+
31
+ function projectSparkChatState(body) {
32
+ if (body.sessionId !== SPARK_SESSION_ID) fail();
33
+ if (!MODES.has(body.toolPermissionMode) || !READINESS.has(body.readiness)) fail();
34
+ const modelProfileId = body.modelProfileId === null ? null : string(body.modelProfileId, 256);
35
+ if ((modelProfileId === null) !== (body.readiness === 'model_required')) fail();
36
+ return { sessionId: SPARK_SESSION_ID, modelProfileId,
37
+ toolPermissionMode: body.toolPermissionMode, readiness: body.readiness };
38
+ }
39
+
40
+ /** Only chat state crosses the relay; preferences and private diagnostics never do. */
41
+ export function projectSparkOpenResponse(body) {
42
+ try {
43
+ if (!body || body.type !== SPARK_OPEN_RESPONSE || typeof body.ok !== 'boolean') return null;
44
+ const result = { type: SPARK_OPEN_RESPONSE, correlationId: id(body.correlationId), ok: body.ok };
45
+ if (body.ok) return { ...result, ...projectSparkChatState(body) };
46
+ if (!ERRORS.has(body.error)) return null;
47
+ return { ...result, error: body.error };
48
+ } catch {
49
+ return null;
50
+ }
51
+ }