mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 20:10:39 +09:00
Merge remote-tracking branch 'public-gh/master' into paperclip-routines
* public-gh/master: (46 commits) chore(lockfile): refresh pnpm-lock.yaml (#1377) fix: manage codex home per company by default Ensure agent home directories exist before use Handle directory entries in imported zip archives Fix portability import and org chart test blockers Fix PR verify failures after merge fix: address greptile follow-up feedback Address remaining Greptile portability feedback docs: clarify quickstart npx usage Add guarded dev restart handling Fix PAP-576 settings toggles and transcript default Add username log censor setting fix: use standard toggle component for permission controls fix: add missing setPrincipalPermission mock in portability tests fix: use fixed 1280x640 dimensions for org chart export image Adjust default CEO onboarding task copy fix: link Agent Company to agentcompanies.io in export README fix: strip agents and projects sections from COMPANY.md export body fix: default company export page to README.md instead of first file Add default agent instructions bundle ... # Conflicts: # packages/adapters/pi-local/src/server/execute.ts # packages/db/src/migrations/meta/0039_snapshot.json # packages/db/src/migrations/meta/_journal.json # server/src/__tests__/agent-permissions-routes.test.ts # server/src/__tests__/agent-skills-routes.test.ts # server/src/services/company-portability.ts # skills/paperclip/references/company-skills.md # ui/src/api/agents.ts
This commit is contained in:
commit
e3c92a20f1
96 changed files with 15366 additions and 1684 deletions
|
|
@ -5,12 +5,12 @@ import {
|
|||
agentsApi,
|
||||
type AgentKey,
|
||||
type ClaudeLoginResult,
|
||||
type AvailableSkill,
|
||||
type AgentPermissionUpdate,
|
||||
} from "../api/agents";
|
||||
import { companySkillsApi } from "../api/companySkills";
|
||||
import { budgetsApi } from "../api/budgets";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { instanceSettingsApi } from "../api/instanceSettings";
|
||||
import { ApiError } from "../api/client";
|
||||
import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts";
|
||||
import { activityApi } from "../api/activity";
|
||||
|
|
@ -109,13 +109,21 @@ const SECRET_ENV_KEY_RE =
|
|||
/(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
|
||||
const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/;
|
||||
|
||||
function redactPathText(value: string, censorUsernameInLogs: boolean) {
|
||||
return redactHomePathUserSegments(value, { enabled: censorUsernameInLogs });
|
||||
}
|
||||
|
||||
function redactPathValue<T>(value: T, censorUsernameInLogs: boolean): T {
|
||||
return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs });
|
||||
}
|
||||
|
||||
function shouldRedactSecretValue(key: string, value: unknown): boolean {
|
||||
if (SECRET_ENV_KEY_RE.test(key)) return true;
|
||||
if (typeof value !== "string") return false;
|
||||
return JWT_VALUE_RE.test(value);
|
||||
}
|
||||
|
||||
function redactEnvValue(key: string, value: unknown): string {
|
||||
function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boolean): string {
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
|
|
@ -126,11 +134,11 @@ function redactEnvValue(key: string, value: unknown): string {
|
|||
}
|
||||
if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE;
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "string") return redactHomePathUserSegments(value);
|
||||
if (typeof value === "string") return redactPathText(value, censorUsernameInLogs);
|
||||
try {
|
||||
return JSON.stringify(redactHomePathUserSegmentsInValue(value));
|
||||
return JSON.stringify(redactPathValue(value, censorUsernameInLogs));
|
||||
} catch {
|
||||
return redactHomePathUserSegments(String(value));
|
||||
return redactPathText(String(value), censorUsernameInLogs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +146,7 @@ function isMarkdown(pathValue: string) {
|
|||
return pathValue.toLowerCase().endsWith(".md");
|
||||
}
|
||||
|
||||
function formatEnvForDisplay(envValue: unknown): string {
|
||||
function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): string {
|
||||
const env = asRecord(envValue);
|
||||
if (!env) return "<unable-to-parse>";
|
||||
|
||||
|
|
@ -147,7 +155,7 @@ function formatEnvForDisplay(envValue: unknown): string {
|
|||
|
||||
return keys
|
||||
.sort()
|
||||
.map((key) => `${key}=${redactEnvValue(key, env[key])}`)
|
||||
.map((key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +346,13 @@ function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation[
|
|||
);
|
||||
}
|
||||
|
||||
function WorkspaceOperationLogViewer({ operation }: { operation: WorkspaceOperation }) {
|
||||
function WorkspaceOperationLogViewer({
|
||||
operation,
|
||||
censorUsernameInLogs,
|
||||
}: {
|
||||
operation: WorkspaceOperation;
|
||||
censorUsernameInLogs: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: logData, isLoading, error } = useQuery({
|
||||
queryKey: ["workspace-operation-log", operation.id],
|
||||
|
|
@ -391,7 +405,7 @@ function WorkspaceOperationLogViewer({ operation }: { operation: WorkspaceOperat
|
|||
>
|
||||
[{chunk.stream}]
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-all">{redactHomePathUserSegments(chunk.chunk)}</span>
|
||||
<span className="whitespace-pre-wrap break-all">{redactPathText(chunk.chunk, censorUsernameInLogs)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -402,7 +416,13 @@ function WorkspaceOperationLogViewer({ operation }: { operation: WorkspaceOperat
|
|||
);
|
||||
}
|
||||
|
||||
function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOperation[] }) {
|
||||
function WorkspaceOperationsSection({
|
||||
operations,
|
||||
censorUsernameInLogs,
|
||||
}: {
|
||||
operations: WorkspaceOperation[];
|
||||
censorUsernameInLogs: boolean;
|
||||
}) {
|
||||
if (operations.length === 0) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -467,7 +487,7 @@ function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOpera
|
|||
<div>
|
||||
<div className="mb-1 text-xs text-red-700 dark:text-red-300">stderr excerpt</div>
|
||||
<pre className="rounded-md bg-red-50 p-2 text-xs whitespace-pre-wrap break-all text-red-800 dark:bg-neutral-950 dark:text-red-100">
|
||||
{redactHomePathUserSegments(operation.stderrExcerpt)}
|
||||
{redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -475,11 +495,16 @@ function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOpera
|
|||
<div>
|
||||
<div className="mb-1 text-xs text-muted-foreground">stdout excerpt</div>
|
||||
<pre className="rounded-md bg-neutral-100 p-2 text-xs whitespace-pre-wrap break-all dark:bg-neutral-950">
|
||||
{redactHomePathUserSegments(operation.stdoutExcerpt)}
|
||||
{redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{operation.logRef && <WorkspaceOperationLogViewer operation={operation} />}
|
||||
{operation.logRef && (
|
||||
<WorkspaceOperationLogViewer
|
||||
operation={operation}
|
||||
censorUsernameInLogs={censorUsernameInLogs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1474,10 +1499,14 @@ function ConfigurationTab({
|
|||
Lets this agent create or hire agents and implicitly assign tasks.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={canCreateAgents ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={canCreateAgents}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
canCreateAgents ? "bg-green-600" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
updatePermissions.mutate({
|
||||
canCreateAgents: !canCreateAgents,
|
||||
|
|
@ -1486,8 +1515,13 @@ function ConfigurationTab({
|
|||
}
|
||||
disabled={updatePermissions.isPending}
|
||||
>
|
||||
{canCreateAgents ? "Enabled" : "Disabled"}
|
||||
</Button>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
|
||||
canCreateAgents ? "translate-x-4.5" : "translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 text-sm">
|
||||
<div className="space-y-1">
|
||||
|
|
@ -1501,10 +1535,8 @@ function ConfigurationTab({
|
|||
role="switch"
|
||||
aria-checked={canAssignTasks}
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-11 shrink-0 rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
canAssignTasks
|
||||
? "bg-green-500 focus-visible:ring-green-500/70"
|
||||
: "bg-input/50 focus-visible:ring-ring",
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
canAssignTasks ? "bg-green-600" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
updatePermissions.mutate({
|
||||
|
|
@ -1516,8 +1548,8 @@ function ConfigurationTab({
|
|||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-4 w-4 transform rounded-full bg-background transition-transform",
|
||||
canAssignTasks ? "translate-x-6" : "translate-x-1",
|
||||
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
|
||||
canAssignTasks ? "translate-x-4.5" : "translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
|
@ -3538,13 +3570,21 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
};
|
||||
}, [isLive, run.companyId, run.id, run.agentId]);
|
||||
|
||||
const censorUsernameInLogs = useQuery({
|
||||
queryKey: queryKeys.instance.generalSettings,
|
||||
queryFn: () => instanceSettingsApi.getGeneral(),
|
||||
}).data?.censorUsernameInLogs === true;
|
||||
|
||||
const adapterInvokePayload = useMemo(() => {
|
||||
const evt = events.find((e) => e.eventType === "adapter.invoke");
|
||||
return redactHomePathUserSegmentsInValue(asRecord(evt?.payload ?? null));
|
||||
}, [events]);
|
||||
return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs);
|
||||
}, [censorUsernameInLogs, events]);
|
||||
|
||||
const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
|
||||
const transcript = useMemo(() => buildTranscript(logLines, adapter.parseStdoutLine), [logLines, adapter]);
|
||||
const transcript = useMemo(
|
||||
() => buildTranscript(logLines, adapter.parseStdoutLine, { censorUsernameInLogs }),
|
||||
[adapter, censorUsernameInLogs, logLines],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTranscriptMode("nice");
|
||||
|
|
@ -3572,7 +3612,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<WorkspaceOperationsSection operations={workspaceOperations} />
|
||||
<WorkspaceOperationsSection
|
||||
operations={workspaceOperations}
|
||||
censorUsernameInLogs={censorUsernameInLogs}
|
||||
/>
|
||||
{adapterInvokePayload && (
|
||||
<div className="rounded-lg border border-border bg-background/60 p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">Invocation</div>
|
||||
|
|
@ -3614,8 +3657,8 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
<div className="text-xs text-muted-foreground mb-1">Prompt</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{typeof adapterInvokePayload.prompt === "string"
|
||||
? redactHomePathUserSegments(adapterInvokePayload.prompt)
|
||||
: JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.prompt), null, 2)}
|
||||
? redactPathText(adapterInvokePayload.prompt, censorUsernameInLogs)
|
||||
: JSON.stringify(redactPathValue(adapterInvokePayload.prompt, censorUsernameInLogs), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3623,7 +3666,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Context</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.context), null, 2)}
|
||||
{JSON.stringify(redactPathValue(adapterInvokePayload.context, censorUsernameInLogs), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3631,7 +3674,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Environment</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
|
||||
{formatEnvForDisplay(adapterInvokePayload.env)}
|
||||
{formatEnvForDisplay(adapterInvokePayload.env, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3707,14 +3750,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
{run.error && (
|
||||
<div className="text-xs text-red-600 dark:text-red-200">
|
||||
<span className="text-red-700 dark:text-red-300">Error: </span>
|
||||
{redactHomePathUserSegments(run.error)}
|
||||
{redactPathText(run.error, censorUsernameInLogs)}
|
||||
</div>
|
||||
)}
|
||||
{run.stderrExcerpt && run.stderrExcerpt.trim() && (
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stderr excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{redactHomePathUserSegments(run.stderrExcerpt)}
|
||||
{redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3722,7 +3765,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">adapter result JSON</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{JSON.stringify(redactHomePathUserSegmentsInValue(run.resultJson), null, 2)}
|
||||
{JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3730,7 +3773,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stdout excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{redactHomePathUserSegments(run.stdoutExcerpt)}
|
||||
{redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3757,9 +3800,9 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
</span>
|
||||
<span className={cn("break-all", color)}>
|
||||
{evt.message
|
||||
? redactHomePathUserSegments(evt.message)
|
||||
? redactPathText(evt.message, censorUsernameInLogs)
|
||||
: evt.payload
|
||||
? JSON.stringify(redactHomePathUserSegmentsInValue(evt.payload))
|
||||
? JSON.stringify(redactPathValue(evt.payload, censorUsernameInLogs))
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue