@ran-sh/dsh-crew 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +183 -6
- package/official-web-bridge/lib/client.js +183 -6
- package/package.json +1 -1
- package/src/client/index.tsx +92 -4
- package/src/credential-reference.mjs +38 -0
- package/src/hub/index.mjs +483 -14
- package/src/install/npx-lifecycle.mjs +456 -398
- package/src/model-catalog.mjs +8 -1
- package/src/provider-delete-adapters.mjs +165 -17
- package/src/provider-inventory.mjs +17 -3
- package/src/provider-layer-migration-adapters.mjs +759 -0
- package/src/provider-layer-migration.mjs +198 -0
- package/src/provider-lifecycle-state.mjs +4 -1
- package/src/provider-profile-store.mjs +193 -4
- package/src/provider-settings-store.mjs +82 -6
- package/src/provider-store-lock.mjs +67 -0
- package/src/runtime-identity.mjs +2 -1
package/src/client/index.tsx
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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',
|
|
@@ -925,6 +927,65 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
925
927
|
}
|
|
926
928
|
};
|
|
927
929
|
|
|
930
|
+
const migrateHarnessProvider = async (entry: any) => {
|
|
931
|
+
if (!entry || !['promote-existing-user', 'materialize-user'].includes(entry.action)) {
|
|
932
|
+
setNotice(entry?.blocked_reason ?? copy.providerLayerMigrationHint);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (!window.confirm(copy.providerLayerMigrationConfirm)) return;
|
|
936
|
+
const id = String(entry.provider_id ?? '');
|
|
937
|
+
if (!id) return;
|
|
938
|
+
setProviderLifecycleBusy(`migrate:${id}`);
|
|
939
|
+
setNotice(copy.providerLayerMigrating);
|
|
940
|
+
try {
|
|
941
|
+
const encoded = encodeURIComponent(id);
|
|
942
|
+
const planned = await post(`/providers/${encoded}/migrate-plan`, {});
|
|
943
|
+
if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
|
|
944
|
+
if (!window.confirm(`${copy.providerLayerMigrationConfirm}\n\n${id}`)) return;
|
|
945
|
+
const result = await post(`/providers/${encoded}/migrate`, { plan_id: planned.plan.plan_id, confirm: true });
|
|
946
|
+
if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
|
|
947
|
+
if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
|
|
948
|
+
const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
|
|
949
|
+
const restarted = await readJson(await fetch(restartPath, {
|
|
950
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
|
|
951
|
+
}), restartPath);
|
|
952
|
+
if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
|
|
953
|
+
const verified = await post(`/providers/${encoded}/verify-migration`, { transaction_id: planned.plan.plan_id, confirm: true });
|
|
954
|
+
if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
|
|
955
|
+
}
|
|
956
|
+
setNotice(copy.saved);
|
|
957
|
+
await refreshProviderInventory();
|
|
958
|
+
} catch (error: any) {
|
|
959
|
+
try { await refreshProviderInventory(); } catch { /* preserve the original migration error */ }
|
|
960
|
+
setNotice(String(error?.message ?? error));
|
|
961
|
+
} finally {
|
|
962
|
+
setProviderLifecycleBusy(null);
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const rollbackHarnessMigration = async (record: any, transactionId: string) => {
|
|
967
|
+
if (!record?.id || !transactionId || !window.confirm(copy.providerRollbackConfirm)) return;
|
|
968
|
+
setProviderLifecycleBusy(`rollback-migrate:${record.id}`);
|
|
969
|
+
setNotice(copy.working);
|
|
970
|
+
try {
|
|
971
|
+
const encoded = encodeURIComponent(record.id);
|
|
972
|
+
const result = await post(`/providers/${encoded}/rollback-migration`, { transaction_id: transactionId, confirm: true });
|
|
973
|
+
if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
|
|
974
|
+
if (result.restart_required === true) {
|
|
975
|
+
const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
|
|
976
|
+
const restarted = await readJson(await fetch(restartPath, {
|
|
977
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
|
|
978
|
+
}), restartPath);
|
|
979
|
+
if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
|
|
980
|
+
}
|
|
981
|
+
setNotice(copy.saved);
|
|
982
|
+
await refreshProviderInventory();
|
|
983
|
+
} catch (error: any) {
|
|
984
|
+
setNotice(String(error?.message ?? error));
|
|
985
|
+
try { await refreshProviderInventory(); } catch { /* preserve the original rollback error */ }
|
|
986
|
+
} finally { setProviderLifecycleBusy(null); }
|
|
987
|
+
};
|
|
988
|
+
|
|
928
989
|
const rollbackHarnessProvider = async (record: any, transactionId: string, transactionState: string = 'VERIFIED') => {
|
|
929
990
|
const continuing = ['ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING'].includes(transactionState);
|
|
930
991
|
const recoverable = ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_APPLYING', 'ROLLBACK_RESTORED'].includes(transactionState);
|
|
@@ -1475,6 +1536,27 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1475
1536
|
summary={sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : '')}
|
|
1476
1537
|
expanded={!!expandedSections.harnessProviders} onToggle={() => toggleSection('harnessProviders')}>
|
|
1477
1538
|
{providerInventoryError && <div style={{ fontSize: 12, color: '#c55' }}>{providerInventoryError}</div>}
|
|
1539
|
+
{!providerInventoryError && (providerInventory?.migration?.providers?.length ?? 0) > 0 && (
|
|
1540
|
+
<div style={{ border: '1px solid rgba(201,135,53,0.45)', borderRadius: 7, padding: '8px 10px', marginBottom: 8 }}>
|
|
1541
|
+
<div style={{ fontWeight: 650, fontSize: 12.5 }}>{copy.providerLayerMigration}</div>
|
|
1542
|
+
<div style={{ fontSize: 11.5, opacity: 0.7, marginTop: 3 }}>{copy.providerLayerMigrationHint}</div>
|
|
1543
|
+
<div style={{ fontSize: 11, marginTop: 6, fontFamily: 'monospace' }}>
|
|
1544
|
+
{(providerInventory.migration.providers ?? []).map((entry: any) => (
|
|
1545
|
+
<div key={entry.provider_id} style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 }}>
|
|
1546
|
+
{entry.provider_id} · {entry.action}{entry.harness_default ? ' · Harness Default' : ''}
|
|
1547
|
+
<span style={{ flex: 1 }} />
|
|
1548
|
+
{['promote-existing-user', 'materialize-user'].includes(entry.action) && <button type="button" style={{ ...S.btn, padding: '2px 7px' }} disabled={providerLifecycleBusy !== null}
|
|
1549
|
+
onClick={() => { void migrateHarnessProvider(entry); }}>
|
|
1550
|
+
{providerLifecycleBusy === `migrate:${entry.provider_id}` ? copy.providerLayerMigrating : copy.providerLayerMigrate}
|
|
1551
|
+
</button>}
|
|
1552
|
+
</div>
|
|
1553
|
+
))}
|
|
1554
|
+
</div>
|
|
1555
|
+
<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' }}>
|
|
1556
|
+
{copy.providerLayerMigrationPlan} → 3210
|
|
1557
|
+
</a>
|
|
1558
|
+
</div>
|
|
1559
|
+
)}
|
|
1478
1560
|
{!providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && (
|
|
1479
1561
|
<div style={{ fontSize: 12, opacity: 0.55 }}>{copy.providerNoInventory}</div>
|
|
1480
1562
|
)}
|
|
@@ -1486,7 +1568,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1486
1568
|
const lifecycleTransactions = providerInventory?.lifecycle_transactions ?? [];
|
|
1487
1569
|
const recoveryTransactions = (providerInventory?.recovery_transactions ?? []).map((entry: any) => ({ ...entry, state: entry.phase }));
|
|
1488
1570
|
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))
|
|
1571
|
+
.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
1572
|
.sort((a: any, b: any) => String(a.updated_at ?? '').localeCompare(String(b.updated_at ?? ''))).at(-1);
|
|
1491
1573
|
return (
|
|
1492
1574
|
<div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
|
|
@@ -1504,12 +1586,18 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1504
1586
|
{providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
|
|
1505
1587
|
</button>
|
|
1506
1588
|
)}
|
|
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 && (
|
|
1589
|
+
{(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
1590
|
<button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
|
|
1509
1591
|
onClick={() => { void rollbackHarnessProvider(record, rollbackTransaction.transaction_id, rollbackTransaction.state); }}>
|
|
1510
1592
|
{providerLifecycleBusy === `rollback:${record.id}` ? copy.working : rollbackTransaction.state === 'ROLLBACK_PENDING' ? copy.providerRollbackContinue : copy.providerRollback}
|
|
1511
1593
|
</button>
|
|
1512
1594
|
)}
|
|
1595
|
+
{rollbackTransaction?.source === 'provider-layer-migration' && rollbackTransaction.transaction_id && (
|
|
1596
|
+
<button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
|
|
1597
|
+
onClick={() => { void rollbackHarnessMigration(record, rollbackTransaction.transaction_id); }}>
|
|
1598
|
+
{providerLifecycleBusy === `rollback-migrate:${record.id}` ? copy.working : copy.providerLayerRollback}
|
|
1599
|
+
</button>
|
|
1600
|
+
)}
|
|
1513
1601
|
</div>
|
|
1514
1602
|
);
|
|
1515
1603
|
})}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Credential references are identifiers, never credential values. Keep this
|
|
2
|
+
// boundary strict because provider metadata is returned to the browser/UI.
|
|
3
|
+
|
|
4
|
+
const ENV_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
|
|
5
|
+
const HANDLE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u;
|
|
6
|
+
const SECRET_LIKE = /^(?:sk|pk|rk|sess|tok|token|key|secret)[_-]/iu;
|
|
7
|
+
const EMBEDDED_SECRET = /(?:^|\s)(?:bearer|basic)\s+|(?:https?|data):|=/iu;
|
|
8
|
+
|
|
9
|
+
function rawText(value) {
|
|
10
|
+
if (typeof value === 'string') return value.trim() || null;
|
|
11
|
+
if (value && typeof value === 'object') {
|
|
12
|
+
const candidate = value.name_or_handle ?? value.name ?? value.reference;
|
|
13
|
+
return typeof candidate === 'string' ? candidate.trim() || null : null;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Classify a reference without returning an unsafe value. `kind: env` is the
|
|
20
|
+
* common apiKeyEnv form; non-env stores may use a bounded opaque handle.
|
|
21
|
+
*/
|
|
22
|
+
export function classifyCredentialReference(value, { kind = 'env' } = {}) {
|
|
23
|
+
const raw = rawText(value);
|
|
24
|
+
if (!raw) return { present: false, value: null, redacted: false };
|
|
25
|
+
const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : 'env';
|
|
26
|
+
const valid = normalizedKind === 'env'
|
|
27
|
+
? ENV_REFERENCE.test(raw) && !SECRET_LIKE.test(raw) && !EMBEDDED_SECRET.test(raw)
|
|
28
|
+
: HANDLE_REFERENCE.test(raw) && !SECRET_LIKE.test(raw) && !EMBEDDED_SECRET.test(raw);
|
|
29
|
+
return { present: true, value: valid ? raw : null, redacted: !valid };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeCredentialReference(value, options = {}) {
|
|
33
|
+
return classifyCredentialReference(value, options).value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hasCredentialReference(value, options = {}) {
|
|
37
|
+
return classifyCredentialReference(value, options).present;
|
|
38
|
+
}
|