Skip to content

Security: YesterdaysLemon/valet

Security

docs/security.md

Valet security boundary

Valet 0.2 is an early, single-operator vertical slice. It is not a hosted vault, a sandbox, or a substitute for upstream branch protections and deployment review.

What the worker receives

A worker receives its own enrollment credential and can claim an opaque one-use lease after approval. It never receives the upstream GitHub credential, App key, minted installation token, deployment HMAC secret, or operator credential through the worker API. Identity comes from its enrollment credential, never from an agent-supplied name or intent.

Requests contain one exact operation, not an arbitrary authenticated URL. Server-side records bind file content, branch, path, PR prose, or deployment SHA to the approval. The execute endpoint accepts only the request ID and claimed lease; it cannot substitute operation parameters. All APIs, the CLI, and MCP use this same broker.

Required deployment isolation

Run the broker under an OS identity or on a host outside the worker's control. Broker code/config, provider sessions, environment, SQLite state, and operator-client files must be inaccessible to the worker. Give the worker only its client file and access to the loopback listener through a constrained tunnel or shared network namespace. The CLI/MCP client deliberately rejects non-loopback broker URLs; use a protected tunnel for remote access.

An unrestricted worker running under your everyday OS account can read your files and use your existing authenticated tools. Valet cannot contain that worker if its secrets/provider account share the same identity. A worker with root, sudo, Docker socket access, or broker-file bind mounts can also bypass the boundary. Linux mode bits are not a Windows ACL configuration recipe. Establish and test separate Windows identities/ACLs before claiming containment there.

The public website is a separate static deployment. It has no broker routes, secret bindings, or account connection. Its handoff interaction is explicitly an illustration.

Policy and approval semantics

Operator config sets repository IDs, default branch, permitted task-branch prefix and file paths, credentials, allowed workers, deployments, and lease TTL ceiling. Changing config requires an operator-controlled restart. Disabling a worker and revoking a request or standing grant persist immediately. Config ceilings are rechecked before execution and every upstream dispatch.

Allow once approves and consumes one exact operation. A request's expiry starts when requested, so waiting for approval does not extend authority. Claiming again before execution rotates the lease and invalidates the previous handle. Always this scope authorizes future content/messages within the exact worker/kind/repository/branch/path scope and approved maximum lease TTL for 7 days by default (maximum 30). A longer lease request needs a new approval. It is not an operation budget for all time. The operator must understand that changed content within that remembered scope will be auto-approved. Deployment never permits a standing grant.

File operations reject traversal, encoded paths, workflow paths, symlinks, submodules, and default-branch writes. Reads use a pinned commit/tree/blob. Writes construct one changed tree entry and update the task ref without force, so concurrent unrelated branch changes cannot be overwritten. Existing file updates require the reviewed expected blob SHA. New files require absence. PR creation opens drafts only; no merge or repository-admin tool exists.

Credentials and upstream behavior

The GitHub adapter uses only api.github.com, explicit repository IDs and minimum App permissions, bounded JSON responses, deadlines and redirect rejection. Minted tokens are held per operation and revoked afterward; a failed cleanup is recorded and GitHub's expiry remains a bound. The App path must be exercised with a real operator-configured App before calling that integration live-verified. Environment/file/CLI credentials use a proxy fallback; a broad upstream credential does not become upstream-scoped just because Valet contains it.

Provider references are operator config, never worker input. The subprocess wrappers use execFile, bounded output, timeouts, and sanitized failure errors. The provider binary/session itself is trusted. For production, install it on a broker-only PATH and use a dedicated least-privilege vault session. App tokens/private keys, root credentials, URL-encoded/base64 forms and their raw values are removed from returned adapter data; selected response schemas and fixed error codes provide the primary defense. Redaction cannot generally detect arbitrary transformations of secrets by a malicious upstream. Trust the configured service and its TLS endpoint.

Deployment

Approval binds one configured app and exact SHA. Valet requires that SHA to be the configured branch tip and the latest matching push run of the configured workflow to have completed successfully. It checks again after resolving the signing credential. The HMAC covers the internally constructed event/repository/branch/SHA bytes. No arbitrary signer is exposed. A request returns a Deploy Manager acceptance receipt; valet deployment ID follows the known origin/job ID until a terminal state or timeout.

CI evidence is a check at dispatch time, not an atomic transaction with GitHub and Deploy Manager. An unrelated actor can move the branch after the final check. The deployed artifact is still pinned to the approved SHA. Approving code to run with an application's production secrets remains a high-trust code review; Valet cannot make malicious deployed code safe.

Revocation, crashes, and evidence

SQLite FULL synchronous transactions reserve a one-use execution and write its receipt before side effects. A lease is never silently restored after a timeout. Restart marks interrupted operations unknown, requiring reconciliation rather than automatic replay. A singleton state lock prevents concurrent broker processes sharing one state file. Preserve the database and associated journal files; deletion of authorization state is not a supported reset.

Revocation prevents subsequent dispatch. It cannot recall a network request already sent, undo a Git write, or stop a deployment already accepted. The implementation rechecks authority between upstream steps; an operation revoked halfway through can leave unreferenced Git objects or a completed external change. Its receipt must be reconciled.

Receipts contain safe IDs/scope/decision/result status and policy revision, not headers, credentials, provider output or file bodies. Operation records necessarily contain the approved file content/PR prose and safe results for the operator/owning worker. Protect the entire database. The local audit is not tamper-proof against the operator/host administrator and has no external timestamp authority. No multi-tenant availability, unbounded retention, or independent security certification is claimed.

Verification

npm test exercises the actual HTTP broker, persistent restart, worker/admin separation, exact scope, expiry, revocation, replay races, audit failure, provider error containment, App minting scope, CI gates, signed deployments, and real SDK MCP stdio initialization/tool calls. Upstreams in those tests are explicit fixtures; they do not prove third-party availability.

The CI Linux isolation test runs broker and adversarial worker as different UIDs, attempts reads of broker files/environment, tries administrative and capability escalation, and then performs an approved brokered operation. It establishes that specific process/file boundary in the test container, not safety for an unrestricted host agent or every production configuration. Consult the release verification receipt for live checks actually completed.

There aren't any published security advisories