@ran-sh/dsh-crew 1.9.0 → 1.10.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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,198 +1,198 @@
1
- // Pure planning for promoting legacy Crew profile providers into the Harness
2
- // user settings layer. Planning never reads credentials or performs writes.
3
-
4
- import { classifyCredentialReference } from './credential-reference.mjs';
5
-
6
- const BUILTIN_PROVIDER_IDS = new Set(['deepseek-official']);
7
-
8
- function text(value) {
9
- return typeof value === 'string' && value.trim() ? value.trim() : null;
10
- }
11
-
12
- function authorityKind(declaration) {
13
- return text(declaration?.declaration_authority?.kind);
14
- }
15
-
16
- function providerId(declaration) {
17
- return text(declaration?.id);
18
- }
19
-
20
- function safeCredentialRef(declaration) {
21
- const ref = declaration?.credential_ref;
22
- const kind = ref && typeof ref === 'object' && typeof ref.kind === 'string' ? ref.kind : 'env';
23
- return classifyCredentialReference(ref, { kind }).value;
24
- }
25
-
26
- function safeProjection(declaration) {
27
- return {
28
- id: providerId(declaration),
29
- display_name: text(declaration?.display_name) ?? providerId(declaration),
30
- credential_ref: safeCredentialRef(declaration),
31
- };
32
- }
33
-
34
- /**
35
- * Build a bounded, secret-free migration plan from composition/base provider
36
- * declarations to Harness user settings. The caller supplies declarations
37
- * already obtained from the two authoritative stores.
38
- */
39
- export function buildProviderLayerMigrationPlan({ declarations = [], catalogProviders = [], harnessDefault = null, routingReferences = [], tombstones = {}, recoveryTransactions = [], catalogEvidence = { ok: true, partial: false }, declarationEvidence = { ok: true }, lifecycleEvidence = { ok: true }, defaultEvidence = { ok: true } } = {}) {
40
- const entries = Array.isArray(declarations) ? declarations : [];
41
- const base = entries.filter((entry) => authorityKind(entry) === 'crew-profile' && providerId(entry));
42
- const user = entries.filter((entry) => authorityKind(entry) === 'harness-settings' && providerId(entry));
43
- const byBase = new Map(base.map((entry) => [providerId(entry), entry]));
44
- const byUser = new Map(user.map((entry) => [providerId(entry), entry]));
45
- const catalog = new Map((Array.isArray(catalogProviders) ? catalogProviders : [])
46
- .map((entry) => [text(entry?.id), entry]).filter(([id]) => id));
47
- const safeTombstones = tombstones && typeof tombstones === 'object' && !Array.isArray(tombstones) ? tombstones : {};
48
- const safeRecoveryTransactions = Array.isArray(recoveryTransactions) ? recoveryTransactions : [];
49
- const unresolvedRecovery = safeRecoveryTransactions.some((entry) => entry?.unresolved === true || !text(entry?.provider_id));
50
- const pendingRecovery = safeRecoveryTransactions.length > 0;
51
- const catalogUnavailable = catalogEvidence?.ok !== true || catalogEvidence?.partial === true;
52
- const declarationUnavailable = declarationEvidence?.ok !== true;
53
- const lifecycleUnavailable = lifecycleEvidence?.ok !== true;
54
- const defaultUnavailable = defaultEvidence?.ok !== true;
55
- const providers = [...new Set([...byBase.keys(), ...byUser.keys()])]
56
- .filter((id) => !BUILTIN_PROVIDER_IDS.has(id))
57
- .map((id) => {
58
- const baseEntry = byBase.get(id) ?? null;
59
- const userEntry = byUser.get(id) ?? null;
60
- const catalogEntry = catalog.get(id) ?? null;
61
- // Adapter ownership is a catalog fact, not a provider-name heuristic.
62
- // An explicit true blocks the transition; missing metadata (including a
63
- // missing catalog entry) also blocks a base-layer transition because
64
- // reusing an unknown adapter id could hide the native Delete action.
65
- const collision = catalogEntry?.adapter_owned === true;
66
- const ownershipUnknown = baseEntry !== null && (catalogEntry === null || typeof catalogEntry.adapter_owned !== 'boolean');
67
- const tombstoned = safeTombstones[id] === 'absent';
68
- const recovery = safeRecoveryTransactions.find((entry) => text(entry?.provider_id) === id);
69
- const blockedCode = lifecycleUnavailable
70
- ? 'PROVIDER_LIFECYCLE_UNAVAILABLE'
71
- : catalogUnavailable
72
- ? 'MODEL_CATALOG_UNAVAILABLE'
73
- : declarationUnavailable
74
- ? 'PROVIDER_SOURCE_UNRESOLVED'
75
- : defaultUnavailable
76
- ? 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE'
77
- : unresolvedRecovery
78
- ? 'PROVIDER_DELETE_RECOVERY_UNRESOLVED'
79
- : pendingRecovery
80
- ? 'PROVIDER_DELETE_RECOVERY_PENDING'
81
- : tombstoned
82
- ? 'PROVIDER_ALREADY_ABSENT'
83
- : null;
84
- const ownershipCode = collision
85
- ? 'HARNESS_PROVIDER_ID_COLLISION'
86
- : ownershipUnknown ? 'HARNESS_PROVIDER_OWNERSHIP_UNAVAILABLE' : null;
87
- const action = blockedCode ? 'blocked' : ownershipCode ? 'collision-review' : baseEntry && userEntry ? 'promote-existing-user' : baseEntry ? 'materialize-user' : 'none';
88
- const currentNativeRemovable = userEntry !== null && baseEntry === null;
89
- const migrationAction = action === 'promote-existing-user' || action === 'materialize-user';
90
- return {
91
- provider_id: id,
92
- action,
93
- current_native_removable: currentNativeRemovable,
94
- target_native_removable: migrationAction ? true : currentNativeRemovable,
95
- native_removable_after: migrationAction ? 'pending-verification' : ownershipCode || blockedCode ? 'unknown' : currentNativeRemovable,
96
- requires_base_removal: baseEntry !== null,
97
- target_user_layer: migrationAction || currentNativeRemovable,
98
- source: { base: baseEntry ? safeProjection(baseEntry) : null, user: userEntry ? safeProjection(userEntry) : null },
99
- credential_reference: safeCredentialRef(userEntry) ?? safeCredentialRef(baseEntry),
100
- collision: ownershipCode ? { reason_code: ownershipCode, ...(ownershipCode === 'HARNESS_PROVIDER_ID_COLLISION' ? { requires_target_id: true } : {}) } : null,
101
- ...(blockedCode ? { blocked_reason: blockedCode } : ownershipCode ? { blocked_reason: ownershipCode } : {}),
102
- referenced_by: (Array.isArray(routingReferences) ? routingReferences : [])
103
- .filter((ref) => ref?.provider === id).map((ref) => ({ provider: id, model: text(ref.model) })),
104
- harness_default: harnessDefault?.provider === id,
105
- };
106
- });
107
- return {
108
- schema_version: 1,
109
- kind: 'provider-layer-migration-plan',
110
- requires_confirmation: providers.some((entry) => entry.action !== 'none'),
111
- providers: providers.filter((entry) => entry.action !== 'none'),
112
- blocked: providers.filter((entry) => entry.action === 'collision-review' || entry.action === 'blocked').map((entry) => ({
113
- provider_id: entry.provider_id,
114
- code: entry.blocked_reason ?? 'HARNESS_PROVIDER_ID_COLLISION',
115
- })),
116
- };
117
- }
118
-
119
- export function hasProviderLayerMigration(plan) {
120
- return Array.isArray(plan?.providers) && plan.providers.length > 0;
121
- }
122
-
123
- const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
124
-
125
- function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
126
- const code = text(error?.code);
127
- return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
128
- }
129
-
130
- /**
131
- * Execute one confirmed provider-layer transition through explicit adapters.
132
- * Adapters own filesystem/CAS/restart details; this state machine never reads
133
- * credentials and never runs for a GET or an unconfirmed request.
134
- */
135
- export async function executeProviderLayerMigration(plan, hooks = {}, { confirm = false, deferRestart = false } = {}) {
136
- if (confirm !== true) return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_CONFIRM_REQUIRED', transaction_id: plan?.plan_id ?? null };
137
- if (!plan?.plan_id || !text(plan.provider_id) || !['promote-existing-user', 'materialize-user'].includes(plan.action)) {
138
- return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_PLAN_INVALID', transaction_id: plan?.plan_id ?? null };
139
- }
140
- for (const name of ['backup', 'materialize', 'removeBase', 'rollback']) {
141
- if (typeof hooks[name] !== 'function') return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
142
- }
143
- if (!deferRestart && (typeof hooks.restart !== 'function' || typeof hooks.verify !== 'function')) {
144
- return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
145
- }
146
- let state = 'PLANNED';
147
- const events = [{ state, at: new Date().toISOString() }];
148
- let rollbackAttempted = false;
149
- let rollbackError = null;
150
- let verification = null;
151
- try {
152
- await hooks.backup(plan);
153
- await hooks.materialize(plan);
154
- await hooks.removeBase(plan);
155
- state = 'APPLIED';
156
- events.push({ state, at: new Date().toISOString() });
157
- if (deferRestart) {
158
- state = 'RESTART_PENDING';
159
- events.push({ state, at: new Date().toISOString() });
160
- } else {
161
- const restarted = await hooks.restart(plan);
162
- if (restarted?.ok === false) throw Object.assign(new Error('provider migration restart failed'), { code: restarted.code });
163
- state = 'VERIFYING';
164
- events.push({ state, at: new Date().toISOString() });
165
- verification = await hooks.verify(plan);
166
- if (verification?.nativeRemovable !== true || verification?.baseAbsent !== true || verification?.userPresent !== true) {
167
- throw Object.assign(new Error('provider migration verification incomplete'), { code: 'PROVIDER_MIGRATION_VERIFY_FAILED' });
168
- }
169
- state = 'VERIFIED';
170
- events.push({ state, at: new Date().toISOString() });
171
- }
172
- } catch (error) {
173
- rollbackAttempted = true;
174
- try { await hooks.rollback(plan); }
175
- catch (rollbackErrorValue) { rollbackError = migrationCode(rollbackErrorValue, 'PROVIDER_MIGRATION_ROLLBACK_FAILED'); }
176
- state = 'FAILED';
177
- events.push({ state, at: new Date().toISOString() });
178
- return {
179
- ok: false,
180
- state,
181
- code: migrationCode(error),
182
- transaction_id: plan.plan_id,
183
- provider_id: plan.provider_id,
184
- rollback_attempted: rollbackAttempted,
185
- ...(rollbackError ? { rollback_error: rollbackError } : {}),
186
- events,
187
- };
188
- }
189
- return {
190
- ok: state === 'VERIFIED' || state === 'RESTART_PENDING',
191
- state,
192
- transaction_id: plan.plan_id,
193
- provider_id: plan.provider_id,
194
- restart_required: true,
195
- ...(verification ? { verification } : {}),
196
- events,
197
- };
198
- }
1
+ // Pure planning for promoting legacy Crew profile providers into the Harness
2
+ // user settings layer. Planning never reads credentials or performs writes.
3
+
4
+ import { classifyCredentialReference } from './credential-reference.mjs';
5
+
6
+ const BUILTIN_PROVIDER_IDS = new Set(['deepseek-official']);
7
+
8
+ function text(value) {
9
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
10
+ }
11
+
12
+ function authorityKind(declaration) {
13
+ return text(declaration?.declaration_authority?.kind);
14
+ }
15
+
16
+ function providerId(declaration) {
17
+ return text(declaration?.id);
18
+ }
19
+
20
+ function safeCredentialRef(declaration) {
21
+ const ref = declaration?.credential_ref;
22
+ const kind = ref && typeof ref === 'object' && typeof ref.kind === 'string' ? ref.kind : 'env';
23
+ return classifyCredentialReference(ref, { kind }).value;
24
+ }
25
+
26
+ function safeProjection(declaration) {
27
+ return {
28
+ id: providerId(declaration),
29
+ display_name: text(declaration?.display_name) ?? providerId(declaration),
30
+ credential_ref: safeCredentialRef(declaration),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Build a bounded, secret-free migration plan from composition/base provider
36
+ * declarations to Harness user settings. The caller supplies declarations
37
+ * already obtained from the two authoritative stores.
38
+ */
39
+ export function buildProviderLayerMigrationPlan({ declarations = [], catalogProviders = [], harnessDefault = null, routingReferences = [], tombstones = {}, recoveryTransactions = [], catalogEvidence = { ok: true, partial: false }, declarationEvidence = { ok: true }, lifecycleEvidence = { ok: true }, defaultEvidence = { ok: true } } = {}) {
40
+ const entries = Array.isArray(declarations) ? declarations : [];
41
+ const base = entries.filter((entry) => authorityKind(entry) === 'crew-profile' && providerId(entry));
42
+ const user = entries.filter((entry) => authorityKind(entry) === 'harness-settings' && providerId(entry));
43
+ const byBase = new Map(base.map((entry) => [providerId(entry), entry]));
44
+ const byUser = new Map(user.map((entry) => [providerId(entry), entry]));
45
+ const catalog = new Map((Array.isArray(catalogProviders) ? catalogProviders : [])
46
+ .map((entry) => [text(entry?.id), entry]).filter(([id]) => id));
47
+ const safeTombstones = tombstones && typeof tombstones === 'object' && !Array.isArray(tombstones) ? tombstones : {};
48
+ const safeRecoveryTransactions = Array.isArray(recoveryTransactions) ? recoveryTransactions : [];
49
+ const unresolvedRecovery = safeRecoveryTransactions.some((entry) => entry?.unresolved === true || !text(entry?.provider_id));
50
+ const pendingRecovery = safeRecoveryTransactions.length > 0;
51
+ const catalogUnavailable = catalogEvidence?.ok !== true || catalogEvidence?.partial === true;
52
+ const declarationUnavailable = declarationEvidence?.ok !== true;
53
+ const lifecycleUnavailable = lifecycleEvidence?.ok !== true;
54
+ const defaultUnavailable = defaultEvidence?.ok !== true;
55
+ const providers = [...new Set([...byBase.keys(), ...byUser.keys()])]
56
+ .filter((id) => !BUILTIN_PROVIDER_IDS.has(id))
57
+ .map((id) => {
58
+ const baseEntry = byBase.get(id) ?? null;
59
+ const userEntry = byUser.get(id) ?? null;
60
+ const catalogEntry = catalog.get(id) ?? null;
61
+ // Adapter ownership is a catalog fact, not a provider-name heuristic.
62
+ // An explicit true blocks the transition; missing metadata (including a
63
+ // missing catalog entry) also blocks a base-layer transition because
64
+ // reusing an unknown adapter id could hide the native Delete action.
65
+ const collision = catalogEntry?.adapter_owned === true;
66
+ const ownershipUnknown = baseEntry !== null && (catalogEntry === null || typeof catalogEntry.adapter_owned !== 'boolean');
67
+ const tombstoned = safeTombstones[id] === 'absent';
68
+ const recovery = safeRecoveryTransactions.find((entry) => text(entry?.provider_id) === id);
69
+ const blockedCode = lifecycleUnavailable
70
+ ? 'PROVIDER_LIFECYCLE_UNAVAILABLE'
71
+ : catalogUnavailable
72
+ ? 'MODEL_CATALOG_UNAVAILABLE'
73
+ : declarationUnavailable
74
+ ? 'PROVIDER_SOURCE_UNRESOLVED'
75
+ : defaultUnavailable
76
+ ? 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE'
77
+ : unresolvedRecovery
78
+ ? 'PROVIDER_DELETE_RECOVERY_UNRESOLVED'
79
+ : pendingRecovery
80
+ ? 'PROVIDER_DELETE_RECOVERY_PENDING'
81
+ : tombstoned
82
+ ? 'PROVIDER_ALREADY_ABSENT'
83
+ : null;
84
+ const ownershipCode = collision
85
+ ? 'HARNESS_PROVIDER_ID_COLLISION'
86
+ : ownershipUnknown ? 'HARNESS_PROVIDER_OWNERSHIP_UNAVAILABLE' : null;
87
+ const action = blockedCode ? 'blocked' : ownershipCode ? 'collision-review' : baseEntry && userEntry ? 'promote-existing-user' : baseEntry ? 'materialize-user' : 'none';
88
+ const currentNativeRemovable = userEntry !== null && baseEntry === null;
89
+ const migrationAction = action === 'promote-existing-user' || action === 'materialize-user';
90
+ return {
91
+ provider_id: id,
92
+ action,
93
+ current_native_removable: currentNativeRemovable,
94
+ target_native_removable: migrationAction ? true : currentNativeRemovable,
95
+ native_removable_after: migrationAction ? 'pending-verification' : ownershipCode || blockedCode ? 'unknown' : currentNativeRemovable,
96
+ requires_base_removal: baseEntry !== null,
97
+ target_user_layer: migrationAction || currentNativeRemovable,
98
+ source: { base: baseEntry ? safeProjection(baseEntry) : null, user: userEntry ? safeProjection(userEntry) : null },
99
+ credential_reference: safeCredentialRef(userEntry) ?? safeCredentialRef(baseEntry),
100
+ collision: ownershipCode ? { reason_code: ownershipCode, ...(ownershipCode === 'HARNESS_PROVIDER_ID_COLLISION' ? { requires_target_id: true } : {}) } : null,
101
+ ...(blockedCode ? { blocked_reason: blockedCode } : ownershipCode ? { blocked_reason: ownershipCode } : {}),
102
+ referenced_by: (Array.isArray(routingReferences) ? routingReferences : [])
103
+ .filter((ref) => ref?.provider === id).map((ref) => ({ provider: id, model: text(ref.model) })),
104
+ harness_default: harnessDefault?.provider === id,
105
+ };
106
+ });
107
+ return {
108
+ schema_version: 1,
109
+ kind: 'provider-layer-migration-plan',
110
+ requires_confirmation: providers.some((entry) => entry.action !== 'none'),
111
+ providers: providers.filter((entry) => entry.action !== 'none'),
112
+ blocked: providers.filter((entry) => entry.action === 'collision-review' || entry.action === 'blocked').map((entry) => ({
113
+ provider_id: entry.provider_id,
114
+ code: entry.blocked_reason ?? 'HARNESS_PROVIDER_ID_COLLISION',
115
+ })),
116
+ };
117
+ }
118
+
119
+ export function hasProviderLayerMigration(plan) {
120
+ return Array.isArray(plan?.providers) && plan.providers.length > 0;
121
+ }
122
+
123
+ const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
124
+
125
+ function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
126
+ const code = text(error?.code);
127
+ return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
128
+ }
129
+
130
+ /**
131
+ * Execute one confirmed provider-layer transition through explicit adapters.
132
+ * Adapters own filesystem/CAS/restart details; this state machine never reads
133
+ * credentials and never runs for a GET or an unconfirmed request.
134
+ */
135
+ export async function executeProviderLayerMigration(plan, hooks = {}, { confirm = false, deferRestart = false } = {}) {
136
+ if (confirm !== true) return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_CONFIRM_REQUIRED', transaction_id: plan?.plan_id ?? null };
137
+ if (!plan?.plan_id || !text(plan.provider_id) || !['promote-existing-user', 'materialize-user'].includes(plan.action)) {
138
+ return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_PLAN_INVALID', transaction_id: plan?.plan_id ?? null };
139
+ }
140
+ for (const name of ['backup', 'materialize', 'removeBase', 'rollback']) {
141
+ if (typeof hooks[name] !== 'function') return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
142
+ }
143
+ if (!deferRestart && (typeof hooks.restart !== 'function' || typeof hooks.verify !== 'function')) {
144
+ return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
145
+ }
146
+ let state = 'PLANNED';
147
+ const events = [{ state, at: new Date().toISOString() }];
148
+ let rollbackAttempted = false;
149
+ let rollbackError = null;
150
+ let verification = null;
151
+ try {
152
+ await hooks.backup(plan);
153
+ await hooks.materialize(plan);
154
+ await hooks.removeBase(plan);
155
+ state = 'APPLIED';
156
+ events.push({ state, at: new Date().toISOString() });
157
+ if (deferRestart) {
158
+ state = 'RESTART_PENDING';
159
+ events.push({ state, at: new Date().toISOString() });
160
+ } else {
161
+ const restarted = await hooks.restart(plan);
162
+ if (restarted?.ok === false) throw Object.assign(new Error('provider migration restart failed'), { code: restarted.code });
163
+ state = 'VERIFYING';
164
+ events.push({ state, at: new Date().toISOString() });
165
+ verification = await hooks.verify(plan);
166
+ if (verification?.nativeRemovable !== true || verification?.baseAbsent !== true || verification?.userPresent !== true) {
167
+ throw Object.assign(new Error('provider migration verification incomplete'), { code: 'PROVIDER_MIGRATION_VERIFY_FAILED' });
168
+ }
169
+ state = 'VERIFIED';
170
+ events.push({ state, at: new Date().toISOString() });
171
+ }
172
+ } catch (error) {
173
+ rollbackAttempted = true;
174
+ try { await hooks.rollback(plan); }
175
+ catch (rollbackErrorValue) { rollbackError = migrationCode(rollbackErrorValue, 'PROVIDER_MIGRATION_ROLLBACK_FAILED'); }
176
+ state = 'FAILED';
177
+ events.push({ state, at: new Date().toISOString() });
178
+ return {
179
+ ok: false,
180
+ state,
181
+ code: migrationCode(error),
182
+ transaction_id: plan.plan_id,
183
+ provider_id: plan.provider_id,
184
+ rollback_attempted: rollbackAttempted,
185
+ ...(rollbackError ? { rollback_error: rollbackError } : {}),
186
+ events,
187
+ };
188
+ }
189
+ return {
190
+ ok: state === 'VERIFIED' || state === 'RESTART_PENDING',
191
+ state,
192
+ transaction_id: plan.plan_id,
193
+ provider_id: plan.provider_id,
194
+ restart_required: true,
195
+ ...(verification ? { verification } : {}),
196
+ events,
197
+ };
198
+ }
@@ -1,103 +1,103 @@
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
- import { classifyCredentialReference } from './credential-reference.mjs';
6
-
7
- const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
8
- const REVISION_PATTERN = /^[a-f0-9]{64}$/;
9
- const STATES = new Set([
10
- 'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
11
- 'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
12
- ]);
13
-
14
- function validId(value) {
15
- return typeof value === 'string' && ID_PATTERN.test(value);
16
- }
17
-
18
- function validRevision(value) {
19
- return typeof value === 'string' && REVISION_PATTERN.test(value);
20
- }
21
-
22
- function validTimestamp(value) {
23
- return typeof value === 'string' && value.length >= 16 && value.length <= 64;
24
- }
25
-
26
- function normalizedCredentialRefs(value) {
27
- if (!Array.isArray(value)) return undefined;
28
- const refs = value.map((ref) => {
29
- if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null;
30
- const kind = typeof ref.kind === 'string' && ref.kind.trim() ? ref.kind.trim() : null;
31
- const name = typeof ref.name_or_handle === 'string' && ref.name_or_handle.trim() ? ref.name_or_handle.trim() : null;
32
- const ownership = typeof ref.ownership === 'string' && ref.ownership.trim() ? ref.ownership.trim() : null;
33
- const safeName = classifyCredentialReference(name, { kind }).value;
34
- return kind && safeName && ownership ? { kind, name_or_handle: safeName, ownership } : null;
35
- }).filter(Boolean).slice(0, 32);
36
- return refs;
37
- }
38
-
39
- function clone(value) {
40
- return JSON.parse(JSON.stringify(value));
41
- }
42
-
43
- function normalizedTransaction(value) {
44
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
45
- if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
46
- const refs = normalizedCredentialRefs(value.credential_refs);
47
- return {
48
- provider_id: value.provider_id,
49
- state: value.state,
50
- ...(validTimestamp(value.updated_at) ? { updated_at: value.updated_at } : {}),
51
- ...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
52
- ...(refs === undefined ? {} : { credential_refs: refs }),
53
- };
54
- }
55
-
56
- /** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
57
- export function normalizeProviderLifecycleState(input = {}) {
58
- const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
59
- if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
60
- if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
61
- for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
62
- }
63
- if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
64
- for (const [transactionId, value] of Object.entries(input.transactions)) {
65
- if (!validId(transactionId)) continue;
66
- const transaction = normalizedTransaction(value);
67
- if (transaction) out.transactions[transactionId] = transaction;
68
- }
69
- }
70
- if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
71
- for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
72
- }
73
- return out;
74
- }
75
-
76
- function requireProviderId(providerId) {
77
- if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
78
- return providerId;
79
- }
80
-
81
- export function markProviderTombstone(state, providerId) {
82
- const id = requireProviderId(providerId);
83
- const out = normalizeProviderLifecycleState(state);
84
- out.tombstones[id] = 'absent';
85
- return out;
86
- }
87
-
88
- export function clearProviderTombstone(state, providerId) {
89
- const id = requireProviderId(providerId);
90
- const out = normalizeProviderLifecycleState(state);
91
- delete out.tombstones[id];
92
- return out;
93
- }
94
-
95
- /** Record non-secret transaction metadata and credential reference names only. */
96
- export function recordProviderTransaction(state, transaction = {}) {
97
- const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
98
- const normalized = normalizedTransaction(transaction);
99
- if (!normalized) throw new TypeError('provider transaction metadata is invalid');
100
- const out = normalizeProviderLifecycleState(state);
101
- out.transactions[transactionId] = clone(normalized);
102
- return out;
103
- }
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
+ import { classifyCredentialReference } from './credential-reference.mjs';
6
+
7
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
8
+ const REVISION_PATTERN = /^[a-f0-9]{64}$/;
9
+ const STATES = new Set([
10
+ 'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
11
+ 'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
12
+ ]);
13
+
14
+ function validId(value) {
15
+ return typeof value === 'string' && ID_PATTERN.test(value);
16
+ }
17
+
18
+ function validRevision(value) {
19
+ return typeof value === 'string' && REVISION_PATTERN.test(value);
20
+ }
21
+
22
+ function validTimestamp(value) {
23
+ return typeof value === 'string' && value.length >= 16 && value.length <= 64;
24
+ }
25
+
26
+ function normalizedCredentialRefs(value) {
27
+ if (!Array.isArray(value)) return undefined;
28
+ const refs = value.map((ref) => {
29
+ if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null;
30
+ const kind = typeof ref.kind === 'string' && ref.kind.trim() ? ref.kind.trim() : null;
31
+ const name = typeof ref.name_or_handle === 'string' && ref.name_or_handle.trim() ? ref.name_or_handle.trim() : null;
32
+ const ownership = typeof ref.ownership === 'string' && ref.ownership.trim() ? ref.ownership.trim() : null;
33
+ const safeName = classifyCredentialReference(name, { kind }).value;
34
+ return kind && safeName && ownership ? { kind, name_or_handle: safeName, ownership } : null;
35
+ }).filter(Boolean).slice(0, 32);
36
+ return refs;
37
+ }
38
+
39
+ function clone(value) {
40
+ return JSON.parse(JSON.stringify(value));
41
+ }
42
+
43
+ function normalizedTransaction(value) {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
45
+ if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
46
+ const refs = normalizedCredentialRefs(value.credential_refs);
47
+ return {
48
+ provider_id: value.provider_id,
49
+ state: value.state,
50
+ ...(validTimestamp(value.updated_at) ? { updated_at: value.updated_at } : {}),
51
+ ...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
52
+ ...(refs === undefined ? {} : { credential_refs: refs }),
53
+ };
54
+ }
55
+
56
+ /** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
57
+ export function normalizeProviderLifecycleState(input = {}) {
58
+ const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
59
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
60
+ if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
61
+ for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
62
+ }
63
+ if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
64
+ for (const [transactionId, value] of Object.entries(input.transactions)) {
65
+ if (!validId(transactionId)) continue;
66
+ const transaction = normalizedTransaction(value);
67
+ if (transaction) out.transactions[transactionId] = transaction;
68
+ }
69
+ }
70
+ if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
71
+ for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ function requireProviderId(providerId) {
77
+ if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
78
+ return providerId;
79
+ }
80
+
81
+ export function markProviderTombstone(state, providerId) {
82
+ const id = requireProviderId(providerId);
83
+ const out = normalizeProviderLifecycleState(state);
84
+ out.tombstones[id] = 'absent';
85
+ return out;
86
+ }
87
+
88
+ export function clearProviderTombstone(state, providerId) {
89
+ const id = requireProviderId(providerId);
90
+ const out = normalizeProviderLifecycleState(state);
91
+ delete out.tombstones[id];
92
+ return out;
93
+ }
94
+
95
+ /** Record non-secret transaction metadata and credential reference names only. */
96
+ export function recordProviderTransaction(state, transaction = {}) {
97
+ const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
98
+ const normalized = normalizedTransaction(transaction);
99
+ if (!normalized) throw new TypeError('provider transaction metadata is invalid');
100
+ const out = normalizeProviderLifecycleState(state);
101
+ out.transactions[transactionId] = clone(normalized);
102
+ return out;
103
+ }