Skip to content

Keep the install_id with the tokens it was issued to - #545

Merged
jeremy merged 11 commits into
mainfrom
security/install-id-with-credentials
Oct 6, 2026
Merged

jeremy merged 11 commits into
mainfrom
security/install-id-with-credentials

Conversation

@jeremy

@jeremy jeremy commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

HEY is about to bind each refresh-token lineage to the install that holds it: basecamp/haystack#8710. The binding is set when the family is issued, to the install_id presented at login. When a different install presents a bound family's token, HEY treats it as a copied token and revokes that session. Today hey-cli keeps install_id in a file per config directory (from #355), while the tokens live in one keychain entry per origin. Two config directories that share a keychain entry would therefore present two installs for one lineage, and both would be signed out. Examples are a different HOME or XDG_CONFIG_HOME, or a sandboxed agent.

Change

  • Credentials now records install_id, the install the tokens were issued to. Login stores it; refresh presents it; the refresh's save keeps it.
  • Credentials saved before this release carry no install. They were issued to the logging-in directory's install_id, which every login and refresh on main presented, and which HEY binds the family to. So they take this directory's install_id when its file holds a well-formed one. That's read, never minted. Otherwise they take an id derived one-way from the refresh token (SHA-256, formatted as a version-4 UUID), the same for every holder. Whichever they take is saved with the rotated tokens, so later refreshes from any config directory sharing the entry present it too.
  • A malformed stored install_id is refused locally with a hey login remedy and never sent: HEY would read it as another install.
  • hey auth status still reports the config directory's install_id, as on main. What has to agree across directories is the install a refresh presents, and the refresh path refuses a malformed stored id with an error any command surfaces.

Rollout order: release this, and get every binary that shares a keychain entry onto it, before haystack#8710 deploys.

  • Older binaries still present their own directory's id on refresh, and drop the new field when they re-save the credential. With the plaintext file store that happens on any origin's save, since it rewrites the whole file. A mixed old/new pair on one keychain entry can still revoke each other. The release floor is the answer.

  • Residual: shared entries the client can't attribute. Credentials saved before this release don't record which config directory logged in. So when two directories share a keychain entry, the client can't know which one HEY bound the family to. Two cases revoke the session once, after which both directories sign in again:

    • Simultaneous: both directories run their first post-upgrade refresh at the same instant after #8710 is live. Store locks are per directory, so each presents its own id.
    • Sequential, after a straggler login: an old binary logged in from directory A after #8710 went live, binding the family to A, and directory B is the first to refresh after upgrading.

    The token-derived id can't help either case, because it matches neither directory. Logins made before #8710 is live aren't exposed: their families are unbound, the first refresh claims them, and sequential refreshes then converge on the saved id. The release floor is what keeps this population small.

Tests: make check and go test -race ./internal/... pass. A fake HEY that binds on first use and revokes on a mismatch covers:

  • Upgrade: credentials whose family is bound to the directory's id refresh as that id, and the session survives.
  • Two directories: sharing a keychain, they converge on the first directory's id across sequential refreshes, never presenting the second's.
  • No usable file: a missing or malformed directory file takes the derived id and leaves the file untouched.
  • Known install: a refresh with a known install presents it without creating a file.
  • Malformed stored id: refused, nothing sent.
  • Mutation checks:
    • deriving first fails the upgrade and convergence tests;
    • minting instead of reading fails the no-file tests;
    • ignoring the stored id fails convergence.

HEY is about to bind each refresh-token lineage to the install that holds it and revoke
the session when another install presents the token. The install_id lives in a file per
config directory while the tokens live in one keychain entry, so two directories sharing
that entry (a different HOME or XDG_CONFIG_HOME, a sandboxed agent) would present two
installs for one lineage and revoke each other.

Credentials now record the install they were issued to. Login stores it; a refresh
presents it. Credentials saved before this adopt the directory's install_id on their first
refresh and keep it, so whichever directory refreshes first settles the id for the rest.
A refresh with a known install never reads or mints a directory's file. auth status shows
the credentials' install when signed in.
@jeremy
jeremy requested a review from a team as a code owner October 6, 2026 06:09
Copilot AI balanced review requested due to automatic review settings October 6, 2026 06:09
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 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-06T08:39:13.184560Z 67e1298 New commits
ℹ️ 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: 3cd2c68566

