diff --git a/README.md b/README.md index 59629ae..502902e 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Everything is in [docs/CONFIG.md](docs/CONFIG.md). | `v` / `x` | Validate the finding / propose a fix | | `y` / `r` | Apply / discard a proposed fix | | `g` | Discuss the finding in the conversation you came from | -| `e` / `u` | Run a scan again / resume a stopped Deep scan | +| `e` / `u` | Run a scan again / resume a stopped scan | | `tab` | Switch between scans and other activity | | `h` | List every key available right now | @@ -189,7 +189,7 @@ Context files for a scan may be absolute paths in other checkouts. Any UTF-8 tex - **Your source goes to your model provider.** Every scan, validation, fix and policy draft shows where, and waits for your approval. - **Costs are estimates.** `scan.maxCostUsd` stops a scan on the SDK's estimate; it is not a billing cap. Validation and fixes have no cost limit. - **Findings arrive when a scan completes**, not while it runs. If a scan fails or is cancelled, whatever it had saved is shown as partial results. -- **Standard scans have no time limit** unless you set one, and treat it as a target. A stopped Standard scan can be run again; a stopped Deep scan can be resumed where it left off. +- **Standard scans have no time limit** unless you set one, and treat it as a target. A scan that lost its connection or was cut off by a restart can be resumed where it left off; any other stopped scan can be run again. - **A fix is written against your last commit.** It does not see uncommitted work, and applying it fails cleanly if you have since changed the same lines. - **Change scans run at Standard depth.** - **Restarts.** Scan results and finding status are kept. A scan that was running is marked interrupted, and policy drafts, validations and unapplied fixes in progress are lost. diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index b5fab5d..0287b30 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -79,4 +79,4 @@ Provenance identifies the staging workflow; `build-manifest.json` links the byte - [npm trust prerequisites](https://docs.npmjs.com/cli/v11/commands/npm-trust/) - [npm dual-use policy](https://docs.npmjs.com/policies/dual-use/) -Verified November 10, 2026; recheck before release. Installed metadata identifies Codex Security 0.9.1 as Apache-2.0, OpenCode plugin/Solid as MIT, and OpenTUI native binaries as MIT AND LGPL-2.1-or-later. Dependencies stay external; this is not comprehensive legal clearance. +Verified November 10, 2026; recheck before release. Installed metadata identifies Codex Security 0.10.0 as Apache-2.0, OpenCode plugin/Solid as MIT, and OpenTUI native binaries as MIT AND LGPL-2.1-or-later. Dependencies stay external; this is not comprehensive legal clearance. diff --git a/package-lock.json b/package-lock.json index 5848574..c7b9e5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "Apache-2.0", "dependencies": { - "@openai/codex-security": "0.9.1", + "@openai/codex-security": "0.10.0", "@opencode/plugin": "2.2.6", "smol-toml": "1.9.1" }, @@ -1027,6 +1027,27 @@ "integrity": "sha512-YzVGTHXf4bzlyAL5KAvHYCdNbbt6zM5cYkAgxqxQPDQM6aZ4ShIIN5yJ+6q7YeonnevxT8k/LmD2tJi/jOiKkg==", "license": "MIT" }, + "node_modules/@clack/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz", + "integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz", + "integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.4.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@effect/opentelemetry": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@effect/opentelemetry/-/opentelemetry-4.1.0.tgz", @@ -1319,6 +1340,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "license": "ISC" + }, "node_modules/@inquirer/ansi": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", @@ -1577,9 +1604,9 @@ "license": "MIT" }, "node_modules/@linear/sdk": { - "version": "100.0.0", - "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-100.0.0.tgz", - "integrity": "sha512-7+n+Xqo6yYtexeDfaFnmT9j7bapkKyjB5cV63STuG8N1Wnjb9vyhpwOpn+t0VE3emGE7ZNEu2Rz1FHSWD84raw==", + "version": "101.1.0", + "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-101.1.0.tgz", + "integrity": "sha512-sDlYKMoTiOQU1C6+B2451qMbrR6zdKbBZfqIGp/dpZdVeOzYQSmp8e4uUHlCdMuFM7sRhQFMIl82AtwoaKYW5g==", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0" @@ -2207,9 +2234,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.167.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0.tgz", - "integrity": "sha512-1FSQgbTIKAdTub5Ln6z36NT4eXDmBRkinfigzycbgn9yLZdJ3UdkQZqr0N7jnl8/D1xxT/Pz3Fl2l7S2dmRaFA==", + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1.tgz", + "integrity": "sha512-39uApTRT9bkhE8tMXTIfZxaZ27oi7hPqWoF2Coyp+iQFAD3NVHgH838IYVUfJaX85leMSP2s4JOmVw0mMSCN5Q==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -2218,19 +2245,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.167.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.167.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.167.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.167.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.167.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.167.0-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.167.1-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.167.1-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.167.1-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.167.1-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.167.1-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.167.1-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.167.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-darwin-arm64.tgz", - "integrity": "sha512-7YtMDZX/jbWH9+fCVQg7BVsov8WvBzu4iuedXJEDlUdZxaTrYX2bEQwHIn3p0gxgSMfm30HGK9+bZm1aGSZaHw==", + "version": "0.167.1-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-darwin-arm64.tgz", + "integrity": "sha512-IMUByWGPhdbJFZzuk18iIdF/mfd21cPwalRTp5vGYIsOCyv3SJGRY5snbzZd0zO37WGuNjfUvH/62LvQHqNT2A==", "cpu": [ "arm64" ], @@ -2245,9 +2272,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.167.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-darwin-x64.tgz", - "integrity": "sha512-wKoBiI6SBSg8fqiEYEOJs/NM/ZzKaCgy1cseX9LJbuDq3GCCKkeW9BUGe1ZEkyhhZ5Yfj+HzdUfp698abP2tDA==", + "version": "0.167.1-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-darwin-x64.tgz", + "integrity": "sha512-SwJf87Qtuktk8qHSDsbNkjVjOerqQRCBgdrqw6gvAlF3tO8UizEp/Vs8qQVznNI1aJfFc6GoJLvcgBGVWVylDg==", "cpu": [ "x64" ], @@ -2262,9 +2289,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.167.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-linux-arm64.tgz", - "integrity": "sha512-5cv+sybRWqpRJL6KCv3YSnIx1GHRmjek997C38n1Zfno4HomKDLkNSjWLDOOc2+ISlVNSRRnySWq4zKmzlY12g==", + "version": "0.167.1-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-linux-arm64.tgz", + "integrity": "sha512-z13jj4X4GDk7CTd1kXHkhXC8Gj8zomd1jYsU2//0KjevbLelPxowWLSTqIcR1rarJCtzveBHsS8gR7B9pAWB0g==", "cpu": [ "arm64" ], @@ -2279,9 +2306,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.167.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-linux-x64.tgz", - "integrity": "sha512-FNc7danfUng9Sp6w+Fh7kofNHHZpA6VMyNKWsWrS3OwHly69gywTWeiuwrUPXpJSho8Df4xqRsHfHRPzj7zYQg==", + "version": "0.167.1-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-linux-x64.tgz", + "integrity": "sha512-qZteglyvMCj1QVymMyPyKdtw38HaXCv5gvw8WN+N0+JJstu3k/d8xiYj9ciL7OwFLhcujgP8bCGJgaREeqrHkw==", "cpu": [ "x64" ], @@ -2295,31 +2322,32 @@ } }, "node_modules/@openai/codex-sdk": { - "version": "0.167.0", - "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.167.0.tgz", - "integrity": "sha512-dps+zHZeB1MJIL9AO8Azasw5vvS8A5AyI/xmvnTObX8868/8uUeB/UHhuM9SJ4BcX0E6q46PnxuOPbJ2bqD+dA==", + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.167.1.tgz", + "integrity": "sha512-EJHNG/bZ+/rptIIZMigTgnqrukDAgc/rmUBXLeDrFwOeunno7tQ4sWt/VPzeRXnKGq6yd4jprpgRlzgpYvayzQ==", "license": "Apache-2.0", "dependencies": { - "@openai/codex": "0.167.0" + "@openai/codex": "0.167.1" }, "engines": { "node": ">=18" } }, "node_modules/@openai/codex-security": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@openai/codex-security/-/codex-security-0.9.1.tgz", - "integrity": "sha512-eptVVvo4BrdY5k1CUCFsiU8SRIwzmceasfHvWVbYaiU3eWKLEIVZZhnC0JT2qbFASuy3dpNuOX/7UvOebg3LOw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@openai/codex-security/-/codex-security-0.10.0.tgz", + "integrity": "sha512-DJWHqzqsqJHKPdP3i5OX/VXRlxYri/PpvJ6lN+PtUx0iEjs6o1ZrCa2cF4c0fuWW6+8TntDH/1ndujqtjyctXA==", "license": "Apache-2.0", "dependencies": { "@inquirer/checkbox": "5.2.8", "@inquirer/confirm": "6.3.5", "@inquirer/input": "5.1.9", "@inquirer/search": "4.3.6", - "@linear/sdk": "100.0.0", + "@linear/sdk": "101.1.0", "@octokit/core": "7.0.8", - "@openai/codex": "0.167.0", - "@openai/codex-sdk": "0.167.0", + "@openai/codex": "0.167.1", + "@openai/codex-sdk": "0.167.1", + "add-mcp": "2.6.1", "ajv": "8.20.0", "fflate": "0.8.3", "incur": "0.7.1", @@ -2328,10 +2356,11 @@ "pdfjs-dist": "6.5.438", "react": "19.3.1", "saxes": "6.0.0", - "semver": "7.8.5", + "semver": "7.8.6", "smol-toml": "1.9.1", "string-width": "8.3.0", "strip-ansi": "7.2.0", + "tokenx": "1.6.0", "yaml": "2.9.1", "yauzl": "3.4.0", "zod": "4.6.7" @@ -2344,18 +2373,6 @@ "node": "^22.13.0 || ^24.0.0 || ^26.0.0" } }, - "node_modules/@openai/codex-security/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@openai/codex-security/node_modules/string-width": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", @@ -2374,9 +2391,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.167.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-win32-arm64.tgz", - "integrity": "sha512-dYeYQZ66LCNnFXyF5vHRsbJQLyfbY3G8vDyn6XG3NxcwMFz4stkgSdC94UPc1NhCxkhpufHlkAm1oe+Z28Pw5Q==", + "version": "0.167.1-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-win32-arm64.tgz", + "integrity": "sha512-KFUMwSMBlQmabCSbpfv7AgouF+kNJE8iNw/nYv+IgHbHCY0v1pAzLFdt3BscYRUD+KNOeJqnp6n4xwCteA5OcQ==", "cpu": [ "arm64" ], @@ -2391,9 +2408,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.167.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.0-win32-x64.tgz", - "integrity": "sha512-duPCBwflc8qhQdLekbL8fDYF646tW4gj0/l9BDnsVhEzoqm6OwxMAJXDFwSOLUUJPCmjBeM+eHYAx4fP/hwfjg==", + "version": "0.167.1-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.167.1-win32-x64.tgz", + "integrity": "sha512-yvUA7s46paDIT5I3Phz8KcoGpSGlWR3pOS0npqQ3LQMpjtTOBWoLPRdjuoKVreuKIuOLDMxwWCID4nwojbbrSA==", "cpu": [ "x64" ], @@ -4119,6 +4136,38 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/add-mcp": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/add-mcp/-/add-mcp-2.6.1.tgz", + "integrity": "sha512-Jimza7/1JLaC3RVoaImX+CJSYKrQZkKuBZoV77zdo5yZwl/ZI66ba2uBVz690VSiqXR3yWKIPaw6Ou4ntzuRwg==", + "license": "Apache-2.0", + "dependencies": { + "@clack/prompts": "^0.9.1", + "@iarna/toml": "^2.2.5", + "chalk": "^5.4.1", + "commander": "^13.1.0", + "jsonc-parser": "^3.3.1", + "yaml": "^2.9.1" + }, + "bin": { + "add-mcp": "dist/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/add-mcp/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4706,6 +4755,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/common-ancestor-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", @@ -6224,6 +6282,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -7306,7 +7370,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7872,6 +7935,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-10.0.1.tgz", diff --git a/package.json b/package.json index 883f8c9..9e26bfa 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test:runtime": "node --test tests/sdk-runtime.test.mjs" }, "dependencies": { - "@openai/codex-security": "0.9.1", + "@openai/codex-security": "0.10.0", "@opencode/plugin": "2.2.6", "smol-toml": "1.9.1" }, diff --git a/src/core/codex-adapter.ts b/src/core/codex-adapter.ts index aa707e4..f498996 100644 --- a/src/core/codex-adapter.ts +++ b/src/core/codex-adapter.ts @@ -82,8 +82,10 @@ export function mapSdkFinding(finding: SdkFinding): Finding | null { * provider answered, and whether trying again can help. */ export function providerFailure(message: string, reason?: string): string { - const answer = /^Reconnecting\.\.\. \d+\/\d+ \((.*)\)$/s.exec(message.trim())?.[1]; - if (!answer) return message; + // The SDK appends its CLI's resume command; the views have their own. + const text = message.replace(/\nResume when connected: .*$/s, '').trim(); + const answer = /^Reconnecting\.\.\. \d+\/\d+ \((.*)\)$/s.exec(text)?.[1]; + if (!answer) return text; const status = /unexpected status (\d{3})/.exec(answer)?.[1]; const refused = reason === 'authorization' || @@ -223,7 +225,7 @@ export class CodexScanService { target: await scanTarget(plan), ...(plan.scanPrompt ? { scanPrompt: plan.scanPrompt } : {}), knowledgeBasePaths: plan.contextFiles.map((file) => resolve(this.root, file.path)), - // Not in the SDK's public types: its CLI resumes a Deep scan with these two options. + // Not in the SDK's public types: its CLI resumes a scan with these two options. ...(plan.sdk.resume ? { resumeScanId: plan.sdk.resume.scanId, outputDir: plan.sdk.resume.outputDir } : {}), @@ -391,6 +393,9 @@ export class CodexScanService { ? 'Cancelled; results incomplete' : 'Codex Security scan failed'; const budget = signal.aborted ? undefined : scanBudgetFailure(error); + // The SDK keeps a scan open only when the connection dropped. A refusal or a failure closes it. + this.#state.resumable = + !signal.aborted && (error as Error)?.constructor?.name === 'ScanInterruptedError'; if (budget) { this.#state.estimatedCostUsd = budget.estimatedCostUsd; const cost = costSummary((error as { cost: Parameters[0] }).cost); diff --git a/src/core/planned-scan.ts b/src/core/planned-scan.ts index 0210604..69d6ec5 100644 --- a/src/core/planned-scan.ts +++ b/src/core/planned-scan.ts @@ -213,8 +213,8 @@ export class PlannedScanService { return state; } /** - * A preview that continues an interrupted Deep scan. The SDK resumes only Deep scans, in the - * original session and output directory, and compares the target with the one it saved. + * A preview that continues an interrupted scan. The SDK resumes it in the original session and + * output directory, and compares the target with the one it saved. */ async #prepareResume(runId: string): Promise { const run = this.#archived.get(runId); @@ -223,7 +223,7 @@ export class PlannedScanService { const previous = run!.plan!; const plan = await prepareScan( this.#project, - { ...previous.inputs, repository: previous.repository, mode: 'deep' }, + { ...previous.inputs, repository: previous.repository, mode: previous.settings.scan.mode }, this.#mock, ); return { diff --git a/src/core/result-summary.ts b/src/core/result-summary.ts index 8866d57..a4f52ac 100644 --- a/src/core/result-summary.ts +++ b/src/core/result-summary.ts @@ -240,7 +240,7 @@ function repositoryFindings(value: unknown): ScanOutcomeSummary['repositoryFindi /** * Project only final returned SDK metadata. This never reads artifacts or serializes a ScanResult, * turnResult, report, finding evidence, command, or arbitrary reviewState extension. - * SDK 0.9.1 can remove reviewState during sealing and does not retain options.coverage. Missing + * The SDK can remove reviewState during sealing and does not retain options.coverage. Missing * detail therefore stays unavailable; even a populated reviewState cannot establish completeness. */ export function summarizeScanResult(result: ScanResult, repository: string): ScanOutcomeSummary { diff --git a/src/core/run-history.ts b/src/core/run-history.ts index d273f4d..10e6434 100644 --- a/src/core/run-history.ts +++ b/src/core/run-history.ts @@ -43,6 +43,7 @@ export class RunHistory { if (s.status === 'running') { s.status = 'error'; s.phase = 'Interrupted'; + s.resumable = true; s.error = 'This scan was still running when OpenCode last stopped, so its outcome is unknown.'; } diff --git a/src/gui/ScanReview.tsx b/src/gui/ScanReview.tsx index 415754e..c09bbc3 100644 --- a/src/gui/ScanReview.tsx +++ b/src/gui/ScanReview.tsx @@ -18,7 +18,7 @@ export function ScanReview(props: {

{props.plan.resume ? 'Resume scan' : 'Review scan'}

- Continues the interrupted Deep scan in its original session. Work it had finished is kept. + Continues the interrupted scan in its original session. Work it had finished is kept.

diff --git a/src/scan-display.ts b/src/scan-display.ts index da03634..0c7f99b 100644 --- a/src/scan-display.ts +++ b/src/scan-display.ts @@ -95,12 +95,12 @@ export function timeLimitLabel(scan: { mode?: string; maxTimeMinutes?: number } const amount = minutes >= 120 ? `${Math.round(minutes / 6) / 10}h` : `${minutes}m`; return scan?.mode === 'deep' ? `${amount} of discovery` : `${amount} target, not a hard stop`; } -/** Why a scan cannot be resumed, or '' when it can. Only the SDK's Deep scans resume. */ +/** Why a scan cannot be resumed, or '' when it can. */ export function resumeProblem(run: ScanState) { if (run.status !== 'error' && run.status !== 'cancelled') return 'Only a scan that stopped before finishing can be resumed.'; - if ((run.mode ?? run.plan?.settings.scan.mode) !== 'deep') - return 'Only Deep scans can be resumed. Run this Standard scan again instead.'; + if (!run.resumable) + return 'This scan failed rather than losing its connection, so it cannot be resumed. Run it again instead.'; if (!run.scanId || !run.scanDirectory) return 'This scan stopped before it saved anything to resume from. Run it again instead.'; if (run.plan?.contextFiles.length) diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 5c4f7e4..d8921cb 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -142,7 +142,7 @@ export function createApp(ctx: Plugin.Context) { await store.refreshScans(); }); - /** Continue an interrupted Deep scan. The confirmation is the approval. */ + /** Continue an interrupted scan. The confirmation is the approval. */ const resumeScan = () => store.perform(async () => { const target = run(); diff --git a/src/types.ts b/src/types.ts index 8c20d64..0f46d02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,7 +31,9 @@ export interface ScanState { archived?: boolean; /** Findings come from the last checkpoint of a scan that did not finish. */ partial?: boolean; - /** Where the SDK keeps this scan's files. Needed to resume a Deep scan. */ + /** The SDK left this scan open after a dropped connection or a restart, so it can continue. */ + resumable?: boolean; + /** Where the SDK keeps this scan's files. Needed to resume it. */ scanDirectory?: string; workers?: { phase: string; planned: number; started: number }; deepProgress?: { completed: number; active: number; maximum: number; consolidating?: boolean }; diff --git a/tests/partial-results.test.mjs b/tests/partial-results.test.mjs index 0de9a17..26a608d 100644 --- a/tests/partial-results.test.mjs +++ b/tests/partial-results.test.mjs @@ -96,6 +96,10 @@ test('a provider failure says whether trying again can help', () => { /^The model provider stopped answering and the retries ran out\. stream disconnected/, ); assert.equal(providerFailure('Python 3.10 is required.'), 'Python 3.10 is required.'); + assert.equal( + providerFailure('stream disconnected\nResume when connected: codex-security scans resume abc'), + 'stream disconnected', + ); }); test('progress before review starts names the phase instead of counting zero files', () => { @@ -122,7 +126,7 @@ test('the time limit reads as a target for Standard and a deadline for Deep', () assert.equal(timeLimitLabel({ mode: 'deep' }), 'SDK default'); }); -test('an interrupted Deep scan resumes in its original session and directory', async (t) => { +test('an interrupted scan resumes in its original session and directory', async (t) => { const { PlannedScanService } = await import('../src/core/planned-scan.ts'); const { project } = await import('./helpers/project.mjs'); const root = await project(t); @@ -133,7 +137,8 @@ test('an interrupted Deep scan resumes in its original session and directory', a seen.push({ resumeScanId: options.resumeScanId, outputDir: options.outputDir }); options.onOutputDirReady('/state/scans/deep-1'); options.onLifecycle({ scanId: 'sdk-deep' }); - if (seen.length !== 2) throw new Error('interrupted'); + if (seen.length === 3) throw new Error('refused'); + if (seen.length !== 2) throw new (class ScanInterruptedError extends Error {})('dropped'); return { findings: { scanId: 'sdk-deep', findings: [] } }; }, async close() {}, @@ -154,10 +159,17 @@ test('an interrupted Deep scan resumes in its original session and directory', a { resumeScanId: 'sdk-deep', outputDir: '/state/scans/deep-1' }, ]); - // Standard scans, and scans that finished, have nothing to resume. + // A scan that finished has nothing to resume. A stopped Standard scan resumes as Standard. await assert.rejects(service.prepare({ resumeRunId: resumed.runId }), /stopped before finishing/); const standard = await service.wait( (await service.start({ planId: (await service.prepare({})).plan.id })).runId, ); - await assert.rejects(service.prepare({ resumeRunId: standard.runId }), /Only Deep scans/); + // A scan the provider refused is closed by the SDK. One that lost its connection is not. + await assert.rejects(service.prepare({ resumeRunId: standard.runId }), /cannot be resumed/); + const dropped = await service.wait( + (await service.start({ planId: (await service.prepare({})).plan.id })).runId, + ); + const again = await service.prepare({ resumeRunId: dropped.runId }); + assert.equal(again.plan.settings.scan.mode, 'standard'); + assert.equal(again.plan.resume.scanId, 'sdk-deep'); });