@gotcos/glasses-server 6.61.6 → 6.62.1
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 +6 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -1
- package/bin/cli.cjs +50 -3
- package/bin/hooks/cos-cursor-observer.cjs +36 -0
- package/bin/hooks/cos-session-hook +46 -11
- package/docs/provider-parity.md +43 -0
- package/managed-runtime-contract.json +1 -0
- package/package.json +5 -2
- package/server/index.ts +135 -16
- package/server/lib/agent-session-binding-store.ts +9 -3
- package/server/lib/agent-session-store.ts +7 -4
- package/server/lib/attached-provider-adapter.ts +158 -45
- package/server/lib/banned-permission-args.ts +42 -1
- package/server/lib/claude-bridge.ts +17 -8
- package/server/lib/claude-hooks-installer.ts +28 -5
- package/server/lib/codex-bridge.ts +17 -2
- package/server/lib/codex-hooks-installer.ts +538 -0
- package/server/lib/codex-live-queue.ts +1 -1
- package/server/lib/codex-model-catalog.ts +49 -2
- package/server/lib/codex-rollout-state.ts +90 -0
- package/server/lib/codex-session-posture.ts +528 -0
- package/server/lib/codex-subagent-activity.ts +119 -0
- package/server/lib/continue-plan.ts +180 -0
- package/server/lib/continue-read-health.ts +26 -0
- package/server/lib/cursor-bridge.ts +26 -6
- package/server/lib/cursor-fork-context.ts +140 -0
- package/server/lib/cursor-model-catalog.ts +62 -5
- package/server/lib/cursor-observer-installer.ts +79 -0
- package/server/lib/cursor-session-model.ts +339 -0
- package/server/lib/cursor-spawn-env.ts +210 -0
- package/server/lib/fork-thread.ts +143 -25
- package/server/lib/health-static-probes.ts +21 -11
- package/server/lib/lens-gist-engines.ts +15 -4
- package/server/lib/lens-gist.ts +1 -1
- package/server/lib/live-cues-cursor.ts +16 -6
- package/server/lib/occupancy-probes.ts +45 -1
- package/server/lib/permission-broker.ts +24 -9
- package/server/lib/provider-actions.ts +149 -0
- package/server/lib/provider-failure-text.ts +27 -0
- package/server/lib/provider-observe.ts +212 -0
- package/server/lib/provider-proof.ts +23 -14
- package/server/lib/query-job-runtime.ts +16 -1
- package/server/lib/query-job-types.ts +2 -1
- package/server/lib/session-act-fields.ts +61 -0
- package/server/lib/session-cancel.ts +95 -4
- package/server/lib/session-continue-facts-real.ts +74 -0
- package/server/lib/session-continue-facts.ts +267 -0
- package/server/lib/session-halt.ts +67 -6
- package/server/lib/session-hook-events.ts +60 -0
- package/server/lib/session-hook-ledger.ts +21 -3
- package/server/lib/session-hook-spool.ts +68 -6
- package/server/lib/session-hooks-runtime.ts +40 -3
- package/server/lib/session-peer-inbox-deps.ts +7 -6
- package/server/lib/session-recommendation.ts +3 -2
- package/server/lib/session-signal-store.ts +81 -3
- package/server/lib/session-stream-events.ts +32 -1
- package/server/lib/session-transcript-watcher.ts +25 -1
- package/server/lib/session-work-observation.ts +46 -0
- package/server/lib/thread-attach-capability.ts +15 -1
- package/server/lib/thread-drain-kick.ts +4 -2
- package/server/lib/thread-occupancy.ts +44 -0
- package/server/lib/thread-turn-queue-deliver.ts +6 -1
- package/server/lib/thread-turn-queue.ts +6 -0
- package/server/lib/turn-request.ts +274 -0
- package/server/routes/agent-session-bindings.ts +224 -22
- package/server/routes/agent-session-stream.ts +26 -7
- package/server/routes/agent-sessions.ts +163 -10
- package/server/routes/health.ts +27 -2
- package/server/routes/session-hooks.ts +60 -3
- package/server/routes/thread-turn-queue.ts +37 -9
- package/server/scripts/hooks-cli.ts +38 -9
package/.env.example
CHANGED
|
@@ -334,3 +334,9 @@ BIND_HOST=0.0.0.0
|
|
|
334
334
|
# time <scripts>/venv/bin/python3 <scripts>/lightrag_search.py --explore "Some Entity"
|
|
335
335
|
# Consistently under ~30s, turn it on. Otherwise leave it off — cues still get
|
|
336
336
|
# memory grounding from Qdrant, which returns in ~1.5s.
|
|
337
|
+
|
|
338
|
+
# Sessions Continue: 0 restores the 6.61 posture (Codex configured sandbox,
|
|
339
|
+
# Cursor Ask). Default 1 preserves session settings; Cursor Run Everything
|
|
340
|
+
# additionally requires a client that acknowledges displaying continue_note.
|
|
341
|
+
# Keep this in ~/.cos-glasses/.env; Control does not yet persist it on update.
|
|
342
|
+
COS_CONTINUE_FULL_PERMISSIONS=1
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## 6.62.1 — 2026-10-05 — Combined provider parity and handoff hardening
|
|
2
|
+
|
|
3
|
+
- Includes the 6.62.0 provider parity, cancellation, hook readiness, and safe Cursor continuation updates.
|
|
4
|
+
- A failed fork names its copy when available and distinguishes usage limits, authentication, and context overflow.
|
|
5
|
+
- Claude, Codex, and Cursor prompt hooks receive a private, short-lived binding to the current user request.
|
|
6
|
+
- Compatible with existing glasses; build 6.10.600 displays the server continuation note and opts into the matching Cursor behavior.
|
|
7
|
+
|
|
8
|
+
### Included 6.62.0 candidate work — Provider parity: Claude, Codex and Cursor session visibility and continuation
|
|
9
|
+
|
|
10
|
+
Originally prepared for a candidate release; these changes are included in 6.62.1. The standalone 6.62.0 npm release was skipped. Pairs with glasses 6.10.600 (copy only); older glasses keep working.
|
|
11
|
+
|
|
12
|
+
**Permission change on Continue (read this).** A Continue now carries the session's OWN permissions:
|
|
13
|
+
- Codex: the session's model, effort and sandbox from its latest `turn_context`. A Desktop thread recorded as `danger-full-access` resumes with it, but only when its folder is already trusted in `~/.codex/config.toml`; otherwise `workspace-write` (network as the session had it). Unreadable settings fall back to the 6.61 posture with a note. 6.61 resumed every Codex Continue read-only.
|
|
14
|
+
- Cursor: Run Everything (`--force`) on the session's own model only for clients sending `X-COS-Continue-Note: 1`; older clients retain Ask mode (6.61: Ask mode on Composer 2.5 Fast). Forks stay read-only for every provider; Cursor's Fork is a new read-only chat seeded with the session's context.
|
|
15
|
+
- Claude: unchanged.
|
|
16
|
+
- Kill switch: `COS_CONTINUE_FULL_PERMISSIONS=0` in `~/.cos-glasses/.env` restores the 6.61 argv byte for byte. Detail and the attach verdict carry a `continue_note` (≤60 chars) saying what will run.
|
|
17
|
+
|
|
18
|
+
**Seeing the work.**
|
|
19
|
+
- One working hint for every provider: an open hook turn reads active for up to 5 minutes after its last event (Esc without Stop decays), on top of the 6.61 reading; Cursor rows are decided by their hooks (they were always quiet before). Codex reads its rollout turn markers until its hooks are trusted.
|
|
20
|
+
- Codex hooks: `--hooks install` (and Control's Install hooks) also writes the COS hook into `~/.codex/hooks.json`. Codex runs untrusted hooks silently never, so run `codex` in Terminal and trust them once ("Trust all and continue"). Trust is read through Codex's own `hooks/list`; COS never writes hook trust. Continue never adds project trust; Codex itself may write project trust when starting a new session. Claude's hooks state, banner and desk cancel stay Claude-only.
|
|
21
|
+
- Cursor observer: its spool files were rejected since 6.61.7 (filename nonce); now accepted, recent rejects re-ingested once, `afterAgentThought` gives Cursor a reasoning line (spooled on disk until drained, then memory and stream only; never the ledger). The observer stops adding thoughts when the drain is stale.
|
|
22
|
+
- `reported_model` on detail for all three; list polling avoids model lookups. Cursor titles use the composer name or the first query. Codex rollout completion can close an observed compaction phase; compaction start still needs hook evidence.
|
|
23
|
+
- `/api/health` and `/api/models`: `providers.{claude,codex,cursor}.{observe,act}` with a reason for every "no"; `threadForkProviders`; `features.sessionCancel.deskCodex` / `deskCursor`.
|
|
24
|
+
|
|
25
|
+
**Acting the same.**
|
|
26
|
+
- Cancel run stops desk Codex (trusted hooks) and desk Cursor runs at their next command, like Claude, after provider-stamped hooks have been observed for that thread and the current hook is ready. Codex gets a deny-only halt reply (it fails open on Claude's); Claude's reply bytes are unchanged and the 6.61.7 script stays halt-ready.
|
|
27
|
+
- Live Continue into an open Claude session is on by default (`COS_CONTINUE_LIVE=0` turns it off). A busy Codex Desktop thread takes a Continue straight into its own queue when COS has nothing queued for it. That hop accepts the moving native head and enters the app queue, which is not shown in the COS Queue. An Interrupt kicks the queue like a Stop. Cursor CLI queued turns auto-run after the chat is free; the stored client acknowledgement determines Ask versus Run Everything.
|
|
28
|
+
- Every Cursor CLI spawn runs on its own config dir (copy of the user's config, chats/hooks/MCP/agents/plans linked), and Continue/Fork refuse if isolation fails, so COS no longer overwrites the user's Cursor default model.
|
|
29
|
+
- Codex and Cursor Messages jobs link to their native session at the first event.
|
|
30
|
+
|
|
31
|
+
Engine limits kept, with honest copy: an idle Cursor IDE composer cannot take an outside message; Cursor approvals cannot be answered remotely in `-p`; a ChatGPT.app thread cannot be steered mid-turn; pure `js` Codex steps are not caught by a cancel.
|
|
32
|
+
|
|
33
|
+
Codex hook trust is refreshed through a bounded `codex app-server` call every five minutes. Cursor hook-open turns quiet beyond five minutes stay OPEN (not actively writing) up to the 30-minute ceiling. Claude/Codex end-state visibility may lag by 30 seconds. Control does not yet persist the full-permissions kill switch through updates; set it in `.env`.
|
|
34
|
+
|
|
35
|
+
## 6.61.7 — 2026-10-04 — Agent activity and compaction visibility
|
|
36
|
+
|
|
37
|
+
- Add display-only child activity to native Sessions list/detail. Codex uses read-only spawn relationships plus each child's lifecycle, with bounded reads, freshness, deduplication and explicit incomplete/unknown counts.
|
|
38
|
+
- Observe Claude subagent identities and compaction hooks. Refresh hooks after upgrading. Native Cursor observers cover subagent start/stop, context compaction and resumed output; installation preserves other hooks. Observers retain no prompts and never trigger queue drains or permission decisions.
|
|
39
|
+
- Stream past oversized top-level Codex context replacement records in bounded chunks instead of declaring completion and disconnecting. Other oversized records keep their existing fallback.
|
|
40
|
+
- Paired with glasses 6.10.590. No model routing or Cursor model-catalog changes. Automated fixture canaries pass; physical device and live-provider acceptance are recorded separately.
|
|
41
|
+
|
|
1
42
|
## 6.61.6 — 2026-10-03
|
|
2
43
|
|
|
3
44
|
- Keep Codex-owned waiting messages visible in Sessions queue counts and queue reads after the COS handoff. Unavailable native reads report unknown, not empty.
|
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ state drafts; the `cursor`, `epoch` and `id:` line stay.
|
|
|
206
206
|
Since 6.49.0 a Continue can land in the running session itself, so the Desktop tab or
|
|
207
207
|
the `claude` in a terminal that you left open shows the turn and answers it with its own
|
|
208
208
|
context, instead of a resume child writing to the transcript behind that window. It is
|
|
209
|
-
|
|
209
|
+
on by default since 6.62.0; set `COS_CONTINUE_LIVE=0` to disable it. The server writes the turn to the session's own inbox
|
|
210
210
|
(the socket Claude Code publishes in `~/.claude/sessions/<pid>.json`, in the line format
|
|
211
211
|
its help text documents), pinned to the full session id, and calls it delivered only
|
|
212
212
|
when the session's transcript shows the message accepted; every other outcome takes the
|
|
@@ -913,3 +913,7 @@ The package includes a checksummed public Python runtime. It uses a unique owner
|
|
|
913
913
|
Run `glasses-memory-setup --runtime-dir /absolute/path/to/cos-memory-runtime --data-root /absolute/path/to/private-memory` with Python 3.11+ installed. Set `COS_SCRIPTS_DIR` to the runtime directory in your server configuration, then restart the server. Add an explicitly granted Markdown/text source using `venv/bin/python3 manage.py grant /path/to/notes.md --title "Project"` from that runtime directory. The bundled README documents captures, reviews, document links, source refresh, upgrade and staged restore. Keep the latest independent deletion checkpoint separately from restorable snapshots.
|
|
914
914
|
|
|
915
915
|
This is a local owner instance, separate from app.gotcos.com tenants and hosted Sessions. Installing the package does not upload your corpus or enable household sharing. A source build or local setup test is not proof of a published registry version; use the release ledger for actual availability.
|
|
916
|
+
|
|
917
|
+
## Provider parity in 6.62.0
|
|
918
|
+
|
|
919
|
+
See [provider capabilities and limits](docs/provider-parity.md) for Continue permissions, client compatibility, hook trust, observation limits and rollback. Glasses 6.10.600 shows the server's Continue note. Older clients retain Cursor Ask behavior; forks remain read-only.
|
package/bin/cli.cjs
CHANGED
|
@@ -146,12 +146,13 @@ function setupSpeakerModel() {
|
|
|
146
146
|
return 0
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// --hooks install|status|uninstall: the Claude Code session hook (6.48.0)
|
|
149
|
+
// --hooks install|status|uninstall: the Claude Code session hook (6.48.0), and the same hook in
|
|
150
|
+
// Codex when Codex is on the Mac (6.62.0; `--codex` for Codex only). Runs before the
|
|
150
151
|
// banner and the runtime checks, resolving tsx itself; the managed runtime never comes
|
|
151
152
|
// through here (Control uses POST /api/session-hooks/install on the running server).
|
|
152
153
|
if (process.argv.includes('--hooks')) {
|
|
153
154
|
if (!['install', 'status', 'uninstall'].includes(HOOKS_ACTION)) {
|
|
154
|
-
console.error('Usage: --hooks install|status|uninstall [--dry-run] [--port N]')
|
|
155
|
+
console.error('Usage: --hooks install|status|uninstall [--dry-run] [--port N] [--codex]')
|
|
155
156
|
process.exit(64)
|
|
156
157
|
}
|
|
157
158
|
let hooksTsx
|
|
@@ -183,7 +184,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
183
184
|
console.log(' npx --yes @gotcos/glasses-server@latest --setup-transcription --transcription-tier balanced|max')
|
|
184
185
|
console.log(' npx --yes @gotcos/glasses-server@latest --setup-speaker-model')
|
|
185
186
|
console.log(' npx --yes @gotcos/glasses-server@latest --prepare-only')
|
|
186
|
-
console.log(' npx --yes @gotcos/glasses-server@latest --hooks install|status|uninstall [--dry-run] [--port N]')
|
|
187
|
+
console.log(' npx --yes @gotcos/glasses-server@latest --hooks install|status|uninstall [--dry-run] [--port N] [--codex]')
|
|
187
188
|
console.log('')
|
|
188
189
|
console.log(' Requirements:')
|
|
189
190
|
console.log(' - Node.js 20.11+')
|
|
@@ -280,9 +281,53 @@ function resolveCursorAgentBinary() {
|
|
|
280
281
|
const homeLocal = resolve(homedir(), '.local', 'bin', 'agent')
|
|
281
282
|
return existsSync(homeLocal) ? homeLocal : null
|
|
282
283
|
}
|
|
284
|
+
// 6.62.0 (plan 3.4): the launcher's two `agent` probes run on a config dir of their own,
|
|
285
|
+
// like every server spawn (server/lib/cursor-spawn-env.ts, which this mirrors: CommonJS
|
|
286
|
+
// cannot import it). Same link list; the census test pins that the two lists agree.
|
|
287
|
+
const CURSOR_SPAWN_LINKS = ['chats', 'hooks.json', 'mcp.json', 'plugins', 'skills-cursor', 'agents', 'plans']
|
|
288
|
+
function cursorSpawnEnvSync() {
|
|
289
|
+
const { mkdtempSync, symlinkSync, rmSync } = require('fs')
|
|
290
|
+
const sourceDir = process.env.CURSOR_CONFIG_DIR?.trim() || join(homedir(), '.cursor')
|
|
291
|
+
const root = join(CONFIG_DIR, 'cursor-cli')
|
|
292
|
+
let dir = null
|
|
293
|
+
const release = () => {
|
|
294
|
+
if (!dir) return
|
|
295
|
+
for (const name of CURSOR_SPAWN_LINKS) {
|
|
296
|
+
try { if (lstatSync(join(dir, name)).isSymbolicLink()) unlinkSync(join(dir, name)) } catch { /* absent */ }
|
|
297
|
+
}
|
|
298
|
+
try { rmSync(dir, { recursive: true, force: true }) } catch { /* best effort */ }
|
|
299
|
+
dir = null
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
mkdirSync(root, { recursive: true, mode: 0o700 })
|
|
303
|
+
dir = mkdtempSync(join(root, 'spawn-'))
|
|
304
|
+
chmodSync(dir, 0o700)
|
|
305
|
+
if (existsSync(join(sourceDir, 'cli-config.json'))) {
|
|
306
|
+
copyFileSync(join(sourceDir, 'cli-config.json'), join(dir, 'cli-config.json'))
|
|
307
|
+
chmodSync(join(dir, 'cli-config.json'), 0o600)
|
|
308
|
+
}
|
|
309
|
+
for (const name of CURSOR_SPAWN_LINKS) {
|
|
310
|
+
if (existsSync(join(sourceDir, name))) symlinkSync(join(sourceDir, name), join(dir, name))
|
|
311
|
+
}
|
|
312
|
+
return { env: { ...process.env, CURSOR_CONFIG_DIR: dir }, release }
|
|
313
|
+
} catch (error) {
|
|
314
|
+
release()
|
|
315
|
+
// 6.62.0 /qa (W6): fail open for these two read-only probes, but never silently.
|
|
316
|
+
console.warn(`[cursor-spawn-env] isolation unavailable for the launcher's agent probe, using the shared config: ${error && error.code ? error.code : error}`)
|
|
317
|
+
return { env: { ...process.env }, release: () => {} }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
283
320
|
function cursorCliState() {
|
|
284
321
|
const binary = resolveCursorAgentBinary()
|
|
285
322
|
if (!binary) return { binary: null, version: null, auth: null }
|
|
323
|
+
const isolation = cursorSpawnEnvSync()
|
|
324
|
+
try {
|
|
325
|
+
return cursorCliStateWith(binary, isolation.env)
|
|
326
|
+
} finally {
|
|
327
|
+
isolation.release()
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function cursorCliStateWith(binary, env) {
|
|
286
331
|
|
|
287
332
|
let version = 'available'
|
|
288
333
|
try {
|
|
@@ -290,6 +335,7 @@ function cursorCliState() {
|
|
|
290
335
|
encoding: 'utf8',
|
|
291
336
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
292
337
|
timeout: 5000,
|
|
338
|
+
env,
|
|
293
339
|
}).trim()
|
|
294
340
|
const versionLine = about.split('\n').map((line) => line.trim()).find((line) => /CLI Version|cursor|agent/i.test(line))
|
|
295
341
|
if (versionLine) version = versionLine
|
|
@@ -300,6 +346,7 @@ function cursorCliState() {
|
|
|
300
346
|
encoding: 'utf8',
|
|
301
347
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
302
348
|
timeout: 7000,
|
|
349
|
+
env,
|
|
303
350
|
})
|
|
304
351
|
// Cursor 2026.09 introduced `grok-4.7-high-fast` without the old
|
|
305
352
|
// `cursor-` prefix. Accept both exact high-fast families, like runtime does.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Read-only native Cursor lifecycle observer. No network, credentials, commands or prompts. An agent
|
|
3
|
+
// thought (6.62.0) is capped at 280 characters and display only: it sits in a 0600 spool file until
|
|
4
|
+
// the server drains it, then lives in memory and on the live feed, and is never written to the ledger.
|
|
5
|
+
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');
|
|
6
|
+
const event=process.argv[2],spool=process.argv[3];
|
|
7
|
+
const names={subagentStart:'SubagentStart',subagentStop:'SubagentStop',preCompact:'PreCompact',postToolUse:'PostToolUse',afterAgentResponse:'PostCompact',stop:'PostCompact',sessionEnd:'PostCompact',afterAgentThought:'AgentThought'};
|
|
8
|
+
let input='',oversize=false;
|
|
9
|
+
process.stdin.setEncoding('utf8');
|
|
10
|
+
process.stdin.on('data',chunk=>{if(input.length+chunk.length>1048576){oversize=true;input=''}else if(!oversize)input+=chunk});
|
|
11
|
+
process.stdin.on('error',()=>{});
|
|
12
|
+
process.stdin.on('end',()=>{
|
|
13
|
+
try {
|
|
14
|
+
if(!oversize && names[event] && spool){
|
|
15
|
+
const p=JSON.parse(input),id=p.parent_conversation_id||p.conversation_id||p.session_id;
|
|
16
|
+
if(typeof id==='string'&&/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)){
|
|
17
|
+
fs.mkdirSync(spool,{recursive:true,mode:0o700});
|
|
18
|
+
// 6.62.0 (QA W16): the hook script's guard. A drain stamp older than a day means nobody is
|
|
19
|
+
// reading the spool, so nothing more is written into it (thoughts least of all).
|
|
20
|
+
let stale=false;try{stale=Date.now()-fs.statSync(path.join(spool,'.last-drain')).mtimeMs>86400000}catch{}
|
|
21
|
+
if(!stale&&fs.readdirSync(spool).length<2000){
|
|
22
|
+
const ts=Date.now(),payload={session_id:id.toLowerCase(),display_only:true};
|
|
23
|
+
const agent=p.subagent_id||p.agent_id;
|
|
24
|
+
if(typeof agent==='string'&&/^[a-zA-Z0-9_-]{1,160}$/.test(agent))payload.agent_id=agent;
|
|
25
|
+
// 6.62.0: the thought itself, at most 280 characters. The server keeps it in memory only.
|
|
26
|
+
if(names[event]==='AgentThought'){if(typeof p.text!=='string'||!p.text.trim())throw 0;payload.text=p.text.slice(0,280)}
|
|
27
|
+
const name=ts+'-'+process.pid+'-'+crypto.randomBytes(6).toString('hex')+'-'+names[event]+'.json';
|
|
28
|
+
const temp=path.join(spool,'.'+name);
|
|
29
|
+
fs.writeFileSync(temp,JSON.stringify({ts,ppid:null,event:names[event],provider:'cursor',payload}),{mode:0o600,flag:'wx'});
|
|
30
|
+
fs.renameSync(temp,path.join(spool,name));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch {} // An observational failure never blocks a user action.
|
|
35
|
+
process.stdout.write(event==='subagentStart'?'{"permission":"allow"}\n':'{}\n');
|
|
36
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# COS session hook (glasses-server 6.48.0; Cursor Stop follow-up 6.51.0; halt check 6.53.0; single entry 6.53.3; away hold wait 6.60.0). Installed into ~/.claude/settings.json by
|
|
2
|
+
# COS session hook (glasses-server 6.48.0; Cursor Stop follow-up 6.51.0; halt check 6.53.0; single entry 6.53.3; away hold wait 6.60.0; Codex and Cursor parity 6.62.0). Installed into ~/.claude/settings.json by
|
|
3
3
|
# `--hooks install`; Claude Code runs it with the hook event name as $1 and the event
|
|
4
4
|
# payload on stdin. It spools one JSON file per event into the hook spool and exits 0
|
|
5
5
|
# on every path. Only PermissionRequest ever talks to the server, and only when the
|
|
@@ -19,10 +19,19 @@
|
|
|
19
19
|
#
|
|
20
20
|
# The installer bakes COS_GLASSES_HOME and COS_HOOK_SPOOL into the command, so this
|
|
21
21
|
# script and the server can never disagree about where the token or the spool is.
|
|
22
|
+
#
|
|
23
|
+
# 6.62.0: the same bytes also run from ~/.codex/hooks.json (as their own copy,
|
|
24
|
+
# cos-session-hook-codex), whose command adds COS_HOOK_PROVIDER=codex; a Codex payload
|
|
25
|
+
# reaching the Claude command is recognised too (codex_of, below). Codex changes three things
|
|
26
|
+
# only: the halt reply is deny-only (Codex rejects `continue:false` in a PreToolUse reply and
|
|
27
|
+
# then RUNS the tool, canary C10; the deny alone blocks it, C12), the spooled envelope says
|
|
28
|
+
# `"provider":"codex"`, and the Cursor Stop follow-up below never runs. Claude and Cursor
|
|
29
|
+
# share the Claude command and keep today's bytes; the server tells them apart by the payload.
|
|
22
30
|
PATH=/usr/bin:/bin:/usr/sbin; umask 077; trap '' PIPE
|
|
23
31
|
HOME=${HOME:-$(cd ~ 2>/dev/null && pwd)}; [ -n "$HOME" ] || exit 0
|
|
24
32
|
GH=${COS_GLASSES_HOME:-$HOME/.cos-glasses}
|
|
25
33
|
SPOOL=${COS_HOOK_SPOOL:-$GH/data/hook-spool}; EVENT="$1"
|
|
34
|
+
PROV=${COS_HOOK_PROVIDER:-}; case "$PROV" in claude|codex|cursor) ;; *) PROV='' ;; esac
|
|
26
35
|
# 6.53.0, the halt check; 6.53.3, one entry for both synchronous events (review A, option a).
|
|
27
36
|
# FIRST, before mkdir, perl and the B8 guard below: a full spool or a stale drain stamp must
|
|
28
37
|
# never stop a cancel. The marker folder sits beside the spool, like the Cursor queue
|
|
@@ -38,13 +47,17 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
|
|
|
38
47
|
HALT="${SPOOL%/*}/session-halt"
|
|
39
48
|
# Any marker at all? A glob is a builtin, and it runs before stdin is touched.
|
|
40
49
|
ANY=''; for F in "$HALT"/*; do [ -e "$F" ] && ANY=1; break; done
|
|
41
|
-
# The FIRST session_id is the payload's own: Claude
|
|
42
|
-
# that happens to carry one comes later.
|
|
43
|
-
#
|
|
50
|
+
# The FIRST session_id is the payload's own: Claude and Codex write it first, and a tool's
|
|
51
|
+
# input that happens to carry one comes later. 6.62.0 (W7): Cursor writes
|
|
52
|
+
# "conversation_id" FIRST (byte 1, the same id) and its session_id ~600 bytes in, behind
|
|
53
|
+
# tool_input, so a large edit pushed it out of this read and an input naming another
|
|
54
|
+
# session matched first. Whichever of the two keys comes first is the payload's own. Any
|
|
55
|
+
# JSON spacing; 36 hex-and-dash characters inside a closed string, or nothing.
|
|
44
56
|
sid_of() {
|
|
45
|
-
SID=''
|
|
46
|
-
case "$
|
|
47
|
-
|
|
57
|
+
SID=''; KEY='"session_id"'; PRE=${HEAD%%'"session_id"'*}
|
|
58
|
+
case "$PRE" in *'"conversation_id"'*) KEY='"conversation_id"' ;; esac
|
|
59
|
+
case "$HEAD" in *"$KEY"*)
|
|
60
|
+
SID=${HEAD#*"$KEY"}; SID=${SID#"${SID%%[!:[:space:]]*}"}
|
|
48
61
|
case "$SID" in '"'*'"'*) SID=${SID#?}; SID=${SID%%'"'*} ;; *) SID='' ;; esac ;;
|
|
49
62
|
esac
|
|
50
63
|
case "$SID" in *[!0-9A-Fa-f-]*) SID='' ;; esac
|
|
@@ -73,6 +86,20 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
|
|
|
73
86
|
case "$TN" in '"AskUserQuestion"'*|'"ExitPlanMode"'*) NEED=1 ;; esac ;;
|
|
74
87
|
esac
|
|
75
88
|
fi
|
|
89
|
+
# 6.62.0 (QA W13): a COS block Codex imported from the Claude settings runs with no
|
|
90
|
+
# COS_HOOK_PROVIDER, and the Claude reply makes Codex fail open. Codex writes its rollout as
|
|
91
|
+
# the payload's SECOND key, right behind session_id; Claude writes its own transcript there,
|
|
92
|
+
# and the same key further in (a tool that reads a rollout) is not the payload's. Read only
|
|
93
|
+
# when a marker or a spool needs it, so a plain tool call stays one read and a `case`.
|
|
94
|
+
codex_of() {
|
|
95
|
+
case "$HEAD" in *'"session_id"'*'"transcript_path"'*) ;; *) return ;; esac
|
|
96
|
+
A=${HEAD#*'"session_id"'}; B=${A%%'"transcript_path"'*}
|
|
97
|
+
[ ${#B} -le 64 ] || return
|
|
98
|
+
TP=${A#*'"transcript_path"'}; TP=${TP#"${TP%%[!:[:space:]]*}"}
|
|
99
|
+
case "$TP" in '"'*) TP=${TP#?}; TP=${TP%%'"'*} ;; *) return ;; esac
|
|
100
|
+
case "$TP" in */.codex/sessions/*) PROV=codex ;; esac
|
|
101
|
+
}
|
|
102
|
+
[ -n "$PROV" ] || { [ -z "$ANY" ] && [ -z "$NEED" ]; } || codex_of
|
|
76
103
|
case "$SID" in *[A-F]*) SID=$(printf '%s' "$SID" | tr 'A-F' 'a-f') ;; esac
|
|
77
104
|
if [ -z "$SID" ]; then :
|
|
78
105
|
elif [ "$EVENT" = UserPromptSubmit ]; then
|
|
@@ -82,7 +109,12 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
|
|
|
82
109
|
# reading the id and clearing it. Then the prompt is spooled as before.
|
|
83
110
|
rm -f "$HALT/$SID" 2>/dev/null
|
|
84
111
|
elif [ -f "$HALT/$SID" ]; then
|
|
85
|
-
|
|
112
|
+
if [ "$PROV" = codex ]; then
|
|
113
|
+
# 6.62.0 (C10, C12): deny only, and a reason that tells the model to stop.
|
|
114
|
+
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cancelled from COS. Stop and do not work around this."}}'
|
|
115
|
+
else
|
|
116
|
+
printf '%s' '{"continue":false,"stopReason":"Cancelled from COS","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cancelled from COS"}}'
|
|
117
|
+
fi
|
|
86
118
|
exec cat >/dev/null
|
|
87
119
|
fi
|
|
88
120
|
# Every other tool call ends here with nothing written and nothing printed.
|
|
@@ -125,14 +157,17 @@ if [ "$EVENT" = PermissionRequest ]; then
|
|
|
125
157
|
if [ ${#2} -le 4 ] && [ "$2" -ge 20 ] && [ "$2" -le 3600 ]; then WAIT=$2; STAMP_WAIT=",\"hookWaitS\":$2"; fi ;;
|
|
126
158
|
esac
|
|
127
159
|
fi
|
|
128
|
-
|
|
160
|
+
# 6.62.0: the provider, when the command said it (only the Codex command does).
|
|
161
|
+
STAMP_PROV=''; [ -z "$PROV" ] || STAMP_PROV=",\"provider\":\"$PROV\""
|
|
162
|
+
{ printf '{"ts":%s,"ppid":%s,"event":"%s"%s%s,"payload":' "$TS" "$PPID" "$EVENT" "$STAMP_WAIT" "$STAMP_PROV"; cat "$T.in"; printf '}\n'; } > "$T"
|
|
129
163
|
rm -f "$T.in"
|
|
130
164
|
# 6.51.0, CURSOR ONLY. Cursor runs this hook from ~/.claude/settings.json for its own
|
|
131
165
|
# composers, and submits a Stop hook's {"followup_message":...} as that composer's next
|
|
132
166
|
# message. A turn COS queued for the composer is handed over here, at the end of its turn.
|
|
133
167
|
# Claude Code sets no CURSOR_VERSION and sends no cursor_version, so a Claude Stop takes
|
|
134
|
-
# the line below this block exactly as before and never touches the network.
|
|
135
|
-
|
|
168
|
+
# the line below this block exactly as before and never touches the network. 6.62.0: nor
|
|
169
|
+
# does a Codex Stop, whatever its environment says: a follow-up is Cursor-only output.
|
|
170
|
+
if [ "$EVENT" = Stop ] && [ "$PROV" != codex ] && [ -n "${CURSOR_VERSION:-}" ] && grep -Eq '"cursor_version"[[:space:]]*:[[:space:]]*"' "$T" 2>/dev/null; then
|
|
136
171
|
cp "$T" "$T.req" 2>/dev/null; mv "$T" "$SPOOL/$TS-$$-$EVENT.json"
|
|
137
172
|
# Nothing queued for this composer (almost every Stop): no request at all. The queue
|
|
138
173
|
# folder sits beside the spool. If it is not there, or the id cannot be read here,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Provider parity (6.62.0)
|
|
2
|
+
|
|
3
|
+
What the server promises about Claude, Codex and Cursor desk sessions, and where each promise stops.
|
|
4
|
+
|
|
5
|
+
## Contract on the wire
|
|
6
|
+
|
|
7
|
+
- `GET /api/health` and `GET /api/models`: `providers.<claude|codex|cursor>.act` (`continueIdle`, `continueBusy`, `fork`, `cancelDesk`, `approvals`, `nativeQueue`, `keepModel`, `permissions`, each false with a reason code). Both routes also carry `providers.<p>.observe` (`hooks`, `liveState`, `children`, `compaction`, `reasoning`).
|
|
8
|
+
- `threadForkSupported` stays a boolean; `threadForkProviders` lists who can fork.
|
|
9
|
+
- `features.sessionCancel`: `cosTurn`, `deskClaude`, `deskCodex`, `deskCursor`.
|
|
10
|
+
- `sessionHooks`: `state` / `installed` are Claude's (Control's banner reads them). `sessionHooks.codex = {present, installed, trust, scriptOk}`; `sessionHooks.cursorObserver = {installed, nodeOk}`.
|
|
11
|
+
- Session rows: `running` and `running_active`; detail additionally carries `reported_model` and `continue_note` (≤60 chars). List polling does not wait for model lookups.
|
|
12
|
+
|
|
13
|
+
## Hooks
|
|
14
|
+
|
|
15
|
+
| Engine | Where | Trust | Turn start / end |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| Claude | `~/.claude/settings.json` | n/a | UserPromptSubmit / Stop, registry idle flip |
|
|
18
|
+
| Codex | `~/.codex/hooks.json` (COS blocks replaced in place) | Codex `hooks/list`; untrusted hooks never run | UserPromptSubmit / Stop, Interrupt |
|
|
19
|
+
| Cursor | the Claude hooks (third-party extensibility) + `~/.cursor/hooks.json` observer | n/a | IDE: UserPromptSubmit / Stop; CLI `-p` fires neither |
|
|
20
|
+
|
|
21
|
+
The halt reply is provider-specific: Claude and Cursor keep the 6.53 bytes; Codex gets deny-only (it fails open on `continue:false`).
|
|
22
|
+
|
|
23
|
+
## Continue posture
|
|
24
|
+
|
|
25
|
+
| Engine | Continue runs with | Fallback |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Claude | the session itself (live inbox) or `claude -p --resume` with the user's default mode | unchanged |
|
|
28
|
+
| Codex | session `-m`, effort, `-s` from the latest `turn_context`; `danger-full-access` only in a trusted folder | 6.61 posture + note |
|
|
29
|
+
| Cursor | `--force --model <session slug>` only after client note acknowledgement, on its own config dir | composer-2.5-fast + note |
|
|
30
|
+
|
|
31
|
+
`COS_CONTINUE_FULL_PERMISSIONS=0` restores the 6.61 argv. Fork is read-only for every engine.
|
|
32
|
+
|
|
33
|
+
## Compatibility and limits
|
|
34
|
+
|
|
35
|
+
Cursor Run Everything requires `X-COS-Continue-Note: 1` on the turn request. Queue admission persists the acknowledgement and replays it on drain. Clients without it retain Ask mode. Continue/Fork refuse if private Cursor configuration isolation fails.
|
|
36
|
+
|
|
37
|
+
Run `codex` in Terminal to review and trust the installed hooks. COS reads hook trust through a bounded app-server call every five minutes; it does not write hook trust. Codex hooks use their own script copy. Desk Cancel requires observed hooks for the specific thread, not merely an installed script. Continue only uses danger-full-access for an already trusted working directory or its canonical Git root, rechecked at spawn; a trusted arbitrary parent does not qualify. Starting new Codex sessions may cause Codex itself to write project trust; resumed sessions do not.
|
|
38
|
+
|
|
39
|
+
Busy Codex Continue accepts the moving head and enters the native app queue, which is not exposed in COS Queue. Cursor CLI queued turns start when the chat is free and use the saved note acknowledgement. Cursor Fork creates a new read-only chat with recent context. Cursor IDE composers cannot accept outside messages, and Cursor CLI approvals cannot be answered remotely.
|
|
40
|
+
|
|
41
|
+
Cursor reasoning is briefly on disk in the observer spool until drained, then in memory and the live stream, never the ledger. A stale drain stops new thought writes. Codex rollout compaction completion closes an observed phase; start needs hook evidence. Cursor open turns quiet beyond five minutes remain OPEN until the 30-minute ceiling. Claude/Codex completion can lag by 30 seconds.
|
|
42
|
+
|
|
43
|
+
Set `COS_CONTINUE_FULL_PERMISSIONS=0` in `~/.cos-glasses/.env` and restart to restore the previous posture; Control does not yet carry this key through updates. Before rolling the server back to 6.61.7, use the pinned 6.62.0 CLI to remove Codex hooks (`--hooks uninstall --codex`), then use Control rollback and reinstall the older hooks. Do not run the old CLI with `--codex`: it does not understand that flag.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.62.1",
|
|
4
4
|
"description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"!server/scripts/nemotron-mutation-gate.mjs",
|
|
51
51
|
"!server/scripts/merged-scribe-mutation-gate.mjs",
|
|
52
52
|
"!server/scripts/nemotron-replay.ts",
|
|
53
|
+
"!server/scripts/turn-request-mutation-gate.mjs",
|
|
53
54
|
"!server/scripts/mutation-gate.mjs",
|
|
54
55
|
"!server/lib/meeting-engine/__fixtures__/**",
|
|
55
56
|
"!server/lib/__fixtures__/**",
|
|
@@ -73,7 +74,9 @@
|
|
|
73
74
|
"docs/work-handoff-requests.md",
|
|
74
75
|
"work-task-runtime",
|
|
75
76
|
"docs/work-task-runtime.md",
|
|
76
|
-
"!server/scripts/build-work-task-runtime.py"
|
|
77
|
+
"!server/scripts/build-work-task-runtime.py",
|
|
78
|
+
"bin/hooks/cos-cursor-observer.cjs",
|
|
79
|
+
"docs/provider-parity.md"
|
|
77
80
|
],
|
|
78
81
|
"author": "COS Contributors",
|
|
79
82
|
"license": "MIT",
|