model-runtime-lane.mjs
file-level
1
files
1
commits
0
hotspots
0
π§ dead
0
π₯ blast risk
| 1 | /** |
| 2 | * Model-runtime lane selection, consent enforcement, and metering boundary. |
| 3 | * |
| 4 | * Phase 1 of the Companion App build plan (feat/companion-app). |
| 5 | * Implements: |
| 6 | * - D2.2 default-lane selection logic (selectLane) |
| 7 | * - D1.2 metering boundary: which lanes bill against Knowtation packs (isManagedLane) |
| 8 | * - D1.4 consent and workspace-policy gate (enforceConsentPolicy) |
| 9 | * |
| 10 | * DESIGN CONSTRAINTS (read before modifying): |
| 11 | * - Pure functions only β no I/O, no process.env reads, no network. |
| 12 | * - All inputs are passed explicitly so functions are composable at every layer |
| 13 | * (browser tab, companion app, gateway, CLI) without environment coupling. |
| 14 | * - Unknown / absent capability fields default to false: fail-closed, never fail-open. |
| 15 | * - See docs/COMPANION-APP-PHASE-1-ADAPTER-SEAM.md for the full seam contract. |
| 16 | * |
| 17 | * Hard constraint from docs/COMPANION-APP-MODEL-ROUTING-AND-ENRICHMENT-ARCHITECTURE.md Β§3: |
| 18 | * The cloud gateway NEVER proxies local inference. Local/companion lanes are invoked |
| 19 | * only by something on the user's machine. selectLane reflects this β it returns 'local' |
| 20 | * only when inBrowserAvailable or companionAvailable is true (client-side evidence). |
| 21 | */ |
| 22 | |
| 23 | /** |
| 24 | * The runtime lane identifiers β must match Scooling's runtimeLaneSchema exactly (D2.4). |
| 25 | * @readonly |
| 26 | */ |
| 27 | export const RUNTIME_LANES = /** @type {const} */ ([ |
| 28 | 'local', |
| 29 | 'self_hosted', |
| 30 | 'enterprise', |
| 31 | 'openrouter', |
| 32 | 'direct_provider', |
| 33 | 'disabled', |
| 34 | ]); |
| 35 | |
| 36 | /** |
| 37 | * @typedef {'local'|'self_hosted'|'enterprise'|'openrouter'|'direct_provider'|'disabled'} RuntimeLane |
| 38 | */ |
| 39 | |
| 40 | /** |
| 41 | * Runtime capability snapshot. |
| 42 | * Describes what inference resources are available in THIS execution context. |
| 43 | * All fields default to false β unknown = unavailable (fail-closed). |
| 44 | * |
| 45 | * @typedef {Object} LaneCapabilities |
| 46 | * @property {boolean} [inBrowserAvailable] |
| 47 | * WebGPU/WebLLM is usable in the current browser context. |
| 48 | * Always false in Node.js, gateway, and companion process contexts. |
| 49 | * @property {boolean} [companionAvailable] |
| 50 | * Companion loopback endpoint confirmed reachable (prior health-check passed). |
| 51 | * Never true in cloud/gateway contexts β the cloud cannot reach localhost (Β§3 constraint). |
| 52 | * @property {boolean} [selfHostedAvailable] |
| 53 | * Self-hosted or enterprise endpoint configured and confirmed reachable. |
| 54 | * @property {boolean} [enterpriseAvailable] |
| 55 | * Enterprise endpoint configured and confirmed reachable. |
| 56 | * @property {boolean} [openrouterKeyAvailable] |
| 57 | * OPENROUTER_API_KEY is present. Enables the BYO-key lane (user's own provider contract). |
| 58 | * @property {boolean} [managedKeyAvailable] |
| 59 | * At least one managed cloud key (DeepInfra/OpenAI/Anthropic) is available AND the |
| 60 | * operator has not locked out the managed lane. |
| 61 | */ |
| 62 | |
| 63 | /** |
| 64 | * Workspace/user lane preferences. |
| 65 | * Expressed by the authenticated user or enforced by workspace policy. |
| 66 | * |
| 67 | * @typedef {Object} LanePreferences |
| 68 | * @property {boolean} [keepOnDevice] |
| 69 | * User enabled "keep my data on my device" toggle. Biases selection toward local lanes. |
| 70 | * Does NOT hard-block managed β if no local compute is available the caller falls through |
| 71 | * to direct_provider, and enforceConsentPolicy then requires explicit per-request consent |
| 72 | * for private data (D2.2 fallback chain: in-browser β companion β managed-with-consent). |
| 73 | * @property {boolean} [orgPrivacyMode] |
| 74 | * Org-level policy: managed lane is OFF. Default to self-hosted/BYO/local. |
| 75 | * When true, selectLane never returns 'direct_provider'. |
| 76 | * @property {boolean} [delegatedManagedAllowed] |
| 77 | * Owner opt-in: delegates may trigger the managed lane against owner packs (D1.4). |
| 78 | * Relevant only when isDelegate=true. |
| 79 | * @property {boolean} [isDelegate] |
| 80 | * Requesting actor is acting on another user's partition (delegate=true from |
| 81 | * resolveEffectiveCanisterUser). Checked against delegatedManagedAllowed in the consent gate. |
| 82 | */ |
| 83 | |
| 84 | /** |
| 85 | * @typedef {'allow'|'cloud_consent_required'|'lane_policy_denied'} ConsentDecision |
| 86 | */ |
| 87 | |
| 88 | /** Default capabilities β all false (fail-closed). */ |
| 89 | const DEFAULT_CAPABILITIES = { |
| 90 | inBrowserAvailable: false, |
| 91 | companionAvailable: false, |
| 92 | selfHostedAvailable: false, |
| 93 | enterpriseAvailable: false, |
| 94 | openrouterKeyAvailable: false, |
| 95 | managedKeyAvailable: false, |
| 96 | }; |
| 97 | |
| 98 | /** Default preferences β individual user, no special mode or delegation. */ |
| 99 | const DEFAULT_PREFERENCES = { |
| 100 | keepOnDevice: false, |
| 101 | orgPrivacyMode: false, |
| 102 | delegatedManagedAllowed: false, |
| 103 | isDelegate: false, |
| 104 | }; |
| 105 | |
| 106 | /** |
| 107 | * Select the highest-privacy capable lane for the current context. |
| 108 | * Implements D2.2 default-lane selection logic from the Phase 0 Decision Record. |
| 109 | * |
| 110 | * Lane priority (org privacy mode OFF): |
| 111 | * 1. local β inBrowser or companion (highest privacy, free, client-side only) |
| 112 | * 2. self_hosted β org or power-user self-hosted endpoint |
| 113 | * 3. enterprise β enterprise org endpoint |
| 114 | * 4. openrouter β BYO key (user pays provider; not metered against packs) |
| 115 | * 5. direct_provider β managed cloud (metered; requires explicit consent for private data) |
| 116 | * 6. disabled β no lane can satisfy; caller falls back to embeddings-only / no inference |
| 117 | * |
| 118 | * When orgPrivacyMode=true: |
| 119 | * managed (direct_provider) is never selected. Priority ranks org-controlled infra first, |
| 120 | * then by data egress (local has zero egress; openrouter routes to a third party), so: |
| 121 | * self_hosted β enterprise β local β openrouter β disabled |
| 122 | * |
| 123 | * @param {LaneCapabilities} capabilities |
| 124 | * @param {LanePreferences} preferences |
| 125 | * @returns {RuntimeLane} |
| 126 | */ |
| 127 | export function selectLane(capabilities, preferences) { |
| 128 | const caps = { ...DEFAULT_CAPABILITIES, ...capabilities }; |
| 129 | const prefs = { ...DEFAULT_PREFERENCES, ...preferences }; |
| 130 | |
| 131 | if (prefs.orgPrivacyMode) { |
| 132 | if (caps.selfHostedAvailable) return 'self_hosted'; |
| 133 | if (caps.enterpriseAvailable) return 'enterprise'; |
| 134 | // Privacy mode ranks zero-egress local above third-party openrouter egress. |
| 135 | if (caps.inBrowserAvailable || caps.companionAvailable) return 'local'; |
| 136 | if (caps.openrouterKeyAvailable) return 'openrouter'; |
| 137 | return 'disabled'; |
| 138 | } |
| 139 | |
| 140 | if (caps.inBrowserAvailable || caps.companionAvailable) return 'local'; |
| 141 | |
| 142 | if (caps.selfHostedAvailable) return 'self_hosted'; |
| 143 | if (caps.enterpriseAvailable) return 'enterprise'; |
| 144 | if (caps.openrouterKeyAvailable) return 'openrouter'; |
| 145 | if (caps.managedKeyAvailable) return 'direct_provider'; |
| 146 | |
| 147 | return 'disabled'; |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Returns true when the given lane emits a metered billing event against Knowtation packs. |
| 152 | * |
| 153 | * D1.2: billing principal is the workspace owner of the target partition. |
| 154 | * Only 'direct_provider' (managed cloud) is metered β local, self_hosted, enterprise, |
| 155 | * and openrouter lanes are never metered (brief Β§6 principle 1): |
| 156 | * - local: zero provider cost (user's own compute) |
| 157 | * - self_hosted / enterprise: org pays its own infrastructure |
| 158 | * - openrouter: user pays their provider contract directly; Knowtation packs uninvolved |
| 159 | * |
| 160 | * @param {string} lane |
| 161 | * @returns {boolean} |
| 162 | */ |
| 163 | export function isManagedLane(lane) { |
| 164 | return lane === 'direct_provider'; |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Lanes that route an owner's note text OFF the owner's own controlled infrastructure |
| 169 | * when invoked by a delegate enriching the owner's partition: |
| 170 | * - 'local' β runs on the DELEGATE's device (companion / in-browser), not the owner's. |
| 171 | * - 'openrouter' β routes to the DELEGATE's own third-party provider contract. |
| 172 | * Both are gated by the owner's "allow delegated enrichment" opt-in (D1.3(2)). |
| 173 | * Org lanes ('self_hosted'/'enterprise') are governed by org policy, not this individual |
| 174 | * opt-in. The managed lane ('direct_provider') is governed by D1.4 (delegatedManagedAllowed). |
| 175 | * @type {ReadonlySet<string>} |
| 176 | */ |
| 177 | const DELEGATED_ENRICHMENT_GATED_LANES = new Set(['local', 'openrouter']); |
| 178 | |
| 179 | /** |
| 180 | * Consent and workspace-policy gate for model calls. |
| 181 | * |
| 182 | * Evaluation order β workspace policy (cannot be overridden by a consentId) before |
| 183 | * per-request consent: |
| 184 | * |
| 185 | * 1. MANAGED-LANE DELEGATE POLICY (D1.4): lane is managed AND isDelegate AND |
| 186 | * NOT delegatedManagedAllowed β 'lane_policy_denied'. |
| 187 | * Reason: the owner has not opted in to delegate-triggered managed-lane spend. |
| 188 | * Surface as a permission/policy message, not a consent prompt. |
| 189 | * |
| 190 | * 2. DELEGATED-ENRICHMENT POLICY (D1.3(2)): the call writes a derived artifact to a |
| 191 | * partition the actor does NOT own (enrichesDelegatedPartition) via a lane that runs |
| 192 | * off the owner's own infrastructure (local companion or the delegate's BYO openrouter) |
| 193 | * AND the owner has NOT enabled delegatedEnrichmentAllowed β 'lane_policy_denied'. |
| 194 | * Reason: "may a member's companion enrich an owner's notes?" β only with the owner's |
| 195 | * opt-in (default OFF). This closes the gate Β§12 canonical defect (a member's companion |
| 196 | * silently enriching an owner's notes). FAIL-CLOSED: default-OFF until the owner opts in. |
| 197 | * |
| 198 | * 3. CONSENT REQUIRED (per-request; fixable by providing a consentId): lane is managed |
| 199 | * AND containsPrivateData AND NOT consentId β 'cloud_consent_required'. |
| 200 | * Reason: private learner data must not reach a managed (cloud) provider without |
| 201 | * explicit per-action consent (D2.3 / D1.4 / brief Β§6). |
| 202 | * |
| 203 | * 4. ALLOW: all other cases. |
| 204 | * |
| 205 | * Notes: |
| 206 | * - A non-enrichment completion (enrichesDelegatedPartition=false) on a non-managed lane |
| 207 | * always allows β the delegate already has read scope (D1.3(1)); no artifact is written. |
| 208 | * - Org lanes (self_hosted / enterprise) are never gated here for delegated enrichment; |
| 209 | * the org controls the endpoint and governs that path by org policy. |
| 210 | * |
| 211 | * @param {{ |
| 212 | * lane: string, |
| 213 | * containsPrivateData: boolean, |
| 214 | * consentId?: string, |
| 215 | * isDelegate: boolean, |
| 216 | * delegatedManagedAllowed: boolean, |
| 217 | * enrichesDelegatedPartition?: boolean, |
| 218 | * delegatedEnrichmentAllowed?: boolean, |
| 219 | * }} params |
| 220 | * @returns {ConsentDecision} |
| 221 | */ |
| 222 | export function enforceConsentPolicy({ |
| 223 | lane, |
| 224 | containsPrivateData, |
| 225 | consentId, |
| 226 | isDelegate, |
| 227 | delegatedManagedAllowed, |
| 228 | enrichesDelegatedPartition = false, |
| 229 | delegatedEnrichmentAllowed = false, |
| 230 | }) { |
| 231 | // 1. Managed-lane delegate spend policy (D1.4) β precedes consent. |
| 232 | if (isManagedLane(lane)) { |
| 233 | if (isDelegate && !delegatedManagedAllowed) return 'lane_policy_denied'; |
| 234 | if (containsPrivateData && !consentId) return 'cloud_consent_required'; |
| 235 | return 'allow'; |
| 236 | } |
| 237 | |
| 238 | // 2. Delegated-enrichment policy (D1.3(2)) for non-managed, off-owner-infra lanes. |
| 239 | if ( |
| 240 | isDelegate && |
| 241 | enrichesDelegatedPartition && |
| 242 | DELEGATED_ENRICHMENT_GATED_LANES.has(lane) && |
| 243 | !delegatedEnrichmentAllowed |
| 244 | ) { |
| 245 | return 'lane_policy_denied'; |
| 246 | } |
| 247 | |
| 248 | return 'allow'; |
| 249 | } |