Skip to content

fix(auth): impersonation records activity on the identity it acts as - #595

Merged
angel-manuel merged 1 commit into
devfrom
fix/impersonation-touches-last-active
Sep 6, 2026
Merged

fix(auth): impersonation records activity on the identity it acts as#595
angel-manuel merged 1 commit into
devfrom
fix/impersonation-touches-last-active

Conversation

@angel-manuel

Copy link
Copy Markdown
Contributor

The bug

last_active_at is written in exactly one place — extractors.rs, on the API key's own identity, and only when that identity is a sub_agent. The impersonation path resolves a target and never touches it.

So an identity reached only through X-Overslash-As has last_active_at frozen at its creation timestamp forever, however much traffic flows through it. An org service key is not a sub-agent, so the existing touch never fires for it either. Impersonation is the one authentication route that produces activity without recording it.

archive_idle_subagents then does exactly what it is meant to do, to identities that are in daily use.

What it cost

In the overfolder-dev org: 0 of 14 identities have ever had last_active_at advance past created_at, and all 6 sub-agents ever created there are archived with reason idle_timeout. Two of them are a live user's built-in agents, archived 4h 0m 44s after creation — subagent_idle_timeout_secs to the second. Every call afterwards 403s with impersonation target is archived, and POST /v1/identities/{id}/restore is the only way back (re-creating collides on the UNIQUE (org_id, external_id) an archived row still holds).

Downstream, that surfaced to a real user as their assistant reporting their calendar and email as "down this morning". They were not.

The fix

Stamp the resolved target as well, after the ACL cap has agreed the caller may act as it, with the same fire-and-forget spawn the key's own identity already gets.

Only the leaf needs stamping: the sweep's NOT EXISTS (live child) guard means a live leaf already holds its ancestors up.

ResolvedTarget now carries kind so the extractor can make the sub_agent check without a second lookup.

Tests

Two, in tests/impersonation.rs, which had no activity coverage at all. Both fail without the fix (verified by reverting the hunk):

  • impersonation_touches_sub_agent_target_last_active — an impersonated call advances the target's last_active_at.
  • impersonated_sub_agent_survives_the_idle_sweep — a sub-agent used this second survives a sweep that would otherwise reap it.

cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings are clean; impersonation (33) and identity_idle_cleanup (22) pass.

Two adjacent things, not fixed here

  • POST /v1/identities with an (org_id, external_id) that collides with an archived row returns 500, not 409 — routes/identities/crud.rs has no is_unique_violation catch, unlike ~10 sibling routes. A caller-caused collision surfacing as a 5xx is both misleading and unactionable, and it is the reason "just re-provision" is not a recovery path for a client.
  • restore is route-gated to sub_agent though the repo function has no such limit, so a kind='agent' identity archived by any path has no API recovery at all.

Downstream counterpart: overfolder#679 (recover from an already-archived identity instead of wedging).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NmBpW2sFtpHcCWE3f2deCy

`last_active_at` was stamped in exactly one place — on the API key's own
identity, and only when that identity is a sub-agent. The impersonation
path never touched the identity it resolved. So an identity reached only
through `X-Overslash-As` — which is every agent identity a white-label
caller like overfolder creates — had `last_active_at` frozen at its
creation timestamp forever, however much traffic flowed through it.

`archive_idle_subagents` then did exactly what it is meant to do, to
identities that were in daily use. In the `overfolder-dev` org, 0 of 14
identities had ever had `last_active_at` advance, and all 6 sub-agents
ever created there are archived with reason `idle_timeout` — including
the two built-in agents that had been serving a live user, archived
4h 0m 44s after creation, the org's timeout to the second. Every call
afterwards 403s with "impersonation target is archived", and restore is
the only way back.

Stamp the resolved target too, after the ACL cap has agreed the caller
may act as it. Only the leaf needs it: the sweep skips any identity with
a live child, so a live leaf holds its ancestors up.

`ResolvedTarget` carries the kind out so the extractor can make that
sub-agent check without a second lookup.

Two tests, both of which fail without the fix: an impersonated call
advances the target's `last_active_at`, and a sub-agent used this second
survives a sweep it would otherwise be reaped by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmBpW2sFtpHcCWE3f2deCy
@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
overslash Ready Ready Preview Sep 5, 2026 9:45am UTC

Request Review

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Code diff size

+26 / −0 across 2 files (net +26)

Area Files Added Removed Net
overslash-api 2 +26 −0 +26

Source files under src/ only (.cjs, .go, .js, .jsx, .mjs, .py, .rs, .svelte, .ts, .tsx); test files and inline #[cfg(test)] modules excluded. 1 other changed file not counted.

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@angel-manuel
angel-manuel merged commit ae986a5 into dev Sep 6, 2026
15 checks passed
@angel-manuel
angel-manuel deleted the fix/impersonation-touches-last-active branch September 6, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant