feat(graperank): run crawl/score without a personal account

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
This commit is contained in:
Claude
2026-07-17 22:41:26 +00:00
parent dc157e32d2
commit 331dca79cd
@@ -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`.