From 3a91c8ce3a8fc7d73a4793af8f5d3ef8a8317e23 Mon Sep 17 00:00:00 2001 From: Adulph3 Date: Wed, 16 Sep 2026 21:04:27 +0300 Subject: [PATCH] Prepare scoped AgentFence v0.2.0 npm release --- .github/workflows/ci.yml | 7 ++- README.md | 43 ++++++++++++++----- SECURITY.md | 5 +-- dist/src/cli/doctor.js | 3 +- dist/src/cli/doctor.js.map | 2 +- dist/src/cli/main.js | 2 +- dist/src/cli/runtime.js | 2 +- dist/src/cli/runtime.js.map | 2 +- dist/src/node/index.js | 4 +- dist/src/reporters/json.js | 2 +- docs/COMPATIBILITY.md | 2 + docs/DECISIONS.md | 8 ++++ docs/PROGRESS.md | 24 +++++++++++ docs/RELEASE.md | 14 ++++-- package-lock.json | 10 ++--- package.json | 12 ++++-- scripts/check-package.mjs | 6 ++- src/cli/doctor.ts | 3 +- src/cli/main.ts | 2 +- src/cli/runtime.ts | 2 +- src/node/index.ts | 4 +- src/reporters/json.ts | 2 +- .../catalog-common-regressions.test.mjs | 2 +- test/security/runtime-cli.test.mjs | 5 ++- test/security/runtime-probe.mjs | 4 +- test/unit/batch7-doctor.test.mjs | 5 ++- test/unit/batch8-package-completion.test.mjs | 13 +++++- test/unit/core.test.mjs | 14 +++--- 28 files changed, 146 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67594e7..2b404b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,14 @@ on: [push, pull_request] permissions: { contents: read } jobs: test: - strategy: { matrix: { os: [ubuntu-latest, macos-latest, windows-latest] } } + strategy: { matrix: { os: [ubuntu-latest, macos-latest, windows-latest], node: ['22.23.2', '24.21.0'] } } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 - with: { node-version: '24.21.0', cache: npm } + with: + node-version: ${{ matrix.node }} + cache: npm - if: runner.os == 'macOS' run: echo "TMPDIR=/private/tmp" >> "$GITHUB_ENV" - run: npm ci --ignore-scripts @@ -17,3 +19,4 @@ jobs: - run: npm test - run: npm run test:coverage - run: npm run build + - run: npm pack --dry-run --ignore-scripts diff --git a/README.md b/README.md index 838ac2d..08dd034 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,15 @@

CI Latest release - Node.js 24 + Node.js 22 and 24 Linux, macOS, and Windows MIT License

- Download v0.1.0 + Quick Start · Installation - · Quick start + · Usage examples · Security

