@qodeca/xezar 0.13.0 → 0.14.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 +72 -39
- package/dist/agent-config/account-identity.d.ts +2 -8
- package/dist/agent-config/account-identity.js +54 -10
- package/dist/agent-config/account-identity.js.map +1 -1
- package/dist/agent-config/catalog.d.ts +49 -7
- package/dist/agent-config/catalog.js +107 -0
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/agent-config/model-settings/pi.d.ts +12 -6
- package/dist/agent-config/model-settings/pi.js +12 -6
- package/dist/agent-config/model-settings/pi.js.map +1 -1
- package/dist/agent-config/seed.js +10 -0
- package/dist/agent-config/seed.js.map +1 -1
- package/dist/contract/github.d.ts +15 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +1527 -895
- package/dist/contract/mcp-api-reference.d.ts +174 -0
- package/dist/contract/mcp-audit.d.ts +103 -0
- package/dist/contract/mcp-discovery.d.ts +257 -0
- package/dist/contract/mcp-event-catalog.d.ts +121 -0
- package/dist/contract/mcp-event-catalog.test.d.ts +1 -0
- package/dist/contract/mcp-idempotency.d.ts +200 -0
- package/dist/contract/mcp-journal.d.ts +268 -0
- package/dist/contract/mcp-leader.d.ts +88 -0
- package/dist/contract/mcp-ownership.d.ts +78 -0
- package/dist/contract/mcp-versioning.d.ts +139 -0
- package/dist/contract/runs.d.ts +81 -0
- package/dist/core/agent-profiles.d.ts +15 -4
- package/dist/core/agent-profiles.js +22 -8
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/opencode-server-runner.js +71 -11
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/secret-redaction.d.ts +7 -1
- package/dist/core/secret-redaction.js +44 -10
- package/dist/core/secret-redaction.js.map +1 -1
- package/dist/index.js +138 -27
- package/dist/index.js.map +1 -1
- package/dist/machine-identity.d.ts +66 -0
- package/dist/machine-identity.js +96 -0
- package/dist/machine-identity.js.map +1 -0
- package/dist/mcp/adapters/claude-code.d.ts +41 -0
- package/dist/mcp/adapters/claude-code.js +40 -0
- package/dist/mcp/adapters/claude-code.js.map +1 -0
- package/dist/mcp/adapters/codex.d.ts +140 -0
- package/dist/mcp/adapters/codex.js +323 -0
- package/dist/mcp/adapters/codex.js.map +1 -0
- package/dist/mcp/adapters/opencode.d.ts +148 -0
- package/dist/mcp/adapters/opencode.js +528 -0
- package/dist/mcp/adapters/opencode.js.map +1 -0
- package/dist/mcp/adapters/pi-link.d.ts +102 -0
- package/dist/mcp/adapters/pi-link.js +235 -0
- package/dist/mcp/adapters/pi-link.js.map +1 -0
- package/dist/mcp/adapters/pi.d.ts +188 -0
- package/dist/mcp/adapters/pi.js +418 -0
- package/dist/mcp/adapters/pi.js.map +1 -0
- package/dist/mcp/api-reference.d.ts +26 -0
- package/dist/mcp/api-reference.js +133 -0
- package/dist/mcp/api-reference.js.map +1 -0
- package/dist/mcp/audit-trail.d.ts +162 -0
- package/dist/mcp/audit-trail.js +306 -0
- package/dist/mcp/audit-trail.js.map +1 -0
- package/dist/mcp/bridge.d.ts +81 -0
- package/dist/mcp/bridge.js +407 -0
- package/dist/mcp/bridge.js.map +1 -0
- package/dist/mcp/connection-file.d.ts +49 -0
- package/dist/mcp/connection-file.js +63 -0
- package/dist/mcp/connection-file.js.map +1 -0
- package/dist/mcp/echo-guard.d.ts +94 -0
- package/dist/mcp/echo-guard.js +160 -0
- package/dist/mcp/echo-guard.js.map +1 -0
- package/dist/mcp/event-catalog.d.ts +120 -0
- package/dist/mcp/event-catalog.js +306 -0
- package/dist/mcp/event-catalog.js.map +1 -0
- package/dist/mcp/event-controller.d.ts +230 -0
- package/dist/mcp/event-controller.js +596 -0
- package/dist/mcp/event-controller.js.map +1 -0
- package/dist/mcp/event-journal.d.ts +57 -0
- package/dist/mcp/event-journal.js +423 -0
- package/dist/mcp/event-journal.js.map +1 -0
- package/dist/mcp/index.d.ts +63 -0
- package/dist/mcp/index.js +386 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ipc.d.ts +202 -0
- package/dist/mcp/ipc.js +206 -0
- package/dist/mcp/ipc.js.map +1 -0
- package/dist/mcp/leader-delivery.d.ts +119 -0
- package/dist/mcp/leader-delivery.js +450 -0
- package/dist/mcp/leader-delivery.js.map +1 -0
- package/dist/mcp/operation-receipts.d.ts +210 -0
- package/dist/mcp/operation-receipts.js +723 -0
- package/dist/mcp/operation-receipts.js.map +1 -0
- package/dist/mcp/project-catalogs.d.ts +23 -0
- package/dist/mcp/project-catalogs.js +25 -0
- package/dist/mcp/project-catalogs.js.map +1 -0
- package/dist/mcp/project-leaders.d.ts +26 -0
- package/dist/mcp/project-leaders.js +17 -0
- package/dist/mcp/project-leaders.js.map +1 -0
- package/dist/mcp/protocol.d.ts +50 -0
- package/dist/mcp/protocol.js +47 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/reconnect.d.ts +297 -0
- package/dist/mcp/reconnect.js +465 -0
- package/dist/mcp/reconnect.js.map +1 -0
- package/dist/mcp/resource-ownership.d.ts +228 -0
- package/dist/mcp/resource-ownership.js +390 -0
- package/dist/mcp/resource-ownership.js.map +1 -0
- package/dist/mcp/service-adapter.d.ts +16488 -0
- package/dist/mcp/service-adapter.js +148 -0
- package/dist/mcp/service-adapter.js.map +1 -0
- package/dist/mcp/service.d.ts +83 -0
- package/dist/mcp/service.js +233 -0
- package/dist/mcp/service.js.map +1 -0
- package/dist/mcp/session-binding.d.ts +105 -0
- package/dist/mcp/session-binding.js +163 -0
- package/dist/mcp/session-binding.js.map +1 -0
- package/dist/mcp/stale-write.d.ts +145 -0
- package/dist/mcp/stale-write.js +219 -0
- package/dist/mcp/stale-write.js.map +1 -0
- package/dist/mcp/tool.d.ts +48 -0
- package/dist/mcp/tool.js +24 -0
- package/dist/mcp/tool.js.map +1 -0
- package/dist/mcp/tools/discovery.d.ts +40 -0
- package/dist/mcp/tools/discovery.js +232 -0
- package/dist/mcp/tools/discovery.js.map +1 -0
- package/dist/mcp/tools/execution-control.d.ts +171 -0
- package/dist/mcp/tools/execution-control.js +513 -0
- package/dist/mcp/tools/execution-control.js.map +1 -0
- package/dist/mcp/tools/handoff-git.d.ts +26 -0
- package/dist/mcp/tools/handoff-git.js +544 -0
- package/dist/mcp/tools/handoff-git.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +2 -0
- package/dist/mcp/tools/index.js +23 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/leader-events.d.ts +46 -0
- package/dist/mcp/tools/leader-events.js +126 -0
- package/dist/mcp/tools/leader-events.js.map +1 -0
- package/dist/mcp/tools/local-handoff.d.ts +102 -0
- package/dist/mcp/tools/local-handoff.js +306 -0
- package/dist/mcp/tools/local-handoff.js.map +1 -0
- package/dist/mcp/tools/project-config.d.ts +340 -0
- package/dist/mcp/tools/project-config.js +1075 -0
- package/dist/mcp/tools/project-config.js.map +1 -0
- package/dist/mcp/tools/results-evidence.d.ts +112 -0
- package/dist/mcp/tools/results-evidence.js +1045 -0
- package/dist/mcp/tools/results-evidence.js.map +1 -0
- package/dist/mcp/tools/task-create.d.ts +70 -0
- package/dist/mcp/tools/task-create.js +473 -0
- package/dist/mcp/tools/task-create.js.map +1 -0
- package/dist/mcp/tools/task-reads.d.ts +102 -0
- package/dist/mcp/tools/task-reads.js +689 -0
- package/dist/mcp/tools/task-reads.js.map +1 -0
- package/dist/mcp/tools/work-organisation.d.ts +2 -0
- package/dist/mcp/tools/work-organisation.js +502 -0
- package/dist/mcp/tools/work-organisation.js.map +1 -0
- package/dist/paths.d.ts +17 -7
- package/dist/paths.js +18 -8
- package/dist/paths.js.map +1 -1
- package/dist/runs/project-writer.js +76 -11
- package/dist/runs/project-writer.js.map +1 -1
- package/dist/runs/retention.d.ts +25 -2
- package/dist/runs/retention.js +63 -2
- package/dist/runs/retention.js.map +1 -1
- package/dist/server/forge/github.js +44 -0
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/forge/types.d.ts +16 -0
- package/dist/server/project-context.d.ts +81 -8
- package/dist/server/project-context.js +143 -35
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/server.d.ts +1050 -53
- package/dist/server/server.js +326 -120
- package/dist/server/server.js.map +1 -1
- package/dist/server/validators.d.ts +26 -6
- package/dist/server/validators.js +17 -0
- package/dist/server/validators.js.map +1 -1
- package/dist/skills-remote.js +64 -6
- package/dist/skills-remote.js.map +1 -1
- package/dist/workflows/run.d.ts +277 -12
- package/dist/workflows/run.js +599 -79
- package/dist/workflows/run.js.map +1 -1
- package/dist/workspace/agent-profiles.d.ts +30 -3
- package/dist/workspace/agent-profiles.js +39 -4
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.js +25 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/project-owner.d.ts +210 -0
- package/dist/workspace/project-owner.js +518 -0
- package/dist/workspace/project-owner.js.map +1 -0
- package/package.json +5 -3
- package/scripts/pi-leader-extension.ts +420 -0
- package/scripts/sync-readme.mjs +83 -2
- package/web/dist/assets/{alert-dialog-BVsVcbFy.js → alert-dialog-D6rP6d39.js} +1 -1
- package/web/dist/assets/arrow-down-DMOfFIzW.js +1 -0
- package/web/dist/assets/arrow-left-C4Ec2BUg.js +1 -0
- package/web/dist/assets/{centered-state-Djgly6Ni.js → centered-state-B0ZqrPaa.js} +12 -12
- package/web/dist/assets/chevron-right-mHJ-crvn.js +1 -0
- package/web/dist/assets/{collapsible-DZ4D-DHG.js → collapsible-DchEHgUM.js} +1 -1
- package/web/dist/assets/{commit-list-B-z1JL9G.js → commit-list-BpMim1Rv.js} +1 -1
- package/web/dist/assets/{compare-variants-CdS8r8E2.js → compare-variants-D5ItQbAq.js} +1 -1
- package/web/dist/assets/{diff-BKew9rQX.js → diff-HqqZ_WDR.js} +2 -2
- package/web/dist/assets/{diff-stat-D0mc5V6Z.js → diff-stat-NsU-3ecr.js} +1 -1
- package/web/dist/assets/{diff-view-xpIcuLIC.js → diff-view-B7c11Xux.js} +1 -1
- package/web/dist/assets/{dropdown-menu-CEQziH7X.js → dropdown-menu-CtTpnyod.js} +1 -1
- package/web/dist/assets/{editable-title-BCX3T2BY.js → editable-title-BXtuGQry.js} +1 -1
- package/web/dist/assets/{ellipsis-vertical-ByVdDc4z.js → ellipsis-vertical-MHtGZSH6.js} +1 -1
- package/web/dist/assets/{file-xBSB5hXO.js → file-DKLw5SOb.js} +1 -1
- package/web/dist/assets/{folder-CldgbZ5A.js → folder-Drw6-ufH.js} +1 -1
- package/web/dist/assets/{git-toolbar-BYlnf4qf.js → git-toolbar-BQH8bORV.js} +1 -1
- package/web/dist/assets/github-ynnpKFCJ.js +1 -0
- package/web/dist/assets/{image-preview-Cv5mZr0n.js → image-preview-DLhMN_R9.js} +1 -1
- package/web/dist/assets/index-BEsrNdXt.js +29 -0
- package/web/dist/assets/index-Da8DOBau.css +2 -0
- package/web/dist/assets/{markdown-CgEEE2ND.js → markdown-q365eN6Y.js} +1 -1
- package/web/dist/assets/{new-task-form-1r2bgsdD.js → new-task-form-CW-hNIWQ.js} +1 -1
- package/web/dist/assets/{pill-q-x22_ej.js → pill-B_TFulXa.js} +1 -1
- package/web/dist/assets/{project-router-VDN4jQM8.js → project-router-CLPc-ImZ.js} +1 -1
- package/web/dist/assets/{prompt-templates-BmZtCe-L.js → prompt-templates-CLc4RxyO.js} +1 -1
- package/web/dist/assets/{repo-git-C2On8SlI.js → repo-git-Is7R6-Tj.js} +1 -1
- package/web/dist/assets/{run-diff-vguEMJzh.js → run-diff-DHMvDlVo.js} +2 -2
- package/web/dist/assets/run-header-DvZJbQ8I.js +1 -0
- package/web/dist/assets/{search-x-QmhaAbg5.js → search-x-Ce2MMetu.js} +1 -1
- package/web/dist/assets/{skill-empty-hint-BrzUwByZ.js → skill-empty-hint-C_xCSvzB.js} +1 -1
- package/web/dist/assets/skills-CivqD4si.js +1 -0
- package/web/dist/assets/{sparkles-CP8QDOgX.js → sparkles-BCqPoj5g.js} +1 -1
- package/web/dist/assets/{square-terminal-CGypshSD.js → square-terminal-ikj5g-oK.js} +1 -1
- package/web/dist/assets/{tab-link-D4lFzzI1.js → tab-link-VsEjFV1Y.js} +1 -1
- package/web/dist/assets/{task-changes-6aopqAmV.js → task-changes-CH-E81-_.js} +1 -1
- package/web/dist/assets/{task-commits-u-dwaCyw.js → task-commits-DqfkMFxp.js} +1 -1
- package/web/dist/assets/{task-files-IMOZnIUK.js → task-files-D_nkHZ_a.js} +2 -2
- package/web/dist/assets/task-thread-bxQompxJ.js +9 -0
- package/web/dist/assets/{textarea-CyBCyKIk.js → textarea-xajuW1L_.js} +1 -1
- package/web/dist/assets/{thread-loading-Cuq2jchC.js → thread-loading-C3JQZbIE.js} +1 -1
- package/web/dist/assets/{trash-2-B6Dp3ZNs.js → trash-2-hJrBhpUD.js} +1 -1
- package/web/dist/assets/{triangle-alert-xBya8FUp.js → triangle-alert-D1u82y1a.js} +1 -1
- package/web/dist/assets/{upload-D6MXEB9p.js → upload-Bn6FBZ59.js} +1 -1
- package/web/dist/assets/{use-desktop-fLDKEJoB.js → use-desktop-DOIMfHfY.js} +1 -1
- package/web/dist/assets/{use-submit-shortcut-JTpNzU-u.js → use-submit-shortcut-Dj4DHTdE.js} +1 -1
- package/web/dist/assets/utils-YwBpOoRN.js +64 -0
- package/web/dist/assets/{workflows-BY3KTkFw.js → workflows-CTaAKz2m.js} +3 -3
- package/web/dist/assets/{zoomable-image-GTYwLtkS.js → zoomable-image-C1-6P3tg.js} +1 -1
- package/web/dist/index.html +23 -23
- package/web/dist/assets/arrow-down-CYgAlZtW.js +0 -1
- package/web/dist/assets/arrow-left-BOqE0a3k.js +0 -1
- package/web/dist/assets/chevron-right-Cc9BWnbN.js +0 -1
- package/web/dist/assets/github-CqGK62Lw.js +0 -1
- package/web/dist/assets/index-3nxVmdn5.css +0 -2
- package/web/dist/assets/index-qX7n11FX.js +0 -9
- package/web/dist/assets/refresh-cw-DcNCj2nu.js +0 -1
- package/web/dist/assets/run-header-DM7MiKj9.js +0 -1
- package/web/dist/assets/skills-Bj-uG_Hz.js +0 -1
- package/web/dist/assets/task-thread-CbMPmrWq.js +0 -9
- package/web/dist/assets/utils--z9hRtl3.js +0 -64
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { runIdParamSchema } from '../contract/index.js';
|
|
2
|
+
import { hc } from 'hono/client';
|
|
3
|
+
import { PROJECT_ID_RE } from '../workspace/config.js';
|
|
4
|
+
/**
|
|
5
|
+
* The one seam every MCP operation reaches xezar's services through (#89, epic #67).
|
|
6
|
+
*
|
|
7
|
+
* N-02 is the rule this module exists for: UI and MCP share business rules, authorization,
|
|
8
|
+
* resource limits, locks and state transitions, and MCP never writes JSON or NDJSON around the
|
|
9
|
+
* services. A-08 is how it shows: a human and a leader act on the SAME task and configuration,
|
|
10
|
+
* and no MCP-only history or configuration exists.
|
|
11
|
+
*
|
|
12
|
+
* HOW. An operation is dispatched IN-PROCESS into the service's own chained route table — the
|
|
13
|
+
* Hono app's `request()` entry, typed by `hc<AppType>` — under `/api/v1/p/<bound project>/…`.
|
|
14
|
+
* No socket, no port, no TCP: the call never leaves the process (the transport is D-01's unix
|
|
15
|
+
* socket, which lands in front of this seam, not inside it). That is the smallest shape that keeps
|
|
16
|
+
* every invariant, because several of the rules a start must obey live ONLY in route handlers
|
|
17
|
+
* today — the models-lock 409, `stepsIssue`, the provider-availability re-probe, the account
|
|
18
|
+
* check, the follow-ups capability ceiling (`POST /runs` in `server/server.ts`). Calling
|
|
19
|
+
* `RunManager.startRun` directly would copy those checks into a second place, which is the drift
|
|
20
|
+
* this seam exists to prevent. Going through the route runs the SAME validator middleware, the
|
|
21
|
+
* same 400/404/409 answers, the same `ProjectContext` (so the same `RunStore`, `RunManager`,
|
|
22
|
+
* `AutomationStore`) and therefore the same workspace-wide `WorkspaceSemaphore` as the cockpit.
|
|
23
|
+
* MCP adds no queue, no agent-process controller, no store and no runner; it opens no file.
|
|
24
|
+
*
|
|
25
|
+
* NARROWING. This is not a proxy to every route (requirements § 8 says that is insufficient):
|
|
26
|
+
* - the project is bound ONCE, at construction, from the connection (D-01/D-02: identity is a
|
|
27
|
+
* property of the connection, never of anything the client says). No operation accepts a
|
|
28
|
+
* project id, a path or a URL;
|
|
29
|
+
* - the operation table is closed — each method is one named route, and there is no raw
|
|
30
|
+
* `request(path)` escape hatch;
|
|
31
|
+
* - a path parameter is checked against `runIdParamSchema` AND refused when it is a dot segment
|
|
32
|
+
* (`.`/`..`), because the URL parser would resolve one and the call would land on a different
|
|
33
|
+
* route than the one named. A refused id is answered without any dispatch.
|
|
34
|
+
*
|
|
35
|
+
* ORIGIN. Every result carries `origin: 'mcp'`, derived from the door the call came through —
|
|
36
|
+
* never from client input, and never from a request header, which any local process could send
|
|
37
|
+
* (D-06 § 10.4 rule 1). Where it is PERSISTED is D-06's audit record (§ 10.2), a separate writer
|
|
38
|
+
* that is later work; D-06 § 12.1 adds no field to the run record. So nothing is written for it
|
|
39
|
+
* here: the record, the event log and the files an MCP operation produces are the cockpit's.
|
|
40
|
+
*
|
|
41
|
+
* NOT READ HERE. Nothing private to `RunManager` — in particular no `ActiveRun` field, which is
|
|
42
|
+
* built at two sites (`execute`, `runContinuation`). An operation sees exactly what the route
|
|
43
|
+
* sends, so there is no second construction site for this seam to keep in step with.
|
|
44
|
+
*/
|
|
45
|
+
/** The origin marker every adapter result carries (N-04). */
|
|
46
|
+
export const MCP_ORIGIN = 'mcp';
|
|
47
|
+
/** Any absolute origin works — the request never leaves the process. The `host` is what the
|
|
48
|
+
* request-origin guard (#426) checks, and an in-process call genuinely is this machine. No
|
|
49
|
+
* `Origin` header is sent, which is how every non-browser local caller already reaches the API. */
|
|
50
|
+
const IN_PROCESS_BASE = 'http://127.0.0.1';
|
|
51
|
+
const IN_PROCESS_HOST = '127.0.0.1';
|
|
52
|
+
const buildClient = (service) => hc(IN_PROCESS_BASE, {
|
|
53
|
+
fetch: async (input, init) => {
|
|
54
|
+
const headers = new Headers(init?.headers);
|
|
55
|
+
headers.set('host', IN_PROCESS_HOST);
|
|
56
|
+
headers.delete('origin');
|
|
57
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
58
|
+
return service.request(url, { ...init, headers });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
/** Thrown only by the constructor: an adapter bound to no valid project must not exist at all. */
|
|
62
|
+
export class McpServiceAdapterError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = 'McpServiceAdapterError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class McpServiceAdapter {
|
|
69
|
+
origin = MCP_ORIGIN;
|
|
70
|
+
projectId;
|
|
71
|
+
api;
|
|
72
|
+
constructor(options) {
|
|
73
|
+
const { projectId } = options;
|
|
74
|
+
// The same rule the scope resolver applies (`projectIdSchema` in server.ts), checked here so
|
|
75
|
+
// a bad binding fails at connect time instead of answering a plausible 404 on every call.
|
|
76
|
+
if (projectId !== 'default' && !PROJECT_ID_RE.test(projectId)) {
|
|
77
|
+
throw new McpServiceAdapterError(`not a project id: ${JSON.stringify(projectId)}`);
|
|
78
|
+
}
|
|
79
|
+
this.projectId = projectId;
|
|
80
|
+
this.api = buildClient(options.service).api.v1.p[':projectId'];
|
|
81
|
+
}
|
|
82
|
+
listRuns() {
|
|
83
|
+
return this.settle(this.api.runs.$get({ param: this.scope() }), [200]);
|
|
84
|
+
}
|
|
85
|
+
getRun(runId) {
|
|
86
|
+
return this.withRunId(runId, (param) => this.api.runs[':id'].$get({ param }), [200]);
|
|
87
|
+
}
|
|
88
|
+
startRun(input) {
|
|
89
|
+
return this.settle(this.api.runs.$post({ param: this.scope(), json: input }), [201]);
|
|
90
|
+
}
|
|
91
|
+
patchRun(runId, input) {
|
|
92
|
+
return this.withRunId(runId, (param) => this.api.runs[':id'].$patch({ param, json: input }), [200]);
|
|
93
|
+
}
|
|
94
|
+
/** `expectedVersion` (#250) is the stale-write guard: the route refuses the change when the run
|
|
95
|
+
* moved since that version was read. Every MCP tool that mutates a run passes one. */
|
|
96
|
+
cancelRun(runId, expectedVersion) {
|
|
97
|
+
return this.withRunId(runId, (param) => this.api.runs[':id'].cancel.$post({ param, json: guard(expectedVersion) }), [200]);
|
|
98
|
+
}
|
|
99
|
+
archiveRun(runId, archived = true, expectedVersion) {
|
|
100
|
+
return this.withRunId(runId, (param) => this.api.runs[':id'].archive.$post({ param, json: { archived, ...guard(expectedVersion) } }), [200]);
|
|
101
|
+
}
|
|
102
|
+
pinRun(runId, pinned = true, expectedVersion) {
|
|
103
|
+
return this.withRunId(runId, (param) => this.api.runs[':id'].pin.$post({ param, json: { pinned, ...guard(expectedVersion) } }), [200]);
|
|
104
|
+
}
|
|
105
|
+
scope() {
|
|
106
|
+
return { projectId: this.projectId };
|
|
107
|
+
}
|
|
108
|
+
/** Validate a run id BEFORE anything is dispatched, then call with the bound scope. */
|
|
109
|
+
withRunId(runId, call, success) {
|
|
110
|
+
const refusal = runIdRefusal(runId);
|
|
111
|
+
if (refusal)
|
|
112
|
+
return Promise.resolve({ ok: false, origin: MCP_ORIGIN, status: 400, error: refusal });
|
|
113
|
+
return this.settle(call({ ...this.scope(), id: runId }), success);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Map the service's answer onto a result. A success status WITHOUT a JSON body is an error,
|
|
117
|
+
* not an `undefined` value: "the service said nothing" and "the service said []" must never
|
|
118
|
+
* read the same to the leader.
|
|
119
|
+
*/
|
|
120
|
+
async settle(pending, success) {
|
|
121
|
+
const res = await pending;
|
|
122
|
+
const body = await res.json().catch(() => undefined);
|
|
123
|
+
if (success.includes(res.status)) {
|
|
124
|
+
if (body === undefined) {
|
|
125
|
+
return { ok: false, origin: MCP_ORIGIN, status: 502, error: `service answered ${res.status} without a body` };
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, origin: MCP_ORIGIN, status: res.status, value: body };
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, origin: MCP_ORIGIN, status: res.status, error: errorMessage(body, res.status), body };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The guard as a body fragment: present only when there is a version to send. */
|
|
133
|
+
function guard(expectedVersion) {
|
|
134
|
+
return expectedVersion === undefined ? {} : { expectedVersion };
|
|
135
|
+
}
|
|
136
|
+
function runIdRefusal(runId) {
|
|
137
|
+
if (runId === '.' || runId === '..' || !runIdParamSchema.safeParse({ id: runId }).success) {
|
|
138
|
+
return `not a run id: ${JSON.stringify(runId)}`;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function errorMessage(body, status) {
|
|
143
|
+
if (body && typeof body === 'object' && typeof body.error === 'string') {
|
|
144
|
+
return body.error;
|
|
145
|
+
}
|
|
146
|
+
return `service answered ${status}`;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=service-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-adapter.js","sourceRoot":"","sources":["../../src/mcp/service-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAiD,MAAM,aAAa,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAc,CAAC;AAOzC;;oGAEoG;AACpG,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC,MAAM,WAAW,GAAG,CAAC,OAAwB,EAAE,EAAE,CAC/C,EAAE,CAAU,eAAe,EAAE;IAC3B,KAAK,EAAE,KAAK,EAAE,KAA6B,EAAE,IAAkB,EAAqB,EAAE;QACpF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QACrC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,GAAG,GAAG,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,CAAC;CACF,CAAC,CAAC;AAyBL,kGAAkG;AAClG,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AASD,MAAM,OAAO,iBAAiB;IACnB,MAAM,GAAG,UAAU,CAAC;IACpB,SAAS,CAAS;IACV,GAAG,CAAY;IAEhC,YAAY,OAAiC;QAC3C,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QAC9B,6FAA6F;QAC7F,0FAA0F;QAC1F,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,sBAAsB,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACrF,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IACjE,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,QAAQ,CAAC,KAAsB;QAC7B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,QAAQ,CAAC,KAAa,EAAE,KAAsB;QAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtG,CAAC;IAED;2FACuF;IACvF,SAAS,CAAC,KAAa,EAAE,eAAwB;QAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7H,CAAC;IAED,UAAU,CAAC,KAAa,EAAE,QAAQ,GAAG,IAAI,EAAE,eAAwB;QACjE,OAAO,IAAI,CAAC,SAAS,CACnB,KAAK,EACL,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,eAAe,CAAC,EAAE,EAAE,CAAC,EACvG,CAAC,GAAG,CAAC,CACN,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,MAAM,GAAG,IAAI,EAAE,eAAwB;QAC3D,OAAO,IAAI,CAAC,SAAS,CACnB,KAAK,EACL,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,eAAe,CAAC,EAAE,EAAE,CAAC,EACjG,CAAC,GAAG,CAAC,CACN,CAAC;IACJ,CAAC;IAEO,KAAK;QACX,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACvC,CAAC;IAED,uFAAuF;IAC/E,SAAS,CACf,KAAa,EACb,IAAqE,EACrE,OAA0B;QAE1B,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACpG,OAAO,IAAI,CAAC,MAAM,CAAI,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACvE,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM,CAAI,OAA0B,EAAE,OAA0B;QAC5E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;QAC1B,MAAM,IAAI,GAAY,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC9D,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,oBAAoB,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC;YAChH,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,IAAS,EAAE,CAAC;QAChF,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5G,CAAC;CACF;AAED,kFAAkF;AAClF,SAAS,KAAK,CAAC,eAAmC;IAChD,OAAO,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1F,OAAO,iBAAiB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IAClD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,IAAa,EAAE,MAAc;IACjD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAQ,IAA4B,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChG,OAAQ,IAA0B,CAAC,KAAK,CAAC;IAC3C,CAAC;IACD,OAAO,oBAAoB,MAAM,EAAE,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ProjectOwnership } from '../workspace/project-owner.ts';
|
|
2
|
+
import { type McpTool, type McpToolContext, type McpToolResult } from './tool.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The service half of the IPC leg (D-01 § 1.2–1.5): the running xezar opens one
|
|
5
|
+
* Unix socket for a project and answers the bridge on it.
|
|
6
|
+
*
|
|
7
|
+
* Project binding lives HERE, not in any frame: the socket was opened for exactly
|
|
8
|
+
* one project, a request has no project field to name another, and `ctx.project`
|
|
9
|
+
* is the project this socket belongs to (§ 1.5, F-01, N-09). There is no loopback
|
|
10
|
+
* HTTP leg, so § 8's "equivalent enforcement for a loopback HTTP alternative" does
|
|
11
|
+
* not arise; the same-user boundary is the directory's 0700 and the socket's 0600
|
|
12
|
+
* (D-01 E7).
|
|
13
|
+
*
|
|
14
|
+
* ## One connection is one MCP session (#302, D-02)
|
|
15
|
+
*
|
|
16
|
+
* The bridge keeps ONE connection for the life of its `xez mcp` process. The session key is minted
|
|
17
|
+
* here, per connection — never read from a frame, so no client can name or borrow another's
|
|
18
|
+
* session. `session/open` makes that session the project's owner through `ProjectOwnership`
|
|
19
|
+
* (D-02.2) or answers project-occupied; `health` and `tools/call` need the session to still own the
|
|
20
|
+
* project, and every mutating call is fenced on its token right before it runs (D-02.3).
|
|
21
|
+
*
|
|
22
|
+
* Who ends a session, and nothing else does (D-02.4):
|
|
23
|
+
* - the connection closing — the bridge exited, was killed, or its client went away. That is the
|
|
24
|
+
* "confirmed termination" signal, observed in milliseconds (D-02 X2);
|
|
25
|
+
* - the owner's lease lapsing, which only a frozen service can cause, because the renewal timer
|
|
26
|
+
* runs in this process and needs no request and no model turn. MODEL SILENCE IS NOT SESSION
|
|
27
|
+
* DEATH: an idle connection keeps its project for as long as it stays open;
|
|
28
|
+
* - the service stopping (`close()`), which ends every session (D-02 § 5).
|
|
29
|
+
* One request finishing, failing or timing out ends nothing.
|
|
30
|
+
*
|
|
31
|
+
* Ending a session touches the owner claim and nothing else (N-05). A tool call still running
|
|
32
|
+
* when its connection closes runs to completion — its answer is simply not sent — and no path
|
|
33
|
+
* here reaches a run: a disconnect never cancels a task.
|
|
34
|
+
*/
|
|
35
|
+
export interface McpServiceOptions {
|
|
36
|
+
readonly project: McpToolContext['project'];
|
|
37
|
+
readonly version: string;
|
|
38
|
+
readonly tools: readonly McpTool[];
|
|
39
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
40
|
+
readonly platform?: NodeJS.Platform;
|
|
41
|
+
/**
|
|
42
|
+
* Extra context every tool call gets — the running app's in-process entry (`service`) in
|
|
43
|
+
* production. It can never replace `project` or `xezarVersion`: the binding is this socket's.
|
|
44
|
+
*/
|
|
45
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
46
|
+
/** The MCP door: wraps every tool call whose arguments parsed (see `startMcpService`). */
|
|
47
|
+
readonly door?: McpDoor;
|
|
48
|
+
/** Where the owner claims live (D-02.8). Defaults to the project's own `.local/xezar`. */
|
|
49
|
+
readonly dataDir?: string;
|
|
50
|
+
/** The owner slot to enforce. Absent: one is built per socket. */
|
|
51
|
+
readonly ownership?: ProjectOwnership;
|
|
52
|
+
/** Told when a session becomes the owner and when its connection closes — push delivery (#309). */
|
|
53
|
+
readonly sessions?: McpSessionObserver;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The two session edges push delivery (#309) follows: `opened` once `session/open` made the session
|
|
57
|
+
* the project's owner, `closed` when its connection closes (before the claim is released). Neither
|
|
58
|
+
* may fail a session: a throw is one warning and the session carries on (N-07).
|
|
59
|
+
*/
|
|
60
|
+
export interface McpSessionObserver {
|
|
61
|
+
opened(sessionKey: string): void;
|
|
62
|
+
closed(sessionKey: string): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Runs around one validated tool call. `invoke` is the tool itself; a throw from either reaches
|
|
66
|
+
* the same handler a tool's own throw does, so nothing secret reaches the response (F-15).
|
|
67
|
+
*/
|
|
68
|
+
export type McpDoor = (call: {
|
|
69
|
+
readonly tool: McpTool;
|
|
70
|
+
readonly args: Record<string, unknown>;
|
|
71
|
+
readonly ctx: McpToolContext;
|
|
72
|
+
}, invoke: () => Promise<McpToolResult>) => Promise<McpToolResult>;
|
|
73
|
+
export interface McpServiceHandle {
|
|
74
|
+
readonly path: string;
|
|
75
|
+
/** Stop answering and remove the socket. Synchronous, so a shutdown handler can call it. */
|
|
76
|
+
close(): void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Open the project's socket. Throws a one-line, readable error when it cannot —
|
|
80
|
+
* whether that is fatal is the CALLER's policy, and the cockpit's policy is that
|
|
81
|
+
* it never is (N-07): see `startMcpService` in ./index.ts.
|
|
82
|
+
*/
|
|
83
|
+
export declare function listenMcpSocket(opts: McpServiceOptions): Promise<McpServiceHandle>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, lstat, mkdir, unlink } from 'node:fs/promises';
|
|
3
|
+
import { createConnection, createServer } from 'node:net';
|
|
4
|
+
import { assertXezarHomeWriteIsSandboxed } from '../paths.js';
|
|
5
|
+
import { projectDataDir } from '../project-data-paths.js';
|
|
6
|
+
import { ProjectOwnership, sessionExpiredError } from '../workspace/project-owner.js';
|
|
7
|
+
import { IPC_PROTOCOL_VERSION, LineFramer, encodeFrame, ipcRequestSchema, mcpSocketDir, mcpSocketLocation, toolCallParamsSchema, } from './ipc.js';
|
|
8
|
+
import { errorResult } from './tool.js';
|
|
9
|
+
/**
|
|
10
|
+
* Open the project's socket. Throws a one-line, readable error when it cannot —
|
|
11
|
+
* whether that is fatal is the CALLER's policy, and the cockpit's policy is that
|
|
12
|
+
* it never is (N-07): see `startMcpService` in ./index.ts.
|
|
13
|
+
*/
|
|
14
|
+
export async function listenMcpSocket(opts) {
|
|
15
|
+
const location = mcpSocketLocation(opts.project, opts.env, opts.platform);
|
|
16
|
+
if (location.kind === 'unavailable')
|
|
17
|
+
throw new Error(location.reason);
|
|
18
|
+
const dir = mcpSocketDir(opts.env);
|
|
19
|
+
assertXezarHomeWriteIsSandboxed(dir, opts.env);
|
|
20
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
21
|
+
// mkdir leaves an existing directory's mode alone; this one is ours alone.
|
|
22
|
+
await chmod(dir, 0o700);
|
|
23
|
+
await clearStaleSocket(location.path);
|
|
24
|
+
// Built only once the socket path is ours: it writes nothing until a session opens.
|
|
25
|
+
const ownership = opts.ownership ??
|
|
26
|
+
new ProjectOwnership({ dataDir: opts.dataDir ?? projectDataDir(opts.project.root), projectId: opts.project.id });
|
|
27
|
+
const sockets = new Set();
|
|
28
|
+
const server = createServer((socket) => {
|
|
29
|
+
sockets.add(socket);
|
|
30
|
+
socket.on('close', () => sockets.delete(socket));
|
|
31
|
+
serveConnection(socket, opts, ownership);
|
|
32
|
+
});
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
server.once('error', reject);
|
|
35
|
+
server.listen(location.path, () => {
|
|
36
|
+
server.off('error', reject);
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
// Node creates the socket at the umask default (755 observed, D-01 E7). The 0700
|
|
41
|
+
// directory already fences it off during this gap.
|
|
42
|
+
try {
|
|
43
|
+
await chmod(location.path, 0o600);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
server.close();
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
server.on('error', (err) => console.warn(`[xez] MCP socket error: ${err.message}`));
|
|
50
|
+
return {
|
|
51
|
+
path: location.path,
|
|
52
|
+
close() {
|
|
53
|
+
for (const socket of sockets)
|
|
54
|
+
socket.destroy();
|
|
55
|
+
server.close();
|
|
56
|
+
// D-02 § 5: a service that stops ends every session, and its claim goes with it.
|
|
57
|
+
ownership.dispose();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A socket file left by a xezar that died without closing is removed; a LIVE one
|
|
63
|
+
* belongs to another cockpit serving the same project and is never stolen; any
|
|
64
|
+
* other file at that path is not ours and is left alone.
|
|
65
|
+
*/
|
|
66
|
+
async function clearStaleSocket(path) {
|
|
67
|
+
let isSocket;
|
|
68
|
+
try {
|
|
69
|
+
isSocket = (await lstat(path)).isSocket();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code === 'ENOENT')
|
|
73
|
+
return;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
if (!isSocket)
|
|
77
|
+
throw new Error(`${path} exists and is not a socket — leaving it alone`);
|
|
78
|
+
if (await socketIsLive(path)) {
|
|
79
|
+
throw new Error('another xezar is already serving this project over MCP');
|
|
80
|
+
}
|
|
81
|
+
await unlink(path);
|
|
82
|
+
}
|
|
83
|
+
function socketIsLive(path) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const probe = createConnection(path);
|
|
86
|
+
probe.once('connect', () => {
|
|
87
|
+
probe.destroy();
|
|
88
|
+
resolve(true);
|
|
89
|
+
});
|
|
90
|
+
probe.once('error', () => resolve(false));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function serveConnection(socket, opts, ownership) {
|
|
94
|
+
// Minted here, per connection. Nothing a client sends can choose it.
|
|
95
|
+
const sessionKey = randomUUID();
|
|
96
|
+
socket.on('error', () => {
|
|
97
|
+
// A bridge that vanished mid-answer is not the cockpit's problem.
|
|
98
|
+
});
|
|
99
|
+
// Confirmed termination (D-02.4 signal 1): the connection is the session, so its close frees the
|
|
100
|
+
// project at once. `release` touches the owner claim and nothing else — calls still running go
|
|
101
|
+
// on running, and no run is touched (N-05).
|
|
102
|
+
socket.once('close', () => {
|
|
103
|
+
// The session's event controller ends with its connection, before the claim goes (#309).
|
|
104
|
+
observe(opts, 'closed', sessionKey);
|
|
105
|
+
ownership.release(sessionKey);
|
|
106
|
+
});
|
|
107
|
+
const send = (response) => {
|
|
108
|
+
if (!socket.destroyed)
|
|
109
|
+
socket.write(encodeFrame(response));
|
|
110
|
+
};
|
|
111
|
+
const framer = new LineFramer((line) => {
|
|
112
|
+
void answer(line, opts, ownership, sessionKey).then(send);
|
|
113
|
+
}, () => send(failure(null, 'bad-frame', 'frame too large')));
|
|
114
|
+
socket.on('data', (chunk) => framer.push(chunk));
|
|
115
|
+
}
|
|
116
|
+
async function answer(line, opts, ownership, sessionKey) {
|
|
117
|
+
let json;
|
|
118
|
+
try {
|
|
119
|
+
json = JSON.parse(line);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return failure(null, 'bad-frame', 'frame is not JSON');
|
|
123
|
+
}
|
|
124
|
+
const parsed = ipcRequestSchema.safeParse(json);
|
|
125
|
+
if (!parsed.success)
|
|
126
|
+
return failure(null, 'bad-frame', 'frame is not a request');
|
|
127
|
+
const request = parsed.data;
|
|
128
|
+
if (request.v !== IPC_PROTOCOL_VERSION) {
|
|
129
|
+
return {
|
|
130
|
+
...failure(request.id, 'version-mismatch', `this xezar speaks bridge protocol ${IPC_PROTOCOL_VERSION}, the bridge sent ${request.v}`),
|
|
131
|
+
serviceVersion: opts.version,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const ctx = { ...opts.context, project: opts.project, xezarVersion: opts.version };
|
|
135
|
+
switch (request.method) {
|
|
136
|
+
case 'session/open': {
|
|
137
|
+
const opened = await openSession(request.id, ownership, sessionKey, opts.project.id);
|
|
138
|
+
// Push delivery starts for the owner at once — on by default, no flag (#309).
|
|
139
|
+
if (opened.ok)
|
|
140
|
+
observe(opts, 'opened', sessionKey);
|
|
141
|
+
return opened;
|
|
142
|
+
}
|
|
143
|
+
case 'health': {
|
|
144
|
+
if (ownership.sessionToken(sessionKey) === undefined)
|
|
145
|
+
return expired(request.id, opts.project.id);
|
|
146
|
+
const result = {
|
|
147
|
+
ipcVersion: IPC_PROTOCOL_VERSION,
|
|
148
|
+
xezarVersion: opts.version,
|
|
149
|
+
project: { id: opts.project.id, name: opts.project.name },
|
|
150
|
+
};
|
|
151
|
+
return { v: IPC_PROTOCOL_VERSION, id: request.id, ok: true, result };
|
|
152
|
+
}
|
|
153
|
+
case 'tools/call': {
|
|
154
|
+
// The token this request is bound to, taken as it arrives; the fence below compares it again.
|
|
155
|
+
const token = ownership.sessionToken(sessionKey);
|
|
156
|
+
if (token === undefined)
|
|
157
|
+
return expired(request.id, opts.project.id);
|
|
158
|
+
const params = toolCallParamsSchema.safeParse(request.params);
|
|
159
|
+
if (!params.success)
|
|
160
|
+
return failure(request.id, 'invalid-params', 'tools/call needs a tool name');
|
|
161
|
+
const tool = opts.tools.find((t) => t.name === params.data.name);
|
|
162
|
+
if (!tool)
|
|
163
|
+
return failure(request.id, 'unknown-tool', `unknown tool: ${params.data.name}`);
|
|
164
|
+
const outcome = await callTool(tool, params.data.arguments, ctx, opts.door, () => ownership.checkMutation(token).ok);
|
|
165
|
+
if (outcome === 'fenced')
|
|
166
|
+
return expired(request.id, opts.project.id);
|
|
167
|
+
return { v: IPC_PROTOCOL_VERSION, id: request.id, ok: true, result: outcome };
|
|
168
|
+
}
|
|
169
|
+
default:
|
|
170
|
+
return failure(request.id, 'unknown-method', `unknown method: ${request.method}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function observe(opts, edge, sessionKey) {
|
|
174
|
+
try {
|
|
175
|
+
opts.sessions?.[edge](sessionKey);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.warn(`[xez] MCP event delivery hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Make this connection's session the project's owner, or say why not (D-02.2, § 4). */
|
|
182
|
+
async function openSession(id, ownership, sessionKey, projectId) {
|
|
183
|
+
let acquired;
|
|
184
|
+
try {
|
|
185
|
+
acquired = await ownership.acquire(sessionKey);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
// The claim could not be written (a read-only data directory, a full disk). Fail closed: no
|
|
189
|
+
// session, so no call runs. The path stays in the cockpit's log, never in a response (F-15).
|
|
190
|
+
console.warn(`[xez] MCP owner claim failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
191
|
+
return failure(id, 'internal', 'xezar could not record which MCP client owns this project; the cockpit log has the details');
|
|
192
|
+
}
|
|
193
|
+
switch (acquired.outcome) {
|
|
194
|
+
case 'owner':
|
|
195
|
+
return { v: IPC_PROTOCOL_VERSION, id, ok: true, result: { owner: true } };
|
|
196
|
+
case 'occupied':
|
|
197
|
+
return { ...failure(id, 'project-occupied', acquired.error.message), rpcError: acquired.error };
|
|
198
|
+
case 'closed':
|
|
199
|
+
// The connection is gone; nobody reads this answer.
|
|
200
|
+
return expired(id, projectId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function callTool(tool, args, ctx, door, stillOwner) {
|
|
204
|
+
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
205
|
+
// An argument error is a tool result, not a protocol error, so the model can
|
|
206
|
+
// correct itself (MCP 2025-11-25, "Error Handling").
|
|
207
|
+
if (!parsed.success) {
|
|
208
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(arguments)'}: ${i.message}`);
|
|
209
|
+
return errorResult(`Invalid arguments for ${tool.name}: ${issues.join('; ')}`);
|
|
210
|
+
}
|
|
211
|
+
// The fence (D-02.3): equality with the live owner's token, immediately before anything that can
|
|
212
|
+
// change state. A read changes nothing, so it only needed the session check on arrival.
|
|
213
|
+
if (tool.annotations?.readOnlyHint !== true && !stillOwner())
|
|
214
|
+
return 'fenced';
|
|
215
|
+
try {
|
|
216
|
+
const invoke = () => tool.call(parsed.data, ctx);
|
|
217
|
+
return await (door ? door({ tool, args: parsed.data, ctx }, invoke) : invoke());
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// The exception text stays in the cockpit's own log: it can quote a command
|
|
221
|
+
// line or a file, and nothing secret may reach a tool response (F-15).
|
|
222
|
+
console.warn(`[xez] MCP tool ${tool.name} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
return errorResult(`${tool.name} failed inside xezar; the cockpit's log has the details.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function expired(id, projectId) {
|
|
227
|
+
const rpcError = sessionExpiredError(projectId);
|
|
228
|
+
return { ...failure(id, 'session-expired', rpcError.message), rpcError };
|
|
229
|
+
}
|
|
230
|
+
function failure(id, code, message) {
|
|
231
|
+
return { v: IPC_PROTOCOL_VERSION, id, ok: false, error: { code, message } };
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/mcp/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { RunRecord } from '../runs/store.ts';
|
|
2
|
+
import { type ProjectContext, type ProjectContexts } from '../server/project-context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The MCP session's project binding (#87; F-01, F-16, N-01, N-09, S-03, A-02).
|
|
5
|
+
*
|
|
6
|
+
* A session is bound to exactly ONE project, once, from a TRUSTED source: the id of
|
|
7
|
+
* the project whose socket accepted the connection (D-01 § 1.5 and § 6 — "the trusted
|
|
8
|
+
* connection binding is which socket the peer connected to"). That id is chosen by the
|
|
9
|
+
* service when it opens the socket; nothing a client sends — a tool argument, a
|
|
10
|
+
* project alias, a URL, prompt text, a resource identifier read out of a result —
|
|
11
|
+
* reaches this module as a project selector, because no method below accepts one.
|
|
12
|
+
* Tasks, files and results are data, not authority (N-09).
|
|
13
|
+
*
|
|
14
|
+
* This is deliberately NOT `resolveProjectScope` (server.ts). That resolver reads the
|
|
15
|
+
* project from the REQUEST and maps `default` to the boot project, which is right for
|
|
16
|
+
* the same-origin cockpit and wrong here (requirements § 8: derive identity from the
|
|
17
|
+
* connection, never forward an arbitrary `projectId`). So:
|
|
18
|
+
*
|
|
19
|
+
* - `default` is never a binding. It is a reserved alias, never an allocated slug
|
|
20
|
+
* (`RESERVED_PROJECT_IDS`), and "whichever project the service booted in" is not a
|
|
21
|
+
* project. A binding names the registry id itself.
|
|
22
|
+
* - There is no fallback. A bound project that is gone fails closed with its own
|
|
23
|
+
* reason; nothing here ever resolves the boot project or any other id.
|
|
24
|
+
* - The binding is re-checked on EVERY access, not just at bind time. The context map
|
|
25
|
+
* caches a built context and would keep handing it out after its folder was deleted
|
|
26
|
+
* or replaced, so each access re-resolves through `ProjectContexts.context(boundId)`
|
|
27
|
+
* and then verifies the root is still the very directory the session was bound to
|
|
28
|
+
* (same realpath, same device and inode). A folder deleted, moved away, swapped for a
|
|
29
|
+
* symlink or recreated at the same path is `missing-root`, never "carry on".
|
|
30
|
+
*
|
|
31
|
+
* Rejections disclose nothing (N-01). Every error message below is a fixed sentence:
|
|
32
|
+
* it never echoes a client-supplied identifier (which could be another project's id or
|
|
33
|
+
* run id), and never names the bound project's path. `projectId` on the error object
|
|
34
|
+
* is only ever the BOUND project's id, which the leader already knows. This module
|
|
35
|
+
* writes no log line.
|
|
36
|
+
*
|
|
37
|
+
* Scope: binding and scope enforcement only. Occupancy, leases and fencing are D-02
|
|
38
|
+
* (#99); the socket is D-01 (#86); the operations themselves go through the shared
|
|
39
|
+
* service adapter (#89). Releasing or losing a binding touches nothing on
|
|
40
|
+
* `RunManager` (N-05).
|
|
41
|
+
*/
|
|
42
|
+
/** Why an MCP session cannot act. `unknown-project` / `missing-root` keep the meaning
|
|
43
|
+
* `ProjectContextError` gives them (404 / 409); `unavailable` is a bound project
|
|
44
|
+
* that could not be opened for any other reason (another process owns its data, a
|
|
45
|
+
* filesystem error) — its cause stays server-side; `not-in-project` is a resource this session
|
|
46
|
+
* cannot see, whether it belongs to another project or to none. */
|
|
47
|
+
export type McpScopeFailure = 'unknown-project' | 'missing-root' | 'unavailable' | 'not-in-project';
|
|
48
|
+
/** The one error this module throws. Carries no client-supplied value. */
|
|
49
|
+
export declare class McpScopeError extends Error {
|
|
50
|
+
readonly reason: McpScopeFailure;
|
|
51
|
+
/** The BOUND project's id — never a foreign or client-supplied one. */
|
|
52
|
+
readonly projectId: string;
|
|
53
|
+
constructor(reason: McpScopeFailure,
|
|
54
|
+
/** The BOUND project's id — never a foreign or client-supplied one. */
|
|
55
|
+
projectId: string,
|
|
56
|
+
/** The underlying failure, for the service's own log only — never sent to a client. */
|
|
57
|
+
cause?: unknown);
|
|
58
|
+
}
|
|
59
|
+
/** What the binding needs from the context map — `ProjectContexts` in production. */
|
|
60
|
+
export type McpProjectContextSource = Pick<ProjectContexts, 'context'>;
|
|
61
|
+
/**
|
|
62
|
+
* The live binding a connection holds. Construct only through `bindMcpSession`.
|
|
63
|
+
* Every read and write an MCP operation makes goes through `project()`, `run()` or
|
|
64
|
+
* `withRun()`; there is no other door to a `ProjectContext` for it.
|
|
65
|
+
*/
|
|
66
|
+
export declare class McpSessionBinding {
|
|
67
|
+
#private;
|
|
68
|
+
private constructor();
|
|
69
|
+
/** The bound registry id. Fixed for the life of the session; there is no setter. */
|
|
70
|
+
get projectId(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Bind a session to `trustedProjectId` — the registry id of the project whose socket
|
|
73
|
+
* accepted this connection, as the SERVICE knows it. Never pass anything read from a
|
|
74
|
+
* client frame, a tool argument, a path or a URL here: that value is untrusted by
|
|
75
|
+
* definition, and binding to it is exactly the escape F-01 forbids.
|
|
76
|
+
*
|
|
77
|
+
* Fails closed with `McpScopeError`: `default` and every other reserved alias, a
|
|
78
|
+
* malformed id, an unregistered project and a missing root are all refused — there
|
|
79
|
+
* is no fallback to the boot project. The constructor is private, so this is the
|
|
80
|
+
* only way a binding comes into existence.
|
|
81
|
+
*/
|
|
82
|
+
static bind(contexts: McpProjectContextSource, trustedProjectId: string): Promise<McpSessionBinding>;
|
|
83
|
+
/**
|
|
84
|
+
* The bound project's context, re-validated on every call. Throws `McpScopeError`
|
|
85
|
+
* when the project is no longer registered, its folder is gone or replaced, or
|
|
86
|
+
* another process owns its data. Never returns any other project's context.
|
|
87
|
+
*/
|
|
88
|
+
project(): Promise<ProjectContext>;
|
|
89
|
+
/**
|
|
90
|
+
* One task of the BOUND project, by id. The id is client data: it is shape-checked
|
|
91
|
+
* against the contract's run-id rule and looked up in the bound project's own store
|
|
92
|
+
* only, so another project's id — or any string naming a project, alias, path or
|
|
93
|
+
* URL — reads as `not-in-project`, identically to an id that exists nowhere. The
|
|
94
|
+
* error never repeats the id.
|
|
95
|
+
*/
|
|
96
|
+
run(runId: unknown): Promise<RunRecord>;
|
|
97
|
+
/**
|
|
98
|
+
* Gate for an operation on one task: resolves the task in the bound project first
|
|
99
|
+
* and only then runs `operation` with the bound context and that task. A foreign or
|
|
100
|
+
* unknown id is refused before `operation` is called, so it causes no side effect.
|
|
101
|
+
*/
|
|
102
|
+
withRun<T>(runId: unknown, operation: (run: RunRecord, ctx: ProjectContext) => T | Promise<T>): Promise<T>;
|
|
103
|
+
}
|
|
104
|
+
/** Bind a session — see `McpSessionBinding.bind`. */
|
|
105
|
+
export declare function bindMcpSession(contexts: McpProjectContextSource, trustedProjectId: string): Promise<McpSessionBinding>;
|