Skip to content

Commit 97c9362

Browse files
author
DavidQ
committed
Enforce strict schema validation and remove all fallback paths across workspace and tools - PR_11_311
1 parent 85f6140 commit 97c9362

6 files changed

Lines changed: 157 additions & 62 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,11 @@ PR_11_310
100100
```bash
101101
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_310 ..."
102102
```
103+
104+
105+
---
106+
PR_11_311
107+
108+
```bash
109+
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_311 ..."
110+
```

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Remove Workspace V2 legacy diagnostics and duplicate session handlers; enforce single import/export/session activation paths - PR 11.310
1+
Enforce strict schema validation-only acceptance for Workspace V2 import/load/activation paths - PR 11.311
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# PR_11_311 Report - Strict Schema Validation Enforcement
2+
3+
## Files Changed
4+
- `tools/workspace-v2/index.js`
5+
- `docs/pr/PR_11_311_STRICT_SCHEMA_VALIDATION_ENFORCEMENT/PLAN_PR.md`
6+
- `docs/pr/PR_11_311_STRICT_SCHEMA_VALIDATION_ENFORCEMENT/BUILD_PR.md`
7+
- `docs/dev/codex_commands.md`
8+
- `docs/dev/commit_comment.txt`
9+
- `docs/dev/reports/PR_11_311_report.md`
10+
11+
## Summary
12+
- Enforced strict validation before use for workspace/session JSON paths in Workspace V2.
13+
- Invalid JSON now blocks load/import/activation with explicit error text.
14+
- Removed fixture/session auto-corrections and fallback payload resolution.
15+
- Kept tools.* manifest structure and existing schema contracts unchanged.
16+
17+
## Key Enforcement
18+
- Invalid workspace manifest import is rejected before apply.
19+
- Invalid `payloadJson` / invalid session payloads are rejected (including `savedSessions`).
20+
- Session activation now validates payload before writing to sessionStorage.
21+
- Session history/library payloads must pass validation to be loadable.
22+
23+
## Validation Commands
24+
- `node --check tools/workspace-v2/index.js`
25+
26+
## Validation Results
27+
- PASS
28+
29+
## Full Samples Smoke
30+
- Skipped.
31+
- Reason: change is limited to Workspace V2 controller validation and does not modify shared sample framework/loader.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# BUILD_PR_11_311
2+
3+
## Implementation
4+
- Tightened `validateWorkspaceToolSessionPayload` to enforce allowed tool IDs and payload requirements.
5+
- Enforced validation in activation path (`activateWorkspaceSession`) before any session write/use.
6+
- Removed fallback resolution from session history payload loading.
7+
- Removed fixture auto-correction (`paletteJson.colors` conversion) and now reject invalid fixture shapes.
8+
- Enforced strict validation for session library entries on read.
9+
- Updated workspace import flow to avoid partial state mutation before activation succeeds.
10+
- Kept manifest structure and schema contracts unchanged.
11+
12+
## Validation
13+
- `node --check tools/workspace-v2/index.js`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# PLAN_PR_11_311
2+
3+
## Purpose
4+
Enforce strict schema validation as the only acceptance path for workspace and tool JSON usage in Workspace V2.
5+
6+
## Scope
7+
- `tools/workspace-v2/index.js`
8+
- imports
9+
- session loads
10+
- tool entry/session activation path
11+
- no schema file edits
12+
13+
## Steps
14+
1. Remove auto-correction paths and fallback payload acceptance.
15+
2. Validate tool payload shape before any activation/use.
16+
3. Validate workspace manifest import before any state mutation.
17+
4. Ensure session history/library loads reject invalid payloads.
18+
5. Ensure explicit visible errors and no partial apply in import flow.
19+
6. Run targeted syntax validation.

tools/workspace-v2/index.js

Lines changed: 85 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,13 +1207,14 @@ class WorkspaceV2SessionProducer {
12071207
if (!parsed.ok || !this.isValidSessionPayload(parsed.value)) {
12081208
return null;
12091209
}
1210+
const payloadValidation = this.validateWorkspaceToolSessionPayload(parsed.value, "activeSession");
1211+
if (!payloadValidation.ok) {
1212+
return null;
1213+
}
12101214
return parsed.value;
12111215
}
12121216

12131217
resolveActiveSessionPayloadForWorkspaceManifest() {
1214-
if (this.isValidSessionPayload(this.currentSessionPayload)) {
1215-
return this.currentSessionPayload;
1216-
}
12171218
return this.readActiveSessionPayloadForLibraryActions();
12181219
}
12191220

@@ -1226,7 +1227,7 @@ class WorkspaceV2SessionProducer {
12261227
if (!recentEntry) {
12271228
return null;
12281229
}
1229-
return this.resolveSessionPayloadFromContextId(sessionId.trim(), recentEntry.payload);
1230+
return this.resolveSessionPayloadFromContextId(sessionId.trim());
12301231
}
12311232

12321233
readSessionPayloadForLibraryWrite(sessionId) {
@@ -1241,6 +1242,10 @@ class WorkspaceV2SessionProducer {
12411242
if (!parsed.ok || !this.isValidSessionPayload(parsed.value)) {
12421243
return null;
12431244
}
1245+
const payloadValidation = this.validateWorkspaceToolSessionPayload(parsed.value, `sessionStorage.${sessionId.trim()}`);
1246+
if (!payloadValidation.ok) {
1247+
return null;
1248+
}
12441249
return parsed.value;
12451250
}
12461251

@@ -1249,9 +1254,6 @@ class WorkspaceV2SessionProducer {
12491254
if (this.isValidSessionPayload(activePayload)) {
12501255
return activePayload;
12511256
}
1252-
if (this.isValidSessionPayload(this.currentSessionPayload)) {
1253-
return this.currentSessionPayload;
1254-
}
12551257
return null;
12561258
}
12571259

@@ -1450,27 +1452,39 @@ class WorkspaceV2SessionProducer {
14501452
if (!this.isValidSessionPayload(sessionPayload)) {
14511453
return { ok: false, message: "Session activation failed: payload is invalid." };
14521454
}
1453-
const versionedPayload = this.withSessionVersion(sessionPayload);
1454-
const sizeValidation = this.validateSessionPayloadSize(versionedPayload);
1455+
const payloadValidation = this.validateWorkspaceToolSessionPayload(sessionPayload, "sessionActivation");
1456+
if (!payloadValidation.ok) {
1457+
return { ok: false, message: payloadValidation.message };
1458+
}
1459+
const sizeValidation = this.validateSessionPayloadSize(sessionPayload);
14551460
if (!sizeValidation.ok) {
14561461
return { ok: false, message: sizeValidation.message };
14571462
}
14581463
sessionStorage.setItem(hostContextId.trim(), sizeValidation.metrics.serializedPayload);
14591464
this.currentHostContextId = hostContextId.trim();
1460-
this.setCurrentSessionPayload(versionedPayload, sourceLabel);
1461-
return { ok: true, message: "", payload: versionedPayload };
1465+
this.setCurrentSessionPayload(sessionPayload, sourceLabel);
1466+
return { ok: true, message: "", payload: sessionPayload };
14621467
}
14631468

14641469
applySessionPayload(sessionPayload, sourceLabel) {
14651470
if (!this.isValidSessionPayload(sessionPayload)) {
14661471
this.statusNode.textContent = "Session payload is invalid. Expected a JSON object payload.";
14671472
return false;
14681473
}
1474+
const payloadValidation = this.validateWorkspaceToolSessionPayload(sessionPayload, "sessionPayload");
1475+
if (!payloadValidation.ok) {
1476+
this.statusNode.textContent = payloadValidation.message;
1477+
return false;
1478+
}
14691479
const toolId = this.selectedToolId();
14701480
if (!toolId) {
14711481
this.statusNode.textContent = "Select a V2 tool before applying session payload.";
14721482
return false;
14731483
}
1484+
if (sessionPayload.toolId.trim() !== toolId) {
1485+
this.statusNode.textContent = `Session payload toolId '${sessionPayload.toolId.trim()}' does not match selected tool '${toolId}'.`;
1486+
return false;
1487+
}
14741488
if (this.hasWorkspaceActivePalette() && this.isPaletteManagerToolId(toolId)) {
14751489
this.statusNode.textContent = this.singleActivePaletteBlockedMessage();
14761490
return false;
@@ -1549,6 +1563,11 @@ class WorkspaceV2SessionProducer {
15491563
this.statusNode.textContent = `Session library entry '${sessionName}' is invalid.`;
15501564
return null;
15511565
}
1566+
const payloadValidation = this.validateWorkspaceToolSessionPayload(parsed[sessionName], `tools.workspace-v2.savedSessions.${sessionName}`);
1567+
if (!payloadValidation.ok) {
1568+
this.statusNode.textContent = `Session library entry '${sessionName}' is invalid: ${payloadValidation.message}`;
1569+
return null;
1570+
}
15521571
}
15531572
return parsed;
15541573
} catch (error) {
@@ -1741,6 +1760,9 @@ class WorkspaceV2SessionProducer {
17411760
if (typeof entry.tool !== "string" || !entry.tool.trim()) return false;
17421761
if (typeof entry.timestamp !== "string" || !entry.timestamp.trim()) return false;
17431762
if (!this.isValidSessionPayload(entry.payload)) return false;
1763+
const payloadValidation = this.validateWorkspaceToolSessionPayload(entry.payload, `history.${entry.hostContextId.trim()}.payload`);
1764+
if (!payloadValidation.ok) return false;
1765+
if (entry.payload.toolId.trim() !== entry.tool.trim()) return false;
17441766
return true;
17451767
}
17461768

@@ -1771,6 +1793,7 @@ class WorkspaceV2SessionProducer {
17711793
});
17721794
if (invalidCount > 0) {
17731795
console.warn(`[WorkspaceV2SessionHistory] Ignored ${invalidCount} invalid history entr${invalidCount === 1 ? "y" : "ies"}.`);
1796+
this.statusNode.textContent = `Session history contains ${invalidCount} invalid entr${invalidCount === 1 ? "y" : "ies"}. Remove invalid entries before loading sessions.`;
17741797
}
17751798
return validEntries;
17761799
}
@@ -2015,19 +2038,20 @@ class WorkspaceV2SessionProducer {
20152038
);
20162039
}
20172040

