@ran-sh/dsh-crew 0.5.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh.md +6 -0
- package/lib/client.js +223 -9
- package/official-web-bridge/lib/client.js +223 -9
- package/package.json +1 -1
- package/src/client/host-readiness.mjs +16 -3
- package/src/client/index.tsx +109 -8
- package/src/config-readiness.mjs +10 -0
- package/src/credential-lifecycle.mjs +73 -0
- package/src/credential-purge-adapters.mjs +127 -0
- package/src/credential-purge-state.mjs +59 -0
- package/src/credential-reference-inventory.mjs +78 -0
- package/src/dsh-cli-runtime.mjs +36 -9
- package/src/extension-contract.mjs +10 -8
- package/src/hub/index.mjs +177 -20
- package/src/hub-client.mjs +1 -1
- package/src/install/npx-lifecycle.mjs +241 -25
- package/src/job-contracts.mjs +9 -4
- package/src/official-web-bridge.mjs +6 -11
- package/src/provider-delete-adapters.mjs +28 -1
- package/src/provider-desired-state.mjs +37 -0
- package/src/provider-lifecycle-state.mjs +15 -1
- package/src/runtime-identity.mjs +24 -9
- package/src/runtime-readiness-snapshot.mjs +87 -0
- package/src/server.mjs +30 -5
|
@@ -179,7 +179,16 @@ function componentState(status, keys, { aligned = false } = {}) {
|
|
|
179
179
|
if (evidence.some((value) => typeof value !== "boolean")) return READINESS_STATES.UNKNOWN;
|
|
180
180
|
return evidence.every(Boolean) && (!aligned || status.components.target_alignment === true) ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
|
|
181
181
|
}
|
|
182
|
-
function runtimeState(runtime) {
|
|
182
|
+
function runtimeState(runtime, readinessSnapshot) {
|
|
183
|
+
if (readinessSnapshot && typeof readinessSnapshot === "object") {
|
|
184
|
+
const identity = readinessSnapshot.runtime;
|
|
185
|
+
if (!(identity?.execution_plane === "hub-3210" && identity.profile === "dsh-crew" && Number(identity.listen_port) === 3210 && typeof identity.runtime_id === "string" && identity.runtime_id.trim())) return READINESS_STATES.UNAVAILABLE;
|
|
186
|
+
const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows) ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === "hub_compatibility") : void 0;
|
|
187
|
+
if (row?.status === "PASS") return READINESS_STATES.READY;
|
|
188
|
+
if (row?.status === "FAIL") return READINESS_STATES.UNAVAILABLE;
|
|
189
|
+
if (row?.status === "NOT_RUN" || row?.status === "SKIP") return READINESS_STATES.DEGRADED;
|
|
190
|
+
return READINESS_STATES.UNKNOWN;
|
|
191
|
+
}
|
|
183
192
|
if (runtime === void 0) return READINESS_STATES.UNKNOWN;
|
|
184
193
|
if (runtime === null) return READINESS_STATES.UNAVAILABLE;
|
|
185
194
|
return runtime?.ok === true && runtime.service === "dsh-crew-hub" ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
|
|
@@ -190,7 +199,7 @@ function bridgeState(surface) {
|
|
|
190
199
|
return READINESS_STATES.UNKNOWN;
|
|
191
200
|
}
|
|
192
201
|
/** Project only structured, non-secret installer/runtime evidence. */
|
|
193
|
-
function projectHostReadiness({ installStatus, runtime, surface } = {}) {
|
|
202
|
+
function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
|
|
194
203
|
const codex = installStatus?.codex;
|
|
195
204
|
const claude = installStatus?.claude;
|
|
196
205
|
const zcode = installStatus?.zcode;
|
|
@@ -230,7 +239,7 @@ function projectHostReadiness({ installStatus, runtime, surface } = {}) {
|
|
|
230
239
|
},
|
|
231
240
|
{
|
|
232
241
|
id: "crew_harness",
|
|
233
|
-
state: runtimeState(runtime),
|
|
242
|
+
state: runtimeState(runtime, readinessSnapshot),
|
|
234
243
|
detail: runtime?.runtime_version ?? null
|
|
235
244
|
},
|
|
236
245
|
{
|
|
@@ -434,7 +443,7 @@ const COPY = {
|
|
|
434
443
|
"follow-dsh": "跟随 Harness · 使用各档独立的模型优先级",
|
|
435
444
|
"deepseek-official": "DeepSeek Official · 使用旧版固定模型路由"
|
|
436
445
|
},
|
|
437
|
-
workerProviderHint: "仅作用于 Hub 模式;Standalone
|
|
446
|
+
workerProviderHint: "仅作用于 Hub 模式;Standalone 仅保留为旧版迁移元数据,不是生产执行路径。",
|
|
438
447
|
workerModels: "Worker 模型",
|
|
439
448
|
refreshHarnessModels: "刷新 Harness 模型",
|
|
440
449
|
refreshingModels: "刷新中…",
|
|
@@ -503,11 +512,23 @@ const COPY = {
|
|
|
503
512
|
providerDeletePlan: "生成删除计划",
|
|
504
513
|
providerDelete: "确认删除",
|
|
505
514
|
providerPlanReady: "计划已生成;再次确认后才会修改 3210 配置。",
|
|
515
|
+
providerRollback: "回滚",
|
|
516
|
+
providerRollbackConfirm: "按已保存事务恢复这个 Harness Provider 及原有路由?",
|
|
517
|
+
providerRollbackUnavailable: "没有可验证的回滚事务",
|
|
506
518
|
providerReplacement: "替换 Harness 默认 Provider(输入 Provider id)",
|
|
507
519
|
providerDeleteConfirm: "确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。",
|
|
508
520
|
providerDeleteBlocked: "当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。",
|
|
509
521
|
providerLifecycleError: "Provider 生命周期操作失败",
|
|
510
522
|
providerNoInventory: "暂时无法读取 3210 Provider inventory。",
|
|
523
|
+
credentialReferences: "Credential 引用",
|
|
524
|
+
credentialReferenceHint: "仅显示引用名、归属和孤儿状态;不会读取或删除密钥。",
|
|
525
|
+
credentialOrphan: "孤儿 · 可单独申请清理",
|
|
526
|
+
credentialInUse: (count) => `使用中 · ${count} 个 Provider`,
|
|
527
|
+
credentialPurgePlan: "申请清理",
|
|
528
|
+
credentialPurgeConfirm: "这是不可恢复的 Crew-owned 凭据清理,确认继续?",
|
|
529
|
+
credentialPurging: "清理中…",
|
|
530
|
+
credentialPurgeUnavailable: "凭据清理不可用",
|
|
531
|
+
credentialPurgeUnverified: "已执行清理,但验证失败;引用仍保留用于恢复检查",
|
|
511
532
|
customProviders: "自定义 Provider",
|
|
512
533
|
addProvider: "+ 添加 Provider",
|
|
513
534
|
noCustomProviders: "暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。",
|
|
@@ -745,7 +766,7 @@ const COPY = {
|
|
|
745
766
|
"follow-dsh": "Follow Harness · use each tier's ordered model priority",
|
|
746
767
|
"deepseek-official": "DeepSeek Official · use legacy fixed model routing"
|
|
747
768
|
},
|
|
748
|
-
workerProviderHint: "Applies to Hub workers only; Standalone
|
|
769
|
+
workerProviderHint: "Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.",
|
|
749
770
|
workerModels: "Worker Models",
|
|
750
771
|
refreshHarnessModels: "Refresh Harness Models",
|
|
751
772
|
refreshingModels: "Refreshing…",
|
|
@@ -814,11 +835,23 @@ const COPY = {
|
|
|
814
835
|
providerDeletePlan: "Plan deletion",
|
|
815
836
|
providerDelete: "Confirm delete",
|
|
816
837
|
providerPlanReady: "Plan ready; a second confirmation is required before changing 3210 config.",
|
|
838
|
+
providerRollback: "Rollback",
|
|
839
|
+
providerRollbackConfirm: "Restore this Harness Provider and its previous routing from the saved transaction?",
|
|
840
|
+
providerRollbackUnavailable: "No verified rollback transaction is available",
|
|
817
841
|
providerReplacement: "Replacement Harness Default provider id",
|
|
818
842
|
providerDeleteConfirm: "Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.",
|
|
819
843
|
providerDeleteBlocked: "This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).",
|
|
820
844
|
providerLifecycleError: "Provider lifecycle operation failed",
|
|
821
845
|
providerNoInventory: "3210 Provider inventory is temporarily unavailable.",
|
|
846
|
+
credentialReferences: "Credential references",
|
|
847
|
+
credentialReferenceHint: "Names, ownership, and orphan status only; values are never read or deleted.",
|
|
848
|
+
credentialOrphan: "orphan · separate purge approval required",
|
|
849
|
+
credentialInUse: (count) => `in use · ${count} provider(s)`,
|
|
850
|
+
credentialPurgePlan: "Purge…",
|
|
851
|
+
credentialPurgeConfirm: "This irreversibly removes a Crew-owned credential. Continue?",
|
|
852
|
+
credentialPurging: "Purging…",
|
|
853
|
+
credentialPurgeUnavailable: "Credential purge unavailable",
|
|
854
|
+
credentialPurgeUnverified: "Purge ran but verification failed; the reference remains visible for recovery",
|
|
822
855
|
customProviders: "Custom providers",
|
|
823
856
|
addProvider: "+ Add provider",
|
|
824
857
|
noCustomProviders: "None yet. Added providers appear in the vision / image-gen selects above.",
|
|
@@ -1406,6 +1439,9 @@ function WorkersPanel({ ctx }) {
|
|
|
1406
1439
|
const [modelCatalogBusy, setModelCatalogBusy] = (0, react.useState)(false);
|
|
1407
1440
|
const [providerInventory, setProviderInventory] = (0, react.useState)(null);
|
|
1408
1441
|
const [providerHealth, setProviderHealth] = (0, react.useState)([]);
|
|
1442
|
+
const [credentialRefs, setCredentialRefs] = (0, react.useState)([]);
|
|
1443
|
+
const [credentialUnverified, setCredentialUnverified] = (0, react.useState)([]);
|
|
1444
|
+
const [credentialLifecycleBusy, setCredentialLifecycleBusy] = (0, react.useState)(null);
|
|
1409
1445
|
const [providerInventoryError, setProviderInventoryError] = (0, react.useState)("");
|
|
1410
1446
|
const [providerLifecycleBusy, setProviderLifecycleBusy] = (0, react.useState)(null);
|
|
1411
1447
|
const [modelPickerTier, setModelPickerTier] = (0, react.useState)(null);
|
|
@@ -1414,6 +1450,7 @@ function WorkersPanel({ ctx }) {
|
|
|
1414
1450
|
const [expandedSections, setExpandedSections] = (0, react.useState)(() => readSectionState(sectionStorage()));
|
|
1415
1451
|
const [surface, setSurface] = (0, react.useState)("detecting");
|
|
1416
1452
|
const [runtimeInfo, setRuntimeInfo] = (0, react.useState)(void 0);
|
|
1453
|
+
const [readinessSnapshot, setReadinessSnapshot] = (0, react.useState)(void 0);
|
|
1417
1454
|
const toggleSection = (sectionId) => {
|
|
1418
1455
|
setExpandedSections((current) => ({
|
|
1419
1456
|
...current,
|
|
@@ -1494,13 +1531,15 @@ function WorkersPanel({ ctx }) {
|
|
|
1494
1531
|
}, [detectSurface]);
|
|
1495
1532
|
const refreshAll = (0, react.useCallback)(async () => {
|
|
1496
1533
|
try {
|
|
1497
|
-
const [j, s, c, pr, pi, ph] = await Promise.all([
|
|
1534
|
+
const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
|
|
1498
1535
|
get("/jobs"),
|
|
1499
1536
|
get("/install/status"),
|
|
1500
1537
|
get("/config"),
|
|
1501
1538
|
get("/presets"),
|
|
1502
1539
|
get("/providers").catch(() => null),
|
|
1503
|
-
get("/provider-health").catch(() => null)
|
|
1540
|
+
get("/provider-health").catch(() => null),
|
|
1541
|
+
get("/credential-references").catch(() => null),
|
|
1542
|
+
get("/extension").catch(() => null)
|
|
1504
1543
|
]);
|
|
1505
1544
|
if (j.ok) setJobs(j.jobs ?? []);
|
|
1506
1545
|
if (s.ok) setStatus(s.status);
|
|
@@ -1514,6 +1553,11 @@ function WorkersPanel({ ctx }) {
|
|
|
1514
1553
|
}))]);
|
|
1515
1554
|
if (pi?.ok) setProviderInventory(pi);
|
|
1516
1555
|
if (ph?.ok) setProviderHealth(ph.health ?? []);
|
|
1556
|
+
if (cr?.ok) {
|
|
1557
|
+
setCredentialRefs(cr.records ?? []);
|
|
1558
|
+
setCredentialUnverified(cr.unverified_purges ?? []);
|
|
1559
|
+
}
|
|
1560
|
+
if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? void 0);
|
|
1517
1561
|
} catch {}
|
|
1518
1562
|
}, [get]);
|
|
1519
1563
|
(0, react.useEffect)(() => {
|
|
@@ -1547,14 +1591,59 @@ function WorkersPanel({ ctx }) {
|
|
|
1547
1591
|
const refreshProviderInventory = (0, react.useCallback)(async () => {
|
|
1548
1592
|
setProviderInventoryError("");
|
|
1549
1593
|
try {
|
|
1550
|
-
const [result, health] = await Promise.all([
|
|
1594
|
+
const [result, health, refs] = await Promise.all([
|
|
1595
|
+
get("/providers"),
|
|
1596
|
+
get("/provider-health").catch(() => null),
|
|
1597
|
+
get("/credential-references").catch(() => null)
|
|
1598
|
+
]);
|
|
1551
1599
|
if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
|
|
1552
1600
|
setProviderInventory(result);
|
|
1553
1601
|
if (health?.ok) setProviderHealth(health.health ?? []);
|
|
1602
|
+
if (refs?.ok) {
|
|
1603
|
+
setCredentialRefs(refs.records ?? []);
|
|
1604
|
+
setCredentialUnverified(refs.unverified_purges ?? []);
|
|
1605
|
+
}
|
|
1554
1606
|
} catch (error) {
|
|
1555
1607
|
setProviderInventoryError(error?.message ?? copy.providerNoInventory);
|
|
1556
1608
|
}
|
|
1557
1609
|
}, [get, copy]);
|
|
1610
|
+
const purgeCredentialRef = (0, react.useCallback)(async (ref) => {
|
|
1611
|
+
if (ref?.ownership !== "crew-owned" || ref?.orphan !== true || ref?.purge_capability !== "eligible") return;
|
|
1612
|
+
const referenceId = String(ref.reference_id ?? "");
|
|
1613
|
+
if (!referenceId || !window.confirm(copy.credentialPurgeConfirm)) return;
|
|
1614
|
+
setCredentialLifecycleBusy(referenceId);
|
|
1615
|
+
setNotice(copy.working);
|
|
1616
|
+
try {
|
|
1617
|
+
const encoded = encodeURIComponent(referenceId);
|
|
1618
|
+
const planned = await post(`/credential-references/${encoded}/purge-plan`, {});
|
|
1619
|
+
if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.credentialPurgeUnavailable);
|
|
1620
|
+
if (!window.confirm(`${copy.credentialPurgeConfirm}\n\n${referenceId}`)) return;
|
|
1621
|
+
const result = await readJson(await fetch(withLang(`/credential-references/${encoded}`), {
|
|
1622
|
+
method: "DELETE",
|
|
1623
|
+
headers: { "content-type": "application/json" },
|
|
1624
|
+
body: JSON.stringify({
|
|
1625
|
+
plan_id: planned.plan.plan_id,
|
|
1626
|
+
expected_revision: planned.plan.expected_revision,
|
|
1627
|
+
confirm: true,
|
|
1628
|
+
lang: locale === "zh" ? "zh" : "en"
|
|
1629
|
+
})
|
|
1630
|
+
}), `/credential-references/${encoded}`);
|
|
1631
|
+
if (!result.ok || result.state !== "VERIFIED") throw new Error(result.code ?? copy.credentialPurgeUnavailable);
|
|
1632
|
+
setNotice(copy.saved);
|
|
1633
|
+
await refreshProviderInventory();
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
setNotice(String(error?.message ?? error));
|
|
1636
|
+
} finally {
|
|
1637
|
+
setCredentialLifecycleBusy(null);
|
|
1638
|
+
}
|
|
1639
|
+
}, [
|
|
1640
|
+
copy,
|
|
1641
|
+
locale,
|
|
1642
|
+
post,
|
|
1643
|
+
readJson,
|
|
1644
|
+
refreshProviderInventory,
|
|
1645
|
+
withLang
|
|
1646
|
+
]);
|
|
1558
1647
|
(0, react.useEffect)(() => {
|
|
1559
1648
|
if (surface === CREW_UI_SURFACES.OFFICIAL) refreshHarnessModels();
|
|
1560
1649
|
}, [surface, refreshHarnessModels]);
|
|
@@ -1922,6 +2011,43 @@ function WorkersPanel({ ctx }) {
|
|
|
1922
2011
|
setProviderLifecycleBusy(null);
|
|
1923
2012
|
}
|
|
1924
2013
|
};
|
|
2014
|
+
const rollbackHarnessProvider = async (record, transactionId) => {
|
|
2015
|
+
if (!record || !transactionId || record.desired_state !== "absent") {
|
|
2016
|
+
setNotice(copy.providerRollbackUnavailable);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
if (!window.confirm(copy.providerRollbackConfirm)) return;
|
|
2020
|
+
setProviderLifecycleBusy(`rollback:${record.id}`);
|
|
2021
|
+
setNotice(copy.working);
|
|
2022
|
+
try {
|
|
2023
|
+
const encoded = encodeURIComponent(record.id);
|
|
2024
|
+
const result = await post(`/providers/${encoded}/rollback`, {
|
|
2025
|
+
transaction_id: transactionId,
|
|
2026
|
+
confirm: true
|
|
2027
|
+
});
|
|
2028
|
+
if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
|
|
2029
|
+
if (result.restart_required === true && result.state === "ROLLBACK_PENDING") {
|
|
2030
|
+
const restartPath = "http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart";
|
|
2031
|
+
const restarted = await readJson(await fetch(restartPath, {
|
|
2032
|
+
method: "POST",
|
|
2033
|
+
headers: { "content-type": "application/json" },
|
|
2034
|
+
body: JSON.stringify({ confirm: true })
|
|
2035
|
+
}), restartPath);
|
|
2036
|
+
if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
|
|
2037
|
+
const verified = await post(`/providers/${encoded}/verify-rollback`, {
|
|
2038
|
+
transaction_id: transactionId,
|
|
2039
|
+
confirm: true
|
|
2040
|
+
});
|
|
2041
|
+
if (!verified.ok || verified.state !== "ROLLED_BACK") throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
|
|
2042
|
+
} else if (result.state !== "ROLLED_BACK") throw new Error(result.code ?? copy.providerLifecycleError);
|
|
2043
|
+
setNotice(copy.saved);
|
|
2044
|
+
await refreshProviderInventory();
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
setNotice(String(error?.message ?? error));
|
|
2047
|
+
} finally {
|
|
2048
|
+
setProviderLifecycleBusy(null);
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
1925
2051
|
/** Card: title + one-line description header, then its fields. */
|
|
1926
2052
|
const block = (text, fields, gridded = true) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1927
2053
|
style: S.block,
|
|
@@ -2021,7 +2147,8 @@ function WorkersPanel({ ctx }) {
|
|
|
2021
2147
|
const hostReadiness = projectHostReadiness({
|
|
2022
2148
|
installStatus: status,
|
|
2023
2149
|
runtime: runtimeInfo,
|
|
2024
|
-
surface
|
|
2150
|
+
surface,
|
|
2151
|
+
readinessSnapshot
|
|
2025
2152
|
});
|
|
2026
2153
|
if (!currentSurfaceResponsibilities.fullControlPlane) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinimalCrewPanel, {
|
|
2027
2154
|
locale,
|
|
@@ -3463,6 +3590,7 @@ function WorkersPanel({ ctx }) {
|
|
|
3463
3590
|
(providerInventory?.records ?? []).map((record) => {
|
|
3464
3591
|
const blocked = record.ownership === "harness" || record.desired_state === "absent" || Number(record.references?.active_jobs ?? 0) > 0;
|
|
3465
3592
|
const lifecycle = record.desired_state === "absent" ? "absent" : record.lifecycle?.enabled === false ? "disabled" : record.lifecycle?.catalogued ? "catalogued" : "configured";
|
|
3593
|
+
const rollbackTransaction = (providerInventory?.lifecycle_transactions ?? []).find((entry) => entry.provider_id === record.id && ["VERIFIED", "RESTART_PENDING"].includes(entry.state));
|
|
3466
3594
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3467
3595
|
style: {
|
|
3468
3596
|
display: "flex",
|
|
@@ -3507,9 +3635,95 @@ function WorkersPanel({ ctx }) {
|
|
|
3507
3635
|
deleteHarnessProvider(record);
|
|
3508
3636
|
},
|
|
3509
3637
|
children: providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan
|
|
3638
|
+
}),
|
|
3639
|
+
record.desired_state === "absent" && rollbackTransaction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3640
|
+
type: "button",
|
|
3641
|
+
style: {
|
|
3642
|
+
...S.btn,
|
|
3643
|
+
padding: "2px 8px"
|
|
3644
|
+
},
|
|
3645
|
+
disabled: providerLifecycleBusy !== null,
|
|
3646
|
+
onClick: () => {
|
|
3647
|
+
rollbackHarnessProvider(record, rollbackTransaction.transaction_id);
|
|
3648
|
+
},
|
|
3649
|
+
children: providerLifecycleBusy === `rollback:${record.id}` ? copy.working : copy.providerRollback
|
|
3510
3650
|
})
|
|
3511
3651
|
]
|
|
3512
3652
|
}, record.id);
|
|
3653
|
+
}),
|
|
3654
|
+
(credentialRefs.length > 0 || credentialUnverified.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3655
|
+
style: {
|
|
3656
|
+
marginTop: 10,
|
|
3657
|
+
borderTop: "1px solid rgba(128,128,128,0.16)",
|
|
3658
|
+
paddingTop: 8
|
|
3659
|
+
},
|
|
3660
|
+
children: [
|
|
3661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3662
|
+
style: {
|
|
3663
|
+
fontWeight: 600,
|
|
3664
|
+
fontSize: 12.5
|
|
3665
|
+
},
|
|
3666
|
+
children: copy.credentialReferences
|
|
3667
|
+
}),
|
|
3668
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3669
|
+
style: {
|
|
3670
|
+
fontSize: 11,
|
|
3671
|
+
opacity: .6,
|
|
3672
|
+
margin: "3px 0 7px"
|
|
3673
|
+
},
|
|
3674
|
+
children: copy.credentialReferenceHint
|
|
3675
|
+
}),
|
|
3676
|
+
credentialUnverified.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3677
|
+
style: {
|
|
3678
|
+
color: "#c55",
|
|
3679
|
+
fontSize: 11.5
|
|
3680
|
+
},
|
|
3681
|
+
children: [
|
|
3682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3683
|
+
style: S.mono,
|
|
3684
|
+
children: entry.reference_id
|
|
3685
|
+
}),
|
|
3686
|
+
" · ",
|
|
3687
|
+
copy.credentialPurgeUnverified
|
|
3688
|
+
]
|
|
3689
|
+
}, `unverified:${entry.reference_id}`)),
|
|
3690
|
+
credentialRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3691
|
+
style: {
|
|
3692
|
+
display: "flex",
|
|
3693
|
+
alignItems: "center",
|
|
3694
|
+
gap: 8,
|
|
3695
|
+
fontSize: 11.5,
|
|
3696
|
+
flexWrap: "wrap"
|
|
3697
|
+
},
|
|
3698
|
+
children: [
|
|
3699
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3700
|
+
style: S.mono,
|
|
3701
|
+
children: ref.reference_id
|
|
3702
|
+
}),
|
|
3703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3704
|
+
style: { opacity: .65 },
|
|
3705
|
+
children: ref.ownership
|
|
3706
|
+
}),
|
|
3707
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3708
|
+
style: { color: ref.orphan ? "#c98735" : void 0 },
|
|
3709
|
+
children: ref.orphan ? copy.credentialOrphan : copy.credentialInUse(ref.consumer_count ?? 0)
|
|
3710
|
+
}),
|
|
3711
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
3712
|
+
ref.ownership === "crew-owned" && ref.orphan === true && ref.purge_capability === "eligible" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3713
|
+
type: "button",
|
|
3714
|
+
style: {
|
|
3715
|
+
...S.btn,
|
|
3716
|
+
padding: "2px 8px"
|
|
3717
|
+
},
|
|
3718
|
+
disabled: credentialLifecycleBusy !== null,
|
|
3719
|
+
onClick: () => {
|
|
3720
|
+
purgeCredentialRef(ref);
|
|
3721
|
+
},
|
|
3722
|
+
children: credentialLifecycleBusy === ref.reference_id ? copy.credentialPurging : copy.credentialPurgePlan
|
|
3723
|
+
})
|
|
3724
|
+
]
|
|
3725
|
+
}, ref.reference_id))
|
|
3726
|
+
]
|
|
3513
3727
|
})
|
|
3514
3728
|
]
|
|
3515
3729
|
}),
|
package/package.json
CHANGED
|
@@ -15,7 +15,20 @@ function componentState(status, keys, { aligned = false } = {}) {
|
|
|
15
15
|
return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function runtimeState(runtime) {
|
|
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
|
+
}
|
|
19
32
|
if (runtime === undefined) return READINESS_STATES.UNKNOWN;
|
|
20
33
|
if (runtime === null) return READINESS_STATES.UNAVAILABLE;
|
|
21
34
|
return runtime?.ok === true && runtime.service === 'dsh-crew-hub'
|
|
@@ -30,7 +43,7 @@ function bridgeState(surface) {
|
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
/** Project only structured, non-secret installer/runtime evidence. */
|
|
33
|
-
export function projectHostReadiness({ installStatus, runtime, surface } = {}) {
|
|
46
|
+
export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
|
|
34
47
|
const codex = installStatus?.codex;
|
|
35
48
|
const claude = installStatus?.claude;
|
|
36
49
|
const zcode = installStatus?.zcode;
|
|
@@ -40,7 +53,7 @@ export function projectHostReadiness({ installStatus, runtime, surface } = {}) {
|
|
|
40
53
|
{ id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
|
|
41
54
|
{ id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
|
|
42
55
|
{ id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
|
|
43
|
-
{ id: 'crew_harness', state: runtimeState(runtime), detail: runtime?.runtime_version ?? null },
|
|
56
|
+
{ id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
|
|
44
57
|
{ id: 'official_bridge', state: bridgeState(surface) },
|
|
45
58
|
];
|
|
46
59
|
}
|
package/src/client/index.tsx
CHANGED
|
@@ -75,7 +75,7 @@ const COPY = {
|
|
|
75
75
|
statePresetHint: '切到 Custom 可单独配置各档状态。',
|
|
76
76
|
workerProvider: 'Worker Provider',
|
|
77
77
|
workerProviderDesc: { 'follow-dsh': '跟随 Harness · 使用各档独立的模型优先级', 'deepseek-official': 'DeepSeek Official · 使用旧版固定模型路由' },
|
|
78
|
-
workerProviderHint: '仅作用于 Hub 模式;Standalone
|
|
78
|
+
workerProviderHint: '仅作用于 Hub 模式;Standalone 仅保留为旧版迁移元数据,不是生产执行路径。',
|
|
79
79
|
workerModels: 'Worker 模型', refreshHarnessModels: '刷新 Harness 模型', refreshingModels: '刷新中…',
|
|
80
80
|
modelPriority: '模型优先级', addPriorityModel: '+ 添加优先模型', modelSearch: '搜索 Provider 或 Model…',
|
|
81
81
|
harnessDefault: 'Harness 默认模型', providerUnavailable: 'Provider 不可用', notAdvertised: '当前未列出',
|
|
@@ -105,10 +105,12 @@ const COPY = {
|
|
|
105
105
|
visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
|
|
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
|
-
providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。',
|
|
108
|
+
providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。', providerRollback: '回滚', providerRollbackConfirm: '按已保存事务恢复这个 Harness Provider 及原有路由?', providerRollbackUnavailable: '没有可验证的回滚事务',
|
|
109
109
|
providerReplacement: '替换 Harness 默认 Provider(输入 Provider id)', providerDeleteConfirm: '确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。',
|
|
110
110
|
providerDeleteBlocked: '当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。', providerLifecycleError: 'Provider 生命周期操作失败',
|
|
111
111
|
providerNoInventory: '暂时无法读取 3210 Provider inventory。',
|
|
112
|
+
credentialReferences: 'Credential 引用', credentialReferenceHint: '仅显示引用名、归属和孤儿状态;不会读取或删除密钥。',
|
|
113
|
+
credentialOrphan: '孤儿 · 可单独申请清理', credentialInUse: (count: number) => `使用中 · ${count} 个 Provider`, credentialPurgePlan: '申请清理', credentialPurgeConfirm: '这是不可恢复的 Crew-owned 凭据清理,确认继续?', credentialPurging: '清理中…', credentialPurgeUnavailable: '凭据清理不可用', credentialPurgeUnverified: '已执行清理,但验证失败;引用仍保留用于恢复检查',
|
|
112
114
|
customProviders: '自定义 Provider', addProvider: '+ 添加 Provider',
|
|
113
115
|
noCustomProviders: '暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。',
|
|
114
116
|
providerName: '名称', modelsField: '模型列表(逗号分隔)',
|
|
@@ -212,7 +214,7 @@ const COPY = {
|
|
|
212
214
|
statePresetHint: 'Switch to Custom to configure each tier state separately.',
|
|
213
215
|
workerProvider: 'Worker Provider',
|
|
214
216
|
workerProviderDesc: { 'follow-dsh': 'Follow Harness · use each tier\'s ordered model priority', 'deepseek-official': 'DeepSeek Official · use legacy fixed model routing' },
|
|
215
|
-
workerProviderHint: 'Applies to Hub workers only; Standalone
|
|
217
|
+
workerProviderHint: 'Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.',
|
|
216
218
|
workerModels: 'Worker Models', refreshHarnessModels: 'Refresh Harness Models', refreshingModels: 'Refreshing…',
|
|
217
219
|
modelPriority: 'Model Priority', addPriorityModel: '+ Add Priority Model', modelSearch: 'Search Provider or Model…',
|
|
218
220
|
harnessDefault: 'Harness Default', providerUnavailable: 'Provider unavailable', notAdvertised: 'Currently not advertised',
|
|
@@ -242,10 +244,12 @@ const COPY = {
|
|
|
242
244
|
visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
|
|
243
245
|
harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.',
|
|
244
246
|
providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
|
|
245
|
-
providerDeletePlan: 'Plan deletion', providerDelete: 'Confirm delete', providerPlanReady: 'Plan ready; a second confirmation is required before changing 3210 config.',
|
|
247
|
+
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',
|
|
246
248
|
providerReplacement: 'Replacement Harness Default provider id', providerDeleteConfirm: 'Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.',
|
|
247
249
|
providerDeleteBlocked: 'This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).', providerLifecycleError: 'Provider lifecycle operation failed',
|
|
248
250
|
providerNoInventory: '3210 Provider inventory is temporarily unavailable.',
|
|
251
|
+
credentialReferences: 'Credential references', credentialReferenceHint: 'Names, ownership, and orphan status only; values are never read or deleted.',
|
|
252
|
+
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',
|
|
249
253
|
customProviders: 'Custom providers', addProvider: '+ Add provider',
|
|
250
254
|
noCustomProviders: 'None yet. Added providers appear in the vision / image-gen selects above.',
|
|
251
255
|
providerName: 'Name', modelsField: 'Models (comma-separated)',
|
|
@@ -526,6 +530,9 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
526
530
|
const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
|
|
527
531
|
const [providerInventory, setProviderInventory] = useState<any>(null);
|
|
528
532
|
const [providerHealth, setProviderHealth] = useState<any[]>([]);
|
|
533
|
+
const [credentialRefs, setCredentialRefs] = useState<any[]>([]);
|
|
534
|
+
const [credentialUnverified, setCredentialUnverified] = useState<any[]>([]);
|
|
535
|
+
const [credentialLifecycleBusy, setCredentialLifecycleBusy] = useState<string | null>(null);
|
|
529
536
|
const [providerInventoryError, setProviderInventoryError] = useState('');
|
|
530
537
|
const [providerLifecycleBusy, setProviderLifecycleBusy] = useState<string | null>(null);
|
|
531
538
|
const [modelPickerTier, setModelPickerTier] = useState<'flash' | 'pro' | null>(null);
|
|
@@ -534,6 +541,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
534
541
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
|
|
535
542
|
const [surface, setSurface] = useState<string>('detecting');
|
|
536
543
|
const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
|
|
544
|
+
const [readinessSnapshot, setReadinessSnapshot] = useState<any>(undefined);
|
|
537
545
|
|
|
538
546
|
const toggleSection = (sectionId: string) => {
|
|
539
547
|
setExpandedSections((current) => ({ ...current, [sectionId]: !current[sectionId] }));
|
|
@@ -609,9 +617,9 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
609
617
|
|
|
610
618
|
const refreshAll = useCallback(async () => {
|
|
611
619
|
try {
|
|
612
|
-
const [j, s, c, pr, pi, ph] = await Promise.all([
|
|
620
|
+
const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
|
|
613
621
|
get('/jobs'), get('/install/status'), get('/config'), get('/presets'),
|
|
614
|
-
get('/providers').catch(() => null), get('/provider-health').catch(() => null),
|
|
622
|
+
get('/providers').catch(() => null), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null), get('/extension').catch(() => null),
|
|
615
623
|
]);
|
|
616
624
|
if (j.ok) setJobs(j.jobs ?? []);
|
|
617
625
|
if (s.ok) setStatus(s.status);
|
|
@@ -622,6 +630,8 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
622
630
|
]);
|
|
623
631
|
if (pi?.ok) setProviderInventory(pi);
|
|
624
632
|
if (ph?.ok) setProviderHealth(ph.health ?? []);
|
|
633
|
+
if (cr?.ok) { setCredentialRefs(cr.records ?? []); setCredentialUnverified(cr.unverified_purges ?? []); }
|
|
634
|
+
if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? undefined);
|
|
625
635
|
} catch { /* instance restarting */ }
|
|
626
636
|
}, [get]);
|
|
627
637
|
|
|
@@ -654,14 +664,40 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
654
664
|
const refreshProviderInventory = useCallback(async () => {
|
|
655
665
|
setProviderInventoryError('');
|
|
656
666
|
try {
|
|
657
|
-
const [result, health] = await Promise.all([get('/providers'), get('/provider-health').catch(() => null)]);
|
|
667
|
+
const [result, health, refs] = await Promise.all([get('/providers'), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null)]);
|
|
658
668
|
if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
|
|
659
669
|
setProviderInventory(result);
|
|
660
670
|
if (health?.ok) setProviderHealth(health.health ?? []);
|
|
671
|
+
if (refs?.ok) { setCredentialRefs(refs.records ?? []); setCredentialUnverified(refs.unverified_purges ?? []); }
|
|
661
672
|
} catch (error: any) {
|
|
662
673
|
setProviderInventoryError(error?.message ?? copy.providerNoInventory);
|
|
663
674
|
}
|
|
664
675
|
}, [get, copy]);
|
|
676
|
+
|
|
677
|
+
const purgeCredentialRef = useCallback(async (ref: any) => {
|
|
678
|
+
if (ref?.ownership !== 'crew-owned' || ref?.orphan !== true || ref?.purge_capability !== 'eligible') return;
|
|
679
|
+
const referenceId = String(ref.reference_id ?? '');
|
|
680
|
+
if (!referenceId || !window.confirm(copy.credentialPurgeConfirm)) return;
|
|
681
|
+
setCredentialLifecycleBusy(referenceId);
|
|
682
|
+
setNotice(copy.working);
|
|
683
|
+
try {
|
|
684
|
+
const encoded = encodeURIComponent(referenceId);
|
|
685
|
+
const planned = await post(`/credential-references/${encoded}/purge-plan`, {});
|
|
686
|
+
if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.credentialPurgeUnavailable);
|
|
687
|
+
if (!window.confirm(`${copy.credentialPurgeConfirm}\n\n${referenceId}`)) return;
|
|
688
|
+
const result = await readJson(await fetch(withLang(`/credential-references/${encoded}`), {
|
|
689
|
+
method: 'DELETE', headers: { 'content-type': 'application/json' },
|
|
690
|
+
body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
|
|
691
|
+
}), `/credential-references/${encoded}`);
|
|
692
|
+
if (!result.ok || result.state !== 'VERIFIED') throw new Error(result.code ?? copy.credentialPurgeUnavailable);
|
|
693
|
+
setNotice(copy.saved);
|
|
694
|
+
await refreshProviderInventory();
|
|
695
|
+
} catch (error: any) {
|
|
696
|
+
setNotice(String(error?.message ?? error));
|
|
697
|
+
} finally {
|
|
698
|
+
setCredentialLifecycleBusy(null);
|
|
699
|
+
}
|
|
700
|
+
}, [copy, locale, post, readJson, refreshProviderInventory, withLang]);
|
|
665
701
|
|
|
666
702
|
useEffect(() => {
|
|
667
703
|
if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
|
|
@@ -884,6 +920,38 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
884
920
|
}
|
|
885
921
|
};
|
|
886
922
|
|
|
923
|
+
const rollbackHarnessProvider = async (record: any, transactionId: string) => {
|
|
924
|
+
if (!record || !transactionId || record.desired_state !== 'absent') {
|
|
925
|
+
setNotice(copy.providerRollbackUnavailable);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (!window.confirm(copy.providerRollbackConfirm)) return;
|
|
929
|
+
setProviderLifecycleBusy(`rollback:${record.id}`);
|
|
930
|
+
setNotice(copy.working);
|
|
931
|
+
try {
|
|
932
|
+
const encoded = encodeURIComponent(record.id);
|
|
933
|
+
const result = await post(`/providers/${encoded}/rollback`, { transaction_id: transactionId, confirm: true });
|
|
934
|
+
if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
|
|
935
|
+
if (result.restart_required === true && result.state === 'ROLLBACK_PENDING') {
|
|
936
|
+
const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
|
|
937
|
+
const restarted = await readJson(await fetch(restartPath, {
|
|
938
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
|
|
939
|
+
}), restartPath);
|
|
940
|
+
if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
|
|
941
|
+
const verified = await post(`/providers/${encoded}/verify-rollback`, { transaction_id: transactionId, confirm: true });
|
|
942
|
+
if (!verified.ok || verified.state !== 'ROLLED_BACK') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
|
|
943
|
+
} else if (result.state !== 'ROLLED_BACK') {
|
|
944
|
+
throw new Error(result.code ?? copy.providerLifecycleError);
|
|
945
|
+
}
|
|
946
|
+
setNotice(copy.saved);
|
|
947
|
+
await refreshProviderInventory();
|
|
948
|
+
} catch (error: any) {
|
|
949
|
+
setNotice(String(error?.message ?? error));
|
|
950
|
+
} finally {
|
|
951
|
+
setProviderLifecycleBusy(null);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
887
955
|
/** Card: title + one-line description header, then its fields. */
|
|
888
956
|
const block = (text: { t: string; d: string }, fields: any, gridded = true) => (
|
|
889
957
|
<div style={S.block}>
|
|
@@ -927,7 +995,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
927
995
|
};
|
|
928
996
|
const modelActivity = aggregateModelInvocations(jobs);
|
|
929
997
|
const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
|
|
930
|
-
const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface });
|
|
998
|
+
const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface, readinessSnapshot });
|
|
931
999
|
|
|
932
1000
|
if (!currentSurfaceResponsibilities.fullControlPlane) {
|
|
933
1001
|
return <MinimalCrewPanel locale={locale} surface={surface} runtime={runtimeInfo} />;
|
|
@@ -1384,6 +1452,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1384
1452
|
{(providerInventory?.records ?? []).map((record: any) => {
|
|
1385
1453
|
const blocked = record.ownership === 'harness' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0;
|
|
1386
1454
|
const lifecycle = record.desired_state === 'absent' ? 'absent' : record.lifecycle?.enabled === false ? 'disabled' : record.lifecycle?.catalogued ? 'catalogued' : 'configured';
|
|
1455
|
+
const rollbackTransaction = (providerInventory?.lifecycle_transactions ?? []).find((entry: any) => entry.provider_id === record.id && ['VERIFIED', 'RESTART_PENDING'].includes(entry.state));
|
|
1387
1456
|
return (
|
|
1388
1457
|
<div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
|
|
1389
1458
|
<span style={{ fontWeight: 600 }}>{record.display_name || record.id}</span>
|
|
@@ -1398,9 +1467,41 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1398
1467
|
{providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
|
|
1399
1468
|
</button>
|
|
1400
1469
|
)}
|
|
1470
|
+
{record.desired_state === 'absent' && rollbackTransaction && (
|
|
1471
|
+
<button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
|
|
1472
|
+
onClick={() => { void rollbackHarnessProvider(record, rollbackTransaction.transaction_id); }}>
|
|
1473
|
+
{providerLifecycleBusy === `rollback:${record.id}` ? copy.working : copy.providerRollback}
|
|
1474
|
+
</button>
|
|
1475
|
+
)}
|
|
1401
1476
|
</div>
|
|
1402
1477
|
);
|
|
1403
1478
|
})}
|
|
1479
|
+
{(credentialRefs.length > 0 || credentialUnverified.length > 0) && (
|
|
1480
|
+
<div style={{ marginTop: 10, borderTop: '1px solid rgba(128,128,128,0.16)', paddingTop: 8 }}>
|
|
1481
|
+
<div style={{ fontWeight: 600, fontSize: 12.5 }}>{copy.credentialReferences}</div>
|
|
1482
|
+
<div style={{ fontSize: 11, opacity: 0.6, margin: '3px 0 7px' }}>{copy.credentialReferenceHint}</div>
|
|
1483
|
+
{credentialUnverified.map((entry: any) => (
|
|
1484
|
+
<div key={`unverified:${entry.reference_id}`} style={{ color: '#c55', fontSize: 11.5 }}>
|
|
1485
|
+
<span style={S.mono}>{entry.reference_id}</span> · {copy.credentialPurgeUnverified}
|
|
1486
|
+
</div>
|
|
1487
|
+
))}
|
|
1488
|
+
{credentialRefs.map((ref: any) => (
|
|
1489
|
+
<div key={ref.reference_id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11.5, flexWrap: 'wrap' as const }}>
|
|
1490
|
+
<span style={S.mono}>{ref.reference_id}</span>
|
|
1491
|
+
<span style={{ opacity: 0.65 }}>{ref.ownership}</span>
|
|
1492
|
+
<span style={{ color: ref.orphan ? '#c98735' : undefined }}>{ref.orphan ? copy.credentialOrphan : copy.credentialInUse(ref.consumer_count ?? 0)}</span>
|
|
1493
|
+
<span style={{ flex: 1 }} />
|
|
1494
|
+
{ref.ownership === 'crew-owned' && ref.orphan === true && ref.purge_capability === 'eligible' && (
|
|
1495
|
+
<button type="button" style={{ ...S.btn, padding: '2px 8px' }}
|
|
1496
|
+
disabled={credentialLifecycleBusy !== null}
|
|
1497
|
+
onClick={() => { void purgeCredentialRef(ref); }}>
|
|
1498
|
+
{credentialLifecycleBusy === ref.reference_id ? copy.credentialPurging : copy.credentialPurgePlan}
|
|
1499
|
+
</button>
|
|
1500
|
+
)}
|
|
1501
|
+
</div>
|
|
1502
|
+
))}
|
|
1503
|
+
</div>
|
|
1504
|
+
)}
|
|
1404
1505
|
</CollapsibleSection>
|
|
1405
1506
|
|
|
1406
1507
|
<CollapsibleSection sectionId="providers" title={copy.sectionNames.providers}
|