From 331dca79cd46ff031769fd7f7dff0229438c5731 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 22:41:26 +0000 Subject: [PATCH] feat(graperank): run crawl/score without a personal account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crawl and score used Context.open, which requires an account solely to default the observer to the logged-in user — but neither uses account keys: crawl never signs, and score signs cards with the machine-level operator key (~/.amy/operator/, independent of any account). Switch both to Context.openOrAnonymous and require an explicit OBSERVER when running anonymously, matching `graperank followers`. A machine acting as a GrapeRank provider for arbitrary observers no longer needs a personal account (the operator key still needs its SecretStore passphrase, as before — that's the reachability monitor + card signer, a real secret). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4 --- .../amethyst/cli/commands/GrapeRankCommand.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt index 3b1a8fa95a..c6c1e5c1a0 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt @@ -265,8 +265,15 @@ object GrapeRankCommand { rigor = args.flag("rigor")?.toDoubleOrNull() ?: GrapeRankParams().rigor, ) - Context.open(dataDir).use { ctx -> + // Crawl never signs and score signs cards with the machine-level operator + // key (`~/.amy/operator/`, independent of any account), so neither needs a + // personal account — run anonymously when there is none, requiring an + // explicit observer since there's no logged-in user to default to. + Context.openOrAnonymous(dataDir).use { ctx -> ctx.prepare() + if (ctx.anonymous && observerArg == null) { + return Output.error("bad_args", "no account — pass an OBSERVER (npub / hex / nprofile / NIP-05)") + } val observer = observerArg?.let { ctx.requireUserHex(it) } ?: ctx.identity.pubKeyHex // Contact lists stream straight into a compact int-CSR structure as the @@ -558,8 +565,13 @@ object GrapeRankCommand { ): Int { val args = Args(rest) val observerArg = args.positionalOrNull(0) - Context.open(dataDir).use { ctx -> + // Crawl never signs, so no account is needed — run anonymously when there is + // none, requiring an explicit observer (no logged-in user to default to). + Context.openOrAnonymous(dataDir).use { ctx -> ctx.prepare() + if (ctx.anonymous && observerArg == null) { + return Output.error("bad_args", "no account — pass an OBSERVER (npub / hex / nprofile / NIP-05)") + } val observer = observerArg?.let { ctx.requireUserHex(it) } ?: ctx.identity.pubKeyHex // Persist-only crawl: no in-memory graph (null builder); every event // still lands in the store for a later `score`.