@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 +38 -0
- package/README.md +27 -14
- package/docs/INSTALLATION.md +13 -2
- package/docs/MIGRATION.md +38 -2
- package/docs/PROTOCOL.md +111 -18
- package/docs/SECURITY.md +18 -0
- package/docs/SELF-HOSTING.md +9 -2
- package/package.json +2 -1
- package/src/auth/auth-database.mjs +6 -3
- package/src/cli/client-capabilities.mjs +12 -2
- package/src/protocol/approval-protocol.mjs +39 -0
- package/src/protocol/protocol.mjs +11 -1
- package/src/protocol/spark-chat-protocol.mjs +51 -0
- package/src/relay/relay-approvals.mjs +86 -0
- package/src/relay/relay-correlations.mjs +8 -3
- package/src/relay/relay-request-adoption.mjs +97 -0
- package/src/relay/relay-requests.mjs +69 -13
- package/src/relay/relay-session-mutations.mjs +39 -5
- package/src/relay/relay.mjs +25 -5
- package/src/relay/request-ownership-schema.mjs +18 -0
- package/src/relay/request-ownership-store.mjs +115 -0
- package/src/transport/http-api.mjs +3 -2
- package/src/transport/server.mjs +13 -3
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.
|
|
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,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
27
|
-
|
|
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
|
-
-
|
|
156
|
-
runtime and request.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- `cancel`
|
|
160
|
-
|
|
161
|
-
- The relay keeps bounded
|
|
162
|
-
|
|
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.
|
|
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**,
|
|
171
|
-
by
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
package/docs/SELF-HOSTING.md
CHANGED
|
@@ -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
|
|
135
|
-
|
|
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.
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|