prism-mcp-server 20.21.21 → 20.21.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/config.js +3 -3
- package/dist/connect.js +7 -0
- package/dist/darkfactory/actionExecutor.js +327 -0
- package/dist/darkfactory/backendReadiness.js +12 -0
- package/dist/darkfactory/clawInvocation.js +18 -6
- package/dist/darkfactory/enablement.js +29 -0
- package/dist/darkfactory/executionAuthority.js +11 -0
- package/dist/darkfactory/hostPolicy.js +12 -0
- package/dist/darkfactory/promptRouting.js +59 -0
- package/dist/darkfactory/runner.js +179 -31
- package/dist/darkfactory/safetyController.js +10 -1
- package/dist/dashboard/server.js +5 -4
- package/dist/dashboard/settingsPolicy.js +2 -0
- package/dist/dashboard/ui.js +37 -1
- package/dist/onboarding/wizard.js +1 -1
- package/dist/server.js +12 -10
- package/dist/storage/configStorage.js +49 -0
- package/dist/storage/index.js +38 -7
- package/dist/storage/portalContracts.js +55 -0
- package/dist/storage/sqlite.js +13 -4
- package/dist/storage/supabase.js +27 -5
- package/dist/storage/synalux.js +77 -1
- package/dist/tools/ledgerHandlers.js +24 -2
- package/dist/tools/pipelineDefinitions.js +7 -1
- package/dist/tools/pipelineHandlers.js +18 -1
- package/dist/tools/sessionMemoryDefinitions.js +7 -1
- package/dist/utils/llm/adapters/anthropic.js +11 -3
- package/dist/utils/llm/adapters/gemini.js +3 -2
- package/dist/utils/llm/adapters/openai.js +3 -3
- package/dist/utils/llm/adapters/traced.js +2 -2
- package/dist/verification/runner.js +11 -2
- package/package.json +1 -1
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
* Using console.log() (stdout) will corrupt the MCP JSON-RPC stream.
|
|
19
19
|
*/
|
|
20
20
|
import { getStorage } from '../storage/index.js';
|
|
21
|
+
import { SqliteStorage } from '../storage/sqlite.js';
|
|
21
22
|
import { VALID_ACTION_TYPES } from './schema.js';
|
|
22
23
|
import { SafetyController } from './safetyController.js';
|
|
23
24
|
import { invokeClawAgent } from './clawInvocation.js';
|
|
24
25
|
import { PRISM_DARK_FACTORY_POLL_MS, PRISM_DARK_FACTORY_MAX_RUNTIME_MS, PRISM_USER_ID, PRISM_VERIFICATION_LAYERS, PRISM_VERIFICATION_DEFAULT_SEVERITY } from '../config.js';
|
|
25
26
|
import { debugLog } from '../utils/logger.js';
|
|
27
|
+
import { executeActions } from './actionExecutor.js';
|
|
26
28
|
import path from 'path';
|
|
27
29
|
import fs from 'fs';
|
|
28
30
|
import * as crypto from 'crypto';
|
|
@@ -30,6 +32,19 @@ import { Gatekeeper } from '../verification/gatekeeper.js';
|
|
|
30
32
|
import { VerificationRunner } from '../verification/runner.js';
|
|
31
33
|
import { computeRubricHash } from '../verification/schema.js';
|
|
32
34
|
import { VerificationGateError } from '../errors.js';
|
|
35
|
+
import { registerPipelineAuthority, isPipelineAuthorized, claimPipelineLease, FACTORY_LEASE_RENEW_MS } from './executionAuthority.js';
|
|
36
|
+
const LEASED_STORAGE_WRITES = new Set(['savePipeline', 'saveVerificationHarness', 'saveVerificationRun']);
|
|
37
|
+
async function locallyAuthorized(pipeline, backend) {
|
|
38
|
+
if (await isPipelineAuthorized(pipeline))
|
|
39
|
+
return true;
|
|
40
|
+
// Existing local SQLite queues already belong to this machine. Remote
|
|
41
|
+
// rows require a receipt created by the local start handler.
|
|
42
|
+
if (backend instanceof SqliteStorage && pipeline.status === 'PENDING') {
|
|
43
|
+
await registerPipelineAuthority(pipeline);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
33
48
|
/** Interval handle for graceful shutdown */
|
|
34
49
|
let runnerInterval = null;
|
|
35
50
|
/** Tracks whether the runner is currently processing a tick (prevents overlap) */
|
|
@@ -48,15 +63,28 @@ async function recoverStalePipelines() {
|
|
|
48
63
|
debugLog('[DarkFactory] No stale pipelines found on startup.');
|
|
49
64
|
return;
|
|
50
65
|
}
|
|
51
|
-
debugLog(`[DarkFactory]
|
|
66
|
+
debugLog(`[DarkFactory] Checking ${runningPipelines.length} RUNNING pipeline(s) for locally authorized recovery.`);
|
|
52
67
|
for (const pipeline of runningPipelines) {
|
|
68
|
+
if (!(await locallyAuthorized(pipeline, storage)))
|
|
69
|
+
continue;
|
|
70
|
+
const lease = await claimPipelineLease(pipeline);
|
|
71
|
+
if (!lease)
|
|
72
|
+
continue;
|
|
53
73
|
try {
|
|
74
|
+
const live = await storage.getPipeline(pipeline.id, pipeline.user_id);
|
|
75
|
+
if (!live || live.status !== 'RUNNING' || !(await isPipelineAuthorized(live)))
|
|
76
|
+
continue;
|
|
77
|
+
if (!lease.reclaimed && !SafetyController.isHeartbeatLapsed(live))
|
|
78
|
+
continue;
|
|
79
|
+
if (!(await lease.isHeld()))
|
|
80
|
+
continue;
|
|
54
81
|
await storage.savePipeline({
|
|
55
|
-
...
|
|
82
|
+
...live,
|
|
56
83
|
status: 'FAILED',
|
|
57
84
|
error: 'Unexpected Server Termination: pipeline was RUNNING when server restarted.',
|
|
58
85
|
current_step: pipeline.current_step,
|
|
59
86
|
});
|
|
87
|
+
await lease.acknowledgeRecovery();
|
|
60
88
|
debugLog(`[DarkFactory] Pipeline ${pipeline.id} marked FAILED (was RUNNING on restart).`);
|
|
61
89
|
}
|
|
62
90
|
catch (err) {
|
|
@@ -64,6 +92,9 @@ async function recoverStalePipelines() {
|
|
|
64
92
|
// by a concurrent process — safe to ignore.
|
|
65
93
|
console.error(`[DarkFactory] Failed to recover pipeline ${pipeline.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
94
|
}
|
|
95
|
+
finally {
|
|
96
|
+
await lease.release();
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
100
|
catch (err) {
|
|
@@ -76,8 +107,10 @@ async function recoverStalePipelines() {
|
|
|
76
107
|
* Called periodically during LLM execution to prove liveness.
|
|
77
108
|
* This is intentionally cheap — just a timestamp update.
|
|
78
109
|
*/
|
|
79
|
-
async function pulseHeartbeat(pipelineId, userId) {
|
|
110
|
+
async function pulseHeartbeat(pipelineId, userId, lease) {
|
|
80
111
|
try {
|
|
112
|
+
if (!(await lease.isHeld()))
|
|
113
|
+
return;
|
|
81
114
|
const storage = await getStorage();
|
|
82
115
|
const pipeline = await storage.getPipeline(pipelineId, userId);
|
|
83
116
|
if (!pipeline || pipeline.status !== 'RUNNING')
|
|
@@ -95,9 +128,9 @@ async function pulseHeartbeat(pipelineId, userId) {
|
|
|
95
128
|
* Creates a heartbeat interval that pulses every 15 seconds during execution.
|
|
96
129
|
* Returns a cleanup function to stop the interval.
|
|
97
130
|
*/
|
|
98
|
-
function startHeartbeatInterval(pipelineId, userId) {
|
|
131
|
+
function startHeartbeatInterval(pipelineId, userId, lease) {
|
|
99
132
|
const interval = setInterval(() => {
|
|
100
|
-
pulseHeartbeat(pipelineId, userId).catch(() => { });
|
|
133
|
+
pulseHeartbeat(pipelineId, userId, lease).catch(() => { });
|
|
101
134
|
}, 15_000);
|
|
102
135
|
return () => clearInterval(interval);
|
|
103
136
|
}
|
|
@@ -113,17 +146,29 @@ async function sweepZombies() {
|
|
|
113
146
|
const running = await storage.listPipelines(undefined, 'RUNNING', PRISM_USER_ID);
|
|
114
147
|
for (const pipeline of running) {
|
|
115
148
|
if (SafetyController.isHeartbeatLapsed(pipeline)) {
|
|
149
|
+
if (!(await locallyAuthorized(pipeline, storage)))
|
|
150
|
+
continue;
|
|
151
|
+
const lease = await claimPipelineLease(pipeline);
|
|
152
|
+
if (!lease)
|
|
153
|
+
continue;
|
|
116
154
|
debugLog(`[DarkFactory] Zombie detected: pipeline ${pipeline.id} heartbeat lapsed.`);
|
|
117
155
|
try {
|
|
156
|
+
const live = await storage.getPipeline(pipeline.id, pipeline.user_id);
|
|
157
|
+
if (!live || live.status !== 'RUNNING' || !SafetyController.isHeartbeatLapsed(live) || !(await lease.isHeld()))
|
|
158
|
+
continue;
|
|
118
159
|
await storage.savePipeline({
|
|
119
|
-
...
|
|
160
|
+
...live,
|
|
120
161
|
status: 'FAILED',
|
|
121
162
|
error: `Zombie pipeline: no heartbeat for ${SafetyController.HEARTBEAT_TIMEOUT_MS / 1000}s.`,
|
|
122
163
|
});
|
|
164
|
+
await lease.acknowledgeRecovery();
|
|
123
165
|
}
|
|
124
166
|
catch {
|
|
125
167
|
// Status guard may fire — pipeline was already terminated
|
|
126
168
|
}
|
|
169
|
+
finally {
|
|
170
|
+
await lease.release();
|
|
171
|
+
}
|
|
127
172
|
// Emit failure experience event
|
|
128
173
|
await emitExperienceEvent(pipeline, 'failure', 'Pipeline zombie-swept due to heartbeat lapse.');
|
|
129
174
|
}
|
|
@@ -316,12 +361,12 @@ export function parseEvaluationOutput(raw) {
|
|
|
316
361
|
* The `scopeViolation` field on the result signals the runner to
|
|
317
362
|
* terminate the entire pipeline (not just the step).
|
|
318
363
|
*/
|
|
319
|
-
async function executeStep(pipeline, spec) {
|
|
364
|
+
async function executeStep(pipeline, spec, lease) {
|
|
320
365
|
const stepStart = new Date().toISOString();
|
|
321
366
|
const step = pipeline.current_step;
|
|
322
367
|
debugLog(`[DarkFactory] Executing step=${step} iter=${pipeline.iteration} pipeline=${pipeline.id}`);
|
|
323
368
|
// Start heartbeat pulse during LLM execution
|
|
324
|
-
const stopHeartbeat = startHeartbeatInterval(pipeline.id, pipeline.user_id);
|
|
369
|
+
const stopHeartbeat = startHeartbeatInterval(pipeline.id, pipeline.user_id, lease);
|
|
325
370
|
try {
|
|
326
371
|
// All steps use the Claw invocation wrapper which applies:
|
|
327
372
|
// - SafetyController boundary prompt
|
|
@@ -440,14 +485,25 @@ async function executeStep(pipeline, spec) {
|
|
|
440
485
|
scopeViolation: scopeError,
|
|
441
486
|
};
|
|
442
487
|
}
|
|
443
|
-
|
|
488
|
+
let observations;
|
|
489
|
+
try {
|
|
490
|
+
observations = await executeActions(parsed.actions, spec, async () => {
|
|
491
|
+
if (!runnerInterval || !(await lease.isHeld()))
|
|
492
|
+
return false;
|
|
493
|
+
const live = await (await getStorage()).getPipeline(pipeline.id, pipeline.user_id);
|
|
494
|
+
return !!runnerInterval && await lease.isHeld() && live?.status === 'RUNNING' && live.current_step === pipeline.current_step;
|
|
495
|
+
}, async () => !!runnerInterval && await lease.isHeld());
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return { iteration: pipeline.iteration, step, started_at: stepStart, completed_at: new Date().toISOString(), success: false, notes: String(error instanceof Error ? error.message : error) };
|
|
499
|
+
}
|
|
444
500
|
return {
|
|
445
501
|
iteration: pipeline.iteration,
|
|
446
502
|
step,
|
|
447
503
|
started_at: stepStart,
|
|
448
504
|
completed_at: new Date().toISOString(),
|
|
449
505
|
success: true,
|
|
450
|
-
notes: parsed.notes || `Executed ${parsed.actions.length} action(s) successfully`,
|
|
506
|
+
notes: `${parsed.notes || `Executed ${parsed.actions.length} action(s) successfully`}\n${observations.join('\n')}`,
|
|
451
507
|
};
|
|
452
508
|
}
|
|
453
509
|
finally {
|
|
@@ -469,25 +525,72 @@ async function runnerTick() {
|
|
|
469
525
|
return;
|
|
470
526
|
}
|
|
471
527
|
tickInProgress = true;
|
|
528
|
+
let lease = null;
|
|
529
|
+
let renewalInterval;
|
|
472
530
|
try {
|
|
473
531
|
// Phase 1: Zombie sweep (cheap — just DB reads)
|
|
474
532
|
await sweepZombies();
|
|
475
533
|
// Phase 2: Promote PENDING → RUNNING, then find a RUNNING pipeline to advance
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
await
|
|
534
|
+
const backend = await getStorage();
|
|
535
|
+
const candidates = [
|
|
536
|
+
...await backend.listPipelines(undefined, 'PENDING', PRISM_USER_ID),
|
|
537
|
+
...await backend.listPipelines(undefined, 'RUNNING', PRISM_USER_ID),
|
|
538
|
+
].sort((a, b) => Date.parse(a.updated_at) - Date.parse(b.updated_at));
|
|
539
|
+
let selected;
|
|
540
|
+
for (const candidate of candidates) {
|
|
541
|
+
if (!(await locallyAuthorized(candidate, backend)))
|
|
542
|
+
continue;
|
|
543
|
+
lease = await claimPipelineLease(candidate);
|
|
544
|
+
if (lease) {
|
|
545
|
+
selected = candidate;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
484
548
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
549
|
+
if (!selected || !lease)
|
|
550
|
+
return;
|
|
551
|
+
const heldLease = lease;
|
|
552
|
+
renewalInterval = setInterval(() => {
|
|
553
|
+
void heldLease.renew().catch(error => {
|
|
554
|
+
console.error(`[DarkFactory] Execution lease renewal failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
555
|
+
});
|
|
556
|
+
}, FACTORY_LEASE_RENEW_MS);
|
|
557
|
+
renewalInterval.unref();
|
|
558
|
+
const pipeline = await backend.getPipeline(selected.id, selected.user_id);
|
|
559
|
+
if (!pipeline || !(await isPipelineAuthorized(pipeline)))
|
|
560
|
+
return;
|
|
561
|
+
if (!['PENDING', 'RUNNING'].includes(pipeline.status)) {
|
|
562
|
+
if (['FAILED', 'ABORTED', 'COMPLETED'].includes(pipeline.status))
|
|
563
|
+
await heldLease.acknowledgeRecovery();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Every persisted lifecycle/verification write must retain this process's
|
|
567
|
+
// authority. File actions and the test command check the same lease below.
|
|
568
|
+
const storage = new Proxy(backend, { get(target, key) {
|
|
569
|
+
const value = Reflect.get(target, key);
|
|
570
|
+
if (typeof value !== 'function')
|
|
571
|
+
return value;
|
|
572
|
+
if (!LEASED_STORAGE_WRITES.has(String(key)))
|
|
573
|
+
return value.bind(target);
|
|
574
|
+
return async (...args) => {
|
|
575
|
+
if (!(await heldLease.isHeld()) || !(await isPipelineAuthorized(pipeline)))
|
|
576
|
+
throw new Error('Factory execution lease lost');
|
|
577
|
+
return Reflect.apply(value, target, args);
|
|
578
|
+
};
|
|
579
|
+
} });
|
|
580
|
+
if (heldLease.reclaimed && pipeline.status === 'RUNNING') {
|
|
581
|
+
await storage.savePipeline({ ...pipeline, status: 'FAILED', error: 'Execution owner was lost; refusing to replay an unfinished step.' });
|
|
582
|
+
await heldLease.acknowledgeRecovery();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (pipeline.status === 'PENDING') {
|
|
586
|
+
await storage.savePipeline({ ...pipeline, status: 'RUNNING' });
|
|
587
|
+
const promoted = await backend.getPipeline(pipeline.id, pipeline.user_id);
|
|
588
|
+
if (!promoted || promoted.status !== 'RUNNING')
|
|
589
|
+
return;
|
|
590
|
+
Object.assign(pipeline, promoted);
|
|
591
|
+
if (heldLease.reclaimed)
|
|
592
|
+
await heldLease.acknowledgeRecovery();
|
|
488
593
|
}
|
|
489
|
-
// Pick the oldest updated pipeline (FIFO fairness)
|
|
490
|
-
const pipeline = running.sort((a, b) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime())[0];
|
|
491
594
|
// Poison Pill Guard: Parse spec with localized error handling.
|
|
492
595
|
// If a pipeline has corrupt/invalid JSON in its spec column,
|
|
493
596
|
// we MUST mark it FAILED immediately. Otherwise the runner will
|
|
@@ -578,7 +681,25 @@ async function runnerTick() {
|
|
|
578
681
|
return;
|
|
579
682
|
}
|
|
580
683
|
// Execute the current step
|
|
581
|
-
const result = await executeStep(pipeline, spec);
|
|
684
|
+
const result = await executeStep(pipeline, spec, heldLease);
|
|
685
|
+
if (!(await heldLease.isHeld()))
|
|
686
|
+
return;
|
|
687
|
+
// Heartbeat pulses persist their own optimistic version while the model runs.
|
|
688
|
+
// Re-read after stopping the pulse and rebase only onto the same live step;
|
|
689
|
+
// never carry a step result across an abort or another runner's advance.
|
|
690
|
+
const afterStep = await storage.getPipeline(pipeline.id, pipeline.user_id);
|
|
691
|
+
if (!afterStep || afterStep.status !== 'RUNNING') {
|
|
692
|
+
debugLog(`[DarkFactory] Step result discarded for ${pipeline.id}: pipeline is no longer RUNNING.`);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (afterStep.current_step !== pipeline.current_step ||
|
|
696
|
+
afterStep.iteration !== pipeline.iteration ||
|
|
697
|
+
(afterStep.eval_revisions ?? 0) !== (pipeline.eval_revisions ?? 0)) {
|
|
698
|
+
debugLog(`[DarkFactory] Step result discarded for ${pipeline.id}: pipeline advanced concurrently.`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
pipeline.updated_at = afterStep.updated_at;
|
|
702
|
+
pipeline.last_heartbeat = afterStep.last_heartbeat;
|
|
582
703
|
// v7.3.1: Scope violation in EXECUTE step → immediate pipeline termination
|
|
583
704
|
if ('scopeViolation' in result && result.scopeViolation) {
|
|
584
705
|
debugLog(`[DarkFactory] Pipeline ${pipeline.id} terminated: scope violation in EXECUTE step.`);
|
|
@@ -594,6 +715,27 @@ async function runnerTick() {
|
|
|
594
715
|
return;
|
|
595
716
|
}
|
|
596
717
|
const currentStep = pipeline.current_step;
|
|
718
|
+
if (currentStep === 'VERIFY') {
|
|
719
|
+
if (!spec.testCommand) {
|
|
720
|
+
result.success = false;
|
|
721
|
+
result.notes = 'Deterministic verification requires spec.testCommand.';
|
|
722
|
+
}
|
|
723
|
+
else
|
|
724
|
+
try {
|
|
725
|
+
const verificationNotes = await executeActions([{ type: 'RUN_TEST', targetPath: '.', command: spec.testCommand }], spec, async () => {
|
|
726
|
+
if (!runnerInterval || !(await heldLease.isHeld()))
|
|
727
|
+
return false;
|
|
728
|
+
const live = await (await getStorage()).getPipeline(pipeline.id, pipeline.user_id);
|
|
729
|
+
return !!runnerInterval && await heldLease.isHeld() && live?.status === 'RUNNING' && live.current_step === pipeline.current_step;
|
|
730
|
+
}, async () => !!runnerInterval && await heldLease.isHeld());
|
|
731
|
+
result.notes = `${result.notes || ''}\n${verificationNotes.join('\n')}`;
|
|
732
|
+
result.success = true;
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
result.success = false;
|
|
736
|
+
result.notes = `Deterministic verification failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
597
739
|
// ── Phase 4: Verification Pipeline Orchestrator ──
|
|
598
740
|
if (currentStep === 'VERIFY' && spec.workingDirectory) {
|
|
599
741
|
const harnessPath = path.join(path.resolve(spec.workingDirectory), 'verification_harness.json');
|
|
@@ -602,13 +744,15 @@ async function runnerTick() {
|
|
|
602
744
|
const rawHarness = fs.readFileSync(harnessPath, 'utf8');
|
|
603
745
|
const harnessData = JSON.parse(rawHarness);
|
|
604
746
|
// GAP-5 fix: Persist the harness so CLI drift detection works for DarkFactory runs
|
|
605
|
-
const
|
|
747
|
+
const minPassRate = harnessData.min_pass_rate ?? 1;
|
|
748
|
+
const rubricHash = computeRubricHash(harnessData.tests, minPassRate);
|
|
606
749
|
const harness = {
|
|
607
750
|
...harnessData,
|
|
608
751
|
project: pipeline.project,
|
|
609
752
|
conversation_id: `dark-factory-${pipeline.id}`,
|
|
610
|
-
created_at:
|
|
753
|
+
created_at: pipeline.started_at,
|
|
611
754
|
rubric_hash: rubricHash,
|
|
755
|
+
min_pass_rate: minPassRate,
|
|
612
756
|
};
|
|
613
757
|
await storage.saveVerificationHarness(harness, pipeline.user_id);
|
|
614
758
|
// GAP-2 fix: Build VerificationConfig from env vars so PRISM_VERIFICATION_LAYERS
|
|
@@ -633,7 +777,7 @@ async function runnerTick() {
|
|
|
633
777
|
project: pipeline.project,
|
|
634
778
|
conversation_id: `dark-factory-${pipeline.id}`,
|
|
635
779
|
run_at: new Date().toISOString(),
|
|
636
|
-
passed: passRate >=
|
|
780
|
+
passed: passRate >= minPassRate && verificationResult.severity_gate.action !== "abort",
|
|
637
781
|
pass_rate: passRate,
|
|
638
782
|
critical_failures: verificationResult.severity_gate.failed_assertions.length,
|
|
639
783
|
coverage_score: coverageScore,
|
|
@@ -641,8 +785,8 @@ async function runnerTick() {
|
|
|
641
785
|
gate_action: verificationResult.severity_gate.action,
|
|
642
786
|
gate_override: false,
|
|
643
787
|
};
|
|
788
|
+
await storage.saveVerificationRun(valResult, pipeline.user_id);
|
|
644
789
|
const { canContinue, validatedResult } = Gatekeeper.executeGate(valResult);
|
|
645
|
-
await storage.saveVerificationRun(validatedResult, pipeline.user_id);
|
|
646
790
|
// GAP-3 fix: Emit verification experience event for ML routing feedback
|
|
647
791
|
try {
|
|
648
792
|
const confidenceScore = Math.round(passRate * 100);
|
|
@@ -774,9 +918,7 @@ async function runnerTick() {
|
|
|
774
918
|
// EXECUTE notes = what the generator did
|
|
775
919
|
// EVALUATE notes = what the evaluator found
|
|
776
920
|
// Other steps: preserve existing notes
|
|
777
|
-
const updatedNotes =
|
|
778
|
-
? result.notes
|
|
779
|
-
: pipeline.notes;
|
|
921
|
+
const updatedNotes = result.notes || pipeline.notes;
|
|
780
922
|
await storage.savePipeline({
|
|
781
923
|
...pipeline,
|
|
782
924
|
current_step: nextStepInfo.step,
|
|
@@ -803,6 +945,12 @@ async function runnerTick() {
|
|
|
803
945
|
console.error(`[DarkFactory] Runner tick error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
804
946
|
}
|
|
805
947
|
finally {
|
|
948
|
+
if (renewalInterval)
|
|
949
|
+
clearInterval(renewalInterval);
|
|
950
|
+
if (lease)
|
|
951
|
+
await lease.release().catch(error => {
|
|
952
|
+
console.error(`[DarkFactory] Execution lease release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
953
|
+
});
|
|
806
954
|
tickInProgress = false;
|
|
807
955
|
}
|
|
808
956
|
}
|
|
@@ -25,7 +25,8 @@ export class SafetyController {
|
|
|
25
25
|
*/
|
|
26
26
|
static LEGAL_TRANSITIONS = {
|
|
27
27
|
'PENDING': ['RUNNING', 'ABORTED'], // Queued → Runner promotes or user aborts
|
|
28
|
-
|
|
28
|
+
// Step progress and heartbeats retain RUNNING throughout the pipeline.
|
|
29
|
+
'RUNNING': ['RUNNING', 'PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'],
|
|
29
30
|
'PAUSED': ['RUNNING', 'ABORTED'],
|
|
30
31
|
'ABORTED': [], // Terminal — no exits
|
|
31
32
|
'COMPLETED': [], // Terminal — no exits
|
|
@@ -168,6 +169,14 @@ export class SafetyController {
|
|
|
168
169
|
if (spec.contextFiles && spec.contextFiles.length > 0) {
|
|
169
170
|
lines.push(`Context Files: ${spec.contextFiles.join(', ')}`);
|
|
170
171
|
}
|
|
172
|
+
if (spec.allowedWritePaths) {
|
|
173
|
+
lines.push(`Allowed WRITE_FILE/PATCH_FILE targetPath values (relative, exactly as listed): ${JSON.stringify(spec.allowedWritePaths)}`);
|
|
174
|
+
}
|
|
175
|
+
if (spec.testCommand) {
|
|
176
|
+
lines.push(`Caller-authorized test command (exact string): ${spec.testCommand}`);
|
|
177
|
+
lines.push(`The VERIFY phase runs this command independently. In EXECUTE, return file changes only; do not add a RUN_TEST action.`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(`All targetPath values must be relative. The executor supplies the working directory; never prefix targetPath with the working directory.`);
|
|
171
180
|
lines.push(``, `── Objective ──`, spec.objective, ``, `── Safety Rules ──`, `1. Do NOT modify files outside the Restricted Workspace.`, `2. Do NOT make network requests unless the objective explicitly requires it.`, `3. Do NOT execute destructive operations (rm -rf, DROP TABLE, etc.).`, `4. Respond ONLY with actions relevant to the current step.`, `5. If you cannot complete the step, explain why and stop.`);
|
|
172
181
|
return lines.join('\n');
|
|
173
182
|
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -40,6 +40,7 @@ import { createDashboardStorageAccessor } from "./storageAccessor.js";
|
|
|
40
40
|
import { DASHBOARD_READ_MESSAGES } from "./readMessages.js";
|
|
41
41
|
import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
|
|
42
42
|
import { isTrustedRequest, isRebindGuardedPath } from "./hostGuard.js";
|
|
43
|
+
import { darkFactoryEnablementState, DARK_FACTORY_SETTINGS_UNAVAILABLE } from "../darkfactory/enablement.js";
|
|
43
44
|
import { resolveDashboardToken, requestHasToken, buildTokenCookie, dashboardTokenCookieName, isDashboardTokenProtectedPath, } from "./dashboardToken.js";
|
|
44
45
|
import { safeCompare, generateToken, isAuthenticated, createRateLimiter, initJWKS, } from "./authUtils.js";
|
|
45
46
|
const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
|
|
@@ -392,7 +393,7 @@ return false;}
|
|
|
392
393
|
type: "boolean",
|
|
393
394
|
title: "Enable Dark Factory",
|
|
394
395
|
description: "Set to true to enable autonomous evaluation pipelines.",
|
|
395
|
-
default:
|
|
396
|
+
default: true
|
|
396
397
|
},
|
|
397
398
|
prismTaskRouterEnabled: {
|
|
398
399
|
type: "boolean",
|
|
@@ -732,11 +733,11 @@ return false;}
|
|
|
732
733
|
const { getAllSettings } = await import("../storage/configStorage.js");
|
|
733
734
|
const settings = await getAllSettings();
|
|
734
735
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
735
|
-
return res.end(JSON.stringify({ settings }));
|
|
736
|
+
return res.end(JSON.stringify({ settings, darkFactory: darkFactoryEnablementState() }));
|
|
736
737
|
}
|
|
737
738
|
catch {
|
|
738
|
-
res.writeHead(
|
|
739
|
-
return res.end(JSON.stringify({
|
|
739
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
740
|
+
return res.end(JSON.stringify({ error: DARK_FACTORY_SETTINGS_UNAVAILABLE }));
|
|
740
741
|
}
|
|
741
742
|
}
|
|
742
743
|
// ─── API: Settings — POST (v3.0 Dashboard Settings) ───
|
|
@@ -29,6 +29,8 @@ export function isDashboardSettingValueAllowed(key, value) {
|
|
|
29
29
|
if (["auto_capture", "hivemind_enabled", "task_router_enabled"].includes(key)) {
|
|
30
30
|
return value === "true" || value === "false";
|
|
31
31
|
}
|
|
32
|
+
if (key === "PRISM_DARK_FACTORY_ENABLED")
|
|
33
|
+
return value === "true" || value === "false";
|
|
32
34
|
if (key === "agent_name")
|
|
33
35
|
return value.length <= 80 && !/[<>]/.test(value);
|
|
34
36
|
if (key === "default_role")
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* ═══════════════════════════════════════════════════════════════════
|
|
15
15
|
*/
|
|
16
16
|
import { ANALYTICS_DATE_PATTERN, ANALYTICS_METRIC_IDS, DASHBOARD_READ_MESSAGES } from './readMessages.js';
|
|
17
|
+
import { DARK_FACTORY_SETTINGS_UNAVAILABLE } from '../darkfactory/enablement.js';
|
|
17
18
|
export function renderDashboardLocalOpenHTML() {
|
|
18
19
|
return `<!DOCTYPE html>
|
|
19
20
|
<html lang="en">
|
|
@@ -505,8 +506,10 @@ export function renderDashboardHTML(version) {
|
|
|
505
506
|
.modal-close:hover { color: var(--text-primary); }
|
|
506
507
|
.setting-row {
|
|
507
508
|
display: flex; justify-content: space-between; align-items: center;
|
|
509
|
+
gap: 1rem;
|
|
508
510
|
padding: 0.75rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
509
511
|
}
|
|
512
|
+
.setting-row > div { min-width: 0; }
|
|
510
513
|
.setting-row:last-child { border-bottom: none; }
|
|
511
514
|
.setting-label { font-size: 0.85rem; color: var(--text-secondary); }
|
|
512
515
|
.setting-desc { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.2rem; }
|
|
@@ -1313,6 +1316,14 @@ export function renderDashboardHTML(version) {
|
|
|
1313
1316
|
</div>
|
|
1314
1317
|
<div class="toggle" id="toggle-task-router" onclick="toggleBootSetting('task_router_enabled', this)"></div>
|
|
1315
1318
|
</div>
|
|
1319
|
+
<div class="setting-row">
|
|
1320
|
+
<div>
|
|
1321
|
+
<div class="setting-label">Dark Factory</div>
|
|
1322
|
+
<div class="setting-desc">Enable pipeline execution tools and the background runner. Restart required; an environment override wins.</div>
|
|
1323
|
+
<div id="dark-factory-state" style="color:var(--text-muted);font-size:0.78rem;margin-top:0.25rem">Loading startup state…</div>
|
|
1324
|
+
</div>
|
|
1325
|
+
<button class="toggle" id="toggle-dark-factory" type="button" role="switch" aria-label="Dark Factory enablement" aria-checked="false" disabled onclick="toggleBootSetting('PRISM_DARK_FACTORY_ENABLED', this)"></button>
|
|
1326
|
+
</div>
|
|
1316
1327
|
<div class="setting-row">
|
|
1317
1328
|
<div>
|
|
1318
1329
|
<div class="setting-label">Storage Backend</div>
|
|
@@ -3993,9 +4004,11 @@ function loadSettings() {
|
|
|
3993
4004
|
return [4 /*yield*/, fetch('/api/settings?t=' + Date.now())];
|
|
3994
4005
|
case 1:
|
|
3995
4006
|
res = _a.sent();
|
|
4007
|
+
if (!res.ok) throw new Error(DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE);
|
|
3996
4008
|
return [4 /*yield*/, res.json()];
|
|
3997
4009
|
case 2:
|
|
3998
4010
|
data = _a.sent();
|
|
4011
|
+
if (!data.darkFactory) throw new Error(DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE);
|
|
3999
4012
|
s = data.settings || {};
|
|
4000
4013
|
// Runtime toggles
|
|
4001
4014
|
if (s.auto_capture === 'true')
|
|
@@ -4019,6 +4032,21 @@ function loadSettings() {
|
|
|
4019
4032
|
document.getElementById('toggle-task-router').classList.add('active');
|
|
4020
4033
|
else
|
|
4021
4034
|
document.getElementById('toggle-task-router').classList.remove('active');
|
|
4035
|
+
if ((s.PRISM_DARK_FACTORY_ENABLED || (data.darkFactory && data.darkFactory.saved ? 'true' : 'false')) === 'true')
|
|
4036
|
+
document.getElementById('toggle-dark-factory').classList.add('active');
|
|
4037
|
+
else
|
|
4038
|
+
document.getElementById('toggle-dark-factory').classList.remove('active');
|
|
4039
|
+
document.getElementById('toggle-dark-factory').setAttribute('aria-checked', document.getElementById('toggle-dark-factory').classList.contains('active') ? 'true' : 'false');
|
|
4040
|
+
if (data.darkFactory) {
|
|
4041
|
+
var df = data.darkFactory;
|
|
4042
|
+
var effective = df.envOverride === null ? df.saved : df.envOverride;
|
|
4043
|
+
var state = df.envOverride === null ? 'Saved: ' + (df.saved ? 'on' : 'off') + ' · Active: ' + (df.active ? 'on' : 'off') + (df.pendingRestart ? ' · restart required' : '') : 'Saved: ' + (df.saved ? 'on' : 'off') + ' · Active override: ' + (df.envOverride ? 'on' : 'off');
|
|
4044
|
+
document.getElementById('dark-factory-state').textContent = state;
|
|
4045
|
+
document.getElementById('toggle-dark-factory').disabled = df.envOverride !== null;
|
|
4046
|
+
document.getElementById('toggle-dark-factory').setAttribute('aria-disabled', df.envOverride !== null ? 'true' : 'false');
|
|
4047
|
+
document.getElementById('toggle-dark-factory').classList.toggle('active', effective);
|
|
4048
|
+
document.getElementById('toggle-dark-factory').setAttribute('aria-checked', effective ? 'true' : 'false');
|
|
4049
|
+
}
|
|
4022
4050
|
// Storage Backend
|
|
4023
4051
|
if (s.PRISM_STORAGE) {
|
|
4024
4052
|
document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
|
|
@@ -4041,6 +4069,9 @@ function loadSettings() {
|
|
|
4041
4069
|
case 3:
|
|
4042
4070
|
e_19 = _a.sent();
|
|
4043
4071
|
console.warn('Settings load failed:', e_19);
|
|
4072
|
+
document.getElementById('dark-factory-state').textContent = DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE;
|
|
4073
|
+
document.getElementById('toggle-dark-factory').disabled = true;
|
|
4074
|
+
document.getElementById('toggle-dark-factory').setAttribute('aria-disabled', 'true');
|
|
4044
4075
|
return [3 /*break*/, 4];
|
|
4045
4076
|
case 4: return [2 /*return*/];
|
|
4046
4077
|
}
|
|
@@ -4093,15 +4124,20 @@ function toggleSetting(key, el) {
|
|
|
4093
4124
|
}
|
|
4094
4125
|
function toggleBootSetting(key, el) {
|
|
4095
4126
|
var isActive = el.classList.toggle('active');
|
|
4127
|
+
el.setAttribute('aria-checked', isActive ? 'true' : 'false');
|
|
4096
4128
|
saveSetting(key, isActive ? 'true' : 'false').then(function (ok) {
|
|
4097
4129
|
if (!ok) {
|
|
4098
4130
|
el.classList.toggle('active'); // rollback on failure
|
|
4131
|
+
el.setAttribute('aria-checked', isActive ? 'false' : 'true');
|
|
4099
4132
|
}
|
|
4100
4133
|
else {
|
|
4101
|
-
showToast(
|
|
4134
|
+
showToast(DARK_FACTORY_RESTART_MESSAGE);
|
|
4135
|
+
loadSettings();
|
|
4102
4136
|
}
|
|
4103
4137
|
});
|
|
4104
4138
|
}
|
|
4139
|
+
var DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE = ${JSON.stringify(DARK_FACTORY_SETTINGS_UNAVAILABLE)};
|
|
4140
|
+
var DARK_FACTORY_RESTART_MESSAGE = 'Saved. Restart your AI client for this to take effect.';
|
|
4105
4141
|
function saveBootSetting(key, value) {
|
|
4106
4142
|
saveSetting(key, value).then(function (ok) {
|
|
4107
4143
|
if (ok)
|
|
@@ -158,7 +158,7 @@ function getAdvancedTourContent() {
|
|
|
158
158
|
title: "🚀 Advanced Features",
|
|
159
159
|
description: "Prism has much more to offer. Here's a quick tour of advanced capabilities:",
|
|
160
160
|
instructions: [
|
|
161
|
-
"**Dark Factory** — Autonomous AI pipelines that run in the background (
|
|
161
|
+
"**Dark Factory** — Autonomous AI pipelines that run in the background (enabled by default; use the dashboard or PRISM_DARK_FACTORY_ENABLED=false to disable)",
|
|
162
162
|
"**Hivemind** — Multi-agent coordination with role-based access (set PRISM_ENABLE_HIVEMIND=true)",
|
|
163
163
|
"**Auto-Scholar** — Autonomous research agent that discovers and synthesizes papers",
|
|
164
164
|
"**Mind Palace Dashboard** — Web UI at localhost:3080 for managing projects, viewing analytics, and configuring settings",
|
package/dist/server.js
CHANGED
|
@@ -60,7 +60,8 @@ ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequ
|
|
|
60
60
|
// Claude Desktop that the attached resource has changed.
|
|
61
61
|
// Without this, the paperclipped context becomes stale.
|
|
62
62
|
SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
63
|
-
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV,
|
|
63
|
+
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV, YDC_API_KEY, } from "./config.js";
|
|
64
|
+
import { initializeDarkFactoryEnabled, isDarkFactoryEnabled } from "./darkfactory/enablement.js";
|
|
64
65
|
import { startWatchdog, drainAlerts } from "./hivemindWatchdog.js";
|
|
65
66
|
import { startScheduler } from "./backgroundScheduler.js";
|
|
66
67
|
import { startDarkFactoryRunner } from "./darkfactory/runner.js";
|
|
@@ -345,7 +346,7 @@ export function getAvailableTools() {
|
|
|
345
346
|
...SESSION_MEMORY_TOOLS,
|
|
346
347
|
...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
|
|
347
348
|
...(getSettingSync("task_router_enabled", String(PRISM_TASK_ROUTER_ENABLED_ENV)) === "true" ? [SESSION_TASK_ROUTE_TOOL] : []),
|
|
348
|
-
...(
|
|
349
|
+
...(isDarkFactoryEnabled() ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
|
|
349
350
|
];
|
|
350
351
|
}
|
|
351
352
|
export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI Agents. On the first user turn of every conversation, ` +
|
|
@@ -357,7 +358,7 @@ export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI A
|
|
|
357
358
|
`trigger rules — read each before proposing any change. Surfacing a name is not loading it: hosts that ` +
|
|
358
359
|
`do not auto-load skill files must fetch each body with knowledge_search, passing the skill name ` +
|
|
359
360
|
`exactly as listed. ` +
|
|
360
|
-
`AFTER
|
|
361
|
+
`AFTER turn one, call session_route_prompt at the start of any turn with a new task, changed work, or reported defect. Pass the verbatim message as {prompt} and held skills as {loaded}; supply current project, working_directory, files_involved and test_command when known. Read and follow returned skills. When prism_factory_route has target factory, automatically call recommended_tool with recommended_args, then monitor status. Routing itself never executes code. ` +
|
|
361
362
|
`Emit no preamble. Print the complete tool result verbatim as the entire first-turn startup display, before any optional ` +
|
|
362
363
|
`answer. Do not summarize, paraphrase, rename headings, reformat, or omit any returned section. Preserve its order and ` +
|
|
363
364
|
`line content. For a greeting-only prompt, stop after the verbatim startup display. ` +
|
|
@@ -981,22 +982,22 @@ export function createServer() {
|
|
|
981
982
|
case "session_start_pipeline":
|
|
982
983
|
if (!SESSION_MEMORY_ENABLED)
|
|
983
984
|
throw new Error("Session memory not configured.");
|
|
984
|
-
if (!
|
|
985
|
-
throw new Error("Dark Factory not enabled.
|
|
985
|
+
if (!isDarkFactoryEnabled())
|
|
986
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
986
987
|
result = await sessionStartPipelineHandler(args);
|
|
987
988
|
break;
|
|
988
989
|
case "session_check_pipeline_status":
|
|
989
990
|
if (!SESSION_MEMORY_ENABLED)
|
|
990
991
|
throw new Error("Session memory not configured.");
|
|
991
|
-
if (!
|
|
992
|
-
throw new Error("Dark Factory not enabled.
|
|
992
|
+
if (!isDarkFactoryEnabled())
|
|
993
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
993
994
|
result = await sessionCheckPipelineStatusHandler(args);
|
|
994
995
|
break;
|
|
995
996
|
case "session_abort_pipeline":
|
|
996
997
|
if (!SESSION_MEMORY_ENABLED)
|
|
997
998
|
throw new Error("Session memory not configured.");
|
|
998
|
-
if (!
|
|
999
|
-
throw new Error("Dark Factory not enabled.
|
|
999
|
+
if (!isDarkFactoryEnabled())
|
|
1000
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
1000
1001
|
result = await sessionAbortPipelineHandler(args);
|
|
1001
1002
|
break;
|
|
1002
1003
|
// ─── v12.1: Developer Onboarding & Framework Bridge ───
|
|
@@ -1288,6 +1289,7 @@ export async function startServer() {
|
|
|
1288
1289
|
// during the Initialize handshake — zero extra latency for resource reads.
|
|
1289
1290
|
// initConfigStorage() is local SQLite only (~5ms), safe to await.
|
|
1290
1291
|
await initConfigStorage();
|
|
1292
|
+
initializeDarkFactoryEnabled();
|
|
1291
1293
|
// v4.6.0: Initialize OTel AFTER the settings cache is warm so that
|
|
1292
1294
|
// initTelemetry() can read otel_enabled/otel_endpoint from getSettingSync()
|
|
1293
1295
|
// synchronously. This is a synchronous call — no await needed.
|
|
@@ -1597,7 +1599,7 @@ async function resumeServerStartup(server) {
|
|
|
1597
1599
|
// Autonomous pipeline orchestration engine. Picks up RUNNING
|
|
1598
1600
|
// pipelines and advances them through PLAN → EXECUTE → VERIFY
|
|
1599
1601
|
// cycles. Non-blocking — uses setInterval to yield between ticks.
|
|
1600
|
-
if (
|
|
1602
|
+
if (isDarkFactoryEnabled() && SESSION_MEMORY_ENABLED) {
|
|
1601
1603
|
storageReady?.then(() => {
|
|
1602
1604
|
startDarkFactoryRunner();
|
|
1603
1605
|
}).catch(err => {
|