fleetgner is a delegated Nostr signer that runs as an MCP server over ContextVM (CVM).
It lets an admin grant specific users permission to request signatures from specific keys, with optional policies that constrain what can be signed.
It exposes two interfaces:
- NIP-46 for existing Nostr clients that already speak remote-signing
- CVM/MCP tools for agents and users that want an ergonomic RPC tool surface
In both cases, the same grant and policy enforcement applies through SigningCore.signDraft().
Nostr signing is powerful and dangerous. A signer that is always-on, shared, or remotely reachable needs explicit access control.
fleetgner exists to make delegated signing:
- Remote — users connect over relays
- Auditable — access decisions are explicit: user, key, grant, policies
- Composable — policies can be added and extended over time
- Simple — the prototype favors a small RPC surface with predictable semantics
- Transport: ContextVM uses Nostr as the transport for MCP-style JSON-RPC messages. The server is started in
index.ts. - CVM tool layer: tools are registered in
createFleetgnerMcpServer(). - NIP-46 gateway: NIP-46 connections are managed by
Nip46Gateway. - Authorization: signing is allowed only when the caller is an enabled user, the target key is enabled, the grant is enabled, and all enabled policies accept the draft in
SigningCore. - Persistence: state lives in SQLite tables defined in
src/db/schema.ts.
The prototype is intentionally built around a few simple concepts.
A user is a caller pubkey that may be allowed to use one or more signing keys.
- Managed through
"user_set"and"user_remove" - Admin is configured separately in
server_configand surfaced in user views asisAdmin - User labels are surfaced in
"user_list"and"user_lookup"
A key is a signing identity imported into the server keyring.
- Created through
"key_import" - Metadata updated through
"key_update"
A grant binds one user to one key.
- Managed through
"grant_set"and"grant_remove" - Grants are enabled by default unless explicitly disabled via the
disableinput allow_unrestrictedis false by default
This means a newly enabled zero-policy grant is rejected unless the admin explicitly opts in to unrestricted access.
Policies are optional restrictions attached to a grant.
- Managed through
"policy_set"and"policy_remove" - Built-ins are listed by
"policy_list"and implemented insrc/policy/registry.ts
Important semantics:
- If a grant has one or more policies,
allow_unrestrictedbecomes irrelevant for effective authorization - If a grant has zero policies, unrestricted signing is only allowed when
allow_unrestrictedwas explicitly set
Connections are transport/session metadata for NIP-46 access.
- Created through
"connection_create_bunker_uri"or"connection_add_nostrconnect_uri" - Enforced through
Nip46Gateway.ensureConnectionAllowed()
Connections are not the authorization model. They are just the communication layer on top of users, keys, grants, and policies.
The recommended onboarding flow for this prototype is:
- Create or update the user with
"user_set" - Import or choose a signing key with
"key_import"/"key_update" - Create a grant with
"grant_set" - Add one or more policies with
"policy_set", unless unrestricted access is truly intended - Create a connection if the user needs NIP-46 access
All mutation tools use disable for boolean state changes. Omit it to keep the current/default enabled state.
Safe-by-default behavior is one of the main prototype design goals.
For "grant_set":
disabledefaults to omitted, which means the grant stays enabled by defaultallow_unrestricteddefaults tofalse- a restricted zero-policy grant can be created first, before policies are added
- signing remains blocked until at least one policy exists, unless
allow_unrestricted: true
This keeps onboarding natural while still preventing accidental full-access grants.
Lists visible users.
- Admin sees all users
- The configured admin always sees itself in the list, even if it was not created as a user row
- Non-admin sees only self
- By default returns users with labels, grants, and policies
- Use
include_connections: trueto also include connection metadata
Admin-only detailed lookup for one user.
- Returns a full user record including label, grant metadata, and connections
- Intended for targeted inspection without listing every user in detail
Admin-only listing of imported signing keys.
NIP-46 connections are restored at startup by restoreNip46Connections().
The current prototype also tracks restore failures in nip46_connections.restore_failure_count.
Connections are stopped when their backing authorization state is no longer valid, and restarted when authorization becomes valid again, including after user/key/grant/policy state changes.
Invalid authorization states include:
- disabled users
- disabled keys
- disabled or removed grants
- policy changes that alter the effective signing rules
This behavior is implemented in Nip46Gateway.
The first prototype ships with a small built-in set in BUILTIN_POLICIES:
allow_kindsdeny_kindsexpires_atmax_content_lengthrequire_tag
These are intentionally simple and meant to cover the initial safety/ergonomics tradeoffs.
bun installSet environment variables used by main():
FLEETGNER_ADMIN_PUBKEY— admin pubkey allowed to manage users, grants, policies, and keysFLEETGNER_SERVER_NSEC_OR_HEX— server identity key for the CVM transportFLEETGNER_RELAYS— comma-separated relay URLs, defaultwss://relay.contextvm.orgFLEETGNER_DB_PATH— SQLite file path, defaultfleetgner.sqlite
bun run index.tsUseful commands from the repository root:
bun test
bun run build
bun run formatThe current automated coverage focuses on repositories, bootstrap behavior, and signing core behavior in test/fleetgner-core.test.ts and test/bootstrap.test.ts.
This repository is currently a first prototype.
That means:
- schema changes are still allowed
- clean refactors are preferred over compatibility workarounds
- UX consistency and safety semantics are being actively refined
The main product goal for this iteration is to keep the implementation tight, simple, and straightforward while proving the delegated signing model end to end.