companion-shell.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Companion App — Phase 5 orchestration shell. |
| 3 | * |
| 4 | * This binding layer composes the Phase 2–4 pure cores with Phase 5 adapters. |
| 5 | * Authority-bearing objects are held by the authority group; runtime operations |
| 6 | * receive only inert values such as URLs, paths, ports, and resource limits. |
| 7 | */ |
| 8 | |
| 9 | import { open, rename, rm } from 'node:fs/promises'; |
| 10 | import path from 'node:path'; |
| 11 | |
| 12 | import { |
| 13 | LIFECYCLE_EVENTS, |
| 14 | canServeInference, |
| 15 | createIntegrityAccumulator, |
| 16 | createLifecycleState, |
| 17 | transitionLifecycle, |
| 18 | validateIntegritySpec, |
| 19 | validateSourceUrl, |
| 20 | } from './companion-runtime-manager.mjs'; |
| 21 | import { selectLane } from './model-runtime-lane.mjs'; |
| 22 | |
| 23 | export const COMPANION_SHELL_REASONS = Object.freeze({ |
| 24 | NOT_READY: 'not_ready', |
| 25 | MANIFEST_UNTRUSTED: 'manifest_untrusted', |
| 26 | INTEGRITY_FAILED: 'integrity_failed', |
| 27 | SPAWN_FAILED: 'spawn_failed', |
| 28 | HEALTH_FAILED: 'health_failed', |
| 29 | INVALID_GROUP: 'invalid_group', |
| 30 | }); |
| 31 | |
| 32 | const DEFAULT_HEALTH_RECENCY_MS = 15_000; |
| 33 | |
| 34 | /** |
| 35 | * Build an authority group. Runtime adapters never receive this object. |
| 36 | * @param {{ keychain?: unknown, oauth?: unknown, manifestFetcher?: Function, canister?: unknown }} params |
| 37 | */ |
| 38 | export function createAuthorityGroup(params = {}) { |
| 39 | return Object.freeze({ |
| 40 | keychain: params.keychain ?? null, |
| 41 | oauth: params.oauth ?? null, |
| 42 | manifestFetcher: params.manifestFetcher ?? null, |
| 43 | canister: params.canister ?? null, |
| 44 | }); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Build a runtime group from inert runtime capabilities only. |
| 49 | * @param {{ spawn: Function, download: Function, healthCheck: Function, statResources?: Function }} params |
| 50 | */ |
| 51 | export function createRuntimeGroup(params = {}) { |
| 52 | if ( |
| 53 | typeof params.spawn !== 'function' || |
| 54 | typeof params.download !== 'function' || |
| 55 | typeof params.healthCheck !== 'function' |
| 56 | ) { |
| 57 | throw new TypeError(COMPANION_SHELL_REASONS.INVALID_GROUP); |
| 58 | } |
| 59 | return Object.freeze({ |
| 60 | spawn: params.spawn, |
| 61 | download: params.download, |
| 62 | healthCheck: params.healthCheck, |
| 63 | statResources: typeof params.statResources === 'function' ? params.statResources : async () => ({ ramBytes: 0, vramBytes: 0, cpuPercent: 0 }), |
| 64 | }); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Validate that model integrity metadata came from a first-party manifest that is |
| 69 | * out-of-band from the model host. |
| 70 | * @param {{ manifestUrl: string, modelUrl: string, expectedDigest: string, expectedSizeBytes: number, allowedSourceUrls: string[] }} spec |
| 71 | */ |
| 72 | export function validateManifestTrustAnchor(spec) { |
| 73 | try { |
| 74 | const manifest = new URL(spec?.manifestUrl); |
| 75 | const model = new URL(spec?.modelUrl); |
| 76 | if (manifest.protocol !== 'https:' || model.protocol !== 'https:') { |
| 77 | return { ok: false, reason: COMPANION_SHELL_REASONS.MANIFEST_UNTRUSTED }; |
| 78 | } |
| 79 | if (manifest.hostname === model.hostname) { |
| 80 | return { ok: false, reason: COMPANION_SHELL_REASONS.MANIFEST_UNTRUSTED }; |
| 81 | } |
| 82 | const src = validateSourceUrl(spec.modelUrl, spec.allowedSourceUrls); |
| 83 | if (!src.ok) return { ok: false, reason: src.reason }; |
| 84 | const integrity = validateIntegritySpec(spec.expectedDigest, spec.expectedSizeBytes); |
| 85 | if (!integrity.ok) return { ok: false, reason: integrity.reason }; |
| 86 | return { ok: true, reason: 'ok' }; |
| 87 | } catch { |
| 88 | return { ok: false, reason: COMPANION_SHELL_REASONS.MANIFEST_UNTRUSTED }; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Pure predicate for the Phase 1 `companionAvailable` seam. |
| 94 | * @param {{ |
| 95 | * integrityVerified?: boolean, |
| 96 | * lifecycleState?: { state: string }, |
| 97 | * lastHealthOkAt?: number|null, |
| 98 | * now: number, |
| 99 | * listenerBound?: boolean, |
| 100 | * loopbackTokenPresent?: boolean, |
| 101 | * healthRecencyMs?: number, |
| 102 | * }} state |
| 103 | */ |
| 104 | export function computeCompanionAvailable(state) { |
| 105 | if (!state || typeof state.now !== 'number' || !Number.isFinite(state.now)) return false; |
| 106 | if (state.integrityVerified !== true) return false; |
| 107 | if (!canServeInference(state.lifecycleState)) return false; |
| 108 | if (state.listenerBound !== true) return false; |
| 109 | if (state.loopbackTokenPresent !== true) return false; |
| 110 | if (typeof state.lastHealthOkAt !== 'number' || !Number.isFinite(state.lastHealthOkAt)) return false; |
| 111 | const recency = state.healthRecencyMs ?? DEFAULT_HEALTH_RECENCY_MS; |
| 112 | return state.now - state.lastHealthOkAt <= recency; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Download to a temp file, feed every byte to the integrity accumulator, fsync, |
| 117 | * finalize, and atomically rename to the verified path. |
| 118 | * @param {{ |
| 119 | * runtimeGroup: ReturnType<typeof createRuntimeGroup>, |
| 120 | * spec: { modelUrl: string, expectedDigest: string, expectedSizeBytes: number, allowedSourceUrls: string[] }, |
| 121 | * tempPath: string, |
| 122 | * verifiedPath: string, |
| 123 | * }} params |
| 124 | */ |
| 125 | export async function downloadVerifyAndStageModel({ runtimeGroup, spec, tempPath, verifiedPath }) { |
| 126 | const acc = createIntegrityAccumulator({ |
| 127 | expectedDigest: spec.expectedDigest, |
| 128 | expectedSizeBytes: spec.expectedSizeBytes, |
| 129 | sourceUrl: spec.modelUrl, |
| 130 | allowedSourceUrls: spec.allowedSourceUrls, |
| 131 | }); |
| 132 | const file = await open(tempPath, 'w', 0o600); |
| 133 | let writes = Promise.resolve(); |
| 134 | try { |
| 135 | await runtimeGroup.download(spec.modelUrl, (chunk) => { |
| 136 | acc.update(chunk); |
| 137 | writes = writes.then(() => file.write(chunk)); |
| 138 | }); |
| 139 | await writes; |
| 140 | await file.sync(); |
| 141 | } catch (err) { |
| 142 | acc.abort(); |
| 143 | await file.close().catch(() => {}); |
| 144 | await rm(tempPath, { force: true }); |
| 145 | throw err; |
| 146 | } |
| 147 | await file.close(); |
| 148 | const verdict = acc.finalize(); |
| 149 | if (!verdict.ok) { |
| 150 | await rm(tempPath, { force: true }); |
| 151 | throw new Error(COMPANION_SHELL_REASONS.INTEGRITY_FAILED); |
| 152 | } |
| 153 | if (!path.isAbsolute(verifiedPath)) throw new TypeError(COMPANION_SHELL_REASONS.INTEGRITY_FAILED); |
| 154 | await rename(tempPath, verifiedPath); |
| 155 | return { ok: true, verifiedPath }; |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Create the run-from-source companion shell. |
| 160 | * @param {{ |
| 161 | * authorityGroup?: ReturnType<typeof createAuthorityGroup>, |
| 162 | * runtimeGroup: ReturnType<typeof createRuntimeGroup>, |
| 163 | * now?: () => number, |
| 164 | * healthRecencyMs?: number, |
| 165 | * }} params |
| 166 | */ |
| 167 | export function createCompanionShell(params) { |
| 168 | const runtimeGroup = createRuntimeGroup(params?.runtimeGroup); |
| 169 | const authorityGroup = params?.authorityGroup ?? createAuthorityGroup(); |
| 170 | const now = params?.now ?? (() => Date.now()); |
| 171 | const healthRecencyMs = params?.healthRecencyMs ?? DEFAULT_HEALTH_RECENCY_MS; |
| 172 | |
| 173 | let lifecycleState = createLifecycleState(); |
| 174 | let integrityVerified = false; |
| 175 | let listenerBound = false; |
| 176 | let loopbackTokenPresent = false; |
| 177 | let lastHealthOkAt = null; |
| 178 | let handle = null; |
| 179 | |
| 180 | function markUnavailable() { |
| 181 | lastHealthOkAt = null; |
| 182 | if (canServeInference(lifecycleState)) { |
| 183 | const drained = transitionLifecycle(lifecycleState, LIFECYCLE_EVENTS.DRAIN); |
| 184 | lifecycleState = drained.ok ? drained.newState : createLifecycleState(); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | return { |
| 189 | authorityGroup, |
| 190 | runtimeGroup, |
| 191 | get state() { |
| 192 | return { |
| 193 | lifecycleState, |
| 194 | integrityVerified, |
| 195 | listenerBound, |
| 196 | loopbackTokenPresent, |
| 197 | lastHealthOkAt, |
| 198 | }; |
| 199 | }, |
| 200 | setListenerBound(value) { |
| 201 | listenerBound = value === true; |
| 202 | if (!listenerBound) markUnavailable(); |
| 203 | }, |
| 204 | setLoopbackTokenPresent(value) { |
| 205 | loopbackTokenPresent = value === true; |
| 206 | if (!loopbackTokenPresent) markUnavailable(); |
| 207 | }, |
| 208 | markIntegrityVerified() { |
| 209 | integrityVerified = true; |
| 210 | }, |
| 211 | markIntegrityFailed() { |
| 212 | integrityVerified = false; |
| 213 | markUnavailable(); |
| 214 | }, |
| 215 | companionAvailable() { |
| 216 | return computeCompanionAvailable({ |
| 217 | integrityVerified, |
| 218 | lifecycleState, |
| 219 | lastHealthOkAt, |
| 220 | listenerBound, |
| 221 | loopbackTokenPresent, |
| 222 | now: now(), |
| 223 | healthRecencyMs, |
| 224 | }); |
| 225 | }, |
| 226 | laneCapabilities(extra = {}) { |
| 227 | return { ...extra, companionAvailable: this.companionAvailable() }; |
| 228 | }, |
| 229 | selectLane(preferences = {}, extraCapabilities = {}) { |
| 230 | return selectLane(this.laneCapabilities(extraCapabilities), preferences); |
| 231 | }, |
| 232 | async startRuntime(spawnOpts) { |
| 233 | if (!integrityVerified) throw new Error(COMPANION_SHELL_REASONS.INTEGRITY_FAILED); |
| 234 | const started = transitionLifecycle(lifecycleState, LIFECYCLE_EVENTS.START); |
| 235 | if (!started.ok) throw new Error(COMPANION_SHELL_REASONS.NOT_READY); |
| 236 | lifecycleState = started.newState; |
| 237 | try { |
| 238 | handle = await runtimeGroup.spawn(spawnOpts); |
| 239 | } catch { |
| 240 | lifecycleState = createLifecycleState(); |
| 241 | throw new Error(COMPANION_SHELL_REASONS.SPAWN_FAILED); |
| 242 | } |
| 243 | const healthy = await runtimeGroup.healthCheck(handle); |
| 244 | if (!healthy) { |
| 245 | await handle.kill?.(); |
| 246 | const failed = transitionLifecycle(lifecycleState, LIFECYCLE_EVENTS.HEALTH_FAIL); |
| 247 | lifecycleState = failed.newState; |
| 248 | lastHealthOkAt = null; |
| 249 | throw new Error(COMPANION_SHELL_REASONS.HEALTH_FAILED); |
| 250 | } |
| 251 | const ready = transitionLifecycle(lifecycleState, LIFECYCLE_EVENTS.HEALTH_OK); |
| 252 | lifecycleState = ready.newState; |
| 253 | lastHealthOkAt = now(); |
| 254 | return handle; |
| 255 | }, |
| 256 | async healthProbe() { |
| 257 | if (!handle) { |
| 258 | markUnavailable(); |
| 259 | return false; |
| 260 | } |
| 261 | const healthy = await runtimeGroup.healthCheck(handle); |
| 262 | if (!healthy) { |
| 263 | markUnavailable(); |
| 264 | return false; |
| 265 | } |
| 266 | lastHealthOkAt = now(); |
| 267 | return true; |
| 268 | }, |
| 269 | async shutdown() { |
| 270 | markUnavailable(); |
| 271 | if (handle && typeof handle.kill === 'function') await handle.kill(); |
| 272 | const stopped = transitionLifecycle({ state: 'draining' }, LIFECYCLE_EVENTS.STOPPED); |
| 273 | lifecycleState = stopped.ok ? stopped.newState : createLifecycleState(); |
| 274 | handle = null; |
| 275 | }, |
| 276 | }; |
| 277 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
1 day ago