humanish 0.0.1 → 0.15.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/AGENTS.md +77 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +435 -8
- package/SECURITY.md +34 -0
- package/dist/actor-contract.d.ts +107 -0
- package/dist/actor-contract.js +286 -0
- package/dist/actor-contract.js.map +1 -0
- package/dist/actor-registry.d.ts +73 -0
- package/dist/actor-registry.js +93 -0
- package/dist/actor-registry.js.map +1 -0
- package/dist/adapter-extension.d.ts +54 -0
- package/dist/adapter-extension.js +214 -0
- package/dist/adapter-extension.js.map +1 -0
- package/dist/argv.d.ts +1 -0
- package/dist/argv.js +8 -0
- package/dist/argv.js.map +1 -0
- package/dist/artifact-reference.d.ts +22 -0
- package/dist/artifact-reference.js +47 -0
- package/dist/artifact-reference.js.map +1 -0
- package/dist/browser-evidence-hygiene.d.ts +7 -0
- package/dist/browser-evidence-hygiene.js +40 -0
- package/dist/browser-evidence-hygiene.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +94 -0
- package/dist/claude-agent-sdk.js +356 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-app-server-ui.d.ts +39 -0
- package/dist/codex-app-server-ui.js +327 -0
- package/dist/codex-app-server-ui.js.map +1 -0
- package/dist/codex-app-server.d.ts +141 -0
- package/dist/codex-app-server.js +747 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/command-failure.d.ts +29 -0
- package/dist/command-failure.js +54 -0
- package/dist/command-failure.js.map +1 -0
- package/dist/computer-use-actor.d.ts +52 -0
- package/dist/computer-use-actor.js +48 -0
- package/dist/computer-use-actor.js.map +1 -0
- package/dist/computer-use.d.ts +232 -0
- package/dist/computer-use.js +615 -0
- package/dist/computer-use.js.map +1 -0
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +27 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/concurrent-shared-world-lab.d.ts +115 -0
- package/dist/concurrent-shared-world-lab.js +1092 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -0
- package/dist/core/git-state.d.ts +31 -0
- package/dist/core/git-state.js +142 -0
- package/dist/core/git-state.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/run-primitives.d.ts +66 -0
- package/dist/core/run-primitives.js +120 -0
- package/dist/core/run-primitives.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +620 -0
- package/dist/cua-actor-lab.js +2834 -0
- package/dist/cua-actor-lab.js.map +1 -0
- package/dist/device-presets.d.ts +67 -0
- package/dist/device-presets.js +50 -0
- package/dist/device-presets.js.map +1 -0
- package/dist/e2b-desktop-executor.d.ts +101 -0
- package/dist/e2b-desktop-executor.js +309 -0
- package/dist/e2b-desktop-executor.js.map +1 -0
- package/dist/e2b-desktop-launch.d.ts +144 -0
- package/dist/e2b-desktop-launch.js +59 -0
- package/dist/e2b-desktop-launch.js.map +1 -0
- package/dist/e2b-detached.d.ts +53 -0
- package/dist/e2b-detached.js +130 -0
- package/dist/e2b-detached.js.map +1 -0
- package/dist/e2b-terminal-lab.d.ts +345 -0
- package/dist/e2b-terminal-lab.js +1467 -0
- package/dist/e2b-terminal-lab.js.map +1 -0
- package/dist/env-file.d.ts +14 -0
- package/dist/env-file.js +108 -0
- package/dist/env-file.js.map +1 -0
- package/dist/feedback.d.ts +51 -0
- package/dist/feedback.js +313 -0
- package/dist/feedback.js.map +1 -0
- package/dist/image-evidence.d.ts +2 -0
- package/dist/image-evidence.js +33 -0
- package/dist/image-evidence.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/init-templates.d.ts +12 -0
- package/dist/init-templates.js +369 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.d.ts +26 -0
- package/dist/init.js +343 -0
- package/dist/init.js.map +1 -0
- package/dist/lab-app-runner.d.ts +78 -0
- package/dist/lab-app-runner.js +403 -0
- package/dist/lab-app-runner.js.map +1 -0
- package/dist/lab-config.d.ts +578 -0
- package/dist/lab-config.js +1751 -0
- package/dist/lab-config.js.map +1 -0
- package/dist/lab-engine.d.ts +77 -0
- package/dist/lab-engine.js +216 -0
- package/dist/lab-engine.js.map +1 -0
- package/dist/lab-preflight.d.ts +67 -0
- package/dist/lab-preflight.js +385 -0
- package/dist/lab-preflight.js.map +1 -0
- package/dist/labs.d.ts +53 -0
- package/dist/labs.js +176 -0
- package/dist/labs.js.map +1 -0
- package/dist/observer-assets.d.ts +2 -0
- package/dist/observer-assets.js +2630 -0
- package/dist/observer-assets.js.map +1 -0
- package/dist/observer-data.d.ts +63 -0
- package/dist/observer-data.js +145 -0
- package/dist/observer-data.js.map +1 -0
- package/dist/observer-static.d.ts +39 -0
- package/dist/observer-static.js +192 -0
- package/dist/observer-static.js.map +1 -0
- package/dist/observer.d.ts +46 -0
- package/dist/observer.js +353 -0
- package/dist/observer.js.map +1 -0
- package/dist/openai-responses-cu.d.ts +113 -0
- package/dist/openai-responses-cu.js +531 -0
- package/dist/openai-responses-cu.js.map +1 -0
- package/dist/oss-lab.d.ts +52 -0
- package/dist/oss-lab.js +299 -0
- package/dist/oss-lab.js.map +1 -0
- package/dist/oss-meta-lab-scoring.d.ts +15 -0
- package/dist/oss-meta-lab-scoring.js +145 -0
- package/dist/oss-meta-lab-scoring.js.map +1 -0
- package/dist/oss-meta-lab.d.ts +279 -0
- package/dist/oss-meta-lab.js +4889 -0
- package/dist/oss-meta-lab.js.map +1 -0
- package/dist/oss-remote-telemetry.d.ts +77 -0
- package/dist/oss-remote-telemetry.js +403 -0
- package/dist/oss-remote-telemetry.js.map +1 -0
- package/dist/persona.d.ts +35 -0
- package/dist/persona.js +133 -0
- package/dist/persona.js.map +1 -0
- package/dist/pi-agent-core.d.ts +77 -0
- package/dist/pi-agent-core.js +203 -0
- package/dist/pi-agent-core.js.map +1 -0
- package/dist/program.d.ts +54 -0
- package/dist/program.js +2347 -0
- package/dist/program.js.map +1 -0
- package/dist/redaction.d.ts +90 -0
- package/dist/redaction.js +292 -0
- package/dist/redaction.js.map +1 -0
- package/dist/run.d.ts +784 -0
- package/dist/run.js +4555 -0
- package/dist/run.js.map +1 -0
- package/dist/scripted-browser-actor.d.ts +294 -0
- package/dist/scripted-browser-actor.js +1286 -0
- package/dist/scripted-browser-actor.js.map +1 -0
- package/dist/scripted-browser-lab.d.ts +107 -0
- package/dist/scripted-browser-lab.js +738 -0
- package/dist/scripted-browser-lab.js.map +1 -0
- package/dist/shared-world-lab.d.ts +208 -0
- package/dist/shared-world-lab.js +1048 -0
- package/dist/shared-world-lab.js.map +1 -0
- package/dist/source-archive.d.ts +82 -0
- package/dist/source-archive.js +365 -0
- package/dist/source-archive.js.map +1 -0
- package/dist/stop-conditions.d.ts +26 -0
- package/dist/stop-conditions.js +66 -0
- package/dist/stop-conditions.js.map +1 -0
- package/dist/terminal-agent-actor.d.ts +36 -0
- package/dist/terminal-agent-actor.js +23 -0
- package/dist/terminal-agent-actor.js.map +1 -0
- package/docs/architecture/actor-contract.md +431 -0
- package/docs/architecture/github-feedback-loop.md +189 -0
- package/docs/architecture/local-codex-tui-actor.md +213 -0
- package/docs/architecture/observer.md +118 -0
- package/docs/architecture/oss-lab-poc.md +242 -0
- package/docs/architecture/project-layout.md +166 -0
- package/docs/architecture/state-driven-executor.md +203 -0
- package/docs/architecture/terminal-product-lane.md +148 -0
- package/docs/contracts/adapter-fixtures.md +80 -0
- package/docs/contracts/core.md +71 -0
- package/docs/contracts/feedback.md +145 -0
- package/docs/contracts/policy.md +305 -0
- package/docs/contracts/run-bundle.md +358 -0
- package/docs/contracts/schemas.md +984 -0
- package/docs/goals/current.md +409 -0
- package/docs/principles/invariants-and-defaults.md +135 -0
- package/docs/principles/self-driving-harness.md +129 -0
- package/docs/product/open-source-install-experience.md +229 -0
- package/docs/ramp/README.md +200 -0
- package/docs/release/open-source-readiness.md +208 -0
- package/docs/release/public-readiness-standard.md +205 -0
- package/docs/roadmap/world-class-open-source-v0.md +311 -0
- package/package.json +102 -8
- package/skills/humanish/SKILL.md +244 -0
- package/skills/humanish/agents/openai.yaml +7 -0
- package/index.js +0 -1
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
// The shared-world lab backend (#164): the SEQUENTIAL deterministic proof-of-concept of the
|
|
2
|
+
// shared-world topology. ONE sandbox provisions a mutable service plane ONCE (clone or packed
|
|
3
|
+
// working tree + serve + seed), then N role SEATS take turns IN DECLARED ORDER (each an isolated browser
|
|
4
|
+
// profile + identity) against the shared loopback app. A read-only state
|
|
5
|
+
// CHECKPOINT (digest probe) runs at baseline + after each role's turn, producing a
|
|
6
|
+
// harness-clocked timeline that PROVES role B acted on a world already containing role A's
|
|
7
|
+
// mutation (the checkpoint after A strictly precedes B's turn in one clock).
|
|
8
|
+
//
|
|
9
|
+
// Doctrine (docs/goals/shared-world-topology/goal.md): the bundle declares a VERIFIED, weaker
|
|
10
|
+
// `attributionClass: shared-world` + a `humanish.shared-world.v1` block whose `attributionLimits`
|
|
11
|
+
// pin the attribution ceiling (sequential-only, no-concurrent-races,
|
|
12
|
+
// delta-attributed-to-turn-not-action). verifyRun fails closed on any overclaim.
|
|
13
|
+
//
|
|
14
|
+
// Safety rails (same as the fan-out packet + the 2026-06-16 prod incident): ONE Sandbox.create;
|
|
15
|
+
// ONE teardown BY exact sandboxId in a finally — NEVER Sandbox.list (account-wide ops are
|
|
16
|
+
// forbidden). Provisioned values are literal-scrubbed before any error/log persists; checkpoints
|
|
17
|
+
// persist DIGEST-ONLY. The concurrent (getHost) topology, a handoff/barrier grammar, an onTurn
|
|
18
|
+
// hook, and real per-role login are named NON-GOALS (PR2+).
|
|
19
|
+
//
|
|
20
|
+
// FIDELITY NOTE: one sandbox has ONE desktop geometry, so a role's `device` is a PROMPT SIGNAL
|
|
21
|
+
// (composed into its persona context) — physical per-role geometry is the concurrent topology's
|
|
22
|
+
// job. Each role's stream viewport records the sandbox's actual rendered resolution (honest).
|
|
23
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { runDesktopCommandOrThrow, toErrorMessage } from "./command-failure.js";
|
|
27
|
+
import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
|
|
28
|
+
import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
|
|
29
|
+
import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson } from "./browser-evidence-hygiene.js";
|
|
30
|
+
import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, makeChromeBrowserStateObserver, makeLaneWriteScreenshot, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, SUBJECT_DIR } from "./cua-actor-lab.js";
|
|
31
|
+
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
32
|
+
import { runDetachedStep } from "./e2b-detached.js";
|
|
33
|
+
import { resolveSeatUrl, sharedWorldValidationReason } from "./lab-config.js";
|
|
34
|
+
import { renderObserver } from "./observer.js";
|
|
35
|
+
import { redactText } from "./redaction.js";
|
|
36
|
+
import { buildRunSource, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, SHARED_WORLD_SCHEMA } from "./run.js";
|
|
37
|
+
export const SHARED_WORLD_LAB_SCHEMA = "humanish.shared-world-lab-result.v1";
|
|
38
|
+
export const SHARED_WORLD_LAB_PROVIDER_METADATA = {
|
|
39
|
+
mode: "shared-world-lab",
|
|
40
|
+
tool: "humanish"
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
|
|
43
|
+
// Settle after opening a seat's browser, before the session's first screenshot.
|
|
44
|
+
const BROWSER_SETTLE_MS = 8_000;
|
|
45
|
+
// Server-side reclamation buffer past the loop's own wall-clock stop.
|
|
46
|
+
const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
|
|
47
|
+
// Room for the one-time clone/install/build/start/probe + the sequential per-role sessions.
|
|
48
|
+
const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
|
|
49
|
+
// Per-checkpoint probe budget (read-only aggregate probes are fast).
|
|
50
|
+
const CHECKPOINT_TIMEOUT_MS = 60_000;
|
|
51
|
+
const DEFAULT_STATE_STEP_TIMEOUT_MS = 5 * 60_000;
|
|
52
|
+
const DEFAULT_MISSION = "You are testing a shared web application other roles also use. The browser is already open at your entry URL. Accomplish what your role asks, then stop.";
|
|
53
|
+
function readPositiveInt(value, fallback) {
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return fallback;
|
|
56
|
+
const parsed = Number.parseInt(value, 10);
|
|
57
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
58
|
+
}
|
|
59
|
+
function makeSharedWorldRunId() {
|
|
60
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
61
|
+
return `shared-world-${stamp}-${randomBytes(4).toString("hex")}`;
|
|
62
|
+
}
|
|
63
|
+
/** Single-quote a value for safe interpolation into the seat-launch shell command. */
|
|
64
|
+
function shellQuote(value) {
|
|
65
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Launch ONE seat's browser with its OWN isolated profile — the shared-world topology needs a
|
|
69
|
+
* fresh browser identity boundary per role (cookies/session isolated per seat). Absent/default
|
|
70
|
+
* preserves the historical shared-world opener (Chrome best-effort). A concrete preference is
|
|
71
|
+
* fail-closed and records the resolved in-sandbox command.
|
|
72
|
+
*/
|
|
73
|
+
async function launchSeatBrowser(desktop, args) {
|
|
74
|
+
const requested = args.browserPreference ?? "default";
|
|
75
|
+
const chromiumFlags = CHROMIUM_EVIDENCE_HYGIENE_FLAGS.map(shellQuote).join(" ");
|
|
76
|
+
const command = [
|
|
77
|
+
"set -euo pipefail",
|
|
78
|
+
`browser_preference=${shellQuote(requested)}`,
|
|
79
|
+
`profile_dir=${shellQuote(args.profileDir)}`,
|
|
80
|
+
`seat_url=${shellQuote(args.seatUrl)}`,
|
|
81
|
+
`chrome_preferences_json=${shellQuote(chromiumEvidenceProfilePreferencesJson())}`,
|
|
82
|
+
'mkdir -p "$profile_dir"',
|
|
83
|
+
"chrome_debug_flags=(" + chromiumFlags + ")",
|
|
84
|
+
"prepare_chrome_profile() {",
|
|
85
|
+
" mkdir -p \"$profile_dir/Default\"",
|
|
86
|
+
" printf '%s\\n' \"$chrome_preferences_json\" > \"$profile_dir/Default/Preferences\"",
|
|
87
|
+
"}",
|
|
88
|
+
"launch_chrome() {",
|
|
89
|
+
" local label=\"$1\"",
|
|
90
|
+
" local binary=\"$2\"",
|
|
91
|
+
" if ! command -v \"$binary\" >/dev/null 2>&1; then return 127; fi",
|
|
92
|
+
" echo \"HUMANISH_BROWSER_RESOLVED=$label\"",
|
|
93
|
+
" pkill -f '[r]emote-debugging-port=9222' 2>/dev/null || true",
|
|
94
|
+
" prepare_chrome_profile",
|
|
95
|
+
" setsid -f \"$binary\" --new-window --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --user-data-dir=\"$profile_dir\" \"${chrome_debug_flags[@]}\" \"$seat_url\" > /dev/null 2>&1 < /dev/null",
|
|
96
|
+
"}",
|
|
97
|
+
"launch_firefox() {",
|
|
98
|
+
" if ! command -v firefox >/dev/null 2>&1; then return 127; fi",
|
|
99
|
+
" echo \"HUMANISH_BROWSER_RESOLVED=firefox\"",
|
|
100
|
+
" setsid -f firefox --new-window --profile \"$profile_dir\" \"$seat_url\" > /dev/null 2>&1 < /dev/null",
|
|
101
|
+
"}",
|
|
102
|
+
"case \"$browser_preference\" in",
|
|
103
|
+
" chrome)",
|
|
104
|
+
" launch_chrome google-chrome google-chrome || launch_chrome google-chrome-stable google-chrome-stable",
|
|
105
|
+
" ;;",
|
|
106
|
+
" chromium)",
|
|
107
|
+
" launch_chrome chromium chromium || launch_chrome chromium-browser chromium-browser",
|
|
108
|
+
" ;;",
|
|
109
|
+
" firefox)",
|
|
110
|
+
" launch_firefox",
|
|
111
|
+
" ;;",
|
|
112
|
+
" default)",
|
|
113
|
+
" launch_chrome google-chrome google-chrome || true",
|
|
114
|
+
" ;;",
|
|
115
|
+
"esac"
|
|
116
|
+
].join("\n");
|
|
117
|
+
const result = await runDesktopCommandOrThrow(() => desktop.commands.run(command, { requestTimeoutMs: args.requestTimeoutMs }), (_info, error) => args.browserPreference !== undefined && args.browserPreference !== "default"
|
|
118
|
+
? new Error(`requested desktop browser "${args.browserPreference}" could not be launched for shared-world seat`)
|
|
119
|
+
: error);
|
|
120
|
+
if (args.browserPreference !== undefined && args.browserPreference !== "default" && result.exitCode !== undefined && result.exitCode !== 0) {
|
|
121
|
+
throw new Error(`requested desktop browser "${args.browserPreference}" could not be launched for shared-world seat`);
|
|
122
|
+
}
|
|
123
|
+
if (args.browserPreference === undefined) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const resolved = (result.stdout ?? "").match(/^HUMANISH_BROWSER_RESOLVED=(\S+)$/m)?.[1];
|
|
127
|
+
return {
|
|
128
|
+
requested,
|
|
129
|
+
...(resolved === undefined ? {} : { resolved })
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Combine a snapshot's per-probe digests into ONE sha256-16 (digest-only; no raw value). */
|
|
133
|
+
export function combineCheckpointDigest(parts) {
|
|
134
|
+
return commandDigestOf(parts.join("\n"));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run ONE checkpoint snapshot LIVE: each declared probe runs read-only via the detached
|
|
138
|
+
* primitive; its stdout is literal-scrubbed (provisioned values + the probe's declared redact
|
|
139
|
+
* literals, folded into `scrub`) then pattern-redacted, then digested. Only the COMBINED digest
|
|
140
|
+
* persists — never the raw value (the seed-step lockdown). Unique step names per snapshot prevent
|
|
141
|
+
* stale-status reuse across snapshots.
|
|
142
|
+
*/
|
|
143
|
+
export async function runCheckpointSnapshot(args) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
for (const probe of args.checkpoints) {
|
|
146
|
+
const result = await runDetachedStep(args.desktop, {
|
|
147
|
+
name: `checkpoint-${args.snapshotIndex}-${probe.name}`,
|
|
148
|
+
command: probe.command,
|
|
149
|
+
cwd: SUBJECT_DIR,
|
|
150
|
+
timeoutMs: CHECKPOINT_TIMEOUT_MS,
|
|
151
|
+
requestTimeoutMs: args.requestTimeoutMs,
|
|
152
|
+
...args.timers
|
|
153
|
+
});
|
|
154
|
+
const scrubbed = redactText(args.scrub(result.logTail));
|
|
155
|
+
parts.push(`${probe.name}=${commandDigestOf(scrubbed)}`);
|
|
156
|
+
}
|
|
157
|
+
const digest = combineCheckpointDigest(parts);
|
|
158
|
+
return {
|
|
159
|
+
kind: "checkpoint",
|
|
160
|
+
name: args.name,
|
|
161
|
+
digest,
|
|
162
|
+
deltaFromPrev: args.prevDigest !== undefined && digest !== args.prevDigest
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** The DECLARED (dry-run) checkpoint snapshot: digest the probe RECIPE (command digests), no run. */
|
|
166
|
+
export function declaredCheckpointSnapshot(name, checkpoints) {
|
|
167
|
+
const parts = checkpoints.map((probe) => `${probe.name}=${commandDigestOf(probe.command)}`);
|
|
168
|
+
return { kind: "checkpoint", name, digest: combineCheckpointDigest(parts), deltaFromPrev: false };
|
|
169
|
+
}
|
|
170
|
+
/** sha256-16 over the ordered seed-step command digests — the seeded-state RECIPE identity. */
|
|
171
|
+
export function seedRecipeDigest(config) {
|
|
172
|
+
const seed = config.subject.state?.seed ?? [];
|
|
173
|
+
return commandDigestOf(seed.map((step) => `${step.name}:${commandDigestOf(step.command)}`).join("\n"));
|
|
174
|
+
}
|
|
175
|
+
/** Build the resolved role roster from actors[0].lanes (the role roster). */
|
|
176
|
+
function buildRoleSpecs(config, serveUrl) {
|
|
177
|
+
const actor = config.actors[0];
|
|
178
|
+
const mission = actor?.mission ?? DEFAULT_MISSION;
|
|
179
|
+
const roster = actor?.lanes ?? [];
|
|
180
|
+
return roster.map((lane, i) => {
|
|
181
|
+
const roleId = lane.id ?? `role-${String(i + 1).padStart(2, "0")}`;
|
|
182
|
+
const device = resolveLaneDevice(config, lane);
|
|
183
|
+
const composed = composeLaneInstructions({
|
|
184
|
+
mission,
|
|
185
|
+
...(lane.persona === undefined ? {} : { persona: lane.persona }),
|
|
186
|
+
...(lane.instruction === undefined ? {} : { instruction: lane.instruction }),
|
|
187
|
+
device: { name: device.name, preset: device.preset }
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
roleId,
|
|
191
|
+
roleIndex: i,
|
|
192
|
+
simId: `sim-${String(i + 1).padStart(3, "0")}`,
|
|
193
|
+
streamId: `stream-${String(i + 1).padStart(3, "0")}`,
|
|
194
|
+
persona: composed.persona,
|
|
195
|
+
instructions: composed.instructions,
|
|
196
|
+
deviceName: device.name,
|
|
197
|
+
...((lane.stopWhen ?? actor?.stopWhen) === undefined ? {} : { stopWhen: (lane.stopWhen ?? actor?.stopWhen) }),
|
|
198
|
+
...(lane.entry === undefined ? {} : { entry: lane.entry }),
|
|
199
|
+
seatUrl: resolveSeatUrl(serveUrl, lane.entry) ?? serveUrl,
|
|
200
|
+
screenshotDir: roleId,
|
|
201
|
+
traceArtifactPath: `actors/${`stream-${String(i + 1).padStart(3, "0")}`}.json`,
|
|
202
|
+
profileDir: `/tmp/seat-${roleId}`
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export async function runSharedWorldLab(options) {
|
|
207
|
+
const { config, dryRun } = options;
|
|
208
|
+
const cwd = path.resolve(options.cwd);
|
|
209
|
+
const hooks = options.hooks ?? {};
|
|
210
|
+
const env = hooks.env ?? process.env;
|
|
211
|
+
const render = hooks.renderObserverFn ?? renderObserver;
|
|
212
|
+
const actorType = config.actors[0]?.type ?? "";
|
|
213
|
+
const fail = (code, message, actorLabel) => ({
|
|
214
|
+
schema: SHARED_WORLD_LAB_SCHEMA,
|
|
215
|
+
ok: false,
|
|
216
|
+
cwd,
|
|
217
|
+
labId: config.id,
|
|
218
|
+
actor: actorLabel ?? actorType,
|
|
219
|
+
topology: "shared-world",
|
|
220
|
+
roleCount: config.actors[0]?.lanes?.length ?? 0,
|
|
221
|
+
sequence: [],
|
|
222
|
+
dryRun,
|
|
223
|
+
runId: options.runId ?? "not-created",
|
|
224
|
+
roles: [],
|
|
225
|
+
warnings: [],
|
|
226
|
+
error: { code, message }
|
|
227
|
+
});
|
|
228
|
+
// Resolve the actor through the registry — the parser validated this, but the engine fails closed
|
|
229
|
+
// rather than trusting a config that arrived through the library door (this fn is npm surface).
|
|
230
|
+
const descriptor = actorRegistry[actorType];
|
|
231
|
+
if (!descriptor || !isCuaActorDescriptor(descriptor)) {
|
|
232
|
+
return fail("HUMANISH_SHARED_WORLD_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
|
|
233
|
+
}
|
|
234
|
+
// Re-enforce the shared-world cross-validation (library API surface).
|
|
235
|
+
const invalidReason = sharedWorldValidationReason(config);
|
|
236
|
+
if (invalidReason) {
|
|
237
|
+
return fail("HUMANISH_SHARED_WORLD_LAB_INVALID", invalidReason, descriptor.id);
|
|
238
|
+
}
|
|
239
|
+
const serve = config.subject.serve;
|
|
240
|
+
const localTreeRoute = config.subject.source === "local-tree";
|
|
241
|
+
const subjectRepo = config.subject.repos?.[0] ?? "";
|
|
242
|
+
const subjectEnvNames = config.subject.env ?? [];
|
|
243
|
+
const checkpoints = config.subject.state?.checkpoint ?? [];
|
|
244
|
+
const roleSpecs = buildRoleSpecs(config, serve.url);
|
|
245
|
+
const roleCount = roleSpecs.length;
|
|
246
|
+
const runSession = hooks.runSession ?? descriptor.runSession;
|
|
247
|
+
// Read keys once into locals (names only; values never logged or persisted).
|
|
248
|
+
const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
|
|
249
|
+
const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
|
|
250
|
+
// Literal scrubber for every known provisioned value (no secret "shape" to pattern-match):
|
|
251
|
+
// provider/E2B keys, subject env values, AND each checkpoint's declared redact literals.
|
|
252
|
+
const knownSecretValues = [
|
|
253
|
+
openaiApiKey,
|
|
254
|
+
e2bApiKey,
|
|
255
|
+
...subjectEnvNames.map((name) => env[name] ?? ""),
|
|
256
|
+
...checkpoints.flatMap((probe) => probe.redact ?? [])
|
|
257
|
+
].filter((value) => value.length >= 4);
|
|
258
|
+
const scrubKnownValues = (text) => knownSecretValues.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
|
|
259
|
+
const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
|
|
260
|
+
const publicRepo = redactRepoLabel ? "repo-01" : subjectRepo;
|
|
261
|
+
const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
|
|
262
|
+
if (!dryRun) {
|
|
263
|
+
const missingKeys = [
|
|
264
|
+
...(openaiApiKey ? [] : ["OPENAI_API_KEY"]),
|
|
265
|
+
...(e2bApiKey ? [] : ["E2B_API_KEY"])
|
|
266
|
+
];
|
|
267
|
+
if (missingKeys.length > 0) {
|
|
268
|
+
return fail("HUMANISH_SHARED_WORLD_LAB_KEYS_MISSING", `Live shared-world labs need ${missingKeys.join(" and ")} in the environment (pass them via --env-file; values are never persisted).`, descriptor.id);
|
|
269
|
+
}
|
|
270
|
+
const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
|
|
271
|
+
if (missingSubjectEnv.length > 0) {
|
|
272
|
+
return fail("HUMANISH_SHARED_WORLD_LAB_SUBJECT_ENV_MISSING", `subject.env declares ${missingSubjectEnv.join(", ")} but the environment does not provide ${missingSubjectEnv.length === 1 ? "it" : "them"} (pass via --env-file; values are never persisted).`, descriptor.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const runId = options.runId ?? makeSharedWorldRunId();
|
|
276
|
+
const artifactRoot = path.join(cwd, ".humanish", "runs", runId);
|
|
277
|
+
const createdAt = new Date().toISOString();
|
|
278
|
+
const timeoutMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
279
|
+
const requestTimeoutMs = readPositiveInt(env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
280
|
+
const redactScreenshots = config.policies?.redactScreenshots === true;
|
|
281
|
+
const timers = hooks.detachedTimers ?? {};
|
|
282
|
+
// ONE sandbox geometry for the shared desktop (run-wide; per-role device is a prompt signal).
|
|
283
|
+
const sandboxDevice = resolveLaneDevice(config, undefined);
|
|
284
|
+
const sandboxResolution = sandboxDevice.resolution;
|
|
285
|
+
const sandboxPreset = sandboxDevice.preset;
|
|
286
|
+
const perRunSandboxMs = config.execution?.desktop?.sandboxTimeoutMs
|
|
287
|
+
?? timeoutMs * Math.max(1, roleCount)
|
|
288
|
+
+ SUBJECT_PROVISION_BUDGET_MS
|
|
289
|
+
+ (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0)
|
|
290
|
+
+ SANDBOX_TIMEOUT_BUFFER_MS;
|
|
291
|
+
await mkdir(artifactRoot, { recursive: true });
|
|
292
|
+
const source = await buildRunSource({ capturedAt: createdAt, cwd, humanishSource: "present", packageName: "humanish" });
|
|
293
|
+
const warnings = [];
|
|
294
|
+
const stateStepRecords = [];
|
|
295
|
+
const roleOutcomes = [];
|
|
296
|
+
const baselineDeclared = declaredCheckpointSnapshot("cp-baseline", checkpoints);
|
|
297
|
+
let baselineCheckpoint = baselineDeclared;
|
|
298
|
+
let subjectCommit;
|
|
299
|
+
let sandboxId;
|
|
300
|
+
let killed = false;
|
|
301
|
+
let failFastReason;
|
|
302
|
+
// Pack the working tree ONCE per run, on the host, BEFORE any sandbox is created (mirrors the
|
|
303
|
+
// cua route's ordering): a packing failure fails the run closed here, never spending sandbox
|
|
304
|
+
// cost. Dry-run packs nothing (no fs side effects; the contract bundle carries no archiveSha256).
|
|
305
|
+
let localTreeArchive;
|
|
306
|
+
let localTreeArchiveBuffer;
|
|
307
|
+
if (localTreeRoute && !dryRun) {
|
|
308
|
+
const packLocalTree = hooks.packLocalTree ?? defaultPackLocalTree;
|
|
309
|
+
try {
|
|
310
|
+
const packed = await packLocalTree({
|
|
311
|
+
root: cwd,
|
|
312
|
+
...(config.subject.localTree?.exclude === undefined ? {} : { extraExclude: config.subject.localTree.exclude }),
|
|
313
|
+
...(config.subject.localTree?.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: config.subject.localTree.maxArchiveBytes })
|
|
314
|
+
});
|
|
315
|
+
localTreeArchive = packed.archive;
|
|
316
|
+
localTreeArchiveBuffer = packed.buffer;
|
|
317
|
+
process.stderr.write(`humanish shared-world local-tree: packed ${packed.archive.fileCount} entries, ${packed.archive.totalBytes} bytes, archiveSha256 ${packed.archive.archiveSha256}`
|
|
318
|
+
+ `${packed.archive.git ? ` (commit ${packed.archive.git.commit.slice(0, 12)}, ${packed.archive.git.dirty ? "dirty" : "clean"} working tree)` : " (not a git work tree)"}\n`);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
return fail("HUMANISH_SHARED_WORLD_LAB_FAILED", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!dryRun) {
|
|
325
|
+
let desktopModule;
|
|
326
|
+
let desktop;
|
|
327
|
+
let runFailed = false;
|
|
328
|
+
try {
|
|
329
|
+
desktopModule = await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
|
|
330
|
+
// ONE Sandbox.create for the whole run (metadata.topology + roleCount; the SUBJECT env is
|
|
331
|
+
// provisioned here on the clone route — the ACTOR key never enters the sandbox). An optional
|
|
332
|
+
// custom desktop template (image) selects Sandbox.create(template, opts); absent keeps the
|
|
333
|
+
// byte-stable Sandbox.create(opts) default.
|
|
334
|
+
desktop = await createDesktopSandbox(desktopModule, {
|
|
335
|
+
apiKey: e2bApiKey,
|
|
336
|
+
requestTimeoutMs,
|
|
337
|
+
timeoutMs: perRunSandboxMs,
|
|
338
|
+
metadata: {
|
|
339
|
+
...SHARED_WORLD_LAB_PROVIDER_METADATA,
|
|
340
|
+
labId: config.id,
|
|
341
|
+
topology: "shared-world",
|
|
342
|
+
roleCount: String(roleCount)
|
|
343
|
+
},
|
|
344
|
+
...(subjectEnvNames.length > 0
|
|
345
|
+
? { envs: Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])) }
|
|
346
|
+
: {}),
|
|
347
|
+
resolution: sandboxResolution,
|
|
348
|
+
dpi: 96,
|
|
349
|
+
lifecycle: { onTimeout: "kill" }
|
|
350
|
+
}, config.execution?.desktop?.template);
|
|
351
|
+
sandboxId = desktop.sandboxId;
|
|
352
|
+
if (hooks.prepareDesktop) {
|
|
353
|
+
await hooks.prepareDesktop(desktop);
|
|
354
|
+
}
|
|
355
|
+
// Provision the shared plane ONCE: clone + install/build + seed + serve + readiness probe
|
|
356
|
+
// (clone route), or upload/extract the once-per-run packed archive + the SAME shared serve
|
|
357
|
+
// pipeline (local-tree route). One stderr line per phase boundary by default; hooks.onPhase
|
|
358
|
+
// (DI seam, mirrors CuaActorLabHooks) overrides it so tests capture instead of writing to
|
|
359
|
+
// real stderr.
|
|
360
|
+
const onSubjectPhase = hooks.onPhase ?? ((event) => {
|
|
361
|
+
process.stderr.write(`humanish shared-world: ${event.message}${event.durationMs === undefined ? "" : ` (${event.durationMs}ms)`}\n`);
|
|
362
|
+
});
|
|
363
|
+
if (localTreeRoute) {
|
|
364
|
+
await provisionLocalTreeSubject(desktop, {
|
|
365
|
+
archiveBuffer: localTreeArchiveBuffer,
|
|
366
|
+
serve,
|
|
367
|
+
...(config.subject.state === undefined ? {} : { state: config.subject.state }),
|
|
368
|
+
requestTimeoutMs,
|
|
369
|
+
scrub: scrubKnownValues,
|
|
370
|
+
onStateStep: (record) => { stateStepRecords.push(record); },
|
|
371
|
+
onPhase: onSubjectPhase,
|
|
372
|
+
...timers
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
subjectCommit = await provisionCloneSubject(desktop, {
|
|
377
|
+
repo: subjectRepo,
|
|
378
|
+
depth: config.subject.clone?.depth ?? 1,
|
|
379
|
+
serve,
|
|
380
|
+
...(config.subject.state === undefined ? {} : { state: config.subject.state }),
|
|
381
|
+
hasGithubToken,
|
|
382
|
+
requestTimeoutMs,
|
|
383
|
+
scrub: scrubKnownValues,
|
|
384
|
+
onCommit: (commit) => { subjectCommit = commit; },
|
|
385
|
+
onStateStep: (record) => { stateStepRecords.push(record); },
|
|
386
|
+
onPhase: onSubjectPhase,
|
|
387
|
+
...timers
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// Baseline checkpoint (before any role acts).
|
|
391
|
+
baselineCheckpoint = await runCheckpointSnapshot({
|
|
392
|
+
desktop,
|
|
393
|
+
snapshotIndex: 0,
|
|
394
|
+
name: "cp-baseline",
|
|
395
|
+
checkpoints,
|
|
396
|
+
prevDigest: undefined,
|
|
397
|
+
scrub: scrubKnownValues,
|
|
398
|
+
requestTimeoutMs,
|
|
399
|
+
timers
|
|
400
|
+
});
|
|
401
|
+
let prevDigest = baselineCheckpoint.digest;
|
|
402
|
+
// Sequential per-role loop (DECLARED order). A HARNESS error blocks the REMAINING roles
|
|
403
|
+
// (the shared-state premise is broken); a MISSION failure is data, never trips fail-fast.
|
|
404
|
+
for (const [index, spec] of roleSpecs.entries()) {
|
|
405
|
+
if (failFastReason) {
|
|
406
|
+
roleOutcomes.push({
|
|
407
|
+
spec,
|
|
408
|
+
screenshots: [],
|
|
409
|
+
skippedReason: `skipped: ${failFastReason}`,
|
|
410
|
+
noEngagement: false,
|
|
411
|
+
harnessError: false
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const screenshots = [];
|
|
416
|
+
const writeScreenshot = makeLaneWriteScreenshot(artifactRoot, { screenshotDir: spec.screenshotDir }, screenshots);
|
|
417
|
+
let session;
|
|
418
|
+
let sessionError;
|
|
419
|
+
let desktopBrowser;
|
|
420
|
+
try {
|
|
421
|
+
// Fresh isolated browser profile per seat, opened at the role's same-origin loopback entry.
|
|
422
|
+
desktopBrowser = await launchSeatBrowser(desktop, {
|
|
423
|
+
...(config.execution?.desktop?.browser === undefined ? {} : { browserPreference: config.execution.desktop.browser }),
|
|
424
|
+
profileDir: spec.profileDir,
|
|
425
|
+
seatUrl: spec.seatUrl,
|
|
426
|
+
requestTimeoutMs
|
|
427
|
+
});
|
|
428
|
+
await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
|
|
429
|
+
const sessionOptions = {
|
|
430
|
+
instructions: spec.instructions,
|
|
431
|
+
persona: spec.persona,
|
|
432
|
+
timeoutMs,
|
|
433
|
+
openai: {
|
|
434
|
+
apiKey: openaiApiKey,
|
|
435
|
+
...(config.actors[0]?.model ? { model: config.actors[0].model } : {})
|
|
436
|
+
},
|
|
437
|
+
desktop: desktop,
|
|
438
|
+
executorOptions: {
|
|
439
|
+
observeBrowserState: makeChromeBrowserStateObserver(desktop, requestTimeoutMs)
|
|
440
|
+
},
|
|
441
|
+
redactScreenshots,
|
|
442
|
+
scrubText: scrubKnownValues,
|
|
443
|
+
writeScreenshot,
|
|
444
|
+
...(spec.stopWhen === undefined ? {} : { stopWhen: spec.stopWhen })
|
|
445
|
+
};
|
|
446
|
+
session = await runSession(sessionOptions);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
sessionError = redactText(scrubKnownValues(toErrorMessage(error)));
|
|
450
|
+
}
|
|
451
|
+
if (session) {
|
|
452
|
+
await mkdir(path.dirname(path.join(artifactRoot, spec.traceArtifactPath)), { recursive: true });
|
|
453
|
+
await writeFile(path.join(artifactRoot, spec.traceArtifactPath), `${JSON.stringify(session.trace, null, 2)}\n`, "utf8");
|
|
454
|
+
if (session.trace.redaction.screenshots === "raw") {
|
|
455
|
+
warnings.push("Screenshots are full-fidelity (raw) for local use — the bundle stays in gitignored .humanish and nothing scans these pixels; review them before sharing anywhere. Set policies.redactScreenshots: true to blur a share-as-is bundle.");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const noEngagement = session !== undefined
|
|
459
|
+
&& session.completionReason === "goal_satisfied"
|
|
460
|
+
&& (session.trace.counts.actions ?? 0) === 0
|
|
461
|
+
&& (session.trace.counts.messages ?? 0) === 0;
|
|
462
|
+
if (noEngagement) {
|
|
463
|
+
warnings.push(`Role ${spec.roleId} returned goal_satisfied with ZERO actions and ZERO messages — likely a blank/still-loading screen; NOT counted as a pass.`);
|
|
464
|
+
}
|
|
465
|
+
const harnessError = sessionError !== undefined || session?.completionReason === "harness_error";
|
|
466
|
+
// Checkpoint AFTER this role's turn (the interaction-proof snapshot). Runs even on a
|
|
467
|
+
// harness-errored turn (the probe is read-only state, independent of the browser seat).
|
|
468
|
+
const afterCheckpoint = await runCheckpointSnapshot({
|
|
469
|
+
desktop,
|
|
470
|
+
snapshotIndex: index + 1,
|
|
471
|
+
name: `cp-after-${spec.roleId}`,
|
|
472
|
+
checkpoints,
|
|
473
|
+
prevDigest,
|
|
474
|
+
scrub: scrubKnownValues,
|
|
475
|
+
requestTimeoutMs,
|
|
476
|
+
timers
|
|
477
|
+
});
|
|
478
|
+
prevDigest = afterCheckpoint.digest;
|
|
479
|
+
roleOutcomes.push({
|
|
480
|
+
spec,
|
|
481
|
+
...(session ? { session } : {}),
|
|
482
|
+
...(sessionError === undefined ? {} : { sessionError }),
|
|
483
|
+
screenshots,
|
|
484
|
+
...(desktopBrowser === undefined ? {} : { desktopBrowser }),
|
|
485
|
+
noEngagement,
|
|
486
|
+
harnessError,
|
|
487
|
+
afterCheckpoint
|
|
488
|
+
});
|
|
489
|
+
if (harnessError && !failFastReason) {
|
|
490
|
+
failFastReason = `role "${spec.roleId}" ended in a harness error — the shared-state premise is broken (fail-fast)`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
runFailed = true;
|
|
496
|
+
warnings.push(`Shared-world run failed before completion: ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
// ONE teardown BY exact sandboxId — NEVER Sandbox.list (the 2026-06-16 prod-incident rail).
|
|
500
|
+
if (desktop && desktopModule) {
|
|
501
|
+
const anyRoleFailed = runFailed
|
|
502
|
+
|| failFastReason !== undefined
|
|
503
|
+
|| roleOutcomes.some((outcome) => outcome.harnessError || outcome.sessionError !== undefined);
|
|
504
|
+
// Each route's own keep flag gates its own run only: a clone.keep can never leak into a
|
|
505
|
+
// local-tree run's teardown decision, and vice versa (mirrors runCuaLane's keepReason).
|
|
506
|
+
const keepReason = config.subject.clone?.keep === true
|
|
507
|
+
? "subject.clone.keep"
|
|
508
|
+
: config.subject.localTree?.keep === true
|
|
509
|
+
? "subject.localTree.keep"
|
|
510
|
+
: undefined;
|
|
511
|
+
const keepForDebug = keepReason !== undefined && anyRoleFailed;
|
|
512
|
+
if (keepForDebug) {
|
|
513
|
+
warnings.push(`Sandbox ${desktop.sandboxId} kept for debugging (${keepReason} on failure); reclaim it via E2B or it will be killed on its server-side timeout.`);
|
|
514
|
+
}
|
|
515
|
+
else if (typeof desktopModule.Sandbox.kill === "function") {
|
|
516
|
+
try {
|
|
517
|
+
await desktopModule.Sandbox.kill(desktop.sandboxId, { requestTimeoutMs: 60_000 });
|
|
518
|
+
killed = true;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
warnings.push(`Sandbox teardown failed (server-side kill-on-timeout will reclaim it): ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
warnings.push("Installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox.");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Subject provenance (invariant 5): the ONE shared plane. Local-tree carries archiveSha256 (the
|
|
531
|
+
// pin - one archive, so no per-lane unanimity math is needed, unlike the cua fan-out route) plus
|
|
532
|
+
// the host-side commit/dirty when the packed root was a git work tree; clone carries repo/commit
|
|
533
|
+
// as before. Mirrors laneSubjectProjection's local-tree branch in cua-actor-lab.ts.
|
|
534
|
+
const subjectState = resolveSubjectState({
|
|
535
|
+
declared: config.subject.state,
|
|
536
|
+
dryRun,
|
|
537
|
+
executed: stateStepRecords
|
|
538
|
+
});
|
|
539
|
+
const planeCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
|
|
540
|
+
const subject = localTreeRoute
|
|
541
|
+
? {
|
|
542
|
+
source: "local-tree",
|
|
543
|
+
...(localTreeArchive === undefined ? {} : { archiveSha256: localTreeArchive.archiveSha256 }),
|
|
544
|
+
...(planeCommit === undefined ? {} : { commit: planeCommit }),
|
|
545
|
+
...(localTreeArchive?.git === undefined ? {} : { dirty: localTreeArchive.git.dirty }),
|
|
546
|
+
envNames: subjectEnvNames,
|
|
547
|
+
state: subjectState
|
|
548
|
+
}
|
|
549
|
+
: {
|
|
550
|
+
source: "clone",
|
|
551
|
+
repo: publicRepo,
|
|
552
|
+
...(subjectCommit === undefined ? {} : { commit: subjectCommit }),
|
|
553
|
+
envNames: subjectEnvNames,
|
|
554
|
+
state: subjectState
|
|
555
|
+
};
|
|
556
|
+
const bundle = buildSharedWorldBundle({
|
|
557
|
+
config,
|
|
558
|
+
descriptor,
|
|
559
|
+
createdAt,
|
|
560
|
+
dryRun,
|
|
561
|
+
runId,
|
|
562
|
+
source,
|
|
563
|
+
roleSpecs,
|
|
564
|
+
roleOutcomes,
|
|
565
|
+
baselineCheckpoint,
|
|
566
|
+
subject,
|
|
567
|
+
sandboxResolution,
|
|
568
|
+
sandboxPreset,
|
|
569
|
+
seedDigest: seedRecipeDigest(config),
|
|
570
|
+
...(planeCommit === undefined ? {} : { subjectCommit: planeCommit }),
|
|
571
|
+
...(failFastReason === undefined ? {} : { failFastReason })
|
|
572
|
+
});
|
|
573
|
+
const adapterWarnings = [];
|
|
574
|
+
await applyBrowserAdapterHooks({
|
|
575
|
+
hooks,
|
|
576
|
+
bundle,
|
|
577
|
+
context: {
|
|
578
|
+
bundle,
|
|
579
|
+
runDir: artifactRoot,
|
|
580
|
+
labId: config.id,
|
|
581
|
+
runId,
|
|
582
|
+
actor: descriptor.id,
|
|
583
|
+
backend: "shared-world",
|
|
584
|
+
dryRun,
|
|
585
|
+
laneCount: roleSpecs.length
|
|
586
|
+
},
|
|
587
|
+
sanitize: (text) => redactText(scrubKnownValues(text)),
|
|
588
|
+
warnings: adapterWarnings,
|
|
589
|
+
hookLabel: "sharedWorldHooks"
|
|
590
|
+
});
|
|
591
|
+
await writeFile(path.join(artifactRoot, "run.json"), `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
|
|
592
|
+
await writeFile(path.join(artifactRoot, "review.json"), `${JSON.stringify(bundle.review, null, 2)}\n`, "utf8");
|
|
593
|
+
await writeFile(path.join(artifactRoot, "review.md"), renderSharedWorldReviewMarkdown(bundle), "utf8");
|
|
594
|
+
await writeFile(path.join(artifactRoot, "events.ndjson"), `${bundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
595
|
+
await writeFile(path.join(cwd, ".humanish", "runs", "latest.json"), `${JSON.stringify({ schema: "humanish.latest-run.v1", runId, path: path.join(".humanish", "runs", runId), updatedAt: createdAt }, null, 2)}\n`, "utf8");
|
|
596
|
+
const observer = await render(cwd, runId, { open: options.open === true });
|
|
597
|
+
const roleOk = (outcome) => {
|
|
598
|
+
return sharedWorldRoleOutcomeOk(outcome, dryRun);
|
|
599
|
+
};
|
|
600
|
+
const allRolesOk = roleSpecs.every((_, index) => roleOk(roleOutcomes[index]));
|
|
601
|
+
const adapterFailure = adapterScoreFailureMessage(bundle);
|
|
602
|
+
const ok = observer.ok && allRolesOk && failFastReason === undefined && adapterFailure === undefined;
|
|
603
|
+
const allWarnings = [...warnings, ...adapterWarnings, ...observer.warnings];
|
|
604
|
+
const roles = roleSpecs.map((spec, index) => {
|
|
605
|
+
const outcome = roleOutcomes[index];
|
|
606
|
+
const base = { id: spec.roleId, index: spec.roleIndex + 1, persona: spec.persona.id, profileDir: spec.profileDir };
|
|
607
|
+
if (dryRun || !outcome) {
|
|
608
|
+
return { ...base, status: "contract_proof_only", ok: dryRun };
|
|
609
|
+
}
|
|
610
|
+
if (outcome.skippedReason !== undefined) {
|
|
611
|
+
return {
|
|
612
|
+
...base,
|
|
613
|
+
status: "blocked",
|
|
614
|
+
ok: false,
|
|
615
|
+
skippedReason: outcome.skippedReason,
|
|
616
|
+
error: { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: outcome.skippedReason }
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const session = outcome.session;
|
|
620
|
+
const thisOk = roleOk(outcome);
|
|
621
|
+
return {
|
|
622
|
+
...base,
|
|
623
|
+
status: session ? session.status : "failed",
|
|
624
|
+
ok: thisOk,
|
|
625
|
+
...(session
|
|
626
|
+
? { session: { status: session.status, completionReason: session.completionReason, reason: session.reason, screenshots: outcome.screenshots.length } }
|
|
627
|
+
: {}),
|
|
628
|
+
...(thisOk
|
|
629
|
+
? {}
|
|
630
|
+
: {
|
|
631
|
+
error: {
|
|
632
|
+
code: "HUMANISH_SHARED_WORLD_LAB_FAILED",
|
|
633
|
+
message: outcome.sessionError
|
|
634
|
+
?? (outcome.noEngagement
|
|
635
|
+
? "Role took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
|
|
636
|
+
: session?.completionReason === "harness_error"
|
|
637
|
+
? `Role seat ended with a harness error: ${session.reason}`
|
|
638
|
+
: "Role did not produce a terminal session.")
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
const sequence = roleOutcomes
|
|
644
|
+
.filter((outcome) => outcome.skippedReason === undefined && outcome.afterCheckpoint !== undefined)
|
|
645
|
+
.map((outcome) => outcome.spec.roleId);
|
|
646
|
+
const errorResult = (() => {
|
|
647
|
+
if (ok)
|
|
648
|
+
return undefined;
|
|
649
|
+
if (!observer.ok) {
|
|
650
|
+
return { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: observer.error?.message ?? "Observer failed for the shared-world run." };
|
|
651
|
+
}
|
|
652
|
+
if (adapterFailure !== undefined) {
|
|
653
|
+
return { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: adapterFailure };
|
|
654
|
+
}
|
|
655
|
+
const passed = roles.filter((role) => role.ok).length;
|
|
656
|
+
return {
|
|
657
|
+
code: "HUMANISH_SHARED_WORLD_LAB_FAILED",
|
|
658
|
+
message: `Shared-world run failed: ${passed}/${roleCount} role(s) passed${failFastReason ? ` (fail-fast: ${failFastReason})` : ""}.`
|
|
659
|
+
};
|
|
660
|
+
})();
|
|
661
|
+
return {
|
|
662
|
+
schema: SHARED_WORLD_LAB_SCHEMA,
|
|
663
|
+
ok,
|
|
664
|
+
cwd,
|
|
665
|
+
labId: config.id,
|
|
666
|
+
actor: descriptor.id,
|
|
667
|
+
topology: "shared-world",
|
|
668
|
+
roleCount,
|
|
669
|
+
sequence,
|
|
670
|
+
dryRun,
|
|
671
|
+
runId,
|
|
672
|
+
...(sandboxId === undefined ? {} : { sandbox: { sandboxId, killed } }),
|
|
673
|
+
subject,
|
|
674
|
+
roles,
|
|
675
|
+
observer,
|
|
676
|
+
warnings: allWarnings,
|
|
677
|
+
...(errorResult === undefined ? {} : { error: errorResult })
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
/** Project the shared-world run into a humanish.run-bundle.v1 with the sharedWorld evidence block. */
|
|
681
|
+
export function buildSharedWorldBundle(args) {
|
|
682
|
+
const { config, descriptor, createdAt, dryRun, roleSpecs, roleOutcomes } = args;
|
|
683
|
+
const simulations = [];
|
|
684
|
+
const streams = [];
|
|
685
|
+
const events = [];
|
|
686
|
+
const appUrl = config.subject.serve?.url ?? "";
|
|
687
|
+
events.push({
|
|
688
|
+
id: "event-000-created",
|
|
689
|
+
at: createdAt,
|
|
690
|
+
level: "info",
|
|
691
|
+
type: "shared-world.run.created",
|
|
692
|
+
message: `Created shared-world run for ${config.id} (actor ${descriptor.id}, ${roleSpecs.length} role(s), ONE shared plane, sequential turns).`
|
|
693
|
+
});
|
|
694
|
+
// Human-readable plane label, byte-stable for the clone route: "clone of <repo>[@<commit>]".
|
|
695
|
+
// The local-tree route has no repo slug, so it labels the packed archive instead (archiveSha256
|
|
696
|
+
// + dirty/clean when the packed root was a git work tree).
|
|
697
|
+
const dryRunPlaneLabel = args.subject.source === "local-tree"
|
|
698
|
+
? "packed working tree"
|
|
699
|
+
: `clone of ${args.subject.repo}`;
|
|
700
|
+
const livePlaneLabel = args.subject.source === "local-tree"
|
|
701
|
+
? (args.subject.archiveSha256
|
|
702
|
+
? `packed working tree (archiveSha256 ${args.subject.archiveSha256}${args.subject.dirty === true ? ", dirty working tree" : args.subject.dirty === false ? ", clean working tree" : ""})`
|
|
703
|
+
: "packed working tree (archive digest unresolved; provisioning failed before resolution)")
|
|
704
|
+
: `clone of ${args.subject.repo}${args.subjectCommit ? `@${args.subjectCommit}` : ""}`;
|
|
705
|
+
events.push({
|
|
706
|
+
id: "event-001-plane",
|
|
707
|
+
at: createdAt,
|
|
708
|
+
level: "info",
|
|
709
|
+
type: "shared-world.plane.provenance",
|
|
710
|
+
message: dryRun
|
|
711
|
+
? `Shared plane declared: ${dryRunPlaneLabel}, served at ${appUrl} in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
|
|
712
|
+
: `Shared plane: ${livePlaneLabel}, served at ${appUrl} in-sandbox; seed recipe ${args.seedDigest}; env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
|
|
713
|
+
simId: roleSpecs[0]?.simId ?? "sim-001",
|
|
714
|
+
streamId: roleSpecs[0]?.streamId ?? "stream-001"
|
|
715
|
+
});
|
|
716
|
+
let eventSeq = 2;
|
|
717
|
+
const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
|
|
718
|
+
roleSpecs.forEach((spec, index) => {
|
|
719
|
+
const outcome = roleOutcomes[index];
|
|
720
|
+
const session = outcome?.session;
|
|
721
|
+
const screenshots = outcome?.screenshots ?? [];
|
|
722
|
+
const lastScreenshot = screenshots[screenshots.length - 1];
|
|
723
|
+
const status = outcome?.skippedReason !== undefined
|
|
724
|
+
? "blocked"
|
|
725
|
+
: session
|
|
726
|
+
? session.status
|
|
727
|
+
: outcome?.sessionError
|
|
728
|
+
? "failed"
|
|
729
|
+
: "contract_proof_only";
|
|
730
|
+
const reason = outcome?.skippedReason
|
|
731
|
+
?? session?.reason
|
|
732
|
+
?? outcome?.sessionError
|
|
733
|
+
?? "Contract role only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.";
|
|
734
|
+
const traceScreenshotMode = session?.trace.redaction.screenshots;
|
|
735
|
+
const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
|
|
736
|
+
? traceScreenshotMode
|
|
737
|
+
: config.policies?.redactScreenshots === true ? "blurred" : "raw";
|
|
738
|
+
simulations.push({
|
|
739
|
+
id: spec.simId,
|
|
740
|
+
index: index + 1,
|
|
741
|
+
personaId: spec.persona.id,
|
|
742
|
+
scenarioId: `shared-world-${config.id}`,
|
|
743
|
+
status,
|
|
744
|
+
streamKind: "browser",
|
|
745
|
+
mode: "browser-sim",
|
|
746
|
+
progress: 100,
|
|
747
|
+
currentStep: reason,
|
|
748
|
+
summary: session
|
|
749
|
+
? `Role ${spec.roleId} (${spec.persona.id}): drove the shared app; ${session.completionReason}.`
|
|
750
|
+
: outcome?.skippedReason !== undefined
|
|
751
|
+
? `Role ${spec.roleId} ${outcome.skippedReason}.`
|
|
752
|
+
: outcome?.sessionError
|
|
753
|
+
? `Role ${spec.roleId} failed before a terminal session verdict: ${outcome.sessionError}`
|
|
754
|
+
: `Contract role ${spec.roleId} (${spec.persona.id}) for ${descriptor.id} against the shared plane at ${appUrl}.`,
|
|
755
|
+
streamIds: [spec.streamId],
|
|
756
|
+
startedAt: createdAt,
|
|
757
|
+
updatedAt: createdAt
|
|
758
|
+
});
|
|
759
|
+
streams.push({
|
|
760
|
+
id: spec.streamId,
|
|
761
|
+
simId: spec.simId,
|
|
762
|
+
kind: "browser",
|
|
763
|
+
label: `Shared-world role ${spec.roleId} — ${config.id}`,
|
|
764
|
+
status,
|
|
765
|
+
transport: "snapshot",
|
|
766
|
+
updatedAt: createdAt,
|
|
767
|
+
embed: lastScreenshot
|
|
768
|
+
? { kind: "screenshot", url: lastScreenshot, title: `Shared desktop, role ${spec.roleId} (${screenshotMode})` }
|
|
769
|
+
: { kind: "placeholder", title: `Shared desktop, role ${spec.roleId}` },
|
|
770
|
+
viewport: {
|
|
771
|
+
width: args.sandboxResolution[0],
|
|
772
|
+
height: args.sandboxResolution[1],
|
|
773
|
+
deviceScaleFactor: args.sandboxPreset.deviceScaleFactor,
|
|
774
|
+
isMobile: args.sandboxPreset.isMobile
|
|
775
|
+
},
|
|
776
|
+
ui: {
|
|
777
|
+
route: spec.seatUrl,
|
|
778
|
+
intent: `Watch role ${spec.roleId} (${spec.persona.id}) drive the SHARED app (one plane; sequential turn).`,
|
|
779
|
+
state: reason,
|
|
780
|
+
...(session ? { actorStatus: session.status } : {}),
|
|
781
|
+
...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
|
|
782
|
+
},
|
|
783
|
+
...(session ? { actor: session.trace } : {}),
|
|
784
|
+
artifacts: [
|
|
785
|
+
{ label: "run bundle", path: "run.json", kind: "bundle" },
|
|
786
|
+
{ label: "review", path: "review.md", kind: "review" },
|
|
787
|
+
{ label: "events", path: "events.ndjson", kind: "events" },
|
|
788
|
+
...(session
|
|
789
|
+
? [{ label: `role ${spec.roleId} actor trace`, path: spec.traceArtifactPath, kind: "trace" }]
|
|
790
|
+
: []),
|
|
791
|
+
...screenshots.map((screenshot, screenshotIndex) => ({
|
|
792
|
+
label: `role ${spec.roleId} screenshot ${String(screenshotIndex + 1).padStart(2, "0")} (${screenshotMode})`,
|
|
793
|
+
path: screenshot,
|
|
794
|
+
kind: "screenshot"
|
|
795
|
+
}))
|
|
796
|
+
]
|
|
797
|
+
});
|
|
798
|
+
// Per-role session event.
|
|
799
|
+
if (session) {
|
|
800
|
+
events.push({
|
|
801
|
+
id: nextEventId(`session-${spec.roleId}`),
|
|
802
|
+
at: createdAt,
|
|
803
|
+
level: session.status === "passed" ? "info" : "warn",
|
|
804
|
+
type: `shared-world.session.${session.completionReason}`,
|
|
805
|
+
message: `Role ${spec.roleId}: ${session.status} — ${session.reason}`,
|
|
806
|
+
simId: spec.simId,
|
|
807
|
+
streamId: spec.streamId
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
else if (outcome?.skippedReason !== undefined) {
|
|
811
|
+
events.push({
|
|
812
|
+
id: nextEventId(`blocked-${spec.roleId}`),
|
|
813
|
+
at: createdAt,
|
|
814
|
+
level: "warn",
|
|
815
|
+
type: "shared-world.session.blocked",
|
|
816
|
+
message: `Role ${spec.roleId} ${outcome.skippedReason}.`,
|
|
817
|
+
simId: spec.simId,
|
|
818
|
+
streamId: spec.streamId
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
else if (outcome?.sessionError) {
|
|
822
|
+
events.push({
|
|
823
|
+
id: nextEventId(`session-error-${spec.roleId}`),
|
|
824
|
+
at: createdAt,
|
|
825
|
+
level: "error",
|
|
826
|
+
type: "shared-world.session.error",
|
|
827
|
+
message: `Role ${spec.roleId}: ${outcome.sessionError}`,
|
|
828
|
+
simId: spec.simId,
|
|
829
|
+
streamId: spec.streamId
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
events.push({
|
|
834
|
+
id: nextEventId(`contract-${spec.roleId}`),
|
|
835
|
+
at: createdAt,
|
|
836
|
+
level: "info",
|
|
837
|
+
type: "shared-world.contract.ready",
|
|
838
|
+
message: `Role ${spec.roleId}: dry-run contract role ready; switch scenario.mode to live for a real shared-world session.`,
|
|
839
|
+
simId: spec.simId,
|
|
840
|
+
streamId: spec.streamId
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
// Build the shared-world evidence block (the timeline + plane + attribution ceiling).
|
|
845
|
+
const seedDigest = args.seedDigest;
|
|
846
|
+
const planeCommit = dryRun ? undefined : args.subjectCommit;
|
|
847
|
+
const timeline = [args.baselineCheckpoint];
|
|
848
|
+
const sequence = [];
|
|
849
|
+
if (dryRun) {
|
|
850
|
+
// Declared (contract) timeline: every role would take a turn; recipe-digest checkpoints, no delta.
|
|
851
|
+
roleSpecs.forEach((spec) => {
|
|
852
|
+
timeline.push({
|
|
853
|
+
kind: "turn",
|
|
854
|
+
roleId: spec.roleId,
|
|
855
|
+
simId: spec.simId,
|
|
856
|
+
streamId: spec.streamId,
|
|
857
|
+
seedDigest
|
|
858
|
+
});
|
|
859
|
+
timeline.push(declaredCheckpointSnapshot(`cp-after-${spec.roleId}`, config.subject.state?.checkpoint ?? []));
|
|
860
|
+
sequence.push(spec.roleId);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
// Executed timeline: only roles that took a turn (skipped roles contribute nothing).
|
|
865
|
+
for (const outcome of roleOutcomes) {
|
|
866
|
+
if (outcome.skippedReason !== undefined || outcome.afterCheckpoint === undefined) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
timeline.push({
|
|
870
|
+
kind: "turn",
|
|
871
|
+
roleId: outcome.spec.roleId,
|
|
872
|
+
simId: outcome.spec.simId,
|
|
873
|
+
streamId: outcome.spec.streamId,
|
|
874
|
+
...(planeCommit === undefined ? {} : { commit: planeCommit }),
|
|
875
|
+
seedDigest
|
|
876
|
+
});
|
|
877
|
+
timeline.push(outcome.afterCheckpoint);
|
|
878
|
+
sequence.push(outcome.spec.roleId);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const sharedWorld = {
|
|
882
|
+
schema: SHARED_WORLD_SCHEMA,
|
|
883
|
+
topology: "shared-world",
|
|
884
|
+
topologyMode: "sequential",
|
|
885
|
+
roleCount: roleSpecs.length,
|
|
886
|
+
plane: {
|
|
887
|
+
...(planeCommit === undefined ? {} : { commit: planeCommit }),
|
|
888
|
+
seedDigest,
|
|
889
|
+
envNames: args.subject.envNames ?? []
|
|
890
|
+
},
|
|
891
|
+
sequence,
|
|
892
|
+
timeline,
|
|
893
|
+
attributionLimits: ["sequential-only", "no-concurrent-races", "delta-attributed-to-turn-not-action"]
|
|
894
|
+
};
|
|
895
|
+
events.push({
|
|
896
|
+
id: nextEventId("timeline"),
|
|
897
|
+
at: createdAt,
|
|
898
|
+
level: "info",
|
|
899
|
+
type: "shared-world.timeline",
|
|
900
|
+
message: `Interaction timeline: ${timeline.length} entries (${sequence.length} turn(s) interleaved with checkpoints); deltas observed on ${timeline.filter((entry) => entry.kind === "checkpoint" && entry.deltaFromPrev).map((entry) => entry.name).join(", ") || "none"}. Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}.`
|
|
901
|
+
});
|
|
902
|
+
if (args.failFastReason) {
|
|
903
|
+
events.push({
|
|
904
|
+
id: nextEventId("fail-fast"),
|
|
905
|
+
at: createdAt,
|
|
906
|
+
level: "warn",
|
|
907
|
+
type: "shared-world.fail-fast",
|
|
908
|
+
message: `Fail-fast: ${args.failFastReason}. Remaining roles were blocked; completed evidence is retained.`
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
// Worst-of review verdict across roles.
|
|
912
|
+
const verdict = dryRun
|
|
913
|
+
? "contract_proof_only"
|
|
914
|
+
: (() => {
|
|
915
|
+
const allPassed = roleOutcomes.length === roleSpecs.length
|
|
916
|
+
&& roleOutcomes.every((outcome) => sharedWorldRoleOutcomeOk(outcome, false));
|
|
917
|
+
if (allPassed)
|
|
918
|
+
return "pass";
|
|
919
|
+
const anyFail = roleOutcomes.some((outcome) => outcome.skippedReason !== undefined
|
|
920
|
+
|| outcome.harnessError
|
|
921
|
+
|| outcome.noEngagement
|
|
922
|
+
|| outcome.sessionError !== undefined
|
|
923
|
+
|| outcome.session === undefined
|
|
924
|
+
|| outcome.session.status === "failed"
|
|
925
|
+
|| outcome.session.status === "blocked");
|
|
926
|
+
if (anyFail)
|
|
927
|
+
return "fail";
|
|
928
|
+
if (roleOutcomes.some((outcome) => outcome.session?.status === "timed_out"))
|
|
929
|
+
return "timed_out";
|
|
930
|
+
return "fail";
|
|
931
|
+
})();
|
|
932
|
+
const passedRoles = roleOutcomes.filter((outcome) => sharedWorldRoleOutcomeOk(outcome, dryRun)).length;
|
|
933
|
+
const configuredBrowser = config.execution?.desktop?.browser;
|
|
934
|
+
const resolvedBrowsers = roleOutcomes
|
|
935
|
+
.map((outcome) => outcome.desktopBrowser?.resolved)
|
|
936
|
+
.filter((value) => value !== undefined);
|
|
937
|
+
const unanimousResolvedBrowser = resolvedBrowsers.length > 0 && new Set(resolvedBrowsers).size === 1
|
|
938
|
+
? resolvedBrowsers[0]
|
|
939
|
+
: undefined;
|
|
940
|
+
const review = {
|
|
941
|
+
schema: REVIEW_SCHEMA,
|
|
942
|
+
verdict,
|
|
943
|
+
summary: dryRun
|
|
944
|
+
? `Dry-run shared-world contract: ${roleSpecs.length} role(s) declared against ONE plane (${descriptor.id}) at ${appUrl}; no desktop launched, $0 spend.`
|
|
945
|
+
: `Shared-world run (ONE plane, sequential): ${passedRoles}/${roleSpecs.length} role(s) reached a terminal, engaged verdict; ${timeline.filter((entry) => entry.kind === "checkpoint" && entry.deltaFromPrev).length} checkpoint delta(s) observed.`,
|
|
946
|
+
gaps: dryRun
|
|
947
|
+
? ["Live shared-world session not yet run (dry-run contract only)."]
|
|
948
|
+
: roleOutcomes
|
|
949
|
+
.filter((outcome) => outcome.skippedReason !== undefined
|
|
950
|
+
|| outcome.sessionError !== undefined
|
|
951
|
+
|| outcome.noEngagement
|
|
952
|
+
|| outcome.session === undefined
|
|
953
|
+
|| outcome.session.status !== "passed")
|
|
954
|
+
.map((outcome) => `${outcome.spec.roleId}: ${outcome.skippedReason ?? outcome.sessionError ?? outcome.session?.reason ?? "did not pass"}`)
|
|
955
|
+
};
|
|
956
|
+
const anyRaw = roleOutcomes.some((outcome) => outcome.session?.trace.redaction.screenshots === "raw");
|
|
957
|
+
const ranLive = roleOutcomes.some((outcome) => outcome.session !== undefined || outcome.sessionError !== undefined);
|
|
958
|
+
return {
|
|
959
|
+
schema: RUN_BUNDLE_SCHEMA,
|
|
960
|
+
runId: args.runId,
|
|
961
|
+
mode: dryRun ? "dry-run" : "live",
|
|
962
|
+
simCount: roleSpecs.length,
|
|
963
|
+
createdAt,
|
|
964
|
+
cwd: PUBLIC_TARGET_CWD,
|
|
965
|
+
artifactRoot: path.join(".humanish", "runs", args.runId),
|
|
966
|
+
source: args.source,
|
|
967
|
+
persona: {
|
|
968
|
+
id: roleSpecs[0]?.persona.id ?? "shared-world-role",
|
|
969
|
+
name: `Shared-world roster (${roleSpecs.length} roles)`,
|
|
970
|
+
source: `lab:${config.id}`,
|
|
971
|
+
sourceDigest: roleSpecs[0]?.persona.promptDigest ?? seedDigest
|
|
972
|
+
},
|
|
973
|
+
scenario: {
|
|
974
|
+
id: `shared-world-${config.id}`,
|
|
975
|
+
title: config.title ?? `Shared-world: ${config.id}`,
|
|
976
|
+
goal: roleSpecs[0]?.instructions ?? "Shared-world sequential interaction.",
|
|
977
|
+
source: `lab:${config.id}`,
|
|
978
|
+
sourceDigest: roleSpecs[0]?.persona.promptDigest ?? seedDigest
|
|
979
|
+
},
|
|
980
|
+
lifecycle: [
|
|
981
|
+
{
|
|
982
|
+
at: createdAt,
|
|
983
|
+
event: "shared-world.run.created",
|
|
984
|
+
message: `Created shared-world run with ONE shared plane and ${roleSpecs.length} sequential role seats (actor ${descriptor.id}).`
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
simulations,
|
|
988
|
+
streams,
|
|
989
|
+
events,
|
|
990
|
+
redaction: {
|
|
991
|
+
status: "passed",
|
|
992
|
+
notes: ranLive
|
|
993
|
+
? anyRaw
|
|
994
|
+
? "Typed text recorded as length only and reasoning/messages pass through text redaction. Some roles captured FULL-FIDELITY (raw) screenshots, retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle. Checkpoints persist digest-only."
|
|
995
|
+
: "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are blurred at capture (policies.redactScreenshots: true) for a share-as-is bundle. Checkpoints persist digest-only."
|
|
996
|
+
: "Dry-run shared-world contract bundle: no desktop launched and no screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs. Checkpoints persist digest-only."
|
|
997
|
+
},
|
|
998
|
+
artifacts: {
|
|
999
|
+
run: "run.json",
|
|
1000
|
+
reviewJson: "review.json",
|
|
1001
|
+
reviewMarkdown: "review.md",
|
|
1002
|
+
observerData: "observer/observer-data.json",
|
|
1003
|
+
events: "events.ndjson"
|
|
1004
|
+
},
|
|
1005
|
+
review,
|
|
1006
|
+
feedbackCandidates: [],
|
|
1007
|
+
// Custom desktop image provenance (the ONE shared plane launched on it); omitted on the default.
|
|
1008
|
+
...(config.execution?.desktop?.template === undefined ? {} : { desktopTemplate: config.execution.desktop.template }),
|
|
1009
|
+
...(configuredBrowser === undefined
|
|
1010
|
+
? {}
|
|
1011
|
+
: { desktopBrowser: { requested: configuredBrowser, ...(unanimousResolvedBrowser === undefined ? {} : { resolved: unanimousResolvedBrowser }) } }),
|
|
1012
|
+
subject: args.subject,
|
|
1013
|
+
attributionClass: "shared-world",
|
|
1014
|
+
sharedWorld
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function sharedWorldRoleOutcomeOk(outcome, dryRun) {
|
|
1018
|
+
if (dryRun)
|
|
1019
|
+
return true;
|
|
1020
|
+
if (!outcome || outcome.skippedReason !== undefined)
|
|
1021
|
+
return false;
|
|
1022
|
+
return outcome.session !== undefined
|
|
1023
|
+
&& outcome.session.status === "passed"
|
|
1024
|
+
&& outcome.session.completionReason !== "harness_error"
|
|
1025
|
+
&& outcome.sessionError === undefined
|
|
1026
|
+
&& !outcome.noEngagement;
|
|
1027
|
+
}
|
|
1028
|
+
function renderSharedWorldReviewMarkdown(bundle) {
|
|
1029
|
+
const plane = bundle.events.find((event) => event.type === "shared-world.plane.provenance");
|
|
1030
|
+
const timeline = bundle.events.find((event) => event.type === "shared-world.timeline");
|
|
1031
|
+
return [
|
|
1032
|
+
`# ${bundle.scenario.title}`,
|
|
1033
|
+
"",
|
|
1034
|
+
`- run: ${bundle.runId}`,
|
|
1035
|
+
`- mode: ${bundle.mode}`,
|
|
1036
|
+
`- attribution class: ${bundle.attributionClass ?? "isolated"}`,
|
|
1037
|
+
`- topology: ${bundle.sharedWorld?.topology ?? "(none)"}`,
|
|
1038
|
+
`- roles: ${bundle.sharedWorld?.roleCount ?? 0}; sequence: ${(bundle.sharedWorld?.sequence ?? []).join(" → ") || "(none)"}`,
|
|
1039
|
+
`- verdict: ${bundle.review.verdict}`,
|
|
1040
|
+
`- summary: ${bundle.review.summary}`,
|
|
1041
|
+
...(plane ? [`- plane: ${plane.message}`] : []),
|
|
1042
|
+
...(timeline ? [`- timeline: ${timeline.message}`] : []),
|
|
1043
|
+
...(bundle.sharedWorld ? [`- attribution limits: ${bundle.sharedWorld.attributionLimits.join(", ")}`] : []),
|
|
1044
|
+
...(bundle.review.gaps.length > 0 ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)] : []),
|
|
1045
|
+
""
|
|
1046
|
+
].join("\n");
|
|
1047
|
+
}
|
|
1048
|
+
//# sourceMappingURL=shared-world-lab.js.map
|