Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ fclt audit
fclt audit --non-interactive --severity high
fclt audit --non-interactive --report-root /absolute/isolated/reports --json
fclt audit fix mcp:github --report /absolute/isolated/reports/static-<sha256>.json --dry-run
fclt audit fix mcp:github --report /absolute/isolated/reports/static-<sha256>.json --yes
```

Audit evaluation is read-only by default: it does not refresh saved reports or
Expand All @@ -511,10 +512,13 @@ Persisted reports are content-addressed authorization envelopes containing the
report payload and a receipt that binds its bytes, evaluated source revisions,
and finding identities in one atomic file. `audit safe` requires the exact
fresh envelope path and `--yes`; legacy `*-latest.json` files and pre-revision-9
detached report/receipt pairs cannot authorize mutation. `audit fix` currently
supports exact-report `--dry-run` inspection only. Automated MCP mutation fails
closed pending descriptor-bound source and destination authorization through
the final commit boundary.
detached report/receipt pairs cannot authorize mutation. `audit fix --dry-run`
inspects exact matches without writing. For a supported inline MCP secret,
`audit fix --yes` moves only the report-authorized value from the bound
canonical source into a bound owner-only local overlay. The descriptor-relative
transaction revalidates both objects and their ancestors at the final commit
boundary, refuses Git-worktree destinations, and fails closed on drift or
replacement without writing external tool homes.

Keep tracked MCP config secret-free. Use local overlays such as `mcp/servers.local.json` for machine-specific secrets.

Expand Down
7 changes: 5 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,11 @@ content-addressed report-and-receipt envelope only to a pre-existing,
non-symlinked root that does not overlap any evaluated source. `audit safe`
requires `--report <exact-report.json> --yes`; legacy latest reports and
detached pre-revision-9 pairs are never trusted for mutation. `audit fix`
supports exact-report `--dry-run` inspection only and automated MCP mutation
fails closed pending a descriptor-bound exact-source/destination commit design.
uses `--dry-run` for zero-write inspection. With explicit `--yes`, supported
inline MCP secrets are moved only from the exact report-bound canonical source
to its bound owner-only local overlay. The descriptor-relative transaction
revalidates source, destination, ancestors, permissions, and identity at the
final commit boundary, and refuses Git-worktree destinations.
`--update-index` is a separate explicit canonical generated-state mutation.

`self-update` detects release-script, npm/Bun, and mise-managed npm installs.
Expand Down
20 changes: 13 additions & 7 deletions docs/security-trust.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ as a separate explicit mutation.

Verified-envelope loading is descriptor-bound and bounded before allocation.
Oversize, sparse, growing, multiply linked, non-private, or identity-changing
envelopes fail closed before they can authorize `audit safe` mutation or an
`audit fix --dry-run` preview.
envelopes fail closed before they can authorize `audit safe`, an `audit fix`
mutation, or a zero-write fix preview.

Persistence currently requires native descriptor-relative `openat`/`linkat`
support (macOS or Linux). Other platforms fail closed rather than falling back
Expand Down Expand Up @@ -152,10 +152,14 @@ Compatibility note: older releases refreshed
`.ai/.facult/audit/*-latest.json` and generated index audit annotations during
every audit. Those implicit writes are removed. Legacy saved `*-latest.json`
reports remain inspection artifacts only; they do not authorize `audit safe`
or the former automated `audit fix` path. `audit safe` mutations require an
or `audit fix`. `audit safe` mutations require an
exact fresh content-addressed report, its receipt, and explicit `--yes`
approval. `audit fix` is limited to zero-write `--dry-run` inspection pending
mutation-time descriptor binding.
approval. `audit fix --dry-run` remains zero-write. Supported `audit fix --yes`
remediation holds the exact report-authorized canonical MCP source and local
destination open with no-follow descriptors, stages owner-only bytes, and
revalidates object identity, permissions, ancestors, source bytes, and the
outside-Git policy at the atomic commit boundary. Drift, replacement, or
cross-root redirection fails closed with rollback and no external artifacts.

Root cause of the old behavior: the static and agent library runners wrote
their latest reports before returning; both non-interactive CLI wrappers then
Expand All @@ -164,19 +168,21 @@ runners; and the typed MCP audit routed to the non-interactive CLI. Read-only
entry points therefore shared persistence code instead of merely evaluating
the scanned source.

Suppress or preview reviewed findings:
Suppress, preview, or remediate reviewed findings:

```bash
fclt audit safe mcp:github --rule static:mcp-env-inline-secret --note "reviewed" \
--report /absolute/isolated/audit-reports/static-<sha256>.json --yes
fclt audit fix mcp:github \
--report /absolute/isolated/audit-reports/static-<sha256>.json --dry-run
fclt audit fix mcp:github \
--report /absolute/isolated/audit-reports/static-<sha256>.json --yes
```

Receipts fail closed when their schema/capability revision, report hash,
finding identities, source path identity or content revision, or 15-minute
freshness window does not match. Supply both exact reports with repeated
`--report` for a combined static/agent safe action or fix dry-run.
`--report` for a combined static/agent safe action or fix selection.

## Secrets

Expand Down
4 changes: 2 additions & 2 deletions scripts/verify-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ if (auditPersistenceContract(process.platform) === "fail-closed") {
!reportName ||
reportNames.length !== 1 ||
envelope?.schemaVersion !== 1 ||
envelope.receipt?.schemaVersion !== 5 ||
envelope.receipt.reportRevision !== 10 ||
envelope.receipt?.schemaVersion !== 6 ||
envelope.receipt.reportRevision !== 11 ||
JSON.stringify(envelope.report) !==
JSON.stringify(JSON.parse(persistedAuditText)) ||
(await Bun.file(auditSkill).text()) !== auditSourceBefore
Expand Down
33 changes: 2 additions & 31 deletions src/audit/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { constants } from "node:fs";
import { lstat, mkdir, mkdtemp, open, realpath, rm } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { basename, join } from "node:path";
import { extractServersObject } from "../mcp-config";
import {
facultConfigPath,
facultRootDir,
Expand Down Expand Up @@ -208,36 +209,6 @@ function parseMaxItemsFlag(argv: string[]): number | null {
return null;
}

function extractMcpServersObject(
parsed: unknown
): Record<string, unknown> | null {
if (!isPlainObject(parsed)) {
return null;
}
const obj = parsed as Record<string, unknown>;
if (isPlainObject(obj.mcpServers)) {
return obj.mcpServers as Record<string, unknown>;
}
for (const [k, v] of Object.entries(obj)) {
if (k.endsWith(".mcpServers") && isPlainObject(v)) {
return v as Record<string, unknown>;
}
}
if (isPlainObject(obj["mcp.servers"])) {
return obj["mcp.servers"] as Record<string, unknown>;
}
if (isPlainObject(obj.servers)) {
return obj.servers as Record<string, unknown>;
}
if (isPlainObject(obj.mcp)) {
const mcp = obj.mcp as Record<string, unknown>;
if (isPlainObject(mcp.servers)) {
return mcp.servers as Record<string, unknown>;
}
}
return null;
}

function mcpSafeText(definition: unknown): string {
if (!isPlainObject(definition)) {
return redactPossibleSecrets(String(definition));
Expand Down Expand Up @@ -1402,7 +1373,7 @@ export async function evaluateAgentAudit(opts?: {
} catch {
continue;
}
const serversObj = extractMcpServersObject(parsed);
const serversObj = extractServersObject(parsed);
if (!serversObj) {
continue;
}
Expand Down
Loading
Loading