feat: track agent API key usage

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-22 18:31:56 +00:00
1 parent a4aac781b7
commit 8b288dbc1e
7 files changed
+211 -50

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ export interface AgentKey {
id: string;
name: string;
scope: AgentApiKeyScope;
lastUsedAt: Date | null;
createdAt: Date;
revokedAt: Date | null;
}
@@ -12,11 +12,14 @@ import {
} from "./agent-configuration-shell";
describe("agent configuration shell", () => {
it("matches both section names and field labels", () => {
it("matches section names, field metadata, help copy, and synonyms", () => {
expect([...filterAgentConfigurationSections("")].slice(0, 2)).toEqual(["runtime", "environment"]);
expect([...filterAgentConfigurationSections("heartbeat")]).toEqual(["schedule"]);
expect([...filterAgentConfigurationSections("heartbeat")]).toEqual(["runtime", "schedule"]);
expect([...filterAgentConfigurationSections("API Keys")]).toEqual(["keys"]);
expect([...filterAgentConfigurationSections("sandbox")]).toEqual(["danger"]);
expect([...filterAgentConfigurationSections("filesystem")]).toEqual(["danger"]);
expect([...filterAgentConfigurationSections("cron")]).toEqual(["schedule"]);
expect([...filterAgentConfigurationSections("periodic tasks")]).toEqual(["schedule"]);
});
it("does not guess an inherited adapter model from available choices", () => {
+98 -12
View File
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { Search, Zap } from "lucide-react";
import type { Agent, AgentEnvConfig } from "@paperclipai/shared";
import { adapterLabels } from "./agent-config-primitives";
import { adapterLabels, help } from "./agent-config-primitives";
import { Badge } from "@/components/ui/badge";
import { cn } from "../lib/utils";
@@ -14,28 +14,114 @@ export type AgentConfigurationSectionId =
| "danger"
| "history";
export type AgentConfigurationSetting = {
key: string;
label: string;
helpText: string;
synonyms?: readonly string[];
};
export const agentConfigurationSections: ReadonlyArray<{
id: AgentConfigurationSectionId;
label: string;
instant?: boolean;
fields: string[];
settings: readonly AgentConfigurationSetting[];
}> = [
{ id: "runtime", label: "Runtime", fields: ["adapter", "model", "effort", "turns", "command", "arguments", "engine", "timeout", "chrome"] },
{ id: "environment", label: "Environment", fields: ["execution environment", "environment override", "variables", "secrets"] },
{ id: "schedule", label: "Schedule & Runs", fields: ["heartbeat", "wake on demand", "cooldown", "concurrent runs", "continuation"] },
{ id: "access", label: "Access & Governance", instant: true, fields: ["trust preset", "boundary", "create agents", "create skills", "assign tasks"] },
{ id: "keys", label: "API Keys", instant: true, fields: ["credentials", "create key", "revoke key"] },
{ id: "danger", label: "Danger & Legacy", fields: ["skip permissions", "bypass sandbox", "working directory", "deprecated"] },
{ id: "history", label: "History", instant: true, fields: ["configuration revisions", "restore"] },
{
id: "runtime",
label: "Runtime",
settings: [
{ key: "adapterType", label: "Adapter type", helpText: help.adapterType },
{ key: "model", label: "Model", helpText: help.model },
{ key: "thinkingEffort", label: "Thinking effort", helpText: help.thinkingEffort, synonyms: ["reasoning"] },
{ key: "maxTurnsPerRun", label: "Max turns per run", helpText: help.maxTurnsPerRun },
{ key: "command", label: "Command", helpText: help.localCommand, synonyms: ["arguments", "args", "engine"] },
{ key: "timeoutSec", label: "Timeout", helpText: help.timeoutSec },
{ key: "chrome", label: "Chrome", helpText: help.chrome },
],
},
{
id: "environment",
label: "Environment",
settings: [
{ key: "defaultEnvironmentId", label: "Execution environment", helpText: "Override the company default execution environment for this agent." },
{ key: "env", label: "Environment variables", helpText: help.envVars },
{ key: "secretAccess", label: "Secret access", helpText: help.secretAccess },
{ key: "workspaceStrategy", label: "Workspace strategy", helpText: help.workspaceStrategy, synonyms: ["worktree"] },
],
},
{
id: "schedule",
label: "Schedule & Runs",
settings: [
{ key: "heartbeat.enabled", label: "Heartbeat on interval", helpText: help.heartbeatInterval, synonyms: ["cron", "schedule", "timer"] },
{ key: "heartbeat.intervalSec", label: "Heartbeat interval", helpText: help.intervalSec, synonyms: ["cron", "cadence"] },
{ key: "heartbeat.wakeOnDemand", label: "Wake on demand", helpText: help.wakeOnDemand },
{ key: "heartbeat.cooldownSec", label: "Cooldown", helpText: help.cooldownSec },
{ key: "heartbeat.maxConcurrentRuns", label: "Max concurrent runs", helpText: help.maxConcurrentRuns },
{ key: "heartbeat.maxTurnContinuation", label: "Continue after max-turn stop", helpText: help.maxTurnContinuationEnabled, synonyms: ["continuation"] },
],
},
{
id: "access",
label: "Access & Governance",
instant: true,
settings: [
{ key: "permissions.trustPreset", label: "Trust preset", helpText: "Choose the default governance boundary for this agent." },
{ key: "permissions.canCreateAgents", label: "Create agents", helpText: "Allow this agent to create or hire other agents." },
{ key: "permissions.canCreateSkills", label: "Create skills", helpText: "Allow this agent to create reusable company skills." },
{ key: "permissions.canAssignTasks", label: "Assign tasks", helpText: "Allow this agent to assign tasks to other agents." },
],
},
{
id: "keys",
label: "API Keys",
instant: true,
settings: [
{ key: "apiKeys", label: "API keys", helpText: "Credentials used by this agent to authenticate calls to the Paperclip server.", synonyms: ["credentials", "revoke"] },
],
},
{
id: "danger",
label: "Danger & Legacy",
settings: [
{ key: "dangerouslySkipPermissions", label: "Skip permissions", helpText: help.dangerouslySkipPermissions },
{ key: "dangerouslyBypassSandbox", label: "Bypass sandbox", helpText: help.dangerouslyBypassSandbox, synonyms: ["filesystem", "network"] },
{ key: "cwd", label: "Working directory", helpText: help.cwd, synonyms: ["deprecated", "legacy"] },
],
},
{
id: "history",
label: "History",
instant: true,
settings: [
{ key: "configurationRevisions", label: "Configuration revisions", helpText: "Review and restore earlier saved agent configurations.", synonyms: ["restore", "rollback"] },
],
},
];
export const agentConfigurationSettingIndex = agentConfigurationSections.flatMap((section) =>
section.settings.map((setting) => ({
...setting,
section: section.id,
sectionLabel: section.label,
sectionAnchor: `config-${section.id}`,
})),
);
export function filterAgentConfigurationSections(query: string): Set<AgentConfigurationSectionId> {
const normalized = query.trim().toLowerCase();
if (!normalized) return new Set(agentConfigurationSections.map((section) => section.id));
return new Set(
agentConfigurationSections
.filter((section) => section.label.toLowerCase().includes(normalized) || section.fields.some((field) => field.includes(normalized)))
.map((section) => section.id),
agentConfigurationSettingIndex
.filter((setting) => [
setting.key,
setting.label,
setting.helpText,
setting.sectionLabel,
...(setting.synonyms ?? []),
].some((value) => value.toLowerCase().includes(normalized)))
.map((setting) => setting.section),
);
}
+33 -20
View File
@@ -4278,26 +4278,39 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
<h3 className="text-xs font-medium text-muted-foreground mb-2">
Active Keys
</h3>
<div className="border border-border rounded-lg divide-y divide-border">
{activeKeys.map((key: AgentKey) => (
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
<div>
<span className="text-sm font-medium">{key.name}</span>
<span className="text-xs text-muted-foreground ml-3">
Created {formatDate(key.createdAt)}
</span>
</div>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive text-xs"
onClick={() => revokeKey.mutate(key.id)}
disabled={revokeKey.isPending}
>
Revoke
</Button>
</div>
))}
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-left">
<thead className="border-b border-border text-xs font-medium text-muted-foreground">
<tr>
<th className="px-4 py-2 font-medium">Name</th>
<th className="px-4 py-2 font-medium">Created</th>
<th className="px-4 py-2 font-medium">Last used</th>
<th className="px-4 py-2"><span className="sr-only">Actions</span></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{activeKeys.map((key: AgentKey) => (
<tr key={key.id}>
<td className="whitespace-nowrap px-4 py-2.5 text-sm font-medium">{key.name}</td>
<td className="whitespace-nowrap px-4 py-2.5 text-xs text-muted-foreground">{formatDate(key.createdAt)}</td>
<td className="whitespace-nowrap px-4 py-2.5 text-xs font-mono text-muted-foreground" title={key.lastUsedAt ? formatDate(key.lastUsedAt) : undefined}>
{key.lastUsedAt ? relativeTime(key.lastUsedAt) : "—"}
</td>
<td className="px-4 py-2.5 text-right">
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive text-xs"
onClick={() => revokeKey.mutate(key.id)}
disabled={revokeKey.isPending}
>
Revoke
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}