@gotcos/glasses-server 6.59.0 → 6.60.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 +28 -0
- package/README.md +79 -7
- package/bin/hooks/cos-session-hook +24 -4
- package/managed-runtime-contract.json +2 -0
- package/package.json +2 -1
- package/server/index.ts +23 -2
- package/server/lib/claude-hooks-installer.ts +120 -17
- package/server/lib/client-liveness.ts +92 -4
- package/server/lib/permission-broker.ts +560 -31
- package/server/lib/session-hooks-runtime.ts +23 -0
- package/server/routes/permission-broker.ts +37 -3
|
@@ -23,28 +23,116 @@
|
|
|
23
23
|
//
|
|
24
24
|
// In memory: a restart forgets it, and the next poll restores it. Until then nothing is
|
|
25
25
|
// held, which is the native dialog, the safe side.
|
|
26
|
+
//
|
|
27
|
+
// 6.60.0 (QA round 1, the contract shared with COS Glasses 6.9.563): the away hold needs
|
|
28
|
+
// more than a poll. A poll QUALIFIES only with `hold=1` (this client shows held questions;
|
|
29
|
+
// 6.9.563 and later send it) AND a finite `presenceAgeMs` within [0, 24 h]: the ms since
|
|
30
|
+
// the latest real wearer evidence (glasses: a ring or temple input, or a device status with
|
|
31
|
+
// wearing true; phone: a foreground touch). A connected lens nobody wears sends no
|
|
32
|
+
// `presenceAgeMs`, and an older client sends no `hold=1`, so neither can make the Mac hold a
|
|
33
|
+
// question for 10 minutes.
|
|
34
|
+
//
|
|
35
|
+
// Kept apart, all in memory (a restart forgets them, which fails toward 6.59.0):
|
|
36
|
+
// - the newest counting poll of any kind (`lastQuestionsPollAt`, for /api/health);
|
|
37
|
+
// - the newest poll WITHOUT `hold=1` (`lastLegacyQuestionsPollAt`): the 6.59.0 liveness,
|
|
38
|
+
// unchanged for every older client;
|
|
39
|
+
// - the newest poll WITH `hold=1` (`lastHoldQuestionsPollAt`);
|
|
40
|
+
// - the newest qualifying presence, on BOTH clocks, each forward-only: monotonic
|
|
41
|
+
// (`performance.now()`, immune to the wall clock stepping) and wall. Its age is the
|
|
42
|
+
// larger of the two (`presenceAgeMs`): on macOS the monotonic clock does not advance
|
|
43
|
+
// while the Mac sleeps, so on its own it would read a pre-sleep presence as younger
|
|
44
|
+
// than it is; the wall clock does advance. A wall clock stepped forward reads older
|
|
45
|
+
// (no hold), never younger.
|
|
46
|
+
|
|
47
|
+
/** 6.60.0: the most a client may report as `presenceAgeMs`. Anything else is no presence. */
|
|
48
|
+
export const PRESENCE_AGE_MAX_MS = 86_400_000
|
|
26
49
|
|
|
27
50
|
let lastPollAt: number | null = null
|
|
51
|
+
let lastLegacyPollAt: number | null = null
|
|
52
|
+
let lastHoldPollAt: number | null = null
|
|
53
|
+
let lastPresenceMono: number | null = null
|
|
54
|
+
let lastPresenceWall: number | null = null
|
|
55
|
+
|
|
56
|
+
/** 6.60.0: the monotonic clock. Milliseconds since this process started; paused while the Mac sleeps. */
|
|
57
|
+
export function monotonicNowMs(): number {
|
|
58
|
+
return performance.now()
|
|
59
|
+
}
|
|
28
60
|
|
|
29
61
|
/** A stored stamp this far ahead of a new poll means the clock stepped back. */
|
|
30
62
|
const CLOCK_STEP_BACK_MS = 5_000
|
|
31
63
|
|
|
64
|
+
/** Newest-wins for a poll stamp, following a clock that stepped back (see below). */
|
|
65
|
+
function newerStamp(stored: number | null, at: number): number {
|
|
66
|
+
return stored === null || at > stored || stored - at > CLOCK_STEP_BACK_MS ? at : stored
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 6.60.0: what a counting poll said about the client, read from its query. */
|
|
70
|
+
export interface QuestionsPollFacts {
|
|
71
|
+
/** `hold=1`: this client can show held questions. */
|
|
72
|
+
hold?: boolean
|
|
73
|
+
/** ms since the latest real wearer evidence; null or absent when there is none since boot. */
|
|
74
|
+
presenceAgeMs?: number | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 6.60.0: a `presenceAgeMs` the contract accepts: a finite whole number within [0, 24 h]. */
|
|
78
|
+
export function validPresenceAgeMs(value: unknown): value is number {
|
|
79
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= PRESENCE_AGE_MAX_MS
|
|
80
|
+
}
|
|
81
|
+
|
|
32
82
|
/**
|
|
33
83
|
* A counting questions poll answered at `at` (server clock). Never moves backwards for a
|
|
34
84
|
* slower request answered later, but a clock that stepped back is followed: a stamp left in
|
|
35
85
|
* the future would read stale to the broker, and ignoring every poll until the clock caught
|
|
36
|
-
* up would leave no live client for that long.
|
|
86
|
+
* up would leave no live client for that long. 6.60.0: `poll` says whether it was a `hold=1`
|
|
87
|
+
* poll and carried qualifying presence; `mono` is the monotonic time it was answered.
|
|
37
88
|
*/
|
|
38
|
-
export function noteQuestionsPoll(at: number): void {
|
|
89
|
+
export function noteQuestionsPoll(at: number, poll: QuestionsPollFacts = {}, mono: number = monotonicNowMs()): void {
|
|
39
90
|
if (!Number.isFinite(at)) return
|
|
40
|
-
|
|
91
|
+
lastPollAt = newerStamp(lastPollAt, at)
|
|
92
|
+
if (poll.hold !== true) {
|
|
93
|
+
lastLegacyPollAt = newerStamp(lastLegacyPollAt, at)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
lastHoldPollAt = newerStamp(lastHoldPollAt, at)
|
|
97
|
+
if (!validPresenceAgeMs(poll.presenceAgeMs) || !Number.isFinite(mono)) return
|
|
98
|
+
// Forward-only on each clock: a slower poll carrying older evidence never moves it back.
|
|
99
|
+
const presenceMono = mono - poll.presenceAgeMs
|
|
100
|
+
const presenceWall = at - poll.presenceAgeMs
|
|
101
|
+
if (lastPresenceMono === null || presenceMono > lastPresenceMono) lastPresenceMono = presenceMono
|
|
102
|
+
if (lastPresenceWall === null || presenceWall > lastPresenceWall) lastPresenceWall = presenceWall
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 6.60.0: how long ago the newest qualifying wearer presence was, or null when no
|
|
107
|
+
* qualifying poll has arrived since this process started. The larger of the monotonic age
|
|
108
|
+
* and the wall-clock age, so a Mac that slept never makes it read younger.
|
|
109
|
+
*/
|
|
110
|
+
export function presenceAgeMs(mono: number = monotonicNowMs(), wall: number = Date.now()): number | null {
|
|
111
|
+
if (lastPresenceMono === null || !Number.isFinite(mono)) return null
|
|
112
|
+
const monoAge = Math.max(0, mono - lastPresenceMono)
|
|
113
|
+
const wallAge = lastPresenceWall !== null && Number.isFinite(wall) ? wall - lastPresenceWall : 0
|
|
114
|
+
return Math.max(monoAge, wallAge)
|
|
41
115
|
}
|
|
42
116
|
|
|
43
|
-
/** When the newest authenticated questions poll arrived, or null
|
|
117
|
+
/** When the newest authenticated questions poll of any kind arrived, or null since boot (/api/health). */
|
|
44
118
|
export function lastQuestionsPollAt(): number | null {
|
|
45
119
|
return lastPollAt
|
|
46
120
|
}
|
|
47
121
|
|
|
122
|
+
/** 6.60.0: the newest poll WITHOUT `hold=1`: the 6.59.0 liveness, for every older client. */
|
|
123
|
+
export function lastLegacyQuestionsPollAt(): number | null {
|
|
124
|
+
return lastLegacyPollAt
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 6.60.0: the newest poll WITH `hold=1`. */
|
|
128
|
+
export function lastHoldQuestionsPollAt(): number | null {
|
|
129
|
+
return lastHoldPollAt
|
|
130
|
+
}
|
|
131
|
+
|
|
48
132
|
export function __resetClientLivenessForTests(): void {
|
|
49
133
|
lastPollAt = null
|
|
134
|
+
lastLegacyPollAt = null
|
|
135
|
+
lastHoldPollAt = null
|
|
136
|
+
lastPresenceMono = null
|
|
137
|
+
lastPresenceWall = null
|
|
50
138
|
}
|