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
9 changes: 9 additions & 0 deletions docs/reference/canonical-lease-renew.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ cooperative code-edit coordination, not a filesystem access-control mechanism.
It does not authorize changing shared runtime data, Git administration, remote
branches, or merging. Use ordinary exclusive leases for those operations.

Accepted origin URLs use the same repository identity rules as Todo declarations:
explicit default transport ports (including SSH `:22` and Git `:9418`) do not
create another repository, while nondefault ports remain distinct. Origins with
passwords or unsafe path segments are rejected before lease acquisition.

Same-worktree aliases, the same Todo, other machines or clones, and grants
without a verified workspace retain existing exclusion. Repository mismatch,
redirected paths and a non-worktree root fail closed. Verified machine discovery
Expand Down Expand Up @@ -103,6 +108,10 @@ JSON 与 Markdown 读回同一仓库字段或未知状态。这只是 Goal 内
不识别物理目录、软链接别名,也不是跨 Goal 锁;不新增配置、promotion 或自动
委派。CLI 与 native provider 检查已覆盖该边界,完整前端/Lark 协作旅程仍需单独交付。

独立 worktree 的 origin URL 与 Todo 声明使用同一仓库身份规则:显式默认端口
(包括 SSH `:22` 和 Git `:9418`)不产生另一个仓库身份,非默认端口仍须匹配。
带密码或不安全路径段的 origin 在获取租约前被拒绝。

## Operate the current lease

Read the current canonical lease and use its owner, execution key and version:
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/work_items/task_lease_workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {BARE_SHA256_PATTERN} from "../content_digest.ts";
import type {JsonObject} from "../effect_program.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {normalizeTodoRepository} from "../todos/work_requirements.ts";
import {leaseWriteRepository} from "./task_lease_repository.ts";

