@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
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/README.md +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +33 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v2-client.js +14 -3
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +124 -240
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime contract admission for the actual OpenCode serve in use.
|
|
3
|
+
*
|
|
4
|
+
* Discovery may still prefer a global CLI / existing serve. Feature open is
|
|
5
|
+
* based on the running serve version against a documented matrix — not on
|
|
6
|
+
* "any 2.x health body" alone. Health success is reachability, not full
|
|
7
|
+
* execution semantics.
|
|
8
|
+
*
|
|
9
|
+
* Evidence bounds (ticket 11):
|
|
10
|
+
* - MIN_VERIFIED: pin / installed verification (`2.0.12`)
|
|
11
|
+
* Any acceptable OpenCode 2.x at or above MIN_VERIFIED may execute. Below-min
|
|
12
|
+
* 2.x and 1.x are refused with upgrade guidance; there is no upper version cap.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
PINNED_OPENCODE2_VERSION,
|
|
17
|
+
compareOpenCode2Versions,
|
|
18
|
+
isAcceptableOpenCode2HealthVersion,
|
|
19
|
+
isOpenCode1xVersion,
|
|
20
|
+
} from './opencode2-pin.js';
|
|
21
|
+
|
|
22
|
+
/** Lowest version with documented OpenChamber + OpenCode 2 contract tests. */
|
|
23
|
+
export const RUNTIME_CONTRACT_MIN_VERIFIED = PINNED_OPENCODE2_VERSION;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional / core capabilities and the minimum serve version that admits them.
|
|
27
|
+
* Tickets 01 / 02 / 05 / 06 share the verified floor until narrower evidence exists.
|
|
28
|
+
*/
|
|
29
|
+
export const RUNTIME_CONTRACT_CAPABILITIES = Object.freeze({
|
|
30
|
+
'core.protocol': {
|
|
31
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
32
|
+
requiredForExecution: true,
|
|
33
|
+
description: 'Core HTTP/SSE session protocol used by OpenChamber',
|
|
34
|
+
},
|
|
35
|
+
'session.prompt': {
|
|
36
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
37
|
+
requiredForExecution: true,
|
|
38
|
+
description: 'Prompt admission and idle send (ticket 02)',
|
|
39
|
+
},
|
|
40
|
+
'session.queuedInput': {
|
|
41
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
42
|
+
requiredForExecution: false,
|
|
43
|
+
description: 'Queued input selection / inheritance (ticket 01)',
|
|
44
|
+
},
|
|
45
|
+
'session.background': {
|
|
46
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
47
|
+
requiredForExecution: false,
|
|
48
|
+
description: 'Background / non-blocking work (ticket 05)',
|
|
49
|
+
},
|
|
50
|
+
'session.generationFallback': {
|
|
51
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
52
|
+
requiredForExecution: false,
|
|
53
|
+
description: 'Session generation fallback helpers (ticket 06)',
|
|
54
|
+
},
|
|
55
|
+
'migration.v1Read': {
|
|
56
|
+
minVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
57
|
+
requiredForExecution: false,
|
|
58
|
+
description: 'Read-only V1 migration status probe',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {unknown} value
|
|
64
|
+
* @returns {string | null}
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeRuntimeVersion(value) {
|
|
67
|
+
if (typeof value !== 'string') return null;
|
|
68
|
+
const trimmed = value.trim().replace(/^v/i, '');
|
|
69
|
+
return trimmed || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string | null | undefined} version
|
|
74
|
+
* @returns {'1x' | 'invalid' | 'below-min' | 'verified' | 'unknown'}
|
|
75
|
+
*/
|
|
76
|
+
export function classifyRuntimeVersionBand(version) {
|
|
77
|
+
const normalized = normalizeRuntimeVersion(version);
|
|
78
|
+
if (!normalized) return 'unknown';
|
|
79
|
+
if (isOpenCode1xVersion(normalized)) return '1x';
|
|
80
|
+
if (!isAcceptableOpenCode2HealthVersion(normalized)) return 'invalid';
|
|
81
|
+
if (compareOpenCode2Versions(normalized, RUNTIME_CONTRACT_MIN_VERIFIED) < 0) {
|
|
82
|
+
return 'below-min';
|
|
83
|
+
}
|
|
84
|
+
return 'verified';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string | null | undefined} version
|
|
89
|
+
* @param {string} minimum
|
|
90
|
+
*/
|
|
91
|
+
export function versionMeetsMinimum(version, minimum) {
|
|
92
|
+
const normalized = normalizeRuntimeVersion(version);
|
|
93
|
+
if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
|
|
94
|
+
return compareOpenCode2Versions(normalized, minimum) >= 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {{
|
|
99
|
+
* serveVersion?: string | null,
|
|
100
|
+
* cliVersion?: string | null,
|
|
101
|
+
* reachable?: boolean,
|
|
102
|
+
* authenticated?: boolean | null,
|
|
103
|
+
* healthOk?: boolean | null,
|
|
104
|
+
* migrationAdmitTranscript?: boolean | null,
|
|
105
|
+
* migrationPhase?: string | null,
|
|
106
|
+
* migrationError?: string | null,
|
|
107
|
+
* }} input
|
|
108
|
+
*/
|
|
109
|
+
export function evaluateRuntimeContract(input = {}) {
|
|
110
|
+
const serveVersion = normalizeRuntimeVersion(input.serveVersion);
|
|
111
|
+
const cliVersion = normalizeRuntimeVersion(input.cliVersion);
|
|
112
|
+
const reachable = input.reachable === true;
|
|
113
|
+
const authenticated = typeof input.authenticated === 'boolean' ? input.authenticated : null;
|
|
114
|
+
const healthOk = typeof input.healthOk === 'boolean' ? input.healthOk : null;
|
|
115
|
+
const migrationAdmit = typeof input.migrationAdmitTranscript === 'boolean'
|
|
116
|
+
? input.migrationAdmitTranscript
|
|
117
|
+
: null;
|
|
118
|
+
const migrationPhase = typeof input.migrationPhase === 'string' ? input.migrationPhase : null;
|
|
119
|
+
const migrationError = typeof input.migrationError === 'string' ? input.migrationError : null;
|
|
120
|
+
|
|
121
|
+
/** @type {string[]} */
|
|
122
|
+
const reasons = [];
|
|
123
|
+
const versionBand = classifyRuntimeVersionBand(serveVersion);
|
|
124
|
+
const versionMismatch = Boolean(
|
|
125
|
+
serveVersion
|
|
126
|
+
&& cliVersion
|
|
127
|
+
&& serveVersion !== cliVersion,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!reachable) {
|
|
131
|
+
reasons.push('unreachable');
|
|
132
|
+
}
|
|
133
|
+
if (authenticated === false) {
|
|
134
|
+
reasons.push('auth-failed');
|
|
135
|
+
}
|
|
136
|
+
if (healthOk === false) {
|
|
137
|
+
reasons.push('health-failed');
|
|
138
|
+
}
|
|
139
|
+
if (!serveVersion) {
|
|
140
|
+
reasons.push('serve-version-unknown');
|
|
141
|
+
} else if (versionBand === '1x') {
|
|
142
|
+
reasons.push('1x-version');
|
|
143
|
+
} else if (versionBand === 'invalid') {
|
|
144
|
+
reasons.push('invalid-version');
|
|
145
|
+
} else if (versionBand === 'below-min') {
|
|
146
|
+
reasons.push('below-min-verified');
|
|
147
|
+
}
|
|
148
|
+
if (versionMismatch) {
|
|
149
|
+
reasons.push('cli-serve-version-mismatch');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @type {Record<string, { available: boolean, reason: string | null, minVersion: string, requiredForExecution: boolean }>} */
|
|
153
|
+
const capabilities = {};
|
|
154
|
+
for (const [id, meta] of Object.entries(RUNTIME_CONTRACT_CAPABILITIES)) {
|
|
155
|
+
let available = false;
|
|
156
|
+
/** @type {string | null} */
|
|
157
|
+
let reason = null;
|
|
158
|
+
if (!reachable) {
|
|
159
|
+
reason = 'unreachable';
|
|
160
|
+
} else if (authenticated === false) {
|
|
161
|
+
reason = 'auth-failed';
|
|
162
|
+
} else if (healthOk === false) {
|
|
163
|
+
reason = 'health-failed';
|
|
164
|
+
} else if (!serveVersion) {
|
|
165
|
+
reason = 'serve-version-unknown';
|
|
166
|
+
} else if (versionBand === '1x') {
|
|
167
|
+
reason = '1x-version';
|
|
168
|
+
} else if (versionBand === 'invalid') {
|
|
169
|
+
reason = 'invalid-version';
|
|
170
|
+
} else if (!versionMeetsMinimum(serveVersion, meta.minVersion)) {
|
|
171
|
+
reason = 'below-capability-min';
|
|
172
|
+
} else {
|
|
173
|
+
available = true;
|
|
174
|
+
}
|
|
175
|
+
capabilities[id] = {
|
|
176
|
+
available,
|
|
177
|
+
reason,
|
|
178
|
+
minVersion: meta.minVersion,
|
|
179
|
+
requiredForExecution: meta.requiredForExecution === true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const protocolCompatible = reachable
|
|
184
|
+
&& authenticated !== false
|
|
185
|
+
&& healthOk !== false
|
|
186
|
+
&& versionBand === 'verified';
|
|
187
|
+
|
|
188
|
+
const requiredCapsOk = Object.values(capabilities)
|
|
189
|
+
.filter((cap) => cap.requiredForExecution)
|
|
190
|
+
.every((cap) => cap.available);
|
|
191
|
+
|
|
192
|
+
const migrationExecutable = migrationAdmit === null
|
|
193
|
+
? null
|
|
194
|
+
: migrationAdmit === true;
|
|
195
|
+
|
|
196
|
+
if (migrationAdmit === false) {
|
|
197
|
+
reasons.push(migrationPhase === 'error' ? 'migration-error' : 'migration-blocked');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const executionAllowed = reachable
|
|
201
|
+
&& authenticated !== false
|
|
202
|
+
&& healthOk !== false
|
|
203
|
+
&& protocolCompatible
|
|
204
|
+
&& requiredCapsOk
|
|
205
|
+
&& migrationAdmit !== false
|
|
206
|
+
&& versionBand === 'verified';
|
|
207
|
+
|
|
208
|
+
/** @type {string} */
|
|
209
|
+
let phase = 'ready';
|
|
210
|
+
if (!reachable) {
|
|
211
|
+
phase = 'unreachable';
|
|
212
|
+
} else if (authenticated === false) {
|
|
213
|
+
phase = 'unauthenticated';
|
|
214
|
+
} else if (!serveVersion || versionBand === 'invalid' || versionBand === 'unknown') {
|
|
215
|
+
phase = 'unknown';
|
|
216
|
+
} else if (versionBand === '1x' || versionBand === 'below-min' || !protocolCompatible) {
|
|
217
|
+
phase = 'incompatible';
|
|
218
|
+
} else if (migrationAdmit === false) {
|
|
219
|
+
phase = 'migration-blocked';
|
|
220
|
+
} else if (healthOk === false) {
|
|
221
|
+
phase = 'unhealthy';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
schemaVersion: 1,
|
|
226
|
+
phase,
|
|
227
|
+
reachable,
|
|
228
|
+
authenticated,
|
|
229
|
+
healthOk,
|
|
230
|
+
protocolCompatible,
|
|
231
|
+
executionAllowed,
|
|
232
|
+
migrationExecutable,
|
|
233
|
+
migrationPhase,
|
|
234
|
+
migrationError,
|
|
235
|
+
serveVersion,
|
|
236
|
+
cliVersion,
|
|
237
|
+
versionBand,
|
|
238
|
+
versionMismatch,
|
|
239
|
+
minVerifiedVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
240
|
+
maxVerifiedVersion: null,
|
|
241
|
+
pinnedVersion: PINNED_OPENCODE2_VERSION,
|
|
242
|
+
capabilities,
|
|
243
|
+
reasons,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether a proxied OpenCode API path is diagnostic-only (must stay available
|
|
249
|
+
* when execution is limited).
|
|
250
|
+
* @param {string} method
|
|
251
|
+
* @param {string} pathWithQuery
|
|
252
|
+
*/
|
|
253
|
+
export function isRuntimeContractDiagnosticPath(method, pathWithQuery) {
|
|
254
|
+
const upper = String(method || 'GET').toUpperCase();
|
|
255
|
+
if (upper !== 'GET' && upper !== 'HEAD') return false;
|
|
256
|
+
const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
|
|
257
|
+
const normalized = pathOnly.replace(/\/+$/, '') || '/';
|
|
258
|
+
return (
|
|
259
|
+
normalized === '/api/info'
|
|
260
|
+
|| normalized === '/api/health'
|
|
261
|
+
|| normalized === '/global/health'
|
|
262
|
+
|| normalized === '/api/experimental/migration/v1'
|
|
263
|
+
|| normalized.endsWith('/info')
|
|
264
|
+
|| normalized.endsWith('/health')
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stop / cancel paths that must remain available when execution is limited
|
|
270
|
+
* (user can still halt in-flight work and read diagnostics).
|
|
271
|
+
* @param {string} method
|
|
272
|
+
* @param {string} pathWithQuery
|
|
273
|
+
*/
|
|
274
|
+
export function isRuntimeContractStopPath(method, pathWithQuery) {
|
|
275
|
+
const upper = String(method || 'GET').toUpperCase();
|
|
276
|
+
if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return false;
|
|
277
|
+
const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
|
|
278
|
+
return /\/session(?:\/[^/]+)?\/(?:interrupt|abort)\b/.test(pathOnly);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Mutating session / turn paths that require executionAllowed.
|
|
283
|
+
* Interrupt/abort are excluded so stop-task remains available under limited execution.
|
|
284
|
+
* @param {string} method
|
|
285
|
+
* @param {string} pathWithQuery
|
|
286
|
+
*/
|
|
287
|
+
export function isRuntimeContractExecutionPath(method, pathWithQuery) {
|
|
288
|
+
const upper = String(method || 'GET').toUpperCase();
|
|
289
|
+
if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return false;
|
|
290
|
+
if (isRuntimeContractStopPath(method, pathWithQuery)) return false;
|
|
291
|
+
const pathOnly = String(pathWithQuery || '').split('?')[0] || '';
|
|
292
|
+
// Official v2 turn + control surfaces (and classic aliases). Reads stay open.
|
|
293
|
+
return /\/session(?:\/[^/]+)?\/(?:prompt|prompt_async|command|shell|revert|unrevert|summarize|fork|generate|background|model|agent|synthetic|compact|skill|move|environment|instructions)(?:\/|$)/.test(pathOnly)
|
|
294
|
+
|| ((upper === 'POST' || upper === 'PATCH') && /\/session\/[^/]+\/inbox\/[^/]+(?:\/(?:steer|queue))?\/?$/.test(pathOnly))
|
|
295
|
+
|| (upper === 'POST' && /\/session\/[^/]+\/form(?:\/|$)/.test(pathOnly))
|
|
296
|
+
|| /\/session(?:\/[^/]+)?\/message(?:\/|$)/.test(pathOnly)
|
|
297
|
+
|| /\/permission\//.test(pathOnly)
|
|
298
|
+
|| /\/question\//.test(pathOnly)
|
|
299
|
+
// Session create / patch title metadata used by host background (queue/goal/scheduled).
|
|
300
|
+
|| (upper === 'POST' && /\/session\/?$/.test(pathOnly))
|
|
301
|
+
|| (upper === 'PATCH' && /\/session\/[^/]+\/?$/.test(pathOnly));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Pending / revoked snapshot published while a new serve instance is starting
|
|
306
|
+
* or after start/restart/target change invalidates the previous permit.
|
|
307
|
+
* @param {string} [reason]
|
|
308
|
+
* @param {{ instanceGeneration?: number | null }} [extra]
|
|
309
|
+
*/
|
|
310
|
+
export function createRevokedRuntimeContract(reason = 'instance-revoked', extra = {}) {
|
|
311
|
+
const base = evaluateRuntimeContract({
|
|
312
|
+
reachable: false,
|
|
313
|
+
authenticated: null,
|
|
314
|
+
healthOk: null,
|
|
315
|
+
serveVersion: null,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
phase: 'pending',
|
|
320
|
+
executionAllowed: false,
|
|
321
|
+
reasons: reason ? [reason, ...base.reasons.filter((entry) => entry !== 'unreachable')] : base.reasons,
|
|
322
|
+
instanceGeneration: typeof extra.instanceGeneration === 'number' ? extra.instanceGeneration : null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether a host-side OpenCode request should be blocked by contract admission.
|
|
328
|
+
* Diagnostics, reads, and stop-task stay available when execution is limited.
|
|
329
|
+
*
|
|
330
|
+
* Production default: execution paths require the **current** contract to set
|
|
331
|
+
* `executionAllowed === true` explicitly. A null/missing contract blocks writes
|
|
332
|
+
* (no silent bypass during startup or after revoke). Unit/DI factories may pass
|
|
333
|
+
* `{ allowMissingContract: true }` when no contract is wired.
|
|
334
|
+
*
|
|
335
|
+
* @param {string} method
|
|
336
|
+
* @param {string} pathWithQuery
|
|
337
|
+
* @param {{ executionAllowed?: boolean } | null | undefined} contract
|
|
338
|
+
* @param {{ allowMissingContract?: boolean }} [options]
|
|
339
|
+
*/
|
|
340
|
+
export function shouldBlockRuntimeContractExecution(method, pathWithQuery, contract, options = {}) {
|
|
341
|
+
if (isRuntimeContractDiagnosticPath(method, pathWithQuery)) return false;
|
|
342
|
+
if (isRuntimeContractStopPath(method, pathWithQuery)) return false;
|
|
343
|
+
if (!isRuntimeContractExecutionPath(method, pathWithQuery)) return false;
|
|
344
|
+
|
|
345
|
+
if (contract == null || typeof contract !== 'object') {
|
|
346
|
+
// Test DI only: production never opts into missing-contract bypass.
|
|
347
|
+
return options.allowMissingContract !== true;
|
|
348
|
+
}
|
|
349
|
+
// Explicit true only — undefined / false / pending all block execution paths.
|
|
350
|
+
return contract.executionAllowed !== true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* User-facing message when execution is blocked by version admission.
|
|
355
|
+
* @param {object | null | undefined} contract
|
|
356
|
+
*/
|
|
357
|
+
export function formatRuntimeContractBlockedError(contract) {
|
|
358
|
+
const serve = contract?.serveVersion ?? null;
|
|
359
|
+
const min = contract?.minVerifiedVersion ?? RUNTIME_CONTRACT_MIN_VERIFIED;
|
|
360
|
+
const pinned = contract?.pinnedVersion ?? PINNED_OPENCODE2_VERSION;
|
|
361
|
+
const reasons = Array.isArray(contract?.reasons) ? contract.reasons : [];
|
|
362
|
+
|
|
363
|
+
if (reasons.includes('below-min-verified')) {
|
|
364
|
+
const current = serve ? `OpenCode ${serve}` : 'This OpenCode version';
|
|
365
|
+
return `${current} is below the minimum supported version (${min}). Upgrade to ${min} or newer (recommended: ${pinned}). Open Settings → About to update in OpenChamber, or upgrade your global \`opencode\` CLI if you manage it manually.`;
|
|
366
|
+
}
|
|
367
|
+
if (reasons.includes('1x-version')) {
|
|
368
|
+
return `OpenCode 1.x is not supported. Install OpenCode 2 (${min} or newer; recommended: ${pinned}).`;
|
|
369
|
+
}
|
|
370
|
+
if (reasons.includes('invalid-version') || reasons.includes('serve-version-unknown')) {
|
|
371
|
+
return `OpenCode version could not be verified. Install OpenCode 2 (${min} or newer; recommended: ${pinned}) and restart the runtime.`;
|
|
372
|
+
}
|
|
373
|
+
if (reasons.includes('migration-blocked') || reasons.includes('migration-error')) {
|
|
374
|
+
return 'OpenCode is finishing V1 history migration. Wait for migration to complete, then try again.';
|
|
375
|
+
}
|
|
376
|
+
if (reasons.includes('auth-failed')) {
|
|
377
|
+
return 'OpenCode authentication failed. Check OpenCode credentials in Settings and restart the runtime.';
|
|
378
|
+
}
|
|
379
|
+
if (reasons.includes('health-failed') || reasons.includes('unreachable')) {
|
|
380
|
+
return 'OpenCode is not reachable. Restart OpenCode from Settings → About or check that the serve process is running.';
|
|
381
|
+
}
|
|
382
|
+
return 'OpenCode is not ready to run sessions yet. Check Settings → About for version and runtime status.';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Structured 409 body shared by proxy + serverOpenCodeFetch.
|
|
387
|
+
* @param {object} contract
|
|
388
|
+
*/
|
|
389
|
+
export function runtimeContractExecutionBlockedBody(contract) {
|
|
390
|
+
return {
|
|
391
|
+
error: formatRuntimeContractBlockedError(contract),
|
|
392
|
+
errorCode: 'RUNTIME_CONTRACT_EXECUTION_BLOCKED',
|
|
393
|
+
phase: contract?.phase ?? null,
|
|
394
|
+
reasons: Array.isArray(contract?.reasons) ? contract.reasons : [],
|
|
395
|
+
serveVersion: contract?.serveVersion ?? null,
|
|
396
|
+
minVerifiedVersion: contract?.minVerifiedVersion ?? null,
|
|
397
|
+
maxVerifiedVersion: contract?.maxVerifiedVersion ?? null,
|
|
398
|
+
pinnedVersion: contract?.pinnedVersion ?? PINNED_OPENCODE2_VERSION,
|
|
399
|
+
contract: contract ?? null,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
4
|
+
classifyRuntimeVersionBand,
|
|
5
|
+
createRevokedRuntimeContract,
|
|
6
|
+
evaluateRuntimeContract,
|
|
7
|
+
formatRuntimeContractBlockedError,
|
|
8
|
+
isRuntimeContractDiagnosticPath,
|
|
9
|
+
isRuntimeContractExecutionPath,
|
|
10
|
+
runtimeContractExecutionBlockedBody,
|
|
11
|
+
shouldBlockRuntimeContractExecution,
|
|
12
|
+
versionMeetsMinimum,
|
|
13
|
+
} from './runtime-contract.js';
|
|
14
|
+
|
|
15
|
+
describe('runtime contract admission (ticket 11)', () => {
|
|
16
|
+
it('records minimum verified bound from pin', () => {
|
|
17
|
+
expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.12');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('classifies version bands with a 2.x floor only', () => {
|
|
21
|
+
expect(classifyRuntimeVersionBand('2.0.12')).toBe('verified');
|
|
22
|
+
expect(classifyRuntimeVersionBand('2.0.14')).toBe('verified');
|
|
23
|
+
expect(classifyRuntimeVersionBand('2.0.15')).toBe('verified');
|
|
24
|
+
expect(classifyRuntimeVersionBand('2.1.0')).toBe('verified');
|
|
25
|
+
expect(classifyRuntimeVersionBand('2.0.5')).toBe('below-min');
|
|
26
|
+
expect(classifyRuntimeVersionBand('1.18.18')).toBe('1x');
|
|
27
|
+
expect(classifyRuntimeVersionBand('not-a-version')).toBe('invalid');
|
|
28
|
+
expect(classifyRuntimeVersionBand(null)).toBe('unknown');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('admits verified serve for core protocol and optional caps', () => {
|
|
32
|
+
const result = evaluateRuntimeContract({
|
|
33
|
+
serveVersion: '2.0.12',
|
|
34
|
+
cliVersion: '2.0.12',
|
|
35
|
+
reachable: true,
|
|
36
|
+
authenticated: true,
|
|
37
|
+
healthOk: true,
|
|
38
|
+
migrationAdmitTranscript: true,
|
|
39
|
+
});
|
|
40
|
+
expect(result.phase).toBe('ready');
|
|
41
|
+
expect(result.protocolCompatible).toBe(true);
|
|
42
|
+
expect(result.executionAllowed).toBe(true);
|
|
43
|
+
expect(result.capabilities['core.protocol'].available).toBe(true);
|
|
44
|
+
expect(result.capabilities['session.queuedInput'].available).toBe(true);
|
|
45
|
+
expect(result.capabilities['session.background'].available).toBe(true);
|
|
46
|
+
expect(result.capabilities['session.generationFallback'].available).toBe(true);
|
|
47
|
+
expect(result.reasons).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('limits older 2.x and keeps diagnostic dimensions explicit', () => {
|
|
51
|
+
const result = evaluateRuntimeContract({
|
|
52
|
+
serveVersion: '2.0.5',
|
|
53
|
+
cliVersion: '2.0.5',
|
|
54
|
+
reachable: true,
|
|
55
|
+
authenticated: true,
|
|
56
|
+
healthOk: true,
|
|
57
|
+
migrationAdmitTranscript: true,
|
|
58
|
+
});
|
|
59
|
+
expect(result.phase).toBe('incompatible');
|
|
60
|
+
expect(result.protocolCompatible).toBe(false);
|
|
61
|
+
expect(result.executionAllowed).toBe(false);
|
|
62
|
+
expect(result.reasons).toContain('below-min-verified');
|
|
63
|
+
expect(result.capabilities['core.protocol'].available).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('admits newer 2.x above the documented floor for execution', () => {
|
|
67
|
+
const result = evaluateRuntimeContract({
|
|
68
|
+
serveVersion: '2.0.15',
|
|
69
|
+
reachable: true,
|
|
70
|
+
authenticated: true,
|
|
71
|
+
healthOk: true,
|
|
72
|
+
migrationAdmitTranscript: true,
|
|
73
|
+
});
|
|
74
|
+
expect(result.versionBand).toBe('verified');
|
|
75
|
+
expect(result.phase).toBe('ready');
|
|
76
|
+
expect(result.protocolCompatible).toBe(true);
|
|
77
|
+
expect(result.executionAllowed).toBe(true);
|
|
78
|
+
expect(result.capabilities['core.protocol'].available).toBe(true);
|
|
79
|
+
expect(result.maxVerifiedVersion).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects 1.x and unparseable versions for execution', () => {
|
|
83
|
+
const oneX = evaluateRuntimeContract({
|
|
84
|
+
serveVersion: '1.18.18',
|
|
85
|
+
reachable: true,
|
|
86
|
+
authenticated: true,
|
|
87
|
+
healthOk: true,
|
|
88
|
+
});
|
|
89
|
+
expect(oneX.phase).toBe('incompatible');
|
|
90
|
+
expect(oneX.executionAllowed).toBe(false);
|
|
91
|
+
expect(oneX.reasons).toContain('1x-version');
|
|
92
|
+
|
|
93
|
+
const bad = evaluateRuntimeContract({
|
|
94
|
+
serveVersion: 'totally-bogus',
|
|
95
|
+
reachable: true,
|
|
96
|
+
authenticated: true,
|
|
97
|
+
healthOk: true,
|
|
98
|
+
});
|
|
99
|
+
expect(bad.phase).toBe('unknown');
|
|
100
|
+
expect(bad.executionAllowed).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('separates auth failure, unreachable, migration block, and cli/serve mismatch', () => {
|
|
104
|
+
expect(evaluateRuntimeContract({ reachable: false }).phase).toBe('unreachable');
|
|
105
|
+
expect(evaluateRuntimeContract({
|
|
106
|
+
reachable: true,
|
|
107
|
+
authenticated: false,
|
|
108
|
+
serveVersion: '2.0.12',
|
|
109
|
+
healthOk: true,
|
|
110
|
+
}).phase).toBe('unauthenticated');
|
|
111
|
+
|
|
112
|
+
const migration = evaluateRuntimeContract({
|
|
113
|
+
serveVersion: '2.0.12',
|
|
114
|
+
reachable: true,
|
|
115
|
+
authenticated: true,
|
|
116
|
+
healthOk: true,
|
|
117
|
+
migrationAdmitTranscript: false,
|
|
118
|
+
migrationPhase: 'running',
|
|
119
|
+
});
|
|
120
|
+
expect(migration.phase).toBe('migration-blocked');
|
|
121
|
+
expect(migration.executionAllowed).toBe(false);
|
|
122
|
+
expect(migration.migrationExecutable).toBe(false);
|
|
123
|
+
|
|
124
|
+
const mismatch = evaluateRuntimeContract({
|
|
125
|
+
serveVersion: '2.0.12',
|
|
126
|
+
cliVersion: '2.0.14',
|
|
127
|
+
reachable: true,
|
|
128
|
+
authenticated: true,
|
|
129
|
+
healthOk: true,
|
|
130
|
+
migrationAdmitTranscript: true,
|
|
131
|
+
});
|
|
132
|
+
expect(mismatch.versionMismatch).toBe(true);
|
|
133
|
+
expect(mismatch.reasons).toContain('cli-serve-version-mismatch');
|
|
134
|
+
// Mismatch is diagnostic; admission still follows serve version.
|
|
135
|
+
expect(mismatch.executionAllowed).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('classifies diagnostic vs execution proxy paths', () => {
|
|
139
|
+
expect(isRuntimeContractDiagnosticPath('GET', '/api/info')).toBe(true);
|
|
140
|
+
expect(isRuntimeContractDiagnosticPath('GET', '/api/health')).toBe(true);
|
|
141
|
+
expect(isRuntimeContractDiagnosticPath('POST', '/api/info')).toBe(false);
|
|
142
|
+
expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/prompt')).toBe(true);
|
|
143
|
+
expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/prompt_async')).toBe(true);
|
|
144
|
+
expect(isRuntimeContractExecutionPath('GET', '/api/session/ses_1/message')).toBe(false);
|
|
145
|
+
// Stop-task remains available when execution is limited.
|
|
146
|
+
expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/interrupt')).toBe(false);
|
|
147
|
+
expect(isRuntimeContractExecutionPath('POST', '/api/session/ses_1/abort')).toBe(false);
|
|
148
|
+
expect(versionMeetsMinimum('2.0.14', '2.0.12')).toBe(true);
|
|
149
|
+
expect(versionMeetsMinimum('2.0.1', '2.0.12')).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it.each([
|
|
153
|
+
'/api/session/ses_1/generate',
|
|
154
|
+
'/api/session/ses_1/background',
|
|
155
|
+
'/api/session/ses_1/inbox/msg_1/steer',
|
|
156
|
+
'/api/session/ses_1/inbox/msg_1/queue',
|
|
157
|
+
'/api/session/ses_1/form/form_1/reply',
|
|
158
|
+
'/api/session/ses_1/model',
|
|
159
|
+
'/api/session/ses_1/agent',
|
|
160
|
+
])('limits the execution control endpoint %s for an unverified runtime', (path) => {
|
|
161
|
+
expect(shouldBlockRuntimeContractExecution('POST', path, { executionAllowed: false })).toBe(true);
|
|
162
|
+
expect(shouldBlockRuntimeContractExecution('POST', path, { executionAllowed: true })).toBe(false);
|
|
163
|
+
expect(shouldBlockRuntimeContractExecution('GET', path, { executionAllowed: false })).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps stopping and cancelling pending work available under limited execution', () => {
|
|
167
|
+
const contract = { executionAllowed: false };
|
|
168
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/interrupt', contract)).toBe(false);
|
|
169
|
+
expect(shouldBlockRuntimeContractExecution('DELETE', '/api/session/ses_1/inbox/msg_1', contract)).toBe(false);
|
|
170
|
+
expect(shouldBlockRuntimeContractExecution('PATCH', '/api/session/ses_1/inbox/msg_1', contract)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('exposes upgrade guidance in blocked execution responses for below-min serve', () => {
|
|
174
|
+
const contract = evaluateRuntimeContract({
|
|
175
|
+
serveVersion: '2.0.5',
|
|
176
|
+
reachable: true,
|
|
177
|
+
authenticated: true,
|
|
178
|
+
healthOk: true,
|
|
179
|
+
migrationAdmitTranscript: true,
|
|
180
|
+
});
|
|
181
|
+
const body = runtimeContractExecutionBlockedBody(contract);
|
|
182
|
+
expect(body.error).toContain('2.0.12');
|
|
183
|
+
expect(body.error).toMatch(/Settings → About/i);
|
|
184
|
+
expect(formatRuntimeContractBlockedError(contract)).toBe(body.error);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('production default blocks execution when contract is null (no silent bypass)', () => {
|
|
188
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', null)).toBe(true);
|
|
189
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', undefined)).toBe(true);
|
|
190
|
+
expect(shouldBlockRuntimeContractExecution('GET', '/api/session/ses_1/message', null)).toBe(false);
|
|
191
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/interrupt', null)).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('requires explicit executionAllowed === true (pending / undefined still block)', () => {
|
|
195
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', { executionAllowed: true })).toBe(false);
|
|
196
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', {})).toBe(true);
|
|
197
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', { executionAllowed: false })).toBe(true);
|
|
198
|
+
const revoked = createRevokedRuntimeContract('instance-revoked', { instanceGeneration: 3 });
|
|
199
|
+
expect(revoked.executionAllowed).toBe(false);
|
|
200
|
+
expect(revoked.phase).toBe('pending');
|
|
201
|
+
expect(revoked.instanceGeneration).toBe(3);
|
|
202
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', revoked)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('DI factory may allow missing contract only when explicitly opted in', () => {
|
|
206
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', null, {
|
|
207
|
+
allowMissingContract: true,
|
|
208
|
+
})).toBe(false);
|
|
209
|
+
// Opt-in does not open denied contracts.
|
|
210
|
+
expect(shouldBlockRuntimeContractExecution('POST', '/api/session/ses_1/prompt', {
|
|
211
|
+
executionAllowed: false,
|
|
212
|
+
}, { allowMissingContract: true })).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|