From cf7860e05b3a469e2d5188793516289955d4152a Mon Sep 17 00:00:00 2001 From: BillyOutlast <172061051+BillyOutlast@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:12:42 -0400 Subject: [PATCH] Fork independence: cut all donor/fork dependencies - Delete scripts/extract-tier2.mjs; remove fork clone from seam-tests CI. - Tier-2 tests self-contained: MANIFEST hash/size pins replace donor byte-identity; glue tests assert dry-run + anti-revert + additive (no regen-from-fork). - MANIFEST provenance rekeyed (donor->origin, historical note only); UPSTREAM_BASELINE drops the donor pin (upstream pin only). - Docs: ai-studio/README, RETIREMENT (independence done in-repo; archival stays owner-side), root README, AGENTS.md. - Local fork fixture (~2GB) deleted; suite 97/97 green without it. --- .github/workflows/seam-tests.yml | 9 +- AGENTS.md | 12 +- CLAUDE_CODE_ADVERSARIAL_PROMPT.md | 119 ++++++++++++++++++ CLAUDE_CODE_HANDOFF_PROMPT.md | 189 +++++++++++++++++++++++++++++ README.md | 16 +-- RETIREMENT.md | 37 +++--- UPSTREAM_BASELINE.json | 3 +- packages/ai-studio/MANIFEST.json | 66 +++++----- packages/ai-studio/README.md | 25 ++-- scripts/extract-tier2.mjs | 88 -------------- scripts/generate-tier2-patches.mjs | 5 +- tests/tier2/extract.test.mjs | 37 ++---- tests/tier2/tier2-patches.test.mjs | 68 ++++------- 13 files changed, 424 insertions(+), 250 deletions(-) create mode 100644 CLAUDE_CODE_ADVERSARIAL_PROMPT.md create mode 100644 CLAUDE_CODE_HANDOFF_PROMPT.md delete mode 100644 scripts/extract-tier2.mjs diff --git a/.github/workflows/seam-tests.yml b/.github/workflows/seam-tests.yml index 9bb1223..62c98da 100644 --- a/.github/workflows/seam-tests.yml +++ b/.github/workflows/seam-tests.yml @@ -15,16 +15,13 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - # Baselines are gitignored locally (multi-GB); CI clones the pins: - # upstream verified-at commit + fork donor baseline (provenance tests). + # Baselines are gitignored locally (multi-GB); CI clones the pin: + # upstream verified-at commit. This repo is fork-independent: no + # donor checkout exists anywhere in test or build paths. - name: Clone pinned upstream baseline run: | git clone --no-checkout https://github.com/4ian/GDevelop review/GDevelop git -C review/GDevelop checkout 8301cdcaa48b496f48a28a6e3a56fa833eaeddff - - name: Clone fork donor baseline - run: | - git clone --no-checkout https://github.com/Heretek-AI/GDevelop-Heretek review/GDevelop-Heretek - git -C review/GDevelop-Heretek checkout f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f - name: Run full suite (seams + patcher + launcher + tier-2 + web) run: npm test - name: Patcher gate (seams + patch inventory) diff --git a/AGENTS.md b/AGENTS.md index cd4062b..bcf924c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,8 +51,10 @@ node scripts/apply-patches.mjs --check # seam gate - The driver (`apply-patches.mjs`) owns two install paths: launcher `.cjs` sync and Tier-2 overlay (`syncTier2Files`); `--asar` and `--target` must both run both. Markers live in `MARKERS` — extend them with any new patch. -- Donor pin (`f7d450a`) lives in three places: `UPSTREAM_BASELINE.json`, - `.github/workflows/seam-tests.yml`, `RETIREMENT.md`. Keep them in sync. -- Upstream sync point awareness: the fork lags upstream, so any new - verbatim delta must be audited for deletions of upstream-new symbols - before shipping (the R1–R3 regression class in `AUDIT_REPORT.md`). +- Upstream pin lives in one place: `UPSTREAM_BASELINE.json` + (`verifiedAtUpstreamCommit`). There is deliberately **no donor/fork pin + anywhere** — this repo is fork-independent (see `RETIREMENT.md`). +- Patch provenance: 07/08/11 are curated forward-ports of fork work + (`patches/REBASE.md`); any *new* verbatim delta must still be audited + for deletions of upstream-new symbols before shipping (the R1–R3 + regression class in `AUDIT_REPORT.md`). diff --git a/CLAUDE_CODE_ADVERSARIAL_PROMPT.md b/CLAUDE_CODE_ADVERSARIAL_PROMPT.md new file mode 100644 index 0000000..7aba65c --- /dev/null +++ b/CLAUDE_CODE_ADVERSARIAL_PROMPT.md @@ -0,0 +1,119 @@ +# CLAUDE CODE ADVERSARIAL PROMPT: ZERO-TRUST AUDIT & E2E VERIFICATION ENGINE + +> **How to Run**: Copy and paste the prompt below into **Claude Code** (or run `claude` inside `/home/john/Projects/GDevelop-Unlocked` with this file as the instruction context). + +*** + +# MISSION: Adversarial Audit, Gap Analysis, & End-to-End Verification of GDevelop-Unlocked + +## 1. Context & Repository Architecture +You are operating in `/home/john/Projects/GDevelop-Unlocked`. +You have three codebases available on disk: +1. **Pristine Upstream GDevelop**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop` + - Upstream GitHub: https://github.com/4ian/GDevelop + - The unmodified official game engine (C++ core, Wasm `GDevelop.js`, Pixi/Three runtime `GDJS`, and React/Electron `newIDE`). +2. **Donor Fork (GDevelop-Heretek)**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek` + - Fork GitHub: https://github.com/Heretek-AI/GDevelop-Heretek + - A 205-file diverging fork that unlocked premium features and introduced a Bring-Your-Own-Key (BYOK) local AI studio. +3. **Surgical Conversion Project (GDevelop-Unlocked)**: `/home/john/Projects/GDevelop-Unlocked` + - Goal: Replace the 205-file fork with a surgical plugin architecture: 8 Tier-1 patches in `patches/`, runtime Electron hooks in `launcher/`, and an extracted `packages/ai-studio` package. + +--- + +## 2. Operating Mindset: Zero-Trust Adversarial Auditor +You must assume a **strict adversarial posture**: +> **Core Hypothesis**: The unlocking mechanisms and AI features imported from `review/GDevelop-Heretek` into `GDevelop-Unlocked` are **fundamentally broken, leaky, incomplete, or phantom-passing**. + +### Strict Operational Rules +1. **Distrust Existing Green Tests**: The 77 tests in `npm test` are largely unit-level assertions against mocks (e.g. `tests/launcher/smoke.test.cjs` uses a fake Electron object `makeFakeElectron`; `tests/patches/apply.test.mjs` only tests dry-run patch string replacements in a temp directory). A passing test does **not** prove functional unlocking in the real editor. +2. **Demand Execution Proof**: You must prove every claim through real network requests, real component lifecycle execution, real HTTP mock backends, and typecheck/compilation checks against upstream. +3. **Hunt for Dropped Fork Glue**: The fork touched 205 files. `GDevelop-Unlocked` attempted to condense this into 8 patches. Any file modified in the fork that was omitted from `patches/` or `launcher/` is a prime suspect for broken functionality or security/data leaks. + +--- + +## 3. Confirmed Vulnerabilities & Failure Points to Investigate + +You must specifically investigate and verify the following known failure modes: + +### Failure Mode A: The `PrepareAiUserContent.js` Cloud Leak & 401 Crash +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/PrepareAiUserContent.js` (~line 112) +- **The Bug**: In pristine upstream, before any AI request is created, `prepareAiUserContent` calls `createAiUserContentPresignedUrls` to upload the project JSON to S3 via `api.gdevelop.io`. +- **The Fork Fix**: Heretek intercepted this with `if (isCustomEndpointEnabled() || userId.startsWith('local-')) return { gameProjectJson: simplifiedProjectJson, ... };`. +- **The Defect in Unlocked**: `GDevelop-Unlocked` **never patched `PrepareAiUserContent.js`**. As a result, running BYOK against pristine upstream attempts to upload local project data to GDevelop's cloud, leaking private game data, and immediately crashes with HTTP 401/403 for `local-byok-user`. + +### Failure Mode B: Dead Studio Runtime in `AskAiEditorContainer.js` +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AskAiEditorContainer.js` +- **The Bug**: `packages/ai-studio` extracted 12 files from `AiGeneration/Studio/` (`UseStudioRuntime.js`, `UseStudioNudge.js`, `Roles.js`, etc.). However, `AskAiEditorContainer.js` was **never patched** in `GDevelop-Unlocked` to mount `useStudioRuntime()` or `useStudioNudge()`. +- **The Defect in Unlocked**: Multi-agent studio loops, subagent spawning, and tool orchestration are completely disconnected dead code. + +### Failure Mode C: 0-Credit Lockout in `AiRequestChat/Utils.js` +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AiRequestChat/Utils.js` +- **The Bug**: Upstream `canPayForAiRequest` disables the send button if the user does not have enough cloud credits. +- **The Fork Fix**: Heretek replaced this with `canAffordAiRequest`, returning `true` whenever `isCustomEndpointEnabled` is active. +- **The Defect in Unlocked**: `GDevelop-Unlocked` omitted this patch. Users with 0 cloud credits cannot submit prompts to their own local models. + +### Failure Mode D: Production Asar & Webpack Hydration Race +- **Location**: `launcher/heretek-hook.cjs` and `launcher/preload-unlock.cjs` +- **The Bug**: In production Electron, `BrowserWindow` is non-configurable, so constructor wrapping can fail. The fallback relies on `did-finish-load` `executeJavaScript`. If React hydrates or Axios initializes before `did-finish-load`, the spoofing wrappers attach too late, exposing upstream paywalls. + +--- + +## 4. Phase-by-Phase Adversarial Tasks + +### Phase 1: Upstream Gating & Entitlement Mapping +Audit `review/GDevelop` and catalog all gating points against `review/GDevelop-Heretek`: +1. **Tier-1 Gating Map**: + - `Usage.js`: `hasValidSubscriptionPlan`, capabilities, quotas, credits. + - `SubscriptionChecker.js`: All 4 placement IDs (`'Disable GDevelop splash at startup'`, `'Debugger'`, `'Hot reloading'`, `'Preview over wifi'`). + - `LoadingScreenEditor.js`: Logo checkbox, watermark checkbox, min duration slider. + - `LocalPreviewLauncher/index.js`: Hot reload modulo (`% 16`) check, network preview check. + - `DebuggerEditorContainer.js`: Debugger & profiler access. +2. **Tier-2 AI Gating Map**: + - `Generation.js`: Request creation, message streaming, tool calling. + - `PrepareAiUserContent.js`: Presigned URL creation & project upload. + - `AskAiEditorContainer.js` & `AiRequestChat/`: Credit checks, subagent hooks, stop/retry controls. + - `PreferencesDialog.js` & `PreferencesContext.js`: AI tab and persistent storage. + +### Phase 2: Build Real Integration & E2E Test Suites +Do not rely on mock objects. Build real, executable integration tests under `tests/integration/`: + +1. **`tests/integration/network-spoof.test.mjs`**: + - Spin up a real local HTTP server using Node's `http` module. + - Exercise real HTTP requests through the preload and hook wrappers (`fetch` and `XMLHttpRequest`). + - Assert: + - `GET /limits` and `GET /subscription-v2` return the unlocked payloads. + - All other URLs, mutation methods (POST, PUT, DELETE), and external domains pass through untouched. + - Axios instances correctly receive spoofed responses without crashing. + +2. **`tests/integration/ai-byok-flow.test.mjs`**: + - Spin up the committed mock AI server: `packages/ai-studio/dev-harness/mock-ai-provider.js` (or inline HTTP server). + - Test the complete generation flow from `PrepareAiUserContent` through `Generation.js` to `CustomAIClient`. + - Assert: + - Zero network requests are sent to `api.gdevelop.io` when custom endpoint is active. + - `local-ai-*` request IDs route exclusively to the local provider. + - Fail-closed leak guard (`assertNoLocalIdsLeak`) throws immediately if a local ID is sent to a hosted URL. + - The 39 `GDEVELOP_OPENAI_TOOLS` schemas parse cleanly and can be dispatched without undefined function errors. + +3. **`tests/integration/react-entitlements.test.mjs`**: + - Mount the React components (`LoadingScreenEditor`, `LocalPreviewLauncher`, `DebuggerEditorContainer`) in a JSDOM environment. + - Assert: + - Watermark and startup logo can be toggled without mounting `SubscriptionChecker` dialogs. + - 20 consecutive hot reloads do not mount paywall dialogs. + - Debugger container mounts without opening the subscription prompt. + +### Phase 3: Author Missing Glue Patches (Remediation) +Generate and apply the missing surgical patches to resolve the confirmed bugs: +1. `patches/09-prepare-ai-content.patch`: Fix `PrepareAiUserContent.js` to prevent cloud upload leaks and 401 crashes in BYOK mode. +2. `patches/10-ai-chat-affordance.patch`: Fix `AiRequestChat/Utils.js` to allow 0-credit prompt submissions when custom endpoint is enabled. +3. `patches/11-studio-runtime-mount.patch`: Patch `AskAiEditorContainer.js` to wire in `useStudioRuntime` and `useStudioNudge`. + +Update `scripts/apply-patches.mjs` markers and verify that `node scripts/apply-patches.mjs --target review/GDevelop --no-verify` applies cleanly. + +--- + +## 5. Required Deliverables +When your execution completes, generate a comprehensive markdown report in `AUDIT_REPORT.md` containing: +1. **Executive Audit Verdict**: Detailed PASS/FAIL assessment of whether `GDevelop-Unlocked` replaces the fork without regressions. +2. **Vulnerability & Regression Matrix**: Table of every discovered bug classified by severity (CRITICAL, HIGH, MEDIUM, LOW), root cause, and remediation status. +3. **Integration Test Suite Results**: Full test run logs demonstrating that all real network and BYOK flows pass. +4. **Upstream PR Recommendations**: Guidance on upstreaming neutral AI interfaces (e.g. `docs/upstream-ai-provider-rfc.md`) vs maintaining surgical patches. diff --git a/CLAUDE_CODE_HANDOFF_PROMPT.md b/CLAUDE_CODE_HANDOFF_PROMPT.md new file mode 100644 index 0000000..07511e5 --- /dev/null +++ b/CLAUDE_CODE_HANDOFF_PROMPT.md @@ -0,0 +1,189 @@ +# HANDOFF + ADVERSARIAL AUDIT PROMPT — GDevelop-Unlocked + +Run `claude` inside `/home/john/Projects/GDevelop-Unlocked` with this file as the instruction context. +Part A is the state left by the previous review session (2026-10-10). Part B is the adversarial audit mission. Read Part A first: it corrects or sharpens several assumptions in Part B. + +--- + +# PART A — Handoff from the previous session + +## A1. What the previous session was + +A findings-only, 5-phase review (test authenticity, integration handling, plugin functionality, cross-cutting consistency) with one subagent per phase. It did **not** finish and produced **no synthesized report**. Nothing was fixed; the repo, `review/` and git state were not modified by it. + +## A2. Repo state at handoff + +- Reviewed commit: `13ddde2` on `feature/ci-e2e` (one commit ahead of `develop`/`main` @ `30da3f8`). `inject-launcher.mjs`, smoke mode, `e2e.yml` exist only on this branch. +- Working tree is **dirty from a different session** (21:27–21:29): `M package.json` (adds `tests/integration/*.test.mjs` to `npm test` + a `test:integration` script), untracked `tests/integration/ai-byok-flow.test.mjs`, `tests/integration/network-spoof.test.mjs`, `CLAUDE_CODE_ADVERSARIAL_PROMPT.md`. **None of these were read or reviewed.** Read them before building on them — Part B Phase 2 asks for files with these exact names. +- Fixtures: `review/GDevelop` @ `8301cdcaa48b496f48a28a6e3a56fa833eaeddff` (`v5.6.283-48`), `review/GDevelop-Heretek` @ `f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f`. Both gitignored, ~2 GB each. +- `npm test` at `13ddde2`: 77/77 green locally on Node 26 and on Node 20.20.2. +- CI is **red**: `seam-tests` fails on the `main` push and on the PR; `e2e` fails on the PR (`gh run list --repo Heretek-AI/GDevelop-Unlocked`). +- Tools available on this machine: `patch`, `Xvfb`/`xvfb-run`, `ollama`, authenticated `gh`, network. A copy of the v5.6.283 Linux release zip and a flat asar extraction are in `/tmp/opencode` (another session's directory — treat as read-only, copy out). + +## A3. Findings confirmed by the lead (re-checked against code/CI, not just reported) + +1. **Donor pin is the wrong commit.** `b9109cfbaaaf19c31b5af4d315317fb67d8903c3` (in `.github/workflows/seam-tests.yml:27`, `UPSTREAM_BASELINE.json:3`, `RETIREMENT.md:28`) contains 0 Tier-2 paths; the real donor is fork HEAD `f7d450a` (31 paths, 418 commits later). This is why `tests/tier2/extract.test.mjs` and `tests/tier2/tier2-patches.test.mjs` fail in CI while passing locally. +2. **Committed mock provider cannot start from this repo.** `package.json` has `"type": "module"`; `packages/ai-studio/dev-harness/mock-ai-provider.js:29` uses `require` → `ReferenceError: require is not defined in ES module scope`. This is why the `byok-mock` job fails. It matters for Part B Phase 2 item 2: the mock must be run from outside the package scope (or via an inline server) unless this is fixed. +3. **`packages/unlock-shim/generation-shim.mjs` is not wired into anything shipped.** Zero references to `selectBackend` / `guardHostedCall` / `assertNoLocalIdsLeak` / `unlock-shim` in all eight patches; at `13ddde2` its only importer is `tests/tier2/shim.test.mjs`. `packages/unlock-shim/README.md:13-16` claims patch 07 calls it. So the "fail-closed leak guard" exists only in a test, not in the delivery path. +4. **Smoke check probes `fetch` only** (`launcher/heretek-hook.cjs:331-350`), while `launcher/README.md:18` says the app's client rides on XHR. +5. **Fake Electron is more permissive than production.** `tests/launcher/hook.test.cjs:44-48` defines `BrowserWindow` with `configurable: true`; the production export is non-configurable. +6. **`isSmokeMode` drops the first user argument** (`launcher/heretek-hook.cjs:324-326`, `slice(2)`); `e2e.yml` only works because the flag is passed last. +7. **Generators hard-code `/tmp/opencode`** (`scripts/generate-tier1-patches.mjs:450-472`, `scripts/generate-tier2-patches.mjs:78-80`), ignoring `TMPDIR`. `tests/tier2/tier2-patches.test.mjs` invokes the tier-2 generator, so plain `npm test` writes there; the tier-2 generator never creates the directory (cold-runner behaviour unchecked). +8. **`generate-tier1-patches.mjs:44-55`**: any unrecognised argument resets `--out` to `/patches`. +9. **`apply.test.mjs:50,109` pass `--no-verify`**, so the driver's `MARKERS`/`verifyTarget` contract is never exercised by a test. +10. **Correction to an earlier assumption:** `apply-patches.mjs:166-171` passes `shell: true`, so `--check` works on Node 20 (ran 29 seam tests, exit 0). CI's `node-version: 20` vs `engines >=22` is a policy mismatch only — nothing needs ≥22. + +## A4. Reported by the consistency reviewer, NOT independently re-checked + +Treat as strong leads; verify before relying on them. + +- **Patches 07/08 are not pure fork deltas.** They are a diff of baseline `8301cdc` against a fork synced only to `b9109cf` (71 upstream commits behind), so they also revert upstream changes in those files — reportedly deleting exports still imported elsewhere at baseline (`createAiAttachmentUploads`/`createAiAttachmentDownloads`, the `newEffectDialogDefaultTab` API). Rests on a static import scan, not a build. +- **Tier-2 is not installed by anything.** After a green `apply-patches.mjs --target`, imports of `'../../AI/CustomAIClient'` from `Generation.js`, `PreferencesDialog.js`, `PreferencesProvider.js` are unresolved — no script places `packages/ai-studio` into the tree. Even when overlaid, 11 of 12 Studio modules have no importer; the fork's wiring (≈17 files, +2650/−307) is not delivered. **This is the same ground as Part B Failure Modes A–C**, which name three of those missing files specifically. +- **Release-asar docs point at a command that cannot work.** `patches/README.md:3-5`, `launcher/README.md:36-38,47-49` and `UPDATER_PIN_MESSAGE` (`launcher/heretek-hook.cjs:42-47`, pinned by `tests/launcher/hook.test.cjs:254`) say `apply-patches.mjs --asar`; a real asar is flat and bundled, and `inject-launcher.mjs` (the path that works) is in no README. +- **XHR path reportedly fails on the code path production uses** — reproduced on a Node test double only, not in real Chromium. Relevant to Part B Failure Mode D; needs a real-browser check. +- **Weekly "drift check" runs against fixed pins**, so it cannot detect new upstream drift. +- **`RETIREMENT.md:10-12,52-54`** precondition and counts are stale/contradictory (now 74 pass / 1 fail / 2 cancelled with an empty `FORK_DIR`; both tier-2 provenance tests need the donor). +- **Seam suite does not cover** the two usage endpoints, the validators, or the targets of patches 02/07/08. +- Determinism: committed patches 01–08 and the extraction equal fresh generator output; only tier-2 is pinned by a test. `MANIFEST.json:2` stores an absolute local path and no donor commit. +- Cosmetic list (stale wording, dead code, redundant tests, userscript README vs behaviour) — re-derive if needed. + +## A5. Integration reviewer — partial results only + +- Full `--target` apply on a pristine scratch upstream: all 8 applied, launcher files synced, markers verified, exit 0. +- Re-apply refused at patch 01's dry-run, exit 1 (tree-unchanged not confirmed by hash). +- Missing fixture fails loudly ("Missing upstream file … Set UPSTREAM_DIR or refresh review/GDevelop"). +- Production Electron is 32.3.3 (`review/GDevelop/newIDE/electron-app/package.json`). + +## A6. Not done + +- **Test-authenticity phase:** work completed (all 16 test files read, mutation spot-checks run in scratch) but the report never reached the lead. Raw logs may still exist under `/tmp/claude-1000/-home-john-Projects-GDevelop-Unlocked/07a31264-f21d-4519-9dbf-fe97194ba181/scratchpad/phase1/logs/` (`mut-results.jsonl`, one record per mutation). That path is session-scoped and may be gone; the lead never read it. +- **Integration phase remainder:** drifted-hunk / half-applied-tree behaviour, fake-vs-production Electron comparison beyond A3.5, `inject-launcher.mjs` edge cases, smoke exit-code logic vs how `e2e.yml` consumes it, what `byok-mock`/`byok-ollama` actually prove (reported: they exercise no client code from this repo). +- **Functionality phase:** stopped before reporting. No Tier-1 residual diff against the donor, no payload-vs-validator check, no live release → inject → repack → Xvfb smoke run, no negative control on the smoke verdict. + +## A7. Process notes for the next session + +- **Several write-ups were cut off by a safety classifier** in the previous session (three subagent reports and two lead responses), mostly while writing detailed tables. When that happens, do not retry or reword the blocked content — record the gap plainly and move on to the next item. Expect this again and plan deliverables so a single cut-off does not lose everything: write findings to disk incrementally as short entries. +- **Have subagents write their findings to a file as they go**, not only in a final message — the previous session lost two complete reports that existed only in final messages. +- **Do not patch `review/GDevelop` in place.** Part B §4 Phase 3 says `apply-patches.mjs --target review/GDevelop`; that mutates the pinned fixture every seam/patch test depends on, and patches are non-idempotent by design. Apply to a scratch copy instead, e.g. `git -C review/GDevelop archive HEAD newIDE/app/src newIDE/electron-app | tar -x -C `. +- **Part B changes the mode from findings-only to remediation** (new tests under `tests/integration/`, new patches 09–11, edits to `scripts/apply-patches.mjs`). The previous brief's non-goals ("no fixes, do not touch `tests/`") no longer apply if Part B is followed — that is the owner's decision; it is flagged here so it is deliberate. +- New patches must come from the generators, not hand edits (`tests/tier2/tier2-patches.test.mjs` asserts committed patches equal fresh generator output). Fix the donor pin (A3.1) first or CI stays red regardless. +- Part B §2 rule 1 says `tests/patches/apply.test.mjs` "only tests dry-run patch string replacements". The previous session observed that it spawns the real driver with `--target` on a temp copy (with `--no-verify`); check the file rather than assuming either description. +- Part B Failure Modes A–C were **not individually verified** in the previous session; A4's "wiring not delivered" finding is consistent with them but is not proof of each. + +--- + +# PART B — Adversarial audit mission + +# MISSION: Adversarial Audit, Gap Analysis, & End-to-End Verification of GDevelop-Unlocked + +## 1. Context & Repository Architecture +You are operating in `/home/john/Projects/GDevelop-Unlocked`. +You have three codebases available on disk: +1. **Pristine Upstream GDevelop**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop` + - Upstream GitHub: https://github.com/4ian/GDevelop + - The unmodified official game engine (C++ core, Wasm `GDevelop.js`, Pixi/Three runtime `GDJS`, and React/Electron `newIDE`). +2. **Donor Fork (GDevelop-Heretek)**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek` + - Fork GitHub: https://github.com/Heretek-AI/GDevelop-Heretek + - A 205-file diverging fork that unlocked premium features and introduced a Bring-Your-Own-Key (BYOK) local AI studio. +3. **Surgical Conversion Project (GDevelop-Unlocked)**: `/home/john/Projects/GDevelop-Unlocked` + - Goal: Replace the 205-file fork with a surgical plugin architecture: 8 Tier-1 patches in `patches/`, runtime Electron hooks in `launcher/`, and an extracted `packages/ai-studio` package. + +--- + +## 2. Operating Mindset: Zero-Trust Adversarial Auditor +You must assume a **strict adversarial posture**: +> **Core Hypothesis**: The unlocking mechanisms and AI features imported from `review/GDevelop-Heretek` into `GDevelop-Unlocked` are **fundamentally broken, leaky, incomplete, or phantom-passing**. + +### Strict Operational Rules +1. **Distrust Existing Green Tests**: The 77 tests in `npm test` are largely unit-level assertions against mocks (e.g. `tests/launcher/smoke.test.cjs` uses a fake Electron object `makeFakeElectron`; `tests/patches/apply.test.mjs` only tests dry-run patch string replacements in a temp directory). A passing test does **not** prove functional unlocking in the real editor. +2. **Demand Execution Proof**: You must prove every claim through real network requests, real component lifecycle execution, real HTTP mock backends, and typecheck/compilation checks against upstream. +3. **Hunt for Dropped Fork Glue**: The fork touched 205 files. `GDevelop-Unlocked` attempted to condense this into 8 patches. Any file modified in the fork that was omitted from `patches/` or `launcher/` is a prime suspect for broken functionality or security/data leaks. + +--- + +## 3. Confirmed Vulnerabilities & Failure Points to Investigate + +You must specifically investigate and verify the following known failure modes: + +### Failure Mode A: The `PrepareAiUserContent.js` Cloud Leak & 401 Crash +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/PrepareAiUserContent.js` (~line 112) +- **The Bug**: In pristine upstream, before any AI request is created, `prepareAiUserContent` calls `createAiUserContentPresignedUrls` to upload the project JSON to S3 via `api.gdevelop.io`. +- **The Fork Fix**: Heretek intercepted this with `if (isCustomEndpointEnabled() || userId.startsWith('local-')) return { gameProjectJson: simplifiedProjectJson, ... };`. +- **The Defect in Unlocked**: `GDevelop-Unlocked` **never patched `PrepareAiUserContent.js`**. As a result, running BYOK against pristine upstream attempts to upload local project data to GDevelop's cloud, leaking private game data, and immediately crashes with HTTP 401/403 for `local-byok-user`. + +### Failure Mode B: Dead Studio Runtime in `AskAiEditorContainer.js` +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AskAiEditorContainer.js` +- **The Bug**: `packages/ai-studio` extracted 12 files from `AiGeneration/Studio/` (`UseStudioRuntime.js`, `UseStudioNudge.js`, `Roles.js`, etc.). However, `AskAiEditorContainer.js` was **never patched** in `GDevelop-Unlocked` to mount `useStudioRuntime()` or `useStudioNudge()`. +- **The Defect in Unlocked**: Multi-agent studio loops, subagent spawning, and tool orchestration are completely disconnected dead code. + +### Failure Mode C: 0-Credit Lockout in `AiRequestChat/Utils.js` +- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AiRequestChat/Utils.js` +- **The Bug**: Upstream `canPayForAiRequest` disables the send button if the user does not have enough cloud credits. +- **The Fork Fix**: Heretek replaced this with `canAffordAiRequest`, returning `true` whenever `isCustomEndpointEnabled` is active. +- **The Defect in Unlocked**: `GDevelop-Unlocked` omitted this patch. Users with 0 cloud credits cannot submit prompts to their own local models. + +### Failure Mode D: Production Asar & Webpack Hydration Race +- **Location**: `launcher/heretek-hook.cjs` and `launcher/preload-unlock.cjs` +- **The Bug**: In production Electron, `BrowserWindow` is non-configurable, so constructor wrapping can fail. The fallback relies on `did-finish-load` `executeJavaScript`. If React hydrates or Axios initializes before `did-finish-load`, the spoofing wrappers attach too late, exposing upstream paywalls. + +--- + +## 4. Phase-by-Phase Adversarial Tasks + +### Phase 1: Upstream Gating & Entitlement Mapping +Audit `review/GDevelop` and catalog all gating points against `review/GDevelop-Heretek`: +1. **Tier-1 Gating Map**: + - `Usage.js`: `hasValidSubscriptionPlan`, capabilities, quotas, credits. + - `SubscriptionChecker.js`: All 4 placement IDs (`'Disable GDevelop splash at startup'`, `'Debugger'`, `'Hot reloading'`, `'Preview over wifi'`). + - `LoadingScreenEditor.js`: Logo checkbox, watermark checkbox, min duration slider. + - `LocalPreviewLauncher/index.js`: Hot reload modulo (`% 16`) check, network preview check. + - `DebuggerEditorContainer.js`: Debugger & profiler access. +2. **Tier-2 AI Gating Map**: + - `Generation.js`: Request creation, message streaming, tool calling. + - `PrepareAiUserContent.js`: Presigned URL creation & project upload. + - `AskAiEditorContainer.js` & `AiRequestChat/`: Credit checks, subagent hooks, stop/retry controls. + - `PreferencesDialog.js` & `PreferencesContext.js`: AI tab and persistent storage. + +### Phase 2: Build Real Integration & E2E Test Suites +Do not rely on mock objects. Build real, executable integration tests under `tests/integration/`: + +1. **`tests/integration/network-spoof.test.mjs`**: + - Spin up a real local HTTP server using Node's `http` module. + - Exercise real HTTP requests through the preload and hook wrappers (`fetch` and `XMLHttpRequest`). + - Assert: + - `GET /limits` and `GET /subscription-v2` return the unlocked payloads. + - All other URLs, mutation methods (POST, PUT, DELETE), and external domains pass through untouched. + - Axios instances correctly receive spoofed responses without crashing. + +2. **`tests/integration/ai-byok-flow.test.mjs`**: + - Spin up the committed mock AI server: `packages/ai-studio/dev-harness/mock-ai-provider.js` (or inline HTTP server). + - Test the complete generation flow from `PrepareAiUserContent` through `Generation.js` to `CustomAIClient`. + - Assert: + - Zero network requests are sent to `api.gdevelop.io` when custom endpoint is active. + - `local-ai-*` request IDs route exclusively to the local provider. + - Fail-closed leak guard (`assertNoLocalIdsLeak`) throws immediately if a local ID is sent to a hosted URL. + - The 39 `GDEVELOP_OPENAI_TOOLS` schemas parse cleanly and can be dispatched without undefined function errors. + +3. **`tests/integration/react-entitlements.test.mjs`**: + - Mount the React components (`LoadingScreenEditor`, `LocalPreviewLauncher`, `DebuggerEditorContainer`) in a JSDOM environment. + - Assert: + - Watermark and startup logo can be toggled without mounting `SubscriptionChecker` dialogs. + - 20 consecutive hot reloads do not mount paywall dialogs. + - Debugger container mounts without opening the subscription prompt. + +### Phase 3: Author Missing Glue Patches (Remediation) +Generate and apply the missing surgical patches to resolve the confirmed bugs: +1. `patches/09-prepare-ai-content.patch`: Fix `PrepareAiUserContent.js` to prevent cloud upload leaks and 401 crashes in BYOK mode. +2. `patches/10-ai-chat-affordance.patch`: Fix `AiRequestChat/Utils.js` to allow 0-credit prompt submissions when custom endpoint is enabled. +3. `patches/11-studio-runtime-mount.patch`: Patch `AskAiEditorContainer.js` to wire in `useStudioRuntime` and `useStudioNudge`. + +Update `scripts/apply-patches.mjs` markers and verify that `node scripts/apply-patches.mjs --target review/GDevelop --no-verify` applies cleanly. + +--- + +## 5. Required Deliverables +When your execution completes, generate a comprehensive markdown report in `AUDIT_REPORT.md` containing: +1. **Executive Audit Verdict**: Detailed PASS/FAIL assessment of whether `GDevelop-Unlocked` replaces the fork without regressions. +2. **Vulnerability & Regression Matrix**: Table of every discovered bug classified by severity (CRITICAL, HIGH, MEDIUM, LOW), root cause, and remediation status. +3. **Integration Test Suite Results**: Full test run logs demonstrating that all real network and BYOK flows pass. +4. **Upstream PR Recommendations**: Guidance on upstreaming neutral AI interfaces (e.g. `docs/upstream-ai-provider-rfc.md`) vs maintaining surgical patches. diff --git a/README.md b/README.md index 6ddb892..60e30bf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # GDevelop-Unlocked -Surgical, patch-stack conversion of the -[GDevelop-Heretek](https://github.com/Heretek-AI/GDevelop-Heretek) fork -(unlocked premium gates + Bring-Your-Own-Key local AI studio) into a -maintainable delivery shape — no 205-file merge treadmill. +Surgical, patch-stack conversion of the premium unlocks + +Bring-Your-Own-Key local AI studio pioneered in the (now-retired) +[GDevelop-Heretek](https://github.com/Heretek-AI/GDevelop-Heretek) fork — +into a maintainable delivery shape with **zero dependency on that fork**. +No 205-file merge treadmill, no donor checkout in any build/test/CI path. Three delivery mechanisms, all applied onto stock upstream GDevelop: @@ -29,14 +30,15 @@ node scripts/apply-patches.mjs --target node scripts/inject-launcher.mjs --appdir ``` -Requirements: Node ≥ 22, `patch`, git. Test fixtures (`review/GDevelop`, -`review/GDevelop-Heretek`, multi-GB) are gitignored; CI clones its pins. +Requirements: Node ≥ 22, `patch`, git. The upstream test fixture +(`review/GDevelop`, multi-GB) is gitignored; CI clones the pinned upstream +commit. Nothing else external is needed — this repo is fork-independent. ## Layout - `patches/` — 11 patches + `README.md` (per-patch targets) + `REBASE.md` - `scripts/` — `apply-patches.mjs` (driver), `generate-tier1/2-patches.mjs` - (generators), `extract-tier2.mjs`, `inject-launcher.mjs` + (generators), `inject-launcher.mjs` - `launcher/` — `heretek-hook.cjs`, `preload-unlock.cjs`, `unlock-payloads.cjs` - `packages/ai-studio/` — extracted Tier-2 runtime + `dev-harness/` mock provider - `packages/unlock-shim/` — routing/leak-guard invariants (reference impl) diff --git a/RETIREMENT.md b/RETIREMENT.md index 794399b..d31602f 100644 --- a/RETIREMENT.md +++ b/RETIREMENT.md @@ -1,17 +1,18 @@ -# Phase 6: Decommission fork — retirement runbook (issue #6) +# Phase 6: Fork independence — DONE in-repo (issue #6) -The 205-file fork (`Heretek-AI/GDevelop-Heretek`) stops being the -delivery vehicle and stays only as the Tier-2 donor archive. Nothing -here is automated: archiving and workflow deletion are destructive and -need a human owner. Do not close issue #6 until every box is ticked. +The 205-file fork (`Heretek-AI/GDevelop-Heretek`) is no longer referenced +by any build, test, or CI path in this repo. Code independence landed +2026-10-11: donor-comparison tests were replaced with self-contained +integrity pins, the fork clone was removed from CI, and the extractor +was deleted. What remains is owner-side archival of the old repo +(destructive, needs a human — do not close issue #6 until ticked). -## Preconditions (M1 + M2 must hold without the fork present) +## Preconditions (re-verified at independence cutover) -- [ ] `npm test` green with the fork checkout absent - (`FORK_DIR` pointed at an empty dir — only `tests/tier2/extract.test.mjs` - needs the donor, and only to re-verify byte-identity). -- [ ] `node scripts/apply-patches.mjs --target ` green - (all 8 patches + launcher sync + marker verify). +- [x] `npm test` green with the fork checkout absent (no test reads any + `FORK_DIR`; self-contained `MANIFEST.json` hashes pin the runtime). +- [x] `node scripts/apply-patches.mjs --target ` green + (all 11 patches + launcher sync + Tier-2 overlay + marker verify). - [ ] Desktop smoke on the patched build: logo uncheck, watermark uncheck, hot reload x17, wifi preview, debugger open (issue #2). - [ ] Desktop E2E: `http://localhost:11434/v1` generation + subagent @@ -43,12 +44,8 @@ gh repo archive Heretek-AI/GDevelop-Heretek --yes ## Post-retirement invariant The delivery path (patcher, hook, preload, shim, userscript) never -*reads* the fork: `GDevelop-Heretek` appears in delivery code only in -provenance comments (`Unlocked in GDevelop-Heretek`) and in donor-dir -defaults owned by the extractor (`scripts/extract-tier2.mjs`, -`scripts/generate-tier2-patches.mjs`) and the two provenance tests -(`tests/tier2/extract.test.mjs`, `tests/tier2/tier2-patches.test.mjs`), -which fail loudly — by design — when the donor is absent. Everything -else in `npm test` is fork-free (verified: 63 pass with `FORK_DIR` -pointed at an empty dir; the 1 failure + 2 cancellations are the -donor-provenance tests failing loudly by design — not silent drift). +*reads* the fork: the name `GDevelop-Heretek` appears only in historical +provenance notes and in unlock-marker strings shipped by design +(`Unlocked in GDevelop-Heretek`). No test, script, or workflow clones, +reads, or regenerates from a donor checkout — verified: the full suite +passes with no fork fixture present. diff --git a/UPSTREAM_BASELINE.json b/UPSTREAM_BASELINE.json index c6910ae..53ae03f 100644 --- a/UPSTREAM_BASELINE.json +++ b/UPSTREAM_BASELINE.json @@ -1,6 +1,5 @@ { - "comment": "Pinned upstream shape the Phase 1 seam tests were written against. CI re-runs the suite against this commit on schedule; any red suite means a patch hunk or hook target moved.", - "donorBaselineCommit": "f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f", + "comment": "Pinned upstream shape the Phase 1 seam tests were written against. CI re-runs the suite against this commit on schedule; any red suite means a patch hunk or hook target moved. This repo is fork-independent: the only external source it reads is stock upstream GDevelop.", "verifiedAtUpstreamCommit": "8301cdcaa4", "verifiedAt": "2026-10-11", "seams": { diff --git a/packages/ai-studio/MANIFEST.json b/packages/ai-studio/MANIFEST.json index e8067a0..8da1fa1 100644 --- a/packages/ai-studio/MANIFEST.json +++ b/packages/ai-studio/MANIFEST.json @@ -1,101 +1,101 @@ { - "donor": "/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek", "files": { "AI/CustomAIClient.js": { - "donor": "newIDE/app/src/AI/CustomAIClient.js", "bytes": 208828, + "origin": "newIDE/app/src/AI/CustomAIClient.js", "sha256": "2dd976b936908115d08374851121a077d2b2b6c314a029a4bc52ab7125f51723" }, "AI/CustomAIClient.spec.js": { - "donor": "newIDE/app/src/AI/CustomAIClient.spec.js", "bytes": 257794, + "origin": "newIDE/app/src/AI/CustomAIClient.spec.js", "sha256": "0152072739b6590f004e886b8a5787cd769a529f044f14e48ca7e78ecc2a560a" }, "AiGeneration/Studio/EditApprovalLabel.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js", "bytes": 1308, + "origin": "newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js", "sha256": "a924efc714123ab45e7d649d581b4d54557e6452fc0df3bcf8ad387c27c9f194" }, "AiGeneration/Studio/FinalizeSubAgents.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js", "bytes": 9111, + "origin": "newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js", "sha256": "708de36210d000af56e795280eaa5dd450e32cedb4a54b7b031353a6cc4a679f" }, "AiGeneration/Studio/LoopGuard.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/LoopGuard.js", "bytes": 9401, + "origin": "newIDE/app/src/AiGeneration/Studio/LoopGuard.js", "sha256": "0ad4c6f5d6312a38f56e0385b8408adec72634c95d7252a3ff193af1e667101e" }, "AiGeneration/Studio/NudgePolicy.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/NudgePolicy.js", "bytes": 3821, + "origin": "newIDE/app/src/AiGeneration/Studio/NudgePolicy.js", "sha256": "c13a8af43b3b3d23ccb2dff2bc3438755d502090fd99f2bad1061d86f8d3c925" }, "AiGeneration/Studio/PlanStore.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/PlanStore.js", "bytes": 13166, + "origin": "newIDE/app/src/AiGeneration/Studio/PlanStore.js", "sha256": "3218ec230b6786d9105fd2854b3389b20b6c68a0889ccc6134f533106b1a3be6" }, "AiGeneration/Studio/RequestWriteGate.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js", "bytes": 5904, + "origin": "newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js", "sha256": "bcd78ad8d0a22ca58ea413b2d1bbdbd07814184a0e28726dcfe8d466707c5064" }, - "AiGeneration/Studio/Roles.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/Roles.js", - "bytes": 12493, - "sha256": "dac71987229c63fcea55fabd049b43ab48beff68cb19813de9d1c77090c6798f" - }, "AiGeneration/Studio/RoleToolPolicy.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js", "bytes": 2097, + "origin": "newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js", "sha256": "66a7d577f12fccaa1b23d89bc7ba02c9bca07058358cc53d3ecfe50d604ef1b8" }, + "AiGeneration/Studio/Roles.js": { + "bytes": 12493, + "origin": "newIDE/app/src/AiGeneration/Studio/Roles.js", + "sha256": "dac71987229c63fcea55fabd049b43ab48beff68cb19813de9d1c77090c6798f" + }, "AiGeneration/Studio/SafeTruncation.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/SafeTruncation.js", "bytes": 2743, + "origin": "newIDE/app/src/AiGeneration/Studio/SafeTruncation.js", "sha256": "24e4e11fb5ee360374cd3bfa092fd01755b99228c34df74547473336d4a799ce" }, "AiGeneration/Studio/SpawnSubAgents.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js", "bytes": 9089, + "origin": "newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js", "sha256": "ffc0658c57b027d20cc50c88bc350ac013369c3fb09ed366ff8736a7aa78d2ae" }, "AiGeneration/Studio/UseStudioNudge.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js", "bytes": 5824, + "origin": "newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js", "sha256": "12df5f3f54a52e3c2c72646e6825360206eec0521dd6f15d73cd39660b21e0d2" }, "AiGeneration/Studio/UseStudioRuntime.js": { - "donor": "newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js", "bytes": 11187, + "origin": "newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js", "sha256": "ecc51b28d00baf1ccd7d38d542e4a9237cd145cd863586c6fae6e587d03564c3" }, "dev-harness/mock-ai-provider.js": { - "donor": "scripts/dev/mock-ai-provider.js", "bytes": 9862, - "sha256": "117611628583d61a41e0f26de0ea0bfe30fd9b6781127e853995ff964cdfc1f1", - "note": "ESM port (import instead of require): repo root is type:module so the verbatim CJS donor throws ReferenceError. Behavior identical; exempt from donor byte-identity in extract.test.mjs." + "note": "ESM port (import instead of require): repo root is type:module so the donor's CJS original throws ReferenceError here. Behavior identical; pinned by hash like every other file.", + "origin": "scripts/dev/mock-ai-provider.js", + "sha256": "117611628583d61a41e0f26de0ea0bfe30fd9b6781127e853995ff964cdfc1f1" }, - "dev-harness/mock-script.example.json": { - "donor": "scripts/dev/mock-script.example.json", - "bytes": 887, - "sha256": "45c2915841b0a6c0f17db7a297a39ca1dfb8b889d74f018f87d1815cadf17c26" + "dev-harness/mock-child-scripts.example.json": { + "bytes": 532, + "origin": "scripts/dev/mock-child-scripts.example.json", + "sha256": "8f785710966f26a7923793f47a785b18d8e8d326bb381f78682a7eb50ac6bdd7" }, "dev-harness/mock-script-multi.json": { - "donor": "scripts/dev/mock-script-multi.json", "bytes": 1314, + "origin": "scripts/dev/mock-script-multi.json", "sha256": "c94da74ac1251e07ae7a4576ea7354a1ef9f76a9294cca4bf473c0807d5ed824" }, "dev-harness/mock-script-specialists.json": { - "donor": "scripts/dev/mock-script-specialists.json", "bytes": 960, + "origin": "scripts/dev/mock-script-specialists.json", "sha256": "31fe97f889935e92bfb3af5056e2579105fb7b48b325fd3dfcc9a5847b4ee6b7" }, - "dev-harness/mock-child-scripts.example.json": { - "donor": "scripts/dev/mock-child-scripts.example.json", - "bytes": 532, - "sha256": "8f785710966f26a7923793f47a785b18d8e8d326bb381f78682a7eb50ac6bdd7" + "dev-harness/mock-script.example.json": { + "bytes": 887, + "origin": "scripts/dev/mock-script.example.json", + "sha256": "45c2915841b0a6c0f17db7a297a39ca1dfb8b889d74f018f87d1815cadf17c26" } - } + }, + "provenance": "Historical origin only: extracted in 2026-10 from the Heretek fork (now retired). This repo never reads the donor \u2014 integrity is self-contained via the pinned bytes/sha256 below." } diff --git a/packages/ai-studio/README.md b/packages/ai-studio/README.md index 5066c16..c1a48ac 100644 --- a/packages/ai-studio/README.md +++ b/packages/ai-studio/README.md @@ -1,19 +1,14 @@ -# packages/ai-studio — Tier-2 extraction (Phase 4, issue #4) +# packages/ai-studio — Tier-2 runtime (self-contained) -~8.9k-line BYOK AI + multi-agent Studio, moved **verbatim** out of the -fork. The fork is a donor, not a delivery vehicle. Never hand-edit -files under `AI/` or `AiGeneration/` here — re-run the extractor: - -```bash -node scripts/extract-tier2.mjs --fork -``` - -`MANIFEST.json` pins every file (sha256); `tests/tier2/extract.test.mjs` -fails if any byte drifts from the donor. +~8.9k-line BYOK AI + multi-agent Studio. Historically extracted from the +(now-retired) Heretek fork; this package is now the canonical source — +there is no donor checkout anywhere in build, test, or CI paths. +`MANIFEST.json` pins every file (bytes + sha256); +`tests/tier2/extract.test.mjs` fails if any byte drifts. ## Contents -| Path | Donor | Notes | +| Path | Historical origin | Notes | |---|---|---| | `AI/CustomAIClient.js` | `newIDE/app/src/AI/CustomAIClient.js` (5870 lines) | OpenAI-compatible client, 39-tool `GDEVELOP_OPENAI_TOOLS`, SSE streaming, `gd-custom-ai-*` localStorage keys. **Zero `api.gdevelop.io` references** (pinned by test) | | `AI/CustomAIClient.spec.js` | co-located spec | Run inside `newIDE/app` (`react-app-rewired test`), not here | @@ -23,10 +18,8 @@ fails if any byte drifts from the donor. ## Wiring Import rewiring is limited to `packages/unlock-shim/generation-shim.mjs` -(routing predicates + fail-closed leak guard) plus the generated glue -patches `07-generation-glue.patch` (Generation.js branches) and -`08-preferences-ai.patch` (`ai` prefs tab + storage). The extracted -modules themselves are byte-identical to the donor. +(routing predicates + fail-closed leak guard) plus the glue patches +`07`–`11` (`patches/REBASE.md` records how the forward-ports were built). ## Desktop E2E (requires a built app — procedure, not automation) diff --git a/scripts/extract-tier2.mjs b/scripts/extract-tier2.mjs deleted file mode 100644 index abc645e..0000000 --- a/scripts/extract-tier2.mjs +++ /dev/null @@ -1,88 +0,0 @@ -// Extracts Tier-2 donor files VERBATIM from the fork into -// packages/ai-studio/ (Phase 4, issue #4). The fork is a donor, not a -// delivery vehicle: these files are never hand-edited here. -// -// node scripts/extract-tier2.mjs --fork review/GDevelop-Heretek -// -// Writes MANIFEST.json (sha256 per file); tests/tier2/extract.test.mjs -// re-verifies byte-identity. To refresh after a fork sync, re-run with -// the new fork checkout — any content change shows up as a manifest -// diff in code review. -import fs from 'node:fs'; -import path from 'node:path'; -import crypto from 'node:crypto'; -import { fileURLToPath } from 'node:url'; - -const here = path.dirname(fileURLToPath(import.meta.url)); -const ROOT = path.resolve(here, '..'); -const DEST = path.join(ROOT, 'packages', 'ai-studio'); - -// Donor paths, relative to the fork root. Kept explicit (not a glob) -// so additions/removals in the fork are noticed, not silently absorbed. -const DONOR_FILES = [ - 'newIDE/app/src/AI/CustomAIClient.js', - 'newIDE/app/src/AI/CustomAIClient.spec.js', - 'newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js', - 'newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js', - 'newIDE/app/src/AiGeneration/Studio/LoopGuard.js', - 'newIDE/app/src/AiGeneration/Studio/NudgePolicy.js', - 'newIDE/app/src/AiGeneration/Studio/PlanStore.js', - 'newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js', - 'newIDE/app/src/AiGeneration/Studio/Roles.js', - 'newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js', - 'newIDE/app/src/AiGeneration/Studio/SafeTruncation.js', - 'newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js', - 'newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js', - 'newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js', - 'scripts/dev/mock-ai-provider.js', - 'scripts/dev/mock-script.example.json', - 'scripts/dev/mock-script-multi.json', - 'scripts/dev/mock-script-specialists.json', - 'scripts/dev/mock-child-scripts.example.json', -]; - -// Fork-relative -> package-relative layout. -function destFor(rel) { - if (rel.startsWith('newIDE/app/src/')) - return rel.slice('newIDE/app/src/'.length); - if (rel.startsWith('scripts/dev/')) - return 'dev-harness/' + rel.slice('scripts/dev/'.length); - throw new Error(`No destination mapping for donor file: ${rel}`); -} - -function parseArgs(argv) { - const out = { fork: path.join(ROOT, 'review', 'GDevelop-Heretek') }; - for (let i = 0; i < argv.length; i++) { - if (argv[i] === '--fork') out.fork = path.resolve(argv[++i]); - } - return out; -} - -const args = parseArgs(process.argv.slice(2)); -const manifest = { donor: args.fork, files: {} }; - -for (const rel of DONOR_FILES) { - const src = path.join(args.fork, rel); - if (!fs.existsSync(src)) { - throw new Error( - `Donor file missing: ${rel} (fork checkout at ${args.fork}). ` + - `If the fork moved it, update DONOR_FILES explicitly.` - ); - } - const content = fs.readFileSync(src); - const dest = path.join(DEST, destFor(rel)); - fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.writeFileSync(dest, content); - manifest.files[destFor(rel)] = { - donor: rel, - bytes: content.length, - sha256: crypto.createHash('sha256').update(content).digest('hex'), - }; - console.log(`extracted ${destFor(rel)} (${content.length} bytes)`); -} - -fs.writeFileSync( - path.join(DEST, 'MANIFEST.json'), - JSON.stringify(manifest, null, 2) + '\n' -); -console.log(`wrote MANIFEST.json (${DONOR_FILES.length} files)`); diff --git a/scripts/generate-tier2-patches.mjs b/scripts/generate-tier2-patches.mjs index 0318737..9c41fd3 100644 --- a/scripts/generate-tier2-patches.mjs +++ b/scripts/generate-tier2-patches.mjs @@ -5,7 +5,10 @@ // deltas are woven through their files, so each patch carries the exact // fork diff — no hand-editing, no silent shaping. // -// node scripts/generate-tier2-patches.mjs --upstream review/GDevelop --fork review/GDevelop-Heretek +// On-demand regeneration of the additive 09/10 deltas only. Needs an +// explicit fork checkout (the retired donor — not present in this repo, +// not needed by any test or CI path): +// node scripts/generate-tier2-patches.mjs --upstream review/GDevelop --fork // // Each patch round-trips (`patch -p1 --dry-run` on a pristine copy) // before it is written. Regeneration is deterministic: committed output diff --git a/tests/tier2/extract.test.mjs b/tests/tier2/extract.test.mjs index c884c9b..a898d94 100644 --- a/tests/tier2/extract.test.mjs +++ b/tests/tier2/extract.test.mjs @@ -1,6 +1,6 @@ -// Tier-2 extraction tests (Phase 4): packages/ai-studio/ stays -// byte-identical to the fork donor, and the extracted client keeps the -// BYOK invariant (no hosted generation references). +// Tier-2 package tests: packages/ai-studio/ is self-contained and intact. +// Tamper-evidence comes from MANIFEST.json hashes pinned in THIS repo — +// no donor checkout, no fork clone, nothing outside this tree. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -11,52 +11,33 @@ import { fileURLToPath } from 'node:url'; const here = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(here, '..', '..'); const PACKAGE = path.join(ROOT, 'packages', 'ai-studio'); -const FORK = - process.env.FORK_DIR || path.join(ROOT, 'review', 'GDevelop-Heretek'); function sha256(abs) { return crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex'); } -describe('tier-2: verbatim extraction', () => { +describe('tier-2: self-contained package integrity', () => { const manifest = JSON.parse( fs.readFileSync(path.join(PACKAGE, 'MANIFEST.json'), 'utf8') ); - it('every manifest file matches the donor byte-for-byte', () => { + it('every manifest file exists with pinned bytes and hash', () => { assert.ok( Object.keys(manifest.files).length >= 19, 'manifest looks truncated' ); for (const [pkgRel, meta] of Object.entries(manifest.files)) { const pkgAbs = path.join(PACKAGE, pkgRel); - const donorAbs = path.join(FORK, meta.donor); assert.ok(fs.existsSync(pkgAbs), `package file missing: ${pkgRel}`); - assert.ok( - fs.existsSync(donorAbs), - `donor moved in fork: ${meta.donor} (update scripts/extract-tier2.mjs explicitly)` - ); - if (pkgRel.startsWith('dev-harness/')) { - // Dev harness is ported, not vendored: the repo root is - // "type": "module" so the donor's CJS `require` throws - // ReferenceError here (see mock-ai-provider.js ESM port). The - // MANIFEST sha below still pins the exact bytes we ship. - assert.equal( - sha256(pkgAbs), - meta.sha256, - `manifest stale for ${pkgRel} (re-run the extractor)` - ); - continue; - } assert.equal( - sha256(pkgAbs), - sha256(donorAbs), - `drift vs donor: ${pkgRel}` + fs.statSync(pkgAbs).size, + meta.bytes, + `size drift (tamper?): ${pkgRel}` ); assert.equal( sha256(pkgAbs), meta.sha256, - `manifest stale for ${pkgRel} (re-run the extractor)` + `hash drift (tamper?): ${pkgRel}` ); } }); diff --git a/tests/tier2/tier2-patches.test.mjs b/tests/tier2/tier2-patches.test.mjs index 9044c75..5293d46 100644 --- a/tests/tier2/tier2-patches.test.mjs +++ b/tests/tier2/tier2-patches.test.mjs @@ -1,9 +1,9 @@ -// Tier-2 glue patch tests (Phase 4): committed verbatim patches (09/10) -// equal fresh generator output (deterministic — any hand-edit or fork -// drift goes red); curated forward-ports (07/08/11, see patches/REBASE.md) -// are pinned instead by anti-revert assertions; everything dry-runs clean -// on pristine upstream copies. -import { describe, it, before, after } from 'node:test'; +// Tier-2 glue patch tests: every patch dry-runs clean on pristine +// upstream copies; curated forward-ports (07/08/11, see patches/REBASE.md) +// and additive deltas (09/10) are pinned by assertions on the committed +// patch text itself. Nothing here needs a donor checkout — this repo +// stands alone (fork-independence, 2026-10-11). +import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; @@ -15,8 +15,6 @@ const here = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(here, '..', '..'); const UPSTREAM = process.env.UPSTREAM_DIR || path.join(ROOT, 'review', 'GDevelop'); -const FORK = - process.env.FORK_DIR || path.join(ROOT, 'review', 'GDevelop-Heretek'); const GLUE = [ // Curated forward-ports (patches/REBASE.md): must dry-run clean and must @@ -37,16 +35,17 @@ const GLUE = [ ], noDelete: ['newEffectDialogDefaultTab'], }, - // Verbatim deltas: committed patches equal fresh output byte-for-byte. + // Purely additive deltas: committed patches must contain zero + // minus-lines (any deletion would be an upstream revert by construction). { patch: '09-prepare-ai-content.patch', files: ['newIDE/app/src/AiGeneration/PrepareAiUserContent.js'], - verbatim: true, + additive: true, }, { patch: '10-ai-chat-affordance.patch', files: ['newIDE/app/src/AiGeneration/AiRequestChat/Utils.js'], - verbatim: true, + additive: true, }, { patch: '11-studio-runtime-mount.patch', @@ -63,39 +62,7 @@ function run(cmd, args, cwd) { }); } -describe('tier-2: glue patches are generated, not hand-edited', () => { - let regenDir; - before(() => { - regenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tier2-regen-')); - const r = run( - 'node', - [ - 'scripts/generate-tier2-patches.mjs', - '--upstream', - UPSTREAM, - '--fork', - FORK, - '--out', - regenDir, - ], - ROOT - ); - assert.equal(r.status, 0, `generator failed:\n${r.stdout}\n${r.stderr}`); - }); - after(() => fs.rmSync(regenDir, { recursive: true, force: true })); - - it('committed verbatim patches equal fresh output byte-for-byte', () => { - for (const { patch, verbatim } of GLUE) { - if (!verbatim) continue; - const committed = fs.readFileSync( - path.join(ROOT, 'patches', patch), - 'utf8' - ); - const fresh = fs.readFileSync(path.join(regenDir, patch), 'utf8'); - assert.equal(fresh, committed, `${patch} was hand-edited or is stale`); - } - }); - +describe('tier-2: glue patches apply cleanly and preserve upstream', () => { it('curated forward-ports never delete upstream-new symbols', () => { for (const { patch, noDelete } of GLUE) { if (!noDelete) continue; @@ -112,6 +79,19 @@ describe('tier-2: glue patches are generated, not hand-edited', () => { } }); + it('additive deltas contain zero deletions', () => { + for (const { patch, additive } of GLUE) { + if (!additive) continue; + const text = fs.readFileSync(path.join(ROOT, 'patches', patch), 'utf8'); + const deletions = text.split('\n').filter(l => /^-[^-]/.test(l)); + assert.equal( + deletions.length, + 0, + `${patch} deletes lines (upstream revert by construction):\n${deletions.slice(0, 5).join('\n')}` + ); + } + }); + it('glue dry-runs clean on pristine upstream copies', () => { const target = fs.mkdtempSync(path.join(os.tmpdir(), 'tier2-dry-')); try {