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
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"feature_directory": "specs/033-pluggable-stores"}
{"feature_directory": "specs/034-local-output-store"}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,5 +381,5 @@ else:
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
[`specs/033-pluggable-stores/plan.md`](specs/033-pluggable-stores/plan.md)
[`specs/034-local-output-store/plan.md`](specs/034-local-output-store/plan.md)
<!-- SPECKIT END -->
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,48 @@ The tree-sitter discovery pipeline is tuned for **web-service shapes**. What wor

If your project doesn't match a supported shape, the generator will still write a report — but it will likely show "Total findings: 0" because no entry points were discovered. That's a coverage gap on our side, not a clean bill of health. Expanding the query set is [tracked in our issue tracker](https://github.com/kusari-oss/darnit/issues?q=is%3Aissue+threat-model+coverage).

## How to Use Darnit

Darnit's product path is **skills invoking MCP tools inside a coding agent** (Claude Code today; other clients coming). You do not call `darnit audit` on the CLI as a daily-use interface -- that command exists for local iteration on framework code and quick sanity checks, not as the front door.

### The product path: skills + MCP

1. Install darnit's MCP config and skills into your coding-agent client:
```bash
darnit install # global (Claude Code default)
darnit install --project # per-project (.mcp.json + .claude/skills/)
darnit install --client claude-desktop
```
2. Restart the client. Skills appear as slash commands.
3. Invoke a skill in your agent -- for OpenSSF Baseline audits:
```
/darnit-audit
```
The skill orchestrates darnit's MCP tools behind the scenes: it runs the audit, handles PENDING_LLM consultations, calls remediation tools, generates attestations, and pulls in project context from `.project/project.yaml`. You reason about the results conversationally; you do not shell out.

See [`docs/getting-started/using-skills.md`](docs/getting-started/using-skills.md) for the full skill catalog, install-target matrix, and multi-repo / profile-selection details.

### The CLI: dev + debug scaffolding

`darnit audit`, `darnit run`, and `darnit serve` all exist for a reason, but only `darnit serve` is a product-facing command (the MCP server the skills talk to). The other two are development tools:

| Command | Intended use | Not for |
|---------|--------------|---------|
| `darnit serve` | The MCP server your coding agent connects to. Started automatically by the client's MCP integration. | Direct interactive use. |
| `darnit audit` | Single-run local check of one repo, without the coding-agent loop. Useful when iterating on framework or TOML controls. | Fleet auditing, remediation-in-conversation, PENDING_LLM handling. |
| `darnit run` | Batch execution for CI regression / parity testing. | Interactive daily-driver audits. |
| `darnit harness` | Full audit driver with in-band LLM dispatch and pluggable question resolvers -- the runtime the skills-via-MCP path uses under the hood. Reachable from the CLI for testing the driver itself. | Daily-driver interactive use (invoke via a skill instead). |

If you're evaluating darnit and just want to see something happen: `uv run darnit audit /path/to/repo` will print a report. If you're actually using darnit on real projects: install the skills.

### Direction of travel

RFC-0001 (`docs/rfcs/0001-core-rearchitecture.md`) formalizes this split: the harness driver (Stage 1+) is the runtime that skills call into, and CLI commands become thin adapters around the harness rather than parallel entry points. When Stage 1 fully lands, the skills path gets more capable (in-band LLM dispatch, question resolvers) and the CLI stays at parity for the sanity-check use case.

## Quick Start

The examples below show the MCP tool signatures that the `/darnit-audit` skill calls under the hood. In normal use you invoke the skill and never see these directly. They're useful for embedding darnit in your own tooling or debugging what the skill orchestrates.

### Run an Audit

```python
Expand Down
143 changes: 143 additions & 0 deletions docs/plugin-authoring/stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,146 @@ audit in a process that actually uses the artifact class you back --
never to zero-config runs, never to audits that skip your kind. Keep
`__init__` cheap and open connections on first `write` if the backend
is expensive to establish.

---

# Writing artifacts outside the repo (feature 034)

Feature 034 ships two additional filesystem-backed backends inside
darnit-core alongside the in-repo defaults. Both are selectable from
`.baseline.toml` under any `[stores.<kind>]` block; both write outside
the audited repository. They exist because the in-repo defaults land
attestations, reports, and audit-cache under `<repo>/.darnit/`, which
is often not where an operator wants them (backups, CI artifact
directories, XDG-idiomatic locations).

## `local-fs`: arbitrary root path

Points a store at any local filesystem path. Config:

```toml
[stores.attestation]
backend = "local-fs"
root = "/absolute/path" # or "~/subpath", or "$VAR/subpath"
```

Path resolution runs in this order at store construction:

1. `$VAR` interpolation via darnit's env-subst helper. A missing
variable raises `KeyError` immediately -- a typo does NOT silently
expand to `""`. This is deliberate: `root` is a compliance-critical
config value; loud failure beats surprise empty writes.
2. `~` expansion via `os.path.expanduser`.
3. Absolute `Path.resolve()`.

Directory creation is deferred to the first write. Filename
sanitization is inherited from the in-repo default -- a bundle_id
containing `../../etc/foo` produces a sanitized filename inside
`root`, not a path escape.

### Example: CI runner with persistent cache + report artifacts

```toml
[stores.cache]
backend = "local-fs"
root = "$RUNNER_CACHE_DIR/darnit"

[stores.report]
backend = "local-fs"
root = "$RUNNER_ARTIFACTS_DIR/darnit-reports"
```

- First-run: cache write goes to `$RUNNER_CACHE_DIR/darnit/`; next-run
cache read hits because the runner restored the cache directory
between jobs.
- Reports land where the runner's artifact-upload step already looks.
Markdown / JSON / SARIF each become their own file under that root;
one info log line per format.

### Multi-repo templating

Darnit has no org-level or user-level config file. If you want the
same `[stores.<kind>]` block active on 30 repos, use one of:

1. **Env-var interpolation (easiest)**. Keep `root = "$DARNIT_ATT_ROOT"`
in every repo's `.baseline.toml`. Set `DARNIT_ATT_ROOT` once per
machine (shell profile, systemd unit, CI runner env). The 30 repos
share the destination without duplicating the literal path.
2. **CI/CD templating**. Your workflow rewrites `.baseline.toml` before
invoking `darnit audit`.
3. **Cookiecutter / repo-init tool**. One-shot copy the block into
each repo when you first onboard it.

### The `.project/` layer (FR-009)

Neither `local-fs` nor `user-local` is registered under
`darnit.stores.project`. `.project/project.yaml` is the CNCF
`.project/` spec's canonical repo-committable artifact and stays in
the repo by design. If you write `[stores.project] backend = "local-fs"`
in `.baseline.toml`, `resolve_stores` raises `StoreNotInstalled`
before any control runs -- the misconfiguration surfaces at audit
start, not in a confusing runtime failure. Redirecting project state
outside the repo means governance tooling can no longer find it, so
this is not supported.

## `user-local`: platform-conventional root

Points a store at the platform-idiomatic user-scoped location. No
`root` config needed. Example:

```toml
[stores.attestation]
backend = "user-local"

[stores.report]
backend = "user-local"

[stores.cache]
backend = "user-local"
```

Resolved paths per platform:

| Platform | Attestations / Reports (data) | Cache |
|---|---|---|
| Linux (XDG defaults) | `~/.local/share/darnit/...` | `~/.cache/darnit/...` |
| Linux (`XDG_DATA_HOME=/X`) | `/X/darnit/...` | (see `XDG_CACHE_HOME`) |
| macOS | `~/Library/Application Support/darnit/...` | `~/Library/Caches/darnit/...` |
| Windows | `%LOCALAPPDATA%\darnit\Data\...` | `%LOCALAPPDATA%\darnit\Cache\...` |
| Unknown | XDG fallback (same as Linux) | XDG fallback |

Passing a `root` kwarg to a `user-local` backend logs a warning and
uses the platform default anyway. Less disruptive than a hard error
for operators who copied a snippet from a `local-fs` example.

## Logging

Every successful outside-repo write emits one info-level log line to
the `darnit.stores.local` logger:

```
INFO darnit.stores.local: wrote attestation (local-fs): /home/mike/darnit-attestations/acme-widget-baseline-attestation.intoto.json
```

The in-repo `Filesystem*Store` defaults do NOT emit to this logger, so
zero-config audits stay log-silent under `darnit.stores.local`.

## Zero-config unchanged

If you do NOT add `[stores.*]` blocks, artifacts continue to land in
`<repo>/.darnit/` exactly as before this feature. `local-fs` and
`user-local` are opt-in.

## Troubleshooting

- **`KeyError: DARNIT_ATT_ROOT`**: the env var referenced in `root`
isn't set. `local-fs` fails fast on missing vars by design. Export
the variable or use a literal path.
- **`StoreOperationError: [local-fs attestation @ ...]`**: the
resolved `root` is unwritable, the disk is full, or the file is
locked. The error message names the backend, kind, and resolved path
so the operator can correlate. Darnit does NOT silently fall back
to the in-repo default.
- **File landed with `_`s in the name**: your identifier contained
filesystem-unsafe characters. The store sanitized them to prevent
path traversal. Rename the caller's identifier for cleaner output.
22 changes: 22 additions & 0 deletions packages/darnit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ darnit = "darnit.cli:main"
# points are logged and skipped at discovery time, never crash the harness.
interactive_terminal = "darnit.harness.interactive_resolver:build"

# Feature 034: outside-repo filesystem store backends. Both `local-fs` (arbitrary
# root path via TOML config) and `user-local` (platform-conventional root, XDG /
# Apple / LOCALAPPDATA) ship in darnit-core alongside the in-repo defaults.
# `user-local` is deliberately NOT registered under `darnit.stores.project` per
# FR-009: `.project/project.yaml` must stay in the repo.
[project.entry-points."darnit.stores.attestation"]
local-fs = "darnit.stores.defaults.local_fs:LocalFsAttestationStore"
user-local = "darnit.stores.defaults.user_local:UserLocalAttestationStore"

[project.entry-points."darnit.stores.report"]
local-fs = "darnit.stores.defaults.local_fs:LocalFsReportStore"
user-local = "darnit.stores.defaults.user_local:UserLocalReportStore"

[project.entry-points."darnit.stores.cache"]
local-fs = "darnit.stores.defaults.local_fs:LocalFsAuditCacheStore"
user-local = "darnit.stores.defaults.user_local:UserLocalAuditCacheStore"

# NOTE: `user-local` is deliberately NOT registered under
# `darnit.stores.project` per FR-009 -- `.project/project.yaml` stays
# in the repo. `[stores.project] backend = "user-local"` MUST raise
# `StoreNotInstalled` at `resolve_stores` time.

[project.optional-dependencies]
attestation = [
"sigstore>=3.0.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/darnit/src/darnit/stores/defaults/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,28 @@

from darnit.stores.defaults.attestation import FilesystemAttestationStore
from darnit.stores.defaults.cache import FilesystemAuditCacheStore
from darnit.stores.defaults.local_fs import (
LocalFsAttestationStore,
LocalFsAuditCacheStore,
LocalFsReportStore,
)
from darnit.stores.defaults.project import FilesystemProjectStateStore
from darnit.stores.defaults.report import FilesystemReportStore
from darnit.stores.defaults.user_local import (
UserLocalAttestationStore,
UserLocalAuditCacheStore,
UserLocalReportStore,
)

__all__ = [
"FilesystemAttestationStore",
"FilesystemAuditCacheStore",
"FilesystemProjectStateStore",
"FilesystemReportStore",
"LocalFsAttestationStore",
"LocalFsAuditCacheStore",
"LocalFsReportStore",
"UserLocalAttestationStore",
"UserLocalAuditCacheStore",
"UserLocalReportStore",
]
Loading
Loading