Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ jobs:
run: bash scripts/ci/workflow-guardrails.sh .
- name: Canon-guardrail regression suite
run: bash scripts/test-canon-guardrail.sh
- name: Public-truth check (public-claims accuracy)
run: bash scripts/ci/public-truth.sh .
5 changes: 3 additions & 2 deletions AUDIT_POSTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ See [`SECURITY.md`](SECURITY.md). All Tier-1 repos must include a `SECURITY.md`

## Audit history

Federation-wide audit history is summarized below. Individual audit reports live in each repo's `audits/` directory; pre-split audit reports live in [`citrate-monorepo-archive`](https://github.com/CitrateNetwork/citrate-monorepo-archive)'s `audits/`.
Federation-wide audit history is summarized below. Individual audit reports live in each repo's `audits/` directory. Pre-split audit reports are kept in a private archive and are not public.

| Date | Auditor | Scope | Outcome |
|---|---|---|---|
| Pre-split | various | Monorepo as of 2026-05-17 | See archive `audits/` |
| Pre-split | internal | Monorepo as of 2026-05-17 | Private archive, not public |
| 2026-09-24 | internal adversarial audit (pre-bounty) | Federation, public repos and live testnet | Remediation in progress |
| Planned: Q3 2026 | TBD | `citrate-chain` Tier-1 pass before `v0.5.0` stable | — |

## Changelog of this document
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ cargo run --release -- --rpc-url http://127.0.0.1:8545
| Network name | Citrate |
| Chain ID | `40204` (hex `0x9d0c`) |
| RPC URL | `https://rpc.citrate.ai` |
| WebSocket | `wss://ws.citrate.ai` |
| WebSocket | not publicly served yet (run a local node for `ws://127.0.0.1:8546`) |
| Block explorer | `https://explorer.citrate.ai` |
| Faucet | `https://faucet.citrate.ai` |
| Chain spec | `citrate-chain/specs/testnet.toml` |
| Chain spec | `citrate-chain/node/config/testnet.toml` |

