Repository navigation
fix: validate managed local file descriptors (API-733) - #731
Conversation
nansen-pr-reviewer summaryThe diff adds substantial local-file hardening, quote claiming, and fail-closed bridge execution markers, with dedicated security coverage and a changeset. The previously reported authentication-directory permission issue is withdrawn because the current implementation applies ownership and write-permission checks to directories as well as files. No new correctness or security defect is verified from the reviewed code, but the CI workflow change requires requesting changes under the repository review policy. Deterministic check: success — No issues found Risk: 4/5 (High) — raised by: CI/build change; security-sensitive paths (auth/crypto/token/session); complexity: 685 added lines Token usage: 122,003 input, 2,518 output, 17,672 cache read | Usage Guide Cooldown: for the next 10 minutes (counting from when this review finished), new pushes to this PR will not trigger another review — the next push after the window expires will. Need a fresh review sooner? Comment |
Codier
left a comment
There was a problem hiding this comment.
Independent first-round review at 8ec5eb16d31d7d4187378a9d15e5ad9e2dcb2e11. Changes requested.
Reviewer A: Claude Opus 5.5, requested high; effective effort observation unavailable. Session ef8caeef-49be-4bc1-8477-07ea0792ad0d.
Opus A1 P2 — openLocalFile lstat-checks the storage root itself (directories = [base], line 20; checkStat on lines 22-26), so a /.nansen that is a symlink is now refused. This contradicts the comment on lines 12-13, which calls the root a trusted CLI path. Quote, bridge, tx-record and cache reads pass root=/.nansen with privateFile=false (trading.js:458/517/597/625/651, bridge.js:250/296, cache-inspect.js:90). On base those reads worked through a symlinked root, and the write paths (saveQuote, saveBridgeQuote, saveTxRecord, setCachedResponse) still follow it. A user who keeps ~/.nansen as a symlink, for example to persistent storage in an agent container or a synced dotfiles directory, therefore gets: trade quote saves a quote that trade execute/bridge execute always refuse; loadTxRecord silently returns null, so bridge-status loses its aggregator hint; cleanupQuotes can never read and expire quote files, so they pile up; and cache stats reports 0 entries while the response cache is still read and written. The brief requires quote claims, bridge quotes and cache stats to keep working. Under the threat boundary in docs/local-file-security.md, refusing a symlinked root adds no protection: anyone able to replace ~/.nansen with a symlink can write regular files there. Requested fix: for these non-private stores, resolve or follow the root once (realpath/stat) and enforce the no-link and identity checks only below it. If refusal is intended, refuse at save time with an error that names the path, so the CLI never writes state it will later reject. Auth reads (privateFile) already matched base's login/prepare refusal of a symlinked directory; that part is out of scope for this finding.
Evidence: Probe on a base vs head source copy, with HOME a temp dir whose .nansen is a symlink to a real 0700 directory (/tmp/a733/probe-bridge-cache.mjs symroot). Base: bridgeLoad=bridge, swapQuote=6 keys, stats responses:1,cost-map:1. Head: bridgeLoad, bridgeReload and swapQuote all ERR LOCAL_FILE_UNSAFE, stats responses:0,cost-map:0,update-check:0 although setCachedResponse had just written an entry. CLI run (DO_NOT_TRACK=1 NANSEN_NO_TELEMETRY=1 NO_UPDATE_NOTIFIER=1 NANSEN_BASE_URL=http://127.0.0.1:9 NANSEN_API_KEY=dummy node src/index.js trade execute --quote 1700000000000-abcd1234 --dry-run) with a valid quote saved under the symlinked root: base reached quote processing (code NO_TRANSACTION); head returned {"error":"Error: Refusing an unsafe local file. ...","code":"LOCAL_FILE_UNSAFE"}. cleanupQuotes swallows read errors (trading.js:652), so unreadable quote files are never unlinked.
Opus A2 P3 — The single LOCAL_FILE_UNSAFE message ('Refusing an unsafe local file. Restore a regular file inside the CLI storage directory.') names no path and no reason: symlink, hard link, symlinked directory, mode/owner, size limit, or a replacement during open. It reaches users unchanged through trade execute (trading.js:4671-4673 wraps it as 'Error: ...' with code LOCAL_FILE_UNSAFE) and bridge execute (loadBridgeQuote is not caught). In the A1 case the cause is a symlinked directory, not a file, so 'restore a regular file' points the user at the wrong fix. This breaks the AGENTS.md rule that errors must be actionable. Requested fix: include the rejected path and which check failed, and keep code LOCAL_FILE_UNSAFE.
Evidence: Head CLI output above: {"success":false,"error":"Error: Refusing an unsafe local file. Restore a regular file inside the CLI storage directory.","code":"LOCAL_FILE_UNSAFE","status":null}. The quote file itself was a regular 0600 file with nlink 1; the only defect was the symlinked ~/.nansen.
Opus limitations: ["No Windows execution evidence. The PR adds Windows-reachable checks that every saved-config read runs: readAuthConfig on each NansenAPI construction, plus quote, bridge and cache reads. These are the nlink === 1 check and dev/ino equality between fs.lstatSync and fs.fstatSync, and on Windows O_NOFOLLOW is absent. CI runs only on ubuntu-latest, the security tests skip the win32 mode cases, and I could only run on Linux (Node 22.23.3). If libuv's path-stat and handle-stat return different dev/ino on Windows (for example on ReFS/Dev Drive or with the newer fast-stat API), every Windows user with a saved ~/.nansen/config.json would get AUTH_STATE_INVALID and every quote execute would fail. Run local-file-security.test.js plus a saved-key nansen research / trade quote\u2192trade execute --dry-run smoke test on a Windows runner before merge. macOS (the qualified browser-login platform) was also not executed but uses standard POSIX lstat/fstat semantics."]
Dedicated independent security review: Factory DeepSeek V4 Pro, max, session 369b0a1c-3018-4df1-8f14-f529729729a9. This evidence is DeepSeek’s, separately from Opus’s report.
Security A1 P3 — Non-security robustness regression: the pre-open identity check rejects a legitimate concurrent atomic rename of ~/.nansen/config.json, failing the whole command with a misleading AUTH_STATE_INVALID. readAuthConfig (src/auth-credentials.js:44) is called unlocked by every command, while login/logout/session-renewal commit config.json through auth-state.atomic() (src/auth-state.js:111-113), which renames a new inode over the path. When the reader's pre-open lstat and open straddle that rename, openLocalFile's before && !sameFile(before, opened) (src/local-file.js:34) throws LOCAL_FILE_UNSAFE, readAuthConfig maps it to configError 'unreadable', and resolveCredential returns kind 'invalid', so assertUsableSelection (src/auth-credentials.js:65) throws AUTH_STATE_INVALID: Saved authentication is unreadable. Restore config.json or run nansen logout after repairing its permissions. The file is a valid 0600 config; nothing is wrong with it. Base 7b54c0e read the new inode and succeeded, so this is introduced by the patch. It fails closed (no other credential is selected, nothing is signed), is transient (a retry succeeds), and the window is microseconds, but the error invites an unnecessary nansen logout (which does remove secure-store credentials). Same root cause on quotes: a read racing claimQuoteForExecution's rename (src/trading.js:597, 517) reports ENOENT/LOCAL_FILE_UNSAFE instead of the 'claimed by another execution' refusal, though the claim is still correctly refused. Requested correction: treat an identity mismatch as retryable for the unlocked config read (bounded retry that re-reads the new inode), or otherwise distinguish a stable attacker replacement from a concurrent CLI rename, and keep the current fail-closed behavior as the final outcome. This finding does not change the security verdict: the changed boundary correctly rejects links and replacements and selects no wrong credential.
Evidence: Reproduced deterministically with real modules: /tmp/api733-probe/race.mjs hooks fs.openSync so a writer commits rename(config.json.new, config.json) (same content, same 0600 mode, new inode) between the reader's pre-open lstat and open. Output: configError: unreadable, selected: {"kind":"invalid","source":"config","error":"unreadable"}, surfaced: AUTH_STATE_INVALID: Saved authentication is unreadable. Restore config.json or run nansen logout after repairing its permissions. The writer's own test asserts the same mapping for a replacement at open (src/tests/local-file-security.test.js, 'reads no secret bytes when config changes to a %s at open'), confirming the rejection is by design for any inode change, including a benign one. Base comparison (/tmp/api733-probe/base-probe.mjs against 7b54c0e extracted by git archive) read through the same race without error.
Security limitations: ["A1 is the only finding and it is not a security defect: the changed boundary fails closed, selects no wrong credential, and no unintended read was reachable through any changed path in my probes.", "Pre-existing link-following reads outside the changed files remain and were not fixed (not regressions, not reachable through the changed paths; the patch's docs do not claim them): src/api.js:352 response-cache payload read (the strongest residual: a planted symlink or regular file at ~/.nansen/cache/.json is parsed and returned as a cached API response), src/cost-cache.js:49/85, src/update-check.js:60/90/160, src/limit-order.js:78 (limit-order JWT; requires a matching walletPubkey and unexpired timestamp to be used, no echo), src/keychain.js:134/242 (wallet .credentials; wrong password fails decryption), src/doctor.js:288 (config shadow comparison; comparison result only, no content output). Exploiting any of them requires write access to the user's ~/.nansen, which the CLI creates 0700, so it is outside the declared scope \u2014 flag for a follow-up decision because the assignment's intent mentions cache reads.", "Windows was not exercised (no Windows runner in this environment). O_NOFOLLOW is absent there and the guard is the lstat/identity comparison; the writer's test simulates the no-O_NOFOLLOW case and docs/local-file-security.md discloses the Windows limits.", "No real OS keychain, Privy wallet, funded wallet, real credentials, or production endpoint was used (per assignment). The secure-store worker was exercised only through its guard process with a synthetic lock plus the mocked suites, so keychain-specific behavior under the changed worker guard is untested here.", "Deliberate, documented tightening beyond link/replacement rejection: auth reads now also require the config file and its directories to be user-owned and not group/other-writable (privateFile in auth-credentials.js:44 and auth-state.js:29/99/133). A user whose ~/.nansen is 0775/0777 previously could read a saved key for read-only commands and now gets AUTH_STATE_INVALID until they chmod; mutation paths already enforced this. Not a gap, but it is user-visible release-note material.", "No funded-wallet E2E, no trades, no production API calls (explicitly excluded). One CLI probe (nansen doctor) runs a built-in API reachability check; it timed out after 5000 ms in this sandbox with no successful production contact."]
Both reviewers independently ran the full suite (110 files, 4554 passed, 9 skipped), lint, and targeted real-filesystem probes. No production security test or trade was authorized. The writer is repairing these findings and the bridge marker replay concern from the GitHub check; a subsequent exact-head Gemini review and security verification are required.
|
Repairs pushed in 0eeba63. Opus A1: non-private symlinked storage roots remain supported, with descendant link checks and root identity recheck; quotes, bridge, tx records and cache statistics have a real-filesystem regression test. A2: unsafe-file errors identify the path and failed check. DeepSeek A1: unlocked config reads revalidate once after an inode change; a valid atomic update succeeds and ongoing replacement still reads no bytes. The bridge marker issue now has a durable execution claim and required flushed marker. Windows Node 22/24 CI now exercises security fixtures and a real CLI saved-key research plus quote-to-dry-run journey using loopback fixtures. Linux full suite: 111 files, 4562 passed, 9 skipped; lint clean. Windows results and independent Gemini/DeepSeek final-head verification remain pending. First-round evidence. |
|
@nansen-pr-reviewer[bot] re-review |
Codier
left a comment
There was a problem hiding this comment.
Independent reviewer B evidence at 1c89eea7168e57a8c52c954f045ef89518b4f8ca. Gemini 3.1 Pro through Factory, requested and observed high, session a90b8cfd-d493-4f27-907d-a470f5578324. Verdict: clean, no findings or merge-safety limitations.
The first clean report omitted runtime evidence; one bounded same-role clarification on the unchanged head completed it. Both original reports are retained.
-
npx vitest run src/__tests__/local-file-security.test.js src/__tests__/local-file-platform.test.js src/__tests__/bridge-broadcast-failclosed.test.js src/__tests__/bridge-quote.test.js src/__tests__/auth-store-lifetime.test.js: 5 files passed, 73 tests passed. Dead code semantically audited (imports/exports/options/branches live, removal leaves no fallback) and BI telemetry verified. -
grep search for openLocalFile, readLocalFile, writeLocalFile, localFileRace, readQuote, keepClaimed: Verified that local-file helpers are correctly consumed by auth, cache-inspect, trading, and bridge systems. readQuote injection and keepClaimed logic confirms robust protection against quote re-use without orphaned variables. -
grep search in src/cli.js and src/telemetry.js for cli_command_failed and LOCAL_FILE_UNSAFE: Confirmed BI behavior. LOCAL_FILE_UNSAFE and QUOTE_STATE_UNSAFE route through existing error_code properties in trackCommandFailed without requiring any schema changes or unsupported new events. -
GitHub configuration check via CODEOWNERS and .github/workflows: Verified that adding the Windows execution prerequisite in the CI YAML does not violate any required CODEOWNERS signoffs and passes on windows-latest.
Exact-head CI passes: Linux Node 22/24 each 111 files, 4569 tests passed, two skipped; Windows Node 22/24 each two files, 28 passed, two POSIX-only skips. Lint, native package checks, CodeQL, Aikido and nansen-review pass. Publishing jobs correctly skip on this PR. The bot withdrew its directory-permission finding; its generic CI policy statement identifies no additional required owner in repository instructions or CODEOWNERS, as Gemini verified.
Separate dedicated security verification by DeepSeek V4 Pro max, session 0af0b877-1f85-4412-9a88-ef1b028b1e29, reports no defect and passed 47 focused tests plus independent real-filesystem and denied CLI probes. Its report preserves the following limitations. A bounded clarification is deciding their merge-safety classification; this COMMENT does not publish approval.
-
Journal metadata read (doctor renewalStatus, metadata-only and unlocked) does not retry an inode mismatch: a concurrent login/journal atomic rewrite racing it can transiently report 'metadata_unreadable' (reproduced with a real atomic rename between lstat and open). Fail-closed, no secret bytes read, no credential selection; operational journal reads run under the auth lock. The scoped retry was for the unlocked config read, which is verified fixed.
-
The config identity retry is single-shot: two replacements in a row, or an unlink race (ENOENT), still fail closed with AUTH_STATE_INVALID and the existing repair message.
-
No openat: an ABA ancestor-swap by a same-user process with rename rights remains theoretically possible (documented in docs/local-file-security.md); probes and the real-race suite show no read of rejected targets through the changed paths, and same-user write access to ~/.nansen already permits planting quote files.
-
Windows is verified through CI on NTFS only (Node 22 and 24, real saved-key research plus quote dry-run and descriptor fixtures). ReFS/Dev Drive lstat/fstat dev/ino semantics are untested; a mismatch there would fail closed with AUTH_STATE_INVALID/LOCAL_FILE_UNSAFE rather than read an unsafe file.
-
macOS was not executed (POSIX lstat/fstat semantics assumed; browser-login is the qualified macOS platform).
-
Pre-existing out-of-scope reads remain unchanged by design: api.js response-cache payload read, cost-cache, update-check, limit-order JWT and keychain wallet credentials. They are not reachable through the changed API-733 paths and are not regressions.
-
The stricter POSIX authentication permissions are user-visible (a group/other-writable or foreign-owned auth path now yields AUTH_STATE_INVALID); the changeset documents this for the release notes.
Codier
left a comment
There was a problem hiding this comment.
Approval forwards independent final-head evidence at 1c89eea7168e57a8c52c954f045ef89518b4f8ca. The OpenAI Codex writer is the author and coordinator; these conclusions come from the selected external reviewers.
- Google Gemini 3.1 Pro through Factory, requested/observed high, session
a90b8cfd-d493-4f27-907d-a470f5578324: complete clean B report and one bounded unchanged-head clarification. Five focused files, 73 tests pass, semantic dead-code and BI audits confirmed. Posted B evidence. - DeepSeek V4 Pro through Factory, requested/observed max, session
d83260de-8ce6-4ec6-ac2b-555dbb41ae9c: clean dedicated security verification and one bounded unchanged-head classification clarification. Four focused files, 47 tests pass, plus independent auth replacement, denied quote/tx reads, root swap, claim lifecycle and real CLI denial probes. All seven prior limits were assessed by the security reviewer itself; no findings, merge-safety limitations or rollout requirements remain. - Claude Opus 5.5 initial review requested high; effective effort observation unavailable. Its root-symlink compatibility and actionable-error findings, plus the initial DeepSeek atomic-config-update finding, were repaired and verified by the final-head reviewers. Initial findings.
At this exact head, Linux Node 22/24 CI each passes 111 files, 4569 tests, two skipped. Windows Node 22/24 each passes both security/CLI files, 28 tests, two POSIX-only skips. Windows executes real saved-key research plus quote creation and dry-run through a loopback mock. Lint, four native-package checks, CodeQL, Aikido and the bot deterministic check pass; release jobs correctly skip. All 18 current check runs are complete and successful or intentionally skipped. The isolated hard-link mutation fails when the load-bearing nlink guard is removed.
Bridge execution now claims and flushes the quote before signing. Required post-broadcast marker read/write/fsync failures retain the claim; a second invocation cannot sign again. Existing definite pre-broadcast retries remain usable. Bot replay feedback is closed and directory-permission feedback withdrawn. Unsafe file errors name the path and reason. Non-private symlinked roots still work; private authentication roots and descendants retain ownership/permission checks. A legitimate atomic config rename is revalidated once, while continuous replacement reads no bytes.
The dedicated security clarification preserves these boundaries in its checks, with explicit reasons they do not block this scoped merge:
- An unlocked doctor journal-metadata race can temporarily report metadata_unreadable. Operational journal reads are locked, this status changes no credentials or persistent state, and the next doctor run clears it.
- A second config replacement or unlink fails closed and is retryable. The unlink outcome already existed in the base.
- Same-OS-user control of regular files or ancestor directories is outside the documented guarantee; this does not provide openat traversal.
- Windows NTFS is verified. ReFS and macOS were not executed. Identity disagreement rejects before reading, and a POSIX directory flush failure aborts before signing. The directory-flush pattern already ships in authentication writes on the qualified macOS browser-login path.
- Payload cache, wallet decryption, update-check and other unchanged reads remain separate hardening scope.
- Stricter POSIX authentication permissions are documented in the patch changeset and fail with a repair message.
Both final reviewers confirmed existing succeeded/failed BI events and error_code properties retain their meaning; no signal or catalog companion is needed. Repository inspection found no additional owning-team or CODEOWNERS gate. The bot statement about CI edits describes its automation policy; the new read-only Windows job adds a package-publish prerequisite, and its current check passes.
No outstanding rollout requirement. This approval does not merge, publish a package, deploy a server or install a watcher. Main and reviewer execution metadata, provider usage snapshots and scope notes are in the PR description; original reports and clarifications remain retained.
Saved authentication and quote files could follow symlinks or change between a path check and a read. This can load an unrelated file as an API key, journal, or trading quote. This PR validates the opened descriptor before reading and fails closed for unsafe managed state. Fixes API-733.
Bridge execution claims and flushes its quote before signing. If a post-broadcast marker cannot be safely read, written or flushed, the quote stays claimed and a second execution is refused. Non-private storage roots may still be symlinked; authentication roots remain link-free. A legitimate atomic config update is revalidated once before reading. Windows Node 22/24 CI adds filesystem tests and a saved-key research/quote/dry-run CLI smoke test against a loopback mock server.
The local-file security note traces every scanner path and documents platform limits. These checks do not sandbox another process running as the same OS user. Explicit file imports keep their documented behavior.
Validation
At current head
1c89eea7168e57a8c52c954f045ef89518b4f8ca, Linux CI Node 22 and 24 each pass 111 files, 4569 tests, and two skips. Windows Node 22 and 24 each pass both filesystem/CLI smoke files, 28 tests, and two POSIX-only skips. The output above is the local run, where optional shell completion dependencies are absent. All CI checks pass, including lint, native-package checks, CodeQL, Aikido and the bot deterministic check.npm run lintandgit diff --checkpass. The 29 security tests use real temporary files and production functions, including the real secure-store worker. The existing auth-process, store-lifetime, quote claim/reuse, bridge, and cache tests pass. An isolated mutation that removes hard-link validation causes the hard-link proof to fail.BI: existing
cli_command_succeededandcli_command_failedevents retain their meaning. Authentication denial uses the existing auth error categories. No event or property changed, and no new BI signal or catalog update is needed.No research-agent tool, prompt, deployment, or other team's behavior changes. CI uses Changesets for package releases; this PR adds a patch changeset without changing the package version. No merge or deployment is part of this task.
Independent reviews
Opus 5.5 high-requested and DeepSeek V4 Pro max first-round findings were repaired. Gemini 3.1 Pro high B review and dedicated DeepSeek V4 Pro max security verification are clean at
1c89eea7168e57a8c52c954f045ef89518b4f8ca. Each used a separate detached checkout. B ran 73 focused tests and confirmed the dead-code/BI checks; security ran 47 focused tests plus independent denied-path and race probes. Each used one bounded unchanged-head clarification. DeepSeek explicitly classified every previously listed platform/scope limit as nonblocking in its retained checks; no findings, merge-safety limitations or rollout requirements remain. All current CI passes. Codier approval is posted on that exact head. The PR remains open and unmerged.AI execution metadata
One OpenAI Codex
gpt-6.1-solmain session implements and coordinates this PR: requested high, session01a10fc2-0e0e-7733-b5ad-d5c548d66901. Combined main-session usage snapshot at 2026-10-06T06:50:03.594104+00:00:{"input_tokens": 19420339, "cached_input_tokens": 19027072, "cache_write_input_tokens": 0, "output_tokens": 98242, "reasoning_output_tokens": 42889, "total_tokens": 19518581}. This replaces the creation snapshot; snapshots are not added together.{"provider": "claude", "model": "claude-opus-5-5", "effort": "high"}; observed{"model": "claude-opus-5-5", "effort": null}; sessionef8caeef-49be-4bc1-8477-07ea0792ad0d; reviewed head8ec5eb16d31d7d4187378a9d15e5ad9e2dcb2e11; status completed. Provider usage:{"claude-haiku-4-5-20251001": {"inputTokens": 3359, "outputTokens": 18, "cacheReadInputTokens": 0, "cacheCreationInputTokens": 0, "webSearchRequests": 0, "costUSD": 0.003449, "contextWindow": 200000, "maxOutputTokens": 32000, "thinkingTokens": 0, "canonicalModel": "claude-haiku-4-5", "provider": "firstParty", "costBasis": "list"}, "claude-opus-5-5": {"inputTokens": 82, "outputTokens": 44705, "cacheReadInputTokens": 3756104, "cacheCreationInputTokens": 123381, "webSearchRequests": 0, "costUSD": 2.6326967999999993, "contextWindow": 1000000, "maxOutputTokens": 128000, "thinkingTokens": 28695, "canonicalModel": "claude-opus-5-5", "provider": "firstParty", "costBasis": "list"}}.{"provider": "factory", "model": "deepseek-v4-pro", "effort": "max", "focus": "security"}; observed{"model": "deepseek-v4-pro", "effort": "max", "autonomy": "high"}; session369b0a1c-3018-4df1-8f14-f529729729a9; reviewed head8ec5eb16d31d7d4187378a9d15e5ad9e2dcb2e11; status completed. Provider usage:{"input_tokens": 268988, "output_tokens": 80174, "cache_read_input_tokens": 7455616, "cache_creation_input_tokens": 0, "factory_credits": 402902, "ttft_ms": 2282.4050632911394}.{"provider": "factory", "model": "gemini-3.1-pro-preview", "effort": "high"}; observed{"model": "gemini-3.1-pro-preview", "effort": "high", "autonomy": "high"}; sessionf1a0b5ff-3f4c-4a53-9961-06932157dd45; reviewed head1c89eea7168e57a8c52c954f045ef89518b4f8ca; status completed. Provider usage:{"input_tokens": 609900, "output_tokens": 6619, "cache_read_input_tokens": 484028, "cache_creation_input_tokens": 0, "factory_credits": 558420, "thinking_tokens": 1672, "ttft_ms": 5706.882352941177}.{"provider": "factory", "model": "deepseek-v4-pro", "effort": "max", "focus": "security"}; observed{"model": "deepseek-v4-pro", "effort": "max", "autonomy": "high"}; session0af0b877-1f85-4412-9a88-ef1b028b1e29; reviewed head1c89eea7168e57a8c52c954f045ef89518b4f8ca; status completed. Provider usage:{"input_tokens": 126922, "output_tokens": 54597, "cache_read_input_tokens": 2810606, "cache_creation_input_tokens": 0, "factory_credits": 203974, "ttft_ms": 1945.1025641025642}.{"provider": "factory", "model": "gemini-3.1-pro-preview", "effort": "high"}; observed{"model": "gemini-3.1-pro-preview", "effort": "high", "autonomy": "high"}; sessiona90b8cfd-d493-4f27-907d-a470f5578324; reviewed head1c89eea7168e57a8c52c954f045ef89518b4f8ca; status completed. Provider usage:{"input_tokens": 161462, "output_tokens": 2484, "cache_read_input_tokens": 118475, "cache_creation_input_tokens": 0, "factory_credits": 150574, "thinking_tokens": 466, "ttft_ms": 3566}.{"provider": "factory", "model": "deepseek-v4-pro", "effort": "max", "focus": "security"}; observed{"model": "deepseek-v4-pro", "effort": "max", "autonomy": "high"}; sessiond83260de-8ce6-4ec6-ac2b-555dbb41ae9c; reviewed head1c89eea7168e57a8c52c954f045ef89518b4f8ca; status completed. Provider usage:{"input_tokens": 203582, "output_tokens": 76899, "cache_read_input_tokens": 863942, "cache_creation_input_tokens": 0, "factory_credits": 244816, "ttft_ms": 2154.9411764705883}.Usage comes from retained provider records. Missing effort or reasoning-token fields are unavailable; they are not inferred. Opus usage includes its separately reported Haiku permission-classifier calls. Factory is the client for underlying Google Gemini and DeepSeek models. Model availability probes are separate retained evidence and do not count as reviews.