bitcd

Concept

A governed change needs approval, an executor to apply it and a way to check the result. bitcd keeps the approval rules and the record of changes in a contract on Starknet, where no actor in the workflow can edit them.

The problem

Approval rules usually live where the actors can reach them. Branch protection gates a pull request, but the CI job that applies it holds more access than the reviewers. An agent’s tool allowlist is a file the agent can edit. A kill switch cut off from its source of truth keeps acting on stale permissions.

bitcd moves the rules out of reach. A Cairo contract holds, per namespace, who approves, how many approvals are needed, how many must come from people and how many may come from one operator. After setup the owner renounces the contract. From then on a policy change needs a quorum. There is no admin key.

What the contract holds

A key/value store with versions, leases, compare-and-set and an event stream. Each value is the fingerprint of an off-chain document, a schema reference and a version. The document itself lives in a store you run, such as S3. Governed writes pass the namespace’s policy first. Readers fetch the document and check it against the fingerprint before acting.

A change, step by step

StepWhoWhat happens
Proposeany active signerThe fingerprint, the key and the expected version go on chain. The proposal stays there whether or not it commits.
ApprovesignersMachines approve with a transaction. People sign a SNIP-12 message that names the exact change. The signature is valid for that change only.
CommitanyoneThe contract checks the policy: threshold, role, human floor, per-operator cap. Too few approvals return policy unmet.
ApplyexecutorsThey wake on the commit event, fetch the document, re-hash it, refuse a mismatch and apply it: a binding, an IAM policy, a Terraform run.
ReportexecutorsObservations go through set_status, which cannot touch desired state. With k-of-n attestation, reports that disagree produce DISAGREE.

At any point before commit, an eligible voter can request a human on one proposal. From then on it needs a signed human approval that machine votes cannot outvote. The request cannot be withdrawn.

operators / agents propose · approve human overseer signs readable intent Bitcd contract (Cairo · Starknet) policy gate threshold · role · min_humans · ratchet DESIRED — consensus-written digest · version (CAS) · tombstone disjoint write surfaces OBSERVED — executor-attested k-of-n attestations · DISAGREE blob store (S3) bytes never on-chain executor / bridge reconcile loop: read→verify→apply→attest live provider k8s · IAM · Terraform every reader fold events → SYNCED / OUTOFSYNC / DISAGREE propose approve signed intent spec bytes (content-addressed) watch desired fetch + verify digest apply attest only, never grant events (the watch stream)
The write path. Proposals and approvals enter through the policy gate. Executors check every document against its fingerprint, apply it and report through a separate status entry point that cannot authorize anything. Every reader folds the event stream into one of three states.

Building blocks

A new workflow is a namespace, a document schema and an off-chain executor. The contract does not change.

You needbitcd gives you
A step that needs sign-offA policy on a namespace and action: threshold, role, min_humans, max_per_operator. Checked at commit.
A human on demandrequest_human_review: the proposal then needs a signed human approval no quorum can outvote.
Mutual exclusionLeases: an optional timeout, a version that fences stale holders, compare-and-set release, a quorum-approved force-unlock.
TimersA grant’s expiry is part of the approved document. Executors revoke on chain time, not on a local clock.
TriggersEvery change is an event. Executors subscribe; consoles and indexers fold the same stream.
Concurrency controlEvery value has a version and every write is compare-and-set. A stale proposal is refused.
Worker agreementk-of-n attestation. Reports that disagree for one version stay at DISAGREE until an agreeing group clears it.
Undo, revoke, killA delete is a proposal under the deletion policy. Executors treat the tombstone as a revoke.
An audit trailThe chain, settled on Ethereum. Agent fleets add a hash-chained log of every vote, refusal and escalation.

The rules

Desired and observed are separate

Desired state is the fingerprint, the version and a tombstone flag. Observed state is what executors found after applying. Desired changes through proposals, or through a lease on an open prefix. Observed changes only through set_status, which has no path to desired. A failed apply cannot rewrite the goal.

This holds as long as executor accounts stay out of the quorums. The bitcd.yaml lints enforce that; the contract alone does not. Executors still hold real credentials for the systems they manage. Scope those credentials.

Policy is data

A policy is six fields on a (prefix, action) pair: threshold, role, min_humans, max_per_operator, allow_open, exists. There is no scripting language. Policies change through the same approval process as everything else. The governance gates, SET_POLICY, SET_ROLE, FORCE_UNLOCK and TOMBSTONE_VALUE, can only get stricter. A prefix set up without a SET_POLICY gate cannot gain one later, so reserve spare prefixes before setup ends.

Verify on read

The chain holds the fingerprint, a schema reference and a storage class. Every bitcd reader re-hashes what it downloads and refuses a mismatch: the executor attests FAILED instead of applying, and the manifest loader answers HALT. The store does not need to be trusted. A short store outage can be covered by a grace period you set. A mismatch gets none. A reader you write yourself has to do the same check.

People sign the change

Where a policy sets min_humans to one or more, a person signs a SNIP-12 message naming the proposal, the action, the prefix and a hash of the parameters. The contract verifies it in approve_sigs by asking the signer’s own account. Only signed approvals count toward min_humans. A plain approve from a human’s account counts as a machine.

The wallet shows the parameters as a hash. A signing tool decodes the stored parameters, fetches the document, checks it against the fingerprint and shows it before asking for a signature. approvalTypedData, signApproval and approveWithSigs in @bitcd/core build, sign and relay the message, and assertRoundTrip checks the hash against the contract first.

Asking for a human

Any signer eligible under a proposal’s policy can call request_human_review. The proposal then needs at least one signed human approval on top of its policy. Machine approvals cannot outvote it and the request cannot be withdrawn. Paging a person is a separate component. The bitcd.yaml lints check that an eligible human signer exists for the policy.

Several executors

A policy can ask for k of n executors to agree before the shared status changes. If reports for one version differ and no group reaches k, the status becomes DISAGREE on chain. A later agreeing group clears it. Older reports cannot overwrite a newer result. Run executors under different operators and set max_per_operator; otherwise the agreement means little. k = 1 works.

Leases and expiry

A lock has a lease with an optional timeout, a version that fences stale holders, compare-and-set release and a quorum-approved force-unlock. Locks back Terraform state locking and a Kubernetes-style leader lease.

A grant’s expiry is part of the document the quorum approved. When chain time passes it, the executor revokes the grant and reports expired. If the executor is down, revocation waits until it is back.

Deleting is a governed write

A delete is a proposal under the prefix’s TOMBSTONE_VALUE policy. It is recorded, not erased. On an open prefix the lease holder deletes directly. Executors treat a tombstone as a revoke: remove the binding, return Terraform inputs to their defaults, halt whatever loads that manifest.

Drift

Executors reconcile: read desired, verify, apply, read the live object back, report. A change made by hand shows up on the next pass as OUTOFSYNC, is put back and is reported. Live objects are reduced to a stable shape before hashing, so server-added fields do not trigger it. The executor only looks at what it manages. The periodic check can be turned off.

Anyone can commit

Any active signer can propose and approve. The check is at commit, which anyone can call. The contract evaluates the policy at that moment and applies the change or refuses with a reason. No committer role exists to lose or compromise. Proposals and approvals stay on chain whether or not they commit. An agent that proposes widening its own allowlist is refused with policy unmet, and the attempt stays visible.

Events

Every change is an event. Executors subscribe over the RPC’s WebSocket. The indexer and consoles fold the same stream. Events do not carry everything, so a reader that needs the full record also reads the contract. The chain is the source of truth, not any component’s database.

Evidence

Agent fleets keep a hash-chained log of every vote, refusal and escalation with the reason given. Each entry commits to the one before it. To defend it against a rewrite that recomputes the chain, checkpoint it somewhere the agents cannot reach. The log records what an agent said; whether the reasoning was sound is your call.

Setup, then no admin

A temporary owner deploys the contract, seeds signers and policies and renounces. Do it first with renouncing off, read everything back, then renounce. After that, policy and signer changes go through quorums and values follow their prefix’s rules. The bitcd CLI builds the setup from one file and refuses an unsafe one before anything is deployed.

The organization in one file

Signers and roles, executors and one block per kind of change go in bitcd.yaml. This block declares one kind:

intent_types:
  k8s-access:                # give this account these permissions, for this long
    connector: dev-cluster     # the executor that applies it
    prefix: k8s/rbac
    schema: k8srbacgrant/v1
    ttl:                       # chain time; the executor revokes on expiry
      default: 24h
      max: 720h
    quorum:                    # two operators, one of them human, one per operator
      threshold: 2
      role: OPERATOR
      min_humans: 1
      max_per_operator: 1
    revoke: same               # taking it away needs the same approvals
    attest: { k: 1 }           # one executor report is enough
    mutable: true              # this prefix's policies can change later

The bitcd CLI validates the file offline, shows the policies it will seed, runs the ceremony and publishes the compiled manifest as a governed value. @bitcd/core holds the client, SNIP-12 approvals, the executor and voter engines, event folds and the ceremony. Your project links it from a side-by-side clone.

bitcd.yaml reference · @bitcd/core reference · connectors

Why Starknet

The gate has to sit outside the control of the actors it governs, including bitcd’s maintainers. A public chain does that. Starknet adds four things bitcd relies on.

  • Every account is a contract. A signer can be a hardware wallet or a team multisig counted as one approver. The contract asks the account whether a signature is valid (SRC6).
  • Typed messages. SNIP-12 lets a person sign a readable description of a change instead of hex.
  • Settlement on Ethereum. The history your auditors read is held by neither you nor us.
  • Fees follow change, not traffic. Governed changes are rare and executors stay quiet while things are healthy. Reads are RPC calls and cost nothing.

Cost

Each account pays for the transactions it sends, including one that is included but reverts.

What happensTransactionsWho pays
A governed changepropose, one approve per machine signer, commitThe account submitting each call
People approvingUp to 64 signatures per approve_sigs callThe relayer; signing is free
An executor reportOne set_status after a change, repair or retryThe executor
A leaseacquire_lock, then release_lockThe holder
Reading, watching, checkingNoneNobody

You run the document store, the executors and the readers. Application traffic uses configuration already applied and adds no transactions.

Guarantees

Authorization and availability are separate. The contract checks the approvals at commit. How fast a change reaches the live system depends on the chain, your RPC provider, the approvers and the executors.

PropertyWhat holds
AuthorizationA governed change cannot commit without the approvals its policy requires. After renouncing, no key can bypass the policy.
IntegrityA document that does not match its fingerprint is refused. The reader stops instead of using it.
LatencyA transaction is accepted on Starknet within seconds and settles on Ethereum once proven. A change also waits on its approvers.
ReactionIn watch mode, events wake executors as a commit lands. A slower periodic check covers the rest.
Chain outageWhat executors applied stays in place. New changes, drift repair and revoking expired grants wait for the chain.
ExpiryLease and grant timeouts run on chain time.

What it cannot do. No step lands while Starknet is not producing blocks, break-glass included, since break-glass is a quorum too. It cannot make an executor honest; independent executors can be compared, and a split is flagged, but a lone or colluding executor can misreport. There is no recovery key. Lost signers are replaced through the governance quorum; if that quorum is lost, what it guards is locked for good.