Skip to content

eesel support: read a customer's workspace read-only from their support conversation - #38

Open
rob-err wants to merge 1 commit into
mainfrom
feat/support-readonly-sessions
Open

rob-err wants to merge 1 commit into
mainfrom
feat/support-readonly-sessions

Conversation

@rob-err

@rob-err rob-err commented Aug 26, 2026

Copy link
Copy Markdown

The CLI half of read-only support impersonation. The server half is built and
reviewed on eeselapp/slack branch claude/eesel-support-agent-f45a70
(agents/support/CLI-HANDOFF.md, agents/support/READONLY-IMPERSONATION.md).

What it does

eesel support read --task <task_id> <read command…>   # one read, against the customer
eesel support status                                   # live sessions, time left
eesel support end [--task <id>]                        # drop cached tokens

POST /support/mint-readonly-token exchanges a support conversation for a
15-minute, read-only token scoped to the workspace owned by that conversation's
verified sender. The support side never names the target, so neither the model
nor anything the customer wrote can steer it.

How it hangs off the existing CLI

Deliberately narrow, per the handoff's scope constraint (assume the CLI works;
don't refactor it). Four one-line hooks in shared code, everything else is one
contiguous new block:

  • require_creds() returns the minted creds while a session is armed, so every
    existing read command targets the customer with no change to itself.
  • Those creds carry ephemeral, which save_creds already refuses to persist,
    so a support read can never become the ambient identity or overwrite the
    operator's own login. The token is cached in its own 0600 store
    (~/.config/eesel/support/) that nothing outside eesel support reads, and
    is never printed.
  • Two calls to a write backstop beside the existing impersonation one, and one
    advisory line on a 401/403.

Writes: the server is the boundary. The three client layers just make the
refusal legible before the call - a read-noun allowlist (chat/new/login
never run here), the parser's own write=True tags, and the backstop.

Verified live

Against a branch env of the server branch (claude-eesel-support-agent-f45a70):

  • caller_not_allowlisted, task_not_in_support_workspace and
    no_verified_sender all reproduced end to end through the CLI, each with its
    named reason: line and exit 3.
  • The whole read surface works with a real read-only token: agents list,
    instructions, files list, integrations list, automations schedules list, skills list, workspace show, billing show usage, settings notifications show, plus --json / --fields / --agent passthrough.
  • Writes refused at every layer; a lapsed token explains itself; an expired
    cache re-mints.
  • The mint's 200 branch itself could not be exercised: it needs an Auth0
    email-verified workspace owner, and a branch env's ephemeral DB has none. The
    post-mint half was verified with a token identical to what the server mints.

50 new tests; full suite 1101 passing.

Known gap, server-side (not fixed here)

tasks list / count / analytics are reads the API serves over POST, and
the read-only gate is method-based, so it refuses them - confirmed live
([read-only-token] refused POST /workspace/tasks). tasks show <id> (GET)
works. Reading a customer's activity, which the design names as a goal, needs
an explicit server-side exemption for /workspace/tasks and
/workspace/tasks/analytics.

🤖 Generated with Claude Code

…rsation

`eesel support read --task <task_id> <read command…>` runs one ordinary read
command against a CUSTOMER's workspace. The server mints the credential:
POST /support/mint-readonly-token exchanges a support conversation for a
15-minute, read-only token scoped to the workspace owned by that
conversation's verified sender. The support side never names the target, so
neither the model nor anything the customer wrote can steer it.

Also `eesel support status` (live sessions and time left) and `eesel support
end` (drop the cached tokens).

How it hangs off the existing CLI, deliberately narrowly:
- `require_creds()` returns the minted creds while a session is armed, so
  every existing read command targets the customer with no change to itself.
- Those creds carry `ephemeral`, which `save_creds` already refuses to
  persist, so a support read can never become the ambient identity or
  overwrite the operator's own login. The token is cached in its own 0600
  store (~/.config/eesel/support/), which nothing outside `support` reads.
- The mint's four documented refusals get named, actionable messages:
  `no_verified_sender` and `sender_not_verified_owner` are expected outcomes
  for plenty of conversations, and have to read as answers, not as a broken
  CLI.

Writes: the SERVER is the boundary (it rejects every mutating method on a
read-only token). The three client layers here just make the refusal legible
before the call: a read-noun allowlist (`chat`/`new`/`login` never run here),
the parser's own `write=True` tags, and a backstop on any mutating request
the first two missed.

Known gap, server-side: `tasks list`/`count`/`analytics` are reads the API
serves over POST, and the read-only gate is method-based, so it blocks them.
`tasks show <id>` (GET) works. Reading a customer's activity list needs an
explicit exemption for /workspace/tasks and /workspace/tasks/analytics.

Server contract + design: agents/support/CLI-HANDOFF.md and
agents/support/READONLY-IMPERSONATION.md on eeselapp/slack branch
claude/eesel-support-agent-f45a70.

Co-Authored-By: Claude Opus 5 (1M context) <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