@qodeca/xezar 0.13.1 → 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 +45 -15
- 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 +24 -1
- package/dist/runs/retention.js +62 -1
- 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 +554 -59
- 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-D62m4j3B.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-DJANqg7Y.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-btG0sdbO.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-BtZId14z.js → task-changes-CH-E81-_.js} +1 -1
- package/web/dist/assets/{task-commits-BoeyJXrv.js → task-commits-DqfkMFxp.js} +1 -1
- package/web/dist/assets/{task-files-DIy2oUnj.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-CPBwzQ7M.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-DG115scU.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-D6vK0jFn.js +0 -1
- package/web/dist/assets/index-CkmR6h7s.css +0 -2
- package/web/dist/assets/index-DVDqF4bM.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-DMfxQtrD.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,163 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { runIdParamSchema } from '../contract/index.js';
|
|
3
|
+
import { ProjectContextError, } from '../server/project-context.js';
|
|
4
|
+
import { PROJECT_ID_RE } from '../workspace/config.js';
|
|
5
|
+
import { RESERVED_PROJECT_IDS } from '../workspace/projects.js';
|
|
6
|
+
/** Fixed, identifier-free sentences — the rejection text is part of the surface (N-01). */
|
|
7
|
+
const MESSAGES = {
|
|
8
|
+
'unknown-project': 'this MCP session is not bound to a registered xezar project; reconnect from the project folder',
|
|
9
|
+
'missing-root': 'the project folder this MCP session is bound to is missing; restore it or reconnect from the project folder',
|
|
10
|
+
unavailable: 'the project this MCP session is bound to cannot be opened right now; the cockpit log has the details',
|
|
11
|
+
'not-in-project': 'no such resource in this project',
|
|
12
|
+
};
|
|
13
|
+
/** The one error this module throws. Carries no client-supplied value. */
|
|
14
|
+
export class McpScopeError extends Error {
|
|
15
|
+
reason;
|
|
16
|
+
projectId;
|
|
17
|
+
constructor(reason,
|
|
18
|
+
/** The BOUND project's id — never a foreign or client-supplied one. */
|
|
19
|
+
projectId,
|
|
20
|
+
/** The underlying failure, for the service's own log only — never sent to a client. */
|
|
21
|
+
cause) {
|
|
22
|
+
super(MESSAGES[reason], cause === undefined ? undefined : { cause });
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
this.projectId = projectId;
|
|
25
|
+
this.name = 'McpScopeError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The live binding a connection holds. Construct only through `bindMcpSession`.
|
|
30
|
+
* Every read and write an MCP operation makes goes through `project()`, `run()` or
|
|
31
|
+
* `withRun()`; there is no other door to a `ProjectContext` for it.
|
|
32
|
+
*/
|
|
33
|
+
export class McpSessionBinding {
|
|
34
|
+
// Runtime-private (`#`), not TypeScript `readonly`: a `readonly` property is still
|
|
35
|
+
// writable at runtime, and the binding must not be re-pointable by any code holding it.
|
|
36
|
+
#projectId;
|
|
37
|
+
#contexts;
|
|
38
|
+
#root;
|
|
39
|
+
constructor(projectId, contexts, root) {
|
|
40
|
+
this.#projectId = projectId;
|
|
41
|
+
this.#contexts = contexts;
|
|
42
|
+
this.#root = root;
|
|
43
|
+
}
|
|
44
|
+
/** The bound registry id. Fixed for the life of the session; there is no setter. */
|
|
45
|
+
get projectId() {
|
|
46
|
+
return this.#projectId;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Bind a session to `trustedProjectId` — the registry id of the project whose socket
|
|
50
|
+
* accepted this connection, as the SERVICE knows it. Never pass anything read from a
|
|
51
|
+
* client frame, a tool argument, a path or a URL here: that value is untrusted by
|
|
52
|
+
* definition, and binding to it is exactly the escape F-01 forbids.
|
|
53
|
+
*
|
|
54
|
+
* Fails closed with `McpScopeError`: `default` and every other reserved alias, a
|
|
55
|
+
* malformed id, an unregistered project and a missing root are all refused — there
|
|
56
|
+
* is no fallback to the boot project. The constructor is private, so this is the
|
|
57
|
+
* only way a binding comes into existence.
|
|
58
|
+
*/
|
|
59
|
+
static async bind(contexts, trustedProjectId) {
|
|
60
|
+
if (typeof trustedProjectId !== 'string' ||
|
|
61
|
+
RESERVED_PROJECT_IDS.has(trustedProjectId) ||
|
|
62
|
+
!PROJECT_ID_RE.test(trustedProjectId)) {
|
|
63
|
+
// Not echoed: a malformed binding id is a caller bug, and the message stays fixed.
|
|
64
|
+
throw new McpScopeError('unknown-project', '');
|
|
65
|
+
}
|
|
66
|
+
const ctx = await resolveContext(contexts, trustedProjectId);
|
|
67
|
+
if (ctx.id !== trustedProjectId)
|
|
68
|
+
throw new McpScopeError('unknown-project', trustedProjectId);
|
|
69
|
+
const root = rootIdentity(ctx.root);
|
|
70
|
+
// The registry stores realpath'd roots. A root that no longer IS its own realpath
|
|
71
|
+
// was swapped for a symlink (or moved) after registration; pinning what it points
|
|
72
|
+
// at now would bind the session to whatever the link names — possibly another
|
|
73
|
+
// project's folder. Refuse instead of pinning.
|
|
74
|
+
if (!root || root.realpath !== ctx.root)
|
|
75
|
+
throw new McpScopeError('missing-root', trustedProjectId);
|
|
76
|
+
return new McpSessionBinding(trustedProjectId, contexts, root);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The bound project's context, re-validated on every call. Throws `McpScopeError`
|
|
80
|
+
* when the project is no longer registered, its folder is gone or replaced, or
|
|
81
|
+
* another process owns its data. Never returns any other project's context.
|
|
82
|
+
*/
|
|
83
|
+
async project() {
|
|
84
|
+
const ctx = await resolveContext(this.#contexts, this.#projectId);
|
|
85
|
+
// Defence in depth: the map is keyed by id, so this holds today. A context whose
|
|
86
|
+
// id is not the bound id must never be handed to an operation, whatever changes.
|
|
87
|
+
if (ctx.id !== this.#projectId)
|
|
88
|
+
throw new McpScopeError('unknown-project', this.#projectId);
|
|
89
|
+
const current = rootIdentity(ctx.root);
|
|
90
|
+
if (!current ||
|
|
91
|
+
current.realpath !== this.#root.realpath ||
|
|
92
|
+
current.dev !== this.#root.dev ||
|
|
93
|
+
current.ino !== this.#root.ino) {
|
|
94
|
+
throw new McpScopeError('missing-root', this.#projectId);
|
|
95
|
+
}
|
|
96
|
+
return ctx;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One task of the BOUND project, by id. The id is client data: it is shape-checked
|
|
100
|
+
* against the contract's run-id rule and looked up in the bound project's own store
|
|
101
|
+
* only, so another project's id — or any string naming a project, alias, path or
|
|
102
|
+
* URL — reads as `not-in-project`, identically to an id that exists nowhere. The
|
|
103
|
+
* error never repeats the id.
|
|
104
|
+
*/
|
|
105
|
+
async run(runId) {
|
|
106
|
+
const ctx = await this.project();
|
|
107
|
+
return lookupRun(ctx, runId, this.#projectId);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Gate for an operation on one task: resolves the task in the bound project first
|
|
111
|
+
* and only then runs `operation` with the bound context and that task. A foreign or
|
|
112
|
+
* unknown id is refused before `operation` is called, so it causes no side effect.
|
|
113
|
+
*/
|
|
114
|
+
async withRun(runId, operation) {
|
|
115
|
+
const ctx = await this.project();
|
|
116
|
+
const run = lookupRun(ctx, runId, this.#projectId);
|
|
117
|
+
return operation(run, ctx);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Bind a session — see `McpSessionBinding.bind`. */
|
|
121
|
+
export function bindMcpSession(contexts, trustedProjectId) {
|
|
122
|
+
return McpSessionBinding.bind(contexts, trustedProjectId);
|
|
123
|
+
}
|
|
124
|
+
/** Resolve the bound id through the context map, translating its failures. Only ever
|
|
125
|
+
* called with the bound id — the one place a `ProjectContexts` lookup happens. */
|
|
126
|
+
async function resolveContext(contexts, projectId) {
|
|
127
|
+
try {
|
|
128
|
+
return await contexts.context(projectId);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err instanceof ProjectContextError)
|
|
132
|
+
throw new McpScopeError(err.reason, projectId, err);
|
|
133
|
+
// Anything else — the writer error, or a raw fs error from building the context —
|
|
134
|
+
// can carry a data directory, a path or a pid in its message; none may reach a
|
|
135
|
+
// client. The original stays on `cause` for the service's own log.
|
|
136
|
+
throw new McpScopeError('unavailable', projectId, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function lookupRun(ctx, runId, projectId) {
|
|
140
|
+
// Dot segments pass the character class but are never run ids; refuse them here so
|
|
141
|
+
// no caller can build a path or URL out of one.
|
|
142
|
+
if (typeof runId !== 'string' || runId === '.' || runId === '..' || !runIdParamSchema.safeParse({ id: runId }).success) {
|
|
143
|
+
throw new McpScopeError('not-in-project', projectId);
|
|
144
|
+
}
|
|
145
|
+
const run = ctx.store.getRun(runId);
|
|
146
|
+
if (!run)
|
|
147
|
+
throw new McpScopeError('not-in-project', projectId);
|
|
148
|
+
return run;
|
|
149
|
+
}
|
|
150
|
+
/** Realpath + device/inode of a directory, or null when it is gone or not a directory. */
|
|
151
|
+
function rootIdentity(root) {
|
|
152
|
+
try {
|
|
153
|
+
const realpath = realpathSync(root);
|
|
154
|
+
const stats = statSync(realpath);
|
|
155
|
+
if (!stats.isDirectory())
|
|
156
|
+
return null;
|
|
157
|
+
return { realpath, dev: stats.dev, ino: stats.ino };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=session-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-binding.js","sourceRoot":"","sources":["../../src/mcp/session-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type ExecutionFailed, type McpVersionedResourceRef, type StaleVersionRejection, type VersionedMutationDone } from '../contract/index.js';
|
|
2
|
+
import type { RunRecord, RunStore } from '../runs/store.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Stale-write rejection for leader mutations (#100) — the mechanism D-06 chose for N-03
|
|
5
|
+
* (`docs/features/mcp-server/mcp-d06-versioning-idempotency-audit-decision.md` § 4). N-03 is an
|
|
6
|
+
* agreed outcome and nothing here makes it optional: a leader mutation based on state a human has
|
|
7
|
+
* changed since is REFUSED, before any effect. It is never merged, never retried with the fresh
|
|
8
|
+
* token, and never offered to anyone for approval (§ 4.4 rule 3). There is no switch that turns
|
|
9
|
+
* the check off, and there must never be one.
|
|
10
|
+
*
|
|
11
|
+
* Two pieces, deliberately tool-agnostic — this is infrastructure a tool calls, not a tool:
|
|
12
|
+
*
|
|
13
|
+
* - `versionToken` computes `rev1:<kind>:<id>:<seq>:<digest12>` (§ 4.2) from a resource's
|
|
14
|
+
* DECISION PROJECTION (§ 4.3): the fields the server reads to judge a mutation plus the fields
|
|
15
|
+
* it writes, and nothing that moves on its own. A token that included a token counter would go
|
|
16
|
+
* stale within seconds of an agent emitting anything, and a check that always fails is an outage
|
|
17
|
+
* people learn to switch off.
|
|
18
|
+
* - `guardedMutation` is the compare-and-swap (§ 4.5). It reads the current token, compares, and
|
|
19
|
+
* applies — all in ONE synchronous stretch. Node runs one stretch of synchronous code at a time,
|
|
20
|
+
* and `ownProjectData` (`runs/project-writer.ts`) keeps a project's state to one writing process,
|
|
21
|
+
* so no other write can land between the check and the effect. That is also why `apply` must
|
|
22
|
+
* not be async: an `await` inside it would open exactly the gap the check exists to close.
|
|
23
|
+
*
|
|
24
|
+
* N-02: the effect `apply` performs is the SAME store call the cockpit's route makes — this module
|
|
25
|
+
* wraps the shared service, it never writes JSON or NDJSON itself. The cockpit keeps calling those
|
|
26
|
+
* methods exactly as it does today — requiring a token there would break every existing client —
|
|
27
|
+
* and the leader's write reaches the very same methods, behind this check.
|
|
28
|
+
*
|
|
29
|
+
* U-M05: the result tells the three endings apart — `done`, `conflict` (rejected, NOTHING applied)
|
|
30
|
+
* and `failed` (the effect ran and failed, so the state may have moved). See
|
|
31
|
+
* `packages/contract/src/mcp-versioning.ts` for the shapes.
|
|
32
|
+
*/
|
|
33
|
+
/** One resource, as the check sees it. Build it from live state on every call — never cache one. */
|
|
34
|
+
export interface VersionedSnapshot {
|
|
35
|
+
readonly ref: McpVersionedResourceRef;
|
|
36
|
+
/**
|
|
37
|
+
* For a run-scoped resource, its highest event `seq`; absent for a resource with no event stream
|
|
38
|
+
* (a config file, an automation). This half catches A→B→A — a human who changes something and
|
|
39
|
+
* changes it back leaves the projection byte-identical, but not the event counter, because `seq`
|
|
40
|
+
* is monotonic and never reused, even across a restart (`RunStore.rehydrateSeq`).
|
|
41
|
+
*/
|
|
42
|
+
readonly seq?: number;
|
|
43
|
+
/** The decision projection. Canonicalized and digested; its content never leaves this module. */
|
|
44
|
+
readonly projection: unknown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* JSON with object keys sorted by UTF-16 code unit at every level, arrays kept in order, no
|
|
48
|
+
* whitespace, and `undefined`-valued keys dropped — D-06 § 5.4 step 4, so a token does not depend
|
|
49
|
+
* on the order in which a record's keys happened to be assigned.
|
|
50
|
+
*
|
|
51
|
+
* Serialized by hand rather than by rebuilding an object for `JSON.stringify`: an engine iterates
|
|
52
|
+
* integer-like keys (`"10"`, `"9"`) numerically whatever order they were inserted in, which is not
|
|
53
|
+
* code-unit order.
|
|
54
|
+
*/
|
|
55
|
+
export declare function canonicalJson(value: unknown): string;
|
|
56
|
+
/** The token a read hands out and a mutation must echo back. Opaque to every client. */
|
|
57
|
+
export declare function versionToken(snapshot: VersionedSnapshot): string;
|
|
58
|
+
/** What `guardedMutation` answers when the change went through: the contract shape plus the value
|
|
59
|
+
* `apply` returned, for the calling tool to project onto its own result. */
|
|
60
|
+
export type GuardedMutationDone<T> = VersionedMutationDone & {
|
|
61
|
+
readonly value: T;
|
|
62
|
+
};
|
|
63
|
+
export type GuardedMutationResult<T> = GuardedMutationDone<T> | StaleVersionRejection | ExecutionFailed;
|
|
64
|
+
/**
|
|
65
|
+
* `apply` has to finish inside the critical section, so an async one is a compile error — this
|
|
66
|
+
* rest parameter demands an impossible extra argument when `apply` returns a promise — and, for a
|
|
67
|
+
* caller that got past the types, a `failed` result at run time.
|
|
68
|
+
*/
|
|
69
|
+
type SyncOnly<T> = [T] extends [never] ? [] : [T] extends [PromiseLike<unknown>] ? [asyncApplyIsNotAllowed: never] : [];
|
|
70
|
+
export interface GuardedMutationInput<T> {
|
|
71
|
+
readonly resource: McpVersionedResourceRef;
|
|
72
|
+
/**
|
|
73
|
+
* The token the leader read. A missing one is REFUSED, never waved through: "no token" and "token
|
|
74
|
+
* matches" must not share a branch (D-06 § 4.4 rule 4). The tool schema already requires it; this
|
|
75
|
+
* is the same guarantee for a caller that bypassed the schema.
|
|
76
|
+
*/
|
|
77
|
+
readonly expectedVersion: string | undefined;
|
|
78
|
+
/** Reads the resource as it is NOW; `undefined` when it no longer exists. */
|
|
79
|
+
readonly read: () => VersionedSnapshot | undefined;
|
|
80
|
+
/** The effect: the shared store call. Synchronous — see `SyncOnly`. */
|
|
81
|
+
readonly apply: () => T;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check the leader's token against the resource's current one and apply the effect only on a match.
|
|
85
|
+
*
|
|
86
|
+
* Never throws for a stale token or a failed effect — both are ANSWERS the leader must reason
|
|
87
|
+
* about, which is why D-05 returns them as ordinary results rather than tool errors.
|
|
88
|
+
*/
|
|
89
|
+
export declare function guardedMutation<T>(input: GuardedMutationInput<T>, ..._sync: SyncOnly<T>): GuardedMutationResult<T>;
|
|
90
|
+
export declare const RUN_RESOURCE_KIND = "run";
|
|
91
|
+
/**
|
|
92
|
+
* A run's decision projection (D-06 § 4.3).
|
|
93
|
+
*
|
|
94
|
+
* D-06 proposed `status`, `archived`, `pinned`, `title`, `titleOrigin`, `autoResumeAt`,
|
|
95
|
+
* `queuedMessages[].id`, `steps[].id`/`steps[].status`, `branch` and `workflow`, and left the list
|
|
96
|
+
* to be reviewed against `runRecordSchema`. Reviewed against it, two additions follow from the
|
|
97
|
+
* rule itself — every field a leader mutation of a run WRITES:
|
|
98
|
+
*
|
|
99
|
+
* - `task`: a queued run's brief stays editable until the scheduler picks it up, and a human
|
|
100
|
+
* editing it is the "edits its brief" case § 4.1 names;
|
|
101
|
+
* - `queuedMessages[].text`, not only the id: a queued follow-up is editable in the same way.
|
|
102
|
+
*
|
|
103
|
+
* Left out on purpose: telemetry (`tokensUsed`, `inputTokens`, `outputTokens`, `costUsd`,
|
|
104
|
+
* `peakRssBytes`, `peakProcCount`, `diffStat`, the per-step counters), presentation (`seenAt`, and
|
|
105
|
+
* the `archivedAt`/`pinnedAt` stamps whose flags ARE covered), and everything derived by the server
|
|
106
|
+
* for display (`titleSummary`, the referenced PR/issue tiers).
|
|
107
|
+
*/
|
|
108
|
+
export declare function runDecisionProjection(run: RunRecord): unknown;
|
|
109
|
+
/** A run as the check sees it right now, or `undefined` when there is no such run. */
|
|
110
|
+
export declare function runVersionSnapshot(store: RunStore, runId: string): VersionedSnapshot | undefined;
|
|
111
|
+
/** The `version` a read of this run hands the leader, or `undefined` when there is no such run. */
|
|
112
|
+
export declare function runVersion(store: RunStore, runId: string): string | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* The route half of the check (#250): the rejection a run-mutating route answers with, or
|
|
115
|
+
* `undefined` when its effect may go ahead.
|
|
116
|
+
*
|
|
117
|
+
* WHY THE ROUTE, NOT THE TOOL. An MCP tool reaches the services through in-process HTTP dispatch
|
|
118
|
+
* (N-02, `service-adapter.ts`), which is async, so a tool cannot hold `guardedMutation`'s
|
|
119
|
+
* synchronous critical section around the store call — only the route handler can. So the route
|
|
120
|
+
* accepts an OPTIONAL `expectedVersion` and calls this RIGHT BEFORE its effect, with nothing but
|
|
121
|
+
* synchronous code between the two: the check and the store call then run in one stretch, exactly
|
|
122
|
+
* as `guardedMutation` would run them (§ 4.5). A handler must never put an `await` between this
|
|
123
|
+
* call and its effect; each call site says so.
|
|
124
|
+
*
|
|
125
|
+
* ABSENT MEANS "NOT ASKED", ON PURPOSE. `undefined` is the cockpit, which has never sent a token and
|
|
126
|
+
* keeps its behaviour byte-for-byte. The leader can never reach that branch: every MCP tool that
|
|
127
|
+
* mutates a run REQUIRES `expectedVersion` in its own schema (D-06 § 4.4 rule 4), and the tool
|
|
128
|
+
* tests pin that each action sends it. Present-but-wrong — empty, foreign, unknown tag, another
|
|
129
|
+
* run's — is always a rejection, never a pass.
|
|
130
|
+
*
|
|
131
|
+
* AN ASYNC EFFECT (a worktree removal, a push, `gh pr create`) cannot sit inside a synchronous
|
|
132
|
+
* stretch at all. Those routes call this right before the effect STARTS, so a stale decision never
|
|
133
|
+
* begins; a change that lands while the effect is already running is outside what a version check
|
|
134
|
+
* can see, and each such effect keeps its own guards (`isActive`, the worktree checks).
|
|
135
|
+
*/
|
|
136
|
+
export declare function staleRunWrite(store: RunStore, runId: string, expectedVersion: string | undefined): StaleVersionRejection | undefined;
|
|
137
|
+
/** The rejection a route answered with, when the body is one — how a tool tells a stale-version
|
|
138
|
+
* 409 from every other 409 (`run already started`, `session closed`, …). */
|
|
139
|
+
export declare function staleRejectionIn(body: unknown): StaleVersionRejection | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* `guardedMutation` for one run: `apply` receives the CURRENT record and performs the shared store
|
|
142
|
+
* call on it (`setPinned`, `updateRun`, …). It is never called when the token is stale.
|
|
143
|
+
*/
|
|
144
|
+
export declare function guardedRunMutation<T>(store: RunStore, runId: string, expectedVersion: string | undefined, apply: (run: RunRecord) => T, ..._sync: SyncOnly<T>): GuardedMutationResult<T>;
|
|
145
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MCP_EXECUTION_FAILED_GUIDANCE, MCP_STALE_VERSION_GUIDANCE, MCP_VERSION_TOKEN_TAG, staleVersionRejectionSchema, } from '../contract/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* JSON with object keys sorted by UTF-16 code unit at every level, arrays kept in order, no
|
|
5
|
+
* whitespace, and `undefined`-valued keys dropped — D-06 § 5.4 step 4, so a token does not depend
|
|
6
|
+
* on the order in which a record's keys happened to be assigned.
|
|
7
|
+
*
|
|
8
|
+
* Serialized by hand rather than by rebuilding an object for `JSON.stringify`: an engine iterates
|
|
9
|
+
* integer-like keys (`"10"`, `"9"`) numerically whatever order they were inserted in, which is not
|
|
10
|
+
* code-unit order.
|
|
11
|
+
*/
|
|
12
|
+
export function canonicalJson(value) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`;
|
|
15
|
+
}
|
|
16
|
+
if (value !== null && typeof value === 'object') {
|
|
17
|
+
const parts = [];
|
|
18
|
+
for (const key of Object.keys(value).sort()) {
|
|
19
|
+
const item = value[key];
|
|
20
|
+
if (item !== undefined)
|
|
21
|
+
parts.push(`${JSON.stringify(key)}:${canonicalJson(item)}`);
|
|
22
|
+
}
|
|
23
|
+
return `{${parts.join(',')}}`;
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(value) ?? 'null';
|
|
26
|
+
}
|
|
27
|
+
/** The token a read hands out and a mutation must echo back. Opaque to every client. */
|
|
28
|
+
export function versionToken(snapshot) {
|
|
29
|
+
const digest = createHash('sha256').update(canonicalJson(snapshot.projection)).digest('hex').slice(0, 12);
|
|
30
|
+
const seq = snapshot.seq === undefined ? '-' : String(snapshot.seq);
|
|
31
|
+
return `${MCP_VERSION_TOKEN_TAG}:${snapshot.ref.kind}:${snapshot.ref.id}:${seq}:${digest}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check the leader's token against the resource's current one and apply the effect only on a match.
|
|
35
|
+
*
|
|
36
|
+
* Never throws for a stale token or a failed effect — both are ANSWERS the leader must reason
|
|
37
|
+
* about, which is why D-05 returns them as ordinary results rather than tool errors.
|
|
38
|
+
*/
|
|
39
|
+
export function guardedMutation(input, ..._sync) {
|
|
40
|
+
const before = input.read();
|
|
41
|
+
const currentVersion = before ? versionToken(before) : undefined;
|
|
42
|
+
// Verbatim comparison is the whole rule, and it covers D-06 § 4.4 rule 5 without parsing: an
|
|
43
|
+
// unparseable, foreign or unknown-tag token can never equal a token this server just minted.
|
|
44
|
+
if (!input.expectedVersion || currentVersion === undefined || input.expectedVersion !== currentVersion) {
|
|
45
|
+
return staleVersionRejection(input.resource, currentVersion);
|
|
46
|
+
}
|
|
47
|
+
let value;
|
|
48
|
+
try {
|
|
49
|
+
value = input.apply();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return executionFailed(input.resource, error);
|
|
53
|
+
}
|
|
54
|
+
if (isThenable(value)) {
|
|
55
|
+
// The effect escaped the critical section. Swallow a later rejection so it cannot crash the
|
|
56
|
+
// process as an unhandled one, and report what is actually known: it ran, and it is not done.
|
|
57
|
+
value.then(undefined, () => undefined);
|
|
58
|
+
return executionFailed(input.resource, new Error('apply returned a promise; a guarded mutation must be synchronous'));
|
|
59
|
+
}
|
|
60
|
+
const after = input.read();
|
|
61
|
+
return {
|
|
62
|
+
status: 'done',
|
|
63
|
+
resource: input.resource,
|
|
64
|
+
...(after ? { version: versionToken(after) } : {}),
|
|
65
|
+
value,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function staleVersionRejection(resource, currentVersion) {
|
|
69
|
+
return {
|
|
70
|
+
status: 'conflict',
|
|
71
|
+
applied: false,
|
|
72
|
+
error: 'stale_version',
|
|
73
|
+
resource,
|
|
74
|
+
// Spread, not `currentVersion: undefined` — a key that is always present in the type but
|
|
75
|
+
// dropped by `JSON.stringify` is exactly the drift the contract-parity rule warns about.
|
|
76
|
+
...(currentVersion === undefined ? {} : { currentVersion }),
|
|
77
|
+
changedSince: true,
|
|
78
|
+
guidance: MCP_STALE_VERSION_GUIDANCE,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function executionFailed(resource, error) {
|
|
82
|
+
// The message stays in the cockpit's own log: it can quote a path or a command line, and nothing
|
|
83
|
+
// secret may reach a tool response (F-15).
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
console.warn(`[xez] MCP mutation of ${resource.kind} ${resource.id} failed after execution: ${message}`);
|
|
86
|
+
return {
|
|
87
|
+
status: 'failed',
|
|
88
|
+
executed: true,
|
|
89
|
+
error: 'execution_failed',
|
|
90
|
+
resource,
|
|
91
|
+
guidance: MCP_EXECUTION_FAILED_GUIDANCE,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isThenable(value) {
|
|
95
|
+
return (value !== null &&
|
|
96
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
97
|
+
typeof value.then === 'function');
|
|
98
|
+
}
|
|
99
|
+
// ---- runs -------------------------------------------------------------------------------------
|
|
100
|
+
export const RUN_RESOURCE_KIND = 'run';
|
|
101
|
+
/**
|
|
102
|
+
* A run's decision projection (D-06 § 4.3).
|
|
103
|
+
*
|
|
104
|
+
* D-06 proposed `status`, `archived`, `pinned`, `title`, `titleOrigin`, `autoResumeAt`,
|
|
105
|
+
* `queuedMessages[].id`, `steps[].id`/`steps[].status`, `branch` and `workflow`, and left the list
|
|
106
|
+
* to be reviewed against `runRecordSchema`. Reviewed against it, two additions follow from the
|
|
107
|
+
* rule itself — every field a leader mutation of a run WRITES:
|
|
108
|
+
*
|
|
109
|
+
* - `task`: a queued run's brief stays editable until the scheduler picks it up, and a human
|
|
110
|
+
* editing it is the "edits its brief" case § 4.1 names;
|
|
111
|
+
* - `queuedMessages[].text`, not only the id: a queued follow-up is editable in the same way.
|
|
112
|
+
*
|
|
113
|
+
* Left out on purpose: telemetry (`tokensUsed`, `inputTokens`, `outputTokens`, `costUsd`,
|
|
114
|
+
* `peakRssBytes`, `peakProcCount`, `diffStat`, the per-step counters), presentation (`seenAt`, and
|
|
115
|
+
* the `archivedAt`/`pinnedAt` stamps whose flags ARE covered), and everything derived by the server
|
|
116
|
+
* for display (`titleSummary`, the referenced PR/issue tiers).
|
|
117
|
+
*/
|
|
118
|
+
export function runDecisionProjection(run) {
|
|
119
|
+
return {
|
|
120
|
+
status: run.status,
|
|
121
|
+
archived: run.archived,
|
|
122
|
+
pinned: run.pinned,
|
|
123
|
+
title: run.title,
|
|
124
|
+
titleOrigin: run.titleOrigin,
|
|
125
|
+
autoResumeAt: run.autoResumeAt,
|
|
126
|
+
task: run.task,
|
|
127
|
+
queuedMessages: run.queuedMessages?.map((message) => ({ id: message.id, text: message.text })),
|
|
128
|
+
steps: run.steps.map((step) => ({ id: step.id, status: step.status })),
|
|
129
|
+
branch: run.branch,
|
|
130
|
+
workflow: run.workflow,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The run's highest event `seq` on disk.
|
|
135
|
+
*
|
|
136
|
+
* D-06 names the highest ALLOCATED seq. The allocation counter is private to the store, so this
|
|
137
|
+
* reads the persisted one: it moves on every persisted event and never goes backwards, which is the
|
|
138
|
+
* property the token needs. The only allocations it cannot see are `emitEphemeral`'s, which are
|
|
139
|
+
* presentation-only and never persisted — § 4.3 would exclude them anyway.
|
|
140
|
+
*/
|
|
141
|
+
function highestEventSeq(store, runId) {
|
|
142
|
+
let max = 0;
|
|
143
|
+
for (const event of store.readEvents(runId)) {
|
|
144
|
+
if (typeof event.seq === 'number' && event.seq > max)
|
|
145
|
+
max = event.seq;
|
|
146
|
+
}
|
|
147
|
+
return max;
|
|
148
|
+
}
|
|
149
|
+
/** A run as the check sees it right now, or `undefined` when there is no such run. */
|
|
150
|
+
export function runVersionSnapshot(store, runId) {
|
|
151
|
+
const run = store.getRun(runId);
|
|
152
|
+
if (!run)
|
|
153
|
+
return undefined;
|
|
154
|
+
return {
|
|
155
|
+
ref: { kind: RUN_RESOURCE_KIND, id: runId },
|
|
156
|
+
seq: highestEventSeq(store, runId),
|
|
157
|
+
projection: runDecisionProjection(run),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** The `version` a read of this run hands the leader, or `undefined` when there is no such run. */
|
|
161
|
+
export function runVersion(store, runId) {
|
|
162
|
+
const snapshot = runVersionSnapshot(store, runId);
|
|
163
|
+
return snapshot ? versionToken(snapshot) : undefined;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The route half of the check (#250): the rejection a run-mutating route answers with, or
|
|
167
|
+
* `undefined` when its effect may go ahead.
|
|
168
|
+
*
|
|
169
|
+
* WHY THE ROUTE, NOT THE TOOL. An MCP tool reaches the services through in-process HTTP dispatch
|
|
170
|
+
* (N-02, `service-adapter.ts`), which is async, so a tool cannot hold `guardedMutation`'s
|
|
171
|
+
* synchronous critical section around the store call — only the route handler can. So the route
|
|
172
|
+
* accepts an OPTIONAL `expectedVersion` and calls this RIGHT BEFORE its effect, with nothing but
|
|
173
|
+
* synchronous code between the two: the check and the store call then run in one stretch, exactly
|
|
174
|
+
* as `guardedMutation` would run them (§ 4.5). A handler must never put an `await` between this
|
|
175
|
+
* call and its effect; each call site says so.
|
|
176
|
+
*
|
|
177
|
+
* ABSENT MEANS "NOT ASKED", ON PURPOSE. `undefined` is the cockpit, which has never sent a token and
|
|
178
|
+
* keeps its behaviour byte-for-byte. The leader can never reach that branch: every MCP tool that
|
|
179
|
+
* mutates a run REQUIRES `expectedVersion` in its own schema (D-06 § 4.4 rule 4), and the tool
|
|
180
|
+
* tests pin that each action sends it. Present-but-wrong — empty, foreign, unknown tag, another
|
|
181
|
+
* run's — is always a rejection, never a pass.
|
|
182
|
+
*
|
|
183
|
+
* AN ASYNC EFFECT (a worktree removal, a push, `gh pr create`) cannot sit inside a synchronous
|
|
184
|
+
* stretch at all. Those routes call this right before the effect STARTS, so a stale decision never
|
|
185
|
+
* begins; a change that lands while the effect is already running is outside what a version check
|
|
186
|
+
* can see, and each such effect keeps its own guards (`isActive`, the worktree checks).
|
|
187
|
+
*/
|
|
188
|
+
export function staleRunWrite(store, runId, expectedVersion) {
|
|
189
|
+
if (expectedVersion === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
const checked = guardedRunMutation(store, runId, expectedVersion, () => undefined);
|
|
192
|
+
return checked.status === 'conflict' ? checked : undefined;
|
|
193
|
+
}
|
|
194
|
+
/** The rejection a route answered with, when the body is one — how a tool tells a stale-version
|
|
195
|
+
* 409 from every other 409 (`run already started`, `session closed`, …). */
|
|
196
|
+
export function staleRejectionIn(body) {
|
|
197
|
+
const parsed = staleVersionRejectionSchema.safeParse(body);
|
|
198
|
+
return parsed.success ? parsed.data : undefined;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* `guardedMutation` for one run: `apply` receives the CURRENT record and performs the shared store
|
|
202
|
+
* call on it (`setPinned`, `updateRun`, …). It is never called when the token is stale.
|
|
203
|
+
*/
|
|
204
|
+
export function guardedRunMutation(store, runId, expectedVersion, apply, ..._sync) {
|
|
205
|
+
return guardedMutation({
|
|
206
|
+
resource: { kind: RUN_RESOURCE_KIND, id: runId },
|
|
207
|
+
expectedVersion,
|
|
208
|
+
read: () => runVersionSnapshot(store, runId),
|
|
209
|
+
apply: () => {
|
|
210
|
+
const run = store.getRun(runId);
|
|
211
|
+
// Unreachable after a passing check — the read above found it in this same synchronous
|
|
212
|
+
// stretch — but a missing run must fail loudly rather than hand `apply` an undefined.
|
|
213
|
+
if (!run)
|
|
214
|
+
throw new Error(`run ${runId} vanished inside the critical section`);
|
|
215
|
+
return apply(run);
|
|
216
|
+
},
|
|
217
|
+
}, ..._sync);
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=stale-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stale-write.js","sourceRoot":"","sources":["../../src/mcp/stale-write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,qBAAqB,EACrB,2BAA2B,GAK5B,MAAM,wBAAwB,CAAC;AAgDhC;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACnG,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAC;YACrD,IAAI,IAAI,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;AACzC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,QAA2B;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1G,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACpE,OAAO,GAAG,qBAAqB,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;AAC7F,CAAC;AAiCD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAI,KAA8B,EAAE,GAAG,KAAkB;IACtF,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC5B,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,6FAA6F;IAC7F,6FAA6F;IAC7F,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,cAAc,KAAK,SAAS,IAAI,KAAK,CAAC,eAAe,KAAK,cAAc,EAAE,CAAC;QACvG,OAAO,qBAAqB,CAAC,KAAK,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC/D,CAAC;IAED,IAAI,KAAQ,CAAC;IACb,IAAI,CAAC;QACH,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,4FAA4F;QAC5F,8FAA8F;QAC9F,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvC,OAAO,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC,CAAC;IACxH,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO;QACL,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,KAAK;KACN,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAiC,EAAE,cAAkC;IAClG,OAAO;QACL,MAAM,EAAE,UAAU;QAClB,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,eAAe;QACtB,QAAQ;QACR,yFAAyF;QACzF,yFAAyF;QACzF,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;QAC3D,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,0BAA0B;KACrC,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,QAAiC,EAAE,KAAc;IACxE,iGAAiG;IACjG,2CAA2C;IAC3C,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,yBAAyB,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,EAAE,4BAA4B,OAAO,EAAE,CAAC,CAAC;IACzG,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,kBAAkB;QACzB,QAAQ;QACR,QAAQ,EAAE,6BAA6B;KACxC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,CACL,KAAK,KAAK,IAAI;QACd,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,UAAU,CAAC;QAC1D,OAAQ,KAA4B,CAAC,IAAI,KAAK,UAAU,CACzD,CAAC;AACJ,CAAC;AAED,kGAAkG;AAElG,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAc;IAClD,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9F,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,KAAe,EAAE,KAAa;IACrD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,GAAG,GAAG;YAAE,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACxE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,kBAAkB,CAAC,KAAe,EAAE,KAAa;IAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,OAAO;QACL,GAAG,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,KAAK,EAAE;QAC3C,GAAG,EAAE,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;QAClC,UAAU,EAAE,qBAAqB,CAAC,GAAG,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,UAAU,CAAC,KAAe,EAAE,KAAa;IACvD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAClD,OAAO,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe,EAAE,KAAa,EAAE,eAAmC;IAC/F,IAAI,eAAe,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACnF,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC;AAED;6EAC6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,MAAM,MAAM,GAAG,2BAA2B,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAe,EACf,KAAa,EACb,eAAmC,EACnC,KAA4B,EAC5B,GAAG,KAAkB;IAErB,OAAO,eAAe,CAAI;QACxB,QAAQ,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,KAAK,EAAE;QAChD,eAAe;QACf,IAAI,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC;QAC5C,KAAK,EAAE,GAAG,EAAE;YACV,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAChC,uFAAuF;YACvF,sFAAsF;YACtF,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,OAAO,KAAK,uCAAuC,CAAC,CAAC;YAC/E,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;KACF,EAAE,GAAG,KAAK,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpToolResult } from './ipc.ts';
|
|
3
|
+
export type { McpToolResult } from './ipc.ts';
|
|
4
|
+
/**
|
|
5
|
+
* What a tool runs with. Tools execute INSIDE the running xezar service, never in
|
|
6
|
+
* the bridge: the bridge only lists them and forwards `tools/call` over the
|
|
7
|
+
* project's socket (D-01 § 3.5–3.6 — the bridge is a transport adapter, not
|
|
8
|
+
* another business engine). So `project` is the project that socket belongs to,
|
|
9
|
+
* and no argument a client sends can change it (D-01 § 1.5, F-01).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately small. The session binding (#87) and the shared business-service
|
|
12
|
+
* adapter (#89) widen it; widening an interface is additive for every tool.
|
|
13
|
+
*/
|
|
14
|
+
export interface McpToolContext {
|
|
15
|
+
readonly project: {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly root: string;
|
|
19
|
+
};
|
|
20
|
+
readonly xezarVersion: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One MCP tool. `inputSchema` is the only definition of its arguments: the JSON
|
|
24
|
+
* Schema clients see is derived from it, and the service validates every call
|
|
25
|
+
* against it before `call` runs. Results follow D-05: the text block is
|
|
26
|
+
* authoritative and `structuredContent` may only add to it.
|
|
27
|
+
*/
|
|
28
|
+
export interface McpTool<S extends z.ZodObject = z.ZodObject> {
|
|
29
|
+
/** `snake_case`, unique across the registry — `tools/index.test.ts` enforces both. */
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly title?: string;
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly inputSchema: S;
|
|
34
|
+
readonly annotations?: {
|
|
35
|
+
readonly readOnlyHint?: boolean;
|
|
36
|
+
readonly destructiveHint?: boolean;
|
|
37
|
+
readonly idempotentHint?: boolean;
|
|
38
|
+
readonly openWorldHint?: boolean;
|
|
39
|
+
};
|
|
40
|
+
call(args: z.output<S>, ctx: McpToolContext): Promise<McpToolResult>;
|
|
41
|
+
}
|
|
42
|
+
/** Keeps a tool's argument type inferred from its own schema. */
|
|
43
|
+
export declare function defineTool<S extends z.ZodObject>(tool: McpTool<S>): McpTool;
|
|
44
|
+
export declare const TOOL_NAME_RE: RegExp;
|
|
45
|
+
export declare function textResult(text: string, structuredContent?: Record<string, unknown>): McpToolResult;
|
|
46
|
+
export declare function errorResult(text: string, structuredContent?: Record<string, unknown>): McpToolResult;
|
|
47
|
+
/** The tool as `tools/list` sends it. */
|
|
48
|
+
export declare function toolListing(tool: McpTool): Record<string, unknown>;
|
package/dist/mcp/tool.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Keeps a tool's argument type inferred from its own schema. */
|
|
3
|
+
export function defineTool(tool) {
|
|
4
|
+
return tool;
|
|
5
|
+
}
|
|
6
|
+
export const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
7
|
+
export function textResult(text, structuredContent) {
|
|
8
|
+
return { content: [{ type: 'text', text }], ...(structuredContent ? { structuredContent } : {}) };
|
|
9
|
+
}
|
|
10
|
+
export function errorResult(text, structuredContent) {
|
|
11
|
+
return { ...textResult(text, structuredContent), isError: true };
|
|
12
|
+
}
|
|
13
|
+
/** The tool as `tools/list` sends it. */
|
|
14
|
+
export function toolListing(tool) {
|
|
15
|
+
const { $schema: _dialect, ...inputSchema } = z.toJSONSchema(tool.inputSchema, { io: 'input' });
|
|
16
|
+
return {
|
|
17
|
+
name: tool.name,
|
|
18
|
+
...(tool.title ? { title: tool.title } : {}),
|
|
19
|
+
description: tool.description,
|
|
20
|
+
inputSchema,
|
|
21
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../../src/mcp/tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAyCxB,iEAAiE;AACjE,MAAM,UAAU,UAAU,CAAwB,IAAgB;IAChE,OAAO,IAA0B,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAErD,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,iBAA2C;IAClF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACpG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,iBAA2C;IACnF,OAAO,EAAE,GAAG,UAAU,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnE,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAChG,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW;QACX,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/D,CAAC;AACJ,CAAC"}
|