Files
Linux_post_install/AgentsReport/architect/2026-08-26_alias-architecture.md
T
Your Name 06a05f0567
gates / consistency-and-conventions (push) Successful in 1m24s
ai continue ...
2026-09-04 11:09:21 -04:00

18 KiB
Raw Blame History

Architecture: pos ai alias — robust activation mechanism

Date: 2026-08-26 Author: Architect (ox-alpha) Status: DECISION_READY (supersedes activation decisions in AgentsReport/architect/2026-08-26_ai-alias-architecture.md)


TL;DR

Decision Choice
D1 — Activation artifact Option B: executable wrapper scripts at ~/.local/bin/<name>, generated from the ENV source of truth. No shell aliases.
D2 — Migration Internal _alias_sync() runs on every pos ai alias invocation; reconciles wrappers ↔ ENV both directions; legacy ai-aliases.sh auto-removed when marker-guarded, else manual guidance. .sh generation stops entirely (no deprecated shim).
D3 — Edge cases Name collisions refused (foreign file or other binary on PATH); empty set → sync deletes all owned wrappers; dead provider → existing pos-ai:121 error is sufficient; list/show formats unchanged, show gains wrapper path.
D4 — Scope fence Only bin/pos-ai-alias + small marker-scan addition to bin/pos-system-uninstall + docs. NOT: bin/pos-ai, ENV format, config-ui, postinstall.sh, menu flows.
Postinstall gap The deferred .bashrc wiring is closed by obsolescence, not implementation — no wiring needed anymore.

Open items: None blocking. One provisional nuance in D2 (legacy-file heuristic guard) flagged inline.


Problem statement

  • Aliases were frozen at source-time: editing provider gemini→openrouter left the stale alias live in running shells → Gemini 429 while list showed openrouter (live failure).
  • Activation required a .bashrc source line that postinstall never wired — the feature was broken-by-omission even before staleness.
  • Non-interactive contexts (cron, scripts, ssh non-login) could never use aliases at all.

Evidence base

  • bin/pos-ai-alias:62-104 — current _alias_regen() writes ai-aliases.sh; success messages at :308 say "Reload shell: source ~/.bashrc".
  • postinstall.sh:80 — PATH export already includes $HOME/.local/bin on pos-managed machines (Debian default ~/.profile also adds it when the dir exists).
  • bin/pos-system-uninstall:96,261 — established precedent for marker-managed user-local binaries ($HOME/.local/bin/pos-ai-hook.sh): discovery list + removal pass already exist as a pattern.
  • bin/pos-ai:658-665, :118-121 — provider resolution flag > env > default; unknown provider yields err "Unknown provider '$p' — available: …" (good runtime failure quality, no pos-ai change needed).
  • Prior architect report D5/D10 chose .sh + .bashrc wiring; the wiring was never implemented. This report supersedes those two decisions; storage (ENV format) and CRUD UX decisions carry forward unchanged.

Decision 1: Activation artifact

Options evaluated

Option A: keep generated bash aliases + wire .bashrc + louder hints + unalias guards

  • Advantages: smallest diff; familiar alias UX.
  • Costs: staleness is inherent — the artifact is a snapshot copied into each shell at source time; the live failure (stale gemini alias) can only be mitigated, never eliminated. Requires postinstall .bashrc wiring (the deferred gap), reload-hint UX that demonstrably fails ("users miss it"), and per-shell unalias guard logic for a mechanism bash makes awkward to retract.
  • Risks: cron/scripts/ssh non-interactive shells get nothing. Two truths (env file vs sourced copy) persist forever.
  • Compatibility impact: none. Operational impact: permanent "did you re-source?" support burden.

