@ran-sh/dsh-crew 0.5.6 → 0.5.7

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 (39) hide show
  1. package/README.md +14 -7
  2. package/README.zh.md +12 -7
  3. package/docs/installation.md +1 -1
  4. package/docs/ui-surfaces.md +65 -46
  5. package/lib/client.js +345 -22
  6. package/official-web-bridge/lib/client.js +345 -22
  7. package/package.json +1 -1
  8. package/src/client/collapsible-sections.mjs +3 -2
  9. package/src/client/index.tsx +227 -51
  10. package/src/config-readiness.mjs +56 -5
  11. package/src/extension-contract.mjs +9 -5
  12. package/src/hub/index.mjs +854 -445
  13. package/src/hub-client.mjs +24 -4
  14. package/src/hub-compatibility.mjs +23 -7
  15. package/src/install/install.mjs +4 -4
  16. package/src/install/npx-lifecycle.mjs +263 -118
  17. package/src/job-contracts.mjs +19 -3
  18. package/src/jobs.mjs +2 -4
  19. package/src/local-request-guard.mjs +60 -0
  20. package/src/mcp-runtime.mjs +7 -5
  21. package/src/model-routing.mjs +141 -49
  22. package/src/multimodal.mjs +0 -0
  23. package/src/official-web-bridge.mjs +242 -84
  24. package/src/policy.mjs +62 -31
  25. package/src/provider-config-scrub.mjs +71 -0
  26. package/src/provider-delete-adapters.mjs +424 -0
  27. package/src/provider-health.mjs +124 -0
  28. package/src/provider-inventory.mjs +132 -0
  29. package/src/provider-lifecycle-state.mjs +81 -0
  30. package/src/provider-lifecycle.mjs +214 -0
  31. package/src/provider-profile-store.mjs +171 -0
  32. package/src/readiness-matrix.mjs +11 -5
  33. package/src/removable-waiter.mjs +29 -0
  34. package/src/runtime-identity.mjs +43 -13
  35. package/src/server.mjs +34 -16
  36. package/src/status-shard.mjs +13 -2
  37. package/src/workflow-runtime.mjs +44 -10
  38. package/windows/start-dsh-crew.cmd +3 -3
  39. package/windows/start-dsh-crew.ps1 +49 -5
