weir-harness 1.2.3 → 2.1.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/cordis.patch.yml +7 -18
- package/lib/client.capability-badge.js +2 -0
- package/lib/client.capability-manager.js +46 -179
- package/lib/client.capability-model.js +428 -3
- package/lib/client.file-lease-badge.js +307 -0
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +124 -36
- package/lib/client.mcp-manager.js +563 -0
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- package/skills/mcp-server-setup/SKILL.md +124 -0
- package/src/capabilities/apply-engine.js +18 -4
- package/src/capabilities/capability-remote.js +296 -45
- package/src/capabilities/initial-selection.js +3 -2
- package/src/capabilities/lifecycle-preload.js +69 -11
- package/src/capabilities/lifecycle-snapshot.js +127 -39
- package/src/capabilities/mcp-admission.js +2 -2
- package/src/capabilities/mcp-config.js +344 -0
- package/src/capabilities/mcp-credentials.js +180 -0
- package/src/capabilities/mcp-diagnostics.js +169 -0
- package/src/capabilities/mcp-drain.js +18 -4
- package/src/capabilities/mcp-facade.js +3 -3
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +368 -142
- package/src/capabilities/mcp-mount.js +219 -79
- package/src/capabilities/mcp-operations.js +359 -0
- package/src/capabilities/mcp-process.js +43 -0
- package/src/capabilities/mcp-registry.js +74 -60
- package/src/capabilities/mcp-runtime.js +103 -0
- package/src/capabilities/read-payloads.js +8 -3
- package/src/capabilities/skill-applicability.js +185 -0
- package/src/capabilities/skill-builtin-migration.js +2 -1
- package/src/capabilities/skill-selection-plugin.js +161 -56
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1299 -0
- package/src/file-lease/index.js +34 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +1013 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +847 -0
- package/src/file-lease/view.js +144 -0
- package/src/file-lease/watch.js +473 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +237 -0
- package/src/lease-broker/server.js +402 -0
- package/src/lease-broker/state.js +482 -0
- package/src/lsp/tools.js +137 -27
- package/src/process/index.js +4 -2
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +2 -24
- package/src/shared/audit.js +9 -1
- package/src/shared/child-scope.js +2 -0
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/src/worktree/rules.js +1 -1
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- package/src/worktree/authority.js +0 -56
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// `@deepseek-ai/dsh-mcp-client` (referenced by package-name string, never
|
|
5
|
-
// statically imported) — created and destroyed at runtime through the
|
|
6
|
-
// host-supported loader API (1.14 PASS-WITH-LIMITS):
|
|
7
|
-
// loader.create(options) → await loader.await() → judge readiness by the
|
|
8
|
-
// child client's fiber state (NEVER at create() return — the group's
|
|
9
|
-
// children are created asynchronously, the 1.14 spike misread exactly this).
|
|
10
|
-
// Startup also removes `weir-mcp-*` residue rows that a crashed previous
|
|
11
|
-
// run persisted into the profile config (loader.create/remove write through
|
|
12
|
-
// to the root tree, CITED dsh-app-boot/lib/index.js:245).
|
|
1
|
+
// Managed MCP Loader lifecycle. Registry configuration is the authority; the
|
|
2
|
+
// stock client is named by string and never statically imported.
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
13
4
|
import { isSegment } from './store/paths.js'
|
|
5
|
+
import { McpLifecycleError, mcpDiagnostic } from './mcp-diagnostics.js'
|
|
14
6
|
|
|
15
7
|
export const MCP_GROUP_PREFIX = 'weir-mcp-'
|
|
16
|
-
|
|
8
|
+
const DEFAULT_STARTUP_TIMEOUT = 15_000
|
|
9
|
+
const DEFAULT_CLEANUP_TIMEOUT = 15_000
|
|
17
10
|
const message = error => (error instanceof Error ? error.message : String(error))
|
|
18
11
|
|
|
19
|
-
/**
|
|
20
|
-
* Composition-time name-ambiguity detection (8.2, first implemented here —
|
|
21
|
-
* only a suggestion in G3b). Two shapes, both fail closed:
|
|
22
|
-
* - one configured name is another's `__` prefix (mcp__alpha__x__echo could
|
|
23
|
-
* come from `alpha` or `alpha__x` — collide is EXECUTED in G3b);
|
|
24
|
-
* - two servers declare the same public tool name.
|
|
25
|
-
* @param {Array<{ identity: string, publicNames?: string[] }>} entries
|
|
26
|
-
* @returns {{ kind: 'ok' } | { kind: 'conflict', conflicts: Array<{ kind: string, a: string, b: string, reason: string }> }}
|
|
27
|
-
*/
|
|
28
12
|
export function detectMcpNameAmbiguity(entries) {
|
|
29
13
|
const conflicts = []
|
|
30
14
|
const identities = entries.map(entry => entry.identity)
|
|
@@ -38,22 +22,13 @@ export function detectMcpNameAmbiguity(entries) {
|
|
|
38
22
|
const declared = new Map()
|
|
39
23
|
for (const entry of entries) {
|
|
40
24
|
for (const name of entry.publicNames ?? []) {
|
|
41
|
-
if (declared.has(name)) {
|
|
42
|
-
|
|
43
|
-
} else {
|
|
44
|
-
declared.set(name, entry.identity)
|
|
45
|
-
}
|
|
25
|
+
if (declared.has(name)) conflicts.push({ kind: 'duplicate-public-name', a: declared.get(name), b: entry.identity, reason: `public tool name "${name}" is declared by both "${declared.get(name)}" and "${entry.identity}"` })
|
|
26
|
+
else declared.set(name, entry.identity)
|
|
46
27
|
}
|
|
47
28
|
}
|
|
48
29
|
return conflicts.length === 0 ? { kind: 'ok' } : { kind: 'conflict', conflicts }
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
/**
|
|
52
|
-
* The isolated-group options for one managed server (plain data handed to
|
|
53
|
-
* loader.create — the client is named by package string so nothing
|
|
54
|
-
* `@deepseek-ai/*` is statically imported).
|
|
55
|
-
* @param {{ identity: string, generation: number, client: unknown }} input
|
|
56
|
-
*/
|
|
57
32
|
export function mcpGroupOptions({ identity, generation, client }) {
|
|
58
33
|
if (!isSegment(identity)) throw new TypeError(`invalid MCP server identity: ${JSON.stringify(identity)}`)
|
|
59
34
|
const group = `${MCP_GROUP_PREFIX}${identity}`
|
|
@@ -69,77 +44,242 @@ export function mcpGroupOptions({ identity, generation, client }) {
|
|
|
69
44
|
}
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
/** Ids of `weir-mcp-*` group entries (crash residue or our own mounts). */
|
|
73
47
|
export const isMcpResidueId = id => typeof id === 'string' && id.startsWith(MCP_GROUP_PREFIX) && !id.endsWith('-facade') && !id.endsWith('-client')
|
|
74
48
|
|
|
75
49
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
50
|
+
* The stock EntryTree methods write through their tree. Its root EntryGroup
|
|
51
|
+
* methods only mutate the runtime store, which is the non-profile-writing seam
|
|
52
|
+
* used for materialized secrets. Any other shape is unsupported, never a
|
|
53
|
+
* fallback to the write-through tree.
|
|
78
54
|
*/
|
|
79
|
-
export function
|
|
80
|
-
|
|
55
|
+
export function resolveEphemeralMcpTree(loader) {
|
|
56
|
+
const root = loader?.root
|
|
57
|
+
const tree = root?.tree
|
|
58
|
+
if (tree?.root !== root || typeof root?.create !== 'function' || typeof root?.remove !== 'function'
|
|
59
|
+
|| typeof tree?.await !== 'function' || typeof tree?.resolve !== 'function') {
|
|
60
|
+
return { kind: 'unsupported', reason: 'ephemeral-loader-unavailable' }
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
kind: 'ok',
|
|
64
|
+
tree: {
|
|
65
|
+
ephemeral: true,
|
|
66
|
+
create: options => root.create(options),
|
|
67
|
+
remove: id => root.remove(id),
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function strictMcpClientConfig(client, { probe = false } = {}) {
|
|
73
|
+
const reconnect = probe
|
|
74
|
+
? { enabled: false, initialDelayMs: 1, maxDelayMs: 1, maxAttempts: 1 }
|
|
75
|
+
: client?.reconnect
|
|
76
|
+
return { ...client, failOnStartupError: true, ...(reconnect ? { reconnect } : {}) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function bounded(promise, timeoutMs, code, label) {
|
|
80
|
+
let timer
|
|
81
|
+
const timeout = new Promise((_, reject) => {
|
|
82
|
+
timer = setTimeout(() => reject(Object.assign(new Error(`${label} timed out after ${timeoutMs}ms`), { code })), timeoutMs)
|
|
83
|
+
})
|
|
84
|
+
try {
|
|
85
|
+
return await Promise.race([Promise.resolve(promise), timeout])
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createMcpMount({ loader, onError = () => {}, startupTimeoutMs = DEFAULT_STARTUP_TIMEOUT, cleanupTimeoutMs = DEFAULT_CLEANUP_TIMEOUT, ephemeralTree } = {}) {
|
|
92
|
+
/** @type {Map<string, { groupId: string, generation: number, state?: string }>} */
|
|
81
93
|
const mounted = new Map()
|
|
82
94
|
|
|
95
|
+
const treeFor = secretBearing => {
|
|
96
|
+
const resolved = ephemeralTree === undefined ? resolveEphemeralMcpTree(loader) : ephemeralTree
|
|
97
|
+
if (resolved?.kind === 'ok' && resolved.tree) return resolved.tree
|
|
98
|
+
if (secretBearing) throw new McpLifecycleError('unsupported', new Error('secret-bearing MCP mount requires a proven non-profile-writing Loader root group'), { status: 'unsupported', action: 'use-secret-free-config' })
|
|
99
|
+
if (typeof loader?.create !== 'function' || typeof loader?.remove !== 'function') {
|
|
100
|
+
throw new McpLifecycleError('unsupported', new Error('MCP Loader lifecycle is unavailable'), { status: 'unsupported' })
|
|
101
|
+
}
|
|
102
|
+
// Compatibility for secret-free legacy records only. New secret-bearing
|
|
103
|
+
// paths can never reach this write-through branch.
|
|
104
|
+
return { ephemeral: false, create: options => loader.create(options), remove: id => loader.remove(id) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function entriesFor(groupId) {
|
|
108
|
+
if (typeof loader?.entries === 'function') {
|
|
109
|
+
return [...loader.entries()].filter(entry => entry?.id === groupId || String(entry?.id ?? '').startsWith(`${groupId}:`))
|
|
110
|
+
}
|
|
111
|
+
if (loader?.store && typeof loader.store === 'object') {
|
|
112
|
+
return Object.values(loader.store).filter(entry => entry?.id === groupId || entry?.options?.id === groupId || String(entry?.options?.id ?? '').startsWith(`${groupId}-`))
|
|
113
|
+
}
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function forgetGroup(groupId, generation) {
|
|
118
|
+
for (const [identity, entry] of mounted) {
|
|
119
|
+
if (entry.groupId === groupId && (!generation || entry.generation === generation)) mounted.delete(identity)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
124
|
+
* Capture and settle group/client/descendant fibers BEFORE removing rows.
|
|
125
|
+
* remove() is not the barrier: its return does not await async plugin
|
|
126
|
+
* disposal. A rejected disposal or an unconfirmed close blocks same-name
|
|
127
|
+
* replacement and leaves enough state for residue recovery.
|
|
87
128
|
*/
|
|
88
|
-
async function
|
|
89
|
-
const
|
|
90
|
-
|
|
129
|
+
async function disposeGroup(groupId, context = {}) {
|
|
130
|
+
const tree = context.tree ?? treeFor(Boolean(context.secretBearing))
|
|
131
|
+
const captured = entriesFor(groupId)
|
|
132
|
+
if (captured?.length === 0) {
|
|
133
|
+
forgetGroup(groupId, context.generation)
|
|
134
|
+
return { status: 'cleaned', removed: false, groupId }
|
|
135
|
+
}
|
|
136
|
+
if (captured === null && context.requireCapture === true) {
|
|
137
|
+
return { status: 'cleanup-unknown', removed: false, groupId, diagnostic: mcpDiagnostic(new Error('cannot capture Loader fibers for disposal'), { code: 'cleanup-unknown', action: 'recover-residue' }) }
|
|
138
|
+
}
|
|
139
|
+
const fibers = (captured ?? []).map(entry => entry?.fiber).filter(Boolean)
|
|
140
|
+
const disposals = fibers.map((fiber, index) => {
|
|
141
|
+
try {
|
|
142
|
+
return bounded(fiber.dispose?.(), cleanupTimeoutMs, 'cleanup-timeout', `MCP disposal ${groupId}[${index}]`)
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
return Promise.reject(cause)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
const settled = await Promise.allSettled(disposals)
|
|
148
|
+
const rejected = settled.find(result => result.status === 'rejected')
|
|
149
|
+
if (rejected) {
|
|
150
|
+
return { status: rejected.reason?.code === 'cleanup-timeout' ? 'cleanup-unknown' : 'cleanup-failed', removed: false, groupId, diagnostic: mcpDiagnostic(rejected.reason, { code: rejected.reason?.code === 'cleanup-timeout' ? 'cleanup-unknown' : 'cleanup-failed', action: 'recover-residue' }) }
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await bounded(tree.remove(groupId), cleanupTimeoutMs, 'cleanup-timeout', `MCP Loader remove ${groupId}`)
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
return { status: cause?.code === 'cleanup-timeout' ? 'cleanup-unknown' : 'cleanup-failed', removed: false, groupId, diagnostic: mcpDiagnostic(cause, { code: cause?.code === 'cleanup-timeout' ? 'cleanup-unknown' : 'cleanup-failed', action: 'recover-residue' }) }
|
|
156
|
+
}
|
|
157
|
+
const remaining = entriesFor(groupId)
|
|
158
|
+
let closeConfirmed = remaining === null ? true : remaining.length === 0
|
|
159
|
+
if (typeof context.confirmClosed === 'function') {
|
|
91
160
|
try {
|
|
92
|
-
await
|
|
93
|
-
removed.push(id)
|
|
161
|
+
closeConfirmed = closeConfirmed && await bounded(context.confirmClosed({ groupId, entries: captured, fibers }), cleanupTimeoutMs, 'cleanup-timeout', `MCP close confirmation ${groupId}`) === true
|
|
94
162
|
} catch (cause) {
|
|
95
|
-
|
|
163
|
+
return { status: 'cleanup-unknown', removed: false, groupId, diagnostic: mcpDiagnostic(cause, { code: 'cleanup-unknown', action: 'recover-residue' }) }
|
|
96
164
|
}
|
|
165
|
+
} else if (captured !== null) {
|
|
166
|
+
closeConfirmed = closeConfirmed && fibers.every(fiber => fiber.uid === null || fiber.state === 4 || fiber.state === 5)
|
|
167
|
+
}
|
|
168
|
+
if (!closeConfirmed || remaining === null && context.requireCapture === true) {
|
|
169
|
+
return { status: 'cleanup-unknown', removed: false, groupId, diagnostic: mcpDiagnostic(new Error('MCP transport closure or Loader row removal could not be confirmed'), { code: 'cleanup-unknown', action: 'recover-residue' }) }
|
|
170
|
+
}
|
|
171
|
+
forgetGroup(groupId, context.generation)
|
|
172
|
+
return { status: 'cleaned', removed: true, groupId }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function sweepResidue(candidateIds) {
|
|
176
|
+
const removed = []
|
|
177
|
+
for (const id of candidateIds.filter(isMcpResidueId)) {
|
|
178
|
+
const cleanup = await disposeGroup(id, { tree: treeFor(false), identity: id.slice(MCP_GROUP_PREFIX.length) })
|
|
179
|
+
if (cleanup.status === 'cleaned') removed.push(id)
|
|
180
|
+
else onError(`residue-sweep-${cleanup.status}:${id}`, cleanup.diagnostic)
|
|
97
181
|
}
|
|
98
182
|
return removed
|
|
99
183
|
}
|
|
100
184
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const options = mcpGroupOptions({ identity, generation, client })
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
185
|
+
async function mountOne(input) {
|
|
186
|
+
const { identity, generation, fiberOf, probe = false, secretBearing = false } = input
|
|
187
|
+
const previous = mounted.get(identity)
|
|
188
|
+
if (!probe && (previous?.state === 'cleanup-failed' || previous?.state === 'cleanup-unknown')) {
|
|
189
|
+
throw new McpLifecycleError(previous.state, new Error(`same-name MCP replacement is blocked while ${identity} is ${previous.state}`), { status: previous.state, action: 'recover-residue' })
|
|
190
|
+
}
|
|
191
|
+
const options = mcpGroupOptions({ identity, generation, client: strictMcpClientConfig(input.client, { probe }) })
|
|
192
|
+
const tree = treeFor(secretBearing)
|
|
193
|
+
let bridgeReady = false
|
|
194
|
+
let succeeded = false
|
|
195
|
+
try {
|
|
196
|
+
if (typeof input.beforeCreate === 'function') await input.beforeCreate({ groupId: options.id, identity, generation, probe, ephemeral: tree.ephemeral === true })
|
|
197
|
+
bridgeReady = true
|
|
198
|
+
await bounded(tree.create(options), startupTimeoutMs, 'startup-timeout', `MCP Loader create ${identity}`)
|
|
199
|
+
await bounded(loader.await(), startupTimeoutMs, 'startup-timeout', `MCP Loader await ${identity}`)
|
|
200
|
+
const fiber = typeof fiberOf === 'function'
|
|
201
|
+
? await bounded(fiberOf(options.id), startupTimeoutMs, 'startup-timeout', `MCP client readiness ${identity}`)
|
|
202
|
+
: { state: 'running' }
|
|
203
|
+
if (fiber === null || fiber === undefined) throw new Error('mcp-mount-error: client entry has no fiber after loader.await()')
|
|
204
|
+
if (fiber.state !== undefined && !['running', 'active', 2].includes(fiber.state)) throw new Error(`mcp-mount-error: client fiber is not ready (state ${String(fiber.state)})`)
|
|
205
|
+
const clientEntry = loader?.store?.[`${options.id}-client`]
|
|
206
|
+
if (clientEntry?.error) throw clientEntry.error
|
|
207
|
+
if (typeof input.readback === 'function') await input.readback({ groupId: options.id, identity, generation, probe })
|
|
208
|
+
const discovery = typeof input.discovery === 'function' ? await input.discovery({ groupId: options.id, identity, generation, probe }) : { toolCount: null, toolNamesPreview: [], truncated: false }
|
|
209
|
+
const result = { status: 'mounted', groupId: options.id, identity, generation, probe, discovery, execution: input.execution ?? null }
|
|
210
|
+
if (!probe) mounted.set(identity, { groupId: options.id, generation, state: 'mounted' })
|
|
211
|
+
succeeded = true
|
|
212
|
+
return result
|
|
213
|
+
} catch (cause) {
|
|
214
|
+
const cleanup = await disposeGroup(options.id, { tree, identity, generation, secretBearing, confirmClosed: input.confirmClosed })
|
|
215
|
+
if (!probe && cleanup.status !== 'cleaned') mounted.set(identity, { groupId: options.id, generation, state: cleanup.status })
|
|
216
|
+
const diagnostic = mcpDiagnostic(cause, { secrets: input.secrets, action: cleanup.status === 'cleaned' ? 'retry' : 'recover-residue' })
|
|
217
|
+
throw new McpLifecycleError(cleanup.status === 'cleaned' ? diagnostic.code : cleanup.status, cause, {
|
|
218
|
+
status: cleanup.status === 'cleaned' ? (probe ? 'probe-failed' : 'mount-failed') : cleanup.status,
|
|
219
|
+
secrets: input.secrets,
|
|
220
|
+
cleanup,
|
|
221
|
+
action: cleanup.status === 'cleaned' ? 'retry' : 'recover-residue',
|
|
222
|
+
})
|
|
223
|
+
} finally {
|
|
224
|
+
if (!succeeded && bridgeReady && typeof input.afterCleanup === 'function') await input.afterCleanup({ groupId: options.id, identity, generation, probe })
|
|
225
|
+
if (succeeded && probe && typeof input.afterProbeReady === 'function') await input.afterProbeReady({ groupId: options.id, identity, generation })
|
|
226
|
+
}
|
|
118
227
|
}
|
|
119
228
|
|
|
120
|
-
|
|
121
|
-
* Unmount one server WITHOUT touching the others (1.14 S3 anomaly: a
|
|
122
|
-
* removal once made a sibling vanish — after remove() every remaining
|
|
123
|
-
* entry is verified and remounted through remount() if missing).
|
|
124
|
-
* @param {string} identity
|
|
125
|
-
* @param {{ alive?: () => string[], remount?: (identity: string) => Promise<void> }} [guards]
|
|
126
|
-
*/
|
|
127
|
-
async function unmount(identity, guards = {}) {
|
|
229
|
+
async function unmountOne(identity, guards = {}, context = {}) {
|
|
128
230
|
const entry = mounted.get(identity)
|
|
129
|
-
if (!entry) return { removed: false }
|
|
130
|
-
|
|
131
|
-
|
|
231
|
+
if (!entry && context.required === true) return { status: 'cleanup-unknown', removed: false, reason: 'mount-record-missing' }
|
|
232
|
+
const groupId = entry?.groupId ?? `${MCP_GROUP_PREFIX}${identity}`
|
|
233
|
+
const cleanup = await disposeGroup(groupId, { ...context, identity, generation: entry?.generation, tree: context.tree ?? treeFor(Boolean(context.secretBearing)) })
|
|
234
|
+
if (cleanup.status !== 'cleaned') {
|
|
235
|
+
if (entry) entry.state = cleanup.status
|
|
236
|
+
return cleanup
|
|
237
|
+
}
|
|
132
238
|
if (typeof guards.alive === 'function' && typeof guards.remount === 'function') {
|
|
133
239
|
for (const [other] of mounted) {
|
|
134
|
-
const
|
|
135
|
-
if (!guards.alive().includes(
|
|
240
|
+
const siblingGroup = `${MCP_GROUP_PREFIX}${other}`
|
|
241
|
+
if (!guards.alive().includes(siblingGroup)) {
|
|
136
242
|
onError(`sibling-vanished-after-unmount:${other}`)
|
|
137
243
|
await guards.remount(other)
|
|
138
244
|
}
|
|
139
245
|
}
|
|
140
246
|
}
|
|
141
|
-
return { removed:
|
|
247
|
+
return { status: 'cleaned', removed: Boolean(entry) || cleanup.removed, groupId }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function probe(input) {
|
|
251
|
+
const suffix = randomUUID().replaceAll('-', '').slice(0, 16)
|
|
252
|
+
const identity = input.identity ?? `probe-${suffix}`
|
|
253
|
+
const serverName = input.serverName ?? `probe${suffix}`
|
|
254
|
+
const client = { ...input.client, serverName }
|
|
255
|
+
let ready
|
|
256
|
+
try {
|
|
257
|
+
ready = await mountOne({ ...input, identity, client, generation: input.generation ?? 0, probe: true })
|
|
258
|
+
} catch (cause) {
|
|
259
|
+
if (cause instanceof McpLifecycleError) throw cause
|
|
260
|
+
throw new McpLifecycleError('probe-failed', cause, { status: 'probe-failed', secrets: input.secrets })
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const cleanup = await disposeGroup(ready.groupId, { tree: treeFor(Boolean(input.secretBearing)), identity, generation: ready.generation, secretBearing: Boolean(input.secretBearing), confirmClosed: input.confirmClosed, requireCapture: true })
|
|
264
|
+
if (cleanup.status !== 'cleaned') return { status: cleanup.status, identity, generation: ready.generation, cleanup, diagnostic: cleanup.diagnostic }
|
|
265
|
+
if (typeof input.afterCleanup === 'function') await input.afterCleanup({ groupId: ready.groupId, identity, generation: ready.generation, probe: true, cleanup })
|
|
266
|
+
return { ...ready, status: 'mounted', cleanup }
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
throw new McpLifecycleError('cleanup-failed', cause, { status: 'cleanup-failed', secrets: input.secrets, action: 'recover-residue' })
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const queues = new Map()
|
|
273
|
+
function serialize(identity, task) {
|
|
274
|
+
const previous = queues.get(identity) ?? Promise.resolve()
|
|
275
|
+
const run = previous.then(task, task)
|
|
276
|
+
const tail = run.catch(() => {})
|
|
277
|
+
queues.set(identity, tail)
|
|
278
|
+
tail.then(() => { if (queues.get(identity) === tail) queues.delete(identity) })
|
|
279
|
+
return run
|
|
142
280
|
}
|
|
281
|
+
const mount = input => input?.probe ? mountOne(input) : serialize(input.identity, () => mountOne(input))
|
|
282
|
+
const unmount = (identity, guards = {}, context = {}) => serialize(identity, () => unmountOne(identity, guards, context))
|
|
143
283
|
|
|
144
|
-
return { mount, unmount, sweepResidue, mounted, message }
|
|
284
|
+
return { mount, unmount, probe, sweepResidue, disposeGroup, mounted, entriesFor, message }
|
|
145
285
|
}
|