@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.
Files changed (133) hide show
  1. package/.env.example +37 -0
  2. package/CHANGELOG.md +587 -0
  3. package/README.md +3 -1
  4. package/docs/web-key-verifier-contract.md +378 -0
  5. package/managed-runtime-contract.json +12 -1
  6. package/package.json +10 -3
  7. package/server/bootstrap.ts +10 -0
  8. package/server/index.ts +155 -30
  9. package/server/lib/agent-session-output-images.ts +627 -49
  10. package/server/lib/agent-session-recent.ts +321 -0
  11. package/server/lib/agent-session-store.ts +27 -14
  12. package/server/lib/agent-session-tool-images.ts +224 -0
  13. package/server/lib/agent-session-turns.ts +19 -2
  14. package/server/lib/api-auth.ts +43 -1
  15. package/server/lib/ask-work-plan.ts +3 -1
  16. package/server/lib/calendar-meetings.ts +70 -0
  17. package/server/lib/calendar-source.ts +536 -0
  18. package/server/lib/claude-hooks-installer.ts +111 -12
  19. package/server/lib/codex-bridge.ts +4 -1
  20. package/server/lib/codex-run-ledger.ts +29 -0
  21. package/server/lib/context-builder.ts +2 -1
  22. package/server/lib/conversation.ts +56 -28
  23. package/server/lib/cors-reject-log.ts +51 -0
  24. package/server/lib/disk-io-meter.ts +202 -0
  25. package/server/lib/drop-p2-runtime.ts +1 -1
  26. package/server/lib/drop-service.ts +47 -12
  27. package/server/lib/drop-store.ts +31 -5
  28. package/server/lib/event-loop-stall.ts +62 -0
  29. package/server/lib/full-hold-engine.ts +114 -0
  30. package/server/lib/full-hold.ts +124 -0
  31. package/server/lib/g2-turn-provenance.ts +128 -9
  32. package/server/lib/learning-needs-you.ts +24 -7
  33. package/server/lib/listener-startup.ts +18 -0
  34. package/server/lib/live-events.ts +533 -0
  35. package/server/lib/local-path-redact.ts +272 -0
  36. package/server/lib/meeting-ask-orientation.ts +40 -6
  37. package/server/lib/meeting-ask-runner.ts +30 -10
  38. package/server/lib/meeting-batch-transcribe.ts +32 -5
  39. package/server/lib/morning-brief-runtime.ts +4 -2
  40. package/server/lib/native-mac-caller.ts +29 -0
  41. package/server/lib/permission-broker.ts +443 -11
  42. package/server/lib/ping-engine.ts +1 -1
  43. package/server/lib/ping-sources.ts +11 -6
  44. package/server/lib/preview-content.ts +172 -0
  45. package/server/lib/preview-error-code.ts +10 -0
  46. package/server/lib/preview-folders-setting.ts +133 -0
  47. package/server/lib/preview-log-throttle.ts +18 -0
  48. package/server/lib/preview-thumbs.ts +139 -0
  49. package/server/lib/secret-path-guard.ts +83 -0
  50. package/server/lib/session-cache-writer.ts +8 -1
  51. package/server/lib/session-files-setting.ts +88 -0
  52. package/server/lib/session-hook-events.ts +23 -1
  53. package/server/lib/session-hooks-runtime.ts +34 -0
  54. package/server/lib/session-links.ts +280 -0
  55. package/server/lib/session-log.ts +313 -4
  56. package/server/lib/session-state-derive.ts +36 -0
  57. package/server/lib/session-stream-events.ts +25 -1
  58. package/server/lib/thread-turn-queue.ts +10 -0
  59. package/server/lib/today-brief.ts +640 -85
  60. package/server/lib/turn-end-ask.ts +320 -0
  61. package/server/lib/turn-footer.ts +3 -3
  62. package/server/lib/web-control-integrity.ts +160 -0
  63. package/server/lib/web-control-offnet-page.ts +58 -0
  64. package/server/lib/web-control-static.ts +53 -9
  65. package/server/lib/web-key-authority.ts +372 -0
  66. package/server/lib/web-key-envelope.ts +98 -0
  67. package/server/lib/web-key-handoff.ts +135 -0
  68. package/server/lib/web-key-origin.ts +21 -0
  69. package/server/lib/web-key-passkey.ts +155 -0
  70. package/server/lib/web-key-registry.ts +825 -0
  71. package/server/lib/web-key-signer-app.ts +152 -0
  72. package/server/lib/web-memories-bridge.ts +3 -1
  73. package/server/lib/web-session-auth.ts +133 -15
  74. package/server/lib/whisper-local.ts +93 -7
  75. package/server/lib/whisper-preview.ts +5 -3
  76. package/server/lib/work-activity.ts +56 -6
  77. package/server/lib/work-orchestrator/snapshot.ts +17 -3
  78. package/server/lib/work-review-runtime.ts +17 -4
  79. package/server/lib/work-session-activity.ts +260 -0
  80. package/server/lib/your-move-count.ts +9 -3
  81. package/server/routes/agent-session-bindings.ts +29 -1
  82. package/server/routes/agent-session-stream.ts +23 -0
  83. package/server/routes/agent-sessions.ts +146 -11
  84. package/server/routes/ask-work-plan.ts +4 -1
  85. package/server/routes/claude-sessions.ts +5 -1
  86. package/server/routes/drops.ts +7 -2
  87. package/server/routes/g2-turns.ts +35 -0
  88. package/server/routes/health.ts +46 -1
  89. package/server/routes/live-events.ts +334 -0
  90. package/server/routes/permission-broker.ts +95 -5
  91. package/server/routes/query-jobs.ts +3 -0
  92. package/server/routes/session-preview.ts +200 -0
  93. package/server/routes/sessions.ts +15 -24
  94. package/server/routes/thread-turn-queue.ts +223 -12
  95. package/server/routes/today-brief.ts +134 -44
  96. package/server/routes/web-control.ts +37 -4
  97. package/server/routes/web-key.ts +284 -0
  98. package/server/routes/welcome-context.ts +28 -4
  99. package/server/routes/work-board.ts +9 -2
  100. package/server/routes/work-orchestrator.ts +12 -7
  101. package/server/scripts/write-web-control-manifest.mjs +43 -0
  102. package/server/web-control/.manifest.json +44 -0
  103. package/server/web-control/app.css +76 -1
  104. package/server/web-control/app.js +2247 -98
  105. package/server/web-control/apple-touch-icon.png +0 -0
  106. package/server/web-control/control.css +25 -0
  107. package/server/web-control/index.html +21 -3
  108. package/server/web-control/memory-review.js +100 -0
  109. package/server/web-control/panes/cos-chat.css +94 -9
  110. package/server/web-control/panes/cos-chat.js +628 -44
  111. package/server/web-control/panes/meetings.css +18 -2
  112. package/server/web-control/panes/meetings.js +24 -8
  113. package/server/web-control/panes/memories.css +17 -2
  114. package/server/web-control/panes/memories.js +127 -42
  115. package/server/web-control/panes/messages.js +21 -2
  116. package/server/web-control/panes/preview.js +318 -0
  117. package/server/web-control/panes/questions.js +162 -21
  118. package/server/web-control/panes/reader-media.js +409 -23
  119. package/server/web-control/panes/sessions.css +32 -3
  120. package/server/web-control/panes/settings.js +97 -6
  121. package/server/web-control/panes/today-actions.css +46 -0
  122. package/server/web-control/panes/today-actions.js +119 -17
  123. package/server/web-control/panes/work.css +13 -0
  124. package/server/web-control/panes/work.js +188 -37
  125. package/server/web-control/shell.js +17 -14
  126. package/server/web-signer/.manifest.json +11 -0
  127. package/server/web-signer/index.html +74 -0
  128. package/server/web-signer/signer-proof.js +348 -0
  129. package/server/web-signer/signer-store.js +39 -0
  130. package/server/web-signer/signer.css +23 -0
  131. package/server/web-signer/signer.js +341 -0
  132. package/shared/web-approval-packets.golden.json +124 -0
  133. package/shared/web-turn-envelope.ts +271 -0
