mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
feat: track agent API key usage
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a4aac781b7
commit
8b288dbc1e
7 files changed
+211
-50
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user