The current documented release is 2.0.1. Security fixes are provided only for the current minor line.
| Version | Supported |
|---|---|
| 2.0.x | ✅ |
| 1.8.x | ❌ |
| <=1.7.x | ❌ |
Didi is local development tooling, not a remote or hostile-host isolation boundary. Phase 6 requires an explicit Godot project, includes a stable project key in each process-unique endpoint, and uses an OS-backed per-session lock to permit one MCP client at a time. Requests remain authenticated with a private 64-hex session token. POSIX defaults are owner-only; Windows grants the owning SID and local administrators and fails startup before pipe creation if that DACL cannot be constructed.
A connection is admitted before it is authenticated, so what an unauthenticated peer can cost the server is bounded on purpose rather than by the token check. A server listens on four connection slots, and each one reads a four-byte length prefix before the handler reaches SessionHost::authorize. That prefix buys a read rather than a buffer: the payload grows as it arrives, in 64 KiB steps, so a frame a peer claims and never sends costs 64 KiB per slot instead of the 128 MiB a frame is allowed to be. A frame that has started arriving and then stalls holds its slot until the frame deadline, which is one second on Windows and five on POSIX, the same split the idle recycle window has and for the same reason.
Mutation confirmation tokens are 64 lowercase hex characters, expire after 120 seconds, are single-use, and are bound to the exact tool, arguments, canonical project, execution mode, session ID, and route generation. Treat them as short-lived capabilities: do not log, persist, publish, or reuse them. Dry-run previews do not call mutation handlers.
Managed Recovery is opt-in process ownership and saved-file protection, not an OS sandbox. It launches only its owned headless editor against a new project copy. Launch and restart execute project code that may access external files or services with the local account's permissions; even an ordinary authorized read can trigger the single automatic restart. runtime_recovery_status, dry runs, and confirmation previews do not relaunch.
Recovery workspaces, checkpoints, preserved project copies, journals, and editor logs contain project-sensitive material and inherit local filesystem permissions. Checkpoint traversal refuses symlinks, reparse points, hardlinks, and special files and enforces portable path naming, but malicious same-account path-replacement races remain outside the security boundary. Five completed checkpoints are retained within per-copy bounds; accumulated containers, preserved copies, journals, and logs have no overall automatic cap. Operators must inspect, protect, archive, and clean up retained artifacts. Existing containers are for salvage and cannot be reused as a new managed workspace.
The blackboard is shared state between agents, not a trust boundary between them. Anything on a board was written by whatever called the tool, and Didi neither interprets nor executes it: values are stored and returned verbatim. Treat a value read from a board with the same caution as any other tool result, and never write a session token, confirmation token, or credential onto one. Boards are files under .didi/blackboard/ inside the project, so they inherit the project's filesystem permissions and nothing narrower. A task lease records who claimed work; it is an agreement between cooperating agents, not an authentication check, and an agent that supplies another's agent_id is not prevented from doing so.
Never include a real session token or descriptor file in an issue, log excerpt, screenshot, test fixture, or documentation example. The editor console's Copy report is safe to paste: it reports the plugin and engine versions, the project, and the state of each check, and it cannot contain a token because the console never reads one. Its descriptor reader copies the fields it names and the token is not among them, and a test fails the build if any addon script starts naming it. DIDI_SESSION_DIR is a controlled deployment/test override: the operator is responsible for ensuring that override is not shared across OS users and has access controls appropriate to the host.
These run continuously so a defect does not depend on someone thinking to look for it. Findings land in the repository's Security tab.
| Check | What it covers | When it runs |
|---|---|---|
| CodeQL | The C++ that parses JSON-RPC off a pipe, the Python tooling, and the workflows. security-extended query set. |
Every pull request that touches code, and weekly |
| OpenSSF Scorecard | Branch protection, token permissions, pinned dependencies, dangerous workflow patterns. Score is public behind the README badge. | Push to main, weekly, and when branch rules change |
| Dependency review | A dependency arriving with a known vulnerability or a copyleft licence. | Every pull request |
| zizmor and actionlint | Workflow security: injectable ${{ }} interpolation, over-broad tokens, credentials left on disk by checkout. |
Every pull request |
| Sanitizers | ASan and UBSan over the whole native suite: real allocation and lifetime paths. | Every pull request that touches code |
| Fuzzing | libFuzzer against the three decoders that read bytes Didi did not write: didi::ipc::readFramePayload, which is the frame reader both transports use at both ends, the JSON-RPC parser, and base64. Built with ASan and UBSan. |
Short run on every code pull request, longer nightly |
| Secret scanning with push protection | A credential committed by accident, blocked at push time. | Every push |
| Dependabot | Security and version updates for the GitHub Actions and the pinned Python dependency. | Weekly and monthly |
Every action a workflow runs is pinned to a commit SHA rather than a tag, and
tools/validate_documentation.py fails the build on any workflow that is not.
See THIRD_PARTY.md for the vendored sources no scanner can
reach.
CodeQL's first pass over this repository raised ten C++ findings. None was a new defect, and all ten are accounted for here rather than left open, because a Security tab that permanently shows ten alerts is one nobody opens — and the next real finding disappears into them.
| Finding | Disposition |
|---|---|
Five integer-multiplication overflows, all in stb_image_write.h |
Dismissed as won't-fix. Vendored upstream code this project copies verbatim and does not modify; see THIRD_PARTY.md. Callers clamp viewport captures to 4096x4096 with a 128 MB frame limit before reaching it. |
Three uncontrolled process operations (test_runner, process_runner, gdscript_diagnostics) |
Dismissed as by design. Launching an operator-nominated Godot or dotnet binary is what these tools do, and they advertise openWorldHint: true on the wire so a client cannot auto-approve them as closed local calls. |
Path injection in session_client.cpp |
Dismissed as by design. DIDI_SESSION_DIR is the documented operator override described above; the descriptor's shape, session ID, PID and process-start identity are all validated before anything acts on it. |
Path injection in phase7_signal_bridge_probe.cpp |
Dismissed as test-only. A probe reading the descriptor path passed as its own argument, not built into the shipped server. |
The common thread in the dismissals is that they sit on the boundary this document already draws: an attacker who can set this process's environment already controls the process. That is a statement about the boundary, not a claim that the code is unreachable.
All ten are dismissals rather than exclusions, and that is not the original
plan. A CodeQL configuration file was added first, listing the vendored headers
under paths-ignore. It was loaded, it was inert, and the alerts stayed open:
paths and paths-ignore apply to interpreted languages and to compiled
languages analysed without a build. This analysis builds, because CodeQL for
C++ observes the real compiler, and the only supported way to narrow a built
analysis is to change what the build compiles -- which is not available for a
header the code under analysis includes. The configuration file has been
removed rather than left in the tree describing a control that was not in
force.
A dismissal is bound to the code it was made against, not to the finding. When
the lines around an alert move, CodeQL closes that alert and raises the same
finding again under a new number, with the dismissal gone. Both process
operations came back that way on 2026-09-18: the commits that killed the whole
process tree on timeout and stopped handing children the server's standard
input shifted the execvp call, and test_runner and process_runner each
reappeared as a new high-severity alert. Neither commit changed what reaches
execvp -- the executable still resolves from GODOT_BIN, GODOT_PATH or
DOTNET_BIN, from a known install location, or from a literal, and no argument
arriving on the wire can reach that slot -- so both were dismissed again for
the reason above. Re-read the taint path before re-dismissing rather than
matching on the rule name. The point of a re-raise is that the code moved.
Nothing read the tab back, so those two and five untriaged Scorecard findings
sat open until someone happened to look (#807). tools/check_code_scanning.py
now reads it weekly from supply-chain.yml and keeps one issue open while
anything is. A re-raise is listed apart from the rest, with the dismissal it
most likely repeats. It dismisses nothing.
Scorecard reports against the repository rather than against the code, and those findings have dispositions of their own.
| Finding | Disposition |
|---|---|
| Branch-Protection, score 3 | Dismissed. Required approvers, CODEOWNERS review and last-push approval are not controls one maintainer can operate, and stale-review dismissal is moot where there are no reviews to go stale. The main protection ruleset enforces what a single maintainer can: every change arrives as a pull request, main cannot be deleted or force-pushed, and CI Gate, Validate Docs and Lint Workflows must pass before merge. |
| Code-Review, 0/9 approved changesets | Dismissed. GitHub does not let an author approve their own pull request, so this score is structurally zero for as long as there is one maintainer. It measures a control this project cannot operate, not one it has declined to. |
| Maintained, score 0 | Dismissed as transient. The only warning is that the repository was created inside the last 90 days, on 2026-08-26. It stops being reported around 2026-11-24, and nothing in the tree moves it either way. |
| CII-Best-Practices, score 0 | Dismissed. The badge is earned by self-certifying the project on bestpractices.dev, which is a registration rather than a change to anything here. Not undertaken. |
Pinned-Dependencies, tools/localci/Dockerfile |
Dismissed. requirements-dev.txt already pins jsonschema exactly and Dependabot watches it. Hashes would cost more than the gap they close: pip switches to hash-checking mode for every install of a file that carries one, so the Windows, macOS and Linux installs in ci.yml and release.yml would each need the full transitive set for their own wheels -- or the image needs a second copy of the pin this repository deliberately keeps in one place. |
Didi ships prebuilt binaries, so the question "did this archive come from that source" has to be answerable by someone who has only the download. Every release carries signed SLSA build provenance generated by the release workflow through Sigstore. There is no long-lived signing key: the certificate is issued to the workflow run's own OIDC identity and expires in minutes, so there is nothing for a maintainer to leak, rotate, or lose.
Each release contains, alongside the platform archives:
| Asset | What it is for |
|---|---|
SHA256SUMS |
The archives are intact and match what was built. |
didi-<tag>.intoto.jsonl |
The signed provenance bundle, for verifying offline. |
The check worth running. Each constraint is doing work, and dropping any of them weakens the answer:
gh attestation verify didi-linux-x64.tar.gz \
--repo saworbit/didi \
--signer-workflow saworbit/didi/.github/workflows/release.yml \
--source-ref refs/tags/v1.6.0| Constraint | What it rules out |
|---|---|
--repo |
An attestation from some other project entirely. |
--signer-workflow |
Anything in this repository other than the release workflow. An attacker can sign something of their own; they cannot produce a signature attributed to this workflow. |
--source-ref |
A rehearsal artifact. The release workflow can also be run manually, and those runs sign too, so their provenance carries the same repository and the same signer workflow. Only the ref separates a published release from a dry run, so pin it to the tag you are verifying. |
Offline, using the bundle from the release rather than the GitHub API. The constraints are the same: verifying offline is about not calling the API, not about checking less.
gh attestation verify didi-linux-x64.tar.gz \
--bundle didi-v1.6.0.intoto.jsonl \
--repo saworbit/didi \
--signer-workflow saworbit/didi/.github/workflows/release.yml \
--source-ref refs/tags/v1.6.0Checksums, if all you want to know is that the download is undamaged:
sha256sum --check --ignore-missing SHA256SUMSSHA256SUMS is itself covered by the attestation, so it cannot be swapped
independently of the archives it describes. A failed verification means the
file is not what this project published: do not run it, and please
report it.
If you discover a security vulnerability in Didi, please do not open a public issue.
Report it through GitHub Private Vulnerability Reporting, which is enabled on this repository, or contact the project maintainers directly. Include the Didi version, operating system, Godot version, session kind (editor or game), whether the default or an overridden descriptor directory was used, and the smallest safe reproduction. Redact tokens, user-specific paths, project content, and unrelated logs.
You can expect an acknowledgement within 5 working days and an assessment of whether the report is confirmed within 14. A confirmed vulnerability is fixed on the current minor line and disclosed through a GitHub Security Advisory once a fix is available. If a report turns out to fall outside the security boundary described above, that is said plainly with the reasoning, rather than left unanswered.
We appreciate your efforts to responsibly disclose findings and will investigate and patch confirmed issues promptly.