Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"smoke:home-browser": "node ../../../examples/dashboard-home-browser-smoke.mjs",
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/** Exercise the existing packaged Team execution UI with the shared TS read model. */
import assert from "node:assert/strict";
import {mkdir} from "node:fs/promises";
import {resolve} from "node:path";
import {delegationPreflight} from "../../../../loopx/control_plane/collaboration/delegation.ts";
import {launchBrowser, loadPlaywright, waitForHttp} from "../../../../examples/dashboard-browser-smoke-support.mjs";
import {outputDir, packaged, port, startServer} from "../../../../examples/personal-workspace-browser/fixture.mjs";
import {openWorkspacePage} from "../../../../examples/personal-workspace-browser/scenario-context.mjs";

await mkdir(outputDir, {recursive: true});
const server = startServer();
let browser;
try {
const url = `http://127.0.0.1:${port}/${packaged ? "chat/" : ""}?statusUrl=/status.json`;
await waitForHttp(url);
browser = await launchBrowser(loadPlaywright().chromium);
for (const zh of [true, false]) {
const context = await openWorkspacePage({newPage: options => browser.newPage({
...options, locale: zh ? "zh-CN" : "en-US",
})}, url);
const {api, page} = context;
let state = "legacy_error";
const inspected = [];
try {
await page.route("**/api/chat/sessions/*/loopx", async route => {
const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
if (body.operation !== "inspect" || state === "available") return route.fallback();
inspected.push(body.binding_id);
if (state === "legacy_error") return route.fulfill({status: 409,
json: {error: "delegation workspace unavailable"}});
const binding = {id: body.binding_id, agent_id: "fixture-worker", todo_id: "fixture-task"};
const check = delegationPreflight({binding, workspace: {state}, authority: null,
preview: null, acceptance: null, validation_files_current: false});
await route.fulfill({json: check});
});
await page.locator(".personal-goal-link", {hasText: "Product Release"}).click();
await page.getByRole("navigation", {name: zh ? "Goal 视图" : "Goal view"})
.getByRole("button", {name: zh ? "对话" : "Chat", exact: true}).click();
await page.getByRole("button", {name: zh ? "开启 LoopX 模式" : "Enable LoopX", exact: true}).click();
const settings = page.locator(".goal-loopx-mode-settings");
await settings.getByLabel(zh ? "已注册的协调身份" : "Registered coordinator").selectOption("lead");
await settings.getByLabel(zh ? "协调员总 token 额度" : "Coordinator total token allowance").fill("100000");
assert.equal(await settings.getByLabel(zh ? "成员执行绑定文件(Goal 配置)" : "Member execution bindings (Goal configuration)").isEditable(), false);
await settings.getByRole("button", {name: zh ? "保存设置" : "Save settings", exact: true}).click();
await page.getByRole("button", {name: zh ? "团队执行情况" : "Team execution", exact: true}).click();
const team = page.getByRole("region", {name: zh ? "团队执行详情" : "Team execution details"});
// Render the error response reproduced on the base, not a baseline UI build.
await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).click();
await team.locator(".goal-team-bindings > li > p[role=alert]").nth(2).waitFor();
for (const [size, viewport] of [["desktop", {width: 1512, height: 982}], ["mobile", {width: 390, height: 844}]]) {
await page.setViewportSize(viewport);
await page.screenshot({path: resolve(outputDir, `workspace-preflight-before-response-${zh ? "zh" : "en"}-${size}.png`), animations: "disabled"});
}
assert.equal(context.errors.length, 3, "Only the three injected baseline HTTP refusals are expected");
for (const error of context.errors) assert.match(error,
/^Failed to load resource: the server responded with a status of 409 \(Conflict\)$/);
context.errors.splice(0); // Keep subsequent recovery checks error-free.
for (const fault of ["missing", "not_directory", "unavailable"]) {
state = fault;
await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).click();
await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).waitFor({state: "visible"});
const expected = zh
? {missing: "目录不存在", not_directory: "绑定位置不是目录", unavailable: "目录无法读取"}[fault]
: {missing: "Directory missing", not_directory: "Bound location is not a directory", unavailable: "Directory could not be read"}[fault];
const observations = team.locator(".goal-team-bindings > li > p[role=status]");
await observations.filter({hasText: expected}).nth(2).waitFor();
assert.equal(await observations.count(), 3);
assert.equal(new Set(inspected.slice(-3)).size, 3);
const content = await observations.first().innerText();
assert.match(content, zh ? /核对原执行配置的工作目录/ : /review the workspace in the original execution configuration/);
assert.match(content, zh ? /权限、验收与运行时尚未检查/ : /Authority, acceptance and runtime uninspected/);
assert.doesNotMatch(content, /晋级|promotion|Canonical authority unavailable|缺少规范权限状态/);
}
const dialog = page.getByRole("dialog", {name: zh ? "团队执行情况" : "Team execution", exact: true});
for (const [size, viewport] of [["desktop", {width: 1512, height: 982}], ["mobile", {width: 390, height: 844}]]) {
await page.setViewportSize(viewport);
assert(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth), "Workspace recovery copy must not overflow the dialog");
await page.screenshot({path: resolve(outputDir, `workspace-preflight-${zh ? "zh" : "en"}-${size}.png`), animations: "disabled"});
}
const member = team.locator(".goal-team-bindings > li").filter({hasText: "local-analyst"});
await member.locator("summary").click();
state = "available";
await member.getByRole("button", {name: zh ? "重新检查此成员" : "Recheck this member", exact: true}).click();
await member.getByText(zh ? /运行时可用性尚未验证/ : /Runtime availability unverified/).waitFor();
assert.equal(await member.getByText(zh ? /绑定工作目录不可用/ : /Bound workspace unavailable/).count(), 0);
assert.equal(api.turnRequests.length, 0, "Inspection/recovery must not start model work");
assert.equal(context.errors.length, 0, context.errors.join("\n"));
console.log(`workspace preflight ${zh ? "Chinese" : "English"}: three faults, exact recheck and desktop/mobile passed`);
} finally {
await context.close();
}
}
} finally {
await browser?.close();
server.kill("SIGTERM");
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,18 @@ for (const [zh, expectedLabel, expectedBoundary] of [
}
if (Object.values(unavailable.effects).some(Boolean)) throw new Error("authority-unavailable response must report zero effects");

console.log("delegation authority-unavailable preflight smoke passed");
for (const workspace_state of ["missing", "not_directory", "unavailable"] as const) {
const check: DelegationPreflight = {...unavailable, state: "workspace_unavailable",
workspace_state, workspace_next_action: "review_operator_workspace_binding",
authority_ready: null, authority_reason: null, authority_state: "uninspected",
authority_next_action: "none"};
for (const zh of [true, false]) {
const html = renderToStaticMarkup(<DelegationPreflightStatus check={check} zh={zh}/>);
if (!html.includes(zh ? "绑定工作目录不可用" : "Bound workspace unavailable")) throw new Error("missing workspace fault label");
if (!html.includes(zh ? "核对原执行配置的工作目录" : "review the workspace in the original execution configuration")) throw new Error("missing workspace recovery action");
if (!html.includes(zh ? "权限、验收与运行时尚未检查" : "Authority, acceptance and runtime uninspected")) throw new Error("workspace fault misreports inspection");
if (/晋级|promotion|Canonical authority unavailable|缺少规范权限状态/.test(html)) throw new Error("workspace fault suggested authority repair/promotion");
}
}

console.log("delegation authority/workspace-unavailable preflight smoke passed");
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export type DelegationPreflight = {
state: "authority_unavailable" | "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
state: "workspace_unavailable" | "authority_unavailable" | "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
workspace_state?: "missing" | "not_directory" | "unavailable";
workspace_next_action?: "review_operator_workspace_binding";
turn_eligible: boolean;
acceptance_ready: boolean;
turn_route: string | null;
authority_ready: boolean;
authority_ready: boolean | null;
authority_reason: string | null;
authority_state: "promotion_required" | "unavailable" | "promoted";
authority_state: "promotion_required" | "unavailable" | "promoted" | "uninspected";
authority_next_action: "preview_reviewed_goal_authority_promotion" | "repair_canonical_authority" | "none";
promotion_from_surface_allowed: false;
executor: {host: string; available: boolean | null; reason: string | null; profile: string | null} | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {DelegationPreflight} from "../../data/delegation-preflight.js";

const labels: Record<DelegationPreflight["state"], {en: string; zh: string}> = {
workspace_unavailable: {en: "Bound workspace unavailable", zh: "绑定工作目录不可用"},
authority_unavailable: {en: "Canonical authority unavailable", zh: "缺少规范权限状态"},
turn_blocked: {en: "Task admission blocked", zh: "当前任务未获准执行"},
acceptance_unavailable: {en: "Acceptance binding unavailable", zh: "缺少有效验收绑定"},
Expand All @@ -9,22 +10,35 @@ const labels: Record<DelegationPreflight["state"], {en: string; zh: string}> = {
launchable: {en: "Local launch prerequisites met", zh: "本机启动条件已满足"},
};

const workspaceDetails = {
missing: {en: "Directory missing", zh: "目录不存在"},
not_directory: {en: "Bound location is not a directory", zh: "绑定位置不是目录"},
unavailable: {en: "Directory could not be read", zh: "目录无法读取"},
};

export function DelegationPreflightStatus({check, zh}: {check: DelegationPreflight; zh: boolean}) {
const detail = check.executor
? [check.executor.host, check.executor.reason].filter(Boolean).join(" · ")
: check.authority_reason;
const disclaimer = check.state === "authority_unavailable"
? (zh ? "未检查或启动执行器" : "No executor was inspected or launched")
: (zh ? "不代表正在执行" : "Does not mean executing");
const authorityAction = check.authority_next_action === "preview_reviewed_goal_authority_promotion"
const workspaceDetail = check.workspace_state ? workspaceDetails[check.workspace_state] : null;
const detail = check.state === "workspace_unavailable"
? (workspaceDetail ? (zh ? workspaceDetail.zh : workspaceDetail.en) : null)
: check.executor
? [check.executor.host, check.executor.reason].filter(Boolean).join(" · ")
: check.authority_reason;
const disclaimer = check.state === "workspace_unavailable"
? (zh ? "权限、验收与运行时尚未检查;未启动成员" : "Authority, acceptance and runtime uninspected; no member launched")
: check.state === "authority_unavailable"
? (zh ? "未检查或启动执行器" : "No executor was inspected or launched")
: (zh ? "不代表正在执行" : "Does not mean executing");
const nextAction = check.workspace_next_action === "review_operator_workspace_binding"
? (zh ? "下一步:核对原执行配置的工作目录,再重新检查" : "Next: review the workspace in the original execution configuration, then recheck")
: check.authority_next_action === "preview_reviewed_goal_authority_promotion"
? (zh ? "下一步:预览整 Goal 协调 Authority 晋级" : "Next: preview whole-Goal coordination-authority promotion")
: check.authority_next_action === "repair_canonical_authority"
? (zh ? "下一步:修复规范 Authority 读回" : "Next: repair canonical authority readback")
: null;
return <p role="status">
{zh ? labels[check.state].zh : labels[check.state].en}
{detail ? ` · ${detail}` : ""}
{authorityAction ? ` · ${authorityAction}` : ""}
{nextAction ? ` · ${nextAction}` : ""}
{` · ${disclaimer}`}
</p>;
}
22 changes: 22 additions & 0 deletions docs/reference/local-delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,28 @@ Normal start still reads current admission and independently validates output.
If the existing Turn rejects preflight, inspection reports that error rather than
manufacturing a launchable result; no request is created.

Workspace failures are observed before authority or Turn inspection, but after
the caller's binding grant and active Goal are checked. `workspace_unavailable`
retains a bounded `workspace_state`: `missing`, `not_directory`, or `unavailable`
(the filesystem could not be read). `authority_ready: null` and
`authority_state: uninspected` do not claim an authority failure or promotion
requirement. `workspace_next_action: review_operator_workspace_binding` asks the
operator to check the original execution configuration and its directory, then
repeat inspection. The same projection is rendered in **Team execution** and
returned by CLI/MCP/Goal Chat. Inspection creates no directory, retargets no
binding, launches no worker and exports no private path or filesystem error.
It is a point-in-time observation; a changed binding fails closed and start
still rechecks the existing work/authority boundaries.

中文:工作目录故障在调用者授权与 Goal 活跃检查之后、Authority 与 Turn 检查之前
返回 `workspace_unavailable`。`workspace_state` 区分 `missing`(不存在)、
`not_directory`(不是目录)和 `unavailable`(文件系统无法读取)。
`authority_ready: null`、`authority_state: uninspected` 表示尚未检查,不能解释为
权限故障或需要晋级。`workspace_next_action: review_operator_workspace_binding`
要求核对原执行配置及绑定目录后重新检查;团队执行页面与 CLI/MCP/Goal Chat 消费
同一投影。检查不创建目录、不改绑、不启动成员,也不暴露私人路径或文件系统错误。
结果只是时点观察;配置变更仍拒绝,实际启动仍须重新通过原有工作与权限边界。

`executor.runtime_probe` preserves the host's bounded probe scope: a DSH
`probing_interpreter` result concerns module availability in the interpreter
running this inspection, not every installation or remote host. A
Expand Down
26 changes: 24 additions & 2 deletions loopx/collaboration_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,28 @@ def operations(self, *, limit: int = 20, cursor: str | None = None) -> dict:
def inspect(self, binding_id: str) -> dict:
"""Observe the real Turn preflight; never create a request or run a host."""
binding = self.binding(binding_id, require_active=True)
if not Path(binding["workspace"]).is_dir():
raise ValueError("delegation workspace unavailable")
# Host filesystem facts only; the shared TS owner projects readiness.
# Do not expose a path/error body or probe authority in a missing cwd.
try:
workspace_state = (
"available" if stat.S_ISDIR(Path(binding["workspace"]).stat().st_mode)
else "not_directory"
)
except FileNotFoundError:
workspace_state = "missing"
except NotADirectoryError:
workspace_state = "not_directory"
except OSError:
workspace_state = "unavailable"
if workspace_state != "available":
if self.binding(binding_id, require_active=True) != binding:
raise ValueError("delegation preflight source changed; retry inspection")
return effect_runtime_result("collaboration.delegation.preflight", {
"binding": {key: binding[key] for key in ("id", "agent_id", "todo_id")},
"workspace": {"state": workspace_state},
"authority": None, "preview": None, "acceptance": None,
"validation_files_current": False,
})
try:
acceptance = delegation_validation.capture(self, binding)
except (OSError, ValueError) as exc:
Expand All @@ -318,6 +338,7 @@ def inspect(self, binding_id: str) -> dict:
promoted = True
return effect_runtime_result("collaboration.delegation.preflight", {
"binding": {key: binding[key] for key in ("id", "agent_id", "todo_id")},
"workspace": {"state": workspace_state},
"authority": {
"ready": False,
"reason": str(exc),
Expand Down Expand Up @@ -363,6 +384,7 @@ def inspect(self, binding_id: str) -> dict:
raise ValueError("delegation preflight source changed; retry inspection")
return effect_runtime_result("collaboration.delegation.preflight", {
"binding": {key: binding[key] for key in ("id", "agent_id", "todo_id")},
"workspace": {"state": workspace_state},
"authority": {"ready": True, "reason": None},
"preview": preview, "acceptance": acceptance["plan"],
"validation_files_current": acceptance["files_current"],
Expand Down
Loading
Loading