Secrets in PCL¶
The full architectural story is in concepts/secrets. This page covers the language surface: what you write in PCL.
Syntax¶
A secret is a record literal with exactly one whitelisted field:
{ from_env = "ENV_VAR_NAME" } # read from os.environ at deploy time
{ from_file = "/path/to/file" } # read from disk at deploy time
{ from_vault = "secret/data/app#tok" } # read from a secret store (ADR-014)
The parser whitelists exactly those three keys and produces a SecretRef AST node, not a Rec. There's no other secret syntax; a fourth kind would require a parser change.
from_vault takes an opaque reference resolved by an oracle you supply at deploy time with --vault-secrets (the shipped adapter reads a flat {ref: value} JSON export). Without it, a from_vault reference is reported as [E11] exactly like an unset env var. See secrets at deploy.
The frontend type-checker types SecretRef as Str, flagged secret. It behaves like a string at use sites; but the lowerer emits an IrSecret, not a string literal, so the canonical derivation sees a reference string ("from_env:NAME"), never the resolved value.
Where you can put them¶
A secret is Str-typed, so anywhere a Str is allowed:
module Api {
pname = "api"
…
source = {
type = "url"
url = "https://internal.example/artifacts/api.tar.xz"
# token = { from_env = "FETCH_TOKEN" } # not yet — see Roadmap
}
}
Secrets in source not yet wired
Stage 6 wires secrets at the deploy boundary; Service.environment.
Resolving secrets at the realise boundary (e.g. an authenticated
type=url fetch) is on the roadmap but not built. For now, the
practical place to put a secret is service.environment per stack.
The Stage 6 surface:
module Api { … }
module ApiStack {
backend = "systemd"
storeModules = ["Api"]
services = [{
name = "api"
package = "Api"
binary = "api"
environment = {
LOG_LEVEL = "info"
DB_PASSWORD = { from_env = "DB_PWD" }
OAUTH_PRIVK = { from_file = "/run/secrets/oauth.key" }
}
}]
}
What's NOT a secret¶
The whitelist is from_env, from_file and from_vault only. Any other single-field record literal is just a normal record:
{password = "..."} parses as Rec<password: Str>. The value flows through the pure layer like any other string and ends up in the store + the manifest. Don't do this. Use one of the three secret forms.
At deploy time¶
punix service deploy resolves every SecretValue in Service.environment via secrets.resolve_secrets(stack, env, fs):
envdefaults toos.environ(aMapping); tests inject a dict.fsdefaults toPath(p).read_bytes(); tests inject a callable.vaultis thefrom_vaultoracle, supplied by--vault-secrets; absent by default.
Unset env var, missing file, or unresolvable vault reference ⇒ [E11]. The resolver collects every missing reference before raising; operators see the full gap in one message.
The resolved values reach one file on the target: a 0600 EnvironmentFile.
→ Concepts: secrets for the hash-exclusion conformance property. → Deploy: secrets at deploy for the operator-facing recipe.