feat(secrets): add scoped repository secret environments (#2237)

This commit is contained in:
Carlos Florêncio
2026-08-05 23:05:54 +01:00
committed by GitHub
parent da66f88570
commit 80e4d77bb1
119 changed files with 6506 additions and 608 deletions
+257
View File
@@ -0,0 +1,257 @@
---
status: shipped
created: 2026-08-03
owner: tbd
---
# Repository and Workspace Secrets
## Why
Repository setup and agent work often need credentials that are specific to a project: package
registry tokens, deployment keys, test service credentials, or API keys. Today users must place
those values on a shared agent or executor profile, even when only one workspace or repository
needs them. This broadens access and becomes ambiguous when a task attaches several repositories.
Kandev needs a repository-level authority grant with workspace-scoped storage and a deterministic,
fail-closed way to build one task environment.
## What
- Secrets can be created as **Global** or **Workspace**.
- Global secrets are available across the current user's workspaces. Workspace secrets belong to
exactly one workspace.
- Existing secrets become Global during migration.
- Agent and executor profiles can select Global secrets only.
- A workspace-owned repository can bind environment variable names to Global secrets or secrets
from that same workspace.
- Every task attaching the repository inherits its bindings. A multi-repository task inherits all
attached repositories' bindings plus its selected executor profile environment.
- Repository bindings contain secret references only, never literal values.
- The effective repository environment is available to repository setup, the agent process, child
shells, and terminal-panel terminals in every supported executor, including explicitly approved
forwarding to SSH.
## Scope and permissions
Global means user-global when authentication is enabled and install-global when authentication is
disabled. It does not make one authenticated user's secret visible to another user.
A caller may:
- manage their own Global secrets;
- manage Workspace secrets only after Kandev authorizes that workspace;
- bind a repository only to a visible Global secret or a secret whose `workspace_id` equals the
repository's `workspace_id`;
- view and edit bindings only through a repository they are authorized to access.
Cross-user and cross-workspace IDs return not found or a generic invalid-binding response without
revealing that the target secret exists. Administrators do not bypass workspace privacy.
All instance-shared profile types, including agent and executor profiles, reject Workspace secret
references both when saving and when resolving legacy or manually inserted data.
## Data model
### Secret
User-visible secret metadata adds:
```text
scope: "global" | "workspace"
workspace_id: string | null
```
Rules:
- `global` requires no workspace ID.
- `workspace` requires an existing, authorized workspace ID.
- scope and workspace are immutable after creation; users create a replacement to move authority.
- existing user-visible rows migrate to `global`.
- deleting a workspace deletes its Workspace secrets.
- secret values remain encrypted with the existing AES-256-GCM store and never appear in list,
repository, task, event, or error payloads.
### Repository secret binding
```text
repository_id: string
key: POSIX environment identifier
secret_id: string
created_at: timestamp
updated_at: timestamp
```
The pair `(repository_id, key)` is unique. The repository foreign key cascades on repository or
workspace deletion. The secret reference deliberately permits a dangling ID so deleting a secret
leaves a broken binding that blocks future launch.
Keys follow profile environment rules: at most 100 bindings per repository, maximum key length 256,
POSIX identifier syntax, no duplicates, and no `TASK_DESCRIPTION` or `KANDEV_*` keys.
## API surface
The existing HTTP and WebSocket secret CRUD contracts become scope-aware.
- Creating a secret accepts `scope` and, for Workspace scope, `workspace_id`. Omitting `scope`
remains backward-compatible and creates Global.
- Secret metadata responses include `scope` and `workspace_id`.
- The default list returns Global secrets. A Workspace-filtered list returns that workspace's
secrets, with an explicit option to include visible Global secrets for a repository selector.
- Update changes name and/or value only; scope is immutable.
- Reveal and delete preserve existing response shapes while applying scope authorization.
Repository create, get, list, update, and repository events include:
```json
{
"secret_bindings": [
{ "key": "NPM_TOKEN", "secret_id": "..." }
]
}
```
Create/update treats the supplied list as the complete desired set and persists repository fields
and bindings atomically. Omitting the field on update preserves the existing set; an explicit empty
list clears it. Automated find-or-create/backfill paths that do not submit bindings preserve them.
## Environment resolution
Kandev resolves one task environment from named origins:
1. Validate all repository keys and collect the selected executor profile entries plus every
attached repository binding without decrypting repository values.
2. Reject collisions with Kandev/runtime-owned values.
3. For a repeated key:
- deduplicate exact references to the same secret ID;
- reject different secret IDs;
- reject literal-versus-secret or different-literal definitions.
4. Reveal each unique repository secret using Global-or-same-workspace authorization.
5. Reject any missing, deleted, unreadable, unauthorized, or wrong-workspace reference.
6. Flatten the validated result and continue normal launch. Agent-profile values retain their
existing fill-missing behavior and may not overwrite this task environment.
Repository/task order never selects a winner. Failure occurs before executor provisioning,
worktree/container/sandbox creation, repository setup, or agent start.
User-facing conflict errors contain the environment key and every relevant repository/executor
origin. Broken-binding errors contain the key and repository origin. Neither contains plaintext or
secret IDs. Routine logs follow the same redaction rule.
## Runtime lifecycle
Bindings are evaluated when a task environment is provisioned or freshly recreated. The flattened
map is the execution's environment snapshot.
- Repository setup scripts see it through the existing launch environment.
- Agents and their child shell commands inherit it.
- A newly opened terminal for that execution receives the same snapshot.
- Local, Worktree, Docker, Remote Docker, Sprites, and standalone transports receive the resolved
map through their normal launch path.
- SSH forwards the explicit repository-approved keys to remote agent and terminal instances in
addition to its managed credential allowlist; it does not forward arbitrary host variables.
Editing a binding or rotating a secret does not mutate an already-running process or terminal. Warm
resume retains the provisioned environment. Reset Environment, cold recreation, or a fresh task
environment resolves the current configuration. A terminal already open keeps its startup
environment; a new terminal uses the current execution snapshot.
## Settings experience
### Global secrets
The existing **Settings > General > Secrets** page becomes explicitly Global. It lists and creates
Global secrets only. Existing create, edit, reveal, and delete interactions remain available.
### Workspace secrets
Each workspace gains **Settings > Workspaces > [workspace] > Secrets**. It uses the same management
surface but creates and lists only secrets for that workspace. The page explains that these secrets
can be selected by repositories in this workspace and cannot be used by shared profiles.
### Repository bindings
The repository editor gains an **Environment secrets** section. Each row has an environment key and
a secret selector containing Global plus same-workspace secrets, with scope labels. Users can add,
remove, and replace rows as part of the repository's existing manual-save flow. A deleted secret is
shown as a missing reference rather than silently dropping the row.
The selector never reveals values. Executor and agent profile selectors show Global secrets only.
## Mobile design contract
- Entry point: the existing mobile Settings navigation exposes Workspace Secrets alongside
Repositories, Workflows, Integrations, and Automations.
- Hierarchy: secret list and create/edit forms use the existing settings page/card composition;
repository bindings stay within the repository editor rather than opening a desktop-only side
panel.
- Interaction: rows stack to one column at phone width, selectors and key inputs use touch-sized
controls, destructive actions retain confirmation, and save/cancel remain reachable without
horizontal scrolling.
- Scroll ownership: the Settings content area remains the single page scroll owner. Repository
editing must not introduce nested vertical scrolling or a clipped fixed footer.
- Parity: mobile can create, edit, reveal, and delete Workspace secrets and add/remove repository
bindings, matching desktop capability.
- Accessibility: scope is conveyed by text/badge as well as color; icon-only actions have localized
accessible labels; missing references and validation errors are announced in text.
## Failure modes
- Invalid scope/workspace combinations are rejected with 400.
- A foreign or nonexistent workspace/secret is not disclosed.
- A shared profile containing a Workspace secret cannot be saved and cannot resolve it at runtime.
- Duplicate or reserved repository environment keys are rejected on repository save.
- A deleted or unreadable bound secret blocks launch and names the affected key/repository without
exposing secret material.
- Conflicting executor/repository or repository/repository bindings block launch and list all
origins; no partial environment is provisioned.
- Database mutation of repository fields and bindings is atomic.
- Secret decryption failures never fall back to a literal, stale value, or another same-named
secret.
- SSH forwarding includes only managed credential keys and the launch's explicitly approved
repository keys.
## Persistence guarantees
- Existing secrets retain IDs, ciphertext, names, owners, and timestamps and are marked Global.
- Repository bindings survive restart and are inherited by new sessions and tasks.
- Secret plaintext is never persisted in repository, task, session, event, or task-environment rows.
- The effective decrypted map is process memory only and is cleared with the execution store.
- Repository deletion removes its bindings. Secret deletion preserves broken binding IDs. Workspace
deletion removes workspace repositories, their bindings, and Workspace secrets.
- Schema changes and migrations are replayable on SQLite and PostgreSQL.
## Scenarios
- **GIVEN** a Global secret and a Workspace secret in workspace A, **WHEN** a repository in A is
edited, **THEN** both are selectable, while a repository in workspace B sees only the Global
secret and B's own Workspace secrets.
- **GIVEN** an executor profile editor, **WHEN** secret options load, **THEN** only Global secrets are
available, and a direct request naming a Workspace secret is rejected.
- **GIVEN** two attached repositories bind `NPM_TOKEN` to the same secret ID, **WHEN** a task launches,
**THEN** the binding is deduplicated and the task provisions once with that value.
- **GIVEN** two repositories bind `NPM_TOKEN` to different secret IDs, **WHEN** launch is requested,
**THEN** launch fails before provisioning and names both repository origins without exposing IDs
or values.
- **GIVEN** an executor literal and repository secret both define `NPM_TOKEN`, **WHEN** launch is
requested, **THEN** launch fails instead of comparing or choosing plaintext.
- **GIVEN** a repository binding whose secret was deleted, **WHEN** a task using that repository
launches, **THEN** launch fails with the repository and key while the stored binding remains
visible as missing.
- **GIVEN** a successful local, Docker, Sprites, or SSH launch, **WHEN** repository setup, the agent,
an agent child shell, and a new terminal read the bound key, **THEN** each receives the same
provisioned value for that execution.
- **GIVEN** a running execution, **WHEN** a secret is rotated, **THEN** the existing process and open
terminals keep their snapshot and a freshly recreated environment receives the new value.
- **GIVEN** a phone viewport, **WHEN** a user manages a Workspace secret and binds it to a repository,
**THEN** the complete flow is usable without horizontal overflow or a desktop-only control.
## Out of scope for v1
- Per-task secret bindings or overrides.
- Workspace secrets on agent or executor profiles.
- Per-branch bindings when a task attaches multiple branches of one repository.
- Automatic environment-key namespacing.
- Order-based precedence or “primary repository wins.”
- Live mutation of running process or terminal environments.
- Secret-value audit logs, version history, or automatic rotation.