@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
|
@@ -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 =
|
|
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)
|
package/server/lib/api-auth.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|