@odla-ai/security
v0.3.1
Published
Evidence-gated vulnerability harness for odla and Cloudflare apps: passive rules, checkpointed recon/hunt, app-attributed hosted independent validation, baselines, and SARIF.
Maintainers
Readme
@odla-ai/security
A model-agnostic vulnerability harness for odla and Cloudflare applications. It keeps reconnaissance, hunting, validation, evidence, and reporting separate; checkpoints progress outside model context; and refuses to call a lead a confirmed vulnerability until code-enforced gates pass.
This package is informed by Cloudflare's Build your own vulnerability
harness
and its released security-audit seed
skill. The article links
the seed skill, not production-harness source. @odla-ai/security is an
independent, smaller implementation of the published lessons, not a port or a
claim to Cloudflare's fleet results.
What ships in 0.2
- Passive JavaScript/TypeScript rules in
generic, plus Cloudflare Wrangler and odla inbound-trust-header checks in the derived profiles. - Immutable repository snapshots with content/manifest digests, capture flags, Git provenance, and fail-closed size limits. Truncated snapshots are rejected.
- A checkpointed
Recon → Hunt → Validate → Reportstate machine. - Line-shift-resistant root-cause fingerprints that stores can index across runs, plus reasoned, expiring baselines. The bundled stores do not query a fleet-wide canonical-finding index yet.
- Optional discovery and validation roles over any structural
@odla-ai/ai-compatibleextract()facade. - Mechanical path, line, trace, threat-model, reproduction, and source-integrity checks in ordinary code.
- An explicit isolated-executor seam. There is deliberately no host-shell fallback.
- JSON for replay, Markdown for humans, and SARIF 2.1.0 for code scanning.
- A passive
odla-securityCLI for local and credential-free CI scans.
The deterministic rules emit candidates, not confirmed vulnerabilities. They are a cheap way to seed deeper hunts and catch a narrow set of high-signal mistakes. A clean run does not prove a codebase is secure, and this project does not claim recall on real repositories.
Install and scan
Availability: first require
npm view @odla-ai/[email protected] versionto succeed. An exact-versionE404means this release is unavailable; it does not prove the package name itself is absent. Do not install or treat the gate as passed. Inside this repository usenpm run security:scan.
npm i -D --save-exact @odla-ai/[email protected]
npx odla-security scan . \
--profile odla \
--out .odla/security/latest \
--fail-on high \
--fail-on-candidates criticalUse --profile odla for applications built with odla; it
includes the generic and Cloudflare checks plus odla trust-boundary rules. Use
--profile cloudflare-app for Cloudflare applications that do not use odla.
CLI options
| Option | Meaning |
| --- | --- |
| [target] | Directory to snapshot; defaults to .. |
| --profile generic\|cloudflare-app\|odla | Select passive rules and hunt taxonomy. |
| --out <directory> | Private JSON, Markdown, SARIF, and checkpoint directory. |
| --run-id <id> | Correlation ID; matching snapshot/config replays untrusted local stages. |
| --baseline <json> | Accountable fingerprint suppressions with owner, reason, and expiry. |
| --fail-on <severity> | Gate confirmed findings; defaults to high. |
| --fail-on-candidates <severity> | Gate unconfirmed leads; defaults to critical. |
| --no-fail-on-candidates | Explicitly disable the passive-lead gate. |
| --allow-incomplete | Accept incomplete autonomous coverage after review. |
| --exclude <path> | Add a repeatable snapshot exclusion. |
Credential-free CI can run the same passive gate and retain reviewed artifacts:
- run: npm ci --no-audit --no-fund
- run: npx odla-security scan . --profile odla --out .odla/security/ci --fail-on high --fail-on-candidates critical
- if: always()
uses: actions/upload-artifact@v6
with:
name: odla-security-passive
path: |
.odla/security/ci/REPORT.md
.odla/security/ci/report.json
.odla/security/ci/report.sarif
retention-days: 14report.sarif is suitable for a code-scanning integration, but unconfirmed
leads are intentionally emitted at note level. Review reports before granting
CI artifacts broader access.
The CLI never calls a model, executes target code, enables a network, writes to production, or fixes source. It writes:
report.json— complete run, coverage, rejected candidates, and evidence;REPORT.md— status-aware human report;report.sarif— code-scanning interchange;state/*.json— atomic private stage checkpoints for audit and diagnostics.
The Node writers create artifacts mode 0600, replace leaves atomically, and
reject symlinks in output/state path components instead of following them out
of the requested directory.
Local checkpoints are intentionally unauthenticated in 0.2, so a later process
replays every security-critical stage instead of trusting stored candidates,
coverage, decisions, or evidence. This prevents checkpoint edits from creating
confirmations or clean false negatives; authenticated distributed receipts are
the next persistence increment.
--fail-on high gates only confirmed, unsuppressed findings. Passive CLI scans
cannot produce confirmed findings because they configure no validator or
executor. Critical candidate gating is on by default;
--no-fail-on-candidates is the explicit opt-out. odla uses the critical tier
for credential-shaped leads without letting noisy high-severity heuristics
block every build.
Use platform-managed AI for autonomous hunts
Hosted odla runs exchange an ordinary app-owner developer credential for two short-lived, role-bound inference grants. Provider keys remain in odla's platform vault, the platform chooses the admin-configured discovery and validation models, and each response must carry a matching run/purpose/role/provider/model receipt before the harness accepts it:
npx @odla-ai/cli security run . --env dev --ack-redacted-sourceFor autonomous SaaS operation, let odla.ai acquire a connected GitHub source server-side instead of uploading a local snapshot:
npx @odla-ai/cli security github connect --env dev # Contents/Metadata read; optional Checks write
npx @odla-ai/cli security plan --env dev
npx @odla-ai/cli security sources --env dev
npx @odla-ai/cli security run --source <source-id> --ref main --env dev --plan-digest <digest-from-security-plan> --ack-redacted-sourceThe server verifies the requested repository, resolves the ref to a commit SHA, and keeps source inside one bounded acquire/analyze stage. No raw archive, file body, or explicit source excerpt is deliberately persisted in D1, hosted report storage, Workflow checkpoints, logs, or GitHub Checks. The private normalized report may retain bounded best-effort credential-pattern-redacted model-derived prose and repository-relative paths for up to 90 days; treat it as sensitive because those values may reveal source semantics. GitHub/provider terms remain separate.
GitHub installation is read authorization, not disclosure consent, so the
redacted-source acknowledgement remains mandatory. security plan shows the
exact discovery/validation provider/model routes, policy revisions and bounds,
prompt bundle, redaction/report contracts, readiness, retention, and
no-execution boundary before consent. A job requires --plan-digest and binds
the acknowledgement to that complete digest; any covered change requires a
refreshed plan and acknowledgement. The CLI never asks for a PAT, installation
id, provider key, or provider/model selection override; it deliberately shows
the read-only route identities in the consent preflight.
The archive envelope is 16 MiB compressed, 48 MiB decompressed, 20,000 entries,
10,000 files, 2 MiB per file, and 16 MiB aggregate file bytes. Links, special
entries, unsafe redirects, duplicate paths, and escaping paths are rejected.
The acquire/analyze step has zero automatic retries; a failed job is terminal
and must be explicitly rerun after correcting the cause. Source ids belong to
one app/environment. The App is not subscribed to Push and ignores one if
unexpectedly delivered. Disconnect cancels
nonterminal work while terminal reports remain until normal expiry. Source jobs
default to cloudflare-app; local-path jobs default to odla.
The CLI follows and gates the job by default. Optional Checks write permission produces a best-effort fixed summary linking to Studio; a Check is not the CLI gate. Target code is not executed, so without an injected isolated executor the hosted pass cannot mechanically confirm a finding. Critical candidates and incomplete/shallow/blocked/unscheduled/budget-exhausted work are the operative signals and cannot be presented as proof of security.
The local-snapshot command handles device authorization and app/run attribution; it never
asks for a provider key. Its only network peer is the configured odla platform
origin. It sends bounded typed operations to /registry/ai/extract; the
odla.ai Worker—not this package—owns the security system prompts and output
schemas, selects the admin-configured provider/model, resolves the vaulted
credential, calls the provider, records o11y/usage, and returns a role-bound
receipt. The lower-level factory below is for embedding the same flow in
another orchestrator.
import {
createPlatformSecurityReasoners,
createSecurityHarness,
odlaProfile,
securityFingerprint,
} from "@odla-ai/security";
import { FileRunStore, snapshotDirectory } from "@odla-ai/security/node";
const snapshot = await snapshotDirectory(".");
const hosted = await createPlatformSecurityReasoners({
platform: "https://odla.ai",
token: process.env.ODLA_DEV_TOKEN!,
appId: "my-app",
env: "dev",
repository: snapshot.repository,
revision: snapshot.revision,
snapshotDigest: snapshot.digest,
clientRunId: crypto.randomUUID(),
sourceDisclosure: "redacted",
});
const harness = createSecurityHarness({
profile: odlaProfile(),
store: new FileRunStore(".odla/security/state"),
discoveryReasoner: hosted.discoveryReasoner,
validationReasoner: hosted.validationReasoner,
});
const report = await harness.run(snapshot, { runId: hosted.run.runId });
await hosted.complete({
reportDigest: await securityFingerprint(report),
coverageStatus: report.coverageStatus,
confirmed: report.metrics.confirmed,
candidates: report.metrics.candidates,
});Opaque inference grants stay inside the returned reasoner closures and are not
exposed in run metadata. Starting a run is attributed to the named app and
environment; odla's engineering environment additionally sends an authorized
self-audit service identity through a fresh platform:security:self approval.
odla-ai security run defaults to 12 fairly sampled hunt tasks, writes private
artifacts under .odla/security/hosted (or --out), keeps source disclosure
redacted, and leaves active reproduction off. --ack-redacted-source is the
explicit approval after reviewing the configured providers' retention and
residency terms. Platform admins, not the local process, select the two models
and their immutable call budgets. Hosted reasoners advertise those ceilings to
the harness: reconnaissance consumes one discovery call, first-pass hunts are
fairly capped to the remainder, and only calls left after first passes may fund
shallow retries. Validation spends its separate grant on vulnerability
candidates in severity, kind, confidence, and reproduction-readiness order.
Skipped hunts become budget_exhausted coverage cells; skipped validations
remain candidate with validationStatus: "budget_exhausted". The report's
callBudget counts make either gap explicit and force coverageStatus to
incomplete. Caller-managed reasoners that do not advertise callBudget keep
the existing caller-defined scheduling behavior. Prior local state never supplies candidates, coverage,
decisions, or confirmation authority. The lower-level factory caller must
write/digest its report and invoke hosted.complete(...); that capability uses
a separate one-time grant and never reuses the credential which started the run.
For hosted calls, maxInputBytes measures the decoded dynamic input field;
the immutable server-owned prompt/tool contract is protected by a separate
wire-size ceiling and does not silently consume that advertised budget.
maxOutputTokens is a hard cap over the reasoners' operation defaults (4,096
for recon, 8,192 for hunt, and 4,096 for validation), not a request to spend the
entire configured maximum on every call. Optional local limits can only narrow
those server bounds.
These are call-count and per-call request bounds, not a separate aggregate
per-run token or USD-spend ceiling.
The platform writes a metadata-only reserved usage row before each provider
call, then finalizes it to success or error; reservations left open by a
Worker interruption become abandoned after one hour. Fixed early-access
rolling 24-hour ceilings are 3 runs per app/environment, 6 per owner, 20 in the
customer pool, and 3 in odla's separate self-audit pool. A platform-ceiling
429 always carries a conservative Retry-After. A provider-side 429 uses
the sanitized provider_rate_limited code after the provider SDK's bounded
retries and carries a bounded Retry-After only when the upstream supplied one.
The broker does not multiply retries or silently change the admin-selected
model. Daily retention removes each run, grant, and usage record after 90
days from its own creation, up to one sweep interval. Provider retention and
residency are separate and still require the disclosure acknowledgement.
For custom or entirely local orchestration, the lower-level adapters remain
structurally typed: createAiDiscoveryReasoner and
createAiValidationReasoner accept any compatible extract() facade. That
path is explicitly caller-managed; it does not provide platform attribution,
opaque grants, or receipt verification.
Discovery and validation are different capabilities. A validator can return
only confirmed, rejected, or needs_reproduction; it has no API for filing
a new finding. With the default requireIndependentModel: true, a two-model
run must declare provider and model for both roles; the harness rejects missing
identity data, the same reasoner identity, or the same provider on both
sides. Hosted adapters verify the platform receipt on every call. On a custom
facade, resolveProvider additionally checks each declared provider against the
actual model router; without a resolver, provider identity is caller-attested
and must not be treated as cryptographic proof of independence. Adapter
identities, models, and methods are captured and frozen at construction.
Setting requireIndependentModel: false can permit a non-independent experiment
to run, but it can never promote that run's output to confirmed.
Source passed to a reasoner is line-numbered, digest-labelled, bounded, and
explicitly marked as untrusted. The default modelSourceDisclosure: "redacted"
withholds untracked, provenance-unknown, and sensitive-path files and redacts
credential-shaped spans; "full" is an explicit provider-disclosure opt-in. This reduces secret
exposure and prompt-injection risk but does not make source trustworthy or
override a model provider's retention/residency terms. Model-proposed commands
are inert data unless the host separately enables active mode and injects an
isolated executor.
Redacted validation also replaces every free-text candidate field,
reproduction argument/expectation, and stdout/stderr body with structural
metadata or byte counts before it crosses to a validator. This prevents a
custom rule from smuggling text copied from a withheld file. AI adapters cap
the complete dynamic prompt with maxInputBytes (128 KiB by default), including
metadata, candidate, and evidence—not only source context. Full disclosure is
the only mode that passes candidate theory text, and execution output remains
best-effort credential-pattern-redacted even there.
The confirmation gate
confirmed means all of these held:
- The candidate names a concrete attacker, crossed boundary, action, and impact.
- Plain code verifies every cited repository-relative path and one-based line,
and enforces
entrypoint → propagation* → sinkordering. - A configured isolated executor observes the proposed reproduction against the exact snapshot digest.
- The executor attests to read-only source, returns the same post-run digest, and plain code compares command arguments, exit status, stdout/stderr expectations, normalized working directories, time, and output budgets.
- A separate validation reasoner tries to disprove the trace, reachability, mitigations, runtime assumptions, impact, and evidence.
Without an independent validator, output stays candidate. Without clean
execution evidence, a validator's positive judgment stays
needs_reproduction. A validator's rejection is advisory and remains visible
as a candidate unless deterministic checks disprove the source trace.
Structural schema validity alone can never confirm a finding.
Active execution is fail closed
The package does not ship a local process executor. To set policy.active:
true, supply an IsolatedExecutor whose isolation is container, vm, or
remote-sandbox and whose source is read-only. The harness rejects active mode
without it and rejects evidence if source changes.
The executor is responsible for a disposable environment with no host secrets, cloud metadata, Docker socket, SSH agent, or production credentials; bounded CPU, memory, disk, process, time, and output; and network disabled unless the run policy explicitly opts in. A container label alone is not proof of a safe sandbox.
The harness bounds and redacts stdout/stderr and argument values before persistence or validation model calls. Executor evidence is materialized once, so validation and persistence cannot observe different accessor-controlled values. Unsupported evidence fields are rejected and omitted; external executor/reasoner error text is also redacted and byte-bounded. Artifacts are still security-sensitive; do not publish them without review.
Baselines are accountable and temporary
{
"version": 1,
"entries": [{
"fingerprint": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"reason": "Accepted while the legacy endpoint is removed in SEC-42",
"owner": "platform-security",
"expiresAt": "2026-08-01T00:00:00.000Z"
}]
}Missing owners, reasons shorter than eight characters, malformed fingerprints/timestamps, and duplicate entries fail the run. Expired entries stop suppressing automatically. Rejected candidates are retained in JSON so repeated scans can measure noise instead of forgetting it.
Persistence and Cloudflare
MemoryRunStore works in tests and short Worker invocations.
FileRunStore atomically records local/CI stages without following state or
directory symlinks. These unauthenticated files are audit records, not trusted
resume authority. Distributed Cloudflare
operation should implement RunStore over D1 or Durable Objects and schedule
each coverage cell with Cloudflare Workflows (or @odla-ai/workflows) using
leases and idempotency keys. Process execution remains a remote Node/VM concern;
Workers are orchestrators, not Linux sandboxes.
Node subpath
Node-only filesystem APIs live at @odla-ai/security/node; keeping them on a
separate entrypoint preserves a Worker-compatible root API:
snapshotDirectory(root: string, options?: SnapshotDirectoryOptions): Promise<RepositorySnapshot>
new FileRunStore(directory: string): FileRunStore
writeSecurityArtifacts(directory: string, report: SecurityReport): Promise<void>snapshotDirectory resolves Git metadata from repository subdirectories and
worktrees. Untracked or provenance-unknown files are still scanned by passive
rules but withheld from model context by default. An output
directory inside the target is automatically excluded by the CLI so a repeated
run does not scan its own prior artifacts.
Step 9 roadmap
The minimum trustworthy single-repository harness is implemented first. Next:
- Authenticated D1/DO task leases, signed attempt receipts, heartbeats, and artifact storage so distributed runs can resume without trusting editable local JSON.
- Versioned attack/prompt bundles and held-out seeded-vulnerability evals.
- Coverage gapfill and narrowly seeded sibling hunts driven by evidence, not every zero-finding task.
- Deterministic candidate indexes followed by model-assisted dedup only when volume warrants it.
- Dependency graph and cross-repository reachability after more than one repository matters.
- Patch proposals and fail-before/pass-after regression evidence on isolated overlays. Humans remain the only merge/deploy authority.
Feedback will create new immutable prompt versions; it will never silently rewrite the prompt attached to an in-flight or historical task.
Current limitations
- The passive rules are regex-based candidate generators, not parsers or SAST.
FileRunStoreis single-host persistence, not a distributed queue.- There is no bundled sandbox, auto-fixer, cross-repo tracer, or production reachability system in 0.1.
- Multiple runs may find different code paths. A zero-finding task is not automatically marked shallow; explicit task-health evidence is required.
- A coverage cell marked complete means its bounded task completed. Areas that do not fit one context remain shallow with a chunking wishlist item, and cells beyond the task budget remain explicitly unscheduled.
- Reconnaissance
areasmust resolve to an exact repository-relative file or directory (or.). Descriptive/unmapped labels do not fall back to the full repository; they remain shallow with an explicit mapping wishlist item. - SARIF includes candidates with their disposition so consumers must not render every result as confirmed.
See the installed package's exported TypeScript declarations/JSDoc or the rendered reference at https://odla.ai/docs/packages/security for the root API surface, and the Node-subpath section above for filesystem adapters.
