| import js from '@eslint/js'; | ||
| import globals from 'globals'; | ||
| export default [ | ||
| js.configs.recommended, | ||
| { | ||
| languageOptions: { | ||
| ecmaVersion: 2022, | ||
| globals: { ...globals.node } | ||
| }, | ||
| rules: { | ||
| 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }] | ||
| } | ||
| } | ||
| ]; |
@@ -20,2 +20,3 @@ name: CI | ||
| - run: npm ci | ||
| - run: npm run lint | ||
| - run: npm test |
+6
-0
@@ -37,2 +37,5 @@ const fs = require('fs').promises; | ||
| function parseCropBox(raw) { | ||
| if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { | ||
| throw new AgentLuxError('VLM_SCHEMA_ERROR', 'VLM response is not a valid JSON object.'); | ||
| } | ||
| const requiredKeys = ['x', 'y', 'width', 'height']; | ||
@@ -191,2 +194,5 @@ for (const key of requiredKeys) { | ||
| try { | ||
| if (typeof delete_after !== 'boolean') { | ||
| throw new AgentLuxError('INPUT_ERROR', 'delete_after must be a boolean.'); | ||
| } | ||
| if (typeof image_path !== 'string' || image_path.trim().length === 0) { | ||
@@ -193,0 +199,0 @@ throw new AgentLuxError('INPUT_ERROR', 'image_path must be a non-empty string.'); |
+7
-1
| { | ||
| "name": "agentlux", | ||
| "version": "1.0.4", | ||
| "version": "1.0.5", | ||
| "description": "Zero-retention AgentSkill bringing the Leica 35mm aesthetic and Henri Cartier-Bresson's geometry to autonomous vision models.", | ||
| "main": "index.js", | ||
| "scripts": { | ||
| "lint": "eslint .", | ||
| "test": "node --test" | ||
@@ -30,3 +31,8 @@ }, | ||
| "sharp": "^0.33.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^10.0.1", | ||
| "eslint": "^10.1.0", | ||
| "globals": "^17.4.0" | ||
| } | ||
| } |
+36
-5
@@ -116,5 +116,35 @@ # AgentLux QA Review Report | ||
| ## Third-Pass Review Findings (round-2 closure) | ||
| ### High | ||
| 1. `parseCropBox` non-object input defense | ||
| - Evidence: VLM returning `null`, a primitive, or an array causes `in` operator to throw native `TypeError`, misclassified as `VLM_NETWORK_ERROR` (retryable). | ||
| - Resolution: added type guard `!raw || typeof raw !== 'object' || Array.isArray(raw)` → `VLM_SCHEMA_ERROR`. | ||
| - Status: fixed. | ||
| 2. `delete_after` string-boolean misuse risk | ||
| - Evidence: passing `delete_after: "false"` is truthy, silently deletes source file. | ||
| - Resolution: added `typeof delete_after !== 'boolean'` check → `INPUT_ERROR`. | ||
| - Status: fixed. | ||
| ### Medium | ||
| 1. No lint/static-check gate in CI | ||
| - Evidence: CI had `npm test` only, no static analysis. | ||
| - Resolution: added ESLint (`@eslint/js` recommended + Node globals), `npm run lint` script, and CI lint step before test. | ||
| - Status: fixed. | ||
| 2. Missing regression tests for delete_failed, timeout, 5xx retry | ||
| - Evidence: `delete_failed` branch, `VLM_TIMEOUT`, and 503/429 retry paths had zero test coverage. | ||
| - Resolution: added 5 tests: string delete_after rejection, delete_failed permission error, null crop defense, VLM_TIMEOUT on hung fetch, HTTP 503 retry-then-succeed. | ||
| - Status: fixed. | ||
| ### Low (deferred) | ||
| 1. Path allowlist/sandbox constraint | ||
| - Acknowledged residual risk; acceptable under current plug-and-play scope. | ||
| - Will add optional `AGENTLUX_ALLOWED_ROOT` in a future iteration. | ||
| ## Test Evidence | ||
| - Command: `npm test` | ||
| - Result: 10 passed, 0 failed (5 original + 5 new) | ||
| - Command: `npm run lint && npm test` | ||
| - Lint: 0 errors, 0 warnings | ||
| - Tests: 15 passed, 0 failed | ||
@@ -125,9 +155,10 @@ ## Priority Roadmap | ||
| - P1.5 (done): retry correctness + env validation + input validation tests + test isolation | ||
| - P2 (recommended next): | ||
| - add lightweight lint/check gate | ||
| - P2 (done): parseCropBox defense + delete_after type guard + ESLint gate + full regression coverage | ||
| - P3 (recommended next): | ||
| - add optional structured logs with request correlation | ||
| - add load tests for large-image throughput profile | ||
| - add optional `AGENTLUX_ALLOWED_ROOT` path sandbox | ||
| ## Release Blockers | ||
| - None remaining for the scoped plan. | ||
| - None remaining. | ||
| - Recommended pre-release checks: | ||
@@ -134,0 +165,0 @@ - verify `OPENAI_API_KEY` present in deployment env |
+128
-0
@@ -220,1 +220,129 @@ const test = require('node:test'); | ||
| }); | ||
| test('rejects string delete_after to prevent truthy misuse', async () => { | ||
| setup(); | ||
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentlux-')); | ||
| const imagePath = path.join(tmpDir, 'in.jpg'); | ||
| await createFixtureImage(imagePath); | ||
| try { | ||
| const result = await agentlux.execute({ image_path: imagePath, delete_after: 'false' }); | ||
| assert.equal(result.status, 'error'); | ||
| assert.equal(result.error_code, 'INPUT_ERROR'); | ||
| await fs.access(imagePath); | ||
| } finally { | ||
| teardown(); | ||
| await fs.rm(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| // --- Deletion failure --- | ||
| test('delete_failed branch surfaces status and message', async () => { | ||
| setup(); | ||
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentlux-')); | ||
| const imagePath = path.join(tmpDir, 'in.jpg'); | ||
| await createFixtureImage(imagePath); | ||
| global.fetch = async () => mockOpenAIResponse({ x: 0, y: 0, width: 30, height: 30, rule: 'r' }); | ||
| await fs.unlink(imagePath); | ||
| await createFixtureImage(imagePath); | ||
| await fs.chmod(tmpDir, 0o555); | ||
| try { | ||
| const result = await agentlux.execute({ image_path: imagePath, delete_after: true }); | ||
| assert.equal(result.status, 'success'); | ||
| assert.equal(result.source_file_deletion, 'delete_failed'); | ||
| assert.equal(typeof result.source_file_deletion_message, 'string'); | ||
| assert.ok(result.source_file_deletion_message.length > 0); | ||
| } finally { | ||
| await fs.chmod(tmpDir, 0o755); | ||
| teardown(); | ||
| await fs.rm(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| // --- VLM null/array defense --- | ||
| test('VLM returning null is VLM_SCHEMA_ERROR without retry', async () => { | ||
| setup(); | ||
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentlux-')); | ||
| const imagePath = path.join(tmpDir, 'in.jpg'); | ||
| await createFixtureImage(imagePath); | ||
| let fetchCalls = 0; | ||
| global.fetch = async () => { fetchCalls += 1; return mockOpenAIResponse(null); }; | ||
| try { | ||
| const result = await agentlux.execute({ image_path: imagePath, delete_after: false }); | ||
| assert.equal(result.status, 'error'); | ||
| assert.equal(result.error_code, 'VLM_SCHEMA_ERROR'); | ||
| assert.equal(fetchCalls, 1, 'null crop must not trigger retries'); | ||
| } finally { | ||
| teardown(); | ||
| await fs.rm(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| // --- Timeout and 5xx retry --- | ||
| test('VLM_TIMEOUT on hung fetch', async () => { | ||
| setup(); | ||
| process.env.AGENTLUX_VLM_TIMEOUT_MS = '100'; | ||
| process.env.AGENTLUX_VLM_MAX_RETRIES = '0'; | ||
| let mod; | ||
| try { | ||
| delete require.cache[require.resolve('../index.js')]; | ||
| mod = require('../index.js'); | ||
| } finally { | ||
| delete process.env.AGENTLUX_VLM_TIMEOUT_MS; | ||
| delete process.env.AGENTLUX_VLM_MAX_RETRIES; | ||
| } | ||
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentlux-')); | ||
| const imagePath = path.join(tmpDir, 'in.jpg'); | ||
| await createFixtureImage(imagePath); | ||
| global.fetch = async (_url, opts) => { | ||
| await new Promise((_resolve, reject) => { | ||
| opts.signal.addEventListener('abort', () => reject(new DOMException('The operation was aborted.', 'AbortError'))); | ||
| }); | ||
| }; | ||
| try { | ||
| const result = await mod.execute({ image_path: imagePath, delete_after: false }); | ||
| assert.equal(result.status, 'error'); | ||
| assert.equal(result.error_code, 'VLM_TIMEOUT'); | ||
| } finally { | ||
| delete require.cache[require.resolve('../index.js')]; | ||
| require('../index.js'); | ||
| teardown(); | ||
| await fs.rm(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| test('HTTP 503 retries then succeeds', async () => { | ||
| setup(); | ||
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentlux-')); | ||
| const imagePath = path.join(tmpDir, 'in.jpg'); | ||
| await createFixtureImage(imagePath); | ||
| let fetchCalls = 0; | ||
| global.fetch = async () => { | ||
| fetchCalls += 1; | ||
| if (fetchCalls === 1) { | ||
| return { ok: false, status: 503, statusText: 'Service Unavailable', text: async () => 'overloaded' }; | ||
| } | ||
| return mockOpenAIResponse({ x: 5, y: 5, width: 40, height: 30, rule: '503 retry' }); | ||
| }; | ||
| try { | ||
| const result = await agentlux.execute({ image_path: imagePath, delete_after: false }); | ||
| assert.equal(result.status, 'success'); | ||
| assert.equal(fetchCalls, 2, '503 should be retried once then succeed'); | ||
| } finally { | ||
| teardown(); | ||
| await fs.rm(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
37384
22.86%8
14.29%564
29.66%3
Infinity%13
44.44%14
27.27%