Bundles
About 1539 wordsAbout 5 min
2026-09-29
How the documents in .sigil files form a bundle, how names resolve in it, and where required policies come from.
Why: Bundles and trust.
Documents
module deploy.common: DeployApproval@1
pub let cleared = split(service.labels["regions"], ",") all in actor.regions
---
policy deploy.guardrails: DeployApproval@1
param min_soak: duration = 24h
when release.soak < min_soak and not release.hotfix {
deny(reason: soak_too_short)
}- A file holds one or more documents. A document is a policy, a module or a kind.
- A document starts with its header,
policy,moduleorkind, and ends where the next header starts or at the end of the file. - A header keyword can't start a statement inside a document, so a header at the top level always starts a new document.
- Inside braces, after
.or as a named argument,policy,moduleandkindare ordinary names: atype Resource { kind: string }body or aresource.kindread never ends a document. See Source files. - The
---separator is optional.sigil fmtalways writes it between documents. See Document separators for how it lexes. - A
---before the first document or after the last one is allowed, and so are several in a row.sigil fmtremoves the extras. - A comment directly above a header belongs to the document that follows it:
sigil fmtwrites the---above the comment, not between the comment and the header. - A file with no documents at all is valid and contributes nothing.
Bundles
A bundle is every document in every file the host or the CLI loads, indexed by the name in each header.
| Rule | Consequence |
|---|---|
| Each name has one definition | A name defined twice in a bundle is a compile error that points at both definitions |
| The host names the root policy | One bundle can hold many policies; the host picks the entry point |
| A module can't be a root | It has no rules to evaluate. Load on a module's name fails with deploy.common is a module, not a policy |
| A bundle holds the documents of one kind | A policy or module written for another kind is a compile error, document is for kind AccessGrant, not DeployApproval. A host with two kinds reads them from separate directories |
| A bundle loads as a whole | A syntax error in any document fails the load, even in a document the root never uses |
Compile takes a one-file bundle | The source string may hold several documents, imports resolve among them, and every document is checked; see Loading |
| The CLI builds one bundle from all its arguments | A name defined in two files is an error there too; see Inputs |
policies.sigil:42:8: error: policy payments.access is defined twice
|
42 | policy payments.access: DeployApproval@1
| ^^^^^^^^^^^^^^^
= help: first defined at teams/payments.sigil:1:1; documents resolve by name, so each name has one definitionOn a hot reload, the host keeps the last policy that loaded; see Reload without an outage. To catch a broken document before a bundle ships, see Check policies in CI.
Name resolution
use deploy.commonresolves to the document nameddeploy.common, wherever it is. A name is never a file path.- Files are plain containers. One key per team, one file per policy, or everything in one file all resolve the same way.
- The imported document must implement the same kind as the importing one; see
use. - A
useof a kind's name is a compile error. - With a trusted source, its documents resolve first: a required policy and everything it uses, and the modules a team imports from it, come from that source, not from the bundle.
Loading files
The host passes the bundle as an fs.FS, such as an embed.FS, os.DirFS or policy.MapFS; see Loading.
| Rule | Detail |
|---|---|
| Extension | The loader reads every file whose name ends in .sigil, in every directory. ConfigMap keys need the extension too (policies.sigil) |
| Dot entries | Every file or directory whose name starts with . is skipped, which includes kubelet's ..data directory and its timestamped siblings in a mounted ConfigMap |
| Symbolic links | Followed. The loader checks each entry with fs.Stat, never with the directory entry's type (DirEntry.Type()) |
policy.MapFS | Turns a ConfigMap's data, a map[string]string, into an in-memory fs.FS with each key as a file name |
The CLI takes files, directories and stdin; see Inputs. To load a ConfigMap, see Policies in a ConfigMap.
Kind documents in a bundle
- Kind documents aren't part of the name index.
- A kind document is never taken as the contract. The kind always comes from the host's Go definition. The CLI, which has no Go definition unless it's a host binary, is the exception; see Kinds.
- A kind document with the host kind's name must match that contract (
Deploy.Schema()) exactly, or the load fails. This catches a stale export. - A kind document for another kind is ignored.
A mismatch fails with kind document DeployApproval doesn't match the host's kind and the help the host's Go definition is the contract; regenerate this file from Schema().
Trusted sources
policy.Require("deploy.guardrails", policy.From(platformFS))
policy.Trusted(vocabularyFS)policy.From(fsys), passed inside policy.Require, names the source a required policy must come from. policy.Trusted(fsys) adds a trusted source without requiring any policy from it, such as the vocabulary modules a platform ships. The loader reads every trusted source into one trusted bundle, separate from the one passed to Load.
//go:embed platform
var platformFS embed.FS // or a platform-owned ConfigMap, mounted separately
//go:embed vocabulary
var vocabularyFS embed.FS
p, err := Deploy.Load(policy.MapFS(cm.Data), "payments.production",
policy.Require("deploy.guardrails", policy.From(platformFS)),
policy.Trusted(vocabularyFS))- A trusted document resolves before the untrusted bundle's. The required policy is taken from the trusted bundle, and so is everything it imports and invokes. A trusted document never resolves a name in the untrusted bundle.
- Every name a trusted source defines is reserved. A document in the untrusted bundle that claims one of them, such as
deploy.guardrailsordeploy.common, is a compile error naming both definitions. Neither side overrides the other. A document that's a byte-for-byte copy of the trusted one is the same definition and is left out, so the bundle may hold the trusted source's directory too, as a repository that holds the platform's directory and the teams' does. - Every trusted document is checked, so a broken one fails the load even when no policy uses it.
- A nil trusted source, or one that holds no policy or module, fails the load, since it would protect nothing:
the trusted source passed to Trusted holds no policies or modules. - Several options may name the same source,
TrustedandFromalike, and the loader reads it once. It recognizes the same source by value for a comparablefs.FS, such asembed.FSoros.DirFS, and by map identity for a map-backed one such aspolicy.MapFS. Two values it can't tell are the same, such as twofs.Subcalls for one directory, are each read, and the second one's documents are copies. - Two trusted sources, or a trusted source and the bundle, may each hold a file of the same path, such as two ConfigMaps keyed
policies.sigil. Every diagnostic quotes the file it's about. - Team policies import and invoke trusted documents by name as usual:
use deploy.guardrailsanduse deploy.common.{cleared}work unchanged. - Without
From, the required policy is looked up like any other document: in the trusted bundle first, then in the untrusted one. - A required policy bounds its own params with
minandmax.Requiretakes no bounds of its own. sigil checkreads trusted paths the same way, from--trustedand the configuration file'strusted; seesigil check.
teams/payments.sigil:3:8: error: module deploy.common is defined twice
|
3 | module deploy.common: DeployApproval@1
| ^^^^^^^^^^^^^
= help: the name belongs to the trusted source, defined at deploy/common.sigil:1:1; documents resolve by name, so each name has one definitionWhy: Why required policies need a trusted source.
File names
Nothing in the language ties a file's path to the names inside it. The path-matches-name lint warns when a file holds a document whose name doesn't match the file's path, with each . turned into / and .sigil appended.
| Name | Expected file |
|---|---|
deploy.common | deploy/common.sigil |
deploy.production | deploy/production.sigil |
payments.production | payments/production.sigil |
- The file may sit under any directory:
policies/deploy/common.sigilmatchesdeploy.commontoo. - The lint is off by default.
- It does nothing for a bundle that arrives through
policy.MapFS. Required policies are protected by trusted sources. - A file holding several documents, such as
deploy.sigilwithdeploy.commonanddeploy.guardrails, gets a warning for each document.
Planned
A prefix rule for multi-document files; see File names for multi-document files.
