weir-harness 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/capabilities/apply-engine.js +20 -4
- package/src/capabilities/lifecycle-feed.js +47 -0
- package/src/capabilities/mcp-drain.js +6 -0
- package/src/capabilities/mcp-facade-plugin.js +14 -4
- package/src/capabilities/mcp-facade.js +38 -7
- package/src/capabilities/mcp-inventory.js +152 -0
- package/src/capabilities/mcp-manager.js +413 -161
- package/src/capabilities/mcp-mount-ownership.js +114 -0
- package/src/capabilities/mcp-mount.js +335 -82
- package/src/capabilities/mcp-operation-coordinator.js +76 -0
- package/src/capabilities/mcp-runtime.js +3 -29
- package/src/capabilities/skill-selection-plugin.js +17 -1
- package/src/file-lease/client.js +61 -3
- package/src/file-lease/negotiation.js +89 -0
- package/src/file-lease/plugin.js +25 -23
- package/src/file-lease/tools.js +71 -121
- package/src/file-lease/watch.js +225 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weir-harness",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weir \u2014 orchestrator-centric DSH agent preset: intent gate, category delegation, todo driver, pull-based background jobs, auto-compaction, hashline edit.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,10 +42,11 @@ export const APPLY_AUDIT_TYPE = AUDIT_TYPES.capabilityApply
|
|
|
42
42
|
* of the removed servers FOR THAT SESSION and returns a handle: snapshot()
|
|
43
43
|
* reports the live in-flight counts (sent with the response, never awaited)
|
|
44
44
|
* and abort() reopens exactly the gates begin() closed when the Apply does
|
|
45
|
-
* not commit
|
|
46
|
-
*
|
|
47
|
-
* (agent, server)-keyed drain here
|
|
48
|
-
*
|
|
45
|
+
* not commit. After acceptance, reopen(restored, sessionId) opens only the
|
|
46
|
+
* servers the committed-over basis shows this Apply restored for that session.
|
|
47
|
+
* Production wires the manager's (agent, server)-keyed drain here
|
|
48
|
+
* (createSessionDrainAdapter, mcp-drain.js).
|
|
49
|
+
* @typedef {{ begin(removed: string[], sessionId: string): { snapshot(): DrainStatus[], abort(): void }, reopen(restored: string[], sessionId: string): void }} ApplyDrain
|
|
49
50
|
* A step-tagged rejection keeps the old authority and the user's draft.
|
|
50
51
|
* @typedef {{ status: 'applied', revision: number, receipt: unknown, effective: unknown, unresolvedWarnings: UnresolvedRef[], draining?: DrainStatus[], warnings?: string[] }
|
|
51
52
|
* | { status: 'duplicate', revision: number, receipt: unknown, effective: unknown, warnings?: string[] }
|
|
@@ -164,6 +165,9 @@ export function createDrainCoordinator() {
|
|
|
164
165
|
},
|
|
165
166
|
}
|
|
166
167
|
},
|
|
168
|
+
reopen(restored) {
|
|
169
|
+
for (const server of restored) stateOf(server).closed = false
|
|
170
|
+
},
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -607,6 +611,18 @@ export function createApplyEngine(options) {
|
|
|
607
611
|
* degraded: (code: string, error?: unknown) => void }} segment
|
|
608
612
|
*/
|
|
609
613
|
function publishAccepted({ sessionId, revision, payload, prepared, viewOptions, basisSets, lease, settle = null, degraded }) {
|
|
614
|
+
// The accepted set, not the transaction-start read, owns restoration. This
|
|
615
|
+
// runs only after a durable commit (or receipt proof), inside the held fence
|
|
616
|
+
// and before lifecycle publication/manager synchronization. A reopen failure
|
|
617
|
+
// cannot reject or roll back the landed write: degrade the response honestly.
|
|
618
|
+
const restoredServers = safeSets(payload).mcpServers.filter(server => !basisSets.mcpServers.includes(server))
|
|
619
|
+
if (restoredServers.length) {
|
|
620
|
+
try {
|
|
621
|
+
drain.reopen(restoredServers, sessionId)
|
|
622
|
+
} catch (error) {
|
|
623
|
+
degraded('drain-reopen-degraded', error)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
610
626
|
// Task 12.4: the net accepted delta is known only HERE (durable pre-write
|
|
611
627
|
// record vs the just-committed payload); notify consumers queue it for
|
|
612
628
|
// the next safe request. durable-pending-notice D2: the acceptance revision
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Per-mount Capability Lifecycle Snapshot ownership. A live token authorizes a
|
|
2
|
+
// manager only when its lazy accessor returns that exact mutable manager face.
|
|
3
|
+
// No mount-order, process-face, preset-name, or sole-feed fallback is permitted.
|
|
4
|
+
|
|
5
|
+
/** @typedef {{ lifecycle: any, manager: (() => any) | null | undefined }} LifecycleFeedToken */
|
|
6
|
+
/**
|
|
7
|
+
* Create an isolated registry for tests or an explicit in-process composition.
|
|
8
|
+
* Registration returns an ownership-scoped, idempotent disposer.
|
|
9
|
+
*/
|
|
10
|
+
export function createLifecycleFeedRegistry() {
|
|
11
|
+
/** @type {Set<LifecycleFeedToken>} */
|
|
12
|
+
const tokens = new Set()
|
|
13
|
+
return {
|
|
14
|
+
register({ lifecycle, manager }) {
|
|
15
|
+
const token = { lifecycle, manager }
|
|
16
|
+
tokens.add(token)
|
|
17
|
+
let live = true
|
|
18
|
+
return () => {
|
|
19
|
+
if (!live) return
|
|
20
|
+
live = false
|
|
21
|
+
tokens.delete(token)
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
lifecycleFor(managerFace) {
|
|
25
|
+
if (managerFace == null) return null
|
|
26
|
+
/** @type {LifecycleFeedToken | null} */
|
|
27
|
+
let owner = null
|
|
28
|
+
for (const token of tokens) {
|
|
29
|
+
let manager
|
|
30
|
+
try {
|
|
31
|
+
manager = typeof token.manager === 'function' ? token.manager() : null
|
|
32
|
+
} catch {
|
|
33
|
+
// A late, absent, or failing manager is denied on this adjudication.
|
|
34
|
+
// Do not cache either the failure or a previous successful owner.
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (manager !== managerFace) continue
|
|
38
|
+
if (owner) return null
|
|
39
|
+
owner = token
|
|
40
|
+
}
|
|
41
|
+
return owner?.lifecycle ?? null
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Internal module-local bridge shared by selection and manager rows. */
|
|
47
|
+
export const lifecycleFeedRegistry = createLifecycleFeedRegistry()
|
|
@@ -130,5 +130,11 @@ export function createSessionDrainAdapter(resolveDrain) {
|
|
|
130
130
|
abort: () => { for (const server of removed) drain.reopen(sessionId, server) },
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
|
+
/** Reopen only servers restored by an accepted Apply for this session. */
|
|
134
|
+
reopen(restored, sessionId) {
|
|
135
|
+
const drain = resolveDrain()
|
|
136
|
+
if (!drain) return
|
|
137
|
+
for (const server of restored) drain.reopen(sessionId, server)
|
|
138
|
+
},
|
|
133
139
|
}
|
|
134
140
|
}
|
|
@@ -14,13 +14,23 @@ import { createMcpFacade } from './mcp-facade.js'
|
|
|
14
14
|
*/
|
|
15
15
|
const bridge = new Map()
|
|
16
16
|
|
|
17
|
-
export function registerMcpFacadeBridge(groupId, payload) {
|
|
18
|
-
bridge.
|
|
19
|
-
|
|
17
|
+
export function registerMcpFacadeBridge(groupId, payload, owner = payload) {
|
|
18
|
+
if (bridge.has(groupId)) {
|
|
19
|
+
throw Object.assign(new Error(`MCP facade group ${groupId} already has an owner`), { code: 'duplicate-mcp-facade-owner' })
|
|
20
|
+
}
|
|
21
|
+
const record = { payload, owner }
|
|
22
|
+
bridge.set(groupId, record)
|
|
23
|
+
return () => {
|
|
24
|
+
// A stale/repeated disposer compares the exact registration and owner. It
|
|
25
|
+
// can never delete a later registration that reused the confirmed-free ID.
|
|
26
|
+
if (bridge.get(groupId) !== record || record.owner !== owner) return false
|
|
27
|
+
bridge.delete(groupId)
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export function mcpFacadeBridgeFor(groupId) {
|
|
23
|
-
return bridge.get(groupId) ?? null
|
|
33
|
+
return bridge.get(groupId)?.payload ?? null
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
export const name = 'weir-mcp-facade'
|
|
@@ -30,9 +30,10 @@ const message = error => (error instanceof Error ? error.message : String(error)
|
|
|
30
30
|
* admit: (agent: unknown) => Promise<boolean>,
|
|
31
31
|
* isEnabledFor: (agent: unknown) => boolean,
|
|
32
32
|
* drain?: { enter(agentId: string, server: string, label?: string): number|null, exit(agentId: string, server: string, token: number|null): void },
|
|
33
|
+
* onRegister?: (name: string, captureRollback: (rollback: () => void) => void) => void, onUnregister?: (name: string) => void,
|
|
33
34
|
* onError?: (reason: string, cause?: unknown) => void }} config
|
|
34
35
|
*/
|
|
35
|
-
export function createMcpFacade({ host, identity, generation, admit, isEnabledFor, drain, onRegister, onError = () => {}, assertPermit }) {
|
|
36
|
+
export function createMcpFacade({ host, identity, generation, admit, isEnabledFor, drain, onRegister, onUnregister, onError = () => {}, assertPermit }) {
|
|
36
37
|
const wrapper = createMcpToolWrapper({ identity, generation, admit, drain, assertPermit })
|
|
37
38
|
|
|
38
39
|
const unknown = (service, method) => {
|
|
@@ -43,14 +44,44 @@ export function createMcpFacade({ host, identity, generation, admit, isEnabledFo
|
|
|
43
44
|
|
|
44
45
|
const tools = {
|
|
45
46
|
register(definition) {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
47
|
+
// Host registration happens first, then the generation-owned fact and
|
|
48
|
+
// every live agent converge in this same synchronous segment.
|
|
48
49
|
const wrapped = wrapper.wrap(definition)
|
|
49
50
|
const registration = /** @type {{ register(d: unknown): unknown }} */ (host.tools).register(wrapped)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (typeof wrapped?.name !== 'string') return registration
|
|
52
|
+
const name = wrapped.name
|
|
53
|
+
const disposeHost = typeof registration === 'function' ? registration : () => {}
|
|
54
|
+
let rollbackRegistration = null
|
|
55
|
+
try {
|
|
56
|
+
onRegister?.(name, rollback => { rollbackRegistration = rollback }, wrapped)
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
// Host-first rollback is fail-closed: owner facts and partial denies
|
|
59
|
+
// stay in place unless Host removal is confirmed.
|
|
60
|
+
try { disposeHost() } catch (rollbackError) {
|
|
61
|
+
onError('facade-host-registration-rollback-failed', rollbackError)
|
|
62
|
+
throw cause
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
if (typeof rollbackRegistration === 'function') rollbackRegistration()
|
|
66
|
+
else onUnregister?.(name) // legacy registration callbacks
|
|
67
|
+
} catch (rollbackError) {
|
|
68
|
+
onError('facade-owner-registration-rollback-failed', rollbackError)
|
|
69
|
+
}
|
|
70
|
+
throw cause
|
|
71
|
+
}
|
|
72
|
+
let hostRemoved = false
|
|
73
|
+
let completed = false
|
|
74
|
+
return () => {
|
|
75
|
+
if (completed) return
|
|
76
|
+
if (!hostRemoved) {
|
|
77
|
+
// Host-first is fail-closed: if removal throws, the manager fact and
|
|
78
|
+
// deny stay in place. A later retry does not double-dispose the Host.
|
|
79
|
+
disposeHost()
|
|
80
|
+
hostRemoved = true
|
|
81
|
+
}
|
|
82
|
+
onUnregister?.(name)
|
|
83
|
+
completed = true
|
|
84
|
+
}
|
|
54
85
|
},
|
|
55
86
|
get(...args) { return /** @type {{ get(...a: unknown[]): unknown }} */ (host.tools).get(...args) },
|
|
56
87
|
list(...args) { return /** @type {{ list(...a: unknown[]): unknown }} */ (host.tools).list(...args) },
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The one read-only MCP Inventory Projection. Registry/runtime/mount/receipt and
|
|
2
|
+
// Loader facts are joined here; probing, mutation, credential resolution, agent
|
|
3
|
+
// visibility and every other operation remain with their existing owners.
|
|
4
|
+
// Pure dependencies keep this module independently testable and typechecked.
|
|
5
|
+
import { resolveMcpGroupIdentity } from './mcp-mount-ownership.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Classify Loader rows once and retain the facts needed by both public views.
|
|
9
|
+
* Every actual group remains a separate fact; same-identity groups never
|
|
10
|
+
* overwrite each other in an identity-keyed Map.
|
|
11
|
+
*
|
|
12
|
+
* @param {any[]} rows
|
|
13
|
+
* @param {Map<string, any>} mounted
|
|
14
|
+
* @param {Record<string, any>} registryServers
|
|
15
|
+
* @param {{ isResidue: (id: string) => boolean, groupPrefix: string, ownership?: any }} options
|
|
16
|
+
*/
|
|
17
|
+
function joinLoaderRows(rows, mounted, registryServers, { isResidue, groupPrefix, ownership = null }) {
|
|
18
|
+
const managed = []
|
|
19
|
+
const unmanaged = []
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
if (typeof row?.id === 'string' && isResidue(row.id)) {
|
|
22
|
+
const resolved = resolveMcpGroupIdentity({ groupId: row.id, mounted, ownership, row })
|
|
23
|
+
// Unresolved generated suffixes are conservative residue, not evidence
|
|
24
|
+
// from which a logical registry identity or mount health may be invented.
|
|
25
|
+
if (!resolved) continue
|
|
26
|
+
managed.push({
|
|
27
|
+
identity: resolved.identity,
|
|
28
|
+
groupId: row.id,
|
|
29
|
+
registry: registryServers[resolved.identity],
|
|
30
|
+
isMounted: resolved.mount != null,
|
|
31
|
+
mount: resolved.mount,
|
|
32
|
+
source: resolved.source,
|
|
33
|
+
allocation: resolved.allocation,
|
|
34
|
+
})
|
|
35
|
+
} else if (row?.name === '@deepseek-ai/dsh-mcp-client' && !(typeof row.id === 'string' && row.id.startsWith(groupPrefix))) {
|
|
36
|
+
unmanaged.push({ serverName: row.config?.serverName ?? row.id ?? 'unknown', state: 'unmanaged', readOnly: true })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { managed, unmanaged }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Internal implementation behind mcp-manager.js's exported partitionMcpRows.
|
|
44
|
+
* Its field selection is the historical compatibility contract; production
|
|
45
|
+
* manager.list() uses the same Loader join and the full projection below.
|
|
46
|
+
*
|
|
47
|
+
* @param {any[]} rows
|
|
48
|
+
* @param {Map<string, any>} mounted
|
|
49
|
+
* @param {Record<string, any>} registryServers
|
|
50
|
+
* @param {{ isResidue: (id: string) => boolean, groupPrefix: string, ownership?: any }} options
|
|
51
|
+
*/
|
|
52
|
+
export function partitionMcpInventory(rows, mounted, registryServers, options) {
|
|
53
|
+
const joined = joinLoaderRows(rows, mounted, registryServers, options)
|
|
54
|
+
return {
|
|
55
|
+
managed: joined.managed.map(fact => ({
|
|
56
|
+
identity: fact.identity,
|
|
57
|
+
groupId: fact.groupId,
|
|
58
|
+
label: fact.registry?.label ?? fact.identity,
|
|
59
|
+
generation: fact.mount?.generation ?? fact.registry?.generation ?? null,
|
|
60
|
+
state: fact.isMounted ? 'mounted' : 'registered',
|
|
61
|
+
})),
|
|
62
|
+
unmanaged: joined.unmanaged,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {{
|
|
68
|
+
* registry: { read: () => Promise<any> },
|
|
69
|
+
* runtime: { snapshot: (identity: string) => any },
|
|
70
|
+
* mounted: Map<string, any>,
|
|
71
|
+
* receipts: { mount: Map<string, any>, test: Map<string, any> },
|
|
72
|
+
* listRows: () => any[],
|
|
73
|
+
* startErrors: Array<{ identity: string | null, reason: string }>,
|
|
74
|
+
* discoveryFor: (identity: string, generation: any) => any,
|
|
75
|
+
* support: () => any,
|
|
76
|
+
* isResidue: (id: string) => boolean,
|
|
77
|
+
* groupPrefix: string,
|
|
78
|
+
* ownership?: any,
|
|
79
|
+
* holdsReservation: (rows: any[], serverName: any) => any,
|
|
80
|
+
* redactEntry: (entry: any) => any,
|
|
81
|
+
* fingerprintEntry: (entry: any) => string,
|
|
82
|
+
* }} dependencies
|
|
83
|
+
*/
|
|
84
|
+
export function createMcpInventoryProjection(dependencies) {
|
|
85
|
+
const {
|
|
86
|
+
registry, runtime, mounted, receipts, listRows, startErrors,
|
|
87
|
+
discoveryFor, support, isResidue, groupPrefix, ownership = null, holdsReservation,
|
|
88
|
+
redactEntry, fingerprintEntry,
|
|
89
|
+
} = dependencies
|
|
90
|
+
|
|
91
|
+
async function list() {
|
|
92
|
+
const current = await registry.read()
|
|
93
|
+
const rows = listRows()
|
|
94
|
+
const registryServers = current.kind === 'ok' ? current.servers : {}
|
|
95
|
+
const loader = joinLoaderRows(rows, mounted, registryServers, { isResidue, groupPrefix, ownership })
|
|
96
|
+
if (current.kind === 'absent') {
|
|
97
|
+
return { managed: [], unmanaged: loader.unmanaged, warnings: [], registryRevision: 0, capabilities: support() }
|
|
98
|
+
}
|
|
99
|
+
if (current.kind !== 'ok') {
|
|
100
|
+
return { managed: [], unmanaged: loader.unmanaged, unavailable: { status: current.kind }, registryRevision: current.revision ?? 0, capabilities: support() }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Registry authority starts the full view. Join every actual Loader group,
|
|
104
|
+
// then select only this manager's exact own mount as mounted evidence.
|
|
105
|
+
const loaderByIdentity = new Map()
|
|
106
|
+
for (const fact of loader.managed) {
|
|
107
|
+
const facts = loaderByIdentity.get(fact.identity) ?? []
|
|
108
|
+
facts.push(fact)
|
|
109
|
+
loaderByIdentity.set(fact.identity, facts)
|
|
110
|
+
}
|
|
111
|
+
const defaults = new Set(current.enabled)
|
|
112
|
+
const managed = Object.values(current.servers).map(entry => {
|
|
113
|
+
const phase = runtime.snapshot(entry.identity)
|
|
114
|
+
const facts = loaderByIdentity.get(entry.identity) ?? []
|
|
115
|
+
const ownMount = mounted.get(entry.identity) ?? null
|
|
116
|
+
const ownLoaderFact = ownMount ? facts.find(fact => fact.groupId === ownMount.groupId) : null
|
|
117
|
+
// Some supported bare/test compositions expose no Loader listing. Keep
|
|
118
|
+
// their existing mount readback, but never let a different actual group
|
|
119
|
+
// satisfy this manager's strict mount evidence.
|
|
120
|
+
const mountedRecord = ownMount && (facts.length === 0 || ownLoaderFact) ? ownMount : null
|
|
121
|
+
const isMounted = phase.phase === 'mounted' && phase.generation === entry.generation && mountedRecord?.generation === entry.generation
|
|
122
|
+
const holder = holdsReservation(rows, entry.client?.serverName ?? entry.identity)
|
|
123
|
+
const state = holder ? 'conflict' : isMounted ? 'mounted' : phase.phase === 'saved-degraded' ? 'degraded' : ['cleanup-failed', 'cleanup-unknown'].includes(phase.phase) ? phase.phase : 'registered'
|
|
124
|
+
const discovery = discoveryFor(entry.identity, entry.generation)
|
|
125
|
+
return {
|
|
126
|
+
identity: entry.identity,
|
|
127
|
+
label: entry.label,
|
|
128
|
+
serverName: entry.client?.serverName ?? entry.identity,
|
|
129
|
+
transport: entry.transport?.kind ?? entry.client?.transport ?? 'unknown',
|
|
130
|
+
incarnation: entry.incarnation ?? null,
|
|
131
|
+
generation: entry.generation,
|
|
132
|
+
defaultEnabled: defaults.has(entry.identity),
|
|
133
|
+
state,
|
|
134
|
+
statusSource: isMounted ? 'registry-runtime-mount-readback' : holder ? 'serverName-reservation' : 'registry-runtime',
|
|
135
|
+
runtimePhase: phase.phase,
|
|
136
|
+
discovery: isMounted ? discovery : { toolCount: null, toolNamesPreview: [], truncated: false },
|
|
137
|
+
lastMount: receipts.mount.get(entry.identity) ?? null,
|
|
138
|
+
lastTest: receipts.test.get(entry.identity) ?? null,
|
|
139
|
+
startErrors: startErrors.filter(error => error.identity === entry.identity).map(error => ({ reason: error.reason })),
|
|
140
|
+
liveReconnect: 'unknown',
|
|
141
|
+
processExit: 'unknown',
|
|
142
|
+
stderr: 'unknown',
|
|
143
|
+
readOnly: false,
|
|
144
|
+
config: redactEntry(entry),
|
|
145
|
+
fingerprint: fingerprintEntry(entry),
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
return { managed, unmanaged: loader.unmanaged, warnings: current.warnings, registryRevision: current.revision, capabilities: support() }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { list }
|
|
152
|
+
}
|