Ask the runner. Authorize the submission. Record the agreement on each job.
John Chilton
The problem · issue #23261
A saved checkbox can speak for the next runner
Author checks a box
→
Workflow saves it
→
Runner never sees it
Reruns can replay that parameter too. The value survives; the runner’s affirmation is missing.
Make the agreement an execution requirement, outside the saved tool inputs.
Tool authoring
Declare terms beside requirements
<tool ... profile="27.0">
<requirements>
<license_agreement id="license_agreement_path" version="1"
path="license_agreement_tool_license.txt">
<label>Test Tool Non-Commercial License</label>
<affirmation>
I certify that I am not using this tool for commercial purposes.
</affirmation>
</license_agreement>
</requirements>
</tool>
An adjacent UTF-8 terms file; inline <text> also works. Profile 27.0+, login required, every declared agreement required.
Abbreviated test fixture · submission binding is the default.
Architecture
The server authorizes job creation
%%{init: {'theme':'base','flowchart':{'nodeSpacing':20,'rankSpacing':26,'padding':12},'themeVariables':{'fontFamily':'Atkinson Hyperlegible','primaryColor':'#25537b','primaryTextColor':'#ffffff','primaryBorderColor':'#2c3143','lineColor':'#58585a','fontSize':'18px','clusterBkg':'#f6f8fa','clusterBorder':'#dfe3e8'}}}%%
flowchart LR
subgraph LOAD["Tool loading and discovery"]
direction TB
A["Tool XML + terms file"] --> B["ResolvedLicenseAgreement"]
B --> C["Discovery API + run forms"]
end
subgraph RUN["Submission and job creation"]
direction TB
D["ToolExecutionContext"] --> E["ToolAccessPrecondition<br/>LicenseAcceptancePrecondition"]
E --> F["LicenseAcceptanceManager<br/>stored terms + user events"]
F --> G["Job +<br/>JobLicenseAcceptanceAssociation"]
end
LOAD -->|"affirmed hashes"| RUN
Access checks run first. A disabled Run button is the early feedback.
Identity and lifetime
Same terms. Explicit scope.
canonical affirmation + terms→versioned SHA-256
binds=“submission”
Default. Affirm for this work. One submission can create many jobs. A new rerun needs fresh authorization.
binds=“user”
May be remembered for this account. Acceptance stays valid until revoked. A one-time affirmation also works.
Changed terms or affirmation → new hash. Same hash with mixed scopes → submission binding wins.
Walkthrough · license_agreement_path_tool
The runner sees the terms
Review → affirm → submit the declared hash. The job records one_time; the next submission prompts again.
Workflow execution
One prompt can authorize a nested, delayed run
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Atkinson Hyperlegible','primaryColor':'#25537b','primaryTextColor':'#ffffff','primaryBorderColor':'#2c3143','lineColor':'#58585a','fontSize':'18px'}}}%%
flowchart LR
A["Walk every tool step<br/>including subworkflows"] --> B["Deduplicate hashes<br/>one prompt per agreement"]
B --> C["Validate request<br/>pin on each invocation"]
C --> D["Schedule step<br/>check and record on job"]
Two nested tool steps share a hash: one prompt. One-time pins survive restarts; remembered acceptance is rechecked.
Revocation blocks later jobs relying on remembered acceptance. Jobs already created keep running.
POST /api/tools and workflow invocations refuse immediately. Forms refresh the agreements when a submission is refused.
POST /api/jobs is asynchronous: the tool request can be accepted, then fail in Celery with the same structured reason.
Persistence · four new tables
Keep the terms and the authorization
Terms + account events
ToolLicenseAgreement Canonical terms by hash. ToolLicenseAcceptanceEvent Append-only accept / revoke history.
Job + invocation associations
Each job: hash, authorization kind, and an event link when persistent. Each invocation: its one-time pins.
Purging an account removes its personal acceptance events. Retained jobs keep the hash and authorization kind.
The database records job authorization; a public job audit API/UI is still future work.
Implementation snapshot · cd37d851bb2
The next proof is a real wrapper
Implemented: parsing, storage, API enforcement, nested workflows, run forms, and preferences.
Recorded local checks cover backend, API, PostgreSQL cleanup, and browser paths. No production wrapper has migrated.
Pilot one unconditional wrapper with its maintainers and an operator reviewing the terms.
Decide the workflow-test default before Planemo support. Conditional terms and alternative entitlements need a later design.
Extra Slides
Remembered acceptance
Users can review and revoke
For user-bound agreements only. Revocation affects future authorization.
Scope boundaries
Every declaration applies to every run
Supported
Unconditional agreements, combined with AND. Shared suite terms, deduplicated by hash. Direct execution of a licensed converter.
Needs another design
Input-dependent terms. Academic use OR a purchased license. Instance-wide administrator acceptance.
Data sources and actions that bypass preconditions fail to load. Converters with agreements are excluded from automatic conversion.
Behavior pinned by tests
The job’s lifetime defines the boundary
Fresh authorization
New rerun or remap. Later persistently authorized workflow steps. A tool reloaded with changed terms.
Existing authorization
Resume a paused job. Automatic resubmission of that job. A delayed step using a matching one-time pin.
Workflow preflight includes when-skipped steps. Local results are recorded in the branch debrief; full suites remain a CI concern.
API integration reference
Discover, display, then submit
GET /api/tools/{id}/license_agreements
GET /api/workflows/{id}/license_agreements
Show terms + affirmation to the runner.
POST /api/tools | /api/jobs | workflow invocation
one_time_license_acceptances: [declared hash]
Optional for binds="user":
POST /api/users/current/license_acceptances
tool_id + license_id + agreement_hash
Never accept client-supplied terms. Reject undeclared hashes. Persistent acceptance must confirm the exact hash displayed.