Skip to content

UN-4016 [FEATURE] auth login, flattened platform key, per-deployment keys and key-error UX - #8

Merged
chandrasekharan-zipstack merged 8 commits into
UN-4016-platform-keyfrom
UN-4016-auth-login
Sep 15, 2026
Merged

chandrasekharan-zipstack merged 8 commits into
UN-4016-platform-keyfrom
UN-4016-auth-login

Conversation

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor

Stacked on #3 (UN-4016-platform-key). Four commits, each independently green.

What changes

Platform key flattened[profiles.X.platform] is gone. The platform key is platform_key on the docstudio block (api_key there stays the deployment key); $UNSTRACT_PLATFORM_KEY and env: indirection are unchanged. auth whoami and deployment ls read the docstudio base_url. The explicit_tiers cross-walk and the PLATFORM product are deleted; a discovered .unstract.toml withholds platform_key like any other credential.

Deployment entries slimmed[profiles.X.deployments."<api_name>"] holds only api_key. deployment run / status take the API name directly, as deployment ls prints it. The key for a run resolves flag > env > per-deployment entry > profile api_key (the entry is specificity within the profile tier, not a tier above the environment), pinned by an ablation test over all four sources. config set docstudio api_key <key> --deployment <api_name> is the write path.

Key-error UX — nothing resolving is a usage error raised before any request, listing every place the CLI looked and the exact remedy for each. A 401/403 is translated to name the deployment, say the supplied key does not authorize it, and give the per-deployment config set … --deployment line (the previously unreachable hint; reachability is tested through both run and status). A 404 points at deployment ls.

Stalenessconfig doctor --probe, when a platform key resolves, cross-checks deployments entries against the live listing and reports orphans as warnings (stale_deployments in the report, never a failure). Without a platform key the check is skipped silently. Gated under --probe so a bare doctor stays offline.

unstract auth login — at a terminal: one hidden, skippable prompt each for platform, deployment and LLMWhisperer keys, at least one required. Platform key is checked with whoami (organisation stored beside it), LLMWhisperer key against the usage endpoint, deployment key stored as given and reported as such. Every check runs before the single 0600 write; keys are stored as literals. Non-interactive twin: --platform-key / --deployment-key / --llmwhisperer-key, any one of them - for stdin, zero prompts; a bare non-TTY run fails fast (exit 2) naming the flags. Re-run rotates in place. Org-mismatch guard: a profile already holding a different org_id is not repointed silently — interactive offers a new profile named from the organisation's display name, or overwrites on request; non-interactive fails naming both organisations and --force. whoami's profile writer is split into shared pieces rather than duplicated; config init now points new users at auth login.

README — quickstart leads with auth login for humans and an environment-only three-liner for agents/CI; the three credentials are described once, by role and where minted; no [profiles.X.platform], no aliases.

Migration: deployment aliases removed

An entry like

[profiles.cloud-us.deployments.invoices]
api_name = "invoice-parser"
org_id = "org_ABC"
api_key = "env:INVOICES_KEY"

no longer resolves. run invoices sends invoices as the API name. Rename the table to the API name and keep only the key that differs from the profile's:

[profiles.cloud-us.deployments."invoice-parser"]
api_key = "env:INVOICES_KEY"

org_id is per profile; an alias that named a different organisation becomes its own profile. Most users can delete the section entirely and run by API name.

Exit codes (auth login)

Situation Code
Stored 0
No key given, or non-TTY without flags, or two - 2
A key rejected (nothing written) 3
Profile belongs to another organisation, no --force (non-TTY) 2
Keys accepted, write failed 10

Testing

ruff check, ruff format --check, pytest (481 passed, 1 skipped). Every behavioural change has a test that fails on revert; the two isatty/command-tree guards in the existing suite were updated deliberately (auth login is the one command that asks whether it may prompt — never what it prints in).

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL

The platform API is served by the same deployment as the API deployments
it manages, so a separate `[profiles.X.platform]` product only ever
duplicated docstudio's host. The key now lives on the docstudio block as
`platform_key`, beside `api_key` and `org_id`; `$UNSTRACT_PLATFORM_KEY`
is unchanged and still outranks the file.

- Delete the cross-product tier walking that existed only to share
  `base_url` between the two blocks.
- Treat `platform_key` as a credential everywhere `api_key` is one:
  scrubbing, the shell-history warning, and the untrusted-project filter.
- `auth` and `docstudio` groups take `--platform-key`; `auth --api-key`
  is gone, it named the wrong key.
- `config init` no longer writes a platform block or a deployments stub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
…ch key a run used

A `[profiles.X.deployments.<api_name>]` entry now holds nothing but the
`api_key` that runs that one deployment. The alias layer -- an entry naming
its own `api_name`, `org_id` and `api_key`, addressed by a local nickname --
is gone: `deployment run` and `status` take the API name directly, as
`deployment ls` prints it, and most profiles need no deployments section
at all.

The key for a run resolves flag > env > per-deployment entry > profile
key. The entry is the most specific value within the profile tier, not a
tier of its own, so `$UNSTRACT_DEPLOYMENT_KEY` still wins over a file
value. Pinned by an ablation test that sets all four sources and asserts
the winner as each is removed.

Nothing resolving is a usage error raised before any request, listing
every place the CLI looked and the exact command for each remedy. A 401
from the server is translated to name the deployment and the
`config set docstudio api_key <key> --deployment <api_name>` line that
gives it a key of its own -- the hint that was previously unreachable. A
404 points at `deployment ls`.

`config set` gains `--deployment API_NAME` as the write path for a
per-deployment key. `config doctor --probe` cross-checks deployment
entries against the live listing when a platform key is available and
reports orphaned entries as warnings, never as failures; without a key
the check is skipped silently, and without `--probe` doctor stays
offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
…ng each one it can

One command for the first-run: at a terminal it asks for the platform,
deployment and LLMWhisperer keys in turn, one hidden and skippable prompt
each, with at least one required. Without a terminal it takes the same
three as `--platform-key`, `--deployment-key` and `--llmwhisperer-key`,
any one of them as `-` to read from stdin, and asks nothing; a bare
non-TTY run fails fast naming the flags.

The platform key is checked with `whoami` and the organisation it
resolves is stored beside it; the LLMWhisperer key is checked against
the usage endpoint; a deployment key has no side-effect-free endpoint,
so it is stored as given and the result says so. Every check runs before
the one write, and the keys land as literals in the 0600 config file.
A re-run replaces the keys given and keeps the rest, so rotation is the
same command.

A profile that already belongs to another organisation is not repointed
silently: at a terminal the command offers a new profile named after the
organisation, or overwrites on request; without one it fails naming both
organisations and `--force`. Validation resolves through the profile
being written, so a new profile is checked against the flags and the
environment rather than borrowing the default profile's host.

`whoami`'s profile writer is split into the pieces `login` shares --
the project-local refusal and the profile-selection ladder -- rather
than duplicated. `config init` stays the skeleton writer and now points
new users at `auth login`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
…edential once

The quickstart opens with `auth login` for a person at a terminal and an
environment-only three-liner for an agent or CI. The three keys are
described once, by the job each does and where it is minted, in place of
the per-command capability lists that repeated one another. The
`[profiles.X.platform]` block and the alias examples are gone with the
features; the config example shows the flattened `platform_key` and a
per-deployment entry as the exception it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
…ment cross-check

Without a platform key the entries cannot be checked against the
organisation, and skipping silently under an explicit flag leaves the
caller wondering why nothing happened. A bare `doctor` stays silent. The
not-found hint now also names `doctor --probe` as the way to find entries
the organisation no longer has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown

via Greptile

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, security, or repository-rule issue remains.

Fix All in Claude CodeFindings

  1. P1 Security Retained Keys Reach New Host
Fix with agent prompt
### Issue 1
src/unstract_cli/commands/platform_cmd.py:379-384
When the organisation-mismatch flow selects an existing destination profile, this replaces its shared `docstudio.base_url`, but credentials omitted from the current login remain in that profile. For example, logging in with only a platform key retains the destination's existing deployment key and per-deployment entries. Later deployment commands send those retained credentials to the new host even though they belonged to the profile's previous host. Clear incompatible retained credentials, refuse the merge, or otherwise preserve a host and credential combination that the user explicitly selected.

**How this was verified:** The login loop skips omitted credentials while replacing the shared host, and deployment clients subsequently send the retained key to that shared host.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR restructures credential configuration and adds a unified authentication workflow.

  • Flattens the platform key into the DocStudio profile and replaces deployment aliases with per-deployment key entries.
  • Adds auth login with interactive and non-interactive credential collection, validation, organization-mismatch protection, and atomic persistence.
  • Improves deployment-key error messages and adds live stale-deployment checks to config doctor --probe.
  • Updates documentation and tests for the revised configuration and authentication model.
  • The latest changes fully replace a confirmed destination profile before writing credentials, preventing keys retained from its former host or organization from being sent to the newly selected host.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[auth login] --> B{Keys supplied as flags?}
    B -->|Yes| C[Read flags or one key from stdin]
    B -->|No, terminal| D[Prompt for three optional keys]
    B -->|No, non-terminal| E[Exit with usage error]
    C --> F[Resolve selected profile and hosts]
    D --> F
    F --> G[Validate platform and LLMWhisperer keys]
    G --> H{Organization mismatch?}
    H -->|No| K[Update selected profile]
    H -->|Non-interactive without force| I[Exit with usage error]
    H -->|Interactive: create another profile| J{Destination already exists?}
    J -->|Yes| L[Confirm full replacement]
    J -->|No| M[Create destination profile]
    L --> M
    M --> K
    H -->|Overwrite accepted or forced| K
    K --> N[Perform one 0600 config write]
Loading

Reviews (4) · Last reviewed commit: "UN-4016 [FIX] Replace, rather than merge..."

Comment thread src/unstract_cli/commands/platform_cmd.py Outdated
Comment thread src/unstract_cli/commands/platform_cmd.py Outdated
Comment thread src/unstract_cli/commands/config_cmd.py Outdated
…a colliding name, fail a failed probe

Three review findings on `auth login` and `doctor --probe`.

A profile `login` writes now records the base URL the key was checked
against whenever it has none of its own (or a flag named one): the
profile the org-mismatch guard creates inherits the host of the profile
the login started from, and a brand-new profile records the host it was
verified on rather than falling back to the built-in default later.

A profile name typed at the guard's prompt that already belongs to a
third organisation is confirmed separately, and declining re-prompts,
so the guard's own remedy cannot perform the overwrite it exists to
prevent.

A deployment listing that fails during `doctor --probe` is now a
failed check -- reported under `problems` with a non-zero exit -- rather
than being swallowed and read as "no stale entries".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
Comment thread src/unstract_cli/commands/platform_cmd.py Outdated
… changes profiles

The organisation-mismatch guard lets the user pick an existing profile as
the destination. That profile's own base_url was kept even though the key
had been validated against the profile the login started from, so the key
could be persisted next to a host it was never checked on. When the final
profile differs from the one the run resolved, the validated host now
replaces whatever the destination recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
Comment on lines +376 to +381
if (
name != checked_as
or "base_url" not in product_block
or ctx.overrides.get(f"{product}.base_url")
):
product_block["base_url"] = resolved.get(product, "base_url")

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 security Retained Keys Reach New Host

When the organisation-mismatch flow selects an existing destination profile, this replaces its shared docstudio.base_url, but credentials omitted from the current login remain in that profile. For example, logging in with only a platform key retains the destination's existing deployment key and per-deployment entries. Later deployment commands send those retained credentials to the new host even though they belonged to the profile's previous host. Clear incompatible retained credentials, refuse the merge, or otherwise preserve a host and credential combination that the user explicitly selected.

How this was verified: The login loop skips omitted credentials while replacing the shared host, and deployment clients subsequently send the retained key to that shared host.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/unstract_cli/commands/platform_cmd.py
Line: 376-381

Comment:
**Retained Keys Reach New Host**

When the organisation-mismatch flow selects an existing destination profile, this replaces its shared `docstudio.base_url`, but credentials omitted from the current login remain in that profile. For example, logging in with only a platform key retains the destination's existing deployment key and per-deployment entries. Later deployment commands send those retained credentials to the new host even though they belonged to the profile's previous host. Clear incompatible retained credentials, refuse the merge, or otherwise preserve a host and credential combination that the user explicitly selected.

**How this was verified:** The login loop skips omitted credentials while replacing the shared host, and deployment clients subsequently send the retained key to that shared host.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed. Fixed in 5e2a701 by taking the "refuse the merge" option: the profile the guard names is now rebuilt from what this login verified (cfg.profiles[name] = {} before the write), so a stored deployment key and per-deployment entries from the old host do not survive into the new one. Since that is a replacement, _new_profile_name now confirms any existing name, not only one owned by another organisation. Pinned by test_a_profile_chosen_at_the_guard_is_replaced_not_merged_into.

…guard names

The organisation-mismatch guard lets the user name an existing profile as
the destination. Its host was already being replaced with the one this key
was checked against, but credentials this login did not supply -- a stored
deployment key, per-deployment entries -- stayed behind and would have been
sent to that host. The named profile is now rebuilt from what this login
verified, and any existing name is confirmed before it is replaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL
@chandrasekharan-zipstack
chandrasekharan-zipstack merged commit a4fa2e4 into UN-4016-platform-key Sep 15, 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