@@ -101,8 +101,29 @@ flowchart LR Raw source bytes still exist in process memory while being analyzed, and portable Node.js checks cannot prove containment against a concurrently hostile filesystem. For adversarial trees, scan an externally prepared immutable local snapshot. Read the complete [privacy model](docs/PRIVACY.md) and [threat model](docs/THREAT_MODEL.md). +## Quick Start + +AgentFence v0.2.0 supports Node.js 22 and 24; Node.js 24 is recommended. Untested odd-numbered releases are not supported. Once the scoped package is published, use it for a one-off scan without a permanent installation: + +```bash +npx @adulph3/agentfence scan +npx @adulph3/agentfence scan --user-configs +``` + +The optional permanent installation keeps the executable name `agentfence`: + +```bash +npm install -g @adulph3/agentfence +agentfence scan +agentfence scan --user-configs +``` + +For an explicit package selection, `npx --package=@adulph3/agentfence agentfence scan` is equivalent. Verify the scoped package's publication before using these registry-backed commands. The unrelated unscoped npm package `agentfence` is **not this project**. npm/npx installation may contact the registry; AgentFence's `scan` and `doctor` commands make no application-initiated network requests. + ## Installation +The platform-specific steps below remain available for the separately verified v0.1.0 GitHub artifact. For v0.2.0, use the scoped npm commands in [Quick Start](#quick-start) after confirming publication. + AgentFence v0.1.0 requires **Node.js 24.x** (`>=24 <25`). It is distributed through GitHub Releases and is **not published to the npm registry**. The commands below install the downloaded local `.tgz` artifact. Official artifact: [`agentfence-0.1.0.tgz`](https://github.com/Adulph3/AgentFence/releases/download/v0.1.0/agentfence-0.1.0.tgz) @@ -227,7 +248,7 @@ SHA-256: On POSIX systems, `--output` creates a new report with mode `0600`. On Windows, report confidentiality depends on the inherited ACL of the selected parent directory, which the operator must restrict appropriately; mocked or lexical Windows tests do not establish real ACL behavior. -## Quick start +## Usage examples Run AgentFence from the project you want to inspect: @@ -246,7 +267,6 @@ agentfence scan . --output report.json # Opt into the fixed allowlist of supported user configuration agentfence scan . --user-configs - # Show High/Critical findings and fail when either is present agentfence scan . --severity high --fail-on high @@ -310,7 +330,7 @@ agentfence scan . --json --fail-on high > agentfence-report.json Treat exit `1` as a configured policy threshold, exit `3` as incomplete coverage requiring review, and exit `2` as a scanner/runtime failure. Reports can contain sensitive configuration observations even though secret values and raw paths are omitted; handle them as security artifacts. -The AgentFence repository's own workflow runs the complete quality-gate sequence on Ubuntu, macOS, and Windows with Node.js 24.21.0. +The AgentFence repository's workflow is configured to run the complete quality-gate sequence on Ubuntu, macOS, and Windows with Node.js 22.23.2 and 24.21.0. ## How AgentFence works @@ -340,7 +360,7 @@ Always verify the checksum before installing. The release artifact and its diges | Check | Status | | --- | --- | -| Current release | v0.1.0 | +| Previously verified GitHub release | v0.1.0 | | Ubuntu / Node.js 24 CI | Passed | | macOS / Node.js 24 CI | Passed | | Windows / Node.js 24 CI | Passed | @@ -366,16 +386,17 @@ Version pinning reduces selector drift but does not prove package integrity. Ins ## Updating and uninstalling -To update, download a newer `.tgz` from [GitHub Releases](https://github.com/Adulph3/AgentFence/releases), verify its published checksum, and install the local file: +For the scoped npm package, update or uninstall with: ```bash -npm install -g ./agentfence-VERSION.tgz +npm install -g @adulph3/agentfence@latest +npm uninstall -g @adulph3/agentfence ``` -To remove AgentFence: +For a GitHub Release artifact, download its `.tgz`, verify the published checksum, and install the local file: ```bash -npm uninstall -g agentfence +npm install -g ./agentfence-VERSION.tgz ``` ## Security reporting diff --git a/SECURITY.md b/SECURITY.md index ad593f5..a922009 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,8 @@ # Security policy -AgentFence v0.1 is supported locally. Do not place credentials, raw reports, private paths, or exploit payloads in public issues. Report suspected vulnerabilities through [GitHub private vulnerability reporting](https://github.com/Adulph3/AgentFence/security/advisories/new). Scanner reports are static observations, not incident verdicts. +AgentFence v0.1.0 is published on GitHub Releases. v0.2.0 is a local distribution candidate, not a published release. Do not place credentials, raw reports, private paths, or exploit payloads in public issues. Report suspected vulnerabilities through [GitHub private vulnerability reporting](https://github.com/Adulph3/AgentFence/security/advisories/new). Scanner reports are static observations, not incident verdicts. If you discover a suspected defect, retain only the minimum synthetic reproduction needed to explain it and avoid publishing scanned configuration, secret values, machine paths, or an exploit chain. There is intentionally no claimed response SLA. -Supported scope is the v0.1.0 GitHub source and release artifact on Node 24; npm -publication and unsupported runtime versions are not a promise of support. +The published v0.1.0 GitHub artifact supports Node 24. The v0.2.0 local candidate targets Node 22 and 24 under the chosen npm name `@adulph3/agentfence`, with the `agentfence` executable. This repository has not published to npm. The unrelated unscoped npm package `agentfence` is not this project; npm publication and other runtimes are not yet a supported public distribution claim. diff --git a/dist/src/cli/doctor.js b/dist/src/cli/doctor.js index 46d7106..0974b36 100644 --- a/dist/src/cli/doctor.js +++ b/dist/src/cli/doctor.js @@ -1,5 +1,6 @@ import { safe, safeJson } from '../security/safe.js'; import { scoreFindings } from '../scoring/score.js'; +import { runtimeSupported } from './runtime.js'; const adapterKinds = ['codex', 'claude-code', 'cursor', 'kiro', 'vscode', 'generic-mcp']; const pass = (test) => { try { return test(); @@ -12,7 +13,7 @@ const defaultScore = () => scoreFindings([], 0).value === null; /** Pure, fixed-surface health report: it deliberately performs no discovery or probes. */ export function doctorReport(services = {}) { const escapePass = pass(services.escapeSelfTest ?? defaultEscape), scorePass = pass(services.scoreSelfTest ?? defaultScore); - return { schemaVersion: '1.0.0', engineVersion: '0.1.0', rulesetVersion: '1.0.0', kind: 'doctor', runtimeSupported: (services.runtimeVersion ?? process.versions.node).split('.')[0] === '24', platform: services.platform ?? process.platform, architecture: services.architecture ?? process.arch, adapters: adapterKinds.map(kind => ({ kind, adapterVersion: '1.0.0' })), checks: [{ id: 'escape', state: escapePass ? 'pass' : 'fail', message: safe('In-memory safe-output self-test') }, { id: 'score', state: scorePass ? 'pass' : 'fail', message: safe('In-memory scoring self-test') }], overrides: services.overrides ?? { codexHome: process.env.CODEX_HOME !== undefined, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR !== undefined } }; + return { schemaVersion: '1.0.0', engineVersion: '0.2.0', rulesetVersion: '1.0.0', kind: 'doctor', runtimeSupported: runtimeSupported(services.runtimeVersion ?? process.versions.node), platform: services.platform ?? process.platform, architecture: services.architecture ?? process.arch, adapters: adapterKinds.map(kind => ({ kind, adapterVersion: '1.0.0' })), checks: [{ id: 'escape', state: escapePass ? 'pass' : 'fail', message: safe('In-memory safe-output self-test') }, { id: 'score', state: scorePass ? 'pass' : 'fail', message: safe('In-memory scoring self-test') }], overrides: services.overrides ?? { codexHome: process.env.CODEX_HOME !== undefined, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR !== undefined } }; } const plain = (report) => `AgentFence doctor\nRuntime supported: ${report.runtimeSupported ? 'yes' : 'no'}\nSelf-tests: ${report.checks.every(check => check.state === 'pass') ? 'pass' : 'fail'}\nAdapters: ${report.adapters.map(adapter => `${adapter.kind}@${adapter.adapterVersion}`).join(', ')}\n`; export function runDoctor(json, services = {}) { diff --git a/dist/src/cli/doctor.js.map b/dist/src/cli/doctor.js.map index 418d778..67a726c 100644 --- a/dist/src/cli/doctor.js.map +++ b/dist/src/cli/doctor.js.map @@ -1 +1 @@ -{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/doctor.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/doctor.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/cli/main.js b/dist/src/cli/main.js index bc8b10f..e74395c 100644 --- a/dist/src/cli/main.js +++ b/dist/src/cli/main.js @@ -37,7 +37,7 @@ if (parsed) { } else if (parsed.command === 'version') { try { - await write(process.stdout, '0.1.0\n'); + await write(process.stdout, '0.2.0\n'); } catch { process.exitCode = 2; diff --git a/dist/src/cli/runtime.js b/dist/src/cli/runtime.js index 92e47c9..7050d46 100644 --- a/dist/src/cli/runtime.js +++ b/dist/src/cli/runtime.js @@ -2,7 +2,7 @@ import { jsonFailure, jsonReport } from '../reporters/json.js'; import { scanError } from '../security/errors.js'; import { exclusiveWrite } from '../fs/output.js'; /** Internal runtime/exit helpers: no CLI or environment bypass is exposed. */ -export const runtimeSupported = (version) => version.split('.')[0] === '24'; +export const runtimeSupported = (version) => /^(?:22|24)\.\d+\.\d+$/.test(version); export const reportExit = (report, interrupted = false) => report.errors.some(error => error.effect === 'fatal') ? 2 : interrupted ? 130 : report.status === 'partial' ? 3 : report.thresholdExceeded ? 1 : 0; /** Internal production destination service; it is not part of the package API. */ export const destinationWriter = (path, writeDiagnostic) => { diff --git a/dist/src/cli/runtime.js.map b/dist/src/cli/runtime.js.map index 5e20dc6..684280d 100644 --- a/dist/src/cli/runtime.js.map +++ b/dist/src/cli/runtime.js.map @@ -1 +1 @@ -{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/cli/runtime.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/cli/runtime.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/node/index.js b/dist/src/node/index.js index c25f491..28e3c51 100644 --- a/dist/src/node/index.js +++ b/dist/src/node/index.js @@ -271,7 +271,7 @@ export async function scanProject(request = {}, services = {}, signal) { if (findings.length > scanFindingLimit) budgetError('AF_FINDINGS_LIMIT', 'analyze'); const reportFindings = findings.slice(0, scanFindingLimit), outputErrors = finalErrors(errors), partial = outputErrors.length > 0 || Boolean(signal?.aborted), score = scoreFindings(findings, analyzed.length, partial), threshold = request.failOn ?? 'high', thresholdExceeded = threshold !== 'none' && reportFindings.some(finding => finding.applicability !== 'inactive' && finding.confidence !== 'low' && severities.indexOf(finding.severity) >= severities.indexOf(threshold)), overrideState = services.configOverrides ?? { codexHome: process.env.CODEX_HOME !== undefined, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR !== undefined }, limitations = [safe('Static local configuration analysis only'), ...(overrideState.codexHome ? [safe('Relocated Codex configuration was not assessed')] : []), ...(overrideState.claudeConfigDir ? [safe('Relocated Claude configuration was not assessed')] : [])]; - return { schemaVersion: '1.0.0', engineVersion: '0.1.0', rulesetVersion: '1.0.0', kind: 'scan', status: partial ? 'partial' : 'complete', scope: { project: 'PROJECT', userConfigs: Boolean(request.userConfigs), exclusions: [safe('Fixed exclusions were not assessed')] }, coverage: { eligibleFiles: candidates, analyzedFiles: analyzed.length, visitedEntries: visited, skippedByReason: walkResult.skipped, limitations }, sources: analyzed.map(item => ({ id: item.id, location: { sourceId: item.id, scope: item.scope, displayPath: safe(`source-${item.id.slice(1)}`) }, ...(item.kind ? { adapter: item.kind } : {}) })), agents: result.agents, mcpServers: result.mcpServers, capabilities: result.capabilities, findings: reportFindings, errors: outputErrors, score, thresholdExceeded, presentation: { minimumSeverity: request.minimumSeverity ?? 'info' } }; + return { schemaVersion: '1.0.0', engineVersion: '0.2.0', rulesetVersion: '1.0.0', kind: 'scan', status: partial ? 'partial' : 'complete', scope: { project: 'PROJECT', userConfigs: Boolean(request.userConfigs), exclusions: [safe('Fixed exclusions were not assessed')] }, coverage: { eligibleFiles: candidates, analyzedFiles: analyzed.length, visitedEntries: visited, skippedByReason: walkResult.skipped, limitations }, sources: analyzed.map(item => ({ id: item.id, location: { sourceId: item.id, scope: item.scope, displayPath: safe(`source-${item.id.slice(1)}`) }, ...(item.kind ? { adapter: item.kind } : {}) })), agents: result.agents, mcpServers: result.mcpServers, capabilities: result.capabilities, findings: reportFindings, errors: outputErrors, score, thresholdExceeded, presentation: { minimumSeverity: request.minimumSeverity ?? 'info' } }; } -function failureReport(request) { return { schemaVersion: '1.0.0', engineVersion: '0.1.0', rulesetVersion: '1.0.0', kind: 'scan', status: 'partial', scope: { project: 'PROJECT', userConfigs: Boolean(request.userConfigs), exclusions: [] }, coverage: { eligibleFiles: 0, analyzedFiles: 0, visitedEntries: 0, skippedByReason: { root: 1 }, limitations: [safe('Requested root was rejected')] }, sources: [], agents: [], mcpServers: [], capabilities: [], findings: [], errors: [scanError('AF_ROOT_INVALID', 'discovery', 'fatal')], score: scoreFindings([], 0, true), thresholdExceeded: false, presentation: { minimumSeverity: request.minimumSeverity ?? 'info' } }; } +function failureReport(request) { return { schemaVersion: '1.0.0', engineVersion: '0.2.0', rulesetVersion: '1.0.0', kind: 'scan', status: 'partial', scope: { project: 'PROJECT', userConfigs: Boolean(request.userConfigs), exclusions: [] }, coverage: { eligibleFiles: 0, analyzedFiles: 0, visitedEntries: 0, skippedByReason: { root: 1 }, limitations: [safe('Requested root was rejected')] }, sources: [], agents: [], mcpServers: [], capabilities: [], findings: [], errors: [scanError('AF_ROOT_INVALID', 'discovery', 'fatal')], score: scoreFindings([], 0, true), thresholdExceeded: false, presentation: { minimumSeverity: request.minimumSeverity ?? 'info' } }; } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/reporters/json.js b/dist/src/reporters/json.js index 25098c7..fdd56cd 100644 --- a/dist/src/reporters/json.js +++ b/dist/src/reporters/json.js @@ -2,5 +2,5 @@ import { safeJson } from '../security/safe.js'; import { LIMITS } from '../application/budget.js'; export function jsonReport(report) { const output = safeJson(report) + '\n'; if (Buffer.byteLength(output) > LIMITS.outputBytes) throw new Error('AF_OUTPUT_LIMIT'); return output; } -export function jsonFailure(code) { return safeJson({ kind: 'failure', schemaVersion: '1.0.0', engineVersion: '0.1.0', errors: [{ code, message: 'Scanner request failed' }] }) + '\n'; } +export function jsonFailure(code) { return safeJson({ kind: 'failure', schemaVersion: '1.0.0', engineVersion: '0.2.0', errors: [{ code, message: 'Scanner request failed' }] }) + '\n'; } //# sourceMappingURL=json.js.map \ No newline at end of file diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 3777e7c..048cd30 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -2,6 +2,8 @@ v0.1 recognizes Codex TOML; Claude Code settings/MCP/instructions; Cursor MCP/hooks/rules; Kiro MCP/hooks/steering; VS Code MCP/settings JSONC; and generic `.mcp.json`. Compatibility is based on the dated official links in the master plan (2026-09-10). +The local v0.2.0 `@adulph3/agentfence` distribution candidate preserves this detection scope and supports Node.js 22 and 24 on the existing platform matrix; Node 24 is recommended. The six hosted OS/runtime combinations have not yet run for v0.2.0. The published v0.1.0 GitHub artifact remains Node-24-only. The binary remains `agentfence`; the unrelated unscoped npm package is not this project. + The scanner preserves direct argv boundaries and classifies only documented MCP fields: transport, enabled state, launcher family, package selector, literal numeric endpoint class, curated/sanitized environment binding class, and recognized filesystem-server roots. It does not resolve DNS, environment values, inputs, headers, URLs, commands, package selectors, or root paths. Project discovery is limited to the fixed registry and excludes `.claude.json`; that file is an opt-in user-allowlist location only. Project `AGENTS.md` is shared evidence and does not confirm Codex, while user `.codex/AGENTS*.md` is possible Codex evidence only. A lone `.mcp.json` is generic MCP evidence, but is Claude-associated once independently valid Claude evidence exists in the same structural workspace. Agent records use local structural workspace ordinals, never raw paths or names. `--user-configs` reads only the fixed Codex, Claude, Cursor, and Kiro locations plus bounded Markdown under Claude rules and Kiro steering; it never crawls a home directory or follows relocation variables. It does not emulate managed/profile/session state, custom config roots, dynamic registration, live MCP descriptions, or unknown argument schemas. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 5d8a8b6..f66e33f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -13,3 +13,11 @@ - D21: link, device, and output containment checks are best-effort portable Node defenses. They reject observed unsafe components but cannot make a concurrent mutation race impossible; that residual limit is documented rather than hidden. +- D22: the requested v0.2.0 milestone is npm/npx distribution readiness and Node + 22/24 compatibility only. The master plan's R2 explicit policy/scope-control + work is deferred; no detection or security invariant changes accompany this + package-version increment. npm publication remains a separately authorized act. +- D23: the owner selected the public scoped npm package `@adulph3/agentfence` + for v0.2.0. The installed executable remains `agentfence`; the unrelated + unscoped registry package is outside this project's control. This naming + decision changes package identity and self-imports only, not detection behavior. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 514750e..58e8089 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -42,3 +42,27 @@ Actual Node 24 Linux validation is complete. No independent local macOS/Windows - GitHub Actions run `34972272811` passed the complete Node 24.21.0 install, typecheck, lint/package, test, coverage, and build sequence on Ubuntu, macOS, and Windows. The concrete compatibility corrections normalize registry-relative discovery paths on Windows while preserving POSIX backslash filenames, use canonical roots in the Claude project-selection test, and keep platform-specific process/sink behavior outside product semantics. - CodeQL default setup run `34972625363` passed for JavaScript/TypeScript. Private vulnerability reporting, vulnerability alerts, Dependabot security updates and automated fixes, secret scanning, and push protection are enabled. Main protection requires the three platform CI checks plus CodeQL, an approving review, resolved conversations, and linear history; force-push and deletion are disabled. - The hosted Windows run validates the supported Node 24 scan/test/build behavior, but it does not establish inherited-ACL confidentiality for arbitrary operator-selected output directories. npm ownership and npm publication remain outside v0.1.0 GitHub release scope. + +## v0.2.0 npm/npx local preparation — unscoped candidate superseded + +The requested distribution-only v0.2.0 candidate retains the v0.1 detector catalog, schema and scoring versions, no-execution/no-runtime-network behavior, safe output, and read-only default. Package metadata and lock root now identify 0.2.0, public-intent metadata, the unchanged CLI binary, a fixed release file allowlist, and `^22.0.0 || ^24.0.0` engines. Production scan, failure, doctor, and version output identify engine 0.2.0. A single strict runtime gate accepts Node 22 and 24 and rejects odd/unsupported majors; doctor and scan use the same gate. The new CI definition has Node 22.23.2 and Node 24.21.0 on Ubuntu, macOS, and Windows, with all existing typecheck, lint/package, test, coverage, build, and dry-run pack steps. These six v0.2.0 hosted combinations are **configured but not yet run**. + +Local Linux validation on real Node 22.23.2/npm 12.0.2 and Node 24.21.0/npm 11.19.0: clean offline `npm ci --ignore-scripts --no-audit --no-fund` passed on each; typecheck, lint/boundaries/package, build/catalog linkage, 35/35 full test files, six-file gated coverage, and benchmark passed on each. Gated scoring/security coverage was 100% lines and 92.73% branches on both. The Node 22 benchmark was p50 688.92 ms, p95 969.35 ms, RSS 135,872,512 bytes; Node 24 was p50 735.65 ms, p95 898.31 ms, RSS 171,483,136 bytes; both meet the p95 2,000 ms and RSS 256 MiB targets. The pre-import security probe now covers doctor as well as scan; it observed no network or child-process calls on either runtime. An online production `npm audit --omit=dev --audit-level=moderate` found zero vulnerabilities. + +The locally packed CLI was exercised through offline `npm exec --package=` and explicit `npx --package=` on both runtimes, without a global installation. Version 0.2.0, help, JSON doctor, clean default-path scan, opt-in `--user-configs` with an empty synthetic home, and a synthetic risky scan all passed. The risky scan returned exit 1 with `AF-SUPPLY-003` and other expected findings; a synthetic secret canary was absent from output and the input's sentinel command was not executed. Separate offline consumers installed the local tarball with lifecycle scripts disabled; root/core/node imports passed on both runtimes. The npm-exec cache needed one explicit dependency download during setup; subsequent package execution and consumer installs were offline. No npm package was published or globally installed. + +The final Node 24 `npm pack --dry-run --ignore-scripts` and actual `npm pack --ignore-scripts` matched: `agentfence-0.2.0.tgz`, 152 allowlisted files, 90,251 packed bytes, 409,121 unpacked bytes, SHA-256 `6b00614aa01e760c3d6530318f63fd323528dd24f001c084b2978bb51fbabf8b`. A repeat Node 24 pack after the final full test pass was byte-identical. Node 22's npm 12 dry-run pack also passed. Archive inspection found no path escapes, links, tests, internal docs, dependencies, caches, temporary files, credential patterns, or local workspace paths. The exact final archive was re-tested through npm exec/npx and fresh offline consumer imports on both real runtimes. Two Node 24 production dogfood scans were byte-identical (SHA-256 `1c00aaaf17e8a40d498fbbf5688e0045fce991b72e955c6dbdb2be0add11a5ef`), with expected partial `AF_WALK_LIMIT`, 161 visited entries, six analyzed files, ten findings, and score 52. The full tests, coverage, typecheck, and lint were rerun on both runtimes after the final security-test edit; all passed. + +The live npm registry check on 2026-09-16 showed that the unscoped `agentfence` package belongs to another maintainer and already contains versions 0.2.0 and 0.3.0. That candidate was never published and is superseded by the owner-selected scoped package `@adulph3/agentfence` below. Existing external evidence gaps include the not-yet-run six-job v0.2.0 CI matrix, Linux denied-network syscall tracing, and actual Windows output-parent ACL validation. + +## v0.2.0 scoped npm candidate — local validation complete + +The owner selected `@adulph3/agentfence` for public npm distribution while retaining the `agentfence` executable and version 0.2.0. The package and lock root agree; package validation rejects an unscoped name or mismatched lock metadata. README, security, compatibility, decisions, and release documentation use scoped npm/npx commands and explicitly distinguish the unrelated unscoped registry package. A read-only registry lookup returned 404 for the scoped name; that does not establish npm scope ownership. No npm publication, global install, commit, push, tag, or release was performed. + +On Linux, Node 22.23.2/npm 12.0.2 and Node 24.21.0/npm 11.19.0 each passed clean offline `npm ci --ignore-scripts --no-audit --no-fund`, typecheck, lint/boundaries/package checks, build/catalog linkage, the complete 35/35-file test suite, six-file gated coverage (100% lines, 92.73% branches), and the benchmark. Node 22 benchmark: p50 912.95 ms, p95 1062.27 ms, RSS 136,642,560 bytes. Node 24 benchmark: p50 955.50 ms, p95 1137.53 ms, RSS 170,135,552 bytes. Both meet the p95 <=2,000 ms and RSS <=256 MiB targets. The direct security probe observed no application network or child-process calls during scan or doctor on either runtime, and did not leak the fixture canary. Offline production dependency audits on both runtimes reported zero vulnerabilities in the available cache. + +The Node 24 dry-run and actual pack agreed on the scoped package. The tested local artifact is `adulph3-agentfence-0.2.0.tgz`: 152 allowlisted files, 90,144 packed bytes, 408,997 unpacked bytes, SHA-256 `fb6327407b814176da352d652b7ff71b7b9d9ae21352066c3091f850b2f2ad56`. Archive inspection found no links, path escapes, tests, internal docs, dependencies, caches, temporary files, credential patterns, or local workspace paths. A second Node 24 pack after coverage/build was byte-identical. Node 22's npm 12 dry-run and actual pack also passed (152 files, 90,056 bytes); its compressed bytes differ, so the Node 24 archive above is the single consumer-tested artifact. + +Using that exact archive and an already provisioned npm cache, offline `npm exec --package=` and explicit `npx --package=` passed on both runtimes without global installation. The packed binary reported 0.2.0; help, JSON doctor, clean scan, and opt-in user-config scan passed. The synthetic risky scan returned exit 1 with `AF-SUPPLY-003`; its secret canary was absent from output and its embedded sentinel command was not executed. Separate isolated offline consumers installed the tarball with lifecycle scripts disabled, imported scoped root/core/node entry points, and completed real packed-CLI scans on both runtimes. Two packed-CLI dogfood runs on each runtime were byte-identical across both runtimes (SHA-256 `1c00aaaf17e8a40d498fbbf5688e0045fce991b72e955c6dbdb2be0add11a5ef`): expected partial `AF_WALK_LIMIT`, six analyzed sources, ten findings, score 52. + +Remaining external gates: owner verification of npm account/scope rights and publication authorization; hosted v0.2.0 Node 22/24 Linux/macOS/Windows CI and required-check configuration; Linux denied-network syscall tracing; actual Windows output-parent ACL validation; and a wholly cache-empty offline consumer. The last is not expected to work without pre-provisioned runtime dependencies, and the local offline audit covers only the available cache. This local result does not claim `@adulph3/agentfence` is published. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 5ee895a..5d028ae 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,11 +1,17 @@ -# Local release readiness +# Release readiness -v0.1 is locally prepared only. Before publication, confirm package ownership, a private security reporting route, Node 24 Linux/macOS/Windows runs, a network-denied packaged runtime test, dependency audit status, and a clean offline tarball consumer. Publishing, tags, remote repositories, provenance, and uploads require separate authorization. +v0.1.0 is published as a verified [GitHub Release](https://github.com/Adulph3/AgentFence/releases/tag/v0.1.0). Its sole official asset is `agentfence-0.1.0.tgz` (SHA-256 `4d5271aad1a7f56f66555752d6a3184643cac0213a712079af33d95709db278b`). It is not an npm release. + +v0.2.0 is a local npm/npx preparation target under the owner-selected public scoped name `@adulph3/agentfence`; it is not yet published. Its executable remains `agentfence`. The unrelated unscoped npm package `agentfence` is not this project. A read-only lookup of `@adulph3/agentfence` on 2026-09-16 returned 404, which is consistent with an unpublished package but does not prove that the owner controls the npm scope. + +The validated local Node 24 artifact is `adulph3-agentfence-0.2.0.tgz` (152 files, 90,144 bytes; SHA-256 `fb6327407b814176da352d652b7ff71b7b9d9ae21352066c3091f850b2f2ad56`). This is a local candidate, not a public npm or GitHub release artifact. Node 22 and 24 offline package execution and isolated consumer checks passed against those bytes; see `docs/PROGRESS.md` for the exact evidence and limits. + +Before any npm publication, the owner must verify npm account/scope access, review the final scoped tarball and security evidence, run the six Node 22/24 Linux/macOS/Windows CI jobs, update branch protection to their actual check names, and explicitly authorize publication. A Linux network-denied packaged runtime trace and actual Windows output-parent ACL validation remain external evidence gaps. Publishing, tags, remote repositories, provenance, and uploads require separate authorization. Local release checklist: run typecheck, boundary/package checks, full tests, schema/CLI examples, deterministic dogfood, benchmark, and a dry-run package; verify the fixed rule registry and report schemas are packaged; review direct and transitive dependencies, licenses and lifecycle scripts; record exact evidence and the master-plan checksum. Treat an audit as evidence only for the reviewed cache or -network state in which it ran. Do not claim a public release until the external -gates above are independently complete. +network state in which it ran. Do not claim the v0.2.0 package is public until +those external gates and an authorized npm publication are independently complete. diff --git a/package-lock.json b/package-lock.json index 29096e2..610360e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "agentfence", - "version": "0.1.0", + "name": "@adulph3/agentfence", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "agentfence", - "version": "0.1.0", + "name": "@adulph3/agentfence", + "version": "0.2.0", "license": "MIT", "dependencies": { "@iarna/toml": "2.2.5", @@ -21,7 +21,7 @@ "typescript": "5.9.2" }, "engines": { - "node": ">=24 <25" + "node": "^22.0.0 || ^24.0.0" } }, "node_modules/@iarna/toml": { diff --git a/package.json b/package.json index 17b5f63..51363f4 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { - "name": "agentfence", - "version": "0.1.0", + "name": "@adulph3/agentfence", + "version": "0.2.0", "description": "Offline static configuration exposure audit for local AI agents", "license": "MIT", "type": "module", - "private": true, - "engines": { "node": ">=24 <25" }, + "engines": { "node": "^22.0.0 || ^24.0.0" }, + "repository": { "type": "git", "url": "git+https://github.com/Adulph3/AgentFence.git" }, + "homepage": "https://github.com/Adulph3/AgentFence#readme", + "bugs": { "url": "https://github.com/Adulph3/AgentFence/issues" }, + "keywords": ["ai-agent", "security", "scanner", "mcp", "codex", "claude-code", "cursor", "kiro"], + "publishConfig": { "access": "public" }, "bin": { "agentfence": "./dist/src/cli/main.js" }, "exports": { ".": "./dist/src/core/public.js", diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 74d9c61..7216f3e 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -10,8 +10,12 @@ const requiredFiles=new Set(['dist','schemas','README.md','LICENSE','SECURITY.md export function validatePackageMetadata(packageMetadata=pkg,lockMetadata=lock){ const root=lockMetadata.packages?.['']; + if(packageMetadata.name!=='@adulph3/agentfence'||packageMetadata.version!=='0.2.0'||packageMetadata.private===true)throw new Error('package name, version, or public state invalid'); if(packageMetadata.license!=='MIT')throw new Error('package license must be MIT'); - if(!root||root.name!==packageMetadata.name||root.version!==packageMetadata.version||root.license!==packageMetadata.license)throw new Error('package and lock root metadata disagree'); + if(lockMetadata.name!==packageMetadata.name||lockMetadata.version!==packageMetadata.version||!root||root.name!==packageMetadata.name||root.version!==packageMetadata.version||root.license!==packageMetadata.license||root.engines?.node!==packageMetadata.engines?.node||root.bin?.agentfence!=='dist/src/cli/main.js')throw new Error('package and lock root metadata disagree'); + if(packageMetadata.engines?.node!=='^22.0.0 || ^24.0.0'||packageMetadata.bin?.agentfence!=='./dist/src/cli/main.js')throw new Error('runtime or CLI metadata invalid'); + if(packageMetadata.repository?.type!=='git'||packageMetadata.repository?.url!=='git+https://github.com/Adulph3/AgentFence.git'||packageMetadata.homepage!=='https://github.com/Adulph3/AgentFence#readme'||packageMetadata.bugs?.url!=='https://github.com/Adulph3/AgentFence/issues'||packageMetadata.publishConfig?.access!=='public')throw new Error('public package metadata invalid'); + if(!Array.isArray(packageMetadata.keywords)||!['security','mcp','codex','claude-code','cursor','kiro'].every(keyword=>packageMetadata.keywords.includes(keyword)))throw new Error('package keywords incomplete'); if(Object.keys(packageMetadata.dependencies??{}).some(x=>!allowed.has(x)))throw new Error('unexpected runtime dependency'); if(packageMetadata.type!=='module'||!packageMetadata.exports?.['./core']||!packageMetadata.exports?.['./node'])throw new Error('package surface incomplete'); if(!Array.isArray(packageMetadata.files)||packageMetadata.files.length!==requiredFiles.size||packageMetadata.files.some(x=>!requiredFiles.has(x))||!packageMetadata.files.includes('LICENSE'))throw new Error('package files allowlist incomplete'); diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index d4c0565..d0a57ba 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1,5 +1,6 @@ import { safe, safeJson } from '../security/safe.js'; import { scoreFindings } from '../scoring/score.js'; +import { runtimeSupported } from './runtime.js'; import type { DoctorReport } from '../core/types.js'; const adapterKinds=['codex','claude-code','cursor','kiro','vscode','generic-mcp'] as const; @@ -18,7 +19,7 @@ export interface DoctorRunResult { readonly output:string; readonly exitCode:0|2 /** Pure, fixed-surface health report: it deliberately performs no discovery or probes. */ export function doctorReport(services:DoctorServices={}):DoctorReport{ const escapePass=pass(services.escapeSelfTest??defaultEscape),scorePass=pass(services.scoreSelfTest??defaultScore); - return {schemaVersion:'1.0.0',engineVersion:'0.1.0',rulesetVersion:'1.0.0',kind:'doctor',runtimeSupported:(services.runtimeVersion??process.versions.node).split('.')[0]==='24',platform:services.platform??process.platform,architecture:services.architecture??process.arch,adapters:adapterKinds.map(kind=>({kind,adapterVersion:'1.0.0'})),checks:[{id:'escape',state:escapePass?'pass':'fail',message:safe('In-memory safe-output self-test')},{id:'score',state:scorePass?'pass':'fail',message:safe('In-memory scoring self-test')}],overrides:services.overrides??{codexHome:process.env.CODEX_HOME!==undefined,claudeConfigDir:process.env.CLAUDE_CONFIG_DIR!==undefined}}; + return {schemaVersion:'1.0.0',engineVersion:'0.2.0',rulesetVersion:'1.0.0',kind:'doctor',runtimeSupported:runtimeSupported(services.runtimeVersion??process.versions.node),platform:services.platform??process.platform,architecture:services.architecture??process.arch,adapters:adapterKinds.map(kind=>({kind,adapterVersion:'1.0.0'})),checks:[{id:'escape',state:escapePass?'pass':'fail',message:safe('In-memory safe-output self-test')},{id:'score',state:scorePass?'pass':'fail',message:safe('In-memory scoring self-test')}],overrides:services.overrides??{codexHome:process.env.CODEX_HOME!==undefined,claudeConfigDir:process.env.CLAUDE_CONFIG_DIR!==undefined}}; } const plain=(report:DoctorReport)=>`AgentFence doctor\nRuntime supported: ${report.runtimeSupported?'yes':'no'}\nSelf-tests: ${report.checks.every(check=>check.state==='pass')?'pass':'fail'}\nAdapters: ${report.adapters.map(adapter=>`${adapter.kind}@${adapter.adapterVersion}`).join(', ')}\n`; export function runDoctor(json:boolean,services:DoctorServices={}):DoctorRunResult{ diff --git a/src/cli/main.ts b/src/cli/main.ts index 64d66f7..7db8b21 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -14,7 +14,7 @@ process.stdout.on('error',()=>{});process.stderr.on('error',()=>{}); const argv=process.argv.slice(2);let parsed;try{parsed=parseArgs([...argv]);}catch{process.exitCode=await fail(argv.includes('--json'),'AF_USAGE',argv.includes('--output'));} if(parsed){ if(parsed.command==='help'){try{await write(process.stdout,usage);}catch{process.exitCode=2;}} - else if(parsed.command==='version'){try{await write(process.stdout,'0.1.0\n');}catch{process.exitCode=2;}} + else if(parsed.command==='version'){try{await write(process.stdout,'0.2.0\n');}catch{process.exitCode=2;}} else if(!runtimeSupported(process.versions.node))process.exitCode=await fail(parsed.json,'AF_RUNTIME_UNSUPPORTED',Boolean(parsed.output)); else if(parsed.command==='doctor'){try{const result=runDoctor(parsed.json);await write(process.stdout,result.output);process.exitCode=result.exitCode;}catch{process.exitCode=2;}} else { diff --git a/src/cli/runtime.ts b/src/cli/runtime.ts index 500e67d..db11f17 100644 --- a/src/cli/runtime.ts +++ b/src/cli/runtime.ts @@ -5,7 +5,7 @@ import { scanError } from '../security/errors.js'; import { exclusiveWrite, type ExclusiveWriteReceipt } from '../fs/output.js'; /** Internal runtime/exit helpers: no CLI or environment bypass is exposed. */ -export const runtimeSupported=(version:string):boolean=>version.split('.')[0]==='24'; +export const runtimeSupported=(version:string):boolean=>/^(?:22|24)\.\d+\.\d+$/.test(version); export const reportExit=(report:ScanReport,interrupted=false):number=>report.errors.some(error=>error.effect==='fatal')?2:interrupted?130:report.status==='partial'?3:report.thresholdExceeded?1:0; export interface ScanRunnerServices { readonly runtimeVersion:string; scan(request:ScanRequest,signal?:AbortSignal):Promise; writeReport(text:string):Promise; writeDiagnostic(text:string):Promise; writeOutput?(text:string,signal?:AbortSignal):Promise; /** Identity-bound destination rollback after a post-write observed interrupt. */ cleanupOutput?():Promise; render?(report:ScanReport):string; } /** Internal production destination service; it is not part of the package API. */ diff --git a/src/node/index.ts b/src/node/index.ts index 3855955..220ba3f 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -110,5 +110,5 @@ try{ if(errors.some(error=>error.code==='AF_FINDINGS_LIMIT'))break; } }finally{await pool.close();} -const result=mergeAnalysisResults(perSource,scanFindingLimit),parentDirectory=(path:string)=>{const index=path.lastIndexOf('/');return index<0?'':path.slice(0,index);},overrideDirectories=new Set(analyzed.filter(item=>/(?:^|\/)AGENTS\.override\.md$/.test(item.relativePath.replace(/\\/g,'/'))).map(item=>parentDirectory(item.relativePath.replace(/\\/g,'/')))),inactiveSources=new Set(analyzed.filter(item=>{const path=item.relativePath.replace(/\\/g,'/');return/(?:^|\/)AGENTS\.md$/.test(path)&&overrideDirectories.has(parentDirectory(path));}).map(item=>item.id)),findings=result.findings.map(finding=>inactiveSources.has(finding.location.sourceId)?{...finding,applicability:'inactive' as const}:finding);errors.push(...result.errors);if(walkResult.limited)budgetError('AF_WALK_LIMIT','discovery');if(findings.length>scanFindingLimit)budgetError('AF_FINDINGS_LIMIT','analyze');const reportFindings=findings.slice(0,scanFindingLimit),outputErrors=finalErrors(errors),partial=outputErrors.length>0||Boolean(signal?.aborted),score=scoreFindings(findings,analyzed.length,partial),threshold=request.failOn??'high',thresholdExceeded=threshold!=='none'&&reportFindings.some(finding=>finding.applicability!=='inactive'&&finding.confidence!=='low'&&severities.indexOf(finding.severity)>=severities.indexOf(threshold)),overrideState=services.configOverrides??{codexHome:process.env.CODEX_HOME!==undefined,claudeConfigDir:process.env.CLAUDE_CONFIG_DIR!==undefined},limitations=[safe('Static local configuration analysis only'),...(overrideState.codexHome?[safe('Relocated Codex configuration was not assessed')]:[]),...(overrideState.claudeConfigDir?[safe('Relocated Claude configuration was not assessed')]:[])];return{schemaVersion:'1.0.0',engineVersion:'0.1.0',rulesetVersion:'1.0.0',kind:'scan',status:partial?'partial':'complete',scope:{project:'PROJECT',userConfigs:Boolean(request.userConfigs),exclusions:[safe('Fixed exclusions were not assessed')]},coverage:{eligibleFiles:candidates,analyzedFiles:analyzed.length,visitedEntries:visited,skippedByReason:walkResult.skipped,limitations},sources:analyzed.map(item=>({id:item.id,location:{sourceId:item.id,scope:item.scope,displayPath:safe(`source-${item.id.slice(1)}`)},...(item.kind?{adapter:item.kind}:{} )})),agents:result.agents,mcpServers:result.mcpServers,capabilities:result.capabilities,findings:reportFindings,errors:outputErrors,score,thresholdExceeded,presentation:{minimumSeverity:request.minimumSeverity??'info'}};} -function failureReport(request:ScanRequest):ScanReport{return{schemaVersion:'1.0.0',engineVersion:'0.1.0',rulesetVersion:'1.0.0',kind:'scan',status:'partial',scope:{project:'PROJECT',userConfigs:Boolean(request.userConfigs),exclusions:[]},coverage:{eligibleFiles:0,analyzedFiles:0,visitedEntries:0,skippedByReason:{root:1},limitations:[safe('Requested root was rejected')]},sources:[],agents:[],mcpServers:[],capabilities:[],findings:[],errors:[scanError('AF_ROOT_INVALID','discovery','fatal')],score:scoreFindings([],0,true),thresholdExceeded:false,presentation:{minimumSeverity:request.minimumSeverity??'info'}};} +const result=mergeAnalysisResults(perSource,scanFindingLimit),parentDirectory=(path:string)=>{const index=path.lastIndexOf('/');return index<0?'':path.slice(0,index);},overrideDirectories=new Set(analyzed.filter(item=>/(?:^|\/)AGENTS\.override\.md$/.test(item.relativePath.replace(/\\/g,'/'))).map(item=>parentDirectory(item.relativePath.replace(/\\/g,'/')))),inactiveSources=new Set(analyzed.filter(item=>{const path=item.relativePath.replace(/\\/g,'/');return/(?:^|\/)AGENTS\.md$/.test(path)&&overrideDirectories.has(parentDirectory(path));}).map(item=>item.id)),findings=result.findings.map(finding=>inactiveSources.has(finding.location.sourceId)?{...finding,applicability:'inactive' as const}:finding);errors.push(...result.errors);if(walkResult.limited)budgetError('AF_WALK_LIMIT','discovery');if(findings.length>scanFindingLimit)budgetError('AF_FINDINGS_LIMIT','analyze');const reportFindings=findings.slice(0,scanFindingLimit),outputErrors=finalErrors(errors),partial=outputErrors.length>0||Boolean(signal?.aborted),score=scoreFindings(findings,analyzed.length,partial),threshold=request.failOn??'high',thresholdExceeded=threshold!=='none'&&reportFindings.some(finding=>finding.applicability!=='inactive'&&finding.confidence!=='low'&&severities.indexOf(finding.severity)>=severities.indexOf(threshold)),overrideState=services.configOverrides??{codexHome:process.env.CODEX_HOME!==undefined,claudeConfigDir:process.env.CLAUDE_CONFIG_DIR!==undefined},limitations=[safe('Static local configuration analysis only'),...(overrideState.codexHome?[safe('Relocated Codex configuration was not assessed')]:[]),...(overrideState.claudeConfigDir?[safe('Relocated Claude configuration was not assessed')]:[])];return{schemaVersion:'1.0.0',engineVersion:'0.2.0',rulesetVersion:'1.0.0',kind:'scan',status:partial?'partial':'complete',scope:{project:'PROJECT',userConfigs:Boolean(request.userConfigs),exclusions:[safe('Fixed exclusions were not assessed')]},coverage:{eligibleFiles:candidates,analyzedFiles:analyzed.length,visitedEntries:visited,skippedByReason:walkResult.skipped,limitations},sources:analyzed.map(item=>({id:item.id,location:{sourceId:item.id,scope:item.scope,displayPath:safe(`source-${item.id.slice(1)}`)},...(item.kind?{adapter:item.kind}:{} )})),agents:result.agents,mcpServers:result.mcpServers,capabilities:result.capabilities,findings:reportFindings,errors:outputErrors,score,thresholdExceeded,presentation:{minimumSeverity:request.minimumSeverity??'info'}};} +function failureReport(request:ScanRequest):ScanReport{return{schemaVersion:'1.0.0',engineVersion:'0.2.0',rulesetVersion:'1.0.0',kind:'scan',status:'partial',scope:{project:'PROJECT',userConfigs:Boolean(request.userConfigs),exclusions:[]},coverage:{eligibleFiles:0,analyzedFiles:0,visitedEntries:0,skippedByReason:{root:1},limitations:[safe('Requested root was rejected')]},sources:[],agents:[],mcpServers:[],capabilities:[],findings:[],errors:[scanError('AF_ROOT_INVALID','discovery','fatal')],score:scoreFindings([],0,true),thresholdExceeded:false,presentation:{minimumSeverity:request.minimumSeverity??'info'}};} diff --git a/src/reporters/json.ts b/src/reporters/json.ts index 1cb7ffc..78d47c3 100644 --- a/src/reporters/json.ts +++ b/src/reporters/json.ts @@ -2,4 +2,4 @@ import type { ScanReport } from '../core/types.js'; import { safeJson } from '../security/safe.js'; import { LIMITS } from '../application/budget.js'; export function jsonReport(report:ScanReport):string{const output=safeJson(report)+'\n';if(Buffer.byteLength(output)>LIMITS.outputBytes)throw new Error('AF_OUTPUT_LIMIT');return output;} -export function jsonFailure(code:string):string{return safeJson({kind:'failure',schemaVersion:'1.0.0',engineVersion:'0.1.0',errors:[{code,message:'Scanner request failed'}]})+'\n';} +export function jsonFailure(code:string):string{return safeJson({kind:'failure',schemaVersion:'1.0.0',engineVersion:'0.2.0',errors:[{code,message:'Scanner request failed'}]})+'\n';} diff --git a/test/security/catalog-common-regressions.test.mjs b/test/security/catalog-common-regressions.test.mjs index 99ff3a8..2c881e2 100644 --- a/test/security/catalog-common-regressions.test.mjs +++ b/test/security/catalog-common-regressions.test.mjs @@ -9,4 +9,4 @@ import { analyzeSources } from '../../dist/src/core/index.js'; test('catalog shared benign declarations avoid unrelated rule families',()=>{const result=analyzeSources([{id:'S1',scope:'project',kind:'generic-mcp',relativePath:'.mcp.json',parseKind:'json',principalId:'P1',content:'{"mcpServers":{"safe":{"command":"reviewed-tool","env":{"NODE_ENV":"production"}}}}'}]);assert.deepEqual(result.findings.map(f=>f.ruleId),['AF-MCP-001']);}); test('catalog shared adversarial structured input preserves a valid sibling and safe output',async()=>{const root=await mkdtemp(join(tmpdir(),'agentfence-catalog-'));const canary='CATALOG_ADVERSARIAL_SECRET_CANARY';try{await writeFile(join(root,'.mcp.json'),JSON.stringify({mcpServers:{broken:{command:7,env:{UNTRUSTED_KEY:canary}},valid:{command:'reviewed-tool'}}}));const report=await scanProject({path:root});assert.equal(report.status,'partial');assert.equal(report.mcpServers.length,1);assert.ok(report.errors.some(error=>error.code==='AF_MCP_ENTITY'));assert.equal(jsonReport(report).includes(canary),false);}finally{await rm(root,{recursive:true,force:true});}}); -test('public finding-budget exhaustion is partial and cannot expose hostile source metadata',async()=>{const core=await import('agentfence/core'),canary='PUBLIC_BUDGET_SECRET_CANARY',result=core.analyzeSources([{id:canary,scope:'project',relativePath:canary,parseKind:'markdown',principalId:canary,content:'\u202e'.repeat(349_525)}]),serialized=JSON.stringify(result);assert.equal(result.findings.length,10_000);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));assert.equal(serialized.includes(canary),false);}); +test('public finding-budget exhaustion is partial and cannot expose hostile source metadata',async()=>{const core=await import('@adulph3/agentfence/core'),canary='PUBLIC_BUDGET_SECRET_CANARY',result=core.analyzeSources([{id:canary,scope:'project',relativePath:canary,parseKind:'markdown',principalId:canary,content:'\u202e'.repeat(349_525)}]),serialized=JSON.stringify(result);assert.equal(result.findings.length,10_000);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));assert.equal(serialized.includes(canary),false);}); diff --git a/test/security/runtime-cli.test.mjs b/test/security/runtime-cli.test.mjs index a741fdb..345fe91 100644 --- a/test/security/runtime-cli.test.mjs +++ b/test/security/runtime-cli.test.mjs @@ -25,7 +25,7 @@ const scanError=(code,effect='partial')=>({code,stage:'analyze',message:`Scanner const report=(overrides={})=>({schemaVersion:'1.0.0',engineVersion:'0.1.0',rulesetVersion:'1.0.0',kind:'scan',status:'complete',scope:{project:'PROJECT',userConfigs:false,exclusions:[]},coverage:{eligibleFiles:0,analyzedFiles:0,visitedEntries:0,skippedByReason:{},limitations:[]},sources:[],agents:[],mcpServers:[],capabilities:[],findings:[],errors:[],score:{modelVersion:'1.0.0',value:100,label:'low',provisional:false,categories:[],groups:[],interpretation:'test'},thresholdExceeded:false,presentation:{minimumSeverity:'info'},...overrides}); const parsed=(writes)=>JSON.parse(writes.join('')); const assertFailure=(writes,code)=>{const value=parsed(writes);assert.equal(validateFailure(value),true,JSON.stringify(validateFailure.errors));assert.equal(value.errors[0].code,code);assert.equal(JSON.stringify(value).includes('RUNTIME_CANARY'),false);}; -test('pre-import runtime probe records no network or process execution calls',async()=>{const dir=await mkdtemp(`${tmpdir()}/agentfence-probe-`),file=`${dir}/result.json`;try{const p=spawn(process.execPath,['test/security/runtime-probe.mjs',file],childOptions);const [code]=await once(p,'close');assert.equal(code,0);const r=JSON.parse(await readFile(file,'utf8'));assert.deepEqual(r.calls,[]);assert.equal(r.secret,false);assert.equal(r.status,'complete');}finally{await rm(dir,{recursive:true,force:true});}}); +test('pre-import scan and doctor probe records no network or process execution calls',async()=>{const dir=await mkdtemp(`${tmpdir()}/agentfence-probe-`),file=`${dir}/result.json`;try{const p=spawn(process.execPath,['test/security/runtime-probe.mjs',file],childOptions);const [code]=await once(p,'close');assert.equal(code,0);const r=JSON.parse(await readFile(file,'utf8'));assert.deepEqual(r.calls,[]);assert.equal(r.secret,false);assert.equal(r.status,'complete');assert.equal(r.doctorExit,0);assert.equal(r.doctorSupported,true);}finally{await rm(dir,{recursive:true,force:true});}}); test('actual host scan and doctor enforce the runtime-specific JSON contract',async()=>{if(supportedHost){const scan=await runToFile(['scan','test/fixtures/risky','--json']);assert.equal(scan.code,1);assert.equal(scan.err,'');const scanValue=JSON.parse(scan.out);assert.equal(validateReport(scanValue),true,JSON.stringify(validateReport.errors));assert.equal(scanValue.thresholdExceeded,true);const doctor=await runToFile(['doctor','--json']);assert.equal(doctor.code,0);assert.equal(doctor.err,'');assert.equal(validateDoctor(JSON.parse(doctor.out)),true,JSON.stringify(validateDoctor.errors));}else{for(const args of [['scan','test/fixtures/risky','--json'],['doctor','--json']]){const r=await runToFile(args);assert.equal(r.code,2);assert.equal(r.err,'');assert.notEqual(r.out,'');const value=JSON.parse(r.out);assert.equal(validateFailure(value),true,JSON.stringify(validateFailure.errors));assert.equal(value.errors[0].code,'AF_RUNTIME_UNSUPPORTED');assert.equal(JSON.stringify(value).includes('AF_TEST_CANARY'),false);}}const help=await run(['--help']),version=await run(['--version']);assert.equal(help.code,0);assert.equal(version.code,0);}); test('production supported-runtime doctor is schema-valid, fixed-surface, and secret-free',async()=>{const result=await runSupported(['doctor','--json'],{env:{CODEX_HOME:'DOCTOR_OVERRIDE_CANARY',CLAUDE_CONFIG_DIR:'DOCTOR_OVERRIDE_CANARY_2'}});assert.equal(result.code,0);assert.equal(result.err,'');const value=JSON.parse(result.out);assert.equal(validateDoctor(value),true,JSON.stringify(validateDoctor.errors));assert.deepEqual(value.adapters.map(adapter=>adapter.kind),['codex','claude-code','cursor','kiro','vscode','generic-mcp']);assert.equal(result.out.includes('DOCTOR_OVERRIDE_CANARY'),false);}); test('CLI early-closed stdout is quiet and controlled on the actual host runtime',async()=>{const r=await run(['scan','test/fixtures/risky','--json'],true);assert.equal(r.signal,null);assert.equal(r.code,supportedHost?1:2);assert.equal(/EPIPE|Error:|AF_TEST_CANARY/.test(r.err),false);}); @@ -34,7 +34,8 @@ test('production supported-runtime output mode keeps stdout empty for success an test('production supported-runtime fatal JSON and plain sinks remain separate',async()=>{const missing=await mkdtemp(`${tmpdir()}/agentfence-missing-`);await rm(missing,{recursive:true,force:true});const json=await runSupported(['scan',missing,'--json']);assert.equal(json.code,2);assert.equal(json.err,'');const failure=JSON.parse(json.out);assert.equal(validateFailure(failure),true,JSON.stringify(validateFailure.errors));assert.equal(failure.errors[0].code,'AF_ROOT_INVALID');const plain=await runSupported(['scan',missing]);assert.equal(plain.code,2);assert.equal(plain.out,'');assert.equal(plain.err,'AgentFence request failed\n');}); test('production supported-runtime EPIPE stays exit 2 for clean and partial scans',{skip:process.platform==='win32'?'requires a POSIX pipeline':false},async()=>{const home=await mkdtemp(`${tmpdir()}/agentfence-home-`),notDirectory=join(home,'not-home');try{await writeFile(notDirectory,'x');for(const options of [{path:'test/fixtures/clean',userConfigs:false,env:{}},{path:'test/fixtures/clean',userConfigs:true,env:{HOME:notDirectory,USERPROFILE:notDirectory}}]){const result=await runClosedPipeline(options.path,options.userConfigs,options.env);assert.equal(result.signal,null);assert.equal(result.code,0);const statuses=result.out.trim().split(/\s+/).map(Number);assert.equal(statuses[0],2);assert.ok(statuses[1]===0||statuses[1]===1);assert.equal(/EPIPE|Error:|AF_TEST_CANARY|at\s/.test(result.err),false);}}finally{await rm(home,{recursive:true,force:true});}}); test('production supported-runtime interruption writes a valid partial report and exits 130',{skip:process.platform==='win32'?'child_process.kill cannot model a Windows console Ctrl+C event':false},async()=>{const root=await mkdtemp(`${tmpdir()}/agentfence-interrupt-`);try{for(let index=0;index<900;index++){const directory=join(root,`d${index}`);await mkdir(directory);await writeFile(join(directory,'AGENTS.md'),'Use reviewed configuration only.');}const interrupted=await runSupported(['scan',root,'--json'],{interruptAfterMs:250});assert.equal(interrupted.signal,null);assert.equal(interrupted.code,130);assert.equal(interrupted.err,'');const output=JSON.parse(interrupted.out);assert.equal(validateReport(output),true,JSON.stringify(validateReport.errors));assert.equal(output.status,'partial');assert.ok(output.errors.some(error=>error.code==='AF_INTERRUPTED'));assert.equal(interrupted.out.includes('AF_TEST_CANARY'),false);}finally{await rm(root,{recursive:true,force:true});}}); -test('internal runtime and exit matrix keeps Node24 scan behavior testable',()=>{assert.equal(runtimeSupported('24.0.0'),true);assert.equal(runtimeSupported('22.23.2'),false);const base={errors:[],status:'complete',thresholdExceeded:false};assert.equal(reportExit({...base,thresholdExceeded:true}),1);assert.equal(reportExit({...base,status:'partial'}),3);assert.equal(reportExit({...base},true),130);assert.equal(reportExit({...base,errors:[{effect:'fatal'}]},true),2);}); +test('internal runtime and exit matrix accepts even LTS majors only',()=>{for(const version of ['22.0.0','22.23.2','24.0.0','24.21.0'])assert.equal(runtimeSupported(version),true,version);for(const version of ['20.19.0','23.11.0','25.0.0','24','24.0.0-pre','24.0.0extra'])assert.equal(runtimeSupported(version),false,version);const base={errors:[],status:'complete',thresholdExceeded:false};assert.equal(reportExit({...base,thresholdExceeded:true}),1);assert.equal(reportExit({...base,status:'partial'}),3);assert.equal(reportExit({...base},true),130);assert.equal(reportExit({...base,errors:[{effect:'fatal'}]},true),2);}); +test('Node 22 injected scan runner has the same complete JSON and exit contract as Node 24',async()=>{for(const runtimeVersion of ['22.0.0','24.0.0']){const writes=[],exit=await runScan({path:'PROJECT'},true,false,{runtimeVersion,scan:async()=>report(),writeReport:async text=>{writes.push(text);},writeDiagnostic:async()=>{}});assert.equal(exit,0);assert.equal(validateReport(parsed(writes)),true,JSON.stringify(validateReport.errors));}}); test('injected Node24 runner returns clean, threshold, and partial exit states',async()=>{for(const [input,expected] of [[report(),0],[report({thresholdExceeded:true}),1],[report({status:'partial',thresholdExceeded:true,errors:[scanError('AF_WALK_LIMIT')],score:{...report().score,provisional:true}}),3]]){const writes=[],exit=await runScan({path:'PROJECT'},true,false,{runtimeVersion:'24.0.0',scan:async()=>input,writeReport:async text=>{writes.push(text);},writeDiagnostic:async()=>{}});assert.equal(exit,expected);const value=parsed(writes);assert.equal(validateReport(value),true,JSON.stringify(validateReport.errors));assert.equal(value.status,input.status);}}); test('injected Node24 runner emits strict root and internal failures',async()=>{const rootWrites=[],rootExit=await runScan({path:'PROJECT'},true,false,{runtimeVersion:'24.0.0',scan:async()=>report({errors:[scanError('AF_ROOT_INVALID','fatal')]}),writeReport:async text=>{rootWrites.push(text);},writeDiagnostic:async()=>{}});assert.equal(rootExit,2);assertFailure(rootWrites,'AF_ROOT_INVALID');const internalWrites=[],internalExit=await runScan({path:'PROJECT'},true,false,{runtimeVersion:'24.0.0',scan:async()=>{throw new Error('RUNTIME_CANARY');},writeReport:async text=>{internalWrites.push(text);},writeDiagnostic:async()=>{}});assert.equal(internalExit,2);assertFailure(internalWrites,'AF_INTERNAL');}); test('injected Node24 runner maps destination failures to AF_OUTPUT_FAILURE without a complete destination',async()=>{const reports=[],diagnostics=[],committed=[],attempts=[];const exit=await runScan({path:'PROJECT'},true,false,{runtimeVersion:'24.0.0',scan:async()=>report(),writeReport:async text=>{reports.push(text);},writeDiagnostic:async text=>{diagnostics.push(text);},writeOutput:async text=>{attempts.push(text);throw new Error('RUNTIME_CANARY');}});assert.equal(exit,2);assert.equal(attempts.length,1);assert.deepEqual(committed,[]);assert.deepEqual(reports,[]);assert.deepEqual(diagnostics,['AgentFence request failed\n']);}); diff --git a/test/security/runtime-probe.mjs b/test/security/runtime-probe.mjs index e6f4e27..67faa22 100644 --- a/test/security/runtime-probe.mjs +++ b/test/security/runtime-probe.mjs @@ -7,6 +7,8 @@ const calls=[]; const mark=name=>function(){calls.push(name);throw new Error('fo for(const [mod,names] of [[http,['request','get','createServer']],[https,['request','get','createServer']],[net,['connect','createConnection','createServer']],[tls,['connect','createServer']],[dns,['lookup','resolve','resolve4','resolve6']],[dgram,['createSocket']],[http2,['connect','createServer','createSecureServer']],[child,['exec','execFile','spawn','fork']]])for(const name of names)if(typeof mod[name]==='function')mod[name]=mark(name); globalThis.fetch=mark('fetch');syncBuiltinESMExports(); const { scanProject }=await import('../../dist/src/node/index.js'); +const { runDoctor }=await import('../../dist/src/cli/doctor.js'); const report=await scanProject({path:fileURLToPath(new URL('../fixtures/risky',import.meta.url))}); -const result=JSON.stringify({calls,status:report.status,findings:report.findings.length,secret:JSON.stringify(report).includes('AF_TEST_CANARY_9b1')}); +const doctor=runDoctor(true); +const result=JSON.stringify({calls,status:report.status,findings:report.findings.length,secret:JSON.stringify(report).includes('AF_TEST_CANARY_9b1'),doctorExit:doctor.exitCode,doctorSupported:JSON.parse(doctor.output).runtimeSupported}); if(process.argv[2])await writeFile(process.argv[2],result);else console.log(result); diff --git a/test/unit/batch7-doctor.test.mjs b/test/unit/batch7-doctor.test.mjs index a7b8832..cfd2caf 100644 --- a/test/unit/batch7-doctor.test.mjs +++ b/test/unit/batch7-doctor.test.mjs @@ -9,7 +9,10 @@ const supported={runtimeVersion:'24.0.0',platform:'linux',architecture:'x64',ove test('injected doctor is deterministic, schema-valid, and exposes fixed support metadata',()=>{ const report=doctorReport(supported),run=runDoctor(true,supported);assert.equal(run.exitCode,0);assert.equal(validate(report),true,JSON.stringify(validate.errors));assert.deepEqual(report.adapters.map(adapter=>[adapter.kind,adapter.adapterVersion]),[['codex','1.0.0'],['claude-code','1.0.0'],['cursor','1.0.0'],['kiro','1.0.0'],['vscode','1.0.0'],['generic-mcp','1.0.0']]);assert.equal(JSON.parse(run.output).overrides.codexHome,true);const plain=runDoctor(false,supported).output;for(const name of ['codex','claude-code','cursor','kiro','vscode','generic-mcp'])assert.ok(plain.includes(`${name}@1.0.0`));assert.equal(plain.includes('DOCTOR_OVERRIDE_CANARY'),false);assert.equal(JSON.parse(doctor(true)).kind,'doctor'); }); +test('injected doctor supports Node 22 and 24 with the same fixed metadata',()=>{ + for(const runtimeVersion of ['22.0.0','22.23.2','24.0.0','24.21.0']){const result=runDoctor(true,{...supported,runtimeVersion}),value=JSON.parse(result.output);assert.equal(result.exitCode,0);assert.equal(value.runtimeSupported,true);assert.equal(value.engineVersion,'0.2.0');assert.equal(validate(value),true,JSON.stringify(validate.errors));} +}); test('injected doctor failures and unsupported runtime exit two with fixed safe output',()=>{ - for(const services of [{...supported,escapeSelfTest:()=>false},{...supported,scoreSelfTest:()=>false},{...supported,runtimeVersion:'22.0.0'}]){const result=runDoctor(true,services),value=JSON.parse(result.output);assert.equal(result.exitCode,2);assert.equal(validate(value),true,JSON.stringify(validate.errors));assert.equal(JSON.stringify(value).includes('DOCTOR_OVERRIDE_CANARY'),false);} + for(const services of [{...supported,escapeSelfTest:()=>false},{...supported,scoreSelfTest:()=>false},...['20.19.0','23.11.0','25.0.0','24.0.0-pre'].map(runtimeVersion=>({...supported,runtimeVersion}))]){const result=runDoctor(true,services),value=JSON.parse(result.output);assert.equal(result.exitCode,2);assert.equal(validate(value),true,JSON.stringify(validate.errors));assert.equal(JSON.stringify(value).includes('DOCTOR_OVERRIDE_CANARY'),false);} }); test('doctor treats an empty recognized override as present without exposing its value',()=>{const prior=process.env.CODEX_HOME;try{process.env.CODEX_HOME='';const report=doctorReport({runtimeVersion:'24.0.0'});assert.equal(report.overrides.codexHome,true);assert.equal(JSON.stringify(report).includes('CODEX_HOME'),false);}finally{if(prior===undefined)delete process.env.CODEX_HOME;else process.env.CODEX_HOME=prior;}}); diff --git a/test/unit/batch8-package-completion.test.mjs b/test/unit/batch8-package-completion.test.mjs index 707fd9f..e975fbc 100644 --- a/test/unit/batch8-package-completion.test.mjs +++ b/test/unit/batch8-package-completion.test.mjs @@ -7,11 +7,20 @@ import { validatePackageMetadata } from '../../scripts/check-package.mjs'; const root=new URL('../..',import.meta.url); -test('Batch 8 package metadata is MIT, locked, and retains the release allowlist',()=>{ - assert.equal(pkg.private,true); +test('public-ready package metadata is MIT, locked, and retains the release allowlist',()=>{ + assert.notEqual(pkg.private,true); + assert.equal(pkg.name,'@adulph3/agentfence'); + assert.equal(pkg.version,'0.2.0'); + assert.equal(lock.name,pkg.name); assert.equal(pkg.license,'MIT'); assert.equal(lock.packages[''].license,'MIT'); assert.doesNotThrow(()=>validatePackageMetadata(pkg,lock)); + assert.throws(()=>validatePackageMetadata({...pkg,name:'agentfence'},lock),/package name/); + assert.throws(()=>validatePackageMetadata(pkg,{...lock,name:'agentfence'}),/metadata disagree/); + assert.throws(()=>validatePackageMetadata({...pkg,bin:{agentfence2:'./dist/src/cli/main.js'}},lock),/CLI metadata/); + assert.throws(()=>validatePackageMetadata({...pkg,private:true},lock),/public state/); + assert.throws(()=>validatePackageMetadata({...pkg,engines:{node:'>=22'}},lock),/metadata disagree|runtime/); + assert.throws(()=>validatePackageMetadata({...pkg,publishConfig:{access:'restricted'}},lock),/public package metadata/); assert.throws(()=>validatePackageMetadata({...pkg,license:'Apache-2.0'},lock),/license/); assert.throws(()=>validatePackageMetadata(pkg,{...lock,packages:{...lock.packages,'':{...lock.packages[''],license:'Apache-2.0'}}}),/metadata disagree/); assert.throws(()=>validatePackageMetadata({...pkg,files:pkg.files.filter(file=>file!=='LICENSE')},lock),/allowlist/); diff --git a/test/unit/core.test.mjs b/test/unit/core.test.mjs index 91cc182..cfe81ad 100644 --- a/test/unit/core.test.mjs +++ b/test/unit/core.test.mjs @@ -16,10 +16,10 @@ test('score is null without analyzed input', () => assert.equal(scoreFindings([] test('public core boundary projects hostile source identifiers to structural IDs',()=>{const canary='RAW_CORE_CANARY';const result=analyzeSources([{id:canary,principalId:canary,scope:'not-a-scope',kind:'not-an-adapter',relativePath:canary,parseKind:'not-a-parser',content:`${canary}\u202e`}]);const output=JSON.stringify(result);assert.equal(output.includes(canary),false);for(const finding of result.findings){assert.match(finding.location.sourceId,/^S[1-9][0-9]*$/);assert.match(finding.principalId??'P1',/^P(?:[0-9]+|-user)?$/);}}); test('public source projection resolves ID collisions and bounds hostile structural IDs',()=>{const canary='PUBLIC_SOURCE_CANARY',base={scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',content:'\u202e'};const collision=analyzeSources([{...base,id:'S2',principalId:'P2'},{...base,id:canary,principalId:canary}]);const sourceIds=collision.findings.map(finding=>finding.location.sourceId);assert.deepEqual(sourceIds,['S2','S3']);assert.equal(new Set(sourceIds).size,2);const huge=analyzeSources([{...base,id:`S${'9'.repeat(10_000)}${canary}`,principalId:`P${'9'.repeat(10_000)}${canary}`}]),serialized=JSON.stringify(huge);assert.equal(serialized.includes(canary),false);assert.ok(huge.findings.every(finding=>finding.location.sourceId.length<=5));}); test('public source count truncation is explicit rather than silent',()=>{const source=index=>({id:`S${index+1}`,scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:`P${index+1}`,content:''});const result=analyzeSources(Array.from({length:1_001},(_,index)=>source(index)));assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));assert.equal(result.errors.filter(error=>error.code==='AF_REPORT_LIMIT').length,1);}); -test('package facade bounds preprocessing and retains one truncation sentinel',async()=>{const core=await import('agentfence/core'),source=index=>({id:`S${index+1}`,scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:`P${index+1}`,content:''}),sources=Array.from({length:1_002},(_,index)=>source(index));Object.defineProperty(sources,1_001,{enumerable:true,get(){throw new Error('UNBOUNDED_PUBLIC_PREPROCESS');}});const result=core.analyzeSources(sources);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));}); -test('package facade ignores unknown fields and acquisition parser metadata',async()=>{const core=await import('agentfence/core'),canary='PUBLIC_UNKNOWN_FIELD_CANARY',source={id:'S1',scope:'project',kind:'generic-mcp',relativePath:'.mcp.json',parseKind:'json',principalId:'P1',content:'{"mcpServers":{}}',parsed:{mcpServers:{hidden:{command:'tool',env:{OPENAI_API_KEY:canary}}}},unknownField:canary};const result=core.analyzeSources([source]);assert.equal(result.findings.some(finding=>finding.ruleId==='AF-SECRET-002'),false);assert.equal(JSON.stringify(result).includes(canary),false);}); -test('public core detects U+202C with safe code-point-aware identity',async()=>{const core=await import('agentfence/core'),result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:'a\u202Cb'}]),finding=result.findings.find(item=>item.ruleId==='AF-UNICODE-001');assert.ok(finding);assert.deepEqual(finding.evidence.codePoints,['U+202C']);assert.deepEqual([finding.location.line,finding.location.column,finding.location.endLine,finding.location.endColumn],[1,2,1,3]);assert.match(finding.id,/^[a-f0-9]{16}$/);}); -test('public core rejects oversized hostile Markdown before Unicode analysis',async()=>{const core=await import('agentfence/core'),canary='PUBLIC_OVERSIZED_CANARY',result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:`${canary}${'\u202e'.repeat(1_000_001)}`}]);assert.equal(result.findings.length,0);assert.ok(result.errors.some(error=>error.code==='AF_OVERSIZED_INPUT'));assert.equal(JSON.stringify(result).includes(canary),false);}); -test('public core caps Unicode allocation at the global finding limit with deterministic retained coordinates',async()=>{const core=await import('agentfence/core'),result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:'\u202e'.repeat(349_525)}]),columns=result.findings.map(finding=>finding.location.column).sort((a,b)=>a-b);assert.equal(result.findings.length,10_000);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));assert.equal(validateAnalyzeResultDto(result),true);assert.deepEqual([columns[0],columns.at(-1)],[1,10_000]);assert.equal(JSON.stringify(result).includes('PUBLIC_UNICODE_CANARY'),false);}); -test('public core stops before cumulative accepted content exceeds 32 MiB',async()=>{const core=await import('agentfence/core'),oneMiB='x'.repeat(1_024*1_024),canary='PUBLIC_TOTAL_BYTES_CANARY',sources=Array.from({length:32},(_,index)=>({id:`S${index+1}`,scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:`P${index+1}`,content:oneMiB}));sources.push({id:'S33',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P33',content:`\u202e${canary}`});const result=core.analyzeSources(sources);assert.equal(result.findings.length,0);assert.ok(result.errors.some(error=>error.code==='AF_TOTAL_BYTES'&&error.sourceId==='S33'));assert.equal(validateAnalyzeResultDto(result),true);assert.equal(JSON.stringify(result).includes(canary),false);}); -test('package core facade excludes internal raw-DT0 helpers and strips acquisition ordinals',async()=>{const core=await import('agentfence/core');assert.equal('prepareSources'in core,false);assert.equal('mergeAnalysisResults'in core,false);const result=core.analyzeSources([{id:'S1',scope:'project',kind:'codex',relativePath:'.codex/config.toml',parseKind:'toml',principalId:'P1',workspaceOrdinal:9007199254740991,content:'[mcp_servers.one]\ncommand = "tool"'},{id:'S2',scope:'project',kind:'codex',relativePath:'packages/x/.codex/config.toml',parseKind:'toml',principalId:'P2',workspaceOrdinal:7,content:'[mcp_servers.one]\ncommand = "tool"'}]);assert.deepEqual(result.agents.map(agent=>agent.id).sort(),['A-codex-S0','A-codex-S1']);assert.equal(JSON.stringify(result).includes('9007199254740991'),false);}); +test('package facade bounds preprocessing and retains one truncation sentinel',async()=>{const core=await import('@adulph3/agentfence/core'),source=index=>({id:`S${index+1}`,scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:`P${index+1}`,content:''}),sources=Array.from({length:1_002},(_,index)=>source(index));Object.defineProperty(sources,1_001,{enumerable:true,get(){throw new Error('UNBOUNDED_PUBLIC_PREPROCESS');}});const result=core.analyzeSources(sources);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));}); +test('package facade ignores unknown fields and acquisition parser metadata',async()=>{const core=await import('@adulph3/agentfence/core'),canary='PUBLIC_UNKNOWN_FIELD_CANARY',source={id:'S1',scope:'project',kind:'generic-mcp',relativePath:'.mcp.json',parseKind:'json',principalId:'P1',content:'{"mcpServers":{}}',parsed:{mcpServers:{hidden:{command:'tool',env:{OPENAI_API_KEY:canary}}}},unknownField:canary};const result=core.analyzeSources([source]);assert.equal(result.findings.some(finding=>finding.ruleId==='AF-SECRET-002'),false);assert.equal(JSON.stringify(result).includes(canary),false);}); +test('public core detects U+202C with safe code-point-aware identity',async()=>{const core=await import('@adulph3/agentfence/core'),result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:'a\u202Cb'}]),finding=result.findings.find(item=>item.ruleId==='AF-UNICODE-001');assert.ok(finding);assert.deepEqual(finding.evidence.codePoints,['U+202C']);assert.deepEqual([finding.location.line,finding.location.column,finding.location.endLine,finding.location.endColumn],[1,2,1,3]);assert.match(finding.id,/^[a-f0-9]{16}$/);}); +test('public core rejects oversized hostile Markdown before Unicode analysis',async()=>{const core=await import('@adulph3/agentfence/core'),canary='PUBLIC_OVERSIZED_CANARY',result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:`${canary}${'\u202e'.repeat(1_000_001)}`}]);assert.equal(result.findings.length,0);assert.ok(result.errors.some(error=>error.code==='AF_OVERSIZED_INPUT'));assert.equal(JSON.stringify(result).includes(canary),false);}); +test('public core caps Unicode allocation at the global finding limit with deterministic retained coordinates',async()=>{const core=await import('@adulph3/agentfence/core'),result=core.analyzeSources([{id:'S1',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P1',content:'\u202e'.repeat(349_525)}]),columns=result.findings.map(finding=>finding.location.column).sort((a,b)=>a-b);assert.equal(result.findings.length,10_000);assert.ok(result.errors.some(error=>error.code==='AF_REPORT_LIMIT'));assert.equal(validateAnalyzeResultDto(result),true);assert.deepEqual([columns[0],columns.at(-1)],[1,10_000]);assert.equal(JSON.stringify(result).includes('PUBLIC_UNICODE_CANARY'),false);}); +test('public core stops before cumulative accepted content exceeds 32 MiB',async()=>{const core=await import('@adulph3/agentfence/core'),oneMiB='x'.repeat(1_024*1_024),canary='PUBLIC_TOTAL_BYTES_CANARY',sources=Array.from({length:32},(_,index)=>({id:`S${index+1}`,scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:`P${index+1}`,content:oneMiB}));sources.push({id:'S33',scope:'project',relativePath:'AGENTS.md',parseKind:'markdown',principalId:'P33',content:`\u202e${canary}`});const result=core.analyzeSources(sources);assert.equal(result.findings.length,0);assert.ok(result.errors.some(error=>error.code==='AF_TOTAL_BYTES'&&error.sourceId==='S33'));assert.equal(validateAnalyzeResultDto(result),true);assert.equal(JSON.stringify(result).includes(canary),false);}); +test('package core facade excludes internal raw-DT0 helpers and strips acquisition ordinals',async()=>{const core=await import('@adulph3/agentfence/core');assert.equal('prepareSources'in core,false);assert.equal('mergeAnalysisResults'in core,false);const result=core.analyzeSources([{id:'S1',scope:'project',kind:'codex',relativePath:'.codex/config.toml',parseKind:'toml',principalId:'P1',workspaceOrdinal:9007199254740991,content:'[mcp_servers.one]\ncommand = "tool"'},{id:'S2',scope:'project',kind:'codex',relativePath:'packages/x/.codex/config.toml',parseKind:'toml',principalId:'P2',workspaceOrdinal:7,content:'[mcp_servers.one]\ncommand = "tool"'}]);assert.deepEqual(result.agents.map(agent=>agent.id).sort(),['A-codex-S0','A-codex-S1']);assert.equal(JSON.stringify(result).includes('9007199254740991'),false);});