ℹ️ 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/cmd/auth.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/auth/auth.go
Comment thread internal/cmd/auth.go Outdated
Comment thread internal/cmd/auth.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…directory

Credentials saved before they carried an install adopted the refreshing directory's
install_id. Two directories sharing the keychain entry hold different ones, and their
store locks are per directory, so racing first refreshes could each adopt their own and
present two installs for one lineage; a refresh whose save failed lost the adoption the
same way. Derive the adopted id one-way from the refresh token instead: every holder of
that credential arrives at the same id with nothing to coordinate or persist first.

auth status reports the id the next refresh will present. The test fake keyring is safe
for concurrent use, as the keychain it stands in for is.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

Comment thread internal/auth/install_id_test.go Outdated
jeremy added 2 commits October 5, 2026 23:36
The test waited unconditionally for both refreshes to reach the server and discarded
their errors, so a refresh that failed first hung it, and a failure after arrival passed
unnoticed. Bound the wait, release the held handler on every path, and assert both
refreshes succeeded.
HEY_TOKEN bypasses the stored credentials, so the install they refresh as isn't the one in
use. Report the directory's install_id on that path, as before, and don't touch the
keychain for it.

@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: 4aec9578a9

ℹ️ 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/auth/install_id_test.go Outdated
Comment thread internal/auth/auth.go
HEY would read a malformed id as another install and revoke the session, and deriving a
replacement would be another install too. Refuse it locally and ask for a fresh sign-in,
which stores a well-formed one.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/auth/auth.go Outdated

@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: 6efeffb3ce

ℹ️ 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/auth/auth.go Outdated
RefreshInstallID dropped the malformed-id error, so status fell back to the directory's
install_id, which no refresh would present. Return the error; status then reports no
install and says why under install_id_error. A credential that fails to load for another
reason now errs too, rather than reading as signed out.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/cmd/auth.go Outdated
Comment thread internal/cmd/auth_commands_test.go Outdated

@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: 860b98d83e

ℹ️ 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/cmd/auth.go Outdated
Comment thread internal/cmd/auth.go Outdated
…e status contract

Styled status printed Logged in with no install and no reason when the stored install_id
was malformed; it now prints the reason and the hey login remedy. The --help text and
docs/cli.md describe what install_id means signed in, signed out and under HEY_TOKEN,
and what install_id_error reports.
@github-actions github-actions Bot added the docs label Oct 6, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/cmd/auth.go Outdated
jeremy added 2 commits October 6, 2026 01:08
Mirroring the credentials' install in auth status opened a new edge each round (HEY_TOKEN,
a malformed id, styled output, token and cookie logins) for a diagnostic this change
doesn't need. What has to agree across config directories is the install a refresh
presents, and that path already refuses a malformed stored id with an error any command
surfaces. Status goes back to reporting the directory's install_id, as on main.
Credentials saved before they carried an install were issued to the logging-in
directory's install_id, which every login and refresh on main presented and which HEY
binds the family to at issuance. Deriving an id from the refresh token for them presented
a different install on the first refresh after upgrading and got the session revoked.
They now take the directory's install_id when its file holds a well-formed one, read
without minting, and the token-derived id only when it doesn't. The refresh's save keeps
the id, so later refreshes from any directory sharing the keychain entry present it.

The concurrency test asserted every holder adopts the derived id, so it passed against a
server with no binding while the upgrade path revoked. It's replaced by a server that
binds and revokes: an upgrade bound to the directory's id survives, two directories
converge sequentially, and a missing or malformed file takes the derived id and mints
nothing.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/auth/auth.go
Comment thread internal/auth/install_id_test.go Outdated
Comment thread internal/auth/install_id.go Outdated

@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: 8d262b045a

ℹ️ 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/auth/auth.go
The binding-server tests read what the server saw without its mutex. Read a snapshot
under it. The install_id read extracted from installID lost its gosec annotation;
restore it.

@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: 67e1298e28

ℹ️ 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/auth/auth.go
@jeremy
jeremy merged commit 9dfe00f into main Oct 6, 2026
25 checks passed
@jeremy
jeremy deleted the security/install-id-with-credentials branch October 6, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants