Skip to content

Commit fb4a418

Browse files
author
DavidQ
committed
Harden Workspace-first games flow by removing standalone bypass, propagating hostContextId for hosted game sessions, and eliminating remaining direct standalone debug launch affordances from Games Hub cards (batches 40–41 PASS).
1 parent 00efb18 commit fb4a418

7 files changed

Lines changed: 100 additions & 7 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Samples2Tools batch 40 summary
2+
Generated: 2026-04-24T22:28:28.115Z
3+
Purpose: remove remaining standalone bypass risk and make hosted game sessions context-aware.
4+
5+
Completed this batch:
6+
- Removed standalone bypass parameter from game launch guard
7+
- Added Workspace Manager shared host context write/remove around hosted game mount lifecycle
8+
- Propagated hostContextId into hosted game iframe URLs
9+
- Revalidated guard coverage across all launchable game pages
10+
11+
Primary validation artifact:
12+
- docs/dev/reports/samples2tools_batch_40_validation.txt (PASS)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Samples2Tools batch 40 validation
2+
Generated: 2026-04-24T22:28:28.115Z
3+
Scope: Workspace-only game execution hardening (no bypass + hosted context propagation).
4+
5+
- [PASS] Game launch guard has no standalone bypass flag :: games/shared/workspaceGameLaunchGuard.js
6+
- [PASS] Game launch guard allows only Workspace-hosted mode :: games/shared/workspaceGameLaunchGuard.js
7+
- [PASS] Workspace Manager writes shared host context for game mounts :: tools/Workspace Manager/main.js
8+
- [PASS] Workspace Manager passes hostContextId to game frame URL :: tools/Workspace Manager/main.js
9+
- [PASS] Workspace Manager removes game host context on unmount :: tools/Workspace Manager/main.js
10+
- [PASS] All launchable game pages include workspace launch guard :: launchable=11, guardIssues=0, missingFiles=0
11+
12+
Checks: 6
13+
Failures: 0
14+
Result: PASS
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Samples2Tools batch 41 summary
2+
Generated: 2026-04-24T22:29:21.567Z
3+
Purpose: ensure primary Games Hub no longer exposes direct standalone debug launch affordances.
4+
5+
Completed this batch:
6+
- Removed Asteroids direct standalone debug link from game cards
7+
- Kept Workspace Manager as the only explicit card-level launch action
8+
9+
Primary validation artifact:
10+
- docs/dev/reports/samples2tools_batch_41_validation.txt (PASS)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Samples2Tools batch 41 validation
2+
Generated: 2026-04-24T22:29:21.567Z
3+
Scope: remove remaining direct standalone launch affordances from primary Games Hub cards.
4+
5+
- [PASS] Removed Asteroids direct debug standalone link :: games/index.render.js
6+
- [PASS] Primary game card launch action is Workspace Manager only :: games/index.render.js
7+
- [PASS] Game card title launch uses workspaceHref-or-href fallback path :: games/index.render.js
8+
9+
Checks: 3
10+
Failures: 0
11+
Result: PASS

