seat: give role sessions a git identity and report the checkout commit (row 104, #1598)

sandboxArgv sets GIT_AUTHOR_NAME/EMAIL and GIT_COMMITTER_NAME/EMAIL from
the instance and business ids (name = instance id, email
<instance>@<business>.mosaic.invalid); spawnSession and the launcher pass
the ids through. checkout results carry the clone's commit (a kept clone
gets none), and the bundle prompt names it. allowed() is unchanged.
Packet agents/darkwing/work/queue-104/r1/ (manifest f99f2ae7). Dewey
approved round 1 in 28293 (posted under jarvis by mistake; 28294 is
Dewey's confirmation). Sage's gate on f00613106 plus the candidate:
every package suite and scripts/test-*.sh green (test-release failed 3
activation checks once, not reproduced; rerun 14/0).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 17:40:33 -05:00
co-authored by Claude Opus 5.5
parent f006131061
commit 0069949ffc
11 changed files with 162 additions and 28 deletions
+56 -5
View File
@@ -83,7 +83,7 @@ async function once(w) {
test("a first launch clones the root at its branch, with the root's path as origin", async (t) => {
const w = world(t);
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "cloned", branch: "main" });
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "cloned", branch: "main", commit: git(w.root, "rev-parse", "HEAD") });
assert.equal(git(w.clone, "symbolic-ref", "--short", "HEAD"), "main");
assert.equal(git(w.clone, "rev-parse", "HEAD"), git(w.root, "rev-parse", "HEAD"));
assert.equal(git(w.clone, "remote", "get-url", "origin"), w.root);
@@ -116,10 +116,10 @@ test("a later launch fast-forwards a clean clone after a commit to the root, the
const w = world(t);
await once(w);
const head = commit(w.root, "two\n");
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "updated", branch: "main" });
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: head });
assert.equal(git(w.clone, "rev-parse", "HEAD"), head);
assert.equal(readFileSync(join(w.clone, "README"), "utf8"), "two\n");
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main" });
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main", commit: head });
assert.equal(git(w.clone, "status", "--porcelain"), "");
});
@@ -127,7 +127,7 @@ test("a clone ahead of the root keeps its commits and counts as current", async
const w = world(t);
await once(w);
const mine = commit(w.clone, "mine\n");
assert.equal((await once(w)).outcome, "current");
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main", commit: mine });
assert.equal(git(w.clone, "rev-parse", "HEAD"), mine);
});
@@ -156,6 +156,57 @@ test("a dirty clone, one on another branch and one diverged are kept as they are
}
});
// Row 104 (#1598): a session that sets its own git identity in the clone's
// config gets the clone kept at every later launch. The host only reads the
// clone's config file and runs no git in it, so the kept result has no commit.
test("a clone with user.name or user.email in its config is kept, and git runs nothing in it", async (t) => {
for (const key of ["user.name", "user.email"]) {
await t.test(key, async (t) => {
const w = world(t);
await once(w);
git(w.clone, "config", key, "someone");
commit(w.root, "two\n");
const bin = join(w.dir, "bin");
mkdirSync(bin);
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
const calls = join(w.dir, "calls");
writeFileSync(join(bin, "git"), `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexec '${real}' "$@"\n`, { mode: 0o755 });
const list = await ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } });
assert.deepEqual(list, [{ project: "stack", path: w.clone, outcome: "kept", reason: "config-changed" }]);
// The one call naming the clone reads its config file, as unsafe() does for every clone.
const named = readFileSync(calls, "utf8").split("\n").filter((l) => l.includes(w.clone));
assert.deepEqual(named.map((l) => l.slice(l.indexOf(" config "))), [` config --file ${join(w.clone, ".git", "config")} --list --null`], "no git ran in the kept clone");
});
}
});
// When git can't read the clone's HEAD after the fast-forward, the commit is
// null, not an empty string. A git on PATH fails the second rev-parse HEAD.
test("a commit git can't read after the fast-forward is null", async (t) => {
const w = world(t);
await once(w);
commit(w.root, "two\n");
const bin = join(w.dir, "bin");
mkdirSync(bin);
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
const count = join(w.dir, "count");
writeFileSync(join(bin, "git"), `#!/bin/sh\ncase " $* " in *" rev-parse HEAD ") echo x >> '${count}'; [ "$(wc -l < '${count}')" -ge 2 ] && exit 1;; esac\nexec '${real}' "$@"\n`, { mode: 0o755 });
const list = await ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } });
assert.deepEqual(list, [{ project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: null }]);
});
// A new clone whose HEAD git can't read refuses like a failed clone, and its
// temp directory goes. A git on PATH fails rev-parse --verify HEAD.
test("a new clone whose commit can't be read refuses the launch and leaves nothing behind", async (t) => {
const w = world(t);
const bin = join(w.dir, "bin");
mkdirSync(bin);
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
writeFileSync(join(bin, "git"), `#!/bin/sh\ncase " $* " in *" rev-parse --verify HEAD ") echo 'fatal: no HEAD' >&2; exit 128;; esac\nexec '${real}' "$@"\n`, { mode: 0o755 });
await assert.rejects(ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } }), (e) => e instanceof CheckoutError && e.project === "stack" && /^the new clone of .* has no readable HEAD: /.test(e.message));
assert.deepEqual(readdirSync(w.workspace), [], "no clone and no temp directory");
});
// A repository outside the workspace whose config and hooks run a command
// that touches `marker`, for the cases that point git there.
function bait(dir, marker) {
@@ -328,7 +379,7 @@ test("every project in the business file gets its own clone at its own branch",
const workspace = join(dir, "ws");
mkdirSync(workspace);
const list = await ensureCheckouts({ workspace, projects: { alpha: { root: a }, beta: { root: b } } });
assert.deepEqual(list.map((c) => [c.project, c.path, c.outcome, c.branch]), [["alpha", join(workspace, "alpha"), "cloned", "main"], ["beta", join(workspace, "beta"), "cloned", "dev"]]);
assert.deepEqual(list.map((c) => [c.project, c.path, c.outcome, c.branch, c.commit]), [["alpha", join(workspace, "alpha"), "cloned", "main", git(a, "rev-parse", "HEAD")], ["beta", join(workspace, "beta"), "cloned", "dev", git(b, "rev-parse", "HEAD")]]);
assert.equal(git(join(workspace, "beta"), "symbolic-ref", "--short", "HEAD"), "dev");
});
+27 -3
View File
@@ -18,7 +18,7 @@ import { Store } from "../../bus/src/store.mjs";
import { SOURCES } from "../../harness/src/bundle.mjs";
import { startTimeOf } from "../src/proc.mjs";
import { newRun, RUNNER, runnerOf, sessionEnv, spawnSession, writeSessionFile } from "../src/session.mjs";
import { binds, prepareRun, REPO, RUNNER_FILES, sandboxArgv, sandboxProblem } from "../src/sandbox.mjs";
import { binds, gitIdentity, prepareRun, REPO, RUNNER_FILES, sandboxArgv, sandboxProblem } from "../src/sandbox.mjs";
const FAKE = new URL("../../harness/tests/fixtures/fake-adapter.mjs", import.meta.url).pathname;
const skip = sandboxProblem() ?? false;
@@ -80,6 +80,26 @@ test("the host's git socket is bound only when given, read-only and before the w
assert.ok(RUNNER_FILES.includes(join(REPO, "packages/harness/src/git-tools.mjs")));
});
// Row 104 (#1598): the git identity comes from the business and the role
// instance, both checked as ids, and sandboxArgv sets it for every session.
test("the git identity is the instance's, at <instance>@<business>.mosaic.invalid, and sandboxArgv refuses without it", (t) => {
const email = "[email protected]";
assert.deepEqual(gitIdentity({ business: "acme", role: "coder" }), { GIT_AUTHOR_NAME: "coder", GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: "coder", GIT_COMMITTER_EMAIL: email });
for (const [business, role] of [[undefined, "coder"], ["acme", undefined], ["acme", ""], ["Acme", "coder"], ["acme", "coder\nx"], ["acme", "coder>"], ["acme", 7]]) {
assert.throws(() => gitIdentity({ business, role }), /^Error: git identity: (business|role) .* isn't an id$/, `${business}/${role}`);
}
const root = scratch(t);
const runDir = join(root, "run");
const list = binds({ runDir, workspace: join(root, "ws"), adapter: join(root, "a.sh"), harness: "pi", brokerSocket: join(root, "bus.sock") });
const argv = sandboxArgv(list, runDir, ["/bin/true"], { business: "acme", role: "coder" });
const set = {};
argv.slice(0, argv.indexOf("--")).forEach((a, i, all) => {
if (a === "--setenv") set[all[i + 1]] = all[i + 2];
});
assert.deepEqual(set, { HOME: join(runDir, "home"), TMPDIR: "/tmp", ...gitIdentity({ business: "acme", role: "coder" }) });
assert.throws(() => sandboxArgv(list, runDir, ["/bin/true"], {}), /git identity: business undefined isn't an id/);
});
// The founder's files and the other sessions' state, as stand-ins. The
// /dev/shm file and the SysV segment are the host's, made for this test and
// removed after it.
@@ -136,6 +156,10 @@ function checks({ s, ws, runDir, run, dataRoot, store }) {
["write and read /tmp", "echo tmp > /tmp/probe && cat /tmp/probe", true, "tmp"],
["run node", `'${process.execPath}' -e 'console.log(6 * 7)'`, true, "42"],
["run pi, the session's harness", `'${join(REPO, "node_modules", ".bin", "pi")}' --version >/dev/null`, true],
// Row 104 (#1598): a commit in a clone with no user config carries the
// instance's identity from the environment, and the clone's config
// gains no user key.
["commit in a fresh clone as the instance", `d=$(mktemp -d) && cd "$d" && git init -q o && git -C o commit --allow-empty -q -m base && git clone -q o c && cd c && echo x > f && git add f && git commit -q -m m && ! git config --file .git/config --get-regexp '^user\\.' && git log -1 --format='%an <%ae>|%cn <%ce>'`, true, "coder <[email protected]>|coder <[email protected]>"],
];
}
@@ -205,7 +229,7 @@ async function session(t, { expose = () => [] } = {}) {
});
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE, ...expose(s)] };
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH }, run), paths });
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH }, run), paths, business: "acme", role: "coder" });
const exited = new Promise((r) => child.once("close", (code, signal) => r({ code, signal })));
t.after(async () => {
try {
@@ -263,7 +287,7 @@ test("a bind that exposes a stand-in, or a missing workspace bind, fails the mat
// without the workspace's.
const all = binds({ runDir: x.runDir, workspace: x.ws, adapter: x.adapter, harness: "pi", brokerSocket: x.brokerSocket });
const direct = (list) => async (cmd) => {
const argv = sandboxArgv(list, x.runDir, ["/bin/sh", "-c", cmd]);
const argv = sandboxArgv(list, x.runDir, ["/bin/sh", "-c", cmd], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
return { rc: r.status, out: (r.stdout + r.stderr).replace(/\n$/, "") };
};
+1 -1
View File
@@ -192,7 +192,7 @@ test("a session runs in its sandbox as pid 1 of its namespace, claims, answers,
});
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE] };
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH, GITEA_TOKEN: "x" }, run), paths });
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH, GITEA_TOKEN: "x" }, run), paths, business: "acme", role: "coder" });
t.after(() => {
try {
process.kill(child.pid, "SIGKILL");