@loicngr/kobo 2.1.26 → 2.1.29
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/AGENTS.md +7 -4
- package/CHANGELOG.md +15 -0
- package/CONFIGURATION.md +124 -71
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/SECURITY.md +33 -6
- package/dist/mcp-server/kobo-tasks-server.js +36 -1021
- package/dist/mcp-server/managed-mcp-dispatcher.js +960 -0
- package/dist/mcp-server/workspace-dialogue-client.js +26 -1
- package/dist/server/index.js +52 -19
- package/dist/server/middleware/host-check-middleware.js +8 -5
- package/dist/server/middleware/network-auth-middleware.js +51 -29
- package/dist/server/middleware/security-headers-middleware.js +19 -0
- package/dist/server/middleware/traffic-limits-middleware.js +74 -0
- package/dist/server/routes/auth.js +73 -0
- package/dist/server/routes/internal-mcp.js +137 -0
- package/dist/server/routes/mcp-credentials.js +30 -0
- package/dist/server/routes/mcp.js +156 -76
- package/dist/server/routes/workspaces.js +19 -3
- package/dist/server/services/agent/engines/claude-code/resolve-binary.js +1 -1
- package/dist/server/services/agent/orchestrator.js +27 -17
- package/dist/server/services/dev-server-service.js +68 -4
- package/dist/server/services/environment-check-service.js +3 -2
- package/dist/server/services/external-mcp-credentials-service.js +110 -0
- package/dist/server/services/forge/github/provider.js +15 -3
- package/dist/server/services/forge/types.js +8 -0
- package/dist/server/services/internal-mcp-capability-service.js +98 -0
- package/dist/server/services/mcp-connection-info-service.js +1 -1
- package/dist/server/services/memory-mcp-service.js +9 -7
- package/dist/server/services/memory-service.js +15 -0
- package/dist/server/services/network-access-service.js +43 -73
- package/dist/server/services/network-session-service.js +182 -0
- package/dist/server/services/settings-service.js +29 -7
- package/dist/server/services/terminal-websocket-service.js +5 -0
- package/dist/server/services/websocket-service.js +5 -0
- package/dist/server/utils/paths.js +2 -1
- package/dist/server/utils/startup-display.js +1 -2
- package/dist/shared/models.js +6 -0
- package/docker-compose.vps.yml +6 -0
- package/docs/deployment-vps.md +24 -12
- package/docs/getting-started.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +14 -14
- package/src/client/dist/pwa/assets/ActivityFeed-CiTe2VUQ.js +2 -0
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-DOr41E2Y.js → AutoLoopFinalReviewSettings-v3jXoH_Q.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-DOCgZMpn.js → ChangelogPage-C5KjAOMG.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-CRV2hxHS.js → ClosePopup-rU8Bm-mC.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-XXyzPEzW.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-MYCexRJu.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-dBpJaQSb.js → DashboardPage-DyhUu26C.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-CvsmC9d5.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-AqLx0MzB.js → DocumentsPanel-ItndGxeB.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BLWaFgh0.js → DrawerToggleButton-DfzS_CD8.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-C1ybi9bZ.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-CRaLJzjc.css → GitPanel-yHvU38dp.css} +1 -1
- package/src/client/dist/pwa/assets/{HealthPage-DeryVLyz.js → HealthPage-C1u35ANL.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BEAGPqei.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-B34DENtz.js → MemoryPanel-CZzI7_nY.js} +1 -1
- package/src/client/dist/pwa/assets/MemoryProposalList-DqNTycY1.js +1 -0
- package/src/client/dist/pwa/assets/{QBadge-CW3Lw-kL.js → QBadge-DiwbRpte.js} +1 -1
- package/src/client/dist/pwa/assets/{QBanner-BT2ZemYu.js → QBanner-Cw89HSjk.js} +1 -1
- package/src/client/dist/pwa/assets/QBtn-kfhHFy7Q.js +1 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-BPcK_rVs.js → QBtnDropdown-D7ml7zHo.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnGroup-CnM4vSJA.js → QBtnGroup-Bioxjp9q.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-DccOOtbh.js → QBtnToggle-Bod-x9im.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-Be16M4Ig.js → QChip-DvhtPv_L.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-CpwQ8L_F.js → QExpansionItem-BVb067oc.js} +1 -1
- package/src/client/dist/pwa/assets/{QItemSection-MdJ1_ZK7.js → QItemSection-ahqRK4JF.js} +1 -1
- package/src/client/dist/pwa/assets/{QList-Cx1UfaUm.js → QList-CAUl0v66.js} +1 -1
- package/src/client/dist/pwa/assets/{QMarkupTable-UhP7fVUp.js → QMarkupTable-B4LLAly-.js} +1 -1
- package/src/client/dist/pwa/assets/{QPage-CqkGHokx.js → QPage-DIy_mRoT.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-BkNSVl7s.js → QScrollArea-CUDWkEHh.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DOnv_ry0.js +1 -0
- package/src/client/dist/pwa/assets/{QSpinnerDots-xdWyaE1i.js → QSpinnerDots-Bea6hAuJ.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-Dp8fitHV.js → QTooltip-B5QJIU3p.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-Ck8LKDCa.js → SchedulePanel-IJOcrHgk.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-BAUZg1zq.js → SearchPage-BpTRWtne.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-Dcq1Jeoo.css → SettingsPage-B4rmd2WX.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-CFaRN9Pk.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-CH8OHmtn.js → SplitWorkspacePage-BZ8wN4ii.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-Dz9EE-Ph.css → TerminalPanel-BcbwuFVR.css} +1 -1
- package/src/client/dist/pwa/assets/TerminalPanel-pY-RGBb6.js +16 -0
- package/src/client/dist/pwa/assets/{ToolCallItem-CLtGTV6w.js → ToolCallItem-C9ksVseJ.js} +4 -4
- package/src/client/dist/pwa/assets/{TouchPan-9SH2dx6m.js → TouchPan-DRynDIve.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Dw4sGf9B.js → TourReplayButton-D4h1QR6C.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-Bkj2DhOn.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-BTJnap9a.js +4 -0
- package/src/client/dist/pwa/assets/{browser-CC4W8r76.js → browser-BcOi2ES2.js} +2 -2
- package/src/client/dist/pwa/assets/build-path-tree-mDco3pi7.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-T5HKzrWh.js +1 -0
- package/src/client/dist/pwa/assets/de-suqWUSXa.js +17 -0
- package/src/client/dist/pwa/assets/detached-fullscreen-CHqMxB-8.js +1 -0
- package/src/client/dist/pwa/assets/dom-CRPVbmiy.js +2 -0
- package/src/client/dist/pwa/assets/ecl-94qehhXe.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-BwfxvQ88.js → editor.api-CRrihXg2.js} +9 -9
- package/src/client/dist/pwa/assets/editor.worker-xrHNhPSq.js +33 -0
- package/src/client/dist/pwa/assets/{efforts-B3I7w5bs.js → efforts-C-p9DzkL.js} +2 -2
- package/src/client/dist/pwa/assets/es-Dcptbz5U.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-gIwrPbeS.js +1 -0
- package/src/client/dist/pwa/assets/{format-CJIzi3Vf.js → format-DeBnwmgW.js} +48 -48
- package/src/client/dist/pwa/assets/{formatters-DQ8cP7nO.js → formatters-CxZZ_WTb.js} +1 -1
- package/src/client/dist/pwa/assets/fr-Db70bqVq.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DbpVO-cL.js → freemarker2-h7OLlqTn.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BHve0PpA.js → handlebars-BCWmi1-q.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BhUkdFPz.js → html-MUMpfUBd.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-DLprfNLs.js +1 -0
- package/src/client/dist/pwa/assets/index-C0MApoXN.css +1 -0
- package/src/client/dist/pwa/assets/index-DLoUeovT.js +20 -0
- package/src/client/dist/pwa/assets/it-Cloo58Nn.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-BLAorPa3.js → javascript-CHDwijEy.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CJGF-h-f.js → liquid-dFrnLevt.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-Cm0oNz_l.js → mdx-BHS7JVD4.js} +1 -1
- package/src/client/dist/pwa/assets/monaco-contributions-DCWZe7nX.js +53 -0
- package/src/client/dist/pwa/assets/{monaco.contribution-hr9s26Ud.js → monaco.contribution-Dx0gcAn4.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-DDMAzwYH.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-EcbxuH_8.js → notify-theme-JuVT96rq.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-DLWiBfdb.js → permissionModes-D-Nexnoo.js} +1 -1
- package/src/client/dist/pwa/assets/{private.use-form-DY1BkePL.js → private.use-form-BcspuzM9.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-vDuTaTJn.js → project-color-DihsE4yN.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BEjbzFAQ.js → python-BtjZnK9y.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BJPT7Wxx.js → razor-DBcOGz0a.js} +1 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-Dplx1ILy.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-C9ihAYBt.js +67 -0
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-BAUgNzMK.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-DPqM46Ef.js → typescript-B320c7wU.js} +1 -1
- package/src/client/dist/pwa/assets/{use-checkbox-v4aAbE6K.js → use-checkbox-DUxgXR4e.js} +1 -1
- package/src/client/dist/pwa/assets/{use-id-BrdYXce0.js → use-id-B7ieAx2m.js} +1 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CHmaewI1.js +1 -0
- package/src/client/dist/pwa/assets/{use-search-index-status-BwwrB410.js → use-search-index-status-B95GPaJP.js} +1 -1
- package/src/client/dist/pwa/assets/use-slide-transition-BJh9xGMa.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-CU5oA7cz.css +1 -0
- package/src/client/dist/pwa/assets/use-tours-C_5R1Y5D.js +2 -0
- package/src/client/dist/pwa/assets/{use-worktree-restore-tzd2FESU.js → use-worktree-restore-Cj8eGE9s.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-CubzsZUU.js → workspace-templates-B9UETKER.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-C6SVzw5_.js → xml-uK8LK4OB.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-OpCqnv4g.js → yaml-B2D1l_tz.js} +1 -1
- package/src/client/dist/pwa/index.html +10 -10
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +13 -19
- package/src/mcp-server/kobo-tasks-server.ts +42 -1146
- package/src/mcp-server/managed-mcp-dispatcher.ts +1071 -0
- package/src/mcp-server/workspace-dialogue-client.ts +26 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DfYrVNEh.js +0 -2
- package/src/client/dist/pwa/assets/ComparisonPanel-b5nTBjK6.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-B7VPHEqm.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-BLQz2Qyx.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Ba7ltCsM.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-B2XvjdWi.js +0 -2
- package/src/client/dist/pwa/assets/MemoryProposalList-BpN_OU8J.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-DrLpbvcr.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Cj6kSVT5.js +0 -9
- package/src/client/dist/pwa/assets/TerminalPanel-CBmZ0IzQ.js +0 -16
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-wd_sObJa.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-D4ODPOG-.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-BBuyB-my.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-BbPNRzAs.js +0 -1
- package/src/client/dist/pwa/assets/de-D9VzPDGF.js +0 -17
- package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +0 -1
- package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +0 -2
- package/src/client/dist/pwa/assets/ecl-D05T4iGw.js +0 -1
- package/src/client/dist/pwa/assets/editor.worker-BLJF_oQx.js +0 -33
- package/src/client/dist/pwa/assets/es-3R4MJuTt.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-Ci9UjnxZ.js +0 -1
- package/src/client/dist/pwa/assets/fr-DXwDVnJ4.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CGLQAADf.js +0 -1
- package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +0 -1
- package/src/client/dist/pwa/assets/index-CNcXBpzz.js +0 -20
- package/src/client/dist/pwa/assets/it-T9AjnG42.js +0 -17
- package/src/client/dist/pwa/assets/monaco-contributions-nBg6wf05.js +0 -53
- package/src/client/dist/pwa/assets/network-auth-C850jGfI.js +0 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-CafzCcmu.js +0 -69
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +0 -1
- package/src/client/dist/pwa/assets/use-slide-transition-B_Nu6u-t.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-B0KtCS9w.js +0 -2
- package/src/client/dist/pwa/assets/use-tours-CTZuBZOi.css +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import { createExternalMcpCredential, EXTERNAL_TOOL_CATALOGUE, listExternalMcpCredentials, revokeExternalMcpCredential, } from '../services/external-mcp-credentials-service.js';
|
|
4
|
+
// Mounted behind the operator authentication gate; MCP credentials cannot reach it.
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
app.use('*', bodyLimit({ maxSize: 32 * 1024 }));
|
|
7
|
+
app.use('*', async (c, next) => {
|
|
8
|
+
c.header('Cache-Control', 'no-store');
|
|
9
|
+
await next();
|
|
10
|
+
});
|
|
11
|
+
app.get('/', (c) => c.json({
|
|
12
|
+
credentials: listExternalMcpCredentials(),
|
|
13
|
+
tools: EXTERNAL_TOOL_CATALOGUE.map((tool) => ({
|
|
14
|
+
name: tool.name,
|
|
15
|
+
readOnly: tool.annotations?.readOnlyHint === true,
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
app.post('/', async (c) => {
|
|
19
|
+
try {
|
|
20
|
+
return c.json(createExternalMcpCredential(await c.req.json()), 201);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid credential request' }, 400);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
app.delete('/:id', (c) => {
|
|
27
|
+
revokeExternalMcpCredential(c.req.param('id'));
|
|
28
|
+
return c.json({ ok: true });
|
|
29
|
+
});
|
|
30
|
+
export default app;
|
|
@@ -4,14 +4,16 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { bodyLimit } from 'hono/body-limit';
|
|
6
6
|
import { MAX_MCP_ATTACHMENT_REQUEST_BYTES } from '../../shared/mcp-attachments.js';
|
|
7
|
-
import {
|
|
8
|
-
import { isProgressBarTool
|
|
9
|
-
import { isWorkspaceCreationTool
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { isMemoryToolName } from '../../shared/memory-tools.js';
|
|
8
|
+
import { isProgressBarTool } from '../../shared/progress-bar-tools.js';
|
|
9
|
+
import { isWorkspaceCreationTool } from '../../shared/workspace-creation-tools.js';
|
|
10
|
+
import { isWorkspaceGroupMessageTool } from '../../shared/workspace-group-message-tools.js';
|
|
11
|
+
import { isWorkspaceLifecycleTool } from '../../shared/workspace-lifecycle-tools.js';
|
|
12
|
+
import { getDb } from '../db/index.js';
|
|
13
|
+
import { authenticateExternalMcpToken, EXTERNAL_TOOL_CATALOGUE, } from '../services/external-mcp-credentials-service.js';
|
|
13
14
|
import { createMcpClientContext } from '../services/mcp-client-context.js';
|
|
14
15
|
import { executeMemoryMcpTool } from '../services/memory-mcp-service.js';
|
|
16
|
+
import { resolveWorkspaceMemoryScope } from '../services/memory-service.js';
|
|
15
17
|
import { executeProgressBarTool, progressErrorData } from '../services/progress-bar-mcp-service.js';
|
|
16
18
|
import { ProgressBarError } from '../services/progress-bar-service.js';
|
|
17
19
|
import { executeWorkspaceCreationTool, WorkspaceCreationMcpError } from '../services/workspace-creation-mcp-service.js';
|
|
@@ -19,7 +21,16 @@ import { executeWorkspaceDialogueTool } from '../services/workspace-dialogue-ser
|
|
|
19
21
|
import { executeWorkspaceGroupMessageTool, WorkspaceGroupMessageMcpError, } from '../services/workspace-group-message-mcp-service.js';
|
|
20
22
|
import { executeWorkspaceLifecycleTool, WorkspaceLifecycleMcpError, } from '../services/workspace-lifecycle-mcp-service.js';
|
|
21
23
|
import { prepareMemoryToolEnvelope } from '../utils/memory-token-budget.js';
|
|
24
|
+
function authenticateRequest(authorization = '') {
|
|
25
|
+
return authenticateExternalMcpToken(authorization.startsWith('Bearer ') ? authorization.slice(7) : '');
|
|
26
|
+
}
|
|
22
27
|
const app = new Hono();
|
|
28
|
+
app.use('*', async (c, next) => {
|
|
29
|
+
if (!authenticateRequest(c.req.header('Authorization')))
|
|
30
|
+
return c.json({ error: 'Dedicated MCP credential required' }, 401);
|
|
31
|
+
c.header('Cache-Control', 'no-store');
|
|
32
|
+
return next();
|
|
33
|
+
});
|
|
23
34
|
// Only creation and message envelopes may carry the larger, base64 attachment payload.
|
|
24
35
|
// Other tools retain their existing 1 MiB limit, including malformed envelopes.
|
|
25
36
|
app.use('*', bodyLimit({ maxSize: MAX_MCP_ATTACHMENT_REQUEST_BYTES }));
|
|
@@ -44,6 +55,9 @@ app.use('*', async (c, next) => {
|
|
|
44
55
|
return c.json({ error: 'MCP request is too large' }, 413);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
58
|
+
// Uploads can outlive their credential. Never retain the pre-body principal.
|
|
59
|
+
if (!authenticateRequest(c.req.header('Authorization')))
|
|
60
|
+
return c.json({ error: 'Dedicated MCP credential required' }, 401);
|
|
47
61
|
return next();
|
|
48
62
|
});
|
|
49
63
|
/** Dispatch only the supported MCP operations through the existing UI handlers. */
|
|
@@ -66,83 +80,27 @@ async function dispatchWorkspaceRequest(pathname, init) {
|
|
|
66
80
|
throw new Error('Unsupported workspace operation');
|
|
67
81
|
}
|
|
68
82
|
app.all('/', async (c) => {
|
|
83
|
+
const currentPrincipal = () => {
|
|
84
|
+
const principal = authenticateRequest(c.req.header('Authorization'));
|
|
85
|
+
if (!principal)
|
|
86
|
+
throw new Error('MCP credential expired or revoked');
|
|
87
|
+
return principal;
|
|
88
|
+
};
|
|
69
89
|
const server = new Server({ name: 'kobo-workspaces', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
70
90
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
71
91
|
sessionIdGenerator: undefined,
|
|
72
92
|
enableJsonResponse: true,
|
|
73
93
|
maxRequestBodySize: MAX_MCP_ATTACHMENT_REQUEST_BYTES,
|
|
74
94
|
});
|
|
75
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
...WORKSPACE_LIFECYCLE_TOOLS,
|
|
80
|
-
...WORKSPACE_GROUP_MESSAGE_TOOLS,
|
|
81
|
-
...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
|
|
82
|
-
...PROGRESS_BAR_TOOLS,
|
|
83
|
-
],
|
|
84
|
-
}));
|
|
95
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
96
|
+
const principal = currentPrincipal();
|
|
97
|
+
return { tools: EXTERNAL_TOOL_CATALOGUE.filter((tool) => canUseExternalMcpTool(principal, tool.name)) };
|
|
98
|
+
});
|
|
85
99
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
? executeProgressBarTool(request.params.name, request.params.arguments ?? {})
|
|
91
|
-
: isMemory
|
|
92
|
-
? executeMemoryMcpTool(request.params.name, request.params.arguments ?? {}, source)
|
|
93
|
-
: isWorkspaceCreationTool(request.params.name)
|
|
94
|
-
? await executeWorkspaceCreationTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
95
|
-
: isWorkspaceLifecycleTool(request.params.name)
|
|
96
|
-
? await executeWorkspaceLifecycleTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
97
|
-
: isWorkspaceGroupMessageTool(request.params.name)
|
|
98
|
-
? executeWorkspaceGroupMessageTool(request.params.name, request.params.arguments ?? {}, source)
|
|
99
|
-
: await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
|
|
100
|
-
if (isMemory) {
|
|
101
|
-
const bounded = prepareMemoryToolEnvelope(data, { targetTokens: 1_000, transport: 'external' });
|
|
102
|
-
if (bounded.data.memoryOutputSuppressed === true)
|
|
103
|
-
return { content: [], isError: true };
|
|
104
|
-
const { __mcpError, ...publicData } = bounded.data;
|
|
105
|
-
return {
|
|
106
|
-
content: [{ type: 'text', text: JSON.stringify(publicData) }],
|
|
107
|
-
structuredContent: publicData,
|
|
108
|
-
...(__mcpError === true || publicData.budgetExhausted === true ? { isError: true } : {}),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const structuredContent = data && typeof data === 'object' && !Array.isArray(data) ? data : undefined;
|
|
112
|
-
return {
|
|
113
|
-
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
114
|
-
...(structuredContent ? { structuredContent } : {}),
|
|
115
|
-
...(structuredContent?.accepted === false ? { isError: true } : {}),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
if (error instanceof ProgressBarError) {
|
|
120
|
-
const data = progressErrorData(error);
|
|
121
|
-
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
122
|
-
}
|
|
123
|
-
if (error instanceof WorkspaceGroupMessageMcpError) {
|
|
124
|
-
const data = { error: error.message, status: error.status };
|
|
125
|
-
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
126
|
-
}
|
|
127
|
-
if (error instanceof WorkspaceCreationMcpError || error instanceof WorkspaceLifecycleMcpError) {
|
|
128
|
-
const data = { error: error.message, status: error.status, stage: error.stage, details: error.details };
|
|
129
|
-
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
130
|
-
}
|
|
131
|
-
if (isMemory) {
|
|
132
|
-
const bounded = prepareMemoryToolEnvelope({
|
|
133
|
-
error: error instanceof Error ? error.message : String(error),
|
|
134
|
-
}, { targetTokens: 1_000, transport: 'external' });
|
|
135
|
-
return {
|
|
136
|
-
isError: true,
|
|
137
|
-
content: [{ type: 'text', text: JSON.stringify(bounded.data) }],
|
|
138
|
-
structuredContent: bounded.data,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
isError: true,
|
|
143
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
144
|
-
};
|
|
145
|
-
}
|
|
100
|
+
const source = createMcpClientContext(c.req.header('X-Kobo-Client-Name'), c.req.header('X-Kobo-Client-Transport') === 'stdio' ? 'stdio' : 'http', c.req.header('X-Kobo-Client-Name-Encoding'));
|
|
101
|
+
// This is the acceptance boundary: already-dispatched operations are not
|
|
102
|
+
// cancelled on later revocation, but every new call reloads the live grant.
|
|
103
|
+
return executeExternalMcpTool(request.params.name, request.params.arguments ?? {}, source, currentPrincipal());
|
|
146
104
|
});
|
|
147
105
|
try {
|
|
148
106
|
await server.connect(transport);
|
|
@@ -155,4 +113,126 @@ app.all('/', async (c) => {
|
|
|
155
113
|
await server.close();
|
|
156
114
|
}
|
|
157
115
|
});
|
|
116
|
+
export async function executeExternalMcpTool(name, args, source, principal) {
|
|
117
|
+
const isMemory = isMemoryToolName(name);
|
|
118
|
+
try {
|
|
119
|
+
if (principal)
|
|
120
|
+
args = authorizeExternalMcpCall(principal, name, args);
|
|
121
|
+
let data = isProgressBarTool(name)
|
|
122
|
+
? executeProgressBarTool(name, args)
|
|
123
|
+
: isMemory
|
|
124
|
+
? executeMemoryMcpTool(name, args, source, principal && !principal.allWorkspaces ? principal.workspaceIds : undefined)
|
|
125
|
+
: isWorkspaceCreationTool(name)
|
|
126
|
+
? await executeWorkspaceCreationTool(name, args, dispatchWorkspaceRequest)
|
|
127
|
+
: isWorkspaceLifecycleTool(name)
|
|
128
|
+
? await executeWorkspaceLifecycleTool(name, args, dispatchWorkspaceRequest)
|
|
129
|
+
: isWorkspaceGroupMessageTool(name)
|
|
130
|
+
? executeWorkspaceGroupMessageTool(name, args, source)
|
|
131
|
+
: await executeWorkspaceDialogueTool(name, args, source);
|
|
132
|
+
if (principal && !principal.allWorkspaces && name === 'list_workspaces' && Array.isArray(data)) {
|
|
133
|
+
data = data.filter((workspace) => principal.workspaceIds.includes(workspace.id));
|
|
134
|
+
}
|
|
135
|
+
if (isMemory) {
|
|
136
|
+
const bounded = prepareMemoryToolEnvelope(data, { targetTokens: 1_000, transport: 'external' });
|
|
137
|
+
if (bounded.data.memoryOutputSuppressed === true)
|
|
138
|
+
return { content: [], isError: true };
|
|
139
|
+
const { __mcpError, ...publicData } = bounded.data;
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: JSON.stringify(publicData) }],
|
|
142
|
+
structuredContent: publicData,
|
|
143
|
+
...(__mcpError === true || publicData.budgetExhausted === true ? { isError: true } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const structuredContent = data && typeof data === 'object' && !Array.isArray(data) ? data : undefined;
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
149
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
150
|
+
...(structuredContent?.accepted === false ? { isError: true } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof ProgressBarError) {
|
|
155
|
+
const data = progressErrorData(error);
|
|
156
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
157
|
+
}
|
|
158
|
+
if (error instanceof WorkspaceGroupMessageMcpError) {
|
|
159
|
+
const data = { error: error.message, status: error.status };
|
|
160
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
161
|
+
}
|
|
162
|
+
if (error instanceof WorkspaceCreationMcpError || error instanceof WorkspaceLifecycleMcpError) {
|
|
163
|
+
const data = { error: error.message, status: error.status, stage: error.stage, details: error.details };
|
|
164
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
165
|
+
}
|
|
166
|
+
if (isMemory) {
|
|
167
|
+
const bounded = prepareMemoryToolEnvelope({
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
}, { targetTokens: 1_000, transport: 'external' });
|
|
170
|
+
return {
|
|
171
|
+
isError: true,
|
|
172
|
+
content: [{ type: 'text', text: JSON.stringify(bounded.data) }],
|
|
173
|
+
structuredContent: bounded.data,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
isError: true,
|
|
178
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
158
182
|
export default app;
|
|
183
|
+
const allWorkspaceOnly = new Set([
|
|
184
|
+
'create_workspace',
|
|
185
|
+
'diagnose_workspace_pr',
|
|
186
|
+
'preview_workspace_group_message',
|
|
187
|
+
'get_workspace_group_message',
|
|
188
|
+
]);
|
|
189
|
+
export function canUseExternalMcpTool(principal, name) {
|
|
190
|
+
return principal.tools.includes(name) && (principal.allWorkspaces || !allWorkspaceOnly.has(name));
|
|
191
|
+
}
|
|
192
|
+
export function authorizeExternalMcpCall(principal, name, args) {
|
|
193
|
+
const deny = () => {
|
|
194
|
+
throw new Error('MCP credential does not authorize this operation');
|
|
195
|
+
};
|
|
196
|
+
if (!canUseExternalMcpTool(principal, name))
|
|
197
|
+
return deny();
|
|
198
|
+
// Resource-wide grants still use each service's normal argument validation.
|
|
199
|
+
if (principal.allWorkspaces)
|
|
200
|
+
return args;
|
|
201
|
+
const definition = EXTERNAL_TOOL_CATALOGUE.find((tool) => tool.name === name);
|
|
202
|
+
if (!definition || Object.keys(args).some((key) => !Object.hasOwn(definition.inputSchema.properties ?? {}, key)))
|
|
203
|
+
return deny();
|
|
204
|
+
if (name === 'list_workspaces')
|
|
205
|
+
return args;
|
|
206
|
+
if (name === 'send_workspace_group_message') {
|
|
207
|
+
if (!Array.isArray(args.workspace_ids) ||
|
|
208
|
+
args.workspace_ids.some((id) => typeof id !== 'string' || !principal.workspaceIds.includes(id)))
|
|
209
|
+
return deny();
|
|
210
|
+
return args;
|
|
211
|
+
}
|
|
212
|
+
if (isMemoryToolName(name)) {
|
|
213
|
+
if (name === 'list_memory_scopes') {
|
|
214
|
+
if (args.workspace_id !== undefined &&
|
|
215
|
+
(typeof args.workspace_id !== 'string' || !principal.workspaceIds.includes(args.workspace_id)))
|
|
216
|
+
return deny();
|
|
217
|
+
return args;
|
|
218
|
+
}
|
|
219
|
+
// Workspace aggregate memory also includes global/project entries. Restricted
|
|
220
|
+
// keys use only the workspace-owned scope, never inherited memory.
|
|
221
|
+
let scope;
|
|
222
|
+
if (typeof args.scope_id === 'string')
|
|
223
|
+
scope = getDb()
|
|
224
|
+
.prepare("SELECT id, workspace_id FROM memory_scopes WHERE id = ? AND level = 'workspace'")
|
|
225
|
+
.get(args.scope_id);
|
|
226
|
+
else if (typeof args.workspace_id === 'string' && principal.workspaceIds.includes(args.workspace_id)) {
|
|
227
|
+
const resolved = resolveWorkspaceMemoryScope(args.workspace_id);
|
|
228
|
+
scope = { id: resolved.id, workspace_id: args.workspace_id };
|
|
229
|
+
}
|
|
230
|
+
if (!scope?.workspace_id || !principal.workspaceIds.includes(scope.workspace_id))
|
|
231
|
+
return deny();
|
|
232
|
+
const { workspace_id: _workspace, ...rest } = args;
|
|
233
|
+
return { ...rest, scope_id: scope.id };
|
|
234
|
+
}
|
|
235
|
+
if (typeof args.workspace_id !== 'string' || !principal.workspaceIds.includes(args.workspace_id))
|
|
236
|
+
return deny();
|
|
237
|
+
return args;
|
|
238
|
+
}
|
|
@@ -36,7 +36,7 @@ import { buildEngineHandoff } from '../services/engine-handoff-service.js';
|
|
|
36
36
|
import { saveWorkspaceFile, shaOf } from '../services/file-editor-service.js';
|
|
37
37
|
import { getForgeProvider } from '../services/forge/registry.js';
|
|
38
38
|
import { resolveForge } from '../services/forge/resolve.js';
|
|
39
|
-
import { ForgeUnavailableError } from '../services/forge/types.js';
|
|
39
|
+
import { ForgeUnavailableError, MergePolicyError } from '../services/forge/types.js';
|
|
40
40
|
import { computeGitStats } from '../services/git-stats-service.js';
|
|
41
41
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, renderNotionInitialPrompt, renderSentryInitialPrompt, } from '../services/initial-prompt-template-service.js';
|
|
42
42
|
import * as notionService from '../services/notion-service.js';
|
|
@@ -4233,11 +4233,18 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
4233
4233
|
}
|
|
4234
4234
|
body = parsed;
|
|
4235
4235
|
}
|
|
4236
|
+
if (body.admin !== undefined && typeof body.admin !== 'boolean') {
|
|
4237
|
+
return c.json({ error: 'admin must be a boolean' }, 400);
|
|
4238
|
+
}
|
|
4239
|
+
const admin = body.admin === true;
|
|
4236
4240
|
const method = body.method;
|
|
4237
4241
|
if (method !== undefined && !isMergeMethod(method)) {
|
|
4238
4242
|
return c.json({ error: 'method must be merge, squash or rebase' }, 400);
|
|
4239
4243
|
}
|
|
4240
4244
|
const provider = getForgeProvider(resolveForge(workspace.projectPath));
|
|
4245
|
+
if (admin && provider.id !== 'github') {
|
|
4246
|
+
return c.json({ error: 'Administrator merge is only supported for GitHub', code: 'merge_admin_unsupported' }, 409);
|
|
4247
|
+
}
|
|
4241
4248
|
if (!provider.capabilities.canMergeRequest) {
|
|
4242
4249
|
return c.json({ error: 'This project has no forge that can merge the PR', code: 'forge_unsupported' }, 409);
|
|
4243
4250
|
}
|
|
@@ -4255,10 +4262,16 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
4255
4262
|
if (snapshot?.state !== 'OPEN') {
|
|
4256
4263
|
return c.json({ error: 'No open PR found for this workspace', code: 'pr_not_open' }, 409);
|
|
4257
4264
|
}
|
|
4258
|
-
if (
|
|
4265
|
+
if (admin && snapshot.mergeable === 'CONFLICTING') {
|
|
4266
|
+
return c.json({ error: 'Resolve merge conflicts before merging', code: 'pr_not_ready' }, 409);
|
|
4267
|
+
}
|
|
4268
|
+
if (!snapshot.readyToMerge && !admin) {
|
|
4259
4269
|
return c.json({ error: 'This PR is not ready to merge', code: 'pr_not_ready' }, 409);
|
|
4260
4270
|
}
|
|
4261
|
-
if (
|
|
4271
|
+
if (admin) {
|
|
4272
|
+
await provider.mergeRequest(workspace.worktreePath, snapshot.number, method, { admin: true });
|
|
4273
|
+
}
|
|
4274
|
+
else if (method === undefined) {
|
|
4262
4275
|
await provider.mergeRequest(workspace.worktreePath, snapshot.number);
|
|
4263
4276
|
}
|
|
4264
4277
|
else {
|
|
@@ -4268,6 +4281,9 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
4268
4281
|
return c.json({ success: true, merged: merged?.state === 'MERGED', branch: workspace.workingBranch });
|
|
4269
4282
|
}
|
|
4270
4283
|
catch (err) {
|
|
4284
|
+
if (err instanceof MergePolicyError) {
|
|
4285
|
+
return c.json({ error: err.message, code: err.code }, 409);
|
|
4286
|
+
}
|
|
4271
4287
|
if (err instanceof ForgeUnavailableError) {
|
|
4272
4288
|
return c.json({ error: err.message, code: 'forge_unavailable' }, 409);
|
|
4273
4289
|
}
|
|
@@ -3,7 +3,7 @@ const localRequire = createRequire(import.meta.url);
|
|
|
3
3
|
/**
|
|
4
4
|
* Detect the live host's platform/arch/libc from `process`. `glibcVersionRuntime`
|
|
5
5
|
* lives in the diagnostic report header on glibc systems and is absent on musl.
|
|
6
|
-
* Available since Node 12; Kōbō requires Node ≥
|
|
6
|
+
* Available since Node 12; Kōbō requires Node ≥ 26.12.1 (see AGENTS.md).
|
|
7
7
|
*/
|
|
8
8
|
export function detectPlatform() {
|
|
9
9
|
const report = process.report;
|
|
@@ -3,7 +3,7 @@ import { nanoid } from 'nanoid';
|
|
|
3
3
|
import { renderWorkflowPolicy } from '../../../shared/workflow-policy.js';
|
|
4
4
|
import { getDb } from '../../db/index.js';
|
|
5
5
|
import { assertAgentStopped } from '../../utils/agent-stop-result.js';
|
|
6
|
-
import { ensureKoboHome, getCompiledMcpServerPath,
|
|
6
|
+
import { ensureKoboHome, getCompiledMcpServerPath, getKoboHome, getMcpServerSourcePath, getSkillsPath, } from '../../utils/paths.js';
|
|
7
7
|
import { assertWorkspaceLifecycleAvailable, workspaceLifecycleReason } from '../../utils/workspace-lifecycle-guard.js';
|
|
8
8
|
import { cancelAutoLoopFinalReview, getAutoLoopReviewLaunch, isAutoLoopReview, prepareAutoLoopReviewReturn, retryAutoLoopReviewReturn, } from '../auto-loop-final-review-service.js';
|
|
9
9
|
import { listLoopMessages, settleLoopMessages } from '../auto-loop-message-service.js';
|
|
@@ -11,6 +11,7 @@ import * as autoLoopService from '../auto-loop-service.js';
|
|
|
11
11
|
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
12
12
|
import * as cronService from '../cron-service.js';
|
|
13
13
|
import { resolveForge } from '../forge/resolve.js';
|
|
14
|
+
import { createInternalMcpCapability, revokeInternalMcpCapability } from '../internal-mcp-capability-service.js';
|
|
14
15
|
import * as lifecycleHookService from '../lifecycle-hook-service.js';
|
|
15
16
|
import { advanceMemoryConversationEpoch, allocateMemoryConversationKey, bindMemoryNativeConversation, createMemoryCapability, revokeMemoryCapability, } from '../memory-agent-runtime.js';
|
|
16
17
|
import { buildMemoryContext, markMemoryContextFailed, markMemoryContextInitialized, markMemoryContextSubmitted, } from '../memory-context-service.js';
|
|
@@ -494,27 +495,19 @@ function readEffectiveSettingsSafe(projectPath) {
|
|
|
494
495
|
};
|
|
495
496
|
}
|
|
496
497
|
}
|
|
497
|
-
function buildMcpServers(workspaceId,
|
|
498
|
+
function buildMcpServers(workspaceId, internalCapability, launchEnv) {
|
|
498
499
|
const mcpServerCompiled = getCompiledMcpServerPath();
|
|
499
500
|
const mcpServerSource = getMcpServerSourcePath();
|
|
500
|
-
const global = getGlobalSettings();
|
|
501
501
|
return [
|
|
502
502
|
{
|
|
503
503
|
name: 'kobo-tasks',
|
|
504
504
|
command: mcpServerCompiled ? 'node' : 'npx',
|
|
505
505
|
args: mcpServerCompiled ? [mcpServerCompiled] : ['tsx', mcpServerSource],
|
|
506
506
|
env: {
|
|
507
|
+
...Object.fromEntries(Object.entries(launchEnv ?? {}).filter(([key, value]) => key !== 'KOBO_NETWORK_TOKEN' && typeof value === 'string')),
|
|
507
508
|
KOBO_WORKSPACE_ID: workspaceId,
|
|
508
|
-
...
|
|
509
|
-
...(memoryCapability ? { KOBO_MEMORY_SESSION_TOKEN: memoryCapability } : {}),
|
|
510
|
-
KOBO_DB_PATH: getDbPath(),
|
|
511
|
-
KOBO_SETTINGS_PATH: getSettingsPath(),
|
|
509
|
+
...(internalCapability ? { KOBO_MCP_SESSION_TOKEN: internalCapability } : {}),
|
|
512
510
|
KOBO_BACKEND_URL: `http://127.0.0.1:${backendPort}`,
|
|
513
|
-
// Behind a proxy, even internal loopback requests require network auth.
|
|
514
|
-
// Per-launch memory/progress capabilities remain independently required.
|
|
515
|
-
...(global.networkAccessBehindProxy && global.networkAccessToken
|
|
516
|
-
? { KOBO_NETWORK_TOKEN: global.networkAccessToken }
|
|
517
|
-
: {}),
|
|
518
511
|
},
|
|
519
512
|
},
|
|
520
513
|
];
|
|
@@ -1361,11 +1354,16 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1361
1354
|
}
|
|
1362
1355
|
const launchOptions = new WeakMap();
|
|
1363
1356
|
const controllerProgressCapabilities = new WeakMap();
|
|
1357
|
+
const controllerInternalMcpCapabilities = new WeakMap();
|
|
1364
1358
|
const controllerMemoryCapabilities = new WeakMap();
|
|
1365
1359
|
const controllerMemoryConversationKeys = new WeakMap();
|
|
1366
1360
|
const controllerMemoryContextIds = new WeakMap();
|
|
1367
1361
|
const memoryCompactionStates = new WeakMap();
|
|
1368
1362
|
function revokeControllerCapabilities(controller) {
|
|
1363
|
+
const internalToken = controllerInternalMcpCapabilities.get(controller);
|
|
1364
|
+
if (internalToken)
|
|
1365
|
+
revokeInternalMcpCapability(internalToken);
|
|
1366
|
+
controllerInternalMcpCapabilities.delete(controller);
|
|
1369
1367
|
const progress = controllerProgressCapabilities.get(controller);
|
|
1370
1368
|
if (progress)
|
|
1371
1369
|
revokeProgressCapability(progress.token);
|
|
@@ -1574,6 +1572,17 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1574
1572
|
}),
|
|
1575
1573
|
readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration,
|
|
1576
1574
|
});
|
|
1575
|
+
const progressCapability = createProgressCapability(workspaceId, agentSessionId);
|
|
1576
|
+
const internalMcpCapability = createInternalMcpCapability({
|
|
1577
|
+
workspaceId,
|
|
1578
|
+
sessionId: agentSessionId,
|
|
1579
|
+
readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration || !!launch?.mcpEnv?.KOBO_FINAL_REVIEW_TOKEN,
|
|
1580
|
+
env: {
|
|
1581
|
+
...launch?.mcpEnv,
|
|
1582
|
+
KOBO_MEMORY_SESSION_TOKEN: memoryCapability.token,
|
|
1583
|
+
KOBO_PROGRESS_SESSION_TOKEN: progressCapability.token,
|
|
1584
|
+
},
|
|
1585
|
+
});
|
|
1577
1586
|
const unavailableIntegrations = [];
|
|
1578
1587
|
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
|
|
1579
1588
|
const options = {
|
|
@@ -1598,7 +1607,10 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1598
1607
|
}
|
|
1599
1608
|
})(),
|
|
1600
1609
|
settings,
|
|
1601
|
-
mcpServers: [
|
|
1610
|
+
mcpServers: [
|
|
1611
|
+
...buildMcpServers(workspaceId, internalMcpCapability.token, internalMcpCapability.env),
|
|
1612
|
+
...integrationServers,
|
|
1613
|
+
],
|
|
1602
1614
|
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
1603
1615
|
};
|
|
1604
1616
|
// Snapshot only application-owned launch instructions. Memory is appended
|
|
@@ -1629,20 +1641,18 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1629
1641
|
.run(new Date().toISOString(), workspaceId, prompt);
|
|
1630
1642
|
};
|
|
1631
1643
|
}
|
|
1632
|
-
const progressCapability = createProgressCapability(workspaceId, agentSessionId);
|
|
1633
|
-
const managedMcp = options.mcpServers?.find((server) => server.name === 'kobo-tasks');
|
|
1634
|
-
if (managedMcp)
|
|
1635
|
-
managedMcp.env.KOBO_PROGRESS_SESSION_TOKEN = progressCapability.token;
|
|
1636
1644
|
try {
|
|
1637
1645
|
beforeDispatch?.();
|
|
1638
1646
|
}
|
|
1639
1647
|
catch (error) {
|
|
1648
|
+
revokeInternalMcpCapability(internalMcpCapability.token);
|
|
1640
1649
|
revokeMemoryCapability(memoryCapability.token);
|
|
1641
1650
|
revokeProgressCapability(progressCapability.token);
|
|
1642
1651
|
throw error;
|
|
1643
1652
|
}
|
|
1644
1653
|
let controller;
|
|
1645
1654
|
controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, controller, ev));
|
|
1655
|
+
controllerInternalMcpCapabilities.set(controller, internalMcpCapability.token);
|
|
1646
1656
|
controllerProgressCapabilities.set(controller, progressCapability);
|
|
1647
1657
|
controllerMemoryCapabilities.set(controller, memoryCapability.token);
|
|
1648
1658
|
controllerMemoryConversationKeys.set(controller, memoryCapability.conversationKey);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
3
5
|
import { resolveInstance, sanitizeBranchName } from '../utils/dev-server-instance.js';
|
|
4
6
|
import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
|
|
5
7
|
import { getProjectSettings } from './settings-service.js';
|
|
@@ -25,6 +27,7 @@ function runCommand(command, args, options = {}) {
|
|
|
25
27
|
// ── State ──────────────────────────────────────────────────────────────────────
|
|
26
28
|
/** workspaceId -> spawned dev-server process */
|
|
27
29
|
const trackedProcesses = new Map();
|
|
30
|
+
const directServerStatuses = new Map();
|
|
28
31
|
const stoppingProcesses = new Map();
|
|
29
32
|
const generations = new Map();
|
|
30
33
|
let shuttingDown = false;
|
|
@@ -34,6 +37,7 @@ export function beginDevServerShutdown() {
|
|
|
34
37
|
/** Test-only: clear the tracked-processes map between tests. */
|
|
35
38
|
export function _resetTrackedProcessesForTests() {
|
|
36
39
|
trackedProcesses.clear();
|
|
40
|
+
directServerStatuses.clear();
|
|
37
41
|
stoppingProcesses.clear();
|
|
38
42
|
generations.clear();
|
|
39
43
|
shuttingDown = false;
|
|
@@ -74,14 +78,18 @@ export async function listRunningContainers(projectName) {
|
|
|
74
78
|
* still running (e.g. `docker compose up -d` is pulling/building images), the
|
|
75
79
|
* status is reported as `'starting'` even if no matching container is visible
|
|
76
80
|
* in `docker ps` yet. This prevents the UI from flashing to `'stopped'` during
|
|
77
|
-
* long build phases.
|
|
81
|
+
* long build phases. Direct processes report running and their URL after a
|
|
82
|
+
* recognized local readiness announcement on stdout or stderr.
|
|
78
83
|
*/
|
|
79
84
|
export async function getStatus(projectPath, workingBranch, workspaceId) {
|
|
80
85
|
const config = resolveInstance(projectPath, workingBranch);
|
|
86
|
+
const directStatus = workspaceId ? directServerStatuses.get(workspaceId) : undefined;
|
|
87
|
+
if (workspaceId && directStatus && trackedProcesses.has(workspaceId))
|
|
88
|
+
return { ...directStatus };
|
|
81
89
|
if (!config) {
|
|
82
90
|
return {
|
|
83
|
-
status: 'unknown',
|
|
84
|
-
instanceName: '',
|
|
91
|
+
status: workspaceId && trackedProcesses.has(workspaceId) ? 'starting' : 'unknown',
|
|
92
|
+
instanceName: workspaceId && trackedProcesses.has(workspaceId) ? sanitizeBranchName(workingBranch) : '',
|
|
85
93
|
projectName: '',
|
|
86
94
|
httpPort: '',
|
|
87
95
|
url: '',
|
|
@@ -163,12 +171,64 @@ export function startDevServer(workspaceId) {
|
|
|
163
171
|
const generation = (generations.get(workspaceId) ?? 0) + 1;
|
|
164
172
|
generations.set(workspaceId, generation);
|
|
165
173
|
trackedProcesses.set(workspaceId, proc);
|
|
166
|
-
//
|
|
174
|
+
// Parse complete lines separately per stream: CLI output and ANSI sequences
|
|
175
|
+
// can span arbitrary pipe chunks. Bound the unfinished line retained in memory.
|
|
176
|
+
const observeOutput = () => {
|
|
177
|
+
const decoder = new StringDecoder('utf8');
|
|
178
|
+
let pending = '';
|
|
179
|
+
return (data) => {
|
|
180
|
+
if (shuttingDown ||
|
|
181
|
+
trackedProcesses.get(workspaceId) !== proc ||
|
|
182
|
+
generations.get(workspaceId) !== generation ||
|
|
183
|
+
stoppingProcesses.has(workspaceId) ||
|
|
184
|
+
directServerStatuses.has(workspaceId))
|
|
185
|
+
return;
|
|
186
|
+
pending += decoder.write(data);
|
|
187
|
+
const lines = pending.split(/[\r\n]/);
|
|
188
|
+
pending = lines.pop().slice(-8192);
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
// Quasar's "App URL....." and Vite/Next's "Local:" are explicit
|
|
191
|
+
// readiness announcements; unrelated links in build output are not.
|
|
192
|
+
const match = stripVTControlCharacters(line).match(/^\s*(?:[»➜-]\s*)?(?:App URL[.\s]*|Local:\s*)(https?:\/\/\S+)\s*$/i);
|
|
193
|
+
if (!match)
|
|
194
|
+
continue;
|
|
195
|
+
let url;
|
|
196
|
+
try {
|
|
197
|
+
url = new URL(match[1]);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (url.username ||
|
|
203
|
+
url.password ||
|
|
204
|
+
!['localhost', '127.0.0.1', '[::1]', '0.0.0.0', '[::]'].includes(url.hostname))
|
|
205
|
+
continue;
|
|
206
|
+
if (url.hostname === '0.0.0.0' || url.hostname === '[::]')
|
|
207
|
+
url.hostname = 'localhost';
|
|
208
|
+
const status = {
|
|
209
|
+
status: 'running',
|
|
210
|
+
instanceName,
|
|
211
|
+
projectName: '',
|
|
212
|
+
httpPort: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
|
213
|
+
url: url.href,
|
|
214
|
+
containers: [],
|
|
215
|
+
};
|
|
216
|
+
directServerStatuses.set(workspaceId, status);
|
|
217
|
+
updateDevServerStatus(workspaceId, 'running');
|
|
218
|
+
emitEphemeral(workspaceId, 'devserver:status', status);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
const observeStdout = observeOutput();
|
|
224
|
+
const observeStderr = observeOutput();
|
|
167
225
|
proc.stdout?.on('data', (data) => {
|
|
168
226
|
console.log(`[dev-server:${instanceName}] ${data.toString().trim()}`);
|
|
227
|
+
observeStdout(data);
|
|
169
228
|
});
|
|
170
229
|
proc.stderr?.on('data', (data) => {
|
|
171
230
|
console.error(`[dev-server:${instanceName}] ${data.toString().trim()}`);
|
|
231
|
+
observeStderr(data);
|
|
172
232
|
});
|
|
173
233
|
proc.on('exit', (code) => {
|
|
174
234
|
// Shell exit does not imply that its background children have stopped.
|
|
@@ -181,6 +241,7 @@ export function startDevServer(workspaceId) {
|
|
|
181
241
|
if (trackedProcesses.get(workspaceId) !== proc || stoppingProcesses.has(workspaceId))
|
|
182
242
|
return;
|
|
183
243
|
trackedProcesses.delete(workspaceId);
|
|
244
|
+
directServerStatuses.delete(workspaceId);
|
|
184
245
|
const currentStatus = await getStatus(workspace.projectPath, workspace.workingBranch);
|
|
185
246
|
if (shuttingDown || generations.get(workspaceId) !== generation || stoppingProcesses.has(workspaceId))
|
|
186
247
|
return;
|
|
@@ -200,6 +261,7 @@ export function startDevServer(workspaceId) {
|
|
|
200
261
|
// A signal failure is also an 'error' event; it does not prove process exit.
|
|
201
262
|
if (!proc.pid)
|
|
202
263
|
trackedProcesses.delete(workspaceId);
|
|
264
|
+
directServerStatuses.delete(workspaceId);
|
|
203
265
|
updateDevServerStatus(workspaceId, 'error');
|
|
204
266
|
console.error(`[dev-server] Process error for workspace ${workspaceId}:`, err);
|
|
205
267
|
emitEphemeral(workspaceId, 'devserver:status', {
|
|
@@ -233,6 +295,7 @@ export async function stopDevServer(workspaceId) {
|
|
|
233
295
|
if (pending)
|
|
234
296
|
return pending;
|
|
235
297
|
generations.set(workspaceId, (generations.get(workspaceId) ?? 0) + 1);
|
|
298
|
+
directServerStatuses.delete(workspaceId);
|
|
236
299
|
const stopping = stopDevServerOwned(workspaceId);
|
|
237
300
|
stoppingProcesses.set(workspaceId, stopping);
|
|
238
301
|
try {
|
|
@@ -333,6 +396,7 @@ async function stopTrackedProcess(workspaceId) {
|
|
|
333
396
|
throw new Error('Dev server stop is not confirmed; retry after the process exits');
|
|
334
397
|
if (trackedProcesses.get(workspaceId) === tracked)
|
|
335
398
|
trackedProcesses.delete(workspaceId);
|
|
399
|
+
directServerStatuses.delete(workspaceId);
|
|
336
400
|
}
|
|
337
401
|
async function stopDevServerOwned(workspaceId) {
|
|
338
402
|
const workspace = getWorkspace(workspaceId);
|
|
@@ -70,9 +70,10 @@ function liveProbe() {
|
|
|
70
70
|
}
|
|
71
71
|
/** No credential files, provider calls, generated prompts, or raw process errors in this report. */
|
|
72
72
|
export async function checkEnvironment(request, probe = liveProbe()) {
|
|
73
|
-
const [major, minor] = probe.nodeVersion.split('.').map(Number);
|
|
73
|
+
const [major, minor, patch] = probe.nodeVersion.split('.').map(Number);
|
|
74
|
+
const supportedNode = major > 26 || (major === 26 && (minor > 12 || (minor === 12 && patch >= 1)));
|
|
74
75
|
const checks = [
|
|
75
|
-
{ code: 'node', status:
|
|
76
|
+
{ code: 'node', status: supportedNode ? 'ok' : 'error' },
|
|
76
77
|
{ code: 'platform', status: ['linux', 'darwin'].includes(probe.platform) ? 'ok' : 'error' },
|
|
77
78
|
];
|
|
78
79
|
const commandAvailable = async (command, args) => {
|