@ran-sh/dsh-crew 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +1 -1
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -332
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +455 -401
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,130 +1,130 @@
1
- // Bounded provider/model callability evidence.
2
- //
3
- // Catalog presence is not proof of a callable route. This store keeps only
4
- // sanitized state and timestamps; raw provider errors never leave the caller.
5
-
6
- export const PROVIDER_HEALTH_STATES = Object.freeze([
7
- 'callable', 'credential-missing', 'quota-exhausted', 'rate-limited',
8
- 'timeout', 'internal-error', 'disabled', 'not-configured', 'tombstoned', 'unprobed',
9
- ]);
10
-
11
- const REASON_CODES = Object.freeze({
12
- callable: 'PROVIDER_CALLABLE',
13
- 'credential-missing': 'CREDENTIAL_MISSING',
14
- 'quota-exhausted': 'QUOTA_EXHAUSTED',
15
- 'rate-limited': 'RATE_LIMITED',
16
- timeout: 'PROBE_TIMEOUT',
17
- 'internal-error': 'PROVIDER_INTERNAL_ERROR',
18
- disabled: 'PROVIDER_DISABLED',
19
- 'not-configured': 'PROVIDER_NOT_CONFIGURED',
20
- tombstoned: 'PROVIDER_TOMBSTONED',
21
- });
22
-
23
- const DEFAULT_TTLS = Object.freeze({
24
- callable: 5 * 60 * 1000,
25
- 'credential-missing': 30 * 1000,
26
- 'quota-exhausted': 15 * 60 * 1000,
27
- 'rate-limited': 60 * 1000,
28
- timeout: 60 * 1000,
29
- 'internal-error': 60 * 1000,
30
- disabled: 5 * 60 * 1000,
31
- 'not-configured': 30 * 1000,
32
- tombstoned: 5 * 60 * 1000,
33
- });
34
-
35
- function text(value) {
36
- return typeof value === 'string' && value.trim() ? value.trim() : null;
37
- }
38
-
39
- function providerKey(provider, model) {
40
- const p = text(provider);
41
- const m = text(model);
42
- return p && m ? `${p}\u0000${m}` : null;
43
- }
44
-
45
- function errorText(error) {
46
- if (!error || typeof error !== 'object') return '';
47
- return [error.code, error.name, error.message, error.status].filter((value) => value !== undefined && value !== null).join(' ').toLowerCase();
48
- }
49
-
50
- /** Map an upstream error to one bounded, secret-free callability state. */
51
- export function classifyProviderError(error = {}) {
52
- const textValue = errorText(error);
53
- const code = text(error?.code)?.toUpperCase() ?? '';
54
- if (code === 'MISSING_CREDENTIAL' || code === 'CREDENTIAL_MISSING' || /no (api )?key|credential.*missing|missing.*credential/.test(textValue)) {
55
- return { state: 'credential-missing', reason_code: REASON_CODES['credential-missing'] };
56
- }
57
- if (code === 'QUOTA_EXCEEDED' || /monthly|account/.test(textValue) && /quota|exhaust|limit/.test(textValue)) {
58
- return { state: 'quota-exhausted', reason_code: REASON_CODES['quota-exhausted'] };
59
- }
60
- if (Number(error?.status) === 429 || code === 'RATE_LIMITED' || /rate.?limit|too many requests/.test(textValue)) {
61
- return { state: 'rate-limited', reason_code: REASON_CODES['rate-limited'] };
62
- }
63
- if (error?.name === 'TimeoutError' || error?.name === 'AbortError' || code === 'TIMEOUT' || /timed? out|timeout|aborted/.test(textValue)) {
64
- return { state: 'timeout', reason_code: REASON_CODES.timeout };
65
- }
66
- return { state: 'internal-error', reason_code: REASON_CODES['internal-error'] };
67
- }
68
-
69
- function boundedTimestamp(value, fallback) {
70
- return Number.isFinite(value) && value >= 0 ? value : fallback;
71
- }
72
-
73
- function view(entry, now) {
74
- if (!entry) return { state: 'unprobed', reason_code: null, observed_at: null, expires_at: null, fresh: false };
75
- const fresh = now < entry.expires_at;
76
- if (!fresh) return { state: 'unprobed', reason_code: null, observed_at: entry.observed_at, expires_at: entry.expires_at, fresh: false };
77
- return { ...entry, fresh: true };
78
- }
79
-
80
- export function createProviderHealthStore({ clock = () => Date.now(), ttls = {}, maxEntries = 256 } = {}) {
81
- const records = new Map();
82
- const limits = { ...DEFAULT_TTLS };
83
- for (const state of PROVIDER_HEALTH_STATES) {
84
- if (Number.isFinite(ttls[state]) && ttls[state] >= 0) limits[state] = Math.min(ttls[state], 24 * 60 * 60 * 1000);
85
- }
86
- const boundedMax = Number.isInteger(maxEntries) && maxEntries > 0 ? Math.min(maxEntries, 2048) : 256;
87
-
88
- const get = (provider, model) => {
89
- const key = providerKey(provider, model);
90
- if (!key) return { provider: text(provider), model: text(model), state: 'unprobed', reason_code: null, observed_at: null, expires_at: null, fresh: false };
91
- const entry = records.get(key);
92
- const current = view(entry, clock());
93
- if (entry && !current.fresh) records.delete(key);
94
- return { provider: text(provider), model: text(model), ...current };
95
- };
96
-
97
- const record = (provider, model, result = {}) => {
98
- const key = providerKey(provider, model);
99
- if (!key) return null;
100
- const now = clock();
101
- const observed = boundedTimestamp(result.observed_at, now);
102
- const previous = records.get(key);
103
- let classified;
104
- if (result.ok === true) classified = { state: 'callable', reason_code: REASON_CODES.callable };
105
- else if (PROVIDER_HEALTH_STATES.includes(result.state) && result.state !== 'unprobed') classified = { state: result.state, reason_code: REASON_CODES[result.state] ?? null };
106
- else classified = classifyProviderError(result.error ?? result);
107
- if (previous && previous.observed_at > observed) return get(provider, model);
108
- if (previous && previous.observed_at === observed) {
109
- const previousFailure = previous.state !== 'callable';
110
- const incomingFailure = classified.state !== 'callable';
111
- if (previousFailure && !incomingFailure) return get(provider, model);
112
- if (previousFailure && incomingFailure && String(previous.state).localeCompare(String(classified.state)) <= 0) return get(provider, model);
113
- }
114
- const expires = observed + limits[classified.state];
115
- records.set(key, {
116
- provider: text(provider), model: text(model), ...classified,
117
- observed_at: observed, expires_at: expires,
118
- });
119
- while (records.size > boundedMax) {
120
- const oldest = [...records.entries()].sort((a, b) => a[1].observed_at - b[1].observed_at)[0]?.[0];
121
- if (!oldest) break;
122
- records.delete(oldest);
123
- }
124
- return get(provider, model);
125
- };
126
-
127
- const list = () => [...records.values()].map((entry) => view(entry, clock())).filter((entry) => entry.fresh).slice(0, boundedMax);
128
-
129
- return { get, record, list, ttls: { ...limits }, maxEntries: boundedMax };
130
- }
1
+ // Bounded provider/model callability evidence.
2
+ //
3
+ // Catalog presence is not proof of a callable route. This store keeps only
4
+ // sanitized state and timestamps; raw provider errors never leave the caller.
5
+
6
+ export const PROVIDER_HEALTH_STATES = Object.freeze([
7
+ 'callable', 'credential-missing', 'quota-exhausted', 'rate-limited',
8
+ 'timeout', 'internal-error', 'disabled', 'not-configured', 'tombstoned', 'unprobed',
9
+ ]);
10
+
11
+ const REASON_CODES = Object.freeze({
12
+ callable: 'PROVIDER_CALLABLE',
13
+ 'credential-missing': 'CREDENTIAL_MISSING',
14
+ 'quota-exhausted': 'QUOTA_EXHAUSTED',
15
+ 'rate-limited': 'RATE_LIMITED',
16
+ timeout: 'PROBE_TIMEOUT',
17
+ 'internal-error': 'PROVIDER_INTERNAL_ERROR',
18
+ disabled: 'PROVIDER_DISABLED',
19
+ 'not-configured': 'PROVIDER_NOT_CONFIGURED',
20
+ tombstoned: 'PROVIDER_TOMBSTONED',
21
+ });
22
+
23
+ const DEFAULT_TTLS = Object.freeze({
24
+ callable: 5 * 60 * 1000,
25
+ 'credential-missing': 30 * 1000,
26
+ 'quota-exhausted': 15 * 60 * 1000,
27
+ 'rate-limited': 60 * 1000,
28
+ timeout: 60 * 1000,
29
+ 'internal-error': 60 * 1000,
30
+ disabled: 5 * 60 * 1000,
31
+ 'not-configured': 30 * 1000,
32
+ tombstoned: 5 * 60 * 1000,
33
+ });
34
+
35
+ function text(value) {
36
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
37
+ }
38
+
39
+ function providerKey(provider, model) {
40
+ const p = text(provider);
41
+ const m = text(model);
42
+ return p && m ? `${p}\u0000${m}` : null;
43
+ }
44
+
45
+ function errorText(error) {
46
+ if (!error || typeof error !== 'object') return '';
47
+ return [error.code, error.name, error.message, error.status].filter((value) => value !== undefined && value !== null).join(' ').toLowerCase();
48
+ }
49
+
50
+ /** Map an upstream error to one bounded, secret-free callability state. */
51
+ export function classifyProviderError(error = {}) {
52
+ const textValue = errorText(error);
53
+ const code = text(error?.code)?.toUpperCase() ?? '';
54
+ if (code === 'MISSING_CREDENTIAL' || code === 'CREDENTIAL_MISSING' || /no (api )?key|credential.*missing|missing.*credential/.test(textValue)) {
55
+ return { state: 'credential-missing', reason_code: REASON_CODES['credential-missing'] };
56
+ }
57
+ if (code === 'QUOTA_EXCEEDED' || /monthly|account/.test(textValue) && /quota|exhaust|limit/.test(textValue)) {
58
+ return { state: 'quota-exhausted', reason_code: REASON_CODES['quota-exhausted'] };
59
+ }
60
+ if (Number(error?.status) === 429 || code === 'RATE_LIMITED' || /rate.?limit|too many requests/.test(textValue)) {
61
+ return { state: 'rate-limited', reason_code: REASON_CODES['rate-limited'] };
62
+ }
63
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError' || code === 'TIMEOUT' || /timed? out|timeout|aborted/.test(textValue)) {
64
+ return { state: 'timeout', reason_code: REASON_CODES.timeout };
65
+ }
66
+ return { state: 'internal-error', reason_code: REASON_CODES['internal-error'] };
67
+ }
68
+
69
+ function boundedTimestamp(value, fallback) {
70
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
71
+ }
72
+
73
+ function view(entry, now) {
74
+ if (!entry) return { state: 'unprobed', reason_code: null, observed_at: null, expires_at: null, fresh: false };
75
+ const fresh = now < entry.expires_at;
76
+ if (!fresh) return { state: 'unprobed', reason_code: null, observed_at: entry.observed_at, expires_at: entry.expires_at, fresh: false };
77
+ return { ...entry, fresh: true };
78
+ }
79
+
80
+ export function createProviderHealthStore({ clock = () => Date.now(), ttls = {}, maxEntries = 256 } = {}) {
81
+ const records = new Map();
82
+ const limits = { ...DEFAULT_TTLS };
83
+ for (const state of PROVIDER_HEALTH_STATES) {
84
+ if (Number.isFinite(ttls[state]) && ttls[state] >= 0) limits[state] = Math.min(ttls[state], 24 * 60 * 60 * 1000);
85
+ }
86
+ const boundedMax = Number.isInteger(maxEntries) && maxEntries > 0 ? Math.min(maxEntries, 2048) : 256;
87
+
88
+ const get = (provider, model) => {
89
+ const key = providerKey(provider, model);
90
+ if (!key) return { provider: text(provider), model: text(model), state: 'unprobed', reason_code: null, observed_at: null, expires_at: null, fresh: false };
91
+ const entry = records.get(key);
92
+ const current = view(entry, clock());
93
+ if (entry && !current.fresh) records.delete(key);
94
+ return { provider: text(provider), model: text(model), ...current };
95
+ };
96
+
97
+ const record = (provider, model, result = {}) => {
98
+ const key = providerKey(provider, model);
99
+ if (!key) return null;
100
+ const now = clock();
101
+ const observed = boundedTimestamp(result.observed_at, now);
102
+ const previous = records.get(key);
103
+ let classified;
104
+ if (result.ok === true) classified = { state: 'callable', reason_code: REASON_CODES.callable };
105
+ else if (PROVIDER_HEALTH_STATES.includes(result.state) && result.state !== 'unprobed') classified = { state: result.state, reason_code: REASON_CODES[result.state] ?? null };
106
+ else classified = classifyProviderError(result.error ?? result);
107
+ if (previous && previous.observed_at > observed) return get(provider, model);
108
+ if (previous && previous.observed_at === observed) {
109
+ const previousFailure = previous.state !== 'callable';
110
+ const incomingFailure = classified.state !== 'callable';
111
+ if (previousFailure && !incomingFailure) return get(provider, model);
112
+ if (previousFailure && incomingFailure && String(previous.state).localeCompare(String(classified.state)) <= 0) return get(provider, model);
113
+ }
114
+ const expires = observed + limits[classified.state];
115
+ records.set(key, {
116
+ provider: text(provider), model: text(model), ...classified,
117
+ observed_at: observed, expires_at: expires,
118
+ });
119
+ while (records.size > boundedMax) {
120
+ const oldest = [...records.entries()].sort((a, b) => a[1].observed_at - b[1].observed_at)[0]?.[0];
121
+ if (!oldest) break;
122
+ records.delete(oldest);
123
+ }
124
+ return get(provider, model);
125
+ };
126
+
127
+ const list = () => [...records.values()].map((entry) => view(entry, clock())).filter((entry) => entry.fresh).slice(0, boundedMax);
128
+
129
+ return { get, record, list, ttls: { ...limits }, maxEntries: boundedMax };
130
+ }
@@ -1,182 +1,182 @@
1
- // Secret-free inventory projection for Harness providers.
2
- //
3
- // Harness owns the live provider/model catalog; Crew owns the routing policy
4
- // and (where explicitly marked) profile provenance. This module only joins
5
- // those observations into a bounded record set for lifecycle/readiness UIs.
6
-
7
- import { classifyCredentialReference } from './credential-reference.mjs';
8
-
9
- const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
10
- const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
11
- const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
12
- export const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
13
-
14
- function text(value) {
15
- return typeof value === 'string' && value.trim() ? value.trim() : null;
16
- }
17
-
18
- function normalizeProviderId(value) {
19
- return text(value);
20
- }
21
-
22
- function normalizeCredentialRefs(declarations) {
23
- const entries = Array.isArray(declarations) ? declarations : [declarations];
24
- const seen = new Map();
25
- const refs = [];
26
- let redacted = false;
27
- for (const declaration of entries) {
28
- const raw = declaration?.credential_refs ?? declaration?.credential_ref;
29
- const values = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
30
- for (const candidate of values) {
31
- const kind = text(typeof candidate === 'object' ? candidate.kind : null) ?? 'env';
32
- const classified = classifyCredentialReference(candidate, { kind });
33
- if (classified.redacted) redacted = true;
34
- const name = classified.value;
35
- const ownership = text(typeof candidate === 'object' ? candidate.ownership : null) ?? 'crew';
36
- const normalized = {
37
- kind: ['env', 'crew-store', 'harness-store', 'unknown'].includes(kind) ? kind : 'unknown',
38
- name_or_handle: name,
39
- ownership: ['crew', 'user', 'external', 'unknown'].includes(ownership) ? ownership : 'unknown',
40
- };
41
- if (!name) continue;
42
- const key = `${normalized.kind}:${normalized.name_or_handle}`;
43
- const existing = seen.get(key);
44
- if (existing) {
45
- if (existing.ownership !== normalized.ownership) existing.ownership = 'unknown';
46
- } else {
47
- seen.set(key, normalized);
48
- refs.push(normalized);
49
- }
50
- }
51
- }
52
- return { refs, redacted };
53
- }
54
-
55
- function findPriorityIndex(policy, key, providerId) {
56
- const list = Array.isArray(policy?.[key]) ? policy[key] : [];
57
- const index = list.findIndex((entry) => normalizeProviderId(entry?.provider) === providerId);
58
- return index >= 0 ? index : null;
59
- }
60
-
61
- function declarationsFor(declarations, id) {
62
- return declarations.filter((entry) => normalizeProviderId(entry?.id) === id);
63
- }
64
-
65
- function normalizeAuthority(value) {
66
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
67
- const kind = text(value.kind);
68
- const locator = text(value.locator);
69
- return kind && locator ? { kind, locator } : null;
70
- }
71
-
72
- function catalogFor(providers, id) {
73
- return providers.find((entry) => normalizeProviderId(entry?.id) === id) ?? null;
74
- }
75
-
76
- function normalizeOrigin(value) {
77
- const origin = text(value) ?? 'unknown';
78
- return LIFECYCLE_ORIGINS.has(origin) ? origin : 'unknown';
79
- }
80
-
81
- function normalizeOwnership(value) {
82
- const ownership = text(value) ?? 'unknown';
83
- return OWNERSHIPS.has(ownership) ? ownership : 'unknown';
84
- }
85
-
86
- function modelIds(provider) {
87
- if (!Array.isArray(provider?.models)) return [];
88
- return [...new Set(provider.models.map((model) => text(typeof model === 'object' ? model?.id : model)).filter(Boolean))];
89
- }
90
-
91
- /**
92
- * Build a secret-free provider inventory from live Harness and Crew-owned
93
- * observations. No credential value is accepted or copied into the result.
94
- */
95
- export function buildProviderInventory({ catalog = {}, declarations = [], policy = {}, tombstones = {}, activeJobs = [], additionalProviderIds = [] } = {}) {
96
- const catalogProviders = Array.isArray(catalog?.providers) ? catalog.providers : [];
97
- const safeDeclarations = Array.isArray(declarations) ? declarations : [];
98
- const ids = [];
99
- const seen = new Set();
100
- for (const source of [catalogProviders, safeDeclarations, Array.isArray(additionalProviderIds) ? additionalProviderIds.map((id) => ({ id })) : []]) {
101
- for (const entry of source) {
102
- const id = normalizeProviderId(entry?.id);
103
- if (!id || seen.has(id)) continue;
104
- seen.add(id);
105
- ids.push(id);
106
- }
107
- }
108
-
109
- const harnessDefault = normalizeProviderId(catalog?.harness_default?.provider);
110
- const records = ids.map((id) => {
111
- const catalogEntry = catalogFor(catalogProviders, id);
112
- const providerDeclarations = declarationsFor(safeDeclarations, id);
113
- const baseDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'crew-profile');
114
- const userDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'harness-settings');
115
- const declaration = providerDeclarations[0] ?? null;
116
- const declared = declaration !== null;
117
- const catalogued = catalogEntry !== null;
118
- const tombstoned = tombstones?.[id] === 'absent';
119
- const configured = declared;
120
- const active = Array.isArray(activeJobs)
121
- ? activeJobs.filter((job) => normalizeProviderId(job?.provider) === id
122
- && !['done', 'failed', 'cancelled'].includes(job?.status)).length
123
- : Number.isInteger(activeJobs?.[id]) ? Math.max(0, activeJobs[id]) : 0;
124
- const declarationFile = text(declaration?.file);
125
- const locator = text(declaration?.locator);
126
-
127
- const authorities = providerDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)).filter(Boolean);
128
- const uniqueAuthorities = [...new Map(authorities.map((authority) => [`${authority.kind}:${authority.locator}`, authority])).values()];
129
- const mutableAuthorities = uniqueAuthorities.filter((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
130
- const immutable = IMMUTABLE_HARNESS_PROVIDER_IDS.includes(id);
131
- const allAuthoritiesKnown = providerDeclarations.length > 0
132
- && uniqueAuthorities.length === providerDeclarations.length
133
- && uniqueAuthorities.every((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
134
- const deleteCapability = immutable ? 'immutable-builtin' : allAuthoritiesKnown && mutableAuthorities.length > 0 ? 'supported' : 'source-unresolved';
135
- const deleteBlocker = immutable ? 'PROVIDER_BUILTIN_IMMUTABLE' : deleteCapability === 'supported' ? null : 'PROVIDER_DELETE_SOURCE_UNRESOLVED';
136
- const origin = normalizeOrigin(declaration?.origin ?? (immutable ? 'builtin' : declared ? 'profile-managed' : 'unknown'));
137
- const ownership = normalizeOwnership(declaration?.ownership ?? (immutable ? 'harness' : declared ? 'crew-managed-profile' : 'unknown'));
138
- const credentialProjection = normalizeCredentialRefs(providerDeclarations);
139
- return {
140
- id,
141
- display_name: text(declaration?.display_name) ?? text(declaration?.name) ?? text(catalogEntry?.name) ?? id,
142
- origin,
143
- ownership,
144
- declaration: {
145
- ...(declarationFile ? { file: declarationFile } : {}),
146
- ...(locator ? { locator } : {}),
147
- ...(normalizeAuthority(declaration?.declaration_authority) ? { authority: normalizeAuthority(declaration.declaration_authority) } : {}),
148
- present: declared,
149
- },
150
- declaration_authorities: uniqueAuthorities,
151
- layers: {
152
- base: [...new Set(baseDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
153
- user: [...new Set(userDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
154
- },
155
- native_removable: userDeclarations.length > 0 && baseDeclarations.length === 0,
156
- delete_capability: deleteCapability,
157
- ...(deleteBlocker ? { delete_blocker: deleteBlocker } : {}),
158
- models: modelIds(catalogEntry),
159
- lifecycle: {
160
- installed: declared || catalogued,
161
- configured,
162
- enabled: !tombstoned && declaration?.enabled !== false,
163
- catalogued,
164
- },
165
- credential_refs: credentialProjection.refs,
166
- ...(credentialProjection.redacted ? { credential_status: 'present-redacted' } : {}),
167
- references: {
168
- harness_default: harnessDefault === id,
169
- ...(harnessDefault === id && normalizeAuthority(catalog?.harness_default_authority) ? { harness_default_authority: normalizeAuthority(catalog.harness_default_authority) } : {}),
170
- worker_priority: findPriorityIndex(policy?.worker, 'priority', id),
171
- worker_escalation: findPriorityIndex(policy?.worker, 'escalation_priority', id),
172
- reviewer_priority: findPriorityIndex(policy?.reviewer ?? policy?.review, 'priority', id),
173
- reviewer_escalation: findPriorityIndex(policy?.reviewer ?? policy?.review, 'escalation_priority', id),
174
- active_jobs: active,
175
- },
176
- desired_state: tombstoned ? 'absent' : 'present',
177
- activation: declared ? 'restart-required' : 'live',
178
- };
179
- });
180
-
181
- return { schema_version: 1, records, harness_default: catalog?.harness_default ?? null };
182
- }
1
+ // Secret-free inventory projection for Harness providers.
2
+ //
3
+ // Harness owns the live provider/model catalog; Crew owns the routing policy
4
+ // and (where explicitly marked) profile provenance. This module only joins
5
+ // those observations into a bounded record set for lifecycle/readiness UIs.
6
+
7
+ import { classifyCredentialReference } from './credential-reference.mjs';
8
+
9
+ const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
10
+ const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
11
+ const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
12
+ export const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
13
+
14
+ function text(value) {
15
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
16
+ }
17
+
18
+ function normalizeProviderId(value) {
19
+ return text(value);
20
+ }
21
+
22
+ function normalizeCredentialRefs(declarations) {
23
+ const entries = Array.isArray(declarations) ? declarations : [declarations];
24
+ const seen = new Map();
25
+ const refs = [];
26
+ let redacted = false;
27
+ for (const declaration of entries) {
28
+ const raw = declaration?.credential_refs ?? declaration?.credential_ref;
29
+ const values = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
30
+ for (const candidate of values) {
31
+ const kind = text(typeof candidate === 'object' ? candidate.kind : null) ?? 'env';
32
+ const classified = classifyCredentialReference(candidate, { kind });
33
+ if (classified.redacted) redacted = true;
34
+ const name = classified.value;
35
+ const ownership = text(typeof candidate === 'object' ? candidate.ownership : null) ?? 'crew';
36
+ const normalized = {
37
+ kind: ['env', 'crew-store', 'harness-store', 'unknown'].includes(kind) ? kind : 'unknown',
38
+ name_or_handle: name,
39
+ ownership: ['crew', 'user', 'external', 'unknown'].includes(ownership) ? ownership : 'unknown',
40
+ };
41
+ if (!name) continue;
42
+ const key = `${normalized.kind}:${normalized.name_or_handle}`;
43
+ const existing = seen.get(key);
44
+ if (existing) {
45
+ if (existing.ownership !== normalized.ownership) existing.ownership = 'unknown';
46
+ } else {
47
+ seen.set(key, normalized);
48
+ refs.push(normalized);
49
+ }
50
+ }
51
+ }
52
+ return { refs, redacted };
53
+ }
54
+
55
+ function findPriorityIndex(policy, key, providerId) {
56
+ const list = Array.isArray(policy?.[key]) ? policy[key] : [];
57
+ const index = list.findIndex((entry) => normalizeProviderId(entry?.provider) === providerId);
58
+ return index >= 0 ? index : null;
59
+ }
60
+
61
+ function declarationsFor(declarations, id) {
62
+ return declarations.filter((entry) => normalizeProviderId(entry?.id) === id);
63
+ }
64
+
65
+ function normalizeAuthority(value) {
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
67
+ const kind = text(value.kind);
68
+ const locator = text(value.locator);
69
+ return kind && locator ? { kind, locator } : null;
70
+ }
71
+
72
+ function catalogFor(providers, id) {
73
+ return providers.find((entry) => normalizeProviderId(entry?.id) === id) ?? null;
74
+ }
75
+
76
+ function normalizeOrigin(value) {
77
+ const origin = text(value) ?? 'unknown';
78
+ return LIFECYCLE_ORIGINS.has(origin) ? origin : 'unknown';
79
+ }
80
+
81
+ function normalizeOwnership(value) {
82
+ const ownership = text(value) ?? 'unknown';
83
+ return OWNERSHIPS.has(ownership) ? ownership : 'unknown';
84
+ }
85
+
86
+ function modelIds(provider) {
87
+ if (!Array.isArray(provider?.models)) return [];
88
+ return [...new Set(provider.models.map((model) => text(typeof model === 'object' ? model?.id : model)).filter(Boolean))];
89
+ }
90
+
91
+ /**
92
+ * Build a secret-free provider inventory from live Harness and Crew-owned
93
+ * observations. No credential value is accepted or copied into the result.
94
+ */
95
+ export function buildProviderInventory({ catalog = {}, declarations = [], policy = {}, tombstones = {}, activeJobs = [], additionalProviderIds = [] } = {}) {
96
+ const catalogProviders = Array.isArray(catalog?.providers) ? catalog.providers : [];
97
+ const safeDeclarations = Array.isArray(declarations) ? declarations : [];
98
+ const ids = [];
99
+ const seen = new Set();
100
+ for (const source of [catalogProviders, safeDeclarations, Array.isArray(additionalProviderIds) ? additionalProviderIds.map((id) => ({ id })) : []]) {
101
+ for (const entry of source) {
102
+ const id = normalizeProviderId(entry?.id);
103
+ if (!id || seen.has(id)) continue;
104
+ seen.add(id);
105
+ ids.push(id);
106
+ }
107
+ }
108
+
109
+ const harnessDefault = normalizeProviderId(catalog?.harness_default?.provider);
110
+ const records = ids.map((id) => {
111
+ const catalogEntry = catalogFor(catalogProviders, id);
112
+ const providerDeclarations = declarationsFor(safeDeclarations, id);
113
+ const baseDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'crew-profile');
114
+ const userDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'harness-settings');
115
+ const declaration = providerDeclarations[0] ?? null;
116
+ const declared = declaration !== null;
117
+ const catalogued = catalogEntry !== null;
118
+ const tombstoned = tombstones?.[id] === 'absent';
119
+ const configured = declared;
120
+ const active = Array.isArray(activeJobs)
121
+ ? activeJobs.filter((job) => normalizeProviderId(job?.provider) === id
122
+ && !['done', 'failed', 'cancelled'].includes(job?.status)).length
123
+ : Number.isInteger(activeJobs?.[id]) ? Math.max(0, activeJobs[id]) : 0;
124
+ const declarationFile = text(declaration?.file);
125
+ const locator = text(declaration?.locator);
126
+
127
+ const authorities = providerDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)).filter(Boolean);
128
+ const uniqueAuthorities = [...new Map(authorities.map((authority) => [`${authority.kind}:${authority.locator}`, authority])).values()];
129
+ const mutableAuthorities = uniqueAuthorities.filter((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
130
+ const immutable = IMMUTABLE_HARNESS_PROVIDER_IDS.includes(id);
131
+ const allAuthoritiesKnown = providerDeclarations.length > 0
132
+ && uniqueAuthorities.length === providerDeclarations.length
133
+ && uniqueAuthorities.every((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
134
+ const deleteCapability = immutable ? 'immutable-builtin' : allAuthoritiesKnown && mutableAuthorities.length > 0 ? 'supported' : 'source-unresolved';
135
+ const deleteBlocker = immutable ? 'PROVIDER_BUILTIN_IMMUTABLE' : deleteCapability === 'supported' ? null : 'PROVIDER_DELETE_SOURCE_UNRESOLVED';
136
+ const origin = normalizeOrigin(declaration?.origin ?? (immutable ? 'builtin' : declared ? 'profile-managed' : 'unknown'));
137
+ const ownership = normalizeOwnership(declaration?.ownership ?? (immutable ? 'harness' : declared ? 'crew-managed-profile' : 'unknown'));
138
+ const credentialProjection = normalizeCredentialRefs(providerDeclarations);
139
+ return {
140
+ id,
141
+ display_name: text(declaration?.display_name) ?? text(declaration?.name) ?? text(catalogEntry?.name) ?? id,
142
+ origin,
143
+ ownership,
144
+ declaration: {
145
+ ...(declarationFile ? { file: declarationFile } : {}),
146
+ ...(locator ? { locator } : {}),
147
+ ...(normalizeAuthority(declaration?.declaration_authority) ? { authority: normalizeAuthority(declaration.declaration_authority) } : {}),
148
+ present: declared,
149
+ },
150
+ declaration_authorities: uniqueAuthorities,
151
+ layers: {
152
+ base: [...new Set(baseDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
153
+ user: [...new Set(userDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
154
+ },
155
+ native_removable: userDeclarations.length > 0 && baseDeclarations.length === 0,
156
+ delete_capability: deleteCapability,
157
+ ...(deleteBlocker ? { delete_blocker: deleteBlocker } : {}),
158
+ models: modelIds(catalogEntry),
159
+ lifecycle: {
160
+ installed: declared || catalogued,
161
+ configured,
162
+ enabled: !tombstoned && declaration?.enabled !== false,
163
+ catalogued,
164
+ },
165
+ credential_refs: credentialProjection.refs,
166
+ ...(credentialProjection.redacted ? { credential_status: 'present-redacted' } : {}),
167
+ references: {
168
+ harness_default: harnessDefault === id,
169
+ ...(harnessDefault === id && normalizeAuthority(catalog?.harness_default_authority) ? { harness_default_authority: normalizeAuthority(catalog.harness_default_authority) } : {}),
170
+ worker_priority: findPriorityIndex(policy?.worker, 'priority', id),
171
+ worker_escalation: findPriorityIndex(policy?.worker, 'escalation_priority', id),
172
+ reviewer_priority: findPriorityIndex(policy?.reviewer ?? policy?.review, 'priority', id),
173
+ reviewer_escalation: findPriorityIndex(policy?.reviewer ?? policy?.review, 'escalation_priority', id),
174
+ active_jobs: active,
175
+ },
176
+ desired_state: tombstoned ? 'absent' : 'present',
177
+ activation: declared ? 'restart-required' : 'live',
178
+ };
179
+ });
180
+
181
+ return { schema_version: 1, records, harness_default: catalog?.harness_default ?? null };
182
+ }