@gotcos/glasses-server 6.99.33 → 7.0.18
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/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
package/server/index.ts
CHANGED
|
@@ -18,6 +18,13 @@ import { createClientInstanceRouter, isMeetingChunkPost, pairedInstanceFrom } fr
|
|
|
18
18
|
import type { ClientInstanceOwner } from './lib/client-instance-claim.js'
|
|
19
19
|
import { G2TurnLedger, G2TurnProvenance, G2_TURN_LEDGER_FILE, type OriginRequest } from './lib/g2-turn-provenance.js'
|
|
20
20
|
import { createG2TurnsRouter } from './routes/g2-turns.js'
|
|
21
|
+
import { WebKeyAuthority, webKeyAuthorityEnabled } from './lib/web-key-authority.js'
|
|
22
|
+
import { deviceAllowlistFrom, WEB_KEY_AUDIT_FILE, WEB_KEY_NONCES_FILE, WEB_KEYS_FILE, WebKeyEnrollments, WebKeyNonces, WebKeyPackets, WebKeyRegistry } from './lib/web-key-registry.js'
|
|
23
|
+
import { WEB_KEY_SIGNER_PORT, webKeySignerOrigin } from './lib/web-key-origin.js'
|
|
24
|
+
import { createWebKeyRouter } from './routes/web-key.js'
|
|
25
|
+
import { WebKeySignHandoffs } from './lib/web-key-handoff.js'
|
|
26
|
+
import { combinedIntegrityReport, createWebKeySignerHandler } from './lib/web-key-signer-app.js'
|
|
27
|
+
import { WebControlIntegrity, webControlIntegrityEnabled } from './lib/web-control-integrity.js'
|
|
21
28
|
import { getTailscaleNudger, installTailscaleNudger } from './lib/tailscale-nudge.js'
|
|
22
29
|
import { queryRouter } from './routes/query.js'
|
|
23
30
|
import { providerProofRouter } from './routes/provider-proof.js'
|
|
@@ -25,6 +32,8 @@ import { transcribeRouter } from './routes/transcribe.js'
|
|
|
25
32
|
import { sessionIndexRouter } from './routes/session-index.js'
|
|
26
33
|
import { agentSessionsRouter } from './routes/agent-sessions.js'
|
|
27
34
|
import { agentSessionStreamRouter } from './routes/agent-session-stream.js'
|
|
35
|
+
import { createLiveEventsRouter, liveEventsEnabled, realLiveSources } from './routes/live-events.js'
|
|
36
|
+
import { LiveEventsHub } from './lib/live-events.js'
|
|
28
37
|
import { createAttachedTurnStream } from './lib/session-stream-producer.js'
|
|
29
38
|
import { claudeSessionsRouter } from './routes/claude-sessions.js'
|
|
30
39
|
import { createSessionHooksRouter } from './routes/session-hooks.js'
|
|
@@ -46,6 +55,9 @@ import {
|
|
|
46
55
|
wirePermissionBrokerToSignals,
|
|
47
56
|
} from './lib/permission-broker.js'
|
|
48
57
|
import { createPermissionBrokerHookRouter, createSessionQuestionsRouter } from './routes/permission-broker.js'
|
|
58
|
+
// X1: the full hold's switches and the engine gate's registry read (kept on their own lines, apart from other lanes' imports).
|
|
59
|
+
import { fullHoldSwitches } from './lib/full-hold.js'
|
|
60
|
+
import { engineForHookProcess, registryStatusForPid } from './lib/full-hold-engine.js'
|
|
49
61
|
import { lastHoldQuestionsPollAt, lastLegacyQuestionsPollAt, lastQuestionsPollAt, presenceAgeMs, webPresenceAgeMs } from './lib/client-liveness.js'
|
|
50
62
|
import {
|
|
51
63
|
createAgentSessionBindingsRouter,
|
|
@@ -122,7 +134,8 @@ import { findPinnedCli, killParakeetChildren, sweepParakeetStaging, sweepStalePa
|
|
|
122
134
|
import { handoffsRouter } from './routes/handoffs.js'
|
|
123
135
|
import { recoveryRouter } from './routes/recovery.js'
|
|
124
136
|
import { promptEditRouter } from './routes/prompt-edit.js'
|
|
125
|
-
import { createTodayBriefRouter, defaultTodayBrief,
|
|
137
|
+
import { createTodayBriefRouter, defaultTodayBrief, readSessionContext } from './routes/today-brief.js'
|
|
138
|
+
import { calendarToday } from './lib/calendar-source.js'
|
|
126
139
|
import { liveCosAskRouter } from './routes/live-cos-ask.js'
|
|
127
140
|
import { meetingAskRouter } from './routes/meeting-ask.js'
|
|
128
141
|
import { meetingAskRuntime } from './lib/meeting-ask-runtime.js'
|
|
@@ -145,6 +158,7 @@ import {
|
|
|
145
158
|
stopCodexModelCatalogRefresh,
|
|
146
159
|
} from './lib/codex-model-catalog.js'
|
|
147
160
|
import { startWhisperServer, stopWhisperServer } from './lib/whisper-local.js'
|
|
161
|
+
import { noteCorsRejection } from './lib/cors-reject-log.js'
|
|
148
162
|
import { startWhisperPreviewServer, stopWhisperPreviewServer } from './lib/whisper-preview.js'
|
|
149
163
|
import { startLocalTtsServer, stopLocalTtsServer } from './lib/tts-local.js'
|
|
150
164
|
import { initSileroVAD } from './lib/vad-silero.js'
|
|
@@ -155,7 +169,7 @@ import { killNemotronChildren } from './lib/nemotron-cli.js'
|
|
|
155
169
|
import { getSessionRaw, logActiveSessionsOnShutdown, startAutoSnapshot } from './lib/conversation.js'
|
|
156
170
|
import { getMediaStore } from './lib/media-store.js'
|
|
157
171
|
import { getTurnFileStore } from './lib/turn-file-store.js'
|
|
158
|
-
import { listenRequiredServers, type RequiredListener } from './lib/listener-startup.js'
|
|
172
|
+
import { listenerPlan, listenRequiredServers, type RequiredListener } from './lib/listener-startup.js'
|
|
159
173
|
import { serverMetrics } from './lib/server-metrics.js'
|
|
160
174
|
import { initializeServerInstanceId } from './lib/server-instance-id.js'
|
|
161
175
|
import { appendPrivateEnvBlock, UnsafeUserConfigPathError } from './lib/secure-user-config.js'
|
|
@@ -187,6 +201,7 @@ import { createBridgeDropWriters } from './lib/drop-destinations.js'
|
|
|
187
201
|
import { dropItEnabled } from './lib/drop-settings.js'
|
|
188
202
|
import { serverTimezone } from './lib/morning-brief-config.js'
|
|
189
203
|
import { createYourMoveCounter } from './lib/your-move-count.js'
|
|
204
|
+
import { workSessionActivity } from './lib/work-session-activity.js'
|
|
190
205
|
import { readWorkJournal } from './lib/work-activity.js'
|
|
191
206
|
import { createDropP2Boot } from './lib/drop-p2-boot.js'
|
|
192
207
|
import { createP2SettingsReader } from './lib/drop-p2-settings.js'
|
|
@@ -203,7 +218,7 @@ import { BOOT_RESUME_LEDGER_FILE, createBootResumeGate, installBootResumeGate }
|
|
|
203
218
|
import { WorkIntakeStore, createOptionalWorkIntakeStore } from './lib/work-intake-store.js'
|
|
204
219
|
import { WorkHandoffRequestStore, createOptionalWorkHandoffRequestStore, handoffRequestCapabilities } from './lib/work-handoff-requests.js'
|
|
205
220
|
import { createWorkBoardReader } from './lib/work-board-reader.js'
|
|
206
|
-
import { listBoard } from './lib/task-store.js'
|
|
221
|
+
import { listBoard, listBoardWithDates } from './lib/task-store.js'
|
|
207
222
|
import { createDefaultWorkReviewRuntime } from './lib/work-review-backend.js'
|
|
208
223
|
import { createMorningBriefRouter } from './routes/morning-brief.js'
|
|
209
224
|
import { getMorningBriefScheduler, startMorningBriefScheduler, stopMorningBriefScheduler } from './lib/morning-brief-runtime.js'
|
|
@@ -225,6 +240,7 @@ import { isPublicApiRequest, requireApiToken } from './lib/api-auth.js'
|
|
|
225
240
|
import { PairingState, pairingHostsFrom } from './lib/glasses-pairing.js'
|
|
226
241
|
import { createPairingRouter } from './routes/pairing.js'
|
|
227
242
|
import { createWebControlRouter, isAllowedCorsOrigin, webControlCsp, webControlEnabled } from './routes/web-control.js'
|
|
243
|
+
import { webControlOffnetResponse } from './lib/web-control-offnet-page.js'
|
|
228
244
|
import { createWebControlStatic } from './lib/web-control-static.js'
|
|
229
245
|
import { isWebControlSocketAllowed, isWebSessionAuthorized, setWebSessionEpochFile } from './lib/web-session-auth.js'
|
|
230
246
|
import { getTailnetSelfName, installTailnetSelfName } from './lib/tailnet-self-name.js'
|
|
@@ -240,7 +256,7 @@ import {
|
|
|
240
256
|
} from './lib/maintenance-lifecycle.js'
|
|
241
257
|
|
|
242
258
|
const app = express()
|
|
243
|
-
import { createThreadTurnQueueRouter, drainAllThreads, steerAvailabilityFor } from './routes/thread-turn-queue.js'
|
|
259
|
+
import { createThreadTurnQueueRouter, drainAllThreads, midTurnAvailabilityFor, steerAvailabilityFor } from './routes/thread-turn-queue.js'
|
|
244
260
|
import { createCursorStopFollowupRouter } from './routes/cursor-stop-followup.js'
|
|
245
261
|
import { queuedThreadKeys, readQueue, readTranscriptTailLines, transcriptTurnVerdict, writeQueue } from './lib/thread-turn-queue-store.js'
|
|
246
262
|
import { cursorDeskHaltReady, hookHaltReady, readHookToken } from './lib/claude-hooks-installer.js'
|
|
@@ -308,9 +324,10 @@ app.use(cors({
|
|
|
308
324
|
if (!origin || origin === 'null') return cb(null, true)
|
|
309
325
|
// Async only for an unrecognised *.ts.net origin (see isAllowedCorsOrigin); every other
|
|
310
326
|
// origin is decided from cached state, as before.
|
|
327
|
+
// S1 (2026-11-09): the rejected origin is logged (origin only, once per origin per hour).
|
|
311
328
|
isAllowedCorsOrigin(origin, getTailnetSelfName()).then(
|
|
312
|
-
ok => ok
|
|
313
|
-
() => cb(new Error('CORS blocked')),
|
|
329
|
+
ok => { if (ok) return cb(null, true); noteCorsRejection(origin, 'not_allowed'); cb(new Error('CORS blocked')) },
|
|
330
|
+
() => { noteCorsRejection(origin, 'check_failed'); cb(new Error('CORS blocked')) },
|
|
314
331
|
)
|
|
315
332
|
},
|
|
316
333
|
// Without this, a cross-origin reader sees only the CORS-safelisted response
|
|
@@ -358,6 +375,17 @@ const permissionBroker = new PermissionBroker({
|
|
|
358
375
|
return !!signal && !signal.ended && signal.waiting?.fingerprint === fingerprint
|
|
359
376
|
},
|
|
360
377
|
signals: brokerSignalSink(sessionSignalStore),
|
|
378
|
+
// X1: the full hold (lib/full-hold.ts). Both switches default OFF; the registry is read only while one is on.
|
|
379
|
+
fullHold: () => fullHoldSwitches(process.env),
|
|
380
|
+
engineFor: (sessionId, ppid) => engineForHookProcess(sessionId, ppid),
|
|
381
|
+
// X1 round 3: the asking engine's own status, read each tick for full-hold items only.
|
|
382
|
+
engineStatus: (sessionId, pid) => registryStatusForPid(pid, sessionId),
|
|
383
|
+
// X1: per kind once the PermissionRequest hook is split; the one number while it is not. Read only by the full hold.
|
|
384
|
+
installedHookTimeoutForKindS: kind => {
|
|
385
|
+
const status = cachedHookStatus()
|
|
386
|
+
const perKind = status.permissionHookTimeouts ? status.permissionHookTimeouts[kind] : undefined
|
|
387
|
+
return perKind === undefined ? status.permissionHookTimeoutS : perKind
|
|
388
|
+
},
|
|
361
389
|
})
|
|
362
390
|
registerPermissionBroker(permissionBroker)
|
|
363
391
|
wirePermissionBrokerToSignals(permissionBroker, sessionSignalStore)
|
|
@@ -381,6 +409,40 @@ app.use(createPermissionBrokerHookRouter({ hookToken: readHookToken, broker: per
|
|
|
381
409
|
// it (tailnetSelf and HTTPS_PORT are read at request time, after they are set below). It is
|
|
382
410
|
// recorded, never vouched: the cookie is an HMAC of API_TOKEN, so any token holder can mint one.
|
|
383
411
|
const clientInstanceOwners = new Map<string, ClientInstanceOwner>()
|
|
412
|
+
// 6.99.31: the enrolled phone key (lib/web-key-authority.ts), behind COS_WEB_KEY_AUTHORITY (default
|
|
413
|
+
// off, read at call time). Off, no envelope is read and no /api/web-key route exists. The stores only
|
|
414
|
+
// read their files here (none exist until the feature is used); nothing is written while it is off.
|
|
415
|
+
const webKeySelfDnsName = (): string | null => {
|
|
416
|
+
const name = tailnetSelf.currentSelfDnsName()
|
|
417
|
+
return typeof name === 'string' && name.trim() ? name.trim().toLowerCase().replace(/\.$/, '') : null
|
|
418
|
+
}
|
|
419
|
+
const webKeyRegistry = new WebKeyRegistry(dataPath(WEB_KEYS_FILE), dataPath(WEB_KEY_AUDIT_FILE))
|
|
420
|
+
const webKeyAuthority = new WebKeyAuthority({
|
|
421
|
+
registry: webKeyRegistry,
|
|
422
|
+
nonces: new WebKeyNonces(dataPath(WEB_KEY_NONCES_FILE)),
|
|
423
|
+
packets: new WebKeyPackets(),
|
|
424
|
+
selfDnsName: webKeySelfDnsName,
|
|
425
|
+
// The ONE origin a phone signs from (review B2/W2): the signer listener on 3144 under this Mac's
|
|
426
|
+
// MagicDNS name. The passkey's clientDataJSON.origin and the cookie gate's extra port use it too.
|
|
427
|
+
expectedOrigin: () => webKeySignerOrigin(webKeySelfDnsName()),
|
|
428
|
+
// Review W1: no vouch unless served-asset integrity is running AND its last check passed.
|
|
429
|
+
// (webControlIntegrity is declared further down; this runs at request time, after it is set.)
|
|
430
|
+
integrity: () => {
|
|
431
|
+
// Phase 3: the signer page on 3144 holds the key, so its own manifest must pass too.
|
|
432
|
+
return combinedIntegrityReport(webControlIntegrity, webKeySignerIntegrity)
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
// Phase 3 (review W2): the 3143 to 3144 handoff. In memory, two minutes, claimed once. Unused while the flag is off.
|
|
436
|
+
const webKeySignHandoffs = new WebKeySignHandoffs()
|
|
437
|
+
// Phase 3: the signer page's own integrity check (server/web-signer/.manifest.json). Built only with the
|
|
438
|
+
// flag on at boot, as the 3144 listener is (listenerPlan); off, the directory is never read.
|
|
439
|
+
const webKeySignerIntegrity: WebControlIntegrity | null = webKeyAuthorityEnabled()
|
|
440
|
+
? new WebControlIntegrity(path.join(path.dirname(fileURLToPath(import.meta.url)), 'web-signer'), { log: line => console.error(line.replace('[web-control-integrity]', '[web-signer-integrity]').replace('/control is', 'the 3144 signer is')) })
|
|
441
|
+
: null
|
|
442
|
+
// Review W6: enrollment starts only from a device on COS_WEB_KEY_DEVICES (empty: none).
|
|
443
|
+
const webKeyEnrollments = new WebKeyEnrollments(webKeyRegistry, { deviceAllowed: deviceAllowlistFrom() })
|
|
444
|
+
// The ports a COS Control Web cookie is honoured on. Flag off: exactly 6.99.29's [3141, 3143].
|
|
445
|
+
const webCookiePorts = (): number[] => (webKeyAuthorityEnabled() ? [PORT, HTTPS_PORT, WEB_KEY_SIGNER_PORT] : [PORT, HTTPS_PORT])
|
|
384
446
|
const g2Provenance = new G2TurnProvenance({
|
|
385
447
|
ledger: new G2TurnLedger(dataPath(G2_TURN_LEDGER_FILE)),
|
|
386
448
|
pairedInstance: (device, now, freshMs) => pairedInstanceFrom(clientInstanceOwners, device, now, freshMs),
|
|
@@ -390,8 +452,9 @@ const g2Provenance = new G2TurnProvenance({
|
|
|
390
452
|
socket: { remoteAddress: req.socket?.remoteAddress ?? undefined },
|
|
391
453
|
}, API_TOKEN, {
|
|
392
454
|
selfDnsName: tailnetSelf.currentSelfDnsName(),
|
|
393
|
-
allowedPorts:
|
|
455
|
+
allowedPorts: webCookiePorts(),
|
|
394
456
|
}),
|
|
457
|
+
webKey: webKeyAuthority,
|
|
395
458
|
})
|
|
396
459
|
|
|
397
460
|
if (threadAttachEnabled()) {
|
|
@@ -440,7 +503,7 @@ const HTTPS_PORT = parseInt(process.env.HTTPS_PORT ?? '3143', 10)
|
|
|
440
503
|
app.use('/api', requireApiToken(API_TOKEN, {
|
|
441
504
|
onAuthenticated: req => pairingState.noteAuthenticated(req.socket?.remoteAddress),
|
|
442
505
|
selfDnsName: () => tailnetSelf.currentSelfDnsName(),
|
|
443
|
-
allowedPorts: () =>
|
|
506
|
+
allowedPorts: () => webCookiePorts(),
|
|
444
507
|
}))
|
|
445
508
|
// Past the token gate, a request on a non-public path carried the token: mark a paired phone.
|
|
446
509
|
// Only the X-Cos-Token header counts: a browser signed in to COS Control Web passed the gate
|
|
@@ -929,6 +992,25 @@ app.use('/api', diagRouter)
|
|
|
929
992
|
app.use('/api', createClientInstanceRouter({ owners: clientInstanceOwners }))
|
|
930
993
|
// G2 authority Tier 1: read-only provenance for one delivered turn (verify_g2_turn.py asks it).
|
|
931
994
|
app.use('/api', createG2TurnsRouter({ provenance: g2Provenance, apiToken: () => API_TOKEN }))
|
|
995
|
+
// 6.99.31: the enrolled phone key registry and ceremony. Not a route at all while COS_WEB_KEY_AUTHORITY is off,
|
|
996
|
+
// except (7.0.9) the anchor status and registration routes with COS_WEB_KEY_ANCHOR_BOOTSTRAP on (routes/web-key.ts).
|
|
997
|
+
// Nothing else in this file reads that switch: the 3144 listener, its integrity and the cookie ports stay on the authority.
|
|
998
|
+
app.use('/api', createWebKeyRouter({
|
|
999
|
+
authority: webKeyAuthority,
|
|
1000
|
+
enrollments: webKeyEnrollments,
|
|
1001
|
+
isWebRequest: req => g2Provenance.originOf(req).origin.client === 'web',
|
|
1002
|
+
apiToken: () => API_TOKEN,
|
|
1003
|
+
selfDnsName: webKeySelfDnsName,
|
|
1004
|
+
expectedOrigin: () => webKeyAuthority.deps.expectedOrigin(),
|
|
1005
|
+
integrityStatus: () => ({ ...(webControlIntegrity?.status() ?? { enabled: false }), signer: webKeySignerIntegrity?.status() ?? null }),
|
|
1006
|
+
// Phase 3: the phone's key steps and the handoff claim answer only on the 3144 signer listener.
|
|
1007
|
+
isSignerRequest: req => req.socket?.localPort === WEB_KEY_SIGNER_PORT,
|
|
1008
|
+
handoffs: webKeySignHandoffs,
|
|
1009
|
+
resolveBinding: bindingId => {
|
|
1010
|
+
const b = agentSessionBindingRegistry.get(bindingId)
|
|
1011
|
+
return b ? { provider: b.provider, sessionId: b.nativeThreadId, epoch: b.epoch, targetKey: b.targetKey } : null
|
|
1012
|
+
},
|
|
1013
|
+
}))
|
|
932
1014
|
app.use('/api', createQueryJobsRouter(queryJobCoordinator, {
|
|
933
1015
|
prepareAdmission: preparePublicDurableQueryAdmission,
|
|
934
1016
|
}))
|
|
@@ -957,7 +1039,9 @@ app.use(['/api/work-board', '/api/tasks'], (req, res, next) => {
|
|
|
957
1039
|
app.use('/api', createWorkBoardRouter({ readBoard: workBoardReader, requests: () => handoffRequestCapabilities(workHandoffRequestStore, Date.now()) }))
|
|
958
1040
|
app.use('/api', createWorkHandoffRequestsRouter({ store: workHandoffRequestStore, readBoard: workBoardReader }))
|
|
959
1041
|
// The Today summary for COS Control Web (6.99.26 candidate): Haiku, off unless switched on, with its own caps and breaker.
|
|
960
|
-
|
|
1042
|
+
// 2026-11-09: the brief reads the DATED board (when each card last moved), through its own bounded, shared reader.
|
|
1043
|
+
const datedWorkBoardReader = createWorkBoardReader(() => listBoardWithDates())
|
|
1044
|
+
app.use('/api', createTodayBriefRouter({ brief: defaultTodayBrief(), board: () => datedWorkBoardReader.read(60_000, 3_000).catch(() => null), context: (keys, into) => readSessionContext(keys, into), calendar: () => calendarToday() }))
|
|
961
1045
|
// Work intake (6.57.0): meeting-derived suggestions, asks and review items. Optional: a bad journal never blocks boot.
|
|
962
1046
|
const workIntakeStore = createOptionalWorkIntakeStore(() => new WorkIntakeStore(dataPath('work-intake')))
|
|
963
1047
|
app.use('/api', createWorkIntakeRouter({ store: workIntakeStore }))
|
|
@@ -1029,6 +1113,7 @@ app.use('/api', createDropsRouter({
|
|
|
1029
1113
|
yourMove: createYourMoveCounter({
|
|
1030
1114
|
journal: () => readWorkJournal(),
|
|
1031
1115
|
rows: () => workBoardReader.read(10_000, 3_000).catch(() => null),
|
|
1116
|
+
sessionActivity: ids => workSessionActivity(ids),
|
|
1032
1117
|
}),
|
|
1033
1118
|
}))
|
|
1034
1119
|
// 6.99.16: the COS thread (routes/cos-thread.ts): one size-capped conversation behind the COS chat's Ask, with a
|
|
@@ -1074,6 +1159,13 @@ app.use('/api', agentSessionsRouter)
|
|
|
1074
1159
|
// router's `/attachability` below. READ-ONLY, and deliberately NOT behind
|
|
1075
1160
|
// COS_THREAD_ATTACH_ENABLED -- see the header of the route for why.
|
|
1076
1161
|
app.use('/api', agentSessionStreamRouter)
|
|
1162
|
+
// 7.0.1 live state: ONE change feed per open Control Web page (docs/live-events-contract.md). Ids, states, counts
|
|
1163
|
+
// and times only. COS_WEB_LIVE_EVENTS=0 registers no route (404), so the page polls exactly as before. The hub reads
|
|
1164
|
+
// nothing and ticks never while no stream is open; its reply counter is one Map increment per Stop.
|
|
1165
|
+
if (liveEventsEnabled()) {
|
|
1166
|
+
const liveEventsHub = new LiveEventsHub({ sources: realLiveSources({ broker: () => permissionBroker }) })
|
|
1167
|
+
app.use('/api', createLiveEventsRouter({ hub: liveEventsHub }))
|
|
1168
|
+
}
|
|
1077
1169
|
// Presence view of Claude Code sessions on this Mac. Dark unless
|
|
1078
1170
|
// COS_CLAUDE_SESSIONS_ENABLED=1 — it projects another product's 0700 state dir.
|
|
1079
1171
|
app.use('/api', claudeSessionsRouter)
|
|
@@ -1132,6 +1224,13 @@ const steerDeps = {
|
|
|
1132
1224
|
waitingOnPrompt: sessionHooksEnabled() ? (signalFor(threadId)?.waiting ?? null) !== null : false,
|
|
1133
1225
|
}),
|
|
1134
1226
|
}
|
|
1227
|
+
/** The attempt journal (the binding registry): Sessions steer, steer mid-turn and the drain stamp. */
|
|
1228
|
+
const queueAttemptJournal = {
|
|
1229
|
+
begin: (provider: string, threadId: string, clientTurnId: string, mode: 'turn' | 'steer', now: number) =>
|
|
1230
|
+
agentSessionBindingRegistry.beginAttempt(provider, threadId, clientTurnId, mode, now),
|
|
1231
|
+
settle: (provider: string, threadId: string, clientTurnId: string, exit: 'sent' | 'not_sent' | 'unknown', now: number) =>
|
|
1232
|
+
agentSessionBindingRegistry.settleAttempt(provider, threadId, clientTurnId, exit, now),
|
|
1233
|
+
}
|
|
1135
1234
|
/** The bounded transcript tail the reconcile and the receipts read evidence from. */
|
|
1136
1235
|
const QUEUE_EVIDENCE_TAIL_BYTES = 4 * 1024 * 1024
|
|
1137
1236
|
const queueEvidenceLines = (provider: string, threadId: string): readonly string[] | null =>
|
|
@@ -1157,6 +1256,10 @@ const queueTurnEvidence = makeQueueTurnEvidence({
|
|
|
1157
1256
|
now: () => Date.now(),
|
|
1158
1257
|
})
|
|
1159
1258
|
|
|
1259
|
+
// Steer mid-turn (COS_STEER_MIDTURN): the park route asks for a drain pass at once. The kick is
|
|
1260
|
+
// made below, after the deps it drains with; until then (and with attach off) this does nothing.
|
|
1261
|
+
let kickQueueDrain: ((threadId: string) => void) | null = null
|
|
1262
|
+
|
|
1160
1263
|
if (threadAttachEnabled()) {
|
|
1161
1264
|
const queueDeps = {
|
|
1162
1265
|
// TWO GATES, ONE ANSWER. `threadOccupancy` sees FOREIGN holders -- another app on
|
|
@@ -1235,16 +1338,12 @@ if (threadAttachEnabled()) {
|
|
|
1235
1338
|
.filter((text): text is string => typeof text === 'string'),
|
|
1236
1339
|
}),
|
|
1237
1340
|
...steerDeps,
|
|
1238
|
-
attempts:
|
|
1239
|
-
begin: (provider: string, threadId: string, clientTurnId: string, mode: 'turn' | 'steer', now: number) =>
|
|
1240
|
-
agentSessionBindingRegistry.beginAttempt(provider, threadId, clientTurnId, mode, now),
|
|
1241
|
-
settle: (provider: string, threadId: string, clientTurnId: string, exit: 'sent' | 'not_sent' | 'unknown', now: number) =>
|
|
1242
|
-
agentSessionBindingRegistry.settleAttempt(provider, threadId, clientTurnId, exit, now),
|
|
1243
|
-
},
|
|
1341
|
+
attempts: queueAttemptJournal,
|
|
1244
1342
|
// 6.53.0: parked turns hold for two minutes after a cancel on their thread.
|
|
1245
1343
|
cancelHoldUntil: (provider: string, threadId: string) => cancelHoldUntilFor(provider, threadId),
|
|
1246
1344
|
// 6.62.0 (W14): a busy Agent CLI chat's parked turn drains here (no Stop hook fires for `-p`).
|
|
1247
1345
|
cursorCliChat: (threadId: string) => continueFactsDeps.cursorChatDir(threadId) !== null,
|
|
1346
|
+
kickDrain: (threadId: string) => { kickQueueDrain?.(threadId) },
|
|
1248
1347
|
now: () => Date.now(),
|
|
1249
1348
|
}
|
|
1250
1349
|
app.use('/api', createThreadTurnQueueRouter(queueDeps))
|
|
@@ -1266,6 +1365,8 @@ if (threadAttachEnabled()) {
|
|
|
1266
1365
|
queuedThreadIds: () => queuedThreadKeys().map(k => k.threadId),
|
|
1267
1366
|
})
|
|
1268
1367
|
registerDrainKickStats(() => drainKick.stats())
|
|
1368
|
+
// The same in-flight guard as the timer and the Stop hooks: a park never starts a second pass.
|
|
1369
|
+
kickQueueDrain = threadId => { drainKick.kick(threadId) }
|
|
1269
1370
|
const queueDrainTimer = setInterval(() => { void drainKick.sweep() }, 20_000)
|
|
1270
1371
|
queueDrainTimer.unref()
|
|
1271
1372
|
if (sessionHooksEnabled()) {
|
|
@@ -1375,6 +1476,8 @@ app.use('/api', createAgentSessionBindingsRouter({
|
|
|
1375
1476
|
bootId: serverMetrics.bootId,
|
|
1376
1477
|
}),
|
|
1377
1478
|
sessionSteer: (provider, threadId, gateReason) => (sessionSteerEnabled() ? steerAvailabilityFor(steerDeps, provider, threadId, gateReason) : null),
|
|
1479
|
+
// Steer mid-turn: the attachability answer's `midTurn` (null, and absent, with COS_STEER_MIDTURN off).
|
|
1480
|
+
sessionMidTurn: (provider, threadId, gateReason) => midTurnAvailabilityFor({ ...steerDeps, attempts: queueAttemptJournal }, provider, threadId, gateReason),
|
|
1378
1481
|
// 6.53.0: cancel a session run from the lens. The route's own in-flight map answers for
|
|
1379
1482
|
// a COS turn; these answer for a run at the desk. Read per request, so Install hooks in
|
|
1380
1483
|
// Control flips `hooks_outdated` to `desk_run` within the status cache's five seconds.
|
|
@@ -1529,8 +1632,13 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
1529
1632
|
if (parakeet.killed || parakeet.removed) console.log(`[stt] shutdown: killed ${parakeet.killed} Parakeet run(s), removed ${parakeet.removed} temp dir(s)`)
|
|
1530
1633
|
} catch { /* best effort */ }
|
|
1531
1634
|
stopCodexModelCatalogRefresh()
|
|
1532
|
-
|
|
1533
|
-
|
|
1635
|
+
// S1 (2026-11-09): both whisper-server children are stopped with SIGKILL and waited for
|
|
1636
|
+
// (about 2 s at most each, inside the 8 s force exit), so an update restart leaves no
|
|
1637
|
+
// Metal-teardown crash report and no orphan reparented to launchd.
|
|
1638
|
+
await Promise.all([
|
|
1639
|
+
stopWhisperServer().catch(err => console.error('[shutdown] whisper-server stop:', err)),
|
|
1640
|
+
stopWhisperPreviewServer({ final: true }),
|
|
1641
|
+
])
|
|
1534
1642
|
stopLocalTtsServer()
|
|
1535
1643
|
clearTimeout(forceExit)
|
|
1536
1644
|
process.exit(0)
|
|
@@ -1552,19 +1660,33 @@ process.on('unhandledRejection', (reason: any) => {
|
|
|
1552
1660
|
// Start HTTPS alongside HTTP — Even Hub WebView prefers HTTPS (iOS ATS).
|
|
1553
1661
|
// Optional: drop cert.pem + key.pem in server/certs/ (e.g. via mkcert) to enable.
|
|
1554
1662
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
1663
|
+
// 6.99.31 (Phase 6): served-asset integrity, OFF by default (lib/web-control-integrity.ts). On, /control
|
|
1664
|
+
// serves only bytes that matched the pack-time manifest, from memory, re-checked every 60 s, with no
|
|
1665
|
+
// disk server behind it. Read once at boot.
|
|
1666
|
+
const webControlIntegrity = webControlEnabled() && webControlIntegrityEnabled()
|
|
1667
|
+
? new WebControlIntegrity(path.join(path.dirname(fileURLToPath(import.meta.url)), 'web-control'))
|
|
1668
|
+
: null
|
|
1555
1669
|
if (webControlEnabled()) {
|
|
1556
1670
|
const webControlDir = path.join(__dirname, 'web-control')
|
|
1671
|
+
const webControlServers = webControlIntegrity
|
|
1672
|
+
? [createWebControlStatic(webControlDir, { integrity: webControlIntegrity })]
|
|
1673
|
+
: [createWebControlStatic(webControlDir), express.static(webControlDir)]
|
|
1557
1674
|
app.use('/control', (req, res, next) => {
|
|
1558
1675
|
// Loopback and Tailscale only, unless COS_WEB_CONTROL_LAN=1 (review W2).
|
|
1559
1676
|
if (!isWebControlSocketAllowed(req.socket?.remoteAddress, process.env, req.headers)) {
|
|
1560
|
-
|
|
1677
|
+
// W1 (audit 02 F1): still 403, but a browser gets a short page that says to use Tailscale (lib/web-control-offnet-page.ts).
|
|
1678
|
+
const offnet = webControlOffnetResponse(/\btext\/html\b/i.test(String(req.headers.accept ?? '')))
|
|
1679
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
1680
|
+
if (offnet.csp) res.setHeader('Content-Security-Policy', offnet.csp)
|
|
1681
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
1682
|
+
res.status(403).type(offnet.type).send(offnet.body)
|
|
1561
1683
|
return
|
|
1562
1684
|
}
|
|
1563
1685
|
res.setHeader('Content-Security-Policy', webControlCsp(req.path))
|
|
1564
1686
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
1565
1687
|
res.setHeader('Referrer-Policy', 'no-referrer')
|
|
1566
1688
|
next()
|
|
1567
|
-
},
|
|
1689
|
+
}, ...webControlServers)
|
|
1568
1690
|
app.use('/api', createWebControlRouter({
|
|
1569
1691
|
apiToken: () => API_TOKEN,
|
|
1570
1692
|
serverVersion: () => managedServerVersion() ?? 'unknown',
|
|
@@ -1610,19 +1732,22 @@ function withRequestTimeout<T extends { requestTimeout: number }>(server: T): T
|
|
|
1610
1732
|
|
|
1611
1733
|
const certDir = path.join(__dirname, 'certs')
|
|
1612
1734
|
const listeners: RequiredListener[] = []
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1735
|
+
const httpsAvailable = existsSync(path.join(certDir, 'cert.pem'))
|
|
1736
|
+
if (!httpsAvailable) console.log('[COS API] No certs found — HTTPS disabled (drop cert.pem/key.pem in server/certs to enable)')
|
|
1737
|
+
// 6.99.31: with COS_WEB_KEY_AUTHORITY on, the phone key's signer origin (https on 3144, same cert)
|
|
1738
|
+
// joins the REQUIRED set like 3143 (lib/listener-startup.ts listenerPlan). Flag off: 6.99.29's set.
|
|
1739
|
+
for (const entry of listenerPlan({ httpsAvailable, webKeyAuthority: webKeyAuthorityEnabled(), httpPort: PORT, httpsPort: HTTPS_PORT, signerPort: WEB_KEY_SIGNER_PORT })) {
|
|
1740
|
+
// Phase 3: 3144 serves the signer page (verified bytes only) and an allowlist of /api routes,
|
|
1741
|
+
// never the big app (lib/web-key-signer-app.ts). It exists only with the flag on.
|
|
1742
|
+
const handler = entry.label === 'HTTPS signer'
|
|
1743
|
+
? createWebKeySignerHandler({ integrity: webKeySignerIntegrity, api: app, origin: () => webKeySignerOrigin(webKeySelfDnsName()) })
|
|
1744
|
+
: app
|
|
1745
|
+
const server = entry.tls
|
|
1746
|
+
? withRequestTimeout(createHttpsServer({ cert: readFileSync(path.join(certDir, 'cert.pem')), key: readFileSync(path.join(certDir, 'key.pem')) }, handler))
|
|
1747
|
+
: withRequestTimeout(createHttpServer(app))
|
|
1748
|
+
listeners.push({ server, port: entry.port, host: BIND_HOST, label: entry.label })
|
|
1621
1749
|
}
|
|
1622
1750
|
|
|
1623
|
-
const httpServer = withRequestTimeout(createHttpServer(app))
|
|
1624
|
-
listeners.push({ server: httpServer, port: PORT, host: BIND_HOST, label: 'HTTP' })
|
|
1625
|
-
|
|
1626
1751
|
listenRequiredServers(listeners).then(() => {
|
|
1627
1752
|
const serverInstanceId = initializeServerInstanceId()
|
|
1628
1753
|
const startupAdmissionsOpen = maintenanceAdmissionsOpen()
|