feat(model): enforce resolved-value checks on carried-forward fields in create_job - #404
Conversation
…in create_job
Of the spec's three processing stages (Template Schemas 7.4: template
validation, job creation, task execution on the worker host), job
creation is the first at which job parameters have real values.
create_job now statically evaluates the carried-forward
session/task-scope format strings - action command/args, environment
variables values, embedded-file data - against check symbol tables
with the parameters bound, and re-runs exactly the resolved-value
checks template validation (pass 8) applies to those fields. A
violation that depends only on parameter values (e.g.
args: ["{{ 'A' * Param.N }}"] with a huge N) now fails at submission
instead of on every worker; task execution remains the enforcement
boundary.
- format_strings.rs: pub(crate) check_carried_forward_step_script /
check_carried_forward_environment walkers; FsEval::for_job_creation
reports only budget exceedances as evaluation errors (create_job may
run under a deliberately different profile; other evaluation errors
are context artifacts and the worker enforces regardless).
- instantiate.rs: build_task_check_symtab (refactored from the script
let-binding type-check block, now always built),
build_env_check_symtab, shared add_unresolved_session_symbols.
- Evaluation budgets (CallerLimits::max_eval_memory_bytes /
max_eval_operations) now apply to every evaluation job creation
performs - job name, let bindings, host requirements, task ranges,
and the new carried-forward checks - closing the "job creation
evaluates under the spec defaults" gap.
- 20 new integration tests asserting full field paths + messages;
stale CallerLimits doc comments updated.
- specs: job-creation.md gains the Resolved-Value Checks on
Carried-Forward Fields section (and current
create_job/evaluate_let_bindings signatures); validation.md and
public-api.md updated to match.
Verified: clippy -D warnings clean; cargo test --workspace green
except two pre-existing environment-specific Windows cross-user logon
failures that reproduce on unmodified main; conformance suite
1133/1133 on Windows.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
e360eb5 to
55529fb
Compare
Where the new enforcement sits in the create_job flowReviewed at This PR fills the one empty cell in the enforcement ledger. Of the spec's three processing stages (Template Schemas §7.4), the S3 row previously read "does not check: anything task or host scoped — The mechanism is a fourth and fifth symbol table. The PATH exclusion is worth calling out as correct rather than incidental: the job-creation symbol table holds real values for non-PATH parameters only, because path mapping is a host operation, so backfilling them as Flowflowchart TD
subgraph S1["S1 validate (pass 8) — values unknown"]
P8["validate_format_strings<br/>FsEval::new, report_eval_errors=true"]
P8G["gate: step.script ONLY<br/>format_strings.rs:1627"]
P8 --> P8G
end
subgraph S3["S3 create_job — job parameters bound (NEW)"]
CJ["create_job — mod.rs:86"]
BUD["EvalBudgets::from_ctx — mod.rs:105"]
IS["instantiate_step — instantiate.rs:20"]
SUG["script_template = resolve_syntax_sugar<br/>instantiate.rs:78"]
TCS["build_task_check_symtab — :263"]
UNRES["add_unresolved_session_symbols<br/>Session.*, PATH Param.* — :215"]
CHK1["check_carried_forward_step_script<br/>format_strings.rs:1054"]
IR1["into_result — ABORTS — :129"]
CHK2["stepEnvironments →<br/>check_carried_forward_environment :1098"]
CHK3["jobEnvironments → same — mod.rs:219"]
CJ --> BUD --> IS --> SUG --> TCS --> UNRES --> CHK1 --> IR1 --> CHK2 --> CHK3
end
subgraph CORE["shared check core — reused from pass 8"]
FSE["FsEval::for_job_creation<br/>report_eval_errors=FALSE — :825"]
VFW["validate_fs_with — :886"]
VE["validate_expressions<br/>opts carry the budgets"]
CRC["check_resolved_constraint — :448"]
ERR["Err arm — :924<br/>compound if/else kind is Other<br/>budget silently dropped"]
FSE --> VFW --> VE
VE -->|Ok| CRC
VE -->|Err| ERR
end
subgraph CAPS["what gets enforced"]
C1["command, args[i] — max_resolved_arg_len (opt-in)"]
C2["embeddedFiles[j].data — max_resolved_data_len (opt-in)"]
C3["variables values — 2048 chars (ALWAYS ON, new)"]
end
CHK1 --> FSE
CHK2 --> FSE
CHK3 --> FSE
CRC --> C1
CRC --> C2
CRC --> C3
P8G -.->|"desugar NOT validated here"| SUG
CHK3 --> RT["S4 run — resolve_action_args,<br/>embedded_files, SessionLimits (authoritative)"]
Call stackTwo things the call stack makes visible that prose hides. The dotted edge in the diagram is the desugar divergence: pass 8's gate at FindingsFour items, in the inline threads. None is a regression against
Three prior conclusions land correctly and are worth naming. The reuse of One correction to my own earlier review. In #399 I wrote that the two caps "have no job-creation stage available, because VerifiedProbes on |
…ow-ups in the resolved-value design
…ow-ups in the resolved-value design
What was the problem/requirement? (What/Why)
A job template can contain placeholder expressions in its text fields,
like
args: ["{{ 'A' * Param.N }}"]— "repeat A, N times" — whereNis only supplied when a job is submitted. The spec defines three
processing stages (Template Schemas §7.4): template validation, job
creation, and task execution on the worker, with the principle that
every problem should fail at the earliest stage where it is knowable.
PRs #383 and #399 added "resolved-value" limits — caps on how big a
field's final text may get — at the first and last stages: validation
rejects what it can prove too long before parameters exist, and the
worker enforces the limits on the final text. The middle stage did
nothing: job creation is the first moment parameter values are real,
yet it carried the affected fields forward unchecked. A job guaranteed
to violate a limit passed submission and failed later, on every worker.
The caller-configurable expression evaluation budgets (memory/operation
limits) had the same gap — job creation used built-in defaults.
What was the solution? (How)
create_jobnow re-runs the same resolved-value checks that templatevalidation runs, with the parameters filled in. For each carried-forward
field — action
command/args, environment variable values, embeddedfile
data, across step scripts and all environments including wraphooks — it evaluates the field against a symbol table where
Param.*have their submitted values and worker-only values (
Session.*,Task.*) are marked "unresolved". Unknown parts count as zero, so thecomputed minimum length is always a safe lower bound. Environment
variable values are checked against the spec's 2048-character limit
(always on);
command/argsanddataonly against the optionalCallerLimitscaps, since the spec sets no maximum for them. Failurescarry a path to the exact field:
Two details: the pass reports limit violations but deliberately ignores
other evaluation errors (
create_jobmay legitimately run with adifferent feature profile than validation, and the worker re-resolves
anyway) — except budget exceedances, which are reported because run
time would exceed them too. And the caller's evaluation budgets now
apply to every evaluation job creation performs (job name,
letbindings, host requirements, task ranges, and the new checks),
matching the other two stages.
What is the impact of this change?
Jobs whose parameter values guarantee a violation now fail at
submission with a precise error instead of on every worker. Callers
that never set the optional caps see almost no change — the exceptions
are parameter-dependent environment variable values over 2048
characters and environment
letbindings that fail with the realparameter values, both of which previously failed on the worker and now
fail at
create_job. No public API signatures changed; job creationdoes roughly the evaluation work one worker would otherwise do per task.
How was this change tested?
cargo test --workspacepasses. (Two pre-existing Windowscross-user logon failures on this machine are environment-specific
and reproduce on unmodified
main.)letbinding flow, lowered budgets, and passing controls, asserting full
error paths + messages per the repo standard.
-D warnings), rustfmt, andcargo docall clean.were addressed and re-verified.
Was this change documented?
CallerLimitsdocs corrected, and the specs updated inthe same commit:
specs/model/job-creation.mdgains a"Resolved-value checks on carried-forward fields" section, with
validation.mdandpublic-api.mdaligned.Is this a breaking change?
No.
Does this change impact security?
No.
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.