> Mainnet target is Q2 2027; the current network is chain 40204. Confirm live endpoint
> status in the [docs](https://docs.citrate.ai) before assuming availability.
Expand Down Expand Up @@ -308,12 +308,12 @@ see each repo's README for specifics.

| Repo | What it builds | Run |
|---|---|---|
| [`citrate-inference-gateway`](https://github.com/CitrateNetwork/citrate-inference-gateway) | x402-metered inference gateway | `cargo run --release` |
| [`citrate-inference-gateway`](https://github.com/CitrateNetwork/citrate-inference-gateway) | Inference gateway (paid routes not deployed yet) | `cargo run --release` |
| [`citrate-compute-pool`](https://github.com/CitrateNetwork/citrate-compute-pool) | Coordinator + workers for pooled training | `cargo run --release` |
| [`citrate-cluster`](https://github.com/CitrateNetwork/citrate-cluster) | GPU-fleet and compute-cluster tooling | `cargo run --release` |
| [`citrate-core`](https://github.com/CitrateNetwork/citrate-core) | Desktop app that runs a full node | `cargo run --release` |
| [`citrate-comms`](https://github.com/CitrateNetwork/citrate-comms) | E2E-encrypted, server-blind team workspace | `pnpm install && pnpm dev` |
| [`citrate-quorum`](https://github.com/CitrateNetwork/citrate-quorum) | Human-in-the-loop governance surface for AI | `cargo run --release` |
| [`citrate-quorum`](https://github.com/CitrateNetwork/citrate-quorum) | HIC (Human In Control) governance surface for AI | `cargo run --release` |
| [`citrate-identity`](https://github.com/CitrateNetwork/citrate-identity) | OIDC/OAuth2 authority (SIWE, passkeys) | `pnpm install && pnpm dev` |
| [`citrate-memories`](https://github.com/CitrateNetwork/citrate-memories) | Content-addressed knowledge graph for agents | `cargo run --release` |
| [`citrate-native`](https://github.com/CitrateNetwork/citrate-native) | Slint desktop wallet + agent client | `cargo run --release` |
Expand Down Expand Up @@ -454,11 +454,11 @@ on:
jobs:
rust:
if: hashFiles('Cargo.toml') != ''
uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@v1 # pin a tag or SHA, not @main
uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6 # pin a SHA, not @main

js:
if: hashFiles('package.json') != ''
uses: citratenetwork/.github/.github/workflows/reusable-js-ci.yml@v1 # pin a tag or SHA, not @main
uses: citratenetwork/.github/.github/workflows/reusable-js-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6 # pin a SHA, not @main
```

### Release notifications
Expand All @@ -474,7 +474,7 @@ no diff for a caller's reviewer to see. Pin to a tag or SHA so an upgrade is a
reviewable change in the caller:

```yaml
uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@v1 # or @<40-hex-sha>
uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@<40-hex-sha> # no release tags exist yet
```

When breaking-change updates are made, cut a new tag here so consumer repos can pin against it.
Expand Down
32 changes: 18 additions & 14 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ This document covers all repositories under the [`CitrateNetwork`](https://githu

Preferred (encrypted, no key exchange): use **GitHub private vulnerability reporting** — on the affected repository, open the **Security** tab → **Report a vulnerability**. This gives a private, GitHub-encrypted channel with no PGP key to fetch.

Alternatively, email **security@citrate.ai**. To encrypt an emailed report, fetch our PGP public key from `keys.openpgp.org` (search `security@citrate.ai`) or via the `Encryption` field of our [`security.txt`](https://citrate.ai/.well-known/security.txt).
Alternatively, email **security@citrate.ai**. We do not publish a PGP key yet, so send sensitive details through private vulnerability reporting rather than plain email. Our [`security.txt`](https://citrate.ai/.well-known/security.txt) lists the same contacts.

> GH-B-012: the previous PGP path pointed at `keys/security@citrate.ai.asc` in the **private** `citrate-monorepo-archive` repo, which no external reporter can read — the documented encryption path did not work. Use private vulnerability reporting instead.
Bounty policy: coming soon. It will be linked here once counsel has reviewed it (OWNER).

Include in your report:
- The repo + commit SHA (or version tag) where you observed the issue
Expand All @@ -22,14 +22,18 @@ We acknowledge within **72 hours** and aim to triage within **5 business days**.

## Scope

Severity tiers and audit cadence per repo are documented in each repo's `AUDIT_TIER.md`. The TL;DR:
Severity tiers and audit cadence per repo are documented in each repo's `AUDIT_TIER.md`. The summary
below is the same table that appears on the [security posture page](https://docs.citrate.ai/security/posture).

| Tier | Audit policy | Vulnerability handling |
|---|---|---|
| **Tier 1** (chain, native app, SDKs, agent-runtime, gateway, compute-pool) | Full audit before every stable release | Coordinated disclosure; CVE assigned for high+ |
| **Tier 3** (docs, and other content/library repos) | Content review only | Triage as docs corrections, no CVE |
| Tier | Repositories | Audit policy | Vulnerability handling |
|---|---|---|---|
| **Tier 1**: consensus, value, keys, identity | `citrate-chain` (node, contracts, ZK), `citrate-core`, `citrate-identity`, `citrate-inference-gateway`, `citrate-compute-pool`, `citrate-coop`, `citrate-agent-runtime`, `citrate-sdk-js`, `citrate-sdk-python` | Full adversarial audit before every stable release | Coordinated disclosure; a GitHub Security Advisory (with a CVE request) for fixed High and Critical issues in released code |
| **Tier 3**: docs and content | `citrate-docs`, `.github`, and other content-only repositories | Content review | Triage as documentation corrections, no CVE |

Per-repo tier is authoritative in each repo's `AUDIT_TIER.md`.
A repository's own `AUDIT_TIER.md` is authoritative for that repository. A public repository without an
`AUDIT_TIER.md` is handled as Tier 1 for reports.

No advisories have been published yet.

## Responsible disclosure

Expand All @@ -53,13 +57,13 @@ We will **not** pursue legal action against researchers who:
- Social engineering of team members.
- Physical access attacks against operator hardware.

## Supply-chain integrity
## Supply-chain integrity (current practice)

- Crates published from `citrate-chain` are signed via cosign keyless OIDC. See the chain's `.github/workflows/release.yml` for the signing pipeline.
- npm packages from `citrate-sdk-*` are published with provenance attestations.
- SBOMs (CycloneDX) attach to every Tier-1 release.
- `citrate-chain`'s release workflow is built to sign artifacts with cosign (keyless OIDC) and attach CycloneDX SBOMs, but no public release carries signed assets yet: the signed `v0.5.0-beta2-tier2` build is still a draft. Treat current prereleases, including the `citrate-core` desktop builds, as unsigned and without SBOMs.
- `@citratelabs/sdk` on npm is published with a provenance attestation. `@citratelabs/marketplace-sdk` is not yet.
- Supply-chain hardening is in progress: required review and CI checks on every public repository, third-party GitHub Actions pinned to commit SHAs, and signed releases with SBOMs.

Verifying a release artifact:
Verifying a signed release artifact, once published:

```bash
# cosign verify-blob with the issuer / identity from the release
Expand All @@ -71,7 +75,7 @@ cosign verify-blob --certificate-identity-regexp 'https://github\.com/CitrateNet

## Audit firms + history

Per-repo audit history lives in each repo's `audits/` directory (when present) or in the [`citrate-monorepo-archive`](https://github.com/CitrateNetwork/citrate-monorepo-archive) for pre-split history. The next planned audit is the chain Tier-1 pass before the `v0.5.0` stable tag.
Per-repo audit history lives in each repo's `audits/` directory, when present. History from before the repositories were split is kept in a private archive and is not public. No external-firm audit has been completed yet; the next planned audit is the chain Tier-1 pass before the `v0.5.0` stable tag.

## Contact

Expand Down
13 changes: 7 additions & 6 deletions profile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ Licensor: **Citrate Inc.** Each repository's `LICENSE` file is authoritative. A
- **`citrate-agent-runtime`** — Capability-scoped agent execution runtime + capsules.
- **`citrate-sdk-js`** — TypeScript SDK (`@citratelabs/sdk`).
- **`citrate-sdk-python`** — Python SDK.
- **`citrate-sdk-marketplace`** — Marketplace SDK (metered, pay-per-call inference).
- **`citrate-sdk-marketplace`**: Marketplace SDK (metered inference; the paid rails are not deployed yet).
- **`citrate-docs`** — The Almanac: docs.citrate.ai.
- **`citrate-explorer`** — CitrateScan, the AI-native BlockDAG explorer.

### Application layer / commercial core — BUSL-1.1 (source-available, converts to Apache-2.0)

- **`citrate-inference-gateway`** — x402-metered, pay-per-call AI inference gateway.
- **`citrate-inference-gateway`**: AI inference gateway. Paid calls (x402 and API-key metering) are not deployed yet.
- **`citrate-compute-pool`** — Coordinator + workers for pooled AI training.
- **`citrate-cluster`** — GPU-fleet and compute-cluster tooling.
- **`citrate-core`** — Desktop app that turns your machine into a full node.
- **`citrate-comms`** — End-to-end-encrypted, server-blind team workspace.
- **`citrate-quorum`** — Human-in-the-loop governance surface for AI.
- **`citrate-quorum`**: HIC (Human In Control) governance surface for AI.
- **`citrate-identity`** — OIDC/OAuth2 authority with SIWE and passkeys.
- **`citrate-memories`** — Content-addressed knowledge graph for agents.
- **`citrate-native`** — Slint desktop wallet and agent client.
Expand All @@ -65,9 +65,10 @@ Usage in any repo's `.github/workflows/ci.yml`:
```yaml
jobs:
rust:
# GH-B-003: pin to a release tag or a full commit SHA, never @main (a mutable
# branch: one push to this repo would change every caller's CI with no diff).
uses: CitrateNetwork/.github/.github/workflows/reusable-rust-ci.yml@v1
# GH-B-003: pin to a full commit SHA, never @main (a mutable branch: one push
# to this repo would change every caller's CI with no diff). This repo has no
# release tags yet, so @v1 does not resolve; use a reviewed commit SHA.
uses: CitrateNetwork/.github/.github/workflows/reusable-rust-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6
with:
working-directory: '.'
apt-packages: 'libclang-dev cmake libssl-dev pkg-config libfontconfig1-dev'
Expand Down
6 changes: 6 additions & 0 deletions scripts/ci/public-truth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# public-truth.sh: the org profile, README and security policy must not make claims
# the code or the live network contradict (public-claims accuracy). Logic lives in public_truth.py.
# Usage: bash scripts/ci/public-truth.sh [root] (exit 1 on any hit)
set -euo pipefail
exec python3 "$(dirname "${BASH_SOURCE[0]}")/public_truth.py" "${1:-.}"
124 changes: 124 additions & 0 deletions scripts/ci/public_truth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Public-truth check for the org profile, README and security policy (public-claims accuracy).

Text is normalised before matching (line breaks and runs of whitespace collapse to one
space, a hyphen split across a line break is joined, markdown emphasis is dropped), so a
reworded or re-wrapped claim is still caught. Exit 1 on any hit.

Usage: python3 scripts/ci/public_truth.py [root]
"""
from __future__ import annotations

import re
import subprocess
import sys
from pathlib import Path

DASH = r"[\s\-‐-―]*"


def normalise(text: str) -> str:
t = re.sub(r"-\s*\n\s*", "-", text) # "Human-in-\n the-loop" -> "Human-in-the-loop"
t = re.sub(r"[*_`]+", "", t) # markdown emphasis / code ticks
t = re.sub(r"\s+", " ", t)
return t


def sentences(t: str) -> list[str]:
return re.split(r"(?<=[.!?|])\s+", t)


RULES: list[tuple[str, re.Pattern, re.Pattern | None]] = [
("owner rule: use HIC (Human In Control), never HITL",
re.compile(r"\bH\.?I\.?T\.?L(s|'s)?\b"), None),
("owner rule: use HIC, never human-in-the-loop",
re.compile(r"human" + DASH + r"in" + DASH + r"(the" + DASH + r")?loop", re.I), None),
("dead host (no DNS / 404 / 530)",
re.compile(r"wss?://ws\.citrate\.ai|scan\.citrate\.ai|rpc2\.citrate\.ai|mirror\.citrate\.ai", re.I), None),
("citrate-chain/specs/testnet.toml does not exist (use node/config/testnet.toml)",
re.compile(r"specs/testnet\.toml"), None),
("links a private repository",
re.compile(r"citrate-monorepo-archive|citrate-agentile-archive", re.I), None),
("no PGP key is published for security@citrate.ai",
re.compile(r"keys\s*\.\s*openpgp\s*\.\s*org|PGP (public )?key (from|at|via)", re.I), None),
("overstated supply-chain claim",
re.compile(r"(\b(are|is) cosign[- ]signed|cosign[- ]signed (releases|crates|artifacts)|signed (via|with) cosign|SBOMs? \(?CycloneDX\)? attach|every (tier-1 )?release (ships|carries|includes|has)|CVE assigned for high)", re.I),
re.compile(r"^.{0,25}(built to|once published|not yet|no public release|in progress)", re.I)),
("paid inference rails described as live (not deployed)",
re.compile(r"(x402|pay" + DASH + r"per" + DASH + r"call|paid (inference|routes|calls))[^.|]{0,80}\b(live|available now|today|metered|generally available|in production)\b|x402-metered", re.I),
re.compile(r"not (yet )?(deployed|live|mounted)", re.I)),
("names an audit finding ID; public copy must not describe open findings",
re.compile(r"\bPBA-[A-Za-z0-9]+-\d+\b"), None),
]


def remote_v_tags(root: Path) -> list[str]:
"""Release tags on origin. Works under a shallow checkout, which fetches no tags."""
for cmd in (["git", "-C", str(root), "ls-remote", "--tags", "origin", "v*"],
["git", "-C", str(root), "tag", "-l", "v*"]):
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
except Exception:
continue
if out.returncode == 0:
return [l.split("refs/tags/")[-1] for l in out.stdout.split() if "v" in l]
return []


def main() -> int:
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
files = sorted(p for p in list(root.glob("*.md")) + list((root / "profile").glob("*.md")) if p.is_file())
errs: list[str] = []
for f in files:
rel = f.relative_to(root)
norm = normalise(f.read_text(errors="replace"))
for label, rx, qual in RULES:
for s in sentences(norm):
m = rx.search(s)
if not m:
continue
if qual is not None:
window = s[max(0, m.start() - 40): m.end() + 40]
if re.search(r"not (yet )?(deployed|live|mounted)|built to|once published|not yet|no public release", window, re.I) and not re.search(r"\bare cosign|\bis cosign", s[m.start():m.end()], re.I):
continue
errs.append(f"{rel}: {label}: ...{s[max(0, m.start() - 40):m.end() + 60]}...")

# Reusable-workflow pins must resolve.
tags = remote_v_tags(root)
for f in files:
for m in re.finditer(r"reusable-[a-z-]+\.yml@(v[0-9][\w.]*)", f.read_text(errors="replace")):
if m.group(1) not in tags:
errs.append(f"{f.relative_to(root)}: pins @{m.group(1)}, which is not a tag on origin; pin a commit SHA")

bounty = root / "BOUNTY.md"
sec = root / "SECURITY.md"
if not bounty.exists():
# No bounty policy is published yet: nothing may point readers at one.
for f in files:
if re.search(r"BOUNTY\.md", f.read_text(errors="replace")):
errs.append(f"{f.relative_to(root)} links BOUNTY.md, which does not exist yet (bounty policy: coming soon)")
else:
b = bounty.read_text()
for section in ("## In scope", "## Not deployed or not running", "## Safe harbor", "## Known issues", "## Rewards"):
if section not in b:
errs.append(f"BOUNTY.md lacks section '{section}'")
ki = b.split("## Known issues", 1)[-1].split("\n## ", 1)[0]
body = re.sub(r"\s+", " ", ki).strip()
if re.search(r"^\s*([-*|]|\d+[.)])\s", ki, re.M) or body not in ("Published per finding once fixed. `OWNER TO FILL`.",):
errs.append("BOUNTY.md Known issues must stay a placeholder until each finding is fixed and the owner publishes it")
if "not in force" in b:
for f in files:
if f.name != "BOUNTY.md" and re.search(r"BOUNTY\.md", f.read_text(errors="replace")):
errs.append(f"{f.relative_to(root)} links BOUNTY.md while BOUNTY.md is marked not in force (counsel sign-off pending)")

for e in errs:
print(f"::error::public-truth: {e}")
if errs:
print(f"public-truth: FAIL ({len(errs)})")
return 1
print(f"public-truth: OK ({len(files)} files)")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading