Standardization batch — no application code changes. Pulls in the
DeBros DAO baseline rules (v0.1.0, sha 51ce3f8) for supply-chain
defense and toolchain pinning.
Files added:
- DEBROS.md + debros.json — adopted-rules manifest
- .debros/compliance/{go,javascript-typescript,zig}.md — per-language
compliance docs
- .github/workflows/security.yml — auto-detecting security CI
(npm audit + go vulncheck), runs on main + weekly cron
- renovate.json — 30-day dependency cooldown, no auto-merge,
vulnerability alerts bypass cooldown
- .nvmrc — pin Node 20.18.0
- vault/.zigversion — pin Zig 0.14.0
- sdk/.npmrc, website/.npmrc — supply-chain hardening
(ignore-scripts, strict-peer-dependencies, save-exact, etc.)
Files modified:
- core/go.mod, os/agent/go.mod, website/invest-api/go.mod —
add `toolchain go1.24.6` directive for reproducible builds
- VERSION + sdk/package.json — bump to 0.122.11
11 KiB
Compliance — Zig
The concrete files every Zig project must have to satisfy DEBROS.md.
Zig is the youngest ecosystem in this rules set. The good news: Zig's design avoids most supply-chain attack vectors (no install-time scripts, dependencies are content-addressed by hash). The bad news: there's no mature vulnerability database, no Renovate support, and no convention-defining popular packages to follow. Compliance leans heavily on manual review.
Status: Zig is pre-1.0 (current stable is
0.13.xas of late 2025). Build APIs change between releases. Treat this document as a moving target — verify the directives below still work on your project's pinned compiler.
Required files
1. build.zig.zon with explicit hashes for every dependency
Tier 3 block. Commits that add a dependency without an explicit hash are rejected.
Every dependency in build.zig.zon MUST include:
url— the source tarballhash— the integrity hash Zig computes
.{
.name = "your-project",
.version = "0.1.0",
.dependencies = .{
.zap = .{
.url = "https://github.com/zigzap/zap/archive/refs/tags/v0.6.0.tar.gz",
.hash = "1220abc123def456...", // explicit, required
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
Zig's zig build will refuse to use a dependency whose downloaded content doesn't match the declared hash. This is equivalent to Go's go.sum and is the bedrock of Zig's supply-chain story.
Never use unhashed path = ... references to remote sources. Local path dependencies are fine for in-monorepo modules; remote sources must always be hashed.
2. .zigversion — pin the compiler
Convention file (read by zigup, mise, asdf via plugin):
0.13.0
CI MUST use the pinned compiler version, not "latest" or "master." Pre-1.0 Zig changes language semantics between minor versions; "latest" is not a safe default.
For projects on Zig master (development versions): commit the exact commit SHA, not "master."
3. Verify the compiler signature on install
The Zig compiler binary is signed with Andrew Kelley's minisign key, published at https://ziglang.org/download/. Every CI environment and every developer's machine MUST verify the signature when installing the compiler.
In CI:
- name: Install Zig with signature verification
run: |
ZIG_VERSION=$(cat .zigversion)
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" -o zig.tar.xz
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz.minisig" -o zig.tar.xz.minisig
minisign -Vm zig.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
tar -xJf zig.tar.xz
The minisign public key above is the canonical one. Treat it as a pinned constant — if it changes, treat that change as a security event and verify out of band (mailing list, official site, multiple sources) before accepting.
4. Review every build.zig
Zig's build.zig is a Zig program. It runs at build time with full system access — it can read files, run subprocesses, hit the network. This is intentional (you can build C deps, run codegen, generate manifests) but it is also the equivalent of npm's postinstall problem at the build layer.
Rules:
- The project's own
build.zigMUST be reviewed line by line in PRs (it's not "configuration," it's executable code with full power) - Dependencies'
build.zigfiles MUST be read when adding the dependency. Subprocess invocations (std.process.Child), file writes outside the cache, or network calls are red flags - No dependency may invoke
std.process.Childto run shell scripts at build time without explicit allowlisting indebros.json.compliance.exceptions[]with a one-line justification
This is the single largest supply-chain risk in Zig. The compiler can't tell "legit codegen" from "exfiltrate ~/.ssh/." Human review is mandatory.
5. Lockfile-equivalent in CI
Zig doesn't have a separate lockfile; build.zig.zon's hash fields ARE the lockfile. CI MUST refuse to build if zig build would update build.zig.zon:
- name: Verify build.zig.zon is up to date
run: |
cp build.zig.zon build.zig.zon.expected
zig build --fetch
diff build.zig.zon build.zig.zon.expected
zig build --fetch resolves dependencies without compiling; if it would mutate build.zig.zon, the diff fails.
6. Compiler-version pinning in CI
Match the .zigversion:
- name: Install pinned Zig
uses: mlugg/setup-zig@v1
with:
version-file: .zigversion
(mlugg/setup-zig is the community-maintained action with signature verification built in.)
File-by-file checklist
| File | Path | Required? | Tier-3 block? |
|---|---|---|---|
build.zig.zon with hashes for every remote dep |
repo root | ✅ | ✅ |
.zigversion |
repo root | ✅ | — |
| CI workflow with compiler signature verification | .github/workflows/security.yml (or equivalent) |
✅ | — |
CI step verifying build.zig.zon is up-to-date |
same | ✅ | — |
Code patterns to enforce
Error handling — Zig's error unions are the friend
Per DEBROS.md §2.2 principle 6: errors carry context. Zig's error types are great but easy to misuse:
// Good — explicit error set, useful context
pub const ConnectError = error{
Timeout,
ConnectionRefused,
AddrInUse,
};
fn connectOlric(port: u16) ConnectError!Connection {
return Connection.init(port) catch |err| switch (err) {
error.Timeout => return error.Timeout,
error.ConnectionRefused => {
std.log.err("olric connection refused on port {d}", .{port});
return error.ConnectionRefused;
},
else => return err,
};
}
// Forbidden — silent swallow
fn connectOlric(port: u16) ?Connection {
return Connection.init(port) catch null; // hides why it failed
}
The try keyword bubbles errors; catch MUST handle them meaningfully (log + return, transform to a domain error, etc.) — never catch unreachable outside of provably-impossible cases.
Allocator discipline
Per DEBROS.md §2.2 principle 4 (validate at boundaries, trust internal code): every public function that allocates takes an std.mem.Allocator parameter. No global state, no hidden allocations.
// Good
pub fn parseConfig(allocator: Allocator, source: []const u8) !Config { ... }
// Forbidden
pub fn parseConfig(source: []const u8) !Config {
const allocator = std.heap.page_allocator; // hidden global
...
}
Tests use std.testing.allocator (catches leaks). Production uses a configured allocator (general-purpose arena, fixed buffer, etc.).
defer for cleanup; errdefer for error paths
Every allocation has a matching defer free (always cleanup) OR errdefer free (cleanup on error only, transfer ownership on success). Ad-hoc cleanup at the bottom of functions is forbidden.
File and function sizes
Per DEBROS.md §2.1:
- Functions ≤50 lines
- Files ≤300 lines
There's no widely-used Zig linter for this yet. Enforce via PR review checklist until tooling lands.
comptime discipline
comptime is powerful but easy to abuse. Rules:
- Use
comptimefor type-level computation (generic containers, compile-time validation of constants) - Never use
comptimefor "performance" without measuring comptimecode is subject to the same length and complexity caps as runtime code- A
comptimebranch that grows past 30 lines is a code smell — extract to a named function
Testing
Zig's built-in test runner is the standard:
test "parseCron rejects empty input" {
try std.testing.expectError(error.EmptyExpression, parseCron(""));
}
- Tests live alongside source (
test { ... }blocks in the same file, OR*_test.zigfiles) - Run via
zig build test - CI MUST run tests on every PR
- Unit suite total runtime <30s (DEBROS.md §2.4)
- No
std.time.sleepin tests — poll a readiness condition or use a fake clock
Dependency additions
When adding a Zig dependency, the agent MUST:
- Pin a tag, not a branch.
refs/tags/v0.6.0is OK;refs/heads/mainis not. Branch refs are mutable; tags should be immutable (verify the tag isn't a moving target on the upstream — some repos rewrite tags). - Read the dep's
build.zigfor subprocess invocations, network calls, or file writes outside the cache. Each is a red flag that requires justification. - Verify the hash. After adding the dep, run
zig build --fetchand confirm the computed hash matches what the upstream advertised. - Check the maintainer's track record. Single-author, low-star Zig repos are higher risk simply because the language attracts experimental code. Prefer deps with an active community.
- Note the lack of Renovate support. Zig dep updates are manual. Document the upstream tag-tracking process in a comment in
build.zig.zon.
Migration from a stock Zig project
- Pin the compiler. Add
.zigversion. - Audit
build.zig.zon. Every remote dependency must have ahash. Runzig build --fetchand copy the computed hashes in. - Read every
build.zigin your dependency tree. Flag anything that runs subprocesses or hits the network at build time. Open issues upstream OR find alternatives. - Add CI with compiler signature verification and
zig build --fetchlockfile check. - Update
debros.jsonto record Zig compliance is satisfied. Note anybuild.zigexceptions you accepted incompliance.exceptions[].
Expect first migration to take a day for projects with several deps — the build.zig review is the slow part.
Notes on Zig's supply-chain story
What Zig protects against (by design):
- Hash-pinned dependencies.
build.zig.zonmutation is loud; a swapped dep fails to build. - No install-time scripts. Dependencies don't run code when fetched (unlike npm postinstall).
- No package registry to compromise. Deps are URLs (usually GitHub tarballs); there's no central index to attack. Each upstream's compromise is isolated.
- Cryptographically-signed compiler releases. The official ziglang.org binaries are minisigned.
What Zig does NOT protect against:
build.zigrunning arbitrary code at build time. This is the equivalent of npm postinstall, but always-on. Human review of every dep'sbuild.zigis the only defense.- Compromised upstream repos. Hash-pinning catches changes to already-fetched versions, but a malicious new release still has whatever malicious content it ships with. There's no
pip-audit/govulncheckequivalent yet. - Tag rewriting. Some upstreams rewrite tags. Hash-pinning catches this on re-fetch, but the social signal is missed. Prefer upstreams with a no-tag-rewrite policy.
- Renovate support. None yet. Track dep updates manually. Open a Renovate config issue upstream if your CI infra needs auto-PRs.
Zig is the youngest ecosystem here and tooling is still catching up. Until the Zig package registry (or an equivalent) emerges, manual review is the floor.