Skip to main content

Platform

Struct Platform 

Source
pub struct Platform { /* private fields */ }
Expand description

A running platform: the sequencer plus the shared view it maintains.

Implementations§

Source§

impl Platform

Source

pub fn start( registry: Registry, log: Box<dyn OpLog>, node_key: ActorKey, ) -> Result<Self, String>

Replays log into a view and starts the admission sequencer over it with registry as the trusted key set plus the daemon’s own node_key (registered automatically, for git-derived ops).

§Errors

Returns a description of any replay failure (a log written through this platform always replays cleanly).

Source

pub fn start_with_review_retention( registry: Registry, log: Box<dyn OpLog>, node_key: ActorKey, retention: ReviewRetention, ) -> Result<Self, String>

Platform::start with explicit live-review retention.

Retention is a startup choice because recovering FIFO request order belongs in the same replay pass that builds the view. The ordinary constructor allocates no tracker and performs no retention checks.

§Errors

Same as Platform::start.

Source

pub fn start_reloading( registry: Registry, log: Box<dyn OpLog>, node_key: ActorKey, keys_file: Option<PathBuf>, ) -> Result<Self, String>

Platform::start with a trusted-keys file that is hot-reloaded (on mtime change) before signature verification: registering a key is appending a line, no restart, and removing one refuses its next submission. The file’s contents replace the whole registry.

§Errors

Same as Platform::start.

Source

pub fn start_reloading_with_review_retention( registry: Registry, log: Box<dyn OpLog>, node_key: ActorKey, keys_file: Option<PathBuf>, retention: ReviewRetention, ) -> Result<Self, String>

Platform::start_reloading with explicit live-review retention.

§Errors

Same as Platform::start.

Source

pub fn with_newcomer_audit( self, audit_path: PathBuf, adjudications_path: PathBuf, incumbent_actor_keys: Vec<String>, ) -> Result<Self, String>

Enables sparse, durable D24 T4 newcomer measurement.

incumbent_actor_keys is the trusted-key snapshot at activation; those actors are excluded because the audit cannot reconstruct their first attempt or time-to-first-acceptance. The audit records only a later actor’s first verified signed-API attempt, its first eventual acceptance, and an optional appeal. Operator adjudications are JSONL rows in the separate file and are re-read for every report.

Both files are created mode 0600. Neither changes the signed op log or any hash input.

§Errors

Unusable paths or an invalid existing audit file.

Source

pub fn with_review_adjudications(self, path: PathBuf) -> Result<Self, String>

Enables the D24 T2 operator classification file: versioned 0600 JSONL rows of {"format_version":1,"review_id":…,"classification":…} where classification is valid, invalid, slop or unclear.

Separate from the signed op log on purpose, exactly like the T4 adjudications: a judgement about a contribution is the operator’s opinion, not a sequenced claim any actor can make. Keying it by review id rather than by verdict is what lets a classification outlive archiving, which discards the verdict bulk.

Enabling it does not make T2 evaluable. The cohort still has no exit rule, the observation window and tripwire subject are unset, and census adjudication cannot survive the flood it would need to detect.

§Errors

The file or its directory cannot be created.

Source

pub fn with_required_assignment(self) -> Self

Refuses any RequestReview that names its own reviewers, so the node’s draw is the only path to a reviewer list (D24 layer 5, “the requester does not choose who reviews them” — enforced rather than merely offered).

Requires a reviewer pool: without one no review can ever be assigned, so every request would stall unassigned.

Scope: node-wide, the blunt instrument. For “only reviews landing somewhere that matters”, see Platform::with_protected_refs, which conditions on RequestReview’s target_ref.

Source

pub fn with_protected_refs(self, path: PathBuf) -> Self

Points the platform at an operator-curated list of protected refs (one <repo>:<refname> pattern per line, # comments allowed, a single trailing * acting as a prefix glob). A RequestReview whose target_ref matches must go through the node’s draw; one naming an unprotected ref, or naming no ref at all, may still pick its own reviewers.

This is the per-review half of D24 layer 5 — “privilege-bearing” finally has a definition the code can read, rather than the node-wide approximation of Platform::with_required_assignment.

Requires a reviewer pool, for the same reason.

On its own this only binds reviews: a requester who omits target_ref still escapes it. Platform::with_required_review is the other half, and closes that.

Source

pub fn with_acl_file(self, path: PathBuf) -> Self

Points the admission policy at the operator’s ACL file, so a landing on a protected ref can ask who owns the repository (D42).

Without this the ownership rule is simply absent and every protected ref keeps the approval-weight gate, which is the behaviour of every node built before D42. Ownership is opt-in per repository even once the file is attached: a repository nobody holds own over is unaffected.

The path, not a parsed table, because the file is hot-reloadable and admission must see an ownership change without a restart — the same reason Platform::with_protected_refs takes a path.

Source

pub fn attach_accounts(&self, store: Arc<Accounts>)

Gives the admission policy the credential store, so a submission signed by an enrolled passkey can be verified (D39).

A setter rather than a constructor argument because accounts and the platform are enabled independently, in either order, by separate flags. Until this is called a passkey submission is refused for want of a store, which is the same answer a node without self-service gives permanently.

Source

pub fn with_required_review(self) -> Self

A protected ref only moves to a commit that an approved review with weight from two distinct operators already named as its destination, and can never be deleted. This is the landing half of the gate: with it, omitting target_ref stops being an escape and becomes a refusal, because the push itself is what gets checked.

Requires Platform::with_protected_refs — with no list nothing is protected and the flag would do nothing.

No exemption for the node’s own key. Every git push reaches the sequencer as a node-signed SetRef, so exempting the node would exempt every push. The consequence is deliberate and operational: switching this on means this daemon’s own repository can only be advanced through a review, choirctl sync included.

Creating a protected ref is allowed (prev == None): there is no history to hijack yet, and since deletion is refused, “delete then re-create” is not a way back in.

Not covered, and not silently implied: force-pushes and non-fast-forward updates are only constrained by the CAS prev git itself supplies. A sufficiently weighted approval of commit X authorizes landing X, whether or not X is a descendant of the current tip.

Source

pub fn with_required_scope(self) -> Self

Admits only ops whose author bound them to this log and a head still in the window — the replay defence, turned on.

Off by default because it is a wire-compatibility break, not because unscoped is safe: an unscoped signature is admissible on any node that trusts the key, and admissible again on the node it came from as soon as CAS state returns to what it expected. Every client in this repository always sends a scope, so turning this on costs them nothing; a client that predates scopes stops working, which is the whole reason for the flag.

Source

pub fn scope_now(&self) -> (ContentHash, Option<ContentHash>)

The log identity a client signs a scope against: this node’s actor id and the head it should name.

Source

pub fn proposal_round(&self, repo: &str, branch: &str) -> Option<ProposalRound>

The round of proposals aimed at branch of repo (D68).

None when the branch does not exist. A round with no proposals is Some with an empty list: “nobody has proposed anything” and “there is nothing to propose to” are different answers, and only the second is a misconfiguration.

Source

pub fn ref_landing(&self, refname: String, base: &str) -> RefLanding

A landing that moves refname from base, signed by this node.

The scope is read per landing rather than captured here, because a round appends as it goes; see crate::queue::ScopeSource.

Source

pub fn check_reporter( &self, name: String, target_ref: Option<String>, ) -> CheckReporter

A check reporter that this node signs and scopes (D49, D68).

The reason set_check_reporter was off by default and composed into nothing: the identity a check is reported under belongs to whoever runs the queue, and a bridge has no node-side identity to use. A node does – its own key – and its landings are in a log that is kept rather than thrown away, so a report against them is answerable later.

Source

pub fn run_proposal_queue( &self, repo: &str, branch: &str, workdir: &Path, ci: &mut dyn CiExecutor, template: JobTemplate, ) -> Option<QueueReport>

Runs one speculative round over this node’s own log (D5, D68).

workdir must be a worktree of the repository this node serves, made with git worktree add --detach against the bare repo, and owned outright by the queue: every speculative merge detaches it and forces it to a candidate state, so a tree anybody else is working in is the wrong argument.

A worktree and not an independent clone, and this is not a preference. A landing names the merge commit the speculator built. An independent clone writes that commit into its own object store, where the served repository cannot see it, so the log would name a commit git does not have — which Platform::reconcile_git_refs correctly reads as the view being wrong and compensates back to git’s value, silently undoing every landing in the round. A worktree shares the object store, so the commit is already there when the op is submitted.

None when the branch does not exist. Otherwise the report says what landed, in order, and every landing in it is an op in this node’s log rather than a number the round kept to itself.

Source

pub fn drain_queue( &self, queue: &mut MergeQueue, ci: &mut dyn CiExecutor, ) -> QueueReport

Drains a caller-built queue through this node’s sequencer.

The escape hatch under Platform::run_proposal_queue, for a caller whose round is not simply “every proposal on this branch”. The sequencer is the point: a landing recorded through anything else is not in the log this node serves.

Source

pub fn with_log_path(self, path: PathBuf) -> Self

Points the platform at the JSON-lines file its op log persists to, so /api/log?from= can serve entries that have already been evicted from the in-memory window. Without it, a reader that has fallen further behind than the window is told so and cannot resync.

The file is append-only, so reading a prefix while the writer thread appends is safe: already-written lines never change.

Source

pub fn with_batch_limit(self, max_ops: usize) -> Self

Sets the absolute operation-count ceiling for one batch request.

Source

pub fn set_key_names(&self, signers: &[TrustedKey])

Replaces the actor-id → bound-name map from a freshly parsed trusted-keys file.

Called from the daemon’s accept loop when the file’s mtime moves, so binding a name to an already-trusted key takes effect on the next request rather than waiting for some later signature failure. That matters because a binding is a tightening: a gate that applies at an unpredictable future moment is not a gate.

Source

pub fn with_log_window_cap(self, cap: usize) -> Self

Shrinks the in-memory /api/log window. Exists so tests can exercise eviction and the resync path without writing 100k ops.

Source

pub fn with_reviewer_pool(self, path: PathBuf) -> Self

Points the platform at an operator-curated pool of eligible reviewer names (one per line, # comments allowed). With a pool set, a RequestReview carrying an empty reviewer list is answered by a node-signed OpKind::AssignReviewers drawn from the pool, excluding the requester — D24 layer 5, so a requester cannot pick a friendly reviewer.

Source

pub fn with_reviewer_conflict_graph( self, path: PathBuf, max_distance: usize, ) -> Self

Excludes reviewer operators whose shortest path from the requester in path is at most max_distance. Each non-comment line is one undirected <operator> <operator> edge. Operator names, not full operator/agent channels, belong in the graph.

The graph is operator-supplied runtime policy rather than op-log state: changing it affects future draws without changing persisted operations or replay. It is read for every draw and fails closed; an unreadable or malformed graph leaves the review unassigned. Distance zero retains the existing same-operator exclusion.

Source§

impl Platform

Source

pub fn git_update( &self, repo: &str, refname: &str, old_hex: &str, new_hex: &str, user: &str, cert: Option<(&str, &str)>, ) -> Result<(), String>

Routes one git ref update (from a repo’s update hook) through the sequencer: CAS against the view, node-signed, totally ordered with API ops. Refs are namespaced <repo>:<refname>; git oids enter the envelope with their own codec (ContentHash::from_git_oid).

A push to refs/for/<branch>/<topic> additionally opens a review targeting that branch, which is the whole magic-refspec path: one git push, no client but git, and reviewers drawn by the node.

§Errors

The policy’s rejection reason (stale CAS = concurrent update git itself would also have refused), or the reason a refs/for/ push could not be read as a proposal.

Source

pub fn git_abort( &self, repo: &str, refname: &str, old_hex: &str, new_hex: &str, user: &str, cert: Option<(&str, &str)>, ) -> Result<(), String>

Retracts a ref op this push already had accepted, because the push as a whole is being refused and git will apply none of it.

pre-receive submits one op per ref but git applies no ref until the hook exits zero, so a push whose third ref is refused has already put two ops in the durable log. Without this the view keeps refs git never created — and they cannot be pushed afterwards either, because the pusher’s old is git’s (absent) value while the view holds the stranded one, so every retry loses the CAS. The ref becomes permanently unpushable.

The log is append-only, so the repair is a compensating op, not an erasure: the abort is part of the history rather than hidden from it. old/new are the same values the accepted op carried, so the inverse restores exactly what git still has.

§Errors

The policy’s rejection reason — most likely a lost CAS, meaning something else moved the ref between the accept and this retraction and the stranded value is no longer what would be undone.

Source

pub fn reconcile_git_refs(&self, root: &Path) -> RefReconciliation

Brings every bare repo under root back into agreement with the view, which is the source of truth. Run at startup, before the node serves anything, so nothing races the repair.

The hook’s retraction path handles a push that is refused while the daemon is alive. This handles the rest, and it does so without needing to know which of them happened: power loss between the hook’s 200 and git writing the ref, a per-ref failure after pre-receive passed (receive.deny*, an update hook, a write error), or a retraction that could not be delivered. All of them leave the same state, and it is the state this reads.

Two repairs, chosen by whether git can honour the view:

  • the commit exists in the repo, so git is simply behind: the ref is written. Git is the follower (D21 single-canonical), so moving it is the defined direction.
  • the commit is absent — a refused push has its objects discarded from the quarantine — so the view names something git can never have: a compensating op puts the view back to git’s value.

A ref git holds and the view does not is reported, never adopted. Appending an op for it would launder an out-of-band update-ref into the signed log as though it had been submitted.

Source

pub fn survey_git_refs(&self, root: &Path) -> Vec<RefFinding>

Every way the repos under root and the view currently disagree, with nothing written and no op appended.

This is the half of Platform::reconcile_git_refs that can be run against a live node. The repair deliberately cannot: it writes refs, so it belongs before the first request, where nothing races it. Reading is safe at any time, and without it a divergence that appears while the daemon is up is invisible until the next restart — which is the difference between a monitor and an autopsy.

Served by GET /api/ref-agreement, which is deliberately its own endpoint rather than a field on /api/view: this shells out to git once per repo, and /api/view is on the hot path.

Scope, stated because RefState::GitOnly reads like a stronger claim than it is: only repos the log already names are compared. A repo with refs and no log entry at all is not surveyed, so this finds an out-of-band ref beside logged ones, not an entire smuggled repo.

Source

pub fn set_workspace_head( &self, workspace: &str, head_hex: &str, attribution: &str, ) -> Result<(), String>

Points workspace at git oid head_hex with a node-signed op, using the view’s current head as the CAS prev (a lost race is a sequencer rejection, not a clobber). attribution is the submission channel (e.g. git/<user>), as for git-derived ops.

§Errors

Bad oid, or the policy’s rejection reason.

Source

pub fn create_change( &self, request: AuthorizedChangeCreate<'_>, attribution: &str, ) -> Result<Accepted, String>

Atomically creates a stable change and registers its workspace at an exact Git revision with a node-signed operation.

Source

pub fn decode_create_change_request( &self, request: &Value, expected_id: &str, expected_owner: &str, expected_workspace: &str, expected_revision: &ContentHash, expected_idempotency_key: &str, ) -> Result<(Witness, Vec<String>), String>

Decodes the exact owner-signed create binding before any physical workspace is copied. Sequencer admission verifies the embedded signature before recording the node-authored change operation.

Source

pub fn submit_archive_change( &self, request: &Value, expected_id: &str, expected_workspace: &str, expected_revision: &ContentHash, attribution: &str, ) -> Result<Accepted, String>

Submits a node-authored OpKind::ArchiveChange carrying the owner’s signed authorization, after verifying that the signed payload names exactly the resource the endpoint already moved.

Source

pub fn validate_archive_change_request( &self, request: &Value, expected_id: &str, expected_workspace: &str, expected_revision: &ContentHash, ) -> Result<(), String>

Checks the archive request’s signed payload shape before the filesystem is renamed. Signature and current-state admission still happen on the sequencer after the rename, with rollback on refusal.

Source

pub fn archive_change_receipt( &self, request: &Value, expected_id: &str, expected_workspace: &str, expected_revision: &ContentHash, attribution: &str, ) -> Option<(u64, ContentHash)>

Original operation identity for an identical completed archive request still present in the durable log window.

Source

pub fn change_state(&self, id: &str) -> Option<ChangeState>

Current materialized state for one stable change.

Source

pub fn change_for_idempotency( &self, owner: &str, key: &str, ) -> Option<(String, ChangeState)>

Finds the change created by one owner-scoped idempotency key.

Source

pub fn archived_change_ids_for_workspace(&self, workspace: &str) -> Vec<String>

Inactive change generations that previously used workspace. This supports migration of the original unversioned archive path when a later generation reuses a deterministic workspace name.

Source

pub fn workspace_head(&self, workspace: &str) -> Option<ContentHash>

Current exact head of an active workspace.

Source

pub fn workspaces_held_by(&self, channel: &str) -> usize

How many workspaces channel currently holds (D37).

Read from the projection replayed out of the op log, so a node that has just restarted answers the same number it answered before — the property a per-user ceiling is worthless without.

Source

pub fn tallied_workspaces(&self) -> Vec<String>

Every workspace the D37 tally is tracking, sorted.

Exposed for the test that pins the tally against View::workspaces: the two are folded from the same operations and a divergence between them is the tripwire on the D37 register row.

Source

pub fn assign_reviewers( &self, id: &str, requester: &str, ) -> Result<Vec<String>, String>

Draws reviewers for unassigned review id and records them with a node-signed op.

Candidates are the pool minus everyone sharing the requester’s operator and, when configured, every operator within the chosen conflict-graph distance. The draw takes at most one reviewer per operator so REQUIRED_APPROVAL_WEIGHT reviewers means that many independent ones under the configured policy.

Excluding only the requester’s own name was the original rule and it does not survive the multi-operator case, which is the normal one: an operator running three agents in the pool satisfies two-person integrity by themselves, and can manufacture more agreement by registering more agents. That is the Sybil move D24 says must be blocked at the operator level, so the exclusion has to be at that level too.

A pool that cannot supply REQUIRED_APPROVAL_WEIGHT distinct operators draws fewer rather than doubling up — a visibly under-assigned review beats one that looks independent and is not.

§Errors

No pool configured, an unreadable pool or conflict graph, malformed graph data, no candidate outside the conflict distance, or the sequencer’s rejection reason (e.g. the review was assigned by a concurrent request).

Source

pub fn durability_failed(&self) -> bool

Whether the sequencer has failed a durability barrier and stopped accepting.

The daemon’s accept loop polls this so a node that can no longer persist exits rather than staying up refusing everything. Process supervision only restarts a process that exits, so without this a transient fsync error is permanent downtime that looks like uptime.

Source

pub fn with_journal(self, path: &Path) -> Result<Self>

Records every admission decision to path as JSONL.

Derived data (see journal): the file is appended to from its own thread, never read back, and its loss or truncation changes no decision this node makes. Safe to rotate by moving it aside; the daemon keeps writing to the open handle until restarted.

§Errors

If path cannot be opened for append.

Source

pub fn with_lag_log(self, path: PathBuf) -> Self

Appends gate breaches to path, one JSON object per line.

Separate from the op log on purpose: a breach is an observation about this node’s storage and load, not a fact about the ordered history, and it must not change a hash anyone else replays.

Source

pub fn with_hooks(self, config: PathBuf, log: PathBuf) -> Result<Self, String>

Enables outbound ref-landed webhooks (D32) from the subscription file config, recording every delivery attempt in log.

Starts one delivery thread. The sequencer’s writer thread never waits on it: it offers events to a bounded queue and drops (counted, and written to log) when that queue is full, because a receiver this node does not control must not be able to delay op admission. See crate::hooks.

§Errors

Returns a message when the subscription file cannot be read or does not parse, or when the delivery thread cannot be started.

Source

pub fn hook_drops(&self) -> u64

Events the webhook queue dropped because it was full. Zero when no --hooks-file is configured.

Source

pub fn lag(&self) -> Arc<LagMeter>

The live latency record, for a caller that wants to tighten the gate or read it without going through the API.

Source

pub fn drain_lag_log(&self)

Writes any breaches recorded since the last drain to the lag log.

Called by the daemon’s accept loop, which is the same place it polls for a failed durability barrier: both are things the writer thread can only report, never act on. No traffic means no drain, which is harmless because no traffic also means no breaches.

Source

pub fn view_seq(&self) -> u64

The sequence the next admitted op will occupy, which is the cheapest complete description of “what state is this node in”.

Exposed for the browser surface’s cache: it asks this before deciding whether to rebuild a page, so an unchanged node costs one u64 read rather than a full view serialization.

Source

pub fn roster(&self) -> (u64, BTreeMap<String, String>)

The store’s generation and the handle-to-name map to render channels through (D46).

Both together because a caller that resolves names has to cache on the generation the map was read at: revoking an account deletes a name and appends no op, so the view sequence does not move and a page keyed on it alone would keep showing the deleted name.

(0, empty) on a node with no accounts store attached, which resolves nothing and renders every channel as itself — the same answer a store gives for an account issued before D46 or issued with an explicit user.

Source

pub fn review_repo(&self, id: &str) -> Option<String>

Repository the review id proposes to land on, in the canonical D29 spelling, or None when the review is unknown or unbound.

Exposed for per-repository authorization: it is what lets posting a verdict require write on the repository under review, instead of the node-wide grant every review op would otherwise need.

Source

pub fn review_json(&self, id: &str) -> Option<Value>

One review as the API renders it, or None when no such review exists.

Exposed for the D34 review page, which needs one review rather than the whole view. Same review_json the API uses, so the page and the API cannot describe a review differently.

Source

pub fn reviews_for_repo(&self, repo: &str) -> Vec<(String, Value)>

Every review whose target ref names repo, newest id last.

A review with no target ref belongs to no repository and is omitted: it cannot be shown under one without asserting a relationship the requester never signed.

Source

pub fn reviews_awaiting(&self, channel: &str) -> Vec<(String, String, Value)>

Every live review that channel was drawn for and has not answered, paired with the repository its target ref names.

The reviewer’s own queue, which the view has always held and no page has ever shown. Three filters and no others:

  • drawn: channel is on the review’s reviewer list. Being able to read a repository is not being asked about it.
  • unanswered: no verdict of theirs stands. A review they have already answered is not owed, whatever anybody else has said.
  • live: not archived. An archived review has dropped its verdicts, so an answer to it would land nowhere.

A review with no target ref is omitted for the same reason Self::reviews_for_repo omits one: it belongs to no repository, and this page’s rows are grouped by one. It cannot become invisible that way — nothing can be drawn on a ref that is not named.

This does no authorization. The caller filters by what the reader may read, because the ACL lives there and a second copy of that decision here is a second copy that can disagree.

Source

pub fn handle_api(&self, method: &str, path: &str, body: &[u8]) -> (u16, String)

Handles one /api/... request, returning (status, json_body).

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.