weir-harness 1.2.3 → 2.0.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.
Files changed (81) hide show
  1. package/cordis.patch.yml +7 -18
  2. package/lib/client.file-lease-badge.js +307 -0
  3. package/lib/client.js +20 -30
  4. package/lib/client.settings-page.js +10 -34
  5. package/package.json +2 -2
  6. package/src/capabilities/store/checked-contract.d.ts +2 -2
  7. package/src/capabilities/store/fs-adapter.js +2 -3
  8. package/src/context-guard/index.js +2 -2
  9. package/src/delegate/supervision-mount.js +5 -5
  10. package/src/delegate/tool.js +2 -10
  11. package/src/file-lease/client.js +1066 -0
  12. package/src/file-lease/index.js +30 -0
  13. package/src/file-lease/lineage.js +207 -0
  14. package/src/file-lease/node-contracts.d.ts +42 -0
  15. package/src/file-lease/plugin.js +789 -0
  16. package/src/file-lease/singleton.js +155 -0
  17. package/src/file-lease/spawn.js +62 -0
  18. package/src/file-lease/tools.js +459 -0
  19. package/src/file-lease/view.js +144 -0
  20. package/src/hashline-edit/index.js +76 -42
  21. package/src/hashline-edit/write-tool.js +90 -0
  22. package/src/lease-broker/entry.js +98 -0
  23. package/src/lease-broker/node-contracts.d.ts +63 -0
  24. package/src/lease-broker/protocol.js +236 -0
  25. package/src/lease-broker/server.js +399 -0
  26. package/src/lease-broker/state.js +425 -0
  27. package/src/lsp/tools.js +137 -27
  28. package/src/settings/index.js +2 -44
  29. package/src/settings/sections.js +2 -24
  30. package/src/shared/audit.js +6 -1
  31. package/src/todo-driver/index.js +1 -6
  32. package/src/worktree/cards.js +6 -21
  33. package/src/worktree/lane-core.js +1 -19
  34. package/src/worktree/lane-workflows.js +11 -32
  35. package/lib/client.edit-lock-maintenance.js +0 -592
  36. package/lib/client.edit-lock-panel.js +0 -405
  37. package/src/edit-lock/adapter.js +0 -57
  38. package/src/edit-lock/admin-ledger.js +0 -149
  39. package/src/edit-lock/admin-recovery.js +0 -151
  40. package/src/edit-lock/admission.js +0 -82
  41. package/src/edit-lock/binding-attestation.js +0 -29
  42. package/src/edit-lock/binding-guidance.js +0 -72
  43. package/src/edit-lock/binding-machine.js +0 -312
  44. package/src/edit-lock/call-context.js +0 -65
  45. package/src/edit-lock/derived-facts.js +0 -67
  46. package/src/edit-lock/domain.js +0 -165
  47. package/src/edit-lock/domains.js +0 -170
  48. package/src/edit-lock/host.js +0 -149
  49. package/src/edit-lock/incarnations.js +0 -150
  50. package/src/edit-lock/index.js +0 -1303
  51. package/src/edit-lock/inspect.js +0 -219
  52. package/src/edit-lock/lifecycle.js +0 -579
  53. package/src/edit-lock/limits.js +0 -61
  54. package/src/edit-lock/maintenance.js +0 -310
  55. package/src/edit-lock/manager.js +0 -875
  56. package/src/edit-lock/negotiation.js +0 -132
  57. package/src/edit-lock/operation-history.js +0 -214
  58. package/src/edit-lock/peer-client.js +0 -86
  59. package/src/edit-lock/peer-transport.js +0 -76
  60. package/src/edit-lock/peer.js +0 -87
  61. package/src/edit-lock/publisher.js +0 -191
  62. package/src/edit-lock/read-authority.js +0 -101
  63. package/src/edit-lock/recovery.js +0 -139
  64. package/src/edit-lock/remote.js +0 -362
  65. package/src/edit-lock/request-binding.js +0 -42
  66. package/src/edit-lock/request-data.js +0 -40
  67. package/src/edit-lock/reservation.js +0 -301
  68. package/src/edit-lock/reserved-runtime.js +0 -45
  69. package/src/edit-lock/resource-identity.js +0 -130
  70. package/src/edit-lock/runtime.js +0 -145
  71. package/src/edit-lock/settle.js +0 -144
  72. package/src/edit-lock/snapshot.js +0 -175
  73. package/src/edit-lock/stale-sweep.js +0 -120
  74. package/src/edit-lock/state.js +0 -462
  75. package/src/edit-lock/store.js +0 -327
  76. package/src/edit-lock/tool-scope.js +0 -29
  77. package/src/edit-lock/view.js +0 -170
  78. package/src/edit-lock/write-scopes.js +0 -48
  79. package/src/edit-lock/write-tool.js +0 -45
  80. package/src/shared/delegate-edit-lifecycle.js +0 -36
  81. package/src/worktree/authority.js +0 -56
@@ -1,310 +0,0 @@
1
- // Settings-plane maintenance remains reachable while enforcement is disabled.
2
- // Inspection is read-only; explicit offline ADMIN OVERRIDE uses its own durable
3
- // ledger and publisher reservation. No model tools or automatic repair.
4
- import { lstatSync, readdirSync, realpathSync } from 'node:fs'
5
- import { join } from 'node:path'
6
- import { AUTHORITY_DIR } from './domains.js'
7
- import { reservationPathFor } from './reservation.js'
8
- import { parseSnapshot } from './snapshot.js'
9
- import { maintenanceState, summarizeAuthorityImage, recoveryProposals } from './inspect.js'
10
- import { readSnapshotBytes, checkedPath, verifyPins, missing, errorMessage, MAX_SNAPSHOT_BYTES } from './read-authority.js'
11
- import { recoverAuthority } from './admin-recovery.js'
12
- import { createAudit, AUDIT_TYPES } from '../shared/audit.js'
13
-
14
- const STATUS_PATH = '/api/weir-edit-lock/maintenance/status'
15
- const INSPECT_PATH = '/api/weir-edit-lock/maintenance/inspect'
16
-
17
- /** @param {unknown} payload @param {number} [status] */
18
- function reply(payload, status = 200) {
19
- return new Response(JSON.stringify(payload), { status, headers: { 'content-type': 'application/json' } })
20
- }
21
- /** @param {string} message @param {number} [status] @param {string} [code] */
22
- function failure(message, status = 400, code) {
23
- return reply({ ok: false, error: { code: code ?? `weir-edit-lock/${status === 400 ? 'invalid' : 'internal'}`, message } }, status)
24
- }
25
-
26
- /**
27
- * Host-lifetime mount evidence exposed by the settings row. Each preset mount
28
- * owns one generation and reports installation, domain opens and failures.
29
- * Reporting only: nothing here gates authority, admission, tools or settings.
30
- * No generations, mixed decisions or incomplete lifecycles mean 'unknown'.
31
- */
32
- export function createEditLockEvidence() {
33
- const mounts = new Map()
34
- let next = 0
35
- return {
36
- // Each handle owns exactly one generation. Late callbacks cannot resurrect it.
37
- recordMount(info) {
38
- const id = ++next
39
- const row = { id, enabled: info.enabled === true, pinned: info.pinned === true,
40
- fixed: info.fixed === true, phase: info.phase ?? 'installed', at: Date.now(),
41
- domains: new Map(), failures: new Map(), error: /** @type {string | null} */ (null) }
42
- mounts.set(id, row)
43
- return {
44
- installed() { if (mounts.has(id) && row.phase === 'installing') row.phase = 'installed' },
45
- disposing() { if (mounts.has(id)) row.phase = 'disposing' },
46
- failed(error) { if (mounts.has(id)) { row.phase = 'failed'; row.error = String(error) } },
47
- dispose() { mounts.delete(id) },
48
- recordDomain(root, info) {
49
- if (mounts.has(id) && row.phase !== 'disposing') row.domains.set(root, { root, mode: info.mode ?? null, error: info.error ?? null, mountId: id, readBindings: info.bindings })
50
- },
51
- clearSessionFailure(sessionId) {
52
- if (mounts.has(id) && row.phase !== 'disposing') row.failures.delete(sessionId)
53
- },
54
- recordSessionFailure(sessionId, reason) {
55
- if (!mounts.has(id) || row.phase === 'disposing') return
56
- if (row.failures.size >= 50) row.failures.delete(row.failures.keys().next().value)
57
- row.failures.set(sessionId, { sessionId, reason: String(reason), at: Date.now(), mountId: id })
58
- },
59
- }
60
- },
61
- snapshot() {
62
- const rows = [...mounts.values()]
63
- return {
64
- mounts: rows.map(({ domains, failures, ...row }) => ({ ...row })),
65
- domains: rows.flatMap(row => [...row.domains.values()].map(({ readBindings, ...domain }) => {
66
- try { return { ...domain, bindings: readBindings?.() ?? [] } }
67
- catch { return { ...domain, bindings: null } }
68
- })),
69
- sessionFailures: rows.flatMap(row => [...row.failures.values()]),
70
- }
71
- },
72
- }
73
- }
74
-
75
- // The bounded, pinned, identity-checked snapshot read is shared with the cold
76
- // status view: read-authority.js. Only the inspection-specific facts stay here.
77
- function domainFacts(root) {
78
- const authorityDir = join(root, AUTHORITY_DIR)
79
- try {
80
- const pins = checkedPath(authorityDir)
81
- if (!pins.at(-1).stat.isDirectory()) throw new Error('authority is not a directory')
82
- let reservation = false
83
- try {
84
- const stat = lstatSync(reservationPathFor(authorityDir))
85
- reservation = !stat.isSymbolicLink() && stat.isDirectory()
86
- } catch (error) { if (!missing(error)) throw error }
87
- verifyPins(pins)
88
- return { root, authorityDir, hasAuthority: true, reservation, error: null }
89
- } catch (error) {
90
- return { root, authorityDir, hasAuthority: missing(error) ? false : null, reservation: null,
91
- error: missing(error) ? null : errorMessage(error) }
92
- }
93
- }
94
-
95
- /**
96
- * The trusted domain-root set: only roots the SERVER derived (live sessions'
97
- * management roots + mount-evidence roots). The client may send a root back,
98
- * but it is accepted only when it canonically matches a derived entry — an
99
- * arbitrary client filesystem path is never read.
100
- * @param {() => string[]} candidateRoots @param {string[]} evidenceRoots
101
- * @returns {Set<string>}
102
- */
103
- export function trustedRoots(candidateRoots, evidenceRoots) {
104
- const trusted = new Set()
105
- for (const root of [...candidateRoots(), ...evidenceRoots]) {
106
- if (typeof root !== 'string' || root.length === 0) continue
107
- try { trusted.add(realpathSync.native(root)) } catch { /* an unresolvable candidate is not trusted */ }
108
- }
109
- return trusted
110
- }
111
-
112
- /**
113
- * Read-only inspection of one trusted domain root's authority. Presence is a
114
- * fact pattern, never inferred: 'none' (no authority directory), 'empty'
115
- * (directory currently empty; history indeterminate), 'no-committed-snapshot' (contents
116
- * the store would refuse), 'not-a-file', 'valid', 'corrupt' (validation
117
- * refusal, message preserved), 'unreadable' (IO failure). The bytes are
118
- * validated in memory only — the file, the reservation and every history are
119
- * left byte-identical.
120
- * @param {string} root - canonical (realpath) trusted root
121
- */
122
- export const MAX_INSPECTION_BYTES = MAX_SNAPSHOT_BYTES
123
- export function inspectAuthority(root) {
124
- const facts = domainFacts(root)
125
- const { authorityDir, reservation } = facts
126
- const base = { root, authorityDir, reservation, endpoint: null }
127
- if (facts.error) return { ...base, presence: 'unreadable', message: facts.error }
128
- if (!facts.hasAuthority) return { ...base, presence: 'none' }
129
- const read = readSnapshotBytes(authorityDir)
130
- // The authority directory vanished between the facts and the read: the same
131
- // race the inline read reported as unreadable.
132
- if (read.presence === 'no-authority') return { ...base, presence: 'unreadable', message: 'authority path changed during inspection' }
133
- if (read.presence === 'no-snapshot') {
134
- try {
135
- const entries = readdirSync(authorityDir)
136
- return { ...base, presence: entries.length ? 'no-committed-snapshot' : 'empty' }
137
- } catch (error) {
138
- return { ...base, presence: 'unreadable', message: errorMessage(error) }
139
- }
140
- }
141
- if (read.presence === 'not-a-file') return { ...base, presence: 'not-a-file' }
142
- if (read.presence !== 'valid') return { ...base, presence: 'unreadable', message: read.message }
143
- try {
144
- const parsed = parseSnapshot(read.bytes, root)
145
- // Both projections derive from the SAME parsed image: the snapshot summary
146
- // and the server-computed recovery proposals (additive, schemaVersion 1).
147
- return { ...base, presence: 'valid', snapshot: summarizeAuthorityImage(parsed), recoveryProposals: recoveryProposals(root, parsed) }
148
- } catch (error) {
149
- return { ...base, presence: 'corrupt', message: errorMessage(error) }
150
- }
151
- }
152
-
153
- /**
154
- * The profile-wide switch status. `saved` comes from the live settings config;
155
- * `enabledAtMount` from mount evidence (undefined → the state is honestly
156
- * 'unknown', never inferred from the saved value). Restart semantics: a saved
157
- * value that differs from the mounted one is a pending request.
158
- * @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
159
- */
160
- export function maintenanceStatus(deps) {
161
- const saved = deps.savedEnabled() === true
162
- const evidence = deps.evidence.snapshot()
163
- const installed = evidence.mounts.filter(row => row.phase === 'installed')
164
- const enabledAtMount = installed.length > 0 && installed.length === evidence.mounts.length
165
- && installed.every(row => row.enabled === installed[0].enabled) ? installed[0].enabled : undefined
166
- const state = maintenanceState({ saved, enabledAtMount })
167
- const evidenceRoots = evidence.domains.map(entry => entry.root)
168
- // Status lists every server-derived root: canonical candidates plus the
169
- // row-reported evidence roots. An evidence root that no longer resolves
170
- // (unmounted volume, deleted workspace) stays listed — it is still the
171
- // row's own report, and vanishing it would hide a blocked domain.
172
- const rootSet = trustedRoots(deps.candidateRoots, evidenceRoots)
173
- for (const root of evidenceRoots) if (typeof root === 'string' && root.length > 0) rootSet.add(root)
174
- const roots = [...rootSet].sort()
175
- const evidenceByRoot = new Map()
176
- for (const entry of evidence.domains) {
177
- try { evidenceByRoot.set(realpathSync.native(entry.root), entry) } catch { evidenceByRoot.set(entry.root, entry) }
178
- }
179
- const domains = roots.map(root => {
180
- const facts = domainFacts(root)
181
- const seen = evidenceByRoot.get(root)
182
- const bindings = evidence.domains.filter(entry => {
183
- try { return realpathSync.native(entry.root) === root } catch { return entry.root === root }
184
- }).flatMap(entry => entry.bindings ?? [])
185
- return { root, hasAuthority: facts.hasAuthority, reservation: facts.reservation, mode: seen?.mode ?? null, error: seen?.error ?? facts.error, bindings }
186
- })
187
- const blocked = [
188
- ...evidence.mounts.filter(row => row.phase !== 'installed').map(row => ({ kind: 'mount', mountId: row.id, reason: row.error ?? row.phase })),
189
- ...evidence.domains.filter(entry => entry.error).map(entry => ({ kind: 'domain', root: entry.root, reason: entry.error })),
190
- ...evidence.sessionFailures.map(row => ({ kind: 'session', sessionId: row.sessionId, reason: row.reason })),
191
- ]
192
- return {
193
- saved,
194
- mounted: enabledAtMount === undefined ? null : enabledAtMount,
195
- mounts: evidence.mounts,
196
- pinned: evidence.mounts.some(row => row.pinned),
197
- state,
198
- scope: 'profile',
199
- restartRequired: state === 'enable-requested' || state === 'disable-requested',
200
- blocked,
201
- domains,
202
- }
203
- }
204
-
205
- /**
206
- * @typedef {{ path: string, methods: string[], requestBody: 'buffered', fetch: (request: { json(): Promise<unknown> }) => Promise<Response> }} MaintenanceRoute
207
- * @typedef {{ fetch: { register(route: MaintenanceRoute): () => void } }} MaintenanceConnection
208
- * @typedef {{ connection?: MaintenanceConnection, get?: (name: 'connection') => MaintenanceConnection | undefined, emit?: (type: string, record: object) => unknown }} MaintenanceScope
209
- */
210
- /**
211
- * Register inspection and explicit administrator recovery on the authenticated
212
- * settings-plane connection (S20). Handler errors become structured payloads;
213
- * recovery errors never expose filesystem paths or claim rollback.
214
- * @param {MaintenanceScope} scope @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
215
- * @returns {() => void} disposer
216
- */
217
- export function registerEditLockMaintenanceEndpoints(scope, deps) {
218
- const connection = scope?.connection ?? scope?.get?.('connection')
219
- if (!connection?.fetch?.register) return () => {}
220
- const disposers = []
221
- disposers.push(connection.fetch.register({
222
- path: STATUS_PATH,
223
- methods: ['POST'],
224
- requestBody: 'buffered',
225
- fetch: async () => {
226
- try {
227
- return reply({ ok: true, value: maintenanceStatus(deps) })
228
- } catch (error) {
229
- return failure(errorMessage(error), 500)
230
- }
231
- },
232
- }))
233
- disposers.push(connection.fetch.register({
234
- path: INSPECT_PATH,
235
- methods: ['POST'],
236
- requestBody: 'buffered',
237
- fetch: async (request) => {
238
- let body
239
- try { body = await request.json() } catch { body = null }
240
- const requested = body !== null && typeof body === 'object' && 'root' in body && typeof body.root === 'string' ? body.root : ''
241
- if (!requested) return failure('body needs { root } from the status domain list')
242
- try {
243
- const evidenceRoots = deps.evidence.snapshot().domains.map(entry => entry.root)
244
- // Exact membership BEFORE any operation on client data. No realpath oracle.
245
- if (!trustedRoots(deps.candidateRoots, evidenceRoots).has(requested)) {
246
- return failure('not an Edit Lock domain root this server derived', 403, 'weir-edit-lock/untrusted-root')
247
- }
248
- return reply({ ok: true, value: inspectAuthority(requested) })
249
- } catch (error) {
250
- return failure(errorMessage(error), 500)
251
- }
252
- },
253
- }))
254
- // Host Connection authenticates browser cookies and checks Host/Origin before
255
- // dispatching exact /api routes. Never register this through a raw web server.
256
- const audit = createAudit({ emit: (type, record) => scope.emit?.(type, record) })
257
- disposers.push(connection.fetch.register({
258
- path: '/api/weir-edit-lock/maintenance/recover',
259
- methods: ['POST'],
260
- requestBody: 'buffered',
261
- fetch: async request => {
262
- let body
263
- try { body = await request.json() } catch { return failure('Invalid recovery JSON') }
264
- const root = body !== null && typeof body === 'object' && 'root' in body && typeof body.root === 'string' ? body.root : ''
265
- try {
266
- const evidenceRoots = deps.evidence.snapshot().domains.map(row => row.root)
267
- if (!trustedRoots(deps.candidateRoots, evidenceRoots).has(root)) return failure('not an Edit Lock domain root this server derived', 403, 'weir-edit-lock/untrusted-root')
268
- const value = await recoverAuthority(body)
269
- audit(null, AUDIT_TYPES.editLockMaintenance, { kind: 'admin-override', recoveryId: value.record.recoveryId, revision: value.revision, idempotent: value.idempotent }, { root })
270
- return reply({ ok: true, value })
271
- } catch (error) {
272
- // Do not return raw filesystem paths, credentials, causes or old content.
273
- const e = /** @type {{code?: string, commitStatus?: string}} */ (error)
274
- const code = e.code?.startsWith('weir-edit-lock/') ? e.code : 'weir-edit-lock/recovery-refused'
275
- return reply({ ok: false, error: { code, message: 'Recovery not acknowledged. Inspect authority and retry the same recovery ID after resolving the cause.', commitStatus: e.commitStatus ?? 'not-acknowledged' } }, 409)
276
- }
277
- },
278
- }))
279
- let disposed = false
280
- return () => {
281
- if (disposed) return
282
- disposed = true
283
- for (const dispose of disposers) dispose?.()
284
- }
285
- }
286
-
287
- /**
288
- * Wire the maintenance endpoints from the profile-level settings row (the row
289
- * cannot add package subpaths, S19). Service resolution goes through
290
- * ctx.inject (S20); nothing registers when the connection service is absent.
291
- * @param {any} ctx @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
292
- * @returns {() => void} idempotent disposer
293
- */
294
- export function wireEditLockMaintenance(ctx, deps) {
295
- let closed = false
296
- let off = () => {}
297
- const injection = ctx.inject?.(['connection'], (/** @type {any} */ scope) => {
298
- if (closed) return () => {}
299
- off()
300
- off = registerEditLockMaintenanceEndpoints(scope, deps)
301
- return off
302
- })
303
- return () => {
304
- if (closed) return
305
- closed = true
306
- off()
307
- if (typeof injection === 'function') injection()
308
- else injection?.dispose?.()
309
- }
310
- }