@nikelyh/offscript 0.0.0-stage → 1.0.0
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/LICENSE +201 -0
- package/README.md +148 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/dist/packages/application/assign-investigation/execute.js +34 -0
- package/dist/packages/application/assign-investigation/ports.js +1 -0
- package/dist/packages/application/assign-investigation/resolve-context.js +41 -0
- package/dist/packages/application/check-target.js +4 -0
- package/dist/packages/application/complete-investigation/execute.js +54 -0
- package/dist/packages/application/complete-investigation/ports.js +1 -0
- package/dist/packages/application/complete-investigation/resolve-context.js +48 -0
- package/dist/packages/application/configure-simulation/execute.js +29 -0
- package/dist/packages/application/configure-simulation/ports.js +1 -0
- package/dist/packages/application/configure-simulation/profiles.js +16 -0
- package/dist/packages/application/delete-configured-simulation.js +11 -0
- package/dist/packages/application/errors.js +8 -0
- package/dist/packages/application/index.js +11 -0
- package/dist/packages/application/outbox/dispatch.js +48 -0
- package/dist/packages/application/outbox/ports.js +16 -0
- package/dist/packages/application/recover-simulation/execute.js +31 -0
- package/dist/packages/application/recover-simulation/ports.js +1 -0
- package/dist/packages/application/request-investigation/execute.js +38 -0
- package/dist/packages/application/request-investigation/ports.js +1 -0
- package/dist/packages/application/request-investigation/resolve-context.js +41 -0
- package/dist/packages/browser/browser-manager.js +91 -0
- package/dist/packages/browser/dom-snapshot.js +186 -0
- package/dist/packages/browser/index.js +5 -0
- package/dist/packages/browser/scope-guard.js +73 -0
- package/dist/packages/browser/session.js +209 -0
- package/dist/packages/browser/tools/browser-tools.js +192 -0
- package/dist/packages/browser/types.js +1 -0
- package/dist/packages/contracts/errors.js +8 -0
- package/dist/packages/contracts/event-envelope.js +28 -0
- package/dist/packages/contracts/event-validation.js +44 -0
- package/dist/packages/contracts/events.js +1 -0
- package/dist/packages/contracts/finding-assessment.js +128 -0
- package/dist/packages/contracts/index.js +15 -0
- package/dist/packages/contracts/investigation-events.js +43 -0
- package/dist/packages/contracts/investigation-summary.js +60 -0
- package/dist/packages/contracts/mappers.js +17 -0
- package/dist/packages/contracts/observation-summary.js +25 -0
- package/dist/packages/contracts/semantic-events.js +41 -0
- package/dist/packages/contracts/serialization.js +18 -0
- package/dist/packages/contracts/simulation-configuration.js +17 -0
- package/dist/packages/contracts/simulation-execution.js +62 -0
- package/dist/packages/contracts/simulation-results.js +52 -0
- package/dist/packages/contracts/simulation-setup.js +60 -0
- package/dist/packages/contracts/summaries.js +42 -0
- package/dist/packages/contracts/target-connection.js +36 -0
- package/dist/packages/contracts/validation.js +42 -0
- package/dist/packages/domain/agent.js +23 -0
- package/dist/packages/domain/environment.js +1 -0
- package/dist/packages/domain/errors.js +14 -0
- package/dist/packages/domain/identity.js +16 -0
- package/dist/packages/domain/index.js +10 -0
- package/dist/packages/domain/investigation-result.js +19 -0
- package/dist/packages/domain/investigation.js +70 -0
- package/dist/packages/domain/observation.js +19 -0
- package/dist/packages/domain/profile.js +19 -0
- package/dist/packages/domain/simulation.js +61 -0
- package/dist/packages/domain/timestamp.js +9 -0
- package/dist/packages/orchestration/browser-exploration-factory.js +135 -0
- package/dist/packages/orchestration/controlled-demo-factory.js +26 -0
- package/dist/packages/orchestration/deterministic-browser-runner.js +92 -0
- package/dist/packages/orchestration/deterministic-runner.js +69 -0
- package/dist/packages/orchestration/event-bridge.js +27 -0
- package/dist/packages/orchestration/guard-inference.js +44 -0
- package/dist/packages/orchestration/index.js +7 -0
- package/dist/packages/orchestration/investigation-reaction.js +53 -0
- package/dist/packages/orchestration/investigation-store.js +96 -0
- package/dist/packages/orchestration/memory-outbox.js +63 -0
- package/dist/packages/orchestration/multi-mode-factory.js +12 -0
- package/dist/packages/orchestration/outbox-publisher.js +13 -0
- package/dist/packages/orchestration/session-storage.js +52 -0
- package/dist/packages/orchestration/session.js +176 -0
- package/dist/packages/orchestration/situations.js +26 -0
- package/dist/packages/orchestration/state.js +32 -0
- package/dist/packages/orchestration/tools/arguments.js +13 -0
- package/dist/packages/orchestration/tools/browser-exploration-tools.js +154 -0
- package/dist/packages/orchestration/tools/browser-interaction-tools.js +74 -0
- package/dist/packages/orchestration/tools/exploration-tools.js +119 -0
- package/dist/packages/persistence/codecs/investigation.js +30 -0
- package/dist/packages/persistence/codecs/simulation.js +69 -0
- package/dist/packages/persistence/codecs/values.js +24 -0
- package/dist/packages/persistence/connection.js +29 -0
- package/dist/packages/persistence/errors.js +8 -0
- package/dist/packages/persistence/index.js +22 -0
- package/dist/packages/persistence/migrate.js +40 -0
- package/dist/packages/persistence/migrations/0000_initial_domain_storage.sql +92 -0
- package/dist/packages/persistence/migrations/0001_transactional_outbox.sql +26 -0
- package/dist/packages/persistence/migrations/0002_simulation_configuration.sql +7 -0
- package/dist/packages/persistence/migrations/meta/0000_snapshot.json +637 -0
- package/dist/packages/persistence/migrations/meta/0001_snapshot.json +812 -0
- package/dist/packages/persistence/migrations/meta/0002_snapshot.json +859 -0
- package/dist/packages/persistence/migrations/meta/_journal.json +27 -0
- package/dist/packages/persistence/outbox/committer.js +54 -0
- package/dist/packages/persistence/outbox/store.js +66 -0
- package/dist/packages/persistence/repositories/configurations.js +41 -0
- package/dist/packages/persistence/repositories/delete-configuration.js +20 -0
- package/dist/packages/persistence/repositories/investigation-writes.js +72 -0
- package/dist/packages/persistence/repositories/investigations.js +27 -0
- package/dist/packages/persistence/repositories/observations.js +36 -0
- package/dist/packages/persistence/repositories/simulation-writes.js +39 -0
- package/dist/packages/persistence/repositories/simulations.js +23 -0
- package/dist/packages/persistence/schema/configurations.js +10 -0
- package/dist/packages/persistence/schema/index.js +5 -0
- package/dist/packages/persistence/schema/investigations.js +53 -0
- package/dist/packages/persistence/schema/observations.js +17 -0
- package/dist/packages/persistence/schema/outbox.js +26 -0
- package/dist/packages/persistence/schema/simulations.js +29 -0
- package/dist/packages/projects/index.js +2 -0
- package/dist/packages/projects/inspect.js +45 -0
- package/dist/packages/projects/suggest-target.js +34 -0
- package/dist/packages/projects/target-probe.js +61 -0
- package/dist/packages/runtime/deadline.js +11 -0
- package/dist/packages/runtime/evidence-store.js +76 -0
- package/dist/packages/runtime/execution-coordinator.js +223 -0
- package/dist/packages/runtime/execution-settlement.js +50 -0
- package/dist/packages/runtime/execution-types.js +1 -0
- package/dist/packages/runtime/host-config.js +26 -0
- package/dist/packages/runtime/host-storage.js +46 -0
- package/dist/packages/runtime/host.js +2 -0
- package/dist/packages/runtime/index.js +1 -0
- package/dist/packages/runtime/local-host.js +280 -0
- package/dist/packages/runtime/outbox-dispatcher.js +61 -0
- package/dist/packages/runtime/session-registry.js +42 -0
- package/dist/packages/server/configuration-routes.js +85 -0
- package/dist/packages/server/contracts.js +13 -0
- package/dist/packages/server/index.js +2 -0
- package/dist/packages/server/security.js +38 -0
- package/dist/packages/server/server.js +44 -0
- package/dist/packages/server/studio-assets.js +44 -0
- package/dist/packages/server/studio-session.js +28 -0
- package/dist/scripts/server/access-file.js +21 -0
- package/dist/scripts/server/cli-results.js +126 -0
- package/dist/scripts/server/open-browser.js +13 -0
- package/dist/scripts/server/run.js +76 -0
- package/dist/scripts/server/startup-error.js +17 -0
- package/dist/scripts/server/studio-entry-file.js +22 -0
- package/dist/studio/assets/index-Dh5KHl3J.css +1 -0
- package/dist/studio/assets/index-uXpu7a13.js +9 -0
- package/dist/studio/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- package/dist/studio/brand/logo-perspectives.svg +1 -0
- package/dist/studio/favicon.svg +1 -0
- package/dist/studio/index.html +14 -0
- package/dist/studio/licenses/jetbrains-mono.txt +93 -0
- package/dist/studio/licenses/space-grotesk.txt +93 -0
- package/dist/studio/participants/badger.png +0 -0
- package/dist/studio/participants/compass.png +0 -0
- package/dist/studio/participants/fox.png +0 -0
- package/dist/studio/participants/owl.png +0 -0
- package/dist/studio/participants/raven.png +0 -0
- package/dist/studio/participants/sparrow.png +0 -0
- package/offscript.mjs +104 -0
- package/package.json +100 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Recognize only simple Vite scripts; never evaluate project configuration. */
|
|
2
|
+
export function suggestTarget(manifest) {
|
|
3
|
+
const scripts = "scripts" in manifest ? manifest.scripts : undefined;
|
|
4
|
+
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
|
|
5
|
+
return;
|
|
6
|
+
for (const scriptName of ["dev", "start"]) {
|
|
7
|
+
const script = scriptName in scripts ? Reflect.get(scripts, scriptName) : undefined;
|
|
8
|
+
if (typeof script !== "string" || !/^vite(?:\s|$)/.test(script) || /[^\w\s.=:/-]/.test(script))
|
|
9
|
+
continue;
|
|
10
|
+
const args = script.trim().split(/\s+/).slice(1);
|
|
11
|
+
let port = "5173";
|
|
12
|
+
let valid = true;
|
|
13
|
+
let explicitPort = false;
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === "--port" || arg?.startsWith("--port=")) {
|
|
17
|
+
port = arg === "--port" ? args[++i] ?? "" : arg.slice(7);
|
|
18
|
+
explicitPort = true;
|
|
19
|
+
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535)
|
|
20
|
+
valid = false;
|
|
21
|
+
}
|
|
22
|
+
else if (arg === "--host" || arg?.startsWith("--host=")) {
|
|
23
|
+
const host = arg === "--host" && args[i + 1] && !args[i + 1]?.startsWith("--") ? args[++i] : arg?.slice(7);
|
|
24
|
+
if (host && !["localhost", "127.0.0.1", "0.0.0.0"].includes(host))
|
|
25
|
+
valid = false;
|
|
26
|
+
}
|
|
27
|
+
else if (arg !== "--strictPort")
|
|
28
|
+
valid = false;
|
|
29
|
+
}
|
|
30
|
+
if (valid)
|
|
31
|
+
return Object.freeze({ url: `http://localhost:${Number(port)}/`, source: explicitPort ? "vite-script" : "vite-default", scriptName });
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { parseLocalTargetUrl } from "../contracts/index.js";
|
|
4
|
+
/** Explicit, bounded checks; no DNS, redirects, cookies, or response bodies. */
|
|
5
|
+
export function createTargetProbe() {
|
|
6
|
+
let active = 0;
|
|
7
|
+
return { async check(input, signal) {
|
|
8
|
+
const url = parseLocalTargetUrl(input);
|
|
9
|
+
signal?.throwIfAborted();
|
|
10
|
+
if (active >= 2)
|
|
11
|
+
return Object.freeze({ url, kind: "busy" });
|
|
12
|
+
active++;
|
|
13
|
+
try {
|
|
14
|
+
return await new Promise((resolve, reject) => {
|
|
15
|
+
const target = new URL(url);
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
let timedOut = false;
|
|
18
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 2500);
|
|
19
|
+
const abort = () => controller.abort();
|
|
20
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
21
|
+
const done = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
|
|
22
|
+
let settled = false;
|
|
23
|
+
function send(hostname) {
|
|
24
|
+
const request = (target.protocol === "https:" ? httpsRequest : httpRequest)({
|
|
25
|
+
protocol: target.protocol, hostname,
|
|
26
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80), method: "GET",
|
|
27
|
+
path: target.pathname + target.search, headers: { Host: target.host, Accept: "text/html" },
|
|
28
|
+
...(target.protocol === "https:" && target.hostname === "localhost" ? { servername: "localhost" } : {}),
|
|
29
|
+
agent: false, maxHeaderSize: 8192, signal: controller.signal,
|
|
30
|
+
}, response => {
|
|
31
|
+
settled = true;
|
|
32
|
+
done();
|
|
33
|
+
resolve(Object.freeze({ url, kind: "response", statusCode: response.statusCode ?? 500 }));
|
|
34
|
+
response.destroy();
|
|
35
|
+
});
|
|
36
|
+
request.on("error", (error) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
if (target.hostname === "localhost" && hostname === "127.0.0.1" && error.code === "ECONNREFUSED" && !controller.signal.aborted) {
|
|
40
|
+
send("::1");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
settled = true;
|
|
44
|
+
done();
|
|
45
|
+
if (signal?.aborted) {
|
|
46
|
+
reject(signal.reason);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const tls = /CERT|TLS|SSL|SELF_SIGNED|UNABLE_TO_VERIFY/.test(error.code ?? "");
|
|
50
|
+
resolve(Object.freeze({ url, kind: timedOut ? "timeout" : tls ? "tls-error" : "unreachable" }));
|
|
51
|
+
});
|
|
52
|
+
request.end();
|
|
53
|
+
}
|
|
54
|
+
send(target.hostname === "[::1]" ? "::1" : "127.0.0.1");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
active--;
|
|
59
|
+
}
|
|
60
|
+
} };
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function withinDeadline(pending, durationMs) {
|
|
2
|
+
let timer;
|
|
3
|
+
try {
|
|
4
|
+
return await Promise.race([pending, new Promise((_, reject) => {
|
|
5
|
+
timer = setTimeout(() => reject(new Error("Host shutdown timed out; cleanup continues without force-closing storage.")), durationMs);
|
|
6
|
+
})]);
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
function assertSafeId(id, label) {
|
|
4
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
5
|
+
throw new Error(`Invalid ${label}: identifiers must only contain alphanumeric characters, hyphens, and underscores.`);
|
|
6
|
+
}
|
|
7
|
+
return id;
|
|
8
|
+
}
|
|
9
|
+
export function createEvidenceStore(baseDir, maxScreenshotsPerSimulation = 50) {
|
|
10
|
+
const root = resolve(baseDir, "evidence");
|
|
11
|
+
function getSimDir(simId) {
|
|
12
|
+
const safeId = assertSafeId(simId, "simulationId");
|
|
13
|
+
const dir = resolve(root, safeId);
|
|
14
|
+
if (!dir.startsWith(root)) {
|
|
15
|
+
throw new Error("Path traversal violation: simulation directory outside evidence root.");
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
function getFilePath(simId, evId) {
|
|
20
|
+
const dir = getSimDir(simId);
|
|
21
|
+
const safeEvId = assertSafeId(evId, "evidenceId");
|
|
22
|
+
const filePath = resolve(dir, `${safeEvId}.png`);
|
|
23
|
+
if (!filePath.startsWith(dir)) {
|
|
24
|
+
throw new Error("Path traversal violation: evidence file path outside simulation directory.");
|
|
25
|
+
}
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
async saveEvidence(simulationId, evidenceId, data, contentType = "image/png") {
|
|
30
|
+
void contentType;
|
|
31
|
+
const dir = getSimDir(simulationId);
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".png")) : [];
|
|
34
|
+
if (files.length >= maxScreenshotsPerSimulation) {
|
|
35
|
+
const filesWithMtime = files.map((fileName) => {
|
|
36
|
+
const fullPath = join(dir, fileName);
|
|
37
|
+
try {
|
|
38
|
+
return { fullPath, mtimeMs: statSync(fullPath).mtimeMs };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { fullPath, mtimeMs: 0 };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
filesWithMtime.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
45
|
+
const oldest = filesWithMtime[0];
|
|
46
|
+
if (oldest)
|
|
47
|
+
rmSync(oldest.fullPath, { force: true });
|
|
48
|
+
}
|
|
49
|
+
const filePath = getFilePath(simulationId, evidenceId);
|
|
50
|
+
writeFileSync(filePath, data);
|
|
51
|
+
},
|
|
52
|
+
async getEvidence(simulationId, evidenceId) {
|
|
53
|
+
try {
|
|
54
|
+
const filePath = getFilePath(simulationId, evidenceId);
|
|
55
|
+
if (!existsSync(filePath))
|
|
56
|
+
return undefined;
|
|
57
|
+
const data = readFileSync(filePath);
|
|
58
|
+
return { data, contentType: "image/png" };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async deleteSimulationEvidence(simulationId) {
|
|
65
|
+
try {
|
|
66
|
+
const dir = getSimDir(simulationId);
|
|
67
|
+
if (existsSync(dir)) {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Safe fallback on invalid simulation identity
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { simulationId, transitionSimulation, updateSimulationAgent } from "../domain/index.js";
|
|
2
|
+
import { ApplicationError } from "../application/index.js";
|
|
3
|
+
import { inspectExecutionFailures, settleSimulationAggregate } from "./execution-settlement.js";
|
|
4
|
+
export function executionCoordinator(dependencies) {
|
|
5
|
+
const { storage, prepareSession, factory, evidenceStore } = dependencies;
|
|
6
|
+
const active = new Map();
|
|
7
|
+
let closed = false;
|
|
8
|
+
function toStatus(record) {
|
|
9
|
+
let turnsCount = record.turnsCount;
|
|
10
|
+
let recentActivity;
|
|
11
|
+
let activeAgents = (record.phase === "starting" || record.phase === "running") ? record.activeAgents : [];
|
|
12
|
+
if (record.session?.snapshot) {
|
|
13
|
+
const snap = record.session.snapshot();
|
|
14
|
+
if (snap.turns)
|
|
15
|
+
turnsCount = snap.turns.reduce((sum, item) => sum + item.count, 0);
|
|
16
|
+
if (snap.recentActivity)
|
|
17
|
+
recentActivity = snap.recentActivity;
|
|
18
|
+
if ((record.phase === "starting" || record.phase === "running") && snap.activeAgents) {
|
|
19
|
+
activeAgents = snap.activeAgents;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
simulationId: record.simulationId, phase: record.phase, mode: record.mode,
|
|
24
|
+
turnsCount, activeAgents,
|
|
25
|
+
...(record.failureReason ? { failureReason: record.failureReason } : {}),
|
|
26
|
+
startedAt: record.startedAt,
|
|
27
|
+
...(record.stoppedAt ? { stoppedAt: record.stoppedAt } : {}),
|
|
28
|
+
...(recentActivity ? { recentActivity } : {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function finalizeExecution(record, outcome, reason) {
|
|
32
|
+
record.phase = "stopping";
|
|
33
|
+
if (record.timer)
|
|
34
|
+
clearTimeout(record.timer);
|
|
35
|
+
const result = await settleSimulationAggregate(storage, record.simulationId, outcome, reason);
|
|
36
|
+
let closeError;
|
|
37
|
+
try {
|
|
38
|
+
await record.session?.close();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
closeError = err;
|
|
42
|
+
}
|
|
43
|
+
record.stoppedAt = new Date().toISOString();
|
|
44
|
+
if (closeError) {
|
|
45
|
+
record.phase = "failed";
|
|
46
|
+
record.failureReason = "SESSION_CLOSE_FAILED";
|
|
47
|
+
}
|
|
48
|
+
else if (!result.settled && (result.reason === "storage-error" || result.reason === "conflict")) {
|
|
49
|
+
record.phase = "failed";
|
|
50
|
+
record.failureReason = result.reason === "conflict" ? "SETTLEMENT_CONFLICT" : "SETTLEMENT_STORAGE_FAILED";
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
record.phase = outcome;
|
|
54
|
+
if (reason)
|
|
55
|
+
record.failureReason = reason;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function handleBudgetExpiry(id) {
|
|
59
|
+
const record = active.get(id);
|
|
60
|
+
if (!record || record.phase !== "running")
|
|
61
|
+
return;
|
|
62
|
+
await finalizeExecution(record, "cancelled", "SESSION_BUDGET_EXHAUSTED");
|
|
63
|
+
}
|
|
64
|
+
function monitorSettlement(id, session, durationMs) {
|
|
65
|
+
if (!session.waitForIdle)
|
|
66
|
+
return;
|
|
67
|
+
void session.waitForIdle(durationMs).then(async (snap) => {
|
|
68
|
+
const record = active.get(id);
|
|
69
|
+
if (!record || record.phase !== "running")
|
|
70
|
+
return;
|
|
71
|
+
const failureCheck = inspectExecutionFailures(snap ?? record.session?.snapshot?.());
|
|
72
|
+
if (failureCheck.hasFailures) {
|
|
73
|
+
await finalizeExecution(record, "failed", failureCheck.reason ?? "SESSION_EXECUTION_FAILED");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await finalizeExecution(record, "completed");
|
|
77
|
+
}
|
|
78
|
+
}).catch(async (cause) => {
|
|
79
|
+
const record = active.get(id);
|
|
80
|
+
if (!record || record.phase !== "running")
|
|
81
|
+
return;
|
|
82
|
+
const isTimeout = cause instanceof Error && cause.message.includes("timed out");
|
|
83
|
+
const reason = isTimeout ? "SESSION_BUDGET_EXHAUSTED" : "SESSION_EXECUTION_FAILED";
|
|
84
|
+
const outcome = isTimeout ? "cancelled" : "failed";
|
|
85
|
+
await finalizeExecution(record, outcome, reason);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function start(id, mode) {
|
|
89
|
+
if (closed)
|
|
90
|
+
throw new ApplicationError("EXECUTION_START_FAILED", "Host is closing.");
|
|
91
|
+
if (!factory)
|
|
92
|
+
throw new ApplicationError("EXECUTION_START_FAILED", "No execution session factory configured on this host.");
|
|
93
|
+
if (mode !== "controlled-demo" && mode !== "browser-exploration") {
|
|
94
|
+
throw new ApplicationError("INVALID_EXECUTION_STATE", `Unsupported execution mode: ${mode}`);
|
|
95
|
+
}
|
|
96
|
+
const existing = active.get(id);
|
|
97
|
+
if (existing && (existing.phase === "starting" || existing.phase === "running" || existing.phase === "stopping")) {
|
|
98
|
+
throw new ApplicationError("EXECUTION_ALREADY_ACTIVE", "Simulation is already executing.");
|
|
99
|
+
}
|
|
100
|
+
const [config, sim] = await Promise.all([
|
|
101
|
+
storage.configurations.findConfiguration(id),
|
|
102
|
+
storage.simulations.findSimulation(id),
|
|
103
|
+
]);
|
|
104
|
+
if (!config || !sim)
|
|
105
|
+
throw new ApplicationError("SIMULATION_NOT_FOUND", "Simulation configuration not found.");
|
|
106
|
+
if (sim.lifecycle.status !== "created") {
|
|
107
|
+
throw new ApplicationError("INVALID_EXECUTION_STATE", `Simulation cannot start from status ${sim.lifecycle.status}.`);
|
|
108
|
+
}
|
|
109
|
+
if (sim.agents.some((agent) => agent.lifecycle.status !== "ready")) {
|
|
110
|
+
throw new ApplicationError("INVALID_EXECUTION_STATE", "All agents must be ready to start.");
|
|
111
|
+
}
|
|
112
|
+
let runningSim = transitionSimulation(sim, { status: "running" });
|
|
113
|
+
for (const agent of sim.agents) {
|
|
114
|
+
runningSim = updateSimulationAgent(runningSim, agent.id, { status: "running" });
|
|
115
|
+
}
|
|
116
|
+
const expectedAgentStatuses = new Map(sim.agents.map((agent) => [agent.id, "ready"]));
|
|
117
|
+
const commit = await storage.simulations.updateSimulationAggregate(runningSim, {
|
|
118
|
+
status: "created",
|
|
119
|
+
agentStatuses: expectedAgentStatuses,
|
|
120
|
+
});
|
|
121
|
+
if (commit === "conflict")
|
|
122
|
+
throw new ApplicationError("EXECUTION_ALREADY_ACTIVE", "Simulation state changed concurrently.");
|
|
123
|
+
if (commit === "not-found")
|
|
124
|
+
throw new ApplicationError("SIMULATION_NOT_FOUND", "Simulation not found.");
|
|
125
|
+
const startedAt = new Date().toISOString();
|
|
126
|
+
const record = {
|
|
127
|
+
simulationId: id, phase: "starting", mode, turnsCount: 0,
|
|
128
|
+
activeAgents: sim.agents.map((agent) => agent.id), startedAt,
|
|
129
|
+
};
|
|
130
|
+
active.set(id, record);
|
|
131
|
+
try {
|
|
132
|
+
const session = prepareSession(id, () => factory(runningSim, config.limits, storage, {
|
|
133
|
+
mode,
|
|
134
|
+
targetUrl: config.targetUrl,
|
|
135
|
+
evidenceStore,
|
|
136
|
+
}));
|
|
137
|
+
record.session = session;
|
|
138
|
+
record.phase = "running";
|
|
139
|
+
record.timer = setTimeout(() => { void handleBudgetExpiry(id); }, config.limits.durationMs);
|
|
140
|
+
monitorSettlement(id, session, config.limits.durationMs);
|
|
141
|
+
for (const agent of sim.agents) {
|
|
142
|
+
const initialPrompt = mode === "browser-exploration"
|
|
143
|
+
? `Explore the application at ${config.targetUrl}. Pursue the goal: ${config.goal}`
|
|
144
|
+
: "Explore the local cart and check items.";
|
|
145
|
+
session.startTurn(agent.id, initialPrompt);
|
|
146
|
+
}
|
|
147
|
+
return toStatus(record);
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
if (record.session) {
|
|
151
|
+
try {
|
|
152
|
+
await record.session.close();
|
|
153
|
+
}
|
|
154
|
+
catch { /* Safe cleanup */ }
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const failedSim = transitionSimulation(runningSim, { status: "failed", reason: "RUNTIME_INITIALIZATION_FAILED" });
|
|
158
|
+
await storage.simulations.updateSimulationAggregate(failedSim, { status: "running" });
|
|
159
|
+
}
|
|
160
|
+
catch { /* Best-effort aggregate update */ }
|
|
161
|
+
record.phase = "failed";
|
|
162
|
+
record.failureReason = "RUNTIME_INITIALIZATION_FAILED";
|
|
163
|
+
record.stoppedAt = new Date().toISOString();
|
|
164
|
+
throw new ApplicationError("EXECUTION_START_FAILED", "Failed to initialize runtime session.", { cause });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function stop(id) {
|
|
168
|
+
simulationId(id);
|
|
169
|
+
const record = active.get(id);
|
|
170
|
+
if (record && (record.phase === "starting" || record.phase === "running")) {
|
|
171
|
+
await finalizeExecution(record, "cancelled");
|
|
172
|
+
return toStatus(record);
|
|
173
|
+
}
|
|
174
|
+
const current = await storage.simulations.findSimulation(id);
|
|
175
|
+
if (!current)
|
|
176
|
+
throw new ApplicationError("SIMULATION_NOT_FOUND", "Simulation not found.");
|
|
177
|
+
if (current.lifecycle.status === "running") {
|
|
178
|
+
await settleSimulationAggregate(storage, id, "cancelled");
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
simulationId: id, phase: "cancelled",
|
|
181
|
+
turnsCount: 0, activeAgents: [],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return inspect(id);
|
|
185
|
+
}
|
|
186
|
+
async function inspect(id) {
|
|
187
|
+
simulationId(id);
|
|
188
|
+
const record = active.get(id);
|
|
189
|
+
if (record)
|
|
190
|
+
return toStatus(record);
|
|
191
|
+
const sim = await storage.simulations.findSimulation(id);
|
|
192
|
+
if (!sim)
|
|
193
|
+
throw new ApplicationError("SIMULATION_NOT_FOUND", "Simulation not found.");
|
|
194
|
+
if (sim.lifecycle.status === "created") {
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
simulationId: id, phase: "idle", turnsCount: 0, activeAgents: [],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (sim.lifecycle.status === "running") {
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
simulationId: id, phase: "interrupted",
|
|
202
|
+
turnsCount: 0, activeAgents: [],
|
|
203
|
+
failureReason: "INTERRUPTED_BY_HOST_RESTART",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
simulationId: id, phase: sim.lifecycle.status,
|
|
208
|
+
turnsCount: 0, activeAgents: [],
|
|
209
|
+
...(sim.lifecycle.status === "failed" ? { failureReason: sim.lifecycle.reason } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function close() {
|
|
213
|
+
closed = true;
|
|
214
|
+
const items = Array.from(active.values());
|
|
215
|
+
for (const record of items) {
|
|
216
|
+
if (record.phase === "starting" || record.phase === "running") {
|
|
217
|
+
await finalizeExecution(record, "cancelled");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
active.clear();
|
|
221
|
+
}
|
|
222
|
+
return Object.freeze({ start, stop, inspect, close, hasActive: (id) => active.has(id) });
|
|
223
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { transitionSimulation, updateSimulationAgent } from "../domain/index.js";
|
|
2
|
+
export async function settleSimulationAggregate(storage, id, outcome, reason) {
|
|
3
|
+
try {
|
|
4
|
+
const currentSim = await storage.simulations.findSimulation(id);
|
|
5
|
+
if (!currentSim)
|
|
6
|
+
return { settled: false, reason: "not-found" };
|
|
7
|
+
if (currentSim.lifecycle.status !== "running")
|
|
8
|
+
return { settled: false, reason: "not-running" };
|
|
9
|
+
let targetSim;
|
|
10
|
+
if (outcome === "completed") {
|
|
11
|
+
let updatedSim = currentSim;
|
|
12
|
+
for (const agent of currentSim.agents) {
|
|
13
|
+
if (agent.lifecycle.status === "running") {
|
|
14
|
+
updatedSim = updateSimulationAgent(updatedSim, agent.id, { status: "completed" });
|
|
15
|
+
}
|
|
16
|
+
else if (agent.lifecycle.status === "ready") {
|
|
17
|
+
updatedSim = updateSimulationAgent(updatedSim, agent.id, { status: "cancelled" });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
targetSim = transitionSimulation(updatedSim, { status: "completed" });
|
|
21
|
+
}
|
|
22
|
+
else if (outcome === "cancelled") {
|
|
23
|
+
targetSim = transitionSimulation(currentSim, { status: "cancelled" });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
targetSim = transitionSimulation(currentSim, {
|
|
27
|
+
status: "failed",
|
|
28
|
+
reason: reason ?? "EXECUTION_FAILED",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const commit = await storage.simulations.updateSimulationAggregate(targetSim, { status: "running" });
|
|
32
|
+
if (commit === "conflict" || commit === "not-found") {
|
|
33
|
+
return { settled: false, reason: commit };
|
|
34
|
+
}
|
|
35
|
+
return { settled: true, outcome, simulation: targetSim };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return { settled: false, reason: "storage-error", error };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function inspectExecutionFailures(snapshot) {
|
|
42
|
+
if (!snapshot || typeof snapshot !== "object")
|
|
43
|
+
return { hasFailures: false };
|
|
44
|
+
const failures = snapshot.failures;
|
|
45
|
+
if (!Array.isArray(failures) || failures.length === 0)
|
|
46
|
+
return { hasFailures: false };
|
|
47
|
+
const first = failures[0];
|
|
48
|
+
const stage = first?.stage ? `_${first.stage.toUpperCase().replace(/[^A-Z0-9]/g, "_")}` : "";
|
|
49
|
+
return { hasFailures: true, reason: `SESSION_EXECUTION_FAILED${stage}` };
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export function localHostConfig(options) {
|
|
4
|
+
if (!options.projectRoot.trim())
|
|
5
|
+
throw new Error("An explicit project root is required.");
|
|
6
|
+
const projectRoot = realpathSync(resolve(options.projectRoot));
|
|
7
|
+
if (!statSync(projectRoot).isDirectory())
|
|
8
|
+
throw new Error("Project root must be a directory.");
|
|
9
|
+
if (options.dataDirectory !== undefined && !options.dataDirectory.trim())
|
|
10
|
+
throw new Error("Data directory must not be blank.");
|
|
11
|
+
const port = options.port ?? 0;
|
|
12
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
13
|
+
throw new Error("Invalid host port.");
|
|
14
|
+
const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 15_000;
|
|
15
|
+
if (!Number.isInteger(shutdownTimeoutMs) || shutdownTimeoutMs < 1 || shutdownTimeoutMs > 60_000)
|
|
16
|
+
throw new Error("Invalid shutdown timeout.");
|
|
17
|
+
const allowedOrigins = [...options.allowedOrigins ?? []];
|
|
18
|
+
for (const origin of allowedOrigins) {
|
|
19
|
+
const url = new URL(origin);
|
|
20
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin !== origin || url.username || url.password)
|
|
21
|
+
throw new Error("Expected a canonical HTTP origin.");
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze({ projectRoot, dataDirectory: resolve(projectRoot, options.dataDirectory ?? ".offscript"),
|
|
24
|
+
...(options.studioDirectory === undefined ? {} : { studioDirectory: realpathSync(resolve(options.studioDirectory)) }),
|
|
25
|
+
port, allowedOrigins: Object.freeze(allowedOrigins), shutdownTimeoutMs });
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function hostStorage(source) {
|
|
2
|
+
let accepting = true;
|
|
3
|
+
const pending = new Set();
|
|
4
|
+
function guard(operation) {
|
|
5
|
+
return (...args) => {
|
|
6
|
+
if (!accepting)
|
|
7
|
+
return Promise.reject(new Error("Host storage is closed to new operations."));
|
|
8
|
+
const result = Promise.resolve().then(() => operation(...args));
|
|
9
|
+
pending.add(result);
|
|
10
|
+
void result.then(() => pending.delete(result), () => pending.delete(result));
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const storage = Object.freeze({
|
|
15
|
+
configurations: Object.freeze({
|
|
16
|
+
deleteConfiguredSimulation: guard(source.configurations.deleteConfiguredSimulation),
|
|
17
|
+
createConfiguredSimulation: guard(source.configurations.createConfiguredSimulation),
|
|
18
|
+
findConfiguration: guard(source.configurations.findConfiguration),
|
|
19
|
+
listConfigurations: guard(source.configurations.listConfigurations),
|
|
20
|
+
}),
|
|
21
|
+
simulations: Object.freeze({
|
|
22
|
+
createSimulation: guard(source.simulations.createSimulation),
|
|
23
|
+
findSimulation: guard(source.simulations.findSimulation),
|
|
24
|
+
updateSimulationAggregate: guard(source.simulations.updateSimulationAggregate),
|
|
25
|
+
}),
|
|
26
|
+
observations: Object.freeze({
|
|
27
|
+
createObservation: guard(source.observations.createObservation),
|
|
28
|
+
findObservation: guard(source.observations.findObservation),
|
|
29
|
+
listObservations: guard(source.observations.listObservations),
|
|
30
|
+
}),
|
|
31
|
+
investigations: Object.freeze({
|
|
32
|
+
findInvestigation: guard(source.investigations.findInvestigation),
|
|
33
|
+
listInvestigations: guard(source.investigations.listInvestigations),
|
|
34
|
+
commitRequest: guard(source.investigations.commitRequest),
|
|
35
|
+
commitAssignment: guard(source.investigations.commitAssignment),
|
|
36
|
+
commitCompletion: guard(source.investigations.commitCompletion),
|
|
37
|
+
}),
|
|
38
|
+
outbox: Object.freeze({
|
|
39
|
+
claim: guard(source.outbox.claim), accept: guard(source.outbox.accept), defer: guard(source.outbox.defer),
|
|
40
|
+
reject: guard(source.outbox.reject), inspect: guard(source.outbox.inspect), hasPending: guard(source.outbox.hasPending),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
return { storage,
|
|
44
|
+
async drain() { accepting = false; await Promise.allSettled(pending); },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createOutboxDispatcher } from "./outbox-dispatcher.js";
|