From e179c61b90b3c1f4545f834297783ffeba60bb63 Mon Sep 17 00:00:00 2001
From: Chumaniac <121109854+Chumaniac@users.noreply.github.com>
Date: Thu, 17 Sep 2026 20:17:38 +0800
Subject: [PATCH] docs(catalog): add IP-035 install ownership is not an update
permission
Add a State And Boundary entry for the case where the write target has a
different owner than the caller: an install LoopX does not own.
- name the classify-then-mutate rule over install_kind, owner,
loopx_apply_supported, and owner_upgrade_command
- pin the fail-closed unsupported_install_owner behavior: no pip guess,
no git pull on a live checkout, no archive snapshot replacing a checkout
- record the mirror-image smell of reporting success without writing
- reference loopx/self_update.py::_install_lifecycle and the four existing
mutation tests in tests/test_self_update_runtime_activation.py
Refs GH-C37
Signed-off-by: Chumaniac <121109854+Chumaniac@users.noreply.github.com>
---
docs/concepts/interaction-pattern-catalog.md | 96 +++++++++++++++++++-
1 file changed, 95 insertions(+), 1 deletion(-)
diff --git a/docs/concepts/interaction-pattern-catalog.md b/docs/concepts/interaction-pattern-catalog.md
index abf2d0c651..8ecaff4850 100644
--- a/docs/concepts/interaction-pattern-catalog.md
+++ b/docs/concepts/interaction-pattern-catalog.md
@@ -88,7 +88,7 @@ Map P0/P1 catalog rows to canary archetypes before picking commands:
| --- | --- | --- | --- | --- | --- |
| Work Routing | IP-001, IP-002, IP-003, IP-007, IP-008, IP-021, IP-029 | Hot-path route canary; Planning governance canary when cadence or repair is involved | `quota should-run`, `interaction_contract`, `work_lane_contract`, scheduler hint, handoff todo state | one eligible delivery fixture, one blocked/fallback fixture, one quiet or monitor fixture | agent turn routing is unsafe: it may spend, wait, notify, or choose fallback incorrectly |
| Human Decision | IP-004, IP-014, IP-017, IP-027, IP-030, IP-033 | Scoped decision canary; Product/readiness canary when first-screen human copy changes | user todos, decision scope, operator-gate/reward preview, deferred resume candidates | one concrete user ask, one scoped non-blocking gate, one preview-or-append dry run | humans may be asked the wrong question, or an agent may continue without the needed decision |
-| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
+| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032, IP-035 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, install ownership, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
| Evidence Lifecycle | IP-012, IP-015 | Evidence lifecycle canary; Product/readiness canary when evidence is rendered | external handle observation, benchmark lifecycle reducer, compact result projection | compact public-safe evidence fixture with raw-material exclusion assertions | progress evidence may be missing, double-counted, or represented with unsafe raw material |
| Planning Governance | IP-010, IP-013, IP-018, IP-024 | Planning governance canary; Hot-path route canary when cadence changes affect execution | stalled run history, autonomous replan obligation, repair delta, cadence hint, plan-to-todo writeback | two-turn stalled fixture plus repair/writeback delta assertion | the agent may keep planning in prose while the machine-visible frontier stays unchanged |
@@ -343,6 +343,7 @@ Projection, authority, write scope, and lease integrity.
| P1 | IP-028 | Connector Runtime Boundary | Connector/runtime owners | notify only if the required owner decision is missing | enforce runtime allow/deny policy before browser or API connector reads can autoload raw material |
| P1 | IP-031 | Manager Context Is Not Turn Authority | Manager connection owner | no interruption; retention is silent | retain group context only and act only on a provider-native mention, verified reply, or existing typed authority |
| P1 | IP-032 | Completed Work Archive With Durable Decision Retention | Archive selector plus controller | no interruption; preview-then-execute readback | treat archived done work as history, keep durable decisions authoritative, and never move another role's lane |
+| P1 | IP-035 | Install Ownership Is Not An Update Permission | Install lifecycle owner plus user | no silent mutation; report the owning installer and its command | classify the install before mutating it; when LoopX does not own it, hand back the owner-owned command instead of switching install channels |
### Evidence Lifecycle
@@ -2419,6 +2420,99 @@ quietly disagree with what the operator believes happened.
assertion that `--role user` leaves `Agent Todo` byte-identical is proposed
and not yet landed.
+#### IP-035 Install Ownership Is Not An Update Permission
+
+**Trigger**
+
+- `loopx update` or `loopx doctor` reports upgrade drift: `requires_upgrade=true`
+ or an install-freshness status that no longer matches the release manifest;
+- the active install is one of three owned kinds, `release_snapshot`,
+ `python_distribution`, or `live_checkout`, and only some of them are writable
+ by LoopX itself;
+- an agent, automation, or operator script wants the upgrade to actually happen
+ in this turn rather than be reported.
+
+**Expected behavior**
+
+An update is a write to an installation LoopX does not always own. Three rules
+keep "newer version available" from becoming "rewrite whatever is installed".
+
+1. **Classify before you mutate.** `install_lifecycle` names the
+ `install_kind`, the `owner` (`loopx_release_snapshot`,
+ `python_package_manager`, or `source_checkout`), `loopx_apply_supported`,
+ the `execution_driver`, and the `owner_upgrade_command`. No mutating step
+ runs before that classification exists.
+2. **An unowned install fails closed instead of guessing.** When
+ `loopx_apply_supported=false`, `update apply` returns `ok=false`,
+ `commands.apply=None`, `changes_applied=false`, and
+ `next_action.kind=use_installation_owner`. It must not substitute a
+ different installer, so a `custom-manager` Python environment never gets a
+ guessed `pip install`; it must not run `git pull` on a `live_checkout`; and
+ it must not replace a source checkout with an archive snapshot.
+3. **Report the owner's command, then stop.** The payload carries
+ `owner_upgrade_command` (or `plan.install_command`) and
+ `post_update_validation=loopx doctor`. When no owner command exists, the
+ correct output is "this install is owned by X" with
+ `next_action.command=None`, not a silent success and not a fabricated
+ command.
+
+IP-006 owns the case where a required *write scope* is not projected at all.
+IP-035 is the sibling case: the scope is known, but the write target itself has
+a different owner. IP-030 owns revision-guarded preview/apply for machine
+configuration; the same "exact target plus explicit approval" discipline applies
+here, except the approval belongs to the install owner rather than to LoopX.
+
+**Visual Model**
+
+```mermaid
+flowchart TD
+ A["requires_upgrade / freshness drift"] --> B{"install_lifecycle.install_kind"}
+ B -->|"release_snapshot"| C{"POSIX?"}
+ C -->|"yes"| D["loopx_apply_supported=true
atomic snapshot replace"]
+ C -->|"no"| E["owner: install-windows.ps1
apply=None"]
+ B -->|"python_distribution"| F{"installer is pip or pipx?"}
+ F -->|"yes"| G["driver python_pip / python_pipx
upgrade the owning environment"]
+ F -->|"no"| H["owner: package manager
no pip guess, apply=None"]
+ B -->|"live_checkout"| I["owner: source_checkout
no git pull, no channel switch"]
+ D --> J["loopx doctor revalidation"]
+ E --> K["use_installation_owner
ok=false, changes_applied=false"]
+ H --> K
+ I --> K
+```
+
+**Bad smell**
+
+An agent sees `requires_upgrade=true` and reaches for the installer it knows
+best: `pip install --upgrade loopx` inside a checkout install, `pipx upgrade`
+against an environment LoopX does not own, or `git pull` on a contributor
+workspace. The result is a second LoopX in a different environment while `loopx`
+on `PATH` still resolves to the old one, or a source checkout silently converted
+into an archive snapshot. The operator experience is "doctor says I am behind"
+forever, plus an environment nobody can attribute ownership for.
+
+The mirror-image smell is reporting success without writing:
+`unsupported_install_owner`, an `apply=None` command, or a `None`
+`next_action.command` is rendered as "update complete", so the same drift is
+rediscovered next turn. A third smell is a fixture that only ever exercises the
+pip path, so no test can tell an owned install from an unowned one.
+
+**Validation**
+
+- `tests/test_self_update_runtime_activation.py` owns the mutation and negative
+ cases: `test_live_checkout_apply_never_mutates_git_or_switches_install_channels`,
+ `test_unknown_package_manager_apply_fails_without_guessing_pip`,
+ `test_python_distribution_apply_uses_the_owning_interpreter_pip`,
+ `test_pipx_distribution_apply_preserves_the_pipx_environment`, and
+ `test_windows_execute_update_fails_closed_without_launching_bash`.
+- `tests/test_doctor_install_freshness.py` pins install-kind classification
+ (`live_checkout`, `python_distribution`) behind the freshness contract.
+- `loopx/self_update.py::_install_lifecycle` owns the kind, owner, driver, and
+ `owner_upgrade_command` predicate; `loopx/doctor.py` supplies the install
+ snapshot it classifies.
+- `examples/loopx-update-smoke.py` and `docs/guides/installing-loopx.md` own the
+ operator-facing update, activation, and recovery path.
+- `examples/interaction-pattern-catalog-smoke.py` protects this entry.
+
### Evidence Lifecycle
#### IP-012 External Evidence Observation