# AgentNet v0.1.45 Lifecycle and Upgrade Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Provide a crash-safe v0.1.44→v0.1.45 upgrade and a resumable package-owned endpoint lifecycle that preserves existing enrollment and asks before harness restart.

**Architecture:** Add one schema-v7 endpoint lifecycle record keyed by exact domain and harness, then compose it with existing setup, enrollment, communication-scope, and supervisor services. The lifecycle coordinator derives status from durable facts rather than storing a second authority state. Upgrade journals preserve exact v0.1.44 files and database state until the new services and endpoint activation have been proven.

**Tech Stack:** Python 3.13–3.14, Pydantic, SQLite/PostgreSQL, systemd, npm package launcher, pytest.

## Global Constraints

- Version is exactly `0.1.45`; only v0.1.44 is an accepted package-state predecessor for this release.
- Database migration is exactly v6→v7 and remains contiguous in SQLite and PostgreSQL.
- Existing server/laptop principal IDs, harness IDs, credential IDs/epochs, communication scope, mailbox cursors, and events are byte-preserved or semantically unchanged.
- Enrollment and communication authorization remain separate facts; lifecycle presentation may combine them but must not.
- Missing, ambiguous, stale, revoked, or cross-domain endpoint state fails closed.
- A restart is represented as `restart_required` until the user explicitly performs it; AgentNet never kills or restarts the active harness.
- Installation and update use the package-owned user-level path and never instruct the user to use `sudo`.
- Affected IDs: `ARC-001`, `ARC-002`, `ID-004..009`, `AUTH-001..004`, `COM-001..003`, `AVL-001..007`, `UX-001..004`, `SEC-003..006`, `OPS-003..006`.

---

### Task 1: Add the complete schema-v7 release contract

**Files:**
- Create: `src/agentnet/storage/endpoint_lifecycle_schema.py`
- Create: `src/agentnet/storage/collaboration_scope_schema.py`
- Create: `src/agentnet/storage/artifact_transfer_schema.py`
- Create: `src/agentnet/storage/invitation_link_schema.py`
- Create: `src/agentnet/storage/release_v7_schema.py`
- Modify: `src/agentnet/storage/sqlite.py:538-607`
- Modify: `src/agentnet/storage/migrations/__init__.py:119-150`
- Modify: `src/agentnet/storage/postgres.py:312-536`
- Modify: `src/agentnet/storage/postgres_catalog.py`
- Test: `tests/storage/test_endpoint_lifecycle_migration.py`
- Test: `tests/storage/test_postgres_migrations.py`

**Interfaces:**
- Consumes: existing `harnesses`, `credentials`, `communication_scopes`, artifact, invitation, and mailbox tables.
- Produces: four focused schema fragments, `RELEASE_V7_SCHEMA_VERSION = 7`, `RELEASE_V7_SCHEMA`, SQLite `SCHEMA_V7`, and PostgreSQL migration 7 named `communication_collaboration_release`.

- [ ] **Step 1: Write failing SQLite migration tests**

```python
def test_v6_to_v7_preserves_identity_scope_and_mailbox(tmp_path):
    store = open_v6_fixture(tmp_path)
    before = exact_security_and_message_snapshot(store)
    store.close()

    upgraded = SQLiteStore(tmp_path / "agentnet.db", migrate=True)

    assert exact_security_and_message_snapshot(upgraded) == before
    assert required_relations(upgraded) >= {
        "endpoint_lifecycle",
        "collaboration_scopes",
        "collaboration_scope_members",
        "artifact_transfers",
        "artifact_transfer_recipients",
        "invitation_links",
    }
    assert current_schema_version(upgraded) == 7


def test_v7_rejects_duplicate_profile_binding(store):
    insert_endpoint(store, profile_key="omp:default", harness_id="harness-a")
    with pytest.raises(sqlite3.IntegrityError):
        insert_endpoint(store, profile_key="omp:default", harness_id="harness-b")

```

- [ ] **Step 2: Verify the migration tests fail**

Run:
```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/storage/test_endpoint_lifecycle_migration.py
```
Expected: FAIL because schema v7 and its six release relations do not exist.

- [ ] **Step 3: Implement strict DDL and catalogs**

