Skip to content

kit 6.4.0

Choose a tag to compare

@github-actions github-actions released this 04 Aug 11:54
· 102 commits to main since this release
3b6df32

Added

  • [policy.agent_writes] now reaches the plugin write surfaces — the last
    ops with no choke point in kit.
    resolve_issue, create_release and
    trigger_deploy were the three ROADMAP named; the arc registered and gated
    every mutating plugin surface instead, adding env_unset (separate from
    env_set because setting a variable is recoverable by setting it again while
    deleting one destroys the only copy of a value and takes down whatever reads
    it), api_token_revoke, webhook_create, webhook_delete, and
    scoped_key_revoke — a third Supabase op that was in neither the
    elevation-scope split nor the registry, because secrets-rotate-cli.ts only
    ever asks about --mode.

    The plugins are standalone zero-dependency packages that must not import
    kit-core, so the enforcement point cannot be a kit function call. What
    crosses the boundary is the resolved DECISION, not the config: kit runs every
    registry op through the one policyDecision and exports the refusals as
    KIT_POLICY_DENY (installPolicyEnv, called from main() beside
    installPolicyHash, and extracted from the boot block so that dropping
    either install fails a test). The plugin-side guard is a membership test with
    no rule in it — nothing to collapse, no empty list to misread, no
    absent-vendor case to get backwards, because all four states resolved before
    the value was written. Serialising the config instead would have put seven
    independent copies of the four-state rule in seven packages.

    Two limits, stated rather than implied: the channel is exactly as strong as
    the KIT_READ_ONLY contract and no stronger — a process that never ran kit
    sees no denials, and absence must mean "no denial" or every repo not using
    the block goes offline the moment a plugin runs outside a kit invocation — and
    a plugin-side refusal is not audited, because a plugin has no path to the
    governed project's log. enforcePolicy() still audits the ops kit gates
    itself.

Fixed

  • kit-plugin-supabase honored no containment gate at all — including on the
    JWT-secret roll.
    Its three write surfaces (rollJwtSecret,
    revokeScopedKey, mintScopedKey) had no assertNotReadOnly(), while all
    six sibling plugins did, and cli.ts + THREAT_MODEL.md both stated that
    read-only was honored by "every kit-plugin write surface". Measured with
    KIT_READ_ONLY=1 set in the process and the client pointed at a local
    listener: all three sent their request, and the roll — which invalidates
    every anon, service_role, signed-URL and session token at once — returned a
    rolled secret. The guard is inline per package because plugins must not
    import kit-core, so src/plugin-write-gates.test.ts now pins all seven
    copies byte-identical and derives the write surfaces from the plugin sources:
    any function issuing a mutating request, or building a GraphQL mutation
    routed through a shared transport, must carry both guards, in the documented
    order, naming an op the registry knows. Exemptions are listed with reasons
    and asserted to still match something. Mutation-proved seven ways (drop
    either guard, swap their order, gate the token revoke on env_set, add a new
    ungated mutation through the transport, loosen one plugin's read-only test to
    "1" only, remove the registry row the revoke depends on — 1 fail each).

    The seven changed plugins are bumped to 0.1.1, and that bump is part of the
    fix rather than bookkeeping: publish_ws in publish.yml skips any package
    whose version is already on npm, so a containment fix left at 0.1.0 would ship
    to the monorepo and leave every installed copy vulnerable — the same way the
    scanner plugins existed but were unreachable to anyone who had not cloned the
    repo until 6.3.1 put them on npm.

  • unknownPolicyEntries() reported to nobody. config.ts,
    docs/OWASP_2025.md, ROADMAP and CHANGELOG all described it as surfacing an
    [policy.agent_writes] entry that names an op kit never asks about; it had
    no production caller. Measured: a repo with vercel = ["env-set"] produced a
    full kit check mentioning neither the typo nor its consequence, exit 0 —
    and the typo INVERTS the operator's intent, because declaring the vendor
    refuses every op not listed, so a misspelling turns a pre-approval into a
    blanket denial for that vendor. This is trap 5 of the enforcement arc ("tests
    over the decision function are not evidence of a working control") recurring
    one module later. Now the policy agent-writes row
    (src/check-policy-ops.ts), wired into checkSecurity() rather than
    runCheckGate so kit ci and kit heal see it too; it names the real ops
    for the vendor, states the consequence, and on the clean path reports how
    many of the registry's ops the policy refuses. Mutation-proved: removing the
    wiring fails only the wiring test while all six function-level tests keep
    passing, which is precisely why they were not enough.

  • kit knobs advertised an op the registry rejected. The knob description
    read sentry = ["resolve_issue"] while resolve_issue was not in
    POLICY_OPS, so kit's own help text described config kit's own checker
    flags. The drift test written for exactly this class hard-coded config.ts;
    it now scans every source file that mentions the block. The historical
    examples in config.ts / policy.ts are cited by op name instead of in the
    live vendor = [...] syntax, because a past example written in the current
    syntax is indistinguishable from a live one to any scanner.

  • The plugin test suites never ran. scripts/test.mjs collected only the
    root dist/, so 11 compiled test files and 76 tests under
    packages/*/dist/ — including every KIT_READ_ONLY=1 refusal test the
    plugins do have — had never been executed by npm test or CI. They pass;
    nobody was watching. A containment test that does not run is worse than no
    test, because it reads as coverage.

  • gate-bash read here-document bodies as commands, and blocked kit's own PR
    description.
    SEGMENT_SPLIT includes \n, so every line of a here-document
    was scanned as its own command. Measured: a gh pr create --body "$(cat <<'EOF' … EOF)" whose body contained the prose `npx tsc --noEmit` was refused
    with BLOCKED — Triage: npm tsc, because a backtick in a sentence became a
    nested command. A false block in a security gate is not a harmless annoyance —
    it is what teaches people to pass --no-verify, and this repo already carries
    one bypassed-hook record.

    splitHeredocs() now separates body from command following what the shell
    actually does: a body fed to a shell (bash <<EOF, cat <<EOF | bash) or
    written to a *.sh-shaped file is scanned as a script; anything else is data.
    The exception that keeps this from being the cheap fix: with an unquoted
    delimiter the shell performs command substitution while building the document,
    so a substitution in the body really runs and is still gated, while the same
    body under <<'EOF' is inert text. An unterminated here-document is scanned
    rather than trusted, and the terminator is matched leniently, because absorbing
    the commands that follow it is the one error direction that could hide a real
    install.

    Verified by driving the real hook rather than the parser: the exact payload that
    blocked the PR now exits 0, while bash <<EOF, cat <<EOF | bash, the
    unquoted-substitution form, script authoring and the plain install all still
    exit 2. src/install-gate-heredoc.test.ts pins both sides of that table —
    mutation-proved seven ways, including the cheap fix of treating every body as
    data (3 fail), dropping the unquoted delimiter's substitutions (2), treating a
    quoted delimiter as expanding (2), and failing to find the terminator (4).

    Named limitation, previously covered only by accident: a body written to a file
    without a shell-script extension and executed later is invisible to this gate,
    as is the easier spelling of the same bypass — a Write tool call, which
    gate-bash never sees.

  • The dependency-surface numbers said 120 installed; the tree is 94. The
    count was measured against the SDK 1.29 tree and never re-measured after the
    1.30 bump moved it (@hono/node-server 1.x → 2.x). It had been copied into
    four places — the ROADMAP heading and body, docs/DATA_FLOW.md, the OWASP A06
    row, and the guard test's own docstring — so the number was wrong everywhere it
    was cited, including in a draft aimed at an upstream maintainer. Re-measured at
    1.30.0: 94 production packages, 90 of them reachable only through the MCP
    SDK, 9 loaded at runtime.
    The loaded count did not move, which is why it is
    now gated: mcp-dependency-surface.test.ts parses the ROADMAP heading and
    fails if the advertised number and the trace disagree (mutation-proved by
    editing the heading to 8 → 1 fail). Only the loaded count is pinned — the
    install count depends on lockfile hoisting, so pinning it would fail on an
    unrelated bump and teach people to edit the assertion instead of reading it.

  • The ROADMAP recorded a limitation without its condition. The upstream ask
    about the SDK's HTTP/OAuth dependencies was marked "cannot be filed from kit's
    own tooling — any repository outside the allowlist answers 403". Measured:
    gh issue list --repo modelcontextprotocol/typescript-sdk exits 0. The 403
    belonged to a different constraint (a cloud session's GitHub token is scoped to
    its one attached repo) and had been copied onto a local-session item where it
    never applied — so an actionable item sat parked as impossible. The ask also
    already existed upstream as
    typescript-sdk#1924,
    so kit contributed its runtime trace to that thread instead of filing a
    duplicate: the issue and its comment both argue from install counts, and nobody
    had shown which packages execute.

  • docs/VERIFY.md documented a gh attestation verify invocation that
    exits 1.
    Both the copy-pasteable block and the CI snippet passed
    --owner sandstream --repo kit; the two flags are alternatives and
    --repo wants the full owner/repo, so gh 2.96 answers
    invalid value provided for repo: kit. It survived because the
    surrounding caveat said the attestation did not exist yet, so nobody
    expected the command to succeed — a broken instruction hiding behind a
    true caveat. src/docs-verify-commands.test.ts now gates the flag
    grammar in VERIFY.md, README.md and publish.yml (offline and
    deterministic; a doc test needing Sigstore would be skipped in CI and
    therefore worthless), mutation-proved by restoring the old form: 2 fail.

Changed

  • The GitHub artifact attestation is measured, so the docs stop hedging.
    gh attestation verify sandstream-kit-6.3.2.tgz --repo sandstream/kit
    exits 0 with exactly one subject, digest.sha256 = de2f6328…85dd,
    predicateType: https://slsa.dev/provenance/v1, and a buildSignerURI of
    publish.yml@refs/tags/v6.3.2 — verified against a tarball whose sha512
    equals npm's own dist.integrity, so the GitHub attestation and the npm
    provenance describe the same bytes (run 30890753307). Accordingly:
    VERIFY.md's caveat is reduced to the fact consumers still need (nothing to
    verify on ≤6.3.1) and now carries the measurement; the CI snippet's
    attestation gate is blocking again, with the || echo "::warning::…"
    removed on the grounds that a warning in a CI log is indistinguishable
    from a passing check to anyone not reading the log; and the OWASP A08 row
    stops citing "the step is in publish.yml" as evidence — the step was
    always there and always errored, which is exactly the reasoning that let
    it survive.

Full changelog: https://github.com/sandstream/kit/blob/v6.4.0/CHANGELOG.md

Verify this release:

git tag -v v6.4.0
npm audit signatures