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

Open
#764 0 comments 0 reactions 1 assignee Claimed by @naveen-bhatt View on GitHub
area:ai-suggestions bug P1
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.