@ran-sh/dsh-crew 1.0.2 → 1.1.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 (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
@@ -18,6 +18,17 @@ import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { npxInstall, npxIntegrate } from '../src/install/npx-lifecycle.mjs';
20
20
 
21
+ // RETIRED as a release gate: the legacy 3080 -> 3210 bridge is retired and
22
+ // npxIntegrate()/npxDetach() always return OFFICIAL_WEB_PROFILE_READ_ONLY, so
23
+ // the bridge journey this script asserts can no longer pass on current code.
24
+ // Kept only as an operator diagnostic: run with --diagnostic-only to skip the
25
+ // retired integrate assertion and report the read-only policy explicitly.
26
+ const diagnosticOnly = process.argv.includes('--diagnostic-only');
27
+ if (diagnosticOnly) {
28
+ console.log('[diagnostic] legacy official-bridge e2e retired; official web profile is read-only; nothing to verify.');
29
+ process.exit(0);
30
+ }
31
+
21
32
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
33
  const candidateVersion = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
23
34
  if (typeof candidateVersion !== 'string' || candidateVersion === '') throw new Error('candidate package version is unavailable');
@@ -128,7 +139,7 @@ try {
128
139
  const target = join(harnessHome, 'runtime');
129
140
  mkdirSync(dirname(target), { recursive: true });
130
141
  symlinkSync(realpathSync(runtimeRoot), target, process.platform === 'win32' ? 'junction' : 'dir');
131
- return { ok: true, version: '0.1.1-rc.2' };
142
+ return { ok: true, version: '0.1.2-rc.1' };
132
143
  },
133
144
  });
134
145
  assert(installed.ok, `disposable install failed: ${logs.join(' | ')}`);