export interface LeaseWorkspace extends JsonObject {
Expand Down Expand Up @@ -84,10 +85,11 @@ export async function observeLeaseWorktree(path: string, overlaps: (path: string
const remote = await git("remote", "get-url", "origin");
const scp = /^[^@/:]+@([^/:]+):(.+)$/u.exec(remote);
const url = scp ? new URL(`ssh://${scp[1]}/${scp[2]}`) : new URL(remote);
if (!["ssh:", "https:", "http:"].includes(url.protocol) || !url.hostname || url.search || url.hash) {
if (!["git:", "ssh:", "https:", "http:"].includes(url.protocol) || !url.hostname || url.search || url.hash) {
throw new EffectRuntimeRequestError("worktree origin must identify a Git repository");
}
const repository = leaseWriteRepository(`git:${url.host.toLowerCase()}/${url.pathname.replace(/^\/+|\/+$/gu, "").replace(/\.git$/u, "")}`)!;
// Validate the transport above, but share the Todo's raw-remote identity codec.
const repository = leaseWriteRepository(normalizeTodoRepository(remote))!;
// Private local paths are not persisted. Filesystem inode identity collapses
// symlink/case aliases; a recreated directory gets a new identity.
const key = async (p: string) => {const s = await stat(p); return digest(`${s.dev}:${s.ino}`);};
Expand Down
49 changes: 49 additions & 0 deletions tests/control_plane_ts/task_lease_workspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import test from "node:test";
import {execFileSync} from "node:child_process";
import {mkdtemp, rm} from "node:fs/promises";
import {platform, tmpdir} from "node:os";
import {join} from "node:path";
import {evaluateTaskLeaseAcquireDecision} from "../../loopx/control_plane/work_items/task_lease_acquire_decision.ts";
import {leaseWorkspace, independentLeaseWorktrees, observeLeaseWorktree} from "../../loopx/control_plane/work_items/task_lease_workspace.ts";

Expand All @@ -9,6 +13,51 @@ test("native observation never interprets a caller path relative to the worker",
await assert.rejects(observeLeaseWorktree(".", () => false), /absolute/);
});

test("observed Git origins use the Todo repository identity contract", {
skip: !["darwin", "linux"].includes(platform()),
}, async t => {
const temporary = await mkdtemp(join(tmpdir(), "loopx-lease-origin-"));
t.after(() => rm(temporary, {recursive: true, force: true}));
const main = join(temporary, "main"), worktree = join(temporary, "worktree");
const git = (...args: string[]) => execFileSync("git", args, {stdio: "pipe"});
git("init", main);
git("-C", main, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.com",
"commit", "--allow-empty", "-m", "fixture");
git("-C", main, "worktree", "add", "-b", "fixture", worktree);
for (const [origin, expected] of [
["https://GITHUB.com:443/example/project.git", repo],
["ssh://git@github.com:22/example/project.git", repo],
["git://github.com:9418/example/project.git", repo],
["git@github.com:example/project.git", repo],
["ssh://git@github.com/example//project.git", repo],
["ssh://git@github.com:2222/example/project.git", "git:github.com:2222/example/project"],
]) {
await t.test(origin, async () => {
git("-C", main, "config", "remote.origin.url", origin);
const observed = await observeLeaseWorktree(worktree, () => false);
assert.equal(observed.repository, expected);
const input = request();
const decision = evaluateTaskLeaseAcquireDecision({...input, other_leases: [],
todo: {...input.todo, task_repository: expected}, command: {...input.command, write_workspace: observed}});
assert.equal(decision.outcome, "apply");
});
}
for (const origin of [
"https://fixture:fixture@github.com/example/project.git",
"https://github.com/example/../project.git",
"https://github.com/example\\project.git",
"https://github.com/example/project.git?query=fixture",
"https://github.com/example/project.git#fragment",
"file:///example/project.git",
"git:github.com/example/project.git",
]) {
await t.test(`reject ${origin}`, async () => {
git("-C", main, "config", "remote.origin.url", origin);
await assert.rejects(observeLeaseWorktree(worktree, () => false));
});
}
});

function request(other: unknown = {...workspace, worktree: "4".repeat(64)}) {
return {handoff_mode: "hard_lease", registered_agents: ["agent-a", "agent-b"],
todo: {todo_id: "todo_a", status: "open", claimed_by: "agent-a", excluded_agents: [], task_repository: repo}, lease: null,
Expand Down
17 changes: 14 additions & 3 deletions tests/test_task_lease_worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@

@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="verified host identity currently supports macOS/Linux")
@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_worktree_scope_admission_and_replay(tmp_path, monkeypatch, capsys, provider):
@pytest.mark.parametrize("origin,repository", [
("https://github.com/example/project.git", "git:github.com/example/project"),
("ssh://git@github.com:22/example/project.git", "git:github.com/example/project"),
("git://github.com:9418/example/project.git", "git:github.com/example/project"),
("ssh://git@github.com:2222/example/project.git", "git:github.com:2222/example/project"),
])
def test_worktree_scope_admission_and_replay(tmp_path, monkeypatch, capsys, provider, origin, repository):
isolate_sqlite_runtime(tmp_path, monkeypatch)
project = tmp_path / "repo"
project.mkdir()
Expand All @@ -22,7 +28,7 @@ def git(*args):

git("init")
git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.com", "commit", "--allow-empty", "-m", "fixture")
git("remote", "add", "origin", "https://github.com/example/project.git")
git("remote", "add", "origin", origin)
a, b = tmp_path / "a", tmp_path / "b"
git("worktree", "add", "-b", "a", str(a))
git("worktree", "add", "-b", "b", str(b))
Expand All @@ -38,7 +44,7 @@ def git(*args):
projection = build_todo_runtime_shadow_projection(goal_id=goal, handoff_mode="hard_lease", leases=[], todos=[{
"schema_version": "todo_item_v0", "todo_id": f"todo_worktree_{key}", "role": "agent", "status": "open", "done": False,
"text": "Isolated code editing", "archive_state": "active", "source_section": "Agent Todo", "index": i,
"task_class": "advancement_task", "claimed_by": owner, "task_repository": "git:github.com/example/project",
"task_class": "advancement_task", "claimed_by": owner, "task_repository": repository,
} for i, (key, owner) in enumerate([("a", "agent-a"), ("b", "agent-b"), ("c", "agent-b"), ("d", "agent-b")], 1)])
initialize_canonical_authority(runtime, goal, projection, state_path=state, provider=provider)
state.unlink()
Expand Down Expand Up @@ -76,9 +82,14 @@ def acquire(key, path, expected=0, scope="src/**"):
# An unrelated ignored link must not prevent a narrow code-edit lease.
(a / "outside").symlink_to(project, target_is_directory=True)
monkeypatch.chdir(a)
git("remote", "set-url", "origin", "ssh://git@github.com:2223/example/project.git")
assert acquire("a", a, expected=1)["error_code"] == "lease_workspace_repository_mismatch"
git("remote", "set-url", "origin", origin)
first = acquire("a", Path("."))
assert first["source_authority"] == provider + "_v0"
assert "write_workspace" in first["lease"]
assert first["lease"]["write_repository"] == repository
assert first["lease"]["write_workspace"]["repository"] == repository
assert str(tmp_path) not in json.dumps(first["lease"])
conflict = acquire("c", alias, expected=1)
assert conflict["error_code"] == "write_scope_conflict"
Expand Down
Loading