fix: 通过主工作区契约委派 tran
This commit is contained in:
@@ -97,7 +97,9 @@ imageRef:
|
|||||||
- UniDesk repo 提供 primary source workspace 与 `unidesk-*` skills;
|
- UniDesk repo 提供 primary source workspace 与 `unidesk-*` skills;
|
||||||
- AgentRun repo 提供 `tools/trans`、`tools/tran`、`tools/apply_patch` 等 runner tool shim;
|
- AgentRun repo 提供 `tools/trans`、`tools/tran`、`tools/apply_patch` 等 runner tool shim;
|
||||||
- `agent_skills` repo 提供 `dad-dev`、`cli-spec`、`docs-spec`、`git-spec`;
|
- `agent_skills` repo 提供 `dad-dev`、`cli-spec`、`docs-spec`、`git-spec`;
|
||||||
- `tools/tran` 只委派 primary UniDesk workspace 的 `scripts/tran`,Windows、host 与 k3s route 解析和 frontend transport 均由 UniDesk 共享实现负责;
|
- runner 通过 `AGENTRUN_WORKSPACE_PATH` 显式传递已物化的 primary workspace;
|
||||||
|
- `tools/tran` 只从该路径委派 primary UniDesk workspace 的 `scripts/tran`,不得根据安装 shim 的 `$0` 反推源码目录;
|
||||||
|
- Windows、host 与 k3s route 解析和 frontend transport 均由 UniDesk 共享实现负责;
|
||||||
- runner 物化阶段自动把 GitHub URL 改写到 G14 git mirror,Artificer 规格本身不声明 mirror 开关或 mirror base URL。
|
- runner 物化阶段自动把 GitHub URL 改写到 G14 git mirror,Artificer 规格本身不声明 mirror 开关或 mirror base URL。
|
||||||
- primary source workspace 与受控远端 tool target 是不同边界:
|
- primary source workspace 与受控远端 tool target 是不同边界:
|
||||||
- Agent 修改的源码仍以 `ResourceBundleRef` 物化出的 primary workspace 为唯一来源;
|
- Agent 修改的源码仍以 `ResourceBundleRef` 物化出的 primary workspace 为唯一来源;
|
||||||
|
|||||||
@@ -206,10 +206,26 @@ runner 对 managed overlay 的逻辑 `tools/` 做统一装配:
|
|||||||
- 顶层带 shebang 的脚本会被 `chmod +x`,并通过 `PATH` 暴露;
|
- 顶层带 shebang 的脚本会被 `chmod +x`,并通过 `PATH` 暴露;
|
||||||
- runtime 配置 `AGENTRUN_RESOURCE_BIN_PATH` 时,只能在该目录写入执行原始 overlay tool 的 shim;
|
- runtime 配置 `AGENTRUN_RESOURCE_BIN_PATH` 时,只能在该目录写入执行原始 overlay tool 的 shim;
|
||||||
- 禁止复制 tool 文件到 bin 目录后改变 `dirname "$0"`、相对 import 或辅助源码解析语义;
|
- 禁止复制 tool 文件到 bin 目录后改变 `dirname "$0"`、相对 import 或辅助源码解析语义;
|
||||||
|
- runner 必须通过 `AGENTRUN_WORKSPACE_PATH` 向 backend tool 显式传递已物化的 primary workspace;
|
||||||
|
- runtime tool 不得根据安装 shim 的 `$0` 反推 primary workspace;
|
||||||
- 非 shebang 文件只作为 bundle 源码、测试或辅助文件,不作为可执行工具发现;
|
- 非 shebang 文件只作为 bundle 源码、测试或辅助文件,不作为可执行工具发现;
|
||||||
- 短命令名称来自 repo 内真实文件,例如 `tools/hwpod`,repo tool 本身承担业务 wrapper 语义。
|
- 短命令名称来自 repo 内真实文件,例如 `tools/hwpod`,repo tool 本身承担业务 wrapper 语义。
|
||||||
|
|
||||||
AgentRun 自身仓库必须提供 `tools/tran`、`tools/trans` 与 `tools/apply_patch`,用于承接 UniDesk frontend `/ws/ssh` 的 scoped client-token 透传。runner 只通过 `executionPolicy.secretScope.toolCredentials[]` 投影 `UNIDESK_SSH_CLIENT_TOKEN`,并通过调用方 `transientEnv` 或 manager 受控默认值注入非敏感 `UNIDESK_MAIN_SERVER_IP`、`UNIDESK_MAIN_SERVER_HOST` 或 `UNIDESK_FRONTEND_URL`;工具不得读取 provider token、主 server SSH key 或完整 frontend 登录态。`tran --help` 必须输出 JSON,并列出当前支持的最小开发面:host/host workspace `script`、`argv`、普通 ssh-like 命令、host workspace `apply-patch`、`k3s kubectl`、`k3s script`、k3s workload `argv/script` 和 k3s workload `apply-patch`。runner 侧 `apply-patch` 只把 stdin patch 和相邻 `tools/apply_patch` helper 传到目标 route 执行,不接触或回显 Secret 值;`upload`、`download` 和 Windows route 未实现时必须显式 `unsupported-operation`,不能静默改走不受控 shell 拼接或 token fallback。
|
AgentRun 自身仓库必须提供 `tools/tran`、`tools/trans` 与 `tools/apply_patch`:
|
||||||
|
|
||||||
|
- 这些工具用于承接 UniDesk frontend `/ws/ssh` 的 scoped client-token 透传;
|
||||||
|
- runner 只通过 `executionPolicy.secretScope.toolCredentials[]` 投影 `UNIDESK_SSH_CLIENT_TOKEN`;
|
||||||
|
- 非敏感 frontend 地址只由受控入口注入:
|
||||||
|
- 可用字段是 `UNIDESK_MAIN_SERVER_IP`、`UNIDESK_MAIN_SERVER_HOST` 或 `UNIDESK_FRONTEND_URL`;
|
||||||
|
- 注入来源是调用方 `transientEnv` 或 manager 受控默认值;
|
||||||
|
- 工具不得读取 provider token、主 server SSH key 或完整 frontend 登录态;
|
||||||
|
- `tools/tran` 只委派 `AGENTRUN_WORKSPACE_PATH` 指向的 primary UniDesk workspace 中的 `scripts/tran`;
|
||||||
|
- route 与 transport 职责保持单一:
|
||||||
|
- Windows、host 与 k3s route 解析和 frontend transport 均由 UniDesk 共享实现负责;
|
||||||
|
- AgentRun 不维护第二套 parser 或 fallback;
|
||||||
|
- runner 侧 `apply-patch` 保持最小职责:
|
||||||
|
- 只把 stdin patch 和相邻 `tools/apply_patch` helper 传到目标 route 执行;
|
||||||
|
- 不接触或回显 Secret 值。
|
||||||
|
|
||||||
#### promptRefs
|
#### promptRefs
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ function resourceMaterializationEnv(env: NodeJS.ProcessEnv, runId: string, attem
|
|||||||
|
|
||||||
function resourceEnvForMaterialized(env: NodeJS.ProcessEnv, materialized: Awaited<ReturnType<typeof materializeResourceBundle>>): NodeJS.ProcessEnv | undefined {
|
function resourceEnvForMaterialized(env: NodeJS.ProcessEnv, materialized: Awaited<ReturnType<typeof materializeResourceBundle>>): NodeJS.ProcessEnv | undefined {
|
||||||
if (!materialized) return undefined;
|
if (!materialized) return undefined;
|
||||||
const sanitized = { ...env };
|
const sanitized = { ...env, AGENTRUN_WORKSPACE_PATH: materialized.workspacePath };
|
||||||
delete sanitized.AGENTRUN_RESOURCE_CREDENTIAL_PATH;
|
delete sanitized.AGENTRUN_RESOURCE_CREDENTIAL_PATH;
|
||||||
let next: NodeJS.ProcessEnv | undefined = sanitized;
|
let next: NodeJS.ProcessEnv | undefined = sanitized;
|
||||||
if (materialized.binPath) {
|
if (materialized.binPath) {
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ process.exit(1);
|
|||||||
assert.equal(assemblyRunnerResult.commandsProcessed, 2);
|
assert.equal(assemblyRunnerResult.commandsProcessed, 2);
|
||||||
const assemblyInputs = (await readTextIfExists(assemblyInputFile)).trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as JsonRecord);
|
const assemblyInputs = (await readTextIfExists(assemblyInputFile)).trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as JsonRecord);
|
||||||
assert.equal(assemblyInputs.length, 2);
|
assert.equal(assemblyInputs.length, 2);
|
||||||
|
assert.equal(assemblyInputs[0]?.workspacePath, assemblyInputs[0]?.cwd, "backend tool env must identify the materialized primary workspace explicitly");
|
||||||
|
assert.equal(assemblyInputs[1]?.workspacePath, assemblyInputs[1]?.cwd, "continued turns must preserve the primary workspace env contract");
|
||||||
const firstAssemblyInput = turnInputText(assemblyInputs[0]);
|
const firstAssemblyInput = turnInputText(assemblyInputs[0]);
|
||||||
const secondAssemblyInput = turnInputText(assemblyInputs[1]);
|
const secondAssemblyInput = turnInputText(assemblyInputs[1]);
|
||||||
assert.match(firstAssemblyInput, /HWLAB v0\.2 runtime prompt self-test/u);
|
assert.match(firstAssemblyInput, /HWLAB v0\.2 runtime prompt self-test/u);
|
||||||
|
|||||||
@@ -31,12 +31,32 @@ const selfTest: SelfTestCase = async (context) => {
|
|||||||
assert.equal(applyPatch.startsWith("#!/bin/sh\n"), true, "tools/apply_patch must be a shebang helper copied with runner tools");
|
assert.equal(applyPatch.startsWith("#!/bin/sh\n"), true, "tools/apply_patch must be a shebang helper copied with runner tools");
|
||||||
assert.equal(agentrunGit.startsWith("#!/usr/bin/env bun\n"), true, "tools/agentrun-git must be a shebang executable discovered by gitbundle tools");
|
assert.equal(agentrunGit.startsWith("#!/usr/bin/env bun\n"), true, "tools/agentrun-git must be a shebang executable discovered by gitbundle tools");
|
||||||
assert.equal(tran.includes("scripts/tran"), true, "tools/tran must delegate to the primary UniDesk workspace implementation");
|
assert.equal(tran.includes("scripts/tran"), true, "tools/tran must delegate to the primary UniDesk workspace implementation");
|
||||||
|
assert.equal(tran.includes("AGENTRUN_WORKSPACE_PATH"), true, "tools/tran must use the explicit primary workspace contract");
|
||||||
assert.equal(tran.includes("UNIDESK_TRAN_REPO_ROOT"), true, "tools/tran must bind the shared implementation to the primary workspace");
|
assert.equal(tran.includes("UNIDESK_TRAN_REPO_ROOT"), true, "tools/tran must bind the shared implementation to the primary workspace");
|
||||||
|
assert.equal(tran.includes("dirname -- \"$0\""), false, "tools/tran must not infer the primary workspace from an installed shim path");
|
||||||
assert.equal(tran.includes("parseRoute"), false, "tools/tran must not carry a second route parser");
|
assert.equal(tran.includes("parseRoute"), false, "tools/tran must not carry a second route parser");
|
||||||
assert.equal(tran.includes("does not support Windows routes"), false, "tools/tran must not reject shared Windows routes");
|
assert.equal(tran.includes("does not support Windows routes"), false, "tools/tran must not reject shared Windows routes");
|
||||||
assert.equal(agentrunGit.includes("GIT_TERMINAL_PROMPT"), true, "tools/agentrun-git must disable interactive git prompts");
|
assert.equal(agentrunGit.includes("GIT_TERMINAL_PROMPT"), true, "tools/agentrun-git must disable interactive git prompts");
|
||||||
assert.equal(agentrunGit.includes("AGENTRUN_GIT_DIRECT_HOSTS"), true, "tools/agentrun-git must expose direct host policy");
|
assert.equal(agentrunGit.includes("AGENTRUN_GIT_DIRECT_HOSTS"), true, "tools/agentrun-git must expose direct host policy");
|
||||||
|
|
||||||
|
const primaryWorkspace = path.join(context.tmp, "tran-primary-workspace");
|
||||||
|
const primaryScripts = path.join(primaryWorkspace, "scripts");
|
||||||
|
await mkdir(primaryScripts, { recursive: true });
|
||||||
|
await writeFile(path.join(primaryScripts, "ssh-cli.ts"), "// UniDesk shared tran entrypoint\n", "utf8");
|
||||||
|
await writeFile(path.join(primaryScripts, "tran"), "#!/bin/sh\nprintf '%s\\n' \"$UNIDESK_TRAN_REPO_ROOT\" \"$*\"\n", "utf8");
|
||||||
|
await chmod(path.join(primaryScripts, "tran"), 0o755);
|
||||||
|
const installedBin = path.join(context.tmp, "installed-resource-bin");
|
||||||
|
await mkdir(installedBin, { recursive: true });
|
||||||
|
const installedTran = path.join(installedBin, "tran");
|
||||||
|
await writeFile(installedTran, `#!/bin/sh\nexec '${path.join(context.root, "tools/tran")}' \"$@\"\n`, "utf8");
|
||||||
|
await chmod(installedTran, 0o755);
|
||||||
|
const delegatedTran = await execFileAsync(installedTran, ["TEST:win", "cmd", "cd"], { cwd: context.tmp, env: { ...process.env, AGENTRUN_WORKSPACE_PATH: primaryWorkspace } });
|
||||||
|
assert.deepEqual(delegatedTran.stdout.trim().split("\n"), [primaryWorkspace, "TEST:win cmd cd"]);
|
||||||
|
await assert.rejects(
|
||||||
|
() => execFileAsync(installedTran, ["--help"], { cwd: context.tmp, env: { ...process.env, AGENTRUN_WORKSPACE_PATH: undefined } }),
|
||||||
|
(error) => error instanceof Error && "stderr" in error && String(error.stderr).includes("runner did not provide an absolute primary workspace path"),
|
||||||
|
);
|
||||||
|
|
||||||
const patchHelp = await execFileAsync(path.join(context.root, "tools/apply_patch"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
const patchHelp = await execFileAsync(path.join(context.root, "tools/apply_patch"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
||||||
assert.equal(patchHelp.stdout.includes("reads *** Begin Patch format"), true);
|
assert.equal(patchHelp.stdout.includes("reads *** Begin Patch format"), true);
|
||||||
const agentrunGitHelp = await execFileAsync(path.join(context.root, "tools/agentrun-git"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
const agentrunGitHelp = await execFileAsync(path.join(context.root, "tools/agentrun-git"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ for await (const line of rl) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (message.method === "turn/start") {
|
if (message.method === "turn/start") {
|
||||||
if (process.env.AGENTRUN_FAKE_CODEX_TURN_INPUT_FILE) appendFileSync(process.env.AGENTRUN_FAKE_CODEX_TURN_INPUT_FILE, `${JSON.stringify({ threadId: message.params?.threadId ?? null, input: message.params?.input ?? null, model: message.params?.model ?? null, effort: message.params?.effort ?? null })}\n`);
|
if (process.env.AGENTRUN_FAKE_CODEX_TURN_INPUT_FILE) appendFileSync(process.env.AGENTRUN_FAKE_CODEX_TURN_INPUT_FILE, `${JSON.stringify({ threadId: message.params?.threadId ?? null, input: message.params?.input ?? null, model: message.params?.model ?? null, effort: message.params?.effort ?? null, workspacePath: process.env.AGENTRUN_WORKSPACE_PATH ?? null, cwd: process.cwd() })}\n`);
|
||||||
if (mode === "reject-unexpected-model" && (observedThreadModel || Object.hasOwn(message.params ?? {}, "model"))) {
|
if (mode === "reject-unexpected-model" && (observedThreadModel || Object.hasOwn(message.params ?? {}, "model"))) {
|
||||||
respond(message.id, null, { code: -32000, message: "turn/start unexpectedly included model" });
|
respond(message.id, null, { code: -32000, message: "turn/start unexpectedly included model" });
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+13
-4
@@ -1,11 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
primary_workspace=${AGENTRUN_WORKSPACE_PATH:-}
|
||||||
primary_workspace=$(CDPATH= cd -- "$self_dir/../../.." && pwd)
|
|
||||||
shared_tran="$primary_workspace/scripts/tran"
|
|
||||||
|
|
||||||
if [ ! -x "$shared_tran" ]; then
|
case "$primary_workspace" in
|
||||||
|
/*) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' '{"ok":false,"failureKind":"tool-unavailable","message":"runner did not provide an absolute primary workspace path","source":"primary-workspace","valuesPrinted":false}' >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
shared_tran="$primary_workspace/scripts/tran"
|
||||||
|
shared_cli="$primary_workspace/scripts/ssh-cli.ts"
|
||||||
|
|
||||||
|
if [ ! -x "$shared_tran" ] || [ ! -f "$shared_cli" ]; then
|
||||||
printf '%s\n' '{"ok":false,"failureKind":"tool-unavailable","message":"primary UniDesk workspace does not provide executable scripts/tran","source":"primary-workspace","valuesPrinted":false}' >&2
|
printf '%s\n' '{"ok":false,"failureKind":"tool-unavailable","message":"primary UniDesk workspace does not provide executable scripts/tran","source":"primary-workspace","valuesPrinted":false}' >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user