~bigbes/agents-dev · parade

main · last commit 13 days ago · 7g0stsfu

← Back to the parade

ah-e5l SPEC section 15: security notes miss the entire Stage 4 credential surface Past Stand

status: closed P3 chore milestone:v0.1.0
bd reopen ah-e5l
Created byEugene Blikh
Ownerbigbes@gmail.com
Created2026-08-04T23:52:27Z
Started2026-08-04T23:54:12Z
Updated2026-08-05T00:04:02Z
Closed2026-08-05T00:04:02Z
Description
Found during the ah-efe sweep; outside that epic's assigned sections.

§15 is still headed 'Security notes (Stages 1-2)' and covers none of what Stage 3-4 added:
- the per-task bearer tokens (256-bit, bound to (task id, attempt), in-memory registry only, constant-time compare, uniform 401 that echoes nothing, dropped on daemon restart);
- .task/tool-auth.json at mode 0600 — the file that puts a live credential inside the agent's own worktree;
- the Yonote bot token, and the token-in-URL vs token-in-header distinction between Telegram and ntfy that the redaction rules depend on.

§9 and §11 now both POINT AT §15 for the 0600 rationale after the ah-efe refresh, so the gap is a dangling reference rather than merely an omission.
Acceptance criteria
§15 is re-headed for the delivered stages and documents the bearer-token lifetime and blast radius, the 0600 tool-auth file, and every credential in the §1 services table with how each is transported and redacted. The §9 and §11 cross-references resolve to real text.

Depends on

No outgoing dependencies.

Depended on by

Nothing depends on this issue.

No comments.

Close reason

