chadtmiller.com/knotless
git that lives in your Atmosphere account
A repository owner can't give the hosted runner a secret
ClosedA design
Where the sealed values live: the appview. The issue's space, with only the owner and the runners as members, would be ideal, but spaces need a spaces-alpha PDS (most owners are on bsky.social), and a public repository has none. Spaces also control access without encrypting, so a value there must be sealed anyway. Public sealed records would work on any PDS but stay on the firehose forever: a runner key that leaks later opens every value ever set. So the appview holds sealed values for now, as GitHub holds Actions secrets sealed to a repository key. It can delete them, and it can never read one. It could swap one in, since HPKE's base mode does not authenticate the sender; that is the cost. A private repository's could move to its space later.
- The runner's key. knotless-run makes a P-256 key on its first start, kept beside its state (
--secrets-key <file>, 0600), and publishes the public half as adev.knotless.runnerKeyrecord in its own account, with a key id. A rotation keeps old private keys until nothing is sealed to them. - Sealing. HPKE base mode, RFC 9180: DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, AES-128-GCM. The AAD binds the owner's DID, the repository's key (its TID, so a rename keeps its secrets and a new repository under the old name gets none), the runner's DID, the name and its refs. The runner rebuilds the AAD from where it found the value, so one copied to another repository, name or runner does not open.
- The appview.
dev.knotless.putSecret,deleteSecretandlistSecrets, with service tokens asinviteRunnerhas. It checks the caller owns the repository and the repository names the runner. The owner lists names, refs, runner and when, never a value; a runner lists its own sealed values for a repository. - CLI and MCP.
knotless secret <url> set NAME [--ref <pattern>]...reads the value from stdin or a hidden prompt and seals a copy to each runner the repository names;lsandrm; an MCPsecrettool to match. - The runner takes its
--secretsfile first, then asks the appview for the names still missing; the file wins a clash. Refs, no secrets for pulls, batches or agents, and masking all stay. An appview it cannot reach makes the checkerror, as an unreadable file does. - Runners.
runners removedeletes that runner's copies;runners addnames the secrets to set again, which nothing can re-seal without the values. - The log. A run says
==> NPM_TOKEN is not granted to <repo> at <ref>for each secret its workflow lists and does not get.
Pulls, in order: the log line; the runner's key; the appview's methods; the CLI, MCP and runner, with the docs.
What happened
I added a
releaseworkflow toknotless://chadtmiller.com/hatkthat publishes to npm. It listssecrets: [NPM_TOKEN], and the repository names the hosted runner (did:plc:4bocpkn5zkwe4wuu4plrj5kt, runner.knotless.dev). The only way for the run to getNPM_TOKENis to SSH into the runner host and edit/etc/knotless/run-secrets.jsonunderdid:plc:…/hatk. That works only because I operate the runner.Anyone else using the hosted runner can't do it. Their workflows that need a secret (publish a package, deploy, push to a mirror) never get one, and nothing tells them why: a secret not granted is simply absent (docs/reference/workflows.md, "Secrets file"). Today their options are to run their own runner (the self-hosted runner issue, 3myioqgnrsh2b) or to hand their token to whoever runs the hosted runner, so the operator holds everyone's credentials.
No CLI command, MCP tool or record lets an owner set a secret for a runner they don't run.
One way to do it
knotless secret set <url> <NAME> [--ref <ref>]...reads the value from stdin or a hidden prompt, never from argv.secret ls/secret rmwork too, and an MCP tool to match.#knotless_secretskey in its DID document, or a record), and the CLI encrypts to every runner the repository names. The owner's PDS, the appview and readers hold only ciphertext.knotless runners add.--secretsfile at each run, and the rules stay the same:refslimits, no secrets for pulls, merge-queue batches or agents, and masking in the log.knotless runners removemakes the owner re-seal, or drops the runner's copies. Rotating is justsetagain.==> NPM_TOKEN is not granted to <repo> at <ref>), not leave it silently absent.