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
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
inputs.effect-utils.inputs.nixpkgs.url = "github:NixOS/nixpkgs/4ba99f3209788ed04f01af38f77ddb803ad6ec63";
inputs.effect-utils.inputs.tsgo.inputs.nixpkgsUnstable.url = "github:NixOS/nixpkgs/44a91898084f46797b5fac650c7e8c9ac38c43d4";
inputs.fractal-fixture = {
url = "github:compoundingtech/fractal/e2721e52463428b4ff655addee129cecd2839763";
url = "github:compoundingtech/fractal/787e8ed592dff34d2bdc2a2b0b584482da567e7f";
flake = false;
};
inputs.python-oracle = {
Expand Down
11 changes: 10 additions & 1 deletion scripts/evidence-sign
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def child_environment(extra=None, environ=None):
environ = os.environ if environ is None else environ
child = {name: environ[name] for name in PASSED_ENVIRONMENT if name in environ}
child.update(PATH=TOOLS['PATH'], LC_ALL='C', GIT_NO_REPLACE_OBJECTS='1', GIT_TERMINAL_PROMPT='0',
GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null')
GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_NO_LAZY_FETCH='1')
child.update(extra or {})
return child

Expand All @@ -215,6 +215,14 @@ def git(repo, *args, stdin=None, env=None):
return done.stdout


def require_complete_commit(repo, commit):
"""The caller hydrates its checkout; the signer never contacts a promisor remote."""
missing = sum(line.startswith('?') for line in
git(repo, 'rev-list', '--objects', '--missing=print', commit).decode().splitlines())
if missing:
raise Refused(f'{missing} missing objects before signing; hydrate the source repository in the caller first')


def check_nix(request):
"""A `nix` request is `nix build .#checks.<system>.<check>` and says what that build produced. The signer
cannot run Nix or see the store, so it holds the claim to its shape: store paths, a NAR hash, the signatures
Expand Down Expand Up @@ -303,6 +311,7 @@ def main():

checkout = Path.cwd()
commit = request['commit']
require_complete_commit(checkout, commit)
if git(checkout, 'cat-file', '-t', commit).decode().strip() != 'commit':
raise Refused('commit is not a commit in the caller\'s checkout')
tree = git(checkout, 'rev-parse', f'{commit}^{{tree}}').decode().strip()
Expand Down
3 changes: 2 additions & 1 deletion scripts/evidence-sign-hardening-test
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ hostile = {'LD_LIBRARY_PATH': '/evil', 'LD_PRELOAD': '/evil.so', 'SSH_AUTH_SOCK'
'GIT_SSH_COMMAND': 'evil', 'PYTHONUNBUFFERED': '1', 'PERL5OPT': '-Mevil', 'HTTPS_PROXY': 'http://evil', 'PATH': '/evil/bin',
'LC_ALL': 'evil', 'HOME': '/srv/example-home', 'TMPDIR': '/var/tmp/x', 'FOO': 'bar'}
child = signer.child_environment(environ=hostile)
assert set(child) == {'HOME', 'TMPDIR', 'PATH', 'LC_ALL', 'GIT_NO_REPLACE_OBJECTS', 'GIT_TERMINAL_PROMPT', 'GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL'}, sorted(child)
assert set(child) == {'HOME', 'TMPDIR', 'PATH', 'LC_ALL', 'GIT_NO_REPLACE_OBJECTS', 'GIT_TERMINAL_PROMPT', 'GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL', 'GIT_NO_LAZY_FETCH'}, sorted(child)
assert child['PATH'] == '/usr/bin:/bin' and child['LC_ALL'] == 'C' and child['HOME'] == '/srv/example-home' and child['TMPDIR'] == '/var/tmp/x'
assert child['GIT_CONFIG_GLOBAL'] == '/dev/null' and child['GIT_CONFIG_NOSYSTEM'] == '1'
assert child['GIT_NO_LAZY_FETCH'] == '1'
extra = signer.child_environment({'GIT_AUTHOR_NAME': 'Evidence signer'}, environ=hostile)
assert extra['GIT_AUTHOR_NAME'] == 'Evidence signer', 'settings the signer supplies itself are kept'
print('passed: git and ssh-keygen get PATH, LC_ALL, HOME, TMPDIR and the signer\'s own git settings; loader, agent, proxy and GIT_* variables are dropped')
Expand Down
6 changes: 4 additions & 2 deletions scripts/evidence-verify-test
Original file line number Diff line number Diff line change
Expand Up @@ -1460,20 +1460,22 @@ class CommittedPolicy(unittest.TestCase):
'.cargo', '.config/nextest.toml', '.github', 'Cargo.lock', 'Cargo.toml', 'claude-channel', 'clients', 'components', 'crates',
'docs/st3', 'docs/vrs', 'evals', 'examples', 'fixtures', 'flake.lock', 'flake.nix', 'nix', 'plugins', 'scripts',
'apps/fractal-web/src/terminal/assets/JetBrainsMonoNerdFontMono-Regular.ttf', # read at run time by a test in crates/fractal-daemon/src/attach_image.rs
'packages/fractal/fixtures', # included by crates/fractal/tests/validate.rs
'slo', 'templates', 'vendor', 'wit']), 'typescript-client': sorted([
'apps/fractal-web/package.json', 'packages/fractal-sdk/package.json',
'apps/fractal-web/src/folders/core.mts', 'apps/ios', # core.mts: found by the lane trace (docs: Lane definitions)
'clients/typescript', 'crates/fractal-client-codegen/templates/declarations.ts.in',
'crates/fractal-tui/tests/fixtures/quality-hour.json', 'docs/st3/client-v0/fixtures',
'fixtures/clients', 'flake.lock', 'flake.nix', 'package.json', 'packages/fractal-ui/package.json',
'docs/site', # the fractal package's docs test renders every path-commented TS sample there
'fixtures/clients', 'flake.lock', 'flake.nix', 'package.json', 'packages/fractal', 'packages/fractal-ui/package.json',
'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'scripts/ci-typescript-client']), 'fractal-web': sorted([
# The web lane detector's paths (scripts/ci-fractal-web-changes, PATTERNS and KIT_PATTERNS, as files that exist)...
'.buckconfig', '.buckconfig.genie.ts', '.buckroot', '.github/repo-settings.json', '.github/repo-settings.json.genie.ts',
'.github/workflows/cache-audit.ts', '.github/workflows/fleet.yml', '.github/workflows/fleet.yml.genie.ts',
'.github/workflows/main-upkeep.yml', '.github/workflows/main-upkeep.yml.genie.ts', '.github/workflows/workspace-ci.ts',
'BUCK', 'BUCK.genie.ts', 'apps/fractal-web', 'buck2', 'clients/typescript/fractal-client', 'clients/typescript/fractal-scenarios',
'clients/typescript/fractal-views', 'docs/st3/client-v0/schemas', 'fixtures', 'flake.lock', 'flake.nix', 'megarepo.kdl',
'megarepo.lock', 'nix/fractal-web.nix', 'nix/web-shell-inputs.json', 'nix/web-shell.nix', 'package.json', 'package.json.genie.ts', 'packages/fractal-sdk', 'packages/fractal-ui',
'megarepo.lock', 'nix/fractal-web-publisher.nix', 'nix/fractal-web.nix', 'nix/web-shell-inputs.json', 'nix/web-shell.nix', 'package.json', 'package.json.genie.ts', 'packages/fractal-sdk', 'packages/fractal-ui',
'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'pnpm-workspace.yaml.genie.ts', 'scripts/ci-fractal-web', 'scripts/ci-fractal-web-changes',
'scripts/ci-fractal-web-gate', 'scripts/ci-fractal-web-kit-graph', 'scripts/ci-fractal-web-licenses',
'scripts/ci-fractal-web-licenses-test', 'scripts/ci-fractal-web-test', 'scripts/ci-typescript-client', 'scripts/ci-web-shell',
Expand Down
64 changes: 64 additions & 0 deletions scripts/evidence-wrap-test
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,70 @@ os.execv({real_git!r}, ['git', *args])
assert refused.returncode == 1 and b'did not verify locally' in refused.stderr, refused.stderr
print('passed: a per-crate record is verified as that crate\'s: a failed record of another crate does not decide it, its own unverified record still blocks a push')

# A local upload-pack disables lazy fetching. HEAD's checkout is not enough:
# the scratch fetch also needs blobs reachable only through old commits.
source = tmp / 'partial-source'
git(tmp, 'init', '-q', str(source))
(source / 'flake.lock').write_text('{"nodes":{}}\n')
(source / 'historical').write_text('only in history\n')
git(source, 'add', '.')
git(source, 'commit', '-q', '-m', 'historical blob')
git(source, 'rm', 'historical')
(source / '.st').mkdir()
(source / '.st/evidence-keys').write_text(signers.read_text())
git(source, 'add', '.')
git(source, 'commit', '-q', '-m', 'remove historical blob')
origin = tmp / 'partial-origin.git'
git(tmp, 'clone', '-q', '--bare', str(source), str(origin))
git(origin, 'config', 'uploadpack.allowFilter', 'true')

def partial_clone(name):
checkout = tmp / name
git(tmp, 'clone', '-q', '--filter=blob:none', origin.as_uri(), str(checkout))
assert git(checkout, 'config', '--get', 'remote.origin.promisor') == 'true'
git(checkout, 'update-ref', 'refs/remotes/origin/main', 'HEAD')
assert any(line.startswith('?') for line in
git(checkout, 'rev-list', '--objects', '--missing=print', 'HEAD').splitlines())
return checkout

partial = partial_clone('partial-checkout')
unavailable = partial_clone('unavailable-checkout')
partial_home = tmp / 'partial-home'
(partial_home / '.config/st').mkdir(parents=True)
(partial_home / '.config/st/evidence-sign.json').write_text(json.dumps(
{'signer': 'fleet/dev', 'ssh_key': str(key), 'scratch_repo': str(tmp / 'partial-scratch.git'),
'test_clock': True, 'path': tool_path()}))

def partial_wrap(checkout):
return run([sys.executable, str(WRAP), '--check', 'linux-gate',
'--toolchain-sha256', toolchain, '--signer', signer,
'--change-id', 'github:example/repo#5', '--', 'true'],
checkout, partial_home)

caller = runpy.run_path(str(ROOT / 'scripts/st-test'))
caller['hydrate_commit'](partial, git(partial, 'rev-parse', 'HEAD'))
signed = partial_wrap(partial)
assert signed.returncode == 0, signed.stderr.decode()
assert not any(line.startswith('?') for line in
git(partial, 'rev-list', '--objects', '--missing=print', 'HEAD').splitlines())
signed_result = json.loads(signed.stderr.decode()[signed.stderr.decode().index('{'):])
git(partial, '-c', 'gpg.format=ssh', '-c', f'gpg.ssh.allowedSignersFile={signers}',
'verify-commit', signed_result['record'])
print('passed: caller hydrates blob:none historical blobs before signing with a verified signature')

marker = tmp / 'transport-ran'
git(unavailable, 'remote', 'set-url', 'origin', 'ssh://invalid.example/repository')
git(unavailable, 'config', 'core.sshCommand', f'touch {marker}')
request = dict(good, commit=git(unavailable, 'rev-parse', 'HEAD'))
refused = subprocess.run([sys.executable, str(SIGN)], cwd=unavailable,
input=json.dumps(request).encode(), capture_output=True,
env=dict(ENV, HOME=str(partial_home), EVIDENCE_SIGN_NOW=DATE))
assert refused.returncode != 0, refused.stderr.decode()
assert b'missing objects before signing; hydrate the source repository in the caller first' in refused.stderr, refused.stderr.decode()
assert not marker.exists(), 'the signer must not run a transport'
assert not git(unavailable, 'for-each-ref', 'refs/tags/evidence', 'refs/notes/evidence')
print('passed: signer refuses missing partial-clone objects without running a transport or publishing evidence')

if write_fixture:
write_fixture.mkdir(parents=True, exist_ok=True)
fixture_home = tmp / 'fixture-home'
Expand Down
2 changes: 1 addition & 1 deletion scripts/lane-definition-closure
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ EXTRA = {
# kit-play-shards.mjs to test it). A package.json whose script a lane's pnpm call runs is in the closure (its `scripts` entry is the
# command). Nothing outside SUBJECT is assumed to be the subject.
SUBJECT = ('Cargo.toml', 'Cargo.lock', 'src', 'tests', 'crates', 'components', 'examples', 'wit', 'vendor',
'apps/fractal-web', 'apps/ios', 'clients/typescript', 'packages/fractal-sdk', 'packages/fractal-ui')
'apps/fractal-web', 'apps/ios', 'clients/typescript', 'packages/fractal', 'packages/fractal-sdk', 'packages/fractal-ui')
HARNESS = ('apps/fractal-web/scripts/ci', 'packages/fractal-ui/scripts')
HARNESS_LANES = {'fractal-web'}
# Where the lanes' own code lives. A file elsewhere that the code names (a workflow, a doc, a fixture, a baseline) is read as data:
Expand Down
34 changes: 34 additions & 0 deletions scripts/st-test
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,38 @@ def git(*args, cwd=None, env=None, check=True, stdin=None):
return done


def hydrate_commit(directory, commit):
"""Unprivileged preparation: local upload-pack cannot lazy-fetch historical blobs."""
def missing_objects():
return [line[1:] for line in
git('rev-list', '--objects', '--missing=print', commit, cwd=directory,
env={'GIT_NO_LAZY_FETCH': '1'}).stdout.decode().splitlines()
if line.startswith('?')]

missing = missing_objects()
if not missing:
return
remotes = [match.group(1) for line in git('config', '--list', cwd=directory).stdout.decode().splitlines()
if (match := re.fullmatch(r'remote\.(.+)\.promisor=true', line))]
if not remotes:
raise Failure(f'cannot hydrate {len(missing)} missing objects before signing: no promisor remote')
while missing:
before = set(missing)
for offset in range(0, len(missing), 200):
batch = missing[offset:offset + 200]
try:
git('-c', 'maintenance.auto=false', '-c', 'gc.auto=0',
'-c', 'fetch.negotiationAlgorithm=noop', 'fetch', '--quiet',
'--no-tags', '--no-write-fetch-head', '--no-auto-maintenance',
'--no-recurse-submodules', '--filter=blob:none', '--stdin', remotes[0],
cwd=directory, stdin=(''.join(oid + '\n' for oid in batch)).encode())
except Failure as error:
raise Failure(f'cannot hydrate missing objects before signing: {error}') from error
missing = missing_objects()
if missing and set(missing) >= before:
raise Failure(f'cannot hydrate {len(missing)} missing objects before signing: fetch made no progress')


# arn:lmig:smalltalk:2026-10-27-dot-fractal-config — embedded for base runners copied alone.
def config_path(base, name):
for directory in ('.fractal', '.st'):
Expand Down Expand Up @@ -3755,6 +3787,8 @@ def run_main(argv, cleanup):
round_number, round_started = top_up, time.monotonic() # 0: the run itself; then the top-ups, including a handed-over round
target, target_lock = None, None
while True:
# Use the caller's normal git context, never the key-bearing signer's.
hydrate_commit(directory, commit)
tools = {}
for name in BASE_SCRIPTS: # from the base as it is now, after any wait for a tree
path = Path(scratch) / name
Expand Down
3 changes: 1 addition & 2 deletions tests/fixtures/fractal/.st/evidence-signed-only.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"about": "Pull requests opened by these GitHub accounts are pre-signed (scripts/st-test --all --push origin) and never run the heavy checks in CI: an enforced check with no valid signature for its tree fails with what to do instead (scripts/ci-evidence-gate, refusal). Read from BASE, so a pull request cannot take itself off the list or enable queue reuse. merge_group is off until conditional post-merge hosted proof is deployed; same-tree permits full-tier evidence for the exact queue tree, with the BASE skip policy's keys, commands, inputs and lanes. Other merge groups run hosted CI; invalid evidence never refuses a merge group.",
"merge_group": "off",
"openers": [
"myobie",
"schickling-assistant"
"myobie"
]
}
10 changes: 8 additions & 2 deletions tests/fixtures/fractal/.st/evidence-skip-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"flake.lock",
"flake.nix",
"nix",
"packages/fractal/fixtures",
"plugins",
"scripts",
"slo",
Expand Down Expand Up @@ -207,6 +208,7 @@
"fixtures",
"flake.lock",
"flake.nix",
"packages/fractal/fixtures",
"plugins/claude",
"scripts/build-stamp-watch-test",
"scripts/check-fractal-boundary",
Expand Down Expand Up @@ -262,7 +264,7 @@
"scripts/script-test-supervisor",
"scripts/script_process.py",
"scripts/sqlite_large_values.py",
"slo/targets.toml",
"slo/objectives.kdl",
"vendor/libghostty-vt-sys",
"wit"
],
Expand Down Expand Up @@ -443,6 +445,7 @@
"fixtures",
"flake.lock",
"flake.nix",
"packages/fractal/fixtures",
"plugins/claude",
"scripts/build-stamp-watch-test",
"scripts/check-fractal-boundary",
Expand Down Expand Up @@ -497,7 +500,7 @@
"scripts/script-test-supervisor",
"scripts/script_process.py",
"scripts/sqlite_large_values.py",
"slo/targets.toml",
"slo/objectives.kdl",
"vendor/libghostty-vt-sys",
"wit"
],
Expand Down Expand Up @@ -607,11 +610,13 @@
"clients/typescript",
"crates/fractal-client-codegen/templates/declarations.ts.in",
"crates/fractal-tui/tests/fixtures/quality-hour.json",
"docs/site",
"docs/st3/client-v0/fixtures",
"fixtures/clients",
"flake.lock",
"flake.nix",
"package.json",
"packages/fractal",
"packages/fractal-ui/package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
Expand Down Expand Up @@ -1041,6 +1046,7 @@
"flake.nix",
"megarepo.kdl",
"megarepo.lock",
"nix/fractal-web-publisher.nix",
"nix/fractal-web.nix",
"nix/web-shell-inputs.json",
"nix/web-shell.nix",
Expand Down
Loading