Option B: executable wrapper scripts at ~/.local/bin/<name>

  • Architecture: ENV stays the single source of truth; tool renders one standalone script per alias:
    #!/usr/bin/env bash
    # Managed by pos ai alias — regenerated automatically; hand-edits are overwritten.
    # Alias: assist | provider: openrouter | session: assist
    set -euo pipefail
    exec pos ai openrouter ask --session assist --system <printf-%q-prompt> "$@"
    
    (empty prompt → omit --system; prompt embedded via existing double-layer printf %q mechanics, reused from _alias_quote_cmd, so it lands as exactly ONE shell word; "$@" passes user args through).
  • Advantages: staleness eliminated — next invocation reads current file bytes; no shell integration of any kind (kills the postinstall gap instead of closing it); works identically in interactive shells, scripts, cron, ssh non-login; removal = delete one marker-identified file; no INTERACTIVE_CMDS/tee-pipe interaction changes; content edits bypass hash caching entirely (bash caches paths, not contents) and new names are found on first PATH scan.
  • Costs: PATH-presence dependency (~/.local/bin must be on PATH — already guaranteed by postinstall.sh:80 and Debian default ~/.profile, but needs a runtime check + guidance); collision policy needed (scripts are filesystem entries, aliases weren't); ~40 lines more logic than Option A.
  • Risks: name shadows a real binary → mitigated by refusal policy (D3); user hand-edits wrapper → healed by sync regeneration (D2), and the header says so.
  • Compatibility impact: activation semantics change (documented). Operational impact: self-healing artifacts; zero shell-config coupling. Migration impact: handled by D2.

Option C: hybrid — wrappers primary + optional still-generated alias file

  • Advantages: covers users attached to aliases.
  • Costs: keeps the stale-snapshot mechanism alive alongside the fix — two activation paths, two truth-drift surfaces, double the validation matrix. Directly contradicts the motivation ("more robust" = fewer failure modes, not one more).
  • Risks: the exact reported bug remains reachable through the optional path.

Decision: Option B

Staleness was an architectural property of source-time snapshots, not an implementation bug — no amount of hints or guards fixes Option A. Option B removes the class of bug (artifact always equals source of truth at invocation time) and deletes the deferred .bashrc wiring requirement rather than implementing it. Option C preserves the bug class for zero new capability. Smallest robust design wins.

Artifact specification (binding for Builder):

Property Value
Location ${HOME}/.local/bin/<name>
Permissions 0755
Ownership marker Line 2 contains literal Managed by pos ai alias (grep target for all ownership checks)
Body set -euo pipefail + single exec pos ai <provider> ask --session <session>[ --system <%q prompt>] "$@"
Quoting Reuse _alias_quote_cmd verbatim (double-layer %q mechanics preserved per brief)
Atomic write mktemp in same dir → mvchmod 755 (same pattern as current _alias_regen)
Pre-commit validation bash -n on rendered wrapper; on failure warn + keep previous file
Secrets None inside (prompt is content, not credential)

[DECIDED]

Decision 2: Migration & back-compat

Regeneration trigger: _alias_sync() on every invocation

New internal function, called at the top of every subcommand dispatch entry (create, edit, remove, list, show, interactive menu) before the subcommand's own logic. It reconciles ~/.local/bin against the ENV file in both directions:

  1. For each ENV entry: render expected wrapper content; if target is missing or differs byte-wise → atomically install. This means:
    • first run after upgrade materializes wrappers for all existing aliases (one-time migration happens on any command, including a harmless list);
    • every create/edit/remove leaves artifacts consistent by construction;
    • hand-edited or half-deleted wrappers are silently healed (idempotent, cheap for realistic alias counts).
  2. Every executable in ~/.local/bin bearing our marker whose name is not in ENV → deleted (covers remove, covers manual ENV edits, covers the empty-set case).
  3. If ≥1 wrapper exists/installed and $HOME/.local/bin is absent from $PATH → loud warn with copy-paste fix (export PATH="$HOME/.local/bin:$PATH" + persist to ~/.profile). Wrappers are still written regardless.
  4. Legacy handling (below).

No public sync subcommand: every subcommand already syncs, so an explicit one adds surface without capability. POS_SUBCMDS header stays create edit remove list show.

Existing ai-aliases.sh: stop writing entirely — no deprecated shim

A shim keeps two activation truths alive, and the sourced-alias-still-shadows-wrapper scenario is precisely the reported failure mode (in interactive bash, aliases take precedence over PATH lookups). The .sh artifact must die, not fade.

On detecting SH_FILE, sync emits a warning block explaining that activation moved to ~/.local/bin/<name> scripts and that stale sourced aliases shadow them until cleaned. Then:

  • If line 13 of the file carry our generator marker (Auto-generated by pos ai alias) → auto-remove the file and print remediation for running shells: an unalias <names> line with names extracted from the .sh contents themselves (the stale file inventories its own definitions — including names no longer in ENV), plus "or simply start a new shell". Auto-remove is safe because (a) the file is regenerable output, not user data, (b) postinstall never shipped the source line, so nothing references it at startup, and (c) the conditional-source idiom ([ -f ] && source) tolerates absence even if a user wired it manually.
  • If the marker does not match (foreign/hand-built file) → leave untouched; advise manual review. Never delete files we didn't generate — same policy as wrapper collisions.

[PROVISIONAL nuance] The auto-remove guard currently checks only the generator-marker header; a user who appended private aliases into our generated file would lose them on upgrade-migration. Accepted risk: the file header says "do not hand-edit", likelihood is low, and the alternative (parsing full-file provenance) buys complexity the requirement doesn't need. Revisit only if a real case appears.

What carries over unchanged

  • ai-aliases.env format, location, chmod 600, comment conventions — untouched. Previously created aliases migrate with zero data conversion.
  • pos config compatibility: no env-key semantics touched.
  • All menu flows, name regex, non-tty guard behavior.

[DECIDED]

Decision 3: Edge cases & subcommand semantics

Name collisions with real binaries — refuse

Create-time check order (after existing ENV-duplicate redirect to edit):

  1. $HOME/.local/bin/<name> exists with marker → not a collision; sync will overwrite (regeneration path).
  2. $HOME/.local/bin/<name> exists without marker → refuse: err "File '~/.local/bin/<name>' already exists and was not created by pos ai alias — pick another name". Never silently overwrite foreign files.
  3. command -v <name> resolves to anything else on PATH (ls, git, gcc, …) → refuse with the conflicting path named.

No override flag. Shadowing an arbitrary binary is never a legitimate intent for an alias feature, a refusal error costs one rename, and a --force surface invites exactly the "surprise factor" this rework is meant to remove. Edit cannot collide (name is the record key); rename remains remove+create (Designer out-of-scope list already excludes renaming).

Empty result set → wrappers fully retracted

With zero ENV entries, sync deletes every marker-bearing wrapper in ~/.local/bin. list prints Aliases (0): as today. No empty husks left behind.

Provider adapter deleted later → runtime failure is already good enough

Wrapper execs pos ai <provider> …; if the adapter vanished, bin/pos-ai:121 errors: Unknown provider '<p>' — available: gemini openrouter. Actionable, names valid alternatives, zero changes to pos-ai. Sync does not prune wrappers whose provider directory entry disappeared (ENV is truth for existence; a temporarily missing adapter shouldn't silently eat user config). The provider picker at edit time only offers installed providers, so edit is the natural repair path.

Subcommand semantics under Option B

Subcommand Change
list Format unchanged; runs after sync so it always reflects disk truth
show <name> Adds one line: Wrapper: ~/.local/bin/<name> (or (not installed) if PATH check failed)
create Gains collision refusals above; success message replaces "Reload shell: source ~/.bashrc" with Available immediately: ~/.local/bin/<name> (+ PATH warning when applicable)
edit Unchanged flow; on save, sync refreshes the wrapper — change is live on next invocation (this kills the reported bug)
remove Unchanged confirm(default=n); success message notes the script was deleted from ~/.local/bin; add hint that running shells may need hash -r only if the name still autocompletes stale (rare; bash normally re-scans when a hashed file vanishes)
menu / -h Help text updated: activation = executable scripts in ~/.local/bin, no sourcing required

[DECIDED]


Decision 4: Scope fence for Builder

Approved outcome

Alias activation via marker-managed wrapper scripts in ~/.local/bin, synced against ai-aliases.env on every invocation, with legacy .sh auto-retirement.

In-scope components/files

File Allowed changes
bin/pos-ai-alias Replace _alias_regen() with _wrapper_path() + _wrapper_render() + _alias_sync(); keep _alias_quote_cmd mechanics verbatim; add _alias_check_path(); wire sync into all dispatch entries; collision checks in _alias_create; message deltas in create/edit/remove/show/usage; legacy .sh retirement block; SH_FILE constant retained solely for migration detection
bin/pos-system-uninstall Add marker-scan of ~/.local/bin (grep for Managed by pos ai alias) to the discovery list (~line 96 area) and removal pass (~line 261 area), mirroring the existing pos-ai-hook.sh pattern — closes uninstall hygiene
DOC/POS.md ai alias section: activation semantics, subcommand table unchanged otherwise (hand-maintained file)
DOC/HOWTO.md / relevant howto Row/section wording update if it mentions sourcing/reload
AGENT_TODO.md Move task to Done (dated) in same commit

Must NOT change

  • bin/pos-ai — any file byte.
  • ai-aliases.env format, fields, header comments, chmod 600.
  • lib/common.sh, lib/menu-lib.sh, lib/config-ui.sh.
  • postinstall.sh — the deferred .bashrc wiring stays unimplemented by design (obsoleted, not added).
  • # POS: / # POS_SUBCMDS: headers (description and subcommand set unchanged → no gen churn beyond none).
  • Menu structure, step counts, name regex, non-tty guard behavior (Designer spec remains authoritative).
  • Other categories' tools; entertainment plugins; completions (no flag/subcmd changes).

Architectural constraints

  1. Atomic writes only (mktemp+mv), never in-place truncation of live wrappers.
  2. Ownership established exclusively via the line-2 marker string; never delete/overwrite files failing the marker test.
  3. All output discipline per Designer spec: tables/results stdout, display/warnings stderr (log/warn/err).
  4. Sync must be idempotent and safe to run concurrently-lossy (single-user tool: last write wins, no locking).
  5. Wrapper body contains no secrets and no absolute paths except the pos lookup by name (PATH-resolved, consistent with old aliases).

Required verification (adversarial where it matters)

  1. bash -n bin/pos-ai-alias; make gen && make check && make lint ending 0 FAIL, 0 WARN.
  2. Quoting round-trip through the NEW artifact: prompts containing ', ", backtick, $(), %, \, unicode, leading/trailing spaces → create each; execute wrapper under a stubbed pos shim on a temp PATH capturing argv; assert --system arrives as exactly one intact word and passthrough args (assist "hi there") append correctly.
  3. Staleness kill-test: create assist(gemini) → run wrapper via shim → edit provider→openrouter → run again → argv shows openrouter with no shell reload (the regression test for the live failure).
  4. Sync idempotency: two consecutive runs → byte-identical artifacts, mtimes stable second run.
  5. Orphan retraction: delete an ENV line manually → next pos ai alias list removes that wrapper; empty ENV → zero owned wrappers remain.
  6. Collision tests: foreign file at ~/.local/bin/<name> → refused; marker file → refreshed; command -v conflict (e.g. gcc) → refused with path named.
  7. Legacy migration: plant prior-generator-format ai-aliases.sh with stale alias assist=…gemini… → any subcommand removes it, prints unalias assist remediation; plant foreign-content file → untouched, warned.
  8. PATH-absent: strip $HOME/.local/bin from PATH → loud warn, wrappers still written.
  9. Non-tty: pos ai alias (menu) still fails cleanly via menu_guard.
  10. Dead-provider runtime: wrapper pointing at removed adapter produces pos-ai:121 available-providers error (assert message quality manually once).

Explicitly out of scope

Rename operation; multi-line prompt input; alias import/export; public sync subcommand; completion headers; pos config integration; systemd/cron integration examples beyond help text.

Open risks

  • Users who sourced ai-aliases.sh into .bashrc manually keep a dead reference — harmless under the conditional-source idiom; warning text covers it.
  • ~/.local/bin absent from PATH in exotic shells (non-login ssh without postinstall) — mitigated by persistent warning + fix line.

[DECIDED]


Builder-ready step order

  1. Core rewrite in bin/pos-ai-alias: _wrapper_path, _wrapper_render (reuse _alias_quote_cmd; marker line 2; set -euo pipefail; exec … "$@"), _alias_check_path, _alias_sync (render-diff-install, orphan sweep, legacy block). Delete _alias_regen body (keep SH_FILE constant for migration).
  2. Wire _alias_sync into every dispatch entry before subcommand logic.
  3. Create-flow collision refusals (marker-aware, command -v check) + success-message swap ("Available immediately", drop reload hints everywhere including menu flows).
  4. Edit/remove/show/list/usage deltas per D3 table.
  5. bin/pos-system-uninstall: marker-scan additions in discovery + removal passes.
  6. Full verification suite (D4 list) — quoting round-trip and staleness kill-test are the acceptance gates.
  7. Docs (DOC/POS.md, HOWTO row) + AGENT_TODO.md Done entry; conventional commit (feat: or fix:).

Recommended next agent: Builder — scope is fully determined; no architectural choices remain. Suggest a Reviewer pass afterward focused on the marker-guard logic (the only place where a bug could delete/overwrite a foreign file).

Architect changes: this report only.