You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
forge: mirror repos on their agent's kind-30618 state (opt-in) - #13
The other half of gitmark 0.0.84, which publishes a NIP-34 repository state (kind 30618) after every mark. With config.mirror set, the forge follows agent keys and pulls their repos when the state changes. Unset, nothing changes: no sockets, no fetches.
One ws socket per relay, one REQ for kinds 30617 and 30618 from the followed pubkeys. The relay filters, so the forge receives only events about repos it mirrors. Reconnect with backoff (1 s doubling to 60 s); every connect re-sends the REQ, which is the catch-up, since a replaceable event comes back as its newest version.
A 30618 is acted on only when the signature verifies, (pubkey, d) is a followed repo (never name alone), created_at is newer than the last state acted on, and a branch differs locally. Then one git fetch from the clone URL (configured, or the key's newest 30617) into refs/mirror/*, at most one per repo per 30 s, 60 s timeout.
Per changed branch, a verdict: not fetched, the branch stays; no mark, it moves; a mark, it moves only if blocktrails.json at that commit lists the marked commit. A state claiming a mark its own trail does not carry moves nothing, and the reason is recorded.
The mirror lives at repos/<pubkey>/<name>.git like a pushed repo, so it has the usual pages and the Anchors tab reads its trail. GET api/repos/<o>/<n>/mirror returns the record: last state seen with action and reason, last fetch with per-branch verdicts, clone URL in use. deactivate() closes the sockets.
The stance holds: the plugin talks to relays and git remotes, never to a chain API. Whether the mark is on the chain stays the browser's question.
Code: forge/lib/mirror.js is the pure part (config parsing, NIP-01 verification, readState, planMirror, branchVerdict), the sockets and git in plugin.js after the announce section. README section "Mirroring on kind-30618".
Tests: 148 pass (138 + 10). Six on the library; four against an in-process relay that answers REQ and pushes events, with a local upstream repo as the clone URL: catch-up on boot, a marked state that moves, a state whose mark the trail does not list that is held, another key's state with the same d ignored, an older state skipped.
Live: a local forge following a cold-run agent's key on the three real relays fetched its repo on the real 30618 gitmark 0.0.84 published, moved the branch with "fetched; the trail lists mark 548ac1cb on txbt4", and served the marks page.
Not here: deploying it on melvin.me (the operator's config), and discovering which keys to follow.
….mirror)
gitmark 0.0.84 publishes a NIP-34 repository state after every mark. A
forge told to follow (pubkey, name) pairs subscribes to those states on
the configured relays and pulls when one changes: one REQ per relay for
kinds 30617 and 30618 from the followed keys, one git fetch per real
change, into refs/mirror/*. A branch moves only to a commit the fetch
brought and, when the state carries a mark, whose blocktrails.json lists
the marked commit. Mirrors are keyed by pubkey plus name, never name
alone. Reconnect with backoff; every connect re-sends the REQ, which is
the catch-up. Unset, nothing happens. The record is
pluginDir/mirror/<pubkey>/<name>.json, served at api/repos/<o>/<n>/mirror.
forge/lib/mirror.js is the pure part (config, event checks, the plan,
the verdict); the sockets and the git are in plugin.js. Ten tests: six
on the library, four against an in-process relay and a local upstream.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It introduces security-sensitive background networking and remote git fetch/ref-moving driven by signed external events, which warrants final human review despite being well-tested.
This PR adds an opt-in mirroring feature to the forge plugin (closing issue #12). When config.mirror is set, the forge follows one or more agent nostr keys over ws relays, subscribes to their NIP-34 repository-state events (kind 30618) and announcements (kind 30617), and performs a git fetch into a local bare repo when a followed repo's state advances. A branch is only moved to the state's commit when that commit was actually fetched and, when the state carries a gitmark, when blocktrails.json at that commit lists the marked commit — otherwise the fetch is kept but no ref moves and the reason is recorded. The design keeps the plugin's existing stance of talking only to relays and git remotes, never to a chain API. Unset, the feature is fully inert.
Changes:
New pure library forge/lib/mirror.js (config parsing, NIP-01 event verification, state/clone parsing, fetch planning, per-branch verdicts).
forge/plugin.js wiring: per-relay sockets with backoff, serialized per-repo fetch/apply, a pluginDir/mirror/<pubkey>/<name>.json record, a GET api/repos/<o>/<n>/mirror endpoint, and socket teardown in deactivate().
Documentation and tests (6 pure-library tests + 4 end-to-end tests against an in-process relay and a local upstream repo).
File
Description
forge/lib/mirror.js
New pure module: parseMirrorConfig, verifyEvent, readState, readClones, planMirror, trailLists, branchVerdict.
forge/plugin.js
Socket lifecycle, fetch/apply pipeline into refs/mirror/*, mirror record persistence, new mirror API route, deactivate() closes sockets.
forge/test.js
Pure-library unit tests and in-process-relay end-to-end tests (catch-up, mark-verified move, held state, wrong-key/stale-state rejection).
forge/README.md
New "Mirroring on kind-30618 (opt-in)" section documenting config, flow, verdicts, and the record endpoint.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It introduces new outbound network sockets and remote git fetch execution driven by signed external events, which warrants final human review despite otherwise solid logic and tests.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Unserialized 30617 updates can be overwritten by mirrorApply
forge/plugin.js:1675
The 30617 announce handler updates the mirror record outside the per-repo mirrorQueues serialization that guards state applies. mirrorApply reads the record once at the top (line 1608) and then awaits a potentially long git fetch (up to 60 s) before writing the full record back (lines 1614/1618/1627/1648). If a 30617 for the same repo is processed during that window, it writes clone/announceAt/clones here, but the in-flight mirrorApply then saves its stale pre-fetch record and silently clobbers those fields. The announce-derived clone URL is lost until the next 30617 arrives. Consider routing this clone update through mirrorEnqueue so it is serialized per repo, or re-reading and merging the clone fields immediately before each saveMirrorRecord in mirrorApply.
Missing integration test for 30617-derived clone URL flow
forge/plugin.js:1671
The documented capability of deriving a clone URL from the followed key's newest 30617 (README: { pubkey, name: 'other' } // clone URL from the key's newest 30617) is not exercised end-to-end. The integration suite only ever pushes kind-30618 states with a configured clone, and the "no clone URL" case never receives a 30617. readClones is unit-tested, but this wiring — receive a 30617, store its clone, then fetch a later 30618 using that stored clone — has no test. Given the forge's thorough integration coverage (e.g. the paired 30617/30618 announce tests), consider adding a case that pushes a 30617 before the state so a mirror with no configured clone still fetches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
The other half of gitmark 0.0.84, which publishes a NIP-34 repository state (kind 30618) after every mark. With
config.mirrorset, the forge follows agent keys and pulls their repos when the state changes. Unset, nothing changes: no sockets, no fetches.How it works
wssocket per relay, oneREQfor kinds 30617 and 30618 from the followed pubkeys. The relay filters, so the forge receives only events about repos it mirrors. Reconnect with backoff (1 s doubling to 60 s); every connect re-sends theREQ, which is the catch-up, since a replaceable event comes back as its newest version.(pubkey, d)is a followed repo (never name alone),created_atis newer than the last state acted on, and a branch differs locally. Then onegit fetchfrom the clone URL (configured, or the key's newest 30617) intorefs/mirror/*, at most one per repo per 30 s, 60 s timeout.blocktrails.jsonat that commit lists the marked commit. A state claiming a mark its own trail does not carry moves nothing, and the reason is recorded.repos/<pubkey>/<name>.gitlike a pushed repo, so it has the usual pages and the Anchors tab reads its trail.GET api/repos/<o>/<n>/mirrorreturns the record: last state seen with action and reason, last fetch with per-branch verdicts, clone URL in use.deactivate()closes the sockets.The stance holds: the plugin talks to relays and git remotes, never to a chain API. Whether the mark is on the chain stays the browser's question.
Code:
forge/lib/mirror.jsis the pure part (config parsing, NIP-01 verification,readState,planMirror,branchVerdict), the sockets and git in plugin.js after the announce section. README section "Mirroring on kind-30618".Tests: 148 pass (138 + 10). Six on the library; four against an in-process relay that answers
REQand pushes events, with a local upstream repo as the clone URL: catch-up on boot, a marked state that moves, a state whose mark the trail does not list that is held, another key's state with the samedignored, an older state skipped.Live: a local forge following a cold-run agent's key on the three real relays fetched its repo on the real 30618 gitmark 0.0.84 published, moved the branch with "fetched; the trail lists mark 548ac1cb on txbt4", and served the marks page.
Not here: deploying it on melvin.me (the operator's config), and discovering which keys to follow.