Skip to content

feat: release job outputs and logs through one path - #9604

Merged
koenvanderveen merged 25 commits into
devfrom
pjwerneck/unify-job-results
Nov 12, 2026
Merged

koenvanderveen merged 25 commits into
devfrom
pjwerneck/unify-job-results

Conversation

@pjwerneck

Copy link
Copy Markdown
Collaborator

Summary

A DO shared job results two ways: grant read in place (share_outputs, share_logs), or move files from staging and grant (release_logs, release_disclosures). This PR leaves one path: outputs wait in staging/ like logs, and JobInfo.release(filenames, recipients) moves them to review/ and grants read access.

Changes

  • Outputs go to staging/ and reach a reader only through a release. share_outputs, share_logs and _release_to_submitter are removed.
  • JobInfo.release with no recipients moves nothing, so blocked outputs stay in staging/.
  • The runner and the enclave both release through JobInfo.release. The enclave now grants read on the disclosures it releases.
  • Private names say release and recipients (_release_job_results, _release_outputs, _disclosure_recipients). The public share_job_results and its flags are unchanged.
  • A job could write a syft.pub.yaml into its outputs and give itself readers, past the output policy and share_outputs_with_submitter=False. The job now fails, and only the DO sees the reason.
  • A rerun kept the read grant on the old outputs, so earlier recipients could read the outputs of the new run. Removing the outputs now removes the grant too.
  • The enclave sent outputs twice: through the read grant and the sync, and through its own push. It now pushes only the job state, which the DOs cannot read through a grant.

Testing

  • New regression tests in test_staging.py, test_policies.py and test_disclosures.py; each fails without its fix.
  • syft-job (167), syft-enclave (302), syft-rds (156) and root unit (724) pass.

Asana task

https://app.asana.com/1/1185126988600652/project/1210542925864934/task/1220052874945931?focus=true

The runner moves the outputs to staging/, like the logs. A reader gets them
only through JobInfo.release(filenames, readers), which replaces
_release_to_submitter, share_outputs and share_logs. release also grants the
files that an earlier release moved, so a later share reaches the DS after an
output policy released the outputs to other readers. The enclave releases the
outputs before it forwards them, and writes the receipt into the staged
outputs.
The enclave releases disclosures to the submitter and the DOs, and pushes them
to the same readers, so each party that gets a file can read it. Outputs go
out through _release_results, which releases them and then pushes them to the
same readers. A release with no readers moves nothing, so blocked outputs stay
in staging/. The DO tutorial points to share_job_results instead of the
removed share_outputs.
release_artifacts becomes the private _move_to_review, so release is the only
public way to release a file. The private share and forward methods become
_release_job_results, _release_results_once, _release_outputs and
_push_new_disclosures, and the people who get a file are recipients
throughout. JobStorage.remove_outputs replaces the two loops that deleted the
outputs of an earlier run.
A syft.pub.yaml in the staged outputs decided who reads them, so a job could
release its own outputs past the output policy. The runner now fails such a
job, shares its outputs with nobody, and records the reason in staging/ for
the DO only. Each run also drops the recipients and the outputs error of the
run before.

shutil.move kept the job's symlinks, so a link into the run copy broke when the
run copy was removed, an absolute link stayed live in the SyftBox, and a
symlinked outputs folder made rerun fail. The outputs are now copied with the
links followed, as the runner did before outputs were staged, and the copy
skips the permission files.
remove_outputs also removes the read grant on the outputs, so the outputs of
a rerun reach only the recipients of the new release.

release() builds no permission context when nothing is released, and the
enclave releases a job only while a granted item is still staged, so a tick
with nothing new does no permission work.

The enclave pushes only the job state. The read grant and the sync already
deliver released outputs and disclosures, so the push sent them twice;
forwarded_artifacts.json goes with it.

The runner docstrings say outputs wait in staging/, and the rerun test now
checks that the released outputs are removed.
The rebase on dev kept an import of syft.sync.transport.protocols, which dev
renamed to syft.sync.connections.protocols, so syft_enclaves.client failed to
import.
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

Comment thread packages/syft-job/src/syft_job/job.py Resolved
Comment thread packages/syft-job/src/syft_job/job.py Resolved
Comment thread packages/syft-job/tests/test_sharing.py Resolved
share({name: recipients}) replaces release(filenames, recipients).
Therefore each file can have its own recipients. share_outputs, share_logs
and share_disclosures replace release_logs and release_disclosures. The
runner and enclave methods that call them use "share" in their names, not
"release".
The runner writes the outputs, the logs, the exit code and the policy files
into the review folder of the job. It gives nobody read access to these
files. JobInfo.share only gives read access to a file. Therefore the commit
removes staging/. The owner record and the api-request build folder move to
review/.

reset_run deletes the files of the last run and removes all read access to
them. JobInfo.rerun and the runner call it. Therefore a new run does not keep
the readers of the old run.

The enclave shares the granted items of each finished job on every tick.
When the runner copies the outputs to review/, it skips each syft.pub.yaml.
The runner ignores case when it compares the names. It logs the skipped
names for the DO. The job no longer fails. Therefore a job that copies a
folder with a permission file in it still shares its outputs.

