@agentdeck/bridge 1.0.15 → 1.0.17
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/dist/auth.d.ts +30 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +162 -29
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +190 -27
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/esp32-serial.d.ts +23 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +50 -1
- package/dist/esp32-serial.js.map +1 -1
- package/dist/http-auth-gate.d.ts +22 -5
- package/dist/http-auth-gate.d.ts.map +1 -1
- package/dist/http-auth-gate.js +14 -2
- package/dist/http-auth-gate.js.map +1 -1
- package/dist/pairing-window.d.ts +95 -0
- package/dist/pairing-window.d.ts.map +1 -0
- package/dist/pairing-window.js +268 -0
- package/dist/pairing-window.js.map +1 -0
- package/dist/passive-observer.d.ts +1 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +13 -4
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +17 -9
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/session-registry.d.ts +1 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +23 -4
- package/dist/usage-event.js.map +1 -1
- package/dist/ws-server.d.ts +36 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +105 -2
- package/dist/ws-server.js.map +1 -1
- package/package.json +3 -3
package/dist/daemon-server.js
CHANGED
|
@@ -46,6 +46,13 @@ const OBSERVED_APPROVAL_ENABLED = process.env.AGENTDECK_OBSERVED_APPROVAL !== '0
|
|
|
46
46
|
/** How long a held gate waits for a device decision before releasing the tool
|
|
47
47
|
* call to Claude's own permission flow. Must stay well under the hook curl's
|
|
48
48
|
* --max-time 60 so the release reaches Claude before curl gives up. */
|
|
49
|
+
/**
|
|
50
|
+
* How long a board gets to answer `auth_provision` before the daemon assumes
|
|
51
|
+
* its firmware predates the message and falls back to the legacy WiFi provision
|
|
52
|
+
* path. Generous relative to a serial round-trip (a board acks in ms) but short
|
|
53
|
+
* enough that a locked-out fleet heals within one connect.
|
|
54
|
+
*/
|
|
55
|
+
const LEGACY_REARM_GRACE_MS = 3_000;
|
|
49
56
|
const OBSERVED_APPROVAL_HOLD_MS = Math.min(50_000, Math.max(5_000, Number(process.env.AGENTDECK_APPROVAL_HOLD_MS) || 25_000));
|
|
50
57
|
/** AskUserQuestion ask-gate (PreToolUse hold so a device can answer the
|
|
51
58
|
* question). Only ever engaged when this daemon has no way to type the answer
|
|
@@ -67,8 +74,9 @@ import { VoiceManager } from './voice.js';
|
|
|
67
74
|
import { VoiceAssistantManager } from './voice-assistant.js';
|
|
68
75
|
import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon, DAEMON_DEFAULT_PORT, probeDaemonHealth, requestDaemonShutdown, scanDaemonPortWindow, shouldConcedePortToOccupant, waitForDaemonExit, waitForPortBindable, writeDaemonInfo, ensureDaemonInfo, removeDaemonInfo, readDaemonInfo, removeDaemonSession, getCandidateDataDirs, getOwnTimelineFile, } from './session-registry.js';
|
|
69
76
|
import { fetchUsageFromApi, hasOAuthToken, resetConsecutiveFailures } from './usage-api.js';
|
|
70
|
-
import { isLocalConnection, validateToken } from './auth.js';
|
|
77
|
+
import { getOrCreateToken, isLocalConnection, validateToken } from './auth.js';
|
|
71
78
|
import { buildPublicHealth, gateHttpRequest, isAuthorizedHttpRequest } from './http-auth-gate.js';
|
|
79
|
+
import { closePairingWindow, getPairingWindowStatus, openPairingWindow, pairingWindowOpen, redeemPairingCode, } from './pairing-window.js';
|
|
72
80
|
import { getLastFrame, renderPreviewFrame, onFrameRendered, offFrameRendered } from './pixoo/pixoo-bridge.js';
|
|
73
81
|
import { loadIDotMatrixDevices } from './idotmatrix/idotmatrix-settings.js';
|
|
74
82
|
import { handlePixooWake } from './pixoo/pixoo-client.js';
|
|
@@ -87,7 +95,7 @@ import { rawSessionId } from '@agentdeck/shared';
|
|
|
87
95
|
import { codexTurnOutcomeFromRollout, lastAgentMessageFromCodexRollout } from './codex-rollout-response.js';
|
|
88
96
|
import { callFoundationModelsHelper } from './foundation-models-helper.js';
|
|
89
97
|
import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
|
|
90
|
-
import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendWifiProvisionToAll, handleESP32Wake, getESP32Ports, getSerialConnectionStatus, getSerialLastError, getSerialReachableBoards } from './esp32-serial.js';
|
|
98
|
+
import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendAuthProvisionToAll, sendWifiProvision, sendWifiProvisionToAll, handleESP32Wake, getESP32Ports, getSerialConnectionStatus, getSerialLastError, getSerialReachableBoards } from './esp32-serial.js';
|
|
91
99
|
import { loadWifiConfig } from './wifi-config.js';
|
|
92
100
|
import { getConnectedAdbDevices, hasAdb, getAdbDeviceCount } from './adb-reverse.js';
|
|
93
101
|
import { getPixooDeviceDetails, pixooDeviceCount } from './pixoo/pixoo-bridge.js';
|
|
@@ -1157,12 +1165,50 @@ export async function startDaemon(opts) {
|
|
|
1157
1165
|
return '/';
|
|
1158
1166
|
}
|
|
1159
1167
|
})();
|
|
1160
|
-
const decision = gateHttpRequest(req.method ?? 'GET', gatePathname, isAuthorizedHttpRequest(req));
|
|
1168
|
+
const decision = gateHttpRequest(req.method ?? 'GET', gatePathname, isAuthorizedHttpRequest(req), pairingWindowOpen());
|
|
1161
1169
|
if (decision === 'public-health') {
|
|
1162
1170
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1163
1171
|
res.end(JSON.stringify(buildPublicHealth(port)));
|
|
1164
1172
|
return;
|
|
1165
1173
|
}
|
|
1174
|
+
if (decision === 'pair-redeem') {
|
|
1175
|
+
// The one path by which an unauthenticated LAN peer can obtain the
|
|
1176
|
+
// pairing token, and only while the operator is holding a window open.
|
|
1177
|
+
// Body: { code: "482913", name?: string, kind?: string }
|
|
1178
|
+
void (async () => {
|
|
1179
|
+
const peerIp = req.socket.remoteAddress ?? '';
|
|
1180
|
+
let body = {};
|
|
1181
|
+
try {
|
|
1182
|
+
body = await readJsonBody(req, 4096);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
// A body we cannot parse is a malformed submission, not a guess —
|
|
1186
|
+
// fall through with an empty object so it burns no attempt.
|
|
1187
|
+
}
|
|
1188
|
+
const result = redeemPairingCode(body.code, { ip: peerIp, name: body.name, kind: body.kind },
|
|
1189
|
+
// Live read, not core.authToken: a token handover (adoptPeerToken)
|
|
1190
|
+
// must reach a device pairing right now, exactly as it reaches the
|
|
1191
|
+
// serial fleet.
|
|
1192
|
+
getOrCreateToken);
|
|
1193
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1194
|
+
if (result.outcome === 'accepted' && result.token) {
|
|
1195
|
+
res.end(JSON.stringify({ token: result.token, port }));
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
res.end(JSON.stringify({
|
|
1199
|
+
error: result.outcome,
|
|
1200
|
+
attemptsRemaining: result.attemptsRemaining,
|
|
1201
|
+
}));
|
|
1202
|
+
}
|
|
1203
|
+
})().catch(() => {
|
|
1204
|
+
try {
|
|
1205
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1206
|
+
res.end(JSON.stringify({ error: 'pairing failed' }));
|
|
1207
|
+
}
|
|
1208
|
+
catch { /* client gone */ }
|
|
1209
|
+
});
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1166
1212
|
if (decision === 'deny') {
|
|
1167
1213
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1168
1214
|
res.end(JSON.stringify({ error: 'Unauthorized — pairing token required for LAN access' }));
|
|
@@ -1242,6 +1288,53 @@ export async function startDaemon(opts) {
|
|
|
1242
1288
|
}));
|
|
1243
1289
|
return;
|
|
1244
1290
|
}
|
|
1291
|
+
// ── Pairing window, operator side ─────────────────────────────────────
|
|
1292
|
+
// Past the LAN gate, so these three are same-machine or token-bearing only:
|
|
1293
|
+
// the CLI runs beside the daemon, and the macOS app is either this process's
|
|
1294
|
+
// host or a local peer. A remote peer opening its own window would be a peer
|
|
1295
|
+
// granting itself a credential.
|
|
1296
|
+
//
|
|
1297
|
+
// POST /pair/open { ttlMs?, redemptions? } → { code, expiresAt, redemptions }
|
|
1298
|
+
// GET /pair/status → who redeemed, who guessed wrong
|
|
1299
|
+
// POST /pair/close → cancel early
|
|
1300
|
+
if (req.method === 'POST' && pathname === '/pair/open') {
|
|
1301
|
+
void (async () => {
|
|
1302
|
+
let body = {};
|
|
1303
|
+
try {
|
|
1304
|
+
body = await readJsonBody(req, 4096);
|
|
1305
|
+
}
|
|
1306
|
+
catch { /* defaults */ }
|
|
1307
|
+
const ttlMs = typeof body.ttlMs === 'number' ? body.ttlMs : undefined;
|
|
1308
|
+
const redemptions = typeof body.redemptions === 'number' ? body.redemptions : undefined;
|
|
1309
|
+
// Operator-named ESP32 peers for a cable-free re-arm. A board cannot type
|
|
1310
|
+
// a code, so the address the operator read off the refusal log is what
|
|
1311
|
+
// authorizes it — see pairing-window.ts `mayAdoptEsp32`.
|
|
1312
|
+
const adoptEsp32Ips = Array.isArray(body.adoptEsp32Ips)
|
|
1313
|
+
? body.adoptEsp32Ips.filter((x) => typeof x === 'string')
|
|
1314
|
+
: undefined;
|
|
1315
|
+
const opened = openPairingWindow({ ttlMs, redemptions, adoptEsp32Ips });
|
|
1316
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1317
|
+
res.end(JSON.stringify(opened));
|
|
1318
|
+
})().catch(() => {
|
|
1319
|
+
try {
|
|
1320
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1321
|
+
res.end(JSON.stringify({ error: 'could not open pairing window' }));
|
|
1322
|
+
}
|
|
1323
|
+
catch { /* client gone */ }
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (req.method === 'GET' && pathname === '/pair/status') {
|
|
1328
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1329
|
+
res.end(JSON.stringify(getPairingWindowStatus()));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (req.method === 'POST' && pathname === '/pair/close') {
|
|
1333
|
+
closePairingWindow();
|
|
1334
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1335
|
+
res.end(JSON.stringify({ open: false }));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1245
1338
|
if (req.method === 'GET' && pathname === '/status') {
|
|
1246
1339
|
const snap = core.stateMachine.getSnapshot();
|
|
1247
1340
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -3033,6 +3126,14 @@ export async function startDaemon(opts) {
|
|
|
3033
3126
|
// Include ESP32 serial connections in client count for polling guards
|
|
3034
3127
|
core.setExternalClientCountProvider(() => esp32ConnectionCount());
|
|
3035
3128
|
}
|
|
3129
|
+
// Cable-free ESP32 re-arm (daemon only — a session bridge shares WsServer and
|
|
3130
|
+
// has no business provisioning boards). Reads the token live, like every other
|
|
3131
|
+
// provisioning path, so a handover reaches a board re-arming right now.
|
|
3132
|
+
core.wsServer.setEsp32AdoptionProvider(() => ({
|
|
3133
|
+
authToken: getOrCreateToken(),
|
|
3134
|
+
bridgeIp: getLanIp(),
|
|
3135
|
+
bridgePort: port,
|
|
3136
|
+
}));
|
|
3036
3137
|
// WS-path display_state re-sync: the 5s serial heartbeat above only covers
|
|
3037
3138
|
// USB-attached boards. WiFi boards (InkDeck) receive display_state edge-
|
|
3038
3139
|
// triggered over the plugin WS — a missed wake edge would leave an e-ink
|
|
@@ -3054,32 +3155,94 @@ export async function startDaemon(opts) {
|
|
|
3054
3155
|
}
|
|
3055
3156
|
}, 30_000);
|
|
3056
3157
|
wifiDeviceInfoPoll.unref?.();
|
|
3057
|
-
//
|
|
3158
|
+
// ONE serial message handler. `onESP32Message` REPLACES rather than adds,
|
|
3159
|
+
// so a second registration silently unhooks the first — which is why the
|
|
3160
|
+
// credential re-arm below shares this callback with WiFi auto-provisioning
|
|
3161
|
+
// instead of registering beside it.
|
|
3058
3162
|
const wifiConfig = loadWifiConfig();
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3163
|
+
const lanIp = getLanIp();
|
|
3164
|
+
// Boards flashed before `auth_provision` existed cannot be re-armed by it —
|
|
3165
|
+
// they drop the unknown type silently, and the daemon has no way to tell
|
|
3166
|
+
// "understood" from "ignored" except by the ack not arriving. So a board
|
|
3167
|
+
// that stays quiet falls back to the one message its firmware does
|
|
3168
|
+
// understand. It is a FALLBACK because it costs a WiFi re-associate: it
|
|
3169
|
+
// fires once per board per token, only after the silence, and only when the
|
|
3170
|
+
// user has auto-provisioning on (it carries their WiFi credentials).
|
|
3171
|
+
const legacyRearmTimers = new Map();
|
|
3172
|
+
const cancelLegacyRearm = (portPath) => {
|
|
3173
|
+
const timer = legacyRearmTimers.get(portPath);
|
|
3174
|
+
if (!timer)
|
|
3175
|
+
return;
|
|
3176
|
+
clearTimeout(timer);
|
|
3177
|
+
legacyRearmTimers.delete(portPath);
|
|
3178
|
+
};
|
|
3179
|
+
onESP32Message((portPath, msg) => {
|
|
3180
|
+
// Keep every serial-attached board armed with the token this daemon is
|
|
3181
|
+
// actually serving. Deliberately not gated on auto-provision: a board
|
|
3182
|
+
// needs a valid credential whether or not this machine hands out WiFi
|
|
3183
|
+
// credentials, and it needs one most when its radio is already up — the
|
|
3184
|
+
// case `shouldSendWifiProvision` skips on purpose. Reads the token live
|
|
3185
|
+
// rather than through `core.authToken` so a handover adoption reaches
|
|
3186
|
+
// the fleet.
|
|
3187
|
+
if (msg.type === 'device_info') {
|
|
3188
|
+
const armed = sendAuthProvisionToAll({
|
|
3189
|
+
type: 'auth_provision',
|
|
3190
|
+
authToken: getOrCreateToken(),
|
|
3191
|
+
bridgeIp: lanIp,
|
|
3192
|
+
bridgePort: port,
|
|
3193
|
+
});
|
|
3194
|
+
if (armed.length > 0)
|
|
3195
|
+
log(`[agentdeck] Pairing token armed on ${armed.length} ESP32 device(s) after ${portPath}`);
|
|
3196
|
+
if (wifiConfig?.autoProvision) {
|
|
3197
|
+
for (const armedPort of armed) {
|
|
3198
|
+
if (legacyRearmTimers.has(armedPort))
|
|
3199
|
+
continue;
|
|
3200
|
+
const timer = setTimeout(() => {
|
|
3201
|
+
legacyRearmTimers.delete(armedPort);
|
|
3202
|
+
const sent = sendWifiProvision(armedPort, {
|
|
3203
|
+
type: 'wifi_provision',
|
|
3204
|
+
ssid: wifiConfig.ssid,
|
|
3205
|
+
password: wifiConfig.password,
|
|
3206
|
+
bridgeIp: lanIp,
|
|
3207
|
+
bridgePort: port,
|
|
3208
|
+
authToken: getOrCreateToken(),
|
|
3209
|
+
});
|
|
3210
|
+
if (sent) {
|
|
3211
|
+
log(`[agentdeck] ${armedPort}: firmware predates auth_provision — re-armed over the legacy WiFi provision path`);
|
|
3212
|
+
}
|
|
3213
|
+
}, LEGACY_REARM_GRACE_MS);
|
|
3214
|
+
timer.unref?.();
|
|
3215
|
+
legacyRearmTimers.set(armedPort, timer);
|
|
3216
|
+
}
|
|
3080
3217
|
}
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3218
|
+
}
|
|
3219
|
+
else if (msg.type === 'auth_provision_ack') {
|
|
3220
|
+
cancelLegacyRearm(portPath);
|
|
3221
|
+
if (msg.changed)
|
|
3222
|
+
log(`[agentdeck] ESP32 pairing token re-armed on ${portPath}`);
|
|
3223
|
+
}
|
|
3224
|
+
if (!wifiConfig?.autoProvision)
|
|
3225
|
+
return;
|
|
3226
|
+
const shouldRefreshIps10Endpoint = msg.type === 'device_info' &&
|
|
3227
|
+
msg.board === 'ips_10' &&
|
|
3228
|
+
msg.wifiConnected &&
|
|
3229
|
+
!msg.wifiRadioParked;
|
|
3230
|
+
if (msg.type === 'device_info' && (!msg.wifiConnected || shouldRefreshIps10Endpoint)) {
|
|
3231
|
+
const sent = sendWifiProvisionToAll({
|
|
3232
|
+
type: 'wifi_provision',
|
|
3233
|
+
ssid: wifiConfig.ssid,
|
|
3234
|
+
password: wifiConfig.password,
|
|
3235
|
+
bridgeIp: lanIp,
|
|
3236
|
+
bridgePort: port,
|
|
3237
|
+
authToken: getOrCreateToken(),
|
|
3238
|
+
});
|
|
3239
|
+
if (sent > 0)
|
|
3240
|
+
log(`[agentdeck] WiFi provision sent to ${sent} ESP32 device(s) after ${portPath}`);
|
|
3241
|
+
}
|
|
3242
|
+
else if (msg.type === 'wifi_provision_ack') {
|
|
3243
|
+
log(msg.success ? `[agentdeck] ESP32 WiFi connected: ${msg.ip} ✓` : `[agentdeck] ESP32 WiFi failed: ${msg.error || 'unknown'}`);
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3083
3246
|
}
|
|
3084
3247
|
log(`[agentdeck] WebSocket server ready on port ${port}`);
|
|
3085
3248
|
log('[agentdeck] Pairing credential ready; run "agentdeck qr" to pair a device.');
|