A Go-first, multi-service compliance mapping platform that compares compliance standards, maps relationships between requirements, and stores the complete graph with full traceability.
CrossCodex delivers composable microservices, provider-agnostic LLM integration, and multi-tenant security with defense-in-depth.
LLM WARNING: This project was written with LLM (AI) assistance.
CrossCodex is in early development. All foundational, domain, and service packages are implemented and tested. The CLI provides approximately 22 commands across project, catalog, run, results, prompt, version, and completion groups with daemon connectivity and embedded single-node mode. See Development below to build from source and run tests.
The graph storage hardening in #148 makes these breaking changes:
- Tenant IDs are limited to 52 characters (3-52 lowercase letters, digits and hyphens). Existing longer tenants stop working and must be re-provisioned under a new ID; see the "Tenant IDs longer than 52 characters" note linked below for the detection query and steps.
- Derived edge ID formats changed, so existing graphs do not deduplicate against newly written edges.
- Temporal graph properties (
valid_from,valid_to,analyzed_at) use a new fixed-width encoding. - Node and edge properties that use a reserved key (such as
idorvalid_from) are rejected. BulkCreateEdgesrejects requests with more thangraph.max_bulk_edgesedges (default 1000).CreateEdgeandBulkCreateEdgesreject atemporal.confidenceoutside 0 to 1.- The never-populated
BulkCreateEdgesResponse.errorsfield (field 3) is removed and reserved;BulkCreateEdgesreports failures only as the RPC status. - The unused
tenants.allowed_tenantsconfig key is removed. A config file orCROSSCODEX_TENANTS_ALLOWED_TENANTSthat still sets it is ignored; delete it.
Graphs written before this change must be rebuilt. Follow the "Upgrade note (#148)" in docs/dev/design-principles.md.
The pre-release schema migrations are collapsed into one baseline, 001_initial_schema. A database migrated by an earlier build cannot be upgraded; crosscodexd refuses it as newer than itself. Drop it (for the compose deployment, task deploy:down VOLUMES=1) and recreate it with crosscodexd migrate.
Tenant administration (#31):
- The gateway refuses every non-health request, with
PermissionDenied, from a tenant whosetenantsrow is missing, suspended or deleted. The initial schema creates thetenant_adminrole thatcrosscodexd admin tenantconnects as, so tenants can only be provisioned aftercrosscodexd migrateruns. Once crosscodexd has migrated, set the role's password andCROSSCODEX_DATABASE_TENANT_ADMIN_DSN, then runcrosscodexd admin tenant createfor each authenticated tenant before sending it traffic; requests from that tenant are refused until then. Roleallstill provisionstenants.default_tenantitself. See Tenant administration.
Identity lifecycle (#195):
- Tenants and SAME_AS verdicts can no longer be deleted by any role short of the database owner or a superuser deliberately disabling triggers (see Identity Lifecycle). Retire a tenant or reviewer with the soft-delete commands under Tenant administration.
adjudicate artifacts deciderequires a registered reviewer; register reviewers withcrosscodexd admin reviewer add.
Prerequisites: Go >= 1.23, Task (taskfile.dev), container engine (podman or docker)
task buildtask dev:upThis starts PostgreSQL with AGE graph extension and pgvector.
The dev database requires mutual TLS. Copy the full DSN that task dev:up prints:
<!-- secretlint-disable-next-line @secretlint/secretlint-rule-database-connection-string -- documentation example with dev-only credentials (user: postgres, password: integration, host: localhost) -->
crosscodex config set database.dsn "postgres://postgres:integration@localhost:15432/crosscodex_test?sslmode=verify-full&sslrootcert=.test-output/certs/ca.pem&sslcert=.test-output/certs/client.pem&sslkey=.test-output/certs/client-key.pem"task fetch:oscal-docsThis downloads official NIST OSCAL catalogs to the catalogs/ directory.
crosscodex project initcrosscodex catalog import catalogs/NIST_SP-800-53_rev5_catalog.json# List all imported catalogs
crosscodex catalog list
# Inspect a specific catalog (use the ID from the list output)
crosscodex catalog inspect <catalog-id>Run crosscodex --help to see all available commands, or crosscodex <command> --help for command-specific usage.
The target architecture consists of seven core services that can run embedded in a single process or distributed across multiple hosts. Today the monorepo provides implemented infrastructure (pkg/config, pkg/db, pkg/storage, pkg/natsbus, pkg/tlsconfig, pkg/authn, pkg/tenant, pkg/telemetry, pkg/llmclient) and implemented domain packages (pkg/analyzer, pkg/attestation, pkg/oscal, pkg/graphdb, pkg/vectordb, pkg/prompt). Implemented services include internal/worker (LLM task execution), internal/synthesis (viability ranking and quality diagnostics), internal/graph (compliance relationship graph), internal/analysis (analysis engine), internal/catalog (OSCAL catalog management), internal/gateway (API gateway), internal/pipeline (job orchestration), and the analyzer plugins (internal/analyzer/classify, internal/analyzer/embedding, internal/analyzer/relationship).
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#2f6dab',
'primaryTextColor': '#1e1e1e',
'primaryBorderColor': '#7c8ba1',
'lineColor': '#7c8ba1',
'edgeLabelBackground': '#eef2f8',
'tertiaryColor': 'transparent',
'tertiaryTextColor': '#7c8ba1',
'tertiaryBorderColor': '#7c8ba1',
'clusterBkg': 'transparent',
'clusterBorder': '#7c8ba1',
'titleColor': '#7c8ba1',
'noteBkgColor': '#eef2f8',
'noteTextColor': '#1e1e1e',
'fontFamily': 'system-ui, sans-serif'
}, 'themeCSS': '.node .nodeLabel{color:#ffffff!important;fill:#ffffff!important;}'}}%%
flowchart TD
subgraph external ["External Sources"]
docs["Documents<br/>PDF, DOCX, HTML"]
end
subgraph processing ["Processing Pipeline"]
ingestion["Ingestion Service<br/>Python + Docling"]
catalog["Catalog Service<br/>Go + OSCAL"]
analysis["Analysis Engine<br/>Go + Plugins"]
llm["LLM Workers<br/>Go + AI Models"]
end
subgraph orchestration ["Orchestration"]
synthesis["Synthesis<br/>Quality Ranking"]
pipeline["Pipeline<br/>Go + NATS"]
end
subgraph datalayer ["Data Layer"]
graphdb["Graph Store<br/>PostgreSQL + AGE"]
vectordb["Vector Store<br/>pgvector"]
end
subgraph infra ["Infrastructure"]
tenant["Tenant Isolation"]
tls["TLS / mTLS"]
authn["Authentication"]
telemetry["Telemetry<br/>OpenTelemetry"]
attestation["Attestation<br/>in-toto"]
end
docs --> ingestion --> catalog --> analysis --> llm
llm --> synthesis --> graphdb
pipeline --> analysis
pipeline --> synthesis
graphdb --> pipeline
analysis --> vectordb
infra -.-> processing
infra -.-> orchestration
infra -.-> datalayer
classDef sysA fill:#2f6dab,color:#ffffff,stroke:#7c8ba1
classDef sysB fill:#1d7848,color:#ffffff,stroke:#7c8ba1
classDef sysC fill:#7457b8,color:#ffffff,stroke:#7c8ba1
classDef sysD fill:#2d747e,color:#ffffff,stroke:#7c8ba1
classDef sysF fill:#5c6a82,color:#ffffff,stroke:#7c8ba1
class ingestion,catalog,analysis,llm sysA
class synthesis,pipeline sysB
class graphdb,vectordb sysC
class tenant,tls,authn,telemetry,attestation sysD
class docs sysF
| Service | Purpose | Technology |
|---|---|---|
| Ingestion | Multi-format document conversion via Docling | Python gRPC service |
| Catalog | OSCAL parsing, document structuring, validation | Go |
| Analysis Engine | Host for analyzer plugins, DAG execution | Go |
| LLM Workers | Horizontally scalable LLM task execution | Go |
| Synthesis | Ranking, viability weighting, quality metrics | Go |
| Graph | openCypher queries via Apache AGE on PostgreSQL | Go |
| Pipeline | Job orchestration, state tracking, retry logic | Go |
- Embedded -- All services in one process with auto-bootstrapped mTLS. Requires PostgreSQL with AGE and pgvector extensions (start with
task dev:up). Local filesystem for object storage. Catalog import, list, and inspect work for OSCAL JSON documents. Runcrosscodexd migratebefore startingcrosscodexdagainst a database.crosscodexd(default--role=all) bootstraps a DB pool and NATS client and runs the graph, worker, and gateway/pipeline roles together in one process; analysis result persistence is durable end-to-end at the library layer (internal/pipeline,pkg/analyzer). - Production (compose) -- Single-host production stack with full mutual TLS across all services (PostgreSQL, NATS, LiteLLM, CrossCodex gateway). One-command bring-up via
task deploy:upafter populatingdeploy/.envand generating certificates withtask deploy:certs. The LiteLLM gateway runs behind a stunnel sidecar to provide end-to-end mTLS. See deploy/README.md for setup, first-use CLI commands, and operations. - Distributed -- Services scale independently with external PostgreSQL cluster (AGE + pgvector), NATS cluster with JetStream, and S3-compatible object storage. Multi-host deployment and FIPS 140 container images tracked in issue #17.
See Service Runtime Guide for the --role flag, role precedence, health-check wiring, and gateway startup preconditions.
CrossCodex follows XDG Base Directory conventions:
$XDG_CONFIG_HOME/crosscodex/
config.yaml # User-level defaults
profiles/
local.yaml # Single-node overrides
distributed.yaml # Cluster overrides
credentials/ # API keys, certificates (mode 0600)
tenants/ # Per-tenant configuration
Project directory:
.crosscodex/
config.yaml # Project-specific overrides
prompts/ # Custom prompt templates
Additional XDG directories:
$XDG_DATA_HOME/crosscodex/
prompts/ # User prompt template layers
$XDG_STATE_HOME/crosscodex/
pki/ # Embedded daemon auto-generated TLS certificates
daemon.pid # Embedded daemon PID and port
Values merge in ascending priority (last wins):
- Compiled defaults
- System config (
/etc/crosscodex/config.yaml) - System drop-ins (
/etc/crosscodex/conf.d/*.yaml) - User config (
$XDG_CONFIG_HOME/crosscodex/config.yaml) - User drop-ins (
$XDG_CONFIG_HOME/crosscodex/conf.d/*.yaml) - Profile (
--profile local) - Project config (
.crosscodex/config.yaml) - Environment variables (
CROSSCODEX_*) - CLI flags (highest priority)
# LLM Gateway
llm:
gateway_url: "http://localhost:4000"
default_model: "qwen3:8b"
timeout: 30
# Storage
storage:
objects:
backend: local # local | s3
# Database
database:
dsn: "${DATABASE_DSN}" # app_user, e.g. postgres://app_user:example-password@localhost:5432/crosscodex
migration_dsn: "${MIGRATION_DSN}" # schema owner, used only by `crosscodexd migrate`, e.g. postgres://owner:example-password@localhost:5432/crosscodex
extensions: [age, vector]
# TLS (Global Default)
tls:
mode: "mutual" # off | server-only | mutual
ca: /etc/crosscodex/tls/ca.crt
cert: /etc/crosscodex/tls/server.crt
key: /etc/crosscodex/tls/server.key
# Multi-tenant X.509 certificate-to-tenant mapping
tenants:
enabled: true
auth:
x509_mappings:
- match:
organization: "Acme*"
org_unit: "Engineering"
tenant: acme-engineering
roles: [admin, writer]
- match:
san_email: "*@partner.com"
tenant: partner-org
roles: [reader]
# Analysis
analysis:
classification:
enabled: true
model: "" # Inherits from llm.default_model
max_text_length: 2000
temperature: 0.0
max_tokens: 20
embedding:
enabled: true
models: ["snowflake-arctic-embed2"] # Embedding model names; storage is model-agnostic (see docs/dev/embeddings.md), switching models needs no schema migration
max_chars: 1500 # Max runes before truncation
batch_size: 50 # Controls per batch call
relationship:
top_k: 20 # Most-similar pairs to retain
candidates:
min_embedding_coverage: 0.8 # Minimum fraction of a job's controls that must have an embedding before candidate generation runs; below this, the candidate_generation stage fails the job (fail-closed)
embed_model: "snowflake-arctic-embed2" # Embedding model candidate generation reads; must be one of analysis.embedding.models
artifact_adjudication: # `crosscodexd admin adjudicate artifacts`
enabled: false # The command refuses to run until true
models: [] # Panel models; required when enabled
samples_per_model: 3 # Votes per model per pair; odd unless allow_even_samples
allow_even_samples: false # Allow an even samples_per_model
sampling_temperature: 0.3 # [0, 2]; 0 is used with one sample per model
max_tokens: 200 # Max tokens per panel reply
consensus_threshold: 0.67 # [0.5, 1]; below it (or on a split) the pair is undecided
max_error_rate: 0.34 # [0, 1]; more failed votes than this fails the panel (0 disables)
max_attempts: 5 # Failed panels before a pair is abandoned
lease_duration: 15m # How long a run holds each leased pair, and each verdict it or `decide` projects; must be positive even when enabled is false
lease_batch: 10 # Pairs leased per round trip
panel_concurrency: 4 # Completions in flight per pair
retry_base_delay: 1m # Wait after a pair's first failed panel, doubling per failure
retry_max_delay: 24h # Cap on that wait; at least retry_base_delay
graph:
max_bulk_edges: 1000 # Most edges per BulkCreateEdges request, range [1, 10000]; larger requests are rejected (global-only)
synthesis:
confidence_threshold: 0.5 # Minimum confidence for viable mappings
max_mappings_per_control: 10 # Maximum mappings per source control
viability:
type_mismatch_factor: 0.8 # Penalty for different classification types
skip_level_factor: 0.7 # Penalty for non-adjacent abstraction levels
integral_to_factor: 1.1 # Boost for INTEGRAL_TO contribution type
assessment:
iqr_good: 20.0 # IQR threshold for good embedding spread
iqr_poor: 10.0 # IQR threshold for poor embedding spread
no_rel_high: 0.97 # NO_RELATIONSHIP rate upper bound
no_rel_low: 0.80 # NO_RELATIONSHIP rate lower bound
contested_warn: 0.20 # Contested pairs warning threshold
actionable_warn: 0.30 # Actionable coverage warning thresholdThe CLI recognizes these environment variables:
| Variable | Purpose | Default |
|---|---|---|
CROSSCODEX_ENDPOINT |
daemon address | localhost:50051 |
CROSSCODEX_COLOR |
Force color output (1) or disable (0) |
Auto-detect (isatty) |
CROSSCODEX_LOGLEVEL |
Log level (debug/info/warn/error) |
warn |
NO_COLOR |
Disable color output (standard convention) | — |
Persistent flags override the configured log level for a single invocation:
-v,--verbose— set log level toinfo(repeat-vvfordebug)--debug— set log level todebug(implies--verbose; wins if combined)
Precedence (highest first): --debug/-vv → -v → logging.level
(config file or CROSSCODEX_LOGLEVEL) → default warn. The resolved level
applies to both the CLI and the auto-started embedded daemon.
CrossCodex uses a Go monorepo with separate repositories for Python ingestion and TypeScript UI:
crosscodex/ # Main monorepo
api/proto/ # Protobuf definitions
pkg/ # Public SDK packages
cmd/ # CLI and daemon binaries
internal/ # Service implementations
deploy/ # Deployment manifests (compose stack, see deploy/README.md)
- Go >= 1.26 -- see
go.modfor exact version - Task (taskfile.dev) -- install via
go install github.com/go-task/task/v3/cmd/task@latest - Buf (buf.build) -- for protobuf code generation
- Container engine (podman or docker) -- for integration tests only
# Build all binaries
task build
# Run all tests
task test
# Run unit tests only
task test:unit
# Lint
task lint
# Generate protobuf code
task generateRun task --list for all available commands including integration tests and development utilities.
| Test Type | Framework | Status |
|---|---|---|
| Unit | Ginkgo/Gomega (BDD) | Available (task test:unit) |
| Integration | Go testing + containers | Available (task test:integration:all) |
| E2E | Venom | Available (task test:e2e) |
| Fuzz | Go native fuzzing | Available (task test:fuzz) |
task test:fuzz fuzzes every Fuzz* target for FUZZ_TIME each (default 10s). It runs one worker per CPU core, so expect it to occupy the machine. Narrow it with PKGS, for example task test:fuzz PKGS=./pkg/backup FUZZ_TIME=1m.
task test already replays every target's seed corpus and committed testdata/fuzz/ inputs, so known crashers are regression-tested on every run. In CI, pull requests fuzz only the packages whose Go files they change (30s per target). The nightly Fuzz (nightly) workflow fuzzes every target for 2m each, uploads any new crash inputs as the fuzz-crashers artifact, and (scheduled runs only) opens or comments on a "Nightly fuzz run failed" issue, which is closed by hand once a run passes. GitHub disables scheduled workflows in public repositories after 60 days without repository activity; re-enable it from the Actions tab.
Gateway stress tests verify concurrent upload handling, resource limits, data integrity, and throughput under load.
# Run in-process stress tests
task test:stress # alias for test:stress:unit
task test:stress:unit
# Run throughput benchmark
task test:stress:bench
# Run full-stack round-trip integration suite
task test:stress:e2eBuild tag: Stress tests compile only under -tags stress and are excluded from the default task test / go build ./... path.
E2E requirements: task test:stress:e2e requires a podman runtime (rootless mode). The taskfile sets TMPDIR=/workspace/.test-output/buildtmp for build scratch. The container storage driver is taken from your podman configuration (storage.conf), matching the rest of the integration suite; if your environment has no overlay support, select vfs via storage.conf or STORAGE_DRIVER=vfs rather than relying on the taskfile.
Embedded daemon end-to-end tests exercise startup (PKI, migrations, tenant provisioning, mTLS server), a full catalog round-trip over the mTLS gateway, graceful shutdown, and error paths.
# Run the embedded daemon e2e suite (starts a PostgreSQL container)
task test:integration:embeddedBuild tag: Embedded e2e tests compile only under -tags integration and require TEST_DATABASE_DSN; the suite skips cleanly when it is unset. The podman TMPDIR / storage-driver notes under Stress Tests apply here too (environment-only, not baked into the Taskfile).
Venom E2E suites exercise the crosscodexd daemon as a black box: they start the daemon and its PostgreSQL container, drive inline OSCAL through the mTLS gateway, and assert the resulting AGE graph.
# Run the hermetic E2E suite (Tier A, no LLM required)
task test:e2e # alias for test:e2e:venom
task test:e2e:venom
# Run the opt-in suite that adds real LLM analysis (Tier B)
task test:e2e:venom:llmTiers: test:e2e:venom (Tier A) is hermetic and asserts the OSCAL-to-AGE structural surface without an LLM. test:e2e:venom:llm (Tier B) is opt-in and additionally spins up Ollama and LiteLLM to exercise real LLM analysis.
Requirements: both suites need venom, psql, and pg_isready on the host plus a podman (or docker) runtime; the LLM tier also needs jq. The podman TMPDIR / storage-driver notes under Stress Tests apply here too.
See CONTRIBUTING.md for development workflow, PR process, and coding standards.
All GitHub Actions workflows follow least-privilege principles:
- Default to no permissions (
permissions: {}) with explicit per-job grants - MegaLinter runs actionlint and other YAML-aware linters for workflow validation
Every layer enforces tenant isolation independently:
| Layer | Mechanism | Purpose |
|---|---|---|
| Gateway | mTLS client certificates, JWT sessions, RBAC | Identity verification |
| Services | Request metadata validation | Context propagation |
| NATS | Tenant-scoped subjects and ACLs | Message isolation |
| PostgreSQL | Row-Level Security policies | Data isolation |
| Object Store | Tenant-prefixed paths, bucket policies | Artifact isolation |
| Graph (AGE) | Separate graph per tenant | Traversal isolation |
| Method | Use Case | How It Works |
|---|---|---|
| X.509 (mTLS) | CLI, service-to-service, automation | Client certificate during TLS handshake |
| GSSAPI (Kerberos) | Enterprise SSO, Active Directory | Kerberos ticket via SPNEGO |
| SAML | Web UI, browser SSO | SAML assertion from IdP |
CrossCodex supports dual builds (standard and FIPS) from the same source:
- FIPS build: BoringCrypto, approved cipher suites only (container image planned — Red Hat UBI base)
- Standard build: Go stdlib crypto (container image planned — distroless base)
- Runtime enforcement:
tls.fips.enabled: truevalidates FIPS compliance
Pipeline outputs include in-toto attestation for audit trails:
- Layout: Signed by Pipeline service declaring authorized stages and functionaries
- Links: Per-stage attestations with input/output hashes, model versions, environment
- Verification: Independent validation via in-toto CLI (CrossCodex verify command planned)
See Cryptographic Attestation Guide for the attestation model, trace correlation, and implementation roadmap.
PostgreSQL with extensions handles all data:
| Store | Extension | Purpose |
|---|---|---|
| Relational | PostgreSQL | Job metadata, catalogs, classifications, tenant config |
| Graph | Apache AGE | Relationship graph, openCypher queries, temporal attributes |
| Vector | pgvector | Embedding similarity search |
Additional storage:
| Store | Technology | Purpose |
|---|---|---|
| Object Store | Local FS / S3 | Documents, embeddings, attestation bundles |
| Message Bus | NATS JetStream | Audit trails, work distribution, service communication |
Built-in observability with OTLP export:
- Traces: Span per stage, span per LLM call, cross-service correlation
- Metrics: Job duration, LLM latency, worker utilization, queue depth
- Logs: Structured logging correlated to trace IDs.
crosscodexdwrites logs to stderr atlogging.level(defaultwarn) inlogging.format(textorjson, defaulttext)
See Telemetry Guide for configuration, Jaeger setup, metrics reference, and instrumentation status.
JetStream provides persistent audit streams:
| Stream | Retention | Content |
|---|---|---|
| Decisions | Indefinite | Final compliance determinations |
| LLM Calls | 90 days | Full prompts, responses, model versions |
| Events | 30 days | Pipeline lifecycle, debugging |
See Audit Streams Guide for provenance headers, message inspection, and trace correlation.
CrossCodex enforces per-data-class retention policy: expired data is archived to
a secondary store and then purged from the primary store, unless a legal hold
covers it. Every operation is scoped to a single tenant. Completed jobs and
catalogs are purged as aggregates with their dependents: an expired catalog is
archived as {catalog, classifications, controls} and then purged together with
its classifications, controls, and embeddings. The archive deliberately
omits embeddings (derived/regenerable, so purge-only); their lifetime is bound
to the parent catalog's retention rather than an independent
retention.tiers.embeddings tier. For how the engine works internally, see the
Retention Lifecycle guide.
All admin commands take --tenant, which must match the tenant the caller is
authenticated for; the server authorizes every request against the caller's
tenant.
# Scan and enforce the retention lifecycle for a tenant.
crosscodex admin retention scan --tenant acme
# Preview without mutating any data.
crosscodex admin retention scan --tenant acme --dry-run
# Show retention-eligible counts and the active-hold total.
crosscodex admin retention stats --tenant acmeA legal hold prevents matching data from being purged for as long as it is
active. An empty scope field is a wildcard (matches everything), so a hold with
no --job/--created-before covers the whole tenant.
# Create a hold. --name is required. The server forces the hold's tenant to the
# caller's tenant, so a hold can never be planted for another tenant.
crosscodex admin hold create --tenant acme --name "litigation-2026-014"
# Narrow a hold to one job and/or only data created before a cutoff (RFC3339),
# and optionally auto-release it at a future timestamp.
crosscodex admin hold create --tenant acme --name "audit-hold" \
--job job-abc123 \
--created-before 2026-01-01T00:00:00Z \
--expires-at 2026-12-31T00:00:00Z
# List active holds (alias: ls).
crosscodex admin hold list --tenant acme
# Release a hold by name. Released holds remain visible in `list` as an audit
# trail but no longer block purges.
crosscodex admin hold release --tenant acme --name "litigation-2026-014"crosscodexd never migrates. Before the first start, and after every upgrade, run:
crosscodexd migrateIt connects as the schema owner (database.migration_dsn, env CROSSCODEX_DATABASE_MIGRATION_DSN), applies pending migrations, sets the passwords of app_user, graph_user, purge_user, tenant_admin and backup_user from the passwords in database.dsn, database.graph_dsn, database.purge_dsn, database.tenant_admin_dsn and backup.dsn, and provisions tenants.default_tenant. It is idempotent. The daemon and the crosscodexd admin commands that open the application database (retention scan, reconcile artifacts, and adjudicate artifacts including review, show and decide) refuse a superuser, BYPASSRLS or table-owner connection, and a schema that is not at the latest migration. admin tenant, admin identity (database.tenant_admin_dsn) and admin backup (backup.dsn) do not run these checks. See Database Migrations. The embedded crosscodex CLI is unchanged: it migrates and provisions with the DSN it is given.
For on-host operation (e.g. an external scheduler running next to the daemon),
crosscodexd exposes in-process one-shot commands. Like healthcheck and
version, they are unauthenticated by design. They run locally against the
daemon's configured resources and select their tenant with --tenant, or run
for every active tenant with --all-tenants (retention scan --all-tenants
covers every tenant, deleted and suspended included, because a deleted tenant's
job data still follows retention):
crosscodexd admin retention scan --tenant acme
crosscodexd admin retention scan --tenant acme --dry-run
crosscodexd admin retention scan --all-tenants
crosscodexd admin reconcile artifacts --tenant acme
crosscodexd admin reconcile artifacts --tenant acme --dry-run --max-edges 10000
crosscodexd admin reconcile artifacts --all-tenants --dry-run
crosscodexd admin adjudicate artifacts --tenant acme
crosscodexd admin adjudicate artifacts --tenant acme --dry-run
crosscodexd admin adjudicate artifacts --tenant acme --reproject
crosscodexd admin adjudicate artifacts --all-tenants --max-pairs 500
crosscodexd admin adjudicate artifacts review --tenant acme
crosscodexd admin adjudicate artifacts show --tenant acme --pair 3f9c0a1b2d4e5f60
crosscodexd admin adjudicate artifacts decide --tenant acme --pair 3f9c0a1b2d4e5f60 --verdict rejected --reviewer jdoe --expect <token>retention scan and an adjudicate artifacts run without --dry-run or
--reproject publish audit events, so they attach to the daemon's NATS server. They refuse to start
when nats.url is empty, because embedded mode would start a second JetStream
server on the daemon's store directory. They also refuse a server whose audit
streams do not exist yet, and never create or update a stream, so a one-shot
cannot change the daemon's stream retention. Start the daemon first.
--all-tenants and --tenant are mutually exclusive, and adjudicate artifacts review and decide take --tenant only. With --all-tenants:
- Tenant list: the command lists tenants through
database.tenant_admin_dsnand skips suspended ones. It refuses to run when that DSN is unset. - Order and output: tenants run one at a time in ID order, on one set of
connections opened for the whole run. Each report on stdout is preceded by a
tenant: <id>line. - Failures: a failing tenant does not stop the rest. Its error goes to
stderr, and once every tenant has run the command prints
<n> of <m> tenants failed: <ids>and exits1. If the command's connections or configuration cannot be opened at all (for example the database, graph or NATS is unreachable, or the adjudicator's prompts or LLM client cannot be set up), the command prints that one error and exits1without running any tenant. - No active tenants (
retention scan: no tenants): the command says so on stderr and exits0.
reconcile artifacts links equivalent artifacts across controls and catalogs:
- Exact duplicates: artifacts with the same type and normalized name join one
ArtifactGroupnode throughMEMBER_OFedges. - Overlapping names: groups of one type whose names overlap (token-set overlap of
0.6 or more) get a scored
SAME_ASedge.
It never merges or deletes nodes. Every run is a full pass, and re-running it over
an unchanged graph writes no graph elements (it re-queues open pairs for
adjudication, which leaves already-queued pairs unchanged), so it is safe to
schedule (cron, Kubernetes CronJob) once per tenant or once with --all-tenants.
- Overlapping runs: do not run two reconciles for the same tenant at the same time.
--max-edges: defaults to 50000. A run that would add moreSAME_ASedges fails before writing anything; this usually means an over-broad artifact name.- Exit codes: 0 on success, 1 on failure, 2 on usage errors.
--dry-run: thenew_*counts report what the run would have written.
reconcile artifacts also queues every overlapping-name pair for
adjudication in PostgreSQL (artifact_pair_verdicts). It skips pairs that are
closed, meaning confirmed or rejected, and reports them in its closed row. It
therefore needs the database as well as the graph.
adjudicate artifacts asks an LLM panel whether each queued pair really
denotes one artifact. Token overlap over-matches containment: "Policy" scores
1.0 against "Password Policy".
- Confirmed: the panel says yes. The scored edge is superseded by a
SAME_ASedge withdetermination_typellm_panel. - Rejected: the panel says no. The scored edge is superseded and nothing replaces it.
- Undecided: the panel split or fell below
analysis.artifact_adjudication.consensus_threshold. Automation does not retry the pair; only a human verdict can change it. - Failed: too many calls failed or replies did not parse. The pair is
retried after a backoff (
retry_base_delay, doubling per failure up toretry_max_delay; 1 minute and 24 hours by default) and is abandoned aftermax_attemptsfailures. - LLM unavailable: if every call for a pair fails because the LLM gateway
is unavailable or not configured, the rate limit is hit, the credentials
cannot be resolved or are rejected, or the model is not allowed, the
tenant's run stops, the command exits
1, and the pair is not charged a failure. An outage or a wrong key therefore cannot abandon the queue; fix the cause and run the command again. With--all-tenants, an outage stops each tenant's run in turn: every tenant fails fast on its first pair, no attempts are charged, and the command exits1naming them. - Human verdicts win: automation never overwrites a pair a human decided
with
decide(see Human verdicts). If one lands while the panel is running, the panel's opinion is stored as dissent and the graph keeps the human verdict.
Other behavior:
- Scheduling: run
reconcile artifactsfirst, thenadjudicate artifacts, each on its own schedule. Concurrent adjudicators are safe: each pair is leased to one run at a time, and a pair whose lease expired before its panel started is skipped and retried by a later run. --max-pairs: the most pairs sent to the panel per tenant per run (default 200), which bounds LLM spend. Every pair costslen(models) × samples_per_modelcompletions.- Report:
reset(with--reproject: verdicts marked for re-projection),recovered(verdicts from earlier runs written to the graph now),orphaned(confirmations whose groups are gone from the graph),leased,confirmed,rejected,undecided,failed,abandoned,dissent(a human decided the pair meanwhile),stale(another run decided it meanwhile),expired(lease expired before the panel started),fenced(a newer verdict replaced the one being written to the graph, or another run took over the write; the newer verdict is written next) andbusy(another run is writing the pair's verdict to the graph and finishes it). - Audit: every panel call publishes an LLM audit event to the
AUDIT_LLMstream (see Audit Trails), so a real run also attaches to the daemon's NATS server (see above). A tenant run's events share the job IDartifact-adjudication-<uuid>, logged asrun.id. --dry-run: reportspending,dueandunprojectedcounts without leasing, calling the LLM or writing verdicts or graph elements. It opens no LLM client and no NATS connection, so it runs on a host without an LLM gateway or NATS (the configuration check below still applies). Like every command that opens the database, it does not migrate: it refuses an outdated schema and points atcrosscodexd migrate.--reproject: with--tenantonly. Marks the tenant's projected confirmed and rejected verdicts, LLM-panel and human alike, unprojected and writes them back to the graph (resetcounts them), then stops: it leases no pairs and makes no LLM calls, so like--dry-runit needs no LLM gateway or NATS. Use it after rebuilding the tenant's graph, then run without--reprojectto adjudicate due pairs. It cannot be combined with--dry-run,--max-pairsor--all-tenants.- Configuration: the command refuses to run until
analysis.artifact_adjudication.enabledistrueandmodelsis set (see Key Configuration Examples).reviewanddecidedo not. - Prompt: the panel uses the
artifact_same_asprompt. Like any other prompt, a YAML file with thatnamein$XDG_DATA_HOME/crosscodex/prompts/or in a directory listed inprompt.layer_pathsoverrides it. Artifact names and samples come from analyzed catalogs, so they are flattened to one line, stripped of invisible characters, capped and escaped before they reach the prompt. - Exit codes: 0 on success, 1 on failure, 2 on usage errors.
- Clocks: leases, and the claims that reserve a verdict while it is
written to the graph, are timed by the clock of the host running the
command (
adjudicateordecide). Keep every such host in sync (NTP); otherwise a skewed host can take a pair whose lease or claim has not yet expired. A host whose clock runs far ahead holds its claims that far into the future: until then every run skips the pair (--dry-runstill counts it asunprojected) and its verdict does not reach the graph.--reprojectdoes not clear a claim; clearing one early takes a manual database update of that pair's row.
After rebuilding a tenant's graph, re-run the reconciler, then run
crosscodexd admin adjudicate artifacts --tenant <tenant> --reproject to write
the tenant's verdicts back to the graph, human verdicts included.
crosscodexd admin backup captures, lists, verifies and restores PostgreSQL (WAL-G base backups plus WAL archiving for PITR), tenant object stores, and the JetStream audit streams:
crosscodexd admin backup run
crosscodexd admin backup list
crosscodexd admin backup verify [--point ID]
crosscodexd admin backup restore --point IDbackup run— captures Postgres, every tenant's objects and the audit streams into a new backup point.backup list— lists complete backup points (and incomplete ones separately).backup verify— checks blob/snapshot checksums, WAL-G base backup presence, WAL continuity and staleness.backup restore— restores objects and audit streams from a backup point; Postgres is restored separately withcrosscodex-db-restore.
Exit codes: 0 ok, 1 failure or stale, 2 usage error. See deploy/README.md, Backups for setup, the run/verify/restore procedures, and PITR.
adjudicate artifacts review, show and decide let a person settle the
SAME_AS pairs the panel could not, or overrule a panel verdict. The workflow
has three steps: list, read, decide.
crosscodexd admin adjudicate artifacts review --tenant acme --limit 20
crosscodexd admin adjudicate artifacts show --tenant acme --pair 3f9c0a1b2d4e5f60
crosscodexd admin adjudicate artifacts decide --tenant acme --pair 3f9c0a1b2d4e5f60 --verdict confirmed --reviewer jdoe --expect <token>1. review lists up to --limit pairs (default 100), oldest first, that
need a person:
undecidedandabandonedpairs;- human verdicts that a later panel run disagreed with (its opinion is stored as dissent).
Its columns are PAIR (the pair ref), STATUS, BY (who decided it:
llm_panel, human, or token_overlap for an abandoned pair), VOTES,
TYPE, LOW and HIGH (the two artifact groups' names), SCORE (token
overlap) and DISSENT (the dissenting verdict, or -). VOTES is the
current panel verdict's split:
2/3 same: two of three valid votes said the groups are the same;2/3 same (1 failed): the same, with one more vote that failed and did not count;unreadable: the stored votes cannot be decoded;showprints the error;-: no panel verdict (an abandoned or human-decided pair).
Names come from analyzed catalogs, so they are flattened to one line,
stripped of control and invisible characters and capped before they are
printed. With nothing to review it prints no pairs need review.
2. show prints everything a reviewer needs to decide one pair, so the
decision rests on the panel's reasoning and not only its vote count:
PAIR 3f9c0a1b2d4e5f60
TYPE policy
LOW Access Control Policy
HIGH Access Policy
SCORE 0.71
STATUS undecided
BY llm_panel / artifact-adjudicator
CONFIDENCE 0.50
DECIDED 2026-10-13T09:14:02.51Z
PROMPT artifact_same_as v1.1.0
VOTES (1 same, 1 distinct, 1 failed; 2 valid of 3)
voter-a same high
Both titles name the organization's access control policy ...
voter-b distinct medium
The second document scopes only remote access ...
voter-c failed -
PANEL DISSENT
none
RELATED PAIRS (showing 1 of 1 that share a group)
7d21e0c4b9a85f36 confirmed llm_panel Access Policy | Access Control Standard
EXPECT 9be41c07d3a2f685
To decide: crosscodexd admin adjudicate artifacts decide --tenant acme --pair 3f9c0a1b2d4e5f60 --verdict <confirmed|rejected> --reviewer <id> --expect 9be41c07d3a2f685
The example is illustrative; values vary. Its sections:
- Header: the pair, its artifact groups, the token-overlap
SCORE, the currentSTATUS, who decided it (BYis determination type / decider),CONFIDENCE,DECIDEDtime and the prompt that produced the panel verdict. VOTES: the split, then each vote's voter, decision (same,distinctorfailed), confidence and justification.PANEL DISSENT: on a human verdict, the panel's opinion, labelledoverridden by the human verdict(the panel verdict the person replaced),given after the human verdict (disputed)(a later panel run disagreed; the pair is inreview) orgiven after the human verdict (agrees). A dissent that cannot be decoded is labelledunreadable. Otherwisenone.LAST ERROR: shown only when the last panel attempt failed.RELATED PAIRS: other pairs that share a group with this one, with their status and decider.--related <n>sets how many to list (default 20, must be positive); the heading says how many exist in total.EXPECT: the tokendecideneeds, and a ready-to-editdecidecommand.
Everything printed from stored data is cleaned the same way as review.
show changes nothing. If the stored votes cannot be decoded, it prints the
pair with an unreadable VOTES section and the EXPECT token anyway, then
exits 1 with the error; the pair can still be decided.
3. decide records the verdict, using the EXPECT token from show
(see the --expect item below).
To change a verdict the panel already reached, find the pair with --name:
crosscodexd admin adjudicate artifacts review --tenant acme --name "access policy"
crosscodexd admin adjudicate artifacts decide --tenant acme --pair 7d21e0c4b9a85f36 --verdict rejected --reviewer jdoe --expect <token>review --name <text> lists, instead of the review queue, up to --limit
pairs in any status (pending, confirmed, rejected, undecided, abandoned),
whoever decided them, whose LOW or HIGH name contains the text. Matching
ignores case, and %, _ and \ match only themselves. The text must be
1–200 bytes of plain text (no control or invisible characters). Same columns,
oldest first; on a pair a person decided, DISSENT shows the panel's
opinion, such as the verdict the person overrode. With no match it prints no pairs match "<text>".
decide records --verdict confirmed or --verdict rejected on one pair:
- Pair ref:
--pairis thePAIRcolumn ofreview, 16 lowercase hex characters derived from the pair's internal key. It is opaque and safe to paste; the key itself embeds artifact names and is never printed. A ref that matches no pair of the tenant records nothing.reviewlists only pairs that need a person; usereview --nameto find the ref of any other pair, such as one the panel confirmed or rejected. --expect: required. TheEXPECTvalueshowprinted for this pair. It identifies the state you read: the verdict, who decided it, its confidence, decision time, score, votes and dissent. If another person or a panel run changed the pair since,deciderecords nothing and sayspair <ref> changed since you viewed it: it is now .... Runshowagain, read the new reasoning, and decide with the new token. Queue bookkeeping (retries, leases, projection) does not change the token. Repeating a verdict the pair already holds from a person is accepted with any valid token. Without--expect,decideexits 2 and names theshowcommand to run.--reviewer: required. The ID of the person recording the verdict, stored asdetermined_byon the verdict and on a confirmation's graph edge (at most 256 bytes, no control or invisible characters). It must be a registered, non-deleted reviewer of the tenant (crosscodexd admin reviewer add, see Tenant administration); otherwisedeciderecords nothing and names the command to run. It is not authenticated: like everycrosscodexd admincommand,decidetrusts whoever can run it on the host.- Authority: a human verdict outranks the panel, and automation never
changes it. Any pair can be decided, including one the panel decided or
another person decided; a panel verdict it overrides is kept as dissent.
Repeating a pair's current human verdict changes nothing and prints
unchanged(the originaldetermined_byis kept), unless a later panel run disagreed with it (the pair is inreview). Thendecidewith the same verdict re-affirms it: it records--reviewerand the new decision time, writes a fresh graph edge for a confirmation (the earlier human edge ends at the new decision time), printsre-affirmed over the panel's later dissent, and the pair leavesreview. The panel's opinion stays as dissent. - Graph:
decidewrites the verdict to the graph at once. A confirmation adds aSAME_ASedge withdetermination_typehumanand confidence 1; either verdict supersedes every other currentSAME_ASedge between the two groups (the token-overlap candidate, a panel edge, an earlier human edge). It then prints the projection report:recovered(verdicts written to the graph),orphaned(confirmations whose groups are gone from the graph),fencedandbusy(as foradjudicate artifacts). - Delays: if an
adjudicaterun is writing the pair's previous verdict to the graph at that moment,deciderecords the verdict, prints that it reaches the graph when that run finishes or by re-runningdecideafter a printed time, and leaves the pair to that run. If its own graph write fails, it says the verdict is recorded and when to re-run (up tolease_durationlater). Re-runningdecidewith the same arguments never records the verdict twice; it finishes the graph write. - Requirements: neither command needs
analysis.artifact_adjudication.enabled, an LLM gateway or NATS; they open only the database and the graph.lease_durationbounds how long one write may hold a pair, so it is validated (must be positive) even when adjudication is disabled, and a non-positive value fails config load. - Rebuilds:
adjudicate artifacts --reprojectwrites human verdicts back to a rebuilt graph along with the panel's. - Exit codes: 0 on success, 1 on failure, 2 on usage errors.
crosscodexd admin tenant create --tenant acme --display-name "Acme Corp"
crosscodexd admin tenant list
crosscodexd admin tenant inspect --tenant acme
crosscodexd admin tenant suspend --tenant acme
crosscodexd admin tenant resume --tenant acme
crosscodexd admin tenant import --file tenants.yamlThese commands connect as the least-privilege tenant_admin role, never the
app or owner credential. Set database.tenant_admin_dsn (or
CROSSCODEX_DATABASE_TENANT_ADMIN_DSN) to a DSN for that role.
crosscodexd migrate creates the role; set its password yourself. The commands
refuse to run while the DSN is empty.
-
create: creates the tenant and its graph, or renames an existing one without changing its status. Printscreatedorupdated. Display names must not be blank or contain control characters. -
suspend/resume: while suspended, the gateway refuses every request from the tenant except health checks, untilresume. Queued pipeline work still drains. Printssuspendedoralready suspended, andresumedoralready active. Both refuse a deleted tenant; runtenant restorefirst. -
import: applies a YAML file in one transaction, so either every tenant is created or renamed, or none is. Renaming a deleted tenant is refused, as withcreate:tenants: - tenant_id: acme display_name: Acme Corp
Tenants and reviewers are never removed, because their judgements are permanent. They are soft-deleted, and a deleted one can be hidden behind a pseudonym:
crosscodexd admin tenant delete --tenant acme --by alice --reason "contract ended"
crosscodexd admin tenant restore --tenant acme --by alice --reason "contract renewed"
crosscodexd admin tenant hide --tenant acme --by alice --reason "customer requested"
crosscodexd admin tenant unhide --tenant acme --by alice --reason "audit follow-up"
crosscodexd admin reviewer add --tenant acme --reviewer jdoe --by alice --reason "joined review team"
crosscodexd admin reviewer list --tenant acme
crosscodexd admin reviewer delete --tenant acme --reviewer jdoe --by alice --reason "left the team"
crosscodexd admin reviewer restore --tenant acme --reviewer jdoe --by alice --reason "rejoined"
crosscodexd admin reviewer hide --tenant acme --reviewer jdoe --by alice --reason "privacy request"
crosscodexd admin reviewer unhide --tenant acme --reviewer jdoe --by alice --reason "audit follow-up"
crosscodexd admin identity trace --pseudonym hidden-reviewer-0123456789abcdef --by alice --reason "audit query"
crosscodexd admin identity export --out /secure/crosscodex-audit-2026-10-22.jsonlThese also connect as tenant_admin. Every command except reviewer list
and identity export changes or reveals an identity, so it requires --by (who is acting, 1-256
bytes) and --reason (1-500 bytes), is signed with the attestation keys
(attestation.private_key_path and attestation.public_key_path) and is
recorded in the database's audit log. Without the keys or
database.tenant_admin_dsn the commands refuse to run and change nothing.
tenant delete: the tenant's data and verdicts are kept, but the gateway and every service refuse it. Retention still applies to its job data.createcannot rename a deleted tenant.tenant restore: brings a deleted tenant back assuspended; runcrosscodexd admin tenant resumeto reactivate it. A hidden tenant must be unhidden first.reviewer add: registers a reviewer soadjudicate artifacts decideaccepts them. Printsadded, oralready registered. A deleted reviewer is restored withreviewer restore, not added again.reviewer delete/restore: a deleted reviewer's verdicts stay, butdeciderefuses new ones from them. Restoring makes them active again. On a deleted tenant,reviewer addandrestoreare refused;delete,hideandunhidestill work.hide/unhide: only a deleted tenant or reviewer can be hidden.hideprints the pseudonym (hidden-tenant-…orhidden-reviewer-…), which then replaces the ID intenant list,reviewer list,retention scan --all-tenantsandadjudicate artifacts show. The pseudonym stays the same across unhide and hide again. Hiding is not erasure: the real ID remains in stored verdicts, graph edges and audit records. Tenant and reviewer IDs starting withhidden-are reserved.identity trace: prints the kind, tenant, identity, status and hidden flag behind a pseudonym. The lookup itself is recorded.identity export: writes the whole audit log to--outas JSON Lines, one event per line inevent_idorder, and printsexported <n> identity events to <path>. Each line hasevent_id,occurred_at(RFC 3339, UTC),action,kind,tenant_id,identity_id,actor,db_user,reason,requestandattestation(base64 of the stored bytes, so their SHA-256 can be recomputed) andattestation_sha256. The file is sensitive: it holds the real IDs of hidden tenants and reviewers, so store it securely. It is created with mode0600and never overwrites an existing file. It is written as<path>.partialfirst and moved into place only when complete; a failed export removes the partial file. It reads only, so it needs no--by,--reasonor attestation keys. Run it before decommissioning a database (see Decommissioning).- Exit codes: 0 on success, 1 on failure, 2 on usage errors.
retention:
# Default retention per data class. Each value is "indefinite", "<n>y",
# "<n>d", or any Go duration (e.g. "720h"). An empty value means "keep".
defaults:
attestation: indefinite
job_results: 90d
catalogs: 2y
embeddings: 90d
# Audit-class tiers are accepted for config completeness but do NOT drive
# engine purges — the retention engine never purges audit data. Audit
# retention is enforced by JetStream stream MaxAge via
# nats.streams.audit_llm_retention / audit_events_retention
# (see docs/dev/audit-streams.md).
audit_decisions: indefinite
audit_llm: 90d
audit_events: 30d
# Optional cron expression for the in-daemon scheduler. Empty = disabled
# (drive scans externally via the one-shot instead; see below).
scan_schedule: ""
# Archive backend for data moved out of the primary store before purge.
archive:
backend: local # local | s3 | "" (disabled)
local:
path: /var/lib/crosscodex/archive
s3:
bucket: crosscodex-archive
storage_class: GLACIER
# Per-tenant overrides. Non-empty fields replace the matching default;
# empty fields fall back to the default.
tenants:
acme:
job_results: 1y
database:
# Purge runs under a dedicated, RLS-scoped purge_user role that the
# delete-path immutability triggers accept. When purge_dsn is unset, scans
# still run but purges fail closed rather than deleting under the wrong role.
purge_dsn: "${PURGE_DATABASE_DSN}" # e.g. postgres://purge_user:...@localhost:5432/crosscodex
backup:
# postgres:// URL for backup_user (LOGIN REPLICATION).
# Required when destination.backend is set. Never logged.
# CROSSCODEX_BACKUP_DSN sets this field from the environment.
dsn: ""
destination:
backend: "" # local | s3 | "" (backup disabled; fails closed)
local:
path: "" # absolute; must not overlap storage.objects.base_path
s3:
bucket: ""
region: ""
endpoint: "" # S3-compatible stores; enables path-style addressing
storage_class: ""
max_age:
# `admin backup verify` reports a store stale once its newest backup
# point is older than this. Each value must be greater than zero.
postgres: 26h
objects: 26h
nats: 26hWhen retention.scan_schedule is left empty, drive scans from an external
scheduler using the daemon one-shot. Example crontab entry running a nightly
scan of every tenant at 02:00:
0 2 * * * crosscodexd admin retention scan --all-tenants >> /var/log/crosscodex/retention.log 2>&1To scan only one tenant, use --tenant acme in place of --all-tenants. Do not
schedule both at the same time: non-dry-run scans share one global lock, so one
of them would fail every night (see Backups).
Artifact deduplication runs the same way. Reconcile first, then adjudicate:
0 3 * * * crosscodexd admin reconcile artifacts --all-tenants >> /var/log/crosscodex/reconcile.log 2>&1
30 3 * * * crosscodexd admin adjudicate artifacts --all-tenants >> /var/log/crosscodex/adjudicate.log 2>&1To fully remove CrossCodex:
-
Remove the binary (location depends on your install method):
rm $(which crosscodex) -
Remove configuration:
rm -rf "${XDG_CONFIG_HOME:-$HOME/.config}/crosscodex" -
Remove data (prompt layers):
rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/crosscodex" -
Remove state (daemon PID, embedded TLS certificates):
rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/crosscodex" -
Remove project-level configuration (run in each project directory):
rm -rf .crosscodex/
-
Remove shell completions (if installed):
rm -f ~/.bash_completion.d/crosscodex
- Issues: github.com/complytime-labs/crosscodex/issues
- Discussions: github.com/complytime-labs/crosscodex/discussions
- License: Apache 2.0
- CrossCodex Ingestion - Python document conversion service
- CrossCodex UI - React web interface
- Docling - Document extraction library
- Apache AGE - Graph extension for PostgreSQL
- NATS - Cloud native messaging system
- in-toto - Supply chain attestation framework