@f5-sales-demo/xcsh 19.105.6 → 19.105.7

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [19.105.7] - 2026-07-31
6
+
7
+ ### Fixed
8
+
9
+ - Fixed exact `provider/model` searches in `/model` and restricted global agent configuration writes to owner-only permissions ([#2741](https://github.com/f5-sales-demo/xcsh/issues/2741))
10
+
5
11
  ## [19.105.6] - 2026-07-31
6
12
 
7
13
  ### Breaking Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.105.6",
4
+ "version": "19.105.7",
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.105.6",
61
- "@f5-sales-demo/pi-agent-core": "19.105.6",
62
- "@f5-sales-demo/pi-ai": "19.105.6",
63
- "@f5-sales-demo/pi-natives": "19.105.6",
64
- "@f5-sales-demo/pi-resource-management": "19.105.6",
65
- "@f5-sales-demo/pi-tui": "19.105.6",
66
- "@f5-sales-demo/pi-utils": "19.105.6",
60
+ "@f5-sales-demo/xcsh-stats": "19.105.7",
61
+ "@f5-sales-demo/pi-agent-core": "19.105.7",
62
+ "@f5-sales-demo/pi-ai": "19.105.7",
63
+ "@f5-sales-demo/pi-natives": "19.105.7",
64
+ "@f5-sales-demo/pi-resource-management": "19.105.7",
65
+ "@f5-sales-demo/pi-tui": "19.105.7",
66
+ "@f5-sales-demo/pi-utils": "19.105.7",
67
67
  "@sinclair/typebox": "^0.34",
68
68
  "@xterm/headless": "^6.0",
69
69
  "ajv": "^8.20",
@@ -0,0 +1,34 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { isEnoent } from "@f5-sales-demo/pi-utils";
4
+
5
+ const AGENT_CONFIG_DIR_MODE = 0o700;
6
+ const AGENT_CONFIG_FILE_MODE = 0o600;
7
+
8
+ export function hardenAgentConfigFileSync(filePath: string): void {
9
+ try {
10
+ fs.chmodSync(filePath, AGENT_CONFIG_FILE_MODE);
11
+ } catch (error) {
12
+ if (!isEnoent(error)) throw error;
13
+ }
14
+ }
15
+
16
+ export async function hardenAgentConfigFile(filePath: string): Promise<void> {
17
+ try {
18
+ await fs.promises.chmod(filePath, AGENT_CONFIG_FILE_MODE);
19
+ } catch (error) {
20
+ if (!isEnoent(error)) throw error;
21
+ }
22
+ }
23
+
24
+ export function writeAgentConfigFileSync(filePath: string, content: string): void {
25
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: AGENT_CONFIG_DIR_MODE });
26
+ hardenAgentConfigFileSync(filePath);
27
+ fs.writeFileSync(filePath, content, { encoding: "utf-8", mode: AGENT_CONFIG_FILE_MODE });
28
+ }
29
+
30
+ export async function writeAgentConfigFile(filePath: string, content: string): Promise<void> {
31
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: AGENT_CONFIG_DIR_MODE });
32
+ await hardenAgentConfigFile(filePath);
33
+ await fs.promises.writeFile(filePath, content, { encoding: "utf-8", mode: AGENT_CONFIG_FILE_MODE });
34
+ }
@@ -16,6 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import { $env, isEnoent, logger, readProviderFromModelsYml } from "@f5-sales-demo/pi-utils";
19
+ import { hardenAgentConfigFileSync, writeAgentConfigFileSync } from "./agent-config-file";
19
20
  import { DEFAULT_MODEL_ROLE } from "./settings-schema";
20
21
 
21
22
  /** Current config schema version. Bump when the generated format changes. */
@@ -189,6 +190,7 @@ const UNRESOLVABLE_DEFAULT_PROVIDER_PREFIXES = ["bench-instant/"];
189
190
  */
190
191
  export function healConfigYmlModelRoles(configPath: string): void {
191
192
  try {
193
+ hardenAgentConfigFileSync(configPath);
192
194
  const content = fs.readFileSync(configPath, "utf-8");
193
195
  if (!content.includes("modelRoles:")) return; // binary default applies
194
196
  const defaultLine = /^(\s*)default:\s*(\S+)\s*$/m;
@@ -197,7 +199,7 @@ export function healConfigYmlModelRoles(configPath: string): void {
197
199
  const [, indent, value] = match;
198
200
  if (UNRESOLVABLE_DEFAULT_PROVIDER_PREFIXES.some(prefix => value.startsWith(prefix))) {
199
201
  const healed = content.replace(defaultLine, `${indent}default: ${DEFAULT_MODEL_ROLE_VALUE}`);
200
- fs.writeFileSync(configPath, healed);
202
+ writeAgentConfigFileSync(configPath, healed);
201
203
  logger.debug("Healed config.yml: replaced unresolvable default modelRole", {
202
204
  configPath,
203
205
  previous: value,
@@ -244,8 +246,7 @@ function backupModelsConfigIfExists(filePath: string): boolean {
244
246
  /** Safely write a file, creating parent directories. */
245
247
  function safeWrite(filePath: string, content: string): boolean {
246
248
  try {
247
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
248
- fs.writeFileSync(filePath, content, "utf-8");
249
+ writeAgentConfigFileSync(filePath, content);
249
250
  return true;
250
251
  } catch (err) {
251
252
  logger.warn("Failed to write config file", { filePath, err });
@@ -35,6 +35,7 @@ import {
35
35
  } from "../modes/theme/theme";
36
36
  import { AgentStorage } from "../session/agent-storage";
37
37
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
38
+ import { hardenAgentConfigFile, writeAgentConfigFile } from "./agent-config-file";
38
39
  import { withFileLock } from "./file-lock";
39
40
  import {
40
41
  type BashInterceptorRule,
@@ -421,6 +422,7 @@ export class Settings {
421
422
 
422
423
  // Migrate from legacy formats if needed
423
424
  await this.#migrateFromLegacy();
425
+ await hardenAgentConfigFile(this.#configPath!);
424
426
 
425
427
  // Load global settings from config.yml
426
428
  this.#global = await this.#loadYaml(this.#configPath!);
@@ -504,7 +506,7 @@ export class Settings {
504
506
  // 3. Write merged settings
505
507
  if (migrated && Object.keys(settings).length > 0) {
506
508
  try {
507
- await Bun.write(this.#configPath, YAML.stringify(settings, null, 2));
509
+ await writeAgentConfigFile(this.#configPath, YAML.stringify(settings, null, 2));
508
510
  logger.debug("Settings: migrated to config.yml", { path: this.#configPath });
509
511
  } catch {}
510
512
  }
@@ -592,7 +594,7 @@ export class Settings {
592
594
 
593
595
  // Update our global with any external changes we preserved
594
596
  this.#global = current;
595
- await Bun.write(configPath, YAML.stringify(this.#global, null, 2));
597
+ await writeAgentConfigFile(configPath, YAML.stringify(this.#global, null, 2));
596
598
  });
597
599
  } catch (error) {
598
600
  logger.warn("Settings: save failed", { error: String(error) });
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.105.6",
21
- "commit": "6e89ab2a2b5debd13ff9fd062afcff3b904a3abe",
22
- "shortCommit": "6e89ab2",
20
+ "version": "19.105.7",
21
+ "commit": "83933f6196f53de668879a65f27dad74176403f5",
22
+ "shortCommit": "83933f6",
23
23
  "branch": "main",
24
- "tag": "v19.105.6",
25
- "commitDate": "2026-07-31T20:15:01Z",
26
- "buildDate": "2026-07-31T20:37:30.839Z",
24
+ "tag": "v19.105.7",
25
+ "commitDate": "2026-07-31T21:30:21Z",
26
+ "buildDate": "2026-07-31T21:51:19.386Z",
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/6e89ab2a2b5debd13ff9fd062afcff3b904a3abe",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.6"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/83933f6196f53de668879a65f27dad74176403f5",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.7"
33
33
  };
@@ -539,7 +539,11 @@ export class ModelSelectorComponent extends Container {
539
539
  this.#sortCanonicalModels(fuzzyMatches);
540
540
  this.#filteredCanonicalModels = fuzzyMatches;
541
541
  } else {
542
- const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
542
+ const fuzzyMatches = fuzzyFilter(
543
+ baseModels,
544
+ query,
545
+ ({ id, model, provider, selector }) => `${selector} ${id} ${provider} ${model.name}`,
546
+ );
543
547
  this.#sortModels(fuzzyMatches);
544
548
  this.#filteredModels = fuzzyMatches;
545
549
  }
@@ -7,6 +7,7 @@ import type { Component } from "@f5-sales-demo/pi-tui";
7
7
  import { Input, Loader, Spacer, Text } from "@f5-sales-demo/pi-tui";
8
8
  import { getAgentDbPath, getAgentDir, getConfigDirName, getProjectDir, t } from "@f5-sales-demo/pi-utils";
9
9
  import { invalidate as invalidateFsCache } from "../../capability/fs";
10
+ import { writeAgentConfigFileSync } from "../../config/agent-config-file";
10
11
  import {
11
12
  generateConfigYml,
12
13
  generateModelsYml,
@@ -997,7 +998,7 @@ export class SelectorController {
997
998
  // Create/heal config.yml for model defaults
998
999
  const configPath = path.join(path.dirname(modelsPath), "config.yml");
999
1000
  if (!fs.existsSync(configPath)) {
1000
- fs.writeFileSync(configPath, generateConfigYml());
1001
+ writeAgentConfigFileSync(configPath, generateConfigYml());
1001
1002
  }
1002
1003
  healConfigYmlModelRoles(configPath);
1003
1004
 
@@ -1234,7 +1235,7 @@ export class SelectorController {
1234
1235
 
1235
1236
  const configPath = path.join(path.dirname(modelsPath), "config.yml");
1236
1237
  if (!fs.existsSync(configPath)) {
1237
- fs.writeFileSync(configPath, generateConfigYml());
1238
+ writeAgentConfigFileSync(configPath, generateConfigYml());
1238
1239
  }
1239
1240
  healConfigYmlModelRoles(configPath);
1240
1241