Fix three fabricated-result bugs, close the RCE, and overhaul the documentation - #139
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
The dependabot advisory GHSA-3936-cmfr-pm3m originally matched pyproject.toml's black==25.1.0, which sat inside the vulnerable range >=24.3.0,<26.3.1. That pin is already corrected to ==26.3.1. Commit bf524a4 then added a black>=26.3.1 floor to docs/requirements.txt on the assumption that black arrived there transitively through the Jupyter stack. It does not: resolving that file from scratch installs 117 packages and black is not one of them. The floor added a dependency rather than constraining one, and because '>=' leaves the version unresolved the dependency graph recorded black with versionInfo: null -- which is why the alert stayed open against docs/requirements.txt after the real cause was fixed. Drop the line, and pin setup.py's dev extra to ==26.3.1 so no unbounded black constraint is left anywhere for the graph to resolve as unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
cleanup_test_resources.py and destroy_cluster.py were deleted by b9c836f ("Issue #72: Delete obsolete development scripts") on the false claim that they were migrated to scripts/aws/ -- that directory never existed. Recover the real source from git history (b9c836f^) and restore it as scripts/aws/cleanup_resources.py and scripts/aws/destroy_cluster.py, hardened per issue #95: - Default to dry run; require --execute to delete anything. - Print every resource (type, id, region) before acting, in both modes. - Only touch resources positively identified as Clustrix-managed, using the same clustrix:managed / clustrix:cluster tags and IAM role naming that clustrix.kubernetes.aws_provisioner.AWSEKSFromScratchProvisioner applies (the original script's VPC cleanup had no such check at all, and its IAM role name guesses never matched what the provisioner actually creates). - Fail loudly with a clear message when AWS credentials are missing, instead of silently falling through to boto3's default credential chain. Add tests/unit/test_aws_cleanup_scripts.py: verifies --help, the missing-credentials failure path, and argparse defaults via real subprocess calls and real argparse round trips (no AWS account, no mocked boto3), plus static (ast-based) proof that every destructive boto3 call is reachable only through execute_plan() and only when gated behind `if args.execute`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
tests/real_world/conftest.py's pytest_collection_modifyitems hook added skip markers for expensive/visual/dartmouth tests but never applied the real_world marker itself. 6 of 81 files under tests/real_world/ carried no @pytest.mark.real_world decorator, so the documented CI-safe command `pytest tests/ -m "not real_world"` silently collected and ran them -- making real SSH connections and cloud API calls. The hook now applies pytest.mark.real_world to every item whose path is under tests/real_world/, closing the hole regardless of whether the file itself declares the marker. The hook is scoped by path (not applied unconditionally) because pytest_collection_modifyitems fires once per session with the full item list, not just items from this directory -- an unscoped version would have marked the entire suite as real_world. Adds tests/unit/test_billable_and_realworld_isolation.py: runs real pytest in subprocesses (collect-only) to prove `-m "not real_world"` now collects zero tests/real_world items, pins the 6 previously-leaking files individually, guards against the marker leaking onto the rest of the suite, and adversarially re-checks the tests/integration billable guard (cwd change, -p no:cacheprovider, --co, direct import) -- no bypass found; that guard's config.args-based design (not invocation_params.args) is unchanged. CLAUDE.md's documented commands (`pytest tests/ -m "not real_world"` and `pytest tests/real_world/ -m real_world`) are both accurate after this fix and need no wording change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ld workflows - tests.yml: default test job now runs `pytest tests/ -m "not real_world" --ignore=tests/real_world --ignore=tests/integration` instead of only tests/unit/ (~350 of ~1,532 non-billable tests). Belt-and-braces with the real_world marker another agent is auto-applying in tests/real_world/conftest.py. Also dropped a `|| true` on the integration-test job's pytest step (verified the 7-test selection it guards passes cleanly without it). - fast_ci.yml: removed `continue-on-error: true` from the mypy step. Verified clean (`Success: no issues found in 69 source files`) once the dev extra's type stub packages (types-PyYAML/requests/paramiko, already declared in pyproject.toml) are actually installed -- no clustrix/ changes needed. - Consolidated the two duplicate real-world-test workflows (hyphen vs underscore) into one canonical real-world-tests.yml, matching what docs/CREDENTIAL_SETUP.md already documented. Deleted real_world_tests.yml, whose jobs depended on fictional infrastructure (Kind clusters, a recovery_report.json/performance_results.json nothing produces). - Replaced the three `if: false` gates with real ones: workflow_dispatch (manual) plus a weekly schedule, gated on secret presence via a check-secrets job (the `secrets` context is not permitted in job-level `if:` -- actionlint caught this). No push/pull_request trigger exists on this workflow at all, so a fork PR cannot invoke it under any condition. Also fixed a dead reference to a nonexistent scripts/test_real_world_credentials.py. - Added hf-jobs-integration: a workflow_dispatch/schedule job gated on HF_TOKEN that submits a real function through clustrix's HF Jobs backend (contextlab namespace, cpu-basic flavor, per the verified #118 config) -- the first integration-test substrate in this repo that can actually run without SSH/cloud credentials. - Bumped actions/setup-python@v4->v5 and actions/cache@v3->v4 across touched files per actionlint (all other actionlint findings and secrets-in-if bugs in files I touched are resolved; actionlint exits 0). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
.flake8 was sitting in the working tree with unresolved conflict markers (<<<<<<< Updated upstream / >>>>>>> Stashed changes). flake8 cannot parse that, so it silently fell back to its defaults -- 79-character lines and none of the per-file-ignores -- and reported violations the project had deliberately configured away. The committed version was correct; restore it. .gitignore and .pre-commit-config.yaml were left in the same unmerged state and matched HEAD exactly. The corruption came from the pre-commit hook's stash/restore cycle running against a tree that other work was modifying at the same time. tests.yml's flake8 step passed --exit-zero, so the step could not fail CI no matter what it found, and carried its own --extend-ignore list that had drifted from .flake8. Drop both; .flake8 is the single source of truth. Verified: flake8 clustrix/ tests/ scripts/ reports 0 findings with the restored config. Also extend both lint steps to cover scripts/, which is already clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
executor_scheduler_status.py: the SLURM status check branched on isinstance(ssh_client, Mock) to skip retry/sacct logic during unit tests. Replaced with the real condition it was standing in for: whether there is a live SSH connection at all (ssh_client is None). No currently-passing test depended on the sniff -- the tests that exercised it were already failing for unrelated reasons (connection_manager.execute_remote_command has moved on from what they mock). notebook_magic_mocks.py: renamed to notebook_magic_fallback.py and stripped of the ~115 lines of fake ipywidgets classes (_MockDropdown, _MockButton, etc.). EnhancedClusterConfigWidget.__init__ already refuses to construct without real IPython+ipywidgets, so those classes' methods were provably dead code -- nothing ever reached them. ipywidgets is an intentional optional dependency (see pyproject.toml's `widgets` extra and the GitHub-Actions-compat test suite that exercises import without it), so a hard-require was not the right call; instead `widgets` is now a placeholder that raises a clear ImportError on any attribute access instead of faking the API. The magics/display/HTML shims that ARE genuinely exercised without IPython installed (ClusterfyMagics's line magic, etc.) are kept as real, honestly-degraded implementations. Also fixed unresolved git-conflict markers left in .gitignore and .pre-commit-config.yaml (discovered while resolving an unrelated stray stash during this work); kept the deliberate current content over the stale stashed alternative in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ct unknown ones Every paramiko.SSHClient() in the SSH-touching modules used AutoAddPolicy(), which trusts any host key on first connection with zero verification -- every SSH connection clustrix made was MITM-able. Adds a single shared helper, clustrix.ssh_security.configure_host_key_policy(), that loads system + user known_hosts and defaults to rejecting unknown host keys with an actionable error naming the host and the exact ssh-keyscan command to fix it. The old insecure behavior is now an explicit, documented opt-in via ClusterConfig.ssh_host_key_policy="auto_add". All 10 owned call sites (ssh_utils.py x3, executor_connections.py, filesystem.py, validation.py x2, cli_credentials.py, kubernetes/lambda_provisioner.py) now call the shared helper instead of repeating the policy decision. The Lambda Cloud provisioner, which connects to freshly-booted ephemeral instances with no prior known_hosts entry, does an explicit ssh-keyscan (reusing ssh_utils.add_host_key) before each connect attempt as a logged trust-on-first-use step rather than blanket auto-trust. Issue #111: Save config files at 0600 and omit secrets by default ClusterConfig.save_to_file/save_config wrote via plain open(path, "w") with no mode and no secret exclusion, so any password/token/API key on the config landed in a 0644 file. Files are now created via os.open() with mode 0o600 (plus an immediate fchmod so a pre-existing, more permissive file is tightened before any content is written -- never after). Secret- bearing fields are omitted by default, determined programmatically from ClusterConfig's field names via the same regex approach already used in scripts/verify_cluster_usecases.py (now imported from clustrix.config as the single source of truth instead of being duplicated). Pass include_secrets=True to write them anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Two AutoAddPolicy call sites were left behind when the rest moved to
configure_host_key_policy. This closes the notebook widget's
_test_ssh_connectivity, which hands its configuration over as a plain
dict rather than a ClusterConfig -- so the shared helper now reads
either shape, keeping the policy decision in one place instead of
letting the widget invent its own.
Verified: dict {'ssh_host_key_policy':'auto_add'} -> AutoAddPolicy with
the warning; empty dict and None -> RejectUnknownHostKeyPolicy.
executor_cloud.py's site is still open; that file is being rewritten in
parallel and the fix goes in there.
Separately, raise the cloudpickle floor from 2.0.0 to 3.0.0. Under
cloudpickle 2.0.0 a by-value-registered local package that defines a
typing.NamedTuple cannot be loaded back when the module object itself
is in the function's globals -- i.e. the ordinary 'import mypkg;
mypkg.f(x)' idiom:
cloudpickle 2.0.0
via_from dumps 953 bytes LOAD: OK -> 2
via_module dumps 3844 bytes LOAD: KeyError: '__module__'
cloudpickle 3.1.1
via_from dumps 1079 bytes LOAD: OK -> 2
via_module dumps 4120 bytes LOAD: OK -> 2
The 'from mypkg import f' spelling happens to work under 2.x because
cloudpickle then embeds only the globals the function actually uses, so
the NamedTuple never enters the payload. That is why this went unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…tions
_execute_single substituted a different callable for the user's function
whenever analyze_function_complexity reported "complex", and that reporting
was backwards: the analyser's except branch returned complexity_score 999,
is_complex True whenever inspect.getsource failed. So for any REPL, notebook
or exec-created function, clustrix attempted a source rewrite that cannot
work, fell through to create_simple_subprocess_fallback, and ran a hardcoded
subprocess whose entire body was `result = "Function execution completed"`.
That string was returned to the caller as the job's answer, with no error.
- delete create_simple_subprocess_fallback outright; it never ran the user's
function and cannot ever produce a correct answer
- _execute_single now serialises the function the caller wrote, always. No
rewrite is substituted, because equivalence of a rewritten function cannot
be verified without running the user's function. It is also unnecessary:
serialize_function already pickles by value via dill(recurse=True) /
cloudpickle, which round-trips nested functions, closures, module globals
and source-less functions. decorator.py no longer imports the flattener at
all, so the substitution cannot be reintroduced by accident.
- analyze_function_complexity reports source_available. On the failure branch
the metrics are None and is_complex is False -- "I could not analyse this"
is now distinguishable from "I analysed it and it is complex".
- auto_flatten_if_needed no longer reports success: True while handing back
the original function. It returns explicit flattened/success/reason/strategy
and skips entirely when there is no source to rewrite. It also no longer
picks a hoisted helper as the main function: the namespace lookup was a
substring match, and helpers are named {parent}_{nested}_hoisted.
- #89/#90 TODOs replaced with the reason they are not being implemented:
flattening has no caller in the execution path, so completing the closure
and global-variable plumbing would only make an unusable rewriter reachable.
New tests run for real -- no mocks. SubprocessJobRunner is a genuine
implementation of the executor contract that ships the real serialized payload
to a fresh interpreter and executes it. Against the pre-fix code these fail
with: "clustrix returned 'Function execution completed' but the function
computes 42".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
The file told anyone reading it to add a cluster type to a ClusterType enum in config.py. There is no ClusterType anywhere in the package -- cluster_type is a plain str. It located ClusterExecutor in clustrix/executor.py, which is a 39-line re-export shim; the implementation is spread across seven executor_*/hf_jobs modules, none of which were mentioned. It claimed functions defined in a REPL 'cannot be serialized', which is false and is the belief that produced the fabricated-result bug: serialization works fine without source, only the inspect.getsource-based features need it. It also carried one half of a mocking policy that contradicted .claude/CLAUDE.md's other half, so developers could cite either and neither governed. Both are replaced by one stated policy: real verification first, mocks only as a cost-control stand-in afterwards, never as a fallback, never inside shipped code, and never a reason to weaken a failing test. Added: the two-venv execution model and why every handoff must stay symmetric; HMAC verification of remote results; host-key verification via ssh_security.configure_host_key_policy; the billable-test guard and why it reads config.args; a backend table that says plainly which backends are actually proven and which are not. Every factual claim in the new text was checked against the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Kubernetes (#119): the worker printed `CLUSTRIX_RESULT:{result}` -- the repr of the result -- and the caller ran it through ast.literal_eval, returning the repr string when that failed and the whole pod log when no marker was found. It now writes a base64 pickle plus an HMAC over those bytes, keyed by a per-job secret passed in CLUSTRIX_RESULT_KEY, and the caller verifies it before deserializing. check_k8s_job_status no longer answers "completed" whenever the API call raises: an outcome that cannot be read is an error. The worker program is now a module-level function, so it can be run directly and tested without a cluster. cluster_type "local" (#120): ClusterExecutor had no branch for it and raised "Unsupported cluster type: local", though the widget offers it. LocalJobManager in local_executor.py wires it to the existing LocalExecutor. PBS (#120): submit_pbs_job never set up a remote environment, so its script activated a virtualenv nothing had created. SLURM and SSH each carried a copy of the two-venv setup and SGE had only half of it; all four now share _stage_job_directory and _setup_job_environment. Placeholder hostnames (#119): azure/gcp/lambda returned cluster_host "placeholder.<provider>.com" (or "") when they could not read an instance's address, which surfaced later as an SSH failure against a domain that does not exist. They now raise, naming the provider, the instance and what could not be determined. Provider interface (#119): only Lambda implements create_instance. Submitting to another provider was accepted and the NotImplementedError then surfaced inside a background thread. submit_cloud_job now checks the interface and the authentication up front and refuses with a message naming both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…opping it
Redacting secrets from saved configs swept environment_variables into
SECRET_FIELDS wholesale, which meant a save/reload cycle silently lost
OMP_NUM_THREADS along with AWS_SECRET_ACCESS_KEY. It also contradicted
the rationale stated directly above it -- derive the secret set from
field names, do not hand-list -- since it was a hand-added exception.
Each entry is now judged on its own key name, so ordinary settings
survive and credentials do not:
environment_variables saved {'OMP_NUM_THREADS','MY_PIPELINE_STAGE',
'AWS_SECRET_ACCESS_KEY','HF_TOKEN'}
loaded {'OMP_NUM_THREADS','MY_PIPELINE_STAGE'}
plaintext AWS secret on disk: False
plaintext HF token on disk : False
file mode: 0o600
Two fields also matched the pattern without holding a secret:
use_env_password is a boolean flag, and password_env_var holds the NAME
of an environment variable rather than its value. Dropping them broke
the auth-fallback round trip while protecting nothing, so the derivation
now excludes 'use_*' and '*_env_var'.
Two tests asserted the old behaviour; their assertions are rewritten
rather than the code reverted, and two new tests cover the mapping.
Also renamed the credential-shaped test fixtures that made
check_for_secrets report 6 findings on the tree. They were real fixtures,
but the scanner was right to be suspicious of 'hunter2-super-secret' and
'sk-real-looking-secret-abcdef123456'. Renaming them to obviously-fake
values keeps the scanner strict rather than teaching it a suppression
marker that could later hide a real secret.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…atterns tutorials #124: Fix MIGRATION.md's incorrect `from clustrix import ClusterConfig` (not re-exported; must be `from clustrix.config import ClusterConfig`). Fix the sphinx duplicate-object warnings (60 -> 4): filesystem.rst, file_packaging.rst, and dependency_analysis.rst each had a blanket `automodule:: :members:` PLUS per-member autoclass/autofunction directives for the same objects; since this project's autodoc_default_options sets members=True globally, both fired. Switched those three pages to `currentmodule` + the explicit-directives-only pattern already used in cost_monitoring.rst. The remaining 4 warnings come from a docstring in clustrix/notebook_magic_config.py (out of docs/ scope). #70: Document Kubernetes auto-provisioning (previously undocumented) in kubernetes_tutorial.rst: local kind-based provisioning (no cloud credentials needed) and the five cloud providers, each explicitly labeled unverified per README's existing wording. Documents a real gotcha found while verifying against the source: `@cluster(provider=...)` does not select the Kubernetes provisioner -- `configure(k8s_provider=...)` does, and it defaults to "aws". #96: New docs/source/tutorials/usage_patterns.rst turning issue #96's deleted-script snippets into verified, runnable patterns. Notes that @cluster falls back to local execution with no cluster configured, which is what makes the examples runnable without a real cluster. #88: Verified clustrix.utils.serialize_function/deserialize_function round-trip a function whose source is unavailable (prints 5). Narrowed usage_patterns.rst's description of the REPL limitation accordingly: it's the source-based features (loop parallelization, GPU-parallel detection, dependency analysis) that need inspect.getsource(), not serialization itself. README wording changes reported separately (README.md is out of this agent's file ownership). Added scripts/check_docs_examples.py: extracts every Python code block from the touched docs, executes the ones that don't need external resources for real (no mocks), and for blocks marked `# cluster-required` checks syntax plus that every imported name actually exists via importlib/hasattr. Also fixed two real bugs found while extending this script's coverage to the pricing docs at the coordinator's request: PRICING_USER_GUIDE.md's CustomPricingClient example was missing the required abstract method _fetch_pricing_from_api (verified TypeError without it), and the documented CostEstimate dataclass had invented fields (provider/hours/region) that don't exist on the real one in clustrix/cost_monitoring.py. Removed all documentation of clustrix.pricing_clients.performance_monitor, .resilience, and .validation_alerts (deleted as orphaned code) from PRICING_API_REFERENCE.md and PRICING_USER_GUIDE.md, replacing each with either the real remaining API or an explicit removal note -- no invented replacement APIs. 36/36 doc code blocks pass scripts/check_docs_examples.py (29 executed for real, 7 statically verified as needing external resources). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…truth Four files disagreed about what version this is: pyproject.toml and setup.py said 0.1.1, clustrix/__init__.py and docs/source/conf.py said 0.1.0. All four now say 0.2.0, the release #127 is about. The set of supported cluster types was written out separately in clustrix/cli.py and the widget dropdown, and they had drifted: the CLI offered slurm/pbs/sge/kubernetes/ssh/local and omitted 'huggingface' entirely, so a backend that is verified working end to end could not be selected from the command line. Both now read config.SUPPORTED_CLUSTER_TYPES. Verified they agree: canonical : ('local','ssh','slurm','pbs','sge','kubernetes','huggingface') CLI choices : ['local','ssh','slurm','pbs','sge','kubernetes','huggingface'] widget : ('local','ssh','slurm','pbs','sge','kubernetes','huggingface') Export ClusterConfig from the package root. ClusterExecutor, ClusterFilesystem and ProfileManager were all exported and it was not, so the obvious import raised ImportError -- which MIGRATION.md had been telling users to write. README's REPL section claimed such functions 'cannot be serialized'. They can: serialization works from the code object. What is actually lost is the source-based features -- loop parallelization, GPU-parallel detection, complexity analysis. Narrowed to say that, since the overstatement is what justified the flattening detour that fabricated results. Last 4 sphinx warnings fixed: autodata pointed at the module that re-exports DEFAULT_CONFIGS rather than the one that defines it, so autodoc fell back to dict.__doc__, whose **kwargs and indented body are not valid RST. Docs now build with zero warnings, down from 60. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
D1 get_environment_requirements dropped every `name @ file:///...` line, which
is how uv renders conda-built packages -- 187 of 563 packages here. It now
reads installed metadata directly, so uv and pip can no longer produce two
different answers (and two different _environment_key values) for the same
machine. Requirements that genuinely cannot be reinstalled remotely
(editable installs, VCS checkouts, bare egg-info source trees) are no
longer silently dropped: they are reported, and a payload that reaches into
one is refused at submit time naming the package.
D2 The walk no longer truncates at a node cap and submits anyway; it finishes,
or raises WalkTooLargeError.
D3 Instance attributes (__dict__ and __slots__) are now walked.
D4 A project-local class subclassing dict/list/tuple is now followed by type,
so it travels instead of arriving stripped of its methods.
D5 _is_local_module falls back to __path__, so PEP 420 namespace packages are
recognised and their children enqueued.
D6 functools.partial and bound methods are followed to their targets.
D7 The unpicklable-object message names the object and where it actually lives
(closure variable, module-level name, attribute) instead of asserting
"module level" and listing every local module in the payload.
D8 A remote interpreter is accepted only when its minor version matches the
local one; payload bytecode does not cross minor versions.
D9 Conda environments are stamped ready only after every install succeeded,
the reuse check requires that stamp, and no install is wrapped in
`|| echo 'Failed to install ...'` any more.
The mock-based environment tests that asserted clustrix shells out to pip are
replaced with real ones against the real environment -- faking freeze output is
exactly why the uv/pip divergence went unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…p the job script being a shell V1 (critical) result.pkl was HMAC-verified before dill.loads and error.pkl was not, so "make the job fail" was a complete bypass: a hostile cluster exits non-zero, writes its own error.pkl, and its __reduce__ runs on the submitting machine. Both call sites wrapped the load in `except Exception: pass`, so a failed attempt was silent. error.pkl is now signed with the same per-job key by every worker path and verified on the same terms; the refusal is raised outside the try so it cannot be swallowed into the text-log fallback. V2 Cloud results were deserialized with no key generated and no verification at all, under a comment claiming the worker wrote them with dill while the same file wrote them with pickle.dump. The cloud path now creates its work dir 0700 with a 0600 key inside, reuses result_signing_lines()/verify_signed_payload(), and writes with dill as the caller has always claimed. V3 Verification failed OPEN when no key was recorded: it warned and loaded anyway, so "no key" -- an adopted job id, a cleared table -- was as good as a valid signature. Missing key, empty key and untracked job are all refusals. V4 Every config value reaching the generated job script was pasted in raw. Ordinary shell words (job dir, env-var values, pip specs, venv paths, interpreter) are shlex.quote()d; places that must stay unquoted (module load arguments, #SBATCH/#PBS/#$ directive bodies, export names) are validated against a strict allowlist and refused naming the config key. pre_execution_commands stay a fragment on purpose. V5 The signing key stayed in the job's environment while the user's function ran, so any dependency could forge a validly tagged result. It is captured and popped before user code in every generated program, popped in the HF bootstrap before pip runs, and the HF log parser now selects the block that verifies rather than the first one printed. V6 `dill or cloudpickle or pickle` silently fell back to stdlib pickle on dill bytes -- the exact failure #121 was filed about. The requirement is now stated and the job fails naming the missing package. Also replaces the last AutoAddPolicy() in the package (executor_cloud) with configure_host_key_policy(). Regression tests are real: a real directory as the remote host, real signing, real tampering, and the generated worker programs really executed. 15 of the 21 new authentication tests and 8 of the updated ones fail against the previous tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…essage Reading installed metadata costs ~0.25s and a submission asks for it several times, so it is cached keyed on sys.path -- the thing that decides which distributions are visible, so any change that could change the answer changes the key. test_an_unembeddable_module_raises_here_not_there asserted the old text, which blamed module level for a lock held in a CLOSURE. It now asserts the message names the closure variable, the function, and the object type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…c tests Salvage review of PR #128 (branch epic/test-coverage-90-percent) found its loop-analysis tests too weak (isinstance(x, list) only) and its "advanced" suite dependent on API that doesn't exist on master (integrate_with_decorator, enhanced_dependency_analysis, etc.) -- not ported. Comprehension auto-parallelization (visit_ListComp/SetComp/DictComp/GeneratorExp, #132's landmine) was likewise not brought across; a test now pins that down. While writing real semantic tests against find_parallelizable_loops, found and fixed four correctness bugs in clustrix/loop_analysis.py: - DependencyAnalyzer.visit_AugAssign never counted `total` in `total += i` as a read (AugAssign targets are Store-only in the AST), so a plain reduction accumulator came back with zero dependencies and is_parallelizable=True. - detect_loops_in_function() didn't dedent inspect.getsource() output, so any function defined inside a class/closure (one indentation level deep) raised IndentationError, silently swallowed, always returning []. - detect_loops_in_function() called _analyze_for_loop/_analyze_while_loop directly via ast.walk() instead of detector.visit(tree), bypassing the current_level bookkeeping -- nested_level was -1 for every loop found via the public API, making find_parallelizable_loops's nesting-depth filter a no-op. - SafeRangeEvaluator couldn't evaluate a literal negative number (-1 is UnaryOp(USub, Constant(1)), not Constant(-1)), so range(10, 0, -1) always fell back to range_info=None. Also found clustrix/dependency_analysis.py's separate, exported LoopAnalyzer._is_loop_parallelizable() (public via clustrix.analyze_function_loops) only checked for break/continue/global despite documenting a "no shared mutable state" criterion -- it approved both the accumulator and shared-list-append patterns above. Fixed by reusing loop_analysis.DependencyAnalyzer instead of a second, weaker implementation. Added tests/unit/test_loop_analysis_semantics.py: real functions, no mocks, asserting on actual dependency/parallelizability values -- loop-carried deps, shared-state mutation, break/continue/return/for-else, nesting levels, enumerate/zip/dict.items() tuple-target blind spots, range() variants, and the comprehension-non-detection regression guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…lel real The two TODOs #89 and #90 ask to implement (global-variable extraction in dependency_resolution.py, closure-variable arguments in function_flattening.py) live inside code that has never produced a runnable output for any input tried. The advanced flattener emits `import <name>` for hoisted helpers and builtins; the basic one dedents the body to column 0, drops `for` headers and prints instead of returning. Verified live on an ordinary nested-helper function: Generated flattened code did not execute: No module named 'helper' Generated flattened code did not execute: name 'i' is not defined Not flattening compute: advanced flattener produced no usable callable serialize_function/deserialize_function already handle every case flattening was meant to rescue. Round-tripped in a fresh interpreter with the defining module off sys.path: nested helper 45, deep nesting 65, module-level helper 19, closure 40, exec()-created 5, args+kwargs 21 -- all matching the direct call. decorator.py no longer reaches for either module. So: delete them. Removed clustrix/function_flattening.py (1027) and dependency_resolution.py (445), and the five test files that only ever tested them. Kept and re-pointed the tests that cover live behaviour: the GPU workflow simulation now proves the serializer round trip instead of flattening, the tensor01 and cluster GPU tests keep their real remote execution and lose only the complexity assertions. clustrix.dependency_analysis (the public analyze_function_dependencies) is a different module and is untouched. Also #120 item 2, the same defect class. _create_local_work_chunks injected `_parallel_<var>` into callees that never declared it, so every chunk raised TypeError, which _execute_local_parallel swallowed under a blanket except and converted into a silent sequential re-run -- auto_parallel never parallelized anything locally and said nothing useful. Chunks are now built only for a signature that can receive them, and TypeError on the parallel path propagates. scripts/check_for_secrets.py is a black-only reformat; it was not black-clean at HEAD and the mandated `black clustrix/ tests/ scripts/` run touches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
There was no CHANGELOG.md anywhere in the repository. This one records what actually changed, and keeps a standing 'Implemented but unverified' section so backends that have never been run against real hardware are never quietly listed as working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
executor_kubernetes.py was rewritten so job status/result collection raise on unreadable outcomes instead of reporting fake success, and pod results are now a signed base64 payload (CLUSTRIX_RESULT_B64 + CLUSTRIX_RESULT_HMAC via decode_signed_result) instead of a bare "CLUSTRIX_RESULT:<repr>" string run through ast.literal_eval. Also, the #80 module refactor moved _get_k8s_result/_get_k8s_error_log/ _cleanup_k8s_job off ClusterExecutor onto executor.k8s_manager, and get_job_status() now requires active_jobs entries to carry a "manager" key. - tests/test_kubernetes_integration.py: run the real build_worker_program() worker as a subprocess to produce genuine signed pod-log output (no hand-written stand-ins for the retired format), call executor.k8s_manager.* where ClusterExecutor has no shortcut, and tag active_jobs entries with "manager": "kubernetes". Removed a dead patch("clustrix.executor.cloudpickle") left over from before cloudpickle usage moved into executor_kubernetes.py. - tests/test_cloud_providers_gcp_real.py: GCPProvider.list_instances(), .create_instance(), .is_valid_region()/.is_valid_zone() never existed (verified via `git log -S`) -- these assertions were against a fabricated API since the tests were added, unrelated to the recent rewrite. Rewritten against the real, currently-implemented API: list_clusters()/create_compute_instance() now raise "Not authenticated with GCP" instead of proceeding with a None client, and region/zone behavior is verified against the provider's actual defaults and its real (unauthenticated) get_available_regions()/ get_available_instance_types() fallback lists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
serialize_function contained:
_ = get_environment_info() # For compatibility with tests
Shipped code calling a function purely so that a mock in
tests/test_integration.py would be hit, then discarding the result. It
is the same anti-pattern as #116's isinstance(..., Mock) branches, and
it cost a 'pip list' subprocess on every job submission to compute
nothing.
Faking that freeze output is also why the environment-replication bug
survived so long: get_environment_requirements() was dropping 187 of
563 packages and every test that mocked the freeze step still passed.
Also remove the guard that let the remote setup continue after failing
to install dill and cloudpickle. The generated worker now refuses to
fall back to stdlib pickle -- pickle serializes a function by qualified
name and cannot resolve it in a fresh interpreter -- so swallowing that
failure only moved the error to a later and far more confusing point.
pip's own upgrade stays non-fatal: pip's version is not part of the
replicated environment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…e widget assertions ProfileManager.__init__ hardcoded config_dir="~/.clustrix/profiles", ignoring CLUSTRIX_CONFIG_DIR. Every caller that constructs ProfileManager() with no explicit config_dir -- the widget's default and notebook_magic_core.py's default -- silently read and wrote a real user's ~/.clustrix, even under tests/conftest.py's isolate_config_dir fixture. On this machine that had accumulated 47+ "Current configuration (N)" profiles in ~/.clustrix/profiles/ profiles.yml. Default now resolves via clustrix.config.get_config_dir(), which honors CLUSTRIX_CONFIG_DIR. That fix alone did not make every failing assertion correct: several tests in test_modern_widget_comprehensive.py encoded a ProfileManager/widget shape that no longer matches the code (single default profile vs. one built-in template per backend; "clustrix.yml" vs. the deliberate "profiles.yml" default; "auto"/"~/.ssh/id_rsa" placeholders vs. the active profile's real ClusterConfig defaults of "pip"/None). Rewrote those assertions to match current, intentional behavior, and rewrote test_widget_initialization_with_mock_ipython (renamed to ..._with_real_ipython) to drive the real installed ipywidgets/IPython instead of the file's MockWidgets shim, per the no-mocking-the-thing-under- test rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
The document described a system that has been deleted, and described it approvingly -- 'complexity-based triggering system works' was never true. Rather than delete it outright, record why the approach was abandoned, since the reasoning is the part worth keeping: it explains what would have to be true before anyone builds this again, and names the two questions the original never answered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
22 failures across the four executor test modules, all of them tests that had been left behind by the split of executor.py into executor_core / executor_connections / executor_schedulers / executor_kubernetes. Most were mock theatre in the sense of #117: they patched names the refactor moved (clustrix.executor.setup_remote_environment, clustrix.executor.cloudpickle, clustrix.executor.logger) so the patches were silent no-ops, replaced backward-compatibility aliases nothing calls any more (_execute_remote_command, _check_job_status, _submit_slurm_job), fed a Mock a canned string and asserted the string came back. None of them could fail for a real reason. Rather than re-point the mocks at the new call graph, these now exercise real code: * cluster_type="local" really runs the function, so submission, status, result collection, error logs and the active_jobs["manager"] routing are all checked against real execution; * create_job_script is pure, so each scheduler's directives are checked against real generated output; * where a real cluster would be needed, the assertion is on the error path -- a submission with no connection must raise and record no job, a failed cancellation must not drop the job from tracking; * the Kubernetes tests use the real kubernetes client against a real kubeconfig file on disk. Assertions deliberately changed, with the reason recorded in each docstring: * test_execute_command_not_connected expected "Not connected"; the shipped message is "SSH client not connected. ...". * test_get_job_status_completed/_failed hand-built an active_jobs entry with no "manager" key; get_job_status now dispatches on it. * test_get_result_success mocked SFTP into writing an unsigned pickle; results are HMAC-verified before deserialization now, so an unsigned one is refused by design. * test_parallel_job_submission passed timeout= to wait_for_result, which takes only a job ID. One test deleted rather than repaired: test_setup_kubernetes_cloud_manager_exception asserted that a Mock raising ImportError produced a log line. CloudProviderManager's constructor stores two attributes and cannot raise, and auto_configure catches its own exceptions, so that branch is unreachable without a mock. Verified: 59 passed, 2 skipped across the four modules (was 22 failed, 35 passed, 2 skipped); tests/unit 573 passed; black 26.3.1, flake8 7.3.0 and mypy all clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ential_manager suites All 14 failures and 2 errors (actually 17 failures + 3 errors once the suite was run fresh) traced to tests written against field/function names that never existed or were renamed, plus fixtures scoped to the wrong test class. Two real bugs turned up in clustrix/auth_fallbacks.py along the way and are fixed here too: requires_password_fallback() crashed with AttributeError whenever a key-setup result explicitly set "error": None (the normal case from ssh_utils.setup_ssh_keys' success path), and get_cluster_password() could fall through to a real, blocking tkinter GUI prompt in this environment because `import clustrix` puts 'ipykernel' in sys.modules as a side effect, making detect_environment() always report "notebook". - tests/test_config_real.py: moved temp_config_dir fixture to module scope (TestConfigurationWorkflows couldn't see the class-scoped one); renamed "partition"->"default_partition", "namespace"->"k8s_namespace", "private_key_path"->"key_file" throughout (fields that were never named that); dropped assertions/kwargs for fields that never existed on ClusterConfig at all (gpu, cleanup_on_failure, node_selector, tolerations, service_account, image_pull_secrets, k8s_project_id, k8s_zone, k8s_gpu_type/count, k8s_preemptible, k8s_autoscaling, k8s_min/max_nodes, account, qos); rewrote test_configuration_precedence, which relied on a CLUSTRIX_DEFAULT_CORES env var override that has no implementation anywhere in config.py. - tests/test_auth_fallbacks_real.py: moved temp_credentials_dir fixture to module scope; rewrote every test that called requires_password_fallback() with a ClusterConfig instead of the Dict[str, Any] key-setup-result it actually takes; fixed get_cluster_password()'s hostname= kwarg name and get_password_gui()/get_password_widget()'s single-prompt-arg signature; gave setup_auth_with_fallback() a real (non-mock) setup_ssh_keys_func callable instead of calling it with the wrong arity; skip the GUI test outside an interactive terminal (mirrors the existing CLI test's skip) since this machine's tkinter would otherwise open a real blocking dialog; rewrote test_secure_password_handling to exercise the real secret redaction on save_to_file() rather than the non-existent repr masking it originally asserted. - tests/test_credential_manager.py: 1Password was removed in Issue #97 ("use only .env, environment vars, and GitHub secrets"), so FlexibleCredentialManager has 3 sources, not the 4 these tests still asserted; fixed the default-location test to compare against get_config_dir() instead of a hardcoded ~/.clustrix, since conftest.py's session-scoped isolate_config_dir fixture deliberately redirects CLUSTRIX_CONFIG_DIR for the whole test run. - clustrix/auth_fallbacks.py: requires_password_fallback() now treats a present-but-None "error" key the same as an absent one. No test in either file touches the developer's real ~/.clustrix (verified via `find ~/.clustrix -newermt` before and after every run); clustrix/ config.py was left untouched per the file-ownership boundary for this issue -- see the sweep report for the config.py defects found instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
tests/test_auth_fallbacks_real.py used password="irrelevant-because-key-already-works" twice, which check_for_secrets correctly reports as an assigned credential -- the scanner cannot know the value is a stand-in, and the CI security job would have failed on it. Renamed to a value the scanner's existing fixture vocabulary recognises, rather than teaching it a suppression marker that could later hide a real secret. executor_schedulers.py carried a second docstring-shaped string in the middle of the class body, left over from a refactor. The class already has a real docstring, so this one was a dead expression statement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…nce docs ClusterConfig's dataclass-generated __repr__ printed every field verbatim, so a password, API key or HF token landed in any traceback, log line or notebook cell that displayed a config: 'hunter2-real' leaks: True 'hf_realtoken' leaks: True 'sk-realkey' leaks: True save_to_file already refused to write those in plaintext; showing them on screen instead was barely an improvement. They are masked as '***' rather than omitted, so it stays visible that a value is set, and environment_variables is masked per entry on the same rule that governs saving -- OMP_NUM_THREADS stays readable, AWS_SECRET_ACCESS_KEY does not. CLAUDE.md's configuration-priority list named 'environment variables' as a level. No such level exists: nothing reads a CLUSTRIX_<FIELD> variable. Only CLUSTRIX_CONFIG_DIR (where files live) and whatever password_env_var names (a password, nothing else) are consulted. That gap matters more now that saved configs omit secrets by default, so the corrected text says plainly that password_env_var is currently the only supported way to supply a credential without writing it to disk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Round three of the documentation review reached docs/source/api/*.rst and the
module docstrings automodule/currentmodule renders from them. Every claim
below was checked against the code before it was rewritten.
api/config.rst
- local_cache_dir is defined (config.py:140) and read nowhere in clustrix/.
configuration.rst was right to say "Not read."; this page was wrong to list
it as a live setting. Say it is accepted and stored but has no effect.
- The hf_token bullet was truncated mid-sentence and wrong: hf_jobs.py:249-251
falls back to $HF_TOKEN and then to the `hf auth login` token cache.
- "Individual settings are *not* configurable" by environment was false.
There is no general CLUSTRIX_<FIELD> layer, but HF_TOKEN, HF_HOME and the
variable named by password_env_var are all read. Say exactly that.
api/cost_monitoring.rst
- generate_cost_report has no duration_seconds parameter; its signature is
(provider, instance_type="default") and it hardcodes estimate_cost(_, 1.0),
so its "session cost" is a one-hour quote. Both places now say so.
- cost_tracking_decorator's instance_type never prices anything:
stop_monitoring calls estimate_cost("default", ...) (cost_monitoring.py:103).
- "Always read pricing_warning" was a trap. aws_pricing.py:100 falls back to
the hardcoded table inside get_instance_pricing, so the monitor labels the
record pricing_source="api" with pricing_warning=None. Verified with no AWS
credentials: p3.2xlarge -> 3.06/hr, source 'api', warning None, while the
logger says "Using hardcoded pricing ... (last updated: 2025-01-01)".
- The price tables are a 2025-01 snapshot, not live pricing. Say so.
- nvidia-sml -> nvidia-smi.
api/file_packaging.rst - the prefix is clustrix_packages_ (file_packaging.py:954),
so the documented cleanup glob never matched.
api/notebook_magic.rst - kubernetes does get dedicated fields (namespace,
image, service account, pull policy; modern_notebook_widget.py:1118-1160).
Same fix already applied to index.rst and README.md; this copy was missed.
api/local_executor.rst - cancel_job raises ValueError for an unknown job ID
and RuntimeError for a known one, not RuntimeError always.
api/dependency_analysis.rst - the example used cluster_exists without importing it.
clustrix/local_executor.py (docstrings only)
- execute_loop_parallel's example raised AttributeError: the chunk worker is a
closure a process pool cannot pickle, so use_threads=True is required. Fixed
and verified by running; expected outputs added and checked with doctest.
- choose_executor_type's CPU example claimed False but yields True as written:
a function defined in a REPL or doctest is unpicklable and takes the threads
branch. Use importable functions and state the caveat.
clustrix/cost_monitoring.py (docstrings only) - the same instance_type and
"current session" corrections, plus the missing `cluster` import in the
example, which raised NameError as published.
scripts/check_docs_examples.py - check docstrings too
Two of the bugs above hid in docstrings, which the checker did not read. It
now also scans every module named by an automodule/currentmodule directive
under docs/source -- derived from the directives, not hand-listed. Inside a
docstring it recognises doctest runs, .. code-block:: python, and literal
blocks introduced by Example::/Examples::/Usage::; each is executed in a fresh
copy of the owning module's globals, or statically verified if marked
# cluster-required. Expected doctest output is not compared; that limit is
stated in the module docstring.
Verification: check_docs_examples 168 blocks, 168 passed, 0 failed;
sphinx build succeeded, no warnings; black/flake8/mypy clean;
pytest -m "not real_world" 1764 passed, 11 skipped, 28 deselected, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…e not The third review found the fabrication this whole cycle exists to remove still sitting on the project's front page. "# Loop gets parallelized automatically" and its variants appeared in README, CLAUDE.md, three API pages, a tutorial and four notebooks. Every one was false twice over. Measured against the real functions the comments were attached to: process_datasets detect_loops=None accepts_chunks=False sample_loop detect_loops=None accepts_chunks=False `for filename in data_files` is not a range at all, so detect_loops declines it outright; and none of these functions accept the _chunk_range_<var>/_chunk_index keywords, so _create_work_chunks produces no chunks and the call runs whole. The reader was promised distribution and got sequential execution. Each comment now says what actually happens and points at the auto-parallelization contract in the limitations page. Worth noting why the tooling did not catch this: scripts/check_docs_examples.py executes each block and checks it does not raise. A block whose code is fine while its comment lies passes cleanly. Comments are not executable, so no amount of example-running would have found these -- only reading them against the code did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ebook tree Round four of the documentation review found false "this loop is automatically parallelized" claims surviving in notebook cells, and a stale duplicate notebook tree that five published Colab badges pointed into. Parallelization. Verified against clustrix/decorator.py (_create_local_work_chunks, _create_work_chunks, _accepts_chunk_kwargs), clustrix/loop_analysis.py and clustrix/utils.py::detect_loops: a loop is only split when its range is a literal range(<int>), its iterations carry no dependency, and the function accepts the chunk keyword (_parallel_<var> locally; _chunk_range_<var> and _chunk_index remotely). Fail any one and the function runs whole and clustrix logs the reason at INFO. - complete_api_demo: rewrote the "Automatic Parallelization" section to state the three conditions, and replaced its example -- `for item in items` over a list argument was labelled "will be parallelized" and is declined on all three counts -- with a pair showing one declined loop and one that really is split (25 chunks, verified locally and in an IPython session). - complete_api_demo: 'parallel_jobs' no longer claims one job per iteration (chunks are ~max_parallel_jobs, and results come back per chunk, unflattened). - complete_api_demo: the two "# Parallelized" comments on range(100) loops were false -- both bodies append to a list and neither callee takes a chunk keyword. One is now labelled as running whole (its point was memory, not parallelism); the Monte Carlo example was rewritten to actually qualify. - complete_api_demo summary: "Automatic Parallelization" bullet now states the conditions. - basic_usage: "# This loop could be parallelized" -> it is not; says why. - filesystem_tutorial: summary bullet claiming loop processing is parallelized contradicted the correction already made at cell 17; now consistent. Duplicate tree. docs/notebooks/ is deleted and every reference repointed at docs/source/notebooks/, which is the tree docs/source/index.rst already builds and which has 15 notebooks to the old tree's 8 (seven of them, including the un-overhauled clustrix_demo.ipynb advertising the deleted auto_gpu_parallel feature and four keywords @cluster does not accept, existed only there). All 12 Colab badges now resolve to tracked paths on master. README's tutorial location updated. Also: nbformat.validate() failed on basic_usage (markdown cells carrying `outputs`) and on four tutorials declaring nbformat_minor 4 while using cell ids, which need 4.5. Fixed; all 15 notebooks now read and validate.
Step 3 was titled "use all your cores on one machine" and did not use them. Its example failed the auto-parallelization contract three ways at once: range(n) is not a literal range, `total += math.sqrt(i)` is a loop-carried dependency, and the function declared no _parallel_i parameter. Any one of those is enough for clustrix to decline. The prose mentioned only the REPL/source condition, which was the one thing that did not apply. This is the most-read page in the set, and the same defect the previous rounds removed from README and the notebooks. The replacement is verified to distribute: values returned: 50000 matches sequential exactly: True 25 chunks across 4 workers, concatenated back in order, identical to what the undecorated function returns. The section now states the three requirements before the example rather than after the disappointment, and notes why the file needs a __main__ guard: the worker processes re-import the module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Round four of the doc review found the overhaul was scoped to docs/source/ and never opened the guides one click outside it. Several instructed readers to run scripts that don't exist. Operations guides, fixed to point at what actually works: - REAL_CLUSTER_JOB_TESTING.md: scripts/run_cluster_job_tests.py moved to tests/real_world/cluster_validation/ in #76 and is only importable as a module from the repo root; repointed all 14 call sites plus one dangling scripts/test_real_world_credentials.py reference. - CREDENTIAL_SETUP.md: dropped two nonexistent diagnostic scripts in favor of the real `run_real_world_tests.py --check-creds`, and corrected the false claim that real-world tests run on push/PR -- the workflow is workflow_dispatch + schedule only, deliberately, per #113/#118. - docs/aws/*: test_aws_preflight.py and test_aws_eks_real.py live in tests/integration/, not the working directory; fixed 9 call sites across 4 files. Also dropped a real AWS account ID hardcoded in a troubleshooting guide and a setup script (the variable was never actually used). Historical records, marked as such rather than rewritten to look correct: - PRICING_API_DEPLOYMENT.md describes a pricing_service daemon, a clustrix[pricing] extra, and an /etc/clustrix/clustrix.yml schema that never existed and that config.py's unknown-key check would reject. Banner added pointing at the real, working pricing_clients/cost_providers system documented in PRICING_API_REFERENCE.md / PRICING_USER_GUIDE.md. - TECHNICAL_DESIGN_AUTH_ENHANCEMENT.md (#66): partially implemented. Banner lists what shipped (use_env_password, password_env_var, the auth_fallbacks chain) against what didn't (create_cluster_widget, validate_kerberos_auth, --password-env-var). - ssh_key_automation_technical_design.md (#57): flagged the one code sample using paramiko.AutoAddPolicy() directly as superseded by configure_host_key_policy(), which every real call site now uses. - COMPLEXITY_THRESHOLD_ANALYSIS.md: "root cause still under investigation" was true when written; the two-venv pickle/dill fix for #120 resolved it. Cross-referenced from function_dependency_design.md, which already pointed here. docs/source/api/config.rst (env-var section only): replaced the false "two CLUSTRIX_ variables plus three more" claim with the actual set read on the ordinary SSH-without-password path (executor_connections.py calling FlexibleCredentialManager.ensure_credential("ssh"), which loads all of ~/.clustrix/.env and reads ~22 SSH/cloud variables) and the separate, non-automatic setup_ssh_keys_with_fallback() path (auth_fallbacks.py's CLUSTRIX_PASSWORD_<HOST>/CLUSTRIX_DEFAULT_PASSWORD/CLUSTER_PASSWORD). pyproject.toml / setup.py: azure_provisioner.py imports azure-mgmt-authorization, which was declared in neither file, and azure-mgmt-compute/-resource/-network were declared only in [test], so `pip install clustrix[azure]` couldn't actually provision anything. Added all four to the azure/cloud/all extras in both files (kept identical). Also found and fixed a real break along the way: azure-mgmt-resource 26.0.0 dropped the `from azure.mgmt.resource import ResourceManagementClient` re-export the code uses, so every affected extra now pins <26.0.0 -- verified by installing clustrix[azure] into a clean venv and importing clustrix.kubernetes.azure_provisioner / clustrix.cloud_providers.azure before and after the cap. Verified: scripts/check_docs_examples.py (0 failed) and --include-notes (239 passed, 63 pre-existing failures, identical before/after via git stash); python -m sphinx -b html docs/source build succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ry job Quick Checks failed on the branch: scripts/aws/cleanup_resources.py and destroy_cluster.py imported boto3 at module scope, so even --help raised ModuleNotFoundError on a machine without the AWS SDK. boto3 is not a clustrix dependency and the CI job installs only the dev extra. The tests asserting that --help works were right; the scripts were wrong. boto3 is now imported inside the client-construction functions, which is where it is actually needed. cleanup_resources.py: help_shown=True destroy_cluster.py: help_shown=True (both with boto3 unavailable) Separately, the fast_ci "CI Status" gate listed security-scan among its needs but never checked its result -- so a failing security scan still printed "All CI checks passed". That is the same decorative-gate problem as the --exit-zero flake8 step removed earlier in this branch. The gate now checks every job it depends on and names the one that failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…at lied
The fifth review made the point that mattered: previous rounds fixed the
files they were handed and left identical claims everywhere else. This
round greps for the claim pattern across every .md, .rst, .ipynb and .py
in the repository instead of working from a list.
Remaining false claims, all verified against the code before removal:
README.md:408 ("This loop will be automatically distributed"),
examples/filesystem_tutorial.py:156 -- the repository's only example
script -- and a slurm_tutorial.ipynb docstring whose own corrective
comment sat three lines below it. None of those loops iterate a literal
range() and none of those functions accept a chunk keyword, so all three
ran sequentially. The only surviving instance is inside
docs/gpu/GPU_PARALLELIZATION_DESIGN.md, which is marked WITHDRAWN and is
a record of a deleted feature.
basic_usage.ipynb carried a fabricated benchmark: two functions with
byte-identical bodies, one decorated parallel=False and the other
parallel=True, timed against each other with a printed "Speedup: Nx".
Neither was ever parallelized, so the ratio measured timing noise
between one code path and itself. Replaced with a benchmark whose two
sides genuinely differ, and a note that a speedup number means nothing
until you have checked the parallel version actually parallelized.
Two gates were not gating:
- scripts/check_quality.py, which CLAUDE.md and README both name as the
recommended pre-commit check, ran `pytest tests/ -q` -- 2200 tests,
including the ~400 under tests/real_world/ that open live SSH and cloud
connections. The check you are told to run before committing was the
thing dialling out. It now uses CI's selector, invoked through
sys.executable rather than whatever pytest is first on PATH.
- scripts/pre_push_check.py passed inline flake8 ignores that duplicated
.flake8's policy with a drifted list and skipped scripts/ entirely, so
it could pass while CI failed. It now runs exactly what CI runs.
docs/testing_guidelines.md documented @pytest.mark.kubernetes, .ssh and
.flaky. None are registered, --strict-markers is on, and no test uses
them, so following the guide produced a hard collection error; .flaky
additionally needs a plugin the project does not depend on. Replaced
with the markers that exist.
Quickstart Step 3: the `for i in range(50_000): pass` loop is
load-bearing -- it is what the analyser splits -- and read as a decoy.
Now explained. The chunk count is os.cpu_count()-dependent and was
stated flat. And whether that step runs locally at all depends on
cluster_host, not cluster_type, so a config file with a host silently
sends it down the remote path; that is now called out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ck scopes The AWS cleanup test assertions were widened by hand and not re-run through black. Quick Checks caught it. fast_ci checked black on clustrix/ tests/ while tests.yml checked clustrix/ tests/ scripts/, so a file under scripts/ could pass one gate and fail the other. Same scope now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
docs-test burned its entire 10-minute cap on "Get:5 noble-security InRelease" and was cancelled -- twice, despite the Acquire::*::Timeout options added for exactly this. Those options bound individual fetches; they did not bound the step. pandoc is genuinely required: nbsphinx renders 30 notebook pages with it, so removing the dependency would silently drop them from the built documentation. Instead, try the install against the runner image's existing package lists first and only refresh them if that fails, under a hard wall-clock bound apt cannot ignore. The step now ends with 'pandoc --version', so if pandoc is still absent the job fails there rather than letting sphinx build notebook-less docs that look fine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Every matrix job failed with 7 tests that pass locally.
Six were `HfHubHTTPError.__init__() missing 1 required keyword-only
argument: 'response'`. Current huggingface_hub makes `response` required
and keyword-only; the version I had locally (0.36.0) still allowed
`HfHubHTTPError("msg")`. Raising the floor to >=0.34.0 earlier in this
branch is what let CI resolve a newer release than my machine had. The
tests now pass a real requests.Response, which is correct on both old and
new versions and is what the library does itself.
The seventh was `ModuleNotFoundError: No module named 'sklearn'`.
tests/test_decorator_real.py::test_machine_learning_workflow trains a
real model -- a good test -- and passed locally only because this
developer environment happens to have scikit-learn installed. Nothing
declared it. Added to the dev extra in both pyproject.toml and setup.py,
which are verified still identical field by field.
That is the same class of defect as the missing azure-mgmt-authorization
found earlier: a dependency that exists on someone's machine and nowhere
in the metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…emon threads
Every CI job printed its pytest summary and then sat there until the
fifteen-minute cap killed it -- 5m41s of dead time on ubuntu/3.11, and the
same on all eight matrix jobs.
Root cause: tests/comprehensive/test_edge_cases_real.py::test_deadlock_prevention
builds a deliberate AB-BA lock inversion in two *non-daemon* threads, joins
them with timeout=5, and abandons them when (as designed) they deadlock.
Py_FinalizeEx calls threading._shutdown(), which joins every surviving
non-daemon thread with no timeout, so the interpreter could never finalize.
The test is also re-run by reflection from test_comprehensive_edge_case_suite,
so four threads were wedged, not two.
Evidence, from a faulthandler dump armed at pytest_unconfigure:
AT-UNCONFIGURE non-daemon alive: 4
ALIVE Thread-6 target=...potential_deadlock.<locals>.worker1
ALIVE Thread-7 target=...potential_deadlock.<locals>.worker2
ALIVE Thread-8 target=...potential_deadlock.<locals>.worker1
ALIVE Thread-9 target=...potential_deadlock.<locals>.worker2
Thread 0x33da2b000: test_edge_cases_real.py line 635 in worker2
Thread 0x33ca1f000: test_edge_cases_real.py line 629 in worker1
Thread 0x33ba13000: test_edge_cases_real.py line 635 in worker2
Thread 0x33aa07000: test_edge_cases_real.py line 629 in worker1
Thread 0x1fbd91d80: threading.py line 1477 in _shutdown
Marking those two threads daemon changes nothing the test observes -- the
deadlock still happens, the joins still time out, is_alive() is still True and
the assertions are untouched -- but abandoning them no longer wedges the
process.
Same defect, second site: tests/real_world/conftest.py::_within claimed to
abandon its worker, but a ThreadPoolExecutor's workers are non-daemon and
concurrent.futures joins all of them, untimed, at interpreter exit even after
shutdown(wait=False). Abandoning a seventy-second DNS lookup only moved the
wait to process exit. Replaced with an actual daemon thread, preserving the
existing semantics (OSError -> None, timeout -> None, anything else re-raised).
Isolating measurement, tests/comprehensive/test_edge_cases_real.py alone:
before: summary at 59.98s, still not exited 90s later
after: summary at 62.60s, exited 0.66s later
Full CI selection:
before: [345.05] 1764 passed ... -- never exited, killed after 6 minutes
after: [343.58] 1764 passed ... / [345.64] EXITED rc=0
SimpleAsyncClusterExecutor owns a four-worker ThreadPoolExecutor and exposes shutdown(), but nothing ever called it, and decorator.py built a fresh executor on every async submission. Five async calls created five pools: BEFORE: distinct thread pools alive: 1 ['ThreadPoolExecutor-4'] AFTER: distinct thread pools alive: 1 ['ThreadPoolExecutor-0'] The name index is the tell: -4 means five pools had been constructed and the earlier four garbage-collected. So this was churn rather than an unbounded leak -- worth stating accurately -- but four threads were being started and torn down per submission for no reason. The pool cannot be closed at the end of the call, because the submitted work outlives it; that is the point of async submission. So the lifetime is the process, and one executor is shared. The class also gains __enter__/__exit__ so callers who *can* bound the lifetime are able to. Caching it introduced exactly the shared-state problem this branch has already fixed twice, and two decorator tests caught it immediately: with the executor cached, a later test received the instance an earlier test had built from a patched class. tests/conftest.py resets it between tests, alongside the config singleton and the credential manager. Found while root-causing the CI hang; not the cause of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…ailures test_single_venv_program_refuses_to_fall_back_to_stdlib_pickle hides dill and cloudpickle from a child interpreter to prove the generated worker refuses rather than feeding dill bytes to stdlib pickle. It did that with a meta_path finder implementing find_module/load_module -- the legacy API Python 3.12 removed. On 3.12 the blocker is ignored entirely, the child imports dill normally, the program succeeds and the assertion that it should have failed fires. So the test was passing vacuously on 3.10 and 3.11 (nothing proved that the blocker worked) and failing on 3.12 for a reason unrelated to what it tests. Rewritten with find_spec, and verified the blocker now actually blocks: import dill under blocker -> rc 1 | ImportError: dill Also set fail-fast: false on the test matrix. One job failing cancelled the other six, so a 3.12-only problem presented as seven broken jobs and hid whatever the other combinations would have said. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
Windows reached 96% of the suite and was cancelled at the 15-minute cap, still making progress -- it was not hung. The cap dates from when this job ran only tests/unit, about 350 tests. It now runs the whole non-billable suite, roughly 1790 (issue #113). Measured on this branch: ubuntu 9m39s-10m46s, macOS 12m56s-13m48s, Windows over 15. The process-spawning tests are the difference -- the serialization round-trips each start a fresh interpreter, which is much more expensive on Windows. 30 minutes bounds a job that legitimately takes longer. It does not relax anything: every test still has to pass, and pytest's own --timeout=120 still bounds any individual test that wedges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
The published widget screenshots were captured by hand in a live notebook, so they carried, legibly, in images linked from README and the documentation index: - the real cluster hostname - the real username - the author's home directory, /Users/<name>/clustrix - the SSH private key path - a real cluster's conda path under /optnfs - a HuggingFace organisation name Regenerated from placeholders: hpc.example.edu, researcher, ~/.clustrix/jobs, your-org. scripts/render_widget_screenshots.py renders the real widget to standalone HTML with those values -- pointing CLUSTRIX_CONFIG_DIR at a throwaway directory first, so the developer's own profile store cannot leak in through the profile name either -- and inlines the stylesheet the widget normally publishes separately, so the page looks the way it does in a notebook. 01-before-light.jpg is deleted rather than regenerated. It showed the pre-rewrite widget, which no longer exists, so there is nothing to re-render; it leaked a home path; and nothing referenced it. The older PNG set under _static/img/screenshots/ was checked image by image and is already placeholder-only (login.hpc.university.edu, your_username, your-gcp-project-id). Left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
clustrix/validation.py hardcoded two real hostnames in a TEST_CLUSTERS list that shipped inside the package. That named someone's actual infrastructure to everyone who installed clustrix, and was useless to them besides -- the validation pass could only ever check machines they did not have access to. The targets now come from CLUSTRIX_VALIDATION_SSH_HOST and CLUSTRIX_VALIDATION_SLURM_HOST. With neither set it says so and checks nothing, rather than attempting to connect to hosts the user does not own: No validation clusters configured. Set CLUSTRIX_VALIDATION_SSH_HOST and/or CLUSTRIX_VALIDATION_SLURM_HOST to run this against your own cluster; nothing will be checked otherwise. That function also referenced a logger the module never defined, so the warning would have raised NameError; added. Also removed: a real hostname used as a widget placeholder, two code comments naming real machines, and a README row naming the SLURM cluster a verified run happened on. The claim is unchanged -- it ran on a real production SLURM cluster -- only the identifier is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
…cs/notes/scripts Replaces real Dartmouth cluster hostnames (discovery/ndoli/tensor01/tensor02 .dartmouth.edu and worker nodes), the real SSH username (f002d6b), and local home paths (/Users/jmanning) with consistent fake placeholders across docs/, notes/, and scripts/. The two evidence transcripts (docs/evidence/*.txt) keep their real results/timings/output verbatim -- only identifying strings were substituted, and a header now documents that. scripts/verify_cluster_usecases.py and scripts/collect_execution_evidence.py no longer hardcode any cluster hostname or username; both now read CLUSTRIX_TEST_SLURM_HOST(_2), CLUSTRIX_TEST_SSH_HOST(_2), and CLUSTRIX_TEST_USERNAME from the environment and fail with a clear message when unset. CONTRIBUTORS.md was left untouched (real third-party emails; needs explicit sign-off before editing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
tests/ named one developer's actual infrastructure in 86 files: the
hostnames discovery/ndoli/tensor01/tensor02.dartmouth.edu and worker
nodes, the SSH account f002d6b, the institution's /dartfs-hpc home
layout, and absolute /Users/jmanning paths. The repository is public,
so all of it was published.
Targets now resolve in ONE place, tests/real_world/credential_manager.py,
from the same environment variables scripts/ already reads:
CLUSTRIX_TEST_SSH_HOST CLUSTRIX_TEST_SSH_HOST_2
CLUSTRIX_TEST_SLURM_HOST CLUSTRIX_TEST_SLURM_HOST_2
CLUSTRIX_TEST_USERNAME
CLUSTRIX_TEST_SLURM_REMOTE_DIR
Tests ask for a role ("ssh", "slurm"), never a hostname, via
get_test_host / require_test_host / require_test_username /
require_test_remote_work_dir. The require_* helpers skip with the
variable name in the reason, so a run that could not find its target
says so instead of failing or quietly passing. No assertion was
weakened and no test was deleted.
The network gate is rebuilt on the same footing: is_dartmouth_network()
becomes can_reach_configured_cluster(), which resolves the configured
hosts under one shared 3s budget (previously one budget per host) and
returns False immediately when nothing is configured. The marker
dartmouth_network becomes cluster_network, registered in
tests/conftest.py so it is declared in every run -- tests/real_world's
own conftest is not loaded when that directory is excluded, which is
the documented CI-safe command.
Files and identifiers named after the machines are renamed by role:
tensor01 -> gpu_cluster, ndoli -> slurm_cluster. Values that were only
ever fixtures now look like fixtures: hpc.example.edu, hpc2.example.edu,
testuser, /remote/home/testuser -- matching the placeholders used in
docs/ and scripts/.
tests/audit_results.json was a 245KB generated report from
audit_antipatterns.py, stale and full of real hostnames; it is removed
and gitignored, and the script that writes it no longer hardcodes an
absolute home path.
One behavioural consequence worth stating: the old gate opened whenever
socket.gethostname() contained ".dartmouth.edu", which on the author's
machine let 14 tests in tests/test_real_world_gpu_functionality.py run
against hostnames that do not exist. Twelve of them passed anyway --
they never reached a cluster. They now skip with a reason, which is why
the suite reports 1753 passed / 23 skipped where it used to report 1764
passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
pyproject.toml still registered a pytest marker named after a specific university's campus network. tests/conftest.py now registers `cluster_network` in its place, so the old registration is deleted rather than renamed -- verified the new marker resolves under --strict-markers (10 of 397 real-world tests select on it). The remaining references were in prose: MIGRATION.md, two docs pages and four notes files describing the old `is_dartmouth_network()` gate, plus `/dartfs-hpc` storage paths. Renamed to match the code as it now stands (`can_reach_configured_cluster`, `cluster_network`) so the docs describe functions that exist, and generic storage paths. The working tree is now free of the real cluster names, username, home paths and account handles. CONTRIBUTORS.md is deliberately untouched -- it carries contributors' email addresses including a third party's, and removing someone's credit is the maintainer's call, not mine. This does NOT address the published git history, which still contains all of it across 29 commits going back to 2025-07-01, on a public repo with 3 forks. That needs a history rewrite and a force-push, which is destructive and outward-facing, so it is flagged for a decision rather than done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
`_write_config_file_securely` called `os.fchmod`, which does not exist on Windows before Python 3.13. It raised AttributeError *after* `os.open` returned a descriptor and *before* `os.fdopen` took ownership of it, so every call leaked the descriptor. That is a genuine cross-platform defect -- POSIX just hides it, while Windows refuses to delete a file with an open handle and surfaced it as `PermissionError: [WinError 32]` in four teardowns. On Windows this also meant `save_to_file`/`save_config` never worked at all. Code fixes (affect real users, not just tests): * clustrix/config.py: guard the fchmod hardening with hasattr and close the descriptor on any failure between open and fdopen. * clustrix/config.py: `_load_default_config` no longer lets a RuntimeError from `Path.home()` (undeterminable home directory) abort `import clustrix`. * clustrix/executor_cloud.py: the job directory is interpolated into generated *Python* source, so quote it with repr() -- bare quotes let an embedded quote close the literal, and a backslash become an escape. * clustrix/credential_manager.py, clustrix/cli_credentials.py: read and write `.env` as UTF-8. The template contains non-ASCII, so on Windows the write died mid-encode and left a zero-byte credential file behind. POSIX-only properties, skipped on Windows with a reason naming the property, and documented in docs/source/limitations.rst (new section: config and credential files are NOT permission-restricted on Windows, with mitigations): * 0600/0644 file-mode assertions (NTFS uses ACLs; chmod only toggles read-only). * Shell-quoting tests that verify quoting by running a real POSIX shell. * Tests whose payload uses fcntl.flock or signal.SIGALRM. Test-harness fixes (portable, no assertion weakened on Linux/macOS): * Read repo source files as UTF-8 rather than the locale codec. * Point USERPROFILE/HOMEDRIVE/HOMEPATH, not just HOME, when redirecting the home directory; Windows expands ~ from those. * Parse the emulated `cat` command with shlex.split instead of splitting on whitespace and stripping quote characters. * Use a POSIX remote_work_dir in the integration test (it goes into a bash job script, and the validator rightly refuses a `C:\...` path). * Measure elapsed time with perf_counter; the Windows wall clock ticks in ~15.6 ms steps and reported 0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
The Windows wall clock ticks in ~15.6 ms steps, so the data-processing workflow's elapsed time measured as exactly 0.0 and `processing_time > 0` failed. Only the Python 3.12 Windows job hit it; 3.11 was a coin flip on the same clock. Same fix as tests/test_decorator_real.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU
This was referenced Aug 19, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A sweep of every open issue, plus a documentation overhaul, plus the defects
that turned up while doing both. 52 commits.
The headline is not the issue count. It is that three separate code paths
were returning fabricated answers instead of running your function, all
reachable by default, and none of them detectable by the test suite as it
stood.
The fabricated-result bugs
create_simple_subprocess_fallback"Function execution completed"_attempt_client_side_gpu_parallelizationdetect_loopsrange fallbackEach was silent — no error, no warning, a plausible-looking value.
1. The subprocess stub. When a function was classed "complex" and flattening
failed,
_execute_singlesubstituted a closure whose entire remote body wasresult = "Function execution completed". Reproduced:analyze_function_complexityreturned999 / is_complex=Truewhenever it couldnot read the source — so flattening was attempted precisely when it could not
work, and any function with one nested helper qualified.
2. GPU parallelization.
auto_gpu_paralleldefaulted toTrueand triggeredon any host reporting ≥2 GPUs. The path never called the decorated function: it
ran a fixed
torch.randn(100, 100)program per GPU and scraped the matrix traceout of stdout.
3. Loop ranges.
detect_loopsfell back torange(10)when it could notevaluate a range, so a loop over
range(n)was chunked as ten iterations:It reached that value by calling
eval()on text sliced out of your source,under a comment admitting the approach was dangerous. Now a literal-only reader
that cannot execute anything.
All three are deleted rather than patched, along with the machinery that existed
only to serve them:
function_flattening.py,dependency_resolution.pyandgpu_utils.py(2,859 lines). Nothing is lost —serialize_functionalreadyround-trips every case flattening was meant to rescue, verified in a fresh
interpreter with the defining module off
sys.path.Security
Remote-to-local code execution. Results were HMAC-verified before
deserialization;
error.pklwas not, and noerror.pkl.hmacexisted anywhere. Ahostile cluster only had to make the job fail. Demonstrated with a payload
whose
__reduce__calledos.system; both read sites were wrapped inexcept Exception: pass, so a failed exploit was silent. After:Also: verification failed open when no key was recorded; cloud results were
never verified at all; SSH host keys were never checked (
AutoAddPolicyat 12sites); the job script had no
shlex.quoteanywhere, soremote_work_dir,module loads and environment variables reached the shell unquoted — two of those
sites inside a
python -c "string. Saved configs were world-readable withcredentials in plaintext, and
ClusterConfig.__repr__printed them into anytraceback.
Correctness
freeze line containing
@was skipped; withuvonPATHevery conda-builtpackage renders as
name @ file:///.... Measured: 563 installed, 376 returned.Now 559, with the six genuinely unreproducible ones (editable installs, git
checkouts) refused at submit time by name rather than dropped.
total += icame back with zero dependencies.
detect_loops_in_functionnever dedented itssource, so every method or closure raised
IndentationErrorinto a swallowedexception and returned
[].callee could not accept and swallowed the
TypeError. The remote path had thesame defect.
ast.literal_evalon the pod log.subclasses, namespace packages and
functools.partial, and degraded silentlyto by-reference at its node cap.
The test suite
The last 13 failures were test pollution, not product bugs — every one passed
in isolation. Three nested leaks:
reset_configrestored 8 of a hundred-oddfields; it restored fields but not the module binding; and a class-local
reset_configfixture shadowed the autouse one. After that file ran, the liveconfig still carried 20 drifted fields, and the widget reads the live config.
A safety hole worth naming: most files under
tests/real_world/carried noreal_worldmarker, so the command CLAUDE.md documented as safe —pytest tests/ -m "not real_world"— executed them, making real SSH and cloudcalls. 26 tests were leaking. The marker is now applied by path.
Documentation
Rewritten, then reviewed adversarially five times until the reviews came back
clean. New:
introduction.rst,quickstart.rst,execution_model.rst,configuration.rst,limitations.rst,troubleshooting.rst.The checker had a hand-written file list, so the pages written that day were not
checked at all. It now discovers everything Sphinx publishes. Making that work
required fixing three defects in the checker itself: an example calling
sys.exit()killed it at the third of twenty files and exited 0; an exampledialling a fictitious host hung it indefinitely (paramiko retries through
EINTR, so the in-process alarm was ignored — each file now runs in its ownsubprocess); and it demanded every third-party import be installed, so an ML
example importing tensorflow counted as broken docs.
The reviews found what the tooling structurally cannot: the checker runs
blocks, it never compares a comment to what the code did.
# Loop gets parallelized automaticallyappeared in README, CLAUDE.md, three API pages andfour notebooks — false in every case, and every block passed.
docs/notebooks/turned out to be a stale duplicate tree that five "Open In Colab" badges pointed
into, so a reader clicking through landed in an un-overhauled copy.
What is still not verified
against a real scheduler.
end to end.
install page now say so and give the git install command.
Reproducing
🤖 Generated with Claude Code
https://claude.ai/code/session_012gTBDPK16HUZ3kHQ2QyjuU