2018-
resolveSessionPayloadFromContextId(contextId, fallbackPayload) {
2041+
resolveSessionPayloadFromContextId(contextId) {
20192042
if (typeof contextId === "string" && contextId.trim()) {
20202043
const raw = sessionStorage.getItem(contextId.trim());
20212044
if (typeof raw === "string") {
20222045
const parsed = this.safeParseJson(raw);
20232046
if (parsed.ok && this.isValidSessionPayload(parsed.value)) {
2047+
const payloadValidation = this.validateWorkspaceToolSessionPayload(parsed.value, `sessionStorage.${contextId.trim()}`);
2048+
if (!payloadValidation.ok) {
2049+
return null;
2050+
}
20242051
return parsed.value;
20252052
}
20262053
}
20272054
}
2028-
if (this.isValidSessionPayload(fallbackPayload)) {
2029-
return fallbackPayload;
2030-
}
20312055
return null;
20322056
}
20332057

@@ -2040,7 +2064,7 @@ class WorkspaceV2SessionProducer {
20402064
if (!this.isValidSessionHistoryEntry(entry)) {
20412065
return;
20422066
}
2043-
const resolvedPayload = this.resolveSessionPayloadFromContextId(entry.hostContextId, entry.payload);
2067+
const resolvedPayload = this.resolveSessionPayloadFromContextId(entry.hostContextId);
20442068
if (!this.isValidSessionPayload(resolvedPayload)) {
20452069
return;
20462070
}
@@ -2678,6 +2702,11 @@ class WorkspaceV2SessionProducer {
26782702
this.setMergedSessionStatus("Enter a merged session ID before using in Diff/Merge.");
26792703
return;
26802704
}
2705+
const payloadValidation = this.validateWorkspaceToolSessionPayload(this.lastMergedSessionResult.payload, "mergedSessionResult");
2706+
if (!payloadValidation.ok) {
2707+
this.setMergedSessionStatus(payloadValidation.message);
2708+
return;
2709+
}
26812710
const serialized = JSON.stringify(this.lastMergedSessionResult.payload);
26822711
sessionStorage.setItem(mergedSessionId, serialized);
26832712
this.addRecentSessionEntry(mergedSessionId, this.lastMergedSessionResult.toolId, this.lastMergedSessionResult.payload);
@@ -3218,58 +3247,31 @@ class WorkspaceV2SessionProducer {
32183247
if (!paletteJson || typeof paletteJson !== "object" || Array.isArray(paletteJson)) {
32193248
return { ok: false, message: "Fixture is invalid. paletteJson must be an object for palette-manager-v2.", value: null };
32203249
}
3221-
if (Object.prototype.hasOwnProperty.call(paletteJson, "colors") && !Array.isArray(paletteJson.swatches)) {
3222-
if (!Array.isArray(paletteJson.colors)) {
3223-
return { ok: false, message: "Fixture is invalid. paletteJson.colors must be an array when swatches is missing.", value: null };
3224-
}
3225-
const convertedSwatches = [];
3226-
for (let index = 0; index < paletteJson.colors.length; index += 1) {
3227-
const colorEntry = paletteJson.colors[index];
3228-
if (typeof colorEntry === "string") {
3229-
if (!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(colorEntry)) {
3230-
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}] must be #RRGGBB or #RRGGBBAA.`, value: null };
3231-
}
3232-
convertedSwatches.push({
3233-
symbol: String.fromCharCode(65 + (index % 26)),
3234-
hex: colorEntry,
3235-
name: `Color ${index + 1}`
3236-
});
3237-
continue;
3238-
}
3239-
if (!colorEntry || typeof colorEntry !== "object" || Array.isArray(colorEntry)) {
3240-
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}] must be a string or object.`, value: null };
3241-
}
3242-
if (typeof colorEntry.hex !== "string" || !/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(colorEntry.hex)) {
3243-
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}].hex must be #RRGGBB or #RRGGBBAA.`, value: null };
3244-
}
3245-
const symbol = typeof colorEntry.symbol === "string" && colorEntry.symbol.length === 1
3246-
? colorEntry.symbol
3247-
: String.fromCharCode(65 + (index % 26));
3248-
const name = typeof colorEntry.name === "string" && colorEntry.name.trim()
3249-
? colorEntry.name
3250-
: `Color ${index + 1}`;
3251-
convertedSwatches.push({
3252-
symbol,
3253-
hex: colorEntry.hex,
3254-
name
3255-
});
3256-
}
3257-
const normalizedPalette = { ...paletteJson };
3258-
delete normalizedPalette.colors;
3259-
normalizedPalette.swatches = convertedSwatches;
3260-
return { ok: true, message: "", value: normalizedPalette };
3250+
if (Object.prototype.hasOwnProperty.call(paletteJson, "colors")) {
3251+
return { ok: false, message: "Fixture is invalid. paletteJson.colors is not supported; use paletteJson.swatches.", value: null };
32613252
}
32623253
if (!Array.isArray(paletteJson.swatches)) {
32633254
return { ok: false, message: "Fixture is invalid. paletteJson.swatches must be an array for palette-manager-v2.", value: null };
32643255
}
3256+
const swatchValidation = this.validatePaletteSwatchesForWorkspaceExport(paletteJson.swatches, "fixture.sessionContext.paletteJson.swatches");
3257+
if (!swatchValidation.ok) {
3258+
return { ok: false, message: swatchValidation.message, value: null };
3259+
}
32653260
return { ok: true, message: "", value: paletteJson };
32663261
}
32673262

32683263
normalizeFixtureSessionContext(toolId, sessionContext) {
32693264
if (!this.isValidSessionPayload(sessionContext)) {
32703265
return { ok: false, message: "Fixture is invalid. Missing sessionContext object.", value: null };
32713266
}
3272-
const normalizedSession = this.withSessionVersion(this.cloneSessionValue(sessionContext));
3267+
const normalizedSession = this.cloneSessionValue(sessionContext);
3268+
const payloadValidation = this.validateWorkspaceToolSessionPayload(normalizedSession, "fixture.sessionContext");
3269+
if (!payloadValidation.ok) {
3270+
return { ok: false, message: payloadValidation.message, value: null };
3271+
}
3272+
if (typeof normalizedSession.toolId !== "string" || normalizedSession.toolId.trim() !== toolId) {
3273+
return { ok: false, message: `Fixture is invalid. sessionContext.toolId must be '${toolId}'.`, value: null };
3274+
}
32733275
if (toolId === "palette-manager-v2") {
32743276
if (Object.prototype.hasOwnProperty.call(normalizedSession, "payloadJson")) {
32753277
return { ok: false, message: "Fixture is invalid. payloadJson is not supported for palette-manager-v2.", value: null };
@@ -3510,7 +3512,18 @@ class WorkspaceV2SessionProducer {
35103512
if (typeof sessionPayload.toolId !== "string" || !sessionPayload.toolId.trim()) {
35113513
return { ok: false, message: `${sessionPath}.toolId is required.` };
35123514
}
3513-
if (sessionPayload.toolId.trim() === "palette-manager-v2") {
3515+
const toolId = sessionPayload.toolId.trim();
3516+
const allowedToolIds = new Set([
3517+
"asset-browser-v2",
3518+
"palette-manager-v2",
3519+
"svg-asset-studio-v2",
3520+
"tilemap-studio-v2",
3521+
"vector-map-editor-v2"
3522+
]);
3523+
if (!allowedToolIds.has(toolId)) {
3524+
return { ok: false, message: `${sessionPath}.toolId '${toolId}' is not supported.` };
3525+
}
3526+
if (toolId === "palette-manager-v2") {
35143527
if (Object.prototype.hasOwnProperty.call(sessionPayload, "payloadJson")) {
35153528
return { ok: false, message: `${sessionPath}.payloadJson is not supported for palette-manager-v2. Use paletteJson.` };
35163529
}
@@ -3530,6 +3543,16 @@ class WorkspaceV2SessionProducer {
35303543
if (Object.prototype.hasOwnProperty.call(sessionPayload.paletteJson, "colors")) {
35313544
return { ok: false, message: `${sessionPath}.paletteJson.colors is not supported. Use paletteJson.swatches.` };
35323545
}
3546+
return { ok: true, message: "" };
3547+
}
3548+
if (!Object.prototype.hasOwnProperty.call(sessionPayload, "payloadJson")) {
3549+
return { ok: false, message: `${sessionPath}.payloadJson is required for ${toolId}.` };
3550+
}
3551+
if (!sessionPayload.payloadJson || typeof sessionPayload.payloadJson !== "object" || Array.isArray(sessionPayload.payloadJson)) {
3552+
return { ok: false, message: `${sessionPath}.payloadJson must be an object for ${toolId}.` };
3553+
}
3554+
if (Object.prototype.hasOwnProperty.call(sessionPayload, "paletteJson")) {
3555+
return { ok: false, message: `${sessionPath}.paletteJson is not supported for ${toolId}.` };
35333556
}
35343557
return { ok: true, message: "" };
35353558
}
@@ -3706,16 +3729,15 @@ class WorkspaceV2SessionProducer {
37063729
this.setImportExportStatus(`Import error: ${validation.message}`);
37073730
return;
37083731
}
3709-
this.workspaceImportedToolEntries = {};
3732+
const nextWorkspaceImportedToolEntries = {};
37103733
const importedToolIds = Object.keys(parsed.tools).sort((left, right) => left.localeCompare(right));
37113734
importedToolIds.forEach((toolId) => {
37123735
if (toolId === "palette-browser" || toolId === "workspace-v2") {
37133736
return;
37143737
}
3715-
this.workspaceImportedToolEntries[toolId] = this.cloneSessionValue(parsed.tools[toolId]);
3738+
nextWorkspaceImportedToolEntries[toolId] = this.cloneSessionValue(parsed.tools[toolId]);
37163739
});
37173740
const workspaceV2Tool = parsed.tools["workspace-v2"];
3718-
this.workspaceManifestGame = this.cloneSessionValue(workspaceV2Tool.game);
37193741
const activeHostContextId = workspaceV2Tool.activeHostContextId.trim();
37203742
const activeToolId = workspaceV2Tool.activeToolId.trim();
37213743
const activePayload = workspaceV2Tool.activeSession;
@@ -3724,6 +3746,8 @@ class WorkspaceV2SessionProducer {
37243746
this.setImportExportStatus(`Import error: ${activation.message}`);
37253747
return;
37263748
}
3749+
this.workspaceManifestGame = this.cloneSessionValue(workspaceV2Tool.game);
3750+
this.workspaceImportedToolEntries = nextWorkspaceImportedToolEntries;
37273751
localStorage.setItem(this.libraryStorageKey, JSON.stringify(workspaceV2Tool.savedSessions));
37283752
this.updateWorkspaceActivePaletteFromManifest(parsed);
37293753
this.workspaceJsonNode.value = JSON.stringify(parsed, null, 2);

0 commit comments

Comments
 (0)