feat(secrets): add scoped repository secret environments (#2237)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user