Done in 6b7bacf. §15 was headed 'Stages 1-2' and described a credential surface predating the entire Stage 3-4 story; §9's 'it carries a bearer token (§15)' pointed into text that said nothing about bearer tokens. §15 is now headed 'Stages 1-4' and opens with an EXPLICIT THREAT MODEL — daemon, zellij server, ahub-run, pi and worktrees are all one unix user, loopback is an assumption not a boundary — so the rest reads as 'don't write secrets down' rather than as containment. It sweeps every credential in §1's services table as a list with transport and redaction: Vikunja token (Bearer header; errors carry method/path/status/body-prefix), webhook secret (inbound HMAC, constant-time, uniform 401), LiteLLM key (never reaches the daemon), Telegram (token IN THE URL, redactURLError unwraps the *url.Error), ntfy (token in the HEADER so the URL is safe, plus the NO_PROXY carve-out), mem0 X-API-Key, the Yonote BOT token (Bearer header, APIError never carries headers, bot identity owns every published doc and Q&A reply), sourcehut SSH (daemon never handles key material), and AgentsView (no credential). Dedicated paragraphs document the per-task bearer token — 32 bytes of crypto/rand hex-encoded, bound to one (task id, attempt), mint idempotent per attempt, invalidate at finalize, in-memory-only registry so a restart drops every in-flight token, subtle.ConstantTimeCompare with no early break, uniform 401 that echoes nothing, blast radius of exactly three calls on one task — and .task/tool-auth.json at 0600: the one live credential inside the agent's own worktree, defense in depth rather than a boundary, attempt-scoped and cleared so tools_api:false leaves no stale credential and tool calls fail closed, git-excluded, but carried into ArchiveWorktree's owner-only tarball with an already-invalidated token. Also added: the three routes that remain unauthenticated on purpose. ONE CLAIM HAD TO BE CORRECTED RATHER THAN EXTENDED — the old 'the pi process inherits only the env it needs' is FALSE (nothing sets cmd.Env anywhere), so every token the daemon expands is readable from inside the agent's pane. The reality is now written down and the fix is filed as a P1 bead. Also reported, not fixed: internal/store MarkQAAnswered's docstring claims it makes delivery once-only but the SQL has no 'AND answered_at IS NULL' — the property actually comes from the caller's guard in reconcile/qa.go:385; and internal/httpapi/tools.go:178 puts err.Error() into the 500 body, the one place the tools API echoes internal detail, inconsistent with the constant-body discipline the same file applies to auth failures.
  • Eugene Blikh created the issue · 2026-08-05T02:52:27Z
  • Eugene Blikh changed status to in_progress · 2026-08-05T02:54:12Z
  • Eugene Blikh closed the issue · 2026-08-05T03:04:02Z
    Done in 6b7bacf. §15 was headed 'Stages 1-2' and described a credential surface predating the entire Stage 3-4 story; §9's 'it carries a bearer token (§15)' pointed into text that said nothing about bearer tokens. §15 is now headed 'Stages 1-4' and opens with an EXPLICIT THREAT MODEL — daemon, zellij server, ahub-run, pi and worktrees are all one unix user, loopback is an assumption not a boundary — so the rest reads as 'don't write secrets down' rather than as containment. It sweeps every credential in §1's services table as a list with transport and redaction: Vikunja token (Bearer header; errors carry method/path/status/body-prefix), webhook secret (inbound HMAC, constant-time, uniform 401), LiteLLM key (never reaches the daemon), Telegram (token IN THE URL, redactURLError unwraps the *url.Error), ntfy (token in the HEADER so the URL is safe, plus the NO_PROXY carve-out), mem0 X-API-Key, the Yonote BOT token (Bearer header, APIError never carries headers, bot identity owns every published doc and Q&A reply), sourcehut SSH (daemon never handles key material), and AgentsView (no credential). Dedicated paragraphs document the per-task bearer token — 32 bytes of crypto/rand hex-encoded, bound to one (task id, attempt), mint idempotent per attempt, invalidate at finalize, in-memory-only registry so a restart drops every in-flight token, subtle.ConstantTimeCompare with no early break, uniform 401 that echoes nothing, blast radius of exactly three calls on one task — and .task/tool-auth.json at 0600: the one live credential inside the agent's own worktree, defense in depth rather than a boundary, attempt-scoped and cleared so tools_api:false leaves no stale credential and tool calls fail closed, git-excluded, but carried into ArchiveWorktree's owner-only tarball with an already-invalidated token. Also added: the three routes that remain unauthenticated on purpose. ONE CLAIM HAD TO BE CORRECTED RATHER THAN EXTENDED — the old 'the pi process inherits only the env it needs' is FALSE (nothing sets cmd.Env anywhere), so every token the daemon expands is readable from inside the agent's pane. The reality is now written down and the fix is filed as a P1 bead. Also reported, not fixed: internal/store MarkQAAnswered's docstring claims it makes delivery once-only but the SQL has no 'AND answered_at IS NULL' — the property actually comes from the caller's guard in reconcile/qa.go:385; and internal/httpapi/tools.go:178 puts err.Error() into the 500 body, the one place the tools API echoes internal detail, inconsistent with the constant-body discipline the same file applies to auth failures.
  • Eugene Blikh added label milestone:v0.1.0 · 2026-08-05T03:10:39Z