@@ -33,7 +33,9 @@ import {
33
33
  payloadText,
34
34
  type AgentProvider,
35
35
  } from './agent-session-store.js'
36
- import type { SessionOutputImage } from './agent-session-output-images.js'
36
+ import type { SessionOutputFile, SessionOutputImage } from './agent-session-output-images.js'
37
+ import type { SessionToolImage } from './agent-session-tool-images.js'
38
+ import type { SessionOutputLink } from './session-links.js'
37
39
  import { unwrapPeerMessage } from './session-stream-events.js'
38
40
  import { withoutTurnFooter } from './turn-footer.js'
39
41
 
@@ -44,9 +46,23 @@ export interface SessionTurn {
44
46
  at?: string
45
47
  /** Opt-in native-session output previews. No local paths or COS message identities. */
46
48
  outputImages?: SessionOutputImage[]
49
+ /** Files a Claude session sent with SendUserFile that are not normalized images (6.99.35). Names only, no paths. */
50
+ outputFiles?: SessionOutputFile[]
51
+ /** Preview engine phase 1: local Markdown links in this reply, as cards. The text carries `[words](cos-file:N)`; base names only, no paths. */
52
+ outputLinks?: SessionOutputLink[]
53
+ /** Images the session looked at in the step around this turn (tool results), or a prompt's attached images. Opaque ids, no bytes, no paths. */
54
+ toolImages?: SessionToolImage[]
55
+ /** Images in that step past TOOL_IMAGES_PER_TURN: counted, never given an id. */
56
+ toolImagesMore?: number
57
+ /** 6.99.35: this message had images or files and "Show files and images your sessions make" is off. No names travel. */
58
+ previewsOff?: true
47
59
  }
48
60
 
49
- export type SessionTurnProjection = (provider: AgentProvider, record: Record<string, unknown>, turn: SessionTurn | null) => SessionTurn | null
61
+ export type SessionTurnProjection = {
62
+ (provider: AgentProvider, record: Record<string, unknown>, turn: SessionTurn | null): SessionTurn | null
63
+ /** A growing read reparses its window from scratch; discard state from the previous pass. */
64
+ reset?: () => void
65
+ }
50
66
 
51
67
  /** What a client may ask for in one read. Forty messages is ~10 lens pages of words. */
52
68
  export const SESSION_TURNS_MAX = 40
@@ -137,6 +153,7 @@ export function sessionTurnFromRecord(provider: AgentProvider, obj: Record<strin
137
153
 
138
154
  /** The newest `limit` messages among these lines (file order in, file order out). */
139
155
  export function recentSessionTurns(provider: AgentProvider, lines: readonly string[], limit: number, project?: SessionTurnProjection): SessionTurn[] {
156
+ project?.reset?.()
140
157
  const out: SessionTurn[] = []
141
158
  for (const line of lines) {
142
159
  const obj = parseJsonLine(line)
@@ -1,7 +1,7 @@
1
1
  import type { RequestHandler } from 'express'
2
2
  import { timingSafeTokenEqual } from './token-auth.js'
3
3
  import { PAIRING_NONCE_BODY } from './glasses-pairing.js'
4
- import { isWebSessionAuthorized } from './web-session-auth.js'
4
+ import { formatSetWebSessionCookie, isWebSessionAuthorized, renewWebSessionCookieValue, verifyWebSessionCookieValue, webSessionCookieFrom } from './web-session-auth.js'
5
5
 
6
6
  // Recovery/setup clients need these availability surfaces before they have a
7
7
  // usable token. Keep private provider state and every mutation route out.
@@ -44,6 +44,31 @@ const DISPLAY_STREAM_CAPABILITY_PATH = /^\/display-stream\/\d{1,15}\.[0-9a-f]{64
44
44
  // Minting, status and decisions stay behind the token AND a loopback socket.
45
45
  const PAIRING_POLL_PATH = new RegExp(`^/pairing/claim/${PAIRING_NONCE_BODY}$`)
46
46
 
47
+ /** Set on a response whose request renewed its browser sign-in: `<new expiry ISO> <30-day cap ISO>`. The page reads it. */
48
+ export const WEB_SESSION_RENEWED_HEADER = 'X-COS-Web-Session'
49
+
50
+ /** `res.locals` key the gate sets to `'token'` when the X-COS-Token header matched. */
51
+ export const API_AUTH_BY_LOCAL = 'cosApiAuthBy'
52
+
53
+ /**
54
+ * Final review S1 (lead 2026-11-10 02:09): `res.locals` key the gate sets ONLY for a request it let in by the browser's
55
+ * sign-in cookie: a check that the same cookie still verifies now (its expiry, the epoch "Sign out every browser" bumps,
56
+ * the 30-day cap). A stream that outlives its opening request calls it on every heartbeat (`streamSignInHolds`).
57
+ */
58
+ export const WEB_SESSION_RECHECK_LOCAL = 'cosWebSessionRecheck'
59
+
60
+ /**
61
+ * For a long-lived stream, on each heartbeat: false once the browser sign-in that opened it no longer verifies, so the
62
+ * stream tears down. A request the API token let in has no re-check and always holds, as does one that never passed
63
+ * the gate (a route mounted without it in a test). The browser reconnects with whatever cookie it holds now; a renewed
64
+ * one verifies there.
65
+ */
66
+ export function streamSignInHolds(res: { locals?: Record<string, unknown> }): boolean {
67
+ const recheck = res.locals?.[WEB_SESSION_RECHECK_LOCAL]
68
+ if (typeof recheck !== 'function') return true
69
+ try { return recheck() === true } catch { return false }
70
+ }
71
+
47
72
  export function isPublicApiRequest(method: string, path: string): boolean {
48
73
  if (PUBLIC_API_PATHS.has(path)) return true
49
74
  if (method === 'POST' && path === '/pairing/claim') return true
@@ -76,6 +101,9 @@ export function requireApiToken(apiToken: string, options: RequireApiTokenOption
76
101
  return (req, res, next) => {
77
102
  if (isPublicApiRequest(req.method, req.path)) return next()
78
103
  if (timingSafeTokenEqual(req.headers['x-cos-token'], apiToken)) {
104
+ // 7.0.4 W1: which door let this request in, for routes that answer the Mac's own helper differently from a
105
+ // browser (lib/native-mac-caller.ts). Set only here, after the header matched; a cookie session never sets it.
106
+ res.locals[API_AUTH_BY_LOCAL] = 'token'
79
107
  if (options.onAuthenticated) {
80
108
  try { options.onAuthenticated(req) } catch { /* observer only */ }
81
109
  }
@@ -86,6 +114,20 @@ export function requireApiToken(apiToken: string, options: RequireApiTokenOption
86
114
  allowedPorts: options.allowedPorts?.(),
87
115
  })
88
116
  if (cookieAuth) {
117
+ // Final review S1: the cookie this request came in with, checked again by any stream it opens (above).
118
+ const opened = webSessionCookieFrom(req.headers.cookie)
119
+ res.locals[WEB_SESSION_RECHECK_LOCAL] = (): boolean => verifyWebSessionCookieValue(opened, apiToken, Date.now())
120
+ // 7.0.x (Miles 2026-11-09, "Renew while in use, 30-day cap"): only here, after the socket, cookie, CSRF and Origin
121
+ // checks above all passed, a cookie in its last 2 days gets a fresh one, never past 30 days from its first sign-in.
122
+ // Appended, so a route's own Set-Cookie (none today on gated routes) is never overwritten. A renewal that cannot
123
+ // be made (outside the window, at the cap) changes nothing.
124
+ try {
125
+ const renewed = renewWebSessionCookieValue(webSessionCookieFrom(req.headers.cookie), apiToken)
126
+ if (renewed) {
127
+ res.append('Set-Cookie', formatSetWebSessionCookie(renewed.value, Math.floor((renewed.expiresAtMs - Date.now()) / 1000), req.secure))
128
+ res.setHeader(WEB_SESSION_RENEWED_HEADER, new Date(renewed.expiresAtMs).toISOString() + ' ' + new Date(renewed.capAtMs).toISOString())
129
+ }
130
+ } catch { /* a failed renewal leaves the request and the old cookie as they were */ }
89
131
  return next()
90
132
  }
91
133
  return res.status(401).json({
@@ -82,7 +82,9 @@ export function prepareAskWorkPlan(request: AskWorkPlanRequest, task: TaskBoardR
82
82
  const evidenceStatus = { snapshotAgeSec: evidence.snapshotAgeSec, liveListAgeSec: evidence.liveListAgeSec,
83
83
  logScanComplete: evidence.logScanComplete, fresh, usageMode: 'cached',
84
84
  scope: 'Canonical task and server session evidence only; no local attachments, local link context, or chat history. Meeting references do not include transcripts.',
85
- disabledTargets: evidence.rows.filter((_, index) => !sessions[index]!.automatic || !sessions[index]!.forkable).map(row => ({ id: row.id,
85
+ // A row is wholly disabled only when neither Continue nor Fork is eligible. The
86
+ // individual automatic/forkable fields still constrain each assignment mode.
87
+ disabledTargets: evidence.rows.filter((_, index) => !sessions[index]!.automatic && !sessions[index]!.forkable).map(row => ({ id: row.id,
86
88
  reasons: [!fresh && 'stale_or_missing_evidence', !evidence.logScanComplete && 'incomplete_session_scan',
87
89
  !row.member && 'not_card_member', !['claude', 'codex'].includes(row.id.split(':')[0]!) && 'unsupported_provider',
88
90
  !providerAllowed(row.id.split(':')[0]!) && 'provider_usage_unavailable', row.busy && 'busy', !row.live && 'not_live',
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Today round 2 (2026-11-09): which calendar rows are distinct, real meetings. ONE helper for the calendar API
3
+ * (calendar-source.ts calendarToday: next, remainingCount, endedRecently) and the Today summary (today-brief.ts
4
+ * withCalendar), so the two never disagree.
5
+ *
6
+ * - Canceled: a title that starts "Canceled:" or "Cancelled:" (how Exchange and Google rename a canceled occurrence),
7
+ * or a row whose `status` says canceled, or `canceled: true` (the snapshot's EventKit status, when it carries one;
8
+ * the 11/09 snapshot had no status field on any of its 154 events).
9
+ * - Distinct: the same title (trimmed, case-folded, spaces collapsed) at the same start is one meeting, whichever
10
+ * calendar it is on. The 11/09 live snapshot had 13 such pairs or triples, 12 of them on ONE Exchange calendar with
11
+ * different external ids (the same invite delivered twice), so "across calendars" alone would have left 12 of them.
12
+ * The first row of each wins, so callers keep their own order.
13
+ * No dependencies: the summary imports it without the calendar reader.
14
+ */
15
+ export const CANCELED_TITLE = /^\s*(?:canceled|cancelled)\s*:/i
16
+ const CANCELED_STATUS = new Set(['canceled', 'cancelled'])
17
+
18
+ export interface MeetingFields { title: unknown; start: unknown; status?: unknown; canceled?: unknown }
19
+
20
+ export function isCanceledMeeting(row: MeetingFields): boolean {
21
+ if (row.canceled === true) return true
22
+ if (typeof row.status === 'string' && CANCELED_STATUS.has(row.status.trim().toLowerCase())) return true
23
+ return typeof row.title === 'string' && CANCELED_TITLE.test(row.title)
24
+ }
25
+
26
+ /** The identity of one meeting: its title and its start instant. */
27
+ export function meetingKey(row: MeetingFields): string {
28
+ const title = typeof row.title === 'string' ? row.title.trim().toLowerCase().replace(/\s+/g, ' ') : ''
29
+ const start = typeof row.start === 'number' ? row.start : Date.parse(String(row.start ?? ''))
30
+ return `${title}|${Number.isFinite(start) ? start : String(row.start ?? '')}`
31
+ }
32
+
33
+ /** The rows that are real meetings, one per meeting, in the order given. */
34
+ export function distinctMeetings<T>(rows: readonly T[], fields: (row: T) => MeetingFields): T[] {
35
+ const seen = new Set<string>()
36
+ const out: T[] = []
37
+ for (const row of rows) {
38
+ const f = fields(row)
39
+ if (isCanceledMeeting(f)) continue
40
+ const key = meetingKey(f)
41
+ if (seen.has(key)) continue
42
+ seen.add(key)
43
+ out.push(row)
44
+ }
45
+ return out
46
+ }
47
+
48
+ /**
49
+ * Prep on every Next up row (Miles typed 2026-11-09 13:00 CST): a calendar hold or status block is not a meeting to
50
+ * prep for. ONE list for the server: /api/calendar/today marks each row `hold`, /api/welcome-context marks nextEvent
51
+ * `hold`, and Control Web and desktop Control read that flag instead of keeping their own copy of the words.
52
+ *
53
+ * A title is a hold when, after leading punctuation ("[Hold] ...", "* Busy"), it starts with one of these words or
54
+ * phrases as a whole word, and it does not name someone with "with" ("Lunch with Jeremy" and "Focus group with the
55
+ * Jewel360 board" are meetings). Case and repeated spaces do not matter. A false "not a hold" only offers Prep on a hold;
56
+ * a false "hold" would hide Prep on a real meeting, so the rule stays narrow.
57
+ */
58
+ export const CALENDAR_HOLD_WORDS: readonly string[] = Object.freeze([
59
+ 'blocked', 'block', 'busy', 'hold', 'holds', 'focus', 'travel', 'personal', 'private', 'ooo', 'out of office', 'lunch',
60
+ 'pto', 'vacation', 'commute', 'dnd', 'do not book', 'do not schedule',
61
+ ])
62
+ // Letters and single spaces only, so the words need no escaping.
63
+ const HOLD_START = new RegExp(`^(?:${CALENDAR_HOLD_WORDS.join('|')})(?![a-z0-9])`)
64
+
65
+ export function isCalendarHold(title: unknown): boolean {
66
+ if (typeof title !== 'string') return false
67
+ const t = title.trim().toLowerCase().replace(/\s+/g, ' ').replace(/^[^a-z0-9]+/, '')
68
+ if (!t || !HOLD_START.test(t)) return false
69
+ return !/(?:^|[^a-z0-9])with(?![a-z0-9])/.test(t)
70
+ }