Skip to content

Commit 051310c

Browse files
dmitriyzhukclaude
andcommitted
fix(api): honor agent allowedOrigins in HTTP CORS; bundle pending platform WIP
main.ts now resolves CORS per-request: for /api/agent/:agentId/* routes it checks the request Origin against that agent's allowedOrigins (mirroring the /ws/client handshake), so the embedded Bridle widget's transcript/message HTTP calls are no longer CORS-blocked on whitelisted customer origins. The static CORS_ORIGIN allowlist still covers dashboard/admin. Also bundles in-progress changes across admin, api (agent/browser/integration/ pod/reins/usage) and the generated SDK clients. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0fb6c2b commit 051310c

31 files changed

Lines changed: 1135 additions & 136 deletions

File tree

admin/slices/agent/agent/components/agent/Provider.vue

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ const { data: usage, pending: usagePending, refresh: refreshUsage } = useAsyncDa
6868
() => usageStore.fetchForAgent(props.id),
6969
{ lazy: true },
7070
);
71+
const { data: metrics, pending: metricsPending, refresh: refreshMetrics } = useAsyncData(
72+
`admin-agent-metrics-${props.id}`,
73+
() => agentStore.fetchMetrics(props.id),
74+
{ lazy: true },
75+
);
7176
const { data: template, pending: templatePending } = useAsyncData(
7277
`admin-agent-template-${props.id}`,
7378
async () => {
@@ -160,6 +165,35 @@ function fmtUsd(n: number) {
160165
return '$' + n.toFixed(2);
161166
}
162167
168+
function fmtBytes(bytes: number): string {
169+
if (!bytes || bytes < 0) return '0 B';
170+
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
171+
let i = 0;
172+
let n = bytes;
173+
while (n >= 1024 && i < units.length - 1) {
174+
n /= 1024;
175+
i += 1;
176+
}
177+
return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
178+
}
179+
180+
function fmtCpu(milli: number): string {
181+
if (!milli || milli < 0) return '0';
182+
if (milli < 1000) return `${Math.round(milli)}m`;
183+
return `${(milli / 1000).toFixed(2)} CPU`;
184+
}
185+
186+
function pct(used: number, total: number): number {
187+
if (!total) return 0;
188+
return Math.min(100, Math.max(0, Math.round((used / total) * 100)));
189+
}
190+
191+
function pctClass(p: number): string {
192+
if (p >= 90) return 'bg-destructive';
193+
if (p >= 70) return 'bg-amber-500';
194+
return 'bg-primary';
195+
}
196+
163197
const restarting = ref(false);
164198
const restartError = ref<string | null>(null);
165199
@@ -437,9 +471,37 @@ watch(activeTab, (tab) => {
437471
if (tab === 'overview') {
438472
refresh();
439473
refreshUsage();
474+
refreshMetrics();
440475
}
441476
});
442477
478+
// Metrics polling while the Overview tab is active. metrics-server samples on
479+
// a ~15s window — 10s polling gives the user near-live numbers without
480+
// hammering the API.
481+
let metricsTimer: ReturnType<typeof setInterval> | null = null;
482+
function stopMetricsPolling() {
483+
if (metricsTimer) {
484+
clearInterval(metricsTimer);
485+
metricsTimer = null;
486+
}
487+
}
488+
function startMetricsPolling() {
489+
stopMetricsPolling();
490+
metricsTimer = setInterval(() => refreshMetrics(), 10_000);
491+
}
492+
watch(
493+
activeTab,
494+
(tab) => {
495+
if (tab === 'overview') {
496+
startMetricsPolling();
497+
} else {
498+
stopMetricsPolling();
499+
}
500+
},
501+
{ immediate: true },
502+
);
503+
onBeforeUnmount(stopMetricsPolling);
504+
443505
</script>
444506

445507
<template>
@@ -865,6 +927,117 @@ watch(activeTab, (tab) => {
865927
</CardContent>
866928
</Card>
867929

930+
<Card>
931+
<CardHeader>
932+
<div class="flex items-start justify-between gap-3">
933+
<div>
934+
<CardTitle>Resource usage</CardTitle>
935+
<CardDescription>
936+
Live pod CPU / memory (metrics-server) and free disk on the
937+
K8s node hosting the pod. Refreshes every 10 seconds.
938+
</CardDescription>
939+
</div>
940+
<Button
941+
variant="outline"
942+
size="sm"
943+
:disabled="metricsPending"
944+
@click="refreshMetrics"
945+
>
946+
<IconRefresh
947+
class="size-4"
948+
:class="{ 'animate-spin': metricsPending }"
949+
/>
950+
</Button>
951+
</div>
952+
</CardHeader>
953+
<CardContent>
954+
<div v-if="metricsPending && !metrics" class="space-y-4">
955+
<Skeleton v-for="i in 3" :key="i" class="h-12 w-full" />
956+
</div>
957+
<div
958+
v-else-if="!metrics"
959+
class="text-sm text-muted-foreground"
960+
>
961+
No metrics available. Either no pod is running yet, or
962+
<code>metrics-server</code> is not installed in the cluster.
963+
</div>
964+
<div v-else class="space-y-5">
965+
<div>
966+
<div class="mb-1 flex items-baseline justify-between gap-2 text-sm">
967+
<span class="font-medium">CPU</span>
968+
<span class="font-mono text-xs text-muted-foreground">
969+
{{ fmtCpu(metrics.pod.cpuMilli) }} /
970+
{{ metrics.pod.cpuLimitMilli ? fmtCpu(metrics.pod.cpuLimitMilli) : '—' }}
971+
<span class="ml-1">({{ pct(metrics.pod.cpuMilli, metrics.pod.cpuLimitMilli) }}%)</span>
972+
</span>
973+
</div>
974+
<div class="h-2 w-full overflow-hidden rounded bg-muted">
975+
<div
976+
class="h-full transition-all"
977+
:class="pctClass(pct(metrics.pod.cpuMilli, metrics.pod.cpuLimitMilli))"
978+
:style="{ width: pct(metrics.pod.cpuMilli, metrics.pod.cpuLimitMilli) + '%' }"
979+
/>
980+
</div>
981+
</div>
982+
983+
<div>
984+
<div class="mb-1 flex items-baseline justify-between gap-2 text-sm">
985+
<span class="font-medium">Memory</span>
986+
<span class="font-mono text-xs text-muted-foreground">
987+
{{ fmtBytes(metrics.pod.memBytes) }} /
988+
{{ metrics.pod.memLimitBytes ? fmtBytes(metrics.pod.memLimitBytes) : '—' }}
989+
<span class="ml-1">({{ pct(metrics.pod.memBytes, metrics.pod.memLimitBytes) }}%)</span>
990+
</span>
991+
</div>
992+
<div class="h-2 w-full overflow-hidden rounded bg-muted">
993+
<div
994+
class="h-full transition-all"
995+
:class="pctClass(pct(metrics.pod.memBytes, metrics.pod.memLimitBytes))"
996+
:style="{ width: pct(metrics.pod.memBytes, metrics.pod.memLimitBytes) + '%' }"
997+
/>
998+
</div>
999+
</div>
1000+
1001+
<div>
1002+
<div class="mb-1 flex items-baseline justify-between gap-2 text-sm">
1003+
<span class="font-medium">
1004+
Node disk
1005+
<span class="ml-1 font-mono text-xs text-muted-foreground">
1006+
{{ metrics.node.name }}
1007+
</span>
1008+
</span>
1009+
<span class="font-mono text-xs text-muted-foreground">
1010+
{{ fmtBytes(metrics.node.diskCapacityBytes - metrics.node.diskAvailBytes) }} /
1011+
{{ fmtBytes(metrics.node.diskCapacityBytes) }}
1012+
<span class="ml-1">
1013+
({{ pct(
1014+
metrics.node.diskCapacityBytes - metrics.node.diskAvailBytes,
1015+
metrics.node.diskCapacityBytes
1016+
) }}%)
1017+
</span>
1018+
</span>
1019+
</div>
1020+
<div class="h-2 w-full overflow-hidden rounded bg-muted">
1021+
<div
1022+
class="h-full transition-all"
1023+
:class="pctClass(pct(
1024+
metrics.node.diskCapacityBytes - metrics.node.diskAvailBytes,
1025+
metrics.node.diskCapacityBytes
1026+
))"
1027+
:style="{ width: pct(
1028+
metrics.node.diskCapacityBytes - metrics.node.diskAvailBytes,
1029+
metrics.node.diskCapacityBytes
1030+
) + '%' }"
1031+
/>
1032+
</div>
1033+
<p class="mt-1 text-xs text-muted-foreground">
1034+
{{ fmtBytes(metrics.node.diskAvailBytes) }} free
1035+
</p>
1036+
</div>
1037+
</div>
1038+
</CardContent>
1039+
</Card>
1040+
8681041
<Card>
8691042
<CardHeader>
8701043
<CardTitle>Runtime</CardTitle>

admin/slices/agent/agent/stores/agent.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ export interface IAgentResources {
5353
memory: string;
5454
}
5555

56+
export interface IAgentMetrics {
57+
pod: {
58+
cpuMilli: number;
59+
memBytes: number;
60+
cpuLimitMilli: number;
61+
memLimitBytes: number;
62+
};
63+
node: {
64+
name: string;
65+
diskAvailBytes: number;
66+
diskCapacityBytes: number;
67+
};
68+
}
69+
5670
export interface IAgentData {
5771
id: string;
5872
name: string;
@@ -348,6 +362,12 @@ export const useAgentStore = defineStore('agent', () => {
348362
return env?.data ?? [];
349363
}
350364

365+
async function fetchMetrics(id: string): Promise<IAgentMetrics | null> {
366+
const res = await client.instance.get(`/agents/${id}/metrics`);
367+
const env = res.data as ApiEnvelope<IAgentMetrics | null> | undefined;
368+
return env?.data ?? null;
369+
}
370+
351371
return {
352372
agents,
353373
fetchAll,
@@ -361,6 +381,7 @@ export const useAgentStore = defineStore('agent', () => {
361381
demoteAdmin,
362382
fetchLogs,
363383
fetchEnv,
384+
fetchMetrics,
364385
isPendingRestart,
365386
markPendingRestart,
366387
clearPendingRestart,

admin/slices/agent/template/components/template/Form.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ const form = reactive<{
3535
}>({
3636
name: props.initialValues?.name ?? '',
3737
description: props.initialValues?.description ?? '',
38-
image: props.initialValues?.image ?? '',
38+
image: props.initialValues?.image ?? 'ghcr.io/cleanslice/runtime:latest',
3939
defaultResources: {
40-
cpu: props.initialValues?.defaultResources?.cpu ?? '500m',
41-
memory: props.initialValues?.defaultResources?.memory ?? '512Mi',
40+
cpu: props.initialValues?.defaultResources?.cpu ?? '2000m',
41+
memory: props.initialValues?.defaultResources?.memory ?? '2Gi',
4242
},
4343
defaultKnowledgeIds: [...(props.initialValues?.defaultKnowledgeIds ?? [])],
4444
});
@@ -70,8 +70,8 @@ function onSubmit(): void {
7070
description: form.description.trim(),
7171
image: form.image.trim(),
7272
defaultResources: {
73-
cpu: form.defaultResources.cpu.trim() || '500m',
74-
memory: form.defaultResources.memory.trim() || '512Mi',
73+
cpu: form.defaultResources.cpu.trim() || '2000m',
74+
memory: form.defaultResources.memory.trim() || '2Gi',
7575
},
7676
defaultKnowledgeIds: [...form.defaultKnowledgeIds],
7777
});

admin/slices/setup/api/data/repositories/api/schemas.gen.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,36 @@ export const CreateSourceDtoSchema = {
572572
required: ["type", "name"],
573573
} as const;
574574

575+
export const AddFromSitemapDtoSchema = {
576+
type: "object",
577+
properties: {
578+
sitemapUrl: {
579+
type: "string",
580+
example: "https://developer.paypal.com/sitemap.xml",
581+
},
582+
urlPrefix: {
583+
type: "string",
584+
example: "https://developer.paypal.com/docs/checkout/",
585+
},
586+
},
587+
required: ["sitemapUrl"],
588+
} as const;
589+
590+
export const AddFromSitemapResultDtoSchema = {
591+
type: "object",
592+
properties: {
593+
added: {
594+
type: "number",
595+
example: 47,
596+
},
597+
discovered: {
598+
type: "number",
599+
example: 51,
600+
},
601+
},
602+
required: ["added", "discovered"],
603+
} as const;
604+
575605
export const AgentPodStatusDtoSchema = {
576606
type: "object",
577607
properties: {
@@ -655,6 +685,65 @@ export const AgentStatusDtoSchema = {
655685
required: ["agent", "pod"],
656686
} as const;
657687

688+
export const AgentPodMetricsDtoSchema = {
689+
type: "object",
690+
properties: {
691+
cpuMilli: {
692+
type: "number",
693+
example: 234,
694+
description: "Current CPU usage in millicores",
695+
},
696+
memBytes: {
697+
type: "number",
698+
example: 471859200,
699+
description: "Current memory usage in bytes",
700+
},
701+
cpuLimitMilli: {
702+
type: "number",
703+
example: 2000,
704+
description: "CPU limit in millicores",
705+
},
706+
memLimitBytes: {
707+
type: "number",
708+
example: 2147483648,
709+
description: "Memory limit in bytes",
710+
},
711+
},
712+
required: ["cpuMilli", "memBytes", "cpuLimitMilli", "memLimitBytes"],
713+
} as const;
714+
715+
export const AgentNodeMetricsDtoSchema = {
716+
type: "object",
717+
properties: {
718+
name: {
719+
type: "string",
720+
example: "k3s-agent-gnk",
721+
},
722+
diskAvailBytes: {
723+
type: "number",
724+
example: 157109764096,
725+
},
726+
diskCapacityBytes: {
727+
type: "number",
728+
example: 163817959424,
729+
},
730+
},
731+
required: ["name", "diskAvailBytes", "diskCapacityBytes"],
732+
} as const;
733+
734+
export const AgentMetricsDtoSchema = {
735+
type: "object",
736+
properties: {
737+
pod: {
738+
$ref: "#/components/schemas/AgentPodMetricsDto",
739+
},
740+
node: {
741+
$ref: "#/components/schemas/AgentNodeMetricsDto",
742+
},
743+
},
744+
required: ["pod", "node"],
745+
} as const;
746+
658747
export const AgentEnvVarDtoSchema = {
659748
type: "object",
660749
properties: {
@@ -1053,6 +1142,11 @@ export const ImportSkillUrlDtoSchema = {
10531142
name: {
10541143
type: "string",
10551144
},
1145+
overwrite: {
1146+
type: "boolean",
1147+
description:
1148+
"If true and a skill with the same slug already exists, fully replace it instead of returning 409.",
1149+
},
10561150
},
10571151
required: ["url"],
10581152
} as const;
@@ -1075,6 +1169,11 @@ export const ImportSkillDtoSchema = {
10751169
description:
10761170
"Override the auto-derived slug. Lowercase letters, digits and dashes.",
10771171
},
1172+
overwrite: {
1173+
type: "boolean",
1174+
description:
1175+
"If true and a skill with the same slug already exists, fully replace it instead of returning 409.",
1176+
},
10781177
},
10791178
required: ["repo", "path"],
10801179
} as const;

0 commit comments

Comments
 (0)