```python
ENDPOINT_LIFECYCLE_SCHEMA_VERSION = 7
ENDPOINT_LIFECYCLE_SCHEMA = """
CREATE TABLE IF NOT EXISTS endpoint_lifecycle (
    domain_id TEXT NOT NULL,
    harness_id TEXT NOT NULL,
    principal_id TEXT NOT NULL,
    harness_kind TEXT NOT NULL CHECK (harness_kind IN ('omp','pi','claude','codex','antigravity','server')),
    profile_key TEXT NOT NULL,
    state TEXT NOT NULL CHECK (state IN ('ready_to_connect','waiting_for_approval','enrolled','access_ready','restart_required','connected','blocked')),
    adapter_generation INTEGER NOT NULL CHECK (adapter_generation >= 1),
    mailbox_cursor INTEGER NOT NULL DEFAULT 0 CHECK (mailbox_cursor >= 0),
    capability_root_digest TEXT,
    state_reason TEXT NOT NULL,
    revision INTEGER NOT NULL CHECK (revision >= 1),
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (domain_id, harness_id),
    UNIQUE (domain_id, harness_kind, profile_key),
    FOREIGN KEY (domain_id, harness_id) REFERENCES harnesses(domain_id, harness_id)
);
CREATE INDEX IF NOT EXISTS idx_endpoint_lifecycle_principal
ON endpoint_lifecycle(domain_id, principal_id, state);
"""
```

Define the remaining fragments with strict checks and foreign keys:

```python
COLLABORATION_SCOPE_SCHEMA = """
CREATE TABLE collaboration_scopes (
    scope_id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, owner_principal_id TEXT NOT NULL,
    allowed_actions_json TEXT NOT NULL, allowed_resource_prefixes_json TEXT NOT NULL,
    policy_revision INTEGER NOT NULL, domain_revocation_epoch INTEGER NOT NULL,
    expires_at INTEGER, state TEXT NOT NULL CHECK (state IN ('active','revoked','expired')),
    revision INTEGER NOT NULL CHECK (revision >= 1), proposal_digest TEXT NOT NULL,
    created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
);
CREATE TABLE collaboration_scope_members (
    scope_id TEXT NOT NULL, harness_id TEXT NOT NULL, joined_revision INTEGER NOT NULL,
    removed_revision INTEGER, PRIMARY KEY (scope_id, harness_id),
    FOREIGN KEY (scope_id) REFERENCES collaboration_scopes(scope_id)
);
"""

ARTIFACT_TRANSFER_SCHEMA = """
CREATE TABLE artifact_transfers (
    transfer_id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, sender_harness_id TEXT NOT NULL,
    idempotency_key TEXT NOT NULL, request_digest TEXT NOT NULL, artifact_id TEXT, event_id TEXT,
    expected_digest TEXT NOT NULL, expected_size INTEGER NOT NULL, media_type TEXT NOT NULL,
    classification TEXT NOT NULL, state TEXT NOT NULL, revision INTEGER NOT NULL,
    created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
    UNIQUE (domain_id, sender_harness_id, idempotency_key)
);
CREATE TABLE artifact_transfer_recipients (
    transfer_id TEXT NOT NULL, harness_id TEXT NOT NULL, custody_state TEXT NOT NULL,
    PRIMARY KEY (transfer_id, harness_id),
    FOREIGN KEY (transfer_id) REFERENCES artifact_transfers(transfer_id)
);
"""

INVITATION_LINK_SCHEMA = """
CREATE TABLE invitation_links (
    invitation_id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE,
    encrypted_offer TEXT NOT NULL, offer_digest TEXT NOT NULL, invited_email_sha256 TEXT NOT NULL,
    sponsor_principal_id TEXT NOT NULL, sponsor_harness_id TEXT NOT NULL,
    sponsor_credential_id TEXT NOT NULL, sponsor_credential_epoch INTEGER NOT NULL,
    policy_revision INTEGER NOT NULL, domain_revocation_epoch INTEGER NOT NULL,
    state TEXT NOT NULL CHECK (state IN ('issued','reserved','consumed','revoked','expired')),
    max_uses INTEGER NOT NULL CHECK (max_uses = 1), use_count INTEGER NOT NULL CHECK (use_count IN (0,1)),
    expires_at INTEGER NOT NULL, revision INTEGER NOT NULL, created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL
);
"""

RELEASE_V7_SCHEMA_VERSION = 7
RELEASE_V7_SCHEMA = (
    ENDPOINT_LIFECYCLE_SCHEMA
    + COLLABORATION_SCOPE_SCHEMA
    + ARTIFACT_TRANSFER_SCHEMA
    + INVITATION_LINK_SCHEMA
)
```

