Merge remote-tracking branch 'origin/master' into fix/pac-bootstrap-diagnostics
This commit is contained in:
@@ -76,7 +76,8 @@ AipodSpec/Artificer、task manifest 和 queue 渐进披露见 [references/resour
|
||||
- 新写入口不要用 `codex submit/steer/resume`;这些 legacy mutation 已冻结。
|
||||
- Artificer 的 runner primary workspace 与任务 Target 必须分开判定:
|
||||
- runner primary workspace 只装配默认 UniDesk resource bundle、`tools/trans`、skill 和轻量准备;
|
||||
- 待修改源码只来自派单上下文中的 `targetWorkspace`,且该路径必须是任务专属 worktree;
|
||||
- 待修改源码只来自派单上下文中的显式 `targetWorkspace`;
|
||||
- admission 只校验绝对路径、存在性和目录类型,不读取 Git 状态,也不因 dirty、ahead、behind 或已有 diff 产生 warning/blocker;
|
||||
- 现场探测、Git、编辑、验证和运行面观察必须使用 `trans <Target>:<targetWorkspace>`;
|
||||
- 不得把 runner 本地结果、默认 bundle 的 Git 状态或容器内同名路径当作 Target 验收;
|
||||
- `unidesk-trans` required skill、runner tools bundle、`unidesk-ssh` SecretRef 和 endpoint presence 缺一项即归为装配失败;
|
||||
|
||||
@@ -32,21 +32,22 @@ bun scripts/cli.ts agentrun create task \
|
||||
- 标题按规范化后的完全匹配解析,重复标题必须改用内部 ID 处理歧义。
|
||||
|
||||
- 默认直接执行 live 单命令;不要把 `--dry-run` 作为派单前置步骤;
|
||||
- live 必须先经 owning YAML 选中的 Target `trans` 重入并完成只读 preflight;
|
||||
- live 必须先经 owning YAML 选中的 Target `trans` 重入并完成最小路径 preflight;
|
||||
- preflight 必须验证:
|
||||
- task worktree 是 Git 根目录且 clean;
|
||||
- origin 与 `--repo` 一致;
|
||||
- current branch 与 `--ref` 一致;
|
||||
- HEAD 与 origin 远端 ref commit 一致;
|
||||
- `targetWorkspace` 已显式提供为绝对路径;
|
||||
- Target 上的该路径存在且为目录;
|
||||
- preflight 禁止读取 task worktree Git 状态:
|
||||
- dirty、ahead、behind、detached 和已有 diff 不产生 warning 或 blocker;
|
||||
- `repo` 和 `ref` 只保留为显式任务身份;
|
||||
- preflight 输出必须披露:
|
||||
- Target route、repo/ref、projectId、HEAD/remote 关系;
|
||||
- Target route、repo/ref、projectId 和 `gitStateInspected=false`;
|
||||
- 合法的 `workspaceRef.kind/path`;
|
||||
- 从 AipodSpec 继承的默认 resource bundle;
|
||||
- session identity;
|
||||
- provider/tool SecretRef 的对象、key 与 presence;
|
||||
- preflight 失败固定 `mutation=false`,不得创建 task;
|
||||
- preflight 通过后在同一命令内 submit 并自动 dispatch,不要求用户再运行第二条命令;
|
||||
- 未显式传 `--idempotency-key` 时,由 Target/repo/ref/MDTODO/verified commit 派生稳定键;
|
||||
- 未显式传 `--idempotency-key` 时,由 Target/repo/ref/targetWorkspace/MDTODO 和完整请求 payload 派生稳定键;
|
||||
- 幂等重试遇到已有 running 或 terminal task 时,返回既有 attempt/run/session,不重复 dispatch;
|
||||
- 最终输出必须包含 task、attempt、run、command、runner 和 session identity。
|
||||
- Target live 的默认 human、`-o json` 和 `-o yaml` 使用同一份有界投影:
|
||||
@@ -60,7 +61,7 @@ Artificer 内部执行边界:
|
||||
|
||||
- primary workspace 只承载默认 UniDesk bundle、`tools/trans` 和 required skills;
|
||||
- 任务源码操作全部使用 `trans <Target>:<targetWorkspace>`;
|
||||
- `targetWorkspace` 必须是任务专属 worktree;
|
||||
- `targetWorkspace` 是用户显式指定的任务工作目录;
|
||||
- runner 本地结果不得替代 Target 原入口证据;
|
||||
- `unidesk-trans`、runner tools bundle、`unidesk-ssh` SecretRef 和 endpoint presence 必须同时存在;
|
||||
- Secret 值不得进入 prompt、task、日志或报告。
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# R5.2 任务报告
|
||||
|
||||
## 结果
|
||||
|
||||
- 已解决 [UniDesk #2310](https://github.com/pikasTech/unidesk/issues/2310)。
|
||||
- 实现提交:`69c37875`。
|
||||
- 独立 PR:[UniDesk #2313](https://github.com/pikasTech/unidesk/pull/2313)。
|
||||
- 未修改生产运行面,未自行合并。
|
||||
|
||||
## 改动
|
||||
|
||||
- AgentRun Target preflight 仅保留 Target 重入标记、显式 `targetWorkspace`、绝对路径和目录存在性校验。
|
||||
- 删除 Git 根、origin、dirty、branch、HEAD 和远端 ref 的全部读取及 admission 结论。
|
||||
- dirty、ahead、behind、detached、branch/ref/origin 不匹配和已有 diff 不产生 warning 或 blocker。
|
||||
- `repo`、`ref` 和 `targetWorkspace` 仅作为显式任务身份参与幂等键。
|
||||
- CLI human、JSON 和 YAML 输出统一披露 `gitStateInspected=false`,不再输出 clean、commit 或 ref relation。
|
||||
- apply 与 dispatch 路径未发现第二处 task worktree Git 状态读取或门禁。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bun test scripts/src/agentrun.test.ts`:26 项通过,0 失败。
|
||||
- 回归夹具实际构造 dirty、ahead=1、behind=1、branch/ref/origin 不匹配状态,preflight 仍成功。
|
||||
- 绝对路径和目录存在性拒绝用例通过。
|
||||
- `bun scripts/cli.ts check --syntax-only`:11 项通过,0 失败。
|
||||
- `git diff --check`:通过。
|
||||
@@ -288,6 +288,10 @@
|
||||
|
||||
泛化改进 PaC consumer 首次 CI/CD bootstrap 与排错:source-artifact 在 PR 前验证流水线源码入口,debug-step 一次返回失败 task/step/exit code/有界日志,closeout --wait 持续观察并精确关联 source、GitOps、Argo 与 runtime digest,history 提示单 consumer 快路径;由 [GitHub issue #2307](https://github.com/pikasTech/unidesk/issues/2307) 跟踪,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R5.1_Task_Report.md)。
|
||||
|
||||
### R5.2 [completed]
|
||||
|
||||
解决 [UniDesk #2310](https://github.com/pikasTech/unidesk/issues/2310):彻底废弃 AgentRun 对 task worktree 的 Git 状态校验,使 dirty、ahead、behind 和已有 diff 均不阻断 create/apply/dispatch;只保留显式 targetWorkspace、绝对路径和目录存在性校验,不读取 Git 状态形成 warning 或 blocker,不修改生产运行面,提交独立 PR 且不自行合并,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R5.2_Task_Report.md)。
|
||||
|
||||
## R6 [completed]
|
||||
|
||||
处理 `529 Overloaded / API is at capacity` 客户可见错误:以 Sub2API 原生 Ops 证据锁定未触发 failover 的账号和规则缺口,在通用非 auth API-key 临时不可调度模板补 529 一分钟冷却并精准批量应用,排除 lyon9801,完成 runtime 回读和修复后短窗口复核,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R6_Task_Report.md)。
|
||||
|
||||
@@ -273,21 +273,20 @@ Artificer 标题契约:
|
||||
|
||||
- 默认直接执行 live 单命令,不把 `--dry-run` 作为派单前置步骤;
|
||||
- 未显式传 `--lane` 时,空选择或只传 YAML 默认 node 都使用 `config/agentrun.yaml#controlPlane.default`;
|
||||
- live 先经 Target `trans` 重入并执行 source/worktree/Aipod/SecretRef 只读预检;
|
||||
- live 先经 Target `trans` 重入,校验显式工作区路径与 Aipod/SecretRef;
|
||||
- 预检通过后在同一命令内 submit 并自动 dispatch;
|
||||
- 幂等重试不得重复创建 attempt 或重复 dispatch;
|
||||
- UniDesk 不实现 AgentRun queue 协议,也不把 task double-write 回旧 Code Queue。
|
||||
|
||||
Artificer Target preflight 必须在 task 创建前完成:
|
||||
|
||||
- `targetWorkspace` 是任务专属 Git worktree 根目录;
|
||||
- worktree 必须 clean;
|
||||
- origin 与 `--repo` 一致;
|
||||
- current branch 与 `--ref` 一致;
|
||||
- HEAD 与 origin 远端 ref commit 一致;
|
||||
- `targetWorkspace` 必须显式提供为 Target 上的绝对路径,且路径存在并为目录;
|
||||
- `repo` 和 `ref` 只作为用户显式提供的任务身份,不用于读取或判断工作区 Git 状态;
|
||||
- dirty、ahead、behind、detached 和已有 diff 均不产生 warning 或 blocker;
|
||||
- create、apply 和 dispatch 不得读取 task worktree Git 状态形成 admission 结论;
|
||||
- AipodSpec 渲染出的 `workspaceRef` 只允许 `kind/path`;
|
||||
- 默认 UniDesk resource bundle 保持继承,不把私有目标 repo 改成 runner primary bundle;
|
||||
- 输出包含 projectId、session identity、SecretRef presence 和 HEAD/remote 关系;
|
||||
- 输出包含 projectId、显式 Target 上下文、session identity 和 SecretRef presence;
|
||||
- 失败固定 `mutation=false`,且不创建 task;
|
||||
- 成功返回 task、attempt、run、command、runner 和 session identity。
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { readFileSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
parseAgentRunClientConfigYaml,
|
||||
@@ -37,6 +40,12 @@ function renderedContentTypeOf(value: unknown): string {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
function gitFixture(cwd: string, args: string[]): string {
|
||||
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
||||
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout));
|
||||
return String(result.stdout).trim();
|
||||
}
|
||||
|
||||
const agentRunClientYaml = [
|
||||
"version: 1",
|
||||
"kind: AgentRunConfig",
|
||||
@@ -219,13 +228,44 @@ describe("AgentRun render-only REST client config", () => {
|
||||
});
|
||||
|
||||
describe("Artificer Target one-command dispatch", () => {
|
||||
test("requires trans re-entry before Target filesystem preflight and derives a stable idempotency key", () => {
|
||||
test("keeps only explicit absolute path and directory existence admission", () => {
|
||||
expect(() => targetTaskRequestFromArgs([
|
||||
"--title", "检查 AgentRun 工作区路径",
|
||||
"--target", "NC01",
|
||||
"--target-workspace", "relative/workspace",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--ref", "fix/path-check",
|
||||
"--mdtodo-id", "R5.2",
|
||||
])).toThrow("--target-workspace 必须是 Target 上的绝对路径");
|
||||
|
||||
const request = targetTaskRequestFromArgs([
|
||||
"--title", "检查 AgentRun 工作区路径",
|
||||
"--target", "NC01",
|
||||
"--target-workspace", "/definitely/missing/agentrun-workspace",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--ref", "fix/path-check",
|
||||
"--mdtodo-id", "R5.2",
|
||||
]);
|
||||
if (request === null) throw new Error("expected Target request");
|
||||
const markerEnv = "UNIDESK_AGENTRUN_TARGET_EXECUTED_PATH_TEST";
|
||||
const previous = process.env[markerEnv];
|
||||
process.env[markerEnv] = request.target;
|
||||
try {
|
||||
expect(() => preflightTargetTask(request, markerEnv)).toThrow("Target 工作区不存在或不是目录");
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[markerEnv];
|
||||
else process.env[markerEnv] = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("requires trans re-entry but admits dirty, ahead, behind, and mismatched Git state", () => {
|
||||
const workspace = mkdtempSync(join(tmpdir(), "unidesk-agentrun-target-"));
|
||||
const request = targetTaskRequestFromArgs([
|
||||
"--title", "修复 SelfMedia 的 OCR 规划器",
|
||||
"--target", "NC01",
|
||||
"--target-workspace", "/root/.worktrees/selfmedia/r6-ocr-visual-planner",
|
||||
"--target-workspace", workspace,
|
||||
"--repo", "pikainc/selfmedia",
|
||||
"--ref", "feat/r6-ocr-visual-planner",
|
||||
"--ref", "declared/ref",
|
||||
"--mdtodo-id", "R6.3",
|
||||
]);
|
||||
expect(request).not.toBeNull();
|
||||
@@ -235,25 +275,53 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
delete process.env[markerEnv];
|
||||
try {
|
||||
expect(() => preflightTargetTask(request, markerEnv)).toThrow("只能在外层 trans 重入 NC01 后执行");
|
||||
|
||||
gitFixture(workspace, ["init", "-b", "actual/ref"]);
|
||||
gitFixture(workspace, ["config", "user.email", "test@example.com"]);
|
||||
gitFixture(workspace, ["config", "user.name", "AgentRun Test"]);
|
||||
writeFileSync(join(workspace, "tracked.txt"), "base\n", "utf8");
|
||||
gitFixture(workspace, ["add", "tracked.txt"]);
|
||||
gitFixture(workspace, ["commit", "-m", "base"]);
|
||||
const baseCommit = gitFixture(workspace, ["rev-parse", "HEAD"]);
|
||||
const baseTree = gitFixture(workspace, ["rev-parse", "HEAD^{tree}"]);
|
||||
writeFileSync(join(workspace, "tracked.txt"), "local\n", "utf8");
|
||||
gitFixture(workspace, ["commit", "-am", "local ahead"]);
|
||||
const remoteCommit = gitFixture(workspace, ["commit-tree", baseTree, "-p", baseCommit, "-m", "remote behind"]);
|
||||
gitFixture(workspace, ["update-ref", "refs/remotes/origin/declared/ref", remoteCommit]);
|
||||
gitFixture(workspace, ["remote", "add", "origin", "https://example.invalid/wrong/repository.git"]);
|
||||
writeFileSync(join(workspace, "tracked.txt"), "dirty diff\n", "utf8");
|
||||
expect(gitFixture(workspace, ["rev-list", "--left-right", "--count", "HEAD...refs/remotes/origin/declared/ref"]))
|
||||
.toMatch(/^1\s+1$/u);
|
||||
expect(gitFixture(workspace, ["status", "--porcelain"])).not.toBe("");
|
||||
|
||||
process.env[markerEnv] = request.target;
|
||||
const preflight = preflightTargetTask(request, markerEnv);
|
||||
expect(preflight).toMatchObject({
|
||||
targetWorkspace: workspace,
|
||||
workspaceRoot: workspace,
|
||||
repository: "pikainc/selfmedia",
|
||||
ref: "declared/ref",
|
||||
sourceAuthority: {
|
||||
kind: "explicit-target-context",
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
});
|
||||
expect(preflight).not.toHaveProperty("verifiedCommit");
|
||||
expect(preflight).not.toHaveProperty("clean");
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[markerEnv];
|
||||
else process.env[markerEnv] = previous;
|
||||
rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
const preflight = {
|
||||
...request,
|
||||
targetRoute: `${request.target}:${request.targetWorkspace}`,
|
||||
projectId: request.repository,
|
||||
workspaceRoot: request.targetWorkspace,
|
||||
currentBranch: request.ref,
|
||||
headCommit: "a".repeat(40),
|
||||
clean: true,
|
||||
originRepository: request.repository,
|
||||
verifiedCommit: "a".repeat(40),
|
||||
matchedRemoteRef: `refs/heads/${request.ref}`,
|
||||
refRelation: "branch-and-head-match-remote-ref",
|
||||
sourceAuthority: {
|
||||
kind: "target-workspace-origin",
|
||||
credentialScope: "target-owned-git-credential",
|
||||
kind: "explicit-target-context",
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
} satisfies AgentRunTargetTaskPreflight;
|
||||
@@ -275,16 +343,9 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
targetRoute: "NC01:/root/unidesk/.worktree/base",
|
||||
projectId: "pikasTech/unidesk",
|
||||
workspaceRoot: "/root/unidesk/.worktree/base",
|
||||
currentBranch: "fix/base",
|
||||
headCommit: "a".repeat(40),
|
||||
clean: true,
|
||||
originRepository: "pikasTech/unidesk",
|
||||
verifiedCommit: "a".repeat(40),
|
||||
matchedRemoteRef: "refs/heads/fix/base",
|
||||
refRelation: "branch-and-head-match-remote-ref",
|
||||
sourceAuthority: {
|
||||
kind: "target-workspace-origin",
|
||||
credentialScope: "target-owned-git-credential",
|
||||
kind: "explicit-target-context",
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
} as const;
|
||||
@@ -300,8 +361,6 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
...fixture,
|
||||
targetRoute: `NC01:${fixture.targetWorkspace}`,
|
||||
workspaceRoot: fixture.targetWorkspace,
|
||||
currentBranch: fixture.ref,
|
||||
matchedRemoteRef: `refs/heads/${fixture.ref}`,
|
||||
},
|
||||
{ aipod: "Artificer", tenantId: "default", projectId: fixture.repository, prompt: fixture.prompt },
|
||||
));
|
||||
@@ -312,8 +371,6 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
...fixtures[0],
|
||||
targetRoute: `NC01:${fixtures[0].targetWorkspace}`,
|
||||
workspaceRoot: fixtures[0].targetWorkspace,
|
||||
currentBranch: fixtures[0].ref,
|
||||
matchedRemoteRef: `refs/heads/${fixtures[0].ref}`,
|
||||
} satisfies AgentRunTargetTaskPreflight;
|
||||
const operation = { aipod: "Artificer", tenantId: "default", projectId: replay.repository, prompt: fixtures[0].prompt };
|
||||
const evidence = targetTaskOperationEvidence(replay, operation);
|
||||
@@ -497,12 +554,9 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
projectId: "pikasTech/unidesk",
|
||||
repository: "pikasTech/unidesk",
|
||||
ref: "fix/artificer-compact-output",
|
||||
verifiedCommit: "a".repeat(40),
|
||||
refRelation: "branch-and-head-match-remote-ref",
|
||||
workspace: {
|
||||
branch: "fix/artificer-compact-output",
|
||||
headCommit: "a".repeat(40),
|
||||
clean: true,
|
||||
root: "/root/.worktrees/unidesk/artificer-compact-output",
|
||||
gitStateInspected: false,
|
||||
},
|
||||
prompt: hugePrompt,
|
||||
},
|
||||
@@ -564,6 +618,9 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
}
|
||||
expect(text).toContain("R6.4");
|
||||
expect(text).toContain("dispatched");
|
||||
expect(text).toContain("gitStateInspected");
|
||||
expect(text).not.toContain("verifiedCommit");
|
||||
expect(text).not.toContain("refRelation");
|
||||
}
|
||||
|
||||
const jsonText = renderedTextOf(renderMutationSummary(
|
||||
@@ -592,6 +649,11 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
});
|
||||
expect((json.autoDispatch as Record<string, unknown>).decision).toBe("dispatched");
|
||||
expect((json.targetTask as Record<string, unknown>).mdtodoId).toBe("R6.4");
|
||||
expect((json.targetTask as Record<string, unknown>).workspace).toEqual({
|
||||
root: "/root/.worktrees/unidesk/artificer-compact-output",
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
});
|
||||
expect(jsonCredentials).toMatchObject({ totalCount: 2, allPresent: true });
|
||||
expect((jsonCredentials.provider as Record<string, unknown>).presentCount).toBe(1);
|
||||
expect((jsonCredentials.tool as Record<string, unknown>).presentCount).toBe(1);
|
||||
|
||||
@@ -532,8 +532,7 @@ export function renderTargetTaskMutationLines(targetTask: Record<string, unknown
|
||||
` MDTODO: ${displayValue(targetTask.mdtodoId)} project=${displayValue(targetTask.projectId)}`,
|
||||
` Target: ${displayValue(targetTask.targetRoute)}`,
|
||||
` Source: ${displayValue(targetTask.repository)} ref=${displayValue(targetTask.ref)}`,
|
||||
` Commit: HEAD=${displayValue(workspace.headCommit)} remote=${displayValue(targetTask.verifiedCommit)}`,
|
||||
` Worktree: branch=${displayValue(workspace.branch)} clean=${displayValue(workspace.clean)} relation=${displayValue(targetTask.refRelation)}`,
|
||||
` Workspace: root=${displayValue(workspace.root)} gitStateInspected=${displayValue(workspace.gitStateInspected)}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -78,12 +78,9 @@ function compactTargetTaskSummary(targetTask: Record<string, unknown>): Record<s
|
||||
projectId: boundedText(targetTask.projectId, 240),
|
||||
repository: boundedText(targetTask.repository, 240),
|
||||
ref: boundedText(targetTask.ref, 240),
|
||||
verifiedCommit: boundedText(targetTask.verifiedCommit, 160),
|
||||
refRelation: boundedText(targetTask.refRelation, 120),
|
||||
workspace: {
|
||||
branch: boundedText(workspace.branch, 240),
|
||||
headCommit: boundedText(workspace.headCommit, 160),
|
||||
clean: booleanOrNull(workspace.clean),
|
||||
root: boundedText(workspace.root, 360),
|
||||
gitStateInspected: booleanOrNull(workspace.gitStateInspected),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export interface AgentRunTargetTaskRequest {
|
||||
readonly title: string;
|
||||
@@ -15,16 +14,9 @@ export interface AgentRunTargetTaskPreflight extends AgentRunTargetTaskRequest {
|
||||
readonly targetRoute: string;
|
||||
readonly projectId: string;
|
||||
readonly workspaceRoot: string;
|
||||
readonly currentBranch: string;
|
||||
readonly headCommit: string;
|
||||
readonly clean: true;
|
||||
readonly originRepository: string;
|
||||
readonly verifiedCommit: string;
|
||||
readonly matchedRemoteRef: string;
|
||||
readonly refRelation: "branch-and-head-match-remote-ref";
|
||||
readonly sourceAuthority: {
|
||||
readonly kind: "target-workspace-origin";
|
||||
readonly credentialScope: "target-owned-git-credential";
|
||||
readonly kind: "explicit-target-context";
|
||||
readonly gitStateInspected: false;
|
||||
readonly valuesPrinted: false;
|
||||
};
|
||||
}
|
||||
@@ -50,7 +42,6 @@ export interface AgentRunTargetTaskOperationEvidence {
|
||||
readonly repository: string;
|
||||
readonly ref: string;
|
||||
readonly targetWorkspace: string;
|
||||
readonly verifiedCommit: string;
|
||||
};
|
||||
readonly reason: "canonical-logical-operation";
|
||||
readonly valuesPrinted: false;
|
||||
@@ -117,94 +108,14 @@ export function preflightTargetTask(request: AgentRunTargetTaskRequest, markerEn
|
||||
);
|
||||
}
|
||||
|
||||
const gitRoot = git(request, ["rev-parse", "--show-toplevel"], "workspace-git-root").trim();
|
||||
let observedRoot: string;
|
||||
try {
|
||||
observedRoot = realpathSync(gitRoot);
|
||||
} catch {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"workspace-git-root",
|
||||
`Target Git 返回了无效工作区根目录:${gitRoot}`,
|
||||
{ targetWorkspace: request.targetWorkspace },
|
||||
);
|
||||
}
|
||||
if (observedRoot !== workspaceRoot) {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"workspace-git-root",
|
||||
`--target-workspace 必须指向 Git 工作区根目录;当前根目录是 ${gitRoot}`,
|
||||
{ targetWorkspace: request.targetWorkspace, observedWorkspaceRoot: gitRoot },
|
||||
);
|
||||
}
|
||||
|
||||
const originUrl = git(request, ["remote", "get-url", "origin"], "origin").trim();
|
||||
const originRepository = repositoryFromRemote(originUrl);
|
||||
if (originRepository === null || originRepository.toLowerCase() !== request.repository.toLowerCase()) {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"origin",
|
||||
`Target 工作区 origin 与 --repo 不一致;期望 ${request.repository}。`,
|
||||
{
|
||||
expectedRepository: request.repository,
|
||||
observedRepository: originRepository,
|
||||
observedRemoteFingerprint: sha256(originUrl),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const status = git(request, ["status", "--porcelain=v1", "--untracked-files=normal"], "workspace-status");
|
||||
const dirtyEntryCount = status.split(/\r?\n/u).filter(Boolean).length;
|
||||
if (dirtyEntryCount > 0) {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"workspace-status",
|
||||
"Target 固定工作区存在未提交修改;请先保存并按语义提交并行改动,再重新派单。",
|
||||
{ targetWorkspace: request.targetWorkspace, dirtyEntryCount },
|
||||
);
|
||||
}
|
||||
|
||||
const currentBranch = git(request, ["branch", "--show-current"], "workspace-branch").trim() || "(detached)";
|
||||
const headCommit = git(request, ["rev-parse", "HEAD"], "workspace-head").trim().toLowerCase();
|
||||
const remote = git(request, ["ls-remote", "--exit-code", "origin", request.ref], "remote-ref");
|
||||
const refs = remote.split(/\r?\n/u)
|
||||
.map((line) => line.trim().split(/\s+/u))
|
||||
.filter((parts) => /^[0-9a-f]{40}$/u.test(parts[0] ?? "") && (parts[1] ?? "").length > 0)
|
||||
.map((parts) => ({ commit: parts[0] as string, ref: parts[1] as string }));
|
||||
const selected = refs.find((item) => item.ref === `refs/heads/${request.ref}`) ?? refs[0];
|
||||
if (selected === undefined) {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"remote-ref",
|
||||
`Target 工作区 origin 中找不到 --ref ${request.ref}。`,
|
||||
{ repository: request.repository, ref: request.ref },
|
||||
);
|
||||
}
|
||||
if (currentBranch !== request.ref || headCommit !== selected.commit.toLowerCase()) {
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
"workspace-ref-relation",
|
||||
"Target 任务 worktree 必须检出 --ref,且 HEAD 必须等于 origin 远端提交;请先完成分支切换、提交和推送。",
|
||||
{
|
||||
targetWorkspace: request.targetWorkspace,
|
||||
currentBranch,
|
||||
expectedBranch: request.ref,
|
||||
headCommit,
|
||||
remoteCommit: selected.commit,
|
||||
matchedRemoteRef: selected.ref,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
targetRoute: `${request.target}:${request.targetWorkspace}`,
|
||||
projectId: request.repository,
|
||||
workspaceRoot,
|
||||
currentBranch,
|
||||
headCommit,
|
||||
clean: true,
|
||||
originRepository,
|
||||
verifiedCommit: selected.commit,
|
||||
matchedRemoteRef: selected.ref,
|
||||
refRelation: "branch-and-head-match-remote-ref",
|
||||
sourceAuthority: {
|
||||
kind: "target-workspace-origin",
|
||||
credentialScope: "target-owned-git-credential",
|
||||
kind: "explicit-target-context",
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
@@ -212,7 +123,7 @@ export function preflightTargetTask(request: AgentRunTargetTaskRequest, markerEn
|
||||
|
||||
export function targetTaskPrompt(preflight: AgentRunTargetTaskPreflight, userPrompt: string | null): string {
|
||||
const context = [
|
||||
"任务目标上下文(由 UniDesk YAML-first CLI 在创建 task 前经 Target trans 只读核验):",
|
||||
"任务目标上下文(由用户显式提供,并由 UniDesk CLI 在 Target trans 重入后校验目录存在):",
|
||||
`- 中文任务标题: ${preflight.title}`,
|
||||
`- MDTODO ID: ${preflight.mdtodoId}`,
|
||||
`- Target: ${preflight.target}`,
|
||||
@@ -220,9 +131,7 @@ export function targetTaskPrompt(preflight: AgentRunTargetTaskPreflight, userPro
|
||||
`- targetRoute: ${preflight.targetRoute}`,
|
||||
`- repository: ${preflight.repository}`,
|
||||
`- ref: ${preflight.ref}`,
|
||||
`- verifiedCommit: ${preflight.verifiedCommit}`,
|
||||
`- refRelation: ${preflight.refRelation}`,
|
||||
"- sourceAuthority: target-workspace-origin(凭据值不进入 task、prompt 或日志)",
|
||||
"- sourceAuthority: explicit-target-context(不读取 Git 状态,不把 dirty/ahead/behind/diff 作为门禁或警告)",
|
||||
"",
|
||||
"执行边界:",
|
||||
`- 目标源码的探测、Git、编辑、验证和运行面观察全部使用 \`trans ${preflight.target}:${preflight.targetWorkspace} <operation>\` 或对应 k3s route。`,
|
||||
@@ -242,14 +151,10 @@ export function targetTaskDisclosure(preflight: AgentRunTargetTaskPreflight): Re
|
||||
repository: preflight.repository,
|
||||
ref: preflight.ref,
|
||||
projectId: preflight.projectId,
|
||||
verifiedCommit: preflight.verifiedCommit,
|
||||
matchedRemoteRef: preflight.matchedRemoteRef,
|
||||
refRelation: preflight.refRelation,
|
||||
workspace: {
|
||||
root: preflight.workspaceRoot,
|
||||
branch: preflight.currentBranch,
|
||||
headCommit: preflight.headCommit,
|
||||
clean: preflight.clean,
|
||||
gitStateInspected: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
sourceAuthority: preflight.sourceAuthority,
|
||||
mutation: false,
|
||||
@@ -272,7 +177,6 @@ export function targetTaskOperationEvidence(
|
||||
repository: preflight.repository,
|
||||
ref: preflight.ref,
|
||||
targetWorkspace: preflight.targetWorkspace,
|
||||
verifiedCommit: preflight.verifiedCommit,
|
||||
};
|
||||
const fingerprint = sha256Hex(JSON.stringify({ version: "v1", scope, payloadHash }));
|
||||
return {
|
||||
@@ -353,33 +257,6 @@ export function isChineseTaskTitle(value: string | undefined): value is string {
|
||||
return typeof value === "string" && /\p{Script=Han}/u.test(value);
|
||||
}
|
||||
|
||||
function git(request: AgentRunTargetTaskRequest, args: string[], stage: string): string {
|
||||
const result = spawnSync("git", args, {
|
||||
cwd: request.targetWorkspace,
|
||||
encoding: "utf8",
|
||||
timeout: 15_000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
if (result.error !== undefined || result.status !== 0) {
|
||||
const stderr = String(result.stderr ?? "").trim().split(/\r?\n/u)[0] ?? "";
|
||||
throw new AgentRunTargetTaskPreflightError(
|
||||
stage,
|
||||
`Target Git 只读预检失败(${stage});请先在 ${request.target}:${request.targetWorkspace} 修复 origin/ref/凭据可达性。`,
|
||||
{
|
||||
target: request.target,
|
||||
targetWorkspace: request.targetWorkspace,
|
||||
repository: request.repository,
|
||||
ref: request.ref,
|
||||
exitCode: result.status,
|
||||
timedOut: result.error?.name === "ETIMEDOUT",
|
||||
stderrSummary: redactGitError(stderr),
|
||||
},
|
||||
);
|
||||
}
|
||||
return String(result.stdout ?? "");
|
||||
}
|
||||
|
||||
function option(args: string[], name: string): string | null {
|
||||
const flag = `--${name}`;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -437,19 +314,6 @@ function validateMdtodoId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function repositoryFromRemote(value: string): string | null {
|
||||
const normalized = value.replace(/\.git$/u, "");
|
||||
const match = normalized.match(/^(?:git@github\.com:|https:\/\/github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/u);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function redactGitError(value: string): string {
|
||||
return value
|
||||
.replace(/https?:\/\/[^@\s]+@/gu, "https://[redacted]@")
|
||||
.replace(/(token|password|authorization)[=:][^\s]+/giu, "$1=[redacted]")
|
||||
.slice(0, 240);
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return `sha256:${sha256Hex(value)}`;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ describe("PikaOA development PaC renderer", () => {
|
||||
expect(manifest.match(/key: DATABASE_URL/gu)).toHaveLength(3);
|
||||
expect(manifest.match(/name: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
|
||||
expect(manifest.match(/key: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
|
||||
expect(manifest).toContain("name: PIKAOA_MAX_UPLOAD_BYTES");
|
||||
expect(manifest).toContain('value: "104857600"');
|
||||
expect(script).toContain("grep -Eq '@sha256:[0-9a-f]{64}$'");
|
||||
expect(manifest).toContain("image: __PIKAOA_API_IMAGE__");
|
||||
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
|
||||
|
||||
@@ -114,6 +114,7 @@ export function renderPikaoaRuntimeManifest(
|
||||
], target.probes.workerLivenessPath, target.probes.workerReadinessPath),
|
||||
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, {}, [
|
||||
{ name: target.runtime.webApiUpstreamEnv, value: `http://${target.runtime.apiServiceName}.${context.namespace}.svc.cluster.local:${target.runtime.apiPort}` },
|
||||
{ name: "PIKAOA_MAX_UPLOAD_BYTES", value: String(target.runtime.attachment.maxBytes) },
|
||||
], target.probes.webLivenessPath, target.probes.webReadinessPath),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user