If SYFT_JOB_ALLOW_OUTPUT_PERMISSION_FILES is true, the runner keeps the
files. SyftJobConfig reads this variable once. It raises an error for any
value other than true or false.
When the enclave distributes a job, it gives each DO of the job read access
to state.yaml. The sync then sends each status change to the DOs. The
enclave no longer pushes state.yaml with the outputs.

A permission rule for one file overrides the folder rule that lets the
submitter read state.yaml. Therefore the rule for state.yaml also names the
submitter.
syft-perms used each path as a rule pattern. The matcher reads *, ?, [ and
\ as glob syntax, and {{ as the start of a template. Therefore a job named *
got a grant that matched the files of every other job. A job named
{{.Year}} made the grant fail.

Each of these characters now goes into a character class of its own.
Therefore a pattern matches only its path. copy_permissions escapes the file
name too. remove_permission_for_user still finds its rule through the path,
not through the pattern.
PermissionModifier.add_permission_for_pattern adds a user to the rule for
a glob pattern in the permission file of a folder. Unlike
add_permission_for_user, it does not escape the pattern, so one rule can
cover files that do not exist yet, such as the state of every job.
grant_ds_review_read becomes grant_ds_state_and_disclosures_read, which
says what it grants. It writes each pattern through
add_permission_for_pattern instead of editing the rule set by hand.

The patterns are written out instead of generated from the codecs. A test
runs once per protocol version, so a new protocol without its patterns
fails.

Setting up a DS folder no longer removes the old read grant on the whole
folder. The runner already does that before any job runs, through
replace_ds_folder_read.
… datasite

Each syft-perms write reloaded every permission file in the datasite. Now
the modifier puts the saved rule set in the context's tree, which is what
the reload did for each file. remove_all_rules now updates the tree too,
so the same context no longer reports a removed grant.
requested_disclosures becomes requested_read_permissions, the data owner's
grant becomes approved_read_permissions (approve(read_permissions=...)),
and granted_disclosures becomes read_permissions_to_share. The module,
the item enum, the helpers, the enclave and api fields, the syft-bg config
key and the files on disk are renamed the same way, with no fallback for
the old names.
Read permissions are now {file in the job's review folder: [emails]}, and
both live in state.yaml (JobStateV2, job protocol 3). The data owner copies
the valid part of the submitter's request from config.yaml at receipt, and
sets the approved part at approval, within the request. Only the data
owner writes state.yaml, so the submitter can never approve itself.
approved_read_permissions.json and the item names (logs, traceback_frames,
return_code) are gone; the DS reads only state.yaml before a share.

An api lists the run files a caller may read, and a call's job requests and
gets them for the caller, so api jobs now share them. The email approval
approves the requested files that its config lists. In the enclave a file
goes only to the emails that the submitter requested and every party
approved, and the enclave reads and writes the state through the job
storage, so older peers get a state they can read.
A submitter asks with share_logs and share_outputs, or with a share_files
dict, not both. The job client builds the requested read permissions from
them; "outputs" is now a file a read permission can name. An enclave
submit also takes share_logs_with_owners and share_outputs_with_owners
for every data owner of the job; share_results_with_do is gone.

approve() takes read_permissions or share_requested_files, not both, and
builds the approved permissions from them and the request. For enclaves
share_requested_files is the default. The runner takes
share_requested_files too. Approved outputs still go only to readers the
output policy allows, and in the enclave only with the receipt.
The output policy now gets read_permissions.json, filled with the job's
approved read permissions from the api, instead of recipients.json. It may
narrow or widen them; the result, with only the submitter and approved
peers as readers, becomes the job's approved read permissions. A deny
approves none, and a failed job shares no outputs. An api call's outputs
follow its read permissions, not share_outputs_with_submitter, and a new
api lets callers read the outputs unless it lists other files.

Read permissions no longer limit the file names to a fixed list. The data
owner drops request entries that leave the job's folder when it receives
the job; the submitter's side only checks the shape.
- Rename the share flags to share_logs_with_submitter /
  share_outputs_with_submitter, add share_job_results, and pass approvals
  as share_files.
- Remove traceback frame capture (traceback_frames.json).
- Add request_stderr / request_stdout to the enclave client. Each data
  owner writes <do_email>_requested_permissions_update.json in the job's
  review folder; the enclave shares a log, or its first n_chars, once
  every data owner made the same request.
- Share all of a finished job's results in distribute_results on every
  tick, so later approvals (outputs included) reach their readers. The
  receipt is still written once.
- Show stdout of failed jobs, and fall back to a shared log head when
  the full log is not there.
A key must be a plain path inside the job's folder: "." (the whole
folder) and second names such as "./stderr.txt" or "stderr.txt/" are
refused, so the logs warning and other checks on file names cannot be
bypassed. The logs warning now also covers a folder that holds a log.
On the DS side, share_files names are tidied before the check, so
"./stderr.txt" becomes "stderr.txt".
A failed api call (error exit, crash or timeout) never runs its output
policy, so the files the api allows went out unchecked. The runner now
skips the share for it and leaves the approval as the api set it.
@koenvanderveen
koenvanderveen merged commit ca66134 into dev Nov 12, 2026
21 checks passed
@koenvanderveen
koenvanderveen deleted the pjwerneck/unify-job-results branch November 12, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants