30 KiB
Browser Didactyl — A Pure-Browser Agent Runtime
Vision
Port Didactyl's agent loop to JavaScript as a standalone web page. The browser
becomes the runtime substrate — no shell, no C binary. You open the page, sign
in as the agent using the agent's Nostr private key (via
nostr-login-lite), and your agent boots: it reads its soul, skills, adoption
list, and memory from relays, listens for triggers, reasons with an LLM, and
acts through a browser-safe tool set.
This is not ai.html (a chat app for conversations) and not
sovereign_browser (a full OS browser in C/WebKitGTK). It is Didactyl's agent
semantics — triggers, two-layer context, skill adoption, tool-call loop —
running natively in a browser tab.
Why This Works
Didactyl is Nostr-first. Identity, memory, skills, soul, and communication all live as Nostr events. The only things tying it to a shell are:
- The C binary process — a browser tab replaces this
- Shell-specific tools —
local_shell_exec,local_file_read,local_file_write,local_http_fetch— these are dropped or replaced with browser equivalents
The agent loop itself is portable:
Boot → load config from Nostr → subscribe to triggers →
on trigger: build context → call LLM → execute tools → loop →
deliver response
A browser can do every step:
| Capability | Browser Implementation |
|---|---|
| Nostr relay connection | WebSocket (NDK worker or nostr-tools) |
| Event signing | NIP-07 window.nostr or n_signer |
| NIP-04/44 encrypt/decrypt | window.nostr.nip04 / window.nostr.nip44 |
| LLM calls | fetch() to OpenAI-compatible endpoint |
| Skill/soul/memory storage | Kind 31123/31124/30078/10123 events |
| Blossom blob storage | HTTP fetch() to Blossom servers |
| Tool execution | JS tool dispatch (browser-safe subset) |
Architecture
flowchart TB
subgraph BrowserTab
UI[Chat UI Page]
CORE[Agent Core JS Module]
TOOLS[Tool Dispatch JS Module]
CTX[Context Builder JS Module]
end
subgraph NostrLayer
RELAYS[Nostr Relays via WebSocket]
EVENTS[Kind 31123 Skills<br/>Kind 10123 Adoption<br/>Kind 30078 Memory/Config<br/>Kind 4/13 DMs]
end
subgraph External
LLM[OpenAI-compatible LLM API]
SIGNER[NIP-07 Signer Extension<br/>or n_signer]
BLOSSOM[Blossom Servers]
end
UI --> CORE
CORE --> CTX
CORE --> TOOLS
CORE -->|WebSocket| RELAYS
RELAYS --> EVENTS
CORE -->|fetch| LLM
CORE -->|window.nostr| SIGNER
TOOLS -->|fetch| BLOSSOM
TOOLS -->|publish events| RELAYS
Boot Sequence
sequenceDiagram
participant User
participant Page as Browser Didactyl
participant NLL as nostr-login-lite
participant Signer as window.nostr
participant Relays as Nostr Relays
User->>Page: Open page
Page->>NLL: NOSTR_LOGIN_LITE.init methods: local, extension, connect, nsigner
NLL->>NLL: Restore persisted auth or show login modal
User->>NLL: Enter agent nsec or connect signer
NLL->>NLL: Install window.nostr facade
Page->>Signer: window.nostr.getPublicKey()
Signer-->>Page: agent pubkey hex
Page->>Relays: Connect to configured relays
Page->>Relays: Subscribe: kind 10002 own pubkey
Page->>Relays: Subscribe: kind 10123 own pubkey
Page->>Relays: Subscribe: kinds 31123,31124 own pubkey
Page->>Relays: Subscribe: kind 30078 own pubkey d=memory
Page->>Relays: Subscribe: kind 30078 own pubkey d=llm_config
Page->>Relays: Subscribe: kind 4 own pubkey p-tag
Relays-->>Page: EOSE + cached events
Page->>Page: Build adoption list + skill cache
Page->>Page: Load LLM config from 30078
Page->>Page: Register trigger subscriptions
Page-->>User: Agent ready — listening for DMs and triggers
Browser-Safe Tool Set
The C agent has ~60 tools. The browser agent starts with a focused subset and can grow. Tools are grouped by what the browser can actually do.
Phase 1 — Core Nostr Tools
These are the agent's hands in a browser. All use window.nostr for signing and
the WebSocket relay pool for publishing.
| Tool | C Equivalent | Browser Implementation |
|---|---|---|
nostr_post |
nostr_post |
Build event → window.nostr.signEvent → publish via relay pool |
nostr_query |
nostr_query |
Send filter over WebSocket, collect until EOSE |
nostr_dm_send |
nostr_dm_send |
NIP-04 encrypt via window.nostr.nip04.encrypt → publish kind 4 |
nostr_dm_send_nip17 |
nostr_dm_send_nip17 |
NIP-44 encrypt → gift-wrap kind 13/14 (if signer supports nip44) |
nostr_delete |
nostr_delete |
Sign kind 5 → publish |
nostr_react |
nostr_react |
Sign kind 7 → publish |
nostr_profile_get |
nostr_profile_get |
Query kind 0 → parse metadata |
nostr_encrypt |
nostr_encrypt |
window.nostr.nip44.encrypt |
nostr_decrypt |
nostr_decrypt |
window.nostr.nip44.decrypt |
Phase 1 — Identity & Context Tools
| Tool | Browser Implementation |
|---|---|
nostr_pubkey / my_pubkey |
Return cached agent pubkey hex |
nostr_npub / my_npub |
bech32 encode via nostr-tools |
agent_identity |
Build identity context block |
nostr_relay_status |
Query relay pool connection states |
tool_list |
Return browser tool schemas |
Phase 1 — Memory & Task Tools
| Tool | C Equivalent | Browser Implementation |
|---|---|---|
memory_save |
memory_save |
NIP-44 encrypt → publish kind 30078 d=memory |
memory_recall |
memory_recall |
Fetch kind 30078 d=memory → decrypt |
task_manage |
task_manage |
Publish/fetch kind 30078 d=tasks |
task_list |
task_list |
Fetch kind 30078 d=tasks → format |
Phase 1 — Skill Tools
| Tool | Browser Implementation |
|---|---|
skill_create |
Build kind 31123/31124 → sign → publish |
skill_edit |
Fetch existing → modify → republish |
skill_list |
Query adopted skills from cache |
skill_adopt |
Update kind 10123 → sign → publish |
skill_remove |
Update kind 10123 → sign → publish |
skill_search |
Query kind 31123 by tag/author |
Phase 1 — Config & Model Tools
| Tool | Browser Implementation |
|---|---|
model_get |
Return active LLM config from 30078 or localStorage |
model_set |
Publish kind 30078 d=llm_config |
config_store |
Encrypt + publish kind 30078 |
config_recall |
Fetch + decrypt kind 30078 |
agent_version |
Return hardcoded browser version string |
Phase 2 — Blossom Tools
| Tool | Browser Implementation |
|---|---|
blossom_upload |
fetch() PUT to Blossom server with signed auth |
blossom_download |
fetch() GET from Blossom server |
blossom_head |
fetch() HEAD |
blossom_list |
fetch() GET list endpoint |
blossom_delete |
fetch() DELETE with signed auth |
Phase 2 — Browser-Native Tools
These have no C equivalent — they are browser-only capabilities:
| Tool | Description |
|---|---|
browser_fetch |
fetch() any URL — replaces local_http_fetch |
browser_tab_open |
Open a URL in a new tab |
browser_clipboard |
Read/write clipboard |
browser_local_storage |
Read/write localStorage key-value pairs |
Explicitly Excluded
These tools cannot run in a browser and are intentionally absent:
| Tool | Reason |
|---|---|
local_shell_exec |
No shell access |
local_file_read |
No filesystem (except via File System Access API, Phase 3) |
local_file_write |
Same |
cashu_wallet_* |
Could be ported later (Cashu is HTTP-based) — Phase 3 |
Context Assembly
The browser agent replicates Didactyl's two-layer context model from
docs/CONTEXT.md and docs/SKILLS.md.
Two-Layer Model
flowchart TD
TRIGGER[Trigger fires: DM, cron, subscription]
TRIGGER --> WALK[Walk adoption list 10123]
WALK --> MATCH{Skill trigger matches?}
MATCH -- yes --> L1[Add to Layer 1]
MATCH -- no --> SKIP[Skip]
L1 --> RESOLVE[Resolve template variables]
RESOLVE --> VARTYPE{Variable type?}
VARTYPE -- tool name --> TOOLEXEC[Execute tool, insert result as markdown]
VARTYPE -- skill d-tag --> SKILLLOOKUP[Insert adopted skill content - Layer 2]
VARTYPE -- unknown --> EMPTY[Resolve to empty]
TOOLEXEC --> FORMAT
SKILLLOOKUP --> FORMAT
EMPTY --> FORMAT
FORMAT[Format document: add title, bump headings, join with ---]
FORMAT --> ROLES[Split into system/user roles]
ROLES --> LLM[Send to LLM with tool schemas]
Template Variable Resolution
The C agent resolves {{variable}} placeholders in skill templates. The browser
agent does the same:
{{skill_d_tag}}→ look up adopted skill, insert content (layer 2){{tool_name}}→ execute tool, convert JSON result to markdown, insert{{message}}→ the incoming trigger payload (DM text, etc.)- Unknown → empty string
Heading Bump
Skill content headings are bumped down one level (# → ##, ## → ###)
during assembly, matching context_bump_headings()
in the C agent.
Role Split
The assembled markdown is split into system/user roles via the same logic as
context_roles_split(). The runtime owns role
construction — skill authors write plain markdown.
Trigger System
The browser agent supports a subset of Didactyl's trigger types:
| Trigger Type | Browser Support | Implementation |
|---|---|---|
dm |
Yes | Subscribe to kind 4 events with own pubkey as p-tag |
nostr-subscription |
Yes | Register WebSocket subscription with skill's filter |
cron |
Yes | setInterval / setTimeout with cron expression parser |
chain |
Yes | Internal event — fire when another skill completes |
webhook |
No | No HTTP server in browser (could use a relay-based webhook relay) |
DM Trigger Flow
sequenceDiagram
participant Admin
participant Relays as Nostr Relays
participant Page as Browser Didactyl
participant LLM as LLM API
Admin->>Relays: Publish kind 4 DM to agent
Relays->>Page: WebSocket event notification
Page->>Page: NIP-04 decrypt
Page->>Page: Dedup check event ID cache
Page->>Page: Classify sender tier admin/wot/stranger
Page->>Page: Build context from triggered skills
Page->>LLM: Chat completion with tools
LLM-->>Page: Response or tool_calls
loop tool call loop
Page->>Page: Execute tool
Page->>LLM: Feed tool result back
LLM-->>Page: Next response
end
Page->>Relays: Publish kind 4 DM reply
Relays->>Admin: Deliver reply
Cron Trigger
Cron expressions are parsed in JS and scheduled via setTimeout. The browser
tab must stay open for cron to fire (a Service Worker could keep it alive in a
future enhancement).
Sender Tier Classification
The C agent classifies senders as ADMIN / WoT / STRANGER. The browser agent replicates this:
- ADMIN — pubkey matches configured admin pubkey
- WoT — pubkey is in admin's kind 3 contact list
- STRANGER — everyone else (configurable: ignore or canned reply)
LLM Integration
Provider Config
LLM config is read from kind 30078 d:llm_config under the agent's own pubkey,
falling back to the shared d:user-settings global_llm namespace (matching
the cross-project settings sync from
sovereign_browser/plans/cross-project-agent-sync.md).
The config shape:
{
"provider": "ppq",
"api_key": "sk-...",
"model": "claude-haiku-4.5",
"base_url": "https://api.ppq.ai",
"max_tokens": 4096,
"temperature": 0.7
}
LLM Call
Uses fetch() to the OpenAI-compatible /v1/chat/completions endpoint. The
existing ai-chat.mjs and
ai-ui.mjs modules provide reusable
sendAiChatJson() and config normalization functions.
Tool-Call Loop
async function agentLoop(messages, toolsSchema, maxTurns) {
for (let turn = 0; turn < maxTurns; turn++) {
const response = await callLLM({ messages, tools: toolsSchema });
if (response.tool_calls.length === 0) {
return response.content; // final answer
}
messages.push({ role: 'assistant', tool_calls: response.tool_calls });
for (const tc of response.tool_calls) {
const result = await toolsExecute(tc.name, tc.arguments);
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
}
}
return null; // max turns exhausted
}
LLM Fallback Chain
Skill llm tags use the CSS font-stack style fallback chain
(anthropic/claude-sonnet, openai/gpt-4o-mini, cheap). The browser agent
resolves this against its configured providers, matching the C agent's
docs/SKILLS.md behavior.
Nostr Connectivity
Relay Pool
Two options:
-
NDK Worker — reuse
ndk-worker.jsfrom the client project. Provides a Web Worker with relay pool, subscription management, event dedup, and NIP-07 signer bridge. This is the fastest path — it is already battle-tested inai.htmland other client pages. -
nostr-tools — lighter weight, direct in-page. Simpler but less infrastructure.
Recommendation: NDK Worker. It already handles relay connection management,
reconnection, event dedup, and signer integration. The browser Didactyl page
would import init-ndk.mjs for the auth +
worker bootstrap, then use the worker's subscribe and publish message types.
Subscriptions on Boot
| Subscription | Filter | Purpose |
|---|---|---|
| Relay list | kind 10002, own pubkey | Discover agent's configured relays |
| Adoption list | kind 10123, own pubkey | Load adopted skills |
| Self skills | kinds 31123, 31124, own pubkey | Load skill definitions |
| Memory | kind 30078, own pubkey, d=memory | Agent long-term memory |
| Tasks | kind 30078, own pubkey, d=tasks | Agent task list |
| LLM config | kind 30078, own pubkey, d=llm_config | Provider/model config |
| DMs | kind 4, own pubkey as p-tag | Admin command channel |
| Admin context | kinds 0, 3, 10002, 1, admin pubkey | WoT + admin awareness |
Event Dedup
The C agent uses an event-ID cache + FNV-1a fingerprint debounce window. The
browser agent uses a Set<string> of seen event IDs with a periodic cleanup
(evict entries older than N minutes). NDK also has built-in dedup.
Identity & Signing
nostr-login-lite — Primary Auth Path
The page uses nostr-login-lite for
authentication and signing. This is the same login library used across the
client project. It provides a NIP-07 compliant window.nostr facade that works
with multiple signing methods:
| Method | How It Works | Use Case |
|---|---|---|
| local | User enters agent nsec; key is AES-GCM encrypted with a session password and stored in localStorage | Primary — sign in as the agent with its private key |
| extension | Delegates to a real NIP-07 browser extension (Alby, nos2x, Amber) | If the agent key is in an extension |
| connect | NIP-46 remote signer (bunker) | Agent key held by a remote signer service |
| nsigner | USB hardware signer via WebUSB/WebSerial | Agent key on n_signer hardware device |
| seedphrase | BIP-39 seed phrase → derive nsec | Agent key from a 12-word seed |
| readonly | npub only, no signing | Inspection mode — read relay data, no actions |
The key insight: you sign in as the agent, with the agent's private key.
nostr-login-lite installs window.nostr with the full NIP-07 surface:
window.nostr.getPublicKey()— returns agent pubkey hexwindow.nostr.signEvent(event)— signs any Nostr event with the agent keywindow.nostr.nip04.encrypt(pubkey, plaintext)— NIP-04 encryptwindow.nostr.nip04.decrypt(pubkey, ciphertext)— NIP-04 decryptwindow.nostr.nip44.encrypt(pubkey, plaintext)— NIP-44 encrypt (if supported)window.nostr.nip44.decrypt(pubkey, ciphertext)— NIP-44 decrypt (if supported)
All tool implementations that need to sign events, encrypt DMs, or decrypt
memory route through window.nostr — exactly like the C agent routes through
its signer. The agent's nsec never touches the agent code directly; it stays
inside nostr-login-lite's encrypted store or the extension/hardware signer.
Initialization
await window.NOSTR_LOGIN_LITE.init({
persistence: true, // Remember login across page reloads
isolateSession: false, // Share across tabs (agent is one identity)
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true, // Browser extension if available
local: true, // Manual nsec entry — primary for agent
readonly: true, // Inspection mode
seedphrase: true, // BIP-39 seed
connect: true, // NIP-46 remote signer
nsigner: true, // USB hardware signer
},
floatingTab: {
enabled: true,
appearance: { text: 'Agent Login' }
}
});
After init, nostr-login-lite either restores a persisted session or shows the
login modal. Once authenticated, window.nostr is installed and the boot
sequence continues.
Admin Identity
The admin pubkey is stored in the agent's kind 30078 d:llm_config or a
dedicated config event. On boot, the page fetches this to determine the admin
identity for DM classification.
File Layout
The page lives in the client project's www/ directory alongside the existing
pages, reusing the shared CSS, NDK worker, and JS modules.
~/lt/client/www/
├── didactyl-agent.html # NEW — the browser Didactyl page
├── css/
│ └── client.css # existing shared styles
├── js/
│ ├── init-ndk.mjs # existing — NDK worker bootstrap + auth
│ ├── ai-ui.mjs # existing — LLM config + chat completions URL
│ ├── ai-chat.mjs # existing — sendAiChatJson()
│ ├── didactyl-agent.mjs # NEW — agent core: boot, trigger dispatch, loop
│ ├── didactyl-context.mjs # NEW — two-layer context assembly
│ ├── didactyl-tools.mjs # NEW — browser-safe tool dispatch + schemas
│ ├── didactyl-skills.mjs # NEW — skill cache, adoption list, trigger matching
│ ├── didactyl-memory.mjs # NEW — memory/task persistence via kind 30078
│ └── didactyl-triggers.mjs # NEW — DM, cron, subscription trigger management
├── ndk-worker.js # existing — relay pool worker
├── nostr-login-lite/ # existing — login library (vendored or built)
└── template.html # existing — page template (auth, sidenav, theme)
Module Responsibilities
| Module | Responsibility |
|---|---|
didactyl-agent.mjs |
Boot sequence, agent loop, message dispatch, UI binding |
didactyl-context.mjs |
Two-layer context assembly, heading bump, role split, template variable resolution |
didactyl-tools.mjs |
Tool schema definitions, tool dispatch, browser-safe tool implementations |
didactyl-skills.mjs |
Skill cache, adoption list management, trigger matching |
didactyl-memory.mjs |
Memory save/recall, task management via kind 30078 |
didactyl-triggers.mjs |
DM listener, cron scheduler, subscription trigger registration |
UI Design
Layout
+------------------------------------------------------------------+
| DIDACTYL AGENT [dark/light] |
+------------------------------------------------------------------+
| Status: Connected to 3 relays · Listening for DMs and triggers |
+------------------------------------------------------------------+
| | |
| +------------------------------+ | +---------------------------+|
| | CONVERSATION / LOG | | | AGENT STATE ||
| | | | | ||
| | [admin] Fix the typo in my | | | Pubkey: npub1... ||
| | latest post | | | Admin: npub1... ||
| | | | | Relays: 3/3 connected ||
| | [agent] I found your post | | | Skills: 12 adopted ||
| | note1abc... | | | Triggers: 3 active ||
| | The typo is "recieve" | | | ||
| | → fixing now... | | | --- Trigger Log --- ||
| | | | | [cron] readme-monitor ||
| | [tool] nostr_query → 1 event | | | [dm] admin message ||
| | [tool] nostr_post → published | | | [sub] new follower ||
| | | | | ||
| | [admin] Thanks! | | | --- Skills --- ||
| | | | | [x] personality ||
| | [agent] Done! | | | [x] chat ||
| | | | | [x] readme-monitor ||
| | | | | [ ] spellcheck ||
| +------------------------------+ | +---------------------------+|
| | |
| +------------------------------+ | +---------------------------+|
| | [Message input...] [Send] | | | [Settings] ||
| +------------------------------+ | +---------------------------+|
+------------------------------------------------------------------+
Key UI Elements
- Conversation log — DM history with the admin, showing tool calls inline
- Agent state panel — pubkey, admin, relay status, skill count, trigger status
- Trigger log — recent trigger fires with type and skill name
- Skills list — adopted skills with toggle (adopts/removes from 10123)
- Message input — send a DM to the agent directly (for testing)
- Settings — LLM provider config, relay list, admin pubkey
Reusable Components
The page builds on template.html from the
client project, inheriting:
- Auth flow (NIP-07 login via
init-ndk.mjs) - Sidenav navigation
- Theme toggle (dark/light)
- Relay status indicator
- Hamburger morphing menu
Implementation Phases
Phase 1 — Minimum Viable Agent
Goal: A browser page that boots from a Nostr identity, loads its config and skills, listens for admin DMs, reasons with an LLM, and replies via DM.
- Create
didactyl-agent.htmlpage shell usingtemplate.htmlas base - Integrate
nostr-login-litefor auth: init with local/extension/connect/nsigner/seedphrase methods, sign in as agent with its nsec - Implement
didactyl-agent.mjsboot sequence: nostr-login-lite auth →window.nostr.getPublicKey()→ relay connect → subscribe to config/skills/adoption/DMs - Implement
didactyl-skills.mjs: skill cache, adoption list parsing, trigger matching fordmtrigger type - Implement
didactyl-context.mjs: two-layer context assembly, heading bump, role split,{{message}}and{{skill_d_tag}}variable resolution - Implement
didactyl-tools.mjs: core Nostr tools (nostr_post,nostr_query,nostr_dm_send,nostr_profile_get,nostr_pubkey,nostr_npub,tool_list), OpenAI-format tool schemas - Implement
didactyl-memory.mjs:memory_save,memory_recall,task_manage,task_listvia kind 30078 - Implement agent loop: build context → call LLM → execute tools → loop → reply as DM
- Implement sender tier classification (admin / WoT / stranger)
- Wire up conversation log UI + agent state panel
- LLM config from kind 30078
d:llm_configwith localStorage fallback
Phase 2 — Full Tool Set + Triggers
Goal: Complete the browser-safe tool set and add cron + subscription triggers.
- Add remaining Nostr tools:
nostr_delete,nostr_react,nostr_dm_send_nip17,nostr_encrypt,nostr_decrypt,nostr_nip05_lookup,nostr_encode,nostr_decode - Add skill tools:
skill_create,skill_edit,skill_list,skill_adopt,skill_remove,skill_search - Add config/model tools:
model_get,model_set,config_store,config_recall,agent_version - Add Blossom tools:
blossom_upload,blossom_download,blossom_head,blossom_list,blossom_delete - Add browser-native tools:
browser_fetch,browser_tab_open,browser_clipboard,browser_local_storage - Implement
didactyl-triggers.mjs: cron trigger scheduler, nostr-subscription trigger registration, chain trigger - Add trigger log UI
- Add skills management UI (adopt/remove, create/edit)
Phase 3 — Advanced
Goal: Push the boundaries of what a browser agent can do.
- Cashu wallet tools (NIP-60) — all HTTP-based, feasible in browser
- File System Access API —
local_file_read/local_file_writefor user-selected directories - Service Worker — keep cron triggers alive when tab is backgrounded
- NIP-17 gift-wrap messaging (requires nip44 signer support)
- Cross-project settings sync — read/write shared
d:user-settingskind 30078 - Broadcast-debounce admin client — fan-out DMs to multiple agents (browser + C), accept first reply (from
DECENTRALIZED_DIDACTYL.md) - Mobile responsive layout
- Offline skill caching via IndexedDB
Compatibility with C Didactyl
Shared State
The browser agent and C agent share all state via Nostr events — no direct communication is needed. Both read and write the same event kinds:
| State | Event | Shared |
|---|---|---|
| Skills | kind 31123/31124 | Yes — both publish and consume |
| Adoption list | kind 10123 | Yes |
| Memory | kind 30078 d=memory | Yes (encrypted, same key needed) |
| Tasks | kind 30078 d=tasks | Yes |
| LLM config | kind 30078 d=llm_config | Yes |
| DMs | kind 4 | Yes |
| Profile | kind 0 | Yes |
Same Keys, Different Runtimes
If the browser agent and C agent use the same nsec, they share a Nostr
identity. This has the same problems described in
DECENTRALIZED_DIDACTYL.md: both subscribe to
the same DMs, both process, both reply. The broadcast-debounce model applies.
Recommended: different keys. The browser agent is a distinct agent with its own identity. The admin talks to whichever agent is appropriate. Skills and soul can be shared via adoption lists.
Skill Portability
Skills are portable by design. A skill published by the C agent works in the browser agent and vice versa, as long as:
- The skill's
toolstag only references browser-available tools - The skill doesn't depend on shell-specific capabilities
The llm fallback chain ensures model portability across runtimes.
Security Considerations
| Concern | Mitigation |
|---|---|
| nsec in browser memory | nostr-login-lite encrypts the nsec with AES-GCM and a session password before storing in localStorage. For maximum security, use nsigner (hardware) or connect (NIP-46 remote) methods so the key never enters the browser. |
| LLM API key exposure | Key is in kind 30078 encrypted event. Browser decrypts to memory. Never logged. |
| Tool execution safety | Browser sandbox limits damage. No shell, no filesystem (Phase 1/2). |
| DM sender spoofing | Verify event signatures before processing (NDK does this). Classify sender tier. |
| Relay trust | Relays are untrusted. Event signatures verified. Encrypted DMs. |
| CORS for LLM calls | OpenAI-compatible APIs typically support CORS. If not, use a proxy. |
| Blossom auth | Signed auth events via window.nostr.signEvent. |
Reusable Infrastructure
| Asset | Location | What It Provides |
|---|---|---|
| NDK Worker | ndk-worker.js |
Relay pool, subscriptions, event dedup, publish |
| NDK Init | init-ndk.mjs |
NIP-07 auth, worker bootstrap, mute list |
| nostr-login-lite | nostr-login-lite |
NIP-07 facade: local nsec, extension, NIP-46, nsigner, seedphrase, readonly |
| LLM Config | ai-ui.mjs |
Config normalization, chat completions URL, provider merge |
| LLM Call | ai-chat.mjs |
sendAiChatJson() — fetch-based LLM call |
| Page Template | template.html |
Auth, sidenav, theme toggle, relay status |
| Shared CSS | client.css |
Theme variables, layout, components |
| Skills Demo | skills-demo.mjs |
Skill discovery, template parsing, execution |
Open Questions
-
Should the browser agent use the same relay list as the C agent (from kind 10002), or its own? Same is simpler and more sovereign. Different allows geographic diversity.
-
NIP-17 gift-wrap support depends on the signer having nip44. Not all NIP-07 extensions support this. Fallback to NIP-04 or skip NIP-17 tools if unavailable.
-
Cron triggers die when the tab closes. Is this acceptable, or do we need a Service Worker? For a true "always-on" agent, the C binary is still the right choice. The browser agent is for interactive/attended use.
-
Should we support the broadcast-debounce admin client pattern from
DECENTRALIZED_DIDACTYL.mdso the admin can fan-out to both browser and C agents? This is a Phase 3 enhancement.