config.example.toml
Rama master · réplica de lectura del repositorio original.
config_version = 1
[server]
host = "0.0.0.0"
port = 23333
log_level = "info"
# Empty means X-Forwarded-For / X-Real-IP are ignored. Add only the CIDRs of
# reverse proxies you operate, for example ["127.0.0.1/32", "10.0.0.0/8"].
trusted_proxies = []
[database]
driver = "sqlite" # currently the only supported database driver
dsn = "./data/depsilo.db"
[storage]
type = "local" # local | s3
path = "./data/cache"
# S3 配置(type = "s3" 时生效)
# bucket = "depsilo"
# endpoint = "http://minio:9000"
# region = "us-east-1"
# access_key = ""
# secret_key = ""
[cache]
max_size_gb = 20
ttl_index = "1h" # 索引元数据刷新间隔(过期后同步刷新;上游失败时返回旧索引)
ttl_blob = "72h" # wheel / .deb 文件
lru_threshold = 90 # 使用率超过此百分比触发 LRU 清理
# 级联部署(多级缓存):本机可把部分上游的出口流量交给另一台 Depsilo,
# 由父节点回源并缓存,局域网内的其他设备再从父节点缓存读取。
# 详见 docs/cascade.md;两端必须配置同一个 token。
# 这些值也可以在 Admin → 上游源 → 级联 页面编辑:保存写回本文件并需要
# 重启生效;token 只写不读,页面不会回显已保存的值。
# [cascade]
# enabled = true
# token = "replace-with-a-random-shared-secret"
# max_hops = 4 # 允许经过的级联节点层数
# max_ttl = "168h" # 父节点缓存新鲜度上限
# allow_insecure_http = false # 非回环地址使用 http:// peer 时必须显式开启
#
# [[cascade.peers]]
# name = "home"
# url = "http://192.168.1.10:23333"
# token = "" # 缺省复用 [cascade].token;可单独覆盖
# forward_credentials = false # 开启后上游 Authorization 会转发给该 peer
# 上游出站网络(重启生效,详见 docs/deployment.md#upstream-network-egress)。
# 本机代理软件开启 TUN/fake-IP 时,公网域名会被解析到 198.18.0.0/15,
# Depsilo 默认不连接这类地址:把 upstream_proxy 设为代理软件的 HTTP 端口。
# 它是所有未单独配置代理、也未走级联的包上游的默认代理;no_proxy 中的
# 上游主机保持直连。Tailscale / Headscale / NetBird 节点地址在 100.64.0.0/10,
# 需要在 private_ranges 中声明后才能作为上游或级联父节点。
# [egress]
# upstream_proxy = "http://127.0.0.1:7890"
# no_proxy = ["mirror.lan", "192.168.0.0/16"]
# private_ranges = ["100.64.0.0/10"]
# Package-policy snapshot refresh behavior. A stale, last-known-good snapshot
# is preferred when the policy store is temporarily unavailable; change to
# use_stale_then_deny, allow, or deny for a different explicit fallback.
[policy]
on_load_error = "use_stale_then_allow"
# 可选:为下游 ccache / sccache 提供本机或团队共享的编译产物缓存。
# 该存储必须与上面的包缓存隔离;启用后通过 Admin API 创建 namespace 凭据。
# 当前只支持单 Depsilo 实例,多个实例不能共享下面的目录/bucket 和容量账本。
[compile_cache]
enabled = false
# 编译缓存客户端实际访问的外部 origin。远程部署建议填 TLS 反向代理地址,
# 并确保 Depsilo 后端 HTTP 端口只对该代理或受信网络开放。
public_url = "http://127.0.0.1:23333"
# 仅在可信 LAN/VPN 且明确接受 bearer 明文传输风险时开启。
allow_insecure_http = false
max_size_gb = 20
max_entries = 500000
max_entry_size_mb = 512
namespace_max_size_gb = 20
namespace_max_entries = 250000
max_concurrent_uploads = 8
# 并发槽位之外最多允许等待的上传;队列满时快速返回 503。
max_queued_uploads = 32
# 单次上传的最长时间;超时会关闭请求体并释放上传槽位。
upload_timeout = "15m"
# 慢速下载的并发与最长传输时间,防止长期占用文件或 S3 响应体。
max_concurrent_downloads = 64
download_timeout = "15m"
# 所有并发上传的暂存数据总上限,且不能小于 max_entry_size_mb。
max_inflight_upload_size_mb = 1024
lru_threshold = 90
[compile_cache.storage]
type = "local" # local | s3
path = "./data/compile-cache"
# 这里的 S3 仅是 Depsilo 内部对象存储,不会向客户端暴露 S3 API。
# S3 必须使用独立 bucket,不能与包缓存共用;关闭 versioning 或配置版本生命周期。
# bucket = "depsilo-compile-cache"
# endpoint = "http://minio:9000"
# region = "us-east-1"
# access_key = ""
# secret_key = ""
# 可选:保存依赖集清单及其内容,独立于可淘汰的包缓存和编译缓存。
# 可导出、导入签名的完整传输包(docs/distribution.md);导入不会激活依赖集,
# 尚未提供离线包服务、安装端点或增量传输。
[distribution]
enabled = false
max_size_bytes = 21474836480 # 20 GiB;满额时拒绝新增内容,不自动淘汰
max_object_bytes = 1073741824 # 1 GiB;单个对象的上传上限
# 导出端:专用的 Ed25519 传输签名私钥(PKCS#8 PEM),与 SBOM/JWT/URL 签名密钥分开。
# 用 `depsilo distribution keygen --out FILE` 生成。
# transfer_signing_key_file = "/etc/depsilo/transfer-signing-key.pem"
# 导入端:只接受这些公钥签名的传输包。修改后需重启;格式错误或重复会导致启动失败。
# trusted_transfer_keys = ["ed25519:..."]
[distribution.storage]
type = "local" # local | s3
# 相对路径与其他 storage.path 一样,相对于服务的工作目录。
# 必须是独立目录,不得与包缓存或编译缓存目录重叠、互相包含。
path = "./data/distribution"
# S3 bucket 名必须与包缓存和编译缓存不同,即使 endpoint 不同也不能同名。
# 不能只靠前缀隔离;禁用 versioning 或配置版本生命周期。
# bucket = "depsilo-distribution"
# endpoint = "http://minio:9000"
# region = "us-east-1"
# access_key = ""
# secret_key = ""
# 对已缓存且具备 HTTP 验证器的 PyPI 兼容元数据主动执行条件检查。
# 不支持可靠变更判定的生态继续依赖正常 TTL 刷新;此任务绝不预拉取制品。
[upstream_updates]
enabled = true
check_interval = "1h"
[access_log]
# Async rollup pipeline for the access_logs table. See
# docs/specs/2026-06-26-access-log-rollup.md.
#
# DELETION WARNING: retention_days and rollup_retention_days physically
# DELETE rows older than the configured horizon. Set either to 0 to keep
# data forever (no sweep).
rollup_enabled = true # write hourly + package_daily rollups
batch_size = 100 # flush after this many rows
batch_interval = "5s" # ...or this much time, whichever first
backfill_on_start = true # one-shot fill rollups from existing rows on boot
retention_days = 7 # raw access_logs sweep horizon (0 = never sweep)
rollup_retention_days = 365 # rollup tables sweep horizon (0 = never sweep)
[auth]
enabled = true
# Omit jwt_secret for automatic instance-key management. On a new instance,
# serve generates data/secrets/jwt.key relative to this config file's directory
# and reuses it on restart. Persist and back up that file with the database.
# Advanced deployments may still supply DEPSILO_AUTH_JWT_SECRET or jwt_secret
# explicitly; existing values are used unchanged. Use at least 32 random bytes
# with no surrounding whitespace. Changing the key invalidates browser JWTs
# and signed npm / extra PyPI artifact URLs.
# jwt_secret = "<your-generated-instance-secret>"
token_ttl = "168h" # 7天
# Optional authentication sources. No external source is enabled or assumed by
# default. Add sources by protocol; currently only OpenID Connect (OIDC) is
# supported. Casdoor is one compatible provider, not a required/default product.
# Each source ID is stable: 1–32 lowercase letters/digits/hyphens, beginning with
# a letter. Rename display_name rather than the ID; bindings retain their ID.
# Nonempty issuer values must be distinct across sources, including disabled
# sources. Disabled drafts may leave the issuer unset until activation.
# Configure Authorization Code + PKCE and register the exact source callback.
# All enabled callbacks must share the instance's browser-visible HTTPS origin.
# Keep a local password administrator for recovery. Changes apply on restart;
# deleting a source never deletes accounts or bindings.
#
# [auth.sources.company]
# protocol = "oidc"
# enabled = false
# display_name = "Company login"
# issuer = "https://identity.example" # exact discovery issuer, including path
# client_id = "depsilo"
# Prefer DEPSILO_AUTH_SOURCES_COMPANY_CLIENT_SECRET; never add real secrets here.
# client_secret = ""
# redirect_url = "https://depsilo.example/api/v1/auth/oidc/company/callback"
# groups_claim = "groups"
# admin_groups = ["company/depsilo-admins"]
# readonly_groups = ["company/depsilo-readers"] # disjoint; unmapped users denied
# session_ttl = "30m" # absolute lifetime, 5m–1h; refresh cannot extend it
# allow_insecure_loopback = false # only isolated HTTP loopback development
#
# Repeat under another stable ID for another issuer. Environment overrides are
# DEPSILO_AUTH_SOURCES_<UPPERCASE_ID_WITH_HYPHENS_AS_UNDERSCORES>_<FIELD>.
# Group lists use comma-separated values. Only source IDs declared in TOML are
# loaded; environment variables cannot introduce another source. Secrets remain
# write-only in Admin; environment-only credentials are never written to TOML.
# Browser editing requires explicit source tables, not inline/dotted-key forms.
#
# Legacy configurations remain supported without renaming or a schema upgrade:
# [auth.oidc]
# enabled = false
# display_name = "OIDC"
# Its callback remains /api/v1/auth/oidc/callback and its environment prefix is
# DEPSILO_AUTH_OIDC_. The reserved source ID "oidc" cannot name a new source.
# Keep existing legacy issuer/client/group settings when upgrading; moving them
# to a different source ID does not migrate account bindings.
[license]
key = "" # Leave empty for Community edition, set License Key for Pro
# On the first start after upgrading, Depsilo imports ordinary ecosystem upstreams
# into the database and records the active ecosystems. After that seed, Admin and
# the database are authoritative: deleting or editing an upstream is not overwritten
# by later restarts. Adding upstreams in config for a previously inactive supported
# ecosystem activates it on the next restart. Docker registries and extra indexes
# remain config-owned and are not managed by Admin Upstream CRUD.
[[pypi.upstreams]]
name = "tuna"
url = "https://pypi.tuna.tsinghua.edu.cn"
priority = 1
[[pypi.upstreams]]
name = "official"
url = "https://pypi.org"
priority = 2
# proxy = "http://your-proxy:7890" # 可选,单独给此上游配 HTTP 代理
[[apt.upstreams]]
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn"
priority = 1
[[apt.upstreams]]
name = "aliyun"
url = "https://mirrors.aliyun.com"
priority = 2
[[npm.upstreams]]
name = "npmmirror"
url = "https://registry.npmmirror.com"
priority = 1
# via = "home" # 可选:该上游通过名为 home 的级联节点出口
[[npm.upstreams]]
name = "official"
url = "https://registry.npmjs.org"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[go.upstreams]]
name = "goproxy-cn"
url = "https://goproxy.cn"
priority = 1
[[go.upstreams]]
name = "official"
url = "https://proxy.golang.org"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[cargo.upstreams]]
name = "rsproxy"
url = "https://rsproxy.cn/index/"
priority = 1
[[cargo.upstreams]]
name = "crates-io"
url = "https://index.crates.io"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[maven.upstreams]]
name = "aliyun"
url = "https://maven.aliyun.com/repository/public"
priority = 1
[[maven.upstreams]]
name = "central"
url = "https://repo.maven.apache.org/maven2/"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[rubygems.upstreams]]
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn/rubygems/"
priority = 1
[[rubygems.upstreams]]
name = "rubygems"
url = "https://rubygems.org"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[composer.upstreams]]
name = "aliyun"
url = "https://mirrors.aliyun.com/composer/"
priority = 1
[[composer.upstreams]]
name = "packagist"
url = "https://repo.packagist.org"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[nuget.upstreams]]
name = "nuget"
url = "https://api.nuget.org"
priority = 1
# proxy = "http://127.0.0.1:7890"
[[conda.upstreams]]
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn/anaconda"
priority = 1
[[conda.upstreams]]
name = "official"
url = "https://repo.anaconda.com"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[cran.upstreams]]
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn/CRAN"
priority = 1
[[cran.upstreams]]
name = "cloud-r"
url = "https://cloud.r-project.org"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[alpine.upstreams]]
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn/alpine"
priority = 1
[[alpine.upstreams]]
name = "official"
url = "https://dl-cdn.alpinelinux.org/alpine"
priority = 2
# proxy = "http://127.0.0.1:7890"
[[helm.upstreams]]
name = "azure-cn"
url = "https://mirror.azure.cn/kubernetes/charts"
priority = 1
[[helm.upstreams]]
name = "bitnami"
url = "https://charts.bitnami.com/bitnami"
priority = 2
# proxy = "http://127.0.0.1:7890"
# Optional directory source; see docs/ldap.md. Use actual endpoint, DNs and CA.
# [auth.sources.directory]
# protocol = "ldap"
# enabled = false
# display_name = "Company directory"
# admin_groups = ["cn=depsilo-admins,ou=groups,dc=example,dc=com"]
# readonly_groups = ["cn=depsilo-readers,ou=groups,dc=example,dc=com"]
# session_ttl = "30m"
# [auth.sources.directory.ldap]
# url = "ldaps://directory.example:636" # ldap:// always requires StartTLS
# bind_dn = "cn=depsilo-reader,ou=services,dc=example,dc=com"
# Bind password: DEPSILO_AUTH_SOURCES_DIRECTORY_LDAP_BIND_PASSWORD
# user_base_dn = "ou=people,dc=example,dc=com"
# user_filter = "(uid={username})"
# subject_attribute = "entryUUID" # or objectGUID for Active Directory
# display_name_attribute = "displayName"
# groups_attribute = "memberOf"
# ca_cert_pem = "" # optional public CA certificates; no private keys
# ─── Hugging Face Hub ─────────────────────────────────────
[[huggingface.upstreams]]
name = "hf-mirror"
url = "https://hf-mirror.com"
priority = 1
[[huggingface.upstreams]]
name = "official"
url = "https://huggingface.co"
priority = 2
# proxy = "http://127.0.0.1:7890" # optional per-upstream HTTP proxy
# A configured proxy resolves redirected artifact hostnames. Its egress policy
# should block loopback/link-local/private targets unless intentionally needed.
[docker]
default_registry = "docker.io"
[[docker.registries]]
name = "dockerhub"
url = "https://registry-1.docker.io"
# username = ""
# password = ""
# proxy = "http://127.0.0.1:7890"
# Every registry, Bearer realm, and redirect hostname is resolved locally and
# checked against the registry's pinned network scope before a proxied request.
# A forward proxy can resolve the same hostname differently, so its DNS policy
# and egress ACL remain a trust boundary and must reject unintended internal
# targets. Registries may nominate a public cross-origin Bearer realm (Docker
# Hub uses auth.docker.io); configuring credentials trusts that delegation.
# [[docker.registries]]
# name = "ghcr"
# url = "https://ghcr.io"
# username = "github-user"
# password = "ghp_xxx"
# ─── Extra PyPI-compatible indexes ───────────
# The built-in PyTorch channel family is enabled by default. It registers
# /pypi-torch/{channel}/simple/ without contacting the upstream until first use.
# Use the channel suffix from PyTorch's official install URL (for example cpu,
# cu..., or rocm...). Artifact URLs declared by each channel are signed by
# Depsilo and fetched through an isolated cache namespace.
[extra_index_presets]
enabled = true
# disabled = ["pytorch"]
# Proxy additional PyPI-compatible indexes at custom paths. Artifact links
# declared by every extra index are signed and cached automatically, so CDN
# host lists are not required. `simple_path` controls the package-list path
# below each upstream: normal PyPI mirrors use "/simple"; flat indexes such as
# download.pytorch.org use "/".
# `name` must be a unique URL-safe slug of at most 128 bytes, without surrounding
# whitespace. `path` may contain multiple literal segments (for example
# "python/private"), but must be unique and must not use Depsilo's occupied
# roots such as api, admin, pypi, assets, p, or v2.
#
# [[extra_indexes]]
# name = "company-python"
# path = "python/company"
# [[extra_indexes.upstreams]]
# name = "company"
# url = "https://packages.example.com/python"
# priority = 1
# probe_mode = "passive"
# [[extra_indexes]]
# name = "vllm-cu130"
# path = "pypi-vllm-cu130"
# [[extra_indexes.upstreams]]
# name = "vllm-wheels"
# url = "https://wheels.vllm.ai/0.19.1/cu130"
# priority = 1
#
# Usage:
# PYTORCH_CHANNEL=cpu
# pip install torch --index-url http://HOST:PORT/pypi-torch/${PYTORCH_CHANNEL}/simple/
[security]
enabled = true
osv_api_url = "https://api.osv.dev"
scan_interval = "24h"
check_ttl = "24h"
# proxy = "http://127.0.0.1:7890"
# ─── Minimum release age — npm, PyPI, Composer, NuGet, Cargo, RubyGems ──
#
# npm enforces the gate from the packument's time[version], carried inside the
# same authenticated token that pins the tarball to its serving Upstream.
# PyPI enforces it from the PEP 691 JSON simple index's upload-time for the
# artifact URL declared by the same Upstream; mirrors that only serve HTML
# (for example some Aliyun mirrors) fail closed while the PyPI gate is enabled.
# Composer enforces it from the p2 metadata entry's own time field. Composer
# clients fall back to the original dist URL after a 451, so the Composer gate
# provides caching, audit events, and best-effort blocking rather than hard
# enforcement without egress control.
# NuGet enforces it from the v3 registration index's published timestamp for
# the same immutable package version; the 1900 unlisted sentinel is treated as
# missing provenance and fails closed.
# Cargo enforces it from the sparse index entry's pubtime for the exact version
# and checksum; entries without pubtime fail closed.
# RubyGems resolves the compact-index created_at and checksum for the exact
# `.gem` artifact; entries without created_at or an ambiguous filename fail
# closed.
# Conda can be enabled with approximate Last-Modified provenance, but only
# after listing it in approximate_sources below. The capability summary labels
# it approximate rather than source-bound.
# CRAN uses the DESCRIPTION Date/Publication for the current source tarball and
# the artifact's Last-Modified for archives and binaries; it requires the same
# approximate acknowledgement.
# Maven and Alpine HEAD the exact artifact through the configured upstream and
# use its Last-Modified; both require the same approximate acknowledgement.
# Helm resolves the chart identity from the same upstream's index.yaml (the
# filename cannot be split safely) and HEADs the exact chart for its
# Last-Modified; it requires the same approximate acknowledgement too.
# Every other ecosystem still rejects positive thresholds at startup because
# its artifact source and timestamp are not yet bound end to end.
#
# Enable the gate and set a positive npm or PyPI threshold to quarantine fresh
# versions; leave the other thresholds at zero. Positive defaults and positive
# thresholds for unbound ecosystems are startup errors. Missing or legacy
# metadata without a publish time fails closed while the gate is enabled.
[supply_chain]
min_release_age_enabled = false
mode = "block"
fail_closed = true
# Ecosystems whose positive thresholds rely on approximate Last-Modified
# provenance must be listed explicitly, for example
# ["conda", "cran", "maven", "alpine", "helm"]. Docker does not provide that
# provenance yet, so listing it here has no effect and its positive threshold
# stays rejected at startup.
approximate_sources = []
# Ecosystems whose positive thresholds rely on first-observation age — the
# clock starts when this instance first pulls the artifact digest, not when
# the publisher released it. Only adapters that record observations are bound;
# listing anything else has no effect. Currently Docker: its registries expose
# no portable publish time, so a positive docker threshold requires this
# acknowledgement, and unseen digests stay quarantined until the threshold
# passes (approve individual images through the quarantine approvals flow).
# observation_sources = ["docker"]
observation_sources = []
[supply_chain.min_release_age]
default = "0"
pypi = "0" # e.g. "3d"; requires a PEP 691 JSON simple-index upstream
npm = "0" # e.g. "7d" once min_release_age_enabled = true
go = "0"
cargo = "0" # e.g. "3d"; uses the sparse index pubtime field
maven = "0" # e.g. "3d"; requires approximate_sources = ["maven"]
rubygems = "0" # e.g. "3d"; uses the compact index created_at field
composer = "0" # best-effort: clients can fall back to the original dist URL
nuget = "0" # e.g. "3d"; uses the v3 registration published timestamp
conda = "0" # e.g. "3d"; requires approximate_sources = ["conda"]
cran = "0" # e.g. "3d"; requires approximate_sources = ["cran"]
helm = "0" # e.g. "3d"; resolves identity from index.yaml; requires approximate_sources = ["helm"]
alpine = "0" # e.g. "3d"; requires approximate_sources = ["alpine"]
docker = "0" # e.g. "3d"; requires observation_sources = ["docker"]
huggingface = "0"
apt = "0"
# ── Known-malicious blocklist ─────────────────────────────────────────
# Syncs OSV MAL-* records for npm, pypi, cargo, rubygems, composer, nuget,
# go, and maven. Explicit version lists and all-version advisories are
# imported; bounded ranges without a version list are skipped.
# PyPI resolves legacy archive filenames (.zip, .egg, .tar.bz2, …) through
# the declaring package's simple index, and RubyGems resolves the exact gem
# (including platform artifacts) through the compact index. An artifact whose
# identity cannot be proven is refused while the dataset covers it, so a
# non-standard filename cannot bypass the blocklist. Upstreams for those two
# ecosystems must serve the corresponding index metadata.
# Records may carry GHSA aliases.
# The quarantine allow-list CANNOT bypass a malware match — the only
# exemption is an audited admin override that expires after 24h.
# mode controls the response to a known-malicious match:
# block (default) — refuse the request with 451 MALICIOUS_BLOCKED
# warn — serve the request and record malware_warned
# Enabled by default; sync failures degrade (last good data keeps
# blocking, an empty local store blocks nothing) and never break the
# proxy itself.
[supply_chain.blocklist]
enabled = true
sync_interval = "6h"
# mode = "block"
# mirror_url = "https://osv-vulnerabilities.storage.googleapis.com"
# proxy = "http://127.0.0.1:7890" # required in mainland China
# ── Compliance (CRA-mode SBOM technical file) ─────────────────────────
# The per-project SBOM export (Pro) always emits purl and version. Adding
# ?cra=true, or the "CRA technical file" option in the Admin UI, adds the
# NTIA/CRA minimum elements: SHA-256 checksums from tamper detection,
# supplier/license fields, and explicit dependency relationships from the
# product to each component. Depsilo never invents supplier or license
# values: declare them below, or the component exports as unknown
# (NOASSERTION) and the technical-file manifest reports the gap.
[compliance]
# Manufacturer/supplier of the product described by the technical file.
organization = ""
# Security contact published in the technical file.
contact = ""
# Optional Ed25519 PKCS#8 PEM key that signs CRA exports (?sign=true).
# openssl genpkey -algorithm ed25519 -out /etc/depsilo/sbom-signing-key.pem
# signing_key_file = "/etc/depsilo/sbom-signing-key.pem"
# Per-package facts. The ID is "<ecosystem>:<package>" for every version or
# "<ecosystem>:<package>@<version>" for one version; the version-specific
# entry wins per field when both exist.
# [[compliance.components]]
# id = "pypi:requests"
# supplier = "Python Software Foundation"
# license = "Apache-2.0"
# ── Audit chain anchoring ─────────────────────────────────────────────
# The audit log is hash-chained (schema v8). Anchoring writes periodic
# checkpoints of the chain head to an append-only file, so rewriting the whole
# database becomes detectable: `depsilo audit verify` and the audit page
# compare the stored chain against these checkpoints. Point the file at a
# different volume, bucket, or backup target than the SQLite database — a
# checkpoint saved next to the database adds little.
[audit]
checkpoint_file = ""
checkpoint_interval = "15m"
# Optional remote anchor: the same NDJSON checkpoint line is POSTed to this
# endpoint (WORM gateway, log platform, or a service that timestamps on
# receipt). Receivers should deduplicate on (head_id, head_hash); a failed
# delivery retries after 60s. The URL and token are never returned by the API.
# checkpoint_url = "https://worm.example/audit-anchors"
# checkpoint_token = ""
# ── Tamper detection (content integrity of immutable artifacts) ──
# Records the SHA-256 of each immutable artifact (wheel/.crate/.gem/…)
# on first fetch. When the artifact is naturally re-fetched (background
# refresh) and the upstream bytes differ, depsilo keeps the trusted
# first-seen copy, refuses to cache the new content, and fires a
# critical alert — a registry silently swapping bytes under the same
# version is a supply-chain compromise signal. Alert-only; never blocks.
[supply_chain.tamper_detection]
enabled = true