@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
|
@@ -21,6 +21,41 @@ const BARE_BANNED_PERMISSION_ARGS = new Set(
|
|
|
21
21
|
BANNED_PERMISSION_ARGS.filter(flag => !flag.startsWith('-')),
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
/** The two spellings of Codex's sandbox option. The allowed value must be the NEXT argv slot. */
|
|
25
|
+
const CODEX_SANDBOX_FLAGS = new Set(['--sandbox', '-s'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 6.62.0 (plan 3.1, D1): the one exception to the ban, for a Continue that carries the
|
|
29
|
+
* session's OWN permissions. ONE object, built once per turn from the Continue plan and
|
|
30
|
+
* handed to BOTH checks in `attached-provider-adapter.ts` (the builder and the spawn
|
|
31
|
+
* boundary), so the two can never disagree about what was allowed.
|
|
32
|
+
*
|
|
33
|
+
* Narrow by construction:
|
|
34
|
+
* - `cursorForce` admits exactly the argv slot `--force` (Cursor's Run Everything). Not
|
|
35
|
+
* `--force=…`, not `-f`, and nothing else on the list.
|
|
36
|
+
* - `codexSandbox` admits the bare token `danger-full-access` only as the value right
|
|
37
|
+
* after `--sandbox` / `-s`. The `--flag=value` spelling and every other position stay
|
|
38
|
+
* banned.
|
|
39
|
+
*
|
|
40
|
+
* Fork (`fork-thread.ts`) and G2 extra-args (`codex-extra-args.ts`) never pass one, so the
|
|
41
|
+
* full ban holds there. Anything that is not exactly this shape counts as no allowance.
|
|
42
|
+
*/
|
|
43
|
+
export interface PermissionAllowance {
|
|
44
|
+
path: 'attached_continue'
|
|
45
|
+
cursorForce?: true
|
|
46
|
+
codexSandbox?: 'danger-full-access'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readAllowance(allowance: unknown): { cursorForce: boolean; codexDanger: boolean } {
|
|
50
|
+
if (!allowance || typeof allowance !== 'object') return { cursorForce: false, codexDanger: false }
|
|
51
|
+
const row = allowance as Record<string, unknown>
|
|
52
|
+
if (row.path !== 'attached_continue') return { cursorForce: false, codexDanger: false }
|
|
53
|
+
return {
|
|
54
|
+
cursorForce: row.cursorForce === true,
|
|
55
|
+
codexDanger: row.codexSandbox === 'danger-full-access',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
/**
|
|
25
60
|
* Is this argv free of every flag plan 4.7 bans?
|
|
26
61
|
*
|
|
@@ -29,12 +64,18 @@ const BARE_BANNED_PERMISSION_ARGS = new Set(
|
|
|
29
64
|
* (`danger-full-access`) match only as their own argv slot. Values of
|
|
30
65
|
* `--workspace` / `-C` are skipped: a cwd containing `--force` is a path,
|
|
31
66
|
* not a permission flag.
|
|
67
|
+
*
|
|
68
|
+
* `allowance` (6.62.0) lifts exactly what `PermissionAllowance` names, and only for the
|
|
69
|
+
* attached Continue path. Omitted, it is the full ban, byte for byte the 6.61 check.
|
|
32
70
|
*/
|
|
33
|
-
export function findBannedPermissionArg(args: readonly string[]): string | null {
|
|
71
|
+
export function findBannedPermissionArg(args: readonly string[], allowance?: PermissionAllowance): string | null {
|
|
72
|
+
const allowed = readAllowance(allowance)
|
|
34
73
|
for (let i = 0; i < args.length; i++) {
|
|
35
74
|
const value = String(args[i])
|
|
36
75
|
const prev = i > 0 ? String(args[i - 1]) : ''
|
|
37
76
|
if (PATH_VALUED_FLAGS.has(prev)) continue
|
|
77
|
+
if (allowed.cursorForce && value === '--force') continue
|
|
78
|
+
if (allowed.codexDanger && value === 'danger-full-access' && CODEX_SANDBOX_FLAGS.has(prev)) continue
|
|
38
79
|
if (BARE_BANNED_PERMISSION_ARGS.has(value)) return value
|
|
39
80
|
if (!value.startsWith('-')) continue
|
|
40
81
|
for (const banned of BANNED_PERMISSION_ARGS) {
|
|
@@ -61,6 +61,7 @@ import { claudePermissionArgs, getClaudeTrustMode } from './claude-permissions.j
|
|
|
61
61
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
62
62
|
import { teeJobTrail, type JobTrailDraft } from './job-trail.js'
|
|
63
63
|
import { claudeSupportsSessionName } from './claude-cli-support.js'
|
|
64
|
+
import { applyTurnRequestEnv, createTurnRequestBinding, spawnWithTurnRequest } from './turn-request.js'
|
|
64
65
|
|
|
65
66
|
// Inactivity = no stdout data for this long → kill (catches stalls)
|
|
66
67
|
const INACTIVITY_BY_MODEL: Record<ClaudeModelPreference, number> = {
|
|
@@ -220,6 +221,8 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
220
221
|
return new Promise<void>((resolve) => {
|
|
221
222
|
const env = { ...process.env }
|
|
222
223
|
delete env.CLAUDECODE
|
|
224
|
+
// 'ready' is not a user query: no turn-request file, and never an inherited path.
|
|
225
|
+
applyTurnRequestEnv(env, null)
|
|
223
226
|
|
|
224
227
|
const proc = spawn('claude', [
|
|
225
228
|
'-p',
|
|
@@ -346,7 +349,7 @@ export interface StreamCallbacks {
|
|
|
346
349
|
* its session id in the first stream event; before this the id reached the job only with
|
|
347
350
|
* the result, so COS Control could not show a New session from Work while it ran.
|
|
348
351
|
*/
|
|
349
|
-
onNativeSession?: (linkage: { cliSessionId: string }) => void
|
|
352
|
+
onNativeSession?: (linkage: { cliSessionId: string; provider?: 'claude' | 'codex' | 'cursor' }) => void
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
/** Claude CLI can emit `subtype: success` with `is_error: true`; the boolean
|
|
@@ -622,12 +625,23 @@ export async function callClaudeStreaming(
|
|
|
622
625
|
cliModelId: resolveClaudeCliModelId(resolvedModel),
|
|
623
626
|
})
|
|
624
627
|
|
|
625
|
-
|
|
628
|
+
// Ultracode is a CLI-only orchestration keyword; history/chat keeps the
|
|
629
|
+
// original user text so the keyword never appears on the lens.
|
|
630
|
+
const cliQuery = resolvedEffort === 'ultracode'
|
|
631
|
+
? `${fullQuery}\n\n${ULTRACODE_KEYWORD}`
|
|
632
|
+
: fullQuery
|
|
633
|
+
// The COS UserPromptSubmit hook sees `cliQuery` (image preamble, attachment block and
|
|
634
|
+
// all). This private file hands it the user's own words, bound by the hash of exactly
|
|
635
|
+
// what goes on stdin; the file dies with the child. See lib/turn-request.ts.
|
|
636
|
+
const turnRequest = createTurnRequestBinding({ userText: query, prompt: cliQuery, turnId: options?.turnId })
|
|
637
|
+
applyTurnRequestEnv(env, turnRequest)
|
|
638
|
+
|
|
639
|
+
const proc = spawnWithTurnRequest(turnRequest, () => spawn('claude', args, {
|
|
626
640
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
627
641
|
env,
|
|
628
642
|
cwd: cliCwd,
|
|
629
643
|
detached: true,
|
|
630
|
-
})
|
|
644
|
+
}))
|
|
631
645
|
|
|
632
646
|
let fullText = ''
|
|
633
647
|
let stderr = ''
|
|
@@ -1061,11 +1075,6 @@ export async function callClaudeStreaming(
|
|
|
1061
1075
|
options.abortSignal.addEventListener('abort', handleAbort, { once: true })
|
|
1062
1076
|
}
|
|
1063
1077
|
|
|
1064
|
-
// Ultracode is a CLI-only orchestration keyword; history/chat keeps the
|
|
1065
|
-
// original user text so the keyword never appears on the lens.
|
|
1066
|
-
const cliQuery = resolvedEffort === 'ultracode'
|
|
1067
|
-
? `${fullQuery}\n\n${ULTRACODE_KEYWORD}`
|
|
1068
|
-
: fullQuery
|
|
1069
1078
|
try {
|
|
1070
1079
|
const providerOwned = await callbacks.onProviderProcess?.({
|
|
1071
1080
|
provider: 'claude',
|
|
@@ -90,6 +90,7 @@ export const HOOK_SUBSCRIPTIONS: readonly HookSubscription[] = [
|
|
|
90
90
|
{ event: 'Notification', matcher: 'permission_prompt|idle_prompt|elicitation_dialog|agent_needs_input', async: true, timeout: 10 },
|
|
91
91
|
{ event: 'SubagentStart', async: true, timeout: 10 },
|
|
92
92
|
{ event: 'SubagentStop', async: true, timeout: 10 },
|
|
93
|
+
{ event: 'PreCompact', async: true, timeout: 10 },
|
|
93
94
|
{ event: 'PostCompact', async: true, timeout: 10 },
|
|
94
95
|
{ event: 'PostModelSwitch', async: true, timeout: 10 },
|
|
95
96
|
]
|
|
@@ -157,7 +158,8 @@ export function currentHookPaths(): HookPaths {
|
|
|
157
158
|
|
|
158
159
|
type HookBlock = { matcher?: unknown; hooks?: unknown }
|
|
159
160
|
|
|
160
|
-
|
|
161
|
+
/** Exported (6.62.0) so the Codex installer names our hooks by the same script path. */
|
|
162
|
+
export function hookIsOurs(h: unknown): boolean {
|
|
161
163
|
return !!h && typeof h === 'object' && typeof (h as { command?: unknown }).command === 'string'
|
|
162
164
|
&& ((h as { command: string }).command.includes(HOOK_SCRIPT_MARKER) || (h as { command: string }).command.includes(HOOK_SCRIPT_NAME + ' '))
|
|
163
165
|
}
|
|
@@ -307,6 +309,7 @@ export interface HookStatus {
|
|
|
307
309
|
* has not run Install hooks since. Questions are then held at most 120 s, as before.
|
|
308
310
|
*/
|
|
309
311
|
priorWaitOnly: boolean
|
|
312
|
+
observerOnlyDrift?: boolean
|
|
310
313
|
/** 6.60.0: our PermissionRequest hook's `timeout` in the settings file, or null (`permissionHookTimeoutS`). */
|
|
311
314
|
permissionHookTimeoutS: number | null
|
|
312
315
|
tokenPresent: boolean
|
|
@@ -362,7 +365,8 @@ export function hookStatus(paths: { settingsPath?: string; scriptPath?: string;
|
|
|
362
365
|
else state = 'installed'
|
|
363
366
|
const priorWaitOnly = state === 'drift' && events.missing.length === 0 && events.drifted.length > 0
|
|
364
367
|
&& events.drifted.every(event => events.priorWait.includes(event))
|
|
365
|
-
|
|
368
|
+
const observerOnlyDrift = state === 'drift' && events.missing.length > 0 && events.missing.every(event => event === 'PreCompact') && events.drifted.every(event => events.priorWait.includes(event))
|
|
369
|
+
return { observerOnlyDrift, state, installed: state === 'installed', settingsPath, scriptPath, scriptSha, packageScriptSha, ...events, priorWaitOnly, permissionHookTimeoutS: permissionWaitS, tokenPresent }
|
|
366
370
|
}
|
|
367
371
|
|
|
368
372
|
/**
|
|
@@ -378,12 +382,20 @@ export function hookStatus(paths: { settingsPath?: string; scriptPath?: string;
|
|
|
378
382
|
* (7433b7f), and it carries the same halt check.
|
|
379
383
|
* - df54677f...: the script 6.53.3 through 6.59.0 shipped. 6.60.0 changed it only for the
|
|
380
384
|
* away hold (the PermissionRequest wait); its halt check is the same.
|
|
385
|
+
* - 0e1bbb98...: the script 6.60.0 through 6.61.7 shipped (B1). 6.62.0 changed it for Codex
|
|
386
|
+
* and Cursor (provider stamp, Codex deny-only reply, Cursor `conversation_id`); its Claude
|
|
387
|
+
* halt reply is byte for byte the same, so a Mac still on it stops desk Claude runs.
|
|
388
|
+
* - 1877c90c...: the 6.62.0 script as first built, before its /qa round taught it to recognise
|
|
389
|
+
* a Codex payload (QA W13). Never published, but installable from a local build of that
|
|
390
|
+
* branch, and its Claude halt check is the same.
|
|
381
391
|
* Each is pinned against its bytes in `server/lib/__fixtures__/`.
|
|
382
392
|
*/
|
|
383
393
|
export const HALT_CAPABLE_PRIOR_SCRIPT_SHAS: readonly string[] = [
|
|
384
394
|
'1158bb06297550128f01fc47caa68806a5287d6da2d05b0d1c812e8ae20764e7',
|
|
385
395
|
'c0b41bf8581cfea498e1e1ac5220fe4e148bd97448d13b60a88879ee5992cc51',
|
|
386
396
|
'df54677f79908893509ee87608cfe3f4a53ced03f88f337f16a63741d6b3d547',
|
|
397
|
+
'0e1bbb9816c9475436f89f6326d5504eccc9aa6519ff5e466b949d53a73d8439',
|
|
398
|
+
'1877c90c5359571dfc74c6d03395104a73925868147e8a50cd9d7b4226b1b5ca',
|
|
387
399
|
]
|
|
388
400
|
|
|
389
401
|
/**
|
|
@@ -397,16 +409,26 @@ export const HALT_CAPABLE_PRIOR_SCRIPT_SHAS: readonly string[] = [
|
|
|
397
409
|
* check, so updating the server never turns cancel from the lens off. Any other drift
|
|
398
410
|
* still is not halt-ready.
|
|
399
411
|
*/
|
|
400
|
-
export function hookHaltReady(status: Pick<HookStatus, 'installed' | 'state' | 'scriptSha'> & Partial<Pick<HookStatus, 'priorWaitOnly' | 'packageScriptSha'>>): boolean {
|
|
412
|
+
export function hookHaltReady(status: Pick<HookStatus, 'installed' | 'state' | 'scriptSha'> & Partial<Pick<HookStatus, 'priorWaitOnly' | 'observerOnlyDrift' | 'packageScriptSha'>>): boolean {
|
|
401
413
|
if (status.installed === true) return true
|
|
402
414
|
const priorScript = typeof status.scriptSha === 'string' && HALT_CAPABLE_PRIOR_SCRIPT_SHAS.includes(status.scriptSha)
|
|
403
415
|
if (status.state === 'script_outdated') return priorScript
|
|
404
|
-
if (status.state === 'drift' && status.priorWaitOnly === true) {
|
|
416
|
+
if (status.state === 'drift' && (status.priorWaitOnly === true || status.observerOnlyDrift === true)) {
|
|
405
417
|
return priorScript || (typeof status.scriptSha === 'string' && status.scriptSha === status.packageScriptSha)
|
|
406
418
|
}
|
|
407
419
|
return false
|
|
408
420
|
}
|
|
409
421
|
|
|
422
|
+
/**
|
|
423
|
+
* 6.62.0 (QA W1): can these hooks stop a desk CURSOR run? Halt-ready, AND the installed script is
|
|
424
|
+
* this package's, byte for byte. Every earlier script reads the first `session_id`, which Cursor
|
|
425
|
+
* writes behind `tool_input`: a large tool call is missed, and one naming another session stops
|
|
426
|
+
* the wrong run. Claude's own readiness (`hookHaltReady`) is unchanged.
|
|
427
|
+
*/
|
|
428
|
+
export function cursorDeskHaltReady(status: Parameters<typeof hookHaltReady>[0]): boolean {
|
|
429
|
+
return hookHaltReady(status) && typeof status.scriptSha === 'string' && status.scriptSha === status.packageScriptSha
|
|
430
|
+
}
|
|
431
|
+
|
|
410
432
|
const INSTALL_COMMAND = 'npx --yes @gotcos/glasses-server@latest --hooks install'
|
|
411
433
|
|
|
412
434
|
/**
|
|
@@ -468,7 +490,8 @@ export interface InstallResult {
|
|
|
468
490
|
|
|
469
491
|
const BACKUPS_KEPT = 3
|
|
470
492
|
|
|
471
|
-
|
|
493
|
+
/** Exported (6.62.0) for the Codex installer: the same backup, the same three kept. */
|
|
494
|
+
export function backupSettings(settingsPath: string): string | null {
|
|
472
495
|
if (!existsSync(settingsPath)) return null
|
|
473
496
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
474
497
|
const backup = `${settingsPath}.cos-backup-${stamp}`
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
import { terminalProviderAuthFailure } from './provider-terminal-error.js'
|
|
86
86
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
87
87
|
import { teeJobTrail } from './job-trail.js'
|
|
88
|
+
import { applyTurnRequestEnv, createTurnRequestBinding, spawnWithTurnRequest } from './turn-request.js'
|
|
88
89
|
|
|
89
90
|
const INACTIVITY_MS = 180_000
|
|
90
91
|
const WALL_MAX_MS = 900_000
|
|
@@ -481,12 +482,19 @@ export async function callCodexStreaming(
|
|
|
481
482
|
)
|
|
482
483
|
}
|
|
483
484
|
|
|
484
|
-
|
|
485
|
+
// The COS UserPromptSubmit hook sees `prompt`, which is SYSTEM INSTRUCTIONS plus USER
|
|
486
|
+
// REQUEST in one text, on a fresh thread and on resume alike. This private file hands it
|
|
487
|
+
// the user's own words, bound by the hash of exactly what goes on stdin; the file dies
|
|
488
|
+
// with the child. See lib/turn-request.ts.
|
|
489
|
+
const turnRequest = createTurnRequestBinding({ userText: query, prompt, turnId: options?.turnId })
|
|
490
|
+
applyTurnRequestEnv(env, turnRequest)
|
|
491
|
+
|
|
492
|
+
const proc = spawnWithTurnRequest(turnRequest, () => spawn(resolvedCodex.path, args, {
|
|
485
493
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
486
494
|
env,
|
|
487
495
|
cwd: codexCwd,
|
|
488
496
|
detached: true,
|
|
489
|
-
})
|
|
497
|
+
}))
|
|
490
498
|
|
|
491
499
|
let fullText = ''
|
|
492
500
|
let stderr = ''
|
|
@@ -770,12 +778,19 @@ export async function callCodexStreaming(
|
|
|
770
778
|
})
|
|
771
779
|
}, WALL_MAX_MS)
|
|
772
780
|
|
|
781
|
+
let nativeSessionAnnounced = false
|
|
773
782
|
function handleEvent(event: any) {
|
|
774
783
|
const nextThreadId = extractCodexThreadId(event)
|
|
775
784
|
if (nextThreadId && nextThreadId !== codexThreadId) {
|
|
776
785
|
codexThreadId = nextThreadId
|
|
777
786
|
updateCodexRun(run.runId, { codexThreadId })
|
|
778
787
|
}
|
|
788
|
+
// 6.62.0 (plan 3.9): the job links to its native thread at the first event naming it,
|
|
789
|
+
// not only at completion.
|
|
790
|
+
if (nextThreadId && !nativeSessionAnnounced) {
|
|
791
|
+
nativeSessionAnnounced = true
|
|
792
|
+
try { callbacks.onNativeSession?.({ provider: 'codex', cliSessionId: nextThreadId }) } catch { /* linkage is best effort */ }
|
|
793
|
+
}
|
|
779
794
|
|
|
780
795
|
const status = toolStatus(event)
|
|
781
796
|
if (status) callbacks.onToolStatus?.(status)
|