uttrflow / uttrflow/uttrflow-swift
AI suggestions in a terminal offer paths that do not exist from the current directory, and destructive lines the model writes
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
In a terminal, AI suggestions can offer a `cd`, `cat`, `vim` or `./script` line whose path does not exist from where the terminal is sitting, and a destructive line can still be drawn. A wrong path or a wrong command is worse than no suggestion.
Measured on origin/main `c447677`.
**Remembered lines are offered from every folder, and only their last word is checked.**
- `PredictStore.candidates(for:matching:)` (`Sources/UttrflowPredictStore/PredictStore.swift:55-67`) reads every surface with the same application, role and locator, ignoring the directory (`surfaceIdentifiers`, `:96-106`). A relative `cd Sources/Login` typed in one project is offered in every other one.
- `Verifier.verdict` (`Sources/UttrflowPredict/Verifier.swift:61-93`) looks up only the line's last word (`CompletionToken(candidate.text)`, `:68`). `cp missing.txt backup/`, `cd build && cat out.log` and `vim a.txt b.txt` are judged on their final word alone.
- A path the machine denies is not a rejection. `Verification.verdict` (`Sources/UttrflowPredict/Verification.swift:72-76`) returns `.plausible` when there is no near neighbour and no model objection, and paths are an open vocabulary (`isClosedVocabulary`, `:115-122`). So `cd ~/old-project` for a directory that was deleted is drawn.
- Words that carry quotes, `$`, `*`, `=` or `~` alone are "free" (`isFree`, `:258-262`), so `cat "My Notes.txt"` and `cd $HOME/gone` are never checked, and `CompletionToken` splits on spaces, so `cd My\ Folder` is looked up as `Folder`.
**The model's lines stand while the machine has not answered.**
- `Verifier.stands` (`Verifier.swift:189-202`) passes a word whenever `known` is `nil` (`Verification.stands`, `Verification.swift:215-218`). The index answers in the background (`EnvironmentIndex.values`, `Sources/UttrflowPredict/EnvironmentSource.swift:74-79`), so the first turn in a new directory lets any invented path through.
- The alternatives built from the machine's values (`SuggestionCoordinator.generate`, `Sources/Uttrflow/Suggestion/SuggestionCoordinator.swift:524-530`) are drawn without passing any gate.
**The working directory is taken on trust.**
- The terminal's directory is `Surface.scope`, which `FieldReading.scope` (`Sources/UttrflowPredictCapture/FieldReading.swift:67-73`) takes from the field's or window's `AXDocument`, and otherwise from the window title. `EnvironmentSource.workingDirectory(of:)` (`EnvironmentSource.swift:142-145`) accepts any scope beginning `/` or `~/`, so a title such as `~/api (-zsh)` is used as a directory, and nothing checks that the directory exists.
- `FieldReading.directory(of:)` (`FieldReading.swift:114-122`) drops the last component of any path with an extension, so a terminal in a folder named like `site.github.io` is scoped to its parent.
**Destructive lines have gaps.**
- A generated line is never passed through `DestructiveCommand`: `SuggestionSession.resolveGenerated` (`Sources/UttrflowPredict/SuggestionSession.swift:254-272`) draws what the model wrote.
- A remembered irreversible line is still drawn when it clearly beats a rival (`PredictionEngine.decision`, `Sources/UttrflowPredict/PredictionEngine.swift:33-37`).
- `DestructiveCommand.destroys` (`Sources/UttrflowPredict/DestructiveCommand.swift:19-44`) reads the head word literally and looks past `sudo` by one token only, so `/bin/rm -rf x`, `\rm -rf x`, `sudo -E rm -rf x`, `xargs rm`, `find . -delete`, `git push --force-with-lease`, `git branch -D x` and `git stash clear` are not recognised.
**Verification launches programs.** `SystemEnvironmentReader.branches` (`Sources/UttrflowPredict/EnvironmentReading+System.swift:54-65`) runs `git for-each-ref` in the terminal's directory to learn the branches, which reading `.git/refs` and `packed-refs` answers without running anything.
## What should happen
Before a terminal suggestion is drawn, whether remembered or generated:
1. Every path-like argument is resolved against the terminal's current directory as the shell would, with `~`, `$HOME`, `..`, quotes and escapes expanded, and the line is suggested only when each exists with the right kind: a directory for `cd`/`pushd`, a file for `cat`/`source`, an executable file for `./x`.
2. An unknown or non-existent working directory suppresses relative paths; absolute and `~` paths are still checked.
3. The command word is a builtin, an alias, or an executable found by checking `PATH` entries on disk.
4. `git checkout ` and `git switch ` are checked against `.git/refs` and `packed-refs`.
5. A word that cannot be resolved without running something (a glob, a variable, command substitution) makes the line unverifiable, and an unverifiable line is not suggested.
6. Verification uses filesystem checks only and never launches a program. It stats the paths it names, never lists a directory, stays within a few milliseconds, and gives up on a slow network volume rather than waiting on it.
7. A destructive line is never offered, from history or from the model.
False negatives are acceptable here; a wrong path is not.
## Related
- #746 and #747 add tests to the path lookups and `ProgramVerbs`; this does not take them.
- #663: the program lookups never time out.
- #614: the two terminal lists disagree, which decides where this check applies.
Contributor guide
Assessment
This issue has not been assessed yet.