Configuration and state¶
F-Layer core separates desired intent from the minimum inventory needed to
locate resources it owns. These contracts are implemented in flayer.core;
cloud provisioning, lifecycle orchestration, and deployment-specific settings
are separate work. See ADR 0001.
Module boundaries¶
| Boundary | Owns | Excludes |
|---|---|---|
flayer.core.contracts |
Schema validation, stack identity, ownership labels | Cloud authentication, cloud API behavior |
flayer.core.config |
Desired resources, profile identifier, credential references, TOML loading | Credential resolution, regional defaults, provider-specific resource settings |
flayer.core.state |
Owned resource locators, strict JSON loading, local atomic persistence | Provider observations, authorization, cloud deletion, generated artifacts |
| Provider adapters | Provider settings, authentication, cloud API translation, ownership verification | Generic state storage policy |
| Deployment profiles | Provider capability composition and profile-specific settings | Implicit core provider selection |
| Diagnostics | Observations and reports with explicit execution boundaries | Desired state or ownership authority |
Desired configuration¶
An explicit TOML file uses schema version 1. The loader fails on a missing
file, invalid TOML, unknown fields, unsupported schema versions, and wrong
types; it never substitutes machine-specific settings or implicit cloud
defaults. Portable names use lowercase letters, digits, and hyphens, start
with a letter, and contain at most 63 characters. scope_id and provider
resource identifiers are opaque printable ASCII strings without whitespace,
limited to 256 characters.
schema_version = 1
profile = "secure-gateway"
[identity]
project = "example"
stack = "demo"
provider = "example-cloud"
scope_id = "example-scope"
owner_id = "example-owner"
[[resources]]
logical_id = "gateway"
kind = "instance"
name = "demo-gateway"
[[credentials]]
name = "cloud-auth"
source = "env"
reference = "EXAMPLE_AUTH"
resources and credentials may be omitted or empty. Logical resource IDs
and credential names are unique. Provider/profile adapters validate their
own detailed settings; version 1 does not accept arbitrary additional
settings in these core tables.
SecretReference accepts env with an uppercase environment variable name,
or file with a bounded nonempty path. No environment value or referenced
file is read while loading configuration. The authentication adapter owns
resolution and any relative-path policy. Configuration contains references,
never token values, passwords, or private-key bodies. The core cannot verify
the contents of an arbitrary reference; callers must protect credential files
and avoid committing private paths.
from flayer.core.config import LoadConfig
config = LoadConfig("./stack.toml")
ownership_labels = config.identity.OwnershipLabels()
The resulting models are frozen dataclasses. Direct construction uses the same field and uniqueness validation as decoding. Desired resources contain logical names, without externally assigned resource IDs.
Owned state¶
StackState records a complete StackIdentity and a tuple of
ResourceState(logical_id, kind, resource_id) values. The snapshot can hold
only the resources acquired so far in an interrupted deployment; no missing
locator is inferred. Provider locators must be unique within their kind,
and logical IDs must be unique across the inventory.
from flayer.core.state import LoadState, ResourceState, SaveState, StackState
state = StackState(
identity=config.identity,
resources=(ResourceState("gateway", "instance", "example-instance"),),
)
SaveState("./runtime/state.json", state, expected_identity=config.identity)
loaded_state = LoadState("./runtime/state.json", expected_identity=config.identity)
The JSON schema has exactly schema_version, identity, and resources.
It does not persist credential references or values, full configuration,
provider response payloads, logs, health reports, generated files, or public
endpoints. An absent file returns None; a corrupt or foreign file raises
StateError. Duplicate JSON fields are rejected. Reads and writes have a 1 MiB
JSON limit. The opened descriptor must be a regular file; nonblocking open where
available prevents a post-validation FIFO substitution from hanging a reader.
Both the initial size and the bounded stream result are checked, so concurrent
file growth cannot bypass the limit. Oversized writes fail before replacement.
Every read, save, and removal requires an expected identity. All five
identity fields must match. SaveState also validates an existing snapshot
under the writer lock before replacing it. Ownership labels are
managed-by=f-layer, flayer-project, flayer-stack, and flayer-owner.
Before an actual cloud operation, a provider adapter must independently
check the provider scope and live resource labels. A state file alone never
authorizes modifying infrastructure.
RemoveState(path, expected_identity) removes only a matching local snapshot
and returns whether a file was removed. It is idempotent for an absent file
and does not delete any cloud resource. Lifecycle code must decide when
discarding a local inventory is safe after provider cleanup.
Persistence and recovery¶
Use a trusted directory owned by the operating-system user. Avoid symlink
ancestors and .. path components. All writers for a snapshot must use this
state API. The implementation rejects a symlink or nonregular target and
uses O_NOFOLLOW on platforms that support it, but cannot prevent a hostile
local user from racing directory changes in storage they control.
Writes acquire the sibling .state.json.lock exclusively, create a unique
temporary file in the same directory, flush and fsync it, and atomically
replace the target. Temporary files have owner-only POSIX permissions.
The parent directory is flushed on POSIX; Windows relies on os.replace
and the caller's directory ACLs. Existing corrupt or foreign snapshots block
save and removal and remain available for investigation.
Failure before replacement preserves the previous snapshot. Failure after replacement may leave the new complete snapshot visible while directory durability remains uncertain. Read and inspect state after such an error before retrying lifecycle work. Atomic file replacement does not guarantee durability on every remote filesystem and is not a cloud transaction.
A competing or stale writer lock causes an immediate StateError. A normal
operation removes its lock and pending temporary file. After a process crash,
confirm no writer is active, inspect the final state and any surviving
temporary snapshot, and then recover the local lock explicitly. The API does
not automatically steal locks, adopt foreign state, repair corruption, or
migrate unknown schema versions.
If the filesystem prevents temporary-file or lock removal, the API reports a
StateError for incomplete cleanup and preserves the underlying exception
chain. Pending artifacts then need the same explicit local recovery. A failure
to create a text stream closes its file descriptor before propagating the
error, so failed persistence does not leak an open descriptor.