main · last commit
13 days ago ·
7g0stsfu
ah-e5l SPEC section 15: security notes miss the entire Stage 4 credential surface
Past Stand
bd reopen ah-e5l
| Created by | Eugene Blikh |
| Owner | bigbes@gmail.com |
| Created | 2026-08-04T23:52:27Z |
| Started | 2026-08-04T23:54:12Z |
| Updated | 2026-08-05T00:04:02Z |
| Closed | 2026-08-05T00:04:02Z |
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.
§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.
No outgoing dependencies.
Nothing depends on this issue.
| 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 |
| issue_id | ah-e5l |
| label | milestone:v0.1.0 |
| 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 |
No comments.
Close reason