@cat-factory/app 0.243.0 → 0.244.0
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/panels/StepToolServers.logic.spec.ts +108 -1
- package/app/components/panels/StepToolServers.logic.ts +150 -2
- package/app/components/panels/StepToolServers.vue +89 -6
- package/app/types/toolServers.ts +2 -0
- package/i18n/locales/de.json +10 -0
- package/i18n/locales/en.json +10 -0
- package/i18n/locales/es.json +10 -0
- package/i18n/locales/fr.json +10 -0
- package/i18n/locales/he.json +10 -0
- package/i18n/locales/it.json +10 -0
- package/i18n/locales/ja.json +10 -0
- package/i18n/locales/pl.json +10 -0
- package/i18n/locales/tr.json +10 -0
- package/i18n/locales/uk.json +10 -0
- package/package.json +2 -2
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
KNOWN_OBSERVED_STATUSES,
|
|
3
4
|
KNOWN_REASONS,
|
|
5
|
+
OBSERVED_STATUS_KEY,
|
|
4
6
|
REASON_KEY,
|
|
5
7
|
REMEDY_KEY,
|
|
8
|
+
observationFor,
|
|
9
|
+
observationIsFault,
|
|
10
|
+
observationText,
|
|
6
11
|
reasonText,
|
|
7
12
|
remedyText,
|
|
13
|
+
unattributedObservations,
|
|
8
14
|
} from './StepToolServers.logic'
|
|
9
|
-
import type { ToolServerUnavailableReason } from '~/types/toolServers'
|
|
15
|
+
import type { ObservedToolServer, ToolServerUnavailableReason } from '~/types/toolServers'
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* A dropped tool server is the whole point of this surface: until it existed, a run that quietly
|
|
@@ -98,3 +104,104 @@ function render(reason: string): { key: string; params?: Record<string, unknown>
|
|
|
98
104
|
})
|
|
99
105
|
return seen
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The OBSERVED half: what the agent's CLI said about the servers the platform wired. Its whole
|
|
110
|
+
* value is a set of distinctions, and each one collapses into "looks fine" if it is lost — so
|
|
111
|
+
* these pin the distinctions rather than the copy.
|
|
112
|
+
*/
|
|
113
|
+
describe('tool-server startup observations', () => {
|
|
114
|
+
it('gives every observed status but `ready` its own copy', () => {
|
|
115
|
+
// `ready` is deliberately absent: a started server's line depends on its tool count, which is
|
|
116
|
+
// three sentences rather than one key. Everything else is exhaustive against the schema, so a
|
|
117
|
+
// member added on the backend fails here instead of rendering blank.
|
|
118
|
+
expect([...Object.keys(OBSERVED_STATUS_KEY), 'ready'].sort()).toEqual(
|
|
119
|
+
[...KNOWN_OBSERVED_STATUSES].sort(),
|
|
120
|
+
)
|
|
121
|
+
const keys = Object.values(OBSERVED_STATUS_KEY)
|
|
122
|
+
expect(new Set(keys).size).toBe(keys.length)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('says NOTHING when no observation was made', () => {
|
|
126
|
+
// The distinction the whole field rests on. A codex run, an image one version behind and an
|
|
127
|
+
// unmapped runner pool all report nothing, and any placeholder here would read as a verdict
|
|
128
|
+
// about a server that was very likely fine.
|
|
129
|
+
expect(observationFor(undefined, 'slack')).toBeNull()
|
|
130
|
+
expect(observationText(null, (key) => key)).toBeNull()
|
|
131
|
+
expect(observationIsFault(null)).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('distinguishes “nobody looked” from “the CLI never loaded it”', () => {
|
|
135
|
+
// Both render as an absent server on the chip if they are conflated, and only the second is
|
|
136
|
+
// evidence of anything. The first must never raise a fault.
|
|
137
|
+
const notLoaded = observationFor([{ id: 'jira', status: 'ready' }], 'slack')
|
|
138
|
+
expect(notLoaded).toEqual({ kind: 'not_loaded' })
|
|
139
|
+
expect(observationText(notLoaded, (key) => key)).toBe(
|
|
140
|
+
'panels.stepDetail.toolServers.observed.notLoaded',
|
|
141
|
+
)
|
|
142
|
+
expect(observationIsFault(notLoaded)).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('separates “started with no tools” from “started, tools uncounted”', () => {
|
|
146
|
+
// A server that connected and exposes nothing reaches the agent exactly like one that was
|
|
147
|
+
// never wired, and every other signal about it says healthy — so it gets its own sentence
|
|
148
|
+
// rather than a "0 tools" that reads as a rendering artefact.
|
|
149
|
+
const none = observationFor([{ id: 'slack', status: 'ready', toolCount: 0 }], 'slack')
|
|
150
|
+
const uncounted = observationFor([{ id: 'slack', status: 'ready' }], 'slack')
|
|
151
|
+
const some = observationFor([{ id: 'slack', status: 'ready', toolCount: 3 }], 'slack')
|
|
152
|
+
expect(observationText(none, (key) => key)).toBe(
|
|
153
|
+
'panels.stepDetail.toolServers.observed.readyNoTools',
|
|
154
|
+
)
|
|
155
|
+
expect(observationText(uncounted, (key) => key)).toBe(
|
|
156
|
+
'panels.stepDetail.toolServers.observed.ready',
|
|
157
|
+
)
|
|
158
|
+
expect(observationText(some, (key) => key)).toBe(
|
|
159
|
+
'panels.stepDetail.toolServers.observed.readyTools',
|
|
160
|
+
)
|
|
161
|
+
// None of the three is a fault: the count is a diagnosis for a person to read, not a verdict
|
|
162
|
+
// the surface should paint red.
|
|
163
|
+
for (const observation of [none, uncounted, some]) {
|
|
164
|
+
expect(observationIsFault(observation)).toBe(false)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('flags the two states the platform promised a tool it did not get', () => {
|
|
169
|
+
for (const status of ['failed', 'needs_auth'] as const) {
|
|
170
|
+
expect(observationIsFault(observationFor([{ id: 'slack', status }], 'slack'))).toBe(true)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('never flags a state the report did not resolve', () => {
|
|
175
|
+
// `unknown` is a fact about THIS build (a word it could not map) or about the moment the
|
|
176
|
+
// report was taken (a server still handshaking), never about the server itself. Painting it
|
|
177
|
+
// as a fault would send an operator to debug a working integration every time a CLI adds a
|
|
178
|
+
// status word or starts a server a moment slower than the session announcement.
|
|
179
|
+
// Cast, because the point is a value the TYPE excludes and the DATA carries: the vocabulary
|
|
180
|
+
// is persisted on a run, so a status recorded by a newer harness (or retired since) reads back
|
|
181
|
+
// here as a member this build has no case for.
|
|
182
|
+
const persisted = observationFor(
|
|
183
|
+
[{ id: 'slack', status: 'reticulating' } as unknown as ObservedToolServer],
|
|
184
|
+
'slack',
|
|
185
|
+
)
|
|
186
|
+
expect(persisted).toEqual({ kind: 'loaded', status: 'unknown' })
|
|
187
|
+
expect(observationIsFault(persisted)).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('states a report naming servers the dispatch did not wire', () => {
|
|
191
|
+
// Empty on every ordinary run (`--strict-mcp-config`). The only way to reach it is a producer
|
|
192
|
+
// describing some OTHER job, and silently filtering those rows would present that report as
|
|
193
|
+
// this run's clean bill of health.
|
|
194
|
+
const record = {
|
|
195
|
+
agentKind: 'coder',
|
|
196
|
+
wired: [{ id: 'slack', label: 'Slack', transport: 'http' as const }],
|
|
197
|
+
unavailable: [],
|
|
198
|
+
observed: [
|
|
199
|
+
{ id: 'slack', status: 'ready' as const },
|
|
200
|
+
{ id: 'stranger', status: 'ready' as const },
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
expect(unattributedObservations(record).map((s) => s.id)).toEqual(['stranger'])
|
|
204
|
+
// …and nothing to state when there was no report at all.
|
|
205
|
+
expect(unattributedObservations({ ...record, observed: undefined })).toEqual([])
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
toolServerObservedStatusSchema,
|
|
3
|
+
toolServerUnavailableReasonSchema,
|
|
4
|
+
} from '@cat-factory/contracts'
|
|
5
|
+
import type {
|
|
6
|
+
ObservedToolServer,
|
|
7
|
+
StepToolServers,
|
|
8
|
+
ToolServerObservedStatus,
|
|
9
|
+
ToolServerUnavailableReason,
|
|
10
|
+
} from '~/types/toolServers'
|
|
3
11
|
|
|
4
12
|
// The step surface's tool-server (MCP) reason mapping. Kept out of the SFC on the same seam as
|
|
5
13
|
// `StepTestReport.logic.ts`, so the vocabulary rule can be asserted without mounting a component.
|
|
@@ -102,3 +110,143 @@ export function remedyText(
|
|
|
102
110
|
): string | null {
|
|
103
111
|
return isKnownReason(reason) ? t(REMEDY_KEY[reason]) : null
|
|
104
112
|
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// The OBSERVED half: what the agent's CLI reported about the servers it loaded.
|
|
116
|
+
//
|
|
117
|
+
// The mapping above answers "why did the platform withhold this tool". This one answers the
|
|
118
|
+
// question that mapping structurally cannot: a server that passed every check, was promised to the
|
|
119
|
+
// agent in its prompt, and then did not come up. The two are rendered together on one chip because
|
|
120
|
+
// they are two halves of one answer about one server — but they are never merged into one status,
|
|
121
|
+
// because a withheld server and a failed one need different people to fix different things.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What this build knows about one WIRED server after joining the CLI's report onto it.
|
|
126
|
+
*
|
|
127
|
+
* `null` and `not_loaded` are the pair this whole type exists to keep apart, and conflating them is
|
|
128
|
+
* the mistake that would make the surface lie. `null` means NO OBSERVATION WAS MADE — the run's
|
|
129
|
+
* harness publishes no report (codex's CLI does not, nor does any image older than the one that
|
|
130
|
+
* introduced this, nor an unmapped runner pool) — so nothing at all is known about whether the
|
|
131
|
+
* server started. `not_loaded` means an observation WAS made and this server was not in it, which
|
|
132
|
+
* is positive evidence that the CLI never loaded it. Rendering the first as the second would
|
|
133
|
+
* accuse every wired server on every deployment one image behind.
|
|
134
|
+
*/
|
|
135
|
+
export type ToolServerObservation =
|
|
136
|
+
| null
|
|
137
|
+
| { kind: 'loaded'; status: ToolServerObservedStatus; toolCount?: number }
|
|
138
|
+
| { kind: 'not_loaded' }
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Join the CLI's report onto one wired server id.
|
|
142
|
+
*
|
|
143
|
+
* Takes the whole `observed` field rather than a prepared map so the absent-vs-empty decision is
|
|
144
|
+
* made HERE, in the one place that owns the distinction, instead of by each caller building the
|
|
145
|
+
* map. An absent field yields `null` for every server; a present one yields either the CLI's line
|
|
146
|
+
* or `not_loaded`.
|
|
147
|
+
*/
|
|
148
|
+
export function observationFor(
|
|
149
|
+
observed: readonly ObservedToolServer[] | undefined,
|
|
150
|
+
id: string,
|
|
151
|
+
): ToolServerObservation {
|
|
152
|
+
if (!observed) return null
|
|
153
|
+
const found = observed.find((server) => server.id === id)
|
|
154
|
+
if (!found) return { kind: 'not_loaded' }
|
|
155
|
+
return {
|
|
156
|
+
kind: 'loaded',
|
|
157
|
+
status: isKnownObservedStatus(found.status) ? found.status : 'unknown',
|
|
158
|
+
// Carried only when the CLI counted, and `0` is carried: a server that connected and exposed
|
|
159
|
+
// nothing reaches the agent exactly like one that was never wired, so it is the most
|
|
160
|
+
// diagnostic count on the field and the one a truthiness guard would erase.
|
|
161
|
+
...(typeof found.toolCount === 'number' ? { toolCount: found.toolCount } : {}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The servers the CLI named that this step's record does not list as wired.
|
|
167
|
+
*
|
|
168
|
+
* Empty by construction on every ordinary run: the harness starts the CLI under
|
|
169
|
+
* `--strict-mcp-config`, so the only servers it can load are the ones this dispatch wrote for it.
|
|
170
|
+
* Surfaced anyway rather than filtered out, because the one way to reach it is a producer whose
|
|
171
|
+
* report does not describe this job — a runner-pool manifest pointing `toolServersPath` at the
|
|
172
|
+
* wrong field, say — and a surface that silently dropped those rows would present a report about
|
|
173
|
+
* some other job as a clean bill of health for this one.
|
|
174
|
+
*/
|
|
175
|
+
export function unattributedObservations(record: StepToolServers): ObservedToolServer[] {
|
|
176
|
+
if (!record.observed) return []
|
|
177
|
+
const wired = new Set(record.wired.map((server) => server.id))
|
|
178
|
+
return record.observed.filter((server) => !wired.has(server.id))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The i18n key per observed status, for a server the CLI DID name. An exhaustive `Record` for the
|
|
183
|
+
* same reason {@link REASON_KEY} is one: a member added to the wire vocabulary must fail to
|
|
184
|
+
* compile here rather than render as a blank line.
|
|
185
|
+
*
|
|
186
|
+
* `ready` is deliberately absent — a started server's line depends on its tool COUNT, which is
|
|
187
|
+
* three separate sentences (counted some, counted none, did not count), so it is resolved by
|
|
188
|
+
* {@link observationText} rather than by a single key.
|
|
189
|
+
*/
|
|
190
|
+
export const OBSERVED_STATUS_KEY: Record<Exclude<ToolServerObservedStatus, 'ready'>, string> = {
|
|
191
|
+
failed: 'panels.stepDetail.toolServers.observed.failed',
|
|
192
|
+
needs_auth: 'panels.stepDetail.toolServers.observed.needsAuth',
|
|
193
|
+
unknown: 'panels.stepDetail.toolServers.observed.unknown',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The observed-status vocabulary as the SCHEMA states it: what a parity assertion grades against. */
|
|
197
|
+
export const KNOWN_OBSERVED_STATUSES = toolServerObservedStatusSchema.options
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether a persisted status is a member THIS build knows, derived from the picklist's own options.
|
|
201
|
+
* A predicate rather than a truthiness check on the lookup, for the reason {@link isKnownReason}
|
|
202
|
+
* gives: an `Object.prototype` member name reads back as a truthy non-key.
|
|
203
|
+
*/
|
|
204
|
+
export function isKnownObservedStatus(status: string): status is ToolServerObservedStatus {
|
|
205
|
+
return (KNOWN_OBSERVED_STATUSES as readonly string[]).includes(status)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render one observation, or `null` when there is nothing to say.
|
|
210
|
+
*
|
|
211
|
+
* `null` is returned for `null` — no observation was made — and it is the whole reason this
|
|
212
|
+
* function exists rather than a template expression: the surface must be BYTE-FOR-BYTE what it was
|
|
213
|
+
* before this field existed on every run that carries no report, which is every codex run, every
|
|
214
|
+
* run on an image one version behind, and every run on an unmapped runner pool. Silence is the
|
|
215
|
+
* only honest rendering of "nobody looked".
|
|
216
|
+
*/
|
|
217
|
+
export function observationText(
|
|
218
|
+
observation: ToolServerObservation,
|
|
219
|
+
t: (key: string, params?: Record<string, unknown>) => string,
|
|
220
|
+
): string | null {
|
|
221
|
+
if (observation === null) return null
|
|
222
|
+
if (observation.kind === 'not_loaded') {
|
|
223
|
+
return t('panels.stepDetail.toolServers.observed.notLoaded')
|
|
224
|
+
}
|
|
225
|
+
if (observation.status !== 'ready') return t(OBSERVED_STATUS_KEY[observation.status])
|
|
226
|
+
if (observation.toolCount === undefined) {
|
|
227
|
+
return t('panels.stepDetail.toolServers.observed.ready')
|
|
228
|
+
}
|
|
229
|
+
// Zero gets its own sentence rather than "0 tools": a server that started and offers nothing is
|
|
230
|
+
// a distinct fault (a narrowed `allowedTools` matching nothing, a vendor that authenticated and
|
|
231
|
+
// served an empty catalog), and it is the one an operator would otherwise never suspect,
|
|
232
|
+
// because every other signal about it says healthy.
|
|
233
|
+
return observation.toolCount === 0
|
|
234
|
+
? t('panels.stepDetail.toolServers.observed.readyNoTools')
|
|
235
|
+
: t('panels.stepDetail.toolServers.observed.readyTools', { count: observation.toolCount })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Whether an observation should DRAW ATTENTION on the chip: the server was promised to the agent
|
|
240
|
+
* and the CLI could not deliver it.
|
|
241
|
+
*
|
|
242
|
+
* `unknown` is deliberately NOT alarming. It covers a word this build could not map (a fact about
|
|
243
|
+
* this build rather than about the server) and a server the CLI reported as still handshaking when
|
|
244
|
+
* it announced the session (a fact about the moment the report was taken). Neither says anything
|
|
245
|
+
* happened to the server, and painting either as a fault would send an operator to debug a working
|
|
246
|
+
* integration every time a CLI adds a status or starts a server a moment slower.
|
|
247
|
+
*/
|
|
248
|
+
export function observationIsFault(observation: ToolServerObservation): boolean {
|
|
249
|
+
if (observation === null) return false
|
|
250
|
+
if (observation.kind === 'not_loaded') return true
|
|
251
|
+
return observation.status === 'failed' || observation.status === 'needs_auth'
|
|
252
|
+
}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
// unavailable server is a chip of its own with its own translated reason, never a shorter list of
|
|
9
9
|
// the wired ones: "absent" and "zero" must not render the same.
|
|
10
10
|
//
|
|
11
|
+
// The wired half carries a SECOND answer when the run's harness reported one: what the agent's
|
|
12
|
+
// CLI said about each server when it started up. The two are deliberately not merged into one
|
|
13
|
+
// status — the platform withholding a tool and the CLI failing to start one are different faults
|
|
14
|
+
// for different people — and the absence of a CLI report renders as nothing at all, because
|
|
15
|
+
// "nobody looked" and "the CLI loaded nothing" are opposite facts and only silence states the
|
|
16
|
+
// first one honestly (a codex run, an older image and an unmapped runner pool all reach it).
|
|
17
|
+
//
|
|
11
18
|
// SELF-HIDING when the record holds nothing, like the sibling panels around it. The record is
|
|
12
19
|
// written on EVERY container dispatch, so both lists empty is the state of every step on every
|
|
13
20
|
// deployment that registers no tool servers at all — the overwhelming default. That state is a
|
|
@@ -16,7 +23,14 @@
|
|
|
16
23
|
// section on every step of every run to say nothing happened. The distinction absent-vs-empty is
|
|
17
24
|
// still carried on the wire and answered by the debug API, which is where a reader asking it looks.
|
|
18
25
|
import type { StepToolServers } from '~/types/toolServers'
|
|
19
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
observationFor,
|
|
28
|
+
observationIsFault,
|
|
29
|
+
observationText,
|
|
30
|
+
reasonText,
|
|
31
|
+
remedyText,
|
|
32
|
+
unattributedObservations,
|
|
33
|
+
} from '~/components/panels/StepToolServers.logic'
|
|
20
34
|
import { agentKindMeta } from '~/utils/catalog'
|
|
21
35
|
|
|
22
36
|
const props = defineProps<{
|
|
@@ -33,8 +47,22 @@ const props = defineProps<{
|
|
|
33
47
|
|
|
34
48
|
const { t } = useI18n()
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Servers the CLI named that this dispatch did not wire. Empty on every ordinary run (the CLI runs
|
|
52
|
+
* under `--strict-mcp-config`), and surfaced rather than filtered because the only way to reach it
|
|
53
|
+
* is a report that describes some other job — which must not be presented as this job's clean bill
|
|
54
|
+
* of health.
|
|
55
|
+
*/
|
|
56
|
+
const unattributed = computed(() => unattributedObservations(props.toolServers))
|
|
57
|
+
|
|
36
58
|
const hasAny = computed(
|
|
37
|
-
() =>
|
|
59
|
+
() =>
|
|
60
|
+
props.toolServers.wired.length > 0 ||
|
|
61
|
+
props.toolServers.unavailable.length > 0 ||
|
|
62
|
+
// A report about servers this dispatch did not wire is the one thing worth showing on an
|
|
63
|
+
// otherwise empty record: both lists empty says the dispatched kind declared none, and a CLI
|
|
64
|
+
// naming servers anyway contradicts exactly that.
|
|
65
|
+
unattributed.value.length > 0,
|
|
38
66
|
)
|
|
39
67
|
|
|
40
68
|
/** Set only when a helper re-dispatch owns this record, so the ordinary case renders no extra line. */
|
|
@@ -44,6 +72,22 @@ const dispatchedAs = computed(() =>
|
|
|
44
72
|
: null,
|
|
45
73
|
)
|
|
46
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Each wired server with the CLI's own verdict joined onto it, when a verdict was reported. Bound
|
|
77
|
+
* here for the same reason `drops` is: the join and the absent-vs-not-loaded decision are pure and
|
|
78
|
+
* assertable without mounting, and resolving them once per record beats once per re-render.
|
|
79
|
+
*/
|
|
80
|
+
const wired = computed(() =>
|
|
81
|
+
props.toolServers.wired.map((server) => {
|
|
82
|
+
const observation = observationFor(props.toolServers.observed, server.id)
|
|
83
|
+
return {
|
|
84
|
+
...server,
|
|
85
|
+
observedText: observationText(observation, (key, params) => t(key, params ?? {})),
|
|
86
|
+
isFault: observationIsFault(observation),
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
47
91
|
/**
|
|
48
92
|
* Each dropped server with both halves of its answer resolved: WHY it was dropped, and what to
|
|
49
93
|
* change so the next run gets it. Bound here rather than called from the template so the pure
|
|
@@ -80,20 +124,59 @@ const drops = computed(() =>
|
|
|
80
124
|
{{ t('panels.stepDetail.toolServers.dispatchedAs', { agent: dispatchedAs }) }}
|
|
81
125
|
</p>
|
|
82
126
|
|
|
83
|
-
<ul v-if="
|
|
127
|
+
<ul v-if="wired.length" class="flex flex-wrap gap-1.5">
|
|
84
128
|
<li
|
|
85
|
-
v-for="server in
|
|
129
|
+
v-for="server in wired"
|
|
86
130
|
:key="server.id"
|
|
87
131
|
data-testid="step-tool-server-wired"
|
|
88
|
-
|
|
132
|
+
:data-observed-fault="server.isFault ? 'true' : undefined"
|
|
133
|
+
class="inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[12px]"
|
|
134
|
+
:class="
|
|
135
|
+
server.isFault
|
|
136
|
+
? 'border-amber-500/30 bg-amber-500/10 text-amber-200'
|
|
137
|
+
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
|
|
138
|
+
"
|
|
89
139
|
:title="
|
|
90
140
|
server.tools?.length
|
|
91
141
|
? t('panels.stepDetail.toolServers.narrowed', { tools: server.tools.join(', ') })
|
|
92
142
|
: t('panels.stepDetail.toolServers.allTools')
|
|
93
143
|
"
|
|
94
144
|
>
|
|
95
|
-
<UIcon
|
|
145
|
+
<UIcon
|
|
146
|
+
:name="server.isFault ? 'i-lucide-triangle-alert' : 'i-lucide-check'"
|
|
147
|
+
class="h-3.5 w-3.5"
|
|
148
|
+
/>
|
|
96
149
|
<span>{{ server.label || server.id }}</span>
|
|
150
|
+
<!--
|
|
151
|
+
The CLI's own verdict, when the run's harness reported one. Absent renders NOTHING: a
|
|
152
|
+
harness that publishes no report has said nothing about this server, and a placeholder
|
|
153
|
+
would read as a verdict.
|
|
154
|
+
-->
|
|
155
|
+
<span
|
|
156
|
+
v-if="server.observedText"
|
|
157
|
+
data-testid="step-tool-server-observed"
|
|
158
|
+
class="opacity-80"
|
|
159
|
+
>{{ server.observedText }}</span
|
|
160
|
+
>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
|
|
164
|
+
<!--
|
|
165
|
+
A report naming servers this dispatch did not wire can only come from a producer describing
|
|
166
|
+
some other job (a runner-pool manifest mapped at the wrong field). Stated rather than
|
|
167
|
+
filtered, so the rest of the report is not read as authoritative about this run.
|
|
168
|
+
-->
|
|
169
|
+
<ul v-if="unattributed.length" class="mt-2 space-y-1.5">
|
|
170
|
+
<li
|
|
171
|
+
v-for="server in unattributed"
|
|
172
|
+
:key="server.id"
|
|
173
|
+
data-testid="step-tool-server-unattributed"
|
|
174
|
+
class="flex items-start gap-1.5 text-[12px] text-slate-400"
|
|
175
|
+
>
|
|
176
|
+
<UIcon name="i-lucide-circle-help" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
177
|
+
<span>{{
|
|
178
|
+
t('panels.stepDetail.toolServers.observed.unattributed', { id: server.id })
|
|
179
|
+
}}</span>
|
|
97
180
|
</li>
|
|
98
181
|
</ul>
|
|
99
182
|
|
package/app/types/toolServers.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// member added on one side only renders as a blank chip rather than failing to compile.
|
|
7
7
|
|
|
8
8
|
export type {
|
|
9
|
+
ObservedToolServer,
|
|
9
10
|
StepToolServers,
|
|
10
11
|
ToolServerUnavailableReason,
|
|
11
12
|
ToolServerAllowedToolsCheck,
|
|
12
13
|
ToolServerCredential,
|
|
13
14
|
ToolServerNotProbeableReason,
|
|
15
|
+
ToolServerObservedStatus,
|
|
14
16
|
ToolServerOAuthGrant,
|
|
15
17
|
ToolServerOAuthStatus,
|
|
16
18
|
ToolServerProbeResult,
|
package/i18n/locales/de.json
CHANGED
|
@@ -2001,6 +2001,16 @@
|
|
|
2001
2001
|
"oauthNotConnected": "Verbinden Sie dieses Board im Infrastruktur-Fenster damit. Ein Deployment ohne ENCRYPTION_KEY hat keinen Ort für eine Berechtigung, das muss ein Betreiber also zuerst setzen.",
|
|
2002
2002
|
"oauthTokenFailed": "Verbinden Sie es im Infrastruktur-Fenster neu, oder warten Sie die Störung des Anbieters ab.",
|
|
2003
2003
|
"overBudget": "Kürzen Sie, was der Agent deklariert, damit ein Lauf alles mitführen kann."
|
|
2004
|
+
},
|
|
2005
|
+
"observed": {
|
|
2006
|
+
"ready": "gestartet",
|
|
2007
|
+
"readyTools": "{count} Tools",
|
|
2008
|
+
"readyNoTools": "gestartet, keine Tools",
|
|
2009
|
+
"failed": "Start fehlgeschlagen",
|
|
2010
|
+
"needsAuth": "Autorisierung erforderlich",
|
|
2011
|
+
"unknown": "Zustand ungeklärt",
|
|
2012
|
+
"notLoaded": "nie geladen",
|
|
2013
|
+
"unattributed": "Die Agenten-CLI meldete einen Server, den dieser Schritt nicht eingebunden hat: {id}."
|
|
2004
2014
|
}
|
|
2005
2015
|
},
|
|
2006
2016
|
"adherence": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -1535,6 +1535,16 @@
|
|
|
1535
1535
|
"oauthNotConnected": "Connect this board to it from the Infrastructure window. A deployment with no ENCRYPTION_KEY has nowhere to keep a grant, so an operator has to set that first.",
|
|
1536
1536
|
"oauthTokenFailed": "Reconnect it from the Infrastructure window, or wait out the vendor's outage.",
|
|
1537
1537
|
"overBudget": "Trim what the agent declares, so one run can carry all of it."
|
|
1538
|
+
},
|
|
1539
|
+
"observed": {
|
|
1540
|
+
"ready": "started",
|
|
1541
|
+
"readyTools": "{count} tools",
|
|
1542
|
+
"readyNoTools": "started, no tools",
|
|
1543
|
+
"failed": "failed to start",
|
|
1544
|
+
"needsAuth": "needs authorization",
|
|
1545
|
+
"unknown": "state unresolved",
|
|
1546
|
+
"notLoaded": "never loaded",
|
|
1547
|
+
"unattributed": "The agent CLI reported a server this step did not wire: {id}."
|
|
1538
1548
|
}
|
|
1539
1549
|
},
|
|
1540
1550
|
"adherence": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "Conecta este tablero con él desde la ventana de Infraestructura. Un despliegue sin ENCRYPTION_KEY no tiene dónde guardar una concesión, así que un operador debe configurarla primero.",
|
|
1445
1445
|
"oauthTokenFailed": "Vuelve a conectarlo desde la ventana de Infraestructura, o espera a que pase la caída del proveedor.",
|
|
1446
1446
|
"overBudget": "Recorta lo que declara el agente, para que una ejecución pueda llevarlo todo."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "iniciado",
|
|
1450
|
+
"readyTools": "{count} herramientas",
|
|
1451
|
+
"readyNoTools": "iniciado, sin herramientas",
|
|
1452
|
+
"failed": "no arrancó",
|
|
1453
|
+
"needsAuth": "requiere autorización",
|
|
1454
|
+
"unknown": "estado sin determinar",
|
|
1455
|
+
"notLoaded": "nunca se cargó",
|
|
1456
|
+
"unattributed": "La CLI del agente informó de un servidor que este paso no conectó: {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "Connectez ce tableau à ce serveur depuis la fenêtre Infrastructure. Un déploiement sans ENCRYPTION_KEY n’a nulle part où conserver une autorisation : un opérateur doit d’abord la définir.",
|
|
1445
1445
|
"oauthTokenFailed": "Reconnectez-le depuis la fenêtre Infrastructure, ou attendez la fin de la panne du fournisseur.",
|
|
1446
1446
|
"overBudget": "Réduisez ce que l’agent déclare, pour qu’une exécution puisse tout transporter."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "démarré",
|
|
1450
|
+
"readyTools": "{count} outils",
|
|
1451
|
+
"readyNoTools": "démarré, aucun outil",
|
|
1452
|
+
"failed": "n'a pas démarré",
|
|
1453
|
+
"needsAuth": "autorisation requise",
|
|
1454
|
+
"unknown": "état non déterminé",
|
|
1455
|
+
"notLoaded": "jamais chargé",
|
|
1456
|
+
"unattributed": "La CLI d'agent a signalé un serveur que cette étape n'a pas branché : {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "חברו את הלוח הזה אליו מחלון התשתית. בפריסה ללא ENCRYPTION_KEY אין היכן לשמור הרשאה, ולכן מפעיל צריך להגדיר אותו קודם.",
|
|
1445
1445
|
"oauthTokenFailed": "חברו אותו מחדש מחלון התשתית, או המתינו לסיום התקלה אצל הספק.",
|
|
1446
1446
|
"overBudget": "צמצמו את מה שהסוכן מצהיר עליו, כדי שריצה אחת תוכל לשאת הכול."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "הופעל",
|
|
1450
|
+
"readyTools": "{count} כלים",
|
|
1451
|
+
"readyNoTools": "הופעל, ללא כלים",
|
|
1452
|
+
"failed": "לא עלה",
|
|
1453
|
+
"needsAuth": "דורש הרשאה",
|
|
1454
|
+
"unknown": "מצב לא ידוע",
|
|
1455
|
+
"notLoaded": "מעולם לא נטען",
|
|
1456
|
+
"unattributed": "ממשק הסוכן דיווח על שרת שהשלב הזה לא חיבר: {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -2001,6 +2001,16 @@
|
|
|
2001
2001
|
"oauthNotConnected": "Collega questa lavagna al server dalla finestra Infrastruttura. Un deployment senza ENCRYPTION_KEY non ha dove conservare una concessione, quindi un operatore deve impostarla prima.",
|
|
2002
2002
|
"oauthTokenFailed": "Ricollegalo dalla finestra Infrastruttura, oppure attendi la fine del disservizio del fornitore.",
|
|
2003
2003
|
"overBudget": "Riduci ciò che l'agente dichiara, così una singola esecuzione può portarlo tutto."
|
|
2004
|
+
},
|
|
2005
|
+
"observed": {
|
|
2006
|
+
"ready": "avviato",
|
|
2007
|
+
"readyTools": "{count} strumenti",
|
|
2008
|
+
"readyNoTools": "avviato, nessuno strumento",
|
|
2009
|
+
"failed": "non si è avviato",
|
|
2010
|
+
"needsAuth": "richiede autorizzazione",
|
|
2011
|
+
"unknown": "stato non determinato",
|
|
2012
|
+
"notLoaded": "mai caricato",
|
|
2013
|
+
"unattributed": "La CLI dell'agente ha segnalato un server che questo passo non ha collegato: {id}."
|
|
2004
2014
|
}
|
|
2005
2015
|
},
|
|
2006
2016
|
"adherence": {
|
package/i18n/locales/ja.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "インフラストラクチャ ウィンドウからこのボードを接続してください。ENCRYPTION_KEY のないデプロイには許可を保管する場所がないため、まず運用者がそれを設定する必要があります。",
|
|
1445
1445
|
"oauthTokenFailed": "インフラストラクチャ ウィンドウから接続し直すか、提供元の障害が収まるのを待ってください。",
|
|
1446
1446
|
"overBudget": "1 回の実行ですべて運べるよう、このエージェントの宣言を減らしてください。"
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "起動済み",
|
|
1450
|
+
"readyTools": "ツール {count} 個",
|
|
1451
|
+
"readyNoTools": "起動済み、ツールなし",
|
|
1452
|
+
"failed": "起動に失敗",
|
|
1453
|
+
"needsAuth": "認可が必要",
|
|
1454
|
+
"unknown": "状態は未確定",
|
|
1455
|
+
"notLoaded": "読み込まれませんでした",
|
|
1456
|
+
"unattributed": "エージェント CLI が、このステップでは接続していないサーバーを報告しました: {id}。"
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "Połącz tę tablicę z serwerem w oknie Infrastruktura. Wdrożenie bez ENCRYPTION_KEY nie ma gdzie przechować zgody, więc operator musi ją najpierw ustawić.",
|
|
1445
1445
|
"oauthTokenFailed": "Połącz go ponownie w oknie Infrastruktura albo przeczekaj awarię dostawcy.",
|
|
1446
1446
|
"overBudget": "Skróć to, co deklaruje agent, aby jedno uruchomienie uniosło całość."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "uruchomiony",
|
|
1450
|
+
"readyTools": "narzędzia: {count}",
|
|
1451
|
+
"readyNoTools": "uruchomiony, bez narzędzi",
|
|
1452
|
+
"failed": "nie wystartował",
|
|
1453
|
+
"needsAuth": "wymaga autoryzacji",
|
|
1454
|
+
"unknown": "stan nieustalony",
|
|
1455
|
+
"notLoaded": "nigdy nie wczytany",
|
|
1456
|
+
"unattributed": "CLI agenta zgłosiło serwer, którego ten krok nie podłączył: {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "Bu panoyu Altyapı penceresinden ona bağlayın. ENCRYPTION_KEY olmayan bir dağıtımda izni saklayacak bir yer yoktur, bu yüzden önce bir operatörün bunu ayarlaması gerekir.",
|
|
1445
1445
|
"oauthTokenFailed": "Altyapı penceresinden yeniden bağlayın ya da sağlayıcının kesintisinin geçmesini bekleyin.",
|
|
1446
1446
|
"overBudget": "Tek bir çalıştırma hepsini taşıyabilsin diye ajanın bildirdiklerini kısaltın."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "başlatıldı",
|
|
1450
|
+
"readyTools": "{count} araç",
|
|
1451
|
+
"readyNoTools": "başlatıldı, araç yok",
|
|
1452
|
+
"failed": "başlatılamadı",
|
|
1453
|
+
"needsAuth": "yetkilendirme gerekiyor",
|
|
1454
|
+
"unknown": "durum belirsiz",
|
|
1455
|
+
"notLoaded": "hiç yüklenmedi",
|
|
1456
|
+
"unattributed": "Ajan CLI'si bu adımın bağlamadığı bir sunucu bildirdi: {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1444,6 +1444,16 @@
|
|
|
1444
1444
|
"oauthNotConnected": "Під’єднайте цю дошку до нього у вікні інфраструктури. Розгортання без ENCRYPTION_KEY не має де зберігати дозвіл, тож оператор має спершу задати його.",
|
|
1445
1445
|
"oauthTokenFailed": "Під’єднайте його заново у вікні інфраструктури або перечекайте збій постачальника.",
|
|
1446
1446
|
"overBudget": "Скоротіть те, що оголошує агент, щоб один запуск ніс усе."
|
|
1447
|
+
},
|
|
1448
|
+
"observed": {
|
|
1449
|
+
"ready": "запущено",
|
|
1450
|
+
"readyTools": "інструментів: {count}",
|
|
1451
|
+
"readyNoTools": "запущено, без інструментів",
|
|
1452
|
+
"failed": "не запустився",
|
|
1453
|
+
"needsAuth": "потрібна авторизація",
|
|
1454
|
+
"unknown": "стан невизначений",
|
|
1455
|
+
"notLoaded": "жодного разу не завантажено",
|
|
1456
|
+
"unattributed": "CLI агента повідомив про сервер, який цей крок не під’єднував: {id}."
|
|
1447
1457
|
}
|
|
1448
1458
|
},
|
|
1449
1459
|
"adherence": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.244.0",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.266.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|