@f5-sales-demo/xcsh 22.14.7 → 23.0.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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Breaking changes
8
+
9
+ - `request secrets encrypt` now emits bare base64; its policy retrieval requires `--name` and defaults to namespace `default`. Public documents default to camelCase YAML. The article consumer adds exactly one `string:///`. Canonical `blindfold` keeps snake_case JSON and location defaults.
10
+ - Both paths accept known camelCase/snake_case aliases and explicit encryption encoding; compatibility `--outfile` writes raw envelopes securely, `--outfmt` selects public document format, and public-key retrieval supports exact unsigned key versions. No legacy authentication or runtime vesctl dependency is introduced.
11
+
12
+
7
13
  ### Added
8
14
 
9
15
  - Added shared host-aware software setup with live management probes, reviewed package-manager routes, checksum-verified per-user archives, and asynchronous CLI/TUI setup preparation ([#4598](https://github.com/f5-sales-demo/xcsh/issues/4598)).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "22.14.7",
4
+ "version": "23.0.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",
@@ -68,13 +68,13 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@agentclientprotocol/sdk": "1.4.0",
71
- "@f5-sales-demo/pi-agent-core": "22.14.7",
72
- "@f5-sales-demo/pi-ai": "22.14.7",
73
- "@f5-sales-demo/pi-natives": "22.14.7",
74
- "@f5-sales-demo/pi-resource-management": "22.14.7",
75
- "@f5-sales-demo/pi-tui": "22.14.7",
76
- "@f5-sales-demo/pi-utils": "22.14.7",
77
- "@f5-sales-demo/xcsh-stats": "22.14.7",
71
+ "@f5-sales-demo/pi-agent-core": "23.0.0",
72
+ "@f5-sales-demo/pi-ai": "23.0.0",
73
+ "@f5-sales-demo/pi-natives": "23.0.0",
74
+ "@f5-sales-demo/pi-resource-management": "23.0.0",
75
+ "@f5-sales-demo/pi-tui": "23.0.0",
76
+ "@f5-sales-demo/pi-utils": "23.0.0",
77
+ "@f5-sales-demo/xcsh-stats": "23.0.0",
78
78
  "@mozilla/readability": "^0.6",
79
79
  "@resvg/resvg-wasm": "2.6.2",
80
80
  "@sinclair/typebox": "0.34.52",
package/src/cli.ts CHANGED
@@ -54,6 +54,7 @@ process.title = APP_NAME;
54
54
 
55
55
  const commands: CommandEntry[] = [
56
56
  { name: "context", load: () => import("./commands/context").then(m => m.default) },
57
+ { name: "request", load: () => import("./commands/request").then(m => m.default) },
57
58
  { name: "blindfold", load: () => import("./commands/blindfold").then(m => m.default) },
58
59
  { name: "remote-control", load: () => import("./commands/remote-control").then(m => m.default) },
59
60
  { name: "apply", load: () => import("./commands/apply").then(m => m.default) },
@@ -125,6 +126,17 @@ function requestsHelp(args: readonly string[]): boolean {
125
126
 
126
127
  /** Run the CLI with the given argv (no `process.argv` prefix). */
127
128
  export function runCli(argv: string[]): Promise<void> {
129
+ if (["blindfold", "request"].includes(argv[0] ?? "")) {
130
+ if (requestsHelp(argv.slice(1)))
131
+ return import("./commands/blindfold-help").then(m => m.showBlindfoldHelp(argv, APP_NAME));
132
+ return import("./commands/blindfold")
133
+ .then(m => m.runBlindfold(argv.slice(1), argv[0] === "request"))
134
+ .catch(error => {
135
+ if (!(error instanceof CliUsageError)) throw error;
136
+ writeCliOutput(process.stderr, `Error: ${error.message}\n`);
137
+ process.exitCode = 2;
138
+ });
139
+ }
128
140
  // --help and --version are handled by run() directly, don't rewrite those.
129
141
  // Everything else that isn't a known subcommand routes to "launch".
130
142
  // Keeping this routing boundary explicit makes the CLI fallback easy to audit.
@@ -0,0 +1,141 @@
1
+ import { resolve } from "node:path";
2
+ import { Args, CliUsageError, Flags, parseCommandArgv } from "@f5-sales-demo/pi-utils/cli";
3
+ import type { BlindfoldArgs, BlindfoldOperation } from "../services/blindfold";
4
+
5
+ export const blindfoldFlags = {
6
+ input: Flags.string({ description: "Secret filename or - for stdin" }),
7
+ "public-key": Flags.string({ description: "Public key JSON/YAML file; requires --policy-document (offline)" }),
8
+ "policy-document": Flags.string({ description: "Policy JSON/YAML file; requires --public-key (offline)" }),
9
+ cert: Flags.string({ description: "PEM certificate chain" }),
10
+ key: Flags.string({ description: "PEM private key" }),
11
+ bundle: Flags.string({ description: "PKCS#12 bundle" }),
12
+ name: Flags.string({ description: "Certificate name or secret policy name" }),
13
+ namespace: Flags.string({ char: "n", description: "Certificate namespace or secret policy namespace" }),
14
+ policy: Flags.string({ description: "Secret policy namespace/name (default shared/ves-io-allow-volterra)" }),
15
+ "passphrase-env": Flags.string({ description: "Passphrase environment variable name" }),
16
+ "context-name": Flags.string({ description: "Assert selected native context (online only)" }),
17
+ encoding: Flags.string({ description: "Encryption text encoding", options: ["base64", "location"] }),
18
+ outfile: Flags.string({ description: "New raw binary envelope file (0600; compatibility only)" }),
19
+ outfmt: Flags.string({ description: "Compatibility public document format", options: ["json", "yaml"] }),
20
+ "key-version": Flags.string({ description: "Unsigned 32-bit public key version; zero selects server default" }),
21
+ output: Flags.string({ description: "Public material format", options: ["json", "yaml"] }),
22
+ "output-file": Flags.string({ description: "New artifact file (0600)" }),
23
+ "result-file": Flags.string({ description: "New public JSON report file (0600)" }),
24
+ "dry-run": Flags.string({ description: "Validate without tenant writes", options: ["client"] }),
25
+ json: Flags.boolean({ description: "Print public JSON report" }),
26
+ };
27
+ const aliases = { "get-public-key": "public-key", "get-policy-document": "policy", encrypt: "encrypt" } as const;
28
+ const supported = ["public-key", "policy", "encrypt", "certificate", "create", "replace"];
29
+ const common = ["context-name", "output-file", "result-file", "json"];
30
+ export function flagsForBlindfold(operation: string, compatibility = false) {
31
+ const names =
32
+ operation === "public-key"
33
+ ? [...common, "output", "key-version", ...(compatibility ? ["outfmt"] : [])]
34
+ : operation === "policy"
35
+ ? [...common, "output", "namespace", "name", ...(compatibility ? ["outfmt"] : ["policy"])]
36
+ : operation === "encrypt"
37
+ ? [
38
+ ...common,
39
+ "input",
40
+ "policy",
41
+ "public-key",
42
+ "policy-document",
43
+ "encoding",
44
+ ...(compatibility ? ["outfile", "outfmt"] : []),
45
+ ]
46
+ : [...common, "cert", "key", "bundle", "name", "namespace", "policy", "passphrase-env", "dry-run"];
47
+ return Object.fromEntries(names.map(name => [name, blindfoldFlags[name as keyof typeof blindfoldFlags]]));
48
+ }
49
+ export function parseBlindfoldCli(
50
+ argv: readonly string[],
51
+ compatibility: boolean,
52
+ stdinIsTTY: boolean,
53
+ ): BlindfoldArgs & { json?: boolean } {
54
+ const parts = [...argv];
55
+ if (compatibility && parts.shift() !== "secrets") throw new CliUsageError("Only request secrets is supported");
56
+ const selected = parts.shift();
57
+ const operation = compatibility ? aliases[selected as keyof typeof aliases] : selected;
58
+ if (!operation || !supported.includes(operation)) throw new CliUsageError("Unknown Blindfold operation");
59
+ for (const arg of parts.slice(0, parts.includes("--") ? parts.indexOf("--") : undefined)) {
60
+ if (/^(--(?:config|server-urls|p12-bundle|cacert|cert|key|hw-key)(?:=|$)|-[acku])/.test(arg) && compatibility)
61
+ throw new CliUsageError("Legacy authentication is unsupported; configure xcsh context with xcsh context");
62
+ }
63
+ const parsed = parseCommandArgv(parts, {
64
+ flags: flagsForBlindfold(operation, compatibility),
65
+ args: operation === "encrypt" ? { file: Args.string() } : {},
66
+ });
67
+ if (parsed.argv.length > (operation === "encrypt" ? 1 : 0)) throw new CliUsageError("Surplus positional arguments");
68
+ const f = parsed.flags;
69
+ if (f.input !== undefined && parsed.args.file !== undefined)
70
+ throw new CliUsageError("Select one secret input source");
71
+ const args: BlindfoldArgs & { json?: boolean } = {
72
+ operation: operation as BlindfoldOperation,
73
+ compatibility,
74
+ encoding: f.encoding as "base64" | "location" | undefined,
75
+ outfile: f.outfile as string | undefined,
76
+ input: (f.input ?? parsed.args.file) as string | undefined,
77
+ publicKey: f["public-key"] as string | undefined,
78
+ policyDocument: f["policy-document"] as string | undefined,
79
+ cert: f.cert as string | undefined,
80
+ key: f.key as string | undefined,
81
+ bundle: f.bundle as string | undefined,
82
+ name: f.name as string | undefined,
83
+ namespace: f.namespace as string | undefined,
84
+ policy: f.policy as string | undefined,
85
+ passphraseEnv: f["passphrase-env"] as string | undefined,
86
+ contextName: f["context-name"] as string | undefined,
87
+ output: (f.output ?? (operation !== "encrypt" ? f.outfmt : undefined)) as "json" | "yaml" | undefined,
88
+ outputFile: f["output-file"] as string | undefined,
89
+ resultFile: f["result-file"] as string | undefined,
90
+ dryRun: f["dry-run"] as "client" | undefined,
91
+ json: f.json as boolean | undefined,
92
+ };
93
+ if (f.output !== undefined && f.outfmt !== undefined)
94
+ throw new CliUsageError("Conflicting public material format selectors");
95
+ if (f["key-version"] !== undefined) {
96
+ const version = String(f["key-version"]);
97
+ if (!/^[0-9]+$/.test(version) || !Number.isSafeInteger(Number(version)) || Number(version) > 0xffffffff)
98
+ throw new CliUsageError("Key version must be an unsigned 32-bit integer");
99
+ args.keyVersion = Number(version);
100
+ }
101
+ if (compatibility && operation === "policy") {
102
+ if (!args.name) throw new CliUsageError("Compatibility policy retrieval requires --name");
103
+ args.namespace ??= "default";
104
+ }
105
+ if (args.outfile && (args.outputFile || args.json || args.encoding))
106
+ throw new CliUsageError("Binary --outfile conflicts with textual encoding, --output-file or --json");
107
+ if (args.outfile && args.resultFile && resolve(args.outfile) === resolve(args.resultFile))
108
+ throw new CliUsageError("Artifact and report destinations must differ");
109
+ if (Boolean(args.publicKey) !== Boolean(args.policyDocument))
110
+ throw new CliUsageError("--public-key and --policy-document must be supplied together");
111
+ if (args.publicKey && args.contextName)
112
+ throw new CliUsageError("Offline encryption does not select a context; omit --context-name");
113
+ if (args.json && (args.outputFile || args.output)) throw new CliUsageError("Conflicting output selections");
114
+ if (operation === "policy" && args.policy && (args.namespace !== undefined || args.name !== undefined))
115
+ throw new CliUsageError("Cannot combine --policy with --namespace or --name");
116
+
117
+ const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
118
+ if (args.policy && (args.policy.split("/").length !== 2 || args.policy.split("/").some(v => !label.test(v))))
119
+ throw new CliUsageError("Policy must be namespace/name");
120
+ if ([args.name, args.namespace].some(v => v !== undefined && !label.test(v)))
121
+ throw new CliUsageError("Invalid resource namespace or name");
122
+ if (
123
+ [args.input, args.publicKey, args.policyDocument, args.outputFile, args.outfile, args.resultFile].some(
124
+ v => v === "",
125
+ )
126
+ )
127
+ throw new CliUsageError("File paths must be nonempty");
128
+ if (["certificate", "create", "replace"].includes(operation)) {
129
+ if (!args.name || (args.bundle ? args.cert || args.key : !args.cert || !args.key))
130
+ throw new CliUsageError("Use --name and either --bundle or --cert with --key");
131
+ }
132
+ if (args.outputFile && args.resultFile && resolve(args.outputFile) === resolve(args.resultFile))
133
+ throw new CliUsageError("Artifact and report destinations must differ");
134
+ if (operation === "encrypt" && args.input === undefined) {
135
+ if (stdinIsTTY) throw new CliUsageError("Encrypt requires an input filename or redirected stdin");
136
+ args.input = "-";
137
+ }
138
+ if (operation === "encrypt" && args.input === "-" && stdinIsTTY)
139
+ throw new CliUsageError("Encrypt stdin must be redirected");
140
+ return args;
141
+ }
@@ -0,0 +1,51 @@
1
+ import { type ArgDescriptor, Args, CliUsageError, Command, renderCommandHelp } from "@f5-sales-demo/pi-utils/cli";
2
+ import { writeCliOutput } from "@f5-sales-demo/pi-utils/cli-output";
3
+ import { flagsForBlindfold } from "./blindfold-args";
4
+ export function showBlindfoldHelp(argv: string[], bin: string): void {
5
+ const compatibility = argv[0] === "request";
6
+ const leaf = compatibility ? argv[2] : argv[1];
7
+ const operation =
8
+ (
9
+ { "get-public-key": "public-key", "get-policy-document": "policy", encrypt: "encrypt" } as Record<
10
+ string,
11
+ string
12
+ >
13
+ )[leaf ?? ""] ?? (!compatibility ? leaf : undefined);
14
+ if (compatibility && argv[1] && !["secrets", "-h", "--help"].includes(argv[1])) {
15
+ writeCliOutput(process.stderr, "Error: Only request secrets is supported\n");
16
+ process.exitCode = 2;
17
+ return;
18
+ }
19
+ if (!operation || ["--help", "-h"].includes(operation)) {
20
+ writeCliOutput(
21
+ process.stdout,
22
+ `${bin} ${compatibility ? "request secrets" : "blindfold"} ${compatibility ? "get-public-key | get-policy-document | encrypt" : "public-key | policy | encrypt | certificate | create | replace"}\nUse an operation followed by --help. Authentication uses xcsh context.\n`,
23
+ );
24
+ return;
25
+ }
26
+ try {
27
+ if (
28
+ !["public-key", "policy", "encrypt", "certificate", "create", "replace"].includes(operation) ||
29
+ (compatibility && !["get-public-key", "get-policy-document", "encrypt"].includes(leaf!))
30
+ )
31
+ throw new CliUsageError("Unknown Blindfold operation");
32
+ class Help extends Command {
33
+ static description =
34
+ operation === "encrypt"
35
+ ? "Encrypt an input file or redirected stdin; paired public material files select offline operation"
36
+ : operation === "policy"
37
+ ? "Retrieve a secret policy document; namespace/name select the policy"
38
+ : "Native Blindfold operation";
39
+ static flags = flagsForBlindfold(operation!, compatibility);
40
+ static args: Record<string, ArgDescriptor> =
41
+ operation === "encrypt"
42
+ ? { file: Args.string({ description: "Filename or -; omit for redirected stdin" }) }
43
+ : {};
44
+ async run() {}
45
+ }
46
+ renderCommandHelp(bin, argv.slice(0, compatibility ? 3 : 2).join(" "), Help);
47
+ } catch (error) {
48
+ writeCliOutput(process.stderr, `Error: ${error instanceof Error ? error.message : "invalid operation"}\n`);
49
+ process.exitCode = 2;
50
+ }
51
+ }
@@ -1,80 +1,55 @@
1
- import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
2
- import { Settings } from "../config/settings";
3
- import { BLINDFOLD_OPERATIONS, type BlindfoldOperation, BlindfoldService } from "../services/blindfold";
4
- import { ContextService } from "../services/xcsh-context";
1
+ import { Args, Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { writeCliOutput } from "@f5-sales-demo/pi-utils/cli-output";
5
3
 
4
+ import { BlindfoldService } from "../services/blindfold";
5
+ import type { ContextService } from "../services/xcsh-context";
6
+ import { blindfoldFlags, parseBlindfoldCli } from "./blindfold-args";
7
+ export async function runBlindfold(argv: string[], compatibility = false): Promise<void> {
8
+ const args = parseBlindfoldCli(argv, compatibility, Boolean(process.stdin.isTTY));
9
+ let context: ContextService | undefined;
10
+ const controller = new AbortController();
11
+ const cancel = () => controller.abort();
12
+ process.once("SIGINT", cancel);
13
+ try {
14
+ let env: Record<string, string | undefined> = {};
15
+ if (!args.publicKey) {
16
+ const { Settings } = await import("../config/settings");
17
+ const { ContextService } = await import("../services/xcsh-context");
18
+ await Settings.init({ cwd: process.cwd() });
19
+ context = await ContextService.getOrInit(undefined, process.cwd());
20
+ env = { ...(Settings.instance.get("bash.environment") as Record<string, string> | undefined), ...process.env };
21
+ }
22
+ const service = new BlindfoldService({
23
+ env,
24
+ emit: content => {
25
+ if (!args.json && !args.outputFile) writeCliOutput(process.stdout, content);
26
+ },
27
+ });
28
+ const report = await service.run(args, { signal: controller.signal });
29
+ if (args.json || args.outputFile || ["create", "replace"].includes(report.operation))
30
+ writeCliOutput(process.stdout, `${JSON.stringify(report, null, 2)}\n`);
31
+ } catch (error) {
32
+ writeCliOutput(
33
+ process.stderr,
34
+ `Blindfold: ${controller.signal.aborted ? "operation cancelled" : error instanceof Error ? error.message : "operation failed"}\n`,
35
+ );
36
+ process.exitCode = controller.signal.aborted ? 130 : 1;
37
+ } finally {
38
+ context?.stopRevalidation();
39
+ process.removeListener("SIGINT", cancel);
40
+ }
41
+ }
6
42
  export default class Blindfold extends Command {
7
43
  static description = "Encrypt secrets natively and create or rotate tenant TLS certificates";
8
44
  static args = {
9
45
  operation: Args.string({
10
46
  required: true,
11
- options: [...BLINDFOLD_OPERATIONS],
12
- description: "public-key, policy, encrypt, certificate, create, or replace",
47
+ description: "public-key, policy, encrypt, certificate, create, replace",
13
48
  }),
49
+ file: Args.string({ description: "Encryption input file or - for redirected stdin" }),
14
50
  };
15
- static flags = {
16
- input: Flags.string({ description: "Secret file to encrypt" }),
17
- cert: Flags.string({ description: "PEM certificate chain" }),
18
- key: Flags.string({ description: "PEM private key" }),
19
- bundle: Flags.string({ description: "PKCS#12 .p12/.pfx bundle" }),
20
- name: Flags.string({ description: "Certificate resource name" }),
21
- namespace: Flags.string({ char: "n", description: "Certificate namespace; defaults to XCSH_NAMESPACE" }),
22
- policy: Flags.string({ description: "Secret policy namespace/name", default: "shared/ves-io-allow-volterra" }),
23
- "passphrase-env": Flags.string({
24
- description: "Name of an environment variable containing the input passphrase",
25
- }),
26
- "context-name": Flags.string({ description: "Expected active context; fail if different" }),
27
- "output-file": Flags.string({ description: "Create a new encrypted artifact or public material file (0600)" }),
28
- "result-file": Flags.string({ description: "Create a new JSON report (0600)" }),
29
- "dry-run": Flags.string({ description: "Validate without tenant writes", options: ["client"] }),
30
- json: Flags.boolean({ description: "Print the public JSON report" }),
31
- };
51
+ static flags = blindfoldFlags;
32
52
  async run(): Promise<void> {
33
- const { args, flags } = await this.parse(Blindfold);
34
- let context: ContextService | undefined;
35
- const controller = new AbortController();
36
- const cancel = () => controller.abort();
37
- process.once("SIGINT", cancel);
38
- try {
39
- await Settings.init({ cwd: process.cwd() });
40
- context = await ContextService.getOrInit(undefined, process.cwd());
41
- const env = {
42
- ...(Settings.instance.get("bash.environment") as Record<string, string> | undefined),
43
- ...process.env,
44
- };
45
- const service = new BlindfoldService({
46
- env,
47
- emit: content => {
48
- if (!flags.json && !flags["output-file"]) process.stdout.write(content);
49
- },
50
- });
51
- const report = await service.run(
52
- {
53
- operation: args.operation as BlindfoldOperation,
54
- input: flags.input,
55
- cert: flags.cert,
56
- key: flags.key,
57
- bundle: flags.bundle,
58
- name: flags.name,
59
- namespace: flags.namespace,
60
- policy: flags.policy,
61
- passphraseEnv: flags["passphrase-env"],
62
- contextName: flags["context-name"],
63
- outputFile: flags["output-file"],
64
- resultFile: flags["result-file"],
65
- dryRun: flags["dry-run"] as "client" | undefined,
66
- },
67
- { signal: controller.signal },
68
- );
69
- if (flags.json || flags["output-file"] || ["create", "replace"].includes(report.operation))
70
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
71
- context.stopRevalidation();
72
- } catch (error) {
73
- process.stderr.write(`Blindfold: ${error instanceof Error ? error.message : "operation failed"}\n`);
74
- process.exitCode = 1;
75
- } finally {
76
- context?.stopRevalidation();
77
- process.removeListener("SIGINT", cancel);
78
- }
53
+ await runBlindfold(this.argv);
79
54
  }
80
55
  }
@@ -0,0 +1,12 @@
1
+ import { Args, Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runBlindfold } from "./blindfold";
3
+ export default class Request extends Command {
4
+ static description = "Native compatibility for request secrets get-public-key, get-policy-document, encrypt";
5
+ static args = {
6
+ secrets: Args.string({ required: true, options: ["secrets"] }),
7
+ operation: Args.string({ required: true, options: ["get-public-key", "get-policy-document", "encrypt"] }),
8
+ };
9
+ async run(): Promise<void> {
10
+ await runBlindfold(this.argv, true);
11
+ }
12
+ }