@gotcos/glasses-server 6.32.0 → 6.34.0

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/CHANGELOG.md CHANGED
@@ -2219,6 +2219,61 @@ unsaved capture, and makes batch status stop lying about finished work.
2219
2219
 
2220
2220
  # Changelog
2221
2221
 
2222
+ ## [6.34.0] - 2026-08-16
2223
+
2224
+ ### A continued turn now runs with the session's own permissions
2225
+
2226
+ - **This widens what Continue can do.** A prompt spoken into the glasses can run
2227
+ tools on the Mac with nobody at the keyboard. Authorized explicitly by Miles
2228
+ after his first real continued turn came back reporting that every tool was
2229
+ disabled, which is not the point of the feature.
2230
+ - Claude drops `--permission-mode plan` and the empty `--tools`/`--allowedTools`
2231
+ pair. Codex now uses `getCodexTrustMode()`, the same posture ordinary Codex runs
2232
+ use on the host, rather than a stricter one invented at this call site. Absent
2233
+ `COS_CODEX_SANDBOX=workspace-write` that is still read-only, so this is never
2234
+ more permissive than the rest of the server.
2235
+ - **Unchanged, and load-bearing:** `COS_THREAD_ATTACH_ENABLED` still gates the
2236
+ surface and off still leaves the routes unregistered; `findBannedPermissionArg`
2237
+ still rejects every real bypass at the spawn boundary; delivery is still gated
2238
+ on a fresh occupancy probe, the epoch floor, the per-target claim and the head
2239
+ watermark. Dropping a lockdown is not adding a bypass.
2240
+ - The old read-only posture had NO test asserting it was present, so it could have
2241
+ been deleted silently. The new posture is pinned, and so is the bypass ban.
2242
+
2243
+ ## [6.33.0] - 2026-08-16
2244
+
2245
+ ### One switch turns Continue on
2246
+
2247
+ Continue was gated by two environment variables, and the second one was
2248
+ invisible. `COS_THREAD_ATTACH_ENABLED=1` registered the write routes;
2249
+ `COS_THREAD_ATTACH_IDLE_HOLDER=1` was additionally needed before COS would write
2250
+ into a thread whose Mac window is open but idle. COS Control's "Continue agent
2251
+ threads" checkbox sets only the first, so health reported the feature ON while
2252
+ every single attempt was refused, because a developer's editor windows stay
2253
+ open and that is precisely the case the second flag covered.
2254
+
2255
+ - **`COS_THREAD_ATTACH_ENABLED=1` is now the whole answer.** It registers the two
2256
+ write routes and wires the transcript clock that tells an idle holder from a
2257
+ working one. Nothing else to set.
2258
+ - **`COS_THREAD_ATTACH_IDLE_HOLDER` is REMOVED, not deprecated.** If you set it
2259
+ by hand, it is now ignored and can be deleted from your LaunchAgent plist or
2260
+ shell profile. Leaving it in place changes nothing either way.
2261
+ - **COS Control needs no update for this.** Its existing checkbox already writes
2262
+ the surviving key, so the toggle that reported ON while refusing now reports
2263
+ ON and works.
2264
+
2265
+ The two switches were never independently useful, and folding them is the honest
2266
+ description of the decision rather than a convenience. The relaxation is what
2267
+ makes a silent fork possible, so "Continue is on" and "a fork may happen" are one
2268
+ choice, and one switch states it.
2269
+
2270
+ Every safety behaviour is unchanged. A holder measured WRITING is still refused
2271
+ with `native_thread_working`; a holder COS cannot measure is still refused with
2272
+ `live_desktop_process`, because only a positive idle observation relaxes the gate
2273
+ and an unreadable transcript is not one. Off still means the write routes are not
2274
+ registered at all, so a disabled server answers 404 rather than 403 and holds no
2275
+ reachable write code.
2276
+
2222
2277
  ## [6.32.0] - 2026-08-16
2223
2278
 
2224
2279
  ### Continue now blocks on WORKING, not on merely open
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.32.0",
3
+ "version": "6.34.0",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
package/server/index.ts CHANGED
@@ -20,15 +20,13 @@ import { transcribeRouter } from './routes/transcribe.js'
20
20
  import { sessionIndexRouter } from './routes/session-index.js'
21
21
  import { agentSessionsRouter } from './routes/agent-sessions.js'
