Skip to content

feat(agents): an assistant's grant may last a year, and agent.list says so - #1686

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

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

Conversation

@frahlg

@frahlg frahlg commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Problem

Fredrik, 17 October 2026: "token ska kunna vara längre än 30 dagar kanske också 1 år". An assistant's grant (role agent) could last at most 30 days.

Change

  • protocol.AgentMaxMs goes from 30 to 365 days. support (14 days) and release (90 days) stay. checkGrant still refuses anything longer with E_CAP_EXCEEDED {cap: "expiresAtMs", max}.
  • agent.list now carries longestMs: {role: ms}, the longest grant the box takes for each role (protocol.LongestMs()). A newer app reads it and offers an older box no more than it takes.
  • docs/agents.md: the Principals table, the agent.list answer, a short "Mixed versions" section, the offer's ttlS, and "Numbers in one place".
  • The approval vectors keep their 30-day ends: the app checks the same bytes, and the length is only data there.

Unchanged: expiry is wall clock and fails closed without a trusted clock; the wall-clock mark still stops a clock set back from reviving a grant; holds still last minutes; durable changes still need the owner's yes.

Mixed versions

An old box does not clamp. It refuses an expiry past 30 days with E_CAP_EXCEEDED. FTW Cloud builds the offer before the owner picks a home, so it cannot ask each box what it takes. So the fix lives in the clients:

  • FTW Cloud's offer asks for a year.
  • The app offers no more than agent.list's longestMs. No longestMs means an older box, and the app offers 30 days at most.
  • Apps already out (up to 0.4.7) and the box page as feat(web): the box page lets an assistant in with no phone app #1682 shipped it offer 30 days at most whatever the offer asks, so they never send an old box more than it takes. A box page is served by its own box, so a newer page never meets an older box.

One case still refuses rather than clamps: a hand-written POST /api/agents to an old box that leaves ttlS out gets the offer's year and is refused. No client in the field does that. On a box with this PR, a missing ttlS grants 30 days (or the offer's length if shorter), and a ttlS past the role's longest grant is refused up front with E_CAP_EXCEEDED {cap: "expiresAtMs", max}, so a huge one cannot wrap.

agent.expiring

It still fires once, in a grant's last day, whatever the length. The words on the phone ("Access ends tomorrow", from the push catalogue that FTW Cloud reads through go/protocol) say "tomorrow", so an earlier notice would need new text in every copy, and older apps would show the wrong words. Renewing takes one Face ID. TestAYearLongGrantWarnsInItsLastDay holds it.

Box page

#1682 merged while this was in progress, so its Access tab is here too. GET /api/agents now carries box.longestMs (in AgentFacts, filled by boxPageAgents.Facts()). The tab offers 1, 7, 30 and 90 days and a year, never past the offer's ttlS or box.longestMs for the role, and still starts at a week, as #1682 chose. Renewing follows the same rule, which also stops it offering a support row 30 days that the box refuses (support ends at 14).

The field lives in AgentFacts, not in the embedded AgentList, because open #1684 replaces that embedding with fields of its own. A copy of #1684 rebased onto master merges with this branch without conflict (git merge-tree).

Seen in a browser against a stub box serving this branch's web/: the offer card lists 1 day to 1 year with 7 days chosen; Claude's renew lists "1 day from now" to "1 year from now"; Sourceful's renew lists 1 and 7 days.

Tests

  • TestAnAssistantMayHaveAYearAndNoMore: a year goes through; a year and a millisecond is refused, at add and at renewal, with max = now + a year.
  • TestTheBoxTellsTheLimitsItHolds: LongestMs() matches what checkGrant holds: 365, 14 and 90 days.
  • TestAgentListTellsAnOwnerTheLongestGrant: an owner's agent.list carries longestMs.
  • TestAYearLongGrantWarnsInItsLastDay: no warning on day 0, day 300 or 25 hours before; one warning in the last day.
  • TestTheBoxPageIsToldTheLongestGrant: the box page's facts carry longestMs.
  • access.test.mjs: a year where the box takes one, 30 days where it says nothing, support renewals capped at its 14 days, a week to start.
  • TestTheBoxPageBoundsALength: no ttlS gives 30 days of a year's offer; a year goes through; a year and a second, and 18446744073709552 s, are refused with max = now + a year.
  • TestTheBoxPageListCarriesNoStrayLongest: GET /api/agents has box.longestMs and no top-level copy.
  • An agent's own agent.list carries no longestMs.
  • make verify and npm test (760 tests): pass.

Changeset

patch: it lengthens an existing choice and adds no new action. Say if you want minor.

Pairs

Merge order: this PR first, then ftw-cloud (pin moved), then the app. Deploy FTW Cloud (srcfl/ftw-cloud#27) before this reaches boxes in a beta, and before the app.

New grants are safe in any order: a newer cloud with older apps and box pages still gives 30 days; a newer box with an older cloud still gets 30-day offers. Renewals are not. The renew pickers (box page editForm, the app's GrantView) offer a year from the box's longestMs alone. An older cloud ends every refresh-token family 30 days after the code swap, whatever the box later says, so with an older cloud a grant renewed for a year still loses its assistant at day 30, and the assistant must connect again. ftw-cloud#27 makes the refresh token follow the grant's end.

🤖 Generated with Claude Code

@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:30:47.286421Z aac7a0f 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.

frahlg and others added 2 commits October 17, 2026 08:29
…ys so

AgentMaxMs goes from 30 days to 365. support (14 days) and release
(90 days) stay. The box refuses anything longer with E_CAP_EXCEEDED, as
before.

agent.list now carries longestMs, the longest grant the box takes for
each role, so a newer app offers an older box no more than the 30 days
it takes. A box without longestMs is taken to allow 30 days for an agent.

agent.expiring still fires once, in a grant's last day, whatever its
length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app checks the same bytes; the length is only data in them.

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

@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: aac7a0f6f5

ℹ️ 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".

@@ -0,0 +1,5 @@
---
"ftw": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark the new access duration as a minor change

Use a minor changeset here: extending assistant access from 30 days to one year is a visible new capability, not a fix. Leaving this as patch causes the release tooling to publish the feature under the wrong version increment.

AGENTS.md reference: AGENTS.md:L171-L175

Useful? React with 👍 / 👎.

Comment thread go/internal/appproto/agents.go Outdated
defer h.cfg.Agents.tell("", protocol.AgentDecided{})
now, _ := h.wall()
out := protocol.AgentList{Agents: []protocol.AgentRow{}, Keys: []protocol.ApprovalKeyInfo{}}
out := protocol.AgentList{Agents: []protocol.AgentRow{}, Keys: []protocol.ApprovalKeyInfo{}, LongestMs: protocol.LongestMs()}

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 Expose grant limits through the local agents endpoint

Mirror LongestMs into handleAgentsList in go/internal/api/api_agents.go. The session-based agent.list is populated here, but the box page reads GET /api/agents, whose protocol.AgentList constructor still omits this field and therefore emits longestMs: null. A box-page client following the documented missing-limit fallback consequently treats this new box like an older one and caps assistant access at 30 days instead of offering the new one-year duration.

Useful? React with 👍 / 👎.

The Access tab now offers 1, 7, 30 and 90 days and a year, never past
the offer's ttlS or the box's longest grant for the role, which GET
/api/agents now carries in box.longestMs. It still starts at a week.

Renewing follows the same rule, so a support row is no longer offered
30 days that the box would refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit to srcfl/ftw-cloud that referenced this pull request Oct 17, 2026
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>
…e limits

Review of #1686:
- POST /api/agents without ttlS now grants 30 days, or the offer's
  length if shorter, not the offer's year.
- A ttlS past the role's longest grant is refused up front with
  E_CAP_EXCEEDED {cap: expiresAtMs, max}, so a huge one cannot wrap
  (18446744073709552 s used to become 384 ms).
- agent.list carries longestMs to owners only; an agent's answer leaves
  it out, as docs/agents.md says. GET /api/agents no longer carries a
  stray top-level "longestMs": null beside box.longestMs.
- The box page falls back per role when box.longestMs is missing:
  30 days for an assistant, 14 for Sourceful, 90 for the release check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 8a7bd9c into master Oct 17, 2026
30 of 32 checks passed
frahlg added a commit to srcfl/ftw-cloud that referenced this pull request Oct 17, 2026
8a7bd9c6e597c4396deb05a5b7cd728f6619f97b on srcfl/ftw master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit to srcfl/ftw-cloud that referenced this pull request Oct 17, 2026
…to a year (#27)

* feat(oauth): an assistant's tokens may last as long as its grant, up 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>

* chore: pin go/protocol at srcfl/ftw claude/agent-grant-year 70c7719c

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

* fix(oauth): a renewed grant keeps an idle assistant's tokens

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>

* chore: pin go/protocol at srcfl/ftw#1686's merge commit

8a7bd9c6e597c4396deb05a5b7cd728f6619f97b on srcfl/ftw master.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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