Skip to content

Organizational Policy

Lexicon lint answers “is this a coherent resource for its domain.” It does not express organizational policy — “no public load balancers in prod,” “every workload carries a cost-center tag,” “instance types from this allowlist.” Those are cross-cutting, org-specific, and often need to branch on environment.

chant does not add a separate policy language for this. Organizational policy is project-authored post-synth checks, run by the existing engine, with the current environment in context — the same PostSynthCheck shape lexicons ship as domain rules, authored by your org.

A policy reads the resolved resources and returns diagnostics. A diagnostic with severity: "error" fails chant build — that is the gate. The check receives ctx.env, so a rule can branch on the environment.

policies/org.ts
// Organizational policy — project-authored post-synth checks.
//
// These are the same `PostSynthCheck` shape lexicons ship as domain rules, but
// authored by *your org* and registered via `lint.policies` in chant.config.ts.
// They run during `chant build` over the resolved resources, with the current
// `--env` in context, so a policy can branch on environment. A check returning
// `severity: "error"` fails the build — the gate.
import {
getPrimaryOutput,
type PostSynthCheck,
type PostSynthContext,
type PostSynthDiagnostic,
} from "@intentius/chant/lint/post-synth";
import { parseYAML } from "@intentius/chant/yaml";
interface Manifest {
kind?: string;
metadata?: { name?: string; labels?: Record<string, string> };
spec?: { tls?: unknown[] };
}
/** Parse every lexicon's serialized output into flat Kubernetes manifests. */
function manifests(ctx: PostSynthContext): Manifest[] {
const out: Manifest[] = [];
for (const [, output] of ctx.outputs) {
for (const doc of getPrimaryOutput(output).split(/\n---\n/)) {
const trimmed = doc.trim();
if (!trimmed) continue;
try {
const m = parseYAML(trimmed);
if (m && typeof m === "object") out.push(m as Manifest);
} catch {
// skip unparseable documents
}
}
}
return out;
}
const COST_CENTER = "acme.io/cost-center";
/** Every workload must be attributable to a cost center — in every environment. */
export const costCenterRequired: PostSynthCheck = {
id: "ORG-COST-CENTER",
description: "every Deployment must carry an acme.io/cost-center label",
check(ctx): PostSynthDiagnostic[] {
const diags: PostSynthDiagnostic[] = [];
for (const m of manifests(ctx)) {
if (m.kind !== "Deployment") continue;
if (!m.metadata?.labels?.[COST_CENTER]) {
diags.push({
checkId: "ORG-COST-CENTER",
severity: "error",
message: `Deployment "${m.metadata?.name}" is missing the ${COST_CENTER} label`,
entity: m.metadata?.name,
});
}
}
return diags;
},
};
/** Production ingress must terminate TLS. Lower environments may skip it. */
export const tlsRequiredInProd: PostSynthCheck = {
id: "ORG-PROD-TLS",
description: "in prod, every Ingress must terminate TLS",
check(ctx): PostSynthDiagnostic[] {
if (ctx.env !== "prod") return []; // the environment-aware branch
const diags: PostSynthDiagnostic[] = [];
for (const m of manifests(ctx)) {
if (m.kind !== "Ingress") continue;
const tls = m.spec?.tls;
if (!Array.isArray(tls) || tls.length === 0) {
diags.push({
checkId: "ORG-PROD-TLS",
severity: "error",
message: `Ingress "${m.metadata?.name}" must terminate TLS in prod`,
entity: m.metadata?.name,
});
}
}
return diags;
},
};

ORG-COST-CENTER runs in every environment; ORG-PROD-TLS only fires when the build is for prod.

Add the policy file(s) to lint.policies in chant.config.ts. This is distinct from lint.plugins (declarative lint rules) by authorship and phase — policies reason about the resolved resources during build — but it is the same engine.

chant.config.ts
import type { ChantConfig } from "@intentius/chant";
// `lint.policies` registers project-authored organizational policy checks. They
// run during `chant build` over the resolved resources, with `--env` in
// context, and fail the build on violation. Distinct from `plugins` (declarative
// lint rules) by authorship and phase — same engine.
export default {
lexicons: ["k8s"],
ownership: { stack: "storefront" },
lint: { policies: ["policies/org.ts"] },
} satisfies ChantConfig;

Policy runs during chant build. Pass the environment with --env (or set ownership.env in the config):

Terminal window
chant build src # cost-center enforced; prod-only rules dormant
chant build src --env dev # same — ORG-PROD-TLS skipped
chant build src --env prod # ORG-PROD-TLS now enforced

For the example above, the prod build fails:

error: [policy:ORG-PROD-TLS] [storefront] Ingress "storefront" must terminate TLS in prod

Policy evaluates the synthesized artifact, offline — it never touches the cloud. Live checks are chant lifecycle, not this.

chant already has the machinery: a post-synth phase that reasons about resolved resources, project-authored rule loading, and the Op gate model for the apply side. A separate policy language (Rego/OPA-style) would duplicate the lint engine and break “TypeScript is the one language.” Org policy is post-synth checks the org authors — nothing more.

chant build already fails on a policy violation, so any pipeline that builds before applying is gated. To gate an apply inside an Op, add a policyGate() step before the apply phase:

import { Op, phase, build, policyGate, kubectlApply } from "@intentius/chant-lexicon-temporal";
export default Op({
name: "deploy",
phases: [
phase("Build", [build(".")]),
// Re-runs lint.policies over the resolved resources; a violation fails the
// workflow here, so nothing is applied.
phase("Policy", [policyGate({ env: "prod" })]),
phase("Apply", [kubectlApply("k8s.yaml")]),
],
});

policyGate builds the project, runs lint.policies with the given env (or ownership.env), and blocks on any violation — non-retryable and single-attempt (a deterministic violation is not worth retrying). It is a plain activity, so it gates under both the local executor and Temporal. A clean evaluation passes through to the apply.

Signed override (deferred). Pausing for a human, signed, audited override to proceed despite a violation requires conditional gating in the generated workflow — it is tracked as a follow-on, not in this version. Today policyGate blocks; loosen the policy or fix the violation to proceed.

The full example is lexicons/k8s/examples/org-policy — it is exercised in CI (clean in dev, blocked in prod).