weir-harness 2.1.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weir-harness",
3
- "version": "2.1.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 — abort() and begin() act on the SAME drain, so a failed Apply
46
- * never leaves a session permanently gated. Production wires the manager's
47
- * (agent, server)-keyed drain here (createSessionDrainAdapter, mcp-drain.js).
48
- * @typedef {{ begin(removed: string[], sessionId: string): { snapshot(): DrainStatus[], abort(): void } }} ApplyDrain
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.set(groupId, payload)
19
- return () => bridge.delete(groupId)
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'
@@ -53,7 +53,7 @@ export function createMcpFacade({ host, identity, generation, admit, isEnabledFo
53
53
  const disposeHost = typeof registration === 'function' ? registration : () => {}
54
54
  let rollbackRegistration = null
55
55
  try {
56
- onRegister?.(name, rollback => { rollbackRegistration = rollback })
56
+ onRegister?.(name, rollback => { rollbackRegistration = rollback }, wrapped)
57
57
  } catch (cause) {
58
58
  // Host-first rollback is fail-closed: owner facts and partial denies
59
59
  // stay in place unless Host removal is confirmed.
@@ -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
+ }