Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

Docs: live updates without payloads, push protection on messages and Diffinery tokens (#75)

Covers #62 beyond its own pages: the push protection scan in the API table and security overview, fixing a finding in a commit or tag message, job, sandbox and model tokens, and what a live update carries and how long its connection lasts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5wwBFck8JHq14gHUM2CL
Canberk Demirci authored · GitHub committed signed1 parent fcbd993commit b5a488e4b52c94b1488fdb84af75a0ec1e402e5aBrowse files
5 files changed+13−4
Files (5)
modifiedapps/diffinery/docs/accounts/tokens.md+2−0View file
@@ -237,6 +237,8 @@
237237 - **Job tokens** (`djt_`) are given to each workflow job as `GITHUB_TOKEN` and `DIFFINERY_TOKEN`. They act as `diffinery-actions[bot]` on the repository the job runs in, with what the workflow's `permissions:` grant, as on GitHub: each API endpoint needs its permission (`contents`, `issues`, `pull-requests`, `statuses`, `checks` and so on), and a request without it gets `403 Resource not accessible by integration`. Without `permissions:` a job gets the repository's default, which is read-only (`contents` and `packages` read) for new repositories; an admin changes it with the repository's workflow permissions setting (`PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`). A job token's role never goes above write, so branch protection applies to it, and it never changes repository settings, secrets, variables, webhooks, runners or branch protection, or creates or changes workflow files. A run whose code came from an agent acts as an agent, so it can't push or merge to the default or protected branches. The token also reads other public repositories (not other private ones), uses the container registry according to `permissions: packages`, and stops working when the job finishes. See [Workflow permissions](/docs/actions/workflow-permissions) and [Secrets and variables](/docs/actions/secrets-and-variables).
238238 - **Sandbox tokens** (`dst_`) are given to each agent sandbox. They act as whoever started the sandbox, capped at write, on that repository, can also read other public repositories, and stop working when the sandbox closes. See [Sandboxes](/docs/agents/sandboxes).
239239
240+[Push protection](/docs/security/push-protection) blocks a push or commit that contains a job, sandbox or [agent task](/docs/agents/tasks) model token (`dmt_`), as it does for `dfy_` tokens, so one printed into a file or a commit message by a job or an agent isn't stored in the repository.
241+
240242 ## Security emails about tokens
241243
242244 | Email | Sent to | When |
modifiedapps/diffinery/docs/reference/limits.md+1−0View file
@@ -311,6 +311,7 @@
311311 | Limit | Value | Applies to |
312312 | --- | --- | --- |
313313 | Worker CPU time | 300,000 ms per request | `limits.cpu_ms`. |
314+| Live update connection | 10 minutes, up to 10 topics | A page's WebSocket is closed with code `4001` after 10 minutes and reconnects at once, so access is checked again. |
314315 | Runner containers | `standard-2`, up to 100 instances | `containers[RunnerContainer]`. |
315316 | Sandbox containers | `standard-1`, up to 50 instances | `containers[SandboxContainer]`. |
316317 | Import containers | `standard-4`, up to 20 instances | `containers[ImportContainer]`: one container per Git transfer of an import. |
modifiedapps/diffinery/docs/repositories/browsing-code.md+2−0View file
@@ -310,6 +310,8 @@
310310
311311 Pages about activity update live over a WebSocket while they are open: the Actions list, workflow runs, job logs (streamed line by line), issues, pull requests and the merge queue. See [Actions](/docs/actions/overview) and [Pull requests](/docs/collaboration/pull-requests).
312312
313+A live update only says that something changed; the page then fetches what changed with your own access, so an update never carries an event's payload or an audit entry. Access is checked when the page connects, and each connection lasts 10 minutes before the page quietly reconnects and is checked again. Someone who loses access to the repository, or a signed-out reader of a repository that was made private, stops getting updates within 10 minutes.
314+
313315 ## The same data through the API
314316
315317 | Data | REST API (`https://diffinery.com/api/v3`) |
modifiedapps/diffinery/docs/security/overview.md+2−2View file
@@ -177,9 +177,9 @@
177177
178178 ## Push protection
179179
180-Every `git push` is scanned for credentials (AWS, GitHub, Diffinery, Slack, Stripe live, Google, Anthropic and OpenAI keys, and private keys) before anything is stored. A push that contains one is rejected as a whole and nothing from it is kept. A file can opt out with a `diffinery:allow-secret` marker, and a repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, which is audited and emailed to the repository's admins.
180+Every `git push` is scanned for credentials (AWS, GitHub, Diffinery, Slack, Stripe live, Google, Anthropic and OpenAI keys, Diffinery's own job, sandbox and model tokens, and private keys) before anything is stored, in file contents (UTF-8 and UTF-16 text) and in commit and tag messages. A push that contains one is rejected as a whole and nothing from it is kept. A file can opt out with a `diffinery:allow-secret` marker (a credential let through that way is still recorded in the audit log), and a repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, which is audited and emailed to the repository's admins.
181181
182-The same scan runs on every commit made without a Git client: the web editor, `POST /repos/{owner}/{repo}/diffinery/commits`, the contents API (`PUT /repos/{owner}/{repo}/contents/{path}`) and the MCP `commit_files` tool, and on the git data API (blobs, and trees with inline `content`). A match is refused with `422` and recorded as `api.blocked_secret` against the caller. Only the commits API has a bypass: a repository admin who is a person sends a reason in `allow_secrets`, which is recorded as `api.secret_bypassed` and emailed to the repository's admins. See [Push protection](/docs/security/push-protection).
182+The same scan runs on every commit made without a Git client: the web editor, `POST /repos/{owner}/{repo}/diffinery/commits`, the contents API (`PUT /repos/{owner}/{repo}/contents/{path}`) and the MCP `commit_files` tool, and on the git data API (blobs, trees with inline `content`, and commit and tag messages). A match is refused with `422` and recorded as `api.blocked_secret` against the caller. Only the commits API has a bypass: a repository admin who is a person sends a reason in `allow_secrets`, which is recorded as `api.secret_bypassed` and emailed to the repository's admins. See [Push protection](/docs/security/push-protection).
183183
184184 ## Tree entry names
185185
modifiedapps/diffinery/docs/security/push-protection.md+6−2View file
@@ -101,7 +101,7 @@
101101 git rev-list --objects --all | grep '^3f2a9c1d0e'
102102 ```
103103
104- The line it prints ends with the path of the file.
104+ The line it prints ends with the path of the file. If it prints no path, the finding is in a commit or tag message: `git cat-file -t 3f2a9c1d0e` says `commit` or `tag`, and `git log -1 3f2a9c1d0e` shows the message.
105105
106106 2. Remove the secret from every commit you are pushing, not only the latest one. For the most recent commit:
107107
@@ -111,7 +111,7 @@
111111 git commit --amend --no-edit
112112 ```
113113
114- If the secret was added in an earlier commit, rewrite those commits with an interactive rebase (`git rebase -i origin/main`) and edit each one that contains it.
114+ If the secret was added in an earlier commit, rewrite those commits with an interactive rebase (`git rebase -i origin/main`) and edit each one that contains it. For a secret in a commit message, reword the commit instead (`git commit --amend` for the latest, `reword` in the rebase for older ones); for an annotated tag, delete and recreate the tag.
115115
116116 3. If the value was a real credential, rotate it at its provider. Treat it as exposed: it was on your machine in a commit and may have been pushed elsewhere.
117117
@@ -129,7 +129,10 @@
129129 | `PUT /repos/{owner}/{repo}/contents/{path}` | The new file content |
130130 | `POST /repos/{owner}/{repo}/git/blobs` | The blob |
131131 | `POST /repos/{owner}/{repo}/git/trees` | Each tree entry given with inline `content`; entries given by `sha` refer to blobs already stored and are not scanned again |
132+| `POST /repos/{owner}/{repo}/git/commits` and `POST /repos/{owner}/{repo}/git/tags` | The commit or tag `message` |
132133
134+The web editor, the commits API, `commit_files` and the contents API (`PUT` and `DELETE`) scan the commit message too. A match in it is reported as `(commit message)` where a file path would be.
135+
133136 The patterns, the binary-file rule and the `diffinery:allow-secret` opt-out are the same as for pushes. Every text file is scanned, whatever its size.
134137
135138 ### When a commit is refused
@@ -271,6 +274,7 @@
271274 | `Only repository admins (people, not agents) can bypass push protection.` | You used `-o allow-secrets=...` without being allowed. Check your role, that you are not using an agent's token, and that your token has the `admin` scope. |
272275 | `-o allow-secrets=` seems to be ignored | The reason is empty. Give a non-empty reason, for example `-o allow-secrets="test fixture"`. |
273276 | A real key was not caught | It is not one of the formats above, it is in a binary file, or the file contains `diffinery:allow-secret`. Rotate the key and remove it. |
277+| `push protection: possible secret detected (...) in (commit message)` (422) | The commit message, not a file, holds what looks like a credential. Change the message and commit again. |
274278 | `push protection: possible secret detected (...)` (422) from the API, MCP or the web editor | A file you committed or wrote matched. Remove the value, or mark a test fixture with `diffinery:allow-secret`. A repository admin who is a person can commit it anyway with `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits`; the other endpoints have no bypass. |
275279 | `... Only repository admins (people, not agents) can bypass push protection.` (422) from the commits API | You sent `allow_secrets` without being allowed. Check your role, that you are not using an agent's token, and that your token has the `admin` scope. |
276280 | An import from GitHub failed with `Push protection found possible secrets in the history (...)` | The source's history contains credentials and the import couldn't bypass. See [Imported history](#imported-history). |