Skip to content

kit 3.0.0

Choose a tag to compare

@sandstream sandstream released this 24 Jul 22:22
57c79d1

kit 3.0 — from a provable local floor to an org-governed control plane: machine
identity (Ed25519) + signable, distributable policy-as-code, governed
cross-device memory
(encrypted sync + device-coupled action items),
attribution-bound tamper-evident audit, a hardened install/scan gate, and a
deterministic, zero-LLM "smart" UX (contextual hints + a discoverable knobs
reference). Everything below is additive over 2.x — no stable command was
removed or downgraded (enforced by the public-surface invariant).

Security

  • Audit attribution is now bound into the HMAC anchor (de-attribution of a
    sealed entry is caught).
    The adversarial pass found that a writer-only
    attacker — no private key, no anchor key — could strip or rewrite the
    kid/sig of an entry that was already signed AND anchored
    , and every layer
    stayed green: the keyless chain re-hashes without those fields, the signature
    tally just re-counts the entry as unsigned (fail-open), and the hash-only
    anchor tip was byte-identical. The anchor seal now folds attribution
    (computeAnchorTipV3 over hash | kid | sig per line; anchor record
    version: 3), so altering or removing the signer of any anchored entry
    changes the tip → hard tip-mismatch; forging a kid/sig onto a sealed
    keyless entry is caught the same way. Backward-compatible: legacy version ≤ 2
    records keep verifying hash-only until the next kit audit anchor re-seals them
    as v3. (Surfaced by the deep adversarial security pass; complements #175.)

Added

  • kit memory install now wires the Claude Code status line too — the setup
    score + open-PAL () count become visible in your terminal.
    Previously PAL
    only reached the agent (via the hook); the human never saw it unless they
    hand-edited settings.json. Install now also sets statusLine to kit statusline — idempotent, and it never clobbers a custom statusLine (reports
    it and leaves it as-is). --no-statusline skips it; kit memory uninstall
    removes only kit's own. (Paired with the device-coupled PAL count, so what you
    see in the bar is this device's real "blocked-on-you".)
  • PAL is device-coupled — an ephemeral session's action items no longer nag your
    durable device.
    Pending-action items are now stamped at creation with an
    origin_device (stable per machine; an ephemeral container gets its own
    throwaway id) and an origin_root (the absolute project path). Reminders, the
    kit statusline count, and kit memory pal list surface only this device's
    items
    by default (legacy NULL-origin rows still show) — so a "blocked-on-you"
    created in a throwaway container/scratch dir doesn't follow you across the gap-#4
    memory sync. kit memory pal list --all shows every device; kit memory pal prune closes this device's open items whose origin directory no longer exists
    (dead ephemeral/scratch dirs). Schema v5; backward-compatible (pre-v5 rows have
    no origin and are left untouched by prune).
  • kit config knobs — a discoverable reference for the power-user env vars +
    .kit.toml fields kit honors.
    A review found capabilities like air-gapped
    SAST (KIT_SEMGREP_CONFIG), capture-time secret redaction (KIT_MEMORY_REDACT),
    read-only lockdown ([policy].default_mode), monorepo triage whitelisting
    ([supply_chain].internal_scopes) and the CI escape hatches (KIT_ELEVATED,
    KIT_PROD_OK, KIT_NON_INTERACTIVE) were only findable in source. kit config knobs lists them grouped, with env/cfg tags and a ⚠ on the ones that bypass
    a safety gate (--json for tooling). Listed in kit config help + the main
    command list. (Companion to the deterministic hint engine.)
  • Smart, deterministic hints — kit surfaces the right opt-in capability at the
    right moment (zero LLM).
    A review found many powerful features (signed policy,
    audit anchoring, container/IaC scanning, malware heuristics) were only
    discoverable by reading source. A tiny rule engine (src/hints.ts) emits one
    short, actionable tip from plain state checks — e.g. "your audit log isn't
    anchored — run kit audit anchor"
    , "your .kit-policy.toml is unsigned — run
    kit policy sign"
    , "you have a Dockerfile but trivy isn't installed",
    "malware heuristics are off — enable GuardDog", "you have an identity but no
    org policy"
    . Shown as a 💡 tip: line after kit check and at session start.
    Each tip shows at most once (a ~/.kit/.hint-* marker suppresses it),
    detectors are fail-soft (a tip never breaks a check or a session), and
    KIT_NO_HINTS=1 silences them all.
  • Private cross-device memory sync over your own git remote (kit memory push
    / kit memory pull).
    Memory design gap #4: the personal store
    (~/.kit/memory.db) is per-machine; this wires the existing encrypted-backup +
    mergeDb primitives to an opt-in transport — YOUR private git repo — so one
    machine pushes and another pulls (last-write-wins), no manual file copy.
    Configurable without being a backdoor, by construction: (1) config is read ONLY
    from ~/.kit/sync.toml (a LOCAL file), never the project tree, so a malicious
    committed .kit.toml/.kit/* in a cloned repo can't redirect your memory; (2)
    the sync remote MUST differ from the project's origin (anti-exfil guard) — your
    secret-dense brain can't be pushed into the project repo; (3) the payload is
    AES-256-GCM encrypted (the remote sees only ciphertext; passphrase via
    KIT_MEMORY_PASSPHRASE, never stored); (4) fully opt-in — no config, no sync.
    Zero new dependencies (git + node:crypto).
  • Memory sync: a transport = "command" option for non-git stores
    (S3/rclone/scp/USB).
    You're not locked into a git remote — set
    transport = "command" in ~/.kit/sync.toml with push_cmd/pull_cmd, and kit
    runs your command with the encrypted blob path exposed as $KIT_MEMORY_BLOB
    (e.g. aws s3 cp "$KIT_MEMORY_BLOB" s3://…, scp, rclone). Same guards
    apply: the command is read only from the LOCAL ~/.kit/sync.toml (never the
    project tree, so a cloned repo can't inject it), the payload is encrypted, and
    it's opt-in. The blob is the real unit; git and command are just two ways to
    move it.
  • Memory sync onboarding: kit memory sync init + opt-in auto-pull/push + a
    one-time nudge.
    Closes the usability gaps in gap #4 (you previously had to
    hand-write ~/.kit/sync.toml, and nothing synced automatically):
    • kit memory sync init writes the local ~/.kit/sync.toml template
      (--remote <url> for git, or --command with --push-cmd/--pull-cmd;
      --auto enables the hooks; --force overwrites). It won't clobber an existing
      config and reminds you to set KIT_MEMORY_PASSPHRASE + create the private repo.
    • [memory.sync] pull_on_start = true / push_on_end = true wire sync into the
      SessionStart/SessionEnd hooks: pull+merge before "where you left off", and
      index+push when the session ends — the missing piece for ephemeral
      containers
      (memory reaches your durable store before the box is reclaimed).
      Both are fail-soft: a session is never blocked by sync.
    • A one-time tip suggests kit memory sync init when you have a non-trivial
      memory store but sync isn't configured yet (suppressed after the first show).

Security

  • Install-gate: closed four bypasses that let an untriaged install through. A
    deep adversarial pass found the PreToolUse install-gate's command parser could
    be defeated by (1) a leading env-var assignment — A=1 npm i evil tokenized so
    no matcher fired; (2) package runners it didn't know — npm exec / pnpm dlx /
    yarn dlx / bun x; (3) a remote tarball URL — npm install https://…/x.tgz
    was dropped as a "local" target on its extension alone; and (4) an install hidden
    in a subshell or -c arg — sh -c '…', $(…), backticks. The parser now strips
    leading wrapper bins + VAR=value assignments, matches the runners, treats any
    scheme:// target as remote (→ fail-closed unverifiable), and recursively scans
    nested commands. A regression test pins each bypass.
  • ReDoS: the GCP private-key redaction pattern no longer hangs on a small hostile
    input.
    "private_key" was matched with two unbounded runs around a literal, so
    a blob of near-miss -----END tokens backtracked catastrophically (~18 KB hung
    ~17 s) — a CPU DoS of the gate reachable via scan-staged, kit memory scan, and
    status redaction. The body is now a single bounded [^"]{1,8000} run ending at the
    close quote (linear; still matches real keys).
  • ReDoS: globToRegExp (memory clusters) collapses stacked wildcards. A
    malicious committed .kit/shared/clusters.json glob of many **/**/ compiled to
    adjacent .*/(?:.*/)? groups that backtracked catastrophically — and the cluster
    map is matched on every UserPromptSubmit, so a pulled branch could hang the agent
    on each prompt. Consecutive wildcard runs now coalesce to a single non-stacking
    group and an over-long glob is refused (matches nothing).
  • Policy trust anchor: canonical signing bytes now cover every key faithfully.
    sortDeep rebuilt objects with out[k] = …, so a __proto__ key was a prototype
    write that silently dropped the key+subtree — letting a [__proto__] table be
    appended to a SIGNED .kit-policy.toml without changing its signature/fingerprint
    ("signed bytes ≠ document"). It now assigns via defineProperty (the key is a real
    own property in the bytes), refuses a TOML date value (no faithful canonical
    form → a Date≡string collision), validatePolicy rejects __proto__/constructor/
    prototype keys, verifyPolicy treats an uncanonicalizable doc as invalid instead
    of crashing, and versionGte is guarded against a non-string. No global prototype
    pollution existed (Node JSON.parse/smol-toml create an own property); this hardens
    canonicalization soundness.

Fixed

  • Air-gap completeness: the update check no longer egresses by default under
    KIT_AIRGAP.
    checkForUpdate (the one outbound call on a normal kit run)
    now short-circuits when air-gap is set, alongside the existing CI/KIT_NO_UPDATE_CHECK
    suppressors — so "no outbound network by default / air-gap mode" is a complete
    posture, not one with a lone npm-registry beacon. (Surfaced by the 3.0 promise audit.)
  • Deterministic gate: bumblebee catalog-staleness no longer flips the verdict on
    wall-clock time.
    A clean supply-chain scan whose threat-intel catalogs are >60
    days old was a warn (failing kit ci --strict) purely because the calendar
    advanced — same repo, same scanners, different verdict by date. Staleness is now
    ADVISORY (status: pass, noted in the detail/suggestion); the gate verdict is a
    function of inputs only. (Surfaced by the 3.0 promise audit.)
  • Secrets: entropy backstop closes a fail-closed shared-memory scan hole. The
    kv-secret heuristic allowlists runtime env prefixes (KIT_/GITHUB_/…), so a
    real high-entropy credential stored under such a prefix slipped past the
    fail-closed kit memory share gate. findSecrets(text, { entropyBackstop }) now
    also flags an ALL-CAPS KEY=value whose value is long + genuinely high-entropy
    (Shannon ≥ 4.2 bits/char) regardless of prefix; shareEntry enables it. Catches
    base64/base62 secrets while clearing hex hashes (~4.0) and dictionary values; the
    noisier code/diff scan is unchanged. (Surfaced by the 3.0 promise audit.)

Changed

  • Frozen contracts: "additive-only" is now test-enforced, not just review
    discipline.
    A new invariant test checks the live surface against the committed
    contracts/public-surface.json baseline and FAILS on a removed stable command, a
    stable → experimental/deprecated downgrade, or a schemaVersion/adapter-sdk
    major regression — closing the gap where a breaking change could be hidden by
    regenerating the byte-for-byte snapshot. (Surfaced by the 3.0 promise audit.)

Added

  • Org-distributed policy verification (kit policy trust) — 3.0 Phase 2
    starter. A committed .kit-policy.signers trust anchor lists the org public
    key(s) allowed to sign the policy; verifyPolicy resolves the signer in trust
    order (a pinned --key → this machine's identity → the org anchor), so ONE
    policy signed by a central org key verifies authentically on every clone — no
    shared secret, only public keys distributed. kit policy trust <pubkey.pem> [--label] / --list / --remove <id> manage it. Fail-CLOSED once an anchor
    exists: a signer that isn't in it fails kit policy check / kit ci (same
    discipline as the HMAC audit anchor), not just a warn. New src/policy-trust.ts;
    PolicyVerifyResult gains via (key|local|org) + anchored. This is the model
    the major bump needs — identity signs the org standard, any repo verifies it.

  • kit ci enforces the signed policy. kit ci now folds evaluatePolicy
    into its gate: a present .kit-policy.toml's requirements appear as policy/*
    checks in the report (text/GitHub/JSON), so a tampered/revoked signature, unmet
    min_kit_version, or (under --strict) a missing required scanner fails CI like
    any other check. Opt-in and backward-compatible — no policy ⇒ no policy/*
    checks ⇒ the verdict is unchanged. Completes Phase 1's "kit ci consumes the
    policy".

  • kit policy check (experimental) — enforce the signed .kit-policy.toml
    against this machine's deterministic state (3.0 Phase 1, part 2). Verifies the
    signature (the trust anchor: warn on unsigned/unknown signer, fail on
    tamper or a revoked signer), then evaluates min_kit_version (current ≥
    required), required_scanners (resolvable mise-first; warn, or fail under
    --strict), and prod_writes_need_approval (.kit.toml [governance.approval]).
    require_triage/thresholds are surfaced (enforced by the install-gate /
    data-source plugins, not duplicated). Opt-in: no policy ⇒ no-op. --json +
    exit code for CI. New evaluatePolicy/versionGte and a shared verifyPolicy
    core (reused by kit policy verify). See docs/POLICY.md.

  • kit policy (experimental) — signable, distributable policy-as-code (3.0
    Phase 1). A separate .kit-policy.toml document holds the org standard
    (require_triage, required_scanners, prod_writes_need_approval,
    min_kit_version, [thresholds]); kit policy init/show/validate/sign/verify.
    Signing ties the standard to a kit identity (Phase 0) over canonical JSON
    (key-sorted), so the signature survives TOML reformatting/comments and breaks
    only on a real policy change; verify checks against locally-known keys (or a
    pinned --key), fail-opens on an unknown signer, and fails on a tampered doc or
    a revoked signer. Separate from the 2.x .kit.toml [policy.agent_writes]
    pre-approval — this is the org-level standard. Enforcement glue (kit check/ci
    consuming it) + signed org bundles + RBAC are the follow-ups. See docs/POLICY.md.

  • Zero-touch environment fueling guide (docs/ENV_FUELING.md). Documents the
    setup-script pattern — kit setup --recommended (config: tools, vault-backed
    secrets, agent gates, verify) + kit memory sync/restore (recall) — so a fresh
    or ephemeral environment (cloud container, Claude Code on the web, CI, new
    laptop) fuels itself with no manual steps. Covers non-interactive setup, where
    the script lives (devcontainer/Dockerfile/CI/web), the memory bridge, and the
    guardrails (secrets never plaintext, private memory never committed, idempotent,
    zero-LLM). Linked from the README quick start.

  • Path→cluster push-surfacing for shared decisions (kit memory context).
    Pull recall can't guarantee you see a settled decision (a bad query misses
    it); the guardrail is now PUSH — touch files under an area and kit
    deterministically surfaces that area's ACTIVE decisions. A committed
    .kit/shared/clusters.json maps area → globs (zero-dep glob→regex matcher).
    kit memory context [paths…|--changed] prints the active decisions for the
    touched areas, and the UserPromptSubmit reminder now appends a bounded notice
    for the area(s) your working-tree changes fall into. Superseded/reversed
    decisions never resurface (uses activeShared). Fail-open (no map ⇒ nothing).

  • kit panic (experimental) — one-command compromise response (the control
    plane's kill-switch). Rotates the local identity (old key archived but its past
    signatures stay verifiable), emits a SIGNED, append-only revocation of the old
    key (~/.kit/revocations.jsonl, signed by the new key — asymmetric, so it
    propagates as public data with no shared secret), records an identity.panic
    event in the tamper-evident audit log, and prints the platform-revocation
    checklist for the accounts kit does NOT own (GitHub / Anthropic / Apple / vault)
    with links. kit audit verify now surfaces entries signed by a revoked key
    (valid as history; the key is no longer trusted for new signatures). New
    identity primitives: recordRevocation, loadRevocations, isRevoked,
    revocationStatement. Honest boundary documented: kit owns its keys + the
    revocation list + the audit; it only orchestrates platform-account revocation.

  • Living shared decisions — lifecycle (status / supersedes / reverses).
    Shared memory entries are now versioned: a change is a NEW append-only entry
    that --supersedes <id> or --reverses <id> an old one (or carries an explicit
    --status). kit memory share validates the referenced id exists. Surfacing
    shows only ACTIVE decisions with their age (e.g. 2y ago) so an aging decision
    is flagged for review, never blind obedience; kit memory area/search badge
    superseded/reversed entries (and show the chain) so "this was tried + reversed"
    still surfaces. active stays implicit (absent field) so pre-lifecycle entries
    are byte-identical. New effectiveStatus, activeShared, formatAge.

  • Shared (curated) memory folded into recall. kit memory search now also
    searches the committed .kit/shared/memory.jsonl tier and surfaces matching
    team decisions/conventions above raw transcript hits (--json now returns
    { messages, shared }). SessionStart recovery re-injects the project's most
    recent durable shared decisions (kinds: decision/convention/security/status),
    so a resumed session regains the settled context, not just the last few raw
    turns. Both paths are project-local and fail-open (a missing/broken shared
    store never breaks recall).

  • Berget AI + Grunden.ai as known providers. Added berget and grunden
    (EU-sovereign, OpenAI-compatible inference) to the service registry with their
    canonical key names (BERGET_API_KEY/BERGET_BASE_URL,
    GRUNDEN_API_KEY/GRUNDEN_BASE_URL) and where to get them, so kit secrets
    treats them as known, vault-resolved keys (never plaintext). Catalog-only by
    design: both are used via the openai SDK with a custom base URL, so there is
    no unique package signal to auto-detect on (keying on the openai dep would
    misattribute every OpenAI user). Agent-backend wiring + a Berget Code gate
    adapter are deferred.

  • Identity-signed audit entries. When a local kit identity exists, each
    appended .kit-audit.jsonl line is signed: kit attaches kid (signer id) +
    sig (Ed25519 signature over the line hash). Best-effort and append-safe — no
    identity means entries are written as before, and a signing failure never
    blocks the append. kid/sig sit outside the hashed remainder, so the hash
    chain still verifies across mixed signed/unsigned/legacy logs. kit audit verify now reports the signature tally (verified / unknown-key / unsigned)
    against the locally-known keys (current + rotated) and FAILS hard on a forged
    signature, while fail-opening on absent/unknown trust. This is the asymmetric
    ATTRIBUTION layer (who produced an entry, verifiable with only the public key)
    orthogonal to the symmetric HMAC INTEGRITY anchor. See docs/AUDIT_ATTESTATION.md §2.5.

Fixed

  • kit check secrets scan: degraded (no-trufflehog) fallback no longer cries
    high-severity on its own noise.
    The basic git grep path now mirrors the
    trufflehog branch's philosophy — it's UNVERIFIED, so it's a medium warn, not
    high, and it filters its two dominant false-positive classes: test/fixture/mock
    files (fake credentials live there by design; the authoritative trufflehog + CI
    gitleaks scanners still cover them and verify live) and all-caps identifier
    VALUES (an env-var name like SOCKET_SECURITY_API_TOKEN is a config key, never a
    secret). Removes a self-inflicted false positive on kit's own source and the
    test fixtures; the authoritative scanners are unchanged.

Full changelog: https://github.com/sandstream/kit/blob/v3.0.0/CHANGELOG.md

Verify this release:

git tag -v v3.0.0
npm audit signatures