diff --git a/CHANGELOG.md b/CHANGELOG.md index 501c94adbb3..b8523f0f111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.78.0] + +### Added + +- Added a Braze-driven promotional banner to the wallet home screen. (#29301) +- Added a Money Account onboarding flow. (#30137) +- Added a postonboarding checklist on the wallet home shown when the balance is empty. (#28851) +- Added an optional onboarding interest questionnaire after metrics opt-in for eligible users. (#30056) +- Added a "Paid by MetaMask" treatment on the mUSD conversion confirmation screen when MetaMask fully sponsors the network, provider, and gas fees. (#30120) +- Added the Money Account withdrawal flow. (#29862) +- Added mUSD support on Monad. (#29897) +- Added Batch Sell token selection for selling up to five same-network tokens. (#29690) +- Added a sort control to a trader's Open and Closed positions on the Top Traders profile screen. (#30027) +- Added a World Cup promotional banner to the Predict feed. (#30070) +- Added websocket streaming integration for OHLCV data. (#29739) +- Added handling for on-ramp provider return deeplinks so users land directly on their order details after completing or cancelling a purchase with an external provider. (#29858) +- Added an AI disclaimer to the What's Happening section. (#30352) +- Added price-change pills on related assets. (#30259) +- Showed the bonus benefits menu for users with the VIP feature enabled. (#29888) +- Displayed the total benefits count on the Rewards benefits preview header. (#30063) + +### Changed + +- Updated the primary CTA on the Money Account onboarding stepper to read "Add funds". (#29909) +- Updated Predict buy previews to include market fees in totals and balance checks. (#29881) +- Updated Bridge navigation to use the native stack with in-screen headers for Bridge, token selection, and quote selection. (#29829) +- Updated the mUSD bonus calculator in Rewards with a fresh design. (#29758) +- Updated the Rewards "theMiracle" logo to be theme-aware. (#30213) +- Aligned previously base-enabled custom network logos (Stable, Flow, XDC, Fraxtal, Hemi, Plasma, Lukso, Rootstock, MSU, Lens, Plume) to a square format consistent with Popular networks. (#29943) +- Aligned carousel card heights for accessibility. (#30201) +- Improved the empty DeFi state navigation to point to Trending v2. (#29927) +- Improved retry behavior when QR hardware wallet signing scans fail. (#29741) +- Removed gas alerts from the confirmation modal in gasless flows and updated the 10 MON minimum-reserve alert copy. (#29835) + +### Fixed + +- Fixed a regression where gas estimate alerts had stopped showing in confirmations. (#30266) +- Fixed underline positioning in the React Native Scrollable Tab View. (#30133) +- Fixed a bug where failed builder fee approval was permanently cached, causing subsequent Perps orders to fail. (#30095) +- Fixed a bug that could repeatedly prompt hardware wallet users while Perps was idle. (#30114) +- Fixed iOS header inset for Perps order screens. (#30143) +- Fixed a bug where a trader's positions could appear stale on the Top Traders profile and position screens, and added pull-to-refresh on both screens. (#30039) +- Fixed limit order margin calculation to use the limit price instead of the market price, preventing "insufficient margin" errors. (#29800) +- Fixed a bug that could leave the swap quote area blank during slippage refresh. (#29975) +- Fixed the Account List opening too quickly. (#29859) +- Fixed an issue where EIP-7702 authorization signatures with leading zero bytes in `r` or `s` could be rejected by relays and public RPCs. (#29717) + ## [7.77.2] ### Fixed @@ -11510,7 +11557,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957) - [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954) -[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.2...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.0...HEAD +[7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.2...v7.78.0 [7.77.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.77.2 [7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1 [7.77.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.3...v7.77.0 diff --git a/app/components/UI/Money/utils/moneyAccountTransactions.test.ts b/app/components/UI/Money/utils/moneyAccountTransactions.test.ts index 235f7edc01b..721d243646f 100644 --- a/app/components/UI/Money/utils/moneyAccountTransactions.test.ts +++ b/app/components/UI/Money/utils/moneyAccountTransactions.test.ts @@ -12,6 +12,8 @@ import { buildMoneyAccountWithdrawBatch, updateMoneyAccountDepositTokenAmount, updateMoneyAccountWithdrawTokenAmount, + getMoneyAccountDepositTransactionsData, + getMoneyAccountWithdrawTransactionsData, } from './moneyAccountTransactions'; import ReduxService from '../../../../core/redux/ReduxService'; import { selectPrimaryMoneyAccount } from '../../../../selectors/moneyAccountController'; @@ -528,4 +530,165 @@ describe('moneyAccountTransactions', () => { ); }); }); + + describe('getMoneyAccountDepositTransactionsData', () => { + beforeEach(() => { + mockGetProviderByChainId.mockReturnValue(MOCK_PROVIDER as never); + mockSelectMoneyAccountVaultConfig.mockReturnValue(MOCK_VAULT_CONFIG); + ( + jest.mocked(ReduxService) as unknown as { + store: { getState: jest.Mock }; + } + ).store = { getState: jest.fn().mockReturnValue({}) }; + }); + + it('returns two hex calldata strings for a valid amount', async () => { + mockPreviewDeposit.mockResolvedValue(ethers.BigNumber.from('1000000')); + + const result = await getMoneyAccountDepositTransactionsData( + MOCK_CHAIN_ID, + '1.0', + ); + + expect(result).toHaveLength(2); + expect(result[0]).toMatch(/^0x/); + expect(result[1]).toMatch(/^0x/); + }); + + it('returns [] when vault config is missing', async () => { + mockSelectMoneyAccountVaultConfig.mockReturnValue(undefined); + + const result = await getMoneyAccountDepositTransactionsData( + MOCK_CHAIN_ID, + '1.0', + ); + + expect(result).toEqual([]); + expect(mockPreviewDeposit).not.toHaveBeenCalled(); + }); + + it('returns [] when provider is missing', async () => { + mockGetProviderByChainId.mockReturnValue(undefined as never); + + const result = await getMoneyAccountDepositTransactionsData( + MOCK_CHAIN_ID, + '1.0', + ); + + expect(result).toEqual([]); + expect(mockPreviewDeposit).not.toHaveBeenCalled(); + }); + + it('calls previewDeposit with the converted token amount', async () => { + mockPreviewDeposit.mockResolvedValue(ethers.BigNumber.from('1000000')); + + await getMoneyAccountDepositTransactionsData(MOCK_CHAIN_ID, '1.0'); + + // 1.0 with 6 decimals = 1_000_000 + expect(mockPreviewDeposit).toHaveBeenCalledWith( + expect.any(String), + '1000000', + MOCK_VAULT_CONFIG.boringVault, + MOCK_VAULT_CONFIG.accountantAddress, + ); + }); + + it('propagates RPC errors', async () => { + mockPreviewDeposit.mockRejectedValue(new Error('RPC timeout')); + + await expect( + getMoneyAccountDepositTransactionsData(MOCK_CHAIN_ID, '1.0'), + ).rejects.toThrow('RPC timeout'); + }); + }); + + describe('getMoneyAccountWithdrawTransactionsData', () => { + const mockGetState = jest.mocked(ReduxService).store.getState as jest.Mock; + const mockSelectVaultConfig = jest.mocked(selectMoneyAccountVaultConfig); + const mockSelectPrimaryMoneyAccount = jest.mocked( + selectPrimaryMoneyAccount, + ); + const mockGetProvider = jest.mocked(getProviderByChainId); + + const MOCK_MONEY_ACCOUNT_ADDRESS = + '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' as Hex; + const MOCK_RECIPIENT = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as Hex; + + beforeEach(() => { + mockGetState.mockReturnValue({}); + mockSelectVaultConfig.mockReturnValue(MOCK_VAULT_CONFIG); + mockSelectPrimaryMoneyAccount.mockReturnValue({ + address: MOCK_MONEY_ACCOUNT_ADDRESS, + } as ReturnType); + mockGetProvider.mockReturnValue( + MOCK_PROVIDER as ReturnType, + ); + mockGetRate.mockResolvedValue(ethers.BigNumber.from('1000000')); + }); + + it('returns two hex calldata strings for a valid amount', async () => { + const result = await getMoneyAccountWithdrawTransactionsData( + MOCK_CHAIN_ID, + '1.0', + MOCK_RECIPIENT, + ); + + expect(result).toHaveLength(2); + expect(result[0]).toMatch(/^0x/); + expect(result[1]).toMatch(/^0x/); + }); + + it('encodes the recipient address in the transfer calldata', async () => { + const result = await getMoneyAccountWithdrawTransactionsData( + MOCK_CHAIN_ID, + '1.0', + MOCK_RECIPIENT, + ); + + expect(result[1].toLowerCase()).toContain( + MOCK_RECIPIENT.toLowerCase().slice(2), + ); + }); + + it('returns [] when vault config is missing', async () => { + mockSelectVaultConfig.mockReturnValue(undefined); + + const result = await getMoneyAccountWithdrawTransactionsData( + MOCK_CHAIN_ID, + '1.0', + MOCK_RECIPIENT, + ); + + expect(result).toEqual([]); + expect(mockGetRate).not.toHaveBeenCalled(); + }); + + it('returns [] when primary money account is missing', async () => { + mockSelectPrimaryMoneyAccount.mockReturnValue(undefined); + + const result = await getMoneyAccountWithdrawTransactionsData( + MOCK_CHAIN_ID, + '1.0', + MOCK_RECIPIENT, + ); + + expect(result).toEqual([]); + expect(mockGetRate).not.toHaveBeenCalled(); + }); + + it('returns [] when provider is missing', async () => { + mockGetProvider.mockReturnValue( + undefined as unknown as ReturnType, + ); + + const result = await getMoneyAccountWithdrawTransactionsData( + MOCK_CHAIN_ID, + '1.0', + MOCK_RECIPIENT, + ); + + expect(result).toEqual([]); + expect(mockGetRate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/app/components/UI/Money/utils/moneyAccountTransactions.ts b/app/components/UI/Money/utils/moneyAccountTransactions.ts index 506cce3069f..0e2c5549095 100644 --- a/app/components/UI/Money/utils/moneyAccountTransactions.ts +++ b/app/components/UI/Money/utils/moneyAccountTransactions.ts @@ -296,6 +296,100 @@ export async function updateMoneyAccountWithdrawTokenAmount( ]; } +/** + * Returns encoded calldata for the approve + deposit batch of a Money Account deposit. + * + * @param chainId - Chain ID in hex + * @param amountHuman - Human-readable deposit amount (e.g. "10.5") + * @returns `[approveData, depositData]`, or `[]` if vault config or provider is unavailable + */ +export async function getMoneyAccountDepositTransactionsData( + chainId: Hex, + amountHuman: string, +): Promise { + const vaultConfig = selectMoneyAccountVaultConfig( + ReduxService.store.getState() as RootState, + ); + if (!vaultConfig) return []; + + const provider = getProviderByChainId(chainId); + if (!provider) return []; + + const musdAddress = getMoneyAccountDepositAssetAddress(chainId); + const amount = BigInt( + calcTokenValue(amountHuman, MUSD_DECIMALS) + .decimalPlaces(0, BigNumber.ROUND_UP) + .toFixed(0), + ); + const minimumMint = + amount === 0n + ? 0n + : applySlippage( + await getExpectedDepositShares({ + lensAddress: vaultConfig.lensAddress, + boringVault: vaultConfig.boringVault, + accountantAddress: vaultConfig.accountantAddress, + musdAddress, + amount, + provider, + }), + ); + + const approveData = buildApproveData(vaultConfig.boringVault, amount); + const depositData = buildDepositData(musdAddress, amount, minimumMint); + + return [approveData, depositData]; +} + +/** + * Returns encoded calldata for the withdraw + transfer batch of a Money Account withdrawal. + * + * @param chainId - Chain ID in hex + * @param amountHuman - Human-readable withdrawal amount (e.g. "10.5") + * @param recipient - EVM address to receive the withdrawn USDC + * @returns `[withdrawData, transferData]`, or `[]` if vault config or provider is unavailable + */ +export async function getMoneyAccountWithdrawTransactionsData( + chainId: Hex, + amountHuman: string, + recipient: Hex, +): Promise { + const state = ReduxService.store.getState() as RootState; + const vaultConfig = selectMoneyAccountVaultConfig(state); + const primaryMoneyAccount = selectPrimaryMoneyAccount(state); + if (!vaultConfig || !primaryMoneyAccount?.address) return []; + + const provider = getProviderByChainId(chainId); + if (!provider) return []; + + const musdAddress = getMoneyAccountDepositAssetAddress(chainId); + const amount = BigInt( + calcTokenValue(amountHuman, MUSD_DECIMALS) + .decimalPlaces(0, BigNumber.ROUND_UP) + .toFixed(0), + ); + const shareAmount = + amount === 0n + ? 0n + : getSharesForWithdrawal( + amount, + await getVaultRate({ + accountantAddress: vaultConfig.accountantAddress, + provider, + }), + ); + + const withdrawData = buildWithdrawData( + musdAddress, + shareAmount, + amount, + primaryMoneyAccount.address, + ); + const transferData = buildErc20TransferData(recipient, amount); + + return [withdrawData, transferData]; +} + // -- Withdrawal helpers ---------------------------------------------------- async function getVaultRate({ diff --git a/scripts/perps/agentic/README.md b/scripts/perps/agentic/README.md index 494ecbb4742..4e6193260f6 100644 --- a/scripts/perps/agentic/README.md +++ b/scripts/perps/agentic/README.md @@ -224,7 +224,7 @@ Compound: `{ all: [...] }`, `{ any: [...] }`, `{ none: [...] }`. | `rebuild-native` | no | yes (no `--repo-update`) | yes | no | | `clean` (legacy `--clean`) | yes | yes with `--repo-update` | yes | no (writes only) | -Cache lives in `$MM_BUILD_CACHE_DIR` (default `~/Library/Caches/mm-mobile-builds` on macOS, `~/.cache/mm-mobile-builds` on Linux), keyed by `@expo/fingerprint` hash. Parallel worktrees at the same fingerprint share one artifact through a per-fingerprint mutex: Linux uses `flock(1)` (auto-released by the kernel on process death); macOS, where `flock` is not in base, uses an atomic `mkdir .lock.d` fallback that is released by the script's `EXIT` trap. If a script is killed with `kill -9` between `mkdir` and the trap, the mutex dir can be left behind — delete it manually under `$MM_BUILD_CACHE_DIR//`. Override retention with `BUILD_CACHE_RETAIN=N` (default 5 per platform). +Cache lives in `$MM_BUILD_CACHE_DIR` (default `~/Library/Caches/mm-mobile-builds` on macOS, `~/.cache/mm-mobile-builds` on Linux), keyed by an agentic `@expo/fingerprint` hash computed by `scripts/perps/agentic/lib/compute-cache-fp.js`. The agentic fingerprint *extends* the project-wide `fingerprint.config.js` (which EAS Build and OTA still consume unchanged) with additional `ignorePaths` for per-worktree build artifacts that don't influence binary semantics (`ios/build/`, `.gradle/`, Xcode `xcuserdata`, NDK `.cxx`, etc.). Binary-affecting inputs — env-populated `xcconfig`, `google-services.json`, and the bundled `InpageBridgeWeb3.js` — stay hashed, so the cache only converges across worktrees when those inputs match. Parallel worktrees at the same fingerprint share one artifact through a per-fingerprint mutex: Linux uses `flock(1)` (auto-released by the kernel on process death); macOS, where `flock` is not in base, uses an atomic `mkdir .lock.d` fallback that is released by the script's `EXIT` trap. If a script is killed with `kill -9` between `mkdir` and the trap, the mutex dir can be left behind — delete it manually under `$MM_BUILD_CACHE_DIR//`. Override retention with `BUILD_CACHE_RETAIN=N` (default 5 per platform). Invoke directly: diff --git a/scripts/perps/agentic/lib/build-cache.sh b/scripts/perps/agentic/lib/build-cache.sh index 839288390b5..397d552ee28 100644 --- a/scripts/perps/agentic/lib/build-cache.sh +++ b/scripts/perps/agentic/lib/build-cache.sh @@ -78,7 +78,10 @@ bc_fingerprint() { fi fi local fp - fp=$(node scripts/generate-fingerprint.js 2>/dev/null || true) + # Use the agentic fingerprint. It extends the project's fingerprint.config.js + # (so EAS/OTA inputs still participate) with additional ignorePaths for + # per-worktree build outputs. See compute-cache-fp.js for the rationale. + fp=$(node scripts/perps/agentic/lib/compute-cache-fp.js 2>/dev/null || true) if [ -z "$fp" ]; then return 1 fi diff --git a/scripts/perps/agentic/lib/compute-cache-fp.js b/scripts/perps/agentic/lib/compute-cache-fp.js new file mode 100644 index 00000000000..6296495021c --- /dev/null +++ b/scripts/perps/agentic/lib/compute-cache-fp.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +// compute-cache-fp.js — agentic-local native-build fingerprint for the +// shared build cache. +// +// Relation to the project fingerprint: +// The repo-wide `scripts/generate-fingerprint.js` is consumed by EAS Build, +// EAS Update, and the OTA fingerprint guard in +// `docs/nightly-ota-updates.md`. Its `fingerprint.config.js` deliberately +// errs on the side of hashing too much — every local build artifact that +// could conceivably influence the produced binary participates in the key +// so a hash collision can never reuse a build whose inputs we cannot +// vouch for. +// +// `@expo/fingerprint`'s `createFingerprintAsync(projectRoot, options)` +// loads `fingerprint.config.js` and applies caller options with these +// semantics (per `@expo/fingerprint` 0.15.x): +// - `extraSources`: caller OVERRIDES the config's list when set. +// - `ignorePaths`: caller is MERGED with the config's list. +// To stay in sync with future edits to `fingerprint.config.js`, we +// `require()` it directly and spread its lists into our options. Our +// added ignorePaths cover per-worktree dev/build artifacts that don't +// affect binary semantics (compile outputs, IDE state, NDK cache, +// per-machine `.xcode.env.local`). +// Binary-affecting inputs — env-populated xcconfig, `google-services.json`, +// the bridge source — stay hashed. The cache only converges across +// worktrees when those inputs match, which is the correct behaviour. + +const fp = require('@expo/fingerprint'); +// Import the project's config so future additions to its extraSources +// automatically flow into the agentic fingerprint. Using require here +// (vs. literally copying the list) means a new entry in +// `fingerprint.config.js` cannot silently leave the agentic cache +// behind. +const projectConfig = require('../../../../fingerprint.config.js'); + +const options = { + // Inherit the project's extraSources and append the runtime JS bridge + // source. The bridge is copied into android/ios assets at build time + // and embedded in the .jsbundle, so its content affects binary output. + extraSources: [ + ...(projectConfig.extraSources || []), + { + type: 'file', + filePath: 'app/core/InpageBridgeWeb3.js', + reasons: ['Bundled into the runtime JS — affects binary behaviour.'], + }, + ], + // Per-worktree dev/build state that does not influence the produced + // binary. All are gitignored and regenerated locally; ignoring them + // lets two slots on the same commit with the same source env share a + // cached `.app`/`.apk`. + ignorePaths: [ + 'ios/build/**', + 'ios/.xcode.env.local', + 'ios/MetaMask.xcworkspace/xcshareddata/swiftpm/**', + 'ios/**/xcuserdata/**', + 'android/.gradle/**', + 'android/app/.cxx/**', + 'android/app/build/**', + // Mirror of app/core/InpageBridgeWeb3.js — already tracked via the + // extraSources entry above; ignore the generated copy so we don't + // double-count it on rebuild. + 'android/app/src/main/assets/InpageBridgeWeb3.js', + ], +}; + +fp.createFingerprintAsync(process.cwd(), options) + .then(({ hash }) => { + process.stdout.write(hash); + }) + .catch((err) => { + process.stderr.write(`compute-cache-fp: ${err.message}\n`); + process.exit(1); + }); diff --git a/scripts/perps/agentic/lib/test-build-cache.sh b/scripts/perps/agentic/lib/test-build-cache.sh index 338c31f8080..8608f21f028 100644 --- a/scripts/perps/agentic/lib/test-build-cache.sh +++ b/scripts/perps/agentic/lib/test-build-cache.sh @@ -184,6 +184,74 @@ echo "$out" | grep -qE "Mode:.*clean.*yarn setup" && pass "clean mode header ren out=$(_capture_for 10 bash scripts/perps/agentic/preflight.sh --clean --check-only 2>&1 | head -20 || true) echo "$out" | grep -qE "Mode:.*clean.*yarn setup" && pass "legacy --clean still maps to clean" || fail "legacy --clean broken" +# ─── 10b. Agentic fp respects the safe/unsafe ignorePath boundary ── +# compute-cache-fp.js ignores per-worktree build outputs but MUST keep +# binary-affecting inputs (xcconfig, google-services.json, the bundled +# InpageBridgeWeb3 source) hashed. Verify both halves of that contract. +hdr "agentic fp respects ignorePath boundary" + +_capture_fp() { + bc_memo_cleanup 2>/dev/null || true + bc_memo_init + bc_fingerprint 2>/dev/null +} + +FP_BASELINE=$(_capture_fp) +[ -n "$FP_BASELINE" ] && pass "baseline fp computed: ${FP_BASELINE:0:12}" \ + || fail "baseline fp empty" + +# (a) Poisoning an IGNORED path must NOT change fp. +mkdir -p ios/build +POISON_IGNORED="ios/build/__bc_test_poison_$$.bin" +echo "poison-$RANDOM" > "$POISON_IGNORED" +FP_IGNORED=$(_capture_fp) +rm -f "$POISON_IGNORED" +if [ "$FP_BASELINE" = "$FP_IGNORED" ]; then + pass "ignored ios/build/ poison did NOT shift fp" +else + fail "ignored ios/build/ poison SHIFTED fp (drift): $FP_BASELINE -> $FP_IGNORED" +fi + +# Restore-trapped poison helper: backs up the file, layers a temporary +# EXIT trap that restores the file AND re-invokes the suite-level cleanup, +# then restores the original suite-level trap before returning. Ensures +# .agent/build-cache cleanup still runs on early abort inside the helper. +_poison_must_shift_fp() { + local label="$1" path="$2" + if [ ! -f "$path" ]; then + fail "missing $path — cannot run boundary test" + return + fi + local bak="/tmp/__bc_test_$(basename "$path")_$$.bak" + cp "$path" "$bak" + # Capture the suite-level EXIT trap so we can re-install it after. + local prev_trap + prev_trap=$(trap -p EXIT) + trap "cp '$bak' '$path' 2>/dev/null; rm -f '$bak' 2>/dev/null; cleanup" EXIT + echo "// __bc_test_poison_$$ $RANDOM" >> "$path" + local fp_after + fp_after=$(_capture_fp) + cp "$bak" "$path" + rm -f "$bak" + # Restore the suite-level cleanup trap. + eval "${prev_trap:-trap - EXIT}" + if [ "$fp_after" != "$FP_BASELINE" ]; then + pass "$label DID shift fp (${fp_after:0:12})" + else + fail "$label was silently ignored — cache could serve stale binary" + fi +} + +# (b) Poisoning a HASHED, binary-affecting path MUST change fp. +_poison_must_shift_fp "InpageBridgeWeb3.js (bridge source)" "app/core/InpageBridgeWeb3.js" + +# (c) Poisoning an inherited project extraSource MUST change fp — proves +# the script repeats fingerprint.config.js extraSources correctly. +_poison_must_shift_fp "scripts/setup.mjs (project extraSource)" "scripts/setup.mjs" + +# Restore baseline state for the rest of the suite. +_capture_fp >/dev/null + # ─── 11. Memo cleanup refuses inherited / unowned BC_MEMO_DIR ────── # Across R6/R7/R8/R9 codex flagged five attack shapes against the memo # directory cleanup. Each scenario sets up a "victim" dir, hands its path @@ -215,7 +283,7 @@ _memo_attack "R9B: EXIT cleanup on inherited memo" "" # Codex R2 B3: --mode fast must hard-fail if the fingerprint command can't # run, instead of silently falling through to the legacy build path. hdr "preflight --mode fast / fingerprint failure" -FP_SCRIPT="scripts/generate-fingerprint.js" +FP_SCRIPT="scripts/perps/agentic/lib/compute-cache-fp.js" FP_BACKUP="${FP_SCRIPT}.test-bak-$$" mv "$FP_SCRIPT" "$FP_BACKUP" restore_fp() { [ -f "$FP_BACKUP" ] && mv "$FP_BACKUP" "$FP_SCRIPT" 2>/dev/null || true; }