# Site-scoped GoChain package delivery

`yeti3.network.packages.PackageAgent` consumes the GoChain delivery protocol on
fixed `https://license.gochain.space`. Instantiate it in the site's isolated
worker with its node token, a persistent `ModuleInstaller`, real lifecycle
adapters, a persistent private journal path, and a `health_check(installed)`
callable returning exactly `True` only after a runtime check. Enable CALMF and
poll `run_once()` every 30–60 seconds with bounded backoff on network failures.
Do not run this inside an HTTP request or the licensing heartbeat worker.

The agent announces its concrete adapters before claiming. GoChain refuses new
purchases when the site has not announced within three minutes or lacks a
required adapter. Purchase and installation queue commit with the token ledger.

Readiness defaults to the legacy `protocol_version=1`. After the central server
supports protocol 2, pass `PackageAgent(..., protocol_version=2)` to advertise
`static` and `templates` separately, alongside `migrations`, `routes`,
`permissions`, `tasks`, `health_checks`. The request shape remains
`{"protocol_version":2,"capabilities":["static"]}` for a static-only host.
There is no automatic downgrade on server rejection.

A concrete asset adapter supports both kinds unless it explicitly sets
`supported_asset_kinds=frozenset({"static"})` (or another subset of
`{"static", "templates"}`). Readiness and local preflight use the same declaration;
unsupported templates fail before downloading or installing anything. Invalid
kind declarations fail closed. With protocol 1, a restricted adapter advertises
no `assets` capability because v1 cannot represent its restriction. Hosts with
restricted adapters must explicitly enable v2 only after central compatibility
is available; publishing SDK changes alone does not deploy or enable it.

Delivery streams an authenticated archive to a private temporary directory,
verifies its exact SHA-256 and size, rejects ZIP traversal, links, duplicate
paths and excessive expansion, and checks the embedded manifest against the
immutable release. The SDK independently validates the whole dependency and
capability graph and orders it topologically. Every engine requirement,
lifecycle adapter, existing payload checksum and bundled acceptance suite is
checked before the first installation. A required lifecycle operation cannot
use a no-op adapter, including `PlannedMigrationAdapter` with its default
`NoopMigrationBackend`. An adapter may also explicitly declare `supported=False`.

Each newly installed package executes bundled unittest acceptance tests and the
site's health probe inside a CALMF span. The report records artifact identities,
test-output digest and CALMF call IDs after reading the committed successful
terminal event from the local spool (`calmf_scope=local_spool`). This does not
prove central ingestion. Zero-test, skipped and expected-failure acceptance
suites are rejected. These are **site-reported evidence**, not
remote attestation. Successful reports must cover every package in the plan.

On failure, newly added packages are uninstalled through SDK lifecycle and asset
ownership handling. Failed rollback needs operator attention. The private
journal makes reporting retryable without reinstallation. Preparation can be
restarted; a durably recorded installed module resumes verification without
installing twice. An interrupted mutation or rollback reports NEEDS_ATTENTION;
it never silently reruns migrations. Protect the journal directory and node token using
the site's service account. Only operator-reviewed packages may be installed;
package code and acceptance tests execute with that service account's rights.

Current boundaries: differing already-installed payloads require a reviewed
upgrade plan; the agent intentionally refuses to overwrite them. Cross-package
database rollback depends on transactional adapters supplied by the site.
Dependency use is included in the root purchase bundle; it does not create
separate sales of dependency products. Subscription renewal is an explicit
purchase after expiry, not an automatic recurring charge. No runtime adapter is
automatically installed on preexisting sites. Production adoption requires
configuring and testing each site's health probe and lifecycle adapters.

## Durable worker and progress contract

### Node preview receipts before purchase (SDK contract)

This SDK implements the agreed central preview contract; central endpoint and
purchase-gate rollout is a separate prerequisite, not established by SDK unit
tests. Enable with `protocol_version=2, preflight_enabled=True` only when those
routes are available. `run_once()` then services a preview before claiming an
installation; an active installation journal takes priority. A host can also
call `run_preflight_once()` explicitly with protocol 2. Unsupported endpoints
are not silently downgraded to installation without a receipt.

Owner `POST /api/network/v1/projects/:project/packages/preflight` accepts
`{site_id,product_id,release_id,configuration:{}}`. The agreed central response
queues a persistent preview (`preview_id`, `status:QUEUED`, `eligible:false`);
owner `GET /api/network/v1/projects/:project/packages/preflights/:id` polls it.
Node `POST /api/network/v1/packages/preflights/claim {}` returns `preview:null`
or a preview containing `id`, `claim_token`, `site_id`, `release_id`, `plan`,
`plan_sha256`, `configuration:{}`, `configuration_sha256`, and RFC3339 UTC
`expires_at`. It grants no artifact download entitlement.

Node `POST /api/network/v1/packages/preflights/:id/report` sends exactly:

```json
{"claim_token":"...","plan_sha256":"...","configuration_sha256":"...","compatible":true,"scope":"manifest_only","state_sha256":"...","profile_sha256":"...","blockers":[]}
```

Checks cover full manifests, graph/capabilities, engine and observed host
requirements, concrete lifecycle support, bundle download budget and available
temporary capacity. There is no archive download, module import, acceptance
execution or runtime mutation. Source inventory, existing candidate modules
without proven artifact identity, unknown requirements and expired/mismatched
previews are rejected. Matching an installed version alone is insufficient to
match its payload checksum to a release ZIP hash. Failed checks report bounded
machine blocker codes, not arbitrary exception text, host paths or credentials.
`manifest_only` never means ZIP integrity, tests or health were verified.

The active `<journal>.preflight.json` stores the claim and exact report privately
for retry. Successful report acknowledgement removes it. An interrupted check
can be repeated; a report transport failure retries identical bytes/fields even
if the host changes, leaving expiry enforcement to central. A permanently
rejected/expired report remains for operator reconciliation rather than
silently claiming another preview. Central must enforce five-minute claim and
receipt TTLs, a ten-minute absolute preview TTL, and no extension on replay.

Hash encoding is SHA-256 of UTF-8 JSON with recursively sorted keys, compact
separators, unescaped Unicode, and integer numbers only. Configuration is always
`{}` in this version. `state_sha256` binds the verified registry revision and
sorted module identities/versions/checksums/dependencies/capabilities.
`profile_sha256` binds engine, protocol, concrete capabilities, Python/OS/runtime
versions, observed host capability names and operator bundle limits. Available
resource amounts are excluded from the stable profile hash and checked freshly
against requirements instead; a disk-space fluctuation is not a runtime change.

The agreed central purchase flow requires `preflight_id` for package products
and consumes the same tenant/site/root-release/pinned-plan compatible receipt
atomically before debit. Installation claim carries
`preflight:{id,scope,plan_sha256,configuration_sha256,state_sha256,profile_sha256}`.
The SDK checks it before download and again after preparation before mutation.
Preview-enabled hosts reject claims without it. Resume after owned mutations
uses the existing durable job revision checks because its original receipt
revision has necessarily changed. A changed host fails the existing installation
report; no refund is fabricated. The server must retain the pinned receipt
binding in the claim; enabling this SDK alone cannot enforce purchase policy.

Keep one stable private journal path per site worker across restarts. Atomic
replacement, file/directory fsync and a process lock protect the journal. Each
mutation has a persisted intent; completed installs record checksum ownership
and expected installed-state revision. Rollback checks both under the SDK state
lock, in reverse installation order, and preserves preexisting packages. If
another operation changes installed state, rollback stops for operator review.
This deliberately prefers a remaining owned module to deleting another job's
work. Generic lifecycle adapters still must compensate their own partial
failures; arbitrary database side effects cannot be inferred by this worker.

The journal retains at most 512 recent step transitions and the current intent,
ownership and verification evidence. After acknowledged terminal reporting it
is archived in `<journal filename>.history/<installation id>.json`. History
retention is an operator responsibility. Acknowledged history strips claim
credentials; the active journal retains them for authenticated retry and must
remain private. New directories use mode 0700 and journal files 0600; existing
directory permissions are not changed and must be restricted by the service
operator. A failed report retains the active journal and retries
only delivery. Legacy journals without a known safe phase need attention.

For each transition the worker persists then sends
`POST /api/network/v1/packages/installations/:id/progress`:

```json
{"claim_token":"...","sequence":1,"stage":"download","package_id":"release-id","completed":0,"total":2}
```

Stages are `preflight`, `download`, `install`, `verify`, `rollback`, `complete`.
`package_id` is optional and identifies the immutable release, not the module.
`completed` counts verified packages, including packages later rolled back;
`total` is the exact claimed plan size. These are counts, not time estimates or
deployment percentages. The server must authenticate node and claim, accept
identical sequence replays, and reject conflicting or stale events. The worker
replays its latest event on restart. Progress transport errors, including 404
before server rollout, do not turn into installation evidence. Explicit
401/403/409/410 responses stop forward execution; rollback remains available.
The terminal report is authoritative and retryable independently of progress.

## Platform requirements and remaining deployment gates

The constructor remains compatible, with optional `platform_snapshot` and
`platform_check` arguments. `platform_snapshot` accepts a `PlatformSnapshot`
from `yeti3.sdk.installer.platform`, or a zero-argument callable that measures
and returns one. Prefer the callable for fresh resource/service observations.
The default snapshot observes the current Python version and
`platform.system().lower()` only; it does not invent runtime versions, available
resources or service readiness. The snapshot is retained in the private journal.

Supported `[requirements]` fields:

| Field | Requirement | Snapshot observation |
| --- | --- | --- |
| `python` | PEP 440 specifier, e.g. `>=3.12,<3.14` | `python` version string |
| `platforms` | Nonempty OS names, e.g. `["linux", "darwin"]` | `platform` name |
| `runtimes` | Mapping, e.g. `{django=">=5,<6", torch=">=2"}` | `runtimes` mapping of measured versions |
| `capabilities` | Nonempty names, e.g. `["postgres.ready", "gpu.cuda"]` | `capabilities` frozenset of positively probed names |
| `resources` | Positive integer minimums | `resources` available integer amounts |

