humanish 0.19.1 → 0.20.1

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.
@@ -41,7 +41,7 @@ import { toErrorMessage } from "./command-failure.js";
41
41
  import { mapWithConcurrency } from "./concurrency.js";
42
42
  import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
43
43
  import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
44
- import { concurrentSharedWorldValidationReason } from "./lab-config.js";
44
+ import { concurrentSharedWorldValidationReason, externalPublicSharedWorldValidationReason } from "./lab-config.js";
45
45
  import { buildObserverData } from "./observer-data.js";
46
46
  import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
47
47
  import { redactText } from "./redaction.js";
@@ -69,6 +69,208 @@ const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
69
69
  const DEFAULT_STATE_STEP_TIMEOUT_MS = 5 * 60_000;
70
70
  const DEFAULT_PROBER_CADENCE_MS = 1000;
71
71
  const DEFAULT_MISSION = "You are one of MANY users hitting a shared web application at the same time. The browser is already open at the app. Accomplish your role's task, then stop.";
72
+ // EXTERNAL-PUBLIC plane class: the honest-downgrade attribution ceiling. The concurrent family
73
+ // (an honest ceiling) PLUS the mandatory external-public disclosures — mirrored in run.ts's required
74
+ // set (CONCURRENT_ATTRIBUTION_LIMITS + EXTERNAL_PUBLIC_EXTRA_LIMITS). Verify fails closed on a missing one.
75
+ export const EXTERNAL_PUBLIC_ATTRIBUTION_LIMITS = [
76
+ ...CONCURRENT_ATTRIBUTION_LIMITS,
77
+ "external-public-plane",
78
+ "operator-attested-target-not-harness-controlled",
79
+ "no-synthetic-attestation",
80
+ "no-authoritative-shared-state-proof",
81
+ "concurrency-by-temporal-co-occupancy-only"
82
+ ];
83
+ // The FLOOR for the host-first handoff barrier deadline (ms). The host seat must surface a
84
+ // shared-session (/lobby/CODE) URL within the deadline or the run fails closed and no follower
85
+ // opens. The effective deadline SCALES with the per-seat run budget (execution.timeoutMs): a fixed
86
+ // 2 min is too tight for a real create-a-lobby flow on a mobile-layout seat once you subtract the
87
+ // seat's own desktop provisioning — the host reaches /lobby/CODE, but after the followers already
88
+ // gave up. So use max(FLOOR, 40% of the budget), capped at the budget. The latch resolves the
89
+ // instant the host actually reaches /lobby, so a generous ceiling only affects the fail-closed case.
90
+ const DEFAULT_HANDOFF_DEADLINE_MS = 120_000;
91
+ const HANDOFF_DEADLINE_BUDGET_FRACTION = 0.4;
92
+ // Runaway backstop for the vision-off-frame handoff relay: at most this many single-frame reads before
93
+ // the host is assumed to be somewhere without a code. The relay stops the instant any path latches, so
94
+ // in practice only a handful fire (the host reaches /lobby within a few turns). NOTE: these reads are
95
+ // out-of-band OpenAI calls (host-lane setup, external-public route only) and are NOT counted against
96
+ // execution.caps.maxUsd — this hard cap is what bounds their spend instead (each read is one cheap
97
+ // single-frame OCR call). If this route ever runs under a strict budget, fold the estimate in.
98
+ const MAX_HOST_VISION_READS = 30;
99
+ // Idle/no-progress backstop for the HOST lane specifically (default is 6/8). The host legitimately sits
100
+ // on an unchanging waiting-room screen while followers provision and join; it must not give up first.
101
+ const HOST_WAIT_IDLE_STEPS = 80;
102
+ const FOLLOWER_WAIT_IDLE_STEPS = 40;
103
+ /**
104
+ * The cineguessr (and general "/lobby/CODE") shared-session URL matcher. A code is exactly 6 chars of
105
+ * the [A-Z2-9] class; a locale prefix (/en/lobby/…) and a query/hash suffix are tolerated. RUNTIME-ONLY
106
+ * input (a live location.href); only the extracted CODE is used, and it lands only as a digest.
107
+ */
108
+ export const LOBBY_CODE_PATTERN = /\/lobby\/([A-Z2-9]{6})(?:$|[/?#])/;
109
+ /** Extract the shared-session CODE from a (runtime-only) observed URL, or undefined. Exported for the
110
+ * handoff regex table test — pure, no side effects, never persists its input. */
111
+ export function extractLobbyCode(url) {
112
+ if (typeof url !== "string")
113
+ return undefined;
114
+ const match = url.match(LOBBY_CODE_PATTERN);
115
+ return match ? match[1] : undefined;
116
+ }
117
+ /**
118
+ * Extract a lobby CODE from free-form ACTOR NARRATION (the host's reasoning/message where it states
119
+ * the lobby URL it sees), where the /lobby/CODE is followed by arbitrary prose (a space, backtick,
120
+ * newline) rather than end-of-string or /?# — so the strict LOBBY_CODE_PATTERN would miss it. Uses a
121
+ * negative-lookahead boundary (exactly 6 code chars). This is the CDP-INDEPENDENT handoff path: the
122
+ * host reads the code on screen and states it, and this reads it from the model's own text. Pure;
123
+ * input is runtime-only; only the code is used (as a digest).
124
+ */
125
+ const LOBBY_CODE_IN_TEXT = /\/lobby\/([A-Z2-9]{6})(?![A-Z2-9])/;
126
+ export function extractLobbyCodeFromNarration(text) {
127
+ if (typeof text !== "string")
128
+ return undefined;
129
+ const inUrl = text.match(LOBBY_CODE_IN_TEXT);
130
+ if (inUrl)
131
+ return inUrl[1];
132
+ // Fallback: an explicitly-labeled bare code (e.g. "LOBBY_CODE=ABC123"), which the host may state
133
+ // if it copied the code rather than the URL. The label is matched case-insensitively, but the CODE
134
+ // itself must be UPPERCASE [A-Z2-9] — a real lobby code always renders uppercase, whereas an /i match
135
+ // on the code class would also grab an ordinary lowercase word after "lobby code " (e.g. "the lobby
136
+ // code screen") and latch a WRONG code. Precision-first, matching parseLobbyCodeReply's rationale.
137
+ const labeled = text.match(/lobby[ _-]?code[=:\s]+([A-Z2-9]{6})(?![A-Za-z2-9])/i);
138
+ return labeled && labeled[1] && /^[A-Z2-9]{6}$/.test(labeled[1]) ? labeled[1] : undefined;
139
+ }
140
+ /** Pull the assistant's plain text out of an OpenAI Responses API body (`output_text` convenience
141
+ * field, else the concatenated `output[].content[].text`). Tolerant of shape drift; pure. */
142
+ export function extractResponsesOutputText(parsed) {
143
+ if (typeof parsed !== "object" || parsed === null)
144
+ return undefined;
145
+ const obj = parsed;
146
+ if (typeof obj.output_text === "string" && obj.output_text.length > 0)
147
+ return obj.output_text;
148
+ const out = obj.output;
149
+ if (!Array.isArray(out))
150
+ return undefined;
151
+ const parts = [];
152
+ for (const item of out) {
153
+ if (typeof item !== "object" || item === null)
154
+ continue;
155
+ const content = item.content;
156
+ if (!Array.isArray(content))
157
+ continue;
158
+ for (const chunk of content) {
159
+ if (typeof chunk === "object" && chunk !== null && typeof chunk.text === "string") {
160
+ parts.push(chunk.text);
161
+ }
162
+ }
163
+ }
164
+ return parts.length > 0 ? parts.join("\n") : undefined;
165
+ }
166
+ /** Parse a vision reply into a lobby CODE. PRECISION-FIRST: accept ONLY when the whole reply IS the
167
+ * six-character code, or when it echoes an explicit /lobby/CODE — never a bare 6-letter token buried in
168
+ * prose (e.g. "I see a home SCREEN"), because a wrong latch fails the entire run, whereas a miss just
169
+ * retries on the next frame while the host keeps waiting. NONE (the instructed "no code" reply) is
170
+ * rejected. Pure. */
171
+ export function parseLobbyCodeReply(reply) {
172
+ if (typeof reply !== "string")
173
+ return undefined;
174
+ const up = reply.trim().toUpperCase();
175
+ if (up.length === 0 || /\bNONE\b/.test(up))
176
+ return undefined;
177
+ if (/^[A-Z2-9]{6}$/.test(up))
178
+ return up; // the well-behaved "code only" reply
179
+ const inUrl = up.match(/\/LOBBY\/([A-Z2-9]{6})(?![A-Z2-9])/); // model echoed the invite link
180
+ return inUrl ? inUrl[1] : undefined;
181
+ }
182
+ const LOBBY_CODE_VISION_PROMPT = "This is a screenshot of a CineGuessr multiplayer lobby. If a waiting-room / invite screen is " +
183
+ "shown, read the 6-character lobby code (characters A-Z and 2-9 only) — it appears near a 'lobby " +
184
+ "code'/'room code' label or inside an invite link of the form /lobby/CODE. Your entire reply MUST be " +
185
+ "exactly those 6 characters in uppercase and NOTHING else (no words, no punctuation). If no lobby " +
186
+ "code is visible on this screen (e.g. it is the home screen or a game round), reply exactly NONE.";
187
+ const LOBBY_CODE_VISION_ENDPOINT = "https://api.openai.com/v1/responses";
188
+ // A single-frame OCR-style read. gpt-5.5 (the CU default) is used deliberately: it reliably reads the
189
+ // 6-char code off a dense MOBILE-viewport waiting room — a smaller/cheaper model (gpt-4.1-mini) was
190
+ // tried and could NOT read it. reasoning.effort stays "low" (minimal) and the output budget is small
191
+ // but comfortably clear of the "incomplete on reasoning overflow" edge. Kept on the same account/key
192
+ // as the actor; the same full-fidelity frame is already sent to this API by the CU provider, so this
193
+ // adds no new data-exposure surface. See onScreenshot in runHostLane.
194
+ const LOBBY_CODE_VISION_MODEL = "gpt-5.5";
195
+ // Output-token budget for the read. The answer is 6 chars, but leave clear margin over any low-effort
196
+ // reasoning tokens so the response never comes back status:"incomplete" with empty output.
197
+ const LOBBY_CODE_VISION_MAX_OUTPUT_TOKENS = 64;
198
+ // Per-read wall-clock cap. Without it a stalled fetch (Node fetch has no default timeout) would leave
199
+ // visionInFlight pinned true and silently kill the relay for the rest of the host run.
200
+ const LOBBY_CODE_VISION_TIMEOUT_MS = 15_000;
201
+ /**
202
+ * Vision-read a lobby CODE straight off a host waiting-room FRAME (the robust, CDP-independent handoff
203
+ * relay). Fail-soft: any network/HTTP/parse problem returns undefined so the caller simply retries on
204
+ * the next frame. The frame is runtime-only; only the extracted code is used (as a digest downstream).
205
+ */
206
+ export async function readLobbyCodeFromFrame(frame, apiKey, options = {}) {
207
+ if (typeof apiKey !== "string" || apiKey.length === 0 || frame.length === 0)
208
+ return undefined;
209
+ const fetchFn = options.fetchFn ?? fetch;
210
+ // Default a wall-clock timeout so a stalled request can't wedge the caller's in-flight guard. An
211
+ // explicit signal (e.g. run abort) takes precedence when provided.
212
+ const signal = options.signal ?? AbortSignal.timeout(LOBBY_CODE_VISION_TIMEOUT_MS);
213
+ const body = {
214
+ model: options.model ?? LOBBY_CODE_VISION_MODEL,
215
+ reasoning: { effort: "low" },
216
+ max_output_tokens: LOBBY_CODE_VISION_MAX_OUTPUT_TOKENS,
217
+ input: [
218
+ {
219
+ role: "user",
220
+ content: [
221
+ { type: "input_text", text: LOBBY_CODE_VISION_PROMPT },
222
+ { type: "input_image", image_url: `data:image/png;base64,${frame.toString("base64")}` }
223
+ ]
224
+ }
225
+ ]
226
+ };
227
+ let res;
228
+ try {
229
+ res = await fetchFn(options.endpoint ?? LOBBY_CODE_VISION_ENDPOINT, {
230
+ method: "POST",
231
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
232
+ body: JSON.stringify(body),
233
+ signal
234
+ });
235
+ }
236
+ catch {
237
+ return undefined; // transient network error: skip this frame, next turn retries
238
+ }
239
+ if (!res.ok)
240
+ return undefined; // never read a non-ok body (it can echo the frame/input)
241
+ let parsed;
242
+ try {
243
+ parsed = await res.json();
244
+ }
245
+ catch {
246
+ return undefined;
247
+ }
248
+ return parseLobbyCodeReply(extractResponsesOutputText(parsed));
249
+ }
250
+ /** A minimal resolve-once latch for the host-first handoff barrier. */
251
+ function deferred() {
252
+ let resolve;
253
+ let reject;
254
+ let done = false;
255
+ const promise = new Promise((res, rej) => {
256
+ resolve = (value) => { if (!done) {
257
+ done = true;
258
+ res(value);
259
+ } };
260
+ reject = (reason) => { if (!done) {
261
+ done = true;
262
+ rej(reason);
263
+ } };
264
+ });
265
+ return { promise, resolve, reject, settled: () => done };
266
+ }
267
+ /** Marker error the host-first barrier rejects with when the deadline elapses (fail-closed). */
268
+ class HandoffTimeoutError extends Error {
269
+ constructor(deadlineMs) {
270
+ super(`the host never produced a /lobby/CODE URL within the ${deadlineMs}ms handoff deadline`);
271
+ this.name = "HandoffTimeoutError";
272
+ }
273
+ }
72
274
  function readPositiveInt(value, fallback) {
73
275
  if (value === undefined)
74
276
  return fallback;
@@ -188,6 +390,35 @@ function buildActorSpec(config, role, index) {
188
390
  traceArtifactPath: `actors/${streamId}.json`
189
391
  };
190
392
  }
393
+ /** Thread the host-yielded lobby CODE into a follower's mission at runtime (external-public route).
394
+ * The CODE flows into the follower's join instruction; it is persisted only as the composed prompt
395
+ * the model reads (never a raw bundle field), and the lab scrubs the CODE from all narration. The
396
+ * follower joins through the real UI (a direct /lobby/CODE visit does not auto-join a non-member). */
397
+ function withLobbyCodeMission(spec, code) {
398
+ return {
399
+ ...spec,
400
+ instructions: `${spec.instructions}\n\nThe multiplayer lobby code is ${code}. On the home screen choose Join, enter this lobby code, enter your name, and submit to join the shared game (do not open a lobby URL directly — go through the Join flow).`
401
+ };
402
+ }
403
+ /** A follower lane that failed closed at the host-first barrier (the host never yielded a /lobby/CODE
404
+ * within the deadline): it NEVER opened a browser, so it carries no session/screenshots — just the
405
+ * handoff-timeout reason. actorLanePassed(...) is false (no session), so it is honestly non-pass. */
406
+ function makeBlockedFollowerOutcome(spec, deadlineMs) {
407
+ return {
408
+ spec,
409
+ sessionError: `handoff barrier: the host never surfaced a /lobby/CODE URL within ${deadlineMs}ms; this follower failed closed WITHOUT opening (no wasted turns).`,
410
+ killed: false,
411
+ streamUrlPresent: false,
412
+ screenshots: [],
413
+ stateStepRecords: [],
414
+ phaseRecords: [],
415
+ warnings: [],
416
+ noEngagement: true,
417
+ selfReportedBlocker: false,
418
+ harnessError: false,
419
+ skippedReason: "handoff-timeout"
420
+ };
421
+ }
191
422
  async function writeConcurrentRunArtifacts(bundle, preparedRunPaths) {
192
423
  const runPaths = await validatePreparedRunArtifactPaths(preparedRunPaths);
193
424
  const publicBundle = {
@@ -253,11 +484,20 @@ export async function runConcurrentSharedWorld(options) {
253
484
  if (!descriptor || !isCuaActorDescriptor(descriptor)) {
254
485
  return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
255
486
  }
256
- // Re-enforce the concurrent cross-validation (library API surface).
257
- const invalidReason = concurrentSharedWorldValidationReason(config);
487
+ // The PLANE-class discriminator (#164 phase 2): an app-url subject is the EXTERNAL-PUBLIC plane (a
488
+ // real operator-owned public deployment used directly as the shared plane — NO getHost, clone,
489
+ // subject sandbox, or seed); everything else is the historical provisioned-getHost plane.
490
+ const planeClass = config.subject.source === "app-url" ? "external-public" : "provisioned-getHost";
491
+ // Re-enforce the cross-validation (library API surface). The external-public branch NEVER touches
492
+ // the getHost synthetic gate — that gate exists because getHost is internet-reachable AND
493
+ // harness-owned; a public site the harness neither provisioned nor exposed has neither property.
494
+ const invalidReason = planeClass === "external-public"
495
+ ? externalPublicSharedWorldValidationReason(config)
496
+ : concurrentSharedWorldValidationReason(config);
258
497
  if (invalidReason) {
259
498
  return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_INVALID", invalidReason, descriptor.id);
260
499
  }
500
+ // provisioned-getHost fields (all absent on the external-public plane — forbidden at validation).
261
501
  const serve = config.subject.serve;
262
502
  const localTreeRoute = config.subject.source === "local-tree";
263
503
  const subjectRepo = config.subject.repos?.[0] ?? "";
@@ -315,6 +555,38 @@ export async function runConcurrentSharedWorld(options) {
315
555
  let snapshotIndex = 0;
316
556
  let liveObserver;
317
557
  const runtimeStreamUrls = [];
558
+ // EXTERNAL-PUBLIC plane state (#164 phase 2). publicAppUrl is the operator-declared shared plane;
559
+ // its ORIGIN is persisted digest-only (publicOriginDigest), never raw (the raw URL + the runtime
560
+ // observed lobby CODE never land — TENSION 3). The latch code is scrubbed from all narration.
561
+ const publicAppUrl = config.subject.appUrl ?? "";
562
+ // The operator-DECLARED origin (from subject.appUrl) — recorded for evidence/reference ONLY. The
563
+ // operator-OWNERSHIP claim rests on the subject.publicTarget.authorized attestation + this declared
564
+ // appUrl, NOT on digest equality (blocker 2): a normal cross-origin redirect (apex->www, http->https;
565
+ // cineguessr.com 307-redirects) makes the seats' OBSERVED origin differ from the declared one, which
566
+ // is expected and MUST NOT fail the run. Persisted digest-only (never the raw origin).
567
+ const declaredOriginDigest = planeClass === "external-public" && publicAppUrl
568
+ ? hostOriginDigest(publicAppUrl)
569
+ : undefined;
570
+ // The OBSERVED convergence origin — computed AFTER fan-out from what the seats ACTUALLY reached (the
571
+ // convergence proof is what the seats OBSERVED, not what was declared). Set iff every observing seat
572
+ // agrees on ONE origin; that agreement IS the convergence proof and becomes plane.publicOriginDigest.
573
+ let publicOriginDigest;
574
+ // Per-lane runtime-only observed state (never persisted raw): the last observed URL and the last
575
+ // observed /lobby/CODE per seat, fed by onObservedUrl. The URL is digested to ORIGIN for each seat's
576
+ // routeHostDigest (no code leaks); the codes drive the cross-seat lobby-convergence digest.
577
+ const observedFinalUrls = new Array(roles.length);
578
+ const observedLobbyCodes = new Array(roles.length);
579
+ let lobbyConvergenceDigest;
580
+ let handoffTimedOut = false;
581
+ // A closure that scrubs the latched lobby CODE from ANY persisted narration once the host resolves
582
+ // it (the 6-char code has no detectable secret shape, so shape-only redaction cannot catch it).
583
+ let latchedLobbyCode;
584
+ const scrubKnownValuesWithLobbyCode = (text) => {
585
+ const base = scrubKnownValues(text);
586
+ return latchedLobbyCode && latchedLobbyCode.length > 0
587
+ ? base.split(latchedLobbyCode).join("[REDACTED_LOBBY_CODE]")
588
+ : base;
589
+ };
318
590
  // Pack the working tree ONCE per run, on the host, BEFORE the subject sandbox is created
319
591
  // (mirrors the sequential route + the cua route's ordering): a packing failure fails the run
320
592
  // closed here, never spending sandbox cost. Dry-run packs nothing.
@@ -337,7 +609,11 @@ export async function runConcurrentSharedWorld(options) {
337
609
  return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
338
610
  }
339
611
  }
340
- if (!dryRun) {
612
+ if (!dryRun && planeClass === "provisioned-getHost") {
613
+ if (!serve) {
614
+ // Defense-in-depth: concurrentSharedWorldValidationReason already required serve above.
615
+ return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_INVALID", "the provisioned-getHost concurrent shared-world route requires `subject.serve`.", descriptor.id);
616
+ }
341
617
  let subjectModule;
342
618
  let subjectDesktop;
343
619
  // Background prober dispose signal (FIX-9: cleared in finally).
@@ -567,16 +843,285 @@ export async function runConcurrentSharedWorld(options) {
567
843
  }
568
844
  }
569
845
  }
570
- const subjectState = resolveSubjectState({ declared: config.subject.state, dryRun, executed: stateStepRecords });
846
+ // EXTERNAL-PUBLIC plane (#164 phase 2): NO subject sandbox, NO getHost, NO prober. The shared plane
847
+ // is the operator-declared public deployment (publicAppUrl); each seat opens it directly and reaches
848
+ // the shared session through the real UI. A host-first barrier extracts the /lobby/CODE from the host
849
+ // seat's CDP-observed URL (onObservedUrl) and threads it into the follower missions; a follower fails
850
+ // closed WITHOUT opening if the host never yields a code within the handoff deadline.
851
+ if (!dryRun && planeClass === "external-public") {
852
+ const cuaHooks = {
853
+ ...(hooks.loadDesktopModule ? { loadDesktopModule: hooks.loadDesktopModule } : {}),
854
+ ...(hooks.detachedTimers ? { detachedTimers: hooks.detachedTimers } : {}),
855
+ ...(hooks.env ? { env: hooks.env } : {}),
856
+ ...(hooks.prepareDesktop ? { prepareDesktop: (desktop) => hooks.prepareDesktop(desktop) } : {}),
857
+ onRuntimeStreamReady: (stream) => {
858
+ runtimeStreamUrls.push({ streamId: stream.streamId, url: stream.url });
859
+ if (liveObserver) {
860
+ attachObserverRuntimeStreamUrls(liveObserver, runtimeStreamUrls);
861
+ }
862
+ }
863
+ };
864
+ const baseActorDeps = {
865
+ config,
866
+ descriptor,
867
+ cloneRoute: false,
868
+ subjectEnvNames: [],
869
+ hasGithubToken: false,
870
+ env,
871
+ openaiApiKey,
872
+ e2bApiKey,
873
+ requestTimeoutMs,
874
+ perLaneSandboxMs: timeoutMs + SANDBOX_TIMEOUT_BUFFER_MS,
875
+ timeoutMs,
876
+ laneCount: roles.length,
877
+ artifactRoot: runPaths,
878
+ redactScreenshots,
879
+ // Scrub the latched lobby CODE (known once the host resolves it) from ALL narration.
880
+ scrubKnownValues: scrubKnownValuesWithLobbyCode,
881
+ runSession,
882
+ now,
883
+ hooks: cuaHooks,
884
+ screenMismatchPolicy: "record-evidence"
885
+ };
886
+ // Publish an attached live Observer BEFORE fan-out (mirrors the provisioned path).
887
+ if (options.onObserverReady) {
888
+ const inProgressBundle = buildConcurrentSharedWorldBundle({
889
+ config,
890
+ descriptor,
891
+ createdAt,
892
+ dryRun: false,
893
+ inProgress: true,
894
+ runId,
895
+ source,
896
+ roles,
897
+ actorSpecs,
898
+ actorResults: [],
899
+ stateSnapshots: [],
900
+ subject: { source: "app-url", envNames: [], state: { provenance: "external-public" } },
901
+ seedDigest,
902
+ planeClass: "external-public",
903
+ // Pre-fan-out snapshot: no seat has observed an origin yet, so the OBSERVED publicOriginDigest
904
+ // is not available; surface the DECLARED origin for the live Observer's reference.
905
+ ...(declaredOriginDigest === undefined ? {} : { declaredOriginDigest })
906
+ });
907
+ await writeConcurrentRunArtifacts(inProgressBundle, runPaths);
908
+ liveObserver = observerResultForConcurrentArtifacts(cwd, runId, artifactRoot, [
909
+ "Live external-public concurrent shared-world Observer is attached before final verification; stream auth URLs are runtime-only and are not persisted."
910
+ ]);
911
+ await options.onObserverReady(liveObserver);
912
+ }
913
+ // The host-first handoff barrier.
914
+ //
915
+ // TEMPORARY SHIM (tracked by #296): this CDP URL-relay handoff — reading the host's /lobby/CODE off
916
+ // its own browser and threading it into the follower missions — is a temporary coordination shim.
917
+ // It is to be augmented/replaced by the actor message bus (faux SMS/email invite) in #297: the
918
+ // human-realistic version is the HOST SENDING the invite link and followers RECEIVING and tapping
919
+ // it, rather than the orchestrator relaying the code out-of-band.
920
+ const lobbyCodeLatch = deferred();
921
+ const handoffDeadlineMs = hooks.handoffDeadlineMs
922
+ ?? Math.min(timeoutMs, Math.max(DEFAULT_HANDOFF_DEADLINE_MS, Math.floor(timeoutMs * HANDOFF_DEADLINE_BUDGET_FRACTION)));
923
+ let deadlineTimer;
924
+ const deadline = new Promise((_resolve, reject) => {
925
+ deadlineTimer = setTimeout(() => reject(new HandoffTimeoutError(handoffDeadlineMs)), handoffDeadlineMs);
926
+ });
927
+ deadline.catch(() => undefined); // never an unhandled rejection
928
+ // Resolve the host->follower handoff latch from WHICHEVER path sees the code first (CDP url-read,
929
+ // host narration, or vision-off-frame). Idempotent: only the first code wins, and it is also stashed
930
+ // as latchedLobbyCode so it gets scrubbed from any later narration. The latched code and observed URLs
931
+ // are runtime-only and land in persisted METADATA only as digests (origin + convergence). (The code
932
+ // is a shareable game code, not a secret, and it still renders in the host's screenshots, which are
933
+ // full-fidelity unless redactScreenshots is set — the digesting is about narration/URL metadata.)
934
+ const latchLobbyCode = (code, laneIndex) => {
935
+ if (latchedLobbyCode !== undefined)
936
+ return;
937
+ observedLobbyCodes[laneIndex] = code;
938
+ latchedLobbyCode = code;
939
+ if (deadlineTimer) {
940
+ clearTimeout(deadlineTimer);
941
+ deadlineTimer = undefined;
942
+ }
943
+ lobbyCodeLatch.resolve(code);
944
+ };
945
+ const makeLaneObservedUrl = (laneIndex, isHost) => (url) => {
946
+ if (typeof url !== "string" || url.length === 0)
947
+ return;
948
+ observedFinalUrls[laneIndex] = url; // runtime-only; digested to origin, never persisted raw
949
+ const code = extractLobbyCode(url);
950
+ if (code !== undefined) {
951
+ observedLobbyCodes[laneIndex] = code;
952
+ if (isHost)
953
+ latchLobbyCode(code, laneIndex);
954
+ }
955
+ };
956
+ // The HOST lane (which yields the /lobby/CODE the followers wait on) runs on its OWN dedicated
957
+ // slot, and the FOLLOWERS run through a bounded pool of size concurrency-1 (blockers 1 & 4):
958
+ // followers block on `Promise.race([lobbyCodeLatch.promise, deadline])` while holding a worker
959
+ // slot, so if the host lane were scheduled INSIDE the same bounded pool it could be starved (never
960
+ // scheduled among the first `concurrency` workers) and the run would die with a spurious
961
+ // HANDOFF_TIMEOUT (e.g. lanes [p2,p3,host] with concurrency 2). Giving the host its own slot,
962
+ // started IMMEDIATELY and OUTSIDE the follower pool, guarantees it is ALWAYS schedulable regardless
963
+ // of its roster position or of concurrency vs lane count — while total in-flight paid desktops stay
964
+ // ≤ the declared concurrency (host + up to concurrency-1 followers), preserving the spend cap.
965
+ const runHostLane = async (spec, laneIndex) => {
966
+ const onObservedUrl = makeLaneObservedUrl(laneIndex, true);
967
+ // CDP-INDEPENDENT handoff paths (the E2B-desktop CDP url-read the onObservedUrl path relies on is
968
+ // unreliable in practice). Two backups, both resolving the SAME latch; whichever sees the code first
969
+ // wins, all digest-only:
970
+ // (1) onMessage — scan the host's own narration IF it happens to state the lobby URL; and
971
+ // (2) onScreenshot — vision-read the code straight off the host's waiting-room frame. This is the
972
+ // robust one: the code is rendered on screen even when CDP fails AND when the host never
973
+ // narrates it, and — crucially — the host is NOT asked to announce anything, so it keeps
974
+ // running (create -> wait for players -> Start -> play) instead of ending on a stray message.
975
+ const onMessage = (text) => {
976
+ if (latchedLobbyCode !== undefined)
977
+ return;
978
+ const code = extractLobbyCodeFromNarration(text);
979
+ if (code !== undefined)
980
+ latchLobbyCode(code, laneIndex);
981
+ };
982
+ let visionInFlight = false;
983
+ let visionReads = 0;
984
+ const onScreenshot = (frame) => {
985
+ // One read at a time, only until latched, and bounded so a host that never reaches a lobby can't
986
+ // rack up unbounded vision calls (~30 cheap single-frame reads is far past when the code appears).
987
+ if (latchedLobbyCode !== undefined || visionInFlight || visionReads >= MAX_HOST_VISION_READS)
988
+ return;
989
+ visionInFlight = true;
990
+ visionReads += 1;
991
+ void readLobbyCodeFromFrame(frame, openaiApiKey)
992
+ .then((code) => {
993
+ if (code !== undefined && latchedLobbyCode === undefined)
994
+ latchLobbyCode(code, laneIndex);
995
+ })
996
+ .catch(() => undefined)
997
+ .finally(() => {
998
+ visionInFlight = false;
999
+ });
1000
+ };
1001
+ // The host's job includes a long LEGITIMATE idle wait — sitting in the waiting room while the
1002
+ // followers provision their own desktops and walk the Join flow (easily 15-30 turns of an
1003
+ // unchanging "waiting for players" screen). At the default idle backstop (6) the host would give up
1004
+ // before anyone arrives, orphaning the lobby (exactly the earlier failure). Raise the host's idle /
1005
+ // no-progress tolerance so it waits patiently; the per-seat timeout still bounds a truly stuck host.
1006
+ const hostSpec = {
1007
+ ...spec,
1008
+ idleSteps: spec.idleSteps ?? HOST_WAIT_IDLE_STEPS,
1009
+ noProgressSteps: spec.noProgressSteps ?? HOST_WAIT_IDLE_STEPS
1010
+ };
1011
+ const startedAt = now();
1012
+ let outcome;
1013
+ try {
1014
+ outcome = await runCuaLane(hostSpec, { ...baseActorDeps, appUrl: publicAppUrl, onObservedUrl, onMessage, onScreenshot });
1015
+ }
1016
+ finally {
1017
+ // If the host finished without ever surfacing a code, release followers to fail closed
1018
+ // immediately rather than wait the full deadline (a no-op if it already resolved).
1019
+ lobbyCodeLatch.reject(new HandoffTimeoutError(handoffDeadlineMs));
1020
+ }
1021
+ const endedAt = now();
1022
+ return { spec, outcome, startedAt, endedAt, route: observedFinalUrls[laneIndex] ?? publicAppUrl };
1023
+ };
1024
+ const runFollowerLane = async (spec, laneIndex) => {
1025
+ const onObservedUrl = makeLaneObservedUrl(laneIndex, false);
1026
+ // FOLLOWER: do NOT compose a mission or open the target until the host yields a lobby code.
1027
+ let code;
1028
+ try {
1029
+ code = await Promise.race([lobbyCodeLatch.promise, deadline]);
1030
+ }
1031
+ catch {
1032
+ // Fail closed WITHOUT opening (no wasted turns against a codeless home page).
1033
+ handoffTimedOut = true;
1034
+ const at = now();
1035
+ return { spec, outcome: makeBlockedFollowerOutcome(spec, handoffDeadlineMs), startedAt: at, endedAt: at, route: publicAppUrl };
1036
+ }
1037
+ // Followers also idle-wait — in the waiting room until the host starts, and between rounds. Raise
1038
+ // their idle backstop too (less than the host's: they wait less), so a follower that joins ahead of
1039
+ // the other does not give up before the game begins. Per-seat timeout still bounds a stuck follower.
1040
+ const followerSpec = {
1041
+ ...withLobbyCodeMission(spec, code),
1042
+ idleSteps: spec.idleSteps ?? FOLLOWER_WAIT_IDLE_STEPS,
1043
+ noProgressSteps: spec.noProgressSteps ?? FOLLOWER_WAIT_IDLE_STEPS
1044
+ };
1045
+ const startedAt = now();
1046
+ const outcome = await runCuaLane(followerSpec, { ...baseActorDeps, appUrl: publicAppUrl, onObservedUrl });
1047
+ const endedAt = now();
1048
+ return { spec, outcome, startedAt, endedAt, route: observedFinalUrls[laneIndex] ?? publicAppUrl };
1049
+ };
1050
+ // Split the roster into the designated host lane and the followers, preserving each follower's
1051
+ // ORIGINAL lane index so results land back in lane order (validation guarantees EXACTLY ONE host).
1052
+ const hostLaneIndex = roles.findIndex((role) => role.host === true);
1053
+ const followerEntries = actorSpecs
1054
+ .map((spec, index) => ({ spec, index }))
1055
+ .filter(({ index }) => index !== hostLaneIndex);
1056
+ const laneResults = new Array(actorSpecs.length);
1057
+ try {
1058
+ const hostPromise = hostLaneIndex >= 0 && actorSpecs[hostLaneIndex] !== undefined
1059
+ ? runHostLane(actorSpecs[hostLaneIndex], hostLaneIndex)
1060
+ : undefined;
1061
+ const followerResultsPromise = mapWithConcurrency(followerEntries, Math.max(1, concurrency - 1), ({ spec, index }) => runFollowerLane(spec, index));
1062
+ const [hostResult, followerResults] = await Promise.all([hostPromise, followerResultsPromise]);
1063
+ if (hostResult !== undefined && hostLaneIndex >= 0) {
1064
+ laneResults[hostLaneIndex] = hostResult;
1065
+ }
1066
+ followerEntries.forEach((entry, i) => { laneResults[entry.index] = followerResults[i]; });
1067
+ actorResults = laneResults;
1068
+ }
1069
+ catch (error) {
1070
+ runError = redactText(scrubKnownValuesWithLobbyCode(toErrorMessage(error)));
1071
+ warnings.push(`External-public concurrent shared-world run failed before completion: ${runError}`);
1072
+ }
1073
+ finally {
1074
+ if (deadlineTimer) {
1075
+ clearTimeout(deadlineTimer);
1076
+ deadlineTimer = undefined;
1077
+ }
1078
+ }
1079
+ // Observed-origin convergence proof (blocker 2): the convergence claim is about what the seats
1080
+ // OBSERVED, not what was DECLARED. Digest each observing seat's origin and require they AGREE on
1081
+ // ONE — that agreement IS the convergence proof and becomes plane.publicOriginDigest. A normal
1082
+ // cross-origin redirect (declared apex -> observed www) is therefore tolerated: the seats still
1083
+ // converge on ONE observed origin. Leave it undefined (verify fails closed) only if the seats did
1084
+ // not converge on a single observed origin (or none observed one).
1085
+ const observedOriginDigests = observedFinalUrls
1086
+ .filter((url) => typeof url === "string" && url.length > 0)
1087
+ .map((url) => hostOriginDigest(url));
1088
+ const distinctObservedOrigins = new Set(observedOriginDigests);
1089
+ publicOriginDigest = distinctObservedOrigins.size === 1
1090
+ ? [...distinctObservedOrigins][0]
1091
+ // NOTHING observed (e.g. a handoff-timeout run where no seat ever navigated): fall back to the
1092
+ // DECLARED origin so a FAILED run's bundle stays structurally valid (every seat's route then
1093
+ // digests to the declared origin too). The run still fails closed for its own reason (HANDOFF_
1094
+ // TIMEOUT / no lobby convergence / no overlap-on-pass). GENUINE divergence (≥2 distinct observed
1095
+ // origins) leaves it undefined so verify fails closed on the non-convergence.
1096
+ : distinctObservedOrigins.size === 0
1097
+ ? declaredOriginDigest
1098
+ : undefined;
1099
+ // Lobby-convergence proof: a digest of the shared /lobby/CODE path iff EVERY seat converged on the
1100
+ // SAME code (a follower stuck on "/" yields no code → no false convergence). Digest-only. NOTE:
1101
+ // observedLobbyCodes may be a SPARSE array (a seat that never observed a code leaves a hole), and
1102
+ // Array.prototype.every SKIPS holes — so count the DEFINED codes explicitly, never rely on every().
1103
+ const definedCodes = observedLobbyCodes.filter((code) => code !== undefined);
1104
+ const distinctCodes = new Set(definedCodes);
1105
+ if (distinctCodes.size === 1 && definedCodes.length === roles.length) {
1106
+ lobbyConvergenceDigest = commandDigestOf(`/lobby/${[...distinctCodes][0]}`);
1107
+ }
1108
+ if (handoffTimedOut && runError === undefined) {
1109
+ runError = `The host seat never produced a /lobby/CODE URL within the ${handoffDeadlineMs}ms handoff deadline; follower seats failed closed without opening.`;
1110
+ }
1111
+ }
1112
+ // Subject provenance: external-public is the operator-declared, operator-owned public deployment
1113
+ // (neither provisioned nor seeded); the provisioned path builds clone/local-tree provenance.
1114
+ const subject = planeClass === "external-public"
1115
+ ? { source: "app-url", envNames: [], state: { provenance: "external-public" } }
1116
+ : buildSubjectProvenance({
1117
+ localTreeRoute,
1118
+ publicRepo,
1119
+ subjectCommit: localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit,
1120
+ localTreeArchive,
1121
+ subjectEnvNames,
1122
+ state: resolveSubjectState({ declared: config.subject.state, dryRun, executed: stateStepRecords })
1123
+ });
571
1124
  const planeCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
572
- const subject = buildSubjectProvenance({
573
- localTreeRoute,
574
- publicRepo,
575
- subjectCommit: planeCommit,
576
- localTreeArchive,
577
- subjectEnvNames,
578
- state: subjectState
579
- });
580
1125
  // Collect per-actor warnings (each lane's own teardown/raw-screenshot notes).
581
1126
  for (const result of actorResults) {
582
1127
  warnings.push(...result.outcome.warnings);
@@ -594,8 +1139,12 @@ export async function runConcurrentSharedWorld(options) {
594
1139
  stateSnapshots,
595
1140
  subject,
596
1141
  seedDigest,
1142
+ planeClass,
597
1143
  ...(planeCommit === undefined ? {} : { subjectCommit: planeCommit }),
598
1144
  ...(getHostUrl === undefined ? {} : { hostDigest: hostOriginDigest(getHostUrl) }),
1145
+ ...(publicOriginDigest === undefined ? {} : { publicOriginDigest }),
1146
+ ...(declaredOriginDigest === undefined ? {} : { declaredOriginDigest }),
1147
+ ...(lobbyConvergenceDigest === undefined ? {} : { lobbyConvergenceDigest }),
599
1148
  ...(runError === undefined ? {} : { runError })
600
1149
  });
601
1150
  const adapterWarnings = [];
@@ -673,6 +1222,13 @@ export async function runConcurrentSharedWorld(options) {
673
1222
  const errorResult = (() => {
674
1223
  if (ok)
675
1224
  return undefined;
1225
+ if (handoffTimedOut) {
1226
+ // Checked BEFORE the observer failure: the host never yielded a /lobby/CODE within the
1227
+ // deadline (followers failed closed without opening), which is the ROOT CAUSE — and it can
1228
+ // itself make the Observer unable to render a coherent run. Report the distinct, honest
1229
+ // handoff-timeout code rather than a generic observer/run failure.
1230
+ return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_HANDOFF_TIMEOUT", message: runError ?? "The host seat never produced a /lobby/CODE URL within the handoff deadline." };
1231
+ }
676
1232
  if (!observer.ok) {
677
1233
  return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: observer.error?.message ?? "Observer failed for the concurrent shared-world run." };
678
1234
  }
@@ -735,13 +1291,15 @@ function actorLanePassed(result) {
735
1291
  export function buildConcurrentSharedWorldBundle(args) {
736
1292
  const { config, descriptor, createdAt, dryRun, actorSpecs, actorResults, roles } = args;
737
1293
  const inProgress = args.inProgress === true;
1294
+ const external = (args.planeClass ?? "provisioned-getHost") === "external-public";
738
1295
  const simulations = [];
739
1296
  const streams = [];
740
1297
  const events = [];
741
- // Public-safe label only — the raw getHost URL never lands in the bundle (it embeds the live
742
- // sandbox id + matches the e2b-URL redaction). The host identity is carried by plane.hostDigest.
743
- const appUrl = "[provisioned-subject]";
744
- const planeCommit = dryRun ? undefined : args.subjectCommit;
1298
+ // Public-safe label only — neither the raw getHost URL (provisioned) nor the raw public origin
1299
+ // (external-public) lands in the bundle. The plane identity is a DIGEST (plane.hostDigest on
1300
+ // getHost; plane.publicOriginDigest on external-public).
1301
+ const appUrl = external ? "[external-public-plane]" : "[provisioned-subject]";
1302
+ const planeCommit = external ? undefined : dryRun ? undefined : args.subjectCommit;
745
1303
  events.push({
746
1304
  id: "event-000-created",
747
1305
  at: createdAt,
@@ -760,14 +1318,20 @@ export function buildConcurrentSharedWorldBundle(args) {
760
1318
  ? `packed working tree (archiveSha256 ${args.subject.archiveSha256}${args.subject.dirty === true ? ", dirty working tree" : args.subject.dirty === false ? ", clean working tree" : ""})`
761
1319
  : "packed working tree (archive digest unresolved; provisioning failed before resolution)")
762
1320
  : `clone of ${args.subject.repo}${args.subjectCommit ? `@${args.subjectCommit}` : ""}`;
1321
+ // External-public plane provenance is HONESTLY different: an operator-declared, operator-OWNED
1322
+ // public deployment humanish neither provisioned nor seeded — NO getHost, NO clone, NO synthetic
1323
+ // attestation (claiming synthetic on a real site is a lie). The origin persists digest-only.
1324
+ const externalPlaneOwner = config.subject.publicTarget?.owner ?? "(operator-declared)";
763
1325
  events.push({
764
1326
  id: "event-001-plane",
765
1327
  at: createdAt,
766
1328
  level: "info",
767
1329
  type: "concurrent-shared-world.plane.provenance",
768
- message: dryRun
769
- ? `Shared plane declared: ${dryRunPlaneLabel}, served + getHost-exposed in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
770
- : `Shared plane: ${livePlaneLabel}, served + exposed at the harness-minted getHost URL; seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
1330
+ message: external
1331
+ ? `Shared plane: an EXTERNAL-PUBLIC deployment (operator-attested owner ${externalPlaneOwner}, authorized) used DIRECTLY as the shared plane — NO getHost, clone, subject sandbox, or seed. The harness OBSERVES that each seat reached the operator-declared origin (publicOriginDigest); it did NOT mint or control the plane. Author-trust ownership attestation, NOT a synthetic-data claim.`
1332
+ : dryRun
1333
+ ? `Shared plane declared: ${dryRunPlaneLabel}, served + getHost-exposed in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
1334
+ : `Shared plane: ${livePlaneLabel}, served + exposed at the harness-minted getHost URL; seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
771
1335
  simId: actorSpecs[0]?.simId ?? "sim-001",
772
1336
  streamId: actorSpecs[0]?.streamId ?? "stream-001"
773
1337
  });
@@ -780,7 +1344,8 @@ export function buildConcurrentSharedWorldBundle(args) {
780
1344
  const session = outcome?.session;
781
1345
  const screenshots = outcome?.screenshots ?? [];
782
1346
  const lastScreenshot = screenshots[screenshots.length - 1];
783
- const route = publicSafeRouteLabel(roles[index]?.entry); // public-safe (host redacted)
1347
+ // public-safe (origin redacted): external-public seats open the public plane; getHost seats a seat path.
1348
+ const route = external ? "[external-public-plane]" : publicSafeRouteLabel(roles[index]?.entry);
784
1349
  const status = session
785
1350
  ? session.status
786
1351
  : outcome?.sessionError
@@ -921,9 +1486,12 @@ export function buildConcurrentSharedWorldBundle(args) {
921
1486
  });
922
1487
  }
923
1488
  });
924
- // Build the concurrent shared-world evidence block. routeHostDigest is sha256-16 of the ORIGIN of
925
- // the getHost seat URL each actor drove (publish-safe; verify confirms it == plane.hostDigest).
926
- const fallbackHostDigest = args.hostDigest ?? commandDigestOf("[provisioned-subject]");
1489
+ // Build the concurrent shared-world evidence block. routeHostDigest is sha256-16 of the ORIGIN each
1490
+ // seat reached: on getHost the seat URL the actor drove (verify confirms == plane.hostDigest); on
1491
+ // external-public the seat's CDP-OBSERVED URL origin (verify confirms == plane.publicOriginDigest).
1492
+ const fallbackHostDigest = external
1493
+ ? (args.publicOriginDigest ?? commandDigestOf("[external-public-plane]"))
1494
+ : (args.hostDigest ?? commandDigestOf("[provisioned-subject]"));
927
1495
  const laneWindows = actorSpecs.map((spec, index) => {
928
1496
  const result = actorResults[index];
929
1497
  const session = result?.outcome.session;
@@ -943,9 +1511,14 @@ export function buildConcurrentSharedWorldBundle(args) {
943
1511
  seedDigest: args.seedDigest
944
1512
  };
945
1513
  });
946
- const stateSeries = dryRun
947
- ? [{ timestamp: 0, digest: declaredStateDigest(config) }]
948
- : [...args.stateSnapshots].sort((a, b) => a.timestamp - b.timestamp);
1514
+ // Option A (external-public): NO authoritative shared-state proof — OMIT stateSeries entirely (there
1515
+ // is no in-sandbox filesystem to digest; concurrency is proven by temporal co-occupancy + lobby
1516
+ // convergence). The provisioned-getHost plane keeps its authoritative in-sandbox checkpoint series.
1517
+ const stateSeries = external
1518
+ ? undefined
1519
+ : dryRun
1520
+ ? [{ timestamp: 0, digest: declaredStateDigest(config) }]
1521
+ : [...args.stateSnapshots].sort((a, b) => a.timestamp - b.timestamp);
949
1522
  const outcomes = actorSpecs.map((spec, index) => {
950
1523
  const result = actorResults[index];
951
1524
  const session = result?.outcome.session;
@@ -962,31 +1535,54 @@ export function buildConcurrentSharedWorldBundle(args) {
962
1535
  ok
963
1536
  };
964
1537
  });
965
- const sharedWorld = {
966
- schema: SHARED_WORLD_SCHEMA,
967
- topology: "shared-world",
968
- topologyMode: "concurrent",
969
- roleCount: actorSpecs.length,
970
- plane: {
1538
+ // The plane block is plane-class-specific. getHost: harness-minted hostDigest + synthetic
1539
+ // attestation. external-public: operator-declared publicOriginDigest, NO hostDigest, NO exposure
1540
+ // (claiming synthetic on a real site would be a lie — verify asserts both ABSENT there).
1541
+ const plane = external
1542
+ ? {
1543
+ seedDigest: args.seedDigest,
1544
+ envNames: [],
1545
+ // publicOriginDigest is the OBSERVED convergence origin; declaredOriginDigest records the
1546
+ // operator-declared origin for reference (a redirect makes them differ — not a failure).
1547
+ ...(args.publicOriginDigest === undefined ? {} : { publicOriginDigest: args.publicOriginDigest }),
1548
+ ...(args.declaredOriginDigest === undefined ? {} : { declaredOriginDigest: args.declaredOriginDigest })
1549
+ }
1550
+ : {
971
1551
  ...(planeCommit === undefined ? {} : { commit: planeCommit }),
972
1552
  seedDigest: args.seedDigest,
973
1553
  envNames: args.subject.envNames ?? [],
974
1554
  ...(args.hostDigest === undefined ? {} : { hostDigest: args.hostDigest }),
975
1555
  exposure: "synthetic"
976
- },
977
- attributionLimits: [...CONCURRENT_ATTRIBUTION_LIMITS],
1556
+ };
1557
+ const sharedWorld = {
1558
+ schema: SHARED_WORLD_SCHEMA,
1559
+ topology: "shared-world",
1560
+ topologyMode: "concurrent",
1561
+ // Byte-stable: the provisioned-getHost plane omits planeClass (absent == provisioned-getHost).
1562
+ ...(external ? { planeClass: "external-public" } : {}),
1563
+ roleCount: actorSpecs.length,
1564
+ plane,
1565
+ attributionLimits: external ? [...EXTERNAL_PUBLIC_ATTRIBUTION_LIMITS] : [...CONCURRENT_ATTRIBUTION_LIMITS],
978
1566
  laneWindows,
979
- stateSeries,
980
- outcomes
1567
+ // Option A: external-public carries NO stateSeries.
1568
+ ...(stateSeries === undefined ? {} : { stateSeries }),
1569
+ outcomes,
1570
+ ...(args.lobbyConvergenceDigest === undefined ? {} : { lobbyConvergenceDigest: args.lobbyConvergenceDigest })
981
1571
  };
982
1572
  const overlaps = actorWindowsOverlap(actorResults);
983
- const deltas = stateSeries.filter((snapshot, i) => i > 0 && snapshot.digest !== stateSeries[i - 1].digest).length;
1573
+ const deltas = (stateSeries ?? []).filter((snapshot, i) => i > 0 && snapshot.digest !== (stateSeries ?? [])[i - 1].digest).length;
1574
+ const stateSeriesLabel = external
1575
+ ? "stateSeries omitted (no authoritative shared-state proof on the external-public plane)"
1576
+ : `stateSeries ${(stateSeries ?? []).length} snapshot(s), ${deltas} delta(s)`;
1577
+ const convergenceLabel = external
1578
+ ? `; lobby convergence ${args.lobbyConvergenceDigest ? "PROVEN (all seats reached one /lobby/CODE)" : "not observed"}`
1579
+ : "";
984
1580
  events.push({
985
1581
  id: nextEventId("concurrency"),
986
1582
  at: createdAt,
987
1583
  level: "info",
988
1584
  type: "concurrent-shared-world.concurrency",
989
- message: `Concurrency: ${laneWindows.length} actor window(s)${dryRun ? " (dry-run contract; $0)" : `, overlap ${overlaps ? "PROVEN" : "not observed"}`}; stateSeries ${stateSeries.length} snapshot(s), ${deltas} delta(s). Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}. ${dryRun ? "This contract-only run proves no live concurrency, scale, or adoption." : "This run reports only its own observed overlap and state changes; it does not prove scale, repeatability, or adopter-harness replacement."}`
1585
+ message: `Concurrency: ${laneWindows.length} actor window(s)${dryRun ? " (dry-run contract; $0)" : `, overlap ${overlaps ? "PROVEN" : "not observed"}`}; ${stateSeriesLabel}${convergenceLabel}. Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}. ${dryRun ? "This contract-only run proves no live concurrency, scale, or adoption." : "This run reports only its own observed overlap and state changes; it does not prove scale, repeatability, or adopter-harness replacement."}`
990
1586
  });
991
1587
  // Concurrent verdict: dryRun → contract; else every actor produced a terminal, engaged PASSED
992
1588
  // session → pass; otherwise fail. Per-persona mission success is the M-of-N in outcomes[].
@@ -1002,11 +1598,18 @@ export function buildConcurrentSharedWorldBundle(args) {
1002
1598
  const review = {
1003
1599
  schema: REVIEW_SCHEMA,
1004
1600
  verdict,
1601
+ // Plane-class-aware: the external-public plane has NO getHost/clone/seed and carries NO
1602
+ // authoritative state series, so its summary must not claim a getHost-exposed plane (dry-run) nor
1603
+ // report "state delta(s) under load" (live) — it reports lobby convergence instead.
1005
1604
  summary: dryRun
1006
- ? `Dry-run concurrent shared-world contract: ${actorSpecs.length} persona(s) declared against ONE getHost-exposed plane (${descriptor.id}); no sandboxes launched, $0 spend.`
1605
+ ? external
1606
+ ? `Dry-run concurrent shared-world contract: ${actorSpecs.length} persona(s) declared against ONE external-public shared plane (a real public deployment used directly; no getHost/clone/seed); no sandboxes launched, $0 spend.`
1607
+ : `Dry-run concurrent shared-world contract: ${actorSpecs.length} persona(s) declared against ONE getHost-exposed plane (${descriptor.id}); no sandboxes launched, $0 spend.`
1007
1608
  : inProgress
1008
1609
  ? `In-progress concurrent shared-world Observer snapshot: ${actorSpecs.length} persona(s) running against ONE shared plane; final verification is pending.`
1009
- : `Concurrent shared-world (ONE plane, ${actorSpecs.length} simultaneous personas): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${passedMissions}/${actorSpecs.length} reached their goal; overlap ${overlaps ? "proven" : "not observed"}; ${deltas} state delta(s) under load.`,
1610
+ : external
1611
+ ? `Concurrent shared-world (ONE external-public plane, ${actorSpecs.length} simultaneous personas): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${passedMissions}/${actorSpecs.length} reached their goal; overlap ${overlaps ? "proven" : "not observed"}; ${args.lobbyConvergenceDigest ? `${actorSpecs.length} seats converged on one lobby` : "lobby convergence not observed"}.`
1612
+ : `Concurrent shared-world (ONE plane, ${actorSpecs.length} simultaneous personas): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${passedMissions}/${actorSpecs.length} reached their goal; overlap ${overlaps ? "proven" : "not observed"}; ${deltas} state delta(s) under load.`,
1010
1613
  gaps: dryRun
1011
1614
  ? ["This dry-run launched no concurrent shared-world session; it proves contract shape only, not live behavior, scale, or adopter-harness replacement."]
1012
1615
  : inProgress