games/index.render.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,6 @@ function renderCard(row, instanceKey = "main") {
183183

184184
const classText = row.classValues.length > 0 ? row.classValues.map((value) => value.split("/").at(-1) || value).join(", ") : "none";
185185
const tagText = row.tags.length > 0 ? row.tags.join(", ") : "none";
186-
const asteroidDebugLink = row.id === "Asteroids"
187-
? '<p><a class="game-title-link" href="/games/Asteroids/index.html?debug=1">Debug Mode</a></p>'
188-
: "";
189186
const launchActions = row.workspaceHref
190187
? `<p class="game-launch-actions"><a class="game-title-link" href="${escapeHtml(row.workspaceHref)}">Open In Workspace Manager</a></p>`
191188
: "";
@@ -196,7 +193,6 @@ function renderCard(row, instanceKey = "main") {
196193
${previewHtml}
197194
<p>${escapeHtml(row.description)}</p>
198195
${launchActions}
199-
${asteroidDebugLink}
200196
<p>Classes: ${escapeHtml(classText)}</p>
201197
<p>Tags: ${escapeHtml(tagText)}</p>
202198
${row.requiresService ? '<p class="game-service-note">Requires background service.</p>' : ""}

games/shared/workspaceGameLaunchGuard.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ export function enforceWorkspaceGameLaunch(gameId) {
1515
const params = url.searchParams;
1616
const hosted = params.get("hosted") === "1";
1717
const hostToolId = (params.get("hostToolId") || "").trim().toLowerCase();
18-
const allowStandalone = params.get("allowStandalone") === "1";
19-
20-
if (allowStandalone || (hosted && hostToolId === "workspace-manager")) {
18+
if (hosted && hostToolId === "workspace-manager") {
2119
return;
2220
}
2321

tools/Workspace Manager/main.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { createToolHostManifest, getToolHostEntryById } from "../../tools/shared/toolHostManifest.js";
22
import { createToolHostRuntime } from "../../tools/shared/toolHostRuntime.js";
3+
import {
4+
removeToolHostSharedContextById,
5+
writeToolHostSharedContext
6+
} from "../../tools/shared/toolHostSharedContext.js";
37

48
const GAMES_METADATA_PATH = "/games/metadata/games.index.metadata.json";
59

@@ -21,6 +25,7 @@ const manifest = createToolHostManifest();
2125
const toolIds = manifest.tools.map((tool) => tool.id);
2226
const hasAvailableTools = toolIds.length > 0;
2327
let currentGameFrame = null;
28+
let currentGameHostContextId = "";
2429

2530
function decodeSamplePresetPayload(encoded) {
2631
if (typeof encoded !== "string" || !encoded.trim()) {
@@ -184,13 +189,21 @@ async function readGameEntryById(gameId) {
184189

185190
function unmountGameFrame() {
186191
if (!currentGameFrame) {
192+
if (currentGameHostContextId) {
193+
removeToolHostSharedContextById(currentGameHostContextId);
194+
currentGameHostContextId = "";
195+
}
187196
return;
188197
}
189198
if (currentGameFrame.parentElement === refs.mountContainer) {
190199
currentGameFrame.removeAttribute("src");
191200
refs.mountContainer.removeChild(currentGameFrame);
192201
}
193202
currentGameFrame = null;
203+
if (currentGameHostContextId) {
204+
removeToolHostSharedContextById(currentGameHostContextId);
205+
currentGameHostContextId = "";
206+
}
194207
}
195208

196209
function mountGameFrame(gameEntry) {
@@ -201,6 +214,24 @@ function mountGameFrame(gameEntry) {
201214
runtime.unmountCurrentTool("switch-to-game");
202215
unmountGameFrame();
203216

217+
const hostContext = writeToolHostSharedContext({
218+
toolId: "workspace-manager",
219+
source: "game-host",
220+
requestedAt: new Date().toISOString(),
221+
sharedContext: {
222+
hostMode: "game",
223+
gameId: gameEntry.id,
224+
gameTitle: gameEntry.title
225+
},
226+
state: {
227+
game: {
228+
id: gameEntry.id,
229+
title: gameEntry.title,
230+
href: gameEntry.href
231+
}
232+
}
233+
});
234+
204235
const frame = document.createElement("iframe");
205236
frame.setAttribute("data-game-host-frame", gameEntry.id);
206237
frame.setAttribute("title", `${gameEntry.title} Workspace Frame`);
@@ -209,6 +240,12 @@ function mountGameFrame(gameEntry) {
209240
gameUrl.searchParams.set("hosted", "1");
210241
gameUrl.searchParams.set("hostToolId", "workspace-manager");
211242
gameUrl.searchParams.set("hostGameId", gameEntry.id);
243+
if (hostContext?.contextId) {
244+
currentGameHostContextId = hostContext.contextId;
245+
gameUrl.searchParams.set("hostContextId", hostContext.contextId);
246+
} else {
247+
currentGameHostContextId = "";
248+
}
212249
frame.src = gameUrl.toString();
213250
refs.mountContainer.replaceChildren(frame);
214251
currentGameFrame = frame;
@@ -348,6 +385,17 @@ function bindEvents() {
348385
}
349386

350387
window.addEventListener("popstate", () => {
388+
const gameId = readInitialGameId();
389+
if (gameId) {
390+
void readGameEntryById(gameId).then((gameEntry) => {
391+
if (!gameEntry) {
392+
writeStatus(`Game "${gameId}" is not available for Workspace Manager launch.`);
393+
return;
394+
}
395+
mountGameFrame(gameEntry);
396+
});
397+
return;
398+
}
351399
const toolId = readInitialToolId();
352400
if (refs.toolSelect instanceof HTMLSelectElement) {
353401
refs.toolSelect.value = toolId;
@@ -357,6 +405,10 @@ function bindEvents() {
357405
mountSelectedTool("popstate");
358406
syncControlState();
359407
});
408+
409+
window.addEventListener("beforeunload", () => {
410+
unmountGameFrame();
411+
});
360412
}
361413

362414
async function init() {

0 commit comments

Comments
 (0)