Skip to content

v4.5: operationalize the core and migrate to Determa 0.2 - #55

Merged
fruwe merged 32 commits into
mainfrom
v4.5
Oct 5, 2026
Merged

fruwe merged 32 commits into
mainfrom
v4.5

Conversation

@fruwe

@fruwe fruwe commented Jun 27, 2026 •

Copy link
Copy Markdown
Member

Behavior

Repair the externalized Terraform runtime and migrate provider, provisioning, and package lifecycles to Determa 0.2.0. Settings and lifecycle transitions use the same public FSM contract. Operation IDs remain monotonic, interrupted operations recover, and hierarchical package observations preserve the root lifecycle.

Provider/package commands receive isolated environments and target-scoped secrets. Secret payloads have restricted permissions before writing/upload, partial uploads are cleaned up, Windows ACL/deletion errors propagate, and installer paths expand only their intended home prefix. Standalone bundles and local plugin sources remain discoverable.

AWS requires an explicitly configured named profile. Eve does not declare, read, inject, or edit AWS key material; AWS tooling owns the profile's credentials and login/SSO session. The unused alternative-authentication feature was removed. Providers without secret fields skip secret-store reads, and failed Terraform environment resolution aborts backend commands. Draft core PRs run CI.

Incus contract and review fixes

Represent system containers with machine kind container and engine incus. Use strict SSH access fields (address, port, protocol/version, username, credential reference, host fingerprint) and a closed provider-discriminated identity envelope. Incus-specific endpoint, remote, project, instance name, and ownership digest live under its identity variant. SSH host identity supports an explicit configured fingerprint or the namespaced Incus authenticated-exec mechanism; guest access remains OS-independent.

Repair observation integration: validate the exact status JSON selected for persistence, normalize raw failed observations through the existing error presentation, disable running-only actions after observed failure, and replace/clear stale authenticated bindings on refresh. Running Incus status supplies its authenticated binding.

Explicitly package schema, FSM, scripts, config, TUI, and shipped OS runtime assets into wheels. Both CI matrix jobs run python scripts/test-wheel, which builds and installs a wheel into a clean venv outside the source checkout, tests eve --help, checks runtime assets, and validates positive/negative command outputs.

The provider implementation and provisioning/lifecycle fixes are in Providers #10. No Determa dependency, FSM, provisional adapter, seed-by-replay restoration, or unreleased API is introduced or changed by these fixes. Retain the published compatible >=0.2.0,<0.3.0 dependency.

Fresh review validation

Cloud validation on 2026-10-05 (Asia/Tokyo):

  • 536 core Python tests passed, 1 intentional skip, excluding only the 20-test TUI smoke file.
  • 27 focused core contract regressions passed; all 16 non-Python suite groups passed.
  • 122 Incus offline tests passed, including actual core provisioning uploads through Incus and combined observation/presentation regressions.
  • 48-plugin combined catalog/conformance passed, 0 failures.
  • Isolated installed-wheel smoke test, schema tests, lint/type checks, formatting, offline CI pipeline, and actual Linux/PowerShell provisioning runners passed.

The complete Python invocation was attempted; lint/type checks and 523 tests passed before it stalled in the unchanged TUI smoke file and was interrupted. An isolated smoke-file run timed out at 120 seconds. The previously documented untouched baseline 13341e8 reproduces the asynchronous shutdown timeout. No TUI code/test configuration changed; a completely green full suite is not claimed.

See the contract and review validation record.

Rollout readiness

Remain draft; no merge requested. Review-fix head: 04919109ee398118d6e8cf5b450f3962ed9754d3. New-head GitHub checks must be evaluated independently of Cloud results.

Live Incus TLS testing is pending restricted client enrollment, verified certificate pinning, and routed controller access. No private-pool access or live resources were used. The live test remains opt-in and excluded from normal CI.

Recovery #56, Providers #11, Linux #7, Windows #5, and AI #3 are merged into their intended branches. Linux #8 and Windows #6 are the main-facing package PRs. Merge core before companions once rollout validation is complete.

The roadmap's Vultr Windows GPU/VDD and Sunshine-streaming validation remains pending. Eve #46 independently changes tui/app.py; review that overlap before landing it.

The write path (tf-init/plan/apply/destroy) vendors the externalized provider
terramate stacks and runs `terramate generate` before invoking terramate. The
read/power commands (status, ip, start, stop, password across aws/gcp/truenas/
vultr) never run tf-init, so on a fresh checkout — or any tree where `up` hasn't
run — terramate can't discover the stacks: status reports "not created" and ip
emits a terramate error box that gets captured verbatim as the IP value. It
only worked because an `up` happened to leave the stacks vendored.

Add a shared core helper the read path uses:

- `stage_provider_stacks` / `scripts/tf-stage <provider>`: vendor the stacks and
  run `terramate generate` only when staging changed something or no generated
  backend file exists yet.
- `prepare_provider_stacks` is now idempotent: it short-circuits when the staged
  copy already matches the source (a read-only content compare that ignores
  generated, destination-only files), returning whether anything was re-staged.
  This keeps frequent callers (status polls) from churning the filesystem and
  clobbering generated z_backend.tf on every call. State is unaffected — the
  local backend lives under .terraform-cache-dir/, outside the staged tree.

The provider commands invoke this via `eve_load_profile_env` (gated on the
terraform engine) in the companion eve-providers change.
fruwe added 28 commits June 27, 2026 14:19
TUI plugin-sources screen now manages local-folder sources without env vars:
- Type column (folder/remote) distinguishes local paths from git URLs
- EditSourceScreen: edit id/url/ref/folder/auth of an existing source
- Default action on configured entries opens Edit (not Remove); recommended
  entries keep add behavior
- is_local_folder() helper + configured_row() + update_source() in plugins.py
- AddSourceScreen placeholder now mentions local paths
- 'e' keybind for Edit; Edit button added to controls row
- Tests: is_local_folder, configured_row with local flag, update_source
  round-trip, TUI smoke test updated for new button label
Remove junk state files (--instance.json, --.json, schtasks directory,
dozens of stale .json/.lock from old testing). Root cause was pre-v3 scripts
that passed raw argv; current argparse-based instance-state doesn't reproduce.
…treaming windows scope, local-folder branch UI) + demo-instance task
…n (v4.5 K)

Bundles are now first-class kind:bundle plugins, not implied by package
bundles: declarations.

Core changes (eve):
- kind enum: provider | package | os | bundle
- New 'members' field: a bundle plugin lists its member package ids
- Removed 'bundles' from schema + validator (packages no longer declare bundles)
- 'supports' moved from global required to provider/package conditionals
- catalog.py: aggregates bundles from kind:bundle plugins (reads 'members')
- catalog_view.py: computes bundle installable_os_families from member packages
- plugin_test.py: bundle/os kinds skip the SSH-readiness boundary check
- Hermetic fixtures: mock-app/mock-win-app bundles: removed; mock-dev/mock-gaming
  bundle plugins created; test golden updated

Package repos (v4.5):
- eve-packages-linux: 9 bundle plugins created under bundles/; bundles: removed
  from 9 package manifests
- eve-packages-windows: gaming-streaming bundle plugin [steam, sunshine, rustdesk]

Tests updated: catalog aggregator tests use kind:bundle; manifest contract
tests check bundle validation; relocation test reads bundle plugins.
…4.5 G, H, J)

Wizard (Bug G): incompatible bundles and packages are now rendered dim
in the new-instance wizard. The support_reason/conflict checks already
prevented selection; now they're visually differentiated too.

Dialog convention (Bug H): plugin-sources screen reordered — Edit is
leftmost, primary-colored, and the default action (Enter/Space). The
Remove toggle button is warning-colored and no longer flips.

Edit label (Bug J): the EditSourceScreen ref field is now labeled
'Branch / ref (snapshot — re-pull to refresh)' making it explicit that
uncommitted changes aren't picked up.
….5 Bug M)

The status probe ran each provider's connectivity command with a bare
environment, so a provider whose 'configured' gate is a secret (Vultr's
VULTR_API_KEY, which lives only in the secrets file) reported unconfigured even
though dispatch could create instances with it; AWS meanwhile reported
'configured' off ambient ~/.aws, bypassing eve's config entirely. The probe now
resolves the same ConfigEnv.environment() + _inject_secrets the dispatch path
uses, so status and dispatch see identical config/secrets.

