Tell an owner what their keyless agent does through the API d4a0043fded4
A grant token's API calls were not marked as an agent's, so an issue or comment an agent made that way looked like its owner's own doing and the owner was not told of it. The request's agent is now the grant's agent, as it is for an agent-marked token (#183). Refs #253
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key c0dea1e00658
- When
- · 2026-10-18 05:59 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
d4a0043fded4b0533c2f0377373bce57e38279a2b3935cdc62be08123505630b- Depends on
- 205d8f863e5c 42fa95540468 8aa5ef384ef1 bff9106edcba d23464c0ac60
- Depended on by
- nothing yet
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 4 atoms, 0 tree operations, 2 files
Browse the tree at this change
modified crates/hub/src/lib.rs +9 −2
| … | 231 unchanged lines |
||
| 232 | 232 | .get(axum::http::header::AUTHORIZATION) |
|
| 233 | 233 | .and_then(|v| v.to_str().ok()) |
|
| 234 | 234 | .and_then(|v| v.strip_prefix("Bearer ")) |
|
| 235 | - | .map(|t| auth::token_hash(t.trim())); |
|
| 235 | + | .map(|t| t.trim().to_string()); |
|
| 236 | 236 | let agent = match token { |
|
| 237 | - | Some(hash) => db::accounts::token_by_hash(&app.db, &hash).await.ok().flatten().filter(|t| t.agent).map(|t| t.name), |
|
| 237 | + | // A keyless agent's grant token (#253) is always an agent's. |
|
| 238 | + | Some(t) if t.starts_with(keyless::GRANT_TOKEN_PREFIX) => db::accounts::agent_grant_by_token(&app.db, &auth::token_hash(&t)) |
|
| 239 | + | .await |
|
| 240 | + | .ok() |
|
| 241 | + | .flatten() |
|
| 242 | + | .filter(|g| g.live(db::now_ms())) |
|
| 243 | + | .map(|g| g.agent), |
|
| 244 | + | Some(t) => db::accounts::token_by_hash(&app.db, &auth::token_hash(&t)).await.ok().flatten().filter(|t| t.agent).map(|t| t.name), |
|
| 238 | 245 | None => None, |
|
| 239 | 246 | }; |
|
| 240 | 247 | notify::AGENT.scope(agent, next.run(req)).await |
|
| … | 104 unchanged lines |
modified crates/hub/tests/flow.rs +13 −0
| … | 5010 unchanged lines |
||
| 5011 | 5011 | assert!(st == StatusCode::OK && me.contains("\"andy\""), "{st} {me}"); |
|
| 5012 | 5012 | let (st, _) = call(&h.app, "GET", "/api/v1/agents/grants", Some(&token), None).await; |
|
| 5013 | 5013 | assert_eq!(st, StatusCode::FORBIDDEN, "an agent does not manage grants"); |
|
| 5014 | + | // What the agent does through the API is told to its owner, as an |
|
| 5015 | + | // agent token's is (#183). |
|
| 5016 | + | let repo = db::repos::get(&h.app.db, "andy", "demo").await.unwrap().unwrap(); |
|
| 5017 | + | db::notify::set_watch(&h.app.db, andy.id, repo.id, true).await.unwrap(); |
|
| 5018 | + | let body = serde_json::json!({ "title": "Filed by the agent", "body": "" }).to_string(); |
|
| 5019 | + | let (st, out) = call(&h.app, "POST", "/api/v1/repos/andy/demo/issues", Some(&token), Some(body)).await; |
|
| 5020 | + | assert_eq!(st, StatusCode::CREATED, "{out}"); |
|
| 5021 | + | let actor: String = sqlx::query_scalar("SELECT actor FROM notifications WHERE account_id = $1 AND title LIKE '%Filed by the agent%'") |
|
| 5022 | + | .bind(andy.id) |
|
| 5023 | + | .fetch_one(&h.app.db) |
|
| 5024 | + | .await |
|
| 5025 | + | .expect("told of the agent's issue"); |
|
| 5026 | + | assert!(actor.starts_with("andy (agent token "), "{actor}"); |
|
| 5014 | 5027 | ||
| 5015 | 5028 | // The agent: a home holding only the grant token, Andy the author. |
|
| 5016 | 5029 | let agent_home = work.path().join("home-claude"); |
|
| … | 50 unchanged lines |