Add session grant buttons to Telegram approval keyboard (fixes #230) - #231
Add session grant buttons to Telegram approval keyboard (fixes #230)#231yablokolabs wants to merge 2 commits into
Conversation
db9cd95 to
cbaca21
Compare
The TUI approval prompt supports granting a category (s) or shell command shape (a) for the remainder of the session, but the Telegram inline keyboard only offered Approve (y) and Deny (n). This forced Telegram users to re-approve every command individually even for repeated safe operations. Add two new inline keyboard rows when applicable: - "Grant category for session" (s) — uses existing canGrantCategory() - "Grant <shape> for session" (a) — uses existing canGrantShape() The ApprovalGate already supports grant scopes via recordGrant(); this change simply wires the bridge callbacks to pass them through resolve(). Fixes AtomicBot-ai#230
cbaca21 to
f3abe05
Compare
|
Reviewed at I verified the security posture against the real Four things before merge — one of them worth fixing: 1. The toast claims a grant the gate refused (
const grantLabel = grant === "category" ? " (category granted for session)" : grant === "shape" ? " (shape granted for session)" : "";Two reproducible cases where the operator is told a standing exception exists when it does not:
No privilege is gained, so this is not a security hole — but it is a false statement about the trust state, on the surface whose whole job is telling the operator what they just authorized. 2. Grant kinds are not re-validated at decision time ( The TUI ( The bridge can't call those helpers right now because it doesn't retain the request — 3. The exact-match keyboard test never covers the shape row (
4. Unbounded
|
The grant rows shipped in AtomicBot-ai#231 built their confirmation toast from the button that was pressed, not from what the gate did with it. Two cases said "granted for session" when nothing was recorded: a `trust_config` request answered with `s` (recordGrant refuses that category outright), and a shell request with no commandShape answered with `a` (the shape button is deliberately withheld there, so the payload can only arrive hand-made). No privilege was gained either way — but the toast is the one surface whose whole job is telling the operator what they just authorised, so a false standing exception there is worse than a plain "Approved". PendingState now retains the ApprovalRequest the keyboard was built from, which fixes the label and the missing re-check together: - The scope is re-validated against that request before it reaches the gate, the way app-key-bindings.ts and run-agent.ts already do. The gate's own recordGrant filter is solid, but it was the only check on this path; a kind that no longer applies now degrades to a plain approval instead of riding on that filter. - The toast names the category or shape from the request itself, so "Approved (shell command granted for session)" is a statement about recorded state rather than about which button was pressed. Also bounds commandShape at 32 chars in the button label. Real shapes are argv[0] basenames, but nothing upstream enforces that, and an oversize reply_markup is rejected by Telegram — which dispatch turns into an auto-deny of an approval the operator never saw. Tests: the keyboard exact-match now covers a request that carries a commandShape, so the row order and every callback_data are pinned — previously a shape row emitting `:s` would have passed green. Three new cases drive the bridge against a real ApprovalGate rather than the recording stub, because only the real gate can show whether the toast and the trust state agree. AGENTS.md said "Telegram/ApprovalBridge never grants" and called it a threat-model decision: grants belonged to surfaces with physical machine access. AtomicBot-ai#230 reverses that and the doc had not caught up. It now records what changed (a remote chat can raise trust for one session) and what did not (no trust_config, no hardline bypass, no move of the standing level, still in-memory and session-scoped).
|
Thanks @sosidudku1 — all four addressed in 8b72e7b, plus one thing the review did not cover that I think outranks the rest. 1 + 2 (toast honesty, missing re-check). Fixed together, as you suggested: One deviation from the literal review text worth calling out: labelling from the request meant the wording changed rather than just becoming conditional. It now names the thing granted — 3 (keyboard coverage). The exact-match test now runs on a request that carries a 4 (unbounded shape). Tests also gained three cases that drive the bridge against a real
The thing I would not merge without a decision on: this PR reverses a documented threat-model carve-out, and #230 reads as though it were an oversight.
and in locked invariant 9: "Grants are TUI/CLI-local (never from Telegram)." So the bridge did not "just never wire the extra buttons" — the rule was that grants require physical access to the host, the same rule that keeps For what it is worth I think the feature is defensible — the alternative escape was raising the standing level to 5, which is more permissive and durable, so the grant is the smaller hammer — and the hard limits all survive: no I have updated all three places in |
Addresses the four points @sosidudku1 raised reviewing AtomicBot-ai#231 at f3abe05. The grant rows shipped in AtomicBot-ai#231 built their confirmation toast from the button that was pressed, not from what the gate did with it. @sosidudku1 found two cases that said "granted for session" when nothing was recorded: a `trust_config` request answered with `s` (recordGrant refuses that category outright), and a shell request with no commandShape answered with `a` (the shape button is deliberately withheld there, so the payload can only arrive hand-made). No privilege was gained either way — but the toast is the one surface whose whole job is telling the operator what they just authorised, so a false standing exception there is worse than a plain "Approved". PendingState now retains the ApprovalRequest the keyboard was built from, which fixes the label and the missing re-check together, as @sosidudku1 suggested: - The scope is re-validated against that request before it reaches the gate, the way app-key-bindings.ts and run-agent.ts already do. The gate's own recordGrant filter is solid, but it was the only check on this path; a kind that no longer applies now degrades to a plain approval instead of riding on that filter. - The toast names the category or shape from the request itself, so "Approved (shell command granted for session)" is a statement about recorded state rather than about which button was pressed. This changes the wording rather than only gating it, so both answerCallbackQuery expectations move with it. Also bounds commandShape at 32 chars in the button label. Real shapes are argv[0] basenames, but nothing upstream enforces that, and an oversize reply_markup is rejected by Telegram — which dispatch turns into an auto-deny of an approval the operator never saw. Tests: the keyboard exact-match now covers a request that carries a commandShape, so the row order and every callback_data are pinned — previously a shape row emitting `:s` would have passed green, exactly as @sosidudku1 called out. Three new cases drive the bridge against a real ApprovalGate rather than the recording stub, because only the real gate can show whether the toast and the trust state agree. AGENTS.md said "Telegram/ApprovalBridge never grants" and called it a threat-model decision: grants belonged to surfaces with physical machine access. AtomicBot-ai#230 reverses that and the doc had not caught up. It now records what changed (a remote chat can raise trust for one session) and what did not (no trust_config, no hardline bypass, no move of the standing level, still in-memory and session-scoped). Reviewed-by: @sosidudku1
8b72e7b to
446cdb2
Compare
Thanks @sosidudku1 for the review at f3abe05 — all four points addressed. The grant rows shipped in AtomicBot-ai#231 built their confirmation toast from the button that was pressed, not from what the gate did with it. @sosidudku1 found two cases that said "granted for session" when nothing was recorded: a `trust_config` request answered with `s` (recordGrant refuses that category outright), and a shell request with no commandShape answered with `a` (the shape button is deliberately withheld there, so the payload can only arrive hand-made). No privilege was gained either way — but the toast is the one surface whose whole job is telling the operator what they just authorised, so a false standing exception there is worse than a plain "Approved". PendingState now retains the ApprovalRequest the keyboard was built from, which fixes the label and the missing re-check together, as @sosidudku1 suggested: - The scope is re-validated against that request before it reaches the gate, the way app-key-bindings.ts and run-agent.ts already do. The gate's own recordGrant filter is solid, but it was the only check on this path; a kind that no longer applies now degrades to a plain approval instead of riding on that filter. - The toast names the category or shape from the request itself, so "Approved (shell command granted for session)" is a statement about recorded state rather than about which button was pressed. This changes the wording rather than only gating it, so both answerCallbackQuery expectations move with it. Also bounds commandShape at 32 chars in the button label. Real shapes are argv[0] basenames, but nothing upstream enforces that, and an oversize reply_markup is rejected by Telegram — which dispatch turns into an auto-deny of an approval the operator never saw. Tests: the keyboard exact-match now covers a request that carries a commandShape, so the row order and every callback_data are pinned — previously a shape row emitting `:s` would have passed green, exactly as @sosidudku1 called out. Three new cases drive the bridge against a real ApprovalGate rather than the recording stub, because only the real gate can show whether the toast and the trust state agree. AGENTS.md said "Telegram/ApprovalBridge never grants" and called it a threat-model decision: grants belonged to surfaces with physical machine access. AtomicBot-ai#230 reverses that and the doc had not caught up. It now records what changed (a remote chat can raise trust for one session) and what did not (no trust_config, no hardline bypass, no move of the standing level, still in-memory and session-scoped). Reviewed-by: @sosidudku1
446cdb2 to
c27698c
Compare
The grant rows shipped in AtomicBot-ai#231 built their confirmation toast from the button that was pressed, not from what the gate did with it. Two cases said "granted for session" when nothing was recorded: a `trust_config` request answered with `s` (recordGrant refuses that category outright), and a shell request with no commandShape answered with `a` (the shape button is deliberately withheld there, so the payload can only arrive hand-made). No privilege was gained either way — but the toast is the one surface whose whole job is telling the operator what they just authorised, so a false standing exception there is worse than a plain "Approved". PendingState now retains the ApprovalRequest the keyboard was built from, which fixes the label and the missing re-check together: - The scope is re-validated against that request before it reaches the gate, the way app-key-bindings.ts and run-agent.ts already do. The gate's own recordGrant filter is solid, but it was the only check on this path; a kind that no longer applies now degrades to a plain approval instead of riding on that filter. - The toast names the category or shape from the request itself, so "Approved (shell command granted for session)" is a statement about recorded state rather than about which button was pressed. Also bounds commandShape at 32 chars in the button label. Real shapes are argv[0] basenames, but nothing upstream enforces that, and an oversize reply_markup is rejected by Telegram — which dispatch turns into an auto-deny of an approval the operator never saw. Tests: the keyboard exact-match now covers a request that carries a commandShape, so the row order and every callback_data are pinned — previously a shape row emitting `:s` would have passed green. Three new cases drive the bridge against a real ApprovalGate rather than the recording stub, because only the real gate can show whether the toast and the trust state agree. AGENTS.md said "Telegram/ApprovalBridge never grants" and called it a threat-model decision: grants belonged to surfaces with physical machine access. AtomicBot-ai#230 reverses that and the doc had not caught up. It now records what changed (a remote chat can raise trust for one session) and what did not (no trust_config, no hardline bypass, no move of the standing level, still in-memory and session-scoped).
c27698c to
60b9206
Compare
The approval toast was built from the button that was pressed, not from what the gate did with it, so two cases claimed "granted for session" when nothing was recorded: a trust_config request answered with `s`, which recordGrant refuses outright, and a shell request with no commandShape answered with `a`. No privilege was gained, but the toast is the surface whose whole job is telling the operator what they just authorised. PendingState now retains the ApprovalRequest the keyboard was built from. The scope is re-validated against it before it reaches the gate, as app-key-bindings.ts and run-agent.ts already do, and the toast names the category or shape from the request itself. commandShape is bounded at 32 chars in the button label. Real shapes are argv[0] basenames, but nothing upstream enforces that, and an oversize reply_markup is rejected by Telegram — which dispatch turns into an auto-deny of an approval nobody saw. The keyboard exact-match test now covers a request carrying a commandShape, pinning row order and every callback_data. Three new cases drive the bridge against a real ApprovalGate instead of the recording stub, which accepts any decision it is handed. AGENTS.md said Telegram never grants and called it a threat-model decision. It now records what changed — a remote chat can raise trust for one session — and what did not: no trust_config, no hardline bypass, no move of the standing level.
60b9206 to
ca2fa1f
Compare
The TUI approval prompt supports granting a category (
s) or shell command shape (a) for the remainder of the session, but the Telegram inline keyboard only offered Approve (y) and Deny (n). This forced Telegram users to re-approve every command individually even for repeated safe operations.Changes
buildKeyboard()now accepts the fullApprovalRequestand conditionally adds:srow ("Grant category for session") whencanGrantCategory()returns truearow ("Grant<shape>for session") whencanGrantShape()returns truehandleCallback()now parsess/acallback kinds and passes the matchinggrantscope throughApprovalGate.resolve()Impact
recordGrant,canGrantCategory,canGrantShape) — no new logic, just new buttons reaching existing codeappr:<id>:s/appr:<id>:a)Closes #230