Merge remote-tracking branch 'origin/master' into fix/1753-pac-yaml-trailing-space

This commit is contained in:
Codex
2026-07-11 08:12:12 +02:00
5 changed files with 203 additions and 15 deletions
+3 -2
View File
@@ -249,7 +249,7 @@ export function sshHelp(scope: SshHelpScope | undefined = undefined): unknown {
"sh and bash helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget. Windows route downloads such as `trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs` automatically map drive-letter paths to the same provider's WSL `/mnt/<drive>` host route and still return verified bytes/SHA-256 evidence.",
"`upload` `download` 只用于必要的二进制文件或生成物;远端文本应避免反复传输,读取使用 `cat`/`rg`,修改使用 `apply-patch`。成功传输的 JSON 会在 `hint` 中重复该提示,通过远端临时文件写入,自动核对两端字节数和 SHA-256,并返回 `verification.automatic=true``verification.verified=true` `verification.match.{bytes,sha256}=true`;该 JSON 已是完整性证据,无需额外执行 `sha256sum`。下载通过 `host.ssh.tcp-pool` 流式传输并输出进度 JSON;受控异步产物任务可传入 `--inactivity-timeout-ms`,只要数据持续流动就不受普通短命令预算影响。Windows route 下载(例如 `trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs`)会自动把盘符路径映射到同一 provider WSL `/mnt/<drive>` host route,同时仍返回字节数和 SHA-256 校验证据。",
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
"`sh` inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY and runs target `/bin/sh`; use `bash` only for Bash syntax such as `pipefail`, arrays, substring expansion, or `[[ ... ]]`, not as a proxy workaround.",
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
@@ -276,7 +276,7 @@ function sshDownloadHelp(): unknown {
command: `${entrypoint} download`,
output: "json",
scope: "download",
description: "Download one remote file through the selected route and verify remote/local bytes plus SHA-256 automatically.",
description: "下载一个必要的二进制文件或生成物,并自动核对远端/本地字节数与 SHA-256;远端文本改用 cat/rg/apply-patch。",
runtime: {
entrypoint,
repoRoot,
@@ -298,6 +298,7 @@ function sshDownloadHelp(): unknown {
"--inactivity-timeout-ms <milliseconds>": "Allow a controlled progress-emitting download to stay open while data continues flowing; ordinary trans operations retain the short runtime budget.",
},
contract: {
hint: "避免对远端文本反复 upload/download;读取优先使用 cat/rg,修改优先使用 apply-patch。",
pathOrder: ["remote-file", "local-file"],
transport: "host.ssh.tcp-pool",
strategy: "tcp-pool-stdout-stream",
+13 -12
View File
@@ -56,7 +56,7 @@ interface WebtermTarget {
privileged: boolean;
pid: "host" | "container";
sourceMounts: Array<{ source: string; target: string; readOnly: boolean }>;
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean }>;
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean; fallbackShell: boolean }>;
sessionStartModes: SessionStartModes;
autoResumePrompt: AutoResumePrompt;
environment: Record<string, string>;
@@ -115,7 +115,7 @@ export function webtermHelp(): Record<string, unknown> {
"bun scripts/cli.ts platform-infra webterm validate",
],
configPath,
description: "Deploy the host-Docker Web Terminal from YAML and expose it through PK01 Caddy HTTPS without touching the existing 7682 service.",
description: "Deploy target-scoped host-Docker Web Terminal instances from YAML and optionally expose them through PK01 Caddy HTTPS.",
};
}
@@ -317,8 +317,6 @@ function parseTarget(record: Record<string, unknown>, path: string, sessionStart
},
},
};
if (target.runtime.hostPort === 7682) throw new Error(`${path}.runtime.hostPort must not reuse existing 7682`);
if (target.runtime.containerName === "web-terminal-7682") throw new Error(`${path}.runtime.containerName must not reuse web-terminal-7682`);
if (!target.publicExposure.publicBaseUrl.startsWith("https://")) throw new Error(`${path}.publicExposure.publicBaseUrl must be https`);
return target;
}
@@ -372,11 +370,14 @@ function resumeCommandWithPrompt(
session: WebtermTarget["runtime"]["autoStartSessions"][number],
prompt: AutoResumePrompt,
): string {
if (!session.resumePrompt) return session.command;
if (!/^\s*(?:mycx|oncx)\s+resume\s+\S+\s*$/.test(session.command)) {
throw new Error(`resumePrompt requires a mycx/oncx resume command: ${session.title}`);
let command = session.command.trim();
if (session.resumePrompt) {
if (!/^\s*(?:mycx|oncx)\s+resume\s+\S+\s*$/.test(session.command)) {
throw new Error(`resumePrompt requires a mycx/oncx resume command: ${session.title}`);
}
command = `${command} ${quoteShellArg(prompt.prompt)}`;
}
return `${session.command.trim()} ${quoteShellArg(prompt.prompt)}`;
return session.fallbackShell ? `${command}; exec /bin/bash -l` : command;
}
function quoteShellArg(value: string): string {
@@ -386,11 +387,10 @@ function quoteShellArg(value: string): string {
function policyChecks(target: WebtermTarget, compose: string): Array<Record<string, unknown> & { ok: boolean }> {
return [
{ name: "enabled", ok: target.enabled, detail: "target must be enabled" },
{ name: "no-7682-host-port", ok: target.runtime.hostPort !== 7682, detail: "existing 7682 service must not be reused" },
{ name: "no-7682-container", ok: target.runtime.containerName !== "web-terminal-7682", detail: "existing 7682 container must not be reused" },
{ name: "compose-path-7683", ok: target.runtime.composePath.includes("7683"), detail: target.runtime.composePath },
{ name: "compose-path", ok: target.runtime.composePath.endsWith(".yaml"), detail: target.runtime.composePath },
{ name: "container-name", ok: compose.includes(`container_name: ${target.runtime.containerName}`), detail: target.runtime.containerName },
{ name: "public-https", ok: target.publicExposure.publicBaseUrl.startsWith("https://"), detail: target.publicExposure.publicBaseUrl },
{ name: "compose-renders-7683", ok: compose.includes(`"${target.runtime.hostPort}:${target.runtime.containerPort}"`), detail: `${target.runtime.hostPort}:${target.runtime.containerPort}` },
{ name: "compose-port", ok: compose.includes(`"${target.runtime.hostPort}:${target.runtime.containerPort}"`), detail: `${target.runtime.hostPort}:${target.runtime.containerPort}` },
{ name: "source-mounts-present", ok: target.runtime.sourceMounts.length >= 3, detail: `mounts=${target.runtime.sourceMounts.length}` },
];
}
@@ -557,6 +557,7 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru
cols: integerField(record, "cols", itemPath),
rows: integerField(record, "rows", itemPath),
resumePrompt: booleanField(record, "resumePrompt", itemPath),
fallbackShell: booleanField(record, "fallbackShell", itemPath),
};
});
}
+11
View File
@@ -99,6 +99,15 @@ const fileTransferWriteRawChunkBytes = 131_072;
const fileTransferWriteB64ChunkChars = 1_398_104;
const fileTransferProgressEveryChunks = 16;
const fileTransferUsageHint = {
code: "prefer-remote-text-operations",
message: "避免对远端文本反复 upload/download;读取优先使用 cat/rg,修改优先使用 apply-patch。upload/download 仅用于必要的二进制文件或生成物传输。",
preferredOperations: {
read: ["cat", "rg"],
edit: ["apply-patch"],
},
};
export function isSshFileTransferOperation(args: string[]): boolean {
const subcommand = args[0] ?? "";
return subcommand === "upload" || subcommand === "download";
@@ -132,6 +141,7 @@ export async function runSshFileTransferOperation(
bytes: expected.bytes,
sha256: expected.sha256,
verified: true,
hint: fileTransferUsageHint,
verification,
transfer: write,
}, null, 2)}\n`);
@@ -149,6 +159,7 @@ export async function runSshFileTransferOperation(
bytes: download.bytes,
sha256: download.sha256,
verified: true,
hint: fileTransferUsageHint,
verification: download.verification,
transfer: download.transfer,
}, null, 2)}\n`);