mirror of
https://github.com/Heretek-AI/GDevelop-Unlocked.git
synced 2026-11-07 02:45:58 -10:00
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.
This commit is contained in:
1 parent
45565be5d6
commit
cf7860e05b
13 files changed
+424
-250
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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 `<repo>/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 <scratch>`.
|
||||
- **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.
|
||||
@@ -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 <upstream-checkout-or-extracted-asar>
|
||||
node scripts/inject-launcher.mjs --appdir <extracted-asar-dir>
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
+17
-20
@@ -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 <pristine-upstream>` 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 <pristine-upstream>` 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.
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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 <fork-checkout>
|
||||
```
|
||||
|
||||
`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)
|
||||
|
||||
|
||||
@@ -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)`);
|
||||
@@ -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 <donor-checkout>
|
||||
//
|
||||
// Each patch round-trips (`patch -p1 --dry-run` on a pristine copy)
|
||||
// before it is written. Regeneration is deterministic: committed output
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user