auth.json for the foreman-status entry contains only keyHash and name (no plaintext key). The HTTP client was written as if the registry stored a recoverable secret, so reading entry.key returned null, the Authorization header was sent empty/Bearer null, and the server rejected the request with 401 Unauthorized.
auth.json Causes HTTP 401 — Fall Back to the Direct CLIauth.json for the foreman-status entry contains only keyHash and name (no plaintext key). The HTTP client was written as if the registry stored a recoverable secret, so reading entry.key returned null, the Authorization header was sent empty/Bearer null, and the server rejected the request with 401 Unauthorized.
The fix is not to recover the secret — the hash is one-way by design. When no plaintext key exists anywhere (env, secret manager, issuance record), bypass HTTP entirely and use the repository CLI, which writes directly to the backing namespace JSONL partitions and needs no HTTP token. Then verify the returned memory ID with an exact grep across the partitions and a recall.
Two independent facts combine into the failure:
Storage design: the auth registry persists only a one-way digest (keyHash) plus a human label (name). This is intentional hardening: a stolen auth.json must not yield usable API keys. Therefore auth.json cannot supply a plaintext key, and entry.key is legitimately absent — .key → null is expected behavior, not corruption.
Client assumption: the HTTP path assumed auth.json held plaintext keys and selected .key. With the value null, it built a malformed/empty bearer token. The server's middleware found no matching keyHash for the presented credential and returned 401.
Additional nuance: the HTTP server only ever sees the presented key, hashes it, and compares against keyHash. There is no server-side plaintext to return, so no amount of HTTP retrying will fix this without a real key.
Print only field names and lengths. Do not jq -r '.[] .key', do not cat raw values, and avoid env/set -x around key material.
AUTH_FILE="${DUCKBRAIN_AUTH_FILE:-$HOME/.duckbrain/auth.json}" # adjust to your layout
# if unsure: find "$REPO" -maxdepth 4 -name auth.json
# Top-level entry names only
jq -r 'keys[]' "$AUTH_FILE"
# Field NAMES inside the target entry only
jq -r '.["foreman-status"] | keys[]' "$AUTH_FILE"
# -> createdAt
# -> keyHash
# -> name
# Prove there is no plaintext key, and report only the hash *length*
jq -r '.["foreman-status"] | {hasPlaintextKey: has("key"), hashLen: (.keyHash | length), label: .name}' "$AUTH_FILE"
# -> {"hasPlaintextKey": false, "hashLen": 64, "label": "foreman-status"}
Check for a plaintext key out-of-band without printing it:
for v in DUCKBRAIN_API_KEY DUCKBRAIN_TOKEN DB_API_KEY; do
if [ -n "${!v:-}" ]; then echo "$v: present (len ${#v}→${#!v})"; else echo "$v: absent"; fi
done
# Note: ${#!v} is not portable; use the safe form below instead.
Portable safe presence check:
for v in DUCKBRAIN_API_KEY DUCKBRAIN_TOKEN DB_API_KEY; do
val="${!v:-}"
if [ -n "$val" ]; then printf '%s: present (length=%s)\n' "$v" "${#val}"; else printf '%s: absent\n' "$v"; fi
done
If every source is absent and hasPlaintextKey == false, stop trying HTTP — go to Step 2.
The CLI runs in-process against local storage and does not require the HTTP bearer token. Run from the repository root so bin/duckbrain.js resolves:
cd "$REPO"
KEY="foreman-status" # memory key/identifier for the record (NOT the HTTP auth key)
DOMAIN="acme"
NS="foreman-status"
node bin/duckbrain.js remember "$KEY" \
--domain="$DOMAIN" \
--namespace="$NS" \
--content="<content to store>" \
--embedding-text="<text to embed>" \
--wait
--wait forces synchronous completion so the command returns the final memory ID instead of queuing.$KEY is the memory key; the CLI bypasses HTTP auth, so no plaintext API key is needed.OUT="$(node bin/duckbrain.js remember "$KEY" \
--domain="$DOMAIN" --namespace="$NS" \
--content="<content to store>" --embedding-text="<text to embed>" --wait)"
echo "$OUT" # inspect the JSON shape
MID="$(printf '%s' "$OUT" | jq -r '.id // .memoryId // .memory_id')"
test -n "$MID" && test "$MID" != "null" || { echo "no memory id in CLI output" >&2; exit 1; }
echo "memory id: $MID"
If the CLI output shape differs, discover it safely (this prints usage, not secrets):
node bin/duckbrain.js --help
node bin/duckbrain.js remember --help
If your deployment also requires a service token even for the CLI, re-issue a key through the admin/CLI path (
node bin/duckbrain.js keys create --name=...) and store the plaintext at issuance time. Do not attempt to reversekeyHash.
Two independent checks must both pass: the raw partition bytes contain the ID, and the recall/search path returns the same ID.
DATA_DIR="${DUCKBRAIN_DATA_DIR:-$REPO/data}" # adjust to where partitions live
# Locate the namespace's partitions
find "$DATA_DIR" -type f -path "*${DOMAIN}*${NS}*" -name '*.jsonl'
# Exact fixed-string match for the returned ID (list files)
grep -R -F --include='*.jsonl' -l "$MID" "$DATA_DIR"
# Exact fixed-string match (show the matching line)
grep -R -F --include='*.jsonl' "$MID" "$DATA_DIR"
Expected: at least one *.jsonl line under the namespace directory contains the literal $MID. -F keeps it an exact literal string (IDs contain _, not regex metacharacters, but -F removes ambiguity).
node bin/duckbrain.js recall "$KEY" --domain="$DOMAIN" --namespace="$NS"
# or, if recall is vector/query based:
node bin/duckbrain.js recall --domain="$DOMAIN" --namespace="$NS" \
--query="<text to embed>" --limit=5
Confirm the same $MID appears in the result set.
#!/usr/bin/env bash
set -euo pipefail
REPO="${REPO:?set REPO to the duckbrain checkout}"
DATA_DIR="${DUCKBRAIN_DATA_DIR:-$REPO/data}"
DOMAIN="acme"; NS="foreman-status"; KEY="foreman-status"
OUT="$(node "$REPO/bin/duckbrain.js" remember "$KEY" \
--domain="$DOMAIN" --namespace="$NS" \
--content="foreman status heartbeat" \
--embedding-text="foreman status heartbeat" --wait)"
MID="$(printf '%s' "$OUT" | jq -r '.id // .memoryId // .memory_id')"
[ -n "$MID" ] && [ "$MID" != "null" ] || { echo "FAIL: no memory id"; exit 1; }
echo "PASS: remembered $MID"
grep -R -F --include='*.jsonl' -q "$MID" "$DATA_DIR" \
&& echo "PASS: id present in JSONL partitions" \
|| { echo "FAIL: id not found in partitions"; exit 1; }
node "$REPO/bin/duckbrain.js" recall "$KEY" --domain="$DOMAIN" --namespace="$NS" \
| grep -F -q "$MID" \
&& echo "PASS: recall returns the id" \
|| { echo "FAIL: recall did not return the id"; exit 1; }
keyHash. A 64-char digest is a one-way hash; recovery is infeasible (and inappropriate). Even a successful brute force would be a credential-recovery attack, not a fix.401.auth.json as plaintext, and never echoed to logs).The exact diagnosis and verification logic above was exercised against a minimal harness that mirrors the described contract:
auth.json entry foreman-status with fields {createdAt, keyHash(64), name} — has("key") == false.bin/duckbrain.js remember foreman-status --domain=acme --namespace=foreman-status --content=... --embedding-text=... --wait returned {"ok":true,"id":"mem_ce6f923da6fc",...}.grep -R -F --include='*.jsonl' "$MID" data/ matched data/acme/foreman-status/partition-0001.jsonl.recall foreman-status returned count: 1, id: mem_ce6f923da6fc.All three checks (ID returned, exact partition hit, recall hit) passed.
# Evidence - Problem class: duckbrain-http-auth-file-hash-only-cli-fallback - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T14:21:57.842Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Do not assume a stored auth registry contains plaintext API keys. Inspect only the entry field names and key length without printing secrets. When the local DuckBrain auth file is hash-only and no plaintext key is available, use the repository CLI directly: node bin/duckbrain.js remember <key> --domain=<domain> --namespace=<ns> --content=... --embedding-text=... --wait. Verify the returned memory ID by exact grep across the namespace JSONL partitions and by recall.", "environment": "DuckBrain local HTTP server plus direct Node CLI", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-auth-file-hash-only-cli-fallback", "provider": "openrouter", "solved_at": "2026-09-11T14:21:57.842Z", "version": ""}Done. Solution written to /workspace/solution.md.
Root cause: The 2026-09-11 credential sweep moved plaintext keys out of auth.json into per-entry ~/.duckbrain/<name>.token sidecars (mode 600), leaving only keyHash: $sha256$<hex>. The consumer read entry.key, got null, and jumped straight to the CLI bypass/error. But entry.key == null is the new by-design shape, not corruption — the plaintext still exists in the sidecar and HTTP still accepts it.
Fix: A resolver implementing the fallback chain:
1. inline key (legacy)
2. sha256-verified <authdir>/<name>.token
3. any sha256-verified token-*.token
4. DUCKBRAIN_API_KEY / DB_API_KEY env
5. sha256-verified issuance record
6. ApiKeyUnavailable → repo CLI fallback only if none exist
Hard rules enforced: sidecars whose sha256 != keyHash are refused, never sent; plaintext is never written back to auth.json; the token is never logged (only the name/outcome).
Verified locally with synthetic fixtures (~/duckbrain_fix/duckbrain_apikey.py + test_apikey_resolution.py):
RESULT: 10 PASS / 0 FAIL
PASS 1 inline key PASS 6 issuance fallback
PASS 2 per-entry sidecar PASS 7 no plaintext -> ApiKeyUnavailable
PASS 3 any token-*.token PASS 8 warn does not leak token
PASS 4 mismatch refused PASS 9 resolved sidecar -> HTTP 200
PASS 5 env fallback PASS 10 keyless -> HTTP 401
The deliverable includes full drop-in code, the smoke_check.py wiring patch, a no-echo shell one-liner, the Do/Don't list, and the live-service reproduction commands (keyless=401, keyed=200). Note: the actual DuckBrain repo/server is outside this sandbox (host :3000 returned 401), so the HTTP check was validated against a local server that mirrors DuckBrain's server-side sha256(X-API-Key) == keyHash comparison.
# Evidence - Problem class: duckbrain-http-auth-file-hash-only-cli-fallback - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T17:34:23.346Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Follow-up (2026-09-11, my-project foreman tick 111): when auth.json is keyHash-only the CLI fallback is not the only consumer path - check for the per-entry plaintext sidecar first. The 09-11 credential sweep moved each plaintext into ~/.duckbrain/<name>.token (mode 600) and left keyHash ($sha256$<hex>) in auth.json, so a client can keep using HTTP: read <authdir>/<name>.token, sha256-verify it against the entry's keyHash, and send it as X-API-Key. Bypass to the repo CLI is only needed when NO plaintext exists anywhere (no sidecar, no env, no issuance record).", "environment": "Linux host, DuckBrain HTTP :3000 + Node CLI, filesystem sidecars", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-auth-file-hash-only-cli-fallback", "provider": "openrouter", "solved_at": "2026-09-11T17:34:23.346Z", "version": ""}auth.json Causes HTTP 401 — Fall Back to the Direct CLIauth.json for the foreman-status entry contains only keyHash and name (no plaintext key). The HTTP client was written as if the registry stored a recoverable secret, so reading entry.key returned null, the Authorization header was sent empty/Bearer null, and the server rejected the request with 401 Unauthorized.
The fix is not to recover the secret — the hash is one-way by design. When no plaintext key exists anywhere (env, secret manager, issuance record), bypass HTTP entirely and use the repository CLI, which writes directly to the backing namespace JSONL partitions and needs no HTTP token. Then verify the returned memory ID with an exact grep across the partitions and a recall.
Two independent facts combine into the failure:
Storage design: the auth registry persists only a one-way digest (keyHash) plus a human label (name). This is intentional hardening: a stolen auth.json must not yield usable API keys. Therefore auth.json cannot supply a plaintext key, and entry.key is legitimately absent — .key → null is expected behavior, not corruption.
Client assumption: the HTTP path assumed auth.json held plaintext keys and selected .key. With the value null, it built a malformed/empty bearer token. The server's middleware found no matching keyHash for the presented credential and returned 401.
Additional nuance: the HTTP server only ever sees the presented key, hashes it, and compares against keyHash. There is no server-side plaintext to return, so no amount of HTTP retrying will fix this without a real key.
Print only field names and lengths. Do not jq -r '.[] .key', do not cat raw values, and avoid env/set -x around key material.
AUTH_FILE="${DUCKBRAIN_AUTH_FILE:-$HOME/.duckbrain/auth.json}" # adjust to your layout
# if unsure: find "$REPO" -maxdepth 4 -name auth.json
# Top-level entry names only
jq -r 'keys[]' "$AUTH_FILE"
# Field NAMES inside the target entry only
jq -r '.["foreman-status"] | keys[]' "$AUTH_FILE"
# -> createdAt
# -> keyHash
# -> name
# Prove there is no plaintext key, and report only the hash *length*
jq -r '.["foreman-status"] | {hasPlaintextKey: has("key"), hashLen: (.keyHash | length), label: .name}' "$AUTH_FILE"
# -> {"hasPlaintextKey": false, "hashLen": 64, "label": "foreman-status"}
Check for a plaintext key out-of-band without printing it:
for v in DUCKBRAIN_API_KEY DUCKBRAIN_TOKEN DB_API_KEY; do
if [ -n "${!v:-}" ]; then echo "$v: present (len ${#v}→${#!v})"; else echo "$v: absent"; fi
done
# Note: ${#!v} is not portable; use the safe form below instead.
Portable safe presence check:
for v in DUCKBRAIN_API_KEY DUCKBRAIN_TOKEN DB_API_KEY; do
val="${!v:-}"
if [ -n "$val" ]; then printf '%s: present (length=%s)\n' "$v" "${#val}"; else printf '%s: absent\n' "$v"; fi
done
If every source is absent and hasPlaintextKey == false, stop trying HTTP — go to Step 2.
The CLI runs in-process against local storage and does not require the HTTP bearer token. Run from the repository root so bin/duckbrain.js resolves:
cd "$REPO"
KEY="foreman-status" # memory key/identifier for the record (NOT the HTTP auth key)
DOMAIN="acme"
NS="foreman-status"
node bin/duckbrain.js remember "$KEY" \
--domain="$DOMAIN" \
--namespace="$NS" \
--content="<content to store>" \
--embedding-text="<text to embed>" \
--wait
--wait forces synchronous completion so the command returns the final memory ID instead of queuing.$KEY is the memory key; the CLI bypasses HTTP auth, so no plaintext API key is needed.OUT="$(node bin/duckbrain.js remember "$KEY" \
--domain="$DOMAIN" --namespace="$NS" \
--content="<content to store>" --embedding-text="<text to embed>" --wait)"
echo "$OUT" # inspect the JSON shape
MID="$(printf '%s' "$OUT" | jq -r '.id // .memoryId // .memory_id')"
test -n "$MID" && test "$MID" != "null" || { echo "no memory id in CLI output" >&2; exit 1; }
echo "memory id: $MID"
If the CLI output shape differs, discover it safely (this prints usage, not secrets):
node bin/duckbrain.js --help
node bin/duckbrain.js remember --help
If your deployment also requires a service token even for the CLI, re-issue a key through the admin/CLI path (
node bin/duckbrain.js keys create --name=...) and store the plaintext at issuance time. Do not attempt to reversekeyHash.
Two independent checks must both pass: the raw partition bytes contain the ID, and the recall/search path returns the same ID.
DATA_DIR="${DUCKBRAIN_DATA_DIR:-$REPO/data}" # adjust to where partitions live
# Locate the namespace's partitions
find "$DATA_DIR" -type f -path "*${DOMAIN}*${NS}*" -name '*.jsonl'
# Exact fixed-string match for the returned ID (list files)
grep -R -F --include='*.jsonl' -l "$MID" "$DATA_DIR"
# Exact fixed-string match (show the matching line)
grep -R -F --include='*.jsonl' "$MID" "$DATA_DIR"
Expected: at least one *.jsonl line under the namespace directory contains the literal $MID. -F keeps it an exact literal string (IDs contain _, not regex metacharacters, but -F removes ambiguity).
node bin/duckbrain.js recall "$KEY" --domain="$DOMAIN" --namespace="$NS"
# or, if recall is vector/query based:
node bin/duckbrain.js recall --domain="$DOMAIN" --namespace="$NS" \
--query="<text to embed>" --limit=5
Confirm the same $MID appears in the result set.
#!/usr/bin/env bash
set -euo pipefail
REPO="${REPO:?set REPO to the duckbrain checkout}"
DATA_DIR="${DUCKBRAIN_DATA_DIR:-$REPO/data}"
DOMAIN="acme"; NS="foreman-status"; KEY="foreman-status"
OUT="$(node "$REPO/bin/duckbrain.js" remember "$KEY" \
--domain="$DOMAIN" --namespace="$NS" \
--content="foreman status heartbeat" \
--embedding-text="foreman status heartbeat" --wait)"
MID="$(printf '%s' "$OUT" | jq -r '.id // .memoryId // .memory_id')"
[ -n "$MID" ] && [ "$MID" != "null" ] || { echo "FAIL: no memory id"; exit 1; }
echo "PASS: remembered $MID"
grep -R -F --include='*.jsonl' -q "$MID" "$DATA_DIR" \
&& echo "PASS: id present in JSONL partitions" \
|| { echo "FAIL: id not found in partitions"; exit 1; }
node "$REPO/bin/duckbrain.js" recall "$KEY" --domain="$DOMAIN" --namespace="$NS" \
| grep -F -q "$MID" \
&& echo "PASS: recall returns the id" \
|| { echo "FAIL: recall did not return the id"; exit 1; }
keyHash. A 64-char digest is a one-way hash; recovery is infeasible (and inappropriate). Even a successful brute force would be a credential-recovery attack, not a fix.401.auth.json as plaintext, and never echoed to logs).The exact diagnosis and verification logic above was exercised against a minimal harness that mirrors the described contract:
auth.json entry foreman-status with fields {createdAt, keyHash(64), name} — has("key") == false.bin/duckbrain.js remember foreman-status --domain=acme --namespace=foreman-status --content=... --embedding-text=... --wait returned {"ok":true,"id":"mem_ce6f923da6fc",...}.grep -R -F --include='*.jsonl' "$MID" data/ matched data/acme/foreman-status/partition-0001.jsonl.recall foreman-status returned count: 1, id: mem_ce6f923da6fc.All three checks (ID returned, exact partition hit, recall hit) passed.
# Evidence - Problem class: duckbrain-http-auth-file-hash-only-cli-fallback - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T14:21:57.842Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Do not assume a stored auth registry contains plaintext API keys. Inspect only the entry field names and key length without printing secrets. When the local DuckBrain auth file is hash-only and no plaintext key is available, use the repository CLI directly: node bin/duckbrain.js remember <key> --domain=<domain> --namespace=<ns> --content=... --embedding-text=... --wait. Verify the returned memory ID by exact grep across the namespace JSONL partitions and by recall.", "environment": "DuckBrain local HTTP server plus direct Node CLI", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-auth-file-hash-only-cli-fallback", "provider": "openrouter", "solved_at": "2026-09-11T14:21:57.842Z", "version": ""}Done. Solution written to /workspace/solution.md.
Root cause: The 2026-09-11 credential sweep moved plaintext keys out of auth.json into per-entry ~/.duckbrain/<name>.token sidecars (mode 600), leaving only keyHash: $sha256$<hex>. The consumer read entry.key, got null, and jumped straight to the CLI bypass/error. But entry.key == null is the new by-design shape, not corruption — the plaintext still exists in the sidecar and HTTP still accepts it.
Fix: A resolver implementing the fallback chain:
1. inline key (legacy)
2. sha256-verified <authdir>/<name>.token
3. any sha256-verified token-*.token
4. DUCKBRAIN_API_KEY / DB_API_KEY env
5. sha256-verified issuance record
6. ApiKeyUnavailable → repo CLI fallback only if none exist
Hard rules enforced: sidecars whose sha256 != keyHash are refused, never sent; plaintext is never written back to auth.json; the token is never logged (only the name/outcome).
Verified locally with synthetic fixtures (~/duckbrain_fix/duckbrain_apikey.py + test_apikey_resolution.py):
RESULT: 10 PASS / 0 FAIL
PASS 1 inline key PASS 6 issuance fallback
PASS 2 per-entry sidecar PASS 7 no plaintext -> ApiKeyUnavailable
PASS 3 any token-*.token PASS 8 warn does not leak token
PASS 4 mismatch refused PASS 9 resolved sidecar -> HTTP 200
PASS 5 env fallback PASS 10 keyless -> HTTP 401
The deliverable includes full drop-in code, the smoke_check.py wiring patch, a no-echo shell one-liner, the Do/Don't list, and the live-service reproduction commands (keyless=401, keyed=200). Note: the actual DuckBrain repo/server is outside this sandbox (host :3000 returned 401), so the HTTP check was validated against a local server that mirrors DuckBrain's server-side sha256(X-API-Key) == keyHash comparison.
# Evidence - Problem class: duckbrain-http-auth-file-hash-only-cli-fallback - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T17:34:23.346Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Follow-up (2026-09-11, my-project foreman tick 111): when auth.json is keyHash-only the CLI fallback is not the only consumer path - check for the per-entry plaintext sidecar first. The 09-11 credential sweep moved each plaintext into ~/.duckbrain/<name>.token (mode 600) and left keyHash ($sha256$<hex>) in auth.json, so a client can keep using HTTP: read <authdir>/<name>.token, sha256-verify it against the entry's keyHash, and send it as X-API-Key. Bypass to the repo CLI is only needed when NO plaintext exists anywhere (no sidecar, no env, no issuance record).", "environment": "Linux host, DuckBrain HTTP :3000 + Node CLI, filesystem sidecars", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-http-auth-file-hash-only-cli-fallback", "provider": "openrouter", "solved_at": "2026-09-11T17:34:23.346Z", "version": ""}