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.
- package/dist/computer-use-actor.d.ts +7 -0
- package/dist/computer-use-actor.js +4 -1
- package/dist/computer-use-actor.js.map +1 -1
- package/dist/computer-use.d.ts +28 -0
- package/dist/computer-use.js +19 -1
- package/dist/computer-use.js.map +1 -1
- package/dist/concurrent-shared-world-lab.d.ts +45 -1
- package/dist/concurrent-shared-world-lab.js +644 -41
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +19 -0
- package/dist/cua-actor-lab.js +15 -2
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/init-templates.js +52 -0
- package/dist/init-templates.js.map +1 -1
- package/dist/lab-config.d.ts +54 -3
- package/dist/lab-config.js +162 -9
- package/dist/lab-config.js.map +1 -1
- package/dist/run.d.ts +46 -2
- package/dist/run.js +226 -1
- package/dist/run.js.map +1 -1
- package/dist/shared-world-lab.d.ts +8 -0
- package/dist/shared-world-lab.js.map +1 -1
- package/docs/architecture/external-public-shared-world.md +117 -0
- package/docs/contracts/schemas.md +64 -5
- package/docs/goals/current.md +1 -1
- package/docs/principles/invariants-and-defaults.md +16 -0
- package/docs/product/cineguessr-3player-external-public.md +67 -0
- package/docs/ramp/README.md +1 -1
- package/docs/release/0.20.0-external-public-shared-world.md +43 -0
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
257
|
-
|
|
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
|
-
|
|
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
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
const
|
|
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:
|
|
769
|
-
? `Shared plane
|
|
770
|
-
:
|
|
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
|
-
|
|
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
|
|
925
|
-
//
|
|
926
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"}`};
|
|
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
|
-
?
|
|
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
|
-
:
|
|
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
|