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:
@@ -196,6 +196,18 @@ and the file's header comment has the table.
|
||||
- Private: `/tmp` is the run's `tmp/`, `HOME` the run's empty `home/`, and
|
||||
`/dev` and `/proc` are the namespace's own.
|
||||
|
||||
The environment is `sessionEnv`'s allowlist plus `HOME`, `TMPDIR` and the
|
||||
session's git identity (#1598). `gitIdentity()` sets `GIT_AUTHOR_NAME` and
|
||||
`GIT_COMMITTER_NAME` to the role instance's id, and `GIT_AUTHOR_EMAIL` and
|
||||
`GIT_COMMITTER_EMAIL` to `<instance>@<business>.mosaic.invalid`, a domain
|
||||
that can't receive mail. The launcher passes the instance and the business
|
||||
id, and `sandboxArgv` throws when either isn't an id. The brief names the
|
||||
role; the instance id is used instead, so two instances of one role commit
|
||||
as different authors (Sage's decision on row 104). The identity lives in the
|
||||
environment and not in the clone's config, because a `user.name` or
|
||||
`user.email` key there makes every later launch keep the clone
|
||||
(`config-changed`, below).
|
||||
|
||||
The root is remounted read-only, so a write anywhere else fails. Nothing
|
||||
else of the host's filesystem exists inside: no data root, no other
|
||||
workspace or run, no `~/.config`, `~/.mosaic`, `~/.ssh` or `~/.claude`, no
|
||||
@@ -254,6 +266,13 @@ writes the root. A clone's origin is the root's path; no credential or
|
||||
forge remote goes into it, and the root's own remotes and config don't
|
||||
reach it.
|
||||
|
||||
Each result also has the clone's branch and, for `cloned`, `updated` and
|
||||
`current`, the commit it is at after the launch (`null` when git can't read
|
||||
it). A kept clone has neither, and the host runs no more git in it to find
|
||||
one. The bundle's prompt names the branch and commit, says the host
|
||||
refreshes a clone only at a launch, and tells the session not to set git
|
||||
config in its clone (#1598).
|
||||
|
||||
A kept clone's reason is one of:
|
||||
|
||||
| Reason | The clone |
|
||||
|
||||
@@ -226,16 +226,20 @@ async function clone(project, root, workspace, dest, g) {
|
||||
if (!at.branch) throw new CheckoutError(project, `the root ${root} has no branch checked out`);
|
||||
// A project id never starts with a dot, so the temp name is no project's.
|
||||
const tmp = join(workspace, `.${project}.clone-${randomBytes(4).toString("hex")}`);
|
||||
let commit;
|
||||
try {
|
||||
const r = await g(["clone", "-q", "--no-hardlinks", "--template=", "--branch", at.branch, "--", root, tmp]);
|
||||
if (!r.ok) throw new CheckoutError(project, `git clone of ${root} failed: ${r.why}`);
|
||||
const head = await g([...inClone(tmp), "rev-parse", "--verify", "HEAD"]);
|
||||
if (!head.ok) throw new CheckoutError(project, `the new clone of ${root} has no readable HEAD: ${head.why}`);
|
||||
commit = head.out.trim();
|
||||
renameSync(tmp, dest);
|
||||
} catch (e) {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
if (e instanceof CheckoutError) throw e;
|
||||
throw new CheckoutError(project, `the clone of ${root} couldn't be put in place: ${e.message}`);
|
||||
}
|
||||
return { outcome: "cloned", branch: at.branch };
|
||||
return { outcome: "cloned", branch: at.branch, commit };
|
||||
}
|
||||
|
||||
async function update(root, dest, g) {
|
||||
@@ -256,15 +260,19 @@ async function update(root, dest, g) {
|
||||
const before = (await g([...inClone(dest), "rev-parse", "HEAD"])).out.trim();
|
||||
if (!(await g([...inClone(dest), "fetch", "-q", "--no-tags", "--no-recurse-submodules", "origin"])).ok) return { outcome: "kept", reason: "fetch-failed" };
|
||||
if (!(await g([...inClone(dest), "merge", "-q", "--ff-only", "--no-edit", `refs/remotes/origin/${at.branch}`])).ok) return { outcome: "kept", reason: "not-fast-forward" };
|
||||
const after = (await g([...inClone(dest), "rev-parse", "HEAD"])).out.trim();
|
||||
return { outcome: before === after ? "current" : "updated", branch: at.branch };
|
||||
const read = await g([...inClone(dest), "rev-parse", "HEAD"]);
|
||||
const after = read.out.trim();
|
||||
return { outcome: before === after ? "current" : "updated", branch: at.branch, commit: read.ok ? after : null };
|
||||
}
|
||||
|
||||
// await ensureCheckouts({ workspace, projects, env, limits }) for the
|
||||
// business file's projects ({ <id>: { root } }). Returns one { project,
|
||||
// path, outcome, reason?, branch? } per project, outcome cloned, updated,
|
||||
// current or kept. Rejects with CheckoutError when a missing clone can't be
|
||||
// made. `limits` ({ timeoutMs, graceMs }) is for the tests.
|
||||
// path, outcome, reason?, branch?, commit? } per project, outcome cloned,
|
||||
// updated, current or kept. branch and commit are the clone's after this
|
||||
// launch (commit null when git couldn't read it); a kept clone has neither,
|
||||
// since git may not be safe to run there. Rejects with CheckoutError when a
|
||||
// missing clone can't be made. `limits` ({ timeoutMs, graceMs }) is for the
|
||||
// tests.
|
||||
export async function ensureCheckouts({ workspace, projects, env = process.env, limits = GIT_LIMITS }) {
|
||||
const g = (args, extra) => git(args, env, extra, limits);
|
||||
const out = [];
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
// run's bundle/ and session.json, which stay read-only
|
||||
// private /tmp (the run's tmp/), HOME (the run's home/), /dev, /proc
|
||||
//
|
||||
// The environment adds HOME, TMPDIR and the role's git identity
|
||||
// (gitIdentity() below).
|
||||
//
|
||||
// Nothing else exists inside: no data root, no other workspace or run
|
||||
// directory, no ~/.config, ~/.mosaic or ~/.ssh, no checkout beyond the
|
||||
// runner's files. The root is remounted read-only, so a write anywhere but
|
||||
@@ -34,6 +37,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { ID_PATTERN } from "../../business/src/vocabulary.mjs";
|
||||
|
||||
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
export const BWRAP = "/usr/bin/bwrap";
|
||||
@@ -132,10 +136,25 @@ export function prepareRun(runDir) {
|
||||
for (const d of ["home", "tmp"]) mkdirSync(join(runDir, d), { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// The git identity a session commits under (#1598): the role instance's
|
||||
// name, and <instance>@<business>.mosaic.invalid, a domain that can't
|
||||
// receive mail. It goes in the environment, so a clone's config needs no
|
||||
// user.name or user.email key, which would keep the clone at the next launch
|
||||
// (checkout.mjs, allowed()). Both ids must be business ids.
|
||||
export function gitIdentity({ business, role }) {
|
||||
for (const [what, id] of [["business", business], ["role", role]]) {
|
||||
if (typeof id !== "string" || !ID_PATTERN.test(id)) throw new Error(`git identity: ${what} ${JSON.stringify(id)} isn't an id`);
|
||||
}
|
||||
const email = `${role}@${business}.mosaic.invalid`;
|
||||
return { GIT_AUTHOR_NAME: role, GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: role, GIT_COMMITTER_EMAIL: email };
|
||||
}
|
||||
|
||||
// The full command line for `command` inside the sandbox. HOME is the run's
|
||||
// home/, empty at the start; the harness makes what it needs there.
|
||||
export function sandboxArgv(list, runDir, command, { bwrap = BWRAP } = {}) {
|
||||
return [bwrap, ...NAMESPACES, ...argsOf(list), "--remount-ro", "/", "--setenv", "HOME", join(runDir, "home"), "--setenv", "TMPDIR", "/tmp", "--chdir", runDir, "--", ...KILL_WITH_PARENT, ...command];
|
||||
// home/, empty at the start; the harness makes what it needs there. The git
|
||||
// identity is the role's, for the business it runs in.
|
||||
export function sandboxArgv(list, runDir, command, { business, role, bwrap = BWRAP }) {
|
||||
const identity = Object.entries(gitIdentity({ business, role })).flatMap(([k, v]) => ["--setenv", k, v]);
|
||||
return [bwrap, ...NAMESPACES, ...argsOf(list), "--remount-ro", "/", "--setenv", "HOME", join(runDir, "home"), "--setenv", "TMPDIR", "/tmp", ...identity, "--chdir", runDir, "--", ...KILL_WITH_PARENT, ...command];
|
||||
}
|
||||
|
||||
// Whether bwrap runs here at all: the namespaces and the system binds, then
|
||||
|
||||
@@ -118,10 +118,11 @@ export function appendLaunchLog(dataRoot, business, entry) {
|
||||
// the host writes once the broker has bound the launch; stdout and stderr go
|
||||
// to runner.log. paths: what sandbox.mjs's binds() takes besides the run
|
||||
// directory (workspace, adapter, harness, brokerSocket, launchSocket,
|
||||
// claudeBin; readOnly for tests). `bwrap` is for the tests.
|
||||
export function spawnSession({ runDir, env, paths, node = process.execPath, bwrap }) {
|
||||
// claudeBin; readOnly for tests). business and role (the instance) give the
|
||||
// session its git identity. `bwrap` is for the tests.
|
||||
export function spawnSession({ runDir, env, paths, business, role, node = process.execPath, bwrap }) {
|
||||
prepareRun(runDir);
|
||||
const argv = sandboxArgv(binds({ ...paths, runDir, node }), runDir, [node, RUNNER, runDir], { bwrap });
|
||||
const argv = sandboxArgv(binds({ ...paths, runDir, node }), runDir, [node, RUNNER, runDir], { business, role, bwrap });
|
||||
const out = openSync(join(runDir, "runner.log"), "a", 0o600);
|
||||
try {
|
||||
return spawn(argv[0], argv.slice(1), { cwd: runDir, env, stdio: ["pipe", out, out], detached: true });
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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$/, "") };
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user