After the keyHash-only auth cutover, DuckBrain no longer accepts (or stores) plaintext API keys in ~/.duckbrain/auth.json. The apiKeys entries now hold only a keyHash. Any request that omits the key — or that sends the stored hash / a scheduler key — is rejected with 401 Unauthorized. The caller must present the plaintext token from the matching sidecar file (~/.duckbrain/
I diagnosed the issue and wrote the solution to /workspace/solution.md. Note: this sandbox does not contain ~/.duckbrain/ or the token sidecars (only the live deployment does), so the live 201/UUID check must run where the sidecars exist; the Python solution block was syntax-verified here.
POST /api/memories — Root Cause and FixAfter the keyHash-only auth cutover, DuckBrain no longer accepts (or stores) plaintext API keys in ~/.duckbrain/auth.json. The apiKeys entries now hold only a keyHash. Any request that omits the key — or that sends the stored hash / a scheduler key — is rejected with 401 Unauthorized. The caller must present the plaintext token from the matching sidecar file (~/.duckbrain/<name>.token) in the X-API-Key header.
POST http://localhost:3000/api/memories?namespace=coding-hermes was issued with no X-API-Key, so the Express auth middleware returned 401 {"error":"Unauthorized: API key required"}.auth.json. That is no longer possible: auth.json only exposes keyHash values, which cannot be replayed as the credential (the server hashes the presented key and compares to keyHash; sending the hash itself yields 401 {"error":"Unauthorized: Invalid API key"}).API_SERVER_KEY, SCHEDULER_*) authenticate a different principal and must not be used for memory writes.~/.duckbrain/foreman-status.token (verified to authenticate successfully for foreman-status writes).Resolve the named entry from auth.json, read its plaintext sidecar without ever logging it, and send it as X-API-Key. Discovery also confirms the sidecar corresponds to the recorded keyHash.
#!/usr/bin/env python3
"""Write a DuckBrain memory using the plaintext key sidecar (keyHash-only cutover)."""
import hashlib
import json
import os
from pathlib import Path
from urllib import request, error
DUCKBRAIN_DIR = Path(os.path.expanduser("~/.duckbrain"))
AUTH_FILE = DUCKBRAIN_DIR / "auth.json"
API_BASE = os.environ.get("DUCKBRAIN_API", "http://localhost:3000")
def _entries(index: dict):
"""Yield (name, entry) from either dict-or-list shaped apiKeys."""
api_keys = index.get("apiKeys", index.get("api_keys", {}))
if isinstance(api_keys, dict):
yield from api_keys.items()
else:
for entry in api_keys:
yield entry.get("name"), entry
def _expected_hash(entry):
if isinstance(entry, dict):
return entry.get("keyHash") or entry.get("key_hash")
return entry # legacy plaintext shape
def _hash_matches(token: str, expected: str) -> bool:
"""Compare the sidecar token to the recorded keyHash."""
for algo in ("sha256", "sha512", "sha1"):
digest = hashlib.new(algo, token.encode("utf-8")).hexdigest()
if expected in (digest, f"{algo}:{digest}"):
return True
return False
def discover_api_key(preferred: str | None = "foreman-status"):
"""
Return (name, token) for a usable plaintext sidecar.
Prefer `preferred`; otherwise return the first sidecar whose hash matches
the keyHash recorded in auth.json. The token is never printed.
"""
index = json.loads(AUTH_FILE.read_text(encoding="utf-8"))
candidates = list(_entries(index))
if preferred:
candidates.sort(key=lambda item: item[0] != preferred)
for name, entry in candidates:
if not name:
continue
sidecar = DUCKBRAIN_DIR / f"{name}.token"
if not sidecar.is_file():
continue
token = sidecar.read_text(encoding="utf-8").strip()
if not token:
continue
expected = _expected_hash(entry)
if expected and not _hash_matches(token, expected):
continue # sidecar does not belong to this entry
return name, token
raise RuntimeError("no plaintext token sidecar matches auth.json")
def write_memory(namespace: str, payload: dict, preferred: str | None = "foreman-status"):
name, token = discover_api_key(preferred)
url = f"{API_BASE}/api/memories?namespace={namespace}"
body = json.dumps(payload).encode("utf-8")
req = request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-API-Key", token) # secret; never echoed
try:
with request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode("utf-8"))
print(f"POST {url} -> {resp.status} (key={name})")
return result
except error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
raise RuntimeError(f"POST {url} -> {exc.code}: {detail}") from exc
if __name__ == "__main__":
out = write_memory(
"coding-hermes",
{
"kind": "foreman-status",
"source": "hermes-agent",
"content": "foreman tick completed",
},
)
print("uuid:", out.get("uuid") or out.get("id"))
Run it:
python3 write_memory.py
# 1. Discover the intended sidecar from auth.json (no secret is printed).
NAME=$(python3 - <<'PY'
import json, os
d = os.path.expanduser("~/.duckbrain")
keys = json.load(open(f"{d}/auth.json"))["apiKeys"]
for n in keys:
if os.path.isfile(f"{d}/{n}.token"):
print(n); break
PY
)
# 2. POST with the token in a 0600 header file, not on the command line.
umask 077
printf 'X-API-Key: %s\n' "$(cat "$HOME/.duckbrain/$NAME.token")" > /tmp/db.headers
curl -sS -X POST "http://localhost:3000/api/memories?namespace=coding-hermes" \
-H @/tmp/db.headers \
-H "Content-Type: application/json" \
-d '{"kind":"foreman-status","source":"hermes-agent","content":"foreman tick completed"}'
rm -f /tmp/db.headers
keyHash from auth.json; the middleware hashes the presented value and compares it to the stored hash, so a replayed hash is an invalid key.API_SERVER_KEY from /etc/coding-hermes/gateway.env, or any SCHEDULER_* value) for memory writes.echo/print/set -x the token, commit it, or pass it on the command line where it can appear in ps/shell history.HTTP 201:POST http://localhost:3000/api/memories?namespace=coding-hermes -> 201 (key=foreman-status)
UUID=$(python3 write_memory.py | awk '/^uuid:/ {print $2}')
# Namespace partition for 2026-09
JSONL="$HOME/.duckbrain/namespaces/coding-hermes/config/2026-09/current.jsonl"
grep -q "$UUID" "$JSONL" && echo "VERIFIED: $UUID present in $JSONL"
VERIFIED: <uuid> present in /home/<user>/.duckbrain/namespaces/coding-hermes/config/2026-09/current.jsonl
~/.duckbrain/foreman-status.token) as X-API-Key — already proven to authenticate.# Evidence - Problem class: duckbrain-http-401-auth - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T17:58:41.322Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A DuckBrain POST to /api/memories returned HTTP 401 when no API key was supplied. auth.json no longer contains plaintext keys: apiKeys entries contain only keyHash. Discover the intended named key in ~/.duckbrain/auth.json, read the matching plaintext sidecar ~/.duckbrain/<name>.token without printing it, and send it as X-API-Key. For foreman status writes, ~/.duckbrain/foreman-status.token authenticated successfully. The retried POST returned 201, and the returned UUID was verified in the requested namespace partition JSONL. Never send the keyHash or scheduler API keys.", "environment": "Hermes Agent foreman tick using DuckBrain HTTP API on localhost:3000 after keyHash-only auth cutover", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-401-auth", "provider": "openrouter", "solved_at": "2026-09-12T17:58:41.322Z", "version": "live deployment 2026-09-12"}I diagnosed the issue and wrote the solution to /workspace/solution.md. Note: this sandbox does not contain ~/.duckbrain/ or the token sidecars (only the live deployment does), so the live 201/UUID check must run where the sidecars exist; the Python solution block was syntax-verified here.
POST /api/memories — Root Cause and FixAfter the keyHash-only auth cutover, DuckBrain no longer accepts (or stores) plaintext API keys in ~/.duckbrain/auth.json. The apiKeys entries now hold only a keyHash. Any request that omits the key — or that sends the stored hash / a scheduler key — is rejected with 401 Unauthorized. The caller must present the plaintext token from the matching sidecar file (~/.duckbrain/<name>.token) in the X-API-Key header.
POST http://localhost:3000/api/memories?namespace=coding-hermes was issued with no X-API-Key, so the Express auth middleware returned 401 {"error":"Unauthorized: API key required"}.auth.json. That is no longer possible: auth.json only exposes keyHash values, which cannot be replayed as the credential (the server hashes the presented key and compares to keyHash; sending the hash itself yields 401 {"error":"Unauthorized: Invalid API key"}).API_SERVER_KEY, SCHEDULER_*) authenticate a different principal and must not be used for memory writes.~/.duckbrain/foreman-status.token (verified to authenticate successfully for foreman-status writes).Resolve the named entry from auth.json, read its plaintext sidecar without ever logging it, and send it as X-API-Key. Discovery also confirms the sidecar corresponds to the recorded keyHash.
#!/usr/bin/env python3
"""Write a DuckBrain memory using the plaintext key sidecar (keyHash-only cutover)."""
import hashlib
import json
import os
from pathlib import Path
from urllib import request, error
DUCKBRAIN_DIR = Path(os.path.expanduser("~/.duckbrain"))
AUTH_FILE = DUCKBRAIN_DIR / "auth.json"
API_BASE = os.environ.get("DUCKBRAIN_API", "http://localhost:3000")
def _entries(index: dict):
"""Yield (name, entry) from either dict-or-list shaped apiKeys."""
api_keys = index.get("apiKeys", index.get("api_keys", {}))
if isinstance(api_keys, dict):
yield from api_keys.items()
else:
for entry in api_keys:
yield entry.get("name"), entry
def _expected_hash(entry):
if isinstance(entry, dict):
return entry.get("keyHash") or entry.get("key_hash")
return entry # legacy plaintext shape
def _hash_matches(token: str, expected: str) -> bool:
"""Compare the sidecar token to the recorded keyHash."""
for algo in ("sha256", "sha512", "sha1"):
digest = hashlib.new(algo, token.encode("utf-8")).hexdigest()
if expected in (digest, f"{algo}:{digest}"):
return True
return False
def discover_api_key(preferred: str | None = "foreman-status"):
"""
Return (name, token) for a usable plaintext sidecar.
Prefer `preferred`; otherwise return the first sidecar whose hash matches
the keyHash recorded in auth.json. The token is never printed.
"""
index = json.loads(AUTH_FILE.read_text(encoding="utf-8"))
candidates = list(_entries(index))
if preferred:
candidates.sort(key=lambda item: item[0] != preferred)
for name, entry in candidates:
if not name:
continue
sidecar = DUCKBRAIN_DIR / f"{name}.token"
if not sidecar.is_file():
continue
token = sidecar.read_text(encoding="utf-8").strip()
if not token:
continue
expected = _expected_hash(entry)
if expected and not _hash_matches(token, expected):
continue # sidecar does not belong to this entry
return name, token
raise RuntimeError("no plaintext token sidecar matches auth.json")
def write_memory(namespace: str, payload: dict, preferred: str | None = "foreman-status"):
name, token = discover_api_key(preferred)
url = f"{API_BASE}/api/memories?namespace={namespace}"
body = json.dumps(payload).encode("utf-8")
req = request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-API-Key", token) # secret; never echoed
try:
with request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode("utf-8"))
print(f"POST {url} -> {resp.status} (key={name})")
return result
except error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
raise RuntimeError(f"POST {url} -> {exc.code}: {detail}") from exc
if __name__ == "__main__":
out = write_memory(
"coding-hermes",
{
"kind": "foreman-status",
"source": "hermes-agent",
"content": "foreman tick completed",
},
)
print("uuid:", out.get("uuid") or out.get("id"))
Run it:
python3 write_memory.py
# 1. Discover the intended sidecar from auth.json (no secret is printed).
NAME=$(python3 - <<'PY'
import json, os
d = os.path.expanduser("~/.duckbrain")
keys = json.load(open(f"{d}/auth.json"))["apiKeys"]
for n in keys:
if os.path.isfile(f"{d}/{n}.token"):
print(n); break
PY
)
# 2. POST with the token in a 0600 header file, not on the command line.
umask 077
printf 'X-API-Key: %s\n' "$(cat "$HOME/.duckbrain/$NAME.token")" > /tmp/db.headers
curl -sS -X POST "http://localhost:3000/api/memories?namespace=coding-hermes" \
-H @/tmp/db.headers \
-H "Content-Type: application/json" \
-d '{"kind":"foreman-status","source":"hermes-agent","content":"foreman tick completed"}'
rm -f /tmp/db.headers
keyHash from auth.json; the middleware hashes the presented value and compares it to the stored hash, so a replayed hash is an invalid key.API_SERVER_KEY from /etc/coding-hermes/gateway.env, or any SCHEDULER_* value) for memory writes.echo/print/set -x the token, commit it, or pass it on the command line where it can appear in ps/shell history.HTTP 201:POST http://localhost:3000/api/memories?namespace=coding-hermes -> 201 (key=foreman-status)
UUID=$(python3 write_memory.py | awk '/^uuid:/ {print $2}')
# Namespace partition for 2026-09
JSONL="$HOME/.duckbrain/namespaces/coding-hermes/config/2026-09/current.jsonl"
grep -q "$UUID" "$JSONL" && echo "VERIFIED: $UUID present in $JSONL"
VERIFIED: <uuid> present in /home/<user>/.duckbrain/namespaces/coding-hermes/config/2026-09/current.jsonl
~/.duckbrain/foreman-status.token) as X-API-Key — already proven to authenticate.# Evidence - Problem class: duckbrain-http-401-auth - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T17:58:41.322Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A DuckBrain POST to /api/memories returned HTTP 401 when no API key was supplied. auth.json no longer contains plaintext keys: apiKeys entries contain only keyHash. Discover the intended named key in ~/.duckbrain/auth.json, read the matching plaintext sidecar ~/.duckbrain/<name>.token without printing it, and send it as X-API-Key. For foreman status writes, ~/.duckbrain/foreman-status.token authenticated successfully. The retried POST returned 201, and the returned UUID was verified in the requested namespace partition JSONL. Never send the keyHash or scheduler API keys.", "environment": "Hermes Agent foreman tick using DuckBrain HTTP API on localhost:3000 after keyHash-only auth cutover", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-401-auth", "provider": "openrouter", "solved_at": "2026-09-12T17:58:41.322Z", "version": "live deployment 2026-09-12"}