Also records the v4.5 architecture workstream (Bugs M-R): one shared config/
secret resolution, a central FSM platform with settings as machine context,
XDG config layout, TUI-over-CLI parity, and streaming<->non-streaming parity.
The eve<->harel seam: EveFsm wraps a harel.Host, loads plugin-declared harel
machines, and seeds each machine's declared `external` esvs from one pluggable
ContextResolver (the shared config/secret resolver M/O need). Settings changes
arrive as harel's `env` event + `refresh`; side effects stay host-driven (eve runs
the probe, delivers `probe_ok`/`probe_fail`).

Proven on a synthetic provider machine: configured/reachable are FSM-computed from
injected context and re-derive on a settings change (status and dispatch physically
cannot diverge). Test importorskips harel so CI stays green until the dependency is
pinned (gated on a harel-python release tag). Reconciles docs/v4.5-fsm-design.md to
harel's real YAML format.
…ence

- provider_context_resolver(): resolves a provider machine's external esvs (named as
  their eve env vars, e.g. VULTR_API_KEY) from the one path dispatch uses
  (os.environ + ConfigEnv.environment() + _inject_secrets), so a provider's FSM
  context is byte-identical to what dispatch runs with (bug M). Hermetically tested.
- Import the statechart engine behind one name (determa_state, falling back to the
  pre-rename harel) so the upcoming rename is a no-op for eve.
- core/fsm/provider.yaml: one shared provider lifecycle (unconfigured -> configured
  -> reachable) for every provider. reachable is a substate of configured reached by
  the connectivity probe (probe_ok/probe_fail).
- provider_configured_resolver(): derives is_configured = every required config_schema
  secret/setting resolves non-empty via the shared path, so 'configured' is
  eve-resolved, never a plugin's ambient self-report (the AWS-off-~/.aws bug becomes
  structurally impossible). Tested end-to-end against the core machine.

Next: rewire scripts/providers-status to load the core machine (configured from the
FSM; connectivity answers reachable only).
… guard

The statechart engine published as determa-state (import determa.state). Pin it in
pyproject + poetry.lock as a real dependency; the FSM platform imports determa.state
(falling back to the pre-rename harel). Remove the skip-if-missing test guard now that
the engine is a hard dependency (a missing required dep should fail loudly, not skip).
configured is now FSM-derived (core/fsm/provider.yaml + provider_configured_resolver):
true iff eve has resolved the provider's required config/secrets, so it can never be a
connectivity command's ambient self-report. The connectivity probe runs only when
configured, and only its reachable + notes are used (its configured field is ignored).

Live-verified: vultr yes/yes, truenas 'not configured', docker/qemu/gcp yes; aws
yes/no with an actionable note. Full suite green (493 passed, 1 skipped).

AWS follow-up (eve-providers): AWS declares only a non-required config_file, so it
reads configured=yes; mark an auth setting required or model it probe-derived if it
should read unconfigured until eve holds a profile.
core/fsm/instance.yaml models the instance lifecycle as two orthogonal regions
(provider terraform lifecycle + provisioning), with distinct transient states so
each operation's outcome is unambiguous, and desired as context. core/fsm/package.yaml
models the package lifecycle (install/remove + observe-driven drift). Both pass the
engine's static validation and are exercised in tests/test_fsm_machines.py.

These are the declarative replacements for state.py's PROVIDER/PROVISION/PACKAGE_STATES.
Wiring them into live persistence/rendering (retiring state_machine.py) is gated on a
state-vocabulary decision (match existing enums vs adopt cleaner set + migrate),
recorded in docs/v4.5-fsm-design.md.
- instance machine: add provider region 'unknown' initial (unobserved) + drift observe_*
  edges from unknown/absent/error, so the old 'unknown' maps cleanly and live status can
  reconcile drift.
- eve_sdk/fsm_migrate.py: translate persisted state files (pre-FSM vocabulary -> core
  machine vocabulary) on read; stable values are identity, terraform micro-stages ->
  unknown, provision error -> provision_error, package reinstalled -> installed.
  Idempotent + tested.

Chosen path: adopt the cleaner vocabulary + migrate. Remaining live wiring (apply the
migration on State.read, switch enum sets, move state_machine.py derivations onto the
machines) is the invasive integration touching instance_view; tracked in the design doc.
… migration)

No important instances, so per the delete-all decision: destroyed + deleted both demo
instances, wiped their per-instance state, and removed eve_sdk/fsm_migrate.py + test
(a runtime migration is no longer needed). Records the delete-all path + the remaining
vocabulary alignment / FSM-driven status steps in the design doc.
Dispatch now records the instance machine's vocabulary: provider_state_for maps
in-flight ops to creating/starting/stopping/destroying (init/plan -> creating), and
package reinstall -> installed. state.py's PROVIDER/PACKAGE state sets, the
observed-state + command-io schemas, the state_machine/sdk/test_state* tests, the
valid-pending fixture, and test-plugins all follow the core/fsm machines. Old terraform
micro-stages (initializing/initialized/planned/changing) and package reinstalled are
gone. Full suite green (496 passed, 1 skipped).
… write-path authority

provider_state is no longer a hand-maintained lookup table. eve_sdk/instance_fsm.py
is the single lifecycle authority: dispatch maps a (command, op_status) to a machine
event (dispatch_event) and State.record_operation(provider_event=...) fires it into
core/fsm/instance.yaml seeded from the persisted provider_state, atomically inside the
state write; the resulting leaf is the new provider_state. The state a status read sees
is exactly what the machine produced — the table and the machine cannot diverge because
there is only the machine.

- delete dispatch.provider_state_for; provider_command.py + scripts/provider-dispatch
  fire events via dispatch_event
- state.py: provider_event path computes the next leaf inside the modify callback
  (race-free); the direct provider_state param stays for instance-state / recovery
- instance.yaml: every concrete provider state accepts every operator command (routing
  to its transient), so a command's terminal outcome holds from any state (start
  recovers absent->running). The provider command, not the machine, decides feasibility:
  a start that can't apply fails -> error, never a silent no-op. Preserves the old
  table's terminal semantics exactly, now with honest transients.
- fsm.py: bind engine as a defined name so eve_sdk.fsm.engine is an explicit export
- test_state.py: full lifecycle + recovery + read-only cases through the machine

Read-path derivations in state_machine.py (effective_provider_state, live-overlay
precedence, aggregate_summary) are display/aggregation policy, not a second lifecycle
definition — one transition authority now, no divergence left.
…unctional engine API

determa-state v0.1.0 replaced the draft grammar and the Host/Instance object model:
machines are now format-1 bundles (format: 1, namespace, shared events with
direction, machines[].root, esvs -> variables) and the engine interface is the pure
functional spec-§8 pair create/dispatch over plain aggregate-state dicts. Port eve's
whole FSM layer accordingly; pin determa-state (>=0.1.0,<0.2.0).

- core/fsm/*.yaml: rewritten as bundles. Format 1 has no orthogonal regions, so the
  instance bundle holds two machines (instance_provider + instance_provision) —
  matching eve's persistence, which keeps provider_state and provision_state as
  separate fields anyway. YAML anchors are rejected (unsupported_yaml_feature), so
  the shared operator-command block is spelled out per concrete state.
- eve_sdk/fsm.py: EveFsm now holds loaded bundles + live aggregate states per
  (scope, key); external variables are seeded as creation bindings; env/refresh
  unchanged in concept. Surface: load/aggregate/fire/refresh/leaves/state/context/
  view. The harel fallback import is gone; import determa.state appears only here.
- eve_sdk/instance_fsm.py: dispatch is pure, so the adapter seeds each provider leaf
  once by replaying a short event prefix from create, caches that aggregate, and
  dispatches the real event against it (the cache stays valid — dispatch never
  mutates its input). dispatch_event mapping unchanged.
- scripts/providers-status: reads fsm.state() instead of engine instance objects.
- tests: fixture bundle + machine/platform tests updated (provision machine gets its
  own lifecycle test); aggregate status is "running" not "active".

Full scripts/test green; test-lifecycle green; providers-status live-verified
(aws/docker/gcp/local-qemu/truenas/vultr — same semantics as before the port).
The old IntType-leak issue is moot: 0.1.0 returns native values.
* feat(v4.5): complete Determa 0.2 migration

* fix(v4.5): close recovery validation and secret handling gaps

* fix(ci): create project environment before test runners

* docs: use UTC date for Cloud validation
@fruwe fruwe changed the title v4.5: operationalize the agnostic core (runtime hardening — DO NOT MERGE until validated) v4.5: operationalize the core and migrate to Determa 0.2 Oct 3, 2026
@fruwe
fruwe marked this pull request as ready for review October 5, 2026 13:57
@fruwe
fruwe merged commit ec58001 into main Oct 5, 2026
6 checks passed
@fruwe
fruwe deleted the v4.5 branch October 5, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant