@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.5
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/.claude-plugin/marketplace.json +16 -17
- package/.claude-plugin/plugin.json +7 -1
- package/README.md +44 -15
- package/README.zh.md +37 -12
- package/agents/ds-flash.md +16 -18
- package/agents/ds-pro.md +16 -24
- package/agents/ds-reviewer.md +15 -15
- package/agents/ds-worker.md +16 -16
- package/bin/dsh-crew.mjs +16 -12
- package/codex/AGENTS.md +37 -85
- package/codex/agents/ds-flash.toml +18 -14
- package/codex/agents/ds-pro.toml +18 -15
- package/codex/agents/ds-reviewer.toml +17 -13
- package/codex/agents/ds-worker.toml +17 -13
- package/codex/prompts/dsh-config.md +17 -1
- package/codex/prompts/dsh-status.md +5 -1
- package/commands/config.md +16 -4
- package/commands/status.md +6 -2
- package/docs/installation.md +54 -9
- package/docs/readiness-matrix.md +14 -6
- package/docs/ui-surfaces.md +32 -17
- package/lib/client.js +769 -177
- package/official-web-bridge/lib/client.js +297 -209
- package/official-web-bridge/overlay-entry.mjs +59 -0
- package/package.json +6 -3
- package/scripts/build-client.mjs +2 -0
- package/scripts/setup.mjs +8 -7
- package/scripts/verify-crew-ui-polish.mjs +145 -0
- package/scripts/verify-history-ui.mjs +97 -0
- package/scripts/verify-npm-install.mjs +1 -1
- package/src/client/history-panel.tsx +108 -0
- package/src/client/host-readiness.mjs +15 -6
- package/src/client/index.tsx +91 -69
- package/src/client/model-callability-view.mjs +17 -0
- package/src/client/panel-chrome.tsx +40 -0
- package/src/client/quick-panel.tsx +88 -73
- package/src/client/readiness-envelope.mjs +34 -0
- package/src/client/task-telemetry.mjs +11 -4
- package/src/config-readiness.mjs +181 -115
- package/src/extension-contract.mjs +44 -26
- package/src/history/admission-gate.mjs +51 -0
- package/src/history/archive-store.mjs +261 -0
- package/src/history/cleanup-plan.mjs +64 -0
- package/src/history/http.mjs +32 -0
- package/src/history/operation.mjs +86 -0
- package/src/history/runner.mjs +51 -0
- package/src/history/runtime.mjs +28 -0
- package/src/history/service.mjs +137 -0
- package/src/history/state.mjs +27 -0
- package/src/hub/index.mjs +141 -24
- package/src/hub-client.mjs +17 -15
- package/src/image-output.mjs +71 -0
- package/src/install/install-legacy.mjs +126 -23
- package/src/install/npx-lifecycle.mjs +373 -64
- package/src/install/official-frontend-assets.mjs +78 -0
- package/src/install/payload-content.mjs +88 -0
- package/src/install/windows-startup.mjs +107 -19
- package/src/install/windows-supervisor-adapter.mjs +443 -0
- package/src/install/windows-supervisor-lifecycle.mjs +782 -0
- package/src/install/zcode.mjs +53 -14
- package/src/job-contracts.mjs +37 -31
- package/src/jobs.mjs +2 -2
- package/src/mcp-runtime.mjs +74 -9
- package/src/model-callability-contract.mjs +79 -0
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +0 -2
- package/src/provider-health.mjs +130 -124
- package/src/readiness-matrix.mjs +20 -15
- package/src/runtime-identity-contract.mjs +34 -0
- package/src/runtime-identity.mjs +14 -4
- package/src/runtime-readiness-snapshot.mjs +285 -87
- package/src/server.mjs +86 -76
- package/src/standalone-sdk.mjs +23 -0
- package/src/supervisor/restart-request.mjs +64 -10
- package/src/workflow-runtime.mjs +28 -13
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +327 -51
- package/windows/supervisor-control.ps1 +467 -0
- package/zcode/AGENTS.md +4 -1
- package/zcode/agents/ds-reviewer.md +31 -36
- package/zcode/agents/ds-worker.md +31 -39
- package/zcode/commands/dsh-config.md +17 -6
- package/zcode/commands/dsh-status.md +5 -4
- package/.mcp.json +0 -8
package/src/install/zcode.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The generated files make ZCode dispatch through the same dsh-crew MCP server
|
|
4
4
|
// used by Codex and Claude while keeping ZCode's native config precedence.
|
|
5
5
|
|
|
6
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { dirname, join, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
@@ -62,8 +62,10 @@ function normalizePath(value) {
|
|
|
62
62
|
|
|
63
63
|
function serverTarget(server) {
|
|
64
64
|
if (!server || typeof server !== 'object') return null;
|
|
65
|
-
if (server.command !== 'node' || !Array.isArray(server.args) || typeof server.args[0] !== 'string') return null;
|
|
66
|
-
|
|
65
|
+
if (server.command !== 'node' || !Array.isArray(server.args) || server.args.length !== 1 || typeof server.args[0] !== 'string') return null;
|
|
66
|
+
const target = normalizePath(server.args[0]);
|
|
67
|
+
if (!target) return null;
|
|
68
|
+
try { return statSync(target).isFile() ? target : null; } catch { return null; }
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function nativeServers(config) {
|
|
@@ -250,13 +252,19 @@ function installTemplates({ home, root, priorFiles = [] }) {
|
|
|
250
252
|
function updateMcp({ home, root }) {
|
|
251
253
|
const source = configSource({ home });
|
|
252
254
|
const target = expectedTarget({ root });
|
|
255
|
+
const normalizedTarget = normalizePath(target);
|
|
253
256
|
const current = source.servers[SERVER];
|
|
254
257
|
const currentTarget = serverTarget(current);
|
|
258
|
+
const currentDeclaredTarget = Array.isArray(current?.args) && typeof current.args[0] === 'string'
|
|
259
|
+
? normalizePath(current.args[0])
|
|
260
|
+
: null;
|
|
255
261
|
const owned = ownership({ home });
|
|
256
|
-
if (current && currentTarget !==
|
|
262
|
+
if (current && currentTarget !== normalizedTarget) {
|
|
257
263
|
// Only an exact previously-owned entry may be repaired. Any foreign
|
|
258
264
|
// command, even if it is also named dsh-crew, is a hard collision.
|
|
259
|
-
if (!(owned
|
|
265
|
+
if (!(owned
|
|
266
|
+
&& normalizePath(owned.config_file) === normalizePath(source.file)
|
|
267
|
+
&& normalizePath(owned.target) === currentDeclaredTarget)) {
|
|
260
268
|
return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
|
|
261
269
|
}
|
|
262
270
|
}
|
|
@@ -303,22 +311,53 @@ function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
|
|
|
303
311
|
const source = configSource({ home });
|
|
304
312
|
const configured = serverTarget(source.servers[SERVER]);
|
|
305
313
|
const owned = ownership({ home });
|
|
314
|
+
const expectedPolicy = managedPolicyBlock(root);
|
|
315
|
+
const installedPolicy = readText(join(home, '.zcode', 'AGENTS.md'));
|
|
316
|
+
const templates = templateFiles({ home, root });
|
|
317
|
+
const templateReady = (entry) => {
|
|
318
|
+
if (!entry) return false;
|
|
319
|
+
const { source: sourceFile, dest } = entry;
|
|
320
|
+
const sourceText = readText(sourceFile);
|
|
321
|
+
const installedText = readText(dest);
|
|
322
|
+
return sourceText !== null && installedText === sourceText;
|
|
323
|
+
};
|
|
324
|
+
const templateBySuffix = (suffix) => templates.find(({ dest }) => dest.endsWith(suffix));
|
|
325
|
+
const ownershipFilesReady = !!owned && templates.every(({ source: sourceFile, dest }) => {
|
|
326
|
+
const expectedText = readText(sourceFile);
|
|
327
|
+
const installedText = readText(dest);
|
|
328
|
+
const record = Array.isArray(owned.files)
|
|
329
|
+
? owned.files.find((entry) => normalizePath(entry?.path) === normalizePath(dest))
|
|
330
|
+
: null;
|
|
331
|
+
return expectedText !== null
|
|
332
|
+
&& installedText === expectedText
|
|
333
|
+
&& record?.managed_sha256 === contentHash(expectedText)
|
|
334
|
+
&& contentHash(installedText) === record.managed_sha256;
|
|
335
|
+
});
|
|
306
336
|
const components = {
|
|
307
337
|
mcp: configured === expected,
|
|
308
|
-
policy:
|
|
309
|
-
worker_agent:
|
|
310
|
-
reviewer_agent:
|
|
311
|
-
config_prompt:
|
|
312
|
-
status_prompt:
|
|
313
|
-
ownership: !!owned
|
|
338
|
+
policy: !!expectedPolicy && typeof installedPolicy === 'string' && installedPolicy.includes(expectedPolicy),
|
|
339
|
+
worker_agent: templateReady(templateBySuffix(join('agents', 'ds-worker.md'))),
|
|
340
|
+
reviewer_agent: templateReady(templateBySuffix(join('agents', 'ds-reviewer.md'))),
|
|
341
|
+
config_prompt: templateReady(templateBySuffix(join('commands', 'dsh-config.md'))),
|
|
342
|
+
status_prompt: templateReady(templateBySuffix(join('commands', 'dsh-status.md'))),
|
|
343
|
+
ownership: !!owned
|
|
344
|
+
&& normalizePath(owned.config_file) === normalizePath(source.file)
|
|
345
|
+
&& normalizePath(owned.target) === expected
|
|
346
|
+
&& ownershipFilesReady,
|
|
314
347
|
};
|
|
315
|
-
|
|
348
|
+
const policyFootprint = typeof installedPolicy === 'string'
|
|
349
|
+
&& installedPolicy.includes(POLICY_START)
|
|
350
|
+
&& installedPolicy.includes(POLICY_END);
|
|
351
|
+
const installed = Object.hasOwn(source.servers, SERVER)
|
|
352
|
+
|| !!owned
|
|
353
|
+
|| policyFootprint
|
|
354
|
+
|| templates.some(({ dest }) => existsSync(dest));
|
|
355
|
+
return { components, source, expected, configured, owned, installed };
|
|
316
356
|
}
|
|
317
357
|
|
|
318
358
|
export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
|
|
319
|
-
const { components, source, expected, configured } = zcodeComponents({ home, root });
|
|
359
|
+
const { components, source, expected, configured, installed } = zcodeComponents({ home, root });
|
|
320
360
|
const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
|
|
321
|
-
const installed = Object.values(components).some(Boolean);
|
|
322
361
|
return {
|
|
323
362
|
installed,
|
|
324
363
|
ready: missing.length === 0,
|
package/src/job-contracts.mjs
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
export const JOB_CONTRACT_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
10
10
|
export const JOB_EVENT_TYPES = Object.freeze([
|
|
11
|
-
'job.created',
|
|
12
|
-
'job.started',
|
|
13
|
-
'runtime.bound',
|
|
14
|
-
'model.selected',
|
|
15
|
-
'model.admitted',
|
|
16
|
-
'agent.created',
|
|
11
|
+
'job.created',
|
|
12
|
+
'job.started',
|
|
13
|
+
'runtime.bound',
|
|
14
|
+
'model.selected',
|
|
15
|
+
'model.admitted',
|
|
16
|
+
'agent.created',
|
|
17
17
|
'model.fallback',
|
|
18
18
|
'worker.started',
|
|
19
19
|
'worker.completed',
|
|
@@ -82,6 +82,11 @@ function evidenceStatus(view) {
|
|
|
82
82
|
if (view?.status === 'failed' || view?.phase === 'failed' || view?.outcome?.execution_status === 'failed') return 'FAIL';
|
|
83
83
|
if (view?.review?.verdict === 'request_changes' || view?.outcome?.task_status === 'partial') return 'PARTIAL';
|
|
84
84
|
if (view?.outcome?.task_status === 'blocked') return 'BLOCKED';
|
|
85
|
+
if (view?.role === 'reviewer' && view?.outcome == null) {
|
|
86
|
+
return view.status === 'done' && view.review?.status === 'done'
|
|
87
|
+
&& view.review.verdict === 'approve' && view.review.delivery_complete === true
|
|
88
|
+
&& view.review.mutated_candidate !== true ? 'PASS' : 'PARTIAL';
|
|
89
|
+
}
|
|
85
90
|
if (view?.status === 'done' && view?.outcome?.task_status === 'success') return 'PASS';
|
|
86
91
|
return 'PARTIAL';
|
|
87
92
|
}
|
|
@@ -122,7 +127,7 @@ function compactSelectionTrace(attempts) {
|
|
|
122
127
|
});
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
function changedFilesFromView(view) {
|
|
130
|
+
function changedFilesFromView(view) {
|
|
126
131
|
if (Array.isArray(view?.candidate?.changed_files)) return view.candidate.changed_files;
|
|
127
132
|
const changes = view?.workspace_diff?.changes;
|
|
128
133
|
if (!changes || typeof changes !== 'object') return [];
|
|
@@ -132,24 +137,24 @@ function changedFilesFromView(view) {
|
|
|
132
137
|
...(Array.isArray(changes.renamed) ? changes.renamed : []),
|
|
133
138
|
...(Array.isArray(changes.untracked) ? changes.untracked : []),
|
|
134
139
|
])];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function boundedExecutionContext(value) {
|
|
138
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
139
|
-
const executionPlane = boundedText(value.execution_plane, 64);
|
|
140
|
-
const profile = boundedText(value.profile, 64);
|
|
141
|
-
const runtimeId = boundedText(value.runtime_id, 128);
|
|
142
|
-
const port = Number.isInteger(value.listen_port) ? value.listen_port : null;
|
|
143
|
-
const ingress = value.ingress === 'official-3080' || value.ingress === 'direct-3210' ? value.ingress : null;
|
|
144
|
-
if (!executionPlane && !profile && !runtimeId && port === null && !ingress) return null;
|
|
145
|
-
return {
|
|
146
|
-
execution_plane: executionPlane,
|
|
147
|
-
profile,
|
|
148
|
-
listen_port: port,
|
|
149
|
-
runtime_id: runtimeId,
|
|
150
|
-
...(ingress ? { ingress } : {}),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function boundedExecutionContext(value) {
|
|
143
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
144
|
+
const executionPlane = boundedText(value.execution_plane, 64);
|
|
145
|
+
const profile = boundedText(value.profile, 64);
|
|
146
|
+
const runtimeId = boundedText(value.runtime_id, 128);
|
|
147
|
+
const port = Number.isInteger(value.listen_port) ? value.listen_port : null;
|
|
148
|
+
const ingress = value.ingress === 'official-3080' || value.ingress === 'direct-3210' ? value.ingress : null;
|
|
149
|
+
if (!executionPlane && !profile && !runtimeId && port === null && !ingress) return null;
|
|
150
|
+
return {
|
|
151
|
+
execution_plane: executionPlane,
|
|
152
|
+
profile,
|
|
153
|
+
listen_port: port,
|
|
154
|
+
runtime_id: runtimeId,
|
|
155
|
+
...(ingress ? { ingress } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
153
158
|
|
|
154
159
|
/**
|
|
155
160
|
* Build the machine-first Result Contract for a workflow.
|
|
@@ -165,6 +170,7 @@ export function buildEvidenceEnvelope(view = {}) {
|
|
|
165
170
|
const errorMessage = boundedText(view.error, 1000);
|
|
166
171
|
const changedFiles = boundedStrings(changedFilesFromView(view), { count: 120, length: 500 });
|
|
167
172
|
const status = evidenceStatus(view);
|
|
173
|
+
const reviewOnly = view.role === 'reviewer' && view.outcome == null;
|
|
168
174
|
const selectionAttempts = Array.isArray(view.child_attempts) && view.child_attempts.length > 0
|
|
169
175
|
? view.child_attempts
|
|
170
176
|
: (view.selection_trace || view.provider || view.model ? [view] : []);
|
|
@@ -176,18 +182,18 @@ export function buildEvidenceEnvelope(view = {}) {
|
|
|
176
182
|
status,
|
|
177
183
|
summary: {
|
|
178
184
|
phase: view.phase ?? null,
|
|
179
|
-
task_status: outcome.task_status ?? null,
|
|
180
|
-
execution_status: outcome.execution_status ?? null,
|
|
185
|
+
task_status: outcome.task_status ?? (reviewOnly && status === 'PASS' ? 'success' : null),
|
|
186
|
+
execution_status: outcome.execution_status ?? (reviewOnly && review?.status === 'done' ? 'completed' : null),
|
|
181
187
|
tests_status: outcome.tests_status ?? null,
|
|
182
|
-
delivery_complete: outcome.delivery?.complete === true,
|
|
188
|
+
delivery_complete: reviewOnly ? review?.delivery_complete === true : outcome.delivery?.complete === true,
|
|
183
189
|
review_verdict: review?.verdict ?? null,
|
|
184
190
|
},
|
|
185
|
-
selection_trace: compactSelectionTrace(selectionAttempts),
|
|
186
|
-
execution_context: boundedExecutionContext(view.execution_context ?? selectionAttempts[0]?.execution_context),
|
|
191
|
+
selection_trace: compactSelectionTrace(selectionAttempts),
|
|
192
|
+
execution_context: boundedExecutionContext(view.execution_context ?? selectionAttempts[0]?.execution_context),
|
|
187
193
|
changed_files: changedFiles,
|
|
188
194
|
changes: boundedStrings(outcome.changes),
|
|
189
195
|
tests: boundedTests(outcome.tests),
|
|
190
|
-
risks: boundedStrings(outcome.risks),
|
|
196
|
+
risks: boundedStrings(reviewOnly ? review?.risks : outcome.risks),
|
|
191
197
|
unverified: boundedStrings(outcome.unverified),
|
|
192
198
|
review: review ? {
|
|
193
199
|
verdict: review.verdict ?? null,
|
package/src/jobs.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// never passes an explicit dshBin that would bypass it. The worker rides the
|
|
9
9
|
// `sdk-minimal` profile with the Crew overlay applied as a --patch.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { createStandaloneHarness } from './standalone-sdk.mjs';
|
|
12
12
|
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
13
13
|
import { createShardWriter } from './status-shard.mjs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -123,7 +123,7 @@ export function startJob({
|
|
|
123
123
|
throw new Error(`DEEPSEEK_API_KEY not found in env or ${join(CONFIG_DIR, '.env')}`);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const harness =
|
|
126
|
+
const harness = createStandaloneHarness({
|
|
127
127
|
profile: 'sdk-minimal',
|
|
128
128
|
patches: [CORDIS],
|
|
129
129
|
dshHome: CREW_DSH_HOME,
|
package/src/mcp-runtime.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// live entirely in workflow-runtime.mjs; the adapters here just execute.
|
|
9
9
|
|
|
10
10
|
import { createWorkflowRuntime } from './workflow-runtime.mjs';
|
|
11
|
-
import { normalizeGlobalConfig } from './policy.mjs';
|
|
11
|
+
import { CONFIG_SCHEMA_VERSION, normalizeGlobalConfig } from './policy.mjs';
|
|
12
12
|
import { buildDirectSelectionTrace, enrichSelectionTrace } from './model-routing.mjs';
|
|
13
13
|
import {
|
|
14
14
|
createIsolatedWorkspace,
|
|
@@ -43,7 +43,72 @@ export function buildEffectiveRuntimeConfig(globalRaw = {}, session = {}) {
|
|
|
43
43
|
if (session?.[key] !== undefined) patch[key] = session[key];
|
|
44
44
|
}
|
|
45
45
|
if (session?.enabled === false) patch.subagents_enabled = false;
|
|
46
|
-
|
|
46
|
+
const base = normalizeGlobalConfig(globalRaw);
|
|
47
|
+
if (Object.keys(patch).length === 0) return base;
|
|
48
|
+
|
|
49
|
+
// Session controls are expressed through the compatibility vocabulary,
|
|
50
|
+
// but a persisted schema-v4 config correctly ignores flat compatibility
|
|
51
|
+
// mirrors. Project the session once through the legacy normalizer, then
|
|
52
|
+
// apply only the policy dimensions the session is allowed to own onto the
|
|
53
|
+
// canonical snapshot. Model priorities, provider routing, fallback,
|
|
54
|
+
// adaptive ordering, health gates, and review gate remain untouched.
|
|
55
|
+
const compatibility = { ...base, ...patch, config_schema_version: 2 };
|
|
56
|
+
delete compatibility.worker;
|
|
57
|
+
delete compatibility.review;
|
|
58
|
+
delete compatibility.execution;
|
|
59
|
+
delete compatibility.legacy;
|
|
60
|
+
// Canonical flat mirrors describe the persisted base and must not outrank
|
|
61
|
+
// this session's compatibility command. Remove the derived role/review
|
|
62
|
+
// mirrors so the legacy projector can recompute them from the requested
|
|
63
|
+
// session mode and review opt-in.
|
|
64
|
+
delete compatibility.worker_state;
|
|
65
|
+
delete compatibility.review_state;
|
|
66
|
+
delete compatibility.auto_review;
|
|
67
|
+
if (patch.collaboration_mode !== undefined) {
|
|
68
|
+
delete compatibility.tier_policy;
|
|
69
|
+
delete compatibility.flash_state;
|
|
70
|
+
delete compatibility.pro_state;
|
|
71
|
+
} else if (patch.tier_policy !== undefined) {
|
|
72
|
+
delete compatibility.collaboration_mode;
|
|
73
|
+
delete compatibility.flash_state;
|
|
74
|
+
delete compatibility.pro_state;
|
|
75
|
+
} else if (patch.flash_state !== undefined || patch.pro_state !== undefined) {
|
|
76
|
+
compatibility.collaboration_mode = 'custom';
|
|
77
|
+
delete compatibility.tier_policy;
|
|
78
|
+
}
|
|
79
|
+
Object.assign(compatibility, patch);
|
|
80
|
+
const projected = normalizeGlobalConfig(compatibility);
|
|
81
|
+
return normalizeGlobalConfig({
|
|
82
|
+
...base,
|
|
83
|
+
config_schema_version: CONFIG_SCHEMA_VERSION,
|
|
84
|
+
subagents_enabled: projected.subagents_enabled,
|
|
85
|
+
main_agent_mode: projected.main_agent_mode,
|
|
86
|
+
execution: {
|
|
87
|
+
...base.execution,
|
|
88
|
+
enabled: projected.subagents_enabled,
|
|
89
|
+
default_effort: projected.execution.default_effort,
|
|
90
|
+
default_timeout_seconds: projected.execution.default_timeout_seconds,
|
|
91
|
+
mode: projected.execution.mode,
|
|
92
|
+
},
|
|
93
|
+
worker: {
|
|
94
|
+
...base.worker,
|
|
95
|
+
state: projected.worker.state,
|
|
96
|
+
model_policy: {
|
|
97
|
+
...base.worker.model_policy,
|
|
98
|
+
strategy: projected.worker.model_policy.strategy,
|
|
99
|
+
escalation: {
|
|
100
|
+
...base.worker.model_policy.escalation,
|
|
101
|
+
enabled: projected.worker.model_policy.escalation.enabled,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
review: {
|
|
106
|
+
...base.review,
|
|
107
|
+
state: projected.review.state,
|
|
108
|
+
auto_review: projected.review.auto_review,
|
|
109
|
+
},
|
|
110
|
+
legacy: projected.legacy,
|
|
111
|
+
});
|
|
47
112
|
}
|
|
48
113
|
|
|
49
114
|
/**
|
|
@@ -69,9 +134,9 @@ export function attemptFromView(view, spec) {
|
|
|
69
134
|
modelClassHint: spec.model_class_hint ?? null,
|
|
70
135
|
strategy: source ?? 'transport-selection',
|
|
71
136
|
candidateSet: logicalAttempt > 0 ? 'escalation' : 'primary',
|
|
72
|
-
provider: view?.provider ?? null,
|
|
73
|
-
model: view?.model ?? null,
|
|
74
|
-
execution_context: view?.execution_context ?? null,
|
|
137
|
+
provider: view?.provider ?? null,
|
|
138
|
+
model: view?.model ?? null,
|
|
139
|
+
execution_context: view?.execution_context ?? null,
|
|
75
140
|
source: source ?? 'transport-selection',
|
|
76
141
|
escalationReason: spec.escalation_reason ?? null,
|
|
77
142
|
});
|
|
@@ -87,10 +152,10 @@ export function attemptFromView(view, spec) {
|
|
|
87
152
|
// `view.attempt` may be an adapter routing-attempt (see below); the
|
|
88
153
|
// workflow's logical attempt number is always the spec value.
|
|
89
154
|
attempt: logicalAttempt,
|
|
90
|
-
provider: view?.provider ?? null,
|
|
91
|
-
model: view?.model ?? null,
|
|
92
|
-
execution_context: view?.execution_context ?? null,
|
|
93
|
-
selection_source: source,
|
|
155
|
+
provider: view?.provider ?? null,
|
|
156
|
+
model: view?.model ?? null,
|
|
157
|
+
execution_context: view?.execution_context ?? null,
|
|
158
|
+
selection_source: source,
|
|
94
159
|
selection_trace: selectionTrace,
|
|
95
160
|
status: view?.status ?? 'failed',
|
|
96
161
|
result: view?.result ?? null,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from './runtime-identity-contract.mjs';
|
|
2
|
+
|
|
3
|
+
export const MODEL_CALLABILITY_SCHEMA_VERSION = 2;
|
|
4
|
+
const ROLES = ['worker', 'reviewer'];
|
|
5
|
+
const STATES = new Set(['CALLABLE', 'NOT_CALLABLE', 'STALE', 'UNKNOWN', 'NOT_APPLICABLE']);
|
|
6
|
+
const SOURCES = new Set(['provider_health', 'execution']);
|
|
7
|
+
const MAX_EVIDENCE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function exactKeys(value, keys) {
|
|
10
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
&& Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function route(value) {
|
|
15
|
+
return typeof value?.provider === 'string' && value.provider.trim()
|
|
16
|
+
&& typeof value?.model === 'string' && value.model.trim()
|
|
17
|
+
? { provider: value.provider.trim(), model: value.model.trim() } : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validEvidence(role, now) {
|
|
21
|
+
if (!SOURCES.has(role.source) || !Number.isFinite(role.observed_at) || role.observed_at > now
|
|
22
|
+
|| !Number.isFinite(role.expires_at) || role.expires_at <= now
|
|
23
|
+
|| role.expires_at - role.observed_at > MAX_EVIDENCE_TTL_MS) return false;
|
|
24
|
+
if (role.source === 'execution') {
|
|
25
|
+
if (!role.last_success || typeof role.last_success !== 'object'
|
|
26
|
+
|| typeof role.last_success.job_id !== 'string' || !role.last_success.job_id.trim()
|
|
27
|
+
|| role.last_success.observed_at !== role.observed_at
|
|
28
|
+
|| role.last_success.expires_at !== role.expires_at) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateModelCallabilityV2({ projection, runtime, expectedEnabledRoles, expectedSelections, now = Date.now() } = {}) {
|
|
34
|
+
const fail = (reason_code) => ({ ok: false, reason_code, state: 'UNKNOWN' });
|
|
35
|
+
if (!projection || typeof projection !== 'object' || projection.schema_version !== MODEL_CALLABILITY_SCHEMA_VERSION) return fail('MODEL_CALLABILITY_SCHEMA_INVALID');
|
|
36
|
+
if (!isExactNativeCrewIdentity(runtime) || !isExactNativeCrewIdentity(projection.runtime_identity)
|
|
37
|
+
|| !sameCompleteRuntimeIdentity(runtime, projection.runtime_identity)
|
|
38
|
+
|| projection.current_runtime_id !== runtime.runtime_id) return fail('MODEL_CALLABILITY_RUNTIME_MISMATCH');
|
|
39
|
+
if (!Number.isFinite(projection.captured_at) || projection.captured_at > now) return fail('MODEL_CALLABILITY_CAPTURE_INVALID');
|
|
40
|
+
if (!exactKeys(projection.enabled_roles, ROLES) || ROLES.some((role) => typeof projection.enabled_roles[role] !== 'boolean')) return fail('MODEL_CALLABILITY_ROLES_INVALID');
|
|
41
|
+
if (expectedEnabledRoles && (!exactKeys(expectedEnabledRoles, ROLES) || ROLES.some((role) => projection.enabled_roles[role] !== expectedEnabledRoles[role]))) return fail('MODEL_CALLABILITY_ROLE_CONFIG_MISMATCH');
|
|
42
|
+
if (expectedSelections && !exactKeys(expectedSelections, ROLES)) return fail('MODEL_CALLABILITY_EXPECTED_SELECTIONS_INVALID');
|
|
43
|
+
if (!exactKeys(projection.roles, ROLES)) return fail('MODEL_CALLABILITY_ROLE_EVIDENCE_MISSING');
|
|
44
|
+
|
|
45
|
+
for (const roleName of ROLES) {
|
|
46
|
+
const role = projection.roles[roleName];
|
|
47
|
+
if (!role || !STATES.has(role.state)) return fail('MODEL_CALLABILITY_ROLE_STATE_INVALID');
|
|
48
|
+
const selected = route(role.selected);
|
|
49
|
+
if (role.selected !== undefined && role.selected !== null && !selected) return fail('MODEL_CALLABILITY_SELECTION_INVALID');
|
|
50
|
+
if (expectedSelections) {
|
|
51
|
+
const expectedRaw = expectedSelections[roleName];
|
|
52
|
+
const expected = expectedRaw === null ? null : route(expectedRaw);
|
|
53
|
+
if (expectedRaw !== null && !expected) return fail('MODEL_CALLABILITY_EXPECTED_SELECTION_INVALID');
|
|
54
|
+
if ((expected === null) !== (selected === null)
|
|
55
|
+
|| expected && (expected.provider !== selected.provider || expected.model !== selected.model)) return fail('MODEL_CALLABILITY_SELECTION_MISMATCH');
|
|
56
|
+
}
|
|
57
|
+
if (projection.enabled_roles[roleName] === false) {
|
|
58
|
+
if (role.state !== 'NOT_APPLICABLE' || role.reason_code !== 'ROLE_DISABLED') return fail('MODEL_CALLABILITY_DISABLED_ROLE_INVALID');
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (['CALLABLE', 'NOT_CALLABLE'].includes(role.state) && !selected) return fail('MODEL_CALLABILITY_SELECTION_INVALID');
|
|
62
|
+
if (projection.enabled_roles[roleName] === true && role.state === 'NOT_APPLICABLE') return fail('MODEL_CALLABILITY_ENABLED_ROLE_NOT_APPLICABLE');
|
|
63
|
+
if (['CALLABLE', 'NOT_CALLABLE'].includes(role.state)) {
|
|
64
|
+
if (!validEvidence(role, now)) return fail('MODEL_CALLABILITY_EVIDENCE_INVALID');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const activeRoles = ROLES.map((role) => projection.roles[role]).filter((_, index) => projection.enabled_roles[ROLES[index]] === true);
|
|
69
|
+
const activeStates = activeRoles.map((role) => role.state);
|
|
70
|
+
const state = activeStates.includes('NOT_CALLABLE') ? 'NOT_CALLABLE'
|
|
71
|
+
: activeStates.length > 0 && activeStates.every((value) => value === 'CALLABLE') ? 'CALLABLE'
|
|
72
|
+
: activeStates.includes('STALE') ? 'STALE' : 'UNKNOWN';
|
|
73
|
+
if (projection.overall !== state) return fail('MODEL_CALLABILITY_OVERALL_CONTRADICTION');
|
|
74
|
+
const evidenceExpirations = activeRoles.map((role) => role.expires_at).filter((value) => Number.isFinite(value));
|
|
75
|
+
const expectedExpires = evidenceExpirations.length ? Math.min(...evidenceExpirations) : null;
|
|
76
|
+
if (projection.expires_at !== expectedExpires) return fail('MODEL_CALLABILITY_EXPIRY_CONTRADICTION');
|
|
77
|
+
if (state === 'CALLABLE' && (!Number.isFinite(projection.expires_at) || projection.expires_at <= now)) return fail('MODEL_CALLABILITY_EXPIRED');
|
|
78
|
+
return { ok: true, state, reason_code: 'MODEL_CALLABILITY_VALID' };
|
|
79
|
+
}
|
package/src/multimodal.mjs
CHANGED
|
Binary file
|
|
@@ -429,8 +429,6 @@ export function registerOfficialWebBridge(ctx, options = {}) {
|
|
|
429
429
|
const QUICK_ALLOWLIST = new Set([
|
|
430
430
|
'/quick-config',
|
|
431
431
|
'/quick-status',
|
|
432
|
-
'/runtime/restart-request',
|
|
433
|
-
'/runtime/restart-status',
|
|
434
432
|
]);
|
|
435
433
|
const disposers = [];
|
|
436
434
|
for (const path of QUICK_ALLOWLIST) {
|