Skip to content

feat(oauth): an assistant's tokens may last as long as its grant, up to a year - #27

Merged
frahlg merged 5 commits into
mainfrom
claude/agent-grant-year
Oct 17, 2026
Merged

frahlg merged 5 commits into
mainfrom
claude/agent-grant-year

Conversation

@frahlg

@frahlg frahlg commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Problem

Fredrik, 17 October 2026: an assistant's access should be able to last longer than 30 days, up to a year. srcfl/ftw#1686 raises the box's limit. FTW Cloud must offer the longer length, and an assistant's tokens must last as long as the grant the box sets, never longer.

Change

  • The offer asks for a year. buildOffer already takes ttlS from protocol.AgentMaxMs, which feat(agents): an assistant's grant may last a year, and agent.list says so ftw#1686 raises to 365 days. FTW Cloud makes the offer before the owner picks a home, so it cannot ask each box what it takes. The app and the box page cap the length at what the box says it takes (longestMs in agent.list and in the box page's facts) and offer 30 days at most to a box that says nothing. Apps and box pages already out offer 30 days at most whatever the offer asks.
  • RefreshTTL is now protocol.AgentMaxMs, a year. The access token stays at ten minutes.
  • A refresh token is not clamped to the grant. Before, a refresh passed the old token's end on, so a family died at the end it had when the code was swapped, even after the owner renewed the grant. Clamping each refresh row to the grant's end at issue would still fail an idle assistant: last used on day 10, renewed for a year on day 29, it would get 400 on day 31. Now a refresh token lasts RefreshTTL from issue. An agent past its end in the box's newest roster is refused at every refresh and every call (agentGone) and swept with its tokens, so a refresh token is never usable past the grant. The access token stays within ten minutes and never past the grant's end. Rotation and reuse detection are unchanged.
  • No roster end past a year. grantOf cuts an end past now + protocol.AgentMaxMs.
  • Comments on the spent-code memory and the offer say what changed.

Tests

  • TestARefreshTokenFollowsTheGrantTheBoxSets: an assistant idle since day 0, whose 30-day grant is renewed for a year on day 29, refreshes with its day-0 refresh token on day 31. Cut to five minutes, the next access token lasts at most 300 s and fails after the cut, and so does the refresh. With a refresh row clamped to the grant, it fails ("an idle assistant lost a renewed grant at its first end").
  • TestARosterEndPastAYearIsCut: a roster end two years out is stored as at most a year from now.
  • TestTheOfferIsSignedAndCarriesTheConsent now pins ttlS at 365 × 86,400.
  • go vet ./..., go test -race ./..., go test -tags ftwclouddev ./..., scripts/check-imports.sh: pass. On one full -race run under heavy load, TestRostersAreNotDropped, TestUnsupportedVersionsAndTheRateLimit and later TestAPurgedHomeLosesItsAgents each failed once on a wait. Each passed on its own (-count=3 and -count=4), and two more full -race runs passed on this branch and on main.

Pairs

Deploy this before the Core beta with srcfl/ftw#1686 and before srcfl/ftw-app#82. New grants are safe in any order, but renewals are not: the newer box page and app offer to renew for a year from the box's longestMs alone, and an older cloud ends every refresh family 30 days after the code swap. Deploying this first is safe: older apps and box pages still give 30 days at most, and a newer box with this cloud's year-long offer gives what the owner picks.

Nothing here is deployed. Deploying is the owner's step.

🤖 Generated with Claude Code

frahlg and others added 2 commits October 17, 2026 08:22
…to a year

The signed offer asks for a year, the new protocol.AgentMaxMs. The owner
picks less in the app or on the box page; both start at 30 days and offer
a box no more than its agent.list says it takes, or 30 days when it says
nothing.

A refresh token lasts at most RefreshTTL, now a year, and never past the
grant's end in the box's newest roster. Each refresh now takes that end
afresh, so a grant the owner renews keeps its tokens; before, a family
died at the end it had when the code was swapped, renewed or not.

The access token stays at ten minutes.

Moves the go/protocol pin to srcfl/ftw claude/agent-grant-year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T06:38:33.397583Z 49d450e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49d450e48f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/oauth/token.go Outdated
return
}
s.issue(w, t.Family, cl.ID, a, scope, t.Expires)
s.issue(w, t.Family, cl.ID, a, scope, s.now().Add(RefreshTTL))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain reuse markers for the sliding family lifetime

When a grant is repeatedly renewed and this line keeps the token family alive for more than a year, store.sweep deletes the original spent_codes entry and each rotated refresh-token row at that credential's own expires_ms, even though newer descendants remain valid. Replaying an authorization code or refresh token older than a year is then treated as an unknown credential rather than reuse, so revokeFamily is not called and the still-live family remains usable; retain these reuse markers until the family actually ends, or keep an absolute family expiry.

Useful? React with 👍 / 👎.

frahlg and others added 3 commits October 17, 2026 10:24
go.mod: main pins go/protocol at cbdd8a58 (#1687, fleetwire and
webpush); this branch needs AgentMaxMs from srcfl/ftw#1686. Pin both at
#1686's branch head 861fa708, which has master merged in. The pin moves
to #1686's merge commit once it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #27: a refresh row was clamped to the grant's
end when issued, and setGrant moves only the agent row. An assistant
last used on day 10 whose owner renewed for a year on day 29 got 400 on
day 31.

Now a refresh token lasts RefreshTTL (a year) from issue; an agent past
its end is still refused at every refresh and every call (agentGone)
and swept with its tokens. The access token stays within ten minutes and
never past the grant's end.

The service also takes no roster end past a year from now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8a7bd9c6e597c4396deb05a5b7cd728f6619f97b on srcfl/ftw master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit f0c69bf into main Oct 17, 2026
2 checks passed
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