Signet — CLI Specification (Design v0.1)
ThesignetCLI: how researchers and pipelines create, sign, submit, and query attestations. Rust binary (crates/signet-cli) built onsignet-bundle; parity with the Spring server's Java implementation is enforced by the shared conformance vectors (backend.md §9). Related: attestation-schema.md, reputation-trust-model.md, architecture.md
1. Design Principles
- Scriptable first. Every command works non-interactively with flags/stdin;
interactive prompts are sugar on top, disabled by --no-input or when no TTY.
- Deterministic exit codes. CI gates branch on exit codes, not parsed prose.
- Local truth. Hashes and commit SHAs are computed from the working tree /
git — never typed by hand. The CLI refuses to attest a dirty file (uncommitted changes ⇒ the hash doesn't match any commit).
- Keys never travel. Signing is local (or keyless via short-lived certs);
no command accepts private key material as an argument value.
- Offline-capable reads. Everything verifiable is verifiable without the
hosted registry (bundles are self-contained; mirrors are first-class).
2. Command Tree
signet
├── auth login | logout | status # identity
├── keys generate | add | list | delegate # self-managed profile + agent delegation
├── attest # create + sign + submit audit
├── fp add # false-positive entry helper
├── verify-record <urn> # staked co-signature
├── flag <urn> # challenge an attestation (staked)
├── resolve <flag> # resolve a dispute (upheld | rejected)
├── disputes # open flags + advisory candidates
├── vouch <identity> # T3 endorsement
├── profile set # self-asserted display name + links
├── coverage # read path query
├── deps # CI dependency-status check (lockfile scan)
├── gate # CI skip-scan gate
├── watch add | remove | test # webhook subscriptions (manage-token gated)
├── targets list | claim | done | skip # auditor work queue
├── ops replay | grant | revoke | list # operator surface: signed privileged
│ | triggers # calls (PD-8); grant/revoke need the
│ # root admin token; `triggers` evaluates
│ # the decision log's revisit conditions
│ # (exit 1 when one fires — cron it)
├── fetch <urn> # get + verify one record
├── inspect <file> # verify + pretty-print a bundle
├── whoami | score # own identity / reputation
└── mirror sync | verify | serve # replica operationsGlobal flags: --registry <url> (default https://api.signet.dev, env SIGNET_REGISTRY), --json, --no-input, -q/--quiet.
3. Identity & Signing Workflow
3.1 Keyless (recommended, humans and CI)
$ signet auth login # opens browser → OIDC → cached session
$ signet auth login --provider githubIn CI, no login step: ambient OIDC tokens (GitHub Actions id-token: write, GitLab CI) are detected automatically; each signature mints a fresh Fulcio cert. Nothing long-lived is stored.
3.2 Self-managed keys
$ signet keys generate # ed25519 → OS keychain / encrypted file
$ signet keys add ssh:~/.ssh/id_ed25519.pub # register existing SSH/PGP key
$ signet attest ... --key ssh # sign with agent-held key (via ssh-agent)3.3 Agent mode (trust model §3 operator binding)
Agents sign with their own key and must attach an operator co-signature. Two supported shapes:
- Online co-sign: operator runs `signet keys delegate --to <agent-key>
--expires 30d`, producing a delegation record the agent embeds; or
- Two-pass: agent produces the bundle (
--output), operator co-signs
(signet inspect --cosign bundle.json) and submits.
signet attest --auditor-type agent fails closed if no operator material is present (schema doc §8 rule 6 enforced client-side too).
4. Writing: signet attest
Run from inside the audited repo checkout — target binding is derived, not typed:
$ signet attest \
--scope lib/router/index.js:1-150 \
--claim vetted-clean:CWE-79,CWE-89 \
--tool manual-code-review \
--tool semgrep@1.79.0:p/owasp-top-ten \
--notes "Input validation at line 42 sanitizes all query parameters." \
--submitBehavior:
- Resolves
repo_urlfromorigin,commit_shafromHEAD; **errors if the
scoped files have uncommitted changes** (principle 3).
- Hashes every scoped file (dir scopes are expanded and pinned per schema §5.1).
- Builds the in-toto statement, JCS-canonicalizes, signs (keyless or
--key),
assembles the bundle.
--submitPOSTs it; otherwise writes./signet-attestation.jsonfor review
or two-pass co-signing. --dry-run prints the statement and stops before signing.
Repeatable flags compose: multiple --scope, --claim status:CWEs, --tool. For non-trivial audits, --from audit.yaml accepts the whole predicate as a reviewed file — the expected path for AI agents and IDE integrations.
4.1 False positives: signet fp add
Purpose-built for the FR-1 false-positive registry — it ingests scanner output directly instead of asking the user to re-describe the finding:
$ semgrep scan --json -o findings.json
$ signet fp add --from-semgrep findings.json --finding 3 \
--category sanitized-elsewhere \
--justification "Redirect target allowlist-validated in lib/utils.js:88." \
--evidence lib/utils.js:88Extracts rule ID, tool version, location; hashes the flagged file; appends the entry to a pending attestation (or creates one). --from-sarif covers every SARIF-emitting tool (CodeQL, Snyk) with one importer.
4.2 Social records
$ signet verify-record urn:signet:att:sha256:ab12... --depth reviewed \
--notes "Reproduced the semgrep run; agree with FP classification."
$ signet flag urn:signet:att:sha256:ab12... --reason incorrect \
--evidence ./poc.md
$ signet vouch github:somebody --context "Colleague; 5y AppSec, reviewed their work."verify-record prints a stake warning before signing (--depth reviewed carries slashing exposure — trust model §5) and shows the target's current dispute status so nobody co-signs a flagged record unknowingly.
5. Reading: signet coverage
$ signet coverage --repo . --min-confidence 0.8 # current checkout @ HEAD
$ signet coverage --repo . --path lib/router/index.js # one file (per-file skip check)
$ signet coverage --purl pkg:npm/express@4.18.2
$ signet coverage --sbom sbom.cdx.json --min-confidence 0.9 --jsonHuman output is a per-file table (status, confidence, CWEs covered, disputes) preceded by the answer's trust context: a recorded mutation incident (!! REGISTRY TAMPERING … / ! release tag repointed …), the mapping grade (verified | attested-tag), the effective monorepo scope, and the AI pre-screen verdict with a [STALE …] marker when it predates verified tampering — a human sees exactly what the gate acts on. --json returns the API response verbatim (architecture §5). SBOM mode (CycloneDX + SPDX) fans out through the bulk endpoint; its table marks rows [TAMPERED], [repointed], and [stale-ai].
Exit codes (used by scripts, distinct from the gate below): 0 query succeeded · 10 network/registry error · 12 invalid input.
6. The CI Gate: signet gate
The skip-scan contract (FR-2) as one command — turns coverage into scanner exclusions and a pass/fail:
$ signet gate --sbom sbom.cdx.json \
--min-confidence 0.9 \
--require-cwe CWE-79,CWE-89 \
--emit-excludes semgrep > .semgrep-excludesBehavior:
- Queries bulk coverage for every component; a file counts as covered only if
confidence ≥ threshold and the required CWE classes are all claimed (per-CWE coverage, trust model §6.2) and no open dispute.
--emit-excludes semgrep|sarif|pathswrites the covered file list in the
target scanner's exclusion format — the scanner then runs only on what's actually unvetted (the entire product value in one flag).
--fail-on disputed|vuln-found(defaultvuln-found): fail the build if
coverage reveals an active-vulnerability-found claim on a dependency — the registry doubles as an advisory feed for exact commits.
- Mutation incidents (PD-2/PD-3 follow-through): a purl answer may carry a
recorded mutation — the registry's persisted evidence that the package's (repo, commit) mapping CHANGED. A verified-grade mutation means the immutable registry artifact itself changed (registry tampering): the gate drops that target's files from the skip-scan list unconditionally and **fails the build under any --fail-on mode except none — tampering outranks both existing categories. An attested-tag** mutation (a release tag was repointed) warns on stderr but does not fail: the coverage in the answer already hangs off the current mapping. Both land in the SARIF log (signet/registry-tampering as error, signet/tag-repointed as warning).
Gate exit codes: 0 gate passed · 1 --fail-on condition hit · 2 policy error (bad flags) · 10 registry unreachable (pipelines choose --offline-ok to degrade to "scan everything" instead of failing).
The lighter, lockfile-level sibling is **signet deps** — no SBOM needed: it autodetects requirements.txt / package-lock.json / Cargo.lock and reports each dependency's audit + AI status, gating on --fail-on none|vuln-found|disputed|ai-findings|ai-high (vuln-found fails when a signed attestation reports an active vulnerability at the pinned version). Mutation incidents follow the gate's contract: a verified-grade mapping change (registry tampering) on any pinned dependency fails every gating mode except none and marks the row [TAMPERED]; a tag repoint warns and marks it [repointed]. --sarif <path> writes a SARIF 2.1.0 log with the same rule ids as the coverage gate at dependency granularity (vulnerabilities and tampering as errors, disputes and repoints as warnings). See ci-integration.md for both in a pipeline.
GitHub Actions sketch:
- uses: signet-dev/setup-signet@v1
- run: signet gate --sbom sbom.cdx.json --min-confidence 0.9 \
--emit-excludes semgrep > .semgrep-excludes
- run: semgrep scan --exclude-file .semgrep-excludes7. Verification & Mirrors
signet fetch <urn>downloads a bundle and fully verifies locally
(signature, cert chain, log inclusion proof, subject hashes vs. --against a local checkout if given) — trust output, not transport.
signet inspect bundle.jsondoes the same for a file (pre-submission review,
two-pass co-signing).
signet mirror syncimplements architecture §8: signed manifest → partitions →
per-bundle verification → local Postgres rebuild. signet mirror verify audits an existing replica. signet mirror serve runs the API binary against the local copy (--seeds my-seeds.json recomputes trust with your anchors).
8. Configuration
Precedence: flags > env (SIGNET_*) > repo .signet.yaml > user config. Repo-level .signet.yaml pins team policy so CI and laptops agree:
registry: https://api.signet.dev
gate:
min_confidence: 0.9
require_cwe: [CWE-79, CWE-89]
fail_on: vuln-found9. Security Considerations
- Session/OIDC tokens in OS keychain, never plaintext dotfiles;
--registry
changes to a non-configured host print a warning (token-theft phishing).
- All timestamps in bundles come from Rekor inclusion, not the local clock.
attestwarns when scope includes files matching secret patterns (don't leak
paths like secrets/prod.env into a public registry — scope metadata is public).
- Rate-limit responses (
429+ tier info) surface as actionable messages, not
retries-by-default (NFR-4 spam posture applies client-side too).
10. MVP Mapping & Open Questions
Milestone fit (architecture §9): M1 ships auth, attest, coverage, fetch, inspect; M2 adds fp, verify-record, flag, vouch, gate; M3 adds keys (self-managed), mirror, delegation.
- Exclusion-format coverage.
--emit-excludesstarts with semgrep + generic
path list + SARIF suppressions. Which formats next (Snyk policy, CodeQL filters) should be driven by user demand, not speculation.
- Line-range UX.
--scope file:1-150is easy to get subtly wrong after a
rebase. Consider --scope-from-diff <range> (attest exactly what a PR touched) as the safer primary workflow.
- Delegation record format. Operator→agent delegation (§3.3) needs its own
small predicate (delegation/v1) — draft alongside M2 social records.
- Editor integration. A
signet lspor VS Code extension surfacing
coverage/FP data inline is high-leverage but out of scope for this spec.