@vantran-se/openchamber-web 1.22.1 → 1.22.2
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 +2 -3
- package/bin/cli.js +4 -1
- package/bin/cli.test.js +92 -0
- package/bin/lib/cli-startup.js +49 -0
- package/bin/lib/commands-startup.js +41 -6
- package/dist/assets/{CodeMirrorCommentWidgets-C53Q7pbX.js → CodeMirrorCommentWidgets-CV1HNV5l.js} +1 -1
- package/dist/assets/{DiffView-Dh1ey2x9.js → DiffView-DkkSa30n.js} +3 -3
- package/dist/assets/DirtyBranchSwitchDialog-DW4WaXPc.js +1 -0
- package/dist/assets/FilesView-DDu8zDy5.js +17 -0
- package/dist/assets/GitView-1Ixo8z-M.js +6 -0
- package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-DvCLL7dT.js} +1 -1
- package/dist/assets/LinearIssuesView-Bs1rZrF2.js +1 -0
- package/dist/assets/{MarkdownImageGallery-bA0PS-tn.js → MarkdownImageGallery-DyJUc6h9.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-Bdm8vMkS.js +12 -0
- package/dist/assets/{OnboardingScreen-D3kYJIb6.js → OnboardingScreen-Dg66B6oB.js} +1 -1
- package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-CAduNjfZ.js} +1 -1
- package/dist/assets/{PierreDiffViewer-Ct2A13NO.js → PierreDiffViewer-CTIb1GYD.js} +2 -2
- package/dist/assets/PlanView-trDaszfq.js +5 -0
- package/dist/assets/SettingsView-DStMfBmy.js +28 -0
- package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DQQFnrSZ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-SjY2XZlA.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DJdzUQPE.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-Iehm4iMH.js} +1 -1
- package/dist/assets/{WalkthroughView-AX-u2pFg.js → WalkthroughView-BALp2d9O.js} +2 -2
- package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-BKZA43vN.js} +2 -2
- package/dist/assets/{contextPanelEmbeddedChat-C9EcRSDF.js → contextPanelEmbeddedChat-D2PM_pEv.js} +1 -1
- package/dist/assets/{de-DqVn5vme.js → de-DlSNLAcy.js} +6 -5
- package/dist/assets/{es-hj43Z53i.js → es-D1GbowZ3.js} +6 -5
- package/dist/assets/{fr-BmkZt2mF.js → fr-BPOLXIAY.js} +6 -5
- package/dist/assets/{index-DmhujXbT.js → index-Ce0LR32V.js} +1 -1
- package/dist/assets/index-D42fPvt4.css +1 -0
- package/dist/assets/index-DlFbCkEy.js +39 -0
- package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-DaB2u_Kv.js} +1 -1
- package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-yJZa2IAC.js} +1 -1
- package/dist/assets/{ja-DioMIEGF.js → ja-Dm2-m9io.js} +7 -6
- package/dist/assets/{ko-BqPgaQJb.js → ko-CIXlzQ5-.js} +7 -6
- package/dist/assets/main-B7qTsd_w.js +930 -0
- package/dist/assets/{main-B2a_JfKZ.js → main-CyQrD9dG.js} +2 -2
- package/dist/assets/{markdownCore-CWRCqSH2.js → markdownCore-CeEwGgEK.js} +2 -2
- package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-7cPEx4sV.js} +2 -2
- package/dist/assets/{mobile-QNcFIVCW.js → mobile-QtzkPTM8.js} +2 -2
- package/dist/assets/{patchFileDiff-BpMQZnYu.js → patchFileDiff-No55fDiD.js} +1 -1
- package/dist/assets/pl-B2zmkOpw.js +16 -0
- package/dist/assets/{pt-BR-BqH8fuCH.js → pt-BR-C-JE5H6y.js} +6 -5
- package/dist/assets/{renderElectronMiniChatApp-D77bmLqo.js → renderElectronMiniChatApp-ePTkBJNS.js} +2 -2
- package/dist/assets/renderMobileApp-D9RjB9DX.js +25 -0
- package/dist/assets/runtimeEndpointReset-DfJIWwwo.js +17 -0
- package/dist/assets/{scheduledTasksApi-BSRCtP6T.js → scheduledTasksApi-DuhdyYEa.js} +1 -1
- package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-EHEg58-n.js} +1 -1
- package/dist/assets/tr-MJmRIzEY.js +7 -0
- package/dist/assets/{uk-BkkfxbOs.js → uk-BmD7eM8k.js} +6 -5
- package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-CuJPKtI1.js} +151 -180
- package/dist/assets/useInlineCommentController-DD7_7OP_.js +2 -0
- package/dist/assets/{useRootScrollLock-C0N9mCwz.js → useRootScrollLock-DRGAe8PN.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-9ZzoH0o0.js} +2 -2
- package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
- package/dist/assets/{web-components-BNMwa0qm.js → web-components-PttKBrhi.js} +1 -1
- package/dist/assets/{zh-CN-C6FMdVgv.js → zh-CN-JJHl4ehT.js} +6 -5
- package/dist/assets/{zh-TW-CdcMjDxv.js → zh-TW-DPc4DEEW.js} +6 -5
- package/dist/index.html +5 -5
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +8 -4
- package/server/index.js +38 -5
- package/server/lib/agent-tool/runtime.js +2 -19
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/runtime.js +4 -4
- package/server/lib/dev-tunnel/tunnel.test.js +20 -0
- package/server/lib/fs/DOCUMENTATION.md +2 -0
- package/server/lib/fs/routes.js +32 -21
- package/server/lib/fs/routes.test.js +77 -0
- package/server/lib/git/DOCUMENTATION.md +3 -1
- package/server/lib/git/routes.js +16 -0
- package/server/lib/git/service.js +107 -17
- package/server/lib/git/service.test.js +171 -0
- 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 +158 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/openchamber-sessions/routes.js +102 -0
- package/server/lib/openchamber-sessions/routes.test.js +88 -0
- package/server/lib/opencode/DOCUMENTATION.md +13 -5
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/openchamber-routes.test.js +2 -2
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/routes.js +4 -4
- package/server/lib/opencode/server-startup-runtime.js +1 -1
- package/server/lib/opencode/settings-helpers.js +28 -0
- package/server/lib/opencode/settings-helpers.test.js +177 -0
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/opencode/settings-runtime.test.js +29 -0
- package/server/lib/opencode/shared.js +4 -1
- 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/package-manager.js +4 -29
- package/server/lib/package-manager.test.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +2 -1
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +1 -1
- package/server/lib/quota/credentials/store.test.js +4 -4
- 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/index.js +7 -0
- package/server/lib/quota/providers/opencode-go.js +1 -0
- package/server/lib/quota/providers/opencode-go.test.js +5 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/tunnel-host.js +3 -1
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/security/request-security.js +21 -6
- package/server/lib/security/request-security.test.js +42 -8
- package/server/lib/session-assist/runtime.js +1 -0
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +3 -1
- package/server/lib/session-goal/runtime.js +77 -11
- package/server/lib/session-goal/runtime.test.js +426 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +4 -0
- package/server/lib/small-model/call.js +8 -2
- package/server/lib/small-model/call.test.js +23 -0
- package/server/lib/small-model/index.js +2 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/system-prompt/runtime.js +2 -21
- package/server/lib/terminal/DOCUMENTATION.md +11 -7
- package/server/lib/terminal/runtime.js +180 -26
- package/server/lib/terminal/runtime.test.js +553 -26
- package/server/lib/terminal/shells.js +17 -0
- package/server/lib/terminal/shells.test.js +32 -1
- package/server/lib/ui-auth/ui-auth.js +4 -1
- package/server/lib/ui-auth/ui-auth.test.js +16 -0
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
- package/dist/assets/FilesView-3ZYP72ki.js +0 -17
- package/dist/assets/GitView-CMuN7z2Z.js +0 -6
- package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
- package/dist/assets/PlanView-6AUjzNQn.js +0 -5
- package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
- package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
- package/dist/assets/index-L_ncV-6H.css +0 -1
- package/dist/assets/index-qES19dlJ.js +0 -7
- package/dist/assets/main-CYgSu6dx.js +0 -928
- package/dist/assets/pl-slULBF4R.js +0 -15
- package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
- package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
- package/dist/assets/tr-CjEbw1Aw.js +0 -6
- package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
package/dist/index.html
CHANGED
|
@@ -509,16 +509,16 @@
|
|
|
509
509
|
pointer-events: none;
|
|
510
510
|
}
|
|
511
511
|
</style>
|
|
512
|
-
<script type="module" crossorigin src="/assets/main-
|
|
512
|
+
<script type="module" crossorigin src="/assets/main-CyQrD9dG.js"></script>
|
|
513
513
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
514
514
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
515
515
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
516
516
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
517
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
517
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
518
518
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
519
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
520
|
-
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-
|
|
521
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
519
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DlFbCkEy.js">
|
|
520
|
+
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-D2PM_pEv.js">
|
|
521
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.css">
|
|
522
522
|
</head>
|
|
523
523
|
<body class="h-full bg-background text-foreground">
|
|
524
524
|
<div id="root" class="h-full">
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-7cPEx4sV.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
81
81
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
85
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DlFbCkEy.js">
|
|
85
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.css">
|
|
86
86
|
</head>
|
|
87
87
|
<body class="h-full bg-background text-foreground">
|
|
88
88
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,15 +63,15 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-QtzkPTM8.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
70
70
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
71
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
71
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-CK21ihK-.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
73
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
74
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
73
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DlFbCkEy.js">
|
|
74
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D42fPvt4.css">
|
|
75
75
|
</head>
|
|
76
76
|
<body class="h-full bg-background text-foreground">
|
|
77
77
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantran-se/openchamber-web",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "bun run build:watch",
|
|
16
16
|
"dev:server": "OPENCHAMBER_RELAY_HOST=${OPENCHAMBER_RELAY_HOST:-off} bun server/index.js --port ${OPENCHAMBER_PORT:-3001}",
|
|
17
|
-
"dev:server:watch": "
|
|
17
|
+
"dev:server:watch": "node ./scripts/dev-server-watch.mjs",
|
|
18
18
|
"build": "vite build",
|
|
19
19
|
"build:watch": "vite build --watch",
|
|
20
20
|
"type-check": "tsc --noEmit",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.1.0",
|
|
27
27
|
"@octokit/rest": "^22.0.1",
|
|
28
|
-
"@opencode-ai/sdk": "1.18.
|
|
28
|
+
"@opencode-ai/sdk": "1.18.29",
|
|
29
29
|
"@simplewebauthn/server": "13.3.1",
|
|
30
30
|
"bun-pty": "^0.4.5",
|
|
31
31
|
"compression": "^1.8.1",
|
|
@@ -107,5 +107,9 @@
|
|
|
107
107
|
"public",
|
|
108
108
|
"package.json",
|
|
109
109
|
"README.md"
|
|
110
|
-
]
|
|
110
|
+
],
|
|
111
|
+
"repository": {
|
|
112
|
+
"type": "git",
|
|
113
|
+
"url": "git+https://github.com/vantran-se/openchamber.git"
|
|
114
|
+
}
|
|
111
115
|
}
|
package/server/index.js
CHANGED
|
@@ -91,6 +91,7 @@ import { createPushRuntime } from './lib/notifications/push-runtime.js';
|
|
|
91
91
|
import { createApnsRuntime } from './lib/notifications/apns-runtime.js';
|
|
92
92
|
import { createNotificationTemplateRuntime } from './lib/notifications/template-runtime.js';
|
|
93
93
|
import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/runtime.js';
|
|
94
|
+
import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
|
|
94
95
|
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
|
|
95
96
|
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
|
|
96
97
|
import { createProjectContextRuntime } from './lib/project-context/runtime.js';
|
|
@@ -110,6 +111,7 @@ import { createDevServerScanner } from './lib/dev-servers/routes.js';
|
|
|
110
111
|
import { createDevTunnelRuntime } from './lib/dev-tunnel/runtime.js';
|
|
111
112
|
import { registerBrowserControlRoutes } from './lib/browser-control/routes.js';
|
|
112
113
|
import { createSystemPromptRuntime } from './lib/system-prompt/runtime.js';
|
|
114
|
+
import { createMcpReconnectRuntime } from './lib/mcp-reconnect/runtime.js';
|
|
113
115
|
import { createOpenChamberSessionService } from './lib/openchamber-sessions/routes.js';
|
|
114
116
|
import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
|
|
115
117
|
import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
|
|
@@ -267,6 +269,11 @@ const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProje
|
|
|
267
269
|
const OPENCHAMBER_USER_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
|
|
268
270
|
const OPENCHAMBER_USER_THEMES_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'themes');
|
|
269
271
|
const OPENCHAMBER_PROJECTS_CONFIG_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'projects');
|
|
272
|
+
// OPENCHAMBER_CHATS_DIR relocates managed chat worktrees — needed when the
|
|
273
|
+
// OpenCode server runs as a separate user that cannot traverse $HOME.
|
|
274
|
+
const OPENCHAMBER_CHATS_DIR = process.env.OPENCHAMBER_CHATS_DIR && process.env.OPENCHAMBER_CHATS_DIR.trim()
|
|
275
|
+
? path.resolve(process.env.OPENCHAMBER_CHATS_DIR.trim())
|
|
276
|
+
: path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats');
|
|
270
277
|
|
|
271
278
|
const MAX_THEME_JSON_BYTES = 512 * 1024;
|
|
272
279
|
|
|
@@ -284,6 +291,7 @@ const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromD
|
|
|
284
291
|
let notificationTemplateRuntime = null;
|
|
285
292
|
let agentToolRuntime = null;
|
|
286
293
|
let systemPromptRuntime = null;
|
|
294
|
+
let mcpReconnectRuntime = null;
|
|
287
295
|
|
|
288
296
|
const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
|
|
289
297
|
const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
|
|
@@ -878,6 +886,19 @@ notificationTriggerRuntime.setGetIsSessionAutoAccepting(
|
|
|
878
886
|
(sessionId, directory) => permissionAutoAcceptRuntime.isSessionAutoAccepting(sessionId, directory),
|
|
879
887
|
);
|
|
880
888
|
|
|
889
|
+
// Queued follow-up messages are delivered by the server so a closed tab or a
|
|
890
|
+
// dropped connection no longer strands them (VS Code keeps its UI-side queue).
|
|
891
|
+
const messageQueueRuntime = createMessageQueueRuntime({
|
|
892
|
+
globalEventHub: globalMessageStreamHub,
|
|
893
|
+
buildOpenCodeUrl,
|
|
894
|
+
getOpenCodeAuthHeaders,
|
|
895
|
+
sessionKnowledgeRuntime,
|
|
896
|
+
broadcastGlobalUiEvent,
|
|
897
|
+
onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
|
|
898
|
+
dataDir: OPENCHAMBER_DATA_DIR,
|
|
899
|
+
});
|
|
900
|
+
messageQueueRuntime.start();
|
|
901
|
+
|
|
881
902
|
const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
|
|
882
903
|
waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
|
|
883
904
|
buildOpenCodeUrl,
|
|
@@ -1208,11 +1229,15 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
|
|
|
1208
1229
|
const managedEnv = includeControl || includeWeb || includeMemory
|
|
1209
1230
|
? await (agentToolRuntime?.prepareManagedOpenCodeEnv({ includeControl, includeWeb, includeMemory }) || {})
|
|
1210
1231
|
: {};
|
|
1211
|
-
if (settings?.optimizeSystemPrompt !== true) return managedEnv;
|
|
1212
1232
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1233
|
+
// Each managed plugin appends itself to the config the previous one produced.
|
|
1234
|
+
let configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
|
|
1235
|
+
if (settings?.optimizeSystemPrompt === true) {
|
|
1236
|
+
({ OPENCODE_CONFIG_CONTENT: configContent } = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent));
|
|
1237
|
+
}
|
|
1238
|
+
// Always on for managed OpenCode: it only retries servers OpenCode gave up on.
|
|
1239
|
+
const mcpReconnectEnv = await mcpReconnectRuntime.prepareManagedOpenCodeEnv(configContent);
|
|
1240
|
+
return { ...managedEnv, ...mcpReconnectEnv };
|
|
1216
1241
|
},
|
|
1217
1242
|
});
|
|
1218
1243
|
|
|
@@ -1295,7 +1320,7 @@ const resolveMemoryProjectId = createMemoryProjectResolver({
|
|
|
1295
1320
|
return sanitizeProjects(settings?.projects || []).map((project) => project.path);
|
|
1296
1321
|
},
|
|
1297
1322
|
resolvePrimaryWorktreeRoot,
|
|
1298
|
-
managedProjectRoots: [path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats')],
|
|
1323
|
+
managedProjectRoots: [...new Set([path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'chats'), OPENCHAMBER_CHATS_DIR])],
|
|
1299
1324
|
});
|
|
1300
1325
|
|
|
1301
1326
|
/**
|
|
@@ -1427,6 +1452,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
|
|
|
1427
1452
|
sessionAssistRuntime,
|
|
1428
1453
|
sessionGoalRuntime,
|
|
1429
1454
|
contextObligatoryRuntime,
|
|
1455
|
+
messageQueueRuntime,
|
|
1430
1456
|
sessionRuntime,
|
|
1431
1457
|
getHealthCheckInterval: () => healthCheckInterval,
|
|
1432
1458
|
clearHealthCheckInterval: (value) => clearInterval(value),
|
|
@@ -1485,6 +1511,11 @@ async function main(options = {}) {
|
|
|
1485
1511
|
path,
|
|
1486
1512
|
dataDir: OPENCHAMBER_DATA_DIR,
|
|
1487
1513
|
});
|
|
1514
|
+
mcpReconnectRuntime = createMcpReconnectRuntime({
|
|
1515
|
+
fsPromises,
|
|
1516
|
+
path,
|
|
1517
|
+
dataDir: OPENCHAMBER_DATA_DIR,
|
|
1518
|
+
});
|
|
1488
1519
|
|
|
1489
1520
|
// Pairing transports advertised to the create-device dialog. LAN reachability is
|
|
1490
1521
|
// derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
|
|
@@ -1877,6 +1908,7 @@ async function main(options = {}) {
|
|
|
1877
1908
|
createFsSearchRuntime: createFsSearchRuntimeFactory,
|
|
1878
1909
|
openchamberDataDir: OPENCHAMBER_DATA_DIR,
|
|
1879
1910
|
openchamberUserConfigRoot: OPENCHAMBER_USER_CONFIG_ROOT,
|
|
1911
|
+
managedChatsRoot: OPENCHAMBER_CHATS_DIR,
|
|
1880
1912
|
normalizeDirectoryPath,
|
|
1881
1913
|
resolveProjectDirectory,
|
|
1882
1914
|
resolveOptionalProjectDirectory,
|
|
@@ -1914,6 +1946,7 @@ async function main(options = {}) {
|
|
|
1914
1946
|
getOpenChamberEventClients: () => uiOpenChamberEventClients,
|
|
1915
1947
|
writeSseEvent,
|
|
1916
1948
|
permissionAutoAcceptRuntime,
|
|
1949
|
+
messageQueueRuntime,
|
|
1917
1950
|
});
|
|
1918
1951
|
|
|
1919
1952
|
const startupPipelineResult = await startupPipelineRuntime.run({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parse as parseJsonc } from 'jsonc-parser';
|
|
2
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { appendManagedPlugin } from '../opencode/managed-plugin-config.js';
|
|
3
3
|
import {
|
|
4
4
|
OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS,
|
|
5
5
|
OPENCHAMBER_AGENT_TOOL_ACTIONS,
|
|
@@ -252,23 +252,6 @@ ${entries.join('')} },
|
|
|
252
252
|
`;
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
-
const mergePluginConfig = (rawConfig, pluginUrl) => {
|
|
256
|
-
const errors = [];
|
|
257
|
-
const parsed = asNonEmptyString(rawConfig) ? parseJsonc(rawConfig, errors, { allowTrailingComma: true }) : {};
|
|
258
|
-
if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
259
|
-
throw new Error('OPENCODE_CONFIG_CONTENT must contain a valid JSON object before OpenChamber can inject its managed tool');
|
|
260
|
-
}
|
|
261
|
-
if (parsed.plugin !== undefined && !Array.isArray(parsed.plugin)) {
|
|
262
|
-
throw new Error('OPENCODE_CONFIG_CONTENT plugin must be an array before OpenChamber can inject its managed tool');
|
|
263
|
-
}
|
|
264
|
-
const configured = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
265
|
-
parsed.plugin = [
|
|
266
|
-
...configured.filter((value) => value !== pluginUrl && (!Array.isArray(value) || value[0] !== pluginUrl)),
|
|
267
|
-
pluginUrl,
|
|
268
|
-
];
|
|
269
|
-
return JSON.stringify(parsed);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
255
|
export const createAgentToolRuntime = (dependencies) => {
|
|
273
256
|
const {
|
|
274
257
|
crypto,
|
|
@@ -296,7 +279,7 @@ export const createAgentToolRuntime = (dependencies) => {
|
|
|
296
279
|
activeToken = crypto.randomBytes(32).toString('base64url');
|
|
297
280
|
const pluginUrl = pathToFileURL(pluginPath).href;
|
|
298
281
|
return {
|
|
299
|
-
OPENCODE_CONFIG_CONTENT:
|
|
282
|
+
OPENCODE_CONFIG_CONTENT: appendManagedPlugin(env.OPENCODE_CONFIG_CONTENT, pluginUrl, 'managed tool'),
|
|
300
283
|
OPENCHAMBER_AGENT_TOOL_URL: `http://127.0.0.1:${port}/api/openchamber/agent-tool`,
|
|
301
284
|
OPENCHAMBER_AGENT_TOOL_TOKEN: activeToken,
|
|
302
285
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Release notes for the update dialog.
|
|
2
|
+
//
|
|
3
|
+
// The repo publishes `changelog/index.json` on `main`: one object per release,
|
|
4
|
+
// newest first, with the app notes grouped as New / Improvements / Fixes /
|
|
5
|
+
// Misc. This module fetches it and renders the releases between the installed
|
|
6
|
+
// version (exclusive) and the offered one (inclusive) as the Markdown the
|
|
7
|
+
// dialog already understands: a `## [x.y.z] - YYYY-MM-DD` header per release,
|
|
8
|
+
// the release title in bold, the intro, then the groups.
|
|
9
|
+
//
|
|
10
|
+
// Any failure (network, 404, unexpected shape) yields null: the update is
|
|
11
|
+
// still offered, only without notes.
|
|
12
|
+
|
|
13
|
+
export const CHANGELOG_INDEX_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/changelog/index.json';
|
|
14
|
+
|
|
15
|
+
const GROUPS = [
|
|
16
|
+
['new', 'New'],
|
|
17
|
+
['improvements', 'Improvements'],
|
|
18
|
+
['fixes', 'Fixes'],
|
|
19
|
+
['misc', 'Misc'],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
23
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
24
|
+
|
|
25
|
+
// The file crosses a network boundary with no schema library on this side, so
|
|
26
|
+
// each field is coerced into its domain shape here; a release without a valid
|
|
27
|
+
// version and date is dropped.
|
|
28
|
+
const text = (value) => (value === null || value === undefined ? null : String(value).trim() || null);
|
|
29
|
+
const bulletList = (value) => (Array.isArray(value) ? value.map((item) => String(item)) : []);
|
|
30
|
+
|
|
31
|
+
const parseRelease = (entry) => {
|
|
32
|
+
if (entry === null || entry === undefined) return null;
|
|
33
|
+
const version = text(entry.version);
|
|
34
|
+
const date = text(entry.date);
|
|
35
|
+
if (!version || !VERSION_PATTERN.test(version) || !date || !DATE_PATTERN.test(date)) return null;
|
|
36
|
+
const app = entry.app ?? {};
|
|
37
|
+
return {
|
|
38
|
+
version,
|
|
39
|
+
date,
|
|
40
|
+
title: text(entry.title),
|
|
41
|
+
intro: text(entry.intro),
|
|
42
|
+
groups: GROUPS.map(([key, heading]) => ({ heading, bullets: bulletList(app[key]) })),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renderRelease = (release) => {
|
|
47
|
+
const lines = [`## [${release.version}] - ${release.date}`, ''];
|
|
48
|
+
if (release.title) lines.push(`**${release.title}**`, '');
|
|
49
|
+
if (release.intro) lines.push(release.intro, '');
|
|
50
|
+
for (const { heading, bullets } of release.groups) {
|
|
51
|
+
if (bullets.length === 0) continue;
|
|
52
|
+
lines.push(`### ${heading}`, '', ...bullets.map((bullet) => `- ${bullet}`), '');
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n').trim();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Markdown for the releases in (fromVersion, toVersion], newest first, or
|
|
59
|
+
* null when the index holds none of them.
|
|
60
|
+
*/
|
|
61
|
+
export function renderUpdateNotes(index, fromVersion, toVersion, compareVersions) {
|
|
62
|
+
if (!Array.isArray(index)) return null;
|
|
63
|
+
const relevant = index
|
|
64
|
+
.map(parseRelease)
|
|
65
|
+
.filter((release) => release !== null)
|
|
66
|
+
.filter((release) => compareVersions(release.version, fromVersion) > 0 && compareVersions(release.version, toVersion) <= 0)
|
|
67
|
+
.sort((a, b) => compareVersions(b.version, a.version));
|
|
68
|
+
if (relevant.length === 0) return null;
|
|
69
|
+
return relevant.map(renderRelease).join('\n\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Fetch the index and render the notes for one update; null on any failure. */
|
|
73
|
+
export async function fetchUpdateNotes(fromVersion, toVersion, compareVersions, options = {}) {
|
|
74
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetchImpl(CHANGELOG_INDEX_URL, { signal: AbortSignal.timeout(options.timeoutMs ?? 10_000) });
|
|
77
|
+
if (!response.ok) return null;
|
|
78
|
+
return renderUpdateNotes(await response.json(), fromVersion, toVersion, compareVersions);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { fetchUpdateNotes, renderUpdateNotes } from './update-notes.js';
|
|
3
|
+
|
|
4
|
+
const compare = (a, b) => {
|
|
5
|
+
const pa = a.split('.').map(Number);
|
|
6
|
+
const pb = b.split('.').map(Number);
|
|
7
|
+
for (let i = 0; i < 3; i += 1) if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
8
|
+
return 0;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const index = [
|
|
12
|
+
{ version: '1.2.3', date: '2026-03-03', title: 'Comments everywhere', intro: 'A short intro.', app: { new: ['**Comments:** on code.'], improvements: [], fixes: ['Chat: no freeze.'], misc: [] }, vscode: null },
|
|
13
|
+
{ version: '1.2.2', date: '2026-03-02', title: null, intro: null, app: { new: [], improvements: ['Faster.'], fixes: [], misc: [] }, vscode: null },
|
|
14
|
+
{ version: '1.2.1', date: '2026-03-01', title: 'Old', intro: null, app: { new: ['Older.'], improvements: [], fixes: [], misc: [] }, vscode: null },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe('renderUpdateNotes', () => {
|
|
18
|
+
it('renders the releases after the installed version up to the offered one, newest first', () => {
|
|
19
|
+
expect(renderUpdateNotes(index, '1.2.1', '1.2.3', compare)).toBe(`## [1.2.3] - 2026-03-03
|
|
20
|
+
|
|
21
|
+
**Comments everywhere**
|
|
22
|
+
|
|
23
|
+
A short intro.
|
|
24
|
+
|
|
25
|
+
### New
|
|
26
|
+
|
|
27
|
+
- **Comments:** on code.
|
|
28
|
+
|
|
29
|
+
### Fixes
|
|
30
|
+
|
|
31
|
+
- Chat: no freeze.
|
|
32
|
+
|
|
33
|
+
## [1.2.2] - 2026-03-02
|
|
34
|
+
|
|
35
|
+
### Improvements
|
|
36
|
+
|
|
37
|
+
- Faster.`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns null when nothing lies in the range or the payload is not an index', () => {
|
|
41
|
+
expect(renderUpdateNotes(index, '1.2.3', '1.2.3', compare)).toBe(null);
|
|
42
|
+
expect(renderUpdateNotes({ entries: [] }, '1.0.0', '9.9.9', compare)).toBe(null);
|
|
43
|
+
expect(renderUpdateNotes([{ version: 'nope' }, { version: '1.2.2', date: '2026' }], '1.0.0', '9.9.9', compare)).toBe(null);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('fetchUpdateNotes', () => {
|
|
48
|
+
it('turns a failed request or a thrown fetch into null', async () => {
|
|
49
|
+
const notFound = async () => ({ ok: false });
|
|
50
|
+
expect(await fetchUpdateNotes('1.0.0', '2.0.0', compare, { fetch: notFound })).toBe(null);
|
|
51
|
+
const throwing = async () => { throw new Error('offline'); };
|
|
52
|
+
expect(await fetchUpdateNotes('1.0.0', '2.0.0', compare, { fetch: throwing })).toBe(null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders a successful response', async () => {
|
|
56
|
+
const ok = async () => ({ ok: true, json: async () => index });
|
|
57
|
+
expect(await fetchUpdateNotes('1.2.2', '1.2.3', compare, { fetch: ok })).toMatch(/^## \[1\.2\.3\] - 2026-03-03\n\n\*\*Comments everywhere\*\*/);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -20,7 +20,13 @@ and what made it fragile per framework.
|
|
|
20
20
|
- `client.js` is the local end: it binds a loopback listener on the user's
|
|
21
21
|
machine and pipes each accepted connection through one WebSocket. It lives in
|
|
22
22
|
this package because it needs a WebSocket client the package already depends
|
|
23
|
-
on; the desktop shell drives it over IPC
|
|
23
|
+
on; the desktop shell drives it over IPC for directly reachable HTTP(S)
|
|
24
|
+
runtimes.
|
|
25
|
+
- Relay-only runtimes use `packages/electron/relay-dev-tunnel.mjs` for the local
|
|
26
|
+
listener. Each accepted connection gets an Electron `MessagePort`; the trusted
|
|
27
|
+
renderer carries its bytes through the active E2EE relay. This keeps relay
|
|
28
|
+
credentials and encryption in their existing renderer owner instead of
|
|
29
|
+
duplicating them in Electron main.
|
|
24
30
|
- Port discovery is not owned here. `runtime.js` is given the reachable set by
|
|
25
31
|
the same dev-server discovery the user's own list is built from.
|
|
26
32
|
- The browser panel decides when to tunnel; this module never chooses a target.
|
|
@@ -40,9 +46,12 @@ and what made it fragile per framework.
|
|
|
40
46
|
usual origin allowlist applies unchanged. That check is a CSRF defence: a
|
|
41
47
|
hostile page can make a browser open a WebSocket carrying ambient cookies,
|
|
42
48
|
and the origin is what exposes it.
|
|
43
|
-
- With no `Origin` the request must carry client-token auth
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
- With no `Origin` the request must carry client-token auth or a short-lived
|
|
50
|
+
URL token. The bearer case is the desktop main process. The URL-token case
|
|
51
|
+
is the trusted renderer carrying the socket through the E2EE relay.
|
|
52
|
+
- Through the E2EE relay, the trusted renderer mints a short-lived URL token
|
|
53
|
+
and includes it in the virtual WebSocket URL. The relay host and URL-token
|
|
54
|
+
allowlists accept exactly `/api/dev-tunnel`, not subpaths.
|
|
46
55
|
- Concurrency is capped per host, not per page, because one page load opens
|
|
47
56
|
many sockets.
|
|
48
57
|
- A connection that cannot be established fails the socket rather than holding
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
*
|
|
21
21
|
* - With an `Origin` header, the request came from a browser context and the
|
|
22
22
|
* usual origin check applies unchanged.
|
|
23
|
-
* - With no `Origin`, the request must carry client-token auth
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - With no `Origin`, the request must carry client-token auth or a short-lived
|
|
24
|
+
* URL token. The URL-token case is used only by the trusted renderer through
|
|
25
|
+
* the E2EE relay; the UI-auth allowlist limits it to this exact path.
|
|
26
26
|
*/
|
|
27
27
|
import net from 'node:net';
|
|
28
28
|
import { WebSocketServer } from 'ws';
|
|
@@ -133,7 +133,7 @@ export function createDevTunnelRuntime({
|
|
|
133
133
|
void (async () => {
|
|
134
134
|
try {
|
|
135
135
|
if (uiAuthController?.enabled) {
|
|
136
|
-
const auth = await uiAuthController.resolveAuthContext(req, null, { allowUrlToken:
|
|
136
|
+
const auth = await uiAuthController.resolveAuthContext(req, null, { allowUrlToken: true });
|
|
137
137
|
if (!auth) {
|
|
138
138
|
rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
|
|
139
139
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import net from 'node:net';
|
|
4
|
+
import WebSocket from 'ws';
|
|
4
5
|
|
|
5
6
|
import { createDevTunnelClient } from './client.js';
|
|
6
7
|
import { createDevTunnelRuntime, isDevTunnelPath } from './runtime.js';
|
|
@@ -228,6 +229,12 @@ describe('dev tunnel authentication', () => {
|
|
|
228
229
|
enabled: true,
|
|
229
230
|
resolveAuthContext: async () => ({ type: 'session' }),
|
|
230
231
|
};
|
|
232
|
+
const urlTokenAuth = {
|
|
233
|
+
enabled: true,
|
|
234
|
+
resolveAuthContext: async (req, _res, options) => (
|
|
235
|
+
options?.allowUrlToken === true && req.url.includes('oc_url_token=good') ? { type: 'client', token: 'url:authenticated' } : null
|
|
236
|
+
),
|
|
237
|
+
};
|
|
231
238
|
|
|
232
239
|
test('accepts a bearer-authenticated client that sends no origin', async () => {
|
|
233
240
|
const devPort = await startDevServer((_req, res) => res.end('ok'));
|
|
@@ -243,6 +250,19 @@ describe('dev tunnel authentication', () => {
|
|
|
243
250
|
expect((await httpGet(localPort, '/')).body).toBe('ok');
|
|
244
251
|
});
|
|
245
252
|
|
|
253
|
+
test('accepts a URL-token client carried by the E2EE relay', async () => {
|
|
254
|
+
const devPort = await startDevServer((_req, res) => res.end('relay-ok'));
|
|
255
|
+
const host = await startHost({ allowedPorts: [devPort], auth: urlTokenAuth });
|
|
256
|
+
|
|
257
|
+
const body = await new Promise((resolve, reject) => {
|
|
258
|
+
const socket = new WebSocket(`ws://127.0.0.1:${host.port}/api/dev-tunnel?port=${devPort}&oc_url_token=good`);
|
|
259
|
+
socket.on('open', () => socket.send('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'));
|
|
260
|
+
socket.on('message', (data) => resolve(Buffer.from(data).toString()));
|
|
261
|
+
socket.on('error', reject);
|
|
262
|
+
});
|
|
263
|
+
expect(body).toContain('relay-ok');
|
|
264
|
+
});
|
|
265
|
+
|
|
246
266
|
test('rejects a client with no credentials', async () => {
|
|
247
267
|
const devPort = await startDevServer((_req, res) => res.end('ok'));
|
|
248
268
|
const host = await startHost({ allowedPorts: [devPort], auth: clientAuth });
|
|
@@ -40,6 +40,8 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
|
|
|
40
40
|
|
|
41
41
|
## Notes for contributors
|
|
42
42
|
- Keep filesystem policy (workspace root checks, error mapping, exec timeout behavior) inside this module, not in the composition root.
|
|
43
|
+
- Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
|
|
44
|
+
- `GET /api/fs/home` answers `{ home, chatsRoot }`. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
|
|
43
45
|
- Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
|
|
44
46
|
- Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
|
|
45
47
|
- If adding new `/api/fs/*` endpoints, add them in `routes.js` and extend this document.
|