22
22
  import { claudeSessionsRouter } from './routes/claude-sessions.js'
23
- import { createAgentSessionBindingsRouter } from './routes/agent-session-bindings.js'
23
+ import {
24
+ createAgentSessionBindingsRouter,
25
+ threadAttachEnabled,
26
+ } from './routes/agent-session-bindings.js'
24
27
  import { AgentSessionBindingRegistry } from './lib/agent-session-binding-registry.js'
25
28
  import { cosSpawnedPids } from './lib/agent-session-ownership-store.js'
26
- import {
27
- idleHolderContinueEnabled,
28
- realOccupancyDirs,
29
- realOccupancyProbes,
30
- withTranscriptClock,
31
- } from './lib/occupancy-probes.js'
29
+ import { buildOccupancyProbes, realOccupancyDirs } from './lib/occupancy-probes.js'
32
30
  import { realAttachedWorkspaceDeps, resolveAttachedWorkspace } from './lib/attached-workspace.js'
33
31
  import { deliverAttachedTurn, realAttachedTurnDeps } from './lib/attached-provider-adapter.js'
34
32
  import { forkThread, realForkDeps } from './lib/fork-thread.js'
@@ -310,13 +308,20 @@ const occupancyDirs = realOccupancyDirs()
310
308
  const nativeHeadDeps = realNativeHeadDeps()
311
309
  const attachedWorkspaceDeps = realAttachedWorkspaceDeps(nativeHeadDeps)
312
310
  /**
313
- * THE ONE PLACE the idle-holder relaxation is switched on (6.32.0).
311
+ * THE ONE PLACE the idle-holder relaxation is switched on, on the ONE flag
312
+ * that also registers the write routes (6.33.0).
314
313
  *
315
314
  * Without the clock, a foreign holder is terminal and Continue is refused for
316
315
  * any thread with a Claude Code window open on it — including the ones Miles
317
316
  * actually works in, because he leaves those windows open. With it, a holder
318
317
  * measured idle is continuable and only a holder measured WRITING is refused.
319
318
  *
319
+ * `threadAttachEnabled()` is the same call `createAgentSessionBindingsRouter`
320
+ * makes below to decide whether the two POST routes exist at all. Reading it
321
+ * here rather than a second env key is what makes "Continue is on" a single
322
+ * answer: the pair of flags this replaced could disagree, and did, reporting
323
+ * the feature enabled while every attempt was refused.
324
+ *
320
325
  * Both gates read this same object — the route's detector via `probes` below,
321
326
  * and the adapter's pre-spawn preflight via `occupancyProbes` at the closure
322
327
  * further down — so they cannot come to different conclusions. That is the whole
@@ -325,9 +330,7 @@ const attachedWorkspaceDeps = realAttachedWorkspaceDeps(nativeHeadDeps)
325
330
  * Read the canary evidence in `thread-occupancy.ts` under THE IDLE-HOLDER
326
331
  * RELAXATION before changing this line.
327
332
  */
328
- const occupancyProbes = idleHolderContinueEnabled()
329
- ? withTranscriptClock(realOccupancyProbes(cosSpawnedPids), nativeHeadDeps)
330
- : realOccupancyProbes(cosSpawnedPids)
333
+ const occupancyProbes = buildOccupancyProbes(cosSpawnedPids, nativeHeadDeps, threadAttachEnabled())
331
334
 
332
335
  /**
333
336
  * The shim between the route's request shape and the adapter's.
@@ -93,6 +93,7 @@ import { isValidNativeThreadId } from './native-thread-id.js'
93
93
  import { isBindableProvider, type BindableProvider } from './agent-session-binding-store.js'
94
94
  import { recordCosSpawn, releaseCosSpawn } from './agent-session-ownership-store.js'
95
95
  import { processStartMs as realProcessStartMs } from './occupancy-probes.js'
96
+ import { getCodexTrustMode } from './codex-run-ledger.js'
96
97
 
97
98
  /** Providers with a certified attached path. Cursor is Fork-only (plan 2.5). */
98
99
  export type AttachedProvider = BindableProvider
@@ -529,17 +530,35 @@ const STRIPPED_ENV_KEYS: readonly string[] = ['CLAUDECODE', 'COS_API_TOKEN']
529
530
  // ---------------------------------------------------------------------------
530
531
 
531
532
  /**
532
- * Claude: `claude -p --resume <id>`, read-only, prompt on stdin.
533
+ * Claude: `claude -p --resume <id>`, prompt on stdin.
533
534
  *
534
535
  * The prompt is NOT an argv element. argv is world-readable through `ps`, and
535
536
  * the prompt is the user's private text; stdin also matches what both ordinary
536
537
  * bridges already do.
537
538
  *
538
- * Two independent read-only layers, because either alone is one assumption
539
- * deep: `--permission-mode plan` means the model cannot mutate regardless of
540
- * its tool list, and the empty `--tools`/`--allowedTools` pair is the exact
541
- * text-only posture `claude-permissions.ts` already ships for untrusted mode.
542
- * If an older CLI rejects `plan`, it exits non-zero and we fail closed.
539
+ * THE TURN INHERITS THE SESSION'S OWN PERMISSIONS (Miles, 2026-08-16, explicit).
540
+ *
541
+ * This used to force two independent read-only layers: `--permission-mode plan`
542
+ * plus an empty `--tools`/`--allowedTools` pair. The result was a Continue that
543
+ * could talk into a thread and do nothing in it -- his first real continued turn
544
+ * came back reporting that every tool was disabled, which is not "like being at
545
+ * the desk". Both layers are gone; a resumed turn now runs as that session
546
+ * ordinarily would.
547
+ *
548
+ * What that means plainly: a prompt spoken into a pair of glasses can run tools
549
+ * on this Mac with nobody at the keyboard. That is the intent, not an oversight.
550
+ * The protections that remain, and that must not be quietly removed with it:
551
+ *
552
+ * 1. `COS_THREAD_ATTACH_ENABLED` still gates the whole surface, and off means
553
+ * the write routes are never registered.
554
+ * 2. `findBannedPermissionArg` still runs at the spawn boundary and still
555
+ * rejects the actual bypasses -- `--dangerously-skip-permissions`, `--yolo`,
556
+ * `danger-full-access` and the rest. Dropping a lockdown is not the same as
557
+ * adding a bypass, and the bypass ban is unchanged.
558
+ * 3. The menu cursor rests on a row that cannot act, so a false-positive ring
559
+ * tap cannot reach Continue (`defaultSessionThreadActionIndex`).
560
+ * 4. Delivery is still gated on a fresh occupancy probe, the epoch floor, the
561
+ * per-target claim and the head watermark.
543
562
  */
544
563
  export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
545
564
  return [
@@ -549,9 +568,6 @@ export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
549
568
  // never observe the session id we are required to verify.
550
569
  '--verbose',
551
570
  '--resume', nativeThreadId,
552
- '--permission-mode', 'plan',
553
- '--tools', '',
554
- '--allowedTools', '',
555
571
  ]
556
572
  }
557
573
 
@@ -571,7 +587,10 @@ export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
571
587
  export function buildCodexAttachedArgs(nativeThreadId: string, cwd: string): string[] {
572
588
  return [
573
589
  'exec',
574
- '--sandbox', 'read-only',
590
+ // The posture ordinary Codex runs use on this host, not a stricter one
591
+ // invented here. `COS_CODEX_SANDBOX=workspace-write` opts in; absent stays
592
+ // read-only, so this is never MORE permissive than the rest of the server.
593
+ '--sandbox', getCodexTrustMode(),
575
594
  '--cd', cwd,
576
595
  'resume',
577
596
  '--json',
@@ -446,21 +446,39 @@ export function realOccupancyProbes(ledger: SpawnLedgerAccessor): OccupancyProbe
446
446
  }
447
447
 
448
448
  /**
449
- * Is the idle-holder relaxation switched on for this install?
449
+ * The probe set this install should run, clock and all.
450
450
  *
451
- * ONE READER, exported so every other module imports it rather than re-reading
452
- * `process.env` the same anti-drift rule `threadAttachEnabled` follows, and for
453
- * the same reason: a second copy is how a surface comes to advertise a write path
454
- * the gate will refuse.
451
+ * ONE DECISION, IN ONE PLACE (6.33.0). `attachEnabled` is the SAME answer that
452
+ * decides whether the two write routes get registered `threadAttachEnabled()`,
453
+ * passed in by the composition root rather than re-read here. There is exactly
454
+ * one reader of `COS_THREAD_ATTACH_ENABLED` in the server, and a second copy is
455
+ * how a surface comes to advertise a write path the gate will refuse.
455
456
  *
456
- * `=== '1'` opt-IN, not `!== '0'` opt-out. This decides whether COS may write
457
- * into a conversation a human still has open, so anything ambiguous unset,
458
- * empty, 'true', 'yes' reads as OFF. It composes with
459
- * `COS_THREAD_ATTACH_ENABLED`: with attach itself off, this flag does nothing at
460
- * all, because there is no write path to relax.
457
+ * WHY THE CLOCK RIDES THE SAME SWITCH. In 6.32.0 the relaxation had a second
458
+ * env key of its own (named in the 6.33.0 changelog entry; it is now ignored),
459
+ * and the two were never independently useful: without the clock, Continue is
460
+ * refused on every thread with an editor window open, which for a working
461
+ * developer is all of them. The pair also cost a real testing cycle, because
462
+ * COS Control's checkbox knew only the first key and reported the feature ON
463
+ * while the gate still refused every session. And the honest reading is that
464
+ * they were always ONE decision: the relaxation is what makes a silent fork
465
+ * possible, so "Continue is on" and "fork is possible" are the same choice, and
466
+ * one switch says so.
467
+ *
468
+ * Attach off is still byte-for-byte the pre-6.28 posture: no clock, so every
469
+ * foreign holder reads `unknown` and refuses, and the write routes do not exist
470
+ * to be reached anyway.
461
471
  */
462
- export function idleHolderContinueEnabled(): boolean {
463
- return process.env.COS_THREAD_ATTACH_IDLE_HOLDER === '1'
472
+ export function buildOccupancyProbes(
473
+ ledger: SpawnLedgerAccessor,
474
+ headDeps: NativeHeadDeps,
475
+ attachEnabled: boolean,
476
+ ): OccupancyProbes {
477
+ const base = realOccupancyProbes(ledger)
478
+ // Strictly `=== true`. A caller that hands over a truthy non-boolean has not
479
+ // answered the question, and the permissive branch is the one that must be
480
+ // earned by a real yes.
481
+ return attachEnabled === true ? withTranscriptClock(base, headDeps) : base
464
482
  }
465
483
 
466
484
  /**
@@ -142,8 +142,9 @@ export interface OccupancyProbes {
142
142
  * foreign holder stays `live_desktop_process` — byte-for-byte the behaviour
143
143
  * that shipped before the idle-holder relaxation existed. That is why it is
144
144
  * optional rather than required: the strict gate is what you get by doing
145
- * nothing, and the relaxation has to be wired on purpose (see `server/index.ts`,
146
- * where the wiring is itself behind `COS_THREAD_ATTACH_IDLE_HOLDER`).
145
+ * nothing, and the relaxation is wired on purpose by `buildOccupancyProbes`
146
+ * in `occupancy-probes.ts`, which attaches the clock only when thread attach
147
+ * itself is on.
147
148
  *
148
149
  * Null is NOT idle. See `holderActivity` for why that distinction is the whole
149
150
  * safety property here.
@@ -525,9 +526,12 @@ export interface OccupancyDirs {
525
526
  // `isActiveRecently` here; its `false` means "stale OR unmeasurable", and only
526
527
  // `holderActivity` separates those.
527
528
  //
528
- // TURNING IT OFF. Unwire `probes.transcriptMtimeMs` (in practice: unset
529
- // `COS_THREAD_ATTACH_IDLE_HOLDER`). Every foreign holder then reads `unknown`
530
- // and refuses, which is the pre-6.32.0 gate exactly. No rollback needed.
529
+ // TURNING IT OFF. Unwire `probes.transcriptMtimeMs`. Since 6.33.0 that happens
530
+ // by unsetting `COS_THREAD_ATTACH_ENABLED`, the one switch that also unregisters
531
+ // the write routes: the relaxation is what makes a silent fork possible, so it
532
+ // is the same decision as Continue itself and no longer has a flag of its own.
533
+ // Every foreign holder then reads `unknown` and refuses, which is the pre-6.32.0
534
+ // gate exactly, and with attach off there is no write route to reach anyway.
531
535
 
532
536
  /**
533
537
  * The Phase 0 attach precondition.