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
The parser whitelists those two keys (from_env, from_file) and produces a SecretRef AST node — not a Rec. There's no other secret syntax. Adding from_vault = "key" (or similar) would be a parser change, not a runtime change.
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 in v0.6. 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 and from_file 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 from_env or from_file.
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.
For from_env, unset ⇒ [E11]. For from_file, missing file ⇒ [E11]. The resolver collects every missing reference before raising — operators see the full gap in one message.
→ Concepts: secrets for the hash-exclusion conformance property. → Deploy: secrets at deploy for the operator-facing recipe.