feat(cli): visionset token create/list/revoke — and workspace resolution, decided once (#26) - #98
Merged
Merged
Conversation
…alks `resolve_workspace_root(explicit)` and `WORKSPACE_ENV_VAR` move beside `DB_FILENAME`, where the fact they read from the other end already lives. The server and the CLI both need the answer and import-linter forbids either importing the other, so the rule belongs above both. Precedence: an explicit path, then a non-empty `VISIONSET_WORKSPACE`, then the nearest `visionset.db` at or above the working directory, then the working directory. Only the third case walks — a stated root traded for its parent is how a credential gets minted into the wrong workspace.
The provisional resolver is gone; both names stay importable from `server.dependencies`, which is where the server's "which workspace do I serve?" question is documented. One deliberate behaviour change: with no `VISIONSET_WORKSPACE` set, a server started below a workspace now serves it instead of answering 500 NOT_A_WORKSPACE.
Three commands over `TokenService`, each resolving a workspace through the kernel's rule and calling exactly one service method. `--workspace`/`-w` is declared per command, not on the root callback: a Click group stops parsing at the first non-option token, so a callback option would reject `visionset token create --name ci --workspace X` — the invocation everybody types. Stdout is data, stderr is prose. `create` puts the secret alone on stdout so `TOKEN=$(visionset token create --name ci)` is exactly the secret, and the shown-once warning on stderr so it survives that redirection. `list` names its three columns one at a time, so neither a secret nor a digest can reach it. Exit codes: 0 success, 1 for any VisionSetError as one sentence on stderr, 2 for Click's own usage errors.
workspaces.md gains the precedence table and the argument for why only cwd detection walks upward; auth.md gains the three commands, the stdout/stderr split, the terminal's exit-code contract, and the behaviour change the server inherited from sharing one resolver. No docs/cli.md: auth.md owns tokens and workspaces.md owns the workspace. One earns its keep when the second command family lands and it has more than a stub to hold.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…ion, decided once (#26) (#98) * feat(kernel): promote workspace resolution — one rule, and only cwd walks `resolve_workspace_root(explicit)` and `WORKSPACE_ENV_VAR` move beside `DB_FILENAME`, where the fact they read from the other end already lives. The server and the CLI both need the answer and import-linter forbids either importing the other, so the rule belongs above both. Precedence: an explicit path, then a non-empty `VISIONSET_WORKSPACE`, then the nearest `visionset.db` at or above the working directory, then the working directory. Only the third case walks — a stated root traded for its parent is how a credential gets minted into the wrong workspace. * refactor(server): read workspace resolution from the kernel The provisional resolver is gone; both names stay importable from `server.dependencies`, which is where the server's "which workspace do I serve?" question is documented. One deliberate behaviour change: with no `VISIONSET_WORKSPACE` set, a server started below a workspace now serves it instead of answering 500 NOT_A_WORKSPACE. * feat(cli): visionset token create/list/revoke, against a real workspace Three commands over `TokenService`, each resolving a workspace through the kernel's rule and calling exactly one service method. `--workspace`/`-w` is declared per command, not on the root callback: a Click group stops parsing at the first non-option token, so a callback option would reject `visionset token create --name ci --workspace X` — the invocation everybody types. Stdout is data, stderr is prose. `create` puts the secret alone on stdout so `TOKEN=$(visionset token create --name ci)` is exactly the secret, and the shown-once warning on stderr so it survives that redirection. `list` names its three columns one at a time, so neither a secret nor a digest can reach it. Exit codes: 0 success, 1 for any VisionSetError as one sentence on stderr, 2 for Click's own usage errors. * docs: the token commands, and where 'which workspace' is decided workspaces.md gains the precedence table and the argument for why only cwd detection walks upward; auth.md gains the three commands, the stdout/stderr split, the terminal's exit-code contract, and the behaviour change the server inherited from sharing one resolver. No docs/cli.md: auth.md owns tokens and workspaces.md owns the workspace. One earns its keep when the second command family lands and it has more than a stub to hold.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #26.
Three real
visionset tokencommands overTokenService, and the half every later surface task depends on: workspace resolution, decided once.Workspace resolution, promoted
WORKSPACE_ENV_VARandresolve_workspace_root(explicit)move out ofserver/dependencies.pyand intokernel/services/workspace_service.py, besideDB_FILENAME— the same fact read from the other end. The rule can live in neither caller: import-linter forbidsvisionset.serverimportingvisionset.cli. Both names stay importable fromserver.dependencies, which is where the server's "which workspace do I serve?" question is documented.Precedence, first match wins:
explicit—--workspace/-wVISIONSET_WORKSPACE, non-emptyvisionset.dbat or above cwdOnly case 3 walks, and that asymmetry is the whole rule. A flag and an env var are somebody stating which workspace; if the stated directory holds none, walking to its parent and quietly minting a credential into whatever lives up there is the worst thing this function could do. Git draws the line in the same place — discovery walks up,
--git-dirandGIT_DIRdo not. Finding nothing is not an error —openowns that refusal and already names the path.VISIONSET_WORKSPACEset, a server started below a workspace now serves it instead of answering 500NOT_A_WORKSPACE. That is the cost of one resolver rather than two; a new test intest_workspace_dependency.pypins it anddocs/auth.mdstates it.Three decisions worth not re-litigating
--workspaceis per command, not on the root callback. A Click group parses withallow_interspersed_args=False, so an option on@app.callback()must precede the subcommand —visionset token create --name ci --workspace Xwould fail with "No such option". Nobody types the flag first.test_the_flag_may_follow_the_subcommandis the test that would have caught the other choice.Stdout is data; stderr is everything a person reads.
createputs the secret alone on stdout, soTOKEN=$(visionset token create --name ci)is exactly the secret, and the shown-once warning on stderr so it survives that redirection.listnames its three columns one at a time rather than dumping the model, so a field added toTokencannot leak into a listing — a digest is not a secret, but it verifies a guess offline.One exit code for the whole error family, not a table. 0 success, 1 any
VisionSetErroras one sentence on stderr, 2 Click's own usage errors. A REST client branches on a machine-readablecodebecause it is a program; a shell branches on zero versus non-zero and a person reads the sentence.cli/_errors.pykeeps a sparse MRO-walked hint map for remedies a terminal can act on — one entry today, becauseNotAWorkspace's own sentence ends in "useWorkspaceService.init", which is a Python API a CLI user cannot call.Smaller calls:
revokeresolves by name in two service calls rather than adding arevoke_by_name— the intermediate read is what prints the name actually matched (names are unique case-insensitively) and short-circuits an already-revoked token, which exits 0 without prompting because a retried command must be safe.--yesskips the prompt;typer.confirm(abort=True)also refuses on EOF, so a destructive command that cannot ask never acts. Thecreate --name/revoke <name>asymmetry is the issue's own spelling, kept rather than silently "fixed".No
docs/cli.md.auth.mdowns tokens,workspaces.mdowns the workspace and now the rule that names one. Acli.mdwritten today would duplicate both; it earns its keep when #33/#34 land a second command family.Ledger
FORMAT_VERSIONstays 11, M3's ledger is spent.openapi.jsonunchanged — verified by re-export, zero diff. server: project and schema endpoints (CRUD + versioning) #27 is still the first task that moves it.VERSIONstays0.0.1.dev0. No new dependency, no import-linter change, no new kernel error, service, model or domain event.tests/cli/(30) and a resolution section intests/kernel/test_workspace_service.py(9).Checks
Plus a manual walk from a directory two levels below a workspace: discovery found it,
$(...)captured exactly the secret, the listing showed neither secret nor digest, re-revoking exited 0, running outside any workspace exited 1 with the hint — and novisionset.db-wal/-shmwas left behind, which is what thefinally: close()is for.