@f5-sales-demo/xcsh 20.1.1 → 20.1.2
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/cli/sandbox-check.ts +50 -31
- package/src/commands/sandbox.ts +1 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +2 -4
- package/src/sandbox/session-fence.ts +1 -0
- package/src/tools/bash.ts +95 -45
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [20.1.2] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Made `xcsh sandbox check` work for sessions rooted directly under the operator home, distinguish harness errors from sandbox failures, and verify this topology against installed release artifacts ([#2807](https://github.com/f5-sales-demo/xcsh/issues/2807))
|
|
10
|
+
|
|
5
11
|
## [20.1.1] - 2026-08-01
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.1.
|
|
4
|
+
"version": "20.1.2",
|
|
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": "20.1.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "20.1.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "20.1.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "20.1.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "20.1.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "20.1.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "20.1.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "20.1.2",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "20.1.2",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "20.1.2",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "20.1.2",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "20.1.2",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "20.1.2",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "20.1.2",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
package/src/cli/sandbox-check.ts
CHANGED
|
@@ -8,15 +8,19 @@ import { Settings } from "../config/settings";
|
|
|
8
8
|
import { fenceForNative } from "../exec/bash-executor";
|
|
9
9
|
import { buildContainmentFence, type ContainmentFence, containmentStatus } from "../sandbox/containment";
|
|
10
10
|
import { evaluateToolCall } from "../sandbox/enforce";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
SANDBOX_CHECK_NAMED_SIBLING_ENV,
|
|
13
|
+
SANDBOX_OPERATOR_HOME_ENV,
|
|
14
|
+
SANDBOX_SESSION_ROOT_ENV,
|
|
15
|
+
} from "../sandbox/session-fence";
|
|
12
16
|
import { BashTool, type ToolSession } from "../tools";
|
|
13
17
|
|
|
14
|
-
export type SandboxCheckResultStatus = "PASS" | "FAIL" | "SKIP";
|
|
18
|
+
export type SandboxCheckResultStatus = "PASS" | "FAIL" | "SKIP" | "ERROR";
|
|
15
19
|
|
|
16
20
|
export interface SandboxCheckResult {
|
|
17
21
|
name: string;
|
|
18
22
|
status: SandboxCheckResultStatus;
|
|
19
|
-
/** Present on failures and skips; paths are generalized before they leave the process. */
|
|
23
|
+
/** Present on failures, errors, and skips; paths are generalized before they leave the process. */
|
|
20
24
|
detail?: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -27,6 +31,7 @@ export interface SandboxCheckReport {
|
|
|
27
31
|
summary: {
|
|
28
32
|
passed: number;
|
|
29
33
|
failed: number;
|
|
34
|
+
errors: number;
|
|
30
35
|
skipped: number;
|
|
31
36
|
};
|
|
32
37
|
}
|
|
@@ -142,15 +147,19 @@ function renderReport(report: SandboxCheckReport, json: boolean, verbose: boolea
|
|
|
142
147
|
|
|
143
148
|
const enforcement = report.osEnforced ? "OS enforced" : "scanner only";
|
|
144
149
|
process.stdout.write(`Sandbox backend: ${report.backend} (${enforcement})\n\n`);
|
|
145
|
-
const
|
|
150
|
+
const nameWidth = Math.max(0, ...report.checks.map(check => check.name.length));
|
|
151
|
+
const statusWidth = Math.max(0, ...report.checks.map(check => check.status.length));
|
|
146
152
|
for (const check of report.checks) {
|
|
147
|
-
process.stdout.write(`${check.status.padEnd(
|
|
153
|
+
process.stdout.write(`${check.status.padEnd(statusWidth)} ${check.name.padEnd(nameWidth)}\n`);
|
|
148
154
|
if (verbose && check.detail) process.stdout.write(` ${check.detail}\n`);
|
|
149
155
|
}
|
|
150
156
|
process.stdout.write(
|
|
151
|
-
`\n${report.summary.passed} passed, ${report.summary.failed} failed, ${report.summary.skipped} skipped\n`,
|
|
157
|
+
`\n${report.summary.passed} passed, ${report.summary.failed} failed, ${report.summary.errors} errors, ${report.summary.skipped} skipped\n`,
|
|
152
158
|
);
|
|
153
|
-
if (
|
|
159
|
+
if (report.checks.some(check => check.name === "conformance matrix setup" && check.status === "ERROR")) {
|
|
160
|
+
process.stdout.write("Conformance matrix did not run.\n");
|
|
161
|
+
}
|
|
162
|
+
if (!verbose && (report.summary.failed > 0 || report.summary.errors > 0)) {
|
|
154
163
|
process.stdout.write("Run `xcsh sandbox check --verbose` for failure details.\n");
|
|
155
164
|
}
|
|
156
165
|
}
|
|
@@ -177,7 +186,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
177
186
|
probe: () => boolean | ProbeOutcome | Promise<boolean | ProbeOutcome>,
|
|
178
187
|
): Promise<void> => {
|
|
179
188
|
if (abortController.signal.aborted) {
|
|
180
|
-
add(name, "
|
|
189
|
+
add(name, "ERROR", "probe aborted before execution; path=<probe>; errno=ABORTED");
|
|
181
190
|
return;
|
|
182
191
|
}
|
|
183
192
|
try {
|
|
@@ -190,18 +199,25 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
190
199
|
);
|
|
191
200
|
} catch (error) {
|
|
192
201
|
const outcome = exceptionOutcome("probe threw", "<probe>", error, redactions);
|
|
193
|
-
add(name, "
|
|
202
|
+
add(name, "ERROR", outcome.detail);
|
|
194
203
|
}
|
|
195
204
|
};
|
|
196
205
|
|
|
197
206
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
207
|
+
const inheritedWorkspace = process.env[SANDBOX_SESSION_ROOT_ENV];
|
|
208
|
+
const inheritedHome = process.env[SANDBOX_OPERATOR_HOME_ENV];
|
|
209
|
+
const inheritedSibling = process.env[SANDBOX_CHECK_NAMED_SIBLING_ENV];
|
|
210
|
+
const inheritedProfile = inheritedWorkspace !== undefined;
|
|
211
|
+
const workspaceInput = inheritedWorkspace ?? process.cwd();
|
|
212
|
+
const homeInput = inheritedHome ?? os.homedir();
|
|
201
213
|
redactions.push([workspaceInput, "<workspace>"], [homeInput, "<operator-home>"]);
|
|
202
|
-
|
|
203
|
-
|
|
214
|
+
// BashTool owns and canonicalises inherited values before applying Seatbelt/Landlock. Re-running
|
|
215
|
+
// realpath here can require metadata access that the live profile deliberately withholds from the
|
|
216
|
+
// session parent — which is the operator home for a `~/<workspace>` layout (#2807).
|
|
217
|
+
const liveWorkspace = inheritedWorkspace ?? (await fs.realpath(workspaceInput));
|
|
218
|
+
const liveHome = inheritedHome ?? (await fs.realpath(homeInput));
|
|
204
219
|
redactions.push([liveWorkspace, "<workspace>"], [liveHome, "<operator-home>"]);
|
|
220
|
+
if (inheritedSibling !== undefined) redactions.push([inheritedSibling, "<session-parent>/<synthetic-sibling>"]);
|
|
205
221
|
|
|
206
222
|
const fixtureBase = inheritedProfile ? liveWorkspace : await fs.realpath(os.tmpdir());
|
|
207
223
|
fixtureRoot = await fs.mkdtemp(path.join(fixtureBase, ".xcsh-sandbox-check-policy-"));
|
|
@@ -309,17 +325,19 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
309
325
|
|
|
310
326
|
await check("named sibling remains reachable", async () => {
|
|
311
327
|
const displayPath = "<session-parent>/<synthetic-sibling>";
|
|
312
|
-
let liveSibling
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
let liveSibling = inheritedSibling;
|
|
329
|
+
if (liveSibling === undefined) {
|
|
330
|
+
try {
|
|
331
|
+
liveSibling = await fs.mkdtemp(path.join(path.dirname(liveWorkspace), ".xcsh-sandbox-check-sibling-"));
|
|
332
|
+
fixturePaths.push(liveSibling);
|
|
333
|
+
nonEnumerableCleanupDirs.add(liveSibling);
|
|
334
|
+
redactions.push([liveSibling, displayPath]);
|
|
335
|
+
const namedFile = path.join(liveSibling, "named.txt");
|
|
336
|
+
await Bun.write(namedFile, "sibling\n");
|
|
337
|
+
knownCleanupLeaves.push(namedFile);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return exceptionOutcome("create named sibling fixture", displayPath, error, redactions);
|
|
340
|
+
}
|
|
323
341
|
}
|
|
324
342
|
const result = await shellProbe(
|
|
325
343
|
'test "$(cat named.txt)" = sibling',
|
|
@@ -485,7 +503,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
485
503
|
});
|
|
486
504
|
} catch (error) {
|
|
487
505
|
const outcome = exceptionOutcome("conformance matrix setup failed", "<probe>", error, redactions);
|
|
488
|
-
add("conformance matrix
|
|
506
|
+
add("conformance matrix setup", "ERROR", outcome.detail);
|
|
489
507
|
} finally {
|
|
490
508
|
process.off("SIGINT", interrupt);
|
|
491
509
|
process.off("SIGTERM", interrupt);
|
|
@@ -511,17 +529,17 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
511
529
|
}
|
|
512
530
|
}
|
|
513
531
|
|
|
514
|
-
if (
|
|
532
|
+
if (cleanupFailures.length > 0) {
|
|
515
533
|
add(
|
|
516
534
|
"synthetic fixtures removed",
|
|
517
|
-
"
|
|
535
|
+
"ERROR",
|
|
518
536
|
sanitizeDetail(
|
|
519
|
-
`fixture cleanup incomplete; path=<synthetic-fixtures>; errno=${
|
|
520
|
-
fixtureRoot === undefined ? "ENOENT" : "unknown"
|
|
521
|
-
}${cleanupFailures.length > 0 ? `; error=${cleanupFailures.join("; ")}` : ""}`,
|
|
537
|
+
`fixture cleanup incomplete; path=<synthetic-fixtures>; errno=unknown; error=${cleanupFailures.join("; ")}`,
|
|
522
538
|
redactions,
|
|
523
539
|
),
|
|
524
540
|
);
|
|
541
|
+
} else if (fixturePaths.length === 0) {
|
|
542
|
+
add("synthetic fixtures removed", "SKIP", "setup created no fixtures; path=<synthetic-fixtures>; errno=none");
|
|
525
543
|
} else {
|
|
526
544
|
add("synthetic fixtures removed", "PASS");
|
|
527
545
|
}
|
|
@@ -534,6 +552,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
534
552
|
summary: {
|
|
535
553
|
passed: checks.filter(result => result.status === "PASS").length,
|
|
536
554
|
failed: checks.filter(result => result.status === "FAIL").length,
|
|
555
|
+
errors: checks.filter(result => result.status === "ERROR").length,
|
|
537
556
|
skipped: checks.filter(result => result.status === "SKIP").length,
|
|
538
557
|
},
|
|
539
558
|
};
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -21,6 +21,6 @@ export default class Sandbox extends Command {
|
|
|
21
21
|
async run(): Promise<void> {
|
|
22
22
|
const { flags } = await this.parse(Sandbox);
|
|
23
23
|
const report = await runSandboxCheck({ json: flags.json, verbose: flags.verbose });
|
|
24
|
-
if (report.summary.failed > 0) process.exitCode = 1;
|
|
24
|
+
if (report.summary.failed > 0 || report.summary.errors > 0) process.exitCode = 1;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.1.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.1.2",
|
|
21
|
+
"commit": "b5534d98c8ad483cc6e8709b44242cf3ea98041d",
|
|
22
|
+
"shortCommit": "b5534d9",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.1.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.1.2",
|
|
25
|
+
"commitDate": "2026-08-01T21:09:01Z",
|
|
26
|
+
"buildDate": "2026-08-01T21:30:18.970Z",
|
|
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/v20.1.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/b5534d98c8ad483cc6e8709b44242cf3ea98041d",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.1.2"
|
|
33
33
|
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ConsoleCatalogData } from "./console-catalog-types";
|
|
4
4
|
|
|
5
|
-
export const CONSOLE_CATALOG_VERSION = "
|
|
5
|
+
export const CONSOLE_CATALOG_VERSION = "ea6ed666f67f016f68f4e538d1f22e44e18d11a2";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
8
|
+
version: "ea6ed666f67f016f68f4e538d1f22e44e18d11a2",
|
|
9
9
|
workflows: {
|
|
10
10
|
"address-allocator/create":
|
|
11
11
|
'---\nschema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - "role_minimum: admin"\nparams:\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\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',
|
|
@@ -978,8 +978,6 @@ export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
|
978
978
|
"---\nschema: urn:xcsh:console:resource:v1\nid: securemesh-site\nlabel: Secure Mesh Sites\n_source: Generated from api-specs-enriched/config/console_ui.yaml\napi:\n kind: securemesh_site\nconsole:\n workspace: multi-cloud-network-connect\n workspace_label: Multi-Cloud Network Connect\n route_prefix: /web/workspaces/multi-cloud-network-connect\n route_pattern: /manage/site_management/legacy_configs/securemesh_site\n menu_path:\n - Manage\n - Site Management\n - Legacy Configurations\n - Secure Mesh Sites\n breadcrumbs:\n - Home\n - Multi-Cloud Network Connect\n - Manage\n - Site Management\n - Legacy Configurations\n - Secure Mesh Sites\n namespace_scoped: false\nadd_action:\n type: tab\n label: Add Secure Mesh Site\nsave_action:\n label: Add Secure Mesh Site\ncancel_action:\n label: Cancel All\nform_tabs:\n - Form\n - Documentation\n - JSON\nform:\n sections:\n - id: metadata\n label: Metadata\n api_fields:\n - metadata.name\n - metadata.labels\n - metadata.description\nenriched_fields:\n count: 25\n source: api-specs-enriched/config/console_field_metadata.yaml\nmetadata:\n confidence: validated\n discovered_at: '2026-06-16'\n console_version: '2025.06'\n",
|
|
979
979
|
"securemesh-site-v2":
|
|
980
980
|
"---\nschema: urn:xcsh:console:resource:v1\nid: securemesh-site-v2\nlabel: Secure Mesh Sites v2\n_source: Generated from api-specs-enriched/config/console_ui.yaml\napi:\n kind: securemesh_site_v2\nconsole:\n workspace: multi-cloud-network-connect\n workspace_label: Multi-Cloud Network Connect\n route_prefix: /web/workspaces/multi-cloud-network-connect\n route_pattern: /manage/site_management/cloud_sites/securemesh_site_v2\n menu_path:\n - Manage\n - Site Management\n - Customer Edges\n - Secure Mesh Sites v2\n breadcrumbs:\n - Home\n - Multi-Cloud Network Connect\n - Manage\n - Site Management\n - Customer Edges\n - Secure Mesh Sites v2\n namespace_scoped: false\nadd_action:\n type: tab\n label: Add Secure Mesh Site v2\nsave_action:\n label: Add Secure Mesh Site v2\ncancel_action:\n label: Cancel All\nform_tabs:\n - Form\n - Documentation\n - JSON\nform:\n sections:\n - id: metadata\n label: Metadata\n api_fields:\n - metadata.name\n - metadata.labels\n - metadata.description\nenriched_fields:\n count: 8\n source: api-specs-enriched/config/console_field_metadata.yaml\nmetadata:\n confidence: validated\n discovered_at: '2026-06-16'\n console_version: '2025.06'\n",
|
|
981
|
-
"securemesh-site-v2-cloud":
|
|
982
|
-
"---\nschema: urn:xcsh:console:resource:v1\nid: securemesh-site-v2-cloud\nlabel: Secure Mesh Sites v2\n_source: Generated from api-specs-enriched/config/console_ui.yaml\napi:\n kind: securemesh_site_v2_cloud\nconsole:\n workspace: multi-cloud-network-connect\n workspace_label: Multi-Cloud Network Connect\n route_prefix: /web/workspaces/multi-cloud-network-connect\n route_pattern: /manage/site_management/cloud_sites/securemesh_site_v2\n menu_path:\n - Manage\n - Site Management\n - Customer Edges\n - Secure Mesh Sites v2\n breadcrumbs:\n - Home\n - Multi-Cloud Network Connect\n - Manage\n - Site Management\n - Customer Edges\n - Secure Mesh Sites v2\n namespace_scoped: false\nadd_action:\n type: tab\n label: Add Secure Mesh Site\nsave_action:\n label: Add Secure Mesh Site\ncancel_action:\n label: Cancel All\nform_tabs:\n - Form\n - Documentation\n - JSON\nform:\n sections:\n - id: metadata\n label: Metadata\n api_fields:\n - metadata.name\n - metadata.labels\n - metadata.description\nmetadata:\n confidence: validated\n discovered_at: '2026-06-17'\n console_version: '2025.06'\n notes: This is the v2 cloud sites path — differs from securemesh_site_v2 which may have a different route\n",
|
|
983
981
|
segment:
|
|
984
982
|
"---\nschema: urn:xcsh:console:resource:v1\nid: segment\nlabel: Segments\n_source: Generated from api-specs-enriched/config/console_ui.yaml\napi:\n kind: segment\nconsole:\n workspace: multi-cloud-network-connect\n workspace_label: Multi-Cloud Network Connect\n route_prefix: /web/workspaces/multi-cloud-network-connect\n route_pattern: /manage/networking/network_configuration/segment\n menu_path:\n - Manage\n - Networking\n - Network Configuration\n - Segments\n breadcrumbs:\n - Home\n - Multi-Cloud Network Connect\n - Manage\n - Networking\n - Network Configuration\n - Segments\n namespace_scoped: false\nadd_action:\n type: tab\n label: Add Segment\nsave_action:\n label: Add Segment\ncancel_action:\n label: Cancel All\nform_tabs:\n - Form\n - Documentation\n - JSON\nform:\n sections:\n - id: metadata\n label: Metadata\n api_fields:\n - metadata.name\n - metadata.labels\n - metadata.description\nenriched_fields:\n count: 4\n source: api-specs-enriched/config/console_field_metadata.yaml\nmetadata:\n confidence: validated\n discovered_at: '2026-06-16'\n console_version: '2025.06'\n",
|
|
985
983
|
"segment-connection":
|
|
@@ -26,6 +26,7 @@ import { buildContainmentFence, type ContainmentFence } from "./containment";
|
|
|
26
26
|
*/
|
|
27
27
|
export const SANDBOX_SESSION_ROOT_ENV = "XCSH_SANDBOX_SESSION_ROOT";
|
|
28
28
|
export const SANDBOX_OPERATOR_HOME_ENV = "XCSH_SANDBOX_OPERATOR_HOME";
|
|
29
|
+
export const SANDBOX_CHECK_NAMED_SIBLING_ENV = "XCSH_SANDBOX_CHECK_NAMED_SIBLING";
|
|
29
30
|
|
|
30
31
|
/** The slice of `Settings` this needs — supplied explicitly so the caller names its own source. */
|
|
31
32
|
export interface SettingsReader {
|
package/src/tools/bash.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import type {
|
|
4
5
|
AgentTool,
|
|
5
6
|
AgentToolContext,
|
|
@@ -18,7 +19,12 @@ import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
|
18
19
|
import type { Theme } from "../modes/theme/theme";
|
|
19
20
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
20
21
|
import { type ContainmentFence, containmentStatus, fenceVerdict } from "../sandbox/containment";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
resolveSessionFence,
|
|
24
|
+
SANDBOX_CHECK_NAMED_SIBLING_ENV,
|
|
25
|
+
SANDBOX_OPERATOR_HOME_ENV,
|
|
26
|
+
SANDBOX_SESSION_ROOT_ENV,
|
|
27
|
+
} from "../sandbox/session-fence";
|
|
22
28
|
import { SECRET_ENV_PATTERNS, type SecretObfuscator } from "../secrets";
|
|
23
29
|
import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
|
|
24
30
|
import { renderStatusLine } from "../tui";
|
|
@@ -178,6 +184,21 @@ function normalizeBashEnv(env: Record<string, string> | undefined): Record<strin
|
|
|
178
184
|
return normalized;
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
/** Canonical context resolved by the unfenced host before a child inherits the live profile. */
|
|
188
|
+
async function canonicalizeSandboxContextPath(input: string): Promise<string> {
|
|
189
|
+
try {
|
|
190
|
+
return await fs.promises.realpath(input);
|
|
191
|
+
} catch {
|
|
192
|
+
// The fence builder already handles an unavailable home conservatively. Keep ordinary bash calls
|
|
193
|
+
// working in that case; the diagnostic will report the path-specific failure if it is invoked.
|
|
194
|
+
return input;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function invokesSandboxCheck(command: string): boolean {
|
|
199
|
+
return /(?:xcsh(?:-[a-z0-9-]+)?|cli\.ts)["']?\s+["']?sandbox["']?\s+["']?check["']?(?:\s|$)/iu.test(command);
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
function escapeBashEnvValueForDisplay(value: string): string {
|
|
182
203
|
return value
|
|
183
204
|
.replaceAll("\\", "\\\\")
|
|
@@ -630,18 +651,25 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
630
651
|
// same boundary rather than two that merely agree today.
|
|
631
652
|
const containmentRoot = this.#containmentRoot();
|
|
632
653
|
const fence = this.#containmentFence(containmentRoot);
|
|
654
|
+
const sandboxCheckInvocation = fence !== undefined && invokesSandboxCheck(command);
|
|
633
655
|
// A nested `xcsh sandbox check` must exercise the grants of this exact live profile. Seatbelt and
|
|
634
656
|
// Landlock restrictions compose and cannot be relaxed by its subprocess, so the diagnostic needs
|
|
635
657
|
// the immutable session anchor even when this individual call uses `cwd` or starts with `cd`.
|
|
636
658
|
// Keep these values host-owned: tool-supplied env cannot replace them.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
659
|
+
let env = requestedEnv;
|
|
660
|
+
let trustedSessionRoot = containmentRoot;
|
|
661
|
+
if (fence !== undefined) {
|
|
662
|
+
const [canonicalSessionRoot, trustedOperatorHome] = await Promise.all([
|
|
663
|
+
canonicalizeSandboxContextPath(containmentRoot),
|
|
664
|
+
canonicalizeSandboxContextPath(os.homedir()),
|
|
665
|
+
]);
|
|
666
|
+
trustedSessionRoot = canonicalSessionRoot;
|
|
667
|
+
env = {
|
|
668
|
+
...requestedEnv,
|
|
669
|
+
[SANDBOX_SESSION_ROOT_ENV]: trustedSessionRoot,
|
|
670
|
+
[SANDBOX_OPERATOR_HOME_ENV]: trustedOperatorHome,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
645
673
|
|
|
646
674
|
const localOptions = {
|
|
647
675
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
@@ -684,6 +712,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
684
712
|
const obfuscator = this.session.obfuscator;
|
|
685
713
|
_sessionObfuscator = obfuscator; // Keep module-level ref fresh for renderer
|
|
686
714
|
const maskSecrets = obfuscator?.hasSecrets() ? (t: string) => obfuscator.obfuscate(t) : undefined;
|
|
715
|
+
if (asyncRequested && sandboxCheckInvocation) {
|
|
716
|
+
throw new ToolError("Sandbox check must run synchronously so its synthetic fixtures can be removed.");
|
|
717
|
+
}
|
|
687
718
|
|
|
688
719
|
if (asyncRequested) {
|
|
689
720
|
if (!this.session.asyncJobManager) {
|
|
@@ -704,7 +735,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
704
735
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec);
|
|
705
736
|
}
|
|
706
737
|
|
|
707
|
-
if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
|
|
738
|
+
if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager && !sandboxCheckInvocation) {
|
|
708
739
|
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
709
740
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
|
710
741
|
const job = this.#startManagedBashJob({
|
|
@@ -762,41 +793,60 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
762
793
|
const osBackend = containmentStatus(fence !== undefined);
|
|
763
794
|
const ptyConfinable = !osBackend.osEnforced || osBackend.backend === "seatbelt";
|
|
764
795
|
const usePty = pty && ptyConfinable && $env.PI_NO_PTY !== "1" && ctx?.hasUI === true && ctx.ui !== undefined;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
796
|
+
let sandboxCheckSibling: string | undefined;
|
|
797
|
+
if (sandboxCheckInvocation) {
|
|
798
|
+
// Landlock cannot add a newly-created child to an already-applied profile. Prepare the named
|
|
799
|
+
// sibling before the child is confined so the diagnostic measures reachability, not whether a
|
|
800
|
+
// nested sandbox can widen itself. The host owns both this path and its cleanup.
|
|
801
|
+
sandboxCheckSibling = await fs.promises.mkdtemp(
|
|
802
|
+
path.join(path.dirname(trustedSessionRoot), ".xcsh-sandbox-check-live-sibling-"),
|
|
803
|
+
);
|
|
804
|
+
await Bun.write(path.join(sandboxCheckSibling, "named.txt"), "sibling\n");
|
|
805
|
+
env = { ...env, [SANDBOX_CHECK_NAMED_SIBLING_ENV]: sandboxCheckSibling };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let result: BashResult | BashInteractiveResult;
|
|
809
|
+
try {
|
|
810
|
+
result = usePty
|
|
811
|
+
? await runInteractiveBashPty(ctx.ui!, {
|
|
812
|
+
command,
|
|
813
|
+
cwd: commandCwd,
|
|
814
|
+
timeoutMs,
|
|
815
|
+
signal,
|
|
816
|
+
env,
|
|
817
|
+
artifactPath,
|
|
818
|
+
artifactId,
|
|
819
|
+
maskSecrets,
|
|
820
|
+
// The same fence that decided `ptyConfinable`, so the gate and the enforcement can
|
|
821
|
+
// never be looking at different answers.
|
|
822
|
+
fence,
|
|
823
|
+
})
|
|
824
|
+
: await executeBash(command, {
|
|
825
|
+
cwd: commandCwd,
|
|
826
|
+
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
827
|
+
timeout: timeoutMs,
|
|
828
|
+
signal,
|
|
829
|
+
env,
|
|
830
|
+
artifactPath,
|
|
831
|
+
artifactId,
|
|
832
|
+
maskSecrets,
|
|
833
|
+
fence,
|
|
834
|
+
onChunk: chunk => {
|
|
835
|
+
tailBuffer.append(chunk);
|
|
836
|
+
if (onUpdate) {
|
|
837
|
+
const preview = maskSecrets ? maskSecrets(tailBuffer.text()) : tailBuffer.text();
|
|
838
|
+
onUpdate({
|
|
839
|
+
content: [{ type: "text", text: preview }],
|
|
840
|
+
details: {},
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
} finally {
|
|
846
|
+
if (sandboxCheckSibling !== undefined) {
|
|
847
|
+
await fs.promises.rm(sandboxCheckSibling, { recursive: true, force: true });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
800
850
|
if (result.cancelled) {
|
|
801
851
|
if (signal?.aborted) {
|
|
802
852
|
throw new ToolAbortError(normalizeResultOutput(result) || "Command aborted");
|