Use the repository’s existing `INTEGER`→`BIGINT` PostgreSQL conversion for this single frozen migration 7. Extend exact catalog checks for every relation, index, check, and foreign key; do not weaken older migration checksum verification.

- [ ] **Step 4: Run SQLite and conditional PostgreSQL tests**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/storage/test_endpoint_lifecycle_migration.py tests/storage/test_postgres_migrations.py
```
Expected: PASS. PostgreSQL-only cases may skip only when the dedicated mutation-authorized test DSN is absent.

- [ ] **Step 5: Commit**

```bash
git add src/agentnet/storage tests/storage
git commit -m "feat(storage): add exact endpoint lifecycle schema"
```

### Task 2: Implement derived endpoint lifecycle coordination

**Files:**
- Create: `src/agentnet/operations/endpoint_lifecycle.py`
- Modify: `src/agentnet/core/app.py:144-698`
- Modify: `src/agentnet/operations/__init__.py`
- Test: `tests/operations/test_endpoint_lifecycle.py`

**Interfaces:**
- Consumes: `StoreBackend`, `VerifiedActor`, current credential binding, communication-scope state, and endpoint row.
- Produces:
```python
class EndpointActivationState(str, Enum): ...
class EndpointLifecycleStatus(BaseModel): ...
class EndpointLifecycleService:
    def register_existing(self, *, actor: VerifiedActor, harness_kind: str, profile_key: str) -> EndpointLifecycleStatus: ...
    def status(self, *, endpoint_id: str) -> EndpointLifecycleStatus: ...
    def reconcile(self, *, endpoint_id: str) -> EndpointLifecycleStatus: ...
    def request_activation(self, *, actor: VerifiedActor, expected_revision: int) -> EndpointLifecycleStatus: ...
    def record_user_restart(self, *, actor: VerifiedActor, expected_generation: int, process_measurement: str) -> EndpointLifecycleStatus: ...
```

- [ ] **Step 1: Write failing lifecycle tests**

```python
def test_access_ready_requires_explicit_restart_before_connected(service, enrolled_actor):
    status = service.register_existing(actor=enrolled_actor, harness_kind="omp", profile_key="default")
    assert status.state == EndpointActivationState.ACCESS_READY

    requested = service.request_activation(actor=enrolled_actor, expected_revision=status.revision)
    assert requested.state == EndpointActivationState.RESTART_REQUIRED

    with pytest.raises(ConflictError, match="explicit user restart"):
        service.record_user_restart(
            actor=enrolled_actor,
            expected_generation=requested.adapter_generation,
            process_measurement="wrong-process",
        )


def test_revoked_credential_reconciliation_blocks_endpoint(service, connected_actor):
    revoke_exact_credential(connected_actor.credential_id)
    assert service.reconcile(endpoint_id=connected_actor.harness_id).state == EndpointActivationState.BLOCKED
```

- [ ] **Step 2: Verify tests fail**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_endpoint_lifecycle.py
```
Expected: FAIL because `EndpointLifecycleService` is absent.

- [ ] **Step 3: Implement the strict model and transitions**

```python
class EndpointActivationState(str, Enum):
    READY_TO_CONNECT = "ready_to_connect"
    WAITING_FOR_APPROVAL = "waiting_for_approval"
    ENROLLED = "enrolled"
    ACCESS_READY = "access_ready"
    RESTART_REQUIRED = "restart_required"
    CONNECTED = "connected"
    BLOCKED = "blocked"

_ALLOWED = {
    EndpointActivationState.READY_TO_CONNECT: {EndpointActivationState.WAITING_FOR_APPROVAL},
    EndpointActivationState.WAITING_FOR_APPROVAL: {EndpointActivationState.ENROLLED, EndpointActivationState.BLOCKED},
    EndpointActivationState.ENROLLED: {EndpointActivationState.ACCESS_READY, EndpointActivationState.BLOCKED},
    EndpointActivationState.ACCESS_READY: {EndpointActivationState.RESTART_REQUIRED, EndpointActivationState.BLOCKED},
    EndpointActivationState.RESTART_REQUIRED: {EndpointActivationState.CONNECTED, EndpointActivationState.BLOCKED},
    EndpointActivationState.CONNECTED: {EndpointActivationState.RESTART_REQUIRED, EndpointActivationState.BLOCKED},
    EndpointActivationState.BLOCKED: {EndpointActivationState.READY_TO_CONNECT},
}
```

Every mutation must use `(domain_id, harness_id, revision)` compare-and-swap and re-check current credential/domain revocation and collaboration authority in the same transaction. `reconcile` may narrow state but never create positive authority.

- [ ] **Step 4: Compose into Core and verify**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_endpoint_lifecycle.py tests/core/test_app.py
```
Expected: PASS; Core exposes one `endpoint_lifecycle` service.

- [ ] **Step 5: Commit**

```bash
git add src/agentnet/operations/endpoint_lifecycle.py src/agentnet/operations/__init__.py src/agentnet/core/app.py tests/operations/test_endpoint_lifecycle.py
git commit -m "feat(operations): derive endpoint activation lifecycle"
```

### Task 3: Add resumable user-level install/update orchestration

**Files:**
- Create: `src/agentnet/operations/client_setup.py`
- Modify: `src/agentnet/cli.py:5277-5374`
- Modify: `npm/bin/agentnet.mjs`
- Modify: `npm/scripts/check-package.mjs`
- Test: `tests/operations/test_client_setup.py`
- Test: `tests/cli/test_client_setup.py`

**Interfaces:**
- Consumes: `EndpointLifecycleService` and existing guided join/OIDC/passkey coordinators.
- Produces CLI commands `agentnet setup`, `agentnet setup status`, and `agentnet setup continue`; normal users invoke none manually because harness package activation calls them.

- [ ] **Step 1: Write failing orchestration tests**

```python
def test_setup_resumes_existing_v0144_enrollment_without_new_join(tmp_path, existing_identity):
    result = run_setup(tmp_path, existing_identity=existing_identity)
    assert result.identity_created is False
    assert result.harness_id == existing_identity.actor.harness_id
    assert result.next_action == "restart_your_agent"


def test_setup_never_restarts_harness(runner, existing_identity):
    result = runner.invoke(["setup", "continue"])
    assert result.exit_code == 0
    assert "Restart your agent to enable AgentNet" in result.stdout
    assert runner.signals_sent == []
```

- [ ] **Step 2: Verify tests fail**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_client_setup.py tests/cli/test_client_setup.py
```
Expected: FAIL because the setup coordinator and commands are absent.

- [ ] **Step 3: Implement a resumable coordinator**

```python
class SetupNextAction(str, Enum):
    OPEN_BROWSER = "open_browser"
    WAIT_FOR_APPROVAL = "wait_for_approval"
    RESTART_YOUR_AGENT = "restart_your_agent"
    CONNECTED = "connected"
    ADMIN_HELP = "administrator_help"

class ClientSetupResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    endpoint_id: str
    state: EndpointActivationState
    next_action: SetupNextAction
    public_url: AnyHttpUrl | None = None
    identity_created: bool
```

Persist only owner-private opaque continuation and durable Core state. Derive enrolled identity from the current credential; reject ambiguous profiles instead of choosing newest/last-active. The npm launcher uses the packaged Python runtime and does not modify shell profiles or request elevated privileges.

- [ ] **Step 4: Verify orchestration and packed metadata**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_client_setup.py tests/cli/test_client_setup.py
npm run check:package
```
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/agentnet/operations/client_setup.py src/agentnet/cli.py npm/bin/agentnet.mjs npm/scripts/check-package.mjs tests/operations/test_client_setup.py tests/cli/test_client_setup.py
git commit -m "feat(setup): add resumable client onboarding"
```

### Task 4: Extend server setup with honest v0.1.44 rollback

**Files:**
- Modify: `src/agentnet/operations/server_setup.py:149-188,1758-1816,2176-2384,3734-4212`
- Modify: `tests/operations/test_server_setup.py`
- Modify: `tests/operations/test_server_setup_recovery.py`
- Modify: `scripts/ci/ordinary-server-upgrade-e2e.sh`
- Modify: `.github/workflows/server-setup-upgrade-e2e.yml`

**Interfaces:**
- Consumes: exact v0.1.44 five-unit setup marker and schema-v6 database.
- Produces: one allowed v0.1.44→v0.1.45 marker profile, exact file/config/database preconditions, rollback journal, and post-upgrade endpoint lifecycle evidence.

- [ ] **Step 1: Add failing upgrade and injected-failure cases**

```python
def test_v0144_upgrade_preserves_exact_enrollment_and_messages(setup_fixture):
    before = setup_fixture.security_and_message_snapshot()
    result = apply_v0145_upgrade(setup_fixture)
    assert result["endpoint_lifecycle"] == "restart_required"
    assert setup_fixture.security_and_message_snapshot() == before


@pytest.mark.parametrize("phase", ["after_units", "after_migration", "after_core_restart"])
def test_upgrade_failure_rolls_back_exact_v0144_state(setup_fixture, phase):
    before = setup_fixture.full_upgrade_snapshot()
    with pytest.raises(ServerSetupError):
        apply_v0145_upgrade(setup_fixture, fail_after=phase)
    assert setup_fixture.full_upgrade_snapshot() == before
```

- [ ] **Step 2: Verify cases fail**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_server_setup.py tests/operations/test_server_setup_recovery.py -k 'v0144 or endpoint_lifecycle'
```
Expected: FAIL because v0.1.45 is not an allowed exact target.

- [ ] **Step 3: Add the single supported package transition**

```python
_FORWARD_ONLY_SETUP_UPGRADES = frozenset({
    # retain existing released transitions
    ("0.1.44", "0.1.45"),
})
```

Journal old marker, units, Core config, schema version, migration catalog, and endpoint-row absence before the first write. Rollback restores only exact journaled bytes and schema-v6 state; unexpected concurrent changes block rollback and preserve evidence.

- [ ] **Step 4: Run focused and real local-service upgrade tests**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q tests/operations/test_server_setup.py tests/operations/test_server_setup_recovery.py
UV_CACHE_DIR=/tmp/uv-cache scripts/ci/ordinary-server-upgrade-e2e.sh
```
Expected: PASS with an exact v0.1.44 source, v0.1.45 target, preserved identity/message snapshot, and no residual old process or temporary secret.

- [ ] **Step 5: Commit**

```bash
git add src/agentnet/operations/server_setup.py tests/operations/test_server_setup.py tests/operations/test_server_setup_recovery.py scripts/ci/ordinary-server-upgrade-e2e.sh .github/workflows/server-setup-upgrade-e2e.yml
git commit -m "feat(setup): upgrade v0.1.44 endpoints safely"
```

### Task 5: Update lifecycle documentation without promoting gates

**Files:**
- Modify: `README.md`
- Modify: `docs/implementation-guide.md`
- Modify: `docs/ARCHITECTURE.md`
- Modify: `docs/SCHEMAS_INTERFACES.md`
- Modify: `REQUIREMENTS_STATUS.md`

**Interfaces:**
- Consumes: observed focused and upgrade-test output.
- Produces: plain-language install/update/restart behavior and exact schema-v7 compatibility statement.

- [ ] **Step 1: Document only observed states**

Use this exact user-facing vocabulary:
```text
Ready to connect
Approve with passkey
Waiting for approval
Agent enrolled
Access ready
Restart your agent to enable AgentNet
Connected
Expired — start again
Wrong work account
Could not connect
Needs administrator help
```

- [ ] **Step 2: Record compatibility and rollback**

Document v0.1.44 as the only supported predecessor for this release, schema v6→v7, no re-enrollment on successful upgrade, and exact rollback limitations. Do not claim broader N/N-1 support than the tested transition.

- [ ] **Step 3: Run documentation-sensitive verification**

```bash
UV_CACHE_DIR=/tmp/uv-cache uv run python scripts/verify_release.py
npm run check:package
```
Expected: PASS; this task does not alter release-manifest or evidence status.

- [ ] **Step 4: Commit**

```bash
git add README.md docs/implementation-guide.md docs/ARCHITECTURE.md docs/SCHEMAS_INTERFACES.md REQUIREMENTS_STATUS.md
git commit -m "docs: describe v0.1.45 endpoint lifecycle"
```
