Scoped token auth
Plugin endpoints use per-invocation Bearer tokens, not the operator admin key. Tokens are issued by plugin_loader before each hook, stored in the plugin_tokens table, and revoked after the hook exits.
issue_plugin_token(name, permissions, timeout_sec) → writes row to plugin_tokens with TTL = hook timeout + 30 s.FUROSHIKI_PLUGIN_API_TOKEN and FUROSHIKI_PLUGIN_API_URL. Explicitly removes FUROSHIKI_API_TOKEN (admin key) from the subprocess environment.Authorization: Bearer <token>. The SDK handles this automatically.plugin_tokens, checks expires_at > now(), and extracts the declared permissions. Write endpoints enforce the required permission flag.revoke_plugin_token(token) deletes the row. Expired tokens are also pruned on the next issue.FUROSHIKI_API_TOKEN in plugin code. That is the operator admin key and is explicitly removed from plugin subprocess environments. Your only credential is FUROSHIKI_PLUGIN_API_TOKEN.
Base URL
All endpoints share the same base URL, injected as FUROSHIKI_PLUGIN_API_URL:
http://127.0.0.1:7432/api/v1/plugin
The port defaults to 7432 (the repair API port). Override with FUROSHIKI_PLUGIN_API_PORT or set the full URL in FUROSHIKI_PLUGIN_API_URL. If Brain is not running, all requests will fail with a connection error — handle this gracefully and degrade.
GET /state
Returns the current Furoshiki state snapshot: mood, emotions, needs, user needs, last conversation time, and mind queue depth.
state = client.get_state()
{
"mood_score": 0.72,
"emotions": {
"joy": 0.6, "loneliness": 0.2
},
"needs": {"connection": 0.5},
"user_needs": {},
"last_conversation_at": "2026-04-10T08:00Z",
"mind_queue_depth": 3
}
GET /emotion-history
Returns emotion_history rows for the last N days, ordered chronologically.
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
days | int | 7 | 1–30 | Days of history to return |
h = client.get_emotion_history(days=3)
{
"days": 3, "count": 18,
"rows": [{
"recorded_at": "2026-04-10T08:00:00",
"emotions": {"joy": 0.6},
"mood_score": 0.72,
"restlessness": 0.3,
"emotional_intensity": 0.5,
"emotional_coherence": 0.8
}]
}
GET /recent-memories
Returns the most recent session memory summaries from the ChromaDB memories collection, sorted by date descending.
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
limit | int | 5 | 1–20 | Max summaries to return |
m = client.get_recent_memories(limit=3)
{
"limit": 3, "count": 3,
"memories": [{
"date": "2026-04-10",
"source": "session_log",
"text": "Talked about weekend plans..."
}]
}
GET /user-facts
Semantic search over the ChromaDB user_facts collection using vector similarity.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query required | string | — | Search query, 1–500 chars |
n | int | 8 | Max results (1–20) |
min_similarity | float | 0.18 | Minimum cosine similarity (0–1) |
facts = client.get_user_facts( "sleep schedule", n=5, min_similarity=0.3, )
{
"query": "sleep schedule",
"count": 2,
"results": [{
"text": "Prefers to sleep by 11pm",
"category": "preference",
"score": 0.72
}]
}
GET /budget-summary
Today's LLM spend vs configured daily and monthly caps. Use before calling /llm to pre-check budget.
b = client.get_budget_summary()
{
"daily_cap_usd": 1.50,
"monthly_cap_usd": 30.00,
"alert_pct": 80.0,
"daily_spend_usd": 0.23,
"monthly_spend_usd": 4.10
}
POST /llm
Submit a prompt to the LLM via Furoshiki's budget-enforced infrastructure. The call is tagged with source plugin:<name> and counted against the daily/monthly cap.
| Field | Type | Default | Notes |
|---|---|---|---|
prompt required | string | — | Max 32 000 chars |
system | string | "" | System prompt, max 8 000 chars |
model | string | default | Shortcut, tier name, or OpenRouter id |
max_tokens | int | 512 | 1–4096 |
source | string | plugin:<name> | Cost-tracking source label |
text = client.call_llm( "Summarise: " + data, system="Be concise.", max_tokens=128, )
{
"ok": true,
"text": "Job finished; uptime 2h 15m."
}
// 429 — budget cap exceeded
{"detail": "Daily LLM budget exceeded"}
POST /mind-queue
Insert one item into mind_queue. Deduplicated by dedupe_key — if a row with the same key already exists, the insert is silently skipped and inserted: false is returned. Items reach the user via voice_dispatcher when the delivery gate opens.
| Field | Type | Default | Notes |
|---|---|---|---|
kind required | string | — | note · reminder · vibe · observation · question · plugin |
summary required | string | — | Short display text, max 500 chars |
detail | string | null | Extended context, max 2 000 chars |
dedupe_key required | string | — | Prevents duplicate inserts, max 200 chars |
priority | string | normal | low · normal · high · urgent |
client.enqueue( kind="note", summary="Long sync still running (3h)", dedupe_key="sync:3h:2026-04-10", priority="normal", )
{
"ok": true,
"inserted": true,
"dedupe_key": "sync:3h:2026-04-10"
}
// already exists (not an error)
{"ok": true, "inserted": false, ...}
Stdout alternative (cron hooks)
Cron hooks may also emit NDJSON lines to stdout instead of calling the API. plugin_loader mediates them after the subprocess exits:
{"type":"mind_queue_item","kind":"note","summary":"...","dedupe_key":"...","priority":"normal"}
POST /log
Write a structured log line to log_store under source plugin:<name>. Visible in the dashboard Logs tab under the plugin's source. Log failures are silently ignored so they never break plugin logic.
| Field | Type | Default | Notes |
|---|---|---|---|
level | string | info | debug · info · warning · error |
message required | string | — | Max 2 000 chars |
context | object | null | Any JSON object; surfaced in dashboard |
client.log( "info", "Plugin finished", context={"ms": 142}, )
{"ok": true}
Error codes
| Status | Meaning | Common cause |
|---|---|---|
401 | Unauthorized | Missing Bearer token, expired token, or token not in plugin_tokens |
403 | Forbidden | Token is valid but the required permission flag (llm, output_mind_queue, etc.) is not declared in PLUGIN.md |
422 | Validation error | Missing required field, value out of range, or invalid enum value |
429 | Rate limited | Daily or monthly LLM budget cap exceeded (/llm only) |
500 | Server error | Unexpected error in the API handler — check repair-api logs |
0 (SDK) | Connection error | Brain not started, wrong port, or network unreachable |
SDK quick reference
The furoshiki_plugin_sdk module is always on PYTHONPATH for plugin subprocesses. Import it directly — no pip install.
from furoshiki_plugin_sdk import PluginClient, PluginAPIError # Constructor — reads env vars automatically client = PluginClient( base_url=None, # defaults to FUROSHIKI_PLUGIN_API_URL token=None, # defaults to FUROSHIKI_PLUGIN_API_TOKEN timeout=15.0, # request timeout in seconds ) # Read (no special permission) client.get_state() → dict client.get_emotion_history(days=7) → dict client.get_recent_memories(limit=5) → dict client.get_budget_summary() → dict # Read (read_user_facts: true) client.get_user_facts(query, n=8, min_similarity=0.18) → dict # Write (llm: true) client.call_llm(prompt, system="", model=None, max_tokens=512) → str # Write (output_mind_queue: true) client.enqueue(kind, summary, dedupe_key, detail="", priority="normal") → dict # Write (any token) client.log(level, message, context=None) → None (silenced on error) # Stdout alternative for cron hooks (no HTTP call) PluginClient.emit_mind_queue_item(kind, summary, dedupe_key, ...) → None