@@ -0,0 +1,81 @@
1
+ // Persistent, secret-free lifecycle intent for Harness provider management.
2
+ // Tombstones prevent an explicitly removed managed route from being re-seeded;
3
+ // transactions retain only bounded audit metadata needed for recovery.
4
+
5
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
6
+ const REVISION_PATTERN = /^[a-f0-9]{64}$/;
7
+ const STATES = new Set([
8
+ 'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
9
+ 'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
10
+ ]);
11
+
12
+ function validId(value) {
13
+ return typeof value === 'string' && ID_PATTERN.test(value);
14
+ }
15
+
16
+ function validRevision(value) {
17
+ return typeof value === 'string' && REVISION_PATTERN.test(value);
18
+ }
19
+
20
+ function clone(value) {
21
+ return JSON.parse(JSON.stringify(value));
22
+ }
23
+
24
+ function normalizedTransaction(value) {
25
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
26
+ if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
27
+ return {
28
+ provider_id: value.provider_id,
29
+ state: value.state,
30
+ ...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
31
+ };
32
+ }
33
+
34
+ /** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
35
+ export function normalizeProviderLifecycleState(input = {}) {
36
+ const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
37
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
38
+ if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
39
+ for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
40
+ }
41
+ if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
42
+ for (const [transactionId, value] of Object.entries(input.transactions)) {
43
+ if (!validId(transactionId)) continue;
44
+ const transaction = normalizedTransaction(value);
45
+ if (transaction) out.transactions[transactionId] = transaction;
46
+ }
47
+ }
48
+ if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
49
+ for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function requireProviderId(providerId) {
55
+ if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
56
+ return providerId;
57
+ }
58
+
59
+ export function markProviderTombstone(state, providerId) {
60
+ const id = requireProviderId(providerId);
61
+ const out = normalizeProviderLifecycleState(state);
62
+ out.tombstones[id] = 'absent';
63
+ return out;
64
+ }
65
+
66
+ export function clearProviderTombstone(state, providerId) {
67
+ const id = requireProviderId(providerId);
68
+ const out = normalizeProviderLifecycleState(state);
69
+ delete out.tombstones[id];
70
+ return out;
71
+ }
72
+
73
+ /** Record only non-secret transaction metadata; credential refs/values are ignored. */
74
+ export function recordProviderTransaction(state, transaction = {}) {
75
+ const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
76
+ const normalized = normalizedTransaction(transaction);
77
+ if (!normalized) throw new TypeError('provider transaction metadata is invalid');
78
+ const out = normalizeProviderLifecycleState(state);
79
+ out.transactions[transactionId] = clone(normalized);
80
+ return out;
81
+ }
@@ -0,0 +1,214 @@
1
+ // Provider deletion planning and transaction state machine.
2
+ //
3
+ // This module is intentionally side-effect free until executeProviderDelete is
4
+ // given explicit adapters. The live profile, credentials and restart hooks are
5
+ // owned by the caller; plans never carry secret values.
6
+
7
+ import { randomUUID } from 'node:crypto';
8
+
9
+ export const PROVIDER_DELETE_STATES = Object.freeze([
10
+ 'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
11
+ 'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
12
+ ]);
13
+
14
+ const TRANSITIONS = Object.freeze({
15
+ PLANNED: Object.freeze(['BLOCKED', 'APPLIED', 'FAILED']),
16
+ BLOCKED: Object.freeze(['PLANNED', 'FAILED']),
17
+ APPLIED: Object.freeze(['RESTART_PENDING', 'FAILED']),
18
+ RESTART_PENDING: Object.freeze(['VERIFYING', 'FAILED']),
19
+ VERIFYING: Object.freeze(['VERIFIED', 'FAILED']),
20
+ VERIFIED: Object.freeze(['ROLLBACK_PENDING']),
21
+ ROLLBACK_PENDING: Object.freeze(['ROLLED_BACK', 'FAILED']),
22
+ ROLLED_BACK: Object.freeze([]),
23
+ FAILED: Object.freeze([]),
24
+ });
25
+
26
+ const DELETEABLE_OWNERSHIPS = new Set(['crew-managed-profile', 'user-managed-profile', 'dynamic-user']);
27
+ const CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/;
28
+
29
+ function text(value) {
30
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
31
+ }
32
+
33
+ function boundedCode(value, fallback = 'PROVIDER_DELETE_FAILED') {
34
+ const candidate = text(value);
35
+ return candidate && CODE_PATTERN.test(candidate) ? candidate : fallback;
36
+ }
37
+
38
+ function fail(code) {
39
+ return { ok: false, code };
40
+ }
41
+
42
+ function recordById(inventory, providerId) {
43
+ const records = Array.isArray(inventory?.records) ? inventory.records : [];
44
+ return records.find((record) => record?.id === providerId) ?? null;
45
+ }
46
+
47
+ function activeJobCount(record, activeJobs) {
48
+ if (Array.isArray(activeJobs)) return activeJobs.filter((job) => job?.provider === record.id).length;
49
+ return Number.isInteger(record?.references?.active_jobs) ? Math.max(0, record.references.active_jobs) : 0;
50
+ }
51
+
52
+ function sanitizedCredentialRefs(record) {
53
+ return (Array.isArray(record?.credential_refs) ? record.credential_refs : []).map((ref) => ({
54
+ kind: text(ref?.kind) ?? 'unknown',
55
+ name_or_handle: text(ref?.name_or_handle) ?? 'unknown',
56
+ ownership: text(ref?.ownership) ?? 'unknown',
57
+ }));
58
+ }
59
+
60
+ function pushState(events, state) {
61
+ events.push({ state, at: new Date().toISOString() });
62
+ }
63
+
64
+ /** Return whether a provider deletion state transition is legal. */
65
+ export function canTransitionProviderDelete(from, to) {
66
+ return PROVIDER_DELETE_STATES.includes(from) && TRANSITIONS[from]?.includes(to) === true;
67
+ }
68
+
69
+ /**
70
+ * Validate a provider deletion request and return a secret-free impact plan.
71
+ * The caller must provide a replacement when deleting the active Harness
72
+ * Default; running jobs and unknown ownership always fail closed.
73
+ */
74
+ export function planProviderDelete({ providerId, inventory, activeJobs = [], replacementDefault = null, expectedRevision } = {}) {
75
+ const id = text(providerId);
76
+ if (!id) return fail('PROVIDER_NOT_FOUND');
77
+ const record = recordById(inventory, id);
78
+ if (!record) return fail('PROVIDER_NOT_FOUND');
79
+ if (!DELETEABLE_OWNERSHIPS.has(record.ownership)) return fail('PROVIDER_OWNERSHIP_AMBIGUOUS');
80
+ if (record.desired_state === 'absent') return fail('PROVIDER_ALREADY_ABSENT');
81
+ if (activeJobCount(record, activeJobs) > 0) return fail('PROVIDER_IN_USE');
82
+
83
+ const replacement = text(replacementDefault);
84
+ const replacementRecord = replacement ? recordById(inventory, replacement) : null;
85
+ if (record.references?.harness_default) {
86
+ if (!replacement || replacement === id || !replacementRecord || replacementRecord.desired_state === 'absent') {
87
+ return fail('PROVIDER_DEFAULT_REPLACEMENT_REQUIRED');
88
+ }
89
+ if (typeof replacementRecord.models?.[0] !== 'string' || replacementRecord.models[0].trim() === '') {
90
+ return fail('PROVIDER_DEFAULT_REPLACEMENT_MODEL_REQUIRED');
91
+ }
92
+ }
93
+
94
+ return {
95
+ ok: true,
96
+ plan: {
97
+ plan_id: randomUUID(),
98
+ provider_id: id,
99
+ provider: { id, display_name: text(record.display_name) ?? id },
100
+ expected_revision: text(expectedRevision) ?? text(record.declaration?.revision),
101
+ replacement_default: replacement,
102
+ was_harness_default: record.references?.harness_default === true,
103
+ ...(replacement
104
+ ? { replacement_default_model: text(replacementRecord?.models?.[0]) }
105
+ : {}),
106
+ will_remove: [
107
+ 'profile declaration', 'runtime desired registration',
108
+ 'worker priority references', 'reviewer priority references',
109
+ 'Harness Default reference', 'cache/health state',
110
+ ],
111
+ credential_refs: sanitizedCredentialRefs(record),
112
+ restart_required: true,
113
+ rollback: { config_snapshot_available: true, credentials_included: false },
114
+ },
115
+ };
116
+ }
117
+
118
+ function hookError(name) {
119
+ return Object.assign(new Error(`provider delete hook missing: ${name}`), { code: 'PROVIDER_LIFECYCLE_HOOK_MISSING' });
120
+ }
121
+
122
+ /**
123
+ * Execute a deletion plan through injected adapters. All mutating adapters are
124
+ * explicit so the Hub/API layer can provide atomic profile/config stores and a
125
+ * supervisor that only restarts the owned 3210 child.
126
+ */
127
+ export async function executeProviderDelete(plan, hooks = {}, { deferRestart = false } = {}) {
128
+ let state = 'PLANNED';
129
+ const events = [];
130
+ pushState(events, state);
131
+ let errorCode = null;
132
+ let verification = null;
133
+ let mutationsStarted = false;
134
+ let rollbackAttempted = false;
135
+ let rollbackErrorCode = null;
136
+ let rollbackRuntimeRestarted = null;
137
+ let rollbackRuntimeVerified = null;
138
+ const transition = (next) => {
139
+ if (!canTransitionProviderDelete(state, next)) throw Object.assign(new Error(`illegal provider delete transition ${state} -> ${next}`), { code: 'PROVIDER_DELETE_STATE_INVALID' });
140
+ state = next;
141
+ pushState(events, state);
142
+ };
143
+ try {
144
+ if (!plan?.plan_id || !plan?.provider_id) throw Object.assign(new Error('provider delete plan is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
145
+ for (const name of ['backup', 'markTombstone', 'scrubReferences', 'removeDeclarations', 'restart', 'rollback']) {
146
+ if (typeof hooks[name] !== 'function') throw hookError(name);
147
+ }
148
+ await hooks.backup(plan);
149
+ // From this point onward at least one destructive adapter may have
150
+ // committed; any later failure must attempt compensation before returning.
151
+ mutationsStarted = true;
152
+ await hooks.markTombstone(plan.provider_id, 'absent', plan);
153
+ await hooks.scrubReferences(plan);
154
+ await hooks.removeDeclarations(plan);
155
+ transition('APPLIED');
156
+ transition('RESTART_PENDING');
157
+ if (!deferRestart) {
158
+ const restarted = await hooks.restart(plan);
159
+ if (restarted?.ok === false) throw Object.assign(new Error('provider delete restart failed'), { code: restarted.code });
160
+ if (typeof hooks.verify !== 'function') throw hookError('verify');
161
+ transition('VERIFYING');
162
+ verification = await hooks.verify(plan);
163
+ if (verification?.providerAbsent !== true || verification?.routingClear !== true || verification?.tombstonePresent !== true) {
164
+ throw Object.assign(new Error('provider delete verification incomplete'), { code: 'PROVIDER_DELETE_VERIFY_FAILED' });
165
+ }
166
+ transition('VERIFIED');
167
+ }
168
+ } catch (error) {
169
+ errorCode = boundedCode(error?.code);
170
+ if (mutationsStarted && typeof hooks.rollback === 'function') {
171
+ rollbackAttempted = true;
172
+ try {
173
+ await hooks.rollback(plan);
174
+ if (typeof hooks.restartRollback === 'function') {
175
+ const restarted = await hooks.restartRollback(plan);
176
+ rollbackRuntimeRestarted = restarted?.ok !== false;
177
+ if (rollbackRuntimeRestarted && typeof hooks.verifyRollback === 'function') {
178
+ const verified = await hooks.verifyRollback(plan);
179
+ rollbackRuntimeVerified = verified?.ok !== false;
180
+ }
181
+ }
182
+ } catch (rollbackError) {
183
+ rollbackErrorCode = boundedCode(rollbackError?.code, 'PROVIDER_DELETE_ROLLBACK_FAILED');
184
+ rollbackRuntimeRestarted = false;
185
+ }
186
+ }
187
+ if (state !== 'FAILED' && canTransitionProviderDelete(state, 'FAILED')) transition('FAILED');
188
+ }
189
+ const result = {
190
+ transaction_id: plan?.plan_id ?? null,
191
+ provider_id: plan?.provider_id ?? null,
192
+ state,
193
+ error_code: errorCode,
194
+ rollback_attempted: rollbackAttempted,
195
+ rollback_error_code: rollbackErrorCode,
196
+ rollback_runtime_restarted: rollbackRuntimeRestarted,
197
+ rollback_runtime_verified: rollbackRuntimeVerified,
198
+ verification,
199
+ events,
200
+ };
201
+ if (typeof hooks.recordTransaction === 'function') {
202
+ try {
203
+ await hooks.recordTransaction(result, plan);
204
+ result.audit_recorded = true;
205
+ } catch (error) {
206
+ result.audit_recorded = false;
207
+ result.audit_error_code = boundedCode(error?.code, 'PROVIDER_LIFECYCLE_RECORD_FAILED');
208
+ }
209
+ }
210
+ if (typeof hooks.release === 'function') {
211
+ try { await hooks.release(plan); } catch {}
212
+ }
213
+ return result;
214
+ }
@@ -0,0 +1,171 @@
1
+ // Safe, structure-aware mutations for the Crew profile's provider patch.
2
+ //
3
+ // This module deliberately does not parse or return credential values. It only
4
+ // recognizes the known llm-pi-ai -> config -> providers mapping and performs
5
+ // bounded, revision-checked text edits while preserving unrelated patch items.
6
+
7
+ import { createHash } from 'node:crypto';
8
+
9
+ const PROVIDER_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+
11
+ function sha256(value) {
12
+ return createHash('sha256').update(value, 'utf8').digest('hex');
13
+ }
14
+
15
+ function indentOf(line) {
16
+ return (line.match(/^\s*/) ?? [''])[0].length;
17
+ }
18
+
19
+ function nonBlank(line) {
20
+ return line.trim() !== '' && !line.trimStart().startsWith('#');
21
+ }
22
+
23
+ function parseProviderMap(source) {
24
+ if (typeof source !== 'string') return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
25
+ const lines = source.split(/\r?\n/);
26
+ const llmStart = lines.findIndex((line) => /^-\s+id:\s*llm-pi-ai\s*$/.test(line));
27
+ if (llmStart < 0) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
28
+
29
+ let blockEnd = lines.length;
30
+ for (let index = llmStart + 1; index < lines.length; index += 1) {
31
+ // Only a true top-level sequence item can end the managed llm-pi-ai item;
32
+ // nested list entries are part of a provider value and must be validated.
33
+ if (indentOf(lines[index]) === 0 && /^-\s+/.test(lines[index])) {
34
+ blockEnd = index;
35
+ break;
36
+ }
37
+ }
38
+
39
+ const providersLine = lines.findIndex((line, index) => index > llmStart && index < blockEnd && /^\s+providers:\s*$/.test(line));
40
+ if (providersLine < 0) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
41
+ const providersIndent = indentOf(lines[providersLine]);
42
+ const providerIndent = providersIndent + 2;
43
+ const entries = [];
44
+
45
+ for (let index = providersLine + 1; index < blockEnd; index += 1) {
46
+ const line = lines[index];
47
+ if (!nonBlank(line)) continue;
48
+ const indent = indentOf(line);
49
+ if (indent <= providersIndent) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
50
+ if (indent !== providerIndent) {
51
+ if (/^\s*-\s+/.test(line)) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
52
+ continue;
53
+ }
54
+ const match = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*):\s*$/);
55
+ if (!match) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
56
+ const id = match[1];
57
+ let end = index + 1;
58
+ while (end < blockEnd) {
59
+ const next = lines[end];
60
+ if (nonBlank(next)) {
61
+ const nextIndent = indentOf(next);
62
+ if (nextIndent < providerIndent) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
63
+ if (nextIndent === providerIndent) break;
64
+ // Provider values may contain nested YAML sequences (for example the
65
+ // `models:` list emitted by the live Harness profile). A sequence at
66
+ // the same indentation as a provider field is malformed, however;
67
+ // valid child items must be indented below that field. Keep the
68
+ // parser structure-aware without attempting to deserialize values.
69
+ if (/^\s*-\s+/.test(next) && nextIndent < providerIndent + 4) {
70
+ return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
71
+ }
72
+ }
73
+ end += 1;
74
+ }
75
+ entries.push({ id, start: index, end });
76
+ index = end - 1;
77
+ }
78
+ if (entries.length === 0) return { ok: false, code: 'PROVIDER_PROFILE_SCHEMA_UNSUPPORTED' };
79
+ return { ok: true, lines, llmStart, blockEnd, providersLine, providerIndent, entries };
80
+ }
81
+
82
+ /** Return only bounded provider ids and a content revision; never values. */
83
+ export function inspectProviderProfile(source) {
84
+ const revision = typeof source === 'string' ? sha256(source) : null;
85
+ const parsed = parseProviderMap(source);
86
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision };
87
+ return {
88
+ ok: true,
89
+ schema: 'cordis-patch-llm-pi-ai-v1',
90
+ providerIds: parsed.entries.map((entry) => entry.id),
91
+ revision,
92
+ };
93
+ }
94
+
95
+ function scalarField(lines, entry, field) {
96
+ const pattern = new RegExp(`^\\s+${field}:\\s*(.*?)\\s*$`);
97
+ for (const line of lines.slice(entry.start, entry.end)) {
98
+ const match = line.match(pattern);
99
+ if (!match) continue;
100
+ const value = match[1].trim();
101
+ if (!value) return null;
102
+ return value.replace(/^(?:"([\\s\\S]*)"|'([\\s\\S]*)')$/, '$1$2');
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /** Return profile provider provenance and credential reference names only. */
108
+ export function readProviderDeclarations(source, { file = 'profile.yml' } = {}) {
109
+ const parsed = parseProviderMap(source);
110
+ if (!parsed.ok) return { ok: false, code: parsed.code };
111
+ const declarations = parsed.entries.map((entry) => {
112
+ const displayName = scalarField(parsed.lines, entry, 'displayName');
113
+ const credentialRef = scalarField(parsed.lines, entry, 'apiKeyEnv');
114
+ return {
115
+ id: entry.id,
116
+ display_name: displayName ?? entry.id,
117
+ origin: 'profile-managed',
118
+ ownership: 'crew-managed-profile',
119
+ file,
120
+ ...(credentialRef ? { credential_ref: credentialRef } : {}),
121
+ };
122
+ });
123
+ return { ok: true, declarations };
124
+ }
125
+
126
+ /**
127
+ * Remove provider declarations from a known profile patch after an optional
128
+ * content-revision check. The returned text is safe to write atomically by the
129
+ * lifecycle layer; this pure helper never touches the filesystem.
130
+ */
131
+ export function removeProviderDeclarations(source, { providerIds = [], expectedRevision } = {}) {
132
+ const currentRevision = typeof source === 'string' ? sha256(source) : null;
133
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) {
134
+ return { ok: false, code: 'PROVIDER_PROFILE_CHANGED', revision: currentRevision };
135
+ }
136
+ const requested = [...new Set(providerIds.filter((id) => typeof id === 'string' && PROVIDER_ID.test(id)))];
137
+ if (requested.length === 0 || requested.length !== providerIds.length) {
138
+ return { ok: false, code: 'PROVIDER_NOT_FOUND', revision: currentRevision };
139
+ }
140
+ const parsed = parseProviderMap(source);
141
+ if (!parsed.ok) return { ok: false, code: parsed.code, revision: currentRevision };
142
+ const byId = new Map(parsed.entries.map((entry) => [entry.id, entry]));
143
+ if (requested.some((id) => !byId.has(id))) {
144
+ return { ok: false, code: 'PROVIDER_NOT_FOUND', revision: currentRevision };
145
+ }
146
+
147
+ const removedSet = new Set(requested);
148
+ const remaining = parsed.entries.filter((entry) => !removedSet.has(entry.id)).map((entry) => entry.id);
149
+ const removeRanges = [];
150
+ if (remaining.length === 0) {
151
+ // Avoid leaving an empty `llm-pi-ai.config.providers` mapping that could
152
+ // fail the next profile parse. Remove the complete managed sequence item.
153
+ removeRanges.push([parsed.llmStart, parsed.blockEnd]);
154
+ } else {
155
+ for (const id of requested) {
156
+ const entry = byId.get(id);
157
+ removeRanges.push([entry.start, entry.end]);
158
+ }
159
+ }
160
+ const shouldRemove = (index) => removeRanges.some(([start, end]) => index >= start && index < end);
161
+ const lines = parsed.lines.filter((_line, index) => !shouldRemove(index));
162
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
163
+ const text = lines.join(newline);
164
+ return {
165
+ ok: true,
166
+ text,
167
+ removed: requested,
168
+ remaining,
169
+ revision: sha256(text),
170
+ };
171
+ }
@@ -13,8 +13,11 @@ export const READINESS_REASON_CODES = Object.freeze({
13
13
  HUB_INCOMPATIBLE: 'HUB_INCOMPATIBLE',
14
14
  PROVIDER_CATALOG_RESOLVED: 'PROVIDER_CATALOG_RESOLVED',
15
15
  PROVIDER_CATALOG_UNAVAILABLE: 'PROVIDER_CATALOG_UNAVAILABLE',
16
- PROVIDER_CATALOG_HEALTH_WARNING: 'PROVIDER_CATALOG_HEALTH_WARNING',
17
- PROVIDER_CATALOG_NOT_REQUIRED: 'PROVIDER_CATALOG_NOT_REQUIRED',
16
+ PROVIDER_CATALOG_HEALTH_WARNING: 'PROVIDER_CATALOG_HEALTH_WARNING',
17
+ PROVIDER_CATALOG_NOT_REQUIRED: 'PROVIDER_CATALOG_NOT_REQUIRED',
18
+ PROVIDER_LIFECYCLE_CONSISTENT: 'PROVIDER_LIFECYCLE_CONSISTENT',
19
+ PROVIDER_LIFECYCLE_INCONSISTENT: 'PROVIDER_LIFECYCLE_INCONSISTENT',
20
+ PROVIDER_INVENTORY_UNAVAILABLE: 'PROVIDER_INVENTORY_UNAVAILABLE',
18
21
  PROVIDER_MODE_UNKNOWN: 'PROVIDER_MODE_UNKNOWN',
19
22
  NO_CI_EVIDENCE: 'NO_CI_EVIDENCE',
20
23
  NO_EXECUTION_EVIDENCE: 'NO_EXECUTION_EVIDENCE',
@@ -29,10 +32,13 @@ const TARGET_ROWS = Object.freeze([
29
32
  ['hub_compatibility', 'live-runtime'],
30
33
  ['provider_catalog', 'live-runtime'],
31
34
  ['model_execution', 'real-execution'],
35
+ ['worker_primary_callable', 'real-execution'],
36
+ ['worker_escalation_callable', 'real-execution'],
37
+ ['reviewer_primary_callable', 'real-execution'],
38
+ ['provider_lifecycle_consistent', 'live-runtime'],
32
39
  ['deepseek_flash', 'real-execution'],
33
- ['deepseek_pro', 'real-execution'],
34
- ['opencode_go_mimo_qwen', 'real-execution'],
35
- ['reviewer_pipeline', 'real-execution'],
40
+ ['deepseek_pro', 'real-execution'],
41
+ ['reviewer_pipeline', 'real-execution'],
36
42
  ['cancellation_timeout_escalation', 'real-execution'],
37
43
  ['standalone_official', 'real-execution'],
38
44
  ]);
@@ -0,0 +1,29 @@
1
+ // Long-poll wait registration that removes itself: the terminal transition
2
+ // bulk-clears job.waiters, so a timed-out waiter that stayed registered would
3
+ // linger (and accumulate across polls) for the remaining lifetime of a
4
+ // long-running job. All three wait surfaces (hub, standalone jobs, workflow
5
+ // runtime) share this one implementation so the cleanup invariant cannot
6
+ // drift again.
7
+
8
+ /**
9
+ * Race the job's waiters against a timeout, always unregistering the waiter
10
+ * afterwards. `timeoutMs <= 0` means "no timeout": wait forever unless
11
+ * `immediateWithoutTimeout` is set, which resolves immediately (poll
12
+ * semantics, used by the hub route).
13
+ */
14
+ export async function raceWaiters(waiters, { timeoutMs = 0, immediateWithoutTimeout = false } = {}) {
15
+ let waiter = null;
16
+ let timer = null;
17
+ try {
18
+ await Promise.race([
19
+ new Promise((resolve) => { waiter = resolve; waiters.push(resolve); }),
20
+ timeoutMs > 0
21
+ ? new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); })
22
+ : (immediateWithoutTimeout ? Promise.resolve() : new Promise(() => {})),
23
+ ]);
24
+ } finally {
25
+ const index = waiters.indexOf(waiter);
26
+ if (index !== -1) waiters.splice(index, 1);
27
+ if (timer !== null) clearTimeout(timer);
28
+ }
29
+ }
@@ -5,10 +5,20 @@
5
5
  // - HUB_PROTOCOL_VERSION changes only when Hub <-> MCP wire semantics become
