feat(agent-vault): agents that run without credentials - #8000
Open
saifsmailbox98 wants to merge 320 commits into
Open
feat(agent-vault): agents that run without credentials#8000saifsmailbox98 wants to merge 320 commits into
saifsmailbox98 wants to merge 320 commits into
Conversation
…field The checkbox was a second control for one field. The secret box now seeds an unchanged marker on edit, so a single field carries all three intents: leave it to keep the stored secret, type over it to replace, clear it to remove a basic password. Focusing selects the marker rather than deleting it, so tabbing through the field and moving on cannot wipe a credential, which is the failure mode the same pattern has in PAM. Switching credential type empties the box, since the sealed secret belongs to the type being replaced.
… shown The list already dropped it; the edit form and Review still showed the port the API appends, so the same host read two ways. The helper moves next to the schema and both callers use it. Also fixes Review claiming a password would be Removed when the credential never had one, and stops that no-op from re-sealing the blob and logging a credential replacement.
… to newest first Connections was the one Agent Vault table with neither sorting nor a Created column, while bundles, sessions and proxies all had both. The API now returns the connection's createdAt, and every column sorts on the same pattern as the bundles table. Both tables now open on Created descending rather than Name ascending, since the row someone just made is the one they came back for.
…ouse empty state Three things from a design pass. Every Agent Vault empty state set frame="dashed" alongside className="border", which the Empty component documents as independent opt-ins, so each drew a CSS border and an SVG frame over it. The rest of the app uses className="border" in 140 places and never combines the two; these six now do the same. The Manage Access sheet no longer labels its list Members or counts it, since the sheet title already says what it manages. The bundle list still shows memberCount, so no read path went unused. Creating an access bundle now grants it to whoever created it, in the same transaction, the way a PAM folder seeds its creator as a member. An admin already reaches every bundle by role so nothing changes for them today; it matters when that admin is later demoted, and it stops a bundle you just made from reading "0 members".
…t accent The tab strip already switched to av, but every button and badge inside the tabs was hardcoded to variant="project", which is Secret Manager's colour, so the one Agent Vault page built on the shared component had a primary action nobody else in the product has. getProductControlVariant returns project for every other product, so nothing else moves.
Agent Vault's Access Control page is about to stop sharing the generic one, so it needs the product-level membership surface PAM already has: list users, groups and identity members, then add, change role and remove. The one thing PAM's version does that ours must do differently is the cleanup on removal. PAM cascades inside the membership table, because its folder and account grants are RESOURCE_SCOPE rows there. Ours calls agentVaultMembershipCleanupService, because bundle grants live in their own table and a direct delete here would leave rows the mint path still honours. Also adds the last-admin guard we did not have, and batches the role lookup rather than querying per membership as PAM does.
Modelled on PamAccessControlPage rather than the shared project one: tabs with counts, a Product Role column, search beside the primary action, and the product accent throughout. Members and groups read from the generic project hooks, which carry the names, emails and pending state the rows need; only identities use our own endpoint, for the role beside each name. That is the same split PAM makes. Two things PAM does are deliberately left out. It brands identities it owns and offers Delete instead of Remove for them, because identities created inside PAM belong to the PAM project; Agent Vault never creates identities, so removal only ever takes the membership. And PAM reads a capabilities endpoint for isResourceAdmin, which means nothing here since bundles carry no roles, so gating stays with ProjectPermissionCan.
…Agent Vault Reverts the product-accent helper and drops the nine ProjectType.AgentVault branches from the shared page and its tabs, now that Agent Vault renders its own. Cert Manager is the standalone product there again, exactly as before this branch touched it.
…ginal form The isCertManager to isStandaloneProduct rename existed only so a second product could share the page. With Agent Vault on its own, it renames a boolean to itself across nine shared files and buys nothing. Back to main, so the only file this branch still touches there is the Agent Vault route.
Users, Groups and Machine Identities, matching the wording the rest of the product uses for the same three things. The counts go with them, and so do the three list queries the shell only ran to produce them; each tab already fetches its own.
…te also stales The Users and Groups tabs read from the generic project hooks, so invalidating only the Agent Vault keys left a role change or a removal on screen until the page was reloaded. The mutations now take the project and invalidate getProjectUsers and getProjectGroupMemberships alongside ours, which is what PAM's do.
Unnests a ternary in removeProductMember and takes the formatter's import order and line wrapping. make reviewable-api and make reviewable-ui both pass.
Three cases: the full add, promote and remove cycle for a machine identity; that removal reaps the bundle grants, which live outside the membership table and would otherwise be missed; and the four guards that keep the product administrable. Identities are inserted directly as the rest of this spec does, since creating one through the API is license-gated.
…select The three add dialogs listed the two roles with no explanation, while the change dialog explained them inside a select. One ProductRoleField now serves all four, built on the v3 RadioGroup pattern for rich rows: title, description, and the av FieldLabel variant so the chosen card carries the product accent. Two options never justified a select, and the descriptions are the part that actually decides which one someone wants.
…tion Neither line answered a question anyone has. Nobody wonders why a list of people to add leaves out the people already added, and nobody is looking for an identity Agent Vault could not have created.
Unmatched Hosts became two described choices rather than a bare select, since Allow and Deny say nothing about what they decide. The column is Uncovered Hosts, and it and Bypass Hosts carry their explanation on hover. Bypass hosts no longer arrive prefilled with package registries. Guessing at someone's traffic put six hosts in a field they never asked for; a placeholder shows the shape instead. The poll interval description says what the number decides rather than repeating the range the input and the API already enforce. Drops the banner under the table. It restated the Allow description for readers who had already chosen.
A connection takes precedence, so the field, the column hover and the proxies page all say that a host an access bundle covers still gets its credential. Also drops the claim that new proxies start with package registries prefilled, which they no longer do.
…and api docs Both still said bypass is evaluated before anything else, which stopped being true when a connection started taking precedence.
Field, column hover, API description and docs all said it skipped interception. It no longer does, and the thing it is actually for reads better anyway: hosts every agent needs that belong to the deployment rather than to one bundle.
The list is an exception to deny, so under allow it has nothing to except and an input that changes nothing is what made it read as a second access rule. The stored value is left alone rather than cleared, so switching back to deny brings it back.
Two lines under a number field that most people leave at 60. It rides on the label now, using the info-icon pattern the app connection forms already use.
DialogContent spaces its own children with gap-6, but this dialog wraps the header, body and footer in a form, so the form was the only child and the gap spaced nothing. Header, fields and footer all sat flush; the poll interval input ended two pixels above the footer border. The form now carries the layout DialogContent expects. Measured at 24px, matching every other dialog.
…oltip A bypassed host still receives a credential when an access bundle covers it, so neither the column hint nor the field description could claim otherwise.
…e browser The min and max attributes made the browser refuse the submit with its own bubble, so react-hook-form never ran and the inline FieldError never showed. Only 3 of the repo's 169 number inputs carry those attributes.
The sidebar had no product header: the overview page carried sidebarTitle "Overview" and no icon, while PAM, KMS and Secrets Scanning set both to the product. The three concept pages also sat inside a collapsed Concepts group, so nothing but Quickstart was visible without expanding it. They are a flat Product reference group now, like the siblings, and the wrapper group is sentence-cased like theirs. Access bundles gets its entry point and names the Template step. The secret-handling paragraph is gone; the edit sheet already shows all of it. Sessions no longer lists the duration presets, which the dropdown shows and which would go stale, and points the CLI reader at --ttl with the format inline. The proxy dialog's submit is Create, so the enrolling sentence stops naming Create Proxy twice, and the enrollment command carries --domain the way every dialog snippet does, since the default is US Cloud.
9008b6f made the session actor columns SET NULL so history outlives the actor, and resolve went on assuming one id was present. With both null the actor became { identity, null }, knex compiled the membership lookups to actorIdentityId IS NULL, and that matched every user row in the project, the admin's included. A session minted by a since-deleted user or machine identity kept brokering its credentials and did so as an admin, immune to grant removal, and showed Active until someone revoked it. Reproduced live: deleting the identity that minted a never-expiring session left the proxy attaching the real bearer token, with the admin's own grant on the bundle removed too. Resolve now answers 401 with its own reason before any lookup runs.
Give the menu item the danger variant and the same ban icon the revoked status badge uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The session row already carries the bundle id, so the badge can be the navigation it looks like. OverflowBadgeList grows an optional getLinkProps that renders the badge as a router link, and an icon that sits ahead of the label. The icon has to go into the hidden measurement badges too, or the overflow math sizes every badge as if it were icon-less and cuts the +N rollup one badge too late. Bundles that were deleted keep their name snapshot with no id, so they stay inert badges rather than links to nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fits The default max-w-lg left ~464px inside the code block, short of the ~540px the --session-token line needs, so the token was clipped behind a horizontal scroll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A duration ms() cannot parse and one under the one minute floor both reported "At least 1 minute.", which reads as nonsense for input like "dsfsdfdsf". Split the two so an unparsable value says so instead. Also space the custom input off the preset select by the 8px the field already uses between its label and its control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ctive ones Icons follow what the rest of the app already uses: pencil to edit, trash to delete, ban to revoke, refresh to reissue. Revoking a connection, a proxy or a member's access is destructive, so those items get the danger variant too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d-by badge Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tables Name and email cells truncate and carry the full value in a title, so one long value no longer pushes the role and action columns out of view. The pending invitation badge moves to the action column where it has room, a member with no first or last name renders an empty marker rather than a blank cell, and the role badge becomes one shared component that carries an icon per role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The list page's create button and row action menu were the last Agent Vault controls without icons. Use the same ones the detail page already uses: plus to create, users to manage access, pencil to edit, trash to delete. Deleting a bundle is destructive, so that item gets the danger variant too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The truncatable name and email columns are capped at max-width 0, so the trailing action column absorbed every pixel of slack and left several hundred points of empty space between the role badge and the row actions. Give the text columns a two-thirds width share so they take the slack instead and the trailing columns sit close to the row actions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A bundle description ran the full width of its cell, pushing the connections, members, and created columns out of view. The name column now claims a fixed share of the row with a floor for narrow viewports, and the name and description each truncate with the full value in a title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ticket icon reads as a support ticket in a product sidebar. A session is an issued, time-boxed credential bound to one actor, so an ID card carries the meaning and holds up at 16px next to the other nav icons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The overlapping tiles outlined each logo with a border only a shade off the tile and the row, so the stack blurred into one shape, and the OpenAI mark is black on transparent, so it vanished into its tile entirely. Custom hosts were tinted with a hashed hue that is not in the theme and read as people avatars. Each connection is now a shaded chip with a ring in the table color so the overlap reads as layers, custom hosts show the same globe the template picker uses, and the OpenAI template points at a white copy of its mark, the way Datadog already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ring that separates the overlapping chips is painted in the resting table color, so once a row lit up on hover it turned into a dark outline around every chip. It now follows the row to the hover color, on the same 75ms fade the row uses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Creating a session is two fields, and the design system puts a form that size in a centered dialog, keeping the sheet for large or multi-step forms. Every other create form in the product is already a dialog, so this one was a full-height panel holding a combobox and a select. The minted token also lands in SessionCreatedDialog, so the flow crossed the screen from a right-side panel to a centered modal halfway through. The discard guard goes with it. Picking one item from a dropdown is thin grounds for a confirm prompt, no other dialog here guards, and both fields reset on open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Revoking a member from a bundle asked for the member's name to be typed back, the same gate used for deleting a bundle. The grant can be given back from the same sheet, so a typed confirmation was heavier than the action warranted. A plain confirm dialog is enough. The dialog stays open while the request runs, so a failure keeps the member in view instead of dismissing the prompt and leaving only the error toast. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Empty primitive grows to fill its flex parent, and the sheet body is a flex column, so the no-members box stretched to the bottom of the sheet. Pin it to its content height so it sits under the Grant Access button like the other list empty states. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every session in a list often belongs to the same actor, so typing the actor's name confirmed nothing about which session is being revoked. Typing the word revoke keeps the deliberate pause without implying the name picks out the session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… pathname d18fe11 moved every useProject gate for PAM and Agent Vault from the pathname to the route matches, since the router changes location a frame before it swaps matches and a pathname read mounts the project frame early. The command menu arrived from main after that sweep with its own PAM gate written the old way, so entering PAM from a page with no project rendered CurrentProjectCommandMenu before the project existed and threw Missing project id from the root boundary. A non-member saw that crash in place of the access-denied page. The gate reads useImplicitProduct like OrgSidebar and ProjectSelect do. It also had no Agent Vault arm, so Agent Vault gets the project section it was missing.
…LI one The page named the Docker and systemd snippets and left the reader to open the dialog for them, while the gateway deployment page shows its deployment variants inline. All three are tabs now, with placeholders where the dialog interpolates the token and the instance, plus the volume's purpose and where the unit file goes.
The segment carried no meaning once the bundle grants took their own agent-vault-access-bundle-member-* keys, and it only made these longer. They are agent-vault-member-* now. PAM keeps pam-product-member-*: it shipped in August, eventType is a plain string on audit_logs, and renaming would orphan every stored row and any log-stream filter built on it.
Every product that has roles worth explaining carries one: PAM, PKI and secrets management each have an Access control concept page, while the shared mechanics live under Identity and access. Agent Vault had neither, so who is an admin, what a member reaches, and how a grant differs from membership were compressed into one sentence on the access bundles page. That sentence is now a link. The page follows PAM's shape: the two levels, the role table, adding members, bundle grants, and what a change does to a running agent.
…e screenshots The pages shortened "access bundle" to "bundle" from the second mention on, and used "mint" for what the dashboard calls Create Session, so two names existed for each of the two things a reader meets first. Both now match the product: access bundle throughout, and create rather than mint, including the CLI reference and the Minting a session heading its anchor was built on. Sessions are created with an access bundle rather than over one. Two older slips went with them: the overview called it an access session, and the access control page repeated a sentence that sessions already carries. The screenshots land here too, fourteen of them across the quickstart and the three product reference pages, with only the images the pages reference.
Two files conflicted, and a third broke without conflicting. docs.json: main restored the Agent Vault nav entry as a working external link to docs.agent-vault.dev (#8077), repairing a group that expanded to nothing. This branch deletes that link because Agent Vault now has its own pages, so the removal wins and the stub page main added goes with it. SignupCompleteStep: main replaced handleOpen and navigate() with a getDestination() that feeds <Link> (#8007). That structure stands and the Agent Vault arm is added beside the PAM one; the imperative version could not survive anyway, since the auto-merged region had already dropped useNavigate. CertificateImportSheet came from main clean and did not compile. This branch moved useWizardSteps into hooks/ so the connection sheet could share it, and main's new file still imported it from the certificate wizard. Repointed, the way the issuance and renewal modals already were.
The last-admin guard used to be a private copy that only needed an
advisory lock, so the mock handed it `{ raw }` and nothing else. Moving
the Agent Vault paths onto the shared assertWillRetainProjectAdmin
changed what the transaction has to be: it still locks through tx.raw,
but then counts live admins with a query built off tx() itself, which
an object cannot answer. Both guard tests died on "tx is not a function"
rather than on anything they were written to check.
The mock is now a callable chain whose count comes from the fixture's
adminMembershipIds, so each test's own setup still decides whether the
guard lets the write through: drop the other admin and both tests fail
on "must keep at least one admin", which is the guard doing its job.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Agent Vault is a new product. Agents run holding no credentials. Their traffic is pointed at a proxy the customer runs, and the proxy attaches the real credential on the way out, only for the hosts that agent is allowed to reach. So an agent can use a secret without ever holding one, and it cannot give itself access to anything more. The proxy and the
infisical avcommands are in Infisical/cli#386; the two merge together.Four things make up the product. A connection is one host and one credential — "api.github.com, and this token". An access bundle is a named set of connections, and it is what you grant to a person, a machine identity, or a group. A session is what an agent runs with: a token naming who it belongs to and which bundles it carries. A proxy is the process the customer deploys where their agent traffic leaves.
Dashboard: a new product with four pages — Access Bundles, Sessions, Proxies and Audit Logs — plus its own Access Control. Adding a credential is a short wizard with templates for services we already know, so you pick GitHub and only supply the token. Session tokens and proxy enrollment tokens are each shown once, at creation.
API: endpoints for bundles, connections, grants, sessions and proxies, plus the two the proxy itself calls — one to check in, one to ask what a session may reach right now. That second one is the only place a credential is ever decrypted.
Access: Agent Vault is organization-wide rather than something you create per project, the same shape as PAM. There are two roles, admin and member. An admin runs the product; a member mints sessions over the bundles they have been granted and sees nothing else. A grant is a resource-scoped row in the shared
membershipstable with a singleconsumerrole, the way PAM stores folder access, so removing someone from the product or the organization, deprovisioning them through SCIM, or deleting a user, identity or group takes their grants with it through the platform's own paths. Deleting a bundle reaps its grant rows in the same transaction and locks the bundle row first, so a grant cannot land mid-delete. The admin who creates a bundle is granted it, but only if they are directly in the product; an admin who is in only through a group reaches it as admin, so no individual grant can outlive its owner's membership.Revoking works everywhere it should. Revoke a session, remove a grant, remove someone from the product, or deactivate them in the organization, and their agent stops receiving credentials within a minute. There is no separate step to remember.
Docs: an overview, a quickstart, and pages for access bundles, sessions, proxies and the CLI commands.
Two things worth knowing, because they look like oversights and are not. The proxy can reach private networks, including inside your own infrastructure — that is deliberate, since an internal API is one of the main things people want this for, and Uncovered Hosts: Deny is there for a proxy that sits somewhere it could reach more than you intend. And there is no audit event per request: creating, revoking and expiring a session are audited, but a check every minute per session would write a hundred thousand rows a day for a modest deployment, so the proxy logs each credential it attaches instead.
Steps to verify the change
infisical av run. The request reaches the upstream with the credential attached, and the agent holds nothing.Type
Checklist