@cat-factory/app 0.48.0 → 0.48.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/settings/KubernetesRunnerForm.vue +205 -0
- package/app/components/settings/ProviderConnectionTab.vue +76 -1
- package/app/composables/api/providerConnections.ts +24 -5
- package/app/composables/useWorkspaceStream.ts +5 -0
- package/app/stores/agentRuns.ts +48 -2
- package/app/stores/workspace.ts +1 -0
- package/app/types/domain.ts +1 -0
- package/app/types/envConfigRepair.ts +15 -0
- package/app/types/providerConnections.ts +20 -4
- package/i18n/locales/en.json +22 -2
- package/i18n/locales/es.json +20 -0
- package/i18n/locales/fr.json +20 -0
- package/i18n/locales/pl.json +20 -0
- package/i18n/locales/uk.json +20 -0
- package/package.json +2 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Kubernetes "agent runner backend" connect form — one option of the runner-pool tab's
|
|
3
|
+
// backend-type selector (the other being the manifest pool). It builds the discriminated
|
|
4
|
+
// `{ kind: 'kubernetes', kubernetes }` config + the `apiToken` secret bundle and emits
|
|
5
|
+
// test/save to the parent tab (which calls the shared provider-connections store).
|
|
6
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
7
|
+
import { KUBERNETES_RUNNER_TOKEN_SECRET_KEY } from '@cat-factory/contracts'
|
|
8
|
+
import type { ProviderConnection } from '~/types/providerConnections'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
connection: ProviderConnection | null
|
|
12
|
+
supportsTest: boolean
|
|
13
|
+
testing: boolean
|
|
14
|
+
busy: boolean
|
|
15
|
+
testResult: { ok: boolean; message?: string } | null
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
test: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
|
|
20
|
+
save: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n()
|
|
24
|
+
|
|
25
|
+
const form = reactive({
|
|
26
|
+
label: '',
|
|
27
|
+
apiServerUrl: '',
|
|
28
|
+
namespace: '',
|
|
29
|
+
image: '',
|
|
30
|
+
imageUi: '',
|
|
31
|
+
caCertPem: '',
|
|
32
|
+
harnessPort: '',
|
|
33
|
+
})
|
|
34
|
+
const apiToken = ref('')
|
|
35
|
+
|
|
36
|
+
// A registered k8s connection exposes its non-secret config, so prefill every non-secret
|
|
37
|
+
// field from it (never the token — secrets are write-only and re-entered on update). This
|
|
38
|
+
// lets an edit change one field without re-typing the whole form.
|
|
39
|
+
watch(
|
|
40
|
+
() => props.connection,
|
|
41
|
+
(c) => {
|
|
42
|
+
if (c?.kind !== 'kubernetes') return
|
|
43
|
+
form.label = c.label
|
|
44
|
+
form.apiServerUrl = c.baseUrl
|
|
45
|
+
const k =
|
|
46
|
+
c.config && (c.config as { kind?: string }).kind === 'kubernetes'
|
|
47
|
+
? (c.config as { kubernetes: Record<string, unknown> }).kubernetes
|
|
48
|
+
: undefined
|
|
49
|
+
if (k) {
|
|
50
|
+
form.namespace = typeof k.namespace === 'string' ? k.namespace : ''
|
|
51
|
+
form.image = typeof k.image === 'string' ? k.image : ''
|
|
52
|
+
form.imageUi = typeof k.imageUi === 'string' ? k.imageUi : ''
|
|
53
|
+
form.caCertPem = typeof k.caCertPem === 'string' ? k.caCertPem : ''
|
|
54
|
+
form.harnessPort = typeof k.harnessPort === 'number' ? String(k.harnessPort) : ''
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{ immediate: true },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const canSave = computed(
|
|
61
|
+
() =>
|
|
62
|
+
!!form.label.trim() &&
|
|
63
|
+
!!form.apiServerUrl.trim() &&
|
|
64
|
+
!!form.namespace.trim() &&
|
|
65
|
+
!!form.image.trim() &&
|
|
66
|
+
!!apiToken.value.trim(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
function buildPayload(): { config: Record<string, unknown>; secrets: Record<string, string> } {
|
|
70
|
+
const kubernetes: Record<string, unknown> = {
|
|
71
|
+
label: form.label.trim(),
|
|
72
|
+
apiServerUrl: form.apiServerUrl.trim(),
|
|
73
|
+
namespace: form.namespace.trim(),
|
|
74
|
+
image: form.image.trim(),
|
|
75
|
+
}
|
|
76
|
+
if (form.imageUi.trim()) kubernetes.imageUi = form.imageUi.trim()
|
|
77
|
+
if (form.caCertPem.trim()) kubernetes.caCertPem = form.caCertPem.trim()
|
|
78
|
+
const port = Number(form.harnessPort)
|
|
79
|
+
if (form.harnessPort.trim() && Number.isFinite(port)) kubernetes.harnessPort = port
|
|
80
|
+
return {
|
|
81
|
+
config: { kind: 'kubernetes', kubernetes },
|
|
82
|
+
secrets: { [KUBERNETES_RUNNER_TOKEN_SECRET_KEY]: apiToken.value.trim() },
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<div class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3">
|
|
89
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
90
|
+
{{
|
|
91
|
+
connection?.kind === 'kubernetes'
|
|
92
|
+
? t('settings.providerConnection.form.updateConfiguration')
|
|
93
|
+
: t('settings.providerConnection.form.connect')
|
|
94
|
+
}}
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
<UFormField :label="t('settings.providerConnection.kubernetes.label')">
|
|
98
|
+
<UInput
|
|
99
|
+
v-model="form.label"
|
|
100
|
+
:placeholder="t('settings.providerConnection.kubernetes.labelPlaceholder')"
|
|
101
|
+
/>
|
|
102
|
+
</UFormField>
|
|
103
|
+
|
|
104
|
+
<UFormField
|
|
105
|
+
:label="t('settings.providerConnection.kubernetes.apiServerUrl')"
|
|
106
|
+
:help="t('settings.providerConnection.kubernetes.apiServerUrlHelp')"
|
|
107
|
+
>
|
|
108
|
+
<UInput v-model="form.apiServerUrl" class="font-mono" placeholder="https://10.0.0.1:6443" />
|
|
109
|
+
</UFormField>
|
|
110
|
+
|
|
111
|
+
<UFormField :label="t('settings.providerConnection.kubernetes.namespace')">
|
|
112
|
+
<UInput v-model="form.namespace" class="font-mono" placeholder="cat-factory" />
|
|
113
|
+
</UFormField>
|
|
114
|
+
|
|
115
|
+
<UFormField
|
|
116
|
+
:label="t('settings.providerConnection.kubernetes.image')"
|
|
117
|
+
:help="t('settings.providerConnection.kubernetes.imageHelp')"
|
|
118
|
+
>
|
|
119
|
+
<UInput
|
|
120
|
+
v-model="form.image"
|
|
121
|
+
class="font-mono"
|
|
122
|
+
placeholder="ghcr.io/acme/cat-factory-executor:latest"
|
|
123
|
+
/>
|
|
124
|
+
</UFormField>
|
|
125
|
+
|
|
126
|
+
<UFormField
|
|
127
|
+
:label="
|
|
128
|
+
t('settings.providerConnection.form.optionalLabel', {
|
|
129
|
+
label: t('settings.providerConnection.kubernetes.imageUi'),
|
|
130
|
+
})
|
|
131
|
+
"
|
|
132
|
+
>
|
|
133
|
+
<UInput v-model="form.imageUi" class="font-mono" />
|
|
134
|
+
</UFormField>
|
|
135
|
+
|
|
136
|
+
<UFormField
|
|
137
|
+
:label="t('settings.providerConnection.kubernetes.apiToken')"
|
|
138
|
+
:help="t('settings.providerConnection.kubernetes.apiTokenHelp')"
|
|
139
|
+
>
|
|
140
|
+
<UInput v-model="apiToken" type="password" class="font-mono" />
|
|
141
|
+
</UFormField>
|
|
142
|
+
|
|
143
|
+
<UFormField
|
|
144
|
+
:label="
|
|
145
|
+
t('settings.providerConnection.form.optionalLabel', {
|
|
146
|
+
label: t('settings.providerConnection.kubernetes.caCertPem'),
|
|
147
|
+
})
|
|
148
|
+
"
|
|
149
|
+
:help="t('settings.providerConnection.kubernetes.caCertPemHelp')"
|
|
150
|
+
>
|
|
151
|
+
<UTextarea
|
|
152
|
+
v-model="form.caCertPem"
|
|
153
|
+
:rows="3"
|
|
154
|
+
class="font-mono"
|
|
155
|
+
placeholder="-----BEGIN CERTIFICATE-----"
|
|
156
|
+
/>
|
|
157
|
+
</UFormField>
|
|
158
|
+
|
|
159
|
+
<UFormField
|
|
160
|
+
:label="
|
|
161
|
+
t('settings.providerConnection.form.optionalLabel', {
|
|
162
|
+
label: t('settings.providerConnection.kubernetes.harnessPort'),
|
|
163
|
+
})
|
|
164
|
+
"
|
|
165
|
+
>
|
|
166
|
+
<UInput v-model="form.harnessPort" type="number" class="font-mono" placeholder="8080" />
|
|
167
|
+
</UFormField>
|
|
168
|
+
|
|
169
|
+
<div v-if="supportsTest" class="flex items-center gap-2">
|
|
170
|
+
<UButton
|
|
171
|
+
color="neutral"
|
|
172
|
+
variant="soft"
|
|
173
|
+
size="sm"
|
|
174
|
+
icon="i-lucide-plug-zap"
|
|
175
|
+
:loading="testing"
|
|
176
|
+
:disabled="!canSave"
|
|
177
|
+
@click="emit('test', buildPayload())"
|
|
178
|
+
>
|
|
179
|
+
{{ t('settings.providerConnection.test.button') }}
|
|
180
|
+
</UButton>
|
|
181
|
+
<span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
|
|
182
|
+
{{ testResult.message ?? t('settings.providerConnection.test.ok') }}
|
|
183
|
+
</span>
|
|
184
|
+
<span v-else-if="testResult" class="text-xs text-rose-400">
|
|
185
|
+
{{ testResult.message ?? t('settings.providerConnection.test.failed') }}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="flex justify-end">
|
|
190
|
+
<UButton
|
|
191
|
+
color="primary"
|
|
192
|
+
size="sm"
|
|
193
|
+
:loading="busy"
|
|
194
|
+
:disabled="!canSave"
|
|
195
|
+
@click="emit('save', buildPayload())"
|
|
196
|
+
>
|
|
197
|
+
{{
|
|
198
|
+
connection?.kind === 'kubernetes'
|
|
199
|
+
? t('common.save')
|
|
200
|
+
: t('settings.providerConnection.form.connect')
|
|
201
|
+
}}
|
|
202
|
+
</UButton>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</template>
|
|
@@ -10,6 +10,7 @@ import { computed, ref, toRaw, watch } from 'vue'
|
|
|
10
10
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
11
11
|
import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
|
|
12
12
|
import ProviderManifestEditor from '~/components/settings/ProviderManifestEditor.vue'
|
|
13
|
+
import KubernetesRunnerForm from '~/components/settings/KubernetesRunnerForm.vue'
|
|
13
14
|
|
|
14
15
|
const props = defineProps<{ kind: ProviderConnectionKind }>()
|
|
15
16
|
|
|
@@ -184,6 +185,57 @@ async function saveManifest(payload: {
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// --- Runner-backend selector (runner-pool only) -------------------------------------
|
|
189
|
+
// The runner-pool tab can configure either the manifest pool OR a native Kubernetes
|
|
190
|
+
// cluster; environments are manifest-only. Defaults to the saved connection's kind.
|
|
191
|
+
const RUNNER_BACKEND_KINDS = ['manifest', 'kubernetes'] as const
|
|
192
|
+
type RunnerBackendKind = (typeof RUNNER_BACKEND_KINDS)[number]
|
|
193
|
+
const backendKind = ref<RunnerBackendKind>('manifest')
|
|
194
|
+
const showBackendSelector = computed(() => props.kind === 'runner-pool')
|
|
195
|
+
const backendKindItems = computed(() =>
|
|
196
|
+
RUNNER_BACKEND_KINDS.map((k) => ({
|
|
197
|
+
label: t(`settings.providerConnection.backend.${k}`),
|
|
198
|
+
value: k,
|
|
199
|
+
})),
|
|
200
|
+
)
|
|
201
|
+
watch(
|
|
202
|
+
() => connection.value,
|
|
203
|
+
(c) => {
|
|
204
|
+
if (c?.kind === 'kubernetes' || c?.kind === 'manifest') backendKind.value = c.kind
|
|
205
|
+
},
|
|
206
|
+
{ immediate: true },
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async function testConfig(payload: {
|
|
210
|
+
config: Record<string, unknown>
|
|
211
|
+
secrets: Record<string, string>
|
|
212
|
+
}) {
|
|
213
|
+
testing.value = true
|
|
214
|
+
testResult.value = null
|
|
215
|
+
try {
|
|
216
|
+
testResult.value = await store.test(props.kind, payload)
|
|
217
|
+
} catch (e) {
|
|
218
|
+
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
219
|
+
} finally {
|
|
220
|
+
testing.value = false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function saveConfig(payload: {
|
|
225
|
+
config: Record<string, unknown>
|
|
226
|
+
secrets: Record<string, string>
|
|
227
|
+
}) {
|
|
228
|
+
busy.value = true
|
|
229
|
+
try {
|
|
230
|
+
await store.register(props.kind, payload)
|
|
231
|
+
toastSaved()
|
|
232
|
+
} catch (e) {
|
|
233
|
+
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
234
|
+
} finally {
|
|
235
|
+
busy.value = false
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
187
239
|
async function remove() {
|
|
188
240
|
busy.value = true
|
|
189
241
|
try {
|
|
@@ -265,8 +317,31 @@ function fieldHelp(key: string): string | undefined {
|
|
|
265
317
|
}}
|
|
266
318
|
</div>
|
|
267
319
|
|
|
320
|
+
<!-- Runner-backend selector: the manifest pool or a native Kubernetes cluster. -->
|
|
321
|
+
<UFormField
|
|
322
|
+
v-if="showBackendSelector"
|
|
323
|
+
:label="t('settings.providerConnection.backend.selectorLabel')"
|
|
324
|
+
>
|
|
325
|
+
<USelect v-model="backendKind" :items="backendKindItems" />
|
|
326
|
+
</UFormField>
|
|
327
|
+
|
|
328
|
+
<!-- Native Kubernetes runner backend. -->
|
|
329
|
+
<KubernetesRunnerForm
|
|
330
|
+
v-if="showBackendSelector && backendKind === 'kubernetes'"
|
|
331
|
+
:connection="connection"
|
|
332
|
+
:supports-test="descriptor.supportsTest"
|
|
333
|
+
:testing="testing"
|
|
334
|
+
:busy="busy"
|
|
335
|
+
:test-result="testResult"
|
|
336
|
+
@test="testConfig"
|
|
337
|
+
@save="saveConfig"
|
|
338
|
+
/>
|
|
339
|
+
|
|
268
340
|
<!-- NATIVE provider: the friendly, descriptor-driven flat field form. -->
|
|
269
|
-
<div
|
|
341
|
+
<div
|
|
342
|
+
v-else-if="isNative"
|
|
343
|
+
class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3"
|
|
344
|
+
>
|
|
270
345
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
271
346
|
{{
|
|
272
347
|
connection
|
|
@@ -58,9 +58,10 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
|
58
58
|
send(CONTRACTS[kind].get, { pathPrefix: ws(workspaceId) }),
|
|
59
59
|
|
|
60
60
|
// The connect form builds the manifest dynamically from a server-provided scaffold, so
|
|
61
|
-
// the FE input keeps `manifest` opaque (`Record<string, unknown>`); narrow on
|
|
62
|
-
// and cast to the matching per-kind contract input at this single boundary (the
|
|
63
|
-
// re-validates
|
|
61
|
+
// the FE input keeps `manifest`/`config` opaque (`Record<string, unknown>`); narrow on
|
|
62
|
+
// the kind and cast to the matching per-kind contract input at this single boundary (the
|
|
63
|
+
// backend re-validates against the precise contract on receipt). The runner-pool provider
|
|
64
|
+
// takes a discriminated `config`; a bare `manifest` is wrapped into the manifest backend.
|
|
64
65
|
registerProviderConnection: (
|
|
65
66
|
workspaceId: string,
|
|
66
67
|
kind: ProviderConnectionKind,
|
|
@@ -73,7 +74,10 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
|
73
74
|
})
|
|
74
75
|
: send(CONTRACTS['runner-pool'].register, {
|
|
75
76
|
pathPrefix: ws(workspaceId),
|
|
76
|
-
body:
|
|
77
|
+
body: {
|
|
78
|
+
config: runnerBackendConfig(body),
|
|
79
|
+
secrets: body.secrets,
|
|
80
|
+
} as RegisterRunnerPoolInput,
|
|
77
81
|
}),
|
|
78
82
|
|
|
79
83
|
updateProviderSecrets: (
|
|
@@ -94,10 +98,25 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
|
94
98
|
})
|
|
95
99
|
: send(CONTRACTS['runner-pool'].test, {
|
|
96
100
|
pathPrefix: ws(workspaceId),
|
|
97
|
-
body:
|
|
101
|
+
body: {
|
|
102
|
+
...(body.manifest || body.config ? { config: runnerBackendConfig(body) } : {}),
|
|
103
|
+
...(body.secrets ? { secrets: body.secrets } : {}),
|
|
104
|
+
} as TestRunnerPoolConnectionInput,
|
|
98
105
|
}),
|
|
99
106
|
|
|
100
107
|
deleteProviderConnection: (workspaceId: string, kind: ProviderConnectionKind) =>
|
|
101
108
|
send(CONTRACTS[kind].unregister, { pathPrefix: ws(workspaceId) }),
|
|
102
109
|
}
|
|
103
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the discriminated runner-backend config from a connect-form payload: an
|
|
114
|
+
* explicit `config` (the Kubernetes form) wins; otherwise a bare `manifest` (the manifest
|
|
115
|
+
* editor) is wrapped into the manifest backend kind.
|
|
116
|
+
*/
|
|
117
|
+
function runnerBackendConfig(
|
|
118
|
+
body: RegisterProviderInput | TestProviderInput,
|
|
119
|
+
): Record<string, unknown> {
|
|
120
|
+
if (body.config) return body.config
|
|
121
|
+
return { kind: 'manifest', manifest: body.manifest ?? {} }
|
|
122
|
+
}
|
|
@@ -64,6 +64,11 @@ export function useWorkspaceStream() {
|
|
|
64
64
|
// or a failed badge) without a full refresh.
|
|
65
65
|
agentRuns.upsertBootstrap(event.job)
|
|
66
66
|
if (event.block) board.upsert(event.block)
|
|
67
|
+
} else if (event.type === 'env-config-repair') {
|
|
68
|
+
// A provider config-repair run advanced — patch its live status/subtasks/outcome so
|
|
69
|
+
// the infrastructure-providers window's "repairing…" indicator updates in place
|
|
70
|
+
// (then flips to ok / residual issues / a failure) without a refetch. No board block.
|
|
71
|
+
agentRuns.upsertEnvConfigRepair(event.job)
|
|
67
72
|
} else if (event.type === 'notification') {
|
|
68
73
|
// A PR needs a merge decision, a pipeline finished, or CI gave up — patch the
|
|
69
74
|
// inbox + per-block badge in place (resolved ones drop out of the inbox).
|
package/app/stores/agentRuns.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AgentFailure,
|
|
5
|
+
AgentRunKind,
|
|
6
|
+
BootstrapJob,
|
|
7
|
+
EnvConfigRepairJob,
|
|
8
|
+
StepSubtasks,
|
|
9
|
+
} from '~/types/domain'
|
|
4
10
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
11
|
import { useExecutionStore } from '~/stores/execution'
|
|
6
12
|
|
|
@@ -45,11 +51,40 @@ export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
|
45
51
|
/** Bootstrap runs for this workspace, newest-first. */
|
|
46
52
|
const bootstrapJobs = ref<BootstrapJob[]>([])
|
|
47
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Env-config-repair runs for this workspace, newest-first. These have NO board block —
|
|
56
|
+
* they're surfaced only on the infrastructure-providers window (looked up by the
|
|
57
|
+
* `repairJobId` the `bootstrapRepo` response returned), so they're held separately and
|
|
58
|
+
* NOT merged into {@link byBlock}.
|
|
59
|
+
*/
|
|
60
|
+
const envConfigRepairJobs = ref<EnvConfigRepairJob[]>([])
|
|
61
|
+
|
|
48
62
|
/** Replace the cached bootstrap runs with a server snapshot. */
|
|
49
63
|
function hydrate(jobs: BootstrapJob[]) {
|
|
50
64
|
bootstrapJobs.value = [...jobs].sort((a, b) => b.createdAt - a.createdAt)
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
/** Replace the cached env-config-repair runs with a server snapshot. */
|
|
68
|
+
function hydrateEnvConfigRepair(jobs: EnvConfigRepairJob[]) {
|
|
69
|
+
envConfigRepairJobs.value = [...jobs].sort((a, b) => b.createdAt - a.createdAt)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Patch an env-config-repair run from a real-time `env-config-repair` event (or after
|
|
74
|
+
* launching one): replace it in place by id, else prepend it. Keeps the infra window's
|
|
75
|
+
* "repairing…" indicator reactive to live progress / outcome without a refetch.
|
|
76
|
+
*/
|
|
77
|
+
function upsertEnvConfigRepair(job: EnvConfigRepairJob) {
|
|
78
|
+
const i = envConfigRepairJobs.value.findIndex((j) => j.id === job.id)
|
|
79
|
+
if (i >= 0) envConfigRepairJobs.value[i] = job
|
|
80
|
+
else envConfigRepairJobs.value.unshift(job)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Look up a single env-config-repair run by id (the infra window tracks one by `repairJobId`). */
|
|
84
|
+
function envConfigRepairById(id: string): EnvConfigRepairJob | undefined {
|
|
85
|
+
return envConfigRepairJobs.value.find((j) => j.id === id)
|
|
86
|
+
}
|
|
87
|
+
|
|
53
88
|
/**
|
|
54
89
|
* Patch a bootstrap run from a real-time `bootstrap` event (or after launching
|
|
55
90
|
* one): replace it in place by id, else prepend it. Keeps the service card
|
|
@@ -126,5 +161,16 @@ export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
|
126
161
|
return kind
|
|
127
162
|
}
|
|
128
163
|
|
|
129
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
bootstrapJobs,
|
|
166
|
+
hydrate,
|
|
167
|
+
upsertBootstrap,
|
|
168
|
+
envConfigRepairJobs,
|
|
169
|
+
hydrateEnvConfigRepair,
|
|
170
|
+
upsertEnvConfigRepair,
|
|
171
|
+
envConfigRepairById,
|
|
172
|
+
byBlock,
|
|
173
|
+
retry,
|
|
174
|
+
stop,
|
|
175
|
+
}
|
|
130
176
|
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -85,6 +85,7 @@ export const useWorkspaceStore = defineStore(
|
|
|
85
85
|
usePipelinesStore().hydrate(snapshot.pipelines, snapshot.pipelineCatalogVersions)
|
|
86
86
|
useExecutionStore().hydrate(snapshot.executions)
|
|
87
87
|
useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [])
|
|
88
|
+
useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
|
|
88
89
|
useNotificationsStore().hydrate(snapshot.notifications ?? [])
|
|
89
90
|
useMergePresetsStore().hydrate(snapshot.mergePresets ?? [])
|
|
90
91
|
useWorkspaceSettingsStore().hydrate(snapshot.settings)
|
package/app/types/domain.ts
CHANGED
|
@@ -105,6 +105,7 @@ export type * from './fragments'
|
|
|
105
105
|
export type * from './documents'
|
|
106
106
|
export type * from './tasks'
|
|
107
107
|
export type * from './bootstrap'
|
|
108
|
+
export type * from './envConfigRepair'
|
|
108
109
|
export type * from './github'
|
|
109
110
|
export type * from './accounts'
|
|
110
111
|
export type * from './notifications'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Environment-provider config-repair domain types. Mirrors the
|
|
3
|
+
// `@cat-factory/contracts` env-config-repair schemas so backend payloads drop
|
|
4
|
+
// straight into the Pinia store.
|
|
5
|
+
//
|
|
6
|
+
// A config-repair run is the durable agent fallback dispatched when mechanical
|
|
7
|
+
// provider-config bootstrap can't produce a valid config: a coding agent fixes the
|
|
8
|
+
// provider's config file in an existing repo and pushes the fix back, then the
|
|
9
|
+
// backend re-validates. It has no board block — it's surfaced only on the
|
|
10
|
+
// infrastructure-providers window that triggered it.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
//
|
|
13
|
+
// All wire shapes are sourced from @cat-factory/contracts (single source of truth).
|
|
14
|
+
|
|
15
|
+
export type { EnvConfigRepairStatus, EnvConfigRepairJob } from '@cat-factory/contracts'
|
|
@@ -19,12 +19,20 @@ export type ProviderConnectionKind = 'environment' | 'runner-pool'
|
|
|
19
19
|
|
|
20
20
|
/** A workspace's provider binding, as exposed to clients (never secret values). */
|
|
21
21
|
export interface ProviderConnection {
|
|
22
|
+
/** The runner-backend kind for a runner-pool connection (`manifest` | `kubernetes`). */
|
|
23
|
+
kind?: string
|
|
22
24
|
providerId: string
|
|
23
25
|
label: string
|
|
24
26
|
baseUrl: string
|
|
25
27
|
connectedAt: number
|
|
26
28
|
/** Which secret/config keys are stored (names only), so the UI shows completeness. */
|
|
27
29
|
secretKeys: string[]
|
|
30
|
+
/**
|
|
31
|
+
* The stored discriminated runner-backend config, sans secrets, so the connect form
|
|
32
|
+
* can prefill its non-secret fields on edit. Shape mirrors the backend
|
|
33
|
+
* `RunnerBackendConfig` ({ kind: 'manifest' | 'kubernetes', … }); kept opaque here.
|
|
34
|
+
*/
|
|
35
|
+
config?: Record<string, unknown>
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
// The connect form builds the manifest dynamically from a server-provided scaffold
|
|
@@ -33,15 +41,23 @@ export interface ProviderConnection {
|
|
|
33
41
|
// per-provider manifest contract on receipt; the composable casts to the contract input
|
|
34
42
|
// type at the single `send` boundary.
|
|
35
43
|
|
|
36
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* The assembled register payload. The environment provider sends a full `manifest`.
|
|
46
|
+
* The runner-pool ("agent runner backend") provider sends a discriminated `config`
|
|
47
|
+
* ({ kind: 'manifest' | 'kubernetes', … }); for back-compat of the manifest editor it
|
|
48
|
+
* may instead send a bare `manifest`, which the composable wraps into the manifest
|
|
49
|
+
* backend config. The write-only secret bundle rides alongside.
|
|
50
|
+
*/
|
|
37
51
|
export interface RegisterProviderInput {
|
|
38
|
-
manifest
|
|
52
|
+
manifest?: Record<string, unknown>
|
|
53
|
+
/** The discriminated runner-backend config (manifest pool or kubernetes). */
|
|
54
|
+
config?: Record<string, unknown>
|
|
39
55
|
secrets: Record<string, string>
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
/** The test/probe payload (manifest-driven
|
|
58
|
+
/** The test/probe payload (manifest-driven, native, or a discriminated runner config). */
|
|
43
59
|
export interface TestProviderInput {
|
|
44
60
|
manifest?: Record<string, unknown>
|
|
45
|
-
config?: Record<string,
|
|
61
|
+
config?: Record<string, unknown>
|
|
46
62
|
secrets?: Record<string, string>
|
|
47
63
|
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -1272,10 +1272,30 @@
|
|
|
1272
1272
|
"blurb": "Where the Tester agent runs against a live preview environment. Configure the per-workspace settings and credentials your provider needs."
|
|
1273
1273
|
},
|
|
1274
1274
|
"runner-pool": {
|
|
1275
|
-
"title": "
|
|
1276
|
-
"blurb": "Where the coding agents run when not using Cloudflare Containers.
|
|
1275
|
+
"title": "Agent runner backend",
|
|
1276
|
+
"blurb": "Where the coding agents run when not using Cloudflare Containers. Choose a self-hosted runner pool (your own scheduler) or a Kubernetes cluster, then configure its endpoint and credentials."
|
|
1277
1277
|
}
|
|
1278
1278
|
},
|
|
1279
|
+
"backend": {
|
|
1280
|
+
"selectorLabel": "Runner backend",
|
|
1281
|
+
"manifest": "Self-hosted pool (manifest)",
|
|
1282
|
+
"kubernetes": "Kubernetes"
|
|
1283
|
+
},
|
|
1284
|
+
"kubernetes": {
|
|
1285
|
+
"label": "Name",
|
|
1286
|
+
"labelPlaceholder": "Production cluster",
|
|
1287
|
+
"apiServerUrl": "API server URL",
|
|
1288
|
+
"apiServerUrlHelp": "The kube-apiserver root, e.g. https://10.0.0.1:6443. The orchestrator reaches each run's pod through the apiserver, so only this endpoint must be reachable.",
|
|
1289
|
+
"namespace": "Namespace",
|
|
1290
|
+
"image": "Executor image",
|
|
1291
|
+
"imageHelp": "The executor-harness image each per-run pod runs.",
|
|
1292
|
+
"imageUi": "UI-tester image",
|
|
1293
|
+
"apiToken": "ServiceAccount token",
|
|
1294
|
+
"apiTokenHelp": "A bearer token with RBAC to create, get and delete pods and pods/proxy in the namespace. Stored encrypted; never shown again.",
|
|
1295
|
+
"caCertPem": "Cluster CA certificate (PEM)",
|
|
1296
|
+
"caCertPemHelp": "Paste the cluster CA bundle so the apiserver's TLS certificate verifies. Omit only for a publicly-trusted CA.",
|
|
1297
|
+
"harnessPort": "Harness port"
|
|
1298
|
+
},
|
|
1279
1299
|
"manifestEditor": {
|
|
1280
1300
|
"title": "Provider manifest",
|
|
1281
1301
|
"jsonLabel": "Manifest (JSON)",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1283,6 +1283,26 @@
|
|
|
1283
1283
|
"saveFailed": "No se pudo guardar la conexión",
|
|
1284
1284
|
"removed": "Conexión eliminada",
|
|
1285
1285
|
"removeFailed": "No se pudo eliminar la conexión"
|
|
1286
|
+
},
|
|
1287
|
+
"backend": {
|
|
1288
|
+
"selectorLabel": "Backend de ejecución",
|
|
1289
|
+
"manifest": "Pool autohospedado (manifiesto)",
|
|
1290
|
+
"kubernetes": "Kubernetes"
|
|
1291
|
+
},
|
|
1292
|
+
"kubernetes": {
|
|
1293
|
+
"label": "Nombre",
|
|
1294
|
+
"labelPlaceholder": "Clúster de producción",
|
|
1295
|
+
"apiServerUrl": "URL del API server",
|
|
1296
|
+
"apiServerUrlHelp": "La raíz del kube-apiserver, p. ej. https://10.0.0.1:6443. El orquestador llega al pod de cada ejecución a través del apiserver, así que solo este endpoint debe ser accesible.",
|
|
1297
|
+
"namespace": "Namespace",
|
|
1298
|
+
"image": "Imagen del ejecutor",
|
|
1299
|
+
"imageHelp": "La imagen de executor-harness que ejecuta cada pod por ejecución.",
|
|
1300
|
+
"imageUi": "Imagen de tester de UI",
|
|
1301
|
+
"apiToken": "Token de ServiceAccount",
|
|
1302
|
+
"apiTokenHelp": "Un token bearer con permisos RBAC para crear, obtener y eliminar pods y pods/proxy en el namespace. Se almacena cifrado; no se vuelve a mostrar.",
|
|
1303
|
+
"caCertPem": "Certificado CA del clúster (PEM)",
|
|
1304
|
+
"caCertPemHelp": "Pega el bundle CA del clúster para que el certificado TLS del apiserver se verifique. Omítelo solo para una CA de confianza pública.",
|
|
1305
|
+
"harnessPort": "Puerto del harness"
|
|
1286
1306
|
}
|
|
1287
1307
|
},
|
|
1288
1308
|
"serviceFragmentDefaults": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1283,6 +1283,26 @@
|
|
|
1283
1283
|
"saveFailed": "Impossible d'enregistrer la connexion",
|
|
1284
1284
|
"removed": "Connexion supprimée",
|
|
1285
1285
|
"removeFailed": "Impossible de supprimer la connexion"
|
|
1286
|
+
},
|
|
1287
|
+
"backend": {
|
|
1288
|
+
"selectorLabel": "Backend d'exécution",
|
|
1289
|
+
"manifest": "Pool auto-hébergé (manifeste)",
|
|
1290
|
+
"kubernetes": "Kubernetes"
|
|
1291
|
+
},
|
|
1292
|
+
"kubernetes": {
|
|
1293
|
+
"label": "Nom",
|
|
1294
|
+
"labelPlaceholder": "Cluster de production",
|
|
1295
|
+
"apiServerUrl": "URL du serveur d'API",
|
|
1296
|
+
"apiServerUrlHelp": "La racine du kube-apiserver, p. ex. https://10.0.0.1:6443. L'orchestrateur atteint le pod de chaque exécution via l'apiserver, donc seul ce point d'accès doit être accessible.",
|
|
1297
|
+
"namespace": "Namespace",
|
|
1298
|
+
"image": "Image de l'exécuteur",
|
|
1299
|
+
"imageHelp": "L'image executor-harness exécutée par chaque pod d'exécution.",
|
|
1300
|
+
"imageUi": "Image testeur d'UI",
|
|
1301
|
+
"apiToken": "Jeton de ServiceAccount",
|
|
1302
|
+
"apiTokenHelp": "Un jeton bearer avec les droits RBAC pour créer, lire et supprimer les pods et pods/proxy dans le namespace. Stocké chiffré ; jamais réaffiché.",
|
|
1303
|
+
"caCertPem": "Certificat CA du cluster (PEM)",
|
|
1304
|
+
"caCertPemHelp": "Collez le bundle CA du cluster pour que le certificat TLS de l'apiserver soit vérifié. À omettre uniquement pour une CA publiquement approuvée.",
|
|
1305
|
+
"harnessPort": "Port du harness"
|
|
1286
1306
|
}
|
|
1287
1307
|
},
|
|
1288
1308
|
"serviceFragmentDefaults": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1283,6 +1283,26 @@
|
|
|
1283
1283
|
"saveFailed": "Nie udało się zapisać połączenia",
|
|
1284
1284
|
"removed": "Połączenie usunięte",
|
|
1285
1285
|
"removeFailed": "Nie udało się usunąć połączenia"
|
|
1286
|
+
},
|
|
1287
|
+
"backend": {
|
|
1288
|
+
"selectorLabel": "Backend wykonawczy",
|
|
1289
|
+
"manifest": "Własny pool (manifest)",
|
|
1290
|
+
"kubernetes": "Kubernetes"
|
|
1291
|
+
},
|
|
1292
|
+
"kubernetes": {
|
|
1293
|
+
"label": "Nazwa",
|
|
1294
|
+
"labelPlaceholder": "Klaster produkcyjny",
|
|
1295
|
+
"apiServerUrl": "URL serwera API",
|
|
1296
|
+
"apiServerUrlHelp": "Główny adres kube-apiserver, np. https://10.0.0.1:6443. Orkiestrator łączy się z podem każdego uruchomienia przez apiserver, więc tylko ten endpoint musi być osiągalny.",
|
|
1297
|
+
"namespace": "Namespace",
|
|
1298
|
+
"image": "Obraz wykonawcy",
|
|
1299
|
+
"imageHelp": "Obraz executor-harness uruchamiany przez każdy pod uruchomienia.",
|
|
1300
|
+
"imageUi": "Obraz testera UI",
|
|
1301
|
+
"apiToken": "Token ServiceAccount",
|
|
1302
|
+
"apiTokenHelp": "Token bearer z uprawnieniami RBAC do tworzenia, odczytu i usuwania podów oraz pods/proxy w namespace. Przechowywany w postaci zaszyfrowanej; nie jest ponownie pokazywany.",
|
|
1303
|
+
"caCertPem": "Certyfikat CA klastra (PEM)",
|
|
1304
|
+
"caCertPemHelp": "Wklej pakiet CA klastra, aby certyfikat TLS apiservera został zweryfikowany. Pomiń tylko dla publicznie zaufanego CA.",
|
|
1305
|
+
"harnessPort": "Port harnessa"
|
|
1286
1306
|
}
|
|
1287
1307
|
},
|
|
1288
1308
|
"serviceFragmentDefaults": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1283,6 +1283,26 @@
|
|
|
1283
1283
|
"saveFailed": "Не вдалося зберегти підключення",
|
|
1284
1284
|
"removed": "Підключення видалено",
|
|
1285
1285
|
"removeFailed": "Не вдалося видалити підключення"
|
|
1286
|
+
},
|
|
1287
|
+
"backend": {
|
|
1288
|
+
"selectorLabel": "Бекенд виконавця",
|
|
1289
|
+
"manifest": "Власний пул (маніфест)",
|
|
1290
|
+
"kubernetes": "Kubernetes"
|
|
1291
|
+
},
|
|
1292
|
+
"kubernetes": {
|
|
1293
|
+
"label": "Назва",
|
|
1294
|
+
"labelPlaceholder": "Робочий кластер",
|
|
1295
|
+
"apiServerUrl": "URL сервера API",
|
|
1296
|
+
"apiServerUrlHelp": "Кореневий адрес kube-apiserver, напр. https://10.0.0.1:6443. Оркестратор звертається до пода кожного запуску через apiserver, тож лише цей endpoint має бути доступним.",
|
|
1297
|
+
"namespace": "Простір імен",
|
|
1298
|
+
"image": "Образ виконавця",
|
|
1299
|
+
"imageHelp": "Образ executor-harness, який запускає кожен под запуску.",
|
|
1300
|
+
"imageUi": "Образ UI-тестувальника",
|
|
1301
|
+
"apiToken": "Токен ServiceAccount",
|
|
1302
|
+
"apiTokenHelp": "Bearer-токен з правами RBAC на створення, читання та видалення подів і pods/proxy у просторі імен. Зберігається зашифрованим; більше не показується.",
|
|
1303
|
+
"caCertPem": "Сертифікат CA кластера (PEM)",
|
|
1304
|
+
"caCertPemHelp": "Вставте CA-набір кластера, щоб TLS-сертифікат apiserver проходив перевірку. Пропустіть лише для публічно довіреного CA.",
|
|
1305
|
+
"harnessPort": "Порт harness"
|
|
1286
1306
|
}
|
|
1287
1307
|
},
|
|
1288
1308
|
"serviceFragmentDefaults": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.2",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "^3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.
|
|
37
|
+
"@cat-factory/contracts": "0.48.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|