Resource keys are `memory_mb`, `disk_mb`, `cpu_count`, `gpu_memory_mb`.
`services` is an alias for required capability names; singular
`platform={name="django", version=">=6"}` is a framework-version requirement.
Unknown fields/resource keys, missing observations and version mismatches block
installation. An optional `platform_check(requirements)` is an additional host
gate returning exactly `True`; it cannot bypass the built-in requirement model.
GPU presence alone does not imply torch/CUDA or model compatibility: declare
and positively probe the relevant capability and runtime requirements.
There is no manifest shell or automatic pip/system-service installation.

Delivery has independent bundle limits: optional constructor arguments
`max_bundle_bytes` (default 256 MiB of declared downloads) and
`max_expanded_bytes` (default 512 MiB of uncompressed ZIP members). The plan's
declared sizes are summed before any download. All ZIP headers are inspected
before the first extraction; their total expansion and total member count
(10,000 across the bundle) must fit. Individually valid archives cannot bypass
the aggregate limit. These trusted host limits are not manifest overrides.

The worker checks actual free space on its temporary filesystem before download
and again before extraction, independently of the manifest's persistent-root
disk probe. The latter check accounts for block-rounded entries/metadata, two
temporary copies (extraction/staging), two persistent copies (payload/assets),
and 64 MiB headroom per filesystem. Needs on a shared filesystem are summed.
This is a conservative capacity observation, not a reservation or OS resource
allocation; concurrent consumers, custom adapter destinations, and arbitrary
memory/CPU use by reviewed Python still require host-level limits.

Dependency installation covers
published SDK modules; Python/system dependencies must already be provisioned
by a compatible host. Current locking requires POSIX, so Windows is not claimed
as supported. Package imports and acceptance suites execute reviewed Python
with worker privileges, not in a sandbox. Suites and health probes must be safe
to repeat. A crash after central claim but before local journal persistence
still requires central lease recovery/operator review; the worker cannot
reconstruct a lost claim. Long installs need a valid central claim throughout;
the central server renews a still-live lease for 15 minutes on each accepted new
progress sequence. Exact replay never extends a lease or revives an expired
claim. The SDK does not generate artificial keepalive events, so an individual
operation lasting longer than the lease remains an operator concern.
Concurrent CALMF draining can remove evidence
before the local check and conservatively fail verification; coordinate spool
draining or supply a durable receipt mechanism before such a deployment.

An inventoried source repository is not an installable release. Production
requires an immutable reviewed artifact, executable acceptance tests, a real
host probe, compatible reversible adapters, persistent worker storage and the
matching central protocol. No production deployment is implied by SDK tests.

## Disposable Rust HTTP delivery canary

`tests/sdk/test_packages_http_canary.py` is opt-in and refuses non-loopback HTTP
or a MySQL database other than `yeti3_network_test`. Supply
`YETI3_RUST_CANARY_URL`, `NETWORK_TEST_DATABASE_URL` and `GOCHAIN_PACKAGE_ROOT`
matching an already migrated disposable Rust server, then run that test file.
It requires the `mysql` CLI. The test never migrates or starts a production
service. It seeds a unique active test release and grants the unique disposable
account an owner benefit. It then uses actual owner/node HTTP routes for preview
queue, SDK manifest-only evaluation, report, owner poll, and purchase with the
resulting `preflight_id`. Receipts, purchases and installation queues are never
inserted directly in SQL. This validates owner-benefit checkout, not a paid-token
debit, payment-provider integration or commercial publication. It does not change
global economy settings or fund/spend chain tokens; central paid-debit tests are
separate. Test catalog prices are fixture values, not production pricing.
Fixtures remain until the disposable database is removed by its owner.

The canary executes the actual SDK client over HTTP against Rust's authenticated
ready/preview/receipt/purchase/claim/progress/report and artifact authorization routes. A test-only
loopback proxy fulfills Rust's `X-Accel-Redirect` with bytes from the private
artifact directory; this is not an nginx configuration test. A separate HTTP
asset server supplies the runtime health probe. Assertions cover actual
acceptance tests, local CALMF terminal persistence, installed state, every
accepted progress transition and owner-visible progress, successful install,
failed-health owned rollback, live-lease extension, replay without extension
and refusal to revive an expired lease. Missing receipts are rejected before
purchase creation even for the benefit fixture. A changed installed-state
revision after receipt consumption blocks deployment before download. Production client origin validation is
unchanged; only the test instance points to the loopback transport.

## Python distributions (SDK 0.2.0)

Asset releases also ship Python wheels with standard pyproject metadata, a
`yeti3.modules` runtime entry point, and a `yeti3.packages` package factory.
`yeti3.sdk.distributions.discover_packages()` lists installed metadata without
importing package code. `installed_package(distribution_name)` verifies wheel
version/module identity and yields its embedded SDK manifest in a temporary
directory; pass it to the persistent SDK installer with real lifecycle adapters.
Installing a wheel alone does not activate a module or bypass GoChain purchase
checks. Production delivery stays authenticated and site-scoped. Wheel archives
and SDK payloads have separate SHA-256 identities.
