@gotcos/glasses-server 7.0.7 → 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 +19 -0
- package/CHANGELOG.md +170 -0
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +9 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +126 -23
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +3 -3
- package/server/lib/api-auth.ts +37 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +54 -9
- package/server/lib/claude-hooks-installer.ts +111 -12
- 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-service.ts +47 -12
- package/server/lib/drop-store.ts +21 -1
- 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/listener-startup.ts +18 -0
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/secret-path-guard.ts +4 -1
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -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/today-brief.ts +522 -58
- 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-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-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/routes/agent-session-bindings.ts +16 -1
- package/server/routes/agent-session-stream.ts +7 -0
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +11 -1
- package/server/routes/live-events.ts +6 -1
- package/server/routes/permission-broker.ts +26 -0
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +16 -9
- package/server/routes/today-brief.ts +91 -36
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +6 -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 +36 -0
- package/server/web-control/app.js +953 -57
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/index.html +14 -1
- package/server/web-control/panes/cos-chat.css +52 -6
- package/server/web-control/panes/cos-chat.js +364 -32
- package/server/web-control/panes/questions.js +135 -14
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/today-actions.css +35 -0
- package/server/web-control/panes/today-actions.js +75 -6
- package/server/web-control/panes/work.css +3 -0
- package/server/web-control/panes/work.js +178 -35
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.18",
|
|
4
4
|
"description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"test": "vitest run --maxWorkers=1",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"mutation:gate": "node server/scripts/mutation-gate.mjs",
|
|
18
|
-
"control2:lab": "node bin/control2-foundation.cjs"
|
|
18
|
+
"control2:lab": "node bin/control2-foundation.cjs",
|
|
19
|
+
"prepack": "node server/scripts/write-web-control-manifest.mjs && node server/scripts/write-web-control-manifest.mjs server/web-signer"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
21
22
|
"even-g2",
|
|
@@ -148,7 +149,13 @@
|
|
|
148
149
|
"docs/test-plan-6.71.8.md",
|
|
149
150
|
"docs/test-plan-6.71.9.md",
|
|
150
151
|
"docs/test-plan-6.71.10.md",
|
|
151
|
-
"docs/test-plan-6.71.11.md"
|
|
152
|
+
"docs/test-plan-6.71.11.md",
|
|
153
|
+
"!server/scripts/web-key-mutation-gate.mjs",
|
|
154
|
+
"!server/scripts/web-key-signer-mutation-gate.mjs",
|
|
155
|
+
"!server/scripts/compact-session-log.ts",
|
|
156
|
+
"!server/scripts/full-hold-mutation-gate.mjs",
|
|
157
|
+
"!server/scripts/turn-end-ask-mutation-gate.mjs",
|
|
158
|
+
"docs/web-key-verifier-contract.md"
|
|
152
159
|
],
|
|
153
160
|
"author": "COS Contributors",
|
|
154
161
|
"license": "MIT",
|
package/server/bootstrap.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// slot used by the installed LaunchAgent before mutable routes or stores load.
|
|
3
3
|
import './env.js'
|
|
4
4
|
import { acquireServerInstanceLock, ServerInstanceActiveError } from './lib/server-instance-lock.js'
|
|
5
|
+
import { diskIoMeterRequested, installDiskIoMeter } from './lib/disk-io-meter.js'
|
|
6
|
+
import { startEventLoopStallMonitor } from './lib/event-loop-stall.js'
|
|
5
7
|
|
|
6
8
|
try {
|
|
7
9
|
const instanceLock = acquireServerInstanceLock()
|
|
@@ -14,3 +16,11 @@ try {
|
|
|
14
16
|
console.error('[COS API] Startup refused: single-instance lock failed.', error)
|
|
15
17
|
process.exit(74)
|
|
16
18
|
}
|
|
19
|
+
|
|
20
|
+
// S1 (2026-11-09): opt-in per-path disk I/O meter (COS_DISK_IO_METER=1, default off). Installed
|
|
21
|
+
// here, before index.ts loads its stores, so module-load writes are counted too.
|
|
22
|
+
if (diskIoMeterRequested()) installDiskIoMeter()
|
|
23
|
+
|
|
24
|
+
// S1 (2026-11-09): log when the event loop was blocked over 2 s (a line only on a stall). Settles
|
|
25
|
+
// whether a client timeout (Mac dictation's 5 s capabilities check) was the server not running JS.
|
|
26
|
+
startEventLoopStallMonitor()
|
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'
|
|
@@ -48,6 +55,9 @@ import {
|
|
|
48
55
|
wirePermissionBrokerToSignals,
|
|
49
56
|
} from './lib/permission-broker.js'
|
|
50
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'
|
|
51
61
|
import { lastHoldQuestionsPollAt, lastLegacyQuestionsPollAt, lastQuestionsPollAt, presenceAgeMs, webPresenceAgeMs } from './lib/client-liveness.js'
|
|
52
62
|
import {
|
|
53
63
|
createAgentSessionBindingsRouter,
|
|
@@ -124,7 +134,8 @@ import { findPinnedCli, killParakeetChildren, sweepParakeetStaging, sweepStalePa
|
|
|
124
134
|
import { handoffsRouter } from './routes/handoffs.js'
|
|
125
135
|
import { recoveryRouter } from './routes/recovery.js'
|
|
126
136
|
import { promptEditRouter } from './routes/prompt-edit.js'
|
|
127
|
-
import { createTodayBriefRouter, defaultTodayBrief,
|
|
137
|
+
import { createTodayBriefRouter, defaultTodayBrief, readSessionContext } from './routes/today-brief.js'
|
|
138
|
+
import { calendarToday } from './lib/calendar-source.js'
|
|
128
139
|
import { liveCosAskRouter } from './routes/live-cos-ask.js'
|
|
129
140
|
import { meetingAskRouter } from './routes/meeting-ask.js'
|
|
130
141
|
import { meetingAskRuntime } from './lib/meeting-ask-runtime.js'
|
|
@@ -147,6 +158,7 @@ import {
|
|
|
147
158
|
stopCodexModelCatalogRefresh,
|
|
148
159
|
} from './lib/codex-model-catalog.js'
|
|
149
160
|
import { startWhisperServer, stopWhisperServer } from './lib/whisper-local.js'
|
|
161
|
+
import { noteCorsRejection } from './lib/cors-reject-log.js'
|
|
150
162
|
import { startWhisperPreviewServer, stopWhisperPreviewServer } from './lib/whisper-preview.js'
|
|
151
163
|
import { startLocalTtsServer, stopLocalTtsServer } from './lib/tts-local.js'
|
|
152
164
|
import { initSileroVAD } from './lib/vad-silero.js'
|
|
@@ -157,7 +169,7 @@ import { killNemotronChildren } from './lib/nemotron-cli.js'
|
|
|
157
169
|
import { getSessionRaw, logActiveSessionsOnShutdown, startAutoSnapshot } from './lib/conversation.js'
|
|
158
170
|
import { getMediaStore } from './lib/media-store.js'
|
|
159
171
|
import { getTurnFileStore } from './lib/turn-file-store.js'
|
|
160
|
-
import { listenRequiredServers, type RequiredListener } from './lib/listener-startup.js'
|
|
172
|
+
import { listenerPlan, listenRequiredServers, type RequiredListener } from './lib/listener-startup.js'
|
|
161
173
|
import { serverMetrics } from './lib/server-metrics.js'
|
|
162
174
|
import { initializeServerInstanceId } from './lib/server-instance-id.js'
|
|
163
175
|
import { appendPrivateEnvBlock, UnsafeUserConfigPathError } from './lib/secure-user-config.js'
|
|
@@ -206,7 +218,7 @@ import { BOOT_RESUME_LEDGER_FILE, createBootResumeGate, installBootResumeGate }
|
|
|
206
218
|
import { WorkIntakeStore, createOptionalWorkIntakeStore } from './lib/work-intake-store.js'
|
|
207
219
|
import { WorkHandoffRequestStore, createOptionalWorkHandoffRequestStore, handoffRequestCapabilities } from './lib/work-handoff-requests.js'
|
|
208
220
|
import { createWorkBoardReader } from './lib/work-board-reader.js'
|
|
209
|
-
import { listBoard } from './lib/task-store.js'
|
|
221
|
+
import { listBoard, listBoardWithDates } from './lib/task-store.js'
|
|
210
222
|
import { createDefaultWorkReviewRuntime } from './lib/work-review-backend.js'
|
|
211
223
|
import { createMorningBriefRouter } from './routes/morning-brief.js'
|
|
212
224
|
import { getMorningBriefScheduler, startMorningBriefScheduler, stopMorningBriefScheduler } from './lib/morning-brief-runtime.js'
|
|
@@ -228,6 +240,7 @@ import { isPublicApiRequest, requireApiToken } from './lib/api-auth.js'
|
|
|
228
240
|
import { PairingState, pairingHostsFrom } from './lib/glasses-pairing.js'
|
|
229
241
|
import { createPairingRouter } from './routes/pairing.js'
|
|
230
242
|
import { createWebControlRouter, isAllowedCorsOrigin, webControlCsp, webControlEnabled } from './routes/web-control.js'
|
|
243
|
+
import { webControlOffnetResponse } from './lib/web-control-offnet-page.js'
|
|
231
244
|
import { createWebControlStatic } from './lib/web-control-static.js'
|
|
232
245
|
import { isWebControlSocketAllowed, isWebSessionAuthorized, setWebSessionEpochFile } from './lib/web-session-auth.js'
|
|
233
246
|
import { getTailnetSelfName, installTailnetSelfName } from './lib/tailnet-self-name.js'
|
|
@@ -311,9 +324,10 @@ app.use(cors({
|
|
|
311
324
|
if (!origin || origin === 'null') return cb(null, true)
|
|
312
325
|
// Async only for an unrecognised *.ts.net origin (see isAllowedCorsOrigin); every other
|
|
313
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).
|
|
314
328
|
isAllowedCorsOrigin(origin, getTailnetSelfName()).then(
|
|
315
|
-
ok => ok
|
|
316
|
-
() => 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')) },
|
|
317
331
|
)
|
|
318
332
|
},
|
|
319
333
|
// Without this, a cross-origin reader sees only the CORS-safelisted response
|
|
@@ -361,6 +375,17 @@ const permissionBroker = new PermissionBroker({
|
|
|
361
375
|
return !!signal && !signal.ended && signal.waiting?.fingerprint === fingerprint
|
|
362
376
|
},
|
|
363
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
|
+
},
|
|
364
389
|
})
|
|
365
390
|
registerPermissionBroker(permissionBroker)
|
|
366
391
|
wirePermissionBrokerToSignals(permissionBroker, sessionSignalStore)
|
|
@@ -384,6 +409,40 @@ app.use(createPermissionBrokerHookRouter({ hookToken: readHookToken, broker: per
|
|
|
384
409
|
// it (tailnetSelf and HTTPS_PORT are read at request time, after they are set below). It is
|
|
385
410
|
// recorded, never vouched: the cookie is an HMAC of API_TOKEN, so any token holder can mint one.
|
|
386
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])
|
|
387
446
|
const g2Provenance = new G2TurnProvenance({
|
|
388
447
|
ledger: new G2TurnLedger(dataPath(G2_TURN_LEDGER_FILE)),
|
|
389
448
|
pairedInstance: (device, now, freshMs) => pairedInstanceFrom(clientInstanceOwners, device, now, freshMs),
|
|
@@ -393,8 +452,9 @@ const g2Provenance = new G2TurnProvenance({
|
|
|
393
452
|
socket: { remoteAddress: req.socket?.remoteAddress ?? undefined },
|
|
394
453
|
}, API_TOKEN, {
|
|
395
454
|
selfDnsName: tailnetSelf.currentSelfDnsName(),
|
|
396
|
-
allowedPorts:
|
|
455
|
+
allowedPorts: webCookiePorts(),
|
|
397
456
|
}),
|
|
457
|
+
webKey: webKeyAuthority,
|
|
398
458
|
})
|
|
399
459
|
|
|
400
460
|
if (threadAttachEnabled()) {
|
|
@@ -443,7 +503,7 @@ const HTTPS_PORT = parseInt(process.env.HTTPS_PORT ?? '3143', 10)
|
|
|
443
503
|
app.use('/api', requireApiToken(API_TOKEN, {
|
|
444
504
|
onAuthenticated: req => pairingState.noteAuthenticated(req.socket?.remoteAddress),
|
|
445
505
|
selfDnsName: () => tailnetSelf.currentSelfDnsName(),
|
|
446
|
-
allowedPorts: () =>
|
|
506
|
+
allowedPorts: () => webCookiePorts(),
|
|
447
507
|
}))
|
|
448
508
|
// Past the token gate, a request on a non-public path carried the token: mark a paired phone.
|
|
449
509
|
// Only the X-Cos-Token header counts: a browser signed in to COS Control Web passed the gate
|
|
@@ -932,6 +992,25 @@ app.use('/api', diagRouter)
|
|
|
932
992
|
app.use('/api', createClientInstanceRouter({ owners: clientInstanceOwners }))
|
|
933
993
|
// G2 authority Tier 1: read-only provenance for one delivered turn (verify_g2_turn.py asks it).
|
|
934
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
|
+
}))
|
|
935
1014
|
app.use('/api', createQueryJobsRouter(queryJobCoordinator, {
|
|
936
1015
|
prepareAdmission: preparePublicDurableQueryAdmission,
|
|
937
1016
|
}))
|
|
@@ -960,7 +1039,9 @@ app.use(['/api/work-board', '/api/tasks'], (req, res, next) => {
|
|
|
960
1039
|
app.use('/api', createWorkBoardRouter({ readBoard: workBoardReader, requests: () => handoffRequestCapabilities(workHandoffRequestStore, Date.now()) }))
|
|
961
1040
|
app.use('/api', createWorkHandoffRequestsRouter({ store: workHandoffRequestStore, readBoard: workBoardReader }))
|
|
962
1041
|
// The Today summary for COS Control Web (6.99.26 candidate): Haiku, off unless switched on, with its own caps and breaker.
|
|
963
|
-
|
|
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() }))
|
|
964
1045
|
// Work intake (6.57.0): meeting-derived suggestions, asks and review items. Optional: a bad journal never blocks boot.
|
|
965
1046
|
const workIntakeStore = createOptionalWorkIntakeStore(() => new WorkIntakeStore(dataPath('work-intake')))
|
|
966
1047
|
app.use('/api', createWorkIntakeRouter({ store: workIntakeStore }))
|
|
@@ -1551,8 +1632,13 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
1551
1632
|
if (parakeet.killed || parakeet.removed) console.log(`[stt] shutdown: killed ${parakeet.killed} Parakeet run(s), removed ${parakeet.removed} temp dir(s)`)
|
|
1552
1633
|
} catch { /* best effort */ }
|
|
1553
1634
|
stopCodexModelCatalogRefresh()
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
+
])
|
|
1556
1642
|
stopLocalTtsServer()
|
|
1557
1643
|
clearTimeout(forceExit)
|
|
1558
1644
|
process.exit(0)
|
|
@@ -1574,19 +1660,33 @@ process.on('unhandledRejection', (reason: any) => {
|
|
|
1574
1660
|
// Start HTTPS alongside HTTP — Even Hub WebView prefers HTTPS (iOS ATS).
|
|
1575
1661
|
// Optional: drop cert.pem + key.pem in server/certs/ (e.g. via mkcert) to enable.
|
|
1576
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
|
|
1577
1669
|
if (webControlEnabled()) {
|
|
1578
1670
|
const webControlDir = path.join(__dirname, 'web-control')
|
|
1671
|
+
const webControlServers = webControlIntegrity
|
|
1672
|
+
? [createWebControlStatic(webControlDir, { integrity: webControlIntegrity })]
|
|
1673
|
+
: [createWebControlStatic(webControlDir), express.static(webControlDir)]
|
|
1579
1674
|
app.use('/control', (req, res, next) => {
|
|
1580
1675
|
// Loopback and Tailscale only, unless COS_WEB_CONTROL_LAN=1 (review W2).
|
|
1581
1676
|
if (!isWebControlSocketAllowed(req.socket?.remoteAddress, process.env, req.headers)) {
|
|
1582
|
-
|
|
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)
|
|
1583
1683
|
return
|
|
1584
1684
|
}
|
|
1585
1685
|
res.setHeader('Content-Security-Policy', webControlCsp(req.path))
|
|
1586
1686
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
1587
1687
|
res.setHeader('Referrer-Policy', 'no-referrer')
|
|
1588
1688
|
next()
|
|
1589
|
-
},
|
|
1689
|
+
}, ...webControlServers)
|
|
1590
1690
|
app.use('/api', createWebControlRouter({
|
|
1591
1691
|
apiToken: () => API_TOKEN,
|
|
1592
1692
|
serverVersion: () => managedServerVersion() ?? 'unknown',
|
|
@@ -1632,19 +1732,22 @@ function withRequestTimeout<T extends { requestTimeout: number }>(server: T): T
|
|
|
1632
1732
|
|
|
1633
1733
|
const certDir = path.join(__dirname, 'certs')
|
|
1634
1734
|
const listeners: RequiredListener[] = []
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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 })
|
|
1643
1749
|
}
|
|
1644
1750
|
|
|
1645
|
-
const httpServer = withRequestTimeout(createHttpServer(app))
|
|
1646
|
-
listeners.push({ server: httpServer, port: PORT, host: BIND_HOST, label: 'HTTP' })
|
|
1647
|
-
|
|
1648
1751
|
listenRequiredServers(listeners).then(() => {
|
|
1649
1752
|
const serverInstanceId = initializeServerInstanceId()
|
|
1650
1753
|
const startupAdmissionsOpen = maintenanceAdmissionsOpen()
|