after moving all to cipher

This commit is contained in:
2026-08-19 10:19:57 +00:00
parent 4df62d2609
commit 9abade1a81
100 changed files with 1286 additions and 4335 deletions

View File

@@ -36,6 +36,11 @@ class Harness:
seed_native: bool
seed_atif: bool
agent_import_path_single_turn: str | None = None
# Browser-opt-in variants (`[metadata] browser = true`). A harness that has no variant
# keeps its normal class: codex, for instance, gains the browser and its disclosure but
# has no `Read` equivalent to switch toolsets for.
agent_import_path_browser: str | None = None
agent_import_path_single_turn_browser: str | None = None
import_path_aliases: tuple[str, ...] = ()
legacy_bare_model_rows: bool = False
default_model: str | None = None
@@ -67,9 +72,22 @@ class Harness:
# be edited in lockstep with the schema.
extra: dict = field(default_factory=dict, compare=False)
def agent_import_path_for(self, *, multi_turn: bool) -> str:
def agent_import_path_for(self, *, multi_turn: bool, browser: bool = False) -> str:
"""Agent class to launch. Multi-turn tasks need the resuming class; a
single-turn task given it would try to resume a session that isn't there."""
single-turn task given it would try to resume a session that isn't there.
``browser`` selects the opt-in variant, which for claude also carries the ``Read``
built-in — a different toolset is a different agent, so it is a different class with
its own name rather than a flag on the canonical one. Harnesses without a variant fall
through to their normal class."""
if browser:
variant = (
self.agent_import_path_browser
if multi_turn
else (self.agent_import_path_single_turn_browser or self.agent_import_path_browser)
)
if variant:
return variant
if multi_turn:
return self.agent_import_path
return self.agent_import_path_single_turn or self.agent_import_path
@@ -180,6 +198,8 @@ _KNOWN_FIELDS = frozenset(
"label",
"agent_import_path",
"agent_import_path_single_turn",
"agent_import_path_browser",
"agent_import_path_single_turn_browser",
"import_path_aliases",
"legacy_bare_model_rows",
"default_model",
@@ -315,6 +335,8 @@ def _build(entry: dict, index: int) -> Harness:
label=entry["label"],
agent_import_path=entry["agent_import_path"],
agent_import_path_single_turn=entry.get("agent_import_path_single_turn"),
agent_import_path_browser=entry.get("agent_import_path_browser"),
agent_import_path_single_turn_browser=entry.get("agent_import_path_single_turn_browser"),
import_path_aliases=tuple(entry.get("import_path_aliases", ())),
legacy_bare_model_rows=bool(entry.get("legacy_bare_model_rows", False)),
default_model=entry.get("default_model"),

View File

@@ -6,10 +6,21 @@
* *inside* it, so the repair has to fix directory modes, not just ownership.
* These tests run unprivileged, so they exercise the mode axis for real and the
* ownership axis only as far as an unprivileged process can (target resolution +
* graceful EPERM), which is the same shape CI runs in.
* graceful EPERM), which is the same shape CI runs in. One case needs root and
* skips otherwise; the rest hold under either uid, which is why the fixtures that
* must look human-owned say so with `ownedByHuman` instead of relying on the
* caller's uid.
*/
import assert from 'node:assert/strict';
import { chmodSync, mkdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import {
chmodSync,
chownSync,
mkdirSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
@@ -28,6 +39,16 @@ function scratch(name: string): string {
return dir;
}
const RUNNING_AS_ROOT = process.getuid?.() === 0;
const HUMAN_UID = RUNNING_AS_ROOT ? 1000 : (process.getuid?.() ?? 0);
const HUMAN_GID = RUNNING_AS_ROOT ? 1000 : (process.getgid?.() ?? 0);
/** Give a fixture a non-root owner, so the repair sees a tree it can hand back. */
function ownedByHuman(path: string): string {
chownSync(path, HUMAN_UID, HUMAN_GID);
return path;
}
test('restores the search bit on a directory that lost it', () => {
const root = scratch('searchbit');
const models = join(root, 'agent-output', 'app', 'models');
@@ -77,12 +98,13 @@ test('leaves already-correct trees untouched', () => {
});
test('does not widen group/other beyond what was already there', () => {
const root = scratch('narrow');
const root = ownedByHuman(scratch('narrow'));
const f = join(root, 'secret.txt');
writeFileSync(f, 'x\n');
ownedByHuman(f);
chmodSync(f, 0o000);
normalizeTreePermissions(root);
normalizeTreePermissions(root, { ownerRef: root });
const mode = statSync(f).mode & 0o777;
assert.equal(mode, 0o600, 'owner rw only — group/other stay closed');
@@ -155,6 +177,47 @@ test('still normalizes modes when the chown target is root', () => {
rmSync(root, { recursive: true, force: true });
});
test('keeps modes narrow for files that have a real owner, even under a root ref', () => {
// The complement of the case below: we declined to chown, but these entries are
// already the human's, so owner bits reach them and nothing should be widened.
const root = ownedByHuman(scratch('root-ref-narrow'));
const f = join(root, 'mine.txt');
writeFileSync(f, 'x\n');
ownedByHuman(f);
chmodSync(f, 0o600);
normalizeTreePermissions(root, { ownerRef: '/' });
assert.equal(statSync(f).mode & 0o077, 0, 'group/other untouched');
rmSync(root, { recursive: true, force: true });
});
test(
'grants read+search to group and other on files stranded root-owned',
{ skip: process.getuid?.() !== 0 ? 'needs root to create root-owned files' : false },
() => {
// The worker authoring container: root process, root-owned workspace. The chown
// is declined, so owner bits land on root and the human — a different uid in
// Explore and on a WSL host — is still locked out of a --w------- capture.
const root = scratch('stranded');
const sub = join(root, 'agent-output');
mkdirSync(sub, { recursive: true });
const f = join(sub, 'answer.md');
writeFileSync(f, 'x\n');
chmodSync(f, 0o200);
chmodSync(sub, 0o300);
normalizeTreePermissions(root, { ownerRef: '/' });
assert.equal(
statSync(f).mode & 0o777,
0o644,
'file readable by everyone, writable by none but root'
);
assert.equal(statSync(sub).mode & 0o777, 0o755, 'directory searchable');
}
);
test('walks a tree as deep as the filesystem allows', () => {
const root = scratch('deep');
// PATH_MAX caps how deep a tree can physically get (~300 levels at these name

View File

@@ -34,9 +34,16 @@ export function resolveWorkspaceOwner(ownerRef: string): { uid: number; gid: num
}
}
/** Owner-rwX mode, preserving every other bit. Dirs also need the search bit. */
function withOwnerAccess(mode: number, isDir: boolean): number {
return mode | (isDir ? 0o700 : 0o600);
/**
* Owner-rwX mode, preserving every other bit. Dirs also need the search bit.
*
* `stranded` means the file stays root-owned because we have no non-root owner to
* give it to. Owner bits then help nobody — whoever has to read it is a different
* user — so read and search are granted more widely. Never write, never +x on files.
*/
function withOwnerAccess(mode: number, isDir: boolean, stranded: boolean): number {
const owner = isDir ? 0o700 : 0o600;
return mode | owner | (stranded ? (isDir ? 0o055 : 0o044) : 0);
}
/**
@@ -75,7 +82,7 @@ export function normalizeTreePermissions(
const isDir = st.isDirectory();
// Mode first: a directory we can't search is one we can't descend into.
const wanted = withOwnerAccess(st.mode, isDir);
const wanted = withOwnerAccess(st.mode, isDir, chownTarget === null && st.uid === 0);
if (wanted !== st.mode) {
try {
chmodSync(path, wanted);