create-principles-disciple 1.146.7 → 1.146.9
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/_release/product-identity.json +1 -1
- package/console/dist/server/models/ExaminerEvidenceModel.d.ts +159 -0
- package/console/dist/server/models/ExaminerEvidenceModel.js +274 -0
- package/console/dist/server/models/ReceiptsConsoleModel.js +15 -1
- package/console/dist/server/routes/examiner-executor.d.ts +40 -4
- package/console/dist/server/routes/examiner-executor.js +54 -45
- package/console/dist/server/routes/examiner-runs.d.ts +196 -0
- package/console/dist/server/routes/examiner-runs.js +938 -0
- package/console/dist/server/routes/examiner-study-inputs.d.ts +147 -0
- package/console/dist/server/routes/examiner-study-inputs.js +263 -0
- package/console/dist/server/routes/examiner.d.ts +26 -17
- package/console/dist/server/routes/examiner.js +261 -12
- package/console/dist/types.d.ts +1 -0
- package/console/dist/web/assets/app.css +92 -0
- package/console/dist/web/assets/app.js +38 -32
- package/console/lab/lib/challenge-openclaw-host.mjs +17 -0
- package/console/lab/lib/challenge-runner.mjs +9 -0
- package/console/lab/lib/examiner-study.mjs +55 -0
- package/host-runtime/dist/internalization-consumer-cycle.js +13 -1
- package/host-runtime/package.json +1 -1
- package/package.json +3 -2
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +282 -282
- package/release-manager/package.json +2 -2
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { sendSuccess, sendError, sendNotFound } from '../utils/response.js';
|
|
4
|
+
import { sendSuccess, sendError, sendNotFound, sendJson } from '../utils/response.js';
|
|
5
5
|
import { PrinciplesConsoleModel } from '../models/PrinciplesConsoleModel.js';
|
|
6
|
-
import {
|
|
6
|
+
import { ExaminerEvidenceModel } from '../models/ExaminerEvidenceModel.js';
|
|
7
|
+
import { loadLabModules, loadHostCapability, preflightCheck, offlineStudyPass, reportMaterialize, listStudyNames, resolveStudyAccess, resolveStudyRoots, studyReportPayload, materializeConsoleStudy, verifyConsoleOwnership, writableRootProblem, isJsonObject, ExaminerPreflightBlockedError, } from './examiner-executor.js';
|
|
8
|
+
import { listSuiteCatalog, isSuiteKey, gatherProductionSnapshot, deriveStudyRuleCodeFacts, readProductionSnapshot, } from './examiner-study-inputs.js';
|
|
9
|
+
import { appendConsoleRunEventLocked, readConsoleRuns, currentConsoleRun, recoverUnfinishedConsoleRuns, acceptConsoleRun, hostDryRunPass, f2AuthorizationGate, CONSOLE_RUN_SCHEMA, CONSOLE_RUN_MODES, ConsoleRunInProgressError, ConsoleLedgerUnreadableError, studyStatusPayload, assertOfflineAdmissionAllowed, } from './examiner-runs.js';
|
|
7
10
|
/* eslint-disable @typescript-eslint/max-params -- node:http handler signature (req, res, workspaceDir, subPath) matches every other route */
|
|
8
11
|
const LAB_ENTRY = path.join('lib', 'examiner-study.mjs');
|
|
9
12
|
const PRINCIPLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
@@ -83,6 +86,30 @@ async function loadLab() {
|
|
|
83
86
|
return null;
|
|
84
87
|
}
|
|
85
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* v0.10.1 §12.3 restart recovery runs ONCE per process per writable root,
|
|
91
|
+
* lazily on the first examiner request touching studies — a crashed holder's
|
|
92
|
+
* unfinished console run becomes CONSOLE_RUN_ABANDONED (RESULT_UNKNOWN,
|
|
93
|
+
* never retried) before any status/run request can observe or extend it.
|
|
94
|
+
*/
|
|
95
|
+
const recoveryInFlight = new Map();
|
|
96
|
+
async function ensureConsoleRunRecovery(roots, labRootDir) {
|
|
97
|
+
const key = roots.writable;
|
|
98
|
+
const pending = recoveryInFlight.get(key);
|
|
99
|
+
if (pending)
|
|
100
|
+
return pending;
|
|
101
|
+
// Cache the IN-FLIGHT promise, not a "done" flag: a transient scan failure
|
|
102
|
+
// (an unavailable studies root) must be retried by the next request instead
|
|
103
|
+
// of leaving a crashed run blocking this study for the process lifetime.
|
|
104
|
+
const attempt = recoverUnfinishedConsoleRuns(roots, labRootDir)
|
|
105
|
+
.then(() => undefined)
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
recoveryInFlight.delete(key);
|
|
108
|
+
logRefusal('run recovery', error);
|
|
109
|
+
});
|
|
110
|
+
recoveryInFlight.set(key, attempt);
|
|
111
|
+
return attempt;
|
|
112
|
+
}
|
|
86
113
|
function readBody(req) {
|
|
87
114
|
return new Promise((resolve) => {
|
|
88
115
|
let raw = '';
|
|
@@ -105,27 +132,47 @@ function readBody(req) {
|
|
|
105
132
|
});
|
|
106
133
|
}
|
|
107
134
|
export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
135
|
+
const method = req.method ?? 'GET';
|
|
136
|
+
// ── v0.10.1 §4/§5: production evidence aggregate (BEFORE the lab gate —
|
|
137
|
+
// production evidence is a property of the workspace runtime, not of the
|
|
138
|
+
// research engine; an installation without the lab still answers honestly).
|
|
139
|
+
// Segment parsing (no regex): /principles/<charset-validated-id>/evidence.
|
|
140
|
+
const evidencePrefix = '/principles/';
|
|
141
|
+
const evidenceSuffix = '/evidence';
|
|
142
|
+
const evidencePrincipleId = subPath.startsWith(evidencePrefix) && subPath.endsWith(evidenceSuffix)
|
|
143
|
+
? subPath.slice(evidencePrefix.length, subPath.length - evidenceSuffix.length)
|
|
144
|
+
: null;
|
|
145
|
+
if (evidencePrincipleId !== null && PRINCIPLE_ID_PATTERN.test(evidencePrincipleId) && method === 'GET') {
|
|
146
|
+
const model = new ExaminerEvidenceModel(workspaceDir);
|
|
147
|
+
const evidence = await model.getPrincipleEvidence(evidencePrincipleId);
|
|
148
|
+
if (!evidence) {
|
|
149
|
+
sendError(res, 404, 'principle_not_found', 'the principle was not found in the workspace ledger');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
sendSuccess(res, { ...evidence });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
108
155
|
const lab = await loadLab();
|
|
109
156
|
if (!lab || !labModules || !labRoot) {
|
|
110
157
|
sendError(res, 503, 'examiner_lab_unavailable', `the Examiner research engine could not be loaded from this installation: ${labUnavailableReason ?? 'unknown reason'}`);
|
|
111
158
|
return;
|
|
112
159
|
}
|
|
113
|
-
const method = req.method ?? 'GET';
|
|
114
160
|
const roots = resolveStudyRoots(labRoot, path.join(workspaceDir, '.pd', 'examiner'));
|
|
115
161
|
const context = { labRoot, roots };
|
|
116
162
|
const rootProblem = writableRootProblem(roots);
|
|
163
|
+
await ensureConsoleRunRecovery(roots, labRoot);
|
|
117
164
|
// GET /overview
|
|
118
165
|
if (subPath === '/overview' && method === 'GET') {
|
|
119
166
|
const writable = listStudyNames(roots.writable);
|
|
120
167
|
const historical = listStudyNames(roots.historical);
|
|
121
168
|
sendSuccess(res, {
|
|
122
|
-
spec: 'SPEC-PD-EXAMINER-001 v0.
|
|
123
|
-
executionScope: 'offline-stub
|
|
169
|
+
spec: 'SPEC-PD-EXAMINER-001 v0.10.1',
|
|
170
|
+
executionScope: 'offline-stub studies validate the research contract; host dry-run checks F2 readiness with ZERO model calls; real host runs require an APPROVED Stage R authorization inside the study and stay refused otherwise',
|
|
124
171
|
gates: {
|
|
125
172
|
gate1_challenge_trust: 'BLOCKED — independent human Gold review has not been performed for any Examiner suite (code tests can never substitute)',
|
|
126
173
|
gate2_behavior_evaluation: 'NOT_READY — requires Gate 1 + real-host authorization',
|
|
127
174
|
gate3_llm_authoring: 'NOT_STARTED',
|
|
128
|
-
gate4_product_integration: 'ACTIVE — the offline research panel (create / run / report) ships with the console; real model calls and real host runs stay refused under this gate',
|
|
175
|
+
gate4_product_integration: 'ACTIVE — the offline research panel (create / run / report) ships with the console; v0.10.1 adds production evidence, host-backed study inputs and the async run contract; real model calls and real host runs stay refused under this gate',
|
|
129
176
|
},
|
|
130
177
|
hostInjectionCapability: lab,
|
|
131
178
|
fullConclusionMinimumLevel: 'CONTEXT_PRESENT',
|
|
@@ -149,6 +196,28 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
149
196
|
sendSuccess(res, { principles, note: 'study inputs are read from the workspace ledger (read-only); eligibility filtering happens inside the Examiner contract' });
|
|
150
197
|
return;
|
|
151
198
|
}
|
|
199
|
+
// ── v0.10.1 §9: challenge-suite catalog (server-fixed keys, read-only).
|
|
200
|
+
if (subPath === '/suites' && method === 'GET') {
|
|
201
|
+
const suites = await listSuiteCatalog(labRoot);
|
|
202
|
+
sendSuccess(res, { suites, note: 'every suite reuses the lab\u2019s authored calibration case variants; gold is SYNTHETIC_OFFLINE until Gate 1 (independent human Gold review) passes' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// ── v0.10.1 §6.2: OpenClaw host readiness (workspace-level, zero model calls).
|
|
206
|
+
if (subPath === '/hosts/openclaw/readiness' && method === 'GET') {
|
|
207
|
+
sendSuccess(res, {
|
|
208
|
+
host: 'openclaw',
|
|
209
|
+
maxProvableInjectionLevel: lab.openclaw ?? 'SUBMITTED',
|
|
210
|
+
executionModes: ['offline-stub', 'host-dry-run', 'f2-real-host'],
|
|
211
|
+
requirements: [
|
|
212
|
+
{ id: 'stage_r_authorization', detail: 'an APPROVED F2-real-host run-authorization.json inside the study, bound to its frozen suite hash (issued by the Owner — never by the console)' },
|
|
213
|
+
{ id: 'host_cli', detail: 'the OpenClaw CLI reachable on PATH (verified by the per-study host dry-run local version probe)' },
|
|
214
|
+
{ id: 'isolation', detail: 'per-arm round directories creatable inside the writable study root' },
|
|
215
|
+
{ id: 'gate1_gold_trust', detail: 'independent human Gold review — currently BLOCKED, so real runs cannot produce formal effect conclusions even when authorized' },
|
|
216
|
+
],
|
|
217
|
+
note: 'this readiness projection performs no host calls; run the per-study host dry-run for the live checklist',
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
152
221
|
// POST /studies — create an offline examiner study (identity server-minted)
|
|
153
222
|
if (subPath === '/studies' && method === 'POST') {
|
|
154
223
|
if (rootProblem) {
|
|
@@ -160,6 +229,9 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
160
229
|
const requestedMode = typeof body.experimentMode === 'string' ? body.experimentMode : '';
|
|
161
230
|
const repetitionsRaw = Number(body.repetitionsPerCase);
|
|
162
231
|
const repetitionsPerCase = Number.isInteger(repetitionsRaw) && repetitionsRaw >= 1 && repetitionsRaw <= 5 ? repetitionsRaw : 2;
|
|
232
|
+
// v0.10.1 §9: the suite travels as a CATALOG KEY (server-fixed list);
|
|
233
|
+
// anything unrecognized falls back to the v0.9 default suite.
|
|
234
|
+
const suiteKey = isSuiteKey(body.suiteKey) ? body.suiteKey : 'didactic-pair';
|
|
163
235
|
if (!PRINCIPLE_ID_PATTERN.test(requestedTarget)) {
|
|
164
236
|
sendError(res, 400, 'invalid_target_principle', 'targetPrincipleId required (charset-validated principle id; one target Principle per study)');
|
|
165
237
|
return;
|
|
@@ -178,18 +250,35 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
178
250
|
}
|
|
179
251
|
const isolatedMode = requestedMode === 'isolated_principle';
|
|
180
252
|
try {
|
|
253
|
+
// v0.10.1 host-backed inputs (SPEC §4/§6/§7): freeze the production
|
|
254
|
+
// snapshot, derive RuleCode facts from RUNTIME activation truth,
|
|
255
|
+
// carry the real pain lineage and compile the selected suite's cases.
|
|
256
|
+
// The study handle is minted inside materializeConsoleStudy, so the
|
|
257
|
+
// suite cases are compiled with per-study ids there via a second pass
|
|
258
|
+
// (suiteCaseDefs is deterministic; the ids are re-minted below).
|
|
259
|
+
const snapshot = await gatherProductionSnapshot(workspaceDir, requestedTarget, 2000);
|
|
260
|
+
const detailOutput = await model.getPrincipleDetail(requestedTarget);
|
|
261
|
+
const painRefs = detailOutput?.principle.derivedFromPainIds ?? [];
|
|
181
262
|
const created = await materializeConsoleStudy(labModules, labRoot, roots, {
|
|
182
263
|
isolatedMode,
|
|
183
264
|
targetIndex,
|
|
184
265
|
ledgerPrinciples,
|
|
185
266
|
repetitionsPerCase,
|
|
267
|
+
suiteKey,
|
|
268
|
+
hostBacked: {
|
|
269
|
+
productionSnapshot: { ...snapshot },
|
|
270
|
+
ruleCodeFacts: deriveStudyRuleCodeFacts(snapshot),
|
|
271
|
+
painRefs,
|
|
272
|
+
},
|
|
186
273
|
});
|
|
187
274
|
sendSuccess(res, {
|
|
188
275
|
studyId: created.studyId,
|
|
189
276
|
targetPrincipleId: created.targetPrincipleId,
|
|
190
277
|
experimentMode: isolatedMode ? 'isolated_principle' : 'full_library_incremental',
|
|
278
|
+
suiteKey,
|
|
191
279
|
plannedPairs: created.plannedPairs,
|
|
192
280
|
manifestHash: created.manifestHash,
|
|
281
|
+
productionSnapshotMarkers: snapshot.experimentVsProduction.markers,
|
|
193
282
|
note: 'offline study created with SYNTHETIC gold — run preflight, then the offline-stub execution; results demonstrate the contract pipeline, never real host behavior',
|
|
194
283
|
});
|
|
195
284
|
}
|
|
@@ -201,6 +290,11 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
201
290
|
}
|
|
202
291
|
// /studies/:studyId[/...]
|
|
203
292
|
const studyMatch = STUDY_ROUTE_PATTERN.exec(subPath);
|
|
293
|
+
// CodeQL js/user-controlled-bypass (dismissed as false positive): this is a route
|
|
294
|
+
// SELECTOR, not an authorization decision. Every privileged action below
|
|
295
|
+
// runs only after resolveStudyAccess() (server-side enumeration + study-id
|
|
296
|
+
// charset allowlist + canonical boundary + ownership marker); a non-match
|
|
297
|
+
// simply falls through to 404.
|
|
204
298
|
if (studyMatch && studyMatch[1]) {
|
|
205
299
|
const rest = studyMatch[2] ?? '';
|
|
206
300
|
const access = resolveStudyAccess(roots, studyMatch[1]);
|
|
@@ -211,7 +305,33 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
211
305
|
const studyDir = access.dir;
|
|
212
306
|
// GET status
|
|
213
307
|
if (rest === '' && method === 'GET') {
|
|
214
|
-
|
|
308
|
+
// P1-4B: an unreadable ledger is reported STRUCTURALLY on the read path
|
|
309
|
+
// (rc-9) and refuses runs on the write path — never shown as "no runs".
|
|
310
|
+
let consoleRuns = null;
|
|
311
|
+
let ledgerProblem = null;
|
|
312
|
+
try {
|
|
313
|
+
consoleRuns = readConsoleRuns(roots, studyDir);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error instanceof ConsoleLedgerUnreadableError) {
|
|
317
|
+
ledgerProblem = { code: error.code, message: error.message };
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
sendSuccess(res, {
|
|
324
|
+
...studyStatusPayload(roots, studyDir),
|
|
325
|
+
readOnly: access.readOnly,
|
|
326
|
+
access: access.reason,
|
|
327
|
+
productionSnapshot: readProductionSnapshot(studyDir),
|
|
328
|
+
consoleRuns: {
|
|
329
|
+
runs: consoleRuns === null ? [] : consoleRuns.slice(-10),
|
|
330
|
+
current: consoleRuns === null ? null : currentConsoleRun(consoleRuns),
|
|
331
|
+
asyncModes: CONSOLE_RUN_MODES,
|
|
332
|
+
ledgerProblem,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
215
335
|
return;
|
|
216
336
|
}
|
|
217
337
|
// POST /preflight — the fail-closed gate over the frozen study facts
|
|
@@ -224,21 +344,140 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
224
344
|
// P1-1/P1-4: the study must be server-owned and writable, and the SERVER
|
|
225
345
|
// preflight must pass BEFORE the runner is entered. A blocker returns 409
|
|
226
346
|
// with no RUN_STARTED event, no reservation and no actor creation.
|
|
347
|
+
// CodeQL js/user-controlled-bypass (dismissed as false positive): route selector. The
|
|
348
|
+
// mode allowlist, access.readOnly refusal and the preflight/Stage R gates
|
|
349
|
+
// below all run before any execution side effect.
|
|
227
350
|
if (rest === '/run' && method === 'POST') {
|
|
228
351
|
const body = await readBody(req);
|
|
229
352
|
// Real execution needs an Owner-approved run-authorization.json plus the
|
|
230
|
-
// Stage R gate
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
353
|
+
// Stage R gate. The reachable modes are fixed: offline-stub (v0.9,
|
|
354
|
+
// synchronous), host-dry-run / f2-real-host (v0.10.1 §12.3 async).
|
|
355
|
+
const requestedMode = typeof body.mode === 'string' ? body.mode : 'offline-stub';
|
|
356
|
+
if (requestedMode !== 'offline-stub' && !CONSOLE_RUN_MODES.includes(requestedMode)) {
|
|
357
|
+
sendError(res, 403, 'real_execution_not_authorized', `unknown execution mode '${requestedMode}' — allowed: offline-stub, host-dry-run, f2-real-host`);
|
|
234
358
|
return;
|
|
235
359
|
}
|
|
236
360
|
if (access.readOnly) {
|
|
237
361
|
sendError(res, 403, 'study_read_only', `this study cannot be executed from the console: ${access.reason}`);
|
|
238
362
|
return;
|
|
239
363
|
}
|
|
364
|
+
// ── v0.10.1 §12.3 async modes (host-dry-run / f2-real-host) ──
|
|
365
|
+
if (requestedMode === 'host-dry-run' || requestedMode === 'f2-real-host') {
|
|
366
|
+
// P1-5: the full Stage R authority contract (schema/stage/binding/
|
|
367
|
+
// window/revocation/probe) is verified BEFORE acceptance; the same
|
|
368
|
+
// authority re-verifies per unit inside the runner for real runs.
|
|
369
|
+
// CodeQL js/user-controlled-bypass (dismissed as false positive): selecting the F2
|
|
370
|
+
// mode ADDS the Stage R gate (it cannot skip one); the other two
|
|
371
|
+
// reachable modes perform no real-host action at all.
|
|
372
|
+
if (requestedMode === 'f2-real-host') {
|
|
373
|
+
const gate = await f2AuthorizationGate(labRoot, studyDir);
|
|
374
|
+
if (!gate.authorized) {
|
|
375
|
+
sendError(res, 403, 'real_execution_not_authorized', `F2 real-host execution was refused: ${gate.reasons.join('; ')}`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// P1-4: read-check-verify-append acceptance happens ATOMICALLY under
|
|
380
|
+
// the existing lab study write lock — two concurrent requests can
|
|
381
|
+
// never both pass. A corrupt/unreadable ledger refuses the run.
|
|
382
|
+
let acceptance;
|
|
383
|
+
try {
|
|
384
|
+
acceptance = await acceptConsoleRun(roots, studyDir, { studyId: access.studyId, mode: requestedMode, labRoot });
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (error instanceof ConsoleRunInProgressError) {
|
|
388
|
+
sendError(res, 409, 'run_in_progress', `console run ${error.runId} (${error.mode}) is still in progress for this study — wait for its terminal event or recover after a restart; never start a second run`, { runId: error.runId, mode: error.mode, acceptedAt: error.acceptedAt });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (error.name === 'OfflineRunInFlightError') {
|
|
392
|
+
sendError(res, 409, 'run_in_progress', 'an offline execution is currently in progress for this study (RUN_STARTED without RUN_FINISHED) — wait for it to settle');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (error.name === 'StudyLockError') {
|
|
396
|
+
sendError(res, 409, 'run_in_progress', 'another writer holds the study lock right now (a concurrent acceptance or the offline runner) — exactly one run may start; retry after the holder settles');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (error instanceof ConsoleLedgerUnreadableError) {
|
|
400
|
+
sendError(res, 500, 'console_ledger_unreadable', `the run was refused because the study event ledger could not be honestly read (${error.code}): ${error.message} — the ledger is never auto-cleared or rebuilt`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
logRefusal('console run acceptance failed', error);
|
|
404
|
+
sendError(res, 500, 'run_acceptance_failed', 'the run could not be accepted (details in the console server log)');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const { runId, acceptedAt } = acceptance;
|
|
408
|
+
// Detached work: the accepted run settles itself through the study
|
|
409
|
+
// ledger. The promise is intentionally not awaited (§12.3: long work
|
|
410
|
+
// never rides the HTTP request); all failures append terminal events.
|
|
411
|
+
// finishedAt is taken at SETTLEMENT time (P1-4C), never up front.
|
|
412
|
+
void (async () => {
|
|
413
|
+
let terminal;
|
|
414
|
+
try {
|
|
415
|
+
if (requestedMode === 'host-dry-run') {
|
|
416
|
+
const dryRun = await hostDryRunPass({ modules: labModules, labRoot, studyDir, context });
|
|
417
|
+
terminal = {
|
|
418
|
+
type: dryRun.ok ? 'CONSOLE_RUN_FINISHED' : 'CONSOLE_RUN_FAILED',
|
|
419
|
+
schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
|
|
420
|
+
mode: 'host-dry-run', acceptedAt, pid: process.pid, owner: 'pd-console',
|
|
421
|
+
finishedAt: new Date().toISOString(),
|
|
422
|
+
outcome: dryRun.ok ? 'DRY_RUN_CHECKS_PASSED' : 'DRY_RUN_CHECKS_FAILED',
|
|
423
|
+
reason: dryRun.ok ? undefined : `failing prerequisites: ${dryRun.checks.filter((entry) => !entry.ok && entry.severity !== 'info').map((entry) => entry.id).join(', ')}`,
|
|
424
|
+
checks: dryRun.checks,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// f2-real-host passed the console gate. Real console-driven
|
|
429
|
+
// execution is NOT enabled this round (SPEC §Owner Decision:
|
|
430
|
+
// real model calls require separate Owner authorization); the
|
|
431
|
+
// run settles LOUDLY with zero model calls — never a stub.
|
|
432
|
+
terminal = {
|
|
433
|
+
type: 'CONSOLE_RUN_FAILED', schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
|
|
434
|
+
mode: 'f2-real-host', acceptedAt, pid: process.pid, owner: 'pd-console',
|
|
435
|
+
finishedAt: new Date().toISOString(),
|
|
436
|
+
outcome: 'F2_EXECUTION_NOT_ENABLED_THIS_ROUND',
|
|
437
|
+
reason: 'authorization was accepted, but console-driven F2 real execution stays disabled this round; use the lab F2 driver under Owner supervision; zero model calls were made',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
logRefusal(`console run ${runId} failed`, error);
|
|
443
|
+
terminal = {
|
|
444
|
+
type: 'CONSOLE_RUN_FAILED', schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
|
|
445
|
+
mode: requestedMode, acceptedAt, pid: process.pid, owner: 'pd-console',
|
|
446
|
+
finishedAt: new Date().toISOString(),
|
|
447
|
+
outcome: 'RUN_ERROR', reason: 'the console run failed (details in the console server log)',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// The terminal event is appended UNDER the study write lock: startup
|
|
451
|
+
// recovery takes the same lock and re-reads the terminal state there,
|
|
452
|
+
// so a settled run can never be turned into ABANDONED afterwards.
|
|
453
|
+
try {
|
|
454
|
+
await appendConsoleRunEventLocked({ labRoot, roots, studyDir }, terminal);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
logRefusal(`console run ${runId} terminal event could not be appended`, error);
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
// 202 Accepted — the §12.3 async contract answer.
|
|
461
|
+
sendJson(res, 202, {
|
|
462
|
+
success: true,
|
|
463
|
+
data: {
|
|
464
|
+
studyId: access.studyId,
|
|
465
|
+
runId,
|
|
466
|
+
mode: requestedMode,
|
|
467
|
+
status: 'ACCEPTED',
|
|
468
|
+
note: requestedMode === 'host-dry-run'
|
|
469
|
+
? 'host dry-run accepted — zero model calls; poll the study status for the console-run checklist'
|
|
470
|
+
: 'F2 real-host run accepted under the Owner authorization — poll the study status; the runner re-verifies the Stage R authority per unit',
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
// ── v0.9 synchronous offline-stub path (unchanged, AC-02 compat) ──
|
|
240
476
|
try {
|
|
241
|
-
|
|
477
|
+
// P1-3 (offline→async): the admission check runs INSIDE the lab's study
|
|
478
|
+
// write lock, before RUN_STARTED/reservation/actor. The callback lives
|
|
479
|
+
// here (composition root) so executor↔runs stay acyclic.
|
|
480
|
+
const summary = await offlineStudyPass(labModules, studyDir, context, () => assertOfflineAdmissionAllowed(roots, studyDir));
|
|
242
481
|
sendSuccess(res, { studyId: access.studyId, units: summary.units.map((unit) => ({ unit: unit.unit, state: unit.state })) });
|
|
243
482
|
}
|
|
244
483
|
catch (error) {
|
|
@@ -246,6 +485,16 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
|
246
485
|
// refuses BEFORE RUN_STARTED; the runner re-verifies UNDER the study
|
|
247
486
|
// lock and refuses on drift discovered after that first check. Either
|
|
248
487
|
// way: no RUN_STARTED, no reservation, no actor, no attempt file.
|
|
488
|
+
if (error instanceof ConsoleRunInProgressError) {
|
|
489
|
+
sendError(res, 409, 'run_in_progress', 'an execution is already running for this study; no run started, no unit reserved, no actor created', {
|
|
490
|
+
runId: error.runId, mode: error.mode, acceptedAt: error.acceptedAt,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (error instanceof ConsoleLedgerUnreadableError) {
|
|
495
|
+
sendError(res, 500, 'console_ledger_unreadable', 'the study event ledger is unreadable; refusing to start an offline run (fail-closed)', { code: error.code });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
249
498
|
if (error instanceof ExaminerPreflightBlockedError) {
|
|
250
499
|
sendError(res, 409, 'preflight_blocked', 'offline preflight blocked; no run started, no unit reserved, no actor created', {
|
|
251
500
|
blockers: error.blockers,
|
package/console/dist/types.d.ts
CHANGED
|
@@ -25,7 +25,13 @@
|
|
|
25
25
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
|
26
26
|
--color-green-600: oklch(62.7% 0.194 149.214);
|
|
27
27
|
--color-green-700: oklch(52.7% 0.154 150.069);
|
|
28
|
+
--color-emerald-400: oklch(76.5% 0.177 163.223);
|
|
29
|
+
--color-emerald-500: oklch(69.6% 0.17 162.48);
|
|
30
|
+
--color-emerald-700: oklch(50.8% 0.118 165.612);
|
|
28
31
|
--color-teal-600: oklch(60% 0.118 184.704);
|
|
32
|
+
--color-sky-400: oklch(74.6% 0.16 232.661);
|
|
33
|
+
--color-sky-500: oklch(68.5% 0.169 237.323);
|
|
34
|
+
--color-sky-700: oklch(50% 0.134 242.749);
|
|
29
35
|
--color-rose-400: oklch(71.2% 0.194 13.428);
|
|
30
36
|
--color-black: #000;
|
|
31
37
|
--color-white: #fff;
|
|
@@ -464,6 +470,9 @@
|
|
|
464
470
|
.mt-\[14px\] {
|
|
465
471
|
margin-top: 14px;
|
|
466
472
|
}
|
|
473
|
+
.mr-1 {
|
|
474
|
+
margin-right: var(--spacing);
|
|
475
|
+
}
|
|
467
476
|
.mr-2 {
|
|
468
477
|
margin-right: calc(var(--spacing) * 2);
|
|
469
478
|
}
|
|
@@ -569,6 +578,10 @@
|
|
|
569
578
|
.table {
|
|
570
579
|
display: table;
|
|
571
580
|
}
|
|
581
|
+
.size-3\.5 {
|
|
582
|
+
width: calc(var(--spacing) * 3.5);
|
|
583
|
+
height: calc(var(--spacing) * 3.5);
|
|
584
|
+
}
|
|
572
585
|
.size-4 {
|
|
573
586
|
width: calc(var(--spacing) * 4);
|
|
574
587
|
height: calc(var(--spacing) * 4);
|
|
@@ -995,6 +1008,9 @@
|
|
|
995
1008
|
.flex-wrap {
|
|
996
1009
|
flex-wrap: wrap;
|
|
997
1010
|
}
|
|
1011
|
+
.content-start {
|
|
1012
|
+
align-content: flex-start;
|
|
1013
|
+
}
|
|
998
1014
|
.items-baseline {
|
|
999
1015
|
align-items: baseline;
|
|
1000
1016
|
}
|
|
@@ -1118,6 +1134,10 @@
|
|
|
1118
1134
|
margin-block-start: calc(14px * var(--tw-space-y-reverse));
|
|
1119
1135
|
margin-block-end: calc(14px * calc(1 - var(--tw-space-y-reverse)));
|
|
1120
1136
|
}
|
|
1137
|
+
.gap-x-3 {
|
|
1138
|
+
-moz-column-gap: calc(var(--spacing) * 3);
|
|
1139
|
+
column-gap: calc(var(--spacing) * 3);
|
|
1140
|
+
}
|
|
1121
1141
|
.gap-x-4 {
|
|
1122
1142
|
-moz-column-gap: calc(var(--spacing) * 4);
|
|
1123
1143
|
column-gap: calc(var(--spacing) * 4);
|
|
@@ -1264,6 +1284,12 @@
|
|
|
1264
1284
|
border-color: color-mix(in oklab, var(--color-amber-400) 50%, transparent);
|
|
1265
1285
|
}
|
|
1266
1286
|
}
|
|
1287
|
+
.border-amber-500\/40 {
|
|
1288
|
+
border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 40%, transparent);
|
|
1289
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1290
|
+
border-color: color-mix(in oklab, var(--color-amber-500) 40%, transparent);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1267
1293
|
.border-amber\/20 {
|
|
1268
1294
|
border-color: color-mix(in srgb, #a66a2a 20%, transparent);
|
|
1269
1295
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1486,6 +1512,12 @@
|
|
|
1486
1512
|
border-color: color-mix(in oklab, var(--color-rose-400) 50%, transparent);
|
|
1487
1513
|
}
|
|
1488
1514
|
}
|
|
1515
|
+
.border-sky-500\/40 {
|
|
1516
|
+
border-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 40%, transparent);
|
|
1517
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1518
|
+
border-color: color-mix(in oklab, var(--color-sky-500) 40%, transparent);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1489
1521
|
.border-transparent {
|
|
1490
1522
|
border-color: transparent;
|
|
1491
1523
|
}
|
|
@@ -1525,6 +1557,18 @@
|
|
|
1525
1557
|
.bg-amber-500 {
|
|
1526
1558
|
background-color: var(--color-amber-500);
|
|
1527
1559
|
}
|
|
1560
|
+
.bg-amber-500\/10 {
|
|
1561
|
+
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent);
|
|
1562
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1563
|
+
background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
.bg-amber-500\/15 {
|
|
1567
|
+
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent);
|
|
1568
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1569
|
+
background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1528
1572
|
.bg-amber\/5 {
|
|
1529
1573
|
background-color: color-mix(in srgb, #a66a2a 5%, transparent);
|
|
1530
1574
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1582,6 +1626,18 @@
|
|
|
1582
1626
|
background-color: color-mix(in oklab, var(--color-destructive) 10%, transparent);
|
|
1583
1627
|
}
|
|
1584
1628
|
}
|
|
1629
|
+
.bg-destructive\/15 {
|
|
1630
|
+
background-color: color-mix(in srgb, #8b3a3a 15%, transparent);
|
|
1631
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1632
|
+
background-color: color-mix(in oklab, var(--color-destructive) 15%, transparent);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
.bg-emerald-500\/15 {
|
|
1636
|
+
background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 15%, transparent);
|
|
1637
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1638
|
+
background-color: color-mix(in oklab, var(--color-emerald-500) 15%, transparent);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1585
1641
|
.bg-gov {
|
|
1586
1642
|
background-color: var(--color-gov);
|
|
1587
1643
|
}
|
|
@@ -1717,6 +1773,18 @@
|
|
|
1717
1773
|
background-color: color-mix(in oklab, var(--color-rose-400) 15%, transparent);
|
|
1718
1774
|
}
|
|
1719
1775
|
}
|
|
1776
|
+
.bg-sky-500\/10 {
|
|
1777
|
+
background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 10%, transparent);
|
|
1778
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1779
|
+
background-color: color-mix(in oklab, var(--color-sky-500) 10%, transparent);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
.bg-sky-500\/15 {
|
|
1783
|
+
background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 15%, transparent);
|
|
1784
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1785
|
+
background-color: color-mix(in oklab, var(--color-sky-500) 15%, transparent);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1720
1788
|
.bg-surface {
|
|
1721
1789
|
background-color: var(--color-surface);
|
|
1722
1790
|
}
|
|
@@ -2231,6 +2299,9 @@
|
|
|
2231
2299
|
.text-destructive {
|
|
2232
2300
|
color: var(--color-destructive);
|
|
2233
2301
|
}
|
|
2302
|
+
.text-emerald-700 {
|
|
2303
|
+
color: var(--color-emerald-700);
|
|
2304
|
+
}
|
|
2234
2305
|
.text-foreground {
|
|
2235
2306
|
color: var(--color-foreground);
|
|
2236
2307
|
}
|
|
@@ -2288,6 +2359,9 @@
|
|
|
2288
2359
|
.text-red-700 {
|
|
2289
2360
|
color: var(--color-red-700);
|
|
2290
2361
|
}
|
|
2362
|
+
.text-sky-700 {
|
|
2363
|
+
color: var(--color-sky-700);
|
|
2364
|
+
}
|
|
2291
2365
|
.text-teal-600 {
|
|
2292
2366
|
color: var(--color-teal-600);
|
|
2293
2367
|
}
|
|
@@ -2917,6 +2991,9 @@
|
|
|
2917
2991
|
.sm\:grid-cols-3 {
|
|
2918
2992
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2919
2993
|
}
|
|
2994
|
+
.sm\:grid-cols-4 {
|
|
2995
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
2996
|
+
}
|
|
2920
2997
|
.sm\:flex-row {
|
|
2921
2998
|
flex-direction: row;
|
|
2922
2999
|
}
|
|
@@ -2962,6 +3039,9 @@
|
|
|
2962
3039
|
}
|
|
2963
3040
|
}
|
|
2964
3041
|
@media (width >= 64rem) {
|
|
3042
|
+
.lg\:col-span-2 {
|
|
3043
|
+
grid-column: span 2 / span 2;
|
|
3044
|
+
}
|
|
2965
3045
|
.lg\:ml-\[256px\] {
|
|
2966
3046
|
margin-left: 256px;
|
|
2967
3047
|
}
|
|
@@ -2971,9 +3051,15 @@
|
|
|
2971
3051
|
.lg\:hidden {
|
|
2972
3052
|
display: none;
|
|
2973
3053
|
}
|
|
3054
|
+
.lg\:grid-cols-2 {
|
|
3055
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3056
|
+
}
|
|
2974
3057
|
.lg\:grid-cols-4 {
|
|
2975
3058
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
2976
3059
|
}
|
|
3060
|
+
.lg\:grid-cols-\[280px_1fr\] {
|
|
3061
|
+
grid-template-columns: 280px 1fr;
|
|
3062
|
+
}
|
|
2977
3063
|
.lg\:pt-0 {
|
|
2978
3064
|
padding-top: 0px;
|
|
2979
3065
|
}
|
|
@@ -2981,12 +3067,18 @@
|
|
|
2981
3067
|
.dark\:text-amber-400:is([data-theme="dark"] *) {
|
|
2982
3068
|
color: var(--color-amber-400);
|
|
2983
3069
|
}
|
|
3070
|
+
.dark\:text-emerald-400:is([data-theme="dark"] *) {
|
|
3071
|
+
color: var(--color-emerald-400);
|
|
3072
|
+
}
|
|
2984
3073
|
.dark\:text-green-400:is([data-theme="dark"] *) {
|
|
2985
3074
|
color: var(--color-green-400);
|
|
2986
3075
|
}
|
|
2987
3076
|
.dark\:text-red-400:is([data-theme="dark"] *) {
|
|
2988
3077
|
color: var(--color-red-400);
|
|
2989
3078
|
}
|
|
3079
|
+
.dark\:text-sky-400:is([data-theme="dark"] *) {
|
|
3080
|
+
color: var(--color-sky-400);
|
|
3081
|
+
}
|
|
2990
3082
|
.\[\&\>span\]\:line-clamp-1 > span {
|
|
2991
3083
|
overflow: hidden;
|
|
2992
3084
|
display: -webkit-box;
|