Skip to content

feat(projects): manage workspace secrets from a Tangent project - #2886

Closed
camielvs wants to merge 5 commits into
masterfrom
cvs/workspace-secrets
Closed

camielvs wants to merge 5 commits into
masterfrom
cvs/workspace-secrets

Conversation

@camielvs

@camielvs camielvs commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Stacked on #2881. Adds a Secrets window to the left dock of the Tangent project page. It manages the secrets of the workspace the project belongs to, so any run in any project of that workspace can use them.

What you can do

  • See the secrets. Each row shows the name, plus either the description or "Added by … · updated …". A line at the top names the workspace: "Shared with every project in ML Research."

  • Add a secret, when the backend's can_create is true. The fields are:

    • name;
    • value, in a password field and never shown again;
    • an optional description;
    • an optional expiry date, sent as the end of the chosen day in UTC.

    The list doesn't show expiry.

  • Replace a value or delete a secret, from a row menu that appears only where can_manage is true:

    • The API has no rename, so Replace keeps the name.
    • Replace pre-fills the description. It sends the description only if it changed, and the expiry only if a date is picked.
    • Delete asks for confirmation first, because runs that use the secret will fail when they start.
  • Failures carry the backend's detail, e.g. "Failed to add secret: Secret 'API_KEY' already exists in this workspace."

Where it goes

  • New visitors see Secrets last in the dock.
  • A saved layout keeps its order, with Secrets added at the end. tangentProjectWindowOrder.test.ts covers that.

Talking to the backend

  • Calls GET/POST /api/workspaces/{workspace_id}/secrets/ and PUT/DELETE …/{secret_name}, following the API contract.
  • These aren't in sdk.gen.ts yet, so workspaceSecretsService.ts calls the hey-api client directly, as projectRunsService.ts does. Regenerating replaces the whole client from whatever backend happens to be running, so that waits until the backend merges.
  • Permissions come only from can_create and can_manage. The client never works them out itself.
  • The value field uses autoComplete="new-password". With a name field above a password field, browsers would otherwise offer to fill in a saved login password.

Reuse

  • The dialog, the expiry picker and the delete action live in src/components/WorkspaceSecrets/, so the editor's secret picker in a later PR can use them.
  • The icon tile of WindowListRow moved into ListRowIcon. A secret row isn't a button (there's nothing to open), but it should look the same.

To confirm once the backend is up

  1. Body vs. query params. The user-secrets API takes secret_name, description and expires_at as query params. This PR sends them in a JSON body, following the brief.
  2. Timestamps. They're read with new Date(). If the backend sends them without a timezone, as /api/secrets/ does, parseAsUtc from secretsStorage.ts should move to src/utils/date.ts and be used here too.
  3. Clearing a field on replace.
    • A description that was emptied is sent as "". Does the backend accept that as a clear?
    • An expiry can't be cleared at all, because "updated only if present" has no way to say so.
  4. Replace needs a value. PUT requires secret_value, so fixing only a description means typing the value again.

Related Issue and Pull requests

Stacked on #2881 (workspace picker). Out of scope here, each a separate PR:

  • choosing between user and workspace secrets in the pipeline editor;
  • clone/rerun handling of workspace_secret arguments;
  • agent tool schemas;
  • the /admin panel in oasis-frontend.

Type of Change

  • New feature

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Screenshots (if applicable)

None yet: the backend endpoints don't exist yet.

Test Instructions

  1. Run oasis-backend with the workspace secrets routes on :8000. In .env, set VITE_BACKEND_API_URL=http://localhost:8000 and VITE_ENABLE_TANGENT=true, then turn on the Projects and Tangent flags in Settings.
  2. Open a Tangent project. Secrets is last in the left dock and names the project's workspace.
  3. Add a secret; it appears in the list. Add another with the same name; the toast gives the backend's reason.
  4. Replace the value of a secret you created; its "updated" time changes.
  5. Delete it: a confirmation appears first, then the row disappears.
  6. As someone who neither created the secret nor is an admin, the row has no menu.
  7. In DevTools → Network, check that no response contains a value.

Unit tests cover the service, the hooks, the dialog, the window and the dock order: pnpm test.

Additional Comments

Test vs production code

About 54% of the net lines are tests: 1021 net lines of tests against 866 of production code.

Files Added Removed Net
Production 15 874 8 866
Test 5 1022 1 1021
Total 20 1896 9 1887
Per file
Production + −
WorkspaceSecretDialog.tsx (new) 249 0
workspaceSecretsService.ts (new) 121 0
SecretsWindowContent.tsx (new) 113 0
SecretRow.tsx (new) 104 0
ExpiryDatePicker.tsx (new) 84 0
useWorkspaceSecrets.ts (new) 84 0
useDeleteWorkspaceSecretAction.tsx (new) 52 0
types.ts 32 0
ListRowIcon.tsx (new) 14 0
useTangentProjectWindows.tsx 7 0
errors.ts 6 0
WindowListRow.tsx 4 8
react-compiler.config.js 2 0
workspaceSecretEvents.ts (new) 1 0
tangentProjectWindowOrder.ts 1 0
Test + −
WorkspaceSecretDialog.test.tsx (new) 316 0
SecretsWindowContent.test.tsx (new) 255 0
useWorkspaceSecrets.test.tsx (new) 215 0
workspaceSecretsService.test.ts (new) 209 0
tangentProjectWindowOrder.test.ts 27 1

🤖 Generated with Claude Code

@camielvs
camielvs added this pull request to stack #2887 October 15, 2026 18:28
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: cvs/workspace-secrets/c5366ed

camielvs and others added 5 commits October 16, 2026 12:13
The New Project dialog and the /projects prompt box each get a workspace
picker. Retired workspaces are left out of it. The preselected workspace
is the one last created in, if it is still active, otherwise the oldest
active one. The API lists workspaces by name, so "first listed" would
move every user's default whenever a workspace that sorts earlier is
added.

Debug in Tangent keeps working in the run's own project, and falls back
to the same default when the run has none.

Project cards now name their workspace.

A refused create now says why (e.g. a workspace retired since the list
was fetched) and refetches the workspace list, and the prompt box says
whether it is waiting on a backend, failed to load workspaces, or has
none to offer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The function and test names carry the intent, and the reasoning is in
the pull request description.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Tangent on, /projects renders the prompt box above the projects
grid, and both said why a project could not be created. The grid now
leaves the "no workspace" and "could not load workspaces" notices to the
prompt box. The section still shows when there is no backend.

The prompt box now creates its project through useCreateProject instead
of a mutation of its own, so the refetch after a failed create and the
error toast live in one place. workspace_changed is captured when the
prompt is sent, so a project created in another tab mid-request no
longer flips it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Tangent project page gets a Secrets window in its left dock, listing the
secrets of the workspace the project belongs to. Anyone the backend allows
(can_create) can add one; the creator or an admin (can_manage) can
replace its value or delete it, after a confirmation. Values are
write-only and never shown. An expiry date can be set when adding or
replacing, and is not shown in the list.

The endpoints are not in the generated client yet, so the service calls
the hey-api client by hand, as projectRunsService does. Regenerating
sdk.gen.ts replaces the whole client from whatever backend is running,
so that waits for the backend to merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scope line now reads "Shared with every project in ⬡ <workspace>.",
with the Shapes icon labelled "Workspace" as on the project cards and the
name in semibold, truncated when it is long.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs force-pushed the cvs/workspace-picker branch from 2c01d7b to e4156a4 Compare October 16, 2026 19:59
@camielvs
camielvs force-pushed the cvs/workspace-secrets branch from 22a2d21 to c5366ed Compare October 16, 2026 19:59
@camielvs
camielvs removed this pull request from stack #2887 October 16, 2026 21:30
@camielvs
camielvs force-pushed the cvs/workspace-picker branch 3 times, most recently from b00e88c to 1305c77 Compare October 20, 2026 20:15
failure: string,
result: { error: unknown; response: Response },
): WorkspaceSecretsApiError {
const reason = describeErrorBody(result.error);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[P2] Redact submitted secret values from validation errors

describeErrorBody() includes each validation issue's raw input in the message. A 422 response containing detail[].input for secret_value therefore puts the submitted secret into this error, and useWorkspaceSecretMutation displays it in a toast. I reproduced this with a targeted service test: the returned error contained the submitted value in plaintext.

Use a redacted error formatter for secret requests that preserves the field and explanation but omits input values, including model-level inputs that could contain the whole request body. Add a regression test asserting that the submitted secret never appears in the error message.

@camielvs

Copy link
Copy Markdown
Collaborator Author

Superseded by the team-impersonation stack (#2927 → #2944). This PR's form fixes and expiry picker moved to #2939. Its Tangent Secrets window (dock registration, ListRowIcon, secret row) moved to #2941, rebuilt on team-scoped secrets. The branch is kept.

@camielvs camielvs closed this Oct 22, 2026
camielvs added a commit that referenced this pull request Oct 26, 2026
Four fixes to the add and replace form, carried over from the
superseded workspace-secrets draft (#2886). They apply to Settings →
Secrets today and to the team secrets screens that follow.

- The value field sets `autocomplete="new-password"`, so browsers stop
  offering a saved login password for it.
- A name containing `/` is refused. The name is a path segment in the
  replace and delete requests, so such a secret could be created but
  never changed or removed.
- Deleting asks first. A deleted secret makes every run that uses it
  fail at launch, and once secrets are shared with a team that is
  everyone's runs.
- Description and expiry fields, using the expiry picker from #2886.
  The backend only writes either when a value is sent, so neither can
  be cleared once set: replacing keeps the current description and
  expiry unless a new one is given, and the form says so. Expiry is not
  enforced by the backend, so the form calls it a reminder. The
  Settings list shows a secret's description under its name.

The E2E `removeSecret` helper now confirms the delete dialog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
camielvs added a commit that referenced this pull request Oct 26, 2026
Tangent's project dock gains a Secrets window. It lists the secrets
that runs submitted in the project use: the team's, when the project's
workspace maps to a team you can act as, and your own otherwise.

- In a team's project the window is headed "Team: <workspace>" with
  the team id as its tooltip, and add, replace and delete act as the
  team. A member whose role can't write sees the list without those
  actions. Hiding them is the only guard, since the backend doesn't
  check write access on `/api/secrets/`.
- Everywhere else it is headed "My secrets".
- When the project has a team but you can't act as it (not a member,
  refused by the backend, or the lookup failed), the window says so and
  lists your own secrets, because that is who your runs there will be
  submitted as. `ActingTeamNotice` holds that copy so the submit dialog
  can say the same thing later.
- Projects on different teams list nothing and say why; submitting is
  blocked in that case.

The add, replace and delete controls take an optional team id and
invalidate that team's cache key. Their failure toasts now show the
backend's reason, which matters once a request can be refused for
acting as a team.

From #2886: the dock registration and its order test, `ListRowIcon`,
and the secret row's look. Replace and delete are inline buttons rather
than a menu, so the row reuses the confirming delete button. Settings →
Secrets stays your own secrets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs deleted the cvs/workspace-secrets branch October 30, 2026 22:23
camielvs added a commit that referenced this pull request Oct 31, 2026
* feat(secrets): harden the secret form

Five fixes to the add and replace form, carried over from the
superseded workspace-secrets draft (#2886). They apply to Settings →
Secrets today and to the team secrets screens that follow.

- The value field sets `autocomplete="new-password"`, so browsers stop
  offering a saved login password for it.
- A name containing `/`, or a name that is `.` or `..`, is refused. The
  name is a path segment in the replace and delete requests, and the URL
  parser collapses dot segments, so such a secret could be created but
  never changed or removed.
- Name and description stop at 255 characters, the size of their
  backend columns, and say so when the limit is reached. Longer values
  failed with a bare "Failed to add secret" or were cut off.
- Deleting asks first, with a "Delete" button. A deleted secret makes
  every run that uses it fail at launch, and once secrets are shared
  with a team that is everyone's runs.
- Description and expiry fields, using the expiry picker from #2886.
  The backend only writes either when a value is sent, so neither can
  be cleared once set: replacing keeps the current description and
  expiry unless a new one is given, and the form says so. Expiry is not
  enforced by the backend, so the form calls it a reminder. The
  Settings list shows a secret's description under its name.

The E2E `removeSecret` helper now confirms the delete dialog, found by
its title. The React Compiler covers the new `ExpiryDatePicker`. Tests
get one `ResizeObserver` stub in `vitest-setup.js` instead of a copy per
suite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(secrets): keep a secret's expiry on the day chosen

An expiry chosen as Dec 25 came back as Dec 26. The form sent the end of
the day as 23:59:59.999 UTC, and a database that stores datetimes to the
second rounds that up to midnight. The form now sends the day's last
whole second.

The hint under a new secret's expiry is gone. Replacing a secret that
already has one still says which date it keeps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
camielvs added a commit that referenced this pull request Oct 31, 2026
* feat(tangent): manage a project's secrets in a Secrets window

Tangent's project dock gains a Secrets window when the project's
workspace maps to a team. It lists the workspace's secrets, the ones
runs in the project use when they act as the team. Your own secrets
stay in Settings → Secrets.

The copy calls the team the workspace. A team takes its workspace's
name, and the workspace is what people know it by.

- In a team's project the window is headed "Workspace: <workspace>" with
  the team id as its tooltip, and add, replace and delete act as the
  team. A member whose role can't write sees the list without those
  actions. Hiding them is the only guard, since the backend doesn't
  check write access on `/api/secrets/`.
- A project whose workspace has no team has no workspace secrets, so it
  gets no Secrets window. The window opens once the team is known, so
  it never appears and then vanishes.
- When the project has a team but you can't act as it (not a member,
  refused by the backend, or the lookup failed), the window says the
  workspace's secrets aren't available and why. It doesn't list your
  own secrets in their place, where they would pass for the
  workspace's.

The add, replace and delete controls take an optional team id and
invalidate that team's cache key. Their failure toasts now show the
backend's reason, which matters once a request can be refused for
acting as a team.

From #2886: the dock registration and its order test, `ListRowIcon`,
and the secret row's look. Replace and delete are inline buttons rather
than a menu, so the row reuses the confirming delete button. Settings →
Secrets stays your own secrets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(tangent): name workspaces with their icon in the Secrets window

Feedback from the first staging pass.

- A team's name, which is its workspace's, now carries the workspace
  icon wherever it's written into copy, with the team id on hover.
  `TeamName` does this, and the add dialog, the delete confirmation and
  the window's notice use it. Its 12px icon sits 0.1em below the
  baseline, which centres it on the capitals of both 12px and 14px
  text. `bold` sets the name in semibold and the icon at stroke 3, the
  weight that matches. The React Compiler covers
  `src/components/shared/Teams`, where it lives.
- Deleting a workspace secret asks 'Delete shared secret "<name>"?'
  and says it's removed for the whole workspace, named in bold: "This
  removes the secret from the <workspace> workspace. Runs that use it
  will fail. This can't be undone." `ConfirmationDialog` now takes any
  node as its description, so the name carries its icon there too.
- Adding a secret is titled "Add a workspace secret".
- The replace dialog shows the secret's key icon at its top left, like
  the secret's row.
- The empty-state line under the list is gone. The add button is enough.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants