18 KiB
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
listshowed openrouter (live failure). - Activation required a
.bashrcsource 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()writesai-aliases.sh; success messages at :308 say "Reload shell: source ~/.bashrc".postinstall.sh:80— PATH export already includes$HOME/.local/binon pos-managed machines (Debian default~/.profilealso 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 yieldserr "Unknown provider '$p' — available: …"(good runtime failure quality, no pos-ai change needed).- Prior architect report D5/D10 chose
.sh+.bashrcwiring; 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
.bashrcwiring (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:
(empty prompt → omit
#!/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> "$@"--system; prompt embedded via existing double-layerprintf %qmechanics, 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
hashcaching entirely (bash caches paths, not contents) and new names are found on first PATH scan. - Costs: PATH-presence dependency (
~/.local/binmust be on PATH — already guaranteed bypostinstall.sh:80and 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 → mv → chmod 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:
- 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).
- first run after upgrade materializes wrappers for all existing aliases (one-time migration happens on any command, including a harmless
- Every executable in
~/.local/binbearing our marker whose name is not in ENV → deleted (covers remove, covers manual ENV edits, covers the empty-set case). - If ≥1 wrapper exists/installed and
$HOME/.local/binis absent from$PATH→ loudwarnwith copy-paste fix (export PATH="$HOME/.local/bin:$PATH"+ persist to~/.profile). Wrappers are still written regardless. - 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 1–3 of the file carry our generator marker (
Auto-generated by pos ai alias) → auto-remove the file and print remediation for running shells: anunalias <names>line with names extracted from the.shcontents 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.envformat, location, chmod 600, comment conventions — untouched. Previously created aliases migrate with zero data conversion.pos configcompatibility: 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):
$HOME/.local/bin/<name>exists with marker → not a collision; sync will overwrite (regeneration path).$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.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.envformat, fields, header comments, chmod 600.lib/common.sh,lib/menu-lib.sh,lib/config-ui.sh.postinstall.sh— the deferred.bashrcwiring 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
- Atomic writes only (mktemp+mv), never in-place truncation of live wrappers.
- Ownership established exclusively via the line-2 marker string; never delete/overwrite files failing the marker test.
- All output discipline per Designer spec: tables/results stdout, display/warnings stderr (
log/warn/err). - Sync must be idempotent and safe to run concurrently-lossy (single-user tool: last write wins, no locking).
- Wrapper body contains no secrets and no absolute paths except the
poslookup by name (PATH-resolved, consistent with old aliases).
Required verification (adversarial where it matters)
bash -n bin/pos-ai-alias;make gen && make check && make lintending0 FAIL, 0 WARN.- Quoting round-trip through the NEW artifact: prompts containing
',", backtick,$(),%,\, unicode, leading/trailing spaces → create each; execute wrapper under a stubbedposshim on a temp PATH capturing argv; assert--systemarrives as exactly one intact word and passthrough args (assist "hi there") append correctly. - 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). - Sync idempotency: two consecutive runs → byte-identical artifacts, mtimes stable second run.
- Orphan retraction: delete an ENV line manually → next
pos ai alias listremoves that wrapper; empty ENV → zero owned wrappers remain. - Collision tests: foreign file at
~/.local/bin/<name>→ refused; marker file → refreshed;command -vconflict (e.g.gcc) → refused with path named. - Legacy migration: plant prior-generator-format
ai-aliases.shwith stalealias assist=…gemini…→ any subcommand removes it, printsunalias assistremediation; plant foreign-content file → untouched, warned. - PATH-absent: strip
$HOME/.local/binfrom PATH → loud warn, wrappers still written. - Non-tty:
pos ai alias(menu) still fails cleanly viamenu_guard. - Dead-provider runtime: wrapper pointing at removed adapter produces
pos-ai:121available-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.shinto.bashrcmanually keep a dead reference — harmless under the conditional-source idiom; warning text covers it. ~/.local/binabsent from PATH in exotic shells (non-login ssh without postinstall) — mitigated by persistent warning + fix line.
[DECIDED]
Builder-ready step order
- 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_regenbody (keep SH_FILE constant for migration). - Wire
_alias_syncinto every dispatch entry before subcommand logic. - Create-flow collision refusals (marker-aware,
command -vcheck) + success-message swap ("Available immediately", drop reload hints everywhere including menu flows). - Edit/remove/show/list/usage deltas per D3 table.
bin/pos-system-uninstall: marker-scan additions in discovery + removal passes.- Full verification suite (D4 list) — quoting round-trip and staleness kill-test are the acceptance gates.
- Docs (
DOC/POS.md, HOWTO row) +AGENT_TODO.mdDone entry; conventional commit (feat:orfix:).
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.