Stored rows — what this pane was built from, as read
issues 1 row
id ah-e5l
content_hash df6ff8af1bf5b6da0c0ec76de99576e8339604959b0fbf074660dfe418de3d22
title SPEC section 15: security notes miss the entire Stage 4 credential surface
description Found during the ah-efe sweep; outside that epic's assigned sections. §15 is still headed 'Security notes (Stages 1-2)' and covers none of what Stage 3-4 added: - the per-task bearer tokens (256-bit, bound to (task id, attempt), in-memory registry only, constant-time compare, uniform 401 that echoes nothing, dropped on daemon restart); - .task/tool-auth.json at mode 0600 — the file that puts a live credential inside the agent's own worktree; - the Yonote bot token, and the token-in-URL vs token-in-header distinction between Telegram and ntfy that the redaction rules depend on. §9 and §11 now both POINT AT §15 for the 0600 rationale after the ah-efe refresh, so the gap is a dangling reference rather than merely an omission.
design
acceptance_criteria §15 is re-headed for the delivered stages and documents the bearer-token lifetime and blast radius, the 0600 tool-auth file, and every credential in the §1 services table with how each is transported and redacted. The §9 and §11 cross-references resolve to real text.
notes
status closed
priority 3
issue_type chore
assignee NULL
estimated_minutes NULL
created_at 2026-08-04T23:52:27Z
created_by Eugene Blikh
owner bigbes@gmail.com
updated_at 2026-08-05T00:04:02Z
closed_at 2026-08-05T00:04:02Z
closed_by_session
external_ref NULL
spec_id
compaction_level 0
compacted_at NULL
compacted_at_commit NULL
original_size NULL
sender
ephemeral 0
wisp_type
pinned 0
is_template 0
mol_type
work_type
source_system
metadata �{}
source_repo
close_reason Done in 6b7bacf. §15 was headed 'Stages 1-2' and described a credential surface predating the entire Stage 3-4 story; §9's 'it carries a bearer token (§15)' pointed into text that said nothing about bearer tokens. §15 is now headed 'Stages 1-4' and opens with an EXPLICIT THREAT MODEL — daemon, zellij server, ahub-run, pi and worktrees are all one unix user, loopback is an assumption not a boundary — so the rest reads as 'don't write secrets down' rather than as containment. It sweeps every credential in §1's services table as a list with transport and redaction: Vikunja token (Bearer header; errors carry method/path/status/body-prefix), webhook secret (inbound HMAC, constant-time, uniform 401), LiteLLM key (never reaches the daemon), Telegram (token IN THE URL, redactURLError unwraps the *url.Error), ntfy (token in the HEADER so the URL is safe, plus the NO_PROXY carve-out), mem0 X-API-Key, the Yonote BOT token (Bearer header, APIError never carries headers, bot identity owns every published doc and Q&A reply), sourcehut SSH (daemon never handles key material), and AgentsView (no credential). Dedicated paragraphs document the per-task bearer token — 32 bytes of crypto/rand hex-encoded, bound to one (task id, attempt), mint idempotent per attempt, invalidate at finalize, in-memory-only registry so a restart drops every in-flight token, subtle.ConstantTimeCompare with no early break, uniform 401 that echoes nothing, blast radius of exactly three calls on one task — and .task/tool-auth.json at 0600: the one live credential inside the agent's own worktree, defense in depth rather than a boundary, attempt-scoped and cleared so tools_api:false leaves no stale credential and tool calls fail closed, git-excluded, but carried into ArchiveWorktree's owner-only tarball with an already-invalidated token. Also added: the three routes that remain unauthenticated on purpose. ONE CLAIM HAD TO BE CORRECTED RATHER THAN EXTENDED — the old 'the pi process inherits only the env it needs' is FALSE (nothing sets cmd.Env anywhere), so every token the daemon expands is readable from inside the agent's pane. The reality is now written down and the fix is filed as a P1 bead. Also reported, not fixed: internal/store MarkQAAnswered's docstring claims it makes delivery once-only but the SQL has no 'AND answered_at IS NULL' — the property actually comes from the caller's guard in reconcile/qa.go:385; and internal/httpapi/tools.go:178 puts err.Error() into the 500 body, the one place the tools API echoes internal detail, inconsistent with the constant-body discipline the same file applies to auth failures.
event_kind
actor
target
payload
await_type
await_id
timeout_ns 0
waiters
hook_bead
role_bead
agent_state
last_activity NULL
role_type
rig
due_at NULL
defer_until NULL
no_history 0
started_at 2026-08-04T23:54:12Z
is_blocked 0
labels 1 row
issue_id ah-e5l
label milestone:v0.1.0
events 4 rows
id 019fcf31-5f72-734f-ab72-5d393767835c
issue_id ah-e5l
event_type created
actor Eugene Blikh
old_value
new_value
comment NULL
created_at 2026-08-05T02:52:27Z
id 019fcf32-f8d7-7674-9221-41dbcbb84af0
issue_id ah-e5l
event_type status_changed
actor Eugene Blikh
old_value {"id":"ah-e5l","title":"SPEC section 15: security notes miss the entire Stage 4 credential surface","description":"Found during the ah-efe sweep; outside that epic's assigned sections.\n\n§15 is still headed 'Security notes (Stages 1-2)' and covers none of what Stage 3-4 added:\n- the per-task bearer tokens (256-bit, bound to (task id, attempt), in-memory registry only, constant-time compare, uniform 401 that echoes nothing, dropped on daemon restart);\n- .task/tool-auth.json at mode 0600 — the file that puts a live credential inside the agent's own worktree;\n- the Yonote bot token, and the token-in-URL vs token-in-header distinction between Telegram and ntfy that the redaction rules depend on.\n\n§9 and §11 now both POINT AT §15 for the 0600 rationale after the ah-efe refresh, so the gap is a dangling reference rather than merely an omission.","acceptance_criteria":"§15 is re-headed for the delivered stages and documents the bearer-token lifetime and blast radius, the 0600 tool-auth file, and every credential in the §1 services table with how each is transported and redacted. The §9 and §11 cross-references resolve to real text.","status":"open","priority":3,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-08-04T23:52:27Z","created_by":"Eugene Blikh","updated_at":"2026-08-04T23:52:27Z"}
new_value {"status":"in_progress"}
comment NULL
created_at 2026-08-05T02:54:12Z
id 019fcf3b-fafe-7f07-88d8-96145d5747ec
issue_id ah-e5l
event_type closed
actor Eugene Blikh
old_value
new_value Done in 6b7bacf. §15 was headed 'Stages 1-2' and described a credential surface predating the entire Stage 3-4 story; §9's 'it carries a bearer token (§15)' pointed into text that said nothing about bearer tokens. §15 is now headed 'Stages 1-4' and opens with an EXPLICIT THREAT MODEL — daemon, zellij server, ahub-run, pi and worktrees are all one unix user, loopback is an assumption not a boundary — so the rest reads as 'don't write secrets down' rather than as containment. It sweeps every credential in §1's services table as a list with transport and redaction: Vikunja token (Bearer header; errors carry method/path/status/body-prefix), webhook secret (inbound HMAC, constant-time, uniform 401), LiteLLM key (never reaches the daemon), Telegram (token IN THE URL, redactURLError unwraps the *url.Error), ntfy (token in the HEADER so the URL is safe, plus the NO_PROXY carve-out), mem0 X-API-Key, the Yonote BOT token (Bearer header, APIError never carries headers, bot identity owns every published doc and Q&A reply), sourcehut SSH (daemon never handles key material), and AgentsView (no credential). Dedicated paragraphs document the per-task bearer token — 32 bytes of crypto/rand hex-encoded, bound to one (task id, attempt), mint idempotent per attempt, invalidate at finalize, in-memory-only registry so a restart drops every in-flight token, subtle.ConstantTimeCompare with no early break, uniform 401 that echoes nothing, blast radius of exactly three calls on one task — and .task/tool-auth.json at 0600: the one live credential inside the agent's own worktree, defense in depth rather than a boundary, attempt-scoped and cleared so tools_api:false leaves no stale credential and tool calls fail closed, git-excluded, but carried into ArchiveWorktree's owner-only tarball with an already-invalidated token. Also added: the three routes that remain unauthenticated on purpose. ONE CLAIM HAD TO BE CORRECTED RATHER THAN EXTENDED — the old 'the pi process inherits only the env it needs' is FALSE (nothing sets cmd.Env anywhere), so every token the daemon expands is readable from inside the agent's pane. The reality is now written down and the fix is filed as a P1 bead. Also reported, not fixed: internal/store MarkQAAnswered's docstring claims it makes delivery once-only but the SQL has no 'AND answered_at IS NULL' — the property actually comes from the caller's guard in reconcile/qa.go:385; and internal/httpapi/tools.go:178 puts err.Error() into the 500 body, the one place the tools API echoes internal detail, inconsistent with the constant-body discipline the same file applies to auth failures.
comment NULL
created_at 2026-08-05T03:04:02Z
id 019fcf42-0a53-7795-b895-6a8872bdb766
issue_id ah-e5l
event_type label_added
actor Eugene Blikh
old_value NULL
new_value NULL
comment Added label: milestone:v0.1.0
created_at 2026-08-05T03:10:39Z