@ran-sh/dsh-crew 1.0.1 → 1.0.3

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.
@@ -0,0 +1,291 @@
1
+ // Structure-aware access to the Crew-owned Harness settings.yaml provider map.
2
+ // Only the known llm-pi-ai.providers mapping is mutable; arbitrary YAML and
3
+ // credential values are never deserialized or returned.
4
+
5
+ import { createHash } from 'node:crypto';
6
+ import { classifyCredentialReference } from './credential-reference.mjs';
7
+ import { readProviderMaterialization } from './provider-profile-store.mjs';
8
+
9
+ const PROVIDER_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
10
+
11
+ function sha256(value) { return createHash('sha256').update(value, 'utf8').digest('hex'); }
12
+ function indentOf(line) { return (line.match(/^\s*/u) ?? [''])[0].length; }
13
+ function nonBlank(line) { return line.trim() !== '' && !line.trimStart().startsWith('#'); }
14
+ function scalarField(lines, entry, field) {
15
+ const pattern = new RegExp(`^\\s+${field}:\\s*(.*?)\\s*$`);
16
+ for (const line of lines.slice(entry.start, entry.end)) {
17
+ const match = line.match(pattern);
18
+ if (!match) continue;
19
+ const value = match[1].trim();
20
+ if (!value) return null;
21
+ return value.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2');
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function parseDefaultModel(source) {
27
+ if (typeof source !== 'string') return { ok: false, code: 'PROVIDER_SETTINGS_DEFAULT_UNSUPPORTED' };
28
+ const lines = source.split(/\r?\n/u);
29
+ const start = lines.findIndex((line) => /^agent-default-model:\s*$/u.test(line));
30
+ if (start < 0) return { ok: false, code: 'PROVIDER_SETTINGS_DEFAULT_UNSUPPORTED' };
31
+ let end = lines.length;
32
+ for (let index = start + 1; index < lines.length; index += 1) {
33
+ if (indentOf(lines[index]) === 0 && nonBlank(lines[index])) { end = index; break; }
34
+ }
35
+ const providerLine = lines.findIndex((line, index) => index > start && index < end && /^ {2}provider:\s*\S.*$/u.test(line));
36
+ const modelLine = lines.findIndex((line, index) => index > start && index < end && /^ {2}model:\s*\S.*$/u.test(line));
37
+ const provider = providerLine >= 0 ? lines[providerLine].replace(/^ {2}provider:\s*/u, '').trim() : null;
38
+ const model = modelLine >= 0 ? lines[modelLine].replace(/^ {2}model:\s*/u, '').trim() : null;
39
+ if (!provider || !model) return { ok: false, code: 'PROVIDER_SETTINGS_DEFAULT_UNSUPPORTED' };
40
+ return { ok: true, lines, start, end, providerLine, modelLine, provider, model };
41
+ }
42
+
43
+ function parseProviderMap(source) {
44
+ if (typeof source !== 'string') return { ok: false, code: 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' };
45
+ const lines = source.split(/\r?\n/u);
46
+ const llmStart = lines.findIndex((line) => /^llm-pi-ai:\s*$/u.test(line));
47
+ if (llmStart < 0) return { ok: false, code: 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' };
48
+ let blockEnd = lines.length;
49
+ for (let index = llmStart + 1; index < lines.length; index += 1) {
50
+ if (indentOf(lines[index]) === 0 && nonBlank(lines[index])) { blockEnd = index; break; }
51
+ }
52
+ const providersLine = lines.findIndex((line, index) => index > llmStart && index < blockEnd && /^ {2}providers:\s*(?:\{\s*\})?\s*$/u.test(line));
53
+ if (providersLine < 0) return { ok: false, code: 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' };
54
+ let providersBlockEnd = blockEnd;
55
+ for (let index = providersLine + 1; index < blockEnd; index += 1) {
56
+ if (nonBlank(lines[index]) && indentOf(lines[index]) <= 2) { providersBlockEnd = index; break; }
57
+ }
58
+ const providerIndent = 4;
59
+ const entries = [];
60
+ for (let index = providersLine + 1; index < blockEnd; index += 1) {
61
+ const line = lines[index];
62
+ if (!nonBlank(line)) continue;
63
+ const indent = indentOf(line);
64
+ if (indent < providerIndent) {
65
+ if (indent > 2) return { ok: false, code: 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' };
66
+ break;
67
+ }
68
+ if (indent !== providerIndent) continue;
69
+ const match = line.match(/^ {4}([A-Za-z0-9][A-Za-z0-9._-]*):\s*$/u);
70
+ if (!match) return { ok: false, code: 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' };
71
+ let end = index + 1;
72
+ while (end < blockEnd) {
73
+ const next = lines[end];
74
+ if (nonBlank(next) && indentOf(next) <= providerIndent) break;
75
+ end += 1;
76
+ }
77
+ entries.push({ id: match[1], start: index, end });
78
+ index = end - 1;
79
+ }
80
+ return { ok: true, lines, llmStart, blockEnd, providersLine, providersBlockEnd, entries };
81
+ }
82
+
83
+ const SENSITIVE_CREDENTIAL_FIELD = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|(?:^|[_-])token(?:$|[_-])|secret|password|authorization|credential|private[_-]?key|client[_-]?secret|cookie|bearer|webhook)/iu;
84
+ const REFERENCE_FIELD_SUFFIX = /(?:env|ref|name|handle|id|file|path)$/iu;
85
+
86
+ function isInlineCredentialLine(line) {
87
+ const inlineKeys = [...line.matchAll(/["']?([A-Za-z][A-Za-z0-9_.-]*)["']?\s*:/gu)].map((match) => match[1]);
88
+ if (inlineKeys.some((field) => SENSITIVE_CREDENTIAL_FIELD.test(field) && !REFERENCE_FIELD_SUFFIX.test(field.replace(/[_.-]/gu, '')))) return true;
89
+ const match = line.match(/^\s*["']?([A-Za-z][A-Za-z0-9_.-]*)["']?\s*:\s*(.*?)\s*$/u);
90
+ if (!match) return false;
91
+ const [, field, value] = match;
92
+ if (!value || /^(?:null|~|\{\}|\[\])$/u.test(value)) return false;
93
+ const normalized = field.replace(/[_.-]/gu, '');
94
+ if (SENSITIVE_CREDENTIAL_FIELD.test(field) && !REFERENCE_FIELD_SUFFIX.test(normalized)) return true;
95
+ return /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|authorization|credential|private[_-]?key|client[_-]?secret|cookie|bearer|webhook)\s*["']?\s*:/iu.test(value);
96
+ }
97
+
98
+ export function hasInlineProviderCredentials(source, { providerIds = [] } = {}) {
99
+ const parsed = parseProviderMap(source);
100
+ if (!parsed.ok) return { ok: false, code: parsed.code };
101
+ const requested = providerIds.length > 0 ? new Set(providerIds) : null;
102
+ const providers = parsed.entries.filter((entry) => !requested || requested.has(entry.id));
103
+ return { ok: true, inline: providers.some((entry) => parsed.lines.slice(entry.start, entry.end).some(isInlineCredentialLine)) };
104
+ }
105
+
106
+ export function inspectProviderSettings(source) {
107
+ const revision = typeof source === 'string' ? sha256(source) : null;
108
+ const parsed = parseProviderMap(source);
109
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision };
110
+ return { ok: true, schema: 'harness-settings-llm-pi-ai-v1', providerIds: parsed.entries.map((entry) => entry.id), revision };
111
+ }
112
+
113
+ export function readProviderSettingsDeclarations(source, { file = 'harness/settings.yaml' } = {}) {
114
+ const parsed = parseProviderMap(source);
115
+ if (!parsed.ok) return { ok: false, code: parsed.code };
116
+ return {
117
+ ok: true,
118
+ declarations: parsed.entries.map((entry) => {
119
+ const rawCredentialRef = scalarField(parsed.lines, entry, 'apiKeyEnv');
120
+ const credential = classifyCredentialReference(rawCredentialRef, { kind: 'env' });
121
+ return {
122
+ id: entry.id,
123
+ display_name: scalarField(parsed.lines, entry, 'displayName') ?? entry.id,
124
+ // settings.yaml is the Harness user layer. Crew profile/base
125
+ // declarations are reported separately by provider-profile-store.
126
+ origin: 'dynamic',
127
+ ownership: 'dynamic-user',
128
+ file,
129
+ declaration_authority: { kind: 'harness-settings', locator: `llm-pi-ai.providers.${entry.id}` },
130
+ ...(credential.value ? { credential_ref: { kind: 'env', name_or_handle: credential.value, ownership: 'unknown' } } : {}),
131
+ ...(credential.redacted ? { credential_status: 'present-redacted' } : {}),
132
+ };
133
+ }),
134
+ };
135
+ }
136
+
137
+ /** Return the same bounded provider projection as the profile parser, adapted
138
+ * to the Harness settings indentation. This is used only for rollback CAS and
139
+ * never exposes credential values. */
140
+ export function readProviderSettingsMaterialization(source, { providerId, file = 'harness/settings.yaml' } = {}) {
141
+ const parsed = parseProviderMap(source);
142
+ if (!parsed.ok) return { ok: false, code: parsed.code };
143
+ const entry = parsed.entries.find((candidate) => candidate.id === providerId);
144
+ if (!entry) return { ok: false, code: 'PROVIDER_NOT_FOUND' };
145
+ const block = parsed.lines.slice(entry.start, entry.end).map((line) => ` ${line}`);
146
+ const synthetic = ['- id: llm-pi-ai', ' config:', ' providers:', ...block, ''].join('\n');
147
+ const result = readProviderMaterialization(synthetic, { providerId, file });
148
+ return result.ok ? { ...result, provider: { ...result.provider, source_file: file } } : result;
149
+ }
150
+
151
+ export function readHarnessDefault(source) {
152
+ const parsed = parseDefaultModel(source);
153
+ if (!parsed.ok) return { ok: false, code: parsed.code };
154
+ return { ok: true, provider: parsed.provider, model: parsed.model, locator: 'agent-default-model' };
155
+ }
156
+
157
+ function yamlScalar(value, max = 2048) {
158
+ if (typeof value !== 'string' || !value.trim() || value.length > max || /[\r\n]/u.test(value)) return null;
159
+ return JSON.stringify(value.trim());
160
+ }
161
+
162
+ /**
163
+ * Add one already-sanitized provider projection to the Harness user layer.
164
+ * The operation is pure and revision-bound; callers perform the atomic write.
165
+ */
166
+ export function addProviderSettings(source, { provider, expectedRevision } = {}) {
167
+ const currentRevision = typeof source === 'string' ? sha256(source) : null;
168
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) return { ok: false, code: 'PROVIDER_SETTINGS_CHANGED', revision: currentRevision };
169
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider) || typeof provider.id !== 'string' || !PROVIDER_ID.test(provider.id)) {
170
+ return { ok: false, code: 'PROVIDER_MATERIALIZATION_INVALID', revision: currentRevision };
171
+ }
172
+ const parsed = parseProviderMap(source);
173
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision: currentRevision };
174
+ if (parsed.entries.some((entry) => entry.id === provider.id)) return { ok: false, code: 'PROVIDER_SETTINGS_PROVIDER_EXISTS', revision: currentRevision };
175
+ const displayName = yamlScalar(provider.display_name ?? provider.id, 256);
176
+ if (!displayName) return { ok: false, code: 'PROVIDER_MATERIALIZATION_INVALID', revision: currentRevision };
177
+ const lines = [` ${provider.id}:`, ` displayName: ${displayName}`];
178
+ const api = yamlScalar(provider.api, 128);
179
+ const baseUrl = yamlScalar(provider.base_url, 2048);
180
+ const credential = provider.credential_ref ? classifyCredentialReference(provider.credential_ref, { kind: 'env' }).value : null;
181
+ if (provider.credential_ref && !credential) return { ok: false, code: 'PROVIDER_CREDENTIAL_REFERENCE_UNSAFE', revision: currentRevision };
182
+ if (api) lines.push(` api: ${api}`);
183
+ if (baseUrl) lines.push(` baseURL: ${baseUrl}`);
184
+ if (credential) lines.push(` apiKeyEnv: ${credential}`);
185
+ const models = Array.isArray(provider.models) ? provider.models.filter((model) => model && typeof model.id === 'string' && model.id.trim() && model.id.length <= 256).slice(0, 256) : [];
186
+ if (models.length > 0) {
187
+ lines.push(' models:');
188
+ for (const model of models) {
189
+ const id = yamlScalar(model.id, 256);
190
+ if (!id) continue;
191
+ lines.push(` - id: ${id}`);
192
+ const name = yamlScalar(model.name, 256);
193
+ if (name) lines.push(` name: ${name}`);
194
+ if (Number.isSafeInteger(model.context_window) && model.context_window > 0) lines.push(` contextWindow: ${model.context_window}`);
195
+ if (Number.isSafeInteger(model.max_tokens) && model.max_tokens > 0) lines.push(` maxTokens: ${model.max_tokens}`);
196
+ if (Array.isArray(model.input) && model.input.every((value) => value === 'text' || value === 'image')) lines.push(` input: [${[...new Set(model.input)].join(', ')}]`);
197
+ if (model.reasoning_efforts && typeof model.reasoning_efforts === 'object' && !Array.isArray(model.reasoning_efforts)) {
198
+ lines.push(' reasoningEfforts:');
199
+ for (const [key, value] of Object.entries(model.reasoning_efforts)) if (/^[A-Za-z][A-Za-z0-9_-]*$/u.test(key) && (value === null || yamlScalar(value, 256))) lines.push(` ${key}: ${value === null ? 'null' : yamlScalar(value, 256)}`);
200
+ }
201
+ if (model.compat && typeof model.compat === 'object' && !Array.isArray(model.compat) && Object.keys(model.compat).length === 0) lines.push(' compat: {}');
202
+ }
203
+ }
204
+ const nextLines = [...parsed.lines];
205
+ const insertAt = parsed.blockEnd;
206
+ if (/^ {2}providers:\s*\{\s*\}\s*$/u.test(nextLines[parsed.providersLine])) nextLines[parsed.providersLine] = ' providers:';
207
+ nextLines.splice(insertAt, 0, ...lines);
208
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
209
+ const text = nextLines.join(newline);
210
+ return { ok: true, text, added: provider.id, revision: sha256(text) };
211
+ }
212
+
213
+ export function replaceHarnessDefault(source, { provider, model, expectedRevision } = {}) {
214
+ const currentRevision = typeof source === 'string' ? sha256(source) : null;
215
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) return { ok: false, code: 'PROVIDER_SETTINGS_CHANGED', revision: currentRevision };
216
+ if (typeof provider !== 'string' || !PROVIDER_ID.test(provider) || typeof model !== 'string' || !model.trim()) return { ok: false, code: 'PROVIDER_DEFAULT_REPLACEMENT_REQUIRED', revision: currentRevision };
217
+ const parsed = parseDefaultModel(source);
218
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision: currentRevision };
219
+ const lines = [...parsed.lines];
220
+ lines[parsed.providerLine] = ` provider: ${provider.trim()}`;
221
+ lines[parsed.modelLine] = ` model: ${model.trim()}`;
222
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
223
+ const text = lines.join(newline);
224
+ return { ok: true, text, previous: { provider: parsed.provider, model: parsed.model }, revision: sha256(text) };
225
+ }
226
+
227
+ /**
228
+ * Apply all Harness settings mutations needed by one provider-delete plan in
229
+ * memory, then return one revision-bound text result for a single atomic write.
230
+ * This prevents replacing agent-default-model and removing its provider from
231
+ * racing each other on the same settings revision.
232
+ */
233
+ export function mutateProviderSettings(source, {
234
+ providerId = null,
235
+ removeProvider = false,
236
+ replacementDefault = null,
237
+ replacementModel = null,
238
+ expectedRevision,
239
+ } = {}) {
240
+ const currentRevision = typeof source === 'string' ? sha256(source) : null;
241
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) return { ok: false, code: 'PROVIDER_SETTINGS_CHANGED', revision: currentRevision };
242
+ const parsed = removeProvider ? parseProviderMap(source) : { ok: true, lines: typeof source === 'string' ? source.split(/\r?\n/u) : [] };
243
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision: currentRevision };
244
+ let providerEntry = null;
245
+ if (removeProvider) {
246
+ if (typeof providerId !== 'string' || !PROVIDER_ID.test(providerId)) return { ok: false, code: 'PROVIDER_NOT_FOUND', revision: currentRevision };
247
+ providerEntry = parsed.entries.find((entry) => entry.id === providerId) ?? null;
248
+ if (!providerEntry) return { ok: false, code: 'PROVIDER_NOT_FOUND', revision: currentRevision };
249
+ }
250
+ let defaultParsed = null;
251
+ if (replacementDefault !== null || replacementModel !== null) {
252
+ if (typeof replacementDefault !== 'string' || !PROVIDER_ID.test(replacementDefault) || typeof replacementModel !== 'string' || !replacementModel.trim()) return { ok: false, code: 'PROVIDER_DEFAULT_REPLACEMENT_REQUIRED', revision: currentRevision };
253
+ defaultParsed = parseDefaultModel(source);
254
+ if (!defaultParsed.ok) return { ok: false, code: defaultParsed.code, revision: currentRevision };
255
+ if (providerId && defaultParsed.provider !== providerId) return { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_CHANGED', revision: currentRevision };
256
+ }
257
+ const ranges = providerEntry ? [[providerEntry.start, providerEntry.end]] : [];
258
+ const finalProvider = providerEntry && parsed.entries.length === 1;
259
+ const nextLines = parsed.lines.map((line, index) => {
260
+ if (ranges.some(([start, end]) => index >= start && index < end)) return null;
261
+ if (finalProvider && index === parsed.providersLine) return ' providers: {}';
262
+ if (defaultParsed?.providerLine === index) return ` provider: ${replacementDefault.trim()}`;
263
+ if (defaultParsed?.modelLine === index) return ` model: ${replacementModel.trim()}`;
264
+ return line;
265
+ }).filter((line) => line !== null);
266
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
267
+ const text = nextLines.join(newline);
268
+ return {
269
+ ok: true,
270
+ text,
271
+ ...(providerEntry ? { removed: [providerId], remaining: parsed.entries.filter((entry) => entry.id !== providerId).map((entry) => entry.id) } : { removed: [], remaining: parsed.entries?.map((entry) => entry.id) ?? [] }),
272
+ ...(defaultParsed ? { previous_default: { provider: defaultParsed.provider, model: defaultParsed.model } } : {}),
273
+ revision: sha256(text),
274
+ };
275
+ }
276
+
277
+ export function removeProviderSettings(source, { providerIds = [], expectedRevision } = {}) {
278
+ const currentRevision = typeof source === 'string' ? sha256(source) : null;
279
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) return { ok: false, code: 'PROVIDER_SETTINGS_CHANGED', revision: currentRevision };
280
+ const requested = [...new Set(providerIds.filter((id) => typeof id === 'string' && PROVIDER_ID.test(id)))];
281
+ if (requested.length === 0 || requested.length !== providerIds.length) return { ok: false, code: 'PROVIDER_NOT_FOUND', revision: currentRevision };
282
+ let next = source;
283
+ const results = [];
284
+ for (const id of requested) {
285
+ const result = mutateProviderSettings(next, { providerId: id, removeProvider: true });
286
+ if (!result.ok) return { ...result, revision: currentRevision };
287
+ next = result.text;
288
+ results.push(result);
289
+ }
290
+ return { ok: true, text: next, removed: requested, remaining: results.at(-1)?.remaining ?? [], revision: sha256(next) };
291
+ }
@@ -0,0 +1,67 @@
1
+ // Persistent token-fenced lock shared by every lifecycle operation that can
2
+ // mutate the Harness profile/settings stores. A stale lock is reclaimed only
3
+ // while a serialized claim is held; malformed metadata fails closed.
4
+
5
+ import { randomUUID } from 'node:crypto';
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { dirname, resolve as resolvePath } from 'node:path';
8
+
9
+ function safeToken(value) { return typeof value === 'string' && /^[A-Za-z0-9-]{16,128}$/u.test(value); }
10
+ function readOwner(file) {
11
+ try {
12
+ const owner = JSON.parse(readFileSync(file, 'utf8'));
13
+ if (!owner || typeof owner !== 'object' || Array.isArray(owner) || !Number.isInteger(owner.pid) || owner.pid <= 0 || !safeToken(owner.token)) return { ok: false, malformed: true };
14
+ return { ok: true, owner };
15
+ } catch { return { ok: false, malformed: true }; }
16
+ }
17
+ function processAlive(pid) {
18
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
19
+ }
20
+
21
+ export async function acquireProviderStoreLock(file) {
22
+ if (typeof file !== 'string' || !file.trim()) throw Object.assign(new Error('provider store lock path is required'), { code: 'PROVIDER_STORE_LOCK_UNAVAILABLE' });
23
+ const lockFile = resolvePath(file);
24
+ mkdirSync(dirname(lockFile), { recursive: true });
25
+ const claimFile = `${lockFile}.claim`;
26
+ const token = randomUUID();
27
+ let claimed = false;
28
+ try {
29
+ try { mkdirSync(claimFile); claimed = true; }
30
+ catch (error) { throw Object.assign(new Error('provider store lock is busy'), { code: error?.code === 'EEXIST' ? 'PROVIDER_STORE_LOCK_BUSY' : 'PROVIDER_STORE_LOCK_UNAVAILABLE' }); }
31
+ while (true) {
32
+ try {
33
+ writeFileSync(lockFile, JSON.stringify({ pid: process.pid, token, created_at: new Date().toISOString() }) + '\n', { flag: 'wx' });
34
+ return {
35
+ ok: true,
36
+ token,
37
+ release: async () => {
38
+ const current = readOwner(lockFile);
39
+ if (current.ok && current.owner.token === token) rmSync(lockFile, { force: true });
40
+ },
41
+ };
42
+ } catch (error) {
43
+ if (error?.code !== 'EEXIST') throw Object.assign(new Error('provider store lock is unavailable'), { code: 'PROVIDER_STORE_LOCK_UNAVAILABLE' });
44
+ const current = readOwner(lockFile);
45
+ if (!current.ok) throw Object.assign(new Error('provider store lock metadata is invalid'), { code: 'PROVIDER_STORE_LOCK_BUSY' });
46
+ if (processAlive(current.owner.pid)) throw Object.assign(new Error('provider store lock is busy'), { code: 'PROVIDER_STORE_LOCK_BUSY' });
47
+ rmSync(lockFile, { force: true });
48
+ }
49
+ }
50
+ } finally { if (claimed) rmSync(claimFile, { recursive: true, force: true }); }
51
+ }
52
+
53
+ export async function recoverProviderStoreLock(file, { confirm = false } = {}) {
54
+ if (confirm !== true) return { ok: false, code: 'PROVIDER_STORE_LOCK_CONFIRM_REQUIRED' };
55
+ if (typeof file !== 'string' || !file.trim()) return { ok: false, code: 'PROVIDER_STORE_LOCK_UNAVAILABLE' };
56
+ const lockFile = resolvePath(file);
57
+ const claimFile = `${lockFile}.claim`;
58
+ try { mkdirSync(dirname(lockFile), { recursive: true }); mkdirSync(claimFile); }
59
+ catch (error) { return { ok: false, code: error?.code === 'EEXIST' ? 'PROVIDER_STORE_LOCK_BUSY' : 'PROVIDER_STORE_LOCK_UNAVAILABLE' }; }
60
+ try {
61
+ if (!existsSync(lockFile)) return { ok: true, recovered: false };
62
+ const current = readOwner(lockFile);
63
+ if (current.ok && processAlive(current.owner.pid)) return { ok: false, code: 'PROVIDER_STORE_LOCK_BUSY' };
64
+ rmSync(lockFile, { force: true });
65
+ return { ok: true, recovered: true };
66
+ } finally { rmSync(claimFile, { recursive: true, force: true }); }
67
+ }
@@ -5,20 +5,20 @@
5
5
  // - HUB_PROTOCOL_VERSION changes only when Hub <-> MCP wire semantics become
6
6
  // incompatible.
7
7
  //
8
- // Keep this module dependency-light so Hub, MCP and tests all use the exact
9
- // same compatibility rules.
10
-
11
- import { randomUUID } from 'node:crypto';
12
-
13
- // Production Crew execution is intentionally bound to the isolated 3210 Hub.
14
- // These fields are public provenance only; no credential or session secret is
15
- // included in the identity contract.
16
- export const PRODUCTION_EXECUTION_PLANE = 'hub-3210';
17
- export const PRODUCTION_PROFILE = 'dsh-crew';
18
- export const PRODUCTION_LISTEN_PORT = 3210;
19
- const RUNTIME_ID = randomUUID();
20
-
21
- export const RUNTIME_VERSION = '1.0.1';
8
+ // Keep this module dependency-light so Hub, MCP and tests all use the exact
9
+ // same compatibility rules.
10
+
11
+ import { randomUUID } from 'node:crypto';
12
+
13
+ // Production Crew execution is intentionally bound to the isolated 3210 Hub.
14
+ // These fields are public provenance only; no credential or session secret is
15
+ // included in the identity contract.
16
+ export const PRODUCTION_EXECUTION_PLANE = 'hub-3210';
17
+ export const PRODUCTION_PROFILE = 'dsh-crew';
18
+ export const PRODUCTION_LISTEN_PORT = 3210;
19
+ const RUNTIME_ID = randomUUID();
20
+
21
+ export const RUNTIME_VERSION = '1.0.3';
22
22
  export const HUB_PROTOCOL_VERSION = 1;
23
23
 
24
24
  export const HUB_CAPABILITIES = Object.freeze([
@@ -33,17 +33,18 @@ export const HUB_CAPABILITIES = Object.freeze([
33
33
  'config',
34
34
  'canonical-events',
35
35
  'evidence',
36
- 'profiles',
37
- 'workspace-context',
38
- 'extension-contract',
39
- 'provider-inventory',
40
- 'provider-lifecycle-v1',
41
- 'provider-health-v1',
42
- 'provider-probe-stream-v1',
43
- 'credential-reference-inventory-v1',
44
- 'credential-purge-v1',
45
- 'runtime-provenance-v1',
46
- ]);
36
+ 'profiles',
37
+ 'workspace-context',
38
+ 'extension-contract',
39
+ 'provider-inventory',
40
+ 'provider-layer-migration-v1',
41
+ 'provider-lifecycle-v1',
42
+ 'provider-health-v1',
43
+ 'provider-probe-stream-v1',
44
+ 'credential-reference-inventory-v1',
45
+ 'credential-purge-v1',
46
+ 'runtime-provenance-v1',
47
+ ]);
47
48
 
48
49
  // Capabilities the current MCP workflow depends on for full Hub execution.
49
50
  // Additional capabilities may be advertised without breaking compatibility.
@@ -60,12 +61,12 @@ export const HUB_COMPATIBILITY_CODES = Object.freeze({
60
61
  UNREACHABLE: 'HUB_UNREACHABLE',
61
62
  HTTP_ERROR: 'HUB_HTTP_ERROR',
62
63
  SERVICE_MISMATCH: 'HUB_SERVICE_MISMATCH',
63
- PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
64
- PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
65
- CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
66
- PROVENANCE_MISSING: 'HUB_PROVENANCE_MISSING',
67
- EXECUTION_PLANE_MISMATCH: 'HUB_EXECUTION_PLANE_MISMATCH',
68
- LISTEN_PORT_MISMATCH: 'HUB_LISTEN_PORT_MISMATCH',
64
+ PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
65
+ PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
66
+ CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
67
+ PROVENANCE_MISSING: 'HUB_PROVENANCE_MISSING',
68
+ EXECUTION_PLANE_MISMATCH: 'HUB_EXECUTION_PLANE_MISMATCH',
69
+ LISTEN_PORT_MISMATCH: 'HUB_LISTEN_PORT_MISMATCH',
69
70
  });
70
71
 
71
72
  function normalizedCapabilities(value) {
@@ -74,17 +75,17 @@ function normalizedCapabilities(value) {
74
75
  }
75
76
 
76
77
  export function getHubRuntimeIdentity() {
77
- return {
78
- service: 'dsh-crew-hub',
79
- surface: 'native-crew-harness',
80
- ui_role: 'runtime',
81
- runtime_version: RUNTIME_VERSION,
82
- protocol_version: HUB_PROTOCOL_VERSION,
83
- execution_plane: PRODUCTION_EXECUTION_PLANE,
84
- profile: PRODUCTION_PROFILE,
85
- listen_port: PRODUCTION_LISTEN_PORT,
86
- runtime_id: RUNTIME_ID,
87
- capabilities: [...HUB_CAPABILITIES],
78
+ return {
79
+ service: 'dsh-crew-hub',
80
+ surface: 'native-crew-harness',
81
+ ui_role: 'runtime',
82
+ runtime_version: RUNTIME_VERSION,
83
+ protocol_version: HUB_PROTOCOL_VERSION,
84
+ execution_plane: PRODUCTION_EXECUTION_PLANE,
85
+ profile: PRODUCTION_PROFILE,
86
+ listen_port: PRODUCTION_LISTEN_PORT,
87
+ runtime_id: RUNTIME_ID,
88
+ capabilities: [...HUB_CAPABILITIES],
88
89
  };
89
90
  }
90
91
 
@@ -95,51 +96,51 @@ export function getHubRuntimeIdentity() {
95
96
  * `compatible` means the response is from dsh-crew and satisfies the current
96
97
  * protocol + required capability contract.
97
98
  */
98
- export function evaluateHubHandshake(body, { requiredCapabilities = REQUIRED_HUB_CAPABILITIES, strictProduction = false } = {}) {
99
+ export function evaluateHubHandshake(body, { requiredCapabilities = REQUIRED_HUB_CAPABILITIES, strictProduction = false } = {}) {
99
100
  const capabilities = normalizedCapabilities(body?.capabilities);
100
101
  const base = {
101
102
  reachable: true,
102
103
  compatible: false,
103
104
  service: typeof body?.service === 'string' ? body.service : null,
104
- runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
105
- protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
106
- execution_plane: typeof body?.execution_plane === 'string' ? body.execution_plane : null,
107
- profile: typeof body?.profile === 'string' ? body.profile : null,
108
- listen_port: Number.isInteger(body?.listen_port) ? body.listen_port : null,
109
- runtime_id: typeof body?.runtime_id === 'string' ? body.runtime_id : null,
105
+ runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
106
+ protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
107
+ execution_plane: typeof body?.execution_plane === 'string' ? body.execution_plane : null,
108
+ profile: typeof body?.profile === 'string' ? body.profile : null,
109
+ listen_port: Number.isInteger(body?.listen_port) ? body.listen_port : null,
110
+ runtime_id: typeof body?.runtime_id === 'string' ? body.runtime_id : null,
110
111
  capabilities,
111
112
  missing_capabilities: [],
112
113
  code: null,
113
114
  };
114
115
 
115
- if (body?.service !== 'dsh-crew-hub') {
116
- return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
117
- }
118
-
119
- // An explicit execution identity that is not the isolated 3210 Crew plane
120
- // must never be accepted as the production Hub. Missing legacy fields remain
121
- // compatible with the older protocol contract and are handled below.
122
- if (body?.execution_plane !== undefined && body.execution_plane !== PRODUCTION_EXECUTION_PLANE) {
123
- return { ...base, code: HUB_COMPATIBILITY_CODES.EXECUTION_PLANE_MISMATCH };
124
- }
125
- if (body?.listen_port !== undefined && body.listen_port !== PRODUCTION_LISTEN_PORT) {
126
- return { ...base, code: HUB_COMPATIBILITY_CODES.LISTEN_PORT_MISMATCH };
127
- }
116
+ if (body?.service !== 'dsh-crew-hub') {
117
+ return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
118
+ }
119
+
120
+ // An explicit execution identity that is not the isolated 3210 Crew plane
121
+ // must never be accepted as the production Hub. Missing legacy fields remain
122
+ // compatible with the older protocol contract and are handled below.
123
+ if (body?.execution_plane !== undefined && body.execution_plane !== PRODUCTION_EXECUTION_PLANE) {
124
+ return { ...base, code: HUB_COMPATIBILITY_CODES.EXECUTION_PLANE_MISMATCH };
125
+ }
126
+ if (body?.listen_port !== undefined && body.listen_port !== PRODUCTION_LISTEN_PORT) {
127
+ return { ...base, code: HUB_COMPATIBILITY_CODES.LISTEN_PORT_MISMATCH };
128
+ }
128
129
 
129
130
  if (!Number.isInteger(body?.protocol_version)) {
130
131
  return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISSING };
131
132
  }
132
133
 
133
- if (body.protocol_version !== HUB_PROTOCOL_VERSION) {
134
- return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISMATCH };
135
- }
136
-
137
- if (strictProduction && !(body?.execution_plane === PRODUCTION_EXECUTION_PLANE
138
- && body?.profile === PRODUCTION_PROFILE
139
- && Number.isInteger(body?.listen_port) && body.listen_port === PRODUCTION_LISTEN_PORT
140
- && typeof body?.runtime_id === 'string' && body.runtime_id.trim().length > 0)) {
141
- return { ...base, code: HUB_COMPATIBILITY_CODES.PROVENANCE_MISSING };
142
- }
134
+ if (body.protocol_version !== HUB_PROTOCOL_VERSION) {
135
+ return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISMATCH };
136
+ }
137
+
138
+ if (strictProduction && !(body?.execution_plane === PRODUCTION_EXECUTION_PLANE
139
+ && body?.profile === PRODUCTION_PROFILE
140
+ && Number.isInteger(body?.listen_port) && body.listen_port === PRODUCTION_LISTEN_PORT
141
+ && typeof body?.runtime_id === 'string' && body.runtime_id.trim().length > 0)) {
142
+ return { ...base, code: HUB_COMPATIBILITY_CODES.PROVENANCE_MISSING };
143
+ }
143
144
 
144
145
  const required = normalizedCapabilities(requiredCapabilities);
145
146
  const advertised = new Set(capabilities);