Skip to content

Document the NIP-98 agent-write pattern (Nostr key → WebID → pod write), + a few papercuts #79

Description

@melvincarvalho

What

jspod already supports giving an autonomous agent scoped, key-authenticated read/write to a pod — no OAuth, no JWT, just a Nostr key. We dogfooded the full loop and it works cleanly, but the recipe isn't documented and we hit a few avoidable papercuts. Filing so it can become docs (and maybe a couple of small DX fixes).

The pattern

An agent holds its own Nostr key. It reads a pod resource publicly and writes with a NIP-98 (kind 27235) Schnorr signature. The server resolves the signing pubkey to an identity two ways:

  1. As a WebID — if the pubkey is published in the pod owner's profile as a CID v1 verificationMethod referenced from authentication. jspod's profile already ships the cid / lws context with the verificationMethod / publicKeyMultibase / publicKeyJwk slots — they're just empty by default.
  2. As did:nostr:<pubkey> — the fallback when the key isn't in that pod's owner profile (the cross-pod / "guest" case). Grant this identity in a WAC ACL.

Recipe

  1. Add the agent's Nostr key to a WebID profile as a Multikey verificationMethod, listed in authentication. Encoding for publicKeyMultibase: f + e701 + 02 + the x-only pubkey hex.
  2. To let the agent write a resource it doesn't own, add a resource .acl granting acl:agent = did:nostr:<pubkey> the acl:Write mode.
  3. Sign each write: a NIP-98 event with u + method tags, sent as Authorization: Nostr <base64(event)>.
  4. General edit: the agent does GET resource, modifies the JSON-LD, then PUTs it back signed. Two primitives (get / put) plus the agent's own reasoning give full CRUD — no per-verb endpoints needed.

Papercuts worth documenting / fixing

  1. The .acl must live in the live --root. Writing it into a same-named pod-data/ directory elsewhere silently no-ops, because the server reads the runtime data root. Easy to get wrong when multiple checkouts exist.
  2. NIP-98 payload tag vs body re-serialization. The verifier hashes the parsed-and-re-serialized request body, so a payload SHA-256 computed over the client's exact bytes mismatches. Omitting the optional payload tag works (the signature still binds URL + method + freshness), but this should be documented — or the verifier should hash the raw body.
  3. --no-bootstrap pods render blank in a browser. The data-browser assets (data-browser-folder.js / .css) aren't placed in /public, so an HTML GET 404s them and shows nothing. Either document "it's headless, copy these in," or serve them from the package regardless of bootstrap.
  4. A resource .acl overrides inherited container defaults. You must restate the owner's Control and public Read alongside the new grant, or they're lost for that one resource.
  5. acl:agent did:nostr matching is exact-string — worth a note so people get the casing/format right.

Suggestion

A small CLI helper would turn this from "hand-edit JSON-LD" into one command, e.g. jspod profile add-nostr-key <pubkey> and/or jspod grant <webid-or-did> <mode> <resource>.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions