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.

plugin_loader
Issues token. Calls issue_plugin_token(name, permissions, timeout_sec) → writes row to plugin_tokens with TTL = hook timeout + 30 s.
plugin_loader
Injects env vars. Sets FUROSHIKI_PLUGIN_API_TOKEN and FUROSHIKI_PLUGIN_API_URL. Explicitly removes FUROSHIKI_API_TOKEN (admin key) from the subprocess environment.
plugin
Sends requests. Every request includes Authorization: Bearer <token>. The SDK handles this automatically.
API
Validates token. Looks up the token in plugin_tokens, checks expires_at > now(), and extracts the declared permissions. Write endpoints enforce the required permission flag.
plugin_loader
Revokes token. After the subprocess exits (or times out), revoke_plugin_token(token) deletes the row. Expired tokens are also pruned on the next issue.
Never use 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:

text
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

GET /api/v1/plugin/state any token

Returns the current Furoshiki state snapshot: mood, emotions, needs, user needs, last conversation time, and mind queue depth.

python · SDK
state = client.get_state()
json · 200 response
{
  "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

GET /api/v1/plugin/emotion-history any token

Returns emotion_history rows for the last N days, ordered chronologically.

ParameterTypeDefaultRangeDescription
daysint71–30Days of history to return
python · SDK
h = client.get_emotion_history(days=3)
json · 200
{
  "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

GET /api/v1/plugin/recent-memories any token

Returns the most recent session memory summaries from the ChromaDB memories collection, sorted by date descending.

ParameterTypeDefaultRangeDescription
limitint51–20Max summaries to return
python · SDK
m = client.get_recent_memories(limit=3)
json · 200
{
  "limit": 3, "count": 3,
  "memories": [{
    "date": "2026-04-10",
    "source": "session_log",
    "text": "Talked about weekend plans..."
  }]
}

GET /user-facts

GET /api/v1/plugin/user-facts read_user_facts: true

Semantic search over the ChromaDB user_facts collection using vector similarity.

ParameterTypeDefaultNotes
query requiredstringSearch query, 1–500 chars
nint8Max results (1–20)
min_similarityfloat0.18Minimum cosine similarity (0–1)
python · SDK
facts = client.get_user_facts(
    "sleep schedule",
    n=5,
    min_similarity=0.3,
)
json · 200
{
  "query": "sleep schedule",
  "count": 2,
  "results": [{
    "text": "Prefers to sleep by 11pm",
    "category": "preference",
    "score": 0.72
  }]
}

GET /budget-summary

GET /api/v1/plugin/budget-summary any token

Today's LLM spend vs configured daily and monthly caps. Use before calling /llm to pre-check budget.

python · SDK
b = client.get_budget_summary()
json · 200
{
  "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

POST /api/v1/plugin/llm llm: true

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.

FieldTypeDefaultNotes
prompt requiredstringMax 32 000 chars
systemstring""System prompt, max 8 000 chars
modelstringdefaultShortcut, tier name, or OpenRouter id
max_tokensint5121–4096
sourcestringplugin:<name>Cost-tracking source label
python · SDK
text = client.call_llm(
    "Summarise: " + data,
    system="Be concise.",
    max_tokens=128,
)
json · 200
{
  "ok": true,
  "text": "Job finished; uptime 2h 15m."
}

// 429 — budget cap exceeded
{"detail": "Daily LLM budget exceeded"}

POST /mind-queue

POST /api/v1/plugin/mind-queue output_mind_queue: true

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.

FieldTypeDefaultNotes
kind requiredstringnote · reminder · vibe · observation · question · plugin
summary requiredstringShort display text, max 500 chars
detailstringnullExtended context, max 2 000 chars
dedupe_key requiredstringPrevents duplicate inserts, max 200 chars
prioritystringnormallow · normal · high · urgent
python · SDK
client.enqueue(
    kind="note",
    summary="Long sync still running (3h)",
    dedupe_key="sync:3h:2026-04-10",
    priority="normal",
)
json · 200
{
  "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:

json · stdout NDJSON
{"type":"mind_queue_item","kind":"note","summary":"...","dedupe_key":"...","priority":"normal"}

POST /log

POST /api/v1/plugin/log any token

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.

FieldTypeDefaultNotes
levelstringinfodebug · info · warning · error
message requiredstringMax 2 000 chars
contextobjectnullAny JSON object; surfaced in dashboard
python · SDK
client.log(
    "info",
    "Plugin finished",
    context={"ms": 142},
)
json · 200
{"ok": true}

Error codes

StatusMeaningCommon cause
401UnauthorizedMissing Bearer token, expired token, or token not in plugin_tokens
403ForbiddenToken is valid but the required permission flag (llm, output_mind_queue, etc.) is not declared in PLUGIN.md
422Validation errorMissing required field, value out of range, or invalid enum value
429Rate limitedDaily or monthly LLM budget cap exceeded (/llm only)
500Server errorUnexpected error in the API handler — check repair-api logs
0 (SDK)Connection errorBrain 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.

pythonscripts/furoshiki_plugin_sdk.py
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