@aiworker/web 1.22.0 → 1.23.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/README.md +4 -4
- package/bin/cli.js +14 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +19 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +19 -17
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +15 -18
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +91 -30
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/DiffView-DluiRSla.js +0 -4
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
createFragmentAssembler,
|
|
16
16
|
decodeJsonPayload,
|
|
17
17
|
decodeTunnelFrame,
|
|
18
|
-
encodeFragmentedMessage,
|
|
19
18
|
encodeJsonPayload,
|
|
20
19
|
encodeTunnelFrame,
|
|
21
20
|
} from './tunnel-codec.js';
|
|
@@ -33,7 +32,9 @@ const ALLOWED_WS_PATHS = new Set([
|
|
|
33
32
|
'/api/event/ws',
|
|
34
33
|
'/api/terminal/ws',
|
|
35
34
|
'/api/dictation/ws',
|
|
35
|
+
'/api/dev-tunnel',
|
|
36
36
|
]);
|
|
37
|
+
export const isAllowedRelayWebSocketPath = (pathname) => ALLOWED_WS_PATHS.has(pathname);
|
|
37
38
|
|
|
38
39
|
// Hop-by-hop headers stripped from tunneled requests; `host` is set by fetch
|
|
39
40
|
// to the loopback origin. content-length is dropped too because the body is
|
|
@@ -72,6 +73,11 @@ const BODY_BUFFER_MAX_BYTES = 512 * 1024;
|
|
|
72
73
|
// completes, so a stalled tunnel converts into an ambiguous transport failure
|
|
73
74
|
// (which the client already retries) instead of a hung loopback request.
|
|
74
75
|
const BODY_DELIVERY_TIMEOUT_MS = 15_000;
|
|
76
|
+
const MAX_PENDING_WS_BYTES = 16 * 1024 * 1024;
|
|
77
|
+
const MAX_PENDING_WS_MESSAGES = 1024;
|
|
78
|
+
// Node ws supports read backpressure. Bun's ws shim does not implement pause;
|
|
79
|
+
// there the bounded queue fails the affected substream explicitly on overflow.
|
|
80
|
+
const CAN_PAUSE_WS_READS = !process.versions.bun;
|
|
75
81
|
|
|
76
82
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
83
|
|
|
@@ -97,13 +103,17 @@ const isWsClosePayload = (parsed) => Boolean(parsed && typeof parsed === 'object
|
|
|
97
103
|
* sendFrame: (plaintextFrame: Uint8Array) => void | Promise<void>,
|
|
98
104
|
* getBufferedAmount: () => number,
|
|
99
105
|
* bodyDeliveryTimeoutMs?: number,
|
|
106
|
+
* responseChunkBytes?: number,
|
|
107
|
+
* cancelPendingFrames?: (streamId: number) => void,
|
|
100
108
|
* }} deps
|
|
101
109
|
*/
|
|
102
|
-
export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBufferedAmount, bodyDeliveryTimeoutMs = BODY_DELIVERY_TIMEOUT_MS }) => {
|
|
110
|
+
export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBufferedAmount, bodyDeliveryTimeoutMs = BODY_DELIVERY_TIMEOUT_MS, responseChunkBytes = MAX_TUNNEL_PAYLOAD_BYTES, cancelPendingFrames = () => {} }) => {
|
|
103
111
|
/** @type {Map<number, { kind: 'http', abort: AbortController, body: { enqueue(payload: Uint8Array): void, close(): void, error(error: Error): void } | null, noBody: boolean } | { kind: 'ws', socket: WebSocket, opened: boolean }>} */
|
|
104
112
|
const streams = new Map();
|
|
105
113
|
const assembler = createFragmentAssembler();
|
|
106
114
|
let closed = false;
|
|
115
|
+
let pendingWsBytes = 0;
|
|
116
|
+
let pendingWsMessages = 0;
|
|
107
117
|
|
|
108
118
|
const send = async (frame) => {
|
|
109
119
|
if (closed) return;
|
|
@@ -126,6 +136,7 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
126
136
|
const stream = streams.get(streamId);
|
|
127
137
|
if (!stream) return;
|
|
128
138
|
dropStream(streamId);
|
|
139
|
+
cancelPendingFrames(streamId);
|
|
129
140
|
if (stream.kind === 'http') {
|
|
130
141
|
try {
|
|
131
142
|
stream.body?.error(new Error(String(reason ?? 'aborted')));
|
|
@@ -203,17 +214,27 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
203
214
|
if (STRIPPED_RESPONSE_HEADERS.has(name)) continue;
|
|
204
215
|
responseHeaders[name] = value;
|
|
205
216
|
}
|
|
206
|
-
|
|
217
|
+
if (closed || stream.abort.signal.aborted) {
|
|
218
|
+
await response.body?.cancel();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
207
221
|
|
|
208
222
|
try {
|
|
223
|
+
await sendJson(TunnelFrameType.HttpResponse, streamId, { status: response.status, headers: responseHeaders });
|
|
209
224
|
if (response.body) {
|
|
210
225
|
for await (const chunk of response.body) {
|
|
211
226
|
if (closed || stream.abort.signal.aborted) return;
|
|
212
227
|
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
213
|
-
|
|
228
|
+
const pieces = chunkPayload(bytes, responseChunkBytes);
|
|
229
|
+
// Offer at most one plaintext-frame budget at a time. The scheduler
|
|
230
|
+
// can batch small slices and interleave streams without buffering the
|
|
231
|
+
// entire source or encrypting hundreds of tiny messages separately.
|
|
232
|
+
const groupSize = Math.max(1, Math.floor(MAX_TUNNEL_PAYLOAD_BYTES / responseChunkBytes));
|
|
233
|
+
for (let offset = 0; offset < pieces.length; offset += groupSize) {
|
|
214
234
|
await waitForBackpressure(stream.abort.signal);
|
|
215
235
|
if (closed || stream.abort.signal.aborted) return;
|
|
216
|
-
await
|
|
236
|
+
await Promise.all(pieces.slice(offset, offset + groupSize).map(piece =>
|
|
237
|
+
send(encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, piece))));
|
|
217
238
|
}
|
|
218
239
|
}
|
|
219
240
|
}
|
|
@@ -425,7 +446,7 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
425
446
|
void sendAbort(streamId, error?.message ?? 'malformed ws open');
|
|
426
447
|
return;
|
|
427
448
|
}
|
|
428
|
-
if (!
|
|
449
|
+
if (!isAllowedRelayWebSocketPath(open.path)) {
|
|
429
450
|
void sendAbort(streamId, 'Path is not allowed through the relay');
|
|
430
451
|
return;
|
|
431
452
|
}
|
|
@@ -446,6 +467,7 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
446
467
|
try {
|
|
447
468
|
socket = new WebSocket(url, open.protocols, {
|
|
448
469
|
headers: dialHeaders,
|
|
470
|
+
maxPayload: MAX_PENDING_WS_BYTES,
|
|
449
471
|
});
|
|
450
472
|
} catch (error) {
|
|
451
473
|
void sendAbort(streamId, error?.message ?? 'ws dial failed');
|
|
@@ -453,6 +475,8 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
453
475
|
}
|
|
454
476
|
const stream = { kind: 'ws', socket, opened: false };
|
|
455
477
|
streams.set(streamId, stream);
|
|
478
|
+
let outputChain = Promise.resolve();
|
|
479
|
+
let socketPendingMessages = 0;
|
|
456
480
|
|
|
457
481
|
socket.on('open', () => {
|
|
458
482
|
if (streams.get(streamId) !== stream) return;
|
|
@@ -463,23 +487,47 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
463
487
|
if (streams.get(streamId) !== stream || closed) return;
|
|
464
488
|
const bytes = Buffer.isBuffer(data) ? new Uint8Array(data) : new Uint8Array(Buffer.concat(data));
|
|
465
489
|
const frameType = isBinary ? TunnelFrameType.WsBinary : TunnelFrameType.WsText;
|
|
466
|
-
|
|
467
|
-
|
|
490
|
+
if (pendingWsBytes + bytes.length > MAX_PENDING_WS_BYTES || pendingWsMessages >= MAX_PENDING_WS_MESSAGES) {
|
|
491
|
+
abortLocalStream(streamId, 'upstream WebSocket exceeded downstream queue limit');
|
|
492
|
+
void sendAbort(streamId, 'upstream WebSocket exceeded downstream queue limit');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
pendingWsBytes += bytes.length;
|
|
496
|
+
pendingWsMessages += 1;
|
|
497
|
+
socketPendingMessages += 1;
|
|
498
|
+
if (CAN_PAUSE_WS_READS) socket.pause();
|
|
499
|
+
outputChain = outputChain.then(async () => {
|
|
500
|
+
// Serialize entire messages, including their fragments. A later close
|
|
501
|
+
// must also wait here or it can overtake the final output.
|
|
502
|
+
const chunks = chunkPayload(bytes, responseChunkBytes);
|
|
503
|
+
const groupSize = Math.max(1, Math.floor(MAX_TUNNEL_PAYLOAD_BYTES / responseChunkBytes));
|
|
504
|
+
for (let offset = 0; offset < chunks.length; offset += groupSize) {
|
|
468
505
|
await waitForBackpressure(null);
|
|
469
506
|
if (streams.get(streamId) !== stream || closed) return;
|
|
470
|
-
await
|
|
507
|
+
await Promise.all(chunks.slice(offset, offset + groupSize).map((chunk, index) =>
|
|
508
|
+
send(encodeTunnelFrame(frameType, streamId, chunk, offset + index < chunks.length - 1))));
|
|
471
509
|
}
|
|
472
|
-
})()
|
|
510
|
+
}).catch(() => {
|
|
511
|
+
abortLocalStream(streamId, 'upstream WebSocket forwarding failed');
|
|
512
|
+
void sendAbort(streamId, 'upstream WebSocket forwarding failed');
|
|
513
|
+
}).finally(() => {
|
|
514
|
+
pendingWsBytes -= bytes.length;
|
|
515
|
+
pendingWsMessages -= 1;
|
|
516
|
+
socketPendingMessages -= 1;
|
|
517
|
+
if (CAN_PAUSE_WS_READS && socketPendingMessages === 0 && streams.get(streamId) === stream && socket.readyState === WebSocket.OPEN) socket.resume();
|
|
518
|
+
});
|
|
473
519
|
});
|
|
474
520
|
socket.on('close', (code, reasonBuffer) => {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
521
|
+
void outputChain.then(async () => {
|
|
522
|
+
if (streams.get(streamId) !== stream) return;
|
|
523
|
+
dropStream(streamId);
|
|
524
|
+
const reason = reasonBuffer ? reasonBuffer.toString('utf8') : '';
|
|
525
|
+
if (stream.opened) {
|
|
526
|
+
await sendJson(TunnelFrameType.WsClose, streamId, { code: code || 1000, reason });
|
|
527
|
+
} else {
|
|
528
|
+
await sendAbort(streamId, reason || `upstream ws closed (${code || 'no code'})`);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
483
531
|
});
|
|
484
532
|
socket.on('error', (error) => {
|
|
485
533
|
if (streams.get(streamId) !== stream) return;
|
|
@@ -510,6 +558,7 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
510
558
|
const stream = streams.get(streamId);
|
|
511
559
|
if (!stream || stream.kind !== 'ws') return;
|
|
512
560
|
dropStream(streamId);
|
|
561
|
+
cancelPendingFrames(streamId);
|
|
513
562
|
let close = { code: 1000, reason: '' };
|
|
514
563
|
try {
|
|
515
564
|
close = decodeJsonPayload(payload, isWsClosePayload);
|
|
@@ -518,6 +567,8 @@ export const createTunnelHost = ({ connectionId, getLocalPort, sendFrame, getBuf
|
|
|
518
567
|
}
|
|
519
568
|
const code = Number.isInteger(close.code) && close.code >= 1000 && close.code <= 4999 ? close.code : 1000;
|
|
520
569
|
try {
|
|
570
|
+
// A paused receiver still needs to read the peer's close handshake.
|
|
571
|
+
if (CAN_PAUSE_WS_READS) stream.socket.resume();
|
|
521
572
|
stream.socket.close(code, typeof close.reason === 'string' ? close.reason : '');
|
|
522
573
|
} catch {
|
|
523
574
|
stream.socket.terminate();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
|
|
4
|
-
import { createTunnelHost } from './tunnel-host.js';
|
|
4
|
+
import { createTunnelHost, isAllowedRelayWebSocketPath } from './tunnel-host.js';
|
|
5
5
|
import { decodeTunnelFrame, encodeTunnelFrame, encodeJsonPayload, TunnelFrameType } from './tunnel-codec.js';
|
|
6
6
|
|
|
7
7
|
const startLoopback = () =>
|
|
@@ -145,3 +145,11 @@ describe('tunnel-host HTTP body forwarding', () => {
|
|
|
145
145
|
await loopback.stop();
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe('relay host WebSocket allowlist', () => {
|
|
150
|
+
test('allows only the exact dev-server tunnel path', () => {
|
|
151
|
+
expect(isAllowedRelayWebSocketPath('/api/dev-tunnel')).toBe(true);
|
|
152
|
+
expect(isAllowedRelayWebSocketPath('/api/dev-tunnel/')).toBe(false);
|
|
153
|
+
expect(isAllowedRelayWebSocketPath('/api/database/ws')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Regression for https://
|
|
4
|
+
* Regression for https://roweb.cn/roweb/openchamber/issues/2710
|
|
5
5
|
* "Scheduled daily task executes twice at the configured time"
|
|
6
6
|
*
|
|
7
|
-
* Root cause: each
|
|
7
|
+
* Root cause: each AiWorker server process keeps its own timers. Two
|
|
8
8
|
* instances that share the same on-disk project config (CLI serve on port 3000
|
|
9
9
|
* + Electron on 57123, or a startup login service + desktop) each arm a timer
|
|
10
10
|
* for the same occurrence and both dispatch.
|
|
@@ -15,7 +15,7 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
|
|
|
15
15
|
sanitizeProjects,
|
|
16
16
|
projectConfigRuntime,
|
|
17
17
|
scheduledTasksRuntime,
|
|
18
|
-
|
|
18
|
+
getAiWorkerEventClients,
|
|
19
19
|
writeSseEvent,
|
|
20
20
|
scheduledTaskService = createScheduledTaskService(dependencies),
|
|
21
21
|
} = dependencies;
|
|
@@ -149,14 +149,14 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
|
|
|
149
149
|
// not of this server: a desktop shell and a browser tab can be connected to
|
|
150
150
|
// the same server at once. Recording it on the connection keeps the answer
|
|
151
151
|
// current without any enable/disable setting to go stale.
|
|
152
|
-
res.
|
|
152
|
+
res.aiworkerBrowserCapable = req.query?.browser === '1';
|
|
153
153
|
|
|
154
|
-
const clients =
|
|
154
|
+
const clients = getAiWorkerEventClients();
|
|
155
155
|
clients.add(res);
|
|
156
156
|
|
|
157
157
|
try {
|
|
158
158
|
writeSseEvent(res, {
|
|
159
|
-
type: '
|
|
159
|
+
type: 'aiworker:event-stream-ready',
|
|
160
160
|
properties: {
|
|
161
161
|
connectedAt: Date.now(),
|
|
162
162
|
},
|
|
@@ -167,7 +167,7 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
|
|
|
167
167
|
const heartbeat = setInterval(() => {
|
|
168
168
|
try {
|
|
169
169
|
writeSseEvent(res, {
|
|
170
|
-
type: '
|
|
170
|
+
type: 'aiworker:heartbeat',
|
|
171
171
|
properties: {
|
|
172
172
|
timestamp: Date.now(),
|
|
173
173
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { AiWorkerControlError } from '../openchamber-control/error.js';
|
|
4
4
|
import { setLoopFileEnabled } from './loops.js';
|
|
5
5
|
|
|
6
6
|
const asNonEmptyString = (value) => {
|
|
@@ -24,10 +24,10 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
24
24
|
|
|
25
25
|
const findProjectByID = async (projectID) => {
|
|
26
26
|
const normalized = asNonEmptyString(projectID);
|
|
27
|
-
if (!normalized) throw new
|
|
27
|
+
if (!normalized) throw new AiWorkerControlError('projectId is required', 400);
|
|
28
28
|
const projects = await listProjects();
|
|
29
29
|
const project = projects.find((entry) => entry.id === normalized) || null;
|
|
30
|
-
if (!project) throw new
|
|
30
|
+
if (!project) throw new AiWorkerControlError('Project not found', 404);
|
|
31
31
|
return project;
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -35,17 +35,17 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
35
35
|
const requestedProjectID = asNonEmptyString(projectId);
|
|
36
36
|
const requestedDirectory = asNonEmptyString(directory);
|
|
37
37
|
if (requestedProjectID && requestedDirectory) {
|
|
38
|
-
throw new
|
|
38
|
+
throw new AiWorkerControlError('Provide only one of projectId or directory', 400);
|
|
39
39
|
}
|
|
40
40
|
if (requestedProjectID) {
|
|
41
41
|
await findProjectByID(requestedProjectID);
|
|
42
42
|
return requestedProjectID;
|
|
43
43
|
}
|
|
44
|
-
if (!requestedDirectory) throw new
|
|
44
|
+
if (!requestedDirectory) throw new AiWorkerControlError('projectId or directory is required', 400);
|
|
45
45
|
const resolvedDirectory = path.resolve(requestedDirectory);
|
|
46
46
|
const projects = await listProjects();
|
|
47
47
|
const project = projects.find((entry) => path.resolve(entry.path) === resolvedDirectory);
|
|
48
|
-
if (!project) throw new
|
|
48
|
+
if (!project) throw new AiWorkerControlError(`Project not found for directory: ${resolvedDirectory}`, 404);
|
|
49
49
|
return project.id;
|
|
50
50
|
};
|
|
51
51
|
|
|
@@ -57,28 +57,28 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
57
57
|
const findLoopTask = async (projectID, taskID) => {
|
|
58
58
|
await findProjectByID(projectID);
|
|
59
59
|
const normalizedTaskID = asNonEmptyString(taskID);
|
|
60
|
-
if (!normalizedTaskID) throw new
|
|
60
|
+
if (!normalizedTaskID) throw new AiWorkerControlError('taskId is required', 400);
|
|
61
61
|
const tasks = await scheduledTasksRuntime.syncProject(projectID);
|
|
62
62
|
const task = tasks.find((entry) => entry?.id === normalizedTaskID) || null;
|
|
63
|
-
if (!task) throw new
|
|
64
|
-
if (!task.loopFile) throw new
|
|
65
|
-
if (!fs.existsSync(task.loopFile)) throw new
|
|
63
|
+
if (!task) throw new AiWorkerControlError('Task not found', 404);
|
|
64
|
+
if (!task.loopFile) throw new AiWorkerControlError('Task is not managed by a loop file', 400);
|
|
65
|
+
if (!fs.existsSync(task.loopFile)) throw new AiWorkerControlError('Loop file not found', 404);
|
|
66
66
|
return task;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
const setLoopEnabled = async (projectID, taskID, enabled) => {
|
|
70
70
|
if (typeof enabled !== 'boolean') {
|
|
71
|
-
throw new
|
|
71
|
+
throw new AiWorkerControlError('enabled must be a boolean', 400);
|
|
72
72
|
}
|
|
73
73
|
const task = await findLoopTask(projectID, taskID);
|
|
74
74
|
try {
|
|
75
75
|
if (!setLoopFileEnabled(task.loopFile, enabled)) {
|
|
76
|
-
throw new
|
|
76
|
+
throw new AiWorkerControlError('Loop file must be valid before changing its enabled state', 400);
|
|
77
77
|
}
|
|
78
78
|
} catch (error) {
|
|
79
|
-
if (error instanceof
|
|
79
|
+
if (error instanceof AiWorkerControlError) throw error;
|
|
80
80
|
const message = error instanceof Error ? error.message : 'Failed to update loop file';
|
|
81
|
-
throw new
|
|
81
|
+
throw new AiWorkerControlError(message, 500);
|
|
82
82
|
}
|
|
83
83
|
const tasks = await scheduledTasksRuntime.syncProject(projectID);
|
|
84
84
|
return tasks.find((entry) => entry.id === taskID) || null;
|
|
@@ -90,7 +90,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
90
90
|
fs.unlinkSync(task.loopFile);
|
|
91
91
|
} catch (error) {
|
|
92
92
|
const message = error instanceof Error ? error.message : 'Failed to delete loop file';
|
|
93
|
-
throw new
|
|
93
|
+
throw new AiWorkerControlError(message, 500);
|
|
94
94
|
}
|
|
95
95
|
return scheduledTasksRuntime.syncProject(projectID);
|
|
96
96
|
};
|
|
@@ -98,7 +98,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
98
98
|
const upsert = async (projectID, taskInput) => {
|
|
99
99
|
await findProjectByID(projectID);
|
|
100
100
|
if (!taskInput || typeof taskInput !== 'object') {
|
|
101
|
-
throw new
|
|
101
|
+
throw new AiWorkerControlError('task payload is required', 400);
|
|
102
102
|
}
|
|
103
103
|
let upserted;
|
|
104
104
|
try {
|
|
@@ -106,7 +106,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
106
106
|
} catch (error) {
|
|
107
107
|
const message = error instanceof Error ? error.message : 'Failed to save scheduled task';
|
|
108
108
|
const invalid = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid');
|
|
109
|
-
throw new
|
|
109
|
+
throw new AiWorkerControlError(message, invalid ? 400 : 500);
|
|
110
110
|
}
|
|
111
111
|
await scheduledTasksRuntime.syncProject(projectID);
|
|
112
112
|
const tasks = await projectConfigRuntime.listScheduledTasks(projectID);
|
|
@@ -120,7 +120,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
120
120
|
const remove = async (projectID, taskID) => {
|
|
121
121
|
await findProjectByID(projectID);
|
|
122
122
|
const normalizedTaskID = asNonEmptyString(taskID);
|
|
123
|
-
if (!normalizedTaskID) throw new
|
|
123
|
+
if (!normalizedTaskID) throw new AiWorkerControlError('taskId is required', 400);
|
|
124
124
|
const current = await projectConfigRuntime.listScheduledTasks(projectID);
|
|
125
125
|
const existing = current.find((task) => task.id === normalizedTaskID) || null;
|
|
126
126
|
if (existing?.loopFile && fs.existsSync(existing.loopFile)) {
|
|
@@ -129,13 +129,13 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
129
129
|
// file exists. The file itself is the removal surface. Once the file is
|
|
130
130
|
// gone (the task is an orphan that the next sync would remove anyway),
|
|
131
131
|
// deleting the row is safe and allowed.
|
|
132
|
-
throw new
|
|
132
|
+
throw new AiWorkerControlError(
|
|
133
133
|
'Loop task is managed by its .agents/loops markdown file; delete the file to remove the task',
|
|
134
134
|
400,
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
const result = await projectConfigRuntime.deleteScheduledTask(projectID, normalizedTaskID);
|
|
138
|
-
if (!result.deleted) throw new
|
|
138
|
+
if (!result.deleted) throw new AiWorkerControlError('Task not found', 404);
|
|
139
139
|
await scheduledTasksRuntime.syncProject(projectID);
|
|
140
140
|
return projectConfigRuntime.listScheduledTasks(projectID);
|
|
141
141
|
};
|
|
@@ -143,14 +143,14 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
143
143
|
const run = async (projectID, taskID) => {
|
|
144
144
|
await findProjectByID(projectID);
|
|
145
145
|
const normalizedTaskID = asNonEmptyString(taskID);
|
|
146
|
-
if (!normalizedTaskID) throw new
|
|
146
|
+
if (!normalizedTaskID) throw new AiWorkerControlError('taskId is required', 400);
|
|
147
147
|
const result = await scheduledTasksRuntime.runNow(projectID, normalizedTaskID);
|
|
148
148
|
if (result.running || result.queued) {
|
|
149
|
-
throw new
|
|
149
|
+
throw new AiWorkerControlError(result.error || 'Task already running', 409);
|
|
150
150
|
}
|
|
151
|
-
if (result.skipped) throw new
|
|
151
|
+
if (result.skipped) throw new AiWorkerControlError('Task not found or disabled', 404);
|
|
152
152
|
if (!result.ok) {
|
|
153
|
-
throw new
|
|
153
|
+
throw new AiWorkerControlError(result.error || 'Task run failed', 500, { task: result.task });
|
|
154
154
|
}
|
|
155
155
|
return {
|
|
156
156
|
task: result.task,
|
|
@@ -164,7 +164,7 @@ export const createScheduledTaskService = (dependencies) => {
|
|
|
164
164
|
const setEnabled = async (projectID, taskID, enabled) => {
|
|
165
165
|
const tasks = await list(projectID);
|
|
166
166
|
const task = tasks.find((entry) => entry?.id === taskID);
|
|
167
|
-
if (!task) throw new
|
|
167
|
+
if (!task) throw new AiWorkerControlError('Task not found', 404);
|
|
168
168
|
const result = await upsert(projectID, { ...task, enabled });
|
|
169
169
|
return result.task;
|
|
170
170
|
};
|
|
@@ -166,7 +166,7 @@ describe('scheduled-task loop-file routes', () => {
|
|
|
166
166
|
sanitizeProjects: vi.fn(),
|
|
167
167
|
projectConfigRuntime: {},
|
|
168
168
|
scheduledTasksRuntime: {},
|
|
169
|
-
|
|
169
|
+
getAiWorkerEventClients: () => new Set(),
|
|
170
170
|
writeSseEvent: vi.fn(),
|
|
171
171
|
});
|
|
172
172
|
return handlers;
|
|
@@ -32,9 +32,9 @@ export const isLoopbackBindHost = (host) => {
|
|
|
32
32
|
export const isNetworkExposedBindHost = (host) => !isLoopbackBindHost(host);
|
|
33
33
|
|
|
34
34
|
export const isUnsafeUnauthenticatedLanAllowed = (env = process.env) =>
|
|
35
|
-
env?.
|
|
35
|
+
env?.AIWORKER_ALLOW_UNAUTHENTICATED_LAN === 'true';
|
|
36
36
|
|
|
37
37
|
export const getUnauthenticatedLanErrorMessage = (host) =>
|
|
38
|
-
`
|
|
39
|
-
+ 'Set --ui-password or
|
|
40
|
-
+ 'or set
|
|
38
|
+
`AiWorker refuses to bind to ${host || 'a network-exposed host'} without UI authentication. `
|
|
39
|
+
+ 'Set --ui-password or AIWORKER_UI_PASSWORD before exposing it over LAN, '
|
|
40
|
+
+ 'or set AIWORKER_ALLOW_UNAUTHENTICATED_LAN=true to accept the risk.';
|
|
@@ -103,7 +103,7 @@ export const createRequestSecurityRuntime = (deps) => {
|
|
|
103
103
|
return { origins, hosts };
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const isRequestOriginAllowed =
|
|
106
|
+
const isRequestOriginAllowed = (req) => {
|
|
107
107
|
const originHeader = typeof req.headers.origin === 'string' ? req.headers.origin.trim() : '';
|
|
108
108
|
if (!originHeader) {
|
|
109
109
|
return false;
|
|
@@ -120,15 +120,21 @@ export const createRequestSecurityRuntime = (deps) => {
|
|
|
120
120
|
return false;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const
|
|
124
|
-
|
|
123
|
+
const forwardedHostHeader = req.headers['x-forwarded-host'];
|
|
124
|
+
const forwardedHost = (Array.isArray(forwardedHostHeader) ? forwardedHostHeader[0] : forwardedHostHeader || '')
|
|
125
|
+
.split(',')[0].trim().toLowerCase();
|
|
126
|
+
const hostHeader = req.headers.host;
|
|
127
|
+
const host = forwardedHost || (Array.isArray(hostHeader) ? hostHeader[0] : hostHeader || '').trim().toLowerCase();
|
|
128
|
+
if (host && host === origin.host.toLowerCase()) return true;
|
|
125
129
|
|
|
126
|
-
// TLS commonly ends at a cloud edge before an HTTP hop to
|
|
130
|
+
// TLS commonly ends at a cloud edge before an HTTP hop to AiWorker.
|
|
127
131
|
// In that setup the browser's Origin is https while a generic reverse
|
|
128
132
|
// proxy reports the upstream request as http. The external host remains
|
|
129
133
|
// authoritative, so compare it directly instead of requiring the proxy to
|
|
130
134
|
// preserve the browser-facing protocol.
|
|
131
|
-
return
|
|
135
|
+
return getRequestOriginCandidates(req).then((candidates) => (
|
|
136
|
+
candidates.origins.has(origin.origin) || candidates.hosts.has(origin.host.toLowerCase())
|
|
137
|
+
));
|
|
132
138
|
};
|
|
133
139
|
|
|
134
140
|
return {
|
|
@@ -9,55 +9,55 @@ describe('request security runtime', () => {
|
|
|
9
9
|
test('allows packaged client origins for remote client transports', async () => {
|
|
10
10
|
const runtime = createRuntime();
|
|
11
11
|
|
|
12
|
-
await
|
|
12
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
13
13
|
headers: {
|
|
14
14
|
origin: 'openchamber-ui://app',
|
|
15
15
|
host: '192.168.1.130:1202',
|
|
16
16
|
},
|
|
17
17
|
socket: {},
|
|
18
|
-
})).
|
|
18
|
+
})).toBe(true);
|
|
19
19
|
|
|
20
|
-
await
|
|
20
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
21
21
|
headers: {
|
|
22
22
|
origin: 'capacitor://localhost',
|
|
23
23
|
host: '192.168.1.130:1202',
|
|
24
24
|
},
|
|
25
25
|
socket: {},
|
|
26
|
-
})).
|
|
26
|
+
})).toBe(true);
|
|
27
27
|
|
|
28
28
|
// Android Capacitor WebView (androidScheme 'https') reports this origin.
|
|
29
|
-
await
|
|
29
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
30
30
|
headers: {
|
|
31
31
|
origin: 'https://localhost',
|
|
32
32
|
host: '192.168.1.130:1202',
|
|
33
33
|
},
|
|
34
34
|
socket: {},
|
|
35
|
-
})).
|
|
35
|
+
})).toBe(true);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
test('rejects unknown origins', async () => {
|
|
39
39
|
const runtime = createRuntime();
|
|
40
40
|
|
|
41
|
-
await
|
|
41
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
42
42
|
headers: {
|
|
43
43
|
origin: 'https://evil.example.com',
|
|
44
44
|
host: '192.168.1.130:1202',
|
|
45
45
|
},
|
|
46
46
|
socket: {},
|
|
47
|
-
})).
|
|
47
|
+
})).toBe(false);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('allows the external host when TLS terminates before an HTTP proxy hop', async () => {
|
|
51
51
|
const runtime = createRuntime();
|
|
52
52
|
|
|
53
|
-
await
|
|
53
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
54
54
|
headers: {
|
|
55
55
|
origin: 'https://devchamber.example.com',
|
|
56
56
|
host: 'devchamber.example.com',
|
|
57
57
|
'x-forwarded-proto': 'http',
|
|
58
58
|
},
|
|
59
59
|
socket: {},
|
|
60
|
-
})).
|
|
60
|
+
})).toBe(true);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
test('uses the forwarded external host without trusting a different origin', async () => {
|
|
@@ -71,13 +71,13 @@ describe('request security runtime', () => {
|
|
|
71
71
|
socket: {},
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
await
|
|
74
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
75
75
|
...request,
|
|
76
76
|
headers: { ...request.headers, origin: 'https://devchamber.example.com' },
|
|
77
|
-
})).
|
|
78
|
-
await
|
|
77
|
+
})).toBe(true);
|
|
78
|
+
expect(await runtime.isRequestOriginAllowed({
|
|
79
79
|
...request,
|
|
80
80
|
headers: { ...request.headers, origin: 'https://evil.example.com' },
|
|
81
|
-
})).
|
|
81
|
+
})).toBe(false);
|
|
82
82
|
});
|
|
83
83
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Session assist: after a session goes idle and stays quiet, generate a short
|
|
2
2
|
// recap of the agent's last reply plus one suggested user follow-up with the
|
|
3
3
|
// small model, and store both on the session's metadata
|
|
4
|
-
// (metadata.
|
|
4
|
+
// (metadata.aiworker.assist). Clients decide visibility from
|
|
5
5
|
// assist.forMessageID — a new message makes the payload stale everywhere
|
|
6
6
|
// without any extra writes.
|
|
7
7
|
//
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
|
+
import { readMergedSettingsSync } from '../opencode/settings-files.js';
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
process.env.
|
|
17
|
-
? path.resolve(process.env.
|
|
18
|
-
: path.join(os.homedir(), '.config', '
|
|
16
|
+
const AIWORKER_SETTINGS_FILE = path.join(
|
|
17
|
+
process.env.AIWORKER_DATA_DIR
|
|
18
|
+
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
19
|
+
: path.join(os.homedir(), '.config', 'aiworker'),
|
|
19
20
|
'settings.json',
|
|
20
21
|
);
|
|
21
22
|
|
|
@@ -23,16 +24,11 @@ const OPENCHAMBER_SETTINGS_FILE = path.join(
|
|
|
23
24
|
// off, no small-model calls and no metadata writes happen at all. Existing
|
|
24
25
|
// payloads stay untouched — clients keep showing them and dismissal still works.
|
|
25
26
|
const getSessionAssistTargets = () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
suggestion: settings?.sessionSuggestionEnabled !== false,
|
|
32
|
-
};
|
|
33
|
-
} catch {
|
|
34
|
-
return { recap: true, suggestion: true };
|
|
35
|
-
}
|
|
27
|
+
const settings = readMergedSettingsSync({ fs, path, settingsFilePath: AIWORKER_SETTINGS_FILE });
|
|
28
|
+
return {
|
|
29
|
+
recap: settings.sessionRecapEnabled !== false,
|
|
30
|
+
suggestion: settings.sessionSuggestionEnabled !== false,
|
|
31
|
+
};
|
|
36
32
|
};
|
|
37
33
|
|
|
38
34
|
const IDLE_QUIET_MS = 60_000;
|
|
@@ -257,6 +253,7 @@ export const createSessionAssistRuntime = ({
|
|
|
257
253
|
prompt: `The latest exchange in the conversation:\n\n${transcript}\n\nWrite ${requestedFields} in the SAME language as this sample from the conversation: "${languageSample}"`,
|
|
258
254
|
system: buildAssistSystemPrompt(targets),
|
|
259
255
|
directory,
|
|
256
|
+
sessionID: sessionId,
|
|
260
257
|
preferredProviderID: typeof lastAssistantInfo.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
|
|
261
258
|
preferredModelID: typeof lastAssistantInfo.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
|
|
262
259
|
});
|
|
@@ -316,8 +313,8 @@ export const createSessionAssistRuntime = ({
|
|
|
316
313
|
const currentMetadata = freshSession?.metadata && typeof freshSession.metadata === 'object'
|
|
317
314
|
? freshSession.metadata
|
|
318
315
|
: (session.metadata && typeof session.metadata === 'object' ? session.metadata : {});
|
|
319
|
-
const currentNamespace = currentMetadata.
|
|
320
|
-
? currentMetadata.
|
|
316
|
+
const currentNamespace = currentMetadata.aiworker && typeof currentMetadata.aiworker === 'object'
|
|
317
|
+
? currentMetadata.aiworker
|
|
321
318
|
: {};
|
|
322
319
|
|
|
323
320
|
console.log(`[session-assist] generated for ${sessionId} via ${generated.providerID}/${generated.modelID}`);
|
|
@@ -327,7 +324,7 @@ export const createSessionAssistRuntime = ({
|
|
|
327
324
|
body: {
|
|
328
325
|
metadata: {
|
|
329
326
|
...currentMetadata,
|
|
330
|
-
|
|
327
|
+
aiworker: {
|
|
331
328
|
...currentNamespace,
|
|
332
329
|
assist: {
|
|
333
330
|
recap,
|