+ {!editingDefinition ? (
+
+
Who provides the value?
+
{
+ const next = value as SecretValueProvider;
+ setSecretValueProvider(next);
+ setCreateKeyEditable(false);
setCreateForm((current) => ({
...current,
- name,
key: createKeyDirty
? current.key
- : secretValueProvider === "user"
- ? normalizeUserSecretKeyForPreview(name)
- : normalizeSecretKeyForPreview(name),
+ : next === "user"
+ ? normalizeUserSecretKeyForPreview(current.name)
+ : normalizeSecretKeyForPreview(current.name),
}));
}}
- placeholder={secretValueProvider === "user" ? "Personal GitHub token" : "OPENAI_API_KEY"}
- autoFocus
- />
-
-
-
- Key {secretValueProvider === "company" ? (optional) : null}
-
-
{
- setCreateKeyDirty(true);
- setCreateForm((current) => ({ ...current, key: event.target.value }));
- }}
- placeholder={secretValueProvider === "user" ? "PERSONAL_GH_TOKEN" : "auto from name"}
- disabled={Boolean(editingDefinition)}
- className={secretValueProvider === "user" ? "font-mono text-sm" : undefined}
- />
-
- {secretValueProvider === "user"
- ? editingDefinition
- ? "Stable env binding key. Cannot be changed."
- : "Env-style key used by user-secret bindings."
- : "Shared secret keys keep lowercase dash normalization."}
+ >
+
+ Company
+ Each user
+
+
+
+ Company stores one shared value. Each user lets every member supply their own value under My secrets.
+ ) : null}
+
+ {secretValueProvider === "company" && !editingDefinition ? (
+
setCreateMode(value as CreateMode)}>
+
+ Managed value
+ External reference
+
+
+ ) : null}
+
+
+ Name
+ {
+ const name = event.target.value;
+ setCreateForm((current) => ({
+ ...current,
+ name,
+ key: createKeyDirty
+ ? current.key
+ : secretValueProvider === "user"
+ ? normalizeUserSecretKeyForPreview(name)
+ : normalizeSecretKeyForPreview(name),
+ }));
+ }}
+ placeholder={secretValueProvider === "user" ? "Personal GitHub token" : "/dev/foo/bar"}
+ autoFocus
+ />
+
+ {secretValueProvider === "company" && createMode === "managed" ? (
+
+ Value
+
+ ) : null}
+ {secretValueProvider === "company" && createMode === "external" ? (
+
+
External reference
+
+ setCreateForm((current) => ({ ...current, externalRef: event.target.value }))
+ }
+ placeholder="arn:aws:secretsmanager:..."
+ className="font-mono text-xs"
+ />
+
+ Existing provider secrets are resolve-only in Paperclip. Rotate the value in the provider,
+ then update this reference only if the path, ARN, or version changes.
+
+
+ ) : null}
+ {secretValueProvider === "user" ? (
+ <>
+
+ Every member supplies their own value under My secrets. Agents resolve the responsible
+ user's value at runtime.
+
+
+
+ Usage guidance (optional)
+
+
+ >
+ ) : null}
+
+
+
+
Key
+ {!createKeyEditable && !editingDefinition ? (
+
setCreateKeyEditable(true)}
+ >
+ Edit
+
+ ) : null}
+
+
{
+ if (!createKeyEditable || editingDefinition) return;
+ setCreateKeyDirty(true);
+ setCreateForm((current) => ({ ...current, key: event.target.value }));
+ }}
+ placeholder={secretValueProvider === "user" ? "PERSONAL_GH_TOKEN" : "auto from name"}
+ disabled={Boolean(editingDefinition)}
+ className={cn(
+ "font-mono text-sm",
+ !createKeyEditable && !editingDefinition && "border-dashed bg-muted/40 text-muted-foreground",
+ )}
+ />
+
+ {editingDefinition
+ ? "Stable env binding key. Cannot be changed."
+ : !createKeyEditable
+ ? "Generated from the name."
+ : secretValueProvider === "user"
+ ? "Env-style key used by user-secret bindings."
+ : "Shared secret keys keep lowercase dash normalization."}
+
+
+
Description (optional)
@@ -2106,53 +2246,9 @@ export function Secrets() {
/>
- {!editingDefinition ? (
-
-
Who provides the value?
-
{
- const next = value as SecretValueProvider;
- setSecretValueProvider(next);
- setCreateForm((current) => ({
- ...current,
- key: createKeyDirty
- ? current.key
- : next === "user"
- ? normalizeUserSecretKeyForPreview(current.name)
- : normalizeSecretKeyForPreview(current.name),
- }));
- }}
- >
-
- Company
- Each user
-
-
-
- Company stores one shared value. Each user lets every member supply their own value under My secrets.
-
-
- ) : null}
-
{secretValueProvider === "company" ? (
<>
-
setCreateMode(value as CreateMode)}>
-
-
- Managed value
-
-
- External reference
-
-
-
+
Provider
{selectedCreateProviderConfig ? (
- ) : (
-
- Existing deployment-level provider settings stay available for backwards compatibility.
-
- )}
-
- {createMode === "managed" ? (
- <>
-
- Paperclip-managed secrets are created in the selected provider and future rotations
- write a new provider version through Paperclip.
- {awsManagedPathPreview ? (
-
- AWS managed path:{" "}
-
- {awsManagedPathPreview}
-
-
) : null}
-
- Value
-
- >
- ) : (
-
-
External reference
-
- setCreateForm((current) => ({ ...current, externalRef: event.target.value }))
- }
- placeholder="arn:aws:secretsmanager:..."
- className="font-mono text-xs"
- />
-
- Existing provider secrets are resolve-only in Paperclip. Rotate the value in the provider,
- then update this reference only if the path, ARN, or version changes.
-
+ {createMode === "managed" ? (
+
+ Paperclip-managed secrets are created in the selected provider and future rotations
+ write a new provider version through Paperclip.
+ {awsManagedPathPreview ? (
+
+ AWS managed path:{" "}
+
+ {awsManagedPathPreview}
+
+
+ ) : null}
- )}
+ ) : null}
>
- ) : (
- <>
-
- Every member supplies their own value under My secrets. Agents resolve the responsible
- user's value at runtime.
-
-
-
- Usage guidance (optional)
-
-
- >
- )}
+ ) : null}
{createError ? (
)
+ .filter(([, binding]) => {
+ if (typeof binding !== "object" || binding === null) return false;
+ const record = binding as Record;
+ return reference.kind === "company"
+ ? record.type === "secret_ref" && record.secretId === reference.secret.id
+ : record.type === "user_secret_ref" && record.key === reference.definition.key;
+ })
+ .map(([key]) => key)
+ .sort();
+}
+
+function AgentAccessSection({
+ companyId,
+ reference,
+}: {
+ companyId: string;
+ reference: AgentAccessReference;
+}) {
+ const queryClient = useQueryClient();
+ const { pushToast } = useToastActions();
+ const [selectedAgentId, setSelectedAgentId] = useState("");
+ const [envKey, setEnvKey] = useState("");
+ const [envKeyDirty, setEnvKeyDirty] = useState(false);
+ const [accessError, setAccessError] = useState(null);
+
+ const referenceId = reference.kind === "company" ? reference.secret.id : reference.definition.id;
+ const referenceName = reference.kind === "company" ? reference.secret.name : reference.definition.name;
+
+ const agentsQuery = useQuery({
+ queryKey: queryKeys.agents.list(companyId),
+ queryFn: () => agentsApi.list(companyId),
+ staleTime: 30_000,
+ });
+ const agents = useMemo(
+ () => (agentsQuery.data ?? []).filter((agent) => agent.status !== "terminated"),
+ [agentsQuery.data],
+ );
+ const agentAccess = useMemo(
+ () =>
+ agents
+ .map((agent) => ({
+ agent,
+ envKeys: envKeysReferencingSecret(
+ (agent.adapterConfig as Record | null)?.env,
+ reference,
+ ),
+ }))
+ .filter((entry) => entry.envKeys.length > 0),
+ [agents, reference],
+ );
+ const grantableAgents = useMemo(
+ () => agents.filter((agent) => !agentAccess.some((entry) => entry.agent.id === agent.id)),
+ [agents, agentAccess],
+ );
+
+ const effectiveEnvKey = envKeyDirty
+ ? envKey
+ : reference.kind === "user"
+ ? reference.definition.key
+ : envKeyFromSecretName(referenceName);
+
+ useEffect(() => {
+ setSelectedAgentId("");
+ setEnvKey("");
+ setEnvKeyDirty(false);
+ setAccessError(null);
+ }, [referenceId]);
+
+ function invalidateAfterChange(agentId: string) {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(companyId) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId) });
+ if (reference.kind === "company") {
+ queryClient.invalidateQueries({ queryKey: queryKeys.secrets.usage(reference.secret.id) });
+ }
+ }
+
+ const grantMutation = useMutation({
+ mutationFn: async ({ agentId, key }: { agentId: string; key: string }) => {
+ // Re-fetch right before patching so we merge into the freshest env config.
+ const detail = await agentsApi.get(agentId, companyId);
+ const adapterConfig = { ...((detail.adapterConfig ?? {}) as Record) };
+ const env = { ...((adapterConfig.env ?? {}) as Record) };
+ if (env[key] !== undefined) {
+ throw new Error(`${detail.name} already has an env var named ${key}.`);
+ }
+ env[key] =
+ reference.kind === "company"
+ ? { type: "secret_ref", secretId: reference.secret.id }
+ : { type: "user_secret_ref", key: reference.definition.key };
+ return agentsApi.update(
+ agentId,
+ { adapterConfig: { ...adapterConfig, env }, replaceAdapterConfig: true },
+ companyId,
+ );
+ },
+ onSuccess: (agent, variables) => {
+ setSelectedAgentId("");
+ setEnvKey("");
+ setEnvKeyDirty(false);
+ setAccessError(null);
+ invalidateAfterChange(variables.agentId);
+ pushToast({ title: "Access granted", body: `${agent.name} now receives ${variables.key}`, tone: "success" });
+ },
+ onError: (error) => setAccessError(readableErrorMessage(error)),
+ });
+
+ const revokeMutation = useMutation({
+ mutationFn: async ({ agentId }: { agentId: string }) => {
+ const detail = await agentsApi.get(agentId, companyId);
+ const adapterConfig = { ...((detail.adapterConfig ?? {}) as Record) };
+ const env = { ...((adapterConfig.env ?? {}) as Record) };
+ const keys = envKeysReferencingSecret(env, reference);
+ if (keys.length === 0) return detail;
+ for (const key of keys) delete env[key];
+ return agentsApi.update(
+ agentId,
+ { adapterConfig: { ...adapterConfig, env }, replaceAdapterConfig: true },
+ companyId,
+ );
+ },
+ onSuccess: (agent, variables) => {
+ setAccessError(null);
+ invalidateAfterChange(variables.agentId);
+ pushToast({ title: "Access removed", body: agent.name, tone: "info" });
+ },
+ onError: (error) => setAccessError(readableErrorMessage(error)),
+ });
+
+ const envKeyValid = ENV_KEY_PATTERN.test(effectiveEnvKey);
+ const canGrant = Boolean(selectedAgentId) && envKeyValid && !grantMutation.isPending;
+
+ return (
+
+
+
+
Agent access
+
+
+ {reference.kind === "company"
+ ? "These agents receive this secret as an environment variable at run start."
+ : "These agents resolve the responsible user's value as an environment variable at run start."}
+
+ {agentsQuery.isPending ? (
+ Loading agents…
+ ) : agentsQuery.isError ? (
+
+ Agent list unavailable. Manage access from each agent's configuration instead.
+
+ ) : (
+ <>
+ {agentAccess.length > 0 ? (
+
+ {agentAccess.map(({ agent, envKeys }) => (
+
+ {agent.name}
+
+ {envKeys.join(", ")}
+
+ revokeMutation.mutate({ agentId: agent.id })}
+ >
+
+
+
+ ))}
+
+ ) : (
+ No agents have access yet.
+ )}
+
+
+
+ Agent
+
+ setSelectedAgentId(event.target.value)}
+ >
+ Select agent…
+ {grantableAgents.map((agent) => (
+
+ {agent.name}
+
+ ))}
+
+
+
+
+ Env var
+
+ {
+ setEnvKeyDirty(true);
+ setEnvKey(event.target.value.toUpperCase());
+ }}
+ className="h-8 font-mono text-xs"
+ placeholder="MY_SECRET"
+ />
+
+
grantMutation.mutate({ agentId: selectedAgentId, key: effectiveEnvKey })}
+ >
+ {grantMutation.isPending ? (
+
+ ) : (
+
+ )}
+ Add
+
+
+ {effectiveEnvKey && !envKeyValid ? (
+
+ Env keys use letters, digits, and underscores, and cannot start with a digit.
+
+ ) : null}
+ {accessError ? (
+ {accessError}
+ ) : null}
+ >
+ )}
+
+ );
+}
+
function SecretDetailsTab({
secret,
providers,