@gotcos/glasses-server 6.99.20 → 6.99.29

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 (65) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/docs/work-orchestrator.md +20 -0
  3. package/package.json +1 -1
  4. package/server/index.ts +48 -5
  5. package/server/lib/agent-session-output-images.ts +280 -0
  6. package/server/lib/agent-session-store.ts +3 -1
  7. package/server/lib/agent-session-turns.ts +18 -7
  8. package/server/lib/client-liveness.ts +38 -3
  9. package/server/lib/codex-subagent-activity.ts +36 -0
  10. package/server/lib/g2-turn-provenance.ts +679 -0
  11. package/server/lib/media-store.ts +41 -2
  12. package/server/lib/permission-broker.ts +94 -17
  13. package/server/lib/queue-turn-evidence.ts +19 -2
  14. package/server/lib/session-halt.ts +15 -1
  15. package/server/lib/session-stream-events.ts +4 -0
  16. package/server/lib/session-subagents.ts +155 -0
  17. package/server/lib/session-turn-attachments.ts +245 -0
  18. package/server/lib/thread-turn-queue-deliver.ts +17 -2
  19. package/server/lib/thread-turn-queue.ts +10 -0
  20. package/server/lib/today-brief.ts +731 -0
  21. package/server/lib/turn-file-store.ts +202 -0
  22. package/server/lib/turn-footer.ts +81 -0
  23. package/server/lib/web-control-static.ts +226 -0
  24. package/server/lib/web-memories-bridge.ts +9 -8
  25. package/server/lib/web-pet-choice.ts +225 -0
  26. package/server/lib/web-pet.ts +226 -0
  27. package/server/lib/work-lens-rounds.ts +29 -4
  28. package/server/routes/agent-session-bindings.ts +164 -16
  29. package/server/routes/agent-sessions.ts +77 -8
  30. package/server/routes/client-instance.ts +12 -1
  31. package/server/routes/cursor-stop-followup.ts +30 -1
  32. package/server/routes/g2-turns.ts +46 -0
  33. package/server/routes/health.ts +9 -0
  34. package/server/routes/media.ts +32 -8
  35. package/server/routes/permission-broker.ts +47 -4
  36. package/server/routes/thread-turn-queue.ts +148 -8
  37. package/server/routes/today-brief.ts +148 -0
  38. package/server/routes/web-control.ts +110 -7
  39. package/server/routes/work-lens-rounds.ts +19 -5
  40. package/server/web-control/app.css +26 -5
  41. package/server/web-control/app.js +1351 -177
  42. package/server/web-control/control.css +1 -1
  43. package/server/web-control/index.html +31 -13
  44. package/server/web-control/memories/memory-workspace.js +1 -1
  45. package/server/web-control/panes/cos-chat.css +22 -0
  46. package/server/web-control/panes/cos-chat.js +264 -48
  47. package/server/web-control/panes/meetings.css +15 -2
  48. package/server/web-control/panes/meetings.js +20 -9
  49. package/server/web-control/panes/memories.css +291 -0
  50. package/server/web-control/panes/memories.js +2208 -0
  51. package/server/web-control/panes/questions.css +39 -1
  52. package/server/web-control/panes/questions.js +419 -64
  53. package/server/web-control/panes/reader-media.js +154 -0
  54. package/server/web-control/panes/sessions.css +2 -0
  55. package/server/web-control/panes/settings.js +136 -6
  56. package/server/web-control/panes/today-actions.css +167 -0
  57. package/server/web-control/panes/today-actions.js +1303 -0
  58. package/server/web-control/panes/work.css +54 -0
  59. package/server/web-control/panes/work.js +1037 -18
  60. package/server/web-control/shell.js +6 -2
  61. package/server/web-control/theme.js +0 -2
  62. package/server/web-control/memories/memories-app.js +0 -1561
  63. package/server/web-control/memories/memories-theme.css +0 -73
  64. package/server/web-control/memories/memories.html +0 -72
  65. package/server/web-control/memories/memory-stewardship.js +0 -63
@@ -0,0 +1,679 @@
1
+ // G2 authority, Tier 1 (ported to 6.99.29 from the unmerged 6.66 design, a3c8c4b): verifiable
2
+ // provenance for the turns this server delivers into an agent session on behalf of the glasses or
3
+ // the phone. Turns from a signed-in COS Control Web browser are recorded too (6.99.29), never vouched.
4
+ //
5
+ // WHY. A turn Miles dictates on the G2 should carry his authority in the Claude, Codex or Cursor
6
+ // session it lands in, and a turn some other local process sends must not. Both arrive through
7
+ // the same routes with the same X-Cos-Token: the token sits in ~/.cos-glasses/.env, and every
8
+ // COS agent session on this Mac can read it. So "an authenticated call made this" proves
9
+ // nothing about who spoke.
10
+ //
11
+ // WHAT COUNTS AS PROOF, and nothing more is claimed:
12
+ // 1. The request's SOCKET address (never X-Forwarded-For, never req.ip) is not loopback and
13
+ // is not one of this Mac's own interface addresses (LAN, Tailscale 100.x, utun, anything
14
+ // os.networkInterfaces() lists). A process on this Mac can only reach the server from one
15
+ // of those, so this rules out every local caller. Also local: link-local addresses, and any
16
+ // address inside the subnet of a VM or container bridge this Mac hosts (bridge100 and kin),
17
+ // because a container dialing the bridge arrives from an address no interface lists.
18
+ // 2. The request names, in X-Cos-Client-Instance, the COS app copy that currently holds the
19
+ // ring for that same device address (the client-instance referee, 6.50.3), seen within
20
+ // PAIRED_INSTANCE_FRESH_MS.
21
+ // verifiedOrigin = (1) && (2). `client` (glasses / phone) is the app's own word in X-Cos-Client
22
+ // and labels the turn; it never makes a turn verified.
23
+ //
24
+ // WEB ORIGIN (6.99.29): RECORDED, NEVER VOUCHED. Miles types into a session's box on COS Control Web
25
+ // from his phone. The browser sends no X-Cos-Token header; it sends the HttpOnly `cos_web_session`
26
+ // cookie. That cookie is NOT a secret the token cannot produce: its signature is an HMAC keyed by
27
+ // the API token itself (web-session-auth.ts, `cos_web_session_v2:<token>`), the epoch sits in
28
+ // web-session.json, and the pairing token also signs a browser in. So any holder of X-Cos-Token
29
+ // (every agent on this Mac) can mint a valid cookie and send from another tailnet node with no
30
+ // token header. A web turn is therefore ledgered with originKind 'web', verifiedOrigin false and
31
+ // reason 'web_cookie_derivable_from_token', gets NO footer, and the endpoint never vouches for it.
32
+ // Real web authority needs a key the token cannot derive (a device key or passkey enrolled out of
33
+ // band); until that exists there is no vouched web origin. `webSession` is still checked in full
34
+ // (cookie signature, expiry, epoch, X-COS-Web and an Origin naming this server, no X-Cos-Token
35
+ // header, a socket that is not this Mac) so the ledger labels the turn honestly.
36
+ //
37
+ // WHAT IS NOT PROVEN. Another device on the tailnet or the LAN that holds the token and has read
38
+ // the live instance id (the app logs it in client diagnostics on this Mac) could pass both
39
+ // tests. So could a VM this Mac runs in BRIDGED mode (it takes its own LAN address from the
40
+ // router, indistinguishable from a second machine). Both are outside this tier's threat model.
41
+ //
42
+ // THE LEDGER is append-only JSONL at <data>/g2-turn-ledger.jsonl, rotated at 5 MB. It is an audit
43
+ // trail. The file is NOT the proof: any local process can write it. The endpoint answers
44
+ // `verifiedOrigin: true` only for a record THIS process wrote and still holds in memory
45
+ // (`sealed`). A record read back from disk (after a restart, or one somebody appended by hand)
46
+ // is reported, never vouched for. A restart therefore makes recent turns unverifiable, which
47
+ // fails closed.
48
+ //
49
+ // QUEUED TURNS re-enter the turn route over loopback (thread-turn-queue-deliver.ts), so their
50
+ // socket origin at delivery is always local. The origin is captured at enqueue (and again at an
51
+ // edit), kept in memory with the hash of the text it vouches for, and carried across the
52
+ // loopback hop by a single-use random token only this process knows. A queue file edited on
53
+ // disk no longer matches the hash and drains as local. A queued origin is forgotten the moment
54
+ // its row settles (delivered, steered, cancelled, refused, expired) and lapses QUEUED_ORIGIN_TTL_MS
55
+ // after it was judged, so a turn that waits longer drains unvouched. Every drain carries a token,
56
+ // even one with no origin left to carry, so the drain is the one caller allowed to add to the
57
+ // parked turn's ledger record.
58
+ //
59
+ // TURN IDS ARE NOT REUSABLE. A record this process holds is never replaced by a different turn
60
+ // under the same id (a client-chosen id could otherwise overwrite a vouched record: a denial of
61
+ // service on verification). Only an identical retry (same target, same text, same origin) or the
62
+ // server's own continuation of a parked turn (drain carry, queue edit, Cursor Stop hand-off) may
63
+ // add to it; the routes refuse anything else before delivery.
64
+
65
+ import { createHash, randomBytes } from 'node:crypto'
66
+ import { appendFileSync, existsSync, renameSync, statSync } from 'node:fs'
67
+ import { readFile } from 'node:fs/promises'
68
+ import { networkInterfaces } from 'node:os'
69
+ import { turnFooter } from './turn-footer.js'
70
+
71
+ export { FOOTER_LINE_REFUSED_COPY, G2_TURN_FOOTER_RE, g2TurnFooter, hasFooterShapedLine, stripG2TurnFooter, TURN_FOOTER_SHAPED_RE, TURN_ID_IN_USE_COPY, turnFooter, turnFooterTag, withoutTurnFooter } from './turn-footer.js'
72
+
73
+ export const G2_TURN_LEDGER_FILE = 'g2-turn-ledger.jsonl'
74
+ export const G2_TURN_LEDGER_ROTATE_BYTES = 5 * 1024 * 1024
75
+ /** Records held in memory (sealed). A turn is verified within minutes, so this is generous. */
76
+ export const G2_TURN_MEMORY_MAX = 2048
77
+ /** The strict id shape: covers the client turn id, the client fork id and the ids minted here. */
78
+ export const G2_TURN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$/
79
+ export const CLIENT_INSTANCE_HEADER = 'x-cos-client-instance'
80
+ export const CLIENT_SURFACE_HEADER = 'x-cos-client'
81
+ export const TURN_CARRY_HEADER = 'x-cos-turn-carry'
82
+ /** The pairing-token header the /api gate reads. Its presence alone rules out a web turn. */
83
+ export const TOKEN_HEADER = 'x-cos-token'
84
+ /** How recently the ring's owner must have claimed for its id to vouch for a turn (the reclaim window). */
85
+ export const PAIRED_INSTANCE_FRESH_MS = 10 * 60_000
86
+ /** A carry token lives this long: the loopback hop answers in well under a second. */
87
+ export const TURN_CARRY_TTL_MS = 30_000
88
+ /** Queued origins held at once (a queue holds 8 per thread). */
89
+ export const QUEUED_ORIGIN_MAX = 512
90
+ /** A queued origin lapses this long after it was judged: a turn that waits longer drains unvouched. */
91
+ export const QUEUED_ORIGIN_TTL_MS = 30 * 60_000
92
+ /**
93
+ * The endpoint vouches for a sealed record only this long after its origin was judged (never from
94
+ * a later delivery). The verifier's own
95
+ * window is 15 minutes; this bounds how long any record can ever vouch, so the verifier's
96
+ * consumed-turn store (kept 7 days) always outlives it.
97
+ */
98
+ export const G2_TURN_VOUCH_MAX_MS = 24 * 3_600_000
99
+ /** Why a Send again copy is not vouched: the request that sent it again was not itself a verified origin. */
100
+ export const RESEND_NOT_REVERIFIED_REASON = 'resend_not_reverified'
101
+ /** Why a web turn is never vouched in 6.99.29 (see the header). */
102
+ export const WEB_UNVOUCHED_REASON = 'web_cookie_derivable_from_token'
103
+ const OWN_ADDRESS_CACHE_MS = 30_000
104
+
105
+ export type TurnClient = 'glasses' | 'phone' | 'web' | 'local' | 'unknown'
106
+
107
+ /** Where a footed turn came from, as the ledger, the endpoint and the verifier name it. */
108
+ export type TurnOriginKind = 'g2' | 'phone' | 'web'
109
+
110
+ export interface TurnOrigin {
111
+ remote: string
112
+ loopback: boolean
113
+ ownAddress: boolean
114
+ clientInstance: string | null
115
+ client: TurnClient
116
+ /** 6.99.29: the request presented an X-Cos-Token header (any value). Never a web turn. */
117
+ tokenPresented?: boolean
118
+ /** 6.99.29: the signed-in COS Control Web cookie path passed, with no token presented. */
119
+ webSession?: boolean
120
+ }
121
+
122
+ /** The origin kind a client label stands for; null for a local or unknown caller (never footed). */
123
+ export function originKindOf(client: TurnClient | string | undefined): TurnOriginKind | null {
124
+ if (client === 'glasses') return 'g2'
125
+ if (client === 'phone') return 'phone'
126
+ if (client === 'web') return 'web'
127
+ return null
128
+ }
129
+
130
+ export interface OriginVerdict {
131
+ origin: TurnOrigin
132
+ verifiedOrigin: boolean
133
+ /** Why it is not vouched (null or absent when it is). */
134
+ reason?: string | null
135
+ /** When this origin was judged (ms). A queued origin lapses from here, never later. */
136
+ judgedAt?: number
137
+ }
138
+
139
+ export interface G2TurnRecord {
140
+ turnId: string
141
+ createdAt: string
142
+ textSha256: string
143
+ origin: TurnOrigin
144
+ /** 6.99.29: g2, phone or web; null when the turn was not footed (a local or unknown caller). */
145
+ originKind: TurnOriginKind | null
146
+ verifiedOrigin: boolean
147
+ /** 6.99.29: why it is not vouched; null when it is. A web turn says WEB_UNVOUCHED_REASON. */
148
+ reason?: string | null
149
+ /** 6.99.29: when its origin was judged (ms): the parking request, for a drained turn. */
150
+ originJudgedAt?: number
151
+ target: { provider: string; sessionId: string; fork?: true }
152
+ via: string
153
+ /**
154
+ * Send again: the row this copy was made from. AN ANNOTATION ONLY, never authority (Codex
155
+ * re-audit of 910eb90): the copy is judged by the request that sent it again, nothing is inherited.
156
+ */
157
+ resendOf?: string
158
+ }
159
+
160
+ /** The shape of an Express request this module reads. Nothing else. */
161
+ export interface OriginRequest {
162
+ method?: string
163
+ socket?: { remoteAddress?: string | null } | null
164
+ headers: Record<string, string | string[] | undefined>
165
+ }
166
+
167
+ // ASCII whitespace only, spelled out, so the Python verifier normalizes identically (JS `\s` and
168
+ // Python `\s` disagree on U+FEFF, U+0085 and U+001C..U+001F).
169
+ const WS_RUN = /[\t\n\v\f\r ]+/g
170
+ const WS_EDGE = /^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g
171
+
172
+ /** Trim, and collapse every run of ASCII whitespace to one space. */
173
+ export function normalizeTurnText(text: string): string {
174
+ return text.replace(WS_EDGE, '').replace(WS_RUN, ' ')
175
+ }
176
+
177
+ export function turnTextSha256(text: string): string {
178
+ return createHash('sha256').update(normalizeTurnText(text), 'utf8').digest('hex')
179
+ }
180
+
181
+ /**
182
+ * Whether a turn of this origin gets the footer: ONLY a vouched glasses or phone turn (QA 6.99.29
183
+ * 5b: it used to read the label, so an unverified glasses turn and every web turn were footed). A
184
+ * web turn is never footed, whatever its verdict says.
185
+ */
186
+ export function footerApplies(verdict: OriginVerdict): boolean {
187
+ if (verdict.verifiedOrigin !== true) return false
188
+ const kind = originKindOf(verdict.origin.client)
189
+ return kind === 'g2' || kind === 'phone'
190
+ }
191
+
192
+ /** The same verdict, never vouched, with the reason why. */
193
+ export function unvouched(verdict: OriginVerdict, reason: string): OriginVerdict {
194
+ return { ...verdict, verifiedOrigin: false, reason }
195
+ }
196
+
197
+ /** The vias of a turn still parked in the queue: the only records a continuation may extend. */
198
+ export const PARKED_TURN_VIAS: ReadonlySet<string> = new Set(['queue', 'queue-edit', 'queue-resend'])
199
+
200
+ /** Whether a later record under the same id is the same turn: same target, same text, same origin. */
201
+ function sameTurn(a: G2TurnRecord, b: G2TurnRecord): boolean {
202
+ return a.target.provider === b.target.provider
203
+ && a.target.sessionId === b.target.sessionId
204
+ && (a.target.fork === true) === (b.target.fork === true)
205
+ && a.textSha256 === b.textSha256
206
+ && a.originKind === b.originKind
207
+ && a.verifiedOrigin === b.verifiedOrigin
208
+ }
209
+
210
+ /** `::ffff:100.64.1.2` and `100.64.1.2` are one address; IPv6 scope ids and case are dropped. */
211
+ export function normalizeAddress(address: string | null | undefined): string {
212
+ let a = (address ?? '').trim().toLowerCase()
213
+ if (a.startsWith('::ffff:') && a.includes('.')) a = a.slice(7)
214
+ const pct = a.indexOf('%')
215
+ if (pct >= 0) a = a.slice(0, pct)
216
+ return a
217
+ }
218
+
219
+ export function isLoopbackAddress(address: string): boolean {
220
+ const a = normalizeAddress(address)
221
+ return a === '::1' || a === '0:0:0:0:0:0:0:1' || a === 'localhost' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(a)
222
+ }
223
+
224
+ /**
225
+ * Interfaces whose whole subnet is this Mac: the host side of a VM or container bridge. Measured
226
+ * 2026-10-07: a process in the cos-qdrant container that dials the bridge100 host address
227
+ * (192.168.64.1) arrives from 192.168.64.2, which is in no interface list. Without this rule any
228
+ * local process could `docker exec` its way to a non-local address.
229
+ */
230
+ export const VIRTUAL_BRIDGE_IFACE_RE = /^(bridge|vmnet|vnic|docker|veth|vboxnet|anpi)/
231
+
232
+ function ipv4ToInt(a: string): number | null {
233
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(a)
234
+ if (!m) return null
235
+ const parts = m.slice(1).map(Number)
236
+ if (parts.some(n => n > 255)) return null
237
+ return ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
238
+ }
239
+
240
+ /** Link-local addresses (169.254/16, fe80::/10) never come from a paired phone over a routed path. */
241
+ export function isLinkLocalAddress(address: string): boolean {
242
+ const a = normalizeAddress(address)
243
+ return /^169\.254\./.test(a) || /^fe[89ab][0-9a-f]:/.test(a)
244
+ }
245
+
246
+ export interface OwnAddressView {
247
+ addresses: ReadonlySet<string>
248
+ /** IPv4 subnets of virtual bridges, as [network, mask]. */
249
+ bridgeSubnets: ReadonlyArray<readonly [number, number]>
250
+ }
251
+
252
+ /** This Mac's own addresses and bridge subnets. Throws through to the caller, which fails closed. */
253
+ export function readOwnAddressView(): OwnAddressView {
254
+ const addresses = new Set<string>()
255
+ const bridgeSubnets: Array<readonly [number, number]> = []
256
+ for (const [name, list] of Object.entries(networkInterfaces())) {
257
+ for (const entry of list ?? []) {
258
+ addresses.add(normalizeAddress(entry.address))
259
+ if (entry.family === 'IPv4' && VIRTUAL_BRIDGE_IFACE_RE.test(name)) {
260
+ const ip = ipv4ToInt(entry.address)
261
+ const mask = ipv4ToInt(entry.netmask)
262
+ if (ip !== null && mask !== null) bridgeSubnets.push([(ip & mask) >>> 0, mask])
263
+ }
264
+ }
265
+ }
266
+ return { addresses, bridgeSubnets }
267
+ }
268
+
269
+ /** Whether this normalized address is this Mac, a VM or container it hosts, or link-local. */
270
+ export function isOwnAddress(address: string, view: OwnAddressView): boolean {
271
+ const a = normalizeAddress(address)
272
+ if (view.addresses.has(a) || isLinkLocalAddress(a)) return true
273
+ const ip = ipv4ToInt(a)
274
+ return ip !== null && view.bridgeSubnets.some(([network, mask]) => ((ip & mask) >>> 0) === network)
275
+ }
276
+
277
+ function headerValue(headers: OriginRequest['headers'], name: string): string | null {
278
+ const raw = headers[name]
279
+ const v = Array.isArray(raw) ? raw[0] : raw
280
+ return typeof v === 'string' ? v.trim() : null
281
+ }
282
+
283
+ const INSTANCE_RE = /^[a-z0-9]{1,16}-[a-z0-9]{1,8}$/
284
+
285
+ export class G2TurnLedger {
286
+ private readonly memory = new Map<string, G2TurnRecord>()
287
+
288
+ constructor(
289
+ private readonly file: string,
290
+ private readonly opts: { rotateBytes?: number; memoryMax?: number } = {},
291
+ ) {}
292
+
293
+ /** The record this process holds (sealed) for this id, or null. Synchronous: memory only. */
294
+ held(turnId: string): G2TurnRecord | null {
295
+ return this.memory.get(turnId) ?? null
296
+ }
297
+
298
+ /**
299
+ * Append one record. Kept in memory (sealed) even when the disk write fails. Never throws.
300
+ * A held id is never replaced unless the caller says this record continues it (`replace`):
301
+ * false, and nothing written, otherwise.
302
+ */
303
+ append(record: G2TurnRecord, opts: { replace?: boolean } = {}): boolean {
304
+ if (this.memory.has(record.turnId) && opts.replace !== true) {
305
+ console.warn(`[g2-turn-ledger] refused to replace held record turnId=${record.turnId}`)
306
+ return false
307
+ }
308
+ this.memory.delete(record.turnId)
309
+ this.memory.set(record.turnId, record)
310
+ const max = this.opts.memoryMax ?? G2_TURN_MEMORY_MAX
311
+ while (this.memory.size > max) this.memory.delete(this.memory.keys().next().value as string)
312
+ try {
313
+ const rotateAt = this.opts.rotateBytes ?? G2_TURN_LEDGER_ROTATE_BYTES
314
+ if (existsSync(this.file) && statSync(this.file).size >= rotateAt) renameSync(this.file, `${this.file}.1`)
315
+ // One write of one line under O_APPEND: a reader never sees half a record.
316
+ appendFileSync(this.file, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 })
317
+ return true
318
+ } catch (error) {
319
+ console.error(`[g2-turn-ledger] append failed: ${error instanceof Error ? error.message : error}`)
320
+ return false
321
+ }
322
+ }
323
+
324
+ /** The newest record for this id. `sealed`: written by this process and held in memory. */
325
+ async find(turnId: string): Promise<{ record: G2TurnRecord; sealed: boolean } | null> {
326
+ const held = this.memory.get(turnId)
327
+ if (held) return { record: held, sealed: true }
328
+ for (const path of [this.file, `${this.file}.1`]) {
329
+ let raw: string
330
+ try { raw = await readFile(path, 'utf8') } catch { continue }
331
+ const lines = raw.split('\n')
332
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
333
+ const line = lines[i]
334
+ if (!line || !line.includes(turnId)) continue
335
+ try {
336
+ const parsed = JSON.parse(line) as G2TurnRecord
337
+ if (parsed && parsed.turnId === turnId) return { record: parsed, sealed: false }
338
+ } catch { /* a torn or foreign line is skipped */ }
339
+ }
340
+ }
341
+ return null
342
+ }
343
+ }
344
+
345
+ export interface G2TurnProvenanceDeps {
346
+ ledger: G2TurnLedger
347
+ /** The ring owner's instance id for this device address, when it claimed within `freshMs`. */
348
+ pairedInstance: (device: string, now: number, freshMs: number) => string | null
349
+ /** This Mac's addresses (default: os.networkInterfaces(), cached 30 s). A throw fails closed. */
350
+ ownAddresses?: () => OwnAddressView
351
+ /**
352
+ * 6.99.29: whether this request passes the signed-in COS Control Web cookie path in full
353
+ * (`isWebSessionAuthorized` with the /api gate's options, in production). Asked only for a
354
+ * request with no X-Cos-Token header. Absent, or a throw: no turn is ever a web turn.
355
+ */
356
+ webSession?: (req: OriginRequest) => boolean
357
+ now?: () => number
358
+ randomToken?: () => string
359
+ }
360
+
361
+ /**
362
+ * What a queued origin is sealed to besides the words (6.99.29, Codex audit of e4758a6): the provider
363
+ * and thread it was parked for, and the digest of its attachment manifest (each file id with the hash
364
+ * of its content; '' for none). The drain recomputes it and a vouched turn whose seal changed is refused.
365
+ */
366
+ export interface QueueSeal { provider: string; threadId: string; attachments: string }
367
+
368
+ /** The digest of an attachment manifest ('' for no files): ids in order, each with its content hash. */
369
+ export function attachmentsDigest(manifest: ReadonlyArray<{ id: string; sha256: string }>): string {
370
+ if (manifest.length === 0) return ''
371
+ return createHash('sha256').update(JSON.stringify(manifest.map(e => [e.id, e.sha256])), 'utf8').digest('hex')
372
+ }
373
+
374
+ /** Whether a seal names exactly this provider, thread and attachment digest. */
375
+ export function sealMatches(seal: QueueSeal, actual: QueueSeal): boolean {
376
+ return seal.provider === actual.provider && seal.threadId === actual.threadId && seal.attachments === actual.attachments
377
+ }
378
+
379
+ /** The refusal copy for a vouched queued turn whose seal no longer matches at its drain. */
380
+ export const QUEUED_TURN_TAMPERED_COPY = 'This queued message changed after it was queued (its words, files or session no longer match), so it was not sent. Send it again from where you typed it.'
381
+
382
+ interface QueuedOrigin { verdict: OriginVerdict; textSha256: string; judgedAt: number; attachments: string }
383
+ interface Carry { clientTurnId: string; verdict: OriginVerdict | null; seal: QueueSeal | null; textSha256: string; expiresAt: number }
384
+
385
+ /** What a route asks before it delivers a client-chosen id: the turn it is about to record. */
386
+ export interface TurnCandidate { prompt: string; verdict: OriginVerdict; provider: string; sessionId: string; fork?: boolean }
387
+
388
+ export class G2TurnProvenance {
389
+ private readonly now: () => number
390
+ private readonly queued = new Map<string, QueuedOrigin>()
391
+ private readonly carries = new Map<string, Carry>()
392
+ private ownCache: { at: number; view: OwnAddressView } | null = null
393
+
394
+ constructor(private readonly deps: G2TurnProvenanceDeps) {
395
+ this.now = deps.now ?? Date.now
396
+ }
397
+
398
+ get ledger(): G2TurnLedger { return this.deps.ledger }
399
+
400
+ /** Null when this Mac's addresses cannot be read: the caller then treats every address as its own. */
401
+ private own(): OwnAddressView | null {
402
+ const at = this.now()
403
+ if (this.ownCache && at - this.ownCache.at < OWN_ADDRESS_CACHE_MS) return this.ownCache.view
404
+ try {
405
+ const view = (this.deps.ownAddresses ?? readOwnAddressView)()
406
+ this.ownCache = { at, view }
407
+ return view
408
+ } catch {
409
+ this.ownCache = null
410
+ return null
411
+ }
412
+ }
413
+
414
+ /** What this request can prove about where it came from. Never throws. */
415
+ originOf(req: OriginRequest): OriginVerdict {
416
+ const rawRemote = req.socket?.remoteAddress ?? null
417
+ const remote = rawRemote ? normalizeAddress(rawRemote) : ''
418
+ const known = remote.length > 0
419
+ const loopback = known && isLoopbackAddress(remote)
420
+ const own = this.own()
421
+ // Fails closed: an unreadable interface list means every address may be this Mac's.
422
+ const ownAddress = known && (own === null || isOwnAddress(remote, own))
423
+ const instanceRaw = headerValue(req.headers, CLIENT_INSTANCE_HEADER)
424
+ const clientInstance = instanceRaw && INSTANCE_RE.test(instanceRaw) ? instanceRaw : null
425
+ const said = headerValue(req.headers, CLIENT_SURFACE_HEADER)
426
+ // 6.99.29: any X-Cos-Token header at all, valid or not, rules the web path out. The token is
427
+ // what agents on this Mac hold; a request that shows it is never Miles's browser.
428
+ const tokenRaw = req.headers[TOKEN_HEADER]
429
+ const tokenPresented = Array.isArray(tokenRaw) ? tokenRaw.length > 0 : typeof tokenRaw === 'string' && tokenRaw.length > 0
430
+ let webSession = false
431
+ if (!tokenPresented && this.deps.webSession) {
432
+ try { webSession = this.deps.webSession(req) === true } catch { webSession = false }
433
+ }
434
+ const local = loopback || ownAddress
435
+ const client: TurnClient = local ? 'local' : !known ? 'unknown' : webSession ? 'web' : said === 'glasses' || said === 'phone' ? said : 'unknown'
436
+ let verifiedOrigin = false
437
+ let reason: string | null = null
438
+ if (!known) reason = 'unknown_socket'
439
+ else if (local) reason = 'local_socket'
440
+ else if (webSession) {
441
+ // The cookie path passed, no token was shown, and the socket is not this Mac. RECORDED, never
442
+ // vouched: the cookie is an HMAC of the API token, so any token holder can mint one (header).
443
+ reason = WEB_UNVOUCHED_REASON
444
+ } else if (clientInstance === null) reason = 'no_client_instance'
445
+ else {
446
+ let paired: string | null = null
447
+ try { paired = this.deps.pairedInstance(remote, this.now(), PAIRED_INSTANCE_FRESH_MS) } catch { paired = null }
448
+ verifiedOrigin = paired !== null && paired === clientInstance
449
+ if (!verifiedOrigin) reason = 'not_ring_owner'
450
+ }
451
+ return {
452
+ origin: { remote: known ? remote : 'unknown', loopback, ownAddress, clientInstance, client, tokenPresented, webSession },
453
+ verifiedOrigin,
454
+ reason,
455
+ judgedAt: this.now(),
456
+ }
457
+ }
458
+
459
+ /**
460
+ * The text to deliver: with the footer for a vouched glasses or phone turn, otherwise unchanged.
461
+ * `provider` is the harness the text is delivered into (claude, codex or cursor), named in the
462
+ * footer's verify command; every caller knows it at this point (Codex re-audit of 910eb90).
463
+ */
464
+ footed(prompt: string, turnId: string, verdict: OriginVerdict, provider: string): string {
465
+ if (!footerApplies(verdict) || !G2_TURN_ID_RE.test(turnId)) return prompt
466
+ return `${prompt.replace(/[\t\n\v\f\r ]+$/, '')}\n${turnFooter(turnId, verdict.origin.client, provider)}`
467
+ }
468
+
469
+ private build(input: { turnId: string; prompt: string; verdict: OriginVerdict; provider: string; sessionId: string; via: string; fork?: boolean; resendOf?: string }): G2TurnRecord {
470
+ const originKind = originKindOf(input.verdict.origin.client)
471
+ // A web turn is never vouched, whatever verdict reached here (header).
472
+ const verifiedOrigin = input.verdict.verifiedOrigin === true && originKind !== 'web'
473
+ const reason = verifiedOrigin ? null : (originKind === 'web' ? WEB_UNVOUCHED_REASON : (input.verdict.reason ?? 'unverified'))
474
+ return {
475
+ turnId: input.turnId,
476
+ createdAt: new Date(this.now()).toISOString(),
477
+ textSha256: turnTextSha256(input.prompt),
478
+ origin: { ...input.verdict.origin },
479
+ originKind,
480
+ verifiedOrigin,
481
+ reason,
482
+ originJudgedAt: typeof input.verdict.judgedAt === 'number' ? input.verdict.judgedAt : this.now(),
483
+ target: { provider: input.provider, sessionId: input.sessionId, ...(input.fork ? { fork: true as const } : {}) },
484
+ via: input.via,
485
+ ...(typeof input.resendOf === 'string' && input.resendOf.length > 0 ? { resendOf: input.resendOf } : {}),
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Whether delivering this candidate under `turnId` would reuse an id this process already holds
491
+ * for a DIFFERENT turn. An identical retry (same target, text and origin) is not reuse. The routes
492
+ * refuse a reuse before anything is sent. Never throws (a throw reads as reuse: refuse).
493
+ */
494
+ reusesTurnId(turnId: string, candidate: TurnCandidate, opts: { continues?: boolean } = {}): boolean {
495
+ try {
496
+ const held = this.deps.ledger.held(turnId)
497
+ if (!held) return false
498
+ return this.conflicts(held, this.build({ turnId, via: 'check', ...candidate }), opts.continues === true)
499
+ } catch {
500
+ return true
501
+ }
502
+ }
503
+
504
+ /**
505
+ * The one reuse rule. A held record is extended only by an identical retry, or by the server's
506
+ * own continuation of a turn that is STILL PARKED (a queue, edit or Send again record). A
507
+ * continuation never replaces a delivered record: a row written into a queue file by hand under
508
+ * a delivered turn's id cannot drain over it.
509
+ */
510
+ private conflicts(held: G2TurnRecord, next: G2TurnRecord, continues: boolean): boolean {
511
+ if (sameTurn(held, next)) return false
512
+ return !(continues && PARKED_TURN_VIAS.has(held.via))
513
+ }
514
+
515
+ /** Whether this process holds any record under this id (a throw reads as held: refuse). */
516
+ holds(turnId: string): boolean {
517
+ try { return this.deps.ledger.held(turnId) !== null } catch { return true }
518
+ }
519
+
520
+ /**
521
+ * Ledger one delivered or queued turn. `prompt` is the text WITHOUT the footer. Never throws.
522
+ * `continues`: the server's own continuation of the turn parked under this id (drain carry, queue
523
+ * edit, Cursor Stop hand-off), which may extend a parked record. Otherwise a held id is never
524
+ * replaced: an identical retry keeps the first record, a different turn is refused (null, nothing
525
+ * written).
526
+ */
527
+ record(input: { turnId: string; prompt: string; verdict: OriginVerdict; provider: string; sessionId: string; via: string; fork?: boolean; continues?: boolean; resendOf?: string }): G2TurnRecord | null {
528
+ try {
529
+ const record = this.build(input)
530
+ const held = this.deps.ledger.held(input.turnId)
531
+ if (held) {
532
+ if (this.conflicts(held, record, input.continues === true)) {
533
+ console.warn(`[g2-turn-ledger] turn id reuse refused turnId=${input.turnId} via=${input.via} held=${held.via}`)
534
+ return null
535
+ }
536
+ // An identical retry that is not a continuation keeps the first record (and its time).
537
+ if (input.continues !== true && sameTurn(held, record)) return held
538
+ }
539
+ this.deps.ledger.append(record, { replace: held !== null })
540
+ return record
541
+ } catch (error) {
542
+ console.error(`[g2-turn-ledger] record failed: ${error instanceof Error ? error.message : error}`)
543
+ return null
544
+ }
545
+ }
546
+
547
+ private queueKey(provider: string, threadId: string, clientTurnId: string): string {
548
+ return `${provider}\u0000${threadId}\u0000${clientTurnId}`
549
+ }
550
+
551
+ /**
552
+ * A turn parked in the queue (or edited there): remember who said these words. The origin lapses
553
+ * QUEUED_ORIGIN_TTL_MS after the verdict was judged (`verdict.judgedAt`), never after this call:
554
+ * an origin cannot be made to last longer by noting it again.
555
+ */
556
+ noteQueued(provider: string, threadId: string, clientTurnId: string, prompt: string, verdict: OriginVerdict, attachments = ''): void {
557
+ const at = this.now()
558
+ for (const [k, held] of this.queued) if (at - held.judgedAt > QUEUED_ORIGIN_TTL_MS) this.queued.delete(k)
559
+ const key = this.queueKey(provider, threadId, clientTurnId)
560
+ this.queued.delete(key)
561
+ this.queued.set(key, { verdict, textSha256: turnTextSha256(prompt), judgedAt: typeof verdict.judgedAt === 'number' ? verdict.judgedAt : at, attachments })
562
+ while (this.queued.size > QUEUED_ORIGIN_MAX) this.queued.delete(this.queued.keys().next().value as string)
563
+ }
564
+
565
+ forgetQueued(provider: string, threadId: string, clientTurnId: string): void {
566
+ this.queued.delete(this.queueKey(provider, threadId, clientTurnId))
567
+ }
568
+
569
+ /**
570
+ * The origin remembered for this queued turn, only while its text is the text it vouched for and
571
+ * within QUEUED_ORIGIN_TTL_MS of when it was judged. A lapsed origin is forgotten here.
572
+ */
573
+ queuedVerdict(provider: string, threadId: string, clientTurnId: string, prompt: string): OriginVerdict | null {
574
+ return this.queuedOrigin(provider, threadId, clientTurnId, prompt)?.verdict ?? null
575
+ }
576
+
577
+ private queuedOrigin(provider: string, threadId: string, clientTurnId: string, prompt: string): QueuedOrigin | null {
578
+ const key = this.queueKey(provider, threadId, clientTurnId)
579
+ const held = this.queued.get(key)
580
+ if (!held) return null
581
+ if (this.now() - held.judgedAt > QUEUED_ORIGIN_TTL_MS) { this.queued.delete(key); return null }
582
+ if (held.textSha256 !== turnTextSha256(prompt)) return null
583
+ return held
584
+ }
585
+
586
+ /**
587
+ * Send again (Codex re-audit of 910eb90, P1). The row's words are MUTABLE (any local process can
588
+ * rewrite the queue file), so a copy never inherits the original row's authority: inheriting it by
589
+ * id once let a local caller change a refused G2 row's words or files and mint a new vouched turn
590
+ * from them. The copy is judged by THE REQUEST THAT SENT IT AGAIN, fresh: a G2 or phone client
591
+ * resending is vouched by its own socket and ring-owner instance, exactly like a new turn, and
592
+ * anything else (an agent's token over loopback, a web cookie, an unknown caller) is recorded
593
+ * unvouched with RESEND_NOT_REVERIFIED_REASON, so it gets no footer. The original id survives only
594
+ * as the record's `resendOf` annotation, which vouches for nothing.
595
+ */
596
+ resendVerdict(req: OriginRequest): OriginVerdict {
597
+ const fresh = this.originOf(req)
598
+ return fresh.verifiedOrigin === true ? fresh : unvouched(fresh, RESEND_NOT_REVERIFIED_REASON)
599
+ }
600
+
601
+ /**
602
+ * A single-use token for the drainer's loopback hop. ALWAYS issued for a drain: it carries the
603
+ * parked origin while one is held for these words (else none, and the turn route judges the hop as
604
+ * the local call it is), and it marks the hop as the queue's own continuation of the parked turn,
605
+ * the one caller allowed to add to that turn's ledger record. Null only if no token can be made.
606
+ */
607
+ issueCarry(turn: { provider: string; threadId: string; clientTurnId: string; prompt: string }): string | null {
608
+ try {
609
+ const held = this.queuedOrigin(turn.provider, turn.threadId, turn.clientTurnId, turn.prompt)
610
+ const at = this.now()
611
+ for (const [token, carry] of this.carries) if (carry.expiresAt <= at) this.carries.delete(token)
612
+ const token = (this.deps.randomToken ?? (() => randomBytes(32).toString('hex')))()
613
+ // The seal rides with the origin: the turn route checks the binding's provider and thread and
614
+ // the files it is about to compose against it, and refuses a vouched turn that no longer matches.
615
+ const seal: QueueSeal | null = held ? { provider: turn.provider, threadId: turn.threadId, attachments: held.attachments } : null
616
+ this.carries.set(token, { clientTurnId: turn.clientTurnId, verdict: held?.verdict ?? null, seal, textSha256: turnTextSha256(turn.prompt), expiresAt: at + TURN_CARRY_TTL_MS })
617
+ return token
618
+ } catch {
619
+ return null
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Spend a carry token. Null unless it is live, unspent, and names this turn and these words;
625
+ * otherwise `{ verdict, seal }`: the parked origin and what it is sealed to, or nulls when none was
626
+ * left to carry.
627
+ */
628
+ takeCarry(token: unknown, clientTurnId: string, prompt: string): { verdict: OriginVerdict | null; seal: QueueSeal | null } | null {
629
+ if (typeof token !== 'string' || token.length === 0) return null
630
+ const carry = this.carries.get(token)
631
+ if (!carry) return null
632
+ this.carries.delete(token)
633
+ if (carry.expiresAt <= this.now()) return null
634
+ if (carry.clientTurnId !== clientTurnId || carry.textSha256 !== turnTextSha256(prompt)) return null
635
+ return { verdict: carry.verdict, seal: carry.seal }
636
+ }
637
+ }
638
+
639
+ /** One answer of `GET /api/g2-turns/:turnId`. */
640
+ export async function describeG2Turn(provenance: G2TurnProvenance, turnId: string, now: number): Promise<Record<string, unknown> | null> {
641
+ const found = await provenance.ledger.find(turnId)
642
+ if (!found) return null
643
+ const { record, sealed } = found
644
+ // Codex re-audit of 910eb90 (P2): the age of an APPROVAL is measured from when its origin was
645
+ // judged (the request Miles sent), never from when it was delivered. A queued, edited or Cursor
646
+ // Stop turn is recorded again at delivery with a new createdAt; measuring from that made a turn
647
+ // that waited 20 minutes in the queue read as seconds old to the verifier's 15-minute check. The
648
+ // earlier of the two is used, so an approval can never read younger than its delivery.
649
+ const createdMs = Date.parse(record.createdAt)
650
+ const judgedMs = typeof record.originJudgedAt === 'number' && Number.isFinite(record.originJudgedAt) ? record.originJudgedAt : null
651
+ const fromMs = judgedMs !== null && Number.isFinite(createdMs) ? Math.min(judgedMs, createdMs) : (judgedMs ?? createdMs)
652
+ const ageMs = Number.isFinite(fromMs) ? Math.max(0, now - fromMs) : null
653
+ // 6.99.29: g2, phone or web (older records carry only `origin.client`).
654
+ const originKind = record.originKind ?? originKindOf(record.origin?.client)
655
+ // Only a record this process wrote and still holds vouches for anything (see the header), never a
656
+ // web turn (its cookie is derivable from the token), and never past G2_TURN_VOUCH_MAX_MS.
657
+ let reason: string | null = null
658
+ if (originKind === 'web') reason = WEB_UNVOUCHED_REASON
659
+ else if (record.verifiedOrigin !== true) reason = typeof record.reason === 'string' ? record.reason : 'unverified'
660
+ else if (!sealed) reason = 'not_sealed'
661
+ else if (ageMs === null || ageMs > G2_TURN_VOUCH_MAX_MS) reason = 'record_too_old'
662
+ return {
663
+ found: true,
664
+ turnId: record.turnId,
665
+ createdAt: record.createdAt,
666
+ /** When the origin this turn carries was judged; `ageMs` (and the vouch limit) run from here. */
667
+ originJudgedAt: Number.isFinite(fromMs) ? new Date(fromMs).toISOString() : null,
668
+ ageMs,
669
+ textSha256: record.textSha256,
670
+ origin: record.origin,
671
+ originKind,
672
+ verifiedOrigin: reason === null,
673
+ reason,
674
+ sealed,
675
+ target: record.target,
676
+ via: record.via,
677
+ ...(typeof record.resendOf === 'string' ? { resendOf: record.resendOf } : {}),
678
+ }
679
+ }