@f5-sales-demo/xcsh 19.102.8 → 19.103.1
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/package.json +8 -8
- package/scripts/capture-ax-fixture.ts +1 -1
- package/scripts/capture-login-fixture.ts +1 -1
- package/scripts/extension-uat-harness.ts +1 -1
- package/scripts/generate-console-catalog.ts +17 -1
- package/scripts/uat-matrix-modalities.ts +1 -1
- package/scripts/uat-matrix.ts +5 -5
- package/src/browser/chat-conformance.json +5 -5
- package/src/commands/manager.ts +1 -1
- package/src/discovery/start-folder.ts +108 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +13 -13
- package/src/modes/components/status-line/git-utils.ts +32 -4
- package/src/prompts/system/start-folder.md +23 -0
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/system-prompt.ts +52 -1
- package/src/utils/git.ts +17 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.103.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
59
59
|
"@mozilla/readability": "^0.6",
|
|
60
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "19.103.1",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "19.103.1",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "19.103.1",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "19.103.1",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "19.103.1",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "19.103.1",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "19.103.1",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import puppeteer from "puppeteer";
|
|
5
5
|
|
|
6
6
|
const CHROME = process.env.CHROME_PATH;
|
|
7
|
-
const BASE = process.env.XCSH_CONSOLE_URL ?? "https://
|
|
7
|
+
const BASE = process.env.XCSH_CONSOLE_URL ?? "https://example.staging.volterra.us";
|
|
8
8
|
const NS = process.env.XCSH_NS ?? "demo";
|
|
9
9
|
const OUT = "test/browser/fixtures/xc-http-lb-create.ax.json";
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import puppeteer from "puppeteer";
|
|
14
14
|
|
|
15
15
|
const CHROME = process.env.CHROME_PATH;
|
|
16
|
-
const BASE = process.env.XCSH_API_URL ?? "https://
|
|
16
|
+
const BASE = process.env.XCSH_API_URL ?? "https://example.staging.volterra.us";
|
|
17
17
|
const PROFILE = "/tmp/xc-login-capture-profile";
|
|
18
18
|
const LOGIN_OUT = "test/browser/fixtures/xc-login-wall.html";
|
|
19
19
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type BridgeServer, startBridgeServer } from "../src/browser/extension-bridge";
|
|
17
17
|
|
|
18
|
-
const BASE = process.env.XCSH_API_URL ?? "https://
|
|
18
|
+
const BASE = process.env.XCSH_API_URL ?? "https://example.staging.volterra.us";
|
|
19
19
|
const ROUTE = `${BASE}/web/workspaces/web-app-and-api-protection/namespaces/demo/manage/load_balancers/http_loadbalancers`;
|
|
20
20
|
|
|
21
21
|
let server: BridgeServer;
|
|
@@ -6,6 +6,22 @@ import * as path from "node:path";
|
|
|
6
6
|
const repoRoot = path.resolve(import.meta.dir, "..");
|
|
7
7
|
const outputPath = path.join(repoRoot, "src/internal-urls/console-catalog.generated.ts");
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The catalog is captured from a live console, so it carries whatever tenant the operator who
|
|
11
|
+
* captured it was signed in to — typically a personal staging tenant named after them. That is a
|
|
12
|
+
* real identifier and must not be committed to a public repository (#2659). Rewrite it at the write
|
|
13
|
+
* site so a re-capture cannot reintroduce it.
|
|
14
|
+
*/
|
|
15
|
+
function sanitizeTenantHosts(text: string): string {
|
|
16
|
+
return (
|
|
17
|
+
text
|
|
18
|
+
.replace(/\b[a-z0-9][a-z0-9-]*\.staging\.volterra\.us\b/gi, "example.staging.volterra.us")
|
|
19
|
+
// Catalog prose names the tenant too ("on the live <name> staging tenant"). Matched by shape
|
|
20
|
+
// rather than by the person's name, so this file never has to carry the identifier it removes.
|
|
21
|
+
.replace(/\b(?!example\b)[A-Za-z][\w.-]*(?=\s+staging\s+tenant\b)/g, "example")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
function resolveCatalogRoot(): string | null {
|
|
10
26
|
const fromEnv = process.env.CONSOLE_CATALOG_DIR;
|
|
11
27
|
if (fromEnv && fs.existsSync(path.join(fromEnv, "catalog"))) return fromEnv;
|
|
@@ -82,7 +98,7 @@ const output = [
|
|
|
82
98
|
"",
|
|
83
99
|
].join("\n");
|
|
84
100
|
|
|
85
|
-
await Bun.write(outputPath, output);
|
|
101
|
+
await Bun.write(outputPath, sanitizeTenantHosts(output));
|
|
86
102
|
console.log(
|
|
87
103
|
`Wrote ${outputPath} (workflows=${Object.keys(data.workflows).length}, resources=${Object.keys(data.resources).length})`,
|
|
88
104
|
);
|
|
@@ -259,7 +259,7 @@ export async function runJson(p: CrudPhrase, cfg: MatrixConfig): Promise<Cell> {
|
|
|
259
259
|
detail: "skip_crud_test=true",
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
|
-
const ns = p.api_namespace || "
|
|
262
|
+
const ns = p.api_namespace || "example-corp";
|
|
263
263
|
const verifyPath =
|
|
264
264
|
p.namespace_scoped === false
|
|
265
265
|
? `/api/web/namespaces/${p.resource_name}`
|
package/scripts/uat-matrix.ts
CHANGED
|
@@ -122,10 +122,10 @@ function applyLimitFilter<T>(items: T[], args: Args, idOf: (t: T) => string): T[
|
|
|
122
122
|
function cfgFromEnv(args: Args): MatrixConfig {
|
|
123
123
|
return {
|
|
124
124
|
xcshBin: process.env.XCSH_BIN ?? "xcsh",
|
|
125
|
-
apiUrl: process.env.XCSH_API_URL ?? "https://
|
|
125
|
+
apiUrl: process.env.XCSH_API_URL ?? "https://example.staging.volterra.us",
|
|
126
126
|
apiToken: process.env.XCSH_API_TOKEN ?? "",
|
|
127
127
|
consoleNamespace: process.env.XCSH_NAMESPACE ?? "demo",
|
|
128
|
-
tenant: (process.env.XCSH_API_URL ?? "https://
|
|
128
|
+
tenant: (process.env.XCSH_API_URL ?? "https://example.staging.volterra.us")
|
|
129
129
|
.replace(/^https?:\/\//, "")
|
|
130
130
|
.split(".")[0]!,
|
|
131
131
|
observable: args.observable,
|
|
@@ -189,7 +189,7 @@ async function listNames(ns: string, apiPath: string, prefix: string, cfg: Matri
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
async function cleanup(cfg: MatrixConfig): Promise<void> {
|
|
192
|
-
console.log("\n[cleanup] deleting uat-* (demo) and ar-test-* (
|
|
192
|
+
console.log("\n[cleanup] deleting uat-* (demo) and ar-test-* (example-corp) resources, parents first...");
|
|
193
193
|
for (const ap of DEMO_CLEANUP_PATHS) {
|
|
194
194
|
for (const name of await listNames(cfg.consoleNamespace, ap, "uat-", cfg)) {
|
|
195
195
|
const code = await apiDelete(`/api/config/namespaces/${cfg.consoleNamespace}/${ap}/${name}`, cfg);
|
|
@@ -197,8 +197,8 @@ async function cleanup(cfg: MatrixConfig): Promise<void> {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
for (const ap of AR_CLEANUP_PATHS) {
|
|
200
|
-
for (const name of await listNames("
|
|
201
|
-
await apiDelete(`/api/config/namespaces/
|
|
200
|
+
for (const name of await listNames("example-corp", ap, "ar-test-", cfg)) {
|
|
201
|
+
await apiDelete(`/api/config/namespaces/example-corp/${ap}/${name}`, cfg);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
console.log("[cleanup] done");
|
|
@@ -669,7 +669,7 @@
|
|
|
669
669
|
"v": 1,
|
|
670
670
|
"capturedAt": 1719000000000,
|
|
671
671
|
"tabId": 7,
|
|
672
|
-
"url": "https://example.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
672
|
+
"url": "https://example-corp.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
673
673
|
"path": "/web/namespaces/default/http_loadbalancers/lb1",
|
|
674
674
|
"title": "lb1 — Distributed Cloud",
|
|
675
675
|
"ax": {
|
|
@@ -707,7 +707,7 @@
|
|
|
707
707
|
"v": 1,
|
|
708
708
|
"capturedAt": 1719000000000,
|
|
709
709
|
"tabId": 7,
|
|
710
|
-
"url": "https://example.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
710
|
+
"url": "https://example-corp.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
711
711
|
"path": "/web/namespaces/default/http_loadbalancers/lb1",
|
|
712
712
|
"title": "lb1 — Distributed Cloud",
|
|
713
713
|
"ax": {
|
|
@@ -922,7 +922,7 @@
|
|
|
922
922
|
"v": 1,
|
|
923
923
|
"capturedAt": 1719000000000,
|
|
924
924
|
"tabId": 7,
|
|
925
|
-
"url": "https://example.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
925
|
+
"url": "https://example-corp.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
926
926
|
"path": "/web/namespaces/default/http_loadbalancers/lb1",
|
|
927
927
|
"title": "lb1 — Distributed Cloud",
|
|
928
928
|
"ax": {
|
|
@@ -967,7 +967,7 @@
|
|
|
967
967
|
"v": 1,
|
|
968
968
|
"capturedAt": 1719000000000,
|
|
969
969
|
"tabId": 7,
|
|
970
|
-
"url": "https://example.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
970
|
+
"url": "https://example-corp.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
971
971
|
"path": "/web/namespaces/default/http_loadbalancers/lb1",
|
|
972
972
|
"title": "lb1 — Distributed Cloud",
|
|
973
973
|
"ax": {
|
|
@@ -1027,7 +1027,7 @@
|
|
|
1027
1027
|
"v": 2,
|
|
1028
1028
|
"capturedAt": 1719000000000,
|
|
1029
1029
|
"tabId": 7,
|
|
1030
|
-
"url": "https://example.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
1030
|
+
"url": "https://example-corp.console.ves.volterra.io/web/namespaces/default/http_loadbalancers/lb1",
|
|
1031
1031
|
"path": "/web/namespaces/default/http_loadbalancers/lb1",
|
|
1032
1032
|
"title": "lb1 — Distributed Cloud",
|
|
1033
1033
|
"ax": {
|
package/src/commands/manager.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* override `XCSH_MANAGER_SOCK`) for NDJSON control frames — one JSON object per
|
|
7
7
|
* line — validated by the pure `manager-core` protocol:
|
|
8
8
|
*
|
|
9
|
-
* {"type":"provision","sessionId":"tab-7","tenant":"example|staging"}
|
|
9
|
+
* {"type":"provision","sessionId":"tab-7","tenant":"example-corp|staging"}
|
|
10
10
|
* → spawn a worker for that sessionId (idempotent). The registry is keyed
|
|
11
11
|
* on sessionId, so two tabs of the SAME tenant get two workers.
|
|
12
12
|
* {"type":"release","sessionId":"tab-7"} → kill + forget that session's worker
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of directory this xcsh instance was started in.
|
|
3
|
+
*
|
|
4
|
+
* The binary maps to a session by its start folder, and those folders are not the same
|
|
5
|
+
* kind of thing. Some are GitHub checkouts where version control is the point. Some hold
|
|
6
|
+
* tenant automations, lab state, credentials and troubleshooting captures that must never
|
|
7
|
+
* reach a repository. Without this distinction the agent will helpfully offer to `git init`
|
|
8
|
+
* the second kind.
|
|
9
|
+
*
|
|
10
|
+
* `classifyRepo` in ../internal-urls/fleet-resolve.ts cannot answer it: it collapses "not a
|
|
11
|
+
* repository at all" and "a repository in an unrecognised org" into the same unclassified
|
|
12
|
+
* verdict, which is precisely the distinction needed here.
|
|
13
|
+
*/
|
|
14
|
+
import { parseGitHubRepo } from "../modes/components/status-line/git-utils";
|
|
15
|
+
import * as git from "../utils/git";
|
|
16
|
+
|
|
17
|
+
export type StartFolderKind =
|
|
18
|
+
/** In a repository with a GitHub remote. Git and GitHub work are both in scope. */
|
|
19
|
+
| "github"
|
|
20
|
+
/** In a repository, but not one on GitHub. Version control is fine; GitHub actions are not. */
|
|
21
|
+
| "git"
|
|
22
|
+
/** Not in a repository. May hold secrets, so publishing it is never volunteered. */
|
|
23
|
+
| "plain";
|
|
24
|
+
|
|
25
|
+
export interface StartFolder {
|
|
26
|
+
readonly kind: StartFolderKind;
|
|
27
|
+
/** `owner/repo`, present only when `kind` is `"github"`. */
|
|
28
|
+
readonly slug?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The start folder is itself git-ignored: inside a repository, but deliberately
|
|
31
|
+
* excluded from it. Set only when it is — absent reads as "not ignored".
|
|
32
|
+
*/
|
|
33
|
+
readonly ignored?: true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Injected so the branches are testable without a real repository on disk. */
|
|
37
|
+
export interface StartFolderDeps {
|
|
38
|
+
/** Repository root for `cwd`, or null when `cwd` is not in one. */
|
|
39
|
+
repoRoot(cwd: string, signal?: AbortSignal): Promise<string | null>;
|
|
40
|
+
/** URL of the `origin` remote, or null when there is none. */
|
|
41
|
+
originUrl(cwd: string, signal?: AbortSignal): Promise<string | null>;
|
|
42
|
+
/** Whether `cwd` itself is excluded by gitignore rules. */
|
|
43
|
+
isIgnored(cwd: string, signal?: AbortSignal): Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const defaultStartFolderDeps: StartFolderDeps = {
|
|
47
|
+
repoRoot: (cwd, signal) => git.repo.root(cwd, signal),
|
|
48
|
+
// `remote.url` reports "no such remote" as undefined; this interface uses null for
|
|
49
|
+
// absent throughout, so normalise here rather than accepting both downstream.
|
|
50
|
+
originUrl: async (cwd, signal) => (await git.remote.url(cwd, "origin", signal)) ?? null,
|
|
51
|
+
isIgnored: (cwd, signal) => git.repo.ignored(cwd, signal),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The decision, with the probing already done.
|
|
56
|
+
*
|
|
57
|
+
* `parseGitHubRepo` owns what counts as a GitHub remote — it handles https, scp-style ssh
|
|
58
|
+
* and `git://`, and rejects look-alike hosts. Re-deriving that here would be a second
|
|
59
|
+
* definition to keep in step.
|
|
60
|
+
*/
|
|
61
|
+
export function classifyStartFolder(repoRoot: string | null, originUrl: string | null, ignored = false): StartFolder {
|
|
62
|
+
if (!repoRoot) return { kind: "plain" };
|
|
63
|
+
const slug = originUrl ? parseGitHubRepo(originUrl) : null;
|
|
64
|
+
const base: StartFolder = slug ? { kind: "github", slug } : { kind: "git" };
|
|
65
|
+
// The kind stays accurate — the repository is real — and the exclusion rides alongside
|
|
66
|
+
// it, rather than pretending a checkout is not a checkout.
|
|
67
|
+
return ignored ? { ...base, ignored: true } : base;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Probe `cwd` and classify it, failing in the safe direction at every step.
|
|
72
|
+
*
|
|
73
|
+
* "Safe" means withholding scope, never inventing it. If we cannot tell whether this is a
|
|
74
|
+
* repository, we say it is not — so a broken probe costs a suggestion rather than leaking a
|
|
75
|
+
* secret. If we know it IS a repository but cannot read the remote, we say `git`: claiming
|
|
76
|
+
* `plain` there would be false and would suppress legitimate version-control work, while
|
|
77
|
+
* `git` is true and still withholds GitHub.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveStartFolder(
|
|
80
|
+
cwd: string,
|
|
81
|
+
deps: StartFolderDeps = defaultStartFolderDeps,
|
|
82
|
+
signal?: AbortSignal,
|
|
83
|
+
): Promise<StartFolder> {
|
|
84
|
+
let root: string | null;
|
|
85
|
+
try {
|
|
86
|
+
root = await deps.repoRoot(cwd, signal);
|
|
87
|
+
} catch {
|
|
88
|
+
return { kind: "plain" };
|
|
89
|
+
}
|
|
90
|
+
if (!root) return { kind: "plain" };
|
|
91
|
+
|
|
92
|
+
let origin: string | null = null;
|
|
93
|
+
try {
|
|
94
|
+
origin = await deps.originUrl(cwd, signal);
|
|
95
|
+
} catch {
|
|
96
|
+
// Repository confirmed, remote unknown — see above.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let ignored = false;
|
|
100
|
+
try {
|
|
101
|
+
ignored = await deps.isIgnored(cwd, signal);
|
|
102
|
+
} catch {
|
|
103
|
+
// `check-ignore` reports "not ignored" as exit 1, not as a failure, so a throw here
|
|
104
|
+
// means git itself is broken — and `repoRoot` already succeeded. Treat it as not
|
|
105
|
+
// ignored rather than cautioning in every repository on a broken probe.
|
|
106
|
+
}
|
|
107
|
+
return classifyStartFolder(root, origin, ignored);
|
|
108
|
+
}
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.103.1",
|
|
21
|
+
"commit": "7eb3957caf90129cfac8291f15746e0f47eb63dc",
|
|
22
|
+
"shortCommit": "7eb3957",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.103.1",
|
|
25
|
+
"commitDate": "2026-07-31T06:21:34Z",
|
|
26
|
+
"buildDate": "2026-07-31T06:47:40.081Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/7eb3957caf90129cfac8291f15746e0f47eb63dc",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.103.1"
|
|
33
33
|
};
|
|
@@ -396,7 +396,7 @@ export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
|
396
396
|
"health-check/create":
|
|
397
397
|
'schema: "urn:xcsh:console:workflow:v1"\nid: "health-check-create"\nlabel: "Create Health Check"\nresource: "health-check"\noperation: "create"\n\npreconditions:\n - "user_logged_in"\n - "namespace_selected"\n - "role_minimum: admin"\n\nparams:\n namespace:\n required: true\n description: "Target namespace"\n example: "demo"\n name:\n required: true\n description: "Health check name (lowercase alphanumeric and hyphens)"\n example: "example-health-check"\n health_check_type:\n required: false\n description: "Health check protocol type"\n example: "HTTP HealthCheck"\n default: "HTTP HealthCheck"\n timeout:\n required: false\n type: "number"\n description: "Timeout for each health check probe in seconds"\n example: 3\n default: 3\n interval:\n required: false\n type: "number"\n description: "Interval between health check probes in seconds"\n example: 15\n default: 15\n unhealthy_threshold:\n required: false\n type: "number"\n description: "Number of consecutive failures before marking unhealthy"\n example: 1\n default: 1\n healthy_threshold:\n required: false\n type: "number"\n description: "Number of consecutive successes before marking healthy"\n example: 3\n default: 3\n\nsteps:\n - id: "navigate-to-list"\n action: "navigate"\n url: "/web/workspaces/web-app-and-api-protection/namespaces/{namespace}/manage/load_balancers/health_checks"\n wait_for: "text(\'Health Checks\')"\n description: "Navigate to Health Checks list page within Web App & API Protection workspace"\n\n - id: "click-add-tab"\n action: "click"\n selector: "text(\'Add Health Check\')"\n wait_for: "textbox[name=\'Name\']"\n description: "Click the Add Health Check tab to open the inline create form"\n note: "This is a tab element, not a button"\n\n - id: "fill-name"\n action: "fill"\n selector: "textbox[name=\'Name\']"\n value: "{name}"\n description: "Enter the health check name in the Name textbox"\n\n - id: "select-type"\n condition: "params.health_check_type is set"\n action: "select"\n selector: "listbox"\n context: "Health Check Parameters section"\n value: "{health_check_type}"\n description: "Select the health check type (HTTP HealthCheck or TCP HealthCheck)"\n\n - id: "set-timeout"\n condition: "params.timeout is set"\n action: "fill"\n selector: "spinbutton[name=\'Timeout\']"\n value: "{timeout}"\n description: "Set the health check timeout value"\n\n - id: "set-interval"\n condition: "params.interval is set"\n action: "fill"\n selector: "spinbutton[name=\'Interval\']"\n value: "{interval}"\n description: "Set the health check interval value"\n\n - id: "set-unhealthy-threshold"\n condition: "params.unhealthy_threshold is set"\n action: "fill"\n selector: "spinbutton[name=\'Unhealthy Threshold\']"\n value: "{unhealthy_threshold}"\n description: "Set the unhealthy threshold value"\n\n - id: "set-healthy-threshold"\n condition: "params.healthy_threshold is set"\n action: "fill"\n selector: "spinbutton[name=\'Healthy Threshold\']"\n value: "{healthy_threshold}"\n description: "Set the healthy threshold value"\n\n - id: "save"\n action: "click"\n selector: "[class*=\'save-bt\']"\n wait_for: "text(\'{name}\')"\n wait_timeout_ms: 60000\n description: "Click the Add Health Check button to save. The \'button\' role disambiguates the footer save button from the same-named tab, so no section context is needed. The wait_for confirms the new resource\'s name appears in the list, which is the creation verification."\n\npostconditions:\n - "resource_list_page_visible"\n - "resource_name_in_list: {name}"\n\nmetadata:\n confidence: "validated"\n discovered_at: "2026-06-15"\n validated_at: "2026-06-18"\n console_version: "2025.06"\n estimated_duration_seconds: 25\n notes: "Selectors use aria/text-based patterns validated against live console. No data-testid attributes exist. Has Show Advanced Fields toggle for additional parameters."\n',
|
|
398
398
|
"health-check/delete":
|
|
399
|
-
'schema: "urn:xcsh:console:workflow:v1"\nid: "health-check-delete"\nlabel: "Delete Health Check"\nresource: "health-check"\noperation: "delete"\n\npreconditions:\n - "user_logged_in"\n - "namespace_selected"\n - "role_minimum: admin"\n - "resource_exists: {name}"\n\nparams:\n namespace:\n required: true\n description: "Namespace containing the health check"\n example: "demo"\n name:\n required: true\n description: "Name of the health check to delete"\n example: "example-health-check"\n\n# Selectors discovered against the live console 2026-06-22. The console has NO\n# data-testid attributes; it uses ves-e2e-test. The per-row actions menu is a\n# shared CrudComponent: a kebab button opens a listbox whose options include\n# Delete, which opens a confirmation dialog with Cancel / Delete buttons.\nsteps:\n - id: "navigate-to-list"\n action: "navigate"\n url: "/web/workspaces/web-app-and-api-protection/namespaces/{namespace}/manage/load_balancers/health_checks"\n wait_for: "text(\'Health Checks\')"\n description: "Navigate to the Health Check list page (Web App & API Protection workspace)"\n\n - id: "scroll-actions-into-view"\n action: "scroll"\n selector: "row:has-text(\'{name}\') >> [ves-e2e-test=\'row-action-dropdown\']"\n context: "row containing {name}"\n description: "The data table is wider than the viewport and the Actions column sits off-screen-right; scroll the row\'s actions button into view before clicking. Avoids enlarging the window."\n note: "For a multi-row list, scope to the datatable-body-row whose cells contain {name}; for a single-row list the first row-action-dropdown is the target."\n\n - id: "open-row-actions"\n action: "click"\n selector: "row:has-text(\'{name}\') >> [ves-e2e-test=\'row-action-dropdown\']"\n context: "row containing {name}"\n wait_for: "option:text(\'Delete\')"\n description: "Click the row kebab to open the actions listbox. Requires a trusted (CDP) click — synthetic JS clicks fire outside Angular\'s zone and never render the overlay."\n\n - id: "click-delete"\n action: "click"\n selector: "option:text(\'Delete\')"\n wait_for: "button:text(\'Delete\')"\n description: "Click the Delete option (ves-e2e-test=\'row-action-item-delete\') to open the confirmation dialog."\n\n - id: "confirm-delete"\n action: "click"\n selector: "button:text(\'Delete\')"\n context: "confirmation dialog"\n wait_for: "text(\'Health Checks\')"\n wait_timeout_ms: 15000\n description: "Click the dialog\'s Delete button to confirm. Cancel via button:text(\'Cancel\') aborts without deleting."\n\n - id: "verify-on-list"\n action: "assert"\n selector: "text(\'Health Checks\')"\n expected_text: "Health Checks"\n description: "Confirm the list page is shown after deletion (assert requires expected_text). Deletion itself is verified independently by the API cross-check."\n\npostconditions:\n - "resource_removed_from_list"\n - "list_page_visible"\n\nmetadata:\n confidence: "validated"\n discovered_at: "2026-06-22"\n console_version: "2025.06"\n estimated_duration_seconds: 15\n selector_scheme: "ves-e2e-test + aria (no data-testid exists)"\n notes: "Validated end-to-end on the live
|
|
399
|
+
'schema: "urn:xcsh:console:workflow:v1"\nid: "health-check-delete"\nlabel: "Delete Health Check"\nresource: "health-check"\noperation: "delete"\n\npreconditions:\n - "user_logged_in"\n - "namespace_selected"\n - "role_minimum: admin"\n - "resource_exists: {name}"\n\nparams:\n namespace:\n required: true\n description: "Namespace containing the health check"\n example: "demo"\n name:\n required: true\n description: "Name of the health check to delete"\n example: "example-health-check"\n\n# Selectors discovered against the live console 2026-06-22. The console has NO\n# data-testid attributes; it uses ves-e2e-test. The per-row actions menu is a\n# shared CrudComponent: a kebab button opens a listbox whose options include\n# Delete, which opens a confirmation dialog with Cancel / Delete buttons.\nsteps:\n - id: "navigate-to-list"\n action: "navigate"\n url: "/web/workspaces/web-app-and-api-protection/namespaces/{namespace}/manage/load_balancers/health_checks"\n wait_for: "text(\'Health Checks\')"\n description: "Navigate to the Health Check list page (Web App & API Protection workspace)"\n\n - id: "scroll-actions-into-view"\n action: "scroll"\n selector: "row:has-text(\'{name}\') >> [ves-e2e-test=\'row-action-dropdown\']"\n context: "row containing {name}"\n description: "The data table is wider than the viewport and the Actions column sits off-screen-right; scroll the row\'s actions button into view before clicking. Avoids enlarging the window."\n note: "For a multi-row list, scope to the datatable-body-row whose cells contain {name}; for a single-row list the first row-action-dropdown is the target."\n\n - id: "open-row-actions"\n action: "click"\n selector: "row:has-text(\'{name}\') >> [ves-e2e-test=\'row-action-dropdown\']"\n context: "row containing {name}"\n wait_for: "option:text(\'Delete\')"\n description: "Click the row kebab to open the actions listbox. Requires a trusted (CDP) click — synthetic JS clicks fire outside Angular\'s zone and never render the overlay."\n\n - id: "click-delete"\n action: "click"\n selector: "option:text(\'Delete\')"\n wait_for: "button:text(\'Delete\')"\n description: "Click the Delete option (ves-e2e-test=\'row-action-item-delete\') to open the confirmation dialog."\n\n - id: "confirm-delete"\n action: "click"\n selector: "button:text(\'Delete\')"\n context: "confirmation dialog"\n wait_for: "text(\'Health Checks\')"\n wait_timeout_ms: 15000\n description: "Click the dialog\'s Delete button to confirm. Cancel via button:text(\'Cancel\') aborts without deleting."\n\n - id: "verify-on-list"\n action: "assert"\n selector: "text(\'Health Checks\')"\n expected_text: "Health Checks"\n description: "Confirm the list page is shown after deletion (assert requires expected_text). Deletion itself is verified independently by the API cross-check."\n\npostconditions:\n - "resource_removed_from_list"\n - "list_page_visible"\n\nmetadata:\n confidence: "validated"\n discovered_at: "2026-06-22"\n console_version: "2025.06"\n estimated_duration_seconds: 15\n selector_scheme: "ves-e2e-test + aria (no data-testid exists)"\n notes: "Validated end-to-end on the live example staging tenant 2026-06-22 (UI delete confirmed; list row removed and API GET returned 404)."\n',
|
|
400
400
|
"health-check/read":
|
|
401
401
|
"schema: urn:xcsh:console:workflow:v1\nid: health-check-read\nlabel: Read Health Checks\nresource: health-check\noperation: read\nparams:\n namespace:\n required: true\n example: demo\n name:\n required: true\n example: example-health-check\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/web-app-and-api-protection/namespaces/{namespace}/manage/load_balancers/health_checks\n wait_for: text('Health Checks')\n description: Navigate to Health Checks list page\n - id: scroll-actions-into-view\n action: scroll\n selector: row:has-text('{name}') >> [ves-e2e-test='row-action-dropdown']\n description: Scroll the Actions column into view for the target row\n - id: open-row-actions\n action: click\n selector: row:has-text('{name}') >> [ves-e2e-test='row-action-dropdown']\n wait_for: option:text('Manage Configuration')\n description: Open the row kebab menu\n note: F5 XC rows have no clickable name → detail page; Read opens the config via the kebab\n - id: open-configuration\n action: click\n selector: option:text('Manage Configuration')\n wait_for: button:text('Edit Configuration')\n wait_timeout_ms: 15000\n description: Open the read-only Manage Configuration view\n note: The config opens read-only (Form/JSON tabs); the agent reads field values via get_page_text/javascript_tool. The\n 'Edit Configuration' button confirms the view loaded.\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: \"2025.06\"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n",
|
|
402
402
|
"health-check/update":
|