6
6
  // incompatible.
7
7
  //
8
- // Keep this module pure and dependency-free so Hub, MCP and tests all use the
9
- // exact same compatibility rules.
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();
10
20
 
11
- export const RUNTIME_VERSION = '0.5.6';
21
+ export const RUNTIME_VERSION = '0.5.7';
12
22
  export const HUB_PROTOCOL_VERSION = 1;
13
23
 
14
24
  export const HUB_CAPABILITIES = Object.freeze([
@@ -43,9 +53,11 @@ export const HUB_COMPATIBILITY_CODES = Object.freeze({
43
53
  UNREACHABLE: 'HUB_UNREACHABLE',
44
54
  HTTP_ERROR: 'HUB_HTTP_ERROR',
45
55
  SERVICE_MISMATCH: 'HUB_SERVICE_MISMATCH',
46
- PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
47
- PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
48
- CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
56
+ PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
57
+ PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
58
+ CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
59
+ EXECUTION_PLANE_MISMATCH: 'HUB_EXECUTION_PLANE_MISMATCH',
60
+ LISTEN_PORT_MISMATCH: 'HUB_LISTEN_PORT_MISMATCH',
49
61
  });
50
62
 
51
63
  function normalizedCapabilities(value) {
@@ -59,8 +71,12 @@ export function getHubRuntimeIdentity() {
59
71
  surface: 'native-crew-harness',
60
72
  ui_role: 'runtime',
61
73
  runtime_version: RUNTIME_VERSION,
62
- protocol_version: HUB_PROTOCOL_VERSION,
63
- capabilities: [...HUB_CAPABILITIES],
74
+ protocol_version: HUB_PROTOCOL_VERSION,
75
+ execution_plane: PRODUCTION_EXECUTION_PLANE,
76
+ profile: PRODUCTION_PROFILE,
77
+ listen_port: PRODUCTION_LISTEN_PORT,
78
+ runtime_id: RUNTIME_ID,
79
+ capabilities: [...HUB_CAPABILITIES],
64
80
  };
65
81
  }
66
82
 
@@ -77,16 +93,30 @@ export function evaluateHubHandshake(body, { requiredCapabilities = REQUIRED_HUB
77
93
  reachable: true,
78
94
  compatible: false,
79
95
  service: typeof body?.service === 'string' ? body.service : null,
80
- runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
81
- protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
96
+ runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
97
+ protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
98
+ execution_plane: typeof body?.execution_plane === 'string' ? body.execution_plane : null,
99
+ profile: typeof body?.profile === 'string' ? body.profile : null,
100
+ listen_port: Number.isInteger(body?.listen_port) ? body.listen_port : null,
101
+ runtime_id: typeof body?.runtime_id === 'string' ? body.runtime_id : null,
82
102
  capabilities,
83
103
  missing_capabilities: [],
84
104
  code: null,
85
105
  };
86
106
 
87
- if (body?.service !== 'dsh-crew-hub') {
88
- return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
89
- }
107
+ if (body?.service !== 'dsh-crew-hub') {
108
+ return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
109
+ }
110
+
111
+ // An explicit execution identity that is not the isolated 3210 Crew plane
112
+ // must never be accepted as the production Hub. Missing legacy fields remain
113
+ // compatible with the older protocol contract and are handled below.
114
+ if (body?.execution_plane !== undefined && body.execution_plane !== PRODUCTION_EXECUTION_PLANE) {
115
+ return { ...base, code: HUB_COMPATIBILITY_CODES.EXECUTION_PLANE_MISMATCH };
116
+ }
117
+ if (body?.listen_port !== undefined && body.listen_port !== PRODUCTION_LISTEN_PORT) {
118
+ return { ...base, code: HUB_COMPATIBILITY_CODES.LISTEN_PORT_MISMATCH };
119
+ }
90
120
 
91
121
  if (!Number.isInteger(body?.protocol_version)) {
92
122
  return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISSING };