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:
dotta 2026-03-20 15:04:55 -05:00
commit e3c92a20f1
96 changed files with 15366 additions and 1684 deletions

View file

@ -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>