@f5-sales-demo/xcsh 19.102.7 → 19.103.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.102.7",
4
+ "version": "19.103.0",
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.102.7",
61
- "@f5-sales-demo/pi-agent-core": "19.102.7",
62
- "@f5-sales-demo/pi-ai": "19.102.7",
63
- "@f5-sales-demo/pi-natives": "19.102.7",
64
- "@f5-sales-demo/pi-resource-management": "19.102.7",
65
- "@f5-sales-demo/pi-tui": "19.102.7",
66
- "@f5-sales-demo/pi-utils": "19.102.7",
60
+ "@f5-sales-demo/xcsh-stats": "19.103.0",
61
+ "@f5-sales-demo/pi-agent-core": "19.103.0",
62
+ "@f5-sales-demo/pi-ai": "19.103.0",
63
+ "@f5-sales-demo/pi-natives": "19.103.0",
64
+ "@f5-sales-demo/pi-resource-management": "19.103.0",
65
+ "@f5-sales-demo/pi-tui": "19.103.0",
66
+ "@f5-sales-demo/pi-utils": "19.103.0",
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://nferreira.staging.volterra.us";
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://nferreira.staging.volterra.us";
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://nferreira.staging.volterra.us";
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;
@@ -97,6 +97,25 @@ const REPO = "f5-sales-demo/api-specs-enriched";
97
97
  const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-spec-index.generated.ts");
98
98
  const catalogOutputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-catalog-index.generated.ts");
99
99
 
100
+ /**
101
+ * Upstream specs use ACME as a placeholder tenant, company and hostname. STYLE_GUIDE.md bans it:
102
+ * it is not trademark-cleared, and in TLS content the name already belongs to RFC 8555. Rewrite it
103
+ * to the `Example` pattern here rather than in the emitted file, so a regeneration cannot bring it
104
+ * back (#2650).
105
+ *
106
+ * `_acme-challenge` is the RFC 8555 DNS-01 record label, not the placeholder, and must survive.
107
+ */
108
+ function sanitizePlaceholders(text: string): string {
109
+ // The sentinel must contain no form of the word being rewritten, or the rewrite eats it.
110
+ const CHALLENGE = " RFC8555_DNS01 ";
111
+ return text
112
+ .replace(/_acme-challenge/gi, CHALLENGE)
113
+ .replace(/ACME/g, "Example")
114
+ .replace(/Acme/g, "Example")
115
+ .replace(/acme/g, "example")
116
+ .replaceAll(CHALLENGE, "_acme-challenge");
117
+ }
118
+
100
119
  const MAX_RETRIES = 3;
101
120
  const INITIAL_BACKOFF_MS = 2000;
102
121
 
@@ -552,7 +571,7 @@ const output = [
552
571
  .filter(l => l !== undefined)
553
572
  .join("\n");
554
573
 
555
- await Bun.write(outputPath, output);
574
+ await Bun.write(outputPath, sanitizePlaceholders(output));
556
575
 
557
576
  const outputSize = (Buffer.byteLength(output) / 1024 / 1024).toFixed(1);
558
577
  console.log(
@@ -596,7 +615,7 @@ if (catalog) {
596
615
  "",
597
616
  ].join("\n");
598
617
 
599
- await Bun.write(catalogOutputPath, catalogOutput);
618
+ await Bun.write(catalogOutputPath, sanitizePlaceholders(catalogOutput));
600
619
  const catalogSize = (Buffer.byteLength(catalogOutput) / 1024 / 1024).toFixed(1);
601
620
  console.log(
602
621
  `Generated ${path.relative(process.cwd(), catalogOutputPath)} (${categories.length} categories, ${catalogSize} MB)`,
@@ -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 || "r-mordasiewicz";
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}`
@@ -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://nferreira.staging.volterra.us",
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://nferreira.staging.volterra.us")
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-* (r-mordasiewicz) resources, parents first...");
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("r-mordasiewicz", ap, "ar-test-", cfg)) {
201
- await apiDelete(`/api/config/namespaces/r-mordasiewicz/${ap}/${name}`, cfg);
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://acme.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://acme.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://acme.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://acme.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://acme.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": {
@@ -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":"acme|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.102.7",
21
- "commit": "a955a498f6c995dae2640639ce70c4c5ec1e62b1",
22
- "shortCommit": "a955a49",
20
+ "version": "19.103.0",
21
+ "commit": "830a2618bfd67610b3b8bc93dcccd4d3417af10e",
22
+ "shortCommit": "830a261",
23
23
  "branch": "main",
24
- "tag": "v19.102.7",
25
- "commitDate": "2026-07-31T02:16:15Z",
26
- "buildDate": "2026-07-31T02:41:44.450Z",
24
+ "tag": "v19.103.0",
25
+ "commitDate": "2026-07-31T04:44:56Z",
26
+ "buildDate": "2026-07-31T05:04:35.541Z",
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/a955a498f6c995dae2640639ce70c4c5ec1e62b1",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.102.7"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/830a2618bfd67610b3b8bc93dcccd4d3417af10e",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.103.0"
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 nferreira staging tenant 2026-06-22 (UI delete confirmed; list row removed and API GET returned 404)."\n',
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":