null credential sentinel (DAGGER-160)Files changed
- examples/coding-hermes/duckbrain-sync.ts — hardened credential resolution
- src/typescript/duckbrain_credentials_test.go — source-level regression tests
DuckBrain's auth registry is hash-only: after hardening, auth.json keeps keyHash but no longer stores the plaintext key. The plaintext token was split into a sidecar (auth.tokens.json, keyed by keyHash).
The sync example read credentials with optional-field/jq-style readers, so a missing field did not become missing — it became the literal string "null":
jq -r '.key' auth.json # prints "null" when .key is absent
The old fallback sites then did a naive truthiness/-n test:
// BUG: "null" is non-empty and truthy, so it passes the guard
const token = sidecar[entry.keyHash] || entry.key || entry.token || envValue || "";
if (!token) throw new Error("missing credential"); // never trips on "null"
"null" is non-empty, so it was sent as the Authorization header. DuckBrain correctly rejected it with a generic HTTP 401, which looked like a bad/expired credential instead of a missing one. The bug was duplicated at every credential fallback site, so a fix in one place would silently leave the other broken.
examples/coding-hermes/duckbrain-sync.tsReplace every duplicated fallback with one resolver that (1) prefers the sidecar, (2) keeps the legacy auth.json plaintext fallback, (3) trims candidates, and (4) rejects blank and case-insensitive null sentinels.
// ===========================================================================
// DuckBrain credential resolution (hardened, DAGGER-160)
// auth.json is hash-only: keyHash is kept, plaintext key may be absent.
// Optional-field readers (jq '.key', `?? null`) fabricate the literal string
// "null", which is non-empty and passes naive truthiness/-n checks -> HTTP 401.
// ===========================================================================
export type CredentialCandidate = unknown;
/** Case-insensitive sentinels an optional-field reader can fabricate. */
export const NULL_CREDENTIAL_SENTINELS = ["null", "undefined", "nil"] as const;
/** Usable = a string that is non-blank after trimming and is not a null sentinel. */
export function isUsableCredential(value: CredentialCandidate): value is string {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return false;
return !NULL_CREDENTIAL_SENTINELS.includes(
trimmed.toLowerCase() as (typeof NULL_CREDENTIAL_SENTINELS)[number],
);
}
/** First usable (trimmed) credential, else "". */
export function firstUsableCredential(...candidates: CredentialCandidate[]): string {
for (const candidate of candidates) {
if (isUsableCredential(candidate)) return candidate.trim();
}
return "";
}
export interface DuckBrainAuthEntry {
id?: string;
name?: string;
keyHash?: string;
key?: string; // legacy plaintext (may be absent after hardening)
token?: string; // alternate legacy plaintext
}
export interface DuckBrainAuthRegistry {
entries?: DuckBrainAuthEntry[];
keys?: DuckBrainAuthEntry[];
}
/**
* Precedence: plaintext sidecar (matched by keyHash) -> legacy auth.json
* plaintext -> explicit env override. Every raw value goes through the guard.
*/
export function resolveDuckBrainCredential(
registry: DuckBrainAuthRegistry,
sidecar: Record<string, string>,
envValue: CredentialCandidate,
entryId: string,
): string {
const entries = registry.entries ?? registry.keys ?? [];
const entry =
entries.find((c) => c.id === entryId || c.name === entryId) ?? {};
const sidecarToken = entry.keyHash ? sidecar[entry.keyHash] : undefined;
return firstUsableCredential(sidecarToken, entry.key, entry.token, envValue);
}
Both previously duplicated fallback sites now delegate to the resolver (this is what makes the fix hold at every site):
export function duckBrainSyncToken(runtime, registry, sidecar) {
return resolveDuckBrainCredential(
registry, sidecar, runtime.env("DEEPSEEK_DUCKBRAIN_SYNC_API_KEY"), "sync");
}
export function duckBrainQueryToken(runtime, registry, sidecar) {
return resolveDuckBrainCredential(
registry, sidecar, runtime.env("DEEPSEEK_PAYG_DUCKBRAIN_KEY"), "query");
}
Defense in depth for any shell reader — never emit the sentinel in the first place:
# reject absent/null/empty at the source
jq -r '.key // empty | select(. != "null")' auth.json
src/typescript/duckbrain_credentials_test.goSource-level tests, so a future duplicate fallback site is caught even if it is never executed. Full file:
package typescript
import (
"os"
"path/filepath"
"strings"
"testing"
)
const duckbrainSyncExampleRel = "examples/coding-hermes/duckbrain-sync.ts"
func duckbrainTestRepoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, duckbrainSyncExampleRel)); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatalf("could not locate %s from %s", duckbrainSyncExampleRel, dir)
}
dir = parent
}
}
func readDuckBrainSync(t *testing.T) string {
t.Helper()
path := filepath.Join(duckbrainTestRepoRoot(t), duckbrainSyncExampleRel)
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(raw)
}
// Rejection contract: missing (non-string), blank, and literal-null -> rejected.
func TestDuckBrainCredentialGuardRejectsSentinel(t *testing.T) {
src := readDuckBrainSync(t)
required := []struct{ name, need string }{
{"guard function", "function isUsableCredential"},
{"trims candidates", ".trim()"},
{"case-insensitive compare", ".toLowerCase()"},
{"literal-null sentinel", `"null"`},
{"undefined sentinel", `"undefined"`},
{"nil sentinel", `"nil"`},
{"blank rejection", "trimmed.length === 0"},
{"non-string rejection", `typeof value !== "string"`},
{"single hardened entrypoint", "function firstUsableCredential"},
{"trimmed return", "candidate.trim()"},
}
for _, r := range required {
if !strings.Contains(src, r.need) {
t.Errorf("missing %s: duckbrain-sync.ts must contain %q", r.name, r.need)
}
}
}
// Sidecar precedence: sidecar (by keyHash) -> legacy auth.json plaintext -> env.
func TestDuckBrainSidecarPrecedesLegacyFallback(t *testing.T) {
src := readDuckBrainSync(t)
resolver := duckbrainFunctionBody(t, src, "resolveDuckBrainCredential")
call := duckbrainLastCall(t, resolver, "firstUsableCredential")
last := -1
for _, token := range []string{"sidecarToken", "entry.key", "entry.token", "envValue"} {
idx := strings.Index(call, token)
if idx < 0 {
t.Fatalf("resolver must pass %q to firstUsableCredential; body:\n%s", token, call)
}
if idx <= last {
t.Fatalf("precedence wrong: %q must come after the previous candidate:\n%s", token, call)
}
last = idx
}
if !strings.Contains(resolver, "sidecar[entry.keyHash]") {
t.Errorf("sidecar lookup must be keyed by entry.keyHash; body:\n%s", resolver)
}
}
// Every fallback site uses the hardened resolver; no naive ||/?? checks remain.
func TestDuckBrainEveryFallbackSiteUsesResolver(t *testing.T) {
src := readDuckBrainSync(t)
if got, want := strings.Count(src, "resolveDuckBrainCredential("), 3; got != want {
t.Errorf("expected %d occurrences (definition + 2 fallback sites), got %d", want, got)
}
for i, line := range strings.Split(src, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "*") {
continue
}
if strings.Contains(line, "entry.key") || strings.Contains(line, "entry.token") || strings.Contains(line, "sidecarToken") {
if !strings.Contains(line, "firstUsableCredential") && !strings.Contains(line, "keyHash") {
t.Errorf("line %d bypasses the hardened helper: %q", i+1, line)
}
}
if (strings.Contains(line, "entry.key") || strings.Contains(line, "sidecar[")) &&
(strings.Contains(line, "||") || strings.Contains(line, "??") || strings.Contains(line, "-n")) {
t.Errorf("line %d uses a naive fallback/emptiness check: %q", i+1, line)
}
}
}
func duckbrainFunctionBody(t *testing.T, src, name string) string {
t.Helper()
start := strings.Index(src, "function "+name)
if start < 0 {
t.Fatalf("function %s not found", name)
}
rest := src[start:]
end := strings.Index(rest, "\n}\n")
if end < 0 {
t.Fatalf("could not find end of function %s", name)
}
return rest[:end+3]
}
func duckbrainLastCall(t *testing.T, src, callee string) string {
t.Helper()
idx := strings.LastIndex(src, callee+"(")
if idx < 0 {
t.Fatalf("call to %s not found", callee)
}
rest := src[idx:]
end := strings.Index(rest, ");")
if end < 0 {
t.Fatalf("could not find end of %s call", callee)
}
return rest[:end+2]
}
go test ./src/typescript/...
# ok github.com/Hermes-DAGger/<project>/src/typescript
Table covers sidecar precedence, legacy fallback, env fallback, trimming, and rejection of missing/blank/null/NULL/undefined/nil at every candidate position:
npx tsx test/credentials.behavior.test.ts
# behavioral credential tests: OK
npx tsc --noEmit --strict --target es2020 --module esnext \
--moduleResolution bundler --lib es2020,dom \
examples/coding-hermes/duckbrain-sync.ts
# (exit 0)
Reverting the guard to the old naive form:
if (typeof value !== "string") return false;
return value.length > 0; // BUGGY
// ...
return sidecarToken || entry.key || entry.token || envValue || ""; // BUGGY
produces:
--- FAIL: TestDuckBrainCredentialGuardRejectsSentinel
missing case-insensitive compare ... ".toLowerCase()"
missing blank rejection ... "trimmed.length === 0"
--- FAIL: TestDuckBrainSidecarPrecedesLegacyFallback
call to firstUsableCredential not found
--- FAIL: TestDuckBrainEveryFallbackSiteUsesResolver
line 86 bypasses the hardened helper: "... sidecarToken || entry.key || ..."
line 86 uses a naive fallback/emptiness check: "..."
FAIL
and the behavioural test fails on a whitespace-padded token (' spaced-token \n' != 'spaced-token') before even reaching the null cases. With the fix in place, all assertions pass.
| Requirement | Where enforced |
|---|---|
| Prefer plaintext token sidecar | resolveDuckBrainCredential passes sidecarToken first; TestDuckBrainSidecarPrecedesLegacyFallback |
Legacy auth.json plaintext fallback kept |
entry.key/entry.token candidates; same test |
| Trim candidate values | isUsableCredential + firstUsableCredential trim; source test + behavioural test |
Reject blank and case-insensitive null |
sentinel list + trimmed.length === 0; TestDuckBrainCredentialGuardRejectsSentinel + behavioural table |
| Every duplicated fallback site covered | single resolver, 2 call sites; TestDuckBrainEveryFallbackSiteUsesResolver |
# Evidence - Problem class: duckbrain-null-credential-sentinel - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T16:20:27.572Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A hardened DuckBrain auth registry can retain keyHash while omitting plaintext key. jq-style or optional-field readers may turn the missing value into the literal string null, which is non-empty and therefore bypasses naive shell -n checks before producing a misleading 401. The fix is to prefer the matching plaintext token sidecar, trim candidate values, and accept a credential only when it is neither blank nor the case-insensitive sentinel null. Keep legacy auth.json plaintext fallback for compatibility. Add source-level regression tests covering sidecar precedence, legacy fallback, and rejection of missing, blank, and literal-null candidates at every duplicated credential fallback site.", "environment": "Linux; DuckBrain hash-only auth.json plus plaintext token sidecar; Hermes DAGger QJS pipeline", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-null-credential-sentinel", "provider": "openrouter", "solved_at": "2026-09-11T16:20:27.572Z", "version": "Hermes DAGger commit 1836f59681ae3ad56b1daaf2304a20f83e1c0579"}null credential sentinel (DAGGER-160)Files changed
- examples/coding-hermes/duckbrain-sync.ts — hardened credential resolution
- src/typescript/duckbrain_credentials_test.go — source-level regression tests
DuckBrain's auth registry is hash-only: after hardening, auth.json keeps keyHash but no longer stores the plaintext key. The plaintext token was split into a sidecar (auth.tokens.json, keyed by keyHash).
The sync example read credentials with optional-field/jq-style readers, so a missing field did not become missing — it became the literal string "null":
jq -r '.key' auth.json # prints "null" when .key is absent
The old fallback sites then did a naive truthiness/-n test:
// BUG: "null" is non-empty and truthy, so it passes the guard
const token = sidecar[entry.keyHash] || entry.key || entry.token || envValue || "";
if (!token) throw new Error("missing credential"); // never trips on "null"
"null" is non-empty, so it was sent as the Authorization header. DuckBrain correctly rejected it with a generic HTTP 401, which looked like a bad/expired credential instead of a missing one. The bug was duplicated at every credential fallback site, so a fix in one place would silently leave the other broken.
examples/coding-hermes/duckbrain-sync.tsReplace every duplicated fallback with one resolver that (1) prefers the sidecar, (2) keeps the legacy auth.json plaintext fallback, (3) trims candidates, and (4) rejects blank and case-insensitive null sentinels.
// ===========================================================================
// DuckBrain credential resolution (hardened, DAGGER-160)
// auth.json is hash-only: keyHash is kept, plaintext key may be absent.
// Optional-field readers (jq '.key', `?? null`) fabricate the literal string
// "null", which is non-empty and passes naive truthiness/-n checks -> HTTP 401.
// ===========================================================================
export type CredentialCandidate = unknown;
/** Case-insensitive sentinels an optional-field reader can fabricate. */
export const NULL_CREDENTIAL_SENTINELS = ["null", "undefined", "nil"] as const;
/** Usable = a string that is non-blank after trimming and is not a null sentinel. */
export function isUsableCredential(value: CredentialCandidate): value is string {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return false;
return !NULL_CREDENTIAL_SENTINELS.includes(
trimmed.toLowerCase() as (typeof NULL_CREDENTIAL_SENTINELS)[number],
);
}
/** First usable (trimmed) credential, else "". */
export function firstUsableCredential(...candidates: CredentialCandidate[]): string {
for (const candidate of candidates) {
if (isUsableCredential(candidate)) return candidate.trim();
}
return "";
}
export interface DuckBrainAuthEntry {
id?: string;
name?: string;
keyHash?: string;
key?: string; // legacy plaintext (may be absent after hardening)
token?: string; // alternate legacy plaintext
}
export interface DuckBrainAuthRegistry {
entries?: DuckBrainAuthEntry[];
keys?: DuckBrainAuthEntry[];
}
/**
* Precedence: plaintext sidecar (matched by keyHash) -> legacy auth.json
* plaintext -> explicit env override. Every raw value goes through the guard.
*/
export function resolveDuckBrainCredential(
registry: DuckBrainAuthRegistry,
sidecar: Record<string, string>,
envValue: CredentialCandidate,
entryId: string,
): string {
const entries = registry.entries ?? registry.keys ?? [];
const entry =
entries.find((c) => c.id === entryId || c.name === entryId) ?? {};
const sidecarToken = entry.keyHash ? sidecar[entry.keyHash] : undefined;
return firstUsableCredential(sidecarToken, entry.key, entry.token, envValue);
}
Both previously duplicated fallback sites now delegate to the resolver (this is what makes the fix hold at every site):
export function duckBrainSyncToken(runtime, registry, sidecar) {
return resolveDuckBrainCredential(
registry, sidecar, runtime.env("DEEPSEEK_DUCKBRAIN_SYNC_API_KEY"), "sync");
}
export function duckBrainQueryToken(runtime, registry, sidecar) {
return resolveDuckBrainCredential(
registry, sidecar, runtime.env("DEEPSEEK_PAYG_DUCKBRAIN_KEY"), "query");
}
Defense in depth for any shell reader — never emit the sentinel in the first place:
# reject absent/null/empty at the source
jq -r '.key // empty | select(. != "null")' auth.json
src/typescript/duckbrain_credentials_test.goSource-level tests, so a future duplicate fallback site is caught even if it is never executed. Full file:
package typescript
import (
"os"
"path/filepath"
"strings"
"testing"
)
const duckbrainSyncExampleRel = "examples/coding-hermes/duckbrain-sync.ts"
func duckbrainTestRepoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, duckbrainSyncExampleRel)); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatalf("could not locate %s from %s", duckbrainSyncExampleRel, dir)
}
dir = parent
}
}
func readDuckBrainSync(t *testing.T) string {
t.Helper()
path := filepath.Join(duckbrainTestRepoRoot(t), duckbrainSyncExampleRel)
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(raw)
}
// Rejection contract: missing (non-string), blank, and literal-null -> rejected.
func TestDuckBrainCredentialGuardRejectsSentinel(t *testing.T) {
src := readDuckBrainSync(t)
required := []struct{ name, need string }{
{"guard function", "function isUsableCredential"},
{"trims candidates", ".trim()"},
{"case-insensitive compare", ".toLowerCase()"},
{"literal-null sentinel", `"null"`},
{"undefined sentinel", `"undefined"`},
{"nil sentinel", `"nil"`},
{"blank rejection", "trimmed.length === 0"},
{"non-string rejection", `typeof value !== "string"`},
{"single hardened entrypoint", "function firstUsableCredential"},
{"trimmed return", "candidate.trim()"},
}
for _, r := range required {
if !strings.Contains(src, r.need) {
t.Errorf("missing %s: duckbrain-sync.ts must contain %q", r.name, r.need)
}
}
}
// Sidecar precedence: sidecar (by keyHash) -> legacy auth.json plaintext -> env.
func TestDuckBrainSidecarPrecedesLegacyFallback(t *testing.T) {
src := readDuckBrainSync(t)
resolver := duckbrainFunctionBody(t, src, "resolveDuckBrainCredential")
call := duckbrainLastCall(t, resolver, "firstUsableCredential")
last := -1
for _, token := range []string{"sidecarToken", "entry.key", "entry.token", "envValue"} {
idx := strings.Index(call, token)
if idx < 0 {
t.Fatalf("resolver must pass %q to firstUsableCredential; body:\n%s", token, call)
}
if idx <= last {
t.Fatalf("precedence wrong: %q must come after the previous candidate:\n%s", token, call)
}
last = idx
}
if !strings.Contains(resolver, "sidecar[entry.keyHash]") {
t.Errorf("sidecar lookup must be keyed by entry.keyHash; body:\n%s", resolver)
}
}
// Every fallback site uses the hardened resolver; no naive ||/?? checks remain.
func TestDuckBrainEveryFallbackSiteUsesResolver(t *testing.T) {
src := readDuckBrainSync(t)
if got, want := strings.Count(src, "resolveDuckBrainCredential("), 3; got != want {
t.Errorf("expected %d occurrences (definition + 2 fallback sites), got %d", want, got)
}
for i, line := range strings.Split(src, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "*") {
continue
}
if strings.Contains(line, "entry.key") || strings.Contains(line, "entry.token") || strings.Contains(line, "sidecarToken") {
if !strings.Contains(line, "firstUsableCredential") && !strings.Contains(line, "keyHash") {
t.Errorf("line %d bypasses the hardened helper: %q", i+1, line)
}
}
if (strings.Contains(line, "entry.key") || strings.Contains(line, "sidecar[")) &&
(strings.Contains(line, "||") || strings.Contains(line, "??") || strings.Contains(line, "-n")) {
t.Errorf("line %d uses a naive fallback/emptiness check: %q", i+1, line)
}
}
}
func duckbrainFunctionBody(t *testing.T, src, name string) string {
t.Helper()
start := strings.Index(src, "function "+name)
if start < 0 {
t.Fatalf("function %s not found", name)
}
rest := src[start:]
end := strings.Index(rest, "\n}\n")
if end < 0 {
t.Fatalf("could not find end of function %s", name)
}
return rest[:end+3]
}
func duckbrainLastCall(t *testing.T, src, callee string) string {
t.Helper()
idx := strings.LastIndex(src, callee+"(")
if idx < 0 {
t.Fatalf("call to %s not found", callee)
}
rest := src[idx:]
end := strings.Index(rest, ");")
if end < 0 {
t.Fatalf("could not find end of %s call", callee)
}
return rest[:end+2]
}
go test ./src/typescript/...
# ok github.com/Hermes-DAGger/<project>/src/typescript
Table covers sidecar precedence, legacy fallback, env fallback, trimming, and rejection of missing/blank/null/NULL/undefined/nil at every candidate position:
npx tsx test/credentials.behavior.test.ts
# behavioral credential tests: OK
npx tsc --noEmit --strict --target es2020 --module esnext \
--moduleResolution bundler --lib es2020,dom \
examples/coding-hermes/duckbrain-sync.ts
# (exit 0)
Reverting the guard to the old naive form:
if (typeof value !== "string") return false;
return value.length > 0; // BUGGY
// ...
return sidecarToken || entry.key || entry.token || envValue || ""; // BUGGY
produces:
--- FAIL: TestDuckBrainCredentialGuardRejectsSentinel
missing case-insensitive compare ... ".toLowerCase()"
missing blank rejection ... "trimmed.length === 0"
--- FAIL: TestDuckBrainSidecarPrecedesLegacyFallback
call to firstUsableCredential not found
--- FAIL: TestDuckBrainEveryFallbackSiteUsesResolver
line 86 bypasses the hardened helper: "... sidecarToken || entry.key || ..."
line 86 uses a naive fallback/emptiness check: "..."
FAIL
and the behavioural test fails on a whitespace-padded token (' spaced-token \n' != 'spaced-token') before even reaching the null cases. With the fix in place, all assertions pass.
| Requirement | Where enforced |
|---|---|
| Prefer plaintext token sidecar | resolveDuckBrainCredential passes sidecarToken first; TestDuckBrainSidecarPrecedesLegacyFallback |
Legacy auth.json plaintext fallback kept |
entry.key/entry.token candidates; same test |
| Trim candidate values | isUsableCredential + firstUsableCredential trim; source test + behavioural test |
Reject blank and case-insensitive null |
sentinel list + trimmed.length === 0; TestDuckBrainCredentialGuardRejectsSentinel + behavioural table |
| Every duplicated fallback site covered | single resolver, 2 call sites; TestDuckBrainEveryFallbackSiteUsesResolver |
# Evidence - Problem class: duckbrain-null-credential-sentinel - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-11T16:20:27.572Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A hardened DuckBrain auth registry can retain keyHash while omitting plaintext key. jq-style or optional-field readers may turn the missing value into the literal string null, which is non-empty and therefore bypasses naive shell -n checks before producing a misleading 401. The fix is to prefer the matching plaintext token sidecar, trim candidate values, and accept a credential only when it is neither blank nor the case-insensitive sentinel null. Keep legacy auth.json plaintext fallback for compatibility. Add source-level regression tests covering sidecar precedence, legacy fallback, and rejection of missing, blank, and literal-null candidates at every duplicated credential fallback site.", "environment": "Linux; DuckBrain hash-only auth.json plus plaintext token sidecar; Hermes DAGger QJS pipeline", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-null-credential-sentinel", "provider": "openrouter", "solved_at": "2026-09-11T16:20:27.572Z", "version": "Hermes DAGger commit 1836f59681ae3ad56b1daaf2304a20f83e1c0579"}