@@ -1,59 +1,62 @@
1
- export const READINESS_STATES = Object.freeze({
2
- READY: 'READY',
3
- DEGRADED: 'DEGRADED',
4
- UNAVAILABLE: 'UNAVAILABLE',
5
- UNKNOWN: 'UNKNOWN',
6
- });
7
-
8
- function componentState(status, keys, { aligned = false } = {}) {
9
- if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
10
- if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
11
- if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
12
- const evidence = keys.map((key) => status.components[key]);
13
- if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
14
- const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
15
- return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
16
- }
17
-
18
- function runtimeState(runtime, readinessSnapshot) {
19
- if (readinessSnapshot && typeof readinessSnapshot === 'object') {
20
- const identity = readinessSnapshot.runtime;
21
- if (!(identity?.execution_plane === 'hub-3210' && identity.profile === 'dsh-crew'
22
- && Number(identity.listen_port) === 3210 && typeof identity.runtime_id === 'string' && identity.runtime_id.trim())) {
23
- return READINESS_STATES.UNAVAILABLE;
24
- }
25
- const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
26
- ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
27
- if (row?.status === 'PASS') return READINESS_STATES.READY;
28
- if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
29
- if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
30
- return READINESS_STATES.UNKNOWN;
31
- }
32
- if (runtime === undefined) return READINESS_STATES.UNKNOWN;
33
- if (runtime === null) return READINESS_STATES.UNAVAILABLE;
34
- return runtime?.ok === true && runtime.service === 'dsh-crew-hub'
35
- ? READINESS_STATES.READY
36
- : READINESS_STATES.DEGRADED;
37
- }
38
-
39
- function bridgeState(surface) {
40
- if (surface === 'official-bridge') return READINESS_STATES.READY;
41
- if (surface === 'native-crew-harness') return READINESS_STATES.UNAVAILABLE;
42
- return READINESS_STATES.UNKNOWN;
43
- }
44
-
45
- /** Project only structured, non-secret installer/runtime evidence. */
46
- export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
47
- const codex = installStatus?.codex;
48
- const claude = installStatus?.claude;
49
- const zcode = installStatus?.zcode;
50
- return [
51
- { id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
52
- { id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
53
- { id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
54
- { id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
55
- { id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
56
- { id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
57
- { id: 'official_bridge', state: bridgeState(surface) },
58
- ];
59
- }
1
+ export const READINESS_STATES = Object.freeze({
2
+ READY: 'READY',
3
+ DEGRADED: 'DEGRADED',
4
+ UNAVAILABLE: 'UNAVAILABLE',
5
+ UNKNOWN: 'UNKNOWN',
6
+ });
7
+
8
+ function componentState(status, keys, { aligned = false } = {}) {
9
+ if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
10
+ if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
11
+ if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
12
+ const evidence = keys.map((key) => status.components[key]);
13
+ if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
14
+ const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
15
+ return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
16
+ }
17
+
18
+ function runtimeState(runtime, readinessSnapshot) {
19
+ if (readinessSnapshot && typeof readinessSnapshot === 'object') {
20
+ const identity = readinessSnapshot.runtime;
21
+ if (!(identity?.execution_plane === 'hub-3210' && identity.profile === 'dsh-crew'
22
+ && Number(identity.listen_port) === 3210 && typeof identity.runtime_id === 'string' && identity.runtime_id.trim())) {
23
+ return READINESS_STATES.UNAVAILABLE;
24
+ }
25
+ const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
26
+ ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
27
+ if (row?.status === 'PASS') return READINESS_STATES.READY;
28
+ if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
29
+ if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
30
+ return READINESS_STATES.UNKNOWN;
31
+ }
32
+ if (runtime === undefined) return READINESS_STATES.UNKNOWN;
33
+ if (runtime === null) return READINESS_STATES.UNAVAILABLE;
34
+ return runtime?.ok === true && runtime.service === 'dsh-crew-hub'
35
+ ? READINESS_STATES.READY
36
+ : READINESS_STATES.DEGRADED;
37
+ }
38
+
39
+ function bridgeState(surface) {
40
+ // The official 3080 quick surface is an OPTIONAL convenience panel. Its
41
+ // absence must never degrade Crew readiness: on the native 3210 control
42
+ // plane the bridge is simply not part of the picture.
43
+ if (surface === 'official-bridge') return READINESS_STATES.READY;
44
+ if (surface === 'native-crew-harness') return READINESS_STATES.UNKNOWN;
45
+ return READINESS_STATES.UNKNOWN;
46
+ }
47
+
48
+ /** Project only structured, non-secret installer/runtime evidence. */
49
+ export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
50
+ const codex = installStatus?.codex;
51
+ const claude = installStatus?.claude;
52
+ const zcode = installStatus?.zcode;
53
+ return [
54
+ { id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
55
+ { id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
56
+ { id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
57
+ { id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
58
+ { id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
59
+ { id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
60
+ { id: 'official_bridge', state: bridgeState(surface) },
61
+ ];
62
+ }
@@ -106,13 +106,14 @@ const COPY = {
106
106
  harnessProviders: 'Harness Providers', harnessProviderHint: '3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
107
107
  providerState: (state: string) => `状态:${state}`, providerModels: (count: number) => `${count} 个模型`, providerJobs: (count: number) => `${count} 个运行任务`,
108
108
  providerOfficialBuiltin: '· 官方内置(不可删除)', providerSourceUnresolved: '· 来源未解析(安全锁定)',
109
+ providerLayerMigration: '检测到旧版 profile Provider', providerLayerMigrationHint: '这些 Provider 仍在 Crew profile/base 层,所以 3210 原生页面不会显示删除。请先按迁移计划提升到 Harness 用户设置层;不会复制密钥。', providerLayerMigrationPlan: '迁移计划', providerLayerMigrate: '迁移到用户层', providerLayerMigrating: '迁移中…', providerLayerMigrationConfirm: '确认把这个 Provider 移到 Harness 用户设置层?会先保存可回滚快照,不会复制密钥值。', providerLayerRollback: '回滚迁移',
109
110
  providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。', providerRollback: '回滚', providerRollbackConfirm: '按已保存事务恢复这个 Harness Provider 及原有路由?', providerRollbackUnavailable: '没有可验证的回滚事务',
110
111
  providerReplacement: '替换 Harness 默认 Provider(输入 Provider id)', providerDeleteConfirm: '确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。',
111
112
  providerDeleteBlocked: '当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。', providerLifecycleError: 'Provider 生命周期操作失败', providerDeletePending: '配置可能已变更,事务仍待重启/验证;请刷新状态后选择回滚。', providerRollbackContinue: '继续回滚验证', recoveryUnresolved: '发现不可自动恢复的事务', recoveryStorage: '存储项', recoveryQuarantine: '隔离', recoveryQuarantineConfirm: '该事务清单无法安全恢复。将其移入隔离区并阻止自动使用,继续?',
112
113
  providerNoInventory: '暂时无法读取 3210 Provider inventory。',
113
114
  credentialReferences: 'Credential 引用', credentialReferenceHint: '仅显示引用名、归属和孤儿状态;不会读取或删除密钥。',
114
115
  credentialOrphan: '孤儿 · 可单独申请清理', credentialInUse: (count: number) => `使用中 · ${count} 个 Provider`, credentialPurgePlan: '申请清理', credentialPurgeConfirm: '这是不可恢复的 Crew-owned 凭据清理,确认继续?', credentialPurging: '清理中…', credentialPurgeUnavailable: '凭据清理不可用', credentialPurgeUnverified: '已执行清理,但验证失败;引用仍保留用于恢复检查',
115
- customProviders: '自定义 Provider', addProvider: '+ 添加 Provider',
116
+ customProviders: '多模态适配器', addProvider: '+ 添加多模态适配器',
116
117
  noCustomProviders: '暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。',
117
118
  providerName: '名称', modelsField: '模型列表(逗号分隔)',
118
119
  providerType: '接入方式', typeApi: 'API · 兼容 OpenAI 接口', typeCli: 'CLI · 本地命令',
@@ -246,13 +247,14 @@ const COPY = {
246
247
  harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.',
247
248
  providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
248
249
  providerOfficialBuiltin: '· official built-in (immutable)', providerSourceUnresolved: '· source unresolved (locked)',
250
+ providerLayerMigration: 'Legacy profile Providers detected', providerLayerMigrationHint: 'These Providers still exist in the Crew profile/base layer, so the native 3210 page cannot show Delete. Promote them to Harness user settings with an explicit plan; keys are never copied.', providerLayerMigrationPlan: 'Migration plan', providerLayerMigrate: 'Migrate to user layer', providerLayerMigrating: 'Migrating…', providerLayerMigrationConfirm: 'Move this Provider into Harness user settings? A rollback snapshot is saved first; credential values are never copied.', providerLayerRollback: 'Rollback migration',
249
251
  providerDeletePlan: 'Plan deletion', providerDelete: 'Confirm delete', providerPlanReady: 'Plan ready; a second confirmation is required before changing 3210 config.', providerRollback: 'Rollback', providerRollbackConfirm: 'Restore this Harness Provider and its previous routing from the saved transaction?', providerRollbackUnavailable: 'No verified rollback transaction is available',
250
252
  providerReplacement: 'Replacement Harness Default provider id', providerDeleteConfirm: 'Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.',
251
253
  providerDeleteBlocked: 'This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).', providerLifecycleError: 'Provider lifecycle operation failed', providerDeletePending: 'Configuration may already be changed; the transaction still needs restart/verification. Refresh status and use rollback if needed.', providerRollbackContinue: 'Continue rollback verification', recoveryUnresolved: 'An unrecoverable recovery transaction is blocking deletion', recoveryStorage: 'storage entry', recoveryQuarantine: 'Quarantine', recoveryQuarantineConfirm: 'This recovery manifest cannot be safely reopened. Move it into the quarantine area and keep it out of automatic recovery?',
252
254
  providerNoInventory: '3210 Provider inventory is temporarily unavailable.',
253
255
  credentialReferences: 'Credential references', credentialReferenceHint: 'Names, ownership, and orphan status only; values are never read or deleted.',
254
256
  credentialOrphan: 'orphan · separate purge approval required', credentialInUse: (count: number) => `in use · ${count} provider(s)`, credentialPurgePlan: 'Purge…', credentialPurgeConfirm: 'This irreversibly removes a Crew-owned credential. Continue?', credentialPurging: 'Purging…', credentialPurgeUnavailable: 'Credential purge unavailable', credentialPurgeUnverified: 'Purge ran but verification failed; the reference remains visible for recovery',
255
- customProviders: 'Custom providers', addProvider: '+ Add provider',
257
+ customProviders: 'Multimodal adapters', addProvider: '+ Add multimodal adapter',
256
258
  noCustomProviders: 'None yet. Added providers appear in the vision / image-gen selects above.',
257
259
  providerName: 'Name', modelsField: 'Models (comma-separated)',
258
260
  providerType: 'Connection', typeApi: 'API · OpenAI-compatible', typeCli: 'CLI · local command',
@@ -638,7 +640,8 @@ function WorkersPanel({ ctx }: { ctx: any }) {
638
640
  }, [get]);
639
641
 
640
642
  useEffect(() => {
641
- if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
643
+ const responsibilities = surfaceResponsibilities(surface);
644
+ if (!responsibilities.fullControlPlane) return undefined;
642
645
  void refreshAll();
643
646
  const timer = setInterval(() => {
644
647
  void Promise.all([get('/jobs'), get('/provider-health').catch(() => null)]).then(([j, health]) => {
@@ -702,11 +705,11 @@ function WorkersPanel({ ctx }: { ctx: any }) {
702
705
  }, [copy, locale, post, readJson, refreshProviderInventory, withLang]);
703
706
 
704
707
  useEffect(() => {
705
- if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
708
+ if (surfaceResponsibilities(surface).fullControlPlane) void refreshHarnessModels();
706
709
  }, [surface, refreshHarnessModels]);
707
710
 
708
711
  useEffect(() => {
709
- if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshProviderInventory();
712
+ if (surfaceResponsibilities(surface).fullControlPlane) void refreshProviderInventory();
710
713
  }, [surface, refreshProviderInventory]);
711
714
 
712
715
  const act = useCallback(async (target: string, confirmName?: string) => {
@@ -737,6 +740,25 @@ function WorkersPanel({ ctx }: { ctx: any }) {
737
740
  return false;
738
741
  }
739
742
  }, [post, get, copy]);
743
+
744
+ /** Request a Crew 3210 restart through the durable supervisor channel
745
+ * (hub writes the request; the Windows watcher executes it). Polls until
746
+ * VERIFIED or a terminal failure. Same-origin against the 3210 API — the
747
+ * legacy 3080 supervisor endpoint is retired. */
748
+ const requestRestart = useCallback(async (reason?: string): Promise<any> => {
749
+ const created = await post('/runtime/restart-request', { confirm: true, reason: reason ?? 'configuration change' });
750
+ if (!created.ok) throw new Error(created.code ?? created.error ?? copy.providerLifecycleError);
751
+ const requestId = created.request_id;
752
+ const deadline = Date.now() + 90_000;
753
+ for (;;) {
754
+ await new Promise((resolve) => setTimeout(resolve, 1000));
755
+ if (Date.now() > deadline) throw new Error('Crew restart timed out');
756
+ const status = await get(`/runtime/restart-status?id=${encodeURIComponent(requestId)}`);
757
+ if (!status.ok) continue; // 404 while the watcher has not picked it up
758
+ if (status.state === 'VERIFIED') return { ok: true, ...status };
759
+ if (status.state !== 'RESTART_REQUESTED') throw new Error(status.state ?? 'Crew restart failed');
760
+ }
761
+ }, [post, get, copy]);
740
762
  /** Selects & checkboxes: apply immediately. */
741
763
  const field = (key: string, value: any) => {
742
764
  setConfig((c: any) => ({ ...c, [key]: value }));
@@ -906,12 +928,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
906
928
  }), path);
907
929
  if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
908
930
  if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
909
- const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
910
- const restarted = await readJson(await fetch(restartPath, {
911
- method: 'POST', headers: { 'content-type': 'application/json' },
912
- body: JSON.stringify({ confirm: true }),
913
- }), restartPath);
914
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
931
+ await requestRestart('provider delete');
915
932
  const verified = await post(`/providers/${encoded}/verify-delete`, { transaction_id: planned.plan.plan_id, confirm: true });
916
933
  if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
917
934
  }
@@ -925,6 +942,57 @@ function WorkersPanel({ ctx }: { ctx: any }) {
925
942
  }
926
943
  };
927
944
 
945
+ const migrateHarnessProvider = async (entry: any) => {
946
+ if (!entry || !['promote-existing-user', 'materialize-user'].includes(entry.action)) {
947
+ setNotice(entry?.blocked_reason ?? copy.providerLayerMigrationHint);
948
+ return;
949
+ }
950
+ if (!window.confirm(copy.providerLayerMigrationConfirm)) return;
951
+ const id = String(entry.provider_id ?? '');
952
+ if (!id) return;
953
+ setProviderLifecycleBusy(`migrate:${id}`);
954
+ setNotice(copy.providerLayerMigrating);
955
+ try {
956
+ const encoded = encodeURIComponent(id);
957
+ const planned = await post(`/providers/${encoded}/migrate-plan`, {});
958
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
959
+ if (!window.confirm(`${copy.providerLayerMigrationConfirm}\n\n${id}`)) return;
960
+ const result = await post(`/providers/${encoded}/migrate`, { plan_id: planned.plan.plan_id, confirm: true });
961
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
962
+ if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
963
+ await requestRestart('provider migrate');
964
+ const verified = await post(`/providers/${encoded}/verify-migration`, { transaction_id: planned.plan.plan_id, confirm: true });
965
+ if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
966
+ }
967
+ setNotice(copy.saved);
968
+ await refreshProviderInventory();
969
+ } catch (error: any) {
970
+ try { await refreshProviderInventory(); } catch { /* preserve the original migration error */ }
971
+ setNotice(String(error?.message ?? error));
972
+ } finally {
973
+ setProviderLifecycleBusy(null);
974
+ }
975
+ };
976
+
977
+ const rollbackHarnessMigration = async (record: any, transactionId: string) => {
978
+ if (!record?.id || !transactionId || !window.confirm(copy.providerRollbackConfirm)) return;
979
+ setProviderLifecycleBusy(`rollback-migrate:${record.id}`);
980
+ setNotice(copy.working);
981
+ try {
982
+ const encoded = encodeURIComponent(record.id);
983
+ const result = await post(`/providers/${encoded}/rollback-migration`, { transaction_id: transactionId, confirm: true });
984
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
985
+ if (result.restart_required === true) {
986
+ await requestRestart('provider migrate');
987
+ }
988
+ setNotice(copy.saved);
989
+ await refreshProviderInventory();
990
+ } catch (error: any) {
991
+ setNotice(String(error?.message ?? error));
992
+ try { await refreshProviderInventory(); } catch { /* preserve the original rollback error */ }
993
+ } finally { setProviderLifecycleBusy(null); }
994
+ };
995
+
928
996
  const rollbackHarnessProvider = async (record: any, transactionId: string, transactionState: string = 'VERIFIED') => {
929
997
  const continuing = ['ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING'].includes(transactionState);
930
998
  const recoverable = ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_APPLYING', 'ROLLBACK_RESTORED'].includes(transactionState);
@@ -942,11 +1010,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
942
1010
  : await post(`/providers/${encoded}/rollback`, { transaction_id: transactionId, confirm: true });
943
1011
  if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
944
1012
  if (result.restart_required === true && result.state === 'ROLLBACK_PENDING') {
945
- const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
946
- const restarted = await readJson(await fetch(restartPath, {
947
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
948
- }), restartPath);
949
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
1013
+ await requestRestart('provider migrate');
950
1014
  const verified = await post(`/providers/${encoded}/verify-rollback`, { transaction_id: transactionId, confirm: true });
951
1015
  if (!verified.ok || verified.state !== 'ROLLED_BACK') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
952
1016
  } else if (result.state !== 'ROLLED_BACK') {
@@ -1475,6 +1539,27 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1475
1539
  summary={sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : '')}
1476
1540
  expanded={!!expandedSections.harnessProviders} onToggle={() => toggleSection('harnessProviders')}>
1477
1541
  {providerInventoryError && <div style={{ fontSize: 12, color: '#c55' }}>{providerInventoryError}</div>}
1542
+ {!providerInventoryError && (providerInventory?.migration?.providers?.length ?? 0) > 0 && (
1543
+ <div style={{ border: '1px solid rgba(201,135,53,0.45)', borderRadius: 7, padding: '8px 10px', marginBottom: 8 }}>
1544
+ <div style={{ fontWeight: 650, fontSize: 12.5 }}>{copy.providerLayerMigration}</div>
1545
+ <div style={{ fontSize: 11.5, opacity: 0.7, marginTop: 3 }}>{copy.providerLayerMigrationHint}</div>
1546
+ <div style={{ fontSize: 11, marginTop: 6, fontFamily: 'monospace' }}>
1547
+ {(providerInventory.migration.providers ?? []).map((entry: any) => (
1548
+ <div key={entry.provider_id} style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 }}>
1549
+ {entry.provider_id} · {entry.action}{entry.harness_default ? ' · Harness Default' : ''}
1550
+ <span style={{ flex: 1 }} />
1551
+ {['promote-existing-user', 'materialize-user'].includes(entry.action) && <button type="button" style={{ ...S.btn, padding: '2px 7px' }} disabled={providerLifecycleBusy !== null}
1552
+ onClick={() => { void migrateHarnessProvider(entry); }}>
1553
+ {providerLifecycleBusy === `migrate:${entry.provider_id}` ? copy.providerLayerMigrating : copy.providerLayerMigrate}
1554
+ </button>}
1555
+ </div>
1556
+ ))}
1557
+ </div>
1558
+ <a href="http://127.0.0.1:3210/" target="_blank" rel="noopener noreferrer" style={{ ...S.btn, display: 'inline-block', textDecoration: 'none', marginTop: 7, padding: '3px 8px' }}>
1559
+ {copy.providerLayerMigrationPlan} → 3210
1560
+ </a>
1561
+ </div>
1562
+ )}
1478
1563
  {!providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && (
1479
1564
  <div style={{ fontSize: 12, opacity: 0.55 }}>{copy.providerNoInventory}</div>
1480
1565
  )}
@@ -1486,7 +1571,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1486
1571
  const lifecycleTransactions = providerInventory?.lifecycle_transactions ?? [];
1487
1572
  const recoveryTransactions = (providerInventory?.recovery_transactions ?? []).map((entry: any) => ({ ...entry, state: entry.phase }));
1488
1573
  const rollbackTransaction = [...lifecycleTransactions, ...recoveryTransactions]
1489
- .filter((entry: any) => entry.provider_id === record.id && ['VERIFIED', 'RESTART_PENDING', 'ROLLBACK_PENDING', 'PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING'].includes(entry.state))
1574
+ .filter((entry: any) => entry.provider_id === record.id && ['VERIFIED', 'RESTART_PENDING', 'ROLLBACK_PENDING', 'PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING', 'PREPARED', 'USER_MATERIALIZED', 'BASE_REMOVED'].includes(entry.state))
1490
1575
  .sort((a: any, b: any) => String(a.updated_at ?? '').localeCompare(String(b.updated_at ?? ''))).at(-1);
1491
1576
  return (
1492
1577
  <div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
@@ -1504,12 +1589,18 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1504
1589
  {providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
1505
1590
  </button>
1506
1591
  )}
1507
- {(record.desired_state === 'absent' || ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING'].includes(rollbackTransaction?.state)) && rollbackTransaction && (
1592
+ {(record.desired_state === 'absent' || ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING'].includes(rollbackTransaction?.state)) && rollbackTransaction && rollbackTransaction.source !== 'provider-layer-migration' && (
1508
1593
  <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1509
1594
  onClick={() => { void rollbackHarnessProvider(record, rollbackTransaction.transaction_id, rollbackTransaction.state); }}>
1510
1595
  {providerLifecycleBusy === `rollback:${record.id}` ? copy.working : rollbackTransaction.state === 'ROLLBACK_PENDING' ? copy.providerRollbackContinue : copy.providerRollback}
1511
1596
  </button>
1512
1597
  )}
1598
+ {rollbackTransaction?.source === 'provider-layer-migration' && rollbackTransaction.transaction_id && (
1599
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1600
+ onClick={() => { void rollbackHarnessMigration(record, rollbackTransaction.transaction_id); }}>
1601
+ {providerLifecycleBusy === `rollback-migrate:${record.id}` ? copy.working : copy.providerLayerRollback}
1602
+ </button>
1603
+ )}
1513
1604
  </div>
1514
1605
  );
1515
1606
  })}
@@ -1753,7 +1844,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1753
1844
 
1754
1845
  export function apply(ctx: any): void {
1755
1846
  let runningCount = 0;
1756
- let officialSurface: boolean | null = null;
1847
+ let badgeSurface: boolean | null = null;
1757
1848
  ctx.slots.inject('settings.section', () => {
1758
1849
  let dispose = register();
1759
1850
  function register() {
@@ -1777,12 +1868,14 @@ export function apply(ctx: any): void {
1777
1868
  const poll = async () => {
1778
1869
  if (document.visibilityState !== 'visible') return;
1779
1870
  try {
1780
- if (officialSurface === null) {
1781
- const response = await fetch(`${API}/bridge-status`, { cache: 'no-store' });
1782
- const bridgeStatus = response.ok ? await response.json() : null;
1783
- officialSurface = classifyCrewSurface({ bridgeStatus }) === CREW_UI_SURFACES.OFFICIAL;
1871
+ if (badgeSurface === null) {
1872
+ const [bridgeStatus, runtime] = await Promise.all([
1873
+ fetch(`${API}/bridge-status`, { cache: 'no-store' }).then((r) => (r.ok ? r.json() : null)).catch(() => null),
1874
+ fetch(`${API}/runtime`, { cache: 'no-store' }).then((r) => (r.ok ? r.json() : null)).catch(() => null),
1875
+ ]);
1876
+ badgeSurface = surfaceResponsibilities(classifyCrewSurface({ bridgeStatus, runtime })).fullControlPlane;
1784
1877
  }
1785
- if (!officialSurface) return;
1878
+ if (!badgeSurface) return;
1786
1879
  const r = await (await fetch(`${API}/jobs`, { cache: 'no-store' })).json();
1787
1880
  const n = r.ok ? (r.jobs ?? []).filter((j: any) => j.status === 'running').length : 0;
1788
1881
  if (n !== runningCount) {
@@ -0,0 +1,10 @@
1
+ import { applyQuick } from './quick-panel';
2
+
3
+ export const inject = ['slots', 'locale'];
4
+
5
+ // Quick-controls entry for the official 3080 surface: master switch,
6
+ // flash/pro model priority, vision/imagegen toggles only. Never ships the
7
+ // full control-plane code.
8
+ export function apply(ctx: any): void {
9
+ applyQuick(ctx);
10
+ }