UN-4016 [FEATURE] auth login, flattened platform key, per-deployment keys and key-error UX - #8
Conversation
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
|
…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
… 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
| 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") |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
a4fa2e4
into
UN-4016-platform-key
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 isplatform_keyon thedocstudioblock (api_keythere stays the deployment key);$UNSTRACT_PLATFORM_KEYandenv:indirection are unchanged.auth whoamianddeployment lsread the docstudiobase_url. Theexplicit_tierscross-walk and thePLATFORMproduct are deleted; a discovered.unstract.tomlwithholdsplatform_keylike any other credential.Deployment entries slimmed —
[profiles.X.deployments."<api_name>"]holds onlyapi_key.deployment run/statustake the API name directly, asdeployment lsprints it. The key for a run resolves flag > env > per-deployment entry > profileapi_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 … --deploymentline (the previously unreachable hint; reachability is tested through bothrunandstatus). A 404 points atdeployment ls.Staleness —
config doctor --probe, when a platform key resolves, cross-checksdeploymentsentries against the live listing and reports orphans as warnings (stale_deploymentsin the report, never a failure). Without a platform key the check is skipped silently. Gated under--probeso a baredoctorstays 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 withwhoami(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 differentorg_idis 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 initnow points new users atauth login.README — quickstart leads with
auth loginfor 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
no longer resolves.
run invoicessendsinvoicesas the API name. Rename the table to the API name and keep only the key that differs from the profile's:org_idis 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)---force(non-TTY)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 loginis the one command that asks whether it may prompt — never what it prints in).🤖 Generated with Claude Code
https://claude.ai/code/session_01J2SKeanczq13ngDQNbgFNL