aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* dashboard: no subscription to this node itselftv2 days1-1/+8
| | | | | | | | | | | | A page opened from another node's page takes that page's subscriptions along; one of them can be this node, another there. The page then followed this node as a subscribed one too, and showed its messages twice, from its own log and from the subscription (`→ LLL PING` and `LLL ← RRR PING`, the same message), on the map as in the console. The page follows no subscription of its own node: not when it knows which node it is, and one begun before that stops once it does; its messages, if any came, are left out.
* dashboard: keys edited, the last removed with a password, failures kepttv2 days5-85/+211
| | | | | | | | | | | | | | | | | | | | | The credentials dialog listed a user's keys whole, to add one or remove one but the last, and any failure replaced the dialog with the error, the form lost. * A key is shown short: its type, the first and last four characters of what tells it apart (an Ed25519 key's base64 after the part all of them share), its comment; the whole line in its tooltip. * A key can be edited, its name and its line: saved under the name, a new name written first and the old one removed. * A user with a password on this node (`--htpasswd`) may remove its last key: it logs in by the password. GET /auth/keys says so (`password`), and DELETE /auth/keys/{id} takes it then; the rules no longer keep the last key on every node (no other node can tell who has a password where; under a strict policy the signed entry keeps one still). * What fails says why beside where it was asked (the add form, the edited row, the removed one), which stays as it was, to be set right and tried again.
* dashboard: a node's menu on a right-click, and going to its dashboardtv2 days1-5/+14
| | | | | | | | | | A right press on a node started dragging it, as any press did: the map took hold of the pointer, so the right-click went to the map and the node's menu didn't open, and letting go put the node in focus. The right button now opens the node's menu alone. Its entry for the node's dashboard is `go to its dashboard`, greyed where the node publishes none (and on this node's own), its tooltip saying where it goes or why not.
* dashboard: move pull slider to link legendtv2 days1-29/+43
|
* dashboard: an entry's menu left of the fingertv3 days1-4/+12
| | | | | | | Opened by a finger held on an entry, its menu had its top left corner at the finger, under the hand holding it, a right hand mostly. By touch it now has its top right corner there; opened by a pointer, its top left, as before.
* dashboard: the document box edited from its menus alonetv3 days1-45/+19
| | | | | | | | With the entries' menus (1238), editing had three ways in: the double-click, the `+` on a map's or list's row, and the menu. The double-click and the `+` go; the menu does all of it. The title, which stands for the root (its row isn't shown), now takes the right-click too: its menu adds a key at the top level.
* dashboard: an entry's menu in the document boxtv3 days1-25/+192
| | | | | | | | | | | | | | The document box edited an entry by a double-click and added to a map or list by the `+` on its row; a key couldn't be renamed, and a list's `+` always asked for an index. A right-click on an entry, or a finger held on it half a second, opens its menu: `Edit value` (as the double-click), `Rename key` for a map's key, and on a map `Add a key`, on a list `Append an item` or `Insert an item at…`. Renaming reads the value, writes it under the new key and deletes the old one (two writes, the value as JSON; a key that exists already isn't overwritten). Inside a field being edited, and where an entry offers nothing, the browser's own menu shows.
* dashboard: where a stream's lag comes fromtv3 days2-25/+63
| | | | | | | | | | | | | A stream's lag in the tooltip of `behind: …` was one figure, from when a node logged a message to when the page had it: a second and a half for a node a LAN away said nothing of where it went. The node now stamps each `messages` event with when it sent it (`{"sent": …, "messages": […]}`), and the tooltip splits each stream's lag: how long its batches waited in the node, from logging the first message to sending the batch (the node's own clock: exact), and how long they took on the way, from sending to arriving. This node's own stream is always listed, beside the three slowest.
* dashboard: the console in the order messages wenttv3 days1-4/+20
| | | | | | | | | | | | The console appended messages as their streams brought them, a subscribed node's later than this node's by its path, each line with its node's own clock's time: lines of one moment came apart, and their times read out of order. Each message is now listed by when its node logged it, on this node's clock, as the map runs them (`aheadOf`): one a slower stream brought late goes in among those of its moment. The time shown is on this node's clock; a subscribed node's own clock's time is in its tooltip.
* dashboard: messages on one timeline, when they wenttv3 days1-28/+120
| | | | | | | | | | | | | | | | | Each node's message stream comes its own way to the page, a subscribed node's over the network to it: messages logged 16 ms apart on three nodes ran over a second apart, as their streams arrived. Messages now run when they went, on this node's clock: each at the time its node logged it, taken back by how far that node's clock is ahead of this node's (the map's clock chain, `aheadOf`), a while behind real time (`replayDelay`): the most any stream lagged in the last 30 s and a tenth of a second more, shown after the sliders (`behind: …`), each stream's lag behind the fastest in its tooltip. The browser's clock is against all streams alike and drops out. A message known only from its receiver (its sender not subscribed) is placed at its arrival, starting a crossing before. Messages lagging more than 10 s (a gap) don't run, nor hold the map behind.
* dashboard: messages as they cometv3 days3-105/+143
| | | | | | | | | | | | | | | | | | The node sent its messages to the page every 2 s (`messages` events, this node's and a subscribed node's), and this node's ran on the map from its counts, which come with the state: the map showed a message 2 to 4 s after it went, the messages of each 2 s spread evenly over the next. The message log now tells those waiting of each message logged (MessageLog::logged_after), and both streams send what was logged a tenth of a second after the first of it (MESSAGES_GATHERED): a busy node about ten events a second, a quiet one each message soon after it went. The page runs messages as their streams bring them, this node's from its own log too (with the addresses its PINGs went to), as far apart as they were sent; more than six on a link and way at once are shared out by kind over the moment, and after a gap only the last 2 s run. The counts still give the rates and what was delivered.
* dashboard: messages run as they left; a link breathes red for those losttv3 days1-11/+126
| | | | | | | | | | | | | | | | | | A message between this node and a subscribed one ran from this node's counts both ways: one the subscribed node sent ran once this node had counted it received, as it arrived. Each message now runs from its sender where the page reads the sender (this node's counts, a subscribed node's messages), so it runs the way it went, also where it never arrived; only one from a node the page doesn't read runs as it arrived. The console lists them as before. Where the page reads both ends of a link, what one sent the other should count received: a message sent and not received within 5 s makes the link breathe red once (`breatheLink`), its tooltip saying how many. Kinds the paths and identity layers count under addresses are left out, as this node's counts leave them out; a subscribed node's tick that came short (the latest 500 of more) starts the count afresh, so what the page didn't see isn't taken as lost.
* dashboard: messages follow the line they went overtv3 days1-27/+68
| | | | | | | | | | | | Dots and flashes ran straight from sender to receiver, whatever line the message took: a PING over another transport, drawn as a curved line beside the one in use, crossed the map off its line. Each message now runs along the line it went over, as drawn (`route`: the link's quadratic curve, its control point where renderGraph put it): a message naming the address it went to (the paths layer's PINGs and PONGs) over that address's transport's line, any other over the line in use; a flash's streak bends with it.
* dashboard: how hard links pull, on a dynamic maptv3 days1-2/+26
| | | | | | | | On a dynamic map, links pull toward their lengths against the nodes pushing each other apart, at one strength. A `pull` slider before the layout's toggle, shown while dynamic, sets it from a quarter to four times as hard (2 to its value, once at first), kept in the browser; the map moves to the new balance smoothly.
* dashboard: no dotted links for an end the page doesn't readtv3 days1-11/+7
| | | | | | | | | | A link was dotted where an end reported no links. Members used to report their links to every dashboard (LINKS); the page now reads a member's links only while subscribed to it, so a dotted link said no more than that its other end isn't subscribed, which the node shows already (hollow), and most links were dotted. The style and its legend entry go; the link's tooltip says when one end alone reports it.
* dashboard: every kind of message coloured and in the legendtv3 days1-4/+8
| | | | | | | | The map's dots and the console coloured six kinds of message; PROBE, PRUNE/GRAFT, NEIGHBOUR and REFUSALS were drawn in the text colour and missing from the legend. Each kind (`traffic`'s counters) now has a colour, and the legend names them all, saying which messages a kind stands for where its name doesn't.
* changes: Context where it is usedtv3 days1-1/+2
| | | | | | Since load_delta is for tests (1190), nothing else in changes.rs uses anyhow's Context, and builds without tests warned of an unused import. load_delta imports it itself.
* screen: what apply_bytes did, without how many it appliedtv3 days1-3/+0
| | | | | | | How many changes apply_bytes applied was read only by the handler of SIGNATURES, which re-screened the changes waiting for them and published those that went in. With SIGNATURES gone (1219), nothing reads it: Outcome loses the field.
* sync: a message carries the signatures of its changestv3 days4-63/+57
| | | | | | | | | | | | | | | A sync message carried the signatures its peer wasn't sent yet, by a cursor per peer. Changes sent again, as after a message taken as delivered and lost on the way, went without them: the peer was past them. Under a policy requiring signatures they waited until the writer's next signed change, or the next session. A message now carries the signatures of the changes it carries (Signatures::for_changes), whatever the peer was sent before: changes sent again arrive vouched for as the first time. Past 256 (a message carrying the batches of many writers), each writer's latest instead, as a delta carries. The cursor goes, and with it resetting it after a failed send or a new session.
* sync: a test exchanging until level, not one answertv3 days1-0/+17
| | | | | | | | | delivered_messages_are_answered_with_changes_we_lack failed now and then: after the answer, the node still lacked the peer's change. Automerge's sync tells what each side has by a Bloom filter, whose false positives take more rounds. The test now exchanges up to four rounds more, each asking for what is still missing, until both are level.
* sync: signatures sent again after a failed sendtv3 days1-6/+16
| | | | | | | | | | A sync message whose send fails gives back the changes it carried: the next message takes them again. The signatures it carried weren't given back: the peer's cursor stayed past them, so the changes went again without them, and under a policy requiring signatures waited for them until the next session; SIGNATURES no longer covers that. A failed send now sends the peer all signatures again with the next changes (each actor's latest, a few hundred at most).
* swarm: no SIGNATUREStv3 days10-167/+11
| | | | | | | | | | | | | | Signatures sent on their own (SIGNATURES, every second, to each peer not sent them yet) flooded every link: a signature made after its change left, or one that came with changes, went to every neighbour, whether or not it got the change along with its signature elsewhere. With every change of a node signed as it leaves (1217) and signatures going where their changes go (1218), none is left for them to carry: the message, its task and its traffic counter go (wire version 46). Signatures waiting for their changes are offered as before when sync messages bring them, and the changes waiting for signatures judged again with the next batch.
* sync: signatures go with changes onlytv3 days2-2/+19
| | | | | | | | | | | A sync message took the signatures its peer wasn't sent yet whenever it had a body, an Automerge message saying only what we have included. A signature belongs with the changes it vouches for: they go along the trees, and a peer that got them elsewhere got the signatures with them. Signatures now go in a message that carries changes (pushed, offered as a delta, or in an Automerge message carrying some: SyncSession:: carrying), and in no other.
* signatures: sign what is senttv3 days6-75/+55
| | | | | | | | | | | | | | | | | | | A node signed its changes no rule signs a while after them (--sign-every 1s): the change left at once, unsigned, and its signature followed on its own a second later, to every neighbour, over every link. A few writes made a cascade of SIGNATURES. Under a policy requiring signatures it signed every change as it was made instead. A node now signs its latest change, if no signature vouches for it, as a sync message takes it (--sign-every sent, the default): the signature leaves in the same message, and goes where the change goes. One signature per message, however many changes it carries, each vouching for those before it: a slow writer's every change is signed, a fast writer's batches are, at no threshold to pick. A policy requiring signatures signs what is sent too, even with `none`. `all` still signs every change as it is made; changes to protocol data, listed paths and ?signed=true writes are signed as they are made, as before. The interval, and the task signing on it, go.
* signatures: a node signs at start what it kept unsigned when it stoppedtv3 days2-0/+60
| | | | | | | | | | | | | | A node commits a change, keeps it in its document file, and may stop before any signature vouches for it. It starts again under a new actor and only ever signed its current actor's latest change: the old one stays unsigned for good. Peers pulling it then wait for a signature under a policy requiring them, and so does everything the node writes after, building on it. Signing every change at commit hid this under such a policy; signing what is sent, next, won't. At start, before anything leaves, a node signs each of its own changes at the document's heads that no signature vouches for: nothing it made since builds on them, so they are heads.
* roots: a peer contradicting --genesis-root refused at the handshaketv3 days1-2/+18
| | | | | | | | | Review 3, R5: a node given its root (--genesis-root) refused a peer naming another only change by change while its document was empty, each with an entry among the refused; the handshake went through, as there was no root of its own yet to compare with. The root given is now compared too: a peer naming another is refused at the handshake, logged once, as one whose document starts from another root.
* state: refused writes limited for the node tootv3 days2-17/+53
| | | | | | | | | | | | Review 3, R5: --refused-writes-per-minute bounds each user, but the cost it guards, a document copy under the write lock, is the node's: ten users, or ten accounts of one, are ten times the budget. The node now takes six times a user's limit within a minute in all (refused::NODE_FACTOR); past it, every user with a write refused within the minute is answered 429 untried, until the oldest leave it. A user none of whose writes was refused writes on, so honest writers aren't held up by others' refusals.
* screen: judged after the policy is gone, tootv3 days3-6/+64
| | | | | | | | | | | | | | Review 3, R4: whether a batch was judged followed the receiving document's policy now (and whether the batch writes one). After the policy was removed, or replaced by a version the node doesn't know, changes written under it and received late went in unjudged, while nodes that had them earlier judged them; REFUSALS gave up likewise. Screening now remembers that the document held a policy, known or not (Screening::policed), and judges what it receives while it does, each change by the policy at its dependencies; REFUSALS judges then too (not under a policy the node doesn't know, as before). Not across a restart, which access.md says.
* remake: a strict remake of a strict swarm isn't followedtv3 days2-0/+28
| | | | | | | | | | | | | | | Review 3, R3: 1186 kept honest nodes from proposing or voting for a strict remake of a swarm strict already, but a majority of members, each with keys of its own, could still commit one, and switch and catchup followed it without asking what kind of remake made the epoch: the registry, policy and bans of a strict swarm changing by a member majority, the honest minority following. A node now refuses to switch to such an epoch (its document's policy strict, the remake's kind strict), and catchup refuses to rejoin one whose chain of epochs holds a strict remake after the swarm was strict (as the old document is, or once a remake since made it so). access.md says so.
* roots: a joiner takes a root once its seeds named ittv3 days5-32/+128
| | | | | | | | | | | | | | | | Review 3, R2: the root pin was set by the first HELLO_REPLY, and the first sync, a round trip later, took that root; a peer naming another afterwards was refused as another history. So the seed that answered first decided, not "none while they disagree", and a malicious seed makes itself fastest. A node joining with an empty document of the first epoch now takes no root (RootPin::Awaiting, roots left for later) until each of its seeds named one, or ROOT_WAIT (10 s) passed since the first did; then the one they name, none while they differ, as before. With --genesis-root it takes that one alone, at once. access.md, the option's help and example.toml say so; switching roots later, while the joiner holds nothing of its own, is postponed.
* signatures: tell held changes as the screen doestv3 days1-4/+4
| | | | | | | | | | With every message's signatures offered now (1205), Signatures::offer runs for each, and it asked get_change_meta_by_hash whether the document holds each signature's change, and each one waiting, up to 4096 of them: Automerge rebuilds the change's metadata from the document's operations to answer. Ask changes::holds instead (1197), there and in forks and kept_for; the metadata is still read where it is used.
* screen: signatures required as the policy a change came under saystv3 days5-51/+96
| | | | | | | | | | | | | | | | | | | | | Review 3, R1: what a node received was vouched for only while its own document's policy required signatures. A joiner's first sync, and every node's message bringing `required`, went in by the rules alone: a malicious first peer could plant changes under other members' actors at their next seqs, their real ones then failing as duplicates for good. The signatures a message carries are always offered now, and the screen always has them: a change waits for one where the policy at the heads it is judged at requires it (rules::Reads::required, read once per heads), whatever the receiving document holds. Changes written under a policy not requiring signatures wait for none, as on every other node; the check for forged changes runs always. Changes waiting for a signature are judged again whenever signatures come. The test of signatures required now takes the history into an empty document; a new one takes changes written under a policy not requiring them without any.
* screen: share the heads a change is judged at with what it builds ontv3 days1-9/+19
| | | | | | | | Each change taken but not yet applied got its own set of the heads it is judged at, built in an ordered set and copied into a vector, and kept so for those building on it. Most build on one change alone and are judged at its heads: share them instead (reference counted), and build a set only for a change with several dependencies.
* screen: move each change along instead of copying ittv3 days1-4/+4
| | | | | | | Screening copied every change it took, and every one left waiting for a signature, bytes and all, from the batch it had ordered and then dropped. Move them out of the batch instead; only a refused change, recorded with why, is still copied.
* screen: order changes by their places, each hash looked up oncetv3 days1-36/+36
| | | | | | | in_order is about a seventh of screening in the profile, most of it hashing change hashes: each change went into and out of three maps, and each dependency into two. Look each hash up once, in a map from hash to place, and keep the rest by place in vectors.
* screen: read once per batch whether the policy is stricttv3 days1-8/+29
| | | | | | | | | | | | The profile of bench_screening puts screening at about 21% of the samples, half of it Automerge applying the changes. Of the other half, reading the policy for every change taken is the largest part (3.4%): reaches_signed reads it, and what users sign, to tell whether the change must go in before the next is judged. Read them once, and again only after the document took the accepted ones; a change to the policy or to what users sign is relevant, taken at once. A change touching nothing reaches nothing.
* document: tell whether a document holds a change without building ittv3 days7-24/+60
| | | | | | | | | | | | | | | With the policy's reads kept, judging a change still costs about one read (some 3.5 µs), and so does judging without a policy: what is left is At::new, which asks get_change_by_hash for each head. Automerge builds the change again from the document's operations to answer, at a cost growing with its history. Screening asks it again for every candidate and every dependency, and sync for heads. Ask by the missing dependencies instead (changes::holds), which look the hash up: one the document holds is not among them. A change queued in the document, waiting for its dependencies, isn't either; but then some are missing, and only then is it asked the slow way. Every check of whether a document holds a change goes through it.
* bench: break screening down after a long history tootv3 days1-7/+13
| | | | | | | | The breakdown judges at the heads after 400 changes. Whether a read at given heads costs more in a document with a long history decides what a read the rules don't keep costs a node with a big document. For the workloads of more than 10000 changes, break it down a second time, judging the last 1000 at the heads after the rest.
* access: read once what rules read at the same headstv3 days3-29/+136
| | | | | | | | | | | | | | | bench_screening shows judging at about 17 µs of a change's 25: some two and a half reads at its heads (the policy, bans, its node's member entry, whether there is a registry, the sealed declarations), each costing as much as working out what the heads cover. Screening judges most of a batch at the same heads, and those reads are the same for every change judged there. Keep them (rules::Reads), by node and by user where they depend on one, while the heads stay the same; screening keeps one for a batch. What the heads hold doesn't change as the document takes changes, so nothing goes stale but by other heads. The benchmark breakdown shows judging so and afresh.
* bench: break screening down per changetv3 days1-0/+103
| | | | | | | | | bench_screening tells what screening costs in all, not where. Print, per change judged, what decoding it, placing its operations (inspect::Inspector) and judging it (rules::judge) cost; of judging, what reading the policy costs, and one read for scale. Judged as screening judges most changes: at the heads after the first ones, whose maps flush the batch.
* state: limit refused writes per usertv3 days8-0/+105
| | | | | | | | | | A write every node would refuse is rolled back by forking the document at the heads before it, under the write lock, at a cost that grows with the document's history. A user could ask for such writes in a loop. Count each user's refused writes over the last minute. Past --refused-writes-per-minute (default 10), its writes are answered 429 without being tried, until the oldest refusal leaves the minute.
* roots: an epoch's root pinned, and named in every handshaketv3 days15-10/+315
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Review 2, N7, the care it raised: a joining node's empty document took as the first epoch's root whatever dependency-less change came first, from whichever peer it joined through; the first epoch's ID is all zeros, so nothing pinned it. A remake's epoch has its ID, the snapshot change's hash, which the rules hold its root to. * Every node names its epoch's root in HELLO and HELLO_REPLY (the epoch's ID for a remake's, its genesis change for the first; wire version 45). A node whose document starts from another root of the same epoch refuses the handshake, logging it: another history under the swarm's ID, a forged root or a genesis started again. * A joining node with an empty document takes as its root only the one it was given (--genesis-root, new), else the one its peers name, and none while they name different ones (access::screen::RootPin, kept in Screening across documents): one peer hands it another root only if it is all the joiner hears, or never, with --genesis-root. A root refused is listed with the refused, what builds on it with it. * The genesis node logs the root (`The swarm's root`); GET /admin/epochs shows it (`root`). A genesis started again under a remembered swarm ID used to merge with the nodes of it still running; they now refuse it (another root), and the docs say to join them with seeds instead. The epoch's identity stays as it is (generation 0 all zeros): records and epoch metadata name it so, and every comparison of epochs with them.
* screen: what a node receives judged, policy or nottv3 days5-29/+53
| | | | | | | | | | | | | | | | | | | | | Review 2, N7: a node screened received changes only while its own document had a policy it enforces. A joiner's empty document has none, so its first sync, as much history as the node it joins through chose, was applied unjudged, and under signatures required unvouched; the same for any node while the policy's own change arrived with later ones. Sync messages, deltas and downloaded changes are now always screened, each change judged by the policy at its dependencies (1135), so the verdicts are the swarm's. While neither the document nor the changes hold a policy (none touches a "policy" key: inspect::touch_key), only an epoch's root is judged and nothing is placed: bench_screening, after 1188, measured placing everything at about as much again as applying (564 ms + 543 ms for 100 000 changes), judging by the rules at about five times (+2.5 s), which a document with a policy pays anyway. access.md no longer lists the joiner's first sync, nor nodes agreeing only as the policy reaches them (judged at the dependencies, they agree), as left to do; changes::load_delta is for tests now.
* store: a write inside a list checked where the rules judge ittv3 days2-6/+75
| | | | | | | | | | | | | | Review 2, N9: the API checked a write's capability on the path asked (servers/list/3/name), the rules on the list it is in (servers/list): a user whose capability lay inside a list had every such write pass the API and refused by the node's own judgement (403, after rolling the document back). inspect::placed places a write's path in the document by the rule the inspector places operations by (Place::child: cut after the first list or text on the way), and the store's writes check capabilities there. One rule for both; a test checks placed against the paths the inspector finds in the change such a write makes.
* screen: changes put in order in one passtv3 days1-19/+37
| | | | | | | | | | | | | | | | bench_screening showed screening growing quadratically: 1 000 changes took 29 ms on top of applying them, 10 000 took 2.8 s, and 100 000 didn't finish in minutes. in_order placed changes in rounds, each taking those, in hash order, whose dependencies were placed: a chain received in one delta, its hashes in no order of their own, took about as many rounds as changes, each over all of them. It now counts, for each change, its dependencies among those received not placed yet, and places the smallest hash of those with none left, then those building on it as their counts reach zero (Kahn's algorithm): n log n, and still the same order on every node, whatever order the changes came in. One with a dependency neither in the document nor among them, and what builds on it, still wait.
* bench: what screening costs while catching uptv3 days1-0/+108
| | | | | | | | | | | Review 2, N7, measured first: a node whose document has no policy (a joiner's empty one) applies what it receives unjudged. Screening it too would cost finding every change's paths and judging it. bench_screening takes 1 000, 10 000 and 100 000 changes into an empty document, as one delta brings them: unjudged, as Automerge applies them, against screened (access::screen::apply), with no policy in them (each passes, read at its dependencies, after its paths were found) and with the policy their first change wrote (each judged by the rules).
* strict: no strict remake of a swarm strict alreadytv3 days2-7/+18
| | | | | | | | | | | | | Review 2, N2: nothing checked that a strict remake's base wasn't strict already. In a strict swarm the registry, the policy and the bans change only as a user who may change them signs; a strict remake committed by a majority of members, each with keys of its own, could drop any of them (unban, remove managers, empty userSigned and the sealed declarations), and the honest minority would follow. A node now takes a strict remake (proposing or voting) only in a swarm not strict yet: turning strict is done once, by a remake; after it, by signatures.
* refusals: a big drop waits for a second member only where there is onetv3 days1-4/+11
| | | | | | | | | | | | Review 2, N8: a node with one neighbour (a swarm of two, a leaf) never hears a second member name the change: its big drop was held for good, an error logged at every REFUSALS. It now waits only where it has another neighbour than the one telling it; with one, one word is all there is. access.md also says what the wait is: it guards against one peer, not two members saying the same (any node with the swarm key registers), and never makes a node drop what its own rules take.
* remake: aborts written as the user who asks; no abort by timetv3 days5-172/+61
| | | | | | | | | | | | | | | | | Review 2, N3: 1150 let any member write an abort decision once a remake had expired a grace period ago, judged by the change's own time, the writer's word. A member could race the coordinator's legitimate commit with an abort dated as it liked; both taken, Automerge shows one, and nodes that acted on the commit split from those that didn't. The two writers of such aborts are operator requests whose endpoints already need write on _continuum/remakes: aborting an expired remake (DELETE /admin/remakes/{id}) and superseding one (POST /admin/remakes?force=supersede). Both now commit as the user who asks (the proposal and its vote with them, for a supersede), which the rules take as one who may write the decision. The clause judging an abort by its time goes, and with it the time the rules carried. An expired remake is still aborted through any member, by an operator.
* strict: a manifest serves one remake, at its basetv3 days4-17/+102
| | | | | | | | | | | | | | Review 2, N1: a strict manifest wasn't bound to its base. Any member could propose an old, legitimately signed one again at the current heads; nodes would verify it and vote, and every value at a signed place changed or added since (a ban, a new user, a rotated key, a changed userSigned or sealed declaration) would be deleted, nothing after the base being left to replay. The manifest now names the epoch and the heads it was made at, and a node takes it (proposing or voting) only for a remake of that epoch with exactly that base. GET /admin/strict-manifest makes it so; continuum-signed strict proposes at those heads already.