Skip to content

A profile whose endpoint is the HEY app answers the reads from it - #646

Open
jeremy wants to merge 8 commits into
mainfrom
app-endpoint
Open

jeremy wants to merge 8 commits into
mainfrom
app-endpoint

Conversation

@jeremy

@jeremy jeremy commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Context

  • An agent on this computer reaches HEY only through HEY's server, with the person's full sign-in.
  • hey local: ask the HEY app on this computer to act for you #640 reached the HEY Linux app's door ($XDG_RUNTIME_DIR/hey/agent.sock, open while Settings › General › "Let Agents Use HEY" is on), but through a hey local command tree of its own.
  • The design that replaces it: the endpoint is a field of a profile (Add named profiles, ported from basecamp-cli #644), the ordinary hey commands answer from the app when the profile says so, behind HEY_EXPERIMENTAL=app, the default stays the server, no auto-switching.
  • This is the first of three: reads here; writes, sent/waiting and exit 9 next; setup and the skill after.

Change

  • A profile may name "endpoint": "app" (or "server", the default); an app profile needs no base_url. Any other value is a config error, never read as the server.
  • With HEY_EXPERIMENTAL=app, an app profile is answered by the app's door: hey box view imbox (imbox.list), hey screener list and hey screener history (screener.list), hey draft list (drafts.list).
  • Answers keep the server's field names (postings with topic_id, creator, seen; email_address; status approved/denied; updated_at, scheduled_delivery_at); the app's own facts ride along (made_as, sendable, kind, addresses); meta.id_kind: "sender" on the screener, whose ids are senders, not clearances.
  • Every envelope, errors included, names meta.endpoint (app or server) in an experimental build, beside meta.profile.
  • On an app profile: no credential is read, migrated or signed in; no account scope; --account, HEY_ACCOUNT_ID, a profile's own and a repository's account_id are refused (exit 1); every other command that reads a server is refused as "not offered by the HEY app on this computer" (exit 1), hey mcp and hey auth/login/logout in their own words; another box, --page likewise; bare hey prints help, no wizard.
  • A shut door is exit 6, "Open HEY … turn on Let Agents Use HEY", never HEY's server instead; the app signed out is exit 3; an answer missing what a listing must have is exit 7.
  • Hidden hey app tools|call, registered only under the gate, runs the door's read tools; a tool of any other effect is refused before it is called.
  • profile list and profile show name the endpoint under the gate.
  • internal/localapp comes from hey local: ask the HEY app on this computer to act for you #640 at fcf918b unchanged.
  • Without the gate an app profile is refused at selection ("not in this build", exit 1), the design's choice over a quiet fallback; help, .surface and the docs are unchanged.

Verification

  • internal/cmd/app_endpoint_test.go (21 tests) and two in internal/config/profiles_test.go, each against a fake door on a temporary Unix socket and a server that fails the test if reached.
  • Each behaviour shown red with its code removed, 30 of 30: the gate's check, hey app's registration, profile list's endpoint, each of the four routes, the refusals of other verbs, --account and HEY_ACCOUNT_ID, the shut door's exit 6, signed out's exit 3, the required has_more, hey app call's read-only check, meta.endpoint, a mistyped or non-string endpoint, completion and the credential-free profile show on an app profile, and the Imbox rows without the server's empty fields.
  • Without the gate: .surface unchanged (make check-surface), hey commands lists no app, profile list rows and server envelopes carry no endpoint, hey app is an unknown command.
  • A fresh review in its own context: no High or Medium; its four Lows fixed in 7241ab2 (completion, no keyring read, narrower Imbox rows, non-string endpoint).
  • Bot review (cubic, greptile, Copilot): 26 threads: 21 fixed, 5 declined on 4 points with reasons in their threads (summary comment); fixes in bc9a34e, 4225008, bd7d6e4, cfd5879, each red without its fix; a second fresh review of the fixes found no High or Medium.
  • make check green; GOOS=darwin, windows, linux build.
  • Unrun: a check against a running HEY Linux app, since none had the door open on thelio.

Alternatives

  • Falling back to the server when the gate is off or the door is shut: rejected, it answers with the person's full sign-in where they chose the app.
  • Mapping the door's sender to creator vs. a door-only sender field: creator {id, name} chosen so a script reads both endpoints alike; the id is the app's.
  • hey profile create --endpoint app: left to the setup step that creates the app profile; the config is written by hand until then.
  • hey auth status, hey doctor and hey config show lines for an app profile: refused or unchanged here, follow-ups with setup.
  • box view sent|spam: the door serves no such list yet, so they are refused like any other box.
  • --ids-only on the app's screener prints sender ids, the server's clearance ids; --json names it in meta.id_kind. Left as is: no command on the app takes either id until the writes land.
  • XDG_RUNTIME_DIR unset and no HEY_LOCAL_SOCKET is exit 1 (usage: run from the desktop session), not exit 6: there is no door to call shut.

hey-cli had no way to reach the HEY desktop app on this computer. #640
wrote one: a JSON-RPC client for the door the Linux app opens at
$XDG_RUNTIME_DIR/hey/agent.sock while "Let Agents Use HEY" is on, and a
fake door for tests on a temporary Unix socket.

This brings internal/localapp over from #640 at fcf918b unchanged, so the
profile endpoint built next talks to the app through it.

Verified by go test ./internal/localapp/..., the package's own tests.

#640's hey local commands do not come with it: the design that replaces
them routes the ordinary commands through the app when a profile says so.
An agent on this computer could reach HEY only through HEY's server, with
the person's full sign-in. #640 reached the HEY Linux app's door instead,
but through a command tree of its own (hey local), apart from the
commands an agent already knows. The design that replaces it makes the
endpoint a field of a profile, so the ordinary commands answer from the
app when the profile says so.

A profile may now name "endpoint": "app" (or "server", the default). With
HEY_EXPERIMENTAL=app set, such a profile is answered by the HEY app on
this computer: hey box view imbox, hey screener list and history, and hey
draft list call the door's imbox.list, screener.list and drafts.list, and
answer under the server's field names (postings with topic_id and
creator, email_address, status, updated_at), the app's own facts beside
them. Every envelope names meta.endpoint, and meta.profile as before. On
an app profile nothing reads a credential or signs in; every other
command that reads a server is refused (exit 1), hey mcp and hey auth
with their own words, and so are --account, HEY_ACCOUNT_ID and a
repository's account_id; a shut door is exit 6 with the way to open it,
never HEY's server instead. A hidden hey app tools|call gives raw access
to the door's read tools. Without the gate an app profile is refused as
not in this build, and help, .surface and the docs are unchanged.

Verified by the new tests in internal/cmd/app_endpoint_test.go and
internal/config/profiles_test.go, each against a fake door on a
temporary Unix socket and a server that fails the test if reached; each
was shown red with its behaviour removed. make check passes, and the
build compiles for darwin, windows and linux.

Rejected: falling back to the server when the gate is off or the door is
shut, which would answer with the person's full sign-in where they chose
the app; hey local's own command tree; writes, sends and exit 9, which
come in the next pull request; hey profile create --endpoint, left to the
setup step that creates the app profile.

@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 19 files

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

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/cmd/box.go Resolved
Comment thread AGENTS.md Resolved
Comment thread internal/cmd/screener_list.go Resolved
Comment thread internal/cmd/app.go Resolved
Comment thread internal/cmd/endpoint.go Resolved
Comment thread internal/cmd/app.go Resolved
Comment thread internal/localapp/client.go Resolved
Comment thread internal/config/profiles.go Resolved
Comment thread internal/config/profiles.go Resolved
Comment thread internal/cmd/app_reads.go Resolved
The fresh review of the app endpoint found four small gaps. Tab
completion was refused on an app profile as a command the app does not
offer. hey profile show and list looked for the app profile's credential
in the keyring, which it never has and which can prompt on macOS. The
Imbox rows carried the server posting's empty note and bubble_up_schedule
and a thread read breadcrumb the app profile refuses. And an endpoint
written as true or 1 was read as the server.

Completion now runs on an app profile; profile show and list name the app
in place of a sign-in and read no credential; the Imbox rows carry only
what the app has, with no breadcrumb; an endpoint that is not a string is
a config error.

Verified by TestAppProfileKeepsCompletionAndReadsNoCredential,
TestAppImboxRowsCarryNoEmptyServerFields and the non-string case of
TestParseProfilesReadsTheEndpoint, each red with its fix removed, and
make check.

Not changed: --ids-only on the app's screener prints sender ids where the
server prints clearance ids, which --json names in meta.id_kind; no
command on the app takes either id yet, so it waits for the writes.
@jeremy
jeremy marked this pull request as ready for review November 12, 2026 07:26
@jeremy
jeremy requested a review from a team as a code owner November 12, 2026 07:26
Copilot AI balanced review requested due to automatic review settings November 12, 2026 07:26
Comment thread internal/config/profiles.go Resolved
Comment thread internal/config/profiles.go Resolved
Comment thread internal/config/profiles.go Resolved
Comment thread internal/cmd/root.go Resolved

@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).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/cmd/app_endpoint_test.go Resolved
The bot review of the app endpoint found eight gaps. --all was accepted
on the app's reads, which answer only what the app has loaded, so a
partial list could answer a request for all of it. hey app took --account
and ignored it. The app quitting mid-answer was exit 7, not the shut
door's exit 6. hey app call on a signed-out app, whose door lists nothing,
was a usage error rather than exit 3. A screener count below the senders
listed was taken as is; a draft with no Cc read as null; a thread with no
sender read as sender 0. An explicit "endpoint": "server" in a nearer
config could not override a farther "app", and an app-only entry was not
counted as a profile by profile create's first-profile default or by
profile delete's repository guard.

Now --all is refused on the app like --page; --account is refused on
hey app; a connection the app closes is exit 6 with the way to open the
door; an empty tool list is exit 3 for hey app call; the screener's count
must cover its list; address lists stay lists; creator.id is left out
when the app gives none; "server" is kept as written, so it overrides;
and config.EntryIsProfile counts an app-only entry wherever entries are
counted.

Verified by the new and widened tests in app_endpoint_test.go and
profiles_test.go, each red with its fix removed, and make check.
…adable store

TestAppProfileKeepsCompletionAndReadsNoCredential ran profile show alone,
with no credential store at all, so a read of the app profile's
credential by profile list, or by show, would have passed unseen. It now
writes a credential file nothing can parse and runs both: either read
fails the run. Shown red with each command's skip removed.

@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 7 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/cmd/app_endpoint_test.go Resolved
Comment thread internal/config/profiles.go Resolved

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.

🟡 Changes recommended

Active issues leave early error envelopes without endpoint metadata, prevent explicit server overrides of app profiles, and cause endpoint-only app profiles to be mishandled during creation and deletion.

3 open issues
What changed in this PR

Builds on named profiles to let existing read commands use the HEY desktop app instead of the server, behind HEY_EXPERIMENTAL=app.

Changes:

  • Adds profile endpoint selection, unsupported-command refusals, and endpoint metadata.
  • Routes Imbox, Screener, and draft listings through the app’s Unix socket, with hidden read-only tool access.
  • Adds protocol and command tests using a fake app connection, plus contributor guidance.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
internal/​localapp/​socket.go Resolves the app’s socket path.
internal/​localapp/​localapptest/​door.go Provides a fake Unix-socket server for tests.
internal/​localapp/​errors.go Defines connection and protocol errors.
internal/​localapp/​client.go Implements MCP initialization, discovery, and calls.
internal/​localapp/​client_test.go Tests protocol handling, results, and timeouts.
internal/​config/​profiles.go Parses and validates profile endpoints.
internal/​config/​profiles_test.go Tests endpoint parsing and selection restrictions.
internal/​cmd/​screener_list.go Routes pending-sender reads to the app.
internal/​cmd/​screener_history.go Routes screening-history reads to the app.
internal/​cmd/​root.go Integrates endpoint routing and response metadata.
internal/​cmd/​profile.go Displays endpoints and adjusts credential checks.
internal/​cmd/​local_config_trust.go Exempts direct app commands from repository configuration.
internal/​cmd/​endpoint.go Centralizes routing, refusals, and app error handling.
internal/​cmd/​drafts.go Routes draft listings to the app.
internal/​cmd/​box.go Routes supported box reads to the app.
internal/​cmd/​app.go Adds hidden tool discovery and read-only calls.
internal/​cmd/​app_reads.go Maps app listings into CLI output formats.
internal/​cmd/​app_endpoint_test.go Tests app routes, gating, refusals, and output.
AGENTS.md Documents experimental endpoint conventions for contributors.

Review effort: Balanced

Comment thread internal/cmd/root.go Resolved
Comment thread internal/config/profiles.go Resolved
Comment thread internal/config/profiles.go Resolved
The delete guard counts a repository entry with "endpoint": "app" and no
base_url as a profile since bc9a34e, but no test pinned it: the old
base_url-only check passed every test. TestARepositoryAppOnlyEntryHoldsUpADelete
deletes a global profile a trusted repository also names as app-only, and
is red with the old check.
RegisterProfile's and localEntrySetsProfile's comments still said an
entry with no base_url is no profile; since bc9a34e an entry the app
answers is one, by config.EntryIsProfile. Comments only.
@jeremy

jeremy commented Nov 12, 2026 •

Copy link
Copy Markdown
Member Author

Review summary for cfd5879. Every thread is answered and resolved; the fixes are in 7241ab2, bc9a34e, 4225008, bd7d6e4 and cfd5879, each shown red with its fix removed, and make check passes.

Declined, each with its reasoning in the thread:

Also left as is, in the description's Alternatives: --ids-only on the app's screener prints sender ids (--json names it in meta.id_kind), and an unset XDG_RUNTIME_DIR is exit 1, not 6.

Unrun: a check against a running HEY Linux app, since none had the door open on thelio.

Reviewers on cfd5879, a comment-only change: cubic ran on it with no new finding; Greptile last reviewed bd7d6e4 and Copilot 7241ab2, re-requested once.

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.

🟡 Changes recommended

Deleting an app profile can still trigger credential lookup or revoke a leftover server sign-in, violating the app endpoint’s credential-free boundary.

2 open issues
3 resolved since last review

Review effort: Balanced

Comment thread internal/cmd/endpoint.go Resolved
Comment thread internal/cmd/app_reads.go Resolved
…er --all

Deleting a profile the HEY app answers looked up its CLI and MCP
credentials and revoked any it found, the one place left where the app
profile touched the credential store: on macOS that can prompt for the
keychain, and it reaches HEY's server for a profile that has no sign-in
of its own. A limited app listing (box view imbox, draft list) said
"Use --all to see everything", which the app endpoint refuses, and on
the Imbox the truncation notice replaced the app's own warning that it
has not loaded every thread.

Now delete skips the credential lookup and revocation for an app
profile, as profile list and show already do; without the experimental
gate the profile is treated as a server one and its credentials go as
before. App listings say "Run it again without --limit to see them
all", and the Imbox appends its not-loaded warning after that line.

Verified: TestAppProfileDeleteReadsNoCredential (an unreadable
credential store fails any lookup; red before, "could not delete
credentials") and TestAppLimitedListingsOfferOnlyWhatTheAppTakes (red
before on both routes and on the lost Imbox warning) pass; make check
green.

Rejected: dropping the leftover credential of a profile hand-edited
from server to app on delete. The app profile's contract is that it has
no credential; a hand edit that leaves one is cleaned by deleting the
profile with the gate off, or by hey auth logout before the edit.
@jeremy

jeremy commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Review threads: 2 resolved (2 fixed, in c588eee, with a red-first test each).

CI and cubic are green on c588eee; no threads open.

This branch has not been deployed

No deployments
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