@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 +6 -0
- package/package.json +8 -8
- package/src/config/agent-config-file.ts +34 -0
- package/src/config/auto-config.ts +4 -3
- package/src/config/settings.ts +4 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/model-selector.ts +5 -1
- package/src/modes/controllers/selector-controller.ts +3 -2
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.
|
|
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.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.105.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.105.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.105.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.105.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.105.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.105.
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.105.7",
|
|
21
|
+
"commit": "83933f6196f53de668879a65f27dad74176403f5",
|
|
22
|
+
"shortCommit": "83933f6",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.105.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1238
|
+
writeAgentConfigFileSync(configPath, generateConfigYml());
|
|
1238
1239
|
}
|
|
1239
1240
|
healConfigYmlModelRoles(configPath);
|
|
1240
1241
|
|