Skip to content

forge: mirror repos on their agent's kind-30618 state (opt-in) - #13

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
forge-mirror
Oct 19, 2026
Merged

melvincarvalho merged 2 commits into
gh-pagesfrom
forge-mirror

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #12.

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.

mirror: {
  relays: ['wss://relay.damus.io', 'wss://nos.lol'],
  follow: [{ pubkey: '<64-hex agent key>', name: 'proj', clone: 'https://github.com/o/proj.git' }],
}

How it works

  • 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

1 open issue
What changed in this PR

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.

Review effort: Balanced

Comment thread forge/plugin.js Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity 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.

Low severity 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.

1 resolved since last review

Review effort: Balanced

@melvincarvalho
melvincarvalho merged commit 798f109 into gh-pages Oct 19, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the forge-mirror branch October 19, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forge: mirror repos on their kind-30618 repository state (opt-in, config.mirror)

2 participants