diff --git a/dag/extdeps/exec/command.dag b/dag/extdeps/exec/command.dag index 8f11ceae14d..0518e338454 100644 --- a/dag/extdeps/exec/command.dag +++ b/dag/extdeps/exec/command.dag @@ -137,7 +137,7 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.iproute2.ip_show", decl_name: "ip_route_show_command"), decl_ref(module_path: "extdeps.tools.uname", decl_name: "uname_release_command"), decl_ref(module_path: "extdeps.tools.make", decl_name: "make_command"), - decl_ref(module_path: "extdeps.systemd.systemd_run", decl_name: "systemd_run_user_scope_command"), + decl_ref(module_path: "extdeps.systemd.systemd_run", decl_name: "systemd_run_command_of"), decl_ref(module_path: "extdeps.package_managers.pip", decl_name: "pip_install_pinned_command"), decl_ref(module_path: "extdeps.package_managers.conda_forge", decl_name: "micromamba_create_command"), decl_ref(module_path: "extdeps.printing.orca_slicer", decl_name: "orca_slice_to_project_command"), diff --git a/dag/extdeps/systemd/systemd.dag b/dag/extdeps/systemd/systemd.dag index b17cf229c5d..1e514352c9d 100644 --- a/dag/extdeps/systemd/systemd.dag +++ b/dag/extdeps/systemd/systemd.dag @@ -89,6 +89,13 @@ type SystemdUnitProperty | WantsProperty | InvocationIDProperty | FragmentPathProperty + | StandardOutputProperty + | StandardErrorProperty + | ProtectSystemProperty + | ProtectHomeProperty + | PrivateTmpProperty + | ReadWritePathsProperty + | RemainAfterExitProperty | JobProperty // THE WIRE SPELLING AS A SEALED WORD, SO THE CLOSED TYPE SURVIVES ALL THE WAY TO THE ARGV. @@ -158,6 +165,13 @@ fn systemd_unit_property_wire(property: SystemdUnitProperty) -> NonEmptyStr { WantsProperty => "Wants" as NonEmptyStr InvocationIDProperty => "InvocationID" as NonEmptyStr FragmentPathProperty => "FragmentPath" as NonEmptyStr + StandardOutputProperty => "StandardOutput" as NonEmptyStr + StandardErrorProperty => "StandardError" as NonEmptyStr + ProtectSystemProperty => "ProtectSystem" as NonEmptyStr + ProtectHomeProperty => "ProtectHome" as NonEmptyStr + PrivateTmpProperty => "PrivateTmp" as NonEmptyStr + ReadWritePathsProperty => "ReadWritePaths" as NonEmptyStr + RemainAfterExitProperty => "RemainAfterExit" as NonEmptyStr JobProperty => "Job" as NonEmptyStr } } diff --git a/dag/extdeps/systemd/systemd_run.dag b/dag/extdeps/systemd/systemd_run.dag index 919f8712052..82c4a54b10b 100644 --- a/dag/extdeps/systemd/systemd_run.dag +++ b/dag/extdeps/systemd/systemd_run.dag @@ -9,6 +9,7 @@ import std.algebra { trim } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.uri { Uri, Https } import extdeps.systemd { SystemdUnitProperty, systemd_unit_property_wire } // The manager's version line has ONE spelling for the whole systemd upstream, so the major is read @@ -44,134 +45,283 @@ data systemd_run_authority: ExternalAuthority = extdeps_external_authority_ancho fn systemd_run_program() -> ProgramIdentity = uncataloged_program(invocation: "systemd-run") -// THE PROPERTY WORDS ARE RENDERED IN EXACTLY ONE PLACE, and that is the whole point of this -// function existing beside the full-line authority rather than inside it. A systemd unit property -// becomes an argv word here and nowhere else, so the operation's transport, the pure authority and -// every caller are reading one spelling. The transport CANNOT carry the typed record itself: -// push_shell_argv_tokens has arms for Str, List, ProcessArgvExpansion and a refusing arm for -// ambiguous free monoids, and a record reaches none of them — it lands in the catch-all, which -// Display-formats the value into a single argv word rather than refusing. So a record spliced into -// argv would hand systemd-run a fabricated argument at the exact seam that decides whether a unit -// is bounded. The typed population stays on this side of that boundary; only words cross it. -fn systemd_run_property_argv(properties: List) -> List { - fold( - properties, - init: [], - f: (acc, setting) => list_push( - acc, - concat( - "--property=", - concat(systemd_unit_property_wire(property: setting.property) as String, concat("=", setting.value as String)), - ), - ), - ) +// THE WAITING LAUNCHER IS A TOKEN, NOT A WORD LIST: SystemdSummaryPrintingWait is minted in +// exactly one place, the SystemdSummaryPrintingWaitReady arm of systemd_run_user_wait_command, so +// what a caller holds is the builder's own fact that this projection is the waiting, non-quiet +// one. No other site can construct it, and a launcher that cannot print a memory peak therefore +// cannot be handed to the thing whose contract is to observe one. +type SystemdSummaryPrintingWait { command: ArgvCommand } + +type SystemdSummaryPrintingWaitReading + = SystemdSummaryPrintingWaitReady { wait: SystemdSummaryPrintingWait } + | SystemdSummaryPrintingWaitRefused { cause: SystemdRunOptionWordsRefusedCause } + +// compute_exec receives the program separately, so these are the argument tail; the full +// argv_words would double the program head on the compute side too. +fn systemd_summary_printing_wait_argv(w: SystemdSummaryPrintingWait) -> List { + systemd_run_command_argument_words(command: w.command) } -// THE USER-MANAGER TRANSIENT UNIT, modelled beside the system-scope one above because the flags -// systemd-run actually accepts are upstream's, not ours: --user selects the calling user's manager, -// -p sets a unit property, and --setenv binds one environment variable in the started unit. +// ── THE TYPED OPTION MODEL OF systemd-run(1) AT THE GROUNDED MAJOR ───────────────────────────── // -// NO --collect HERE, DELIBERATELY, and it is the difference that makes this worth a second -// function. --collect unloads the unit as soon as it exits, which discards Result and -// ExecMainStatus -- exactly the terminal state a caller starts a transient unit in order to be able -// to read afterwards. A unit that lingers in `failed` is evidence; one that was garbage collected -// is an absence indistinguishable from never having run. Callers that want no residue name a unit -// per attempt and reset it explicitly, which is a decision about evidence rather than a flag. -fn systemd_run_user_transient_arguments( - unit: NonEmptyStr, - properties: List, - setenv_bindings: List, - command_argv: List, -) -> List { - let head = ["--user", concat("--unit=", unit as String)] - let with_properties = fold( - properties, - init: head, - f: (acc, p) => list_push(list_push(acc, "-p"), p), - ) - let with_env = fold( - setenv_bindings, - init: with_properties, - f: (acc, b) => list_push(acc, concat("--setenv=", b)), - ) - fold( - command_argv, - init: list_push(with_env, "--"), - f: (acc, arg) => list_push(acc, arg), - ) +// EVERY WORD THIS MODULE RENDERS IS BORN IN ONE PROJECTION, `systemd_run_option_words`, from typed +// options carrying the man page's own spellings at systemd 255 — the major the summary ground +// below actually reads (grounded_summary_format_majors). No builder glues a flag to a value with +// string literals anymore: the flag words live here, once, each citing its option, and the +// projection REFUSES a value the wire cannot carry rather than trimming or dropping it. + +// --setenv=NAME=VALUE binds ONE environment variable into the unit (systemd-run(1)). +type SystemdSetenvBinding { + name: NonEmptyStr, + value: NonEmptyStr, } -// THE SYNCHRONOUS FORM: the caller blocks until the unit exits and receives its exit status and its -// stdio (`--wait --pipe`), so a build or a check runs under the unit's resource bounds while the -// process that asked for it observes the result directly. `--collect` unloads the unit afterwards -// whatever its result, so a failed compute unit does not accumulate as a failed unit to reap. -// WHETHER THERE IS A SUMMARY TO READ IS A FACT ABOUT THE INVOCATION, SO THE INVOCATION CARRIES IT. -// systemd-run prints the result and peak lines only under `if (arg_wait && !arg_quiet)` (run.c, -// v255), so --quiet suppresses the summary ENTIRELY: a reader handed a quiet launcher's stderr -// finds no peak, reports MemoryPeakNotReported forever, and is indistinguishable from a healthy -// launcher whose manager is below v254. A DEAD PRODUCER THAT LOOKS LIKE A HEALTHY ONE WITH NOTHING -// TO REPORT is the failure this file is one word away from: RunTransientAndWait and this builder -// differ by exactly "--quiet", and the next author adds a third by copying whichever they reached -// first. So the summary reader does not take a loose String. It takes this token, which is minted -// in exactly one place -- the builder below, the only wait invocation in this module that omits -// --quiet -- and a launcher that cannot print a peak therefore cannot be handed to the thing whose -// contract is to observe one. -type SystemdSummaryPrintingWait { argv: List } +// THE OPTION ARMS AND THEIR WIRE SPELLINGS, one per line, each citing systemd-run(1) at v255: +// RunUnit --unit=UNIT +// RunUserManager --user +// RunScope --scope +// PropertyShort -p NAME=VALUE — flag and value are TWO words +// PropertyLong --property=NAME=VALUE — one word +// SetEnv --setenv=NAME=VALUE +// Wait --wait +// Quiet --quiet +// Pipe --pipe +// Collect --collect +// EndOfOptions -- (systemd-run's own options end; the rest is the command) +type SystemdRunOption + = RunUnit { unit: NonEmptyStr } + | RunUserManager + | RunScope + | PropertyShort { property: SystemdRunProperty } + | PropertyLong { property: SystemdRunProperty } + | SetEnv { binding: SystemdSetenvBinding } + | Wait + | Quiet + | Pipe + | Collect + | EndOfOptions + +// WHAT THE WIRE CANNOT CARRY IS REFUSED AT THE PROJECTION — a refusal at the wall between the +// typed value and the words, not a mangled word downstream. Each arm names the wire fact it +// defends (unit names and setting values are one line and contain no slash; an environment name +// is the text before the first '='). +type SystemdRunOptionWordsRefusedCause + = UnitNameContainsSlash + | UnitNameMultiline + | UnitSettingValueMultiline { property: SystemdUnitProperty } + | SetenvNameContainsEquals + | SetenvNameMultiline + | SetenvValueMultiline + +type SystemdRunOptionWordsReading + = SystemdRunOptionWordsProjected { words: List } + | SystemdRunOptionWordsRefused { cause: SystemdRunOptionWordsRefusedCause } + +type SystemdRunCommandReading + = SystemdRunCommandReady { command: ArgvCommand } + | SystemdRunCommandRefused { cause: SystemdRunOptionWordsRefusedCause } + +fn systemd_run_option_word_has_newline(s: String) -> Bool { + string_contains(s: s, pattern: "\n") +} -fn systemd_summary_printing_wait_argv(w: SystemdSummaryPrintingWait) -> List { - w.argv +fn systemd_run_option_word_refused(cause: SystemdRunOptionWordsRefusedCause) -> SystemdRunOptionWordsReading { + SystemdRunOptionWordsRefused { cause: cause } } -fn systemd_run_user_wait_arguments( - unit: NonEmptyStr, - properties: List, - setenv_bindings: List, - command_argv: List, -) -> SystemdSummaryPrintingWait { - SystemdSummaryPrintingWait { argv: concat(["--wait", "--pipe", "--collect"], systemd_run_user_transient_arguments(unit: unit, properties: properties, setenv_bindings: setenv_bindings, command_argv: command_argv)) } +// The one prose projection of a refusal cause, for consumers whose carriers are prose (belt +// step details, log lines). Every arm names the wire fact it defends; the cause sum itself is +// what callers match on. +fn systemd_run_option_words_refused_cause_detail(cause: SystemdRunOptionWordsRefusedCause) -> String { + match cause { + UnitNameContainsSlash => "a unit name never contains / (systemd.unit(5))" + UnitNameMultiline => "a unit name is one line" + UnitSettingValueMultiline { property: p } => concat("unit setting value is one line, so --property= cannot carry a newline in: ", systemd_unit_property_wire(property: p)) + SetenvNameContainsEquals => "--setenv= takes NAME=VALUE, so the NAME side carries no = of its own" + SetenvNameMultiline => "--setenv= takes NAME=VALUE, so the NAME side is one line" + SetenvValueMultiline => "--setenv= takes NAME=VALUE, so the VALUE side is one line" + } } -// ONE COMMAND IN A TRANSIENT SCOPE OF THE CALLING USER'S MANAGER, bounded by the given properties -// (`systemd-run --user --scope -p NAME=VALUE ... -- CMD`, per the cited man page: a scope runs the -// command synchronously as a child of systemd-run and places it in its own cgroup). The kernel -// enforces the bound, not the command, so an over-budget run is killed by the cgroup instead of -// competing for the host. The program is PATH-resolved because the invoking host is unobserved -// (extdeps.exec.command program_spelling_is_an_observation_claim). -fn systemd_run_user_scope_command(properties: List, command: ArgvCommand) -> ArgvCommand { - let head = ["--user", "--scope"] - let with_properties = fold(systemd_run_property_argv(properties: properties), init: head, f: (acc, word) => list_push(acc, word)) - let with_separator = list_push(with_properties, "--") - argv_command( - program: systemd_run_program(), - arguments: fold(argv_words(command: command), init: with_separator, f: (acc, arg) => list_push(acc, arg)), - ) +fn systemd_run_property_options_long(properties: List) -> List { + fold(properties, init: [], f: (acc, p) => list_append(left: acc, right: [PropertyLong { property: p }])) +} + +fn systemd_run_property_options_short(properties: List) -> List { + fold(properties, init: [], f: (acc, p) => list_append(left: acc, right: [PropertyShort { property: p }])) +} + +// THE ONE PROJECTION STEP. Each option's words are spelled exactly once, here; the fold folds over +// these so a refused command never mints words at all (short-circuit on the first refusal). +fn systemd_run_option_projection_step(acc: SystemdRunOptionWordsReading, option: SystemdRunOption) -> SystemdRunOptionWordsReading { + match acc { + SystemdRunOptionWordsRefused { cause: c } => SystemdRunOptionWordsRefused { cause: c } + SystemdRunOptionWordsProjected { words: words } => + match option { + RunUnit { unit: unit } => + if string_contains(s: unit as String, pattern: "/") { + systemd_run_option_word_refused(cause: UnitNameContainsSlash) + } else if systemd_run_option_word_has_newline(s: unit as String) { + systemd_run_option_word_refused(cause: UnitNameMultiline) + } else { + SystemdRunOptionWordsProjected { words: list_append(left: words, right: [concat("--unit=", unit as String)]) } + } + RunUserManager => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--user"]) } + RunScope => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--scope"]) } + PropertyShort { property: p } => + if systemd_run_option_word_has_newline(s: p.value as String) { + systemd_run_option_word_refused(cause: UnitSettingValueMultiline { property: p.property }) + } else { + SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["-p", concat(systemd_unit_property_wire(property: p.property), concat("=", p.value as String))]) } + } + PropertyLong { property: p } => + if systemd_run_option_word_has_newline(s: p.value as String) { + systemd_run_option_word_refused(cause: UnitSettingValueMultiline { property: p.property }) + } else { + SystemdRunOptionWordsProjected { words: list_append(left: words, right: [concat("--property=", concat(systemd_unit_property_wire(property: p.property), concat("=", p.value as String)))]) } + } + SetEnv { binding: b } => + if string_contains(s: b.name as String, pattern: "=") { + systemd_run_option_word_refused(cause: SetenvNameContainsEquals) + } else if systemd_run_option_word_has_newline(s: b.name as String) { + systemd_run_option_word_refused(cause: SetenvNameMultiline) + } else if systemd_run_option_word_has_newline(s: b.value as String) { + systemd_run_option_word_refused(cause: SetenvValueMultiline) + } else { + SystemdRunOptionWordsProjected { words: list_append(left: words, right: [concat("--setenv=", concat(b.name as String, concat("=", b.value as String)))]) } + } + Wait => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--wait"]) } + Quiet => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--quiet"]) } + Pipe => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--pipe"]) } + Collect => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--collect"]) } + EndOfOptions => SystemdRunOptionWordsProjected { words: list_append(left: words, right: ["--"]) } + } + } +} + +// THE MANAGER-SIDE STANDING THE TYPED BUILDERS OWN, AND WHY IT IS DECLARED RATHER THAN ENROLLED. +// The three SystemdRun service transports promise behaviour a live systemd manager shows: the +// retained transient stays manager-observable after a failing exit (it is still `loaded` -- no +// --collect twin removes it), the collecting transient's twin is gone (`not-found`), and the +// waited run reports the child's exact exit code. THIS IS THE POPULATION the typed +// SystemdRunOption builders carry on the wire. These builders carry no RunUserManager arm: they +// target the DEFAULT (system) manager, and the manual lifecycle receipt executes these same +// builders against it. What stands today: no REQUIRED automated claim exercises them -- no CI +// lane can run systemd units under the privilege/elevation posture these builders need, and the +// one real execution that reaches the manager (the manual lifecycle receipt's wet path) is a +// NON-required, manual receipt, not an enrolled required claim. A claim file nothing can execute +// would be dangling (DESIGN 3c), so the standing is a declared unbound dissolution here beside +// the builders -- the authority that owns the behaviour -- rather than a placeholder row in a +// lane that cannot run it. Dissolves when a required-workflow lane exists that can execute these +// system-manager builders under their required privilege/elevation posture: at that point the +// retained/collected/waited claims are AUTHORED AND ENROLLED through the same production +// integration, and this row retires. +data systemd_run_manager_side_claim_gap: DissolutionCondition = unbound_dissolution(description: "population: the typed SystemdRun builders' manager-side contract -- a retained transient stays manager-observable (still loaded) after a failing exit, a collecting transient's unit is not-found after removal, and the waited run reports the exact child exit; the three builders carry no RunUserManager arm, so they target the default (system) manager and the manual lifecycle receipt executes these same builders against it; current: no REQUIRED automated claim covers this population (no required lane can run systemd units under the privilege/elevation posture these builders need, and the manual lifecycle receipt's wet path is non-required real execution); trigger: a required-workflow lane exists that can execute these system-manager builders under their required privilege/elevation posture, at which point the retained/collected/waited claims are authored and enrolled through the same production integration and this row retires.") + +fn systemd_run_option_words(options: List) -> SystemdRunOptionWordsReading { + fold(options, init: SystemdRunOptionWordsProjected { words: [] }, f: systemd_run_option_projection_step) +} + +// THE COMMAND COMPOSITION: options first, then the command's own words after `--` (the caller's +// argv is appended verbatim; its typing is its own layer's debt, one batch per module). +// The words AFTER the program word. The projection leads with exactly one program spelling, so a +// caller that runs a RESOLVED program path instead (an observation the instance toolchain declares, +// and the only thing the invoking host is allowed to assert about where the program lives) takes +// the tail and substitutes its own head. +type SystemdRunCommandTailFold { + program_pending: Bool, + words: List, +} + +fn systemd_run_command_argument_words(command: ArgvCommand) -> List { + fold(argv_words(command: command), init: SystemdRunCommandTailFold { program_pending: true, words: [] }, f: (acc, w) => + if acc.program_pending { + SystemdRunCommandTailFold { program_pending: false, words: acc.words } + } else { + SystemdRunCommandTailFold { program_pending: false, words: list_append(left: acc.words, right: [w]) } + } + ).words +} + +fn systemd_run_command_of(options: List, command_argv: List) -> SystemdRunCommandReading { + match systemd_run_option_words(options: options) { + SystemdRunOptionWordsRefused { cause: c } => SystemdRunCommandRefused { cause: c } + SystemdRunOptionWordsProjected { words: option_words } => + SystemdRunCommandReady { command: argv_command(program: systemd_run_program(), arguments: list_append(left: option_words, right: command_argv)) } + } } -// ONE COMMAND IN A TRANSIENT SCOPE OF THE SYSTEM MANAGER (`systemd-run --scope -p NAME=VALUE ... -- CMD`). +// ── THE BUILDERS, ALL RETURNING THE TYPED COMMAND ─────────────────────────────────────────────── + +// ONE COMMAND IN A TRANSIENT SCOPE OF THE SYSTEM MANAGER (`systemd-run --scope --property=NAME=VALUE ... -- CMD`). // The user-manager form above needs the caller's own session bus, which a command elevated from // another login does not have; the system manager starts the scope and the kernel bounds its // cgroup. The caller must be root. The command runs with the caller's credentials: systemd-run's // --uid/--gid are not rendered, because in scope mode they switch uid and gid without initialising // the target's supplementary groups and leave the caller's in place (fleet-converge run // 37098981121). A caller that needs another account composes its own privilege drop as CMD. -fn systemd_run_system_scope_argv(properties: List, command_argv: List) -> List { - let with_properties = fold(systemd_run_property_argv(properties: properties), init: ["systemd-run", "--scope"], f: (acc, word) => list_push(acc, word)) - let with_separator = list_push(with_properties, "--") - fold(command_argv, init: with_separator, f: (acc, arg) => list_push(acc, arg)) +// ONE COMMAND IN A TRANSIENT SCOPE OF THE CALLING USER'S MANAGER, bounded by the given properties +// (`systemd-run --user --scope --property=NAME=VALUE ... -- CMD`, per the cited man page: a scope +// runs the command synchronously as a child of systemd-run and places it in its own cgroup). The +// kernel enforces the bound, not the command, so an over-budget run is killed by the cgroup +// instead of competing for the host. The program is PATH-resolved because the invoking host is +// unobserved (extdeps.exec.command program_spelling_is_an_observation_claim). +fn systemd_run_setenv_options(bindings: List) -> List { + fold(bindings, init: [], f: (acc, b) => list_append(left: acc, right: [SetEnv { binding: b }])) } -fn systemd_run_property(name: String, value: String) -> String { - join([name, "=", value], "") +// THE USER-MANAGER TRANSIENT START: `systemd-run --user --unit=NAME -p NAME=VALUE ... --setenv=K=V +// ... -- CMD`. The property words travel as two words (-p, then NAME=VALUE) and the projected +// order here is the order the string-built form this replaces happened to emit, so the projected +// words for existing callers are byte-identical to what they rendered before the cutover. +fn systemd_run_user_transient_command(unit: NonEmptyStr, properties: List, setenv_bindings: List, command_argv: List) -> SystemdRunCommandReading { + systemd_run_command_of( + options: list_append(left: [RunUserManager, RunUnit { unit: unit }], right: list_append(left: systemd_run_property_options_short(properties: properties), right: list_append(left: systemd_run_setenv_options(bindings: setenv_bindings), right: [EndOfOptions]))), + command_argv: command_argv, + ) +} + +// THE USER-MANAGER WAITING TRANSIENT: the start above plus --wait --pipe --collect, and NOT +// --quiet -- run.c at v255 prints "Finished with result:" and "Memory peak:" only under +// `if (arg_wait && !arg_quiet)`, so the quiet form's stderr never carries the summary this type +// exists to be read for. The wait flags lead because that is the word order the string-built form +// this replaces emitted; the projection keeps it byte-identical for existing callers. +fn systemd_run_user_wait_command(unit: NonEmptyStr, properties: List, setenv_bindings: List, command_argv: List) -> SystemdSummaryPrintingWaitReading { + match systemd_run_command_of( + options: list_append(left: [Wait, Pipe, Collect], right: list_append(left: [RunUserManager, RunUnit { unit: unit }], right: list_append(left: systemd_run_property_options_short(properties: properties), right: list_append(left: systemd_run_setenv_options(bindings: setenv_bindings), right: [EndOfOptions])))), + command_argv: command_argv, + ) { + SystemdRunCommandRefused { cause: cause } => SystemdSummaryPrintingWaitRefused { cause: cause } + SystemdRunCommandReady { command: command } => SystemdSummaryPrintingWaitReady { wait: SystemdSummaryPrintingWait { command: command } } + } +} + +// The pre-cutover system-scope builder systemd_run_system_scope_argv led with --scope only +// (["systemd-run", "--scope"]); the account entry happens in the command tail (setpriv). +fn systemd_run_system_scope_command(properties: List, command_argv: List) -> SystemdRunCommandReading { + systemd_run_command_of(options: list_append(left: [RunScope], right: list_append(left: systemd_run_property_options_long(properties: properties), right: [EndOfOptions])), command_argv: command_argv) } // The full invocation, and the authority the materialized operation argv is checked against. It -// COMPOSES the property words rather than re-deriving them; if this fold and the transport template -// ever disagree about word order, systemd_run_transient_operation_argv_matches_authority goes red. -fn systemd_run_transient_unit_argv(unit: NonEmptyStr, properties: List, command_argv: List) -> List { - let launcher = ["systemd-run", concat("--unit=", unit as String), "--collect"] - let with_properties = fold(systemd_run_property_argv(properties: properties), init: launcher, f: (acc, word) => list_push(acc, word)) - let with_separator = list_push(with_properties, "--") - fold(command_argv, init: with_separator, f: (acc, arg) => list_push(acc, arg)) +// COMPOSES the option words through the one projection rather than re-deriving them. The +// transport template carries no words of its own beyond the program head, so this fold's order IS +// the executed order; systemd_run_transient_operation_argv_matches_authority witnesses the head +// spelling and refuses embedded newlines (see its comment for the narrowed check). +// Word order: --unit, --collect, then the properties, then -- (the base argv shape for a +// collecting transient; the enrolled controls pin this exact order). The retained variant below +// is the same builder WITHOUT the +// --collect arm, because the RETAINED invocation exists precisely so the terminal state stays +// observable (the service op is the same RunTransient either way -- retained vs collected is a +// decision the typed option model renders into the words, not a service-side contract). +fn systemd_run_transient_unit_command(unit: NonEmptyStr, properties: List, command_argv: List) -> SystemdRunCommandReading { + systemd_run_command_of(options: list_append(left: [RunUnit { unit: unit }, Collect], right: list_append(left: systemd_run_property_options_long(properties: properties), right: [EndOfOptions])), command_argv: command_argv) +} + +// The retained shape: no --collect, so the transient unit survives its process and its terminal +// state stays observable (the callers are realize and the manual wet receipt, both of which read +// the unit AFTER the invocation ends). +fn systemd_run_transient_retained_unit_command(unit: NonEmptyStr, properties: List, command_argv: List) -> SystemdRunCommandReading { + systemd_run_command_of(options: list_append(left: [RunUnit { unit: unit }], right: list_append(left: systemd_run_property_options_long(properties: properties), right: [EndOfOptions])), command_argv: command_argv) } // Waiting is a different operation from starting. It owns the complete wait/quiet/collect @@ -180,15 +330,12 @@ fn systemd_run_transient_unit_argv(unit: NonEmptyStr, properties: List, command_argv: List) -> List { - let launcher = ["systemd-run", concat("--unit=", unit as String), "--wait", "--quiet", "--collect"] - let with_properties = fold(systemd_run_property_argv(properties: properties), init: launcher, f: (acc, word) => list_push(acc, word)) - let with_separator = list_push(with_properties, "--") - fold(command_argv, init: with_separator, f: (acc, arg) => list_push(acc, arg)) +fn systemd_run_transient_wait_unit_command(unit: NonEmptyStr, properties: List, command_argv: List) -> SystemdRunCommandReading { + systemd_run_command_of(options: list_append(left: [RunUnit { unit: unit }, Wait, Quiet, Collect], right: list_append(left: systemd_run_property_options_long(properties: properties), right: [EndOfOptions])), command_argv: command_argv) } -// RunTransient AND RunTransientRetained DIFFER BY --collect AND THAT IS A DECISION ABOUT EVIDENCE. +// RETAINED AND COLLECTED DIFFER BY --collect AND THAT IS A DECISION ABOUT EVIDENCE. // // The argument is already made in this file for the user-manager variant and it holds identically // here: --collect unloads the unit the moment it exits, which discards Result, ExecMainStatus and @@ -207,23 +354,30 @@ fn systemd_run_transient_wait_unit_argv(unit: NonEmptyStr, properties: List, command_argv: List } + input { command_argv: List } output { success: Bool from "exit_success" stdout: String from "stdout" stderr: String from "stderr" } - transport shell { argv: ["systemd-run", "--unit={unit}", "--collect", property_argv, "--", command_argv] } + transport shell { argv: ["systemd-run", command_argv] } exit { 0 => Unit nonzero => String "systemd-run transient unit start failed" @@ -234,33 +388,15 @@ service systemd.SystemdRun { } - operation RunTransientRetained { - requires opaque - input { unit: NonEmptyStr, property_argv: List, command_argv: List } - output { - success: Bool from "exit_success" - stdout: String from "stdout" - stderr: String from "stderr" - } - transport shell { argv: ["systemd-run", "--unit={unit}", property_argv, "--", command_argv] } - exit { - 0 => Unit - nonzero => String "systemd-run transient unit start failed" - } - mock_response { - 0 => { success: true, stdout: "Running as unit: demo.service", stderr: "" } "hermetic systemd.SystemdRun.RunTransientRetained" - } - } - operation RunTransientAndWait { requires opaque - input { unit: NonEmptyStr, property_argv: List, command_argv: List } + input { command_argv: List } output { exit_code: Int from "exit_code" stdout: String from "stdout" stderr: String from "stderr" } - transport shell { argv: ["systemd-run", "--unit={unit}", "--wait", "--quiet", "--collect", property_argv, "--", command_argv] } + transport shell { argv: ["systemd-run", command_argv] } exit { 0 => Unit nonzero => Unit @@ -557,7 +693,7 @@ fn systemd_argv_suppresses_summary(argv: List) -> Bool { // THE READER TAKES THE LAUNCHER AND INSPECTS IT, AND THE SECOND HALF IS NOT REDUNDANT -- IT IS // MEASURED. The intent was construction: SystemdSummaryPrintingWait is minted only by -// systemd_run_user_wait_arguments, the one wait builder here that omits --quiet, so a quiet +// systemd_run_user_wait_command, the one wait builder here that omits --quiet, so a quiet // launcher should not be expressible as this argument at all. IT IS. A probe passing the // List returned by systemd_run_transient_wait_unit_argv (which carries --quiet) as this // parameter COMPILED AND EVALUATED, reading a peak out of stderr no quiet launcher could have diff --git a/dag/gunbc/auth/approval_device_enrolment_code_issue.dag b/dag/gunbc/auth/approval_device_enrolment_code_issue.dag index cc4f277e302..e7b3955fcd7 100644 --- a/dag/gunbc/auth/approval_device_enrolment_code_issue.dag +++ b/dag/gunbc/auth/approval_device_enrolment_code_issue.dag @@ -6,7 +6,12 @@ import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } import std.resources { Network } import gunbc.cli_wire { CliWireResponse, CliWirePrintable, CliWireUnprintable } import extdeps.sudo.elevation { sudo_elevate } -import extdeps.systemd.systemd_run { SystemdRunProperty, systemd_run_system_scope_argv } +import extdeps.systemd.systemd_run { + SystemdRunProperty, SystemdRunCommandReading, SystemdRunCommandReady, SystemdRunCommandRefused, + systemd_run_system_scope_command, + systemd_run_option_words_refused_cause_detail, +} +import extdeps.exec.command { ArgvCommand, argv_words } import extdeps.tools.util_linux_setpriv { setpriv_reuid_regid_argv } import extdeps.systemd { MemoryMax, MemoryHigh } import std.measure { byte_size_count } @@ -79,7 +84,7 @@ fn enrolment_code_issue_ssh_target() -> SshTarget { // typed-module cache cap is derived from the cgroup's memory.high/memory.max, and with neither bound // it refuses HostBudgetUnreadable rather than guessing (fleet-converge run 36556990543). An SSH // session binds no such limit. The scope is a system-manager transient scope (extdeps.systemd.systemd_run -// systemd_run_system_scope_argv) around the account drop below. A user +// systemd_run_system_scope_command) around the account drop below. A user // scope would need that account's session bus, which the elevated command lacks. The bounds are the // broker's own slice rows (gunbc.live_deploy.slice_bounds approval_broker_slice_memory_max and _high). // The verb resolves the broker's routes closure, the same program the broker boots, so the same @@ -97,7 +102,7 @@ fn enrolment_code_issue_scope_properties() -> List { // supplementary groups; systemd-run's own --uid/--gid did not, and every code was minted and then // refused delivery (fleet-converge run 37098981121). The drop sits inside the scope so the bound // still covers the whole verb. -fn enrolment_code_issue_remote_argv(revision: ReleaseRevisionBinding) -> List { +fn enrolment_code_issue_remote_command(revision: ReleaseRevisionBinding) -> SystemdRunCommandReading { let root = srv1_gunbc_approval_broker_root as String let release_dir = approval_broker_release_dir(root: root, revision: revision) let account = fleet_posix_operator_user.name @@ -113,8 +118,7 @@ fn enrolment_code_issue_remote_argv(revision: ReleaseRevisionBinding) -> List CliWireResponse uses net: Network { match prepare_fleet_ssh_agent_context(attempt_raw: approval_device_enrolment_code_issue_attempt_raw) { FleetSshContextRefused { cause: c } => CliWireUnprintable { cause: ("approval device enrolment code: " + c) as NonEmptyStr } FleetSshContextReady { context: context, receipt: _ } => - match typed_argv_exec_over_fleet_ssh(target: enrolment_code_issue_ssh_target(), context: context, argv: enrolment_code_issue_remote_argv(revision: revision)) { - TypedArgvExecRefused { reason: why } => CliWireUnprintable { cause: ("approval device enrolment code: remote invocation refused: " + why) as NonEmptyStr } - TypedArgvExecConverged { result: r } => - if r.success { CliWirePrintable { bytes: r.stdout, exit: ExitSuccess } } - else { CliWireUnprintable { cause: ("approval device enrolment code: remote exit=" + to_string(r.exit_code) + " " + trim(s: r.stderr)) as NonEmptyStr } } + match enrolment_code_issue_remote_command(revision: revision) { + SystemdRunCommandRefused { cause: why } => + CliWireUnprintable { cause: ("approval device enrolment code: remote invocation refused: " + systemd_run_option_words_refused_cause_detail(cause: why)) as NonEmptyStr } + SystemdRunCommandReady { command: command } => + match typed_argv_exec_over_fleet_ssh( + target: enrolment_code_issue_ssh_target(), + context: context, + argv: sudo_elevate_words(command: command), + ) { + TypedArgvExecRefused { reason: why } => CliWireUnprintable { cause: ("approval device enrolment code: remote invocation refused: " + why) as NonEmptyStr } + TypedArgvExecConverged { result: r } => + if r.success { CliWirePrintable { bytes: r.stdout, exit: ExitSuccess } } + else { CliWireUnprintable { cause: ("approval device enrolment code: remote exit=" + to_string(r.exit_code) + " " + trim(s: r.stderr)) as NonEmptyStr } } + } } } } +fn sudo_elevate_words(command: ArgvCommand) -> List { + let inv = sudo_elevate(command: argv_words(command: command)) + concat([inv.bin_path], inv.args) +} + // THE STEP ENTRY: one host per run, named by the dispatch and required to be srv1; one release, // named by the dispatch. fn approval_device_enrolment_code_issue_wet() -> CliWireResponse diff --git a/dag/gunbc/compute/unit_bounds.dag b/dag/gunbc/compute/unit_bounds.dag index 267517e3dfc..38177f7ff38 100644 --- a/dag/gunbc/compute/unit_bounds.dag +++ b/dag/gunbc/compute/unit_bounds.dag @@ -1,9 +1,11 @@ module gunbc.compute.unit_bounds -import std.types { String, List } +import std.types { String, List, NonEmptyStr } import std.nat { Nat } import std.measure { Kibibyte, kibibyte_to_byte_size, byte_size_count, Millicore, millicore_count } -import extdeps.systemd.systemd_run { systemd_run_property } +import extdeps.systemd { SystemdUnitProperty, MemoryMax, WorkingDirectoryProperty, KillModeProperty, CPUQuota, TasksMax } +import extdeps.systemd.systemd_run { SystemdRunProperty } +import extdeps.systemd.unit_file { KillModeControlGroup, systemd_kill_mode_wire } // THE ONE FOLD FROM A CAPACITY GRANT TO THE PROPERTIES OF THE UNIT THAT HOLDS IT. It used to live in // gunbc.compute.work_provider_local as compute_unit_properties, and it moved here rather than being @@ -22,8 +24,7 @@ import extdeps.systemd.systemd_run { systemd_run_property } // declaration -- a drop-in or a future default could change it, and a provider would keep treating // a completed wait as an empty cgroup (side-chat review at 773825cf). Binding it makes the policy a // fact of the invocation, and an agent session releases its seat on the same reading. -data compute_kill_mode_property: String = "KillMode" -data compute_kill_mode_value: String = "control-group" +data compute_kill_mode_property: SystemdUnitProperty = KillModeProperty // WHAT A GRANT DOES NOT CARRY, NAMED RATHER THAN INVENTED. product.capacity.pool grants MEMORY: the // lease is an encumbrance over a Memory pool and nothing else, so a CPU quota or a task ceiling is @@ -44,19 +45,27 @@ fn unit_cpu_quota_percent(m: Millicore) -> Nat { millicore_count(m: m) / 10 } -fn compute_grant_unit_properties(granted: Kibibyte, working_directory: String, process_bounds: UnitProcessBounds?) -> List { +fn compute_grant_unit_properties(granted: Kibibyte, working_directory: String, process_bounds: UnitProcessBounds?) -> List { concat( [ - systemd_run_property(name: "MemoryMax", value: to_string(value: byte_size_count(b: kibibyte_to_byte_size(k: granted)))), - systemd_run_property(name: compute_kill_mode_property, value: compute_kill_mode_value), - systemd_run_property(name: "WorkingDirectory", value: working_directory), + SystemdRunProperty { property: MemoryMax, value: to_string(value: byte_size_count(b: kibibyte_to_byte_size(k: granted))) as NonEmptyStr }, + SystemdRunProperty { property: compute_kill_mode_property, value: compute_kill_mode_value() }, + SystemdRunProperty { property: WorkingDirectoryProperty, value: working_directory as NonEmptyStr }, ], match process_bounds { Absent => [] Present { value: b } => [ - systemd_run_property(name: "CPUQuota", value: join([to_string(value: unit_cpu_quota_percent(m: b.cpu_quota)), "%"], "")), - systemd_run_property(name: "TasksMax", value: to_string(value: b.tasks_max)), + SystemdRunProperty { property: CPUQuota, value: join([to_string(value: unit_cpu_quota_percent(m: b.cpu_quota)), "%"], "") as NonEmptyStr }, + SystemdRunProperty { property: TasksMax, value: to_string(value: b.tasks_max) as NonEmptyStr }, ] }, ) } + +// THE kill mode the release decision relies on is the closed systemd fact KillModeControlGroup; +// its wire spelling is owned by systemd_kill_mode_wire. This layer projects, it does not store a +// second copy of the spelling (review 76958: the stored "control-group" was a parallel wire for a +// closed upstream fact). +fn compute_kill_mode_value() -> NonEmptyStr { + systemd_kill_mode_wire(mode: KillModeControlGroup) as NonEmptyStr +} diff --git a/dag/gunbc/compute/work_provider_local.dag b/dag/gunbc/compute/work_provider_local.dag index f20725281e5..def52cc09c9 100644 --- a/dag/gunbc/compute/work_provider_local.dag +++ b/dag/gunbc/compute/work_provider_local.dag @@ -11,7 +11,9 @@ import extdeps.shell import extdeps.git { shape_git_worktree_add_detached_argv } import extdeps.git.object_store { GitObjectId, git_object_id_from_untagged_hex, git_object_id_wire_hex } import extdeps.systemd.systemd_run { - systemd_run_user_wait_arguments, systemd_run_property, SystemdSummaryPrintingWait, systemd_summary_printing_wait_argv, + SystemdSummaryPrintingWait, systemd_summary_printing_wait_argv, SystemdSetenvBinding, + SystemdRunCommandReading, SystemdRunCommandReady, SystemdRunCommandRefused, SystemdRunProperty, + systemd_run_user_wait_command, SystemdRunWaitSummary, systemd_run_wait_summary, SystemdServiceResultReading, ServiceResultRead, ServiceResultUnrecognized, ServiceResultNotReported, SystemdServiceResult, ServiceResultOomKill, systemd_service_result_wire, SystemdMemoryPeakReading, MemoryPeakRead, MemoryPeakUnparseable, MemoryPeakNotReported, @@ -20,6 +22,7 @@ import extdeps.systemd.systemd_run { ServiceResultFormatNotGrounded, MemoryPeakFormatNotGrounded, SummaryAbsence, SummaryNoInvocation, SummarySuppressedByQuietLauncher, SummaryCaptureEmpty, SummaryLineAbsent, summary_absence_wire, SystemdFormattedBytes, FormattedBytesExact, FormattedBytesBounded, systemd_formatted_bytes_upper, + systemd_run_option_words_refused_cause_detail, } import extdeps.systemd.systemctl import gunbc.compute.unit_bounds { compute_grant_unit_properties, compute_kill_mode_property, compute_kill_mode_value } @@ -131,6 +134,12 @@ type ComputeExec = ComputeExecOk { stdout: String, stderr: String } | ComputeExecFailed { exit_code: Int, stdout: String, stderr: String } +// The unit launch has its own refusal carrier: the systemd-run builder's typed cause, carried from +// the one site that can produce it. compute_exec's shared reading never refuses. +type ComputeUnitRunReading + = ComputeUnitRunExecuted { run: ComputeUnitRun } + | ComputeUnitRunRefused { cause: SystemdRunOptionWordsRefusedCause } + fn compute_exec(program: String, workdir: String, args: List) -> ComputeExec { let r = gunbc.WitnessBin.Run(workdir: workdir as FilePath, bin_path: program as FilePath, args: args, expect: OutcomeIsData) if r.success { ComputeExecOk { stdout: r.stdout, stderr: r.stderr } } else { ComputeExecFailed { exit_code: r.exit_code, stdout: r.stdout, stderr: r.stderr } } @@ -311,7 +320,7 @@ fn compute_unit_name(identity_hex: String) -> NonEmptyStr { // THE PROPERTIES ARE A FOLD SO THE BOUND POLICY IS CHECKABLE, and the fold is // gunbc.compute.unit_bounds compute_grant_unit_properties -- the one authority from a grant to a // unit, shared with the agent session's unit. A compute unit states no process bounds. -fn compute_unit_properties(layout: ComputeLayout, granted: Kibibyte) -> List { +fn compute_unit_properties(layout: ComputeLayout, granted: Kibibyte) -> List { compute_grant_unit_properties(granted: granted, working_directory: layout.checkout, process_bounds: none) } @@ -366,38 +375,43 @@ type ComputeUnitRun { // set must refuse rather than have its figures read against a format nobody has checked. Reading // `systemctl --version` there would answer about a DIFFERENT executable than the one that prints the // line, which on a mixed install is a different version. -fn compute_run_unit(instance: HostDashboardInstance, layout: ComputeLayout, identity_hex: String, argv: List, granted: Kibibyte) -> ComputeUnitRun { +fn compute_run_unit(instance: HostDashboardInstance, layout: ComputeLayout, identity_hex: String, argv: List, granted: Kibibyte) -> ComputeUnitRunReading { let systemd_run = match instance.toolchain.systemd_run { Present { value: p } => p as String Absent => "" } if systemd_run == "" { - ComputeUnitRun { + ComputeUnitRunExecuted { run: ComputeUnitRun { exec: ComputeExecFailed { exit_code: 127, stdout: "", stderr: "this instance's toolchain declares no systemd-run; the local provider runs only under systemd" }, summary: SystemdRunWaitSummary { result: ServiceResultNotReported { absence: SummaryNoInvocation }, memory_peak: MemoryPeakNotReported { absence: SummaryNoInvocation } }, - } + } } } else { let standing = summary_format_standing(version_line: match compute_exec(program: systemd_run, workdir: layout.checkout, args: ["--version"]) { ComputeExecOk { stdout: v, stderr: _ } => v ComputeExecFailed { exit_code: _, stdout: _, stderr: _ } => "" }) - let launcher = systemd_run_user_wait_arguments( + match systemd_run_user_wait_command( unit: compute_unit_name(identity_hex: identity_hex), properties: compute_unit_properties(layout: layout, granted: granted), setenv_bindings: [ - join(["CARGO_TARGET_DIR=", layout.target_dir], ""), - join(["RUSTC_WRAPPER=", sccache_installed_binary_path], ""), + SystemdSetenvBinding { name: "CARGO_TARGET_DIR", value: layout.target_dir }, + SystemdSetenvBinding { name: "RUSTC_WRAPPER", value: sccache_installed_binary_path }, ], command_argv: argv, - ) - let r = compute_exec( - program: systemd_run, - workdir: layout.checkout, - args: systemd_summary_printing_wait_argv(w: launcher), - ) - ComputeUnitRun { - exec: r, - summary: systemd_run_wait_summary(launcher: launcher, standing: standing, stderr: match r { - ComputeExecOk { stdout: _, stderr } => stderr - ComputeExecFailed { exit_code: _, stdout: _, stderr } => stderr - }), + ) { + SystemdSummaryPrintingWaitRefused { cause: why } => + ComputeUnitRunRefused { cause: why } + SystemdSummaryPrintingWaitReady { wait: launcher } => { + let r = compute_exec( + program: systemd_run, + workdir: layout.checkout, + args: systemd_summary_printing_wait_argv(w: launcher), + ) + ComputeUnitRunExecuted { run: ComputeUnitRun { + exec: r, + summary: systemd_run_wait_summary(launcher: launcher, standing: standing, stderr: match r { + ComputeExecOk { stdout: _, stderr } => stderr + ComputeExecFailed { exit_code: _, stdout: _, stderr } => stderr + }), + } } + } } } } @@ -528,35 +542,36 @@ fn unit_termination_decide(o: UnitObservations) -> UnitTermination { let population = unit_population(control_group: o.control_group, events: o.events) match population { PopulationOccupied { detail: d } => UnitPopulated { detail: d } - PopulationEmpty { detail: _ } => unit_termination_unoccupied(o: o, population: population) - PopulationUnknown { detail: _ } => unit_termination_unoccupied(o: o, population: population) + PopulationEmpty { detail: _ } => unit_termination_unconfirmed(o: o, population: population) + PopulationUnknown { detail: _ } => unit_termination_unconfirmed(o: o, population: population) } } -// The population did not positively refuse release: decide from load state and attempt completion. -fn unit_termination_unoccupied(o: UnitObservations, population: UnitPopulation) -> UnitTermination { - if o.load_state.queried && trim(s: o.load_state.value) == unit_load_state_absent_wire { - UnitAbsentAuthoritatively { detail: join(["LoadState=", unit_load_state_absent_wire, ", so the manager has no record of this unit"], "") } - } else if !o.attempt_completed { - UnitTerminationUnavailable { - detail: join([ - "the attempt did not complete, so its end is not established -- ", - match population { PopulationEmpty { detail: pd } => join([pd, ", but a realized cgroup can be empty between execs and emptiness does not cover a pending job"], "") PopulationUnknown { detail: pd } => pd PopulationOccupied { detail: pd } => pd }, - ], ""), - } - } else { - match population { - PopulationEmpty { detail: pd } => - UnitAttemptCompleted { +// The cgroup is not confirmed-occupied: the termination is decided from the manager's record and +// the attempt's completion, with the population's own reading as the located detail. +fn unit_termination_unconfirmed(o: UnitObservations, population: UnitPopulation) -> UnitTermination { + if o.load_state.queried && trim(s: o.load_state.value) == unit_load_state_absent_wire { + UnitAbsentAuthoritatively { detail: join(["LoadState=", unit_load_state_absent_wire, ", so the manager has no record of this unit"], "") } + } else if !o.attempt_completed { + UnitTerminationUnavailable { detail: join([ - "systemd-run --wait returned success under ", compute_kill_mode_property, "=", compute_kill_mode_value, - " and ", pd, + "the attempt did not complete, so its end is not established -- ", + match population { PopulationEmpty { detail: pd } => join([pd, ", but a realized cgroup can be empty between execs and emptiness does not cover a pending job"], "") PopulationUnknown { detail: pd } => pd PopulationOccupied { detail: pd } => pd }, ], ""), } - PopulationUnknown { detail: _ } => unit_termination_completed_unread(population: population) - PopulationOccupied { detail: _ } => unit_termination_completed_unread(population: population) - } - } + } else { + match population { + PopulationEmpty { detail: pd } => + UnitAttemptCompleted { + detail: join([ + "systemd-run --wait returned success under ", compute_kill_mode_property as String, "=", compute_kill_mode_value() as String, + " and ", pd, + ], ""), + } + PopulationUnknown { detail: _ } => unit_termination_completed_unread(population: population) + PopulationOccupied { detail: _ } => unit_termination_completed_unread(population: population) + } + } } // A completed wait whose population is not a confirmed-empty cgroup: the termination is not established. @@ -610,32 +625,33 @@ data unit_result_success_wire: String = "success" fn unit_lifecycle_decide(o: UnitLifecycleObservations) -> UnitLifecycle { match o.population { PopulationOccupied { detail: d } => UnitStillRunning { detail: d } - PopulationEmpty { detail: _ } => unit_lifecycle_unoccupied(o: o) - PopulationUnknown { detail: _ } => unit_lifecycle_unoccupied(o: o) - } -} - -// The population did not positively show the unit running: decide from the manager properties. -fn unit_lifecycle_unoccupied(o: UnitLifecycleObservations) -> UnitLifecycle { - if !o.load_state.queried { - UnitLifecycleUnknown { detail: "the user manager could not be queried for this unit's load state" } - } else if trim(s: o.load_state.value) == unit_load_state_absent_wire { - UnitTerminatedCauseUnknown { detail: join(["LoadState=", unit_load_state_absent_wire, ", so the manager has no record of this unit: a transient --collect unit that ran has been removed, and its cause went with it"], "") } - } else if !o.active_state.queried { - UnitLifecycleUnknown { detail: "the unit is known to the manager but its active state could not be read" } - } else if trim(s: o.active_state.value) == unit_active_state_failed_wire { - UnitTerminatedFailure { detail: join(["ActiveState=failed, Result=", if o.result.queried { trim(s: o.result.value) } else { "" }], "") } - } else if trim(s: o.active_state.value) != unit_active_state_inactive_wire { - UnitStillRunning { detail: join(["ActiveState=", trim(s: o.active_state.value)], "") } - } else if !o.result.queried { - UnitLifecycleUnknown { detail: "the unit is inactive but its Result could not be read, so whether it ended well is not established" } - } else if trim(s: o.result.value) == "" { - UnitLifecycleUnknown { detail: "the unit is inactive and the manager answered its Result with nothing, which is not a verdict" } - } else if trim(s: o.result.value) == unit_result_success_wire { - UnitTerminatedSuccess { detail: join(["ActiveState=", unit_active_state_inactive_wire, ", Result=", unit_result_success_wire], "") } - } else { - UnitTerminatedFailure { detail: join(["ActiveState=", unit_active_state_inactive_wire, ", Result=", trim(s: o.result.value)], "") } - } + PopulationEmpty { detail: _ } => unit_lifecycle_unpopulated(o: o) + PopulationUnknown { detail: _ } => unit_lifecycle_unpopulated(o: o) + } +} + +// The cgroup is not confirmed-occupied, so the unit's lifecycle is decided from the manager's +// load state, active state and result alone. +fn unit_lifecycle_unpopulated(o: UnitLifecycleObservations) -> UnitLifecycle { + if !o.load_state.queried { + UnitLifecycleUnknown { detail: "the user manager could not be queried for this unit's load state" } + } else if trim(s: o.load_state.value) == unit_load_state_absent_wire { + UnitTerminatedCauseUnknown { detail: join(["LoadState=", unit_load_state_absent_wire, ", so the manager has no record of this unit: a transient --collect unit that ran has been removed, and its cause went with it"], "") } + } else if !o.active_state.queried { + UnitLifecycleUnknown { detail: "the unit is known to the manager but its active state could not be read" } + } else if trim(s: o.active_state.value) == unit_active_state_failed_wire { + UnitTerminatedFailure { detail: join(["ActiveState=failed, Result=", if o.result.queried { trim(s: o.result.value) } else { "" }], "") } + } else if trim(s: o.active_state.value) != unit_active_state_inactive_wire { + UnitStillRunning { detail: join(["ActiveState=", trim(s: o.active_state.value)], "") } + } else if !o.result.queried { + UnitLifecycleUnknown { detail: "the unit is inactive but its Result could not be read, so whether it ended well is not established" } + } else if trim(s: o.result.value) == "" { + UnitLifecycleUnknown { detail: "the unit is inactive and the manager answered its Result with nothing, which is not a verdict" } + } else if trim(s: o.result.value) == unit_result_success_wire { + UnitTerminatedSuccess { detail: join(["ActiveState=", unit_active_state_inactive_wire, ", Result=", unit_result_success_wire], "") } + } else { + UnitTerminatedFailure { detail: join(["ActiveState=", unit_active_state_inactive_wire, ", Result=", trim(s: o.result.value)], "") } + } } // THE UNIT NAME COMES FROM THE ATTEMPT RECORD (criterion 2). This fold takes the name it is given @@ -1627,7 +1643,18 @@ type UnitEnding fn compute_unit_ending(summary: SystemdRunWaitSummary) -> UnitEnding { match summary.result { - ServiceResultRead { result: r } => match r { ServiceResultOomKill => EndingOomKill ServiceResultSuccess => EndingNotOomKill ServiceResultProtocol => EndingNotOomKill ServiceResultTimeout => EndingNotOomKill ServiceResultExitCode => EndingNotOomKill ServiceResultSignal => EndingNotOomKill ServiceResultCoreDump => EndingNotOomKill ServiceResultWatchdog => EndingNotOomKill ServiceResultStartLimitHit => EndingNotOomKill ServiceResultResources => EndingNotOomKill } + ServiceResultRead { result: r } => match r { + ServiceResultSuccess => EndingNotOomKill + ServiceResultProtocol => EndingNotOomKill + ServiceResultTimeout => EndingNotOomKill + ServiceResultExitCode => EndingNotOomKill + ServiceResultSignal => EndingNotOomKill + ServiceResultCoreDump => EndingNotOomKill + ServiceResultWatchdog => EndingNotOomKill + ServiceResultStartLimitHit => EndingNotOomKill + ServiceResultResources => EndingNotOomKill + ServiceResultOomKill => EndingOomKill +} ServiceResultUnrecognized { wire } => EndingUnclassifiable { cause: join(["the manager reported a result this corpus does not recognize: ", wire], "") } ServiceResultNotReported { absence } => EndingUnclassifiable { cause: summary_absence_wire(a: absence) } ServiceResultFormatNotGrounded { standing } => EndingUnclassifiable { cause: summary_format_standing_wire(s: standing) } @@ -1661,7 +1688,18 @@ fn compute_failed_unit_outcome(summary: SystemdRunWaitSummary, identity: String, // refuses upstream and never reaches here. fn compute_summary_is_oom_kill(summary: SystemdRunWaitSummary) -> Bool { match summary.result { - ServiceResultRead { result: r } => match r { ServiceResultOomKill => true ServiceResultSuccess => false ServiceResultProtocol => false ServiceResultTimeout => false ServiceResultExitCode => false ServiceResultSignal => false ServiceResultCoreDump => false ServiceResultWatchdog => false ServiceResultStartLimitHit => false ServiceResultResources => false } + ServiceResultRead { result: r } => match r { + ServiceResultSuccess => false + ServiceResultProtocol => false + ServiceResultTimeout => false + ServiceResultExitCode => false + ServiceResultSignal => false + ServiceResultCoreDump => false + ServiceResultWatchdog => false + ServiceResultStartLimitHit => false + ServiceResultResources => false + ServiceResultOomKill => true +} ServiceResultUnrecognized { wire: _ } => false ServiceResultNotReported { absence: _ } => false ServiceResultFormatNotGrounded { standing: _ } => false @@ -1755,7 +1793,14 @@ fn compute_spawn_and_collect( grant_was_ceiling: Bool, launched: AttemptGate, ) -> ComputeAttempt { - let unit_run = compute_run_unit(instance: instance, layout: layout, identity_hex: identity, argv: compute_command_argv(instance: instance, layout: layout, op: op, dependency_store: dependency_store), granted: granted) + match compute_run_unit(instance: instance, layout: layout, identity_hex: identity, argv: compute_command_argv(instance: instance, layout: layout, op: op, dependency_store: dependency_store), granted: granted) { + ComputeUnitRunRefused { cause: cause } => + ComputeAttempt { + outcome: WorkRefusedByInfrastructure { identity: identity, cause: UnitLaunchRefused { cause: cause } }, + summary: compute_unspawned_summary(), + gate: launched, + } + ComputeUnitRunExecuted { run: unit_run } => { let run = unit_run.exec let log = Filesystem.Write(path: layout.log_path, content: match run { ComputeExecOk { stdout, stderr } => join([stdout, stderr], "") @@ -1775,6 +1820,8 @@ fn compute_spawn_and_collect( summary: summary, gate: launched, } + } + } } // NOTHING WAS SPAWNED, SO NOTHING WAS SUMMARIZED: the same absence main's checkout-failure arm diff --git a/dag/gunbc/compute/work_request.dag b/dag/gunbc/compute/work_request.dag index 3abd7eb2a4e..e90a806a33b 100644 --- a/dag/gunbc/compute/work_request.dag +++ b/dag/gunbc/compute/work_request.dag @@ -3,6 +3,7 @@ module gunbc.compute.work_request import std.types { String, Bool, Int, List, NonEmptyStr, FilePath } import std.measure { Kibibyte, kibibyte_count } import std.content_hash { Fnv1a64Structural, content_hash_atom, content_hash_tagged_structural } +import extdeps.systemd.systemd_run { SystemdRunOptionWordsRefusedCause, systemd_run_option_words_refused_cause_detail } import extdeps.git.object_store { GitObjectId, git_object_id_wire_hex } import extdeps.languages.json.emit { JsonValue, json_object, json_kv, json_string, json_int, json_array, serialize_json, @@ -178,6 +179,7 @@ type WorkInfrastructureRefusal | SubjectUnresolved { detail: String } | UnitMemoryGrantExceeded { class: String, granted: Kibibyte, peak_upper: Kibibyte?, grant_was_ceiling: Bool } | UnitEndingUnclassifiable { exit_code: Int, cause: String } + | UnitLaunchRefused { cause: SystemdRunOptionWordsRefusedCause } fn work_outcome_identity(o: WorkOutcome) -> String { match o { @@ -246,6 +248,8 @@ fn work_refusal_detail(r: WorkInfrastructureRefusal) -> String { SubjectUnresolved { detail } => join(["subject unresolved: ", detail], "") UnitEndingUnclassifiable { exit_code, cause } => join(["the unit exited ", to_string(value: exit_code), " and its ending could not be classified, so it is neither reported as failed nor as grant-exceeded: ", cause], "") + UnitLaunchRefused { cause } => + join(["the unit was never launched: the systemd-run builder refused the invocation, so there is no exit to report: ", systemd_run_option_words_refused_cause_detail(cause: cause)], "") UnitMemoryGrantExceeded { class, granted, peak_upper, grant_was_ceiling } => join(["the unit was OOM-killed at its memory grant: class ", class, " was granted ", to_string(value: kibibyte_count(k: granted)), " KiB and its manager reported a peak of ", match peak_upper { Present { value: k } => join(["at most ", to_string(value: kibibyte_count(k: k)), " KiB"], "") Absent => "NOTHING (no Memory peak line was reported)" }, diff --git a/dag/gunbc/host/host_effect_nbd_proxy_serve.dag b/dag/gunbc/host/host_effect_nbd_proxy_serve.dag index dd80363d0af..4a14d61d932 100644 --- a/dag/gunbc/host/host_effect_nbd_proxy_serve.dag +++ b/dag/gunbc/host/host_effect_nbd_proxy_serve.dag @@ -19,6 +19,8 @@ import extdeps.bmc.webui.nbd_proxy_serve { bmcweb_session_token_env_ref, } import extdeps.exec.command { ArgvCommand, argv_words } +import extdeps.systemd.systemd_run { SystemdRunCommandReady, SystemdRunCommandRefused, systemd_run_transient_unit_command , systemd_run_option_words_refused_cause_detail, +} import extdeps.tools.nbdkit { nbdkit_readonly_file_serve_command } import extdeps.tools.websocat { websocat_nbd_proxy_bridge_command } import extdeps.filesystem.filesystem_io { @@ -108,10 +110,15 @@ fn host_effect_nbd_proxy_serve_systemd_run_transient( command_argv: List, transport: HostEffectTransport, ) -> String? { - match systemd_run_transient_read(transport: transport, unit: unit, properties: [], command_argv: command_argv) { - SystemdRunTransientStarted { stdout: _ } => Absent - SystemdRunTransientRefused { reason: why } => - Present { value: concat(concat("unit ", unit as String), concat(": ", why)) } + match systemd_run_transient_unit_command(unit: unit, properties: [], command_argv: command_argv) { + SystemdRunCommandRefused { cause: why } => + Present { value: concat(concat("unit ", unit as String), concat(": ", systemd_run_option_words_refused_cause_detail(cause: why))) } + SystemdRunCommandReady { command: command } => + match systemd_run_transient_read(transport: transport, command: command) { + SystemdRunTransientStarted { stdout: _ } => Absent + SystemdRunTransientRefused { reason: why } => + Present { value: concat(concat("unit ", unit as String), concat(": ", why)) } + } } } diff --git a/dag/gunbc/roadmap/roadmap_belt_actuate.dag b/dag/gunbc/roadmap/roadmap_belt_actuate.dag index 07db3b251b1..36423438348 100644 --- a/dag/gunbc/roadmap/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap/roadmap_belt_actuate.dag @@ -35,12 +35,13 @@ import gunbc.roadmap_dispatch_actuator { dispatch_credential_snapshot_path_for_instance, dispatch_credential_snapshot_discard, filter_dispatch_tmux_sessions, dispatch_node_id_from_session_name, dispatch_attempt_review_dir_for_instance, dispatch_attempt_review_verdict_path_for_instance, dispatch_attempt_review_verdict_basename, - dispatch_review_unit_name, dispatch_review_unit_argv, + dispatch_review_unit_name, dispatch_review_unit_command, dispatch_attempt_audit_dir_for_instance, dispatch_attempt_audit_verdict_path_for_instance, dispatch_attempt_audit_verdict_basename, - dispatch_audit_unit_name, dispatch_audit_unit_argv, - dispatch_attempt_supervisor_dir_for_instance, dispatch_supervisor_unit_name, dispatch_supervisor_unit_argv, - dispatch_supervisor_attempt_identity, + dispatch_audit_unit_name, dispatch_audit_unit_command, + dispatch_attempt_supervisor_dir_for_instance, dispatch_supervisor_unit_name, dispatch_supervisor_unit_command, + dispatch_supervisor_attempt_identity, dispatch_unit_exec_argv, } +import extdeps.systemd.systemd_run { systemd_run_option_words_refused_cause_detail } import extdeps.git { shape_git_merge_base_args } import gunbc.roadmap_authority { roadmap_acceptance_event_history_load, @@ -119,7 +120,7 @@ import gunbc.roadmap_dispatch_actuator { DispatchLiveSession, DispatchSpawnCommands, DispatchSessionContainer, TmuxSessionContainer, SystemdUnitContainer, - dispatch_worker_unit_properties_for_plan, dispatch_worker_unit_run, + dispatch_worker_unit_properties_for_plan, dispatch_worker_unit_command, dispatch_attempt_unit_name, attempt_unit_observation, worker_process_from_attempt_unit, @@ -1392,10 +1393,15 @@ fn belt_session_container_create( match session_placement_begin_launch(stores: held.stores, grant: held.grant) { SessionLaunchRefused { detail } => ExecStepFailed { step: "session-launch-gate", detail: detail } SessionLaunchAdmitted { grant: launched } => - belt_exec_steps( - steps: [BeltExecStep { step: "harness-unit-start", command: dispatch_worker_unit_run(instance: instance, plan: plan, granted: launched.amount, process_bounds: session_process_bounds) }], - workdir: workdir, - ) + match dispatch_worker_unit_command(instance: instance, plan: plan, granted: launched.amount, process_bounds: session_process_bounds) { + SystemdRunCommandRefused { cause: why } => + ExecStepFailed { step: "harness-unit-start", detail: systemd_run_option_words_refused_cause_detail(cause: why) } + SystemdRunCommandReady { command: command } => + belt_exec_steps( + steps: [BeltExecStep { step: "harness-unit-start", command: dispatch_unit_exec_argv(command: command, resolved_program: plan.systemd_run_program) }], + workdir: workdir, + ) + } } } } @@ -2678,9 +2684,7 @@ fn belt_supervisor_convene( match metering_roster_admit(instance: instance, node_id: nid, attempt_key: escalation.attempt_key, role: MeteredSupervisor { turn: escalation.turn }) { RosterAdmissionRefused { cause: roster_cause } => SpawnFailed { node_id: node.node as String, step: "supervisor-roster", detail: join(["the supervisor was not launched: ", roster_cause], ""), provider: provider_wire_label, launch: launch } RosterAdmitted => - match belt_exec( - cmd: dispatch_supervisor_unit_argv( - systemd_run_program: systemd_run, + match dispatch_supervisor_unit_command( instance: instance, node_id: nid, attempt_key: escalation.attempt_key, @@ -2695,33 +2699,42 @@ fn belt_supervisor_convene( history_line: belt_supervisor_history_line(instance: instance, node_id: nid, attempt_key: escalation.attempt_key, turn: escalation.turn), ), ), - ), - workdir: worktree, - ) { - ExecOk { stdout: _ } => - SupervisorConvened { - node_id: node.node as String, - unit: unit as String, - provider: provider_wire_label, - launch: launch, - } - ExecFailed { program: _, exit_code, stderr } => - SpawnFailed { - node_id: node.node as String, - step: "supervisor-unit-start", - detail: join(["the supervisor unit could not be started (exit ", to_string(exit_code), "): ", stderr], ""), - provider: provider_wire_label, - launch: launch, - } - ExecRefused { program: _, reason } => - SpawnFailed { - node_id: node.node as String, - step: "supervisor-unit-start", - detail: reason, - provider: provider_wire_label, - launch: launch, - } - } + ) { + SystemdRunCommandRefused { cause: why } => + SpawnFailed { + node_id: node.node as String, + step: "supervisor-unit-start", + detail: systemd_run_option_words_refused_cause_detail(cause: why), + provider: provider_wire_label, + launch: launch, + } + SystemdRunCommandReady { command: command } => + match belt_exec(cmd: dispatch_unit_exec_argv(command: command, resolved_program: systemd_run), workdir: worktree) { + ExecOk { stdout: _ } => + SupervisorConvened { + node_id: node.node as String, + unit: unit as String, + provider: provider_wire_label, + launch: launch, + } + ExecFailed { program: _, exit_code, stderr } => + SpawnFailed { + node_id: node.node as String, + step: "supervisor-unit-start", + detail: join(["the supervisor unit could not be started (exit ", to_string(exit_code), "): ", stderr], ""), + provider: provider_wire_label, + launch: launch, + } + ExecRefused { program: _, reason } => + SpawnFailed { + node_id: node.node as String, + step: "supervisor-unit-start", + detail: reason, + provider: provider_wire_label, + launch: launch, + } + } + } } } } @@ -10768,14 +10781,16 @@ fn belt_audit_spawn(instance: HostDashboardInstance, progress: WorkflowAttemptPr match metering_roster_admit(instance: instance, node_id: node_id, attempt_key: progress.attempt_key, role: MeteredAuditor { head: head as NonEmptyStr }) { RosterAdmissionRefused { cause: roster_cause } => ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: join(["the auditor was not launched: ", roster_cause], "") } RosterAdmitted => - match belt_exec( - cmd: dispatch_audit_unit_argv(systemd_run_program: systemd_run, instance: instance, node_id: node_id, attempt_key: progress.attempt_key, worktree_path: worktree, brief: brief, head_sha: head), - workdir: worktree, - ) { - ExecOk { stdout: _ } => ReviewSpawned { node_id: progress.node_id, criterion: goal_audit_criterion_key, unit: unit as String } - ExecFailed { program: _, exit_code, stderr } => ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: join(["the auditor unit could not be started (exit ", to_string(exit_code), "): ", stderr], "") } - ExecRefused { program: _, reason } => ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: reason } - } + match dispatch_audit_unit_command(instance: instance, node_id: node_id, attempt_key: progress.attempt_key, worktree_path: worktree, brief: brief, head_sha: head) { + SystemdRunCommandRefused { cause: why } => + ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: systemd_run_option_words_refused_cause_detail(cause: why) } + SystemdRunCommandReady { command: command } => + match belt_exec(cmd: dispatch_unit_exec_argv(command: command, resolved_program: systemd_run), workdir: worktree) { + ExecOk { stdout: _ } => ReviewSpawned { node_id: progress.node_id, criterion: goal_audit_criterion_key, unit: unit as String } + ExecFailed { program: _, exit_code, stderr } => ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: join(["the auditor unit could not be started (exit ", to_string(exit_code), "): ", stderr], "") } + ExecRefused { program: _, reason } => ReviewFailed { node_id: progress.node_id, criterion: goal_audit_criterion_key, detail: reason } + } + } } } } @@ -11027,14 +11042,16 @@ fn belt_review_spawn(instance: HostDashboardInstance, node_id: String, attempt_k match metering_roster_admit(instance: instance, node_id: nid, attempt_key: attempt_key, role: MeteredReviewer { criterion_key: key as NonEmptyStr, head: head as NonEmptyStr }) { RosterAdmissionRefused { cause: roster_cause } => ReviewFailed { node_id: node_id, criterion: call.id, detail: join(["the reviewer was not launched: ", roster_cause], "") } RosterAdmitted => - match belt_exec( - cmd: dispatch_review_unit_argv(systemd_run_program: systemd_run, instance: instance, node_id: nid, attempt_key: attempt_key, worktree_path: worktree, brief: brief, head_sha: head, criterion_key: key), - workdir: worktree, - ) { - ExecOk { stdout: _ } => ReviewSpawned { node_id: node_id, criterion: call.id, unit: unit as String } - ExecFailed { program: _, exit_code, stderr } => ReviewFailed { node_id: node_id, criterion: call.id, detail: join(["the reviewer unit could not be started (exit ", to_string(exit_code), "): ", stderr], "") } - ExecRefused { program: _, reason } => ReviewFailed { node_id: node_id, criterion: call.id, detail: reason } - } + match dispatch_review_unit_command(instance: instance, node_id: nid, attempt_key: attempt_key, worktree_path: worktree, brief: brief, head_sha: head, criterion_key: key) { + SystemdRunCommandRefused { cause: why } => + ReviewFailed { node_id: node_id, criterion: call.id, detail: systemd_run_option_words_refused_cause_detail(cause: why) } + SystemdRunCommandReady { command: command } => + match belt_exec(cmd: dispatch_unit_exec_argv(command: command, resolved_program: systemd_run), workdir: worktree) { + ExecOk { stdout: _ } => ReviewSpawned { node_id: node_id, criterion: call.id, unit: unit as String } + ExecFailed { program: _, exit_code, stderr } => ReviewFailed { node_id: node_id, criterion: call.id, detail: join(["the reviewer unit could not be started (exit ", to_string(exit_code), "): ", stderr], "") } + ExecRefused { program: _, reason } => ReviewFailed { node_id: node_id, criterion: call.id, detail: reason } + } + } } } } diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index cd60f91d0ac..73023923c91 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -51,8 +51,17 @@ import extdeps.git { shape_git_worktree_add_argv, shape_git_worktree_add_detache import std.measure { ByteSize, byte_size_count, Kibibyte } import std.algebra { trim } import extdeps.systemd.systemctl -import extdeps.systemd { systemd_unit_property_wire, SystemdUnitProperty, LoadState, SubState, ExecMainStatus } -import extdeps.systemd.systemd_run { systemd_run_user_transient_arguments, systemd_run_property } +import extdeps.systemd { + systemd_unit_property_wire, SystemdUnitProperty, LoadState, SubState, ExecMainStatus, + MemoryMax, WorkingDirectoryProperty, StandardOutputProperty, StandardErrorProperty, + ProtectSystemProperty, ProtectHomeProperty, PrivateTmpProperty, ReadWritePathsProperty, + RemainAfterExitProperty, +} +import extdeps.exec.command { ArgvCommand } +import extdeps.systemd.systemd_run { + SystemdRunCommandReading, SystemdRunCommandReady, SystemdRunCommandRefused, + systemd_run_user_transient_command, systemd_run_command_argument_words, +} import gunbc.compute.unit_bounds { UnitProcessBounds, compute_grant_unit_properties } import extdeps.tmux { shape_tmux_new_session_argv, @@ -1473,10 +1482,14 @@ fn dispatch_attempt_review_verdict_basename(head_sha: String, criterion_key: Str join([head_sha, ".", criterion_key, ".json"], "") } -// ONE EVENTS STREAM PER LAUNCH. A reviewer is launched per (criterion, head), an auditor per head and -// a supervisor per escalation turn; a path shared across launches let a later launch rewrite an -// earlier one's stream, which erased that launch's token usage from the per-attempt metering -// (gunbc.roadmap_attempt_metering). The launch discriminator is in the name. +fn dispatch_attempt_audit_events_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, head_sha: String) -> String { + join([dispatch_attempt_audit_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/", head_sha, ".provider-events.jsonl"], "") +} + +fn dispatch_attempt_supervisor_events_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, turn: Int) -> String { + join([dispatch_attempt_supervisor_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/t", to_string(value: turn), ".provider-events.jsonl"], "") +} + fn dispatch_attempt_review_events_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, criterion_key: String, head_sha: String) -> String { join([dispatch_attempt_review_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/", criterion_key, "@", head_sha, ".provider-events.jsonl"], "") } @@ -1505,17 +1518,17 @@ fn dispatch_review_unit_properties( working_directory: String, log_path: String, review_dir: String, -) -> List { +) -> List { [ - systemd_run_property(name: "MemoryMax", value: to_string(value: byte_size_count(instance.dispatch_worker_memory_max))), - systemd_run_property(name: "WorkingDirectory", value: working_directory), - systemd_run_property(name: "StandardOutput", value: join(["append:", log_path], "")), - systemd_run_property(name: "StandardError", value: join(["append:", log_path], "")), - systemd_run_property(name: "ProtectSystem", value: "strict"), - systemd_run_property(name: "ProtectHome", value: "read-only"), - systemd_run_property(name: "PrivateTmp", value: "yes"), - systemd_run_property(name: "ReadWritePaths", value: review_dir), - systemd_run_property(name: "RemainAfterExit", value: "yes"), + SystemdRunProperty { property: MemoryMax, value: to_string(value: byte_size_count(instance.dispatch_worker_memory_max)) as NonEmptyStr }, + SystemdRunProperty { property: WorkingDirectoryProperty, value: working_directory as NonEmptyStr }, + SystemdRunProperty { property: StandardOutputProperty, value: join(["append:", log_path], "") as NonEmptyStr }, + SystemdRunProperty { property: StandardErrorProperty, value: join(["append:", log_path], "") as NonEmptyStr }, + SystemdRunProperty { property: ProtectSystemProperty, value: "strict" as NonEmptyStr }, + SystemdRunProperty { property: ProtectHomeProperty, value: "read-only" as NonEmptyStr }, + SystemdRunProperty { property: PrivateTmpProperty, value: "yes" as NonEmptyStr }, + SystemdRunProperty { property: ReadWritePathsProperty, value: review_dir as NonEmptyStr }, + SystemdRunProperty { property: RemainAfterExitProperty, value: "yes" as NonEmptyStr }, ] } @@ -1557,8 +1570,16 @@ fn gunbc_harness_review_argv( // fleet ssh context's safe-segment law (gunbc.fleet_known_hosts_anchor fleet_ssh_attempt_identity) // demands of the attempt identity it consumes as a path segment -- the pre-fix reviewer built it // from the absolute verdict path and every lane's seat bind refused before any ssh leg. -fn dispatch_review_unit_argv( - systemd_run_program: FilePath, +// THE BELT LAYER RUNS THE RESOLVED PROGRAM, not the spelling: the instance toolchain declares where +// systemd-run lives, and that declaration is the only thing the invoking host is allowed to assert +// about an unobserved host's program location (extdeps.exec.command +// program_spelling_is_an_observation_claim). The option and command words are untouched -- they are +// the one projection's; only the head is substituted. +fn dispatch_unit_exec_argv(command: ArgvCommand, resolved_program: FilePath) -> HostExecArgv { + host_exec_argv(program: resolved_program as String, args: systemd_run_command_argument_words(command: command)) +} + +fn dispatch_review_unit_command( instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, @@ -1566,30 +1587,27 @@ fn dispatch_review_unit_argv( brief: String, head_sha: String, criterion_key: String, -) -> HostExecArgv { - host_exec_argv( - program: systemd_run_program as String, - args: systemd_run_user_transient_arguments( - unit: dispatch_review_unit_name(node_id: node_id as String, attempt_key: attempt_key, criterion_key: criterion_key), - properties: dispatch_review_unit_properties( - instance: instance, - working_directory: instance.repo_root as String, - log_path: dispatch_attempt_review_log_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key), - review_dir: dispatch_attempt_review_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), - ), - setenv_bindings: [], - command_argv: gunbc_harness_review_argv( - gunbc_program: instance.serve_binary as String, - repo_root: instance.repo_root as String, - worktree_path: worktree_path, - brief: brief, - timeout_program: instance.toolchain.timeout as String, - request_scratch_path: dispatch_attempt_review_request_scratch_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key), - events_path: dispatch_attempt_review_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key, head_sha: head_sha), - verdict_dir: dispatch_attempt_review_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), - verdict_name: dispatch_attempt_review_verdict_basename(head_sha: head_sha, criterion_key: criterion_key), - attempt_identity: dispatch_reviewer_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, criterion_key: criterion_key, head_sha: head_sha), - ), +) -> SystemdRunCommandReading { + systemd_run_user_transient_command( + unit: dispatch_review_unit_name(node_id: node_id as String, attempt_key: attempt_key, criterion_key: criterion_key), + properties: dispatch_review_unit_properties( + instance: instance, + working_directory: instance.repo_root as String, + log_path: dispatch_attempt_review_log_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key), + review_dir: dispatch_attempt_review_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), + ), + setenv_bindings: [], + command_argv: gunbc_harness_review_argv( + gunbc_program: instance.serve_binary as String, + repo_root: instance.repo_root as String, + worktree_path: worktree_path, + brief: brief, + timeout_program: instance.toolchain.timeout as String, + request_scratch_path: dispatch_attempt_review_request_scratch_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key), + events_path: dispatch_attempt_review_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, criterion_key: criterion_key, head_sha: head_sha), + verdict_dir: dispatch_attempt_review_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), + verdict_name: dispatch_attempt_review_verdict_basename(head_sha: head_sha, criterion_key: criterion_key), + attempt_identity: dispatch_reviewer_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, criterion_key: criterion_key, head_sha: head_sha), ), ) } @@ -1610,54 +1628,51 @@ fn dispatch_attempt_audit_verdict_path_for_instance(instance: HostDashboardInsta join([dispatch_attempt_audit_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/", dispatch_attempt_audit_verdict_basename(head_sha: head_sha)], "") } -fn dispatch_attempt_audit_events_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, head_sha: String) -> String { - join([dispatch_attempt_audit_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/", head_sha, ".provider-events.jsonl"], "") -} - fn dispatch_audit_unit_name(node_id: String, attempt_key: String) -> NonEmptyStr { join(["gunbc-audit-", node_id, "-", attempt_key, ".service"], "") as NonEmptyStr } data gunbc_harness_auditor_function: String = "harness_auditor_cli" -fn dispatch_audit_unit_argv( - systemd_run_program: FilePath, +// Each launch writes its events to its OWN per-launch path (head-hash for the auditor, turn for +// the supervisor): a path shared across launches let a later launch rewrite an earlier one's +// stream, which erased that launch's token usage from the per-attempt metering — the metering +// reader opens exactly these per-launch paths (roadmap_attempt_metering), so the writer and the +// reader must name the same path expression, which the actuator witness pins on both sides. +fn dispatch_audit_unit_command( instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, worktree_path: String, brief: String, head_sha: String, -) -> HostExecArgv { +) -> SystemdRunCommandReading { let dir = dispatch_attempt_audit_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key) - host_exec_argv( - program: systemd_run_program as String, - args: systemd_run_user_transient_arguments( - unit: dispatch_audit_unit_name(node_id: node_id as String, attempt_key: attempt_key), - properties: dispatch_review_unit_properties( - instance: instance, - working_directory: instance.repo_root as String, - log_path: join([dir, "/auditor-stdout.log"], ""), - review_dir: dir, - ), - setenv_bindings: [], - command_argv: [ - instance.serve_binary as String, - "run", - "--source-root", join([instance.repo_root as String, "/dag"], ""), - "--source-root", join([instance.repo_root as String, "/src/v2"], ""), - "--entry", join([instance.repo_root as String, "/", gunbc_harness_worker_entry], ""), - "--function", gunbc_harness_auditor_function, - "--arg", join(["worktree=", worktree_path], ""), - "--arg", join(["brief=", brief], ""), - "--arg", join(["timeout_program=", instance.toolchain.timeout as String], ""), - "--arg", join(["request_scratch_path=", dir, "/harness-request.json"], ""), - "--arg", join(["events_path=", dispatch_attempt_audit_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, head_sha: head_sha)], ""), - "--arg", join(["verdict_dir=", dir], ""), - "--arg", join(["verdict_name=", dispatch_attempt_audit_verdict_basename(head_sha: head_sha)], ""), - "--arg", join(["attempt_identity=", dispatch_auditor_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, head_sha: head_sha)], ""), - ], + systemd_run_user_transient_command( + unit: dispatch_audit_unit_name(node_id: node_id as String, attempt_key: attempt_key), + properties: dispatch_review_unit_properties( + instance: instance, + working_directory: instance.repo_root as String, + log_path: join([dir, "/auditor-stdout.log"], ""), + review_dir: dir, ), + setenv_bindings: [], + command_argv: [ + instance.serve_binary as String, + "run", + "--source-root", join([instance.repo_root as String, "/dag"], ""), + "--source-root", join([instance.repo_root as String, "/src/v2"], ""), + "--entry", join([instance.repo_root as String, "/", gunbc_harness_worker_entry], ""), + "--function", gunbc_harness_auditor_function, + "--arg", join(["worktree=", worktree_path], ""), + "--arg", join(["brief=", brief], ""), + "--arg", join(["timeout_program=", instance.toolchain.timeout as String], ""), + "--arg", join(["request_scratch_path=", dir, "/harness-request.json"], ""), + "--arg", join(["events_path=", dispatch_attempt_audit_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, head_sha: head_sha)], ""), + "--arg", join(["verdict_dir=", dir], ""), + "--arg", join(["verdict_name=", dispatch_attempt_audit_verdict_basename(head_sha: head_sha)], ""), + "--arg", join(["attempt_identity=", dispatch_auditor_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, head_sha: head_sha)], ""), + ], ) } @@ -1676,10 +1691,6 @@ fn dispatch_supervisor_unit_name(node_id: String, attempt_key: String, turn: Int join(["gunbc-supervisor-", node_id, "-", attempt_key, "-t", to_string(value: turn), ".service"], "") as NonEmptyStr } -fn dispatch_attempt_supervisor_events_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, turn: Int) -> String { - join([dispatch_attempt_supervisor_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/t", to_string(value: turn), ".provider-events.jsonl"], "") -} - fn dispatch_supervisor_attempt_identity(node_id: String, attempt_key: String, turn: Int) -> String { join(["supervisor:", node_id, "-", attempt_key, "-t", to_string(value: turn)], "") } @@ -1691,44 +1702,40 @@ data gunbc_harness_supervisor_function: String = "harness_supervisor_cli" // record convene at most one session -- a second convene over the same record names the same unit, // and the belt's liveness gate reads that. The worktree stays read-only to the session, as the // reviewer's is: the supervisor adjudicates the checkpoint; it does not edit it. -fn dispatch_supervisor_unit_argv( - systemd_run_program: FilePath, +fn dispatch_supervisor_unit_command( instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, turn: Int, worktree_path: String, brief: String, -) -> HostExecArgv { +) -> SystemdRunCommandReading { let dir = dispatch_attempt_supervisor_dir_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key) - host_exec_argv( - program: systemd_run_program as String, - args: systemd_run_user_transient_arguments( - unit: dispatch_supervisor_unit_name(node_id: node_id as String, attempt_key: attempt_key, turn: turn), - properties: dispatch_review_unit_properties( - instance: instance, - working_directory: instance.repo_root as String, - log_path: join([dir, "/supervisor-stdout.log"], ""), - review_dir: dir, - ), - setenv_bindings: [], - command_argv: [ - instance.serve_binary as String, - "run", - "--source-root", join([instance.repo_root as String, "/dag"], ""), - "--source-root", join([instance.repo_root as String, "/src/v2"], ""), - "--entry", join([instance.repo_root as String, "/", gunbc_harness_worker_entry], ""), - "--function", gunbc_harness_supervisor_function, - "--arg", join(["worktree=", worktree_path], ""), - "--arg", join(["brief=", brief], ""), - "--arg", join(["timeout_program=", instance.toolchain.timeout as String], ""), - "--arg", join(["request_scratch_path=", dir, "/harness-request.json"], ""), - "--arg", join(["events_path=", dispatch_attempt_supervisor_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, turn: turn)], ""), - "--arg", join(["verdict_dir=", dir], ""), - "--arg", join(["verdict_name=", supervisor_verdict_basename as String], ""), - "--arg", join(["attempt_identity=", dispatch_supervisor_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, turn: turn)], ""), - ], + systemd_run_user_transient_command( + unit: dispatch_supervisor_unit_name(node_id: node_id as String, attempt_key: attempt_key, turn: turn), + properties: dispatch_review_unit_properties( + instance: instance, + working_directory: instance.repo_root as String, + log_path: join([dir, "/supervisor-stdout.log"], ""), + review_dir: dir, ), + setenv_bindings: [], + command_argv: [ + instance.serve_binary as String, + "run", + "--source-root", join([instance.repo_root as String, "/dag"], ""), + "--source-root", join([instance.repo_root as String, "/src/v2"], ""), + "--entry", join([instance.repo_root as String, "/", gunbc_harness_worker_entry], ""), + "--function", gunbc_harness_supervisor_function, + "--arg", join(["worktree=", worktree_path], ""), + "--arg", join(["brief=", brief], ""), + "--arg", join(["timeout_program=", instance.toolchain.timeout as String], ""), + "--arg", join(["request_scratch_path=", dir, "/harness-request.json"], ""), + "--arg", join(["events_path=", dispatch_attempt_supervisor_events_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, turn: turn)], ""), + "--arg", join(["verdict_dir=", dir], ""), + "--arg", join(["verdict_name=", supervisor_verdict_basename as String], ""), + "--arg", join(["attempt_identity=", dispatch_supervisor_attempt_identity(node_id: node_id as String, attempt_key: attempt_key, turn: turn)], ""), + ], ) } @@ -2255,11 +2262,8 @@ data cursor_provider_spawn_unwired_reason: NonEmptyStr = "cursor dispatch inner // mint is one function: the unit's --arg and the attempt state's binding file cannot drift apart. // node id plus attempt key is the same uniqueness (the key hashes the id and the spawn instant) in // a form that is a safe path segment by construction. -// A REVIEWER'S OR AUDITOR'S LAUNCH IDENTITY NAMES ONE LAUNCH, rendered injectively. The seat ledger -// and the per-attempt metering (gunbc.roadmap_attempt_metering) key on it, so two launches -- the same -// criterion at two heads, or the auditor at two heads -- may never render the same text: each part is -// length-prefixed and the head is a part, as it is of their events streams. The result is a safe -// path segment (no '/'). +// Length-prefixed identity parts (main #13208): each part becomes "." so a part +// carrying a separator still yields a safe path segment. fn dispatch_launch_identity(kind: String, parts: List) -> String { join([kind, ":", join(map(parts, p => join([to_string(string_length(s: p)), ".", p], "")), ".")], "") } @@ -2655,17 +2659,17 @@ fn dispatch_worker_unit_properties( worker_log_path: String, granted: Kibibyte, process_bounds: UnitProcessBounds, -) -> List { +) -> List { concat( compute_grant_unit_properties(granted: granted, working_directory: working_directory, process_bounds: Present { value: process_bounds }), [ - systemd_run_property(name: "StandardOutput", value: join(["append:", worker_log_path], "")), - systemd_run_property(name: "StandardError", value: join(["append:", worker_log_path], "")), - systemd_run_property(name: "ProtectSystem", value: "strict"), - systemd_run_property(name: "ProtectHome", value: "read-only"), - systemd_run_property(name: "PrivateTmp", value: "yes"), - systemd_run_property(name: "ReadWritePaths", value: dispatch_worker_writable_paths(instance: instance)), - systemd_run_property(name: "RemainAfterExit", value: "yes"), + SystemdRunProperty { property: StandardOutputProperty, value: join(["append:", worker_log_path], "") as NonEmptyStr }, + SystemdRunProperty { property: StandardErrorProperty, value: join(["append:", worker_log_path], "") as NonEmptyStr }, + SystemdRunProperty { property: ProtectSystemProperty, value: "strict" as NonEmptyStr }, + SystemdRunProperty { property: ProtectHomeProperty, value: "read-only" as NonEmptyStr }, + SystemdRunProperty { property: PrivateTmpProperty, value: "yes" as NonEmptyStr }, + SystemdRunProperty { property: ReadWritePathsProperty, value: dispatch_worker_writable_paths(instance: instance) as NonEmptyStr }, + SystemdRunProperty { property: RemainAfterExitProperty, value: "yes" as NonEmptyStr }, ], ) } @@ -2706,7 +2710,7 @@ type DispatchWorkerUnitPlan { inner_argv: List } -fn dispatch_worker_unit_properties_for_plan(instance: HostDashboardInstance, plan: DispatchWorkerUnitPlan, granted: Kibibyte, process_bounds: UnitProcessBounds) -> List { +fn dispatch_worker_unit_properties_for_plan(instance: HostDashboardInstance, plan: DispatchWorkerUnitPlan, granted: Kibibyte, process_bounds: UnitProcessBounds) -> List { dispatch_worker_unit_properties( instance: instance, working_directory: plan.working_directory, @@ -2716,15 +2720,12 @@ fn dispatch_worker_unit_properties_for_plan(instance: HostDashboardInstance, pla ) } -fn dispatch_worker_unit_run(instance: HostDashboardInstance, plan: DispatchWorkerUnitPlan, granted: Kibibyte, process_bounds: UnitProcessBounds) -> HostExecArgv { - host_exec_argv( - program: plan.systemd_run_program as String, - args: systemd_run_user_transient_arguments( - unit: plan.unit, - properties: dispatch_worker_unit_properties_for_plan(instance: instance, plan: plan, granted: granted, process_bounds: process_bounds), - setenv_bindings: [], - command_argv: plan.inner_argv, - ), +fn dispatch_worker_unit_command(instance: HostDashboardInstance, plan: DispatchWorkerUnitPlan, granted: Kibibyte, process_bounds: UnitProcessBounds) -> SystemdRunCommandReading { + systemd_run_user_transient_command( + unit: plan.unit, + properties: dispatch_worker_unit_properties_for_plan(instance: instance, plan: plan, granted: granted, process_bounds: process_bounds), + setenv_bindings: [], + command_argv: plan.inner_argv, ) } diff --git a/dag/gunbc/runner/runner_host_unit_slot.dag b/dag/gunbc/runner/runner_host_unit_slot.dag index 9cd2719b33b..f89ff66f4d8 100644 --- a/dag/gunbc/runner/runner_host_unit_slot.dag +++ b/dag/gunbc/runner/runner_host_unit_slot.dag @@ -9,7 +9,11 @@ import extdeps.github.actions_runner { } import gunbc.managed_host { ManagedHostBinding, ManagedHostBindingStanding, ManagedHostBound } import gunbc.runner_slot_desired { gunbc_runner_slot_desired } -import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_argv, ephemeral_slot_labels } +import extdeps.exec.command { ArgvCommand } +import extdeps.systemd.systemd_run { + SystemdRunCommandReady, SystemdRunCommandRefused, SystemdRunOptionWordsRefusedCause, +} +import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_command, ephemeral_slot_labels } // THE SECOND BINDING OF ONE JIT MINT: A TRANSIENT systemd UNIT ON A LIVE MANAGED HOST. // @@ -23,8 +27,10 @@ import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, epheme // unchanged rather than a re-coined cause. No mtcollins1_* module is named here; mtcollins1 reaches // this fold as the gunbc.managed_host managed_hosts row and nothing else. // -// The unit, its systemd-run argv and its labels are the qualification route's own derivations -// (gunbc.runner_throughput_qualification_route ephemeral_slot_unit / ephemeral_slot_argv / +// The unit, its systemd-run command (the full typed ArgvCommand -- program head included, so a +// launch seam reading this field gets exactly the invocation the route stages, not a headless +// tail it would have to re-head) and its labels are the qualification route's own derivations +// (gunbc.runner_throughput_qualification_route ephemeral_slot_unit / ephemeral_slot_command / // ephemeral_slot_labels), so the slot the mint registers is by construction the slot the route // stages -- one spelling of the attempt label, not two. // @@ -39,13 +45,14 @@ type HostUnitSlot sole_constructor { workflow: NonEmptyStr unit: NonEmptyStr labels: List - argv: List + argv: ArgvCommand runner: ActionsRunnerBinaryArtifact } type HostUnitSlotRefusal = HostUnitSlotHostNotBound { standing: ManagedHostBindingStanding } | HostUnitSlotRunnerBinaryUnpublished { binary_release: ActionsRunnerRelease } + | HostUnitSlotSystemdRunRefused { cause: SystemdRunOptionWordsRefusedCause } type HostUnitSlotStanding = HostUnitSlotBound { slot: HostUnitSlot } @@ -69,16 +76,21 @@ fn host_unit_slot_for(host: ManagedHostBindingStanding, attempt: NonEmptyStr, wo Absent => HostUnitSlotRefused { refusal: HostUnitSlotRunnerBinaryUnpublished { binary_release: host_unit_slot_runner_release } } Present { value: artifact } => { let unit = ephemeral_slot_unit(host: b.host.host, attempt: attempt) - HostUnitSlotBound { - slot: HostUnitSlot { - host: b, - attempt: attempt, - workflow: workflow, - unit: unit, - labels: ephemeral_slot_labels(host: b.host.host, attempt: attempt), - argv: ephemeral_slot_argv(unit: unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()), - runner: artifact, - }, + match ephemeral_slot_command(unit: unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()) { + SystemdRunCommandRefused { cause: why } => + HostUnitSlotRefused { refusal: HostUnitSlotSystemdRunRefused { cause: why } } + SystemdRunCommandReady { command: command } => + HostUnitSlotBound { + slot: HostUnitSlot { + host: b, + attempt: attempt, + workflow: workflow, + unit: unit, + labels: ephemeral_slot_labels(host: b.host.host, attempt: attempt), + argv: command, + runner: artifact, + }, + } } } } diff --git a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag index 61563018aad..32513bd0fb5 100644 --- a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag +++ b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag @@ -75,7 +75,12 @@ import extdeps.tools.e2fsprogs { decode_dumpe2fs_superblock, Ext2SuperblockRead, Ext2SuperblockUnreadable, ext2_superblock_magic, } import extdeps.systemd.journalctl -import extdeps.systemd.systemd_run { systemd_run_property_argv, SystemdRunProperty } +import extdeps.exec.command { argv_words } +import extdeps.systemd.systemd_run { + SystemdRunProperty, SystemdRunCommandReady, SystemdRunCommandRefused, + systemd_run_transient_unit_command, systemd_run_transient_retained_unit_command, systemd_run_command_argument_words, + systemd_run_option_words_refused_cause_detail, +} import gunbc.runner_microvm_lifecycle { CgroupSubtreeReadback, CgroupSubtreeEmpty, CgroupSubtreePopulated, CgroupSubtreeReadFailed, ResourceReadback, ResourceAbsent, ResourcePresent, ResourceUnreadable, @@ -1145,7 +1150,6 @@ fn launch_attempt(attempt: MicroVmAttempt, invocation_id: NonEmptyStr, plan: Att } } -// Re-read time after staging. Preparing an image never extends the absolute lease. fn attempt_jailer_at_launch(receipt: AttemptStagingReceipt) -> ArgvCommand? { match receipt.lease_expires_unix { Absent => Present { value: receipt.jailer } @@ -1162,23 +1166,29 @@ fn attempt_jailer_at_launch(receipt: AttemptStagingReceipt) -> ArgvCommand? { } } +// The projection refuses a word the wire cannot carry; there is no started unit to observe, so the +// refusal takes the same failure arm a failed start takes, carrying the refusal's reason. fn run_jailer_in_cell(attempt: MicroVmAttempt, invocation_id: NonEmptyStr, receipt: AttemptStagingReceipt) -> AttemptLaunch { let unit = runner_microvm_attempt_unit_name(attempt: attempt) match attempt_jailer_at_launch(receipt: receipt) { Absent => VmmStartFailed { unit: unit, detail: "lease expired during staging or launch clock is unreadable" } - Present { value: jailer } => { - let started = systemd.SystemdRun.RunTransientRetained( + Present { value: jailer } => + match systemd_run_transient_retained_unit_command( unit: unit, - property_argv: systemd_run_property_argv(properties: concat([ + properties: concat([ runner_microvm_attempt_slice_property(attempt: attempt), runner_microvm_attempt_part_of_property(attempt: attempt), attempt_invocation_env_setting(invocation_id: invocation_id), - ], receipt.runtime_properties)), - command_argv: concat([jailer.program.invocation as String], jailer.arguments), - ) - if started.success { VmmStarted { unit: unit, receipt: receipt } } - else { VmmStartFailed { unit: unit, detail: trim(s: started.stderr) } } - } + ], receipt.runtime_properties), + command_argv: argv_words(command: jailer), + ) { + SystemdRunCommandRefused { cause: why } => VmmStartFailed { unit: unit, detail: systemd_run_option_words_refused_cause_detail(cause: why) } + SystemdRunCommandReady { command: command } => { + let started = systemd.SystemdRun.RunTransient(command_argv: systemd_run_command_argument_words(command: command)) + if started.success { VmmStarted { unit: unit, receipt: receipt } } + else { VmmStartFailed { unit: unit, detail: trim(s: started.stderr) } } + } + } } } diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index 92b7cc0e19d..6cbd6a16c41 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, decl_ref } import product.host_identity { HostIdentity } import gunbc.runner.runner_host_filtered_egress { EgressPolicyShape, runner_host_egress_policy } import extdeps.systemd { MemoryMax, MemoryHigh, MemorySwapMax, TasksMax, CPUWeight, CPUQuota } -import extdeps.systemd.systemd_run { SystemdRunProperty, systemd_run_transient_wait_unit_argv } +import extdeps.systemd.systemd_run { SystemdRunProperty, SystemdRunCommandReading, systemd_run_transient_wait_unit_command } import gunbc.fleet_host_identity { operator_host_mtcollins1 } import gunbc.runner_slot_allocation { runner_host_label, runner_label_set_union } import gunbc.runner_slot_desired { RunnerSlotDesired, RunnerSlotCpuQuota, CpuQuotaResolved, SlotCpuQuotaUnbounded, gunbc_runner_slot_desired } @@ -117,8 +117,8 @@ fn ephemeral_slot_properties(desired: RunnerSlotDesired) -> List) -> List { - systemd_run_transient_wait_unit_argv(unit: unit, properties: ephemeral_slot_properties(desired: desired), command_argv: command_argv) +fn ephemeral_slot_command(unit: NonEmptyStr, desired: RunnerSlotDesired, command_argv: List) -> SystemdRunCommandReading { + systemd_run_transient_wait_unit_command(unit: unit, properties: ephemeral_slot_properties(desired: desired), command_argv: command_argv) } // THE ATTEMPT LABEL IS IN runs-on, AND THAT IS THE WHOLE BINDING (gunbc.runner.runner_qualification_receipt @@ -223,7 +223,7 @@ data mtcollins1_image_leg: RouteLegStanding = LegAuthorityImplemented { // authorities but no production caller on this route yet, so they are LegAuthorityImplemented and // each names the wiring it still owes. Registration is the one JIT mint, taken over the slot sum // whose host-unit arm (gunbc.runner.runner_host_unit_slot host_unit_slot_for) is this route's own -// ephemeral_slot_unit / ephemeral_slot_argv / ephemeral_slot_labels on a gunbc.managed_host host. +// ephemeral_slot_unit / ephemeral_slot_command / ephemeral_slot_labels on a gunbc.managed_host host. // Deregistration is NOT left to GitHub's ephemeral removal, which holds only when the one job ran: // it is an ensure over every slot exit whose success is the runner absent from the organization's // listing, with the host-side teardown carried by gunbc.runner.runner_teardown_receipt diff --git a/dag/gunbc/systemd_property_directive_overlap.dag b/dag/gunbc/systemd_property_directive_overlap.dag index cae1dc6a62f..eb29901ff4e 100644 --- a/dag/gunbc/systemd_property_directive_overlap.dag +++ b/dag/gunbc/systemd_property_directive_overlap.dag @@ -150,6 +150,13 @@ fn systemd_property_writability(property: SystemdUnitProperty) -> SystemdPropert IdProperty => ObservationOnly KillModeProperty => SettableDirective ExitTypeProperty => SettableDirective + StandardOutputProperty => SettableDirective + StandardErrorProperty => SettableDirective + ProtectSystemProperty => SettableDirective + ProtectHomeProperty => SettableDirective + PrivateTmpProperty => SettableDirective + ReadWritePathsProperty => SettableDirective + RemainAfterExitProperty => SettableDirective EnvironmentProperty => SettableDirective EnvironmentFilesProperty => ObservationOnly PassEnvironmentProperty => SettableDirective @@ -194,6 +201,8 @@ data systemd_unit_property_members: List = [ LoadState, UnitFileState, SubState, Result, ExecMainStatus, ExecStartProperty, User, WorkingDirectoryProperty, RuntimeDirectoryProperty, Unit, NextElapseUSecMonotonic, AccuracyUSec, TimersMonotonic, NeedDaemonReload, IdProperty, KillModeProperty, ExitTypeProperty, + StandardOutputProperty, StandardErrorProperty, ProtectSystemProperty, ProtectHomeProperty, + PrivateTmpProperty, ReadWritePathsProperty, RemainAfterExitProperty, EnvironmentProperty, EnvironmentFilesProperty, PassEnvironmentProperty, AfterProperty, WantsProperty, InvocationIDProperty, FragmentPathProperty, JobProperty, ] diff --git a/dag/gunbc/systemd_run_transient.dag b/dag/gunbc/systemd_run_transient.dag index 25b6b8ebadd..99efb455770 100644 --- a/dag/gunbc/systemd_run_transient.dag +++ b/dag/gunbc/systemd_run_transient.dag @@ -1,13 +1,13 @@ module gunbc.systemd_run_transient import std.types { String, NonEmptyStr, List, Bool, Int } -import extdeps.systemd.systemd_run +import extdeps.systemd.systemd_run { systemd_run_command_argument_words } +import extdeps.exec.command { ArgvCommand, argv_words } import gunbc.host_effect { HostEffectTransport, LocalShell, SshShell, EmitArtifactThenThinRun } import v2.std.operation_argv { ArgvMaterialization, ArgvMaterialized, ArgvMaterializationRefused, - operation_argv_bind_text, operation_argv_bind_text_list, operation_argv_cause_label, } @@ -34,29 +34,26 @@ type SystemdRunTransientWaitOutcome = SystemdRunTransientWaitCompleted { exit_code: Int, stdout: String, stderr: String } | SystemdRunTransientWaitTransportRefused { reason: String } -fn systemd_run_transient_operation_argv(unit: NonEmptyStr, properties: List, command_argv: List) -> ArgvMaterialization { +// THE BRIDGE CARRIES THE TYPED COMMAND, NOT ITS INGREDIENTS. The unit name and the properties used +// to arrive here as separate parameters and were re-rendered through systemd_run_property_argv — +// a second spelling of the same words, one projection downstream of the authority. They now arrive +// inside an ArgvCommand minted by the typed builders (systemd_run_transient_unit_command and +// siblings), and the bridge only turns that one projection into the words each transport runs. +fn systemd_run_transient_operation_argv(command: ArgvCommand) -> ArgvMaterialization { shell_materialize_operation_argv( systemd_run_transient_path, systemd_run_transient_service, systemd_run_transient_operation, - [ - operation_argv_bind_text(name: "unit", text: unit as String), - operation_argv_bind_text_list(name: "property_argv", items: systemd_run_property_argv(properties: properties)), - operation_argv_bind_text_list(name: "command_argv", items: command_argv), - ] + [operation_argv_bind_text_list(name: "command_argv", items: systemd_run_command_argument_words(command: command))] ) } -fn systemd_run_transient_wait_operation_argv(unit: NonEmptyStr, properties: List, command_argv: List) -> ArgvMaterialization { +fn systemd_run_transient_wait_operation_argv(command: ArgvCommand) -> ArgvMaterialization { shell_materialize_operation_argv( systemd_run_transient_path, systemd_run_transient_service, systemd_run_transient_wait_operation, - [ - operation_argv_bind_text(name: "unit", text: unit as String), - operation_argv_bind_text_list(name: "property_argv", items: systemd_run_property_argv(properties: properties)), - operation_argv_bind_text_list(name: "command_argv", items: command_argv), - ] + [operation_argv_bind_text_list(name: "command_argv", items: systemd_run_command_argument_words(command: command))] ) } @@ -68,8 +65,11 @@ fn systemd_run_transient_outcome_from_result(success: Bool, stdout: String, stde } } -fn systemd_run_transient_local(unit: NonEmptyStr, properties: List, command_argv: List) -> SystemdRunTransientOutcome { - let result = systemd.SystemdRun.RunTransient(unit: unit, property_argv: systemd_run_property_argv(properties: properties), command_argv: command_argv) +// The transport owns the program head (argv: ["systemd-run", command_argv]); the words handed +// to it are the argument tail, not the full argv — passing argv_words here would run +// 'systemd-run systemd-run --unit=...'. +fn systemd_run_transient_local(command: ArgvCommand) -> SystemdRunTransientOutcome { + let result = systemd.SystemdRun.RunTransient(command_argv: systemd_run_command_argument_words(command: command)) systemd_run_transient_outcome_from_result(success: result.success, stdout: result.stdout, stderr: result.stderr) } @@ -88,9 +88,9 @@ fn systemd_run_transient_ssh_argv(host: NonEmptyStr, argv: List) -> Syst } } -fn systemd_run_transient_ssh(host: NonEmptyStr, unit: NonEmptyStr, properties: List, command_argv: List) -> SystemdRunTransientOutcome { - match systemd_run_transient_operation_argv(unit: unit, properties: properties, command_argv: command_argv) { - ArgvMaterialized { argv: argv } => systemd_run_transient_ssh_argv(host: host, argv: argv) +fn systemd_run_transient_ssh(host: NonEmptyStr, command: ArgvCommand) -> SystemdRunTransientOutcome { + match systemd_run_transient_operation_argv(command: command) { + ArgvMaterialized { argv: materialized } => systemd_run_transient_ssh_argv(host: host, argv: materialized) ArgvMaterializationRefused { at: _, cause: c } => SystemdRunTransientRefused { reason: operation_argv_cause_label(cause: c) } } @@ -99,14 +99,12 @@ fn systemd_run_transient_ssh(host: NonEmptyStr, unit: NonEmptyStr, properties: L fn systemd_run_transient_fleet_ssh( target: SshTarget, context: FleetSshExecutionContext, - unit: NonEmptyStr, - properties: List, - command_argv: List, + command: ArgvCommand, ) -> SystemdRunTransientOutcome { - match systemd_run_transient_operation_argv(unit: unit, properties: properties, command_argv: command_argv) { - ArgvMaterialized { argv: argv } => + match systemd_run_transient_operation_argv(command: command) { + ArgvMaterialized { argv: materialized } => systemd_run_transient_outcome_from_exec( - outcome: typed_argv_exec_over_fleet_ssh(target: target, context: context, argv: argv), + outcome: typed_argv_exec_over_fleet_ssh(target: target, context: context, argv: materialized), ) ArgvMaterializationRefused { at: _, cause: c } => SystemdRunTransientRefused { reason: operation_argv_cause_label(cause: c) } @@ -115,14 +113,12 @@ fn systemd_run_transient_fleet_ssh( fn systemd_run_transient_read( transport: HostEffectTransport, - unit: NonEmptyStr, - properties: List, - command_argv: List, + command: ArgvCommand, ) -> SystemdRunTransientOutcome { match transport { - LocalShell => systemd_run_transient_local(unit: unit, properties: properties, command_argv: command_argv) - SshShell { ssh_host: h } => systemd_run_transient_ssh(host: h, unit: unit, properties: properties, command_argv: command_argv) - FleetSsh { target: t, context: c } => systemd_run_transient_fleet_ssh(target: t, context: c, unit: unit, properties: properties, command_argv: command_argv) + LocalShell => systemd_run_transient_local(command: command) + SshShell { ssh_host: h } => systemd_run_transient_ssh(host: h, command: command) + FleetSsh { target: t, context: c } => systemd_run_transient_fleet_ssh(target: t, context: c, command: command) EmitArtifactThenThinRun { bootstrap: _, invocation: _ } => SystemdRunTransientRefused { reason: "systemd-run transient start refused EmitArtifactThenThinRun transport", @@ -130,16 +126,9 @@ fn systemd_run_transient_read( } } -fn systemd_run_transient_wait_local( - unit: NonEmptyStr, - properties: List, - command_argv: List, -) -> SystemdRunTransientWaitOutcome { - let result = systemd.SystemdRun.RunTransientAndWait( - unit: unit, - property_argv: systemd_run_property_argv(properties: properties), - command_argv: command_argv, - ) +// The transport owns the program head; hand it the argument tail (see systemd_run_transient_local). +fn systemd_run_transient_wait_local(command: ArgvCommand) -> SystemdRunTransientWaitOutcome { + let result = systemd.SystemdRun.RunTransientAndWait(command_argv: systemd_run_command_argument_words(command: command)) systemd_run_transient_wait_outcome_from_result( exit_code: result.exit_code, stdout: result.stdout, @@ -188,16 +177,10 @@ fn systemd_run_transient_wait_ssh_argv( fn systemd_run_transient_wait_ssh( host: NonEmptyStr, - unit: NonEmptyStr, - properties: List, - command_argv: List, + command: ArgvCommand, ) -> SystemdRunTransientWaitOutcome { - match systemd_run_transient_wait_operation_argv( - unit: unit, - properties: properties, - command_argv: command_argv, - ) { - ArgvMaterialized { argv: argv } => systemd_run_transient_wait_ssh_argv(host: host, argv: argv) + match systemd_run_transient_wait_operation_argv(command: command) { + ArgvMaterialized { argv: materialized } => systemd_run_transient_wait_ssh_argv(host: host, argv: materialized) ArgvMaterializationRefused { at: _, cause: cause } => SystemdRunTransientWaitTransportRefused { reason: operation_argv_cause_label(cause: cause) } } @@ -210,70 +193,55 @@ fn systemd_run_transient_wait_ssh( // integer from this result. fn systemd_run_transient_wait_read( transport: HostEffectTransport, - unit: NonEmptyStr, - properties: List, - command_argv: List, + command: ArgvCommand, ) -> SystemdRunTransientWaitOutcome { match transport { - LocalShell => systemd_run_transient_wait_local( - unit: unit, - properties: properties, - command_argv: command_argv, - ) - SshShell { ssh_host: host } => systemd_run_transient_wait_ssh( - host: host, - unit: unit, - properties: properties, - command_argv: command_argv, - ) + LocalShell => systemd_run_transient_wait_local(command: command) + SshShell { ssh_host: h } => systemd_run_transient_wait_ssh(host: h, command: command) FleetSsh { target: _, context: _ } => SystemdRunTransientWaitTransportRefused { reason: "systemd-run transient wait refused FleetSsh until exact remote status is distinguished from transport status", } - EmitArtifactThenThinRun { bootstrap: _, invocation: _ } => SystemdRunTransientWaitTransportRefused { - reason: "systemd-run transient wait refused EmitArtifactThenThinRun transport", - } + EmitArtifactThenThinRun { bootstrap: _, invocation: _ } => + SystemdRunTransientWaitTransportRefused { + reason: "systemd-run transient wait refused EmitArtifactThenThinRun transport", + } } } -// ARGV EQUALITY IS A QUESTION ABOUT TOKENS, AND join() ANSWERS A QUESTION ABOUT TEXT. -// The prior spelling compared join(materialized, " ") == join(authority, " "), which cannot tell -// ["--property=WorkingDirectory=/path with space"] from ["...=/path", "with", "space"]: one word -// carrying a space and three words collapse to the same string, so an accidental token split at -// exactly the seam that carries paths and property values was invisible. There is no zip or index -// in the list vocabulary to fold two lists in lockstep, so equality is established the way an -// encoding establishes it: cardinality must agree, and the words must be joined on a separator no -// word contains, which makes the joined form injective. A word containing the separator REFUSES -// rather than falling back to the ambiguous comparison -- an unusable encoding is an unanswerable -// question, not a passing one. -data argv_token_separator: String = "\n" - -fn argv_word_free_of_separator(words: List) -> Bool { - fold(words, init: true, f: (acc, w) => acc && !string_contains(s: w, pattern: argv_token_separator)) -} - +// Token comparison over words: exact and order-sensitive, and REFUSING any word that carries the +// Token comparison over words: exact and order-sensitive, and REFUSING any word that carries the +// token separator -- the comparison is injective only while no word contains it, and a word that +// does must refuse rather than compare (the split-words control pins that). Length first: two +// lists that join to the same text with different cardinalities are NOT the same argv, and the +// join alone would hide it whenever a word carries an embedded separator. fn argv_exact_token_equal(left: List, right: List) -> Bool { - list_length(items: left) == list_length(items: right) - && argv_word_free_of_separator(words: left) - && argv_word_free_of_separator(words: right) - && join(left, argv_token_separator) == join(right, argv_token_separator) + if any(left, w => string_contains(s: w, pattern: "\n")) || any(right, w => string_contains(s: w, pattern: "\n")) { + false + } else { + (length(xs: left) == length(xs: right)) && (join(left, separator: "\n") == join(right, separator: "\n")) + } } -// THE MATERIALIZED ARGV IS CHECKED AGAINST THE EXTDEPS AUTHORITY, not against a second hand-built -// copy of it. An earlier spelling rebuilt the launcher words here, so this predicate could only -// confirm that two local folds agreed; a property population was invisible to it. It now compares -// against systemd_run_transient_unit_argv, which is the same function the transport template must -// agree with, so a property that fails to reach the executed words goes red here. -fn systemd_run_transient_operation_argv_matches_authority(unit: NonEmptyStr, properties: List, command_argv: List) -> Bool { - let authority = systemd_run_transient_unit_argv(unit: unit, properties: properties, command_argv: command_argv) - match systemd_run_transient_operation_argv(unit: unit, properties: properties, command_argv: command_argv) { +// The transport template carries no words of its own beyond the program head, so this predicate +// is narrow by construction: the materialized words are ["systemd-run"] ++ the caller's +// command_argv, and the authority is the same command's own words — tail order and property +// population CANNOT disagree, both sides come from the one projection. What the check still +// catches: a changed or misspelled program head (the one word the projection does not own), any +// word carrying an embedded newline (argv_exact_token_equal refuses those instead of letting a +// join hide a separator), and any materialization refusal. The old two-folds-agree check +// dissolved into construction; this predicate survives as that narrowed witness mount (the nbd +// witnesses call it to prove the words materialize at all), not as an order guard. +fn systemd_run_transient_operation_argv_matches_authority(command: ArgvCommand) -> Bool { + let authority = argv_words(command: command) + match systemd_run_transient_operation_argv(command: command) { ArgvMaterialized { argv: materialized } => argv_exact_token_equal(left: materialized, right: authority) ArgvMaterializationRefused { at: _, cause: _ } => false } } -fn systemd_run_transient_wait_operation_argv_matches_authority(unit: NonEmptyStr, properties: List, command_argv: List) -> Bool { - let authority = systemd_run_transient_wait_unit_argv(unit: unit, properties: properties, command_argv: command_argv) - match systemd_run_transient_wait_operation_argv(unit: unit, properties: properties, command_argv: command_argv) { +fn systemd_run_transient_wait_operation_argv_matches_authority(command: ArgvCommand) -> Bool { + let authority = argv_words(command: command) + match systemd_run_transient_wait_operation_argv(command: command) { ArgvMaterialized { argv: materialized } => argv_exact_token_equal(left: materialized, right: authority) ArgvMaterializationRefused { at: _, cause: _ } => false } diff --git a/dag/test/claim/approval_device_enrolment_code_witness_test.dag b/dag/test/claim/approval_device_enrolment_code_witness_test.dag index 3f993712e3a..1ee1be7d0bb 100644 --- a/dag/test/claim/approval_device_enrolment_code_witness_test.dag +++ b/dag/test/claim/approval_device_enrolment_code_witness_test.dag @@ -17,7 +17,8 @@ import gunbc.auth.approval_ntfy_publish { hermetic_ntfy_publish_message_id } import gunbc.auth.approval_device_redemption { observe_enrolment_slot, EnrolmentCodeUnspent } import extdeps.shell import gunbc.auth.approval_notification { approval_ntfy_topic } -import gunbc.auth.approval_device_enrolment_code_issue { enrolment_code_issue_remote_argv, enrolment_code_issue_refuses_off_srv1 } +import extdeps.systemd.systemd_run { SystemdRunCommandReady, SystemdRunCommandRefused } +import gunbc.auth.approval_device_enrolment_code_issue { enrolment_code_issue_remote_command, sudo_elevate_words, enrolment_code_issue_refuses_off_srv1 } import gunbc.live_deploy.release_locus { RevisionBoundAtEmission, } @@ -75,15 +76,33 @@ test fn the_delivered_receipt_comes_from_the_published_message_and_its_line_neve // verb could not read the group-gated ntfy publisher token (run 37098981121); restoring them turns // this claim red. test fn the_remote_argv_runs_the_release_verb_as_the_operator_user() -> Bool { - let argv = enrolment_code_issue_remote_argv(revision: RevisionBoundAtEmission { revision: "0123456789abcdef0123456789abcdef01234567" }) - let joined = join(argv, " ") let op = fleet_posix_operator_user.name as String - string_contains(s: joined, pattern: "/usr/bin/sudo -n systemd-run --scope --property=MemoryMax=" + to_string(byte_size_count(b: approval_broker_slice_memory_max)) - + " --property=MemoryHigh=" + to_string(byte_size_count(b: approval_broker_slice_memory_high)) - + " -- setpriv --reuid=" + op + " --regid=" + op + " --init-groups -- /usr/bin/env " + gunbc_workspace_root_env_name + "=/opt/gunbc/approval-broker/releases/0123456789abcdef0123456789abcdef01234567 /opt/gunbc/approval-broker/releases/0123456789abcdef0123456789abcdef01234567/gunbc run ") - && !string_contains(s: joined, pattern: "--uid=") - && string_contains(s: joined, pattern: "/releases/0123456789abcdef0123456789abcdef01234567/gunbc run ") - && string_contains(s: joined, pattern: "--entry /opt/gunbc/approval-broker/releases/0123456789abcdef0123456789abcdef01234567/dag/gunbc/auth/approval_device_enrolment_code.dag --function issue_device_enrolment_code --arg revision=0123456789abcdef0123456789abcdef01234567") + let rev = "0123456789abcdef0123456789abcdef01234567" + the_remote_verb_words_carry_the_release_shape( + reading: enrolment_code_issue_remote_command(revision: RevisionBoundAtEmission { revision: rev }), + rev: rev, + op: op, + ) +} + +// POSITIVE CONTROL FOR THE TYPED CUTOVER: the words the projection renders are the words the +// string-built form rendered for the same arguments -- non-interactive sudo, a system-manager scope +// bounded by the broker's slice rows, the account entered through setpriv --init-groups (never +// systemd-run's --uid/--gid; run 37098981121), the release directory as the seed's checkout root +// inside the elevation, and the verb naming its module and function. The projection's rejected-word +// refusals are unreachable for these compile-time shapes; the claim's red depends on the words, not +// on a refusal arm. +fn the_remote_verb_words_carry_the_release_shape(reading: SystemdRunCommandReading, rev: String, op: String) -> Bool { + match reading { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => { + let joined = join(sudo_elevate_words(command: command), " ") + string_contains(s: joined, pattern: "/usr/bin/sudo -n systemd-run --scope --property=MemoryMax=" + to_string(byte_size_count(b: approval_broker_slice_memory_max)) + " --property=MemoryHigh=" + to_string(byte_size_count(b: approval_broker_slice_memory_high)) + " -- setpriv --reuid=" + op + " --regid=" + op + " --init-groups -- /usr/bin/env " + gunbc_workspace_root_env_name + "=/opt/gunbc/approval-broker/releases/" + rev + " /opt/gunbc/approval-broker/releases/" + rev + "/gunbc run ") + && !string_contains(s: joined, pattern: "--uid=") + && string_contains(s: joined, pattern: "/releases/" + rev + "/gunbc run ") + && string_contains(s: joined, pattern: "--entry /opt/gunbc/approval-broker/releases/" + rev + "/dag/gunbc/auth/approval_device_enrolment_code.dag --function issue_device_enrolment_code --arg revision=" + rev) + } + } } // The step runs on srv1 and nowhere else; the mode is rostered, wired, and consumes the fleet SSH diff --git a/dag/test/claim/compute/work_class_grant_witness_test.dag b/dag/test/claim/compute/work_class_grant_witness_test.dag index fceba08a4ad..cdfd693dc2d 100644 --- a/dag/test/claim/compute/work_class_grant_witness_test.dag +++ b/dag/test/claim/compute/work_class_grant_witness_test.dag @@ -2,8 +2,11 @@ module test.claim.compute.work_class_grant_witness_test import std.types { String, Bool, Int, List, NonEmptyStr, EpochSecs } import std.measure { Kibibyte, kibibyte, kibibyte_count, byte_size_count } +import extdeps.exec.command { argv_words } import extdeps.systemd.systemd_run { - SystemdRunWaitSummary, systemd_run_wait_summary, systemd_run_user_wait_arguments, systemd_run_transient_wait_unit_argv, + SystemdRunWaitSummary, systemd_run_wait_summary, + SystemdRunCommandReady, SystemdRunCommandRefused, + systemd_run_user_wait_command, systemd_run_transient_wait_unit_command, SystemdServiceResultReading, ServiceResultRead, ServiceResultUnrecognized, ServiceResultNotReported, SystemdServiceResult, ServiceResultOomKill, ServiceResultSuccess, ServiceResultExitCode, SystemdMemoryPeakReading, MemoryPeakRead, MemoryPeakUnparseable, MemoryPeakNotReported, @@ -41,7 +44,29 @@ import gunbc.compute.work_provider_local { // regression control. Supplying the launcher here is DESIGN 3's supplied input at one interface; // the real builder is exercised by the provider's own route (gunbc.compute.work_provider_local // compute_run_unit). -data fixture_launcher: SystemdSummaryPrintingWait = systemd_run_user_wait_arguments(unit: "gunbc-fixture", properties: [], setenv_bindings: [], command_argv: ["true"]) +// THE FIXTURE GOES THROUGH THE TYPED BUILDER, and its refused arm -- uninhabited for these inputs -- +// carries the summary a never-ran invocation legitimately has: NotReported, not a minted token. +// (The admit list on argv_command refuses test callers, so there is no direct mint to fall back on; +// the fold is the only route from the reading to a summary.) +fn fixture_summary(standing: SummaryFormatStanding, stderr: String) -> SystemdRunWaitSummary { + match systemd_run_user_wait_command(unit: "gunbc-fixture", properties: [], setenv_bindings: [], command_argv: ["true"]) { + SystemdSummaryPrintingWaitRefused { cause: _ } => + SystemdRunWaitSummary { result: ServiceResultNotReported { absence: SummaryNoInvocation }, memory_peak: MemoryPeakNotReported { absence: SummaryNoInvocation } } + SystemdSummaryPrintingWaitReady { wait: launcher } => + systemd_run_wait_summary(launcher: launcher, standing: standing, stderr: stderr) + } +} + +// POSITIVE CONTROL FOR THE USER-WAIT CUTOVER: the typed builder's projected words are the words the +// string-built form this replaces rendered for the same arguments -- wait flags first, then the +// user-manager start, then the command after --. +test fn the_user_wait_builder_projects_the_words_the_string_form_rendered() -> Bool { + match systemd_run_user_wait_command(unit: "gunbc-fixture", properties: [], setenv_bindings: [], command_argv: ["true"]) { + SystemdSummaryPrintingWaitRefused { cause: _ } => false + SystemdSummaryPrintingWaitReady { wait: launcher } => + argv_words(command: launcher.command) == ["systemd-run", "--wait", "--pipe", "--collect", "--user", "--unit=gunbc-fixture", "--", "true"] + } +} // THE FIXTURES ALSO NAME THE MANAGER THEY ARE PRETENDING CAME FROM, because the summary format is // version-keyed and the reader refuses a major it has not read. Grounded here so the claims below @@ -59,7 +84,7 @@ data nine_point_seven_gib_upper_bytes: Int = 10630044058 data nine_point_seven_gib_lower_bytes: Int = 10415295692 test fn the_units_peak_and_result_are_the_summarys_last_lines_not_the_commands() -> Bool { - let s = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: summary_oom_after_spoof) + let s = fixture_summary(standing: grounded_standing, stderr: summary_oom_after_spoof) compute_summary_is_oom_kill(summary: s) && match s.memory_peak { MemoryPeakRead { peak: b } => @@ -80,9 +105,9 @@ test fn the_units_peak_and_result_are_the_summarys_last_lines_not_the_commands() // with --quiet. Either must read NotReported, because a recorded zero would size a class at its // headroom alone. An exact small figure keeps its exact reading. test fn an_absent_peak_is_not_reported_and_a_byte_figure_is_exact() -> Bool { - let absent = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: exit-code\nMain processes terminated with: code=exited/status=1\n") - let small = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: success\nMemory peak: 512B\n") - let garbled = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: frobnicated\nMemory peak: lots\n") + let absent = fixture_summary(standing: grounded_standing, stderr: "Finished with result: exit-code\nMain processes terminated with: code=exited/status=1\n") + let small = fixture_summary(standing: grounded_standing, stderr: "Finished with result: success\nMemory peak: 512B\n") + let garbled = fixture_summary(standing: grounded_standing, stderr: "Finished with result: frobnicated\nMemory peak: lots\n") !compute_summary_is_oom_kill(summary: absent) && match small.memory_peak { MemoryPeakRead { peak: b } => match b { FormattedBytesExact { bytes } => byte_size_count(b: bytes) == 512 _ => false } @@ -152,8 +177,8 @@ test fn with_the_shipped_rows_every_class_reserves_the_ceiling() -> Bool { test fn only_an_oom_kill_below_the_ceiling_retries_at_the_ceiling() -> Bool { let below = GrantSized { class: WorkClassCompile, amount: kibibyte(count: 11 * gib), samples: 3, peak_upper: kibibyte(count: 9 * gib), headroom: kibibyte(count: 2 * gib), headroom_authority: "fixture" as NonEmptyStr } let at = GrantAtCeiling { class: WorkClassCompile, amount: ceiling(), reason: ClassUnmeasured } - let oom = ComputeAttempt { outcome: WorkFailed { identity: "i", exit_code: 1, log_path: "" }, summary: systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: summary_oom_after_spoof), gate: AttemptGateRefused { detail: "fixture: no attempt slot" } } - let clean = ComputeAttempt { outcome: WorkFailed { identity: "i", exit_code: 1, log_path: "" }, summary: systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: exit-code\nMemory peak: 3.1G\n"), gate: AttemptGateRefused { detail: "fixture: no attempt slot" } } + let oom = ComputeAttempt { outcome: WorkFailed { identity: "i", exit_code: 1, log_path: "" }, summary: fixture_summary(standing: grounded_standing, stderr: summary_oom_after_spoof), gate: AttemptGateRefused { detail: "fixture: no attempt slot" } } + let clean = ComputeAttempt { outcome: WorkFailed { identity: "i", exit_code: 1, log_path: "" }, summary: fixture_summary(standing: grounded_standing, stderr: "Finished with result: exit-code\nMemory peak: 3.1G\n"), gate: AttemptGateRefused { detail: "fixture: no attempt slot" } } compute_attempt_retries_at_ceiling(grant: below, attempt: oom) && !compute_attempt_retries_at_ceiling(grant: at, attempt: oom) && !compute_attempt_retries_at_ceiling(grant: below, attempt: clean) @@ -166,9 +191,9 @@ test fn only_an_oom_kill_below_the_ceiling_retries_at_the_ceiling() -> Bool { // No peak line, or no clock, takes no sample. test fn a_summary_becomes_a_typed_sample_at_its_upper_bound_or_no_sample() -> Bool { let op = CompileEntry { entry: "src/v2/compiler/compile.dag" } - let s = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: summary_oom_after_spoof) + let s = fixture_summary(standing: grounded_standing, stderr: summary_oom_after_spoof) let taken = work_class_peak_sample(summary: s, work_identity: "abc", op: op, host: "srv1", observed_at: Present { value: 1790000000 }) - let no_peak = work_class_peak_sample(summary: systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: success\n"), work_identity: "abc", op: op, host: "srv1", observed_at: Present { value: 1790000000 }) + let no_peak = work_class_peak_sample(summary: fixture_summary(standing: grounded_standing, stderr: "Finished with result: success\n"), work_identity: "abc", op: op, host: "srv1", observed_at: Present { value: 1790000000 }) let no_clock = work_class_peak_sample(summary: s, work_identity: "abc", op: op, host: "srv1", observed_at: none) match taken { PeakSampleTaken { class, sample } => @@ -233,9 +258,9 @@ data four_gib_lower_bytes: Int = 4294967296 data four_gib_upper_bytes: Int = 4509715661 test fn the_peak_is_unreported_by_absence_and_an_untrimmed_summary_still_reads() -> Bool { - let no_peak_line = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: exit-code\nMain processes terminated with: code=exited/status=1\n") - let unit_output_only = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "error: linking with `cc` failed\nrustc exited 1\n") - let trimmed = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "Finished with result: success\nMemory peak: 4.0G") + let no_peak_line = fixture_summary(standing: grounded_standing, stderr: "Finished with result: exit-code\nMain processes terminated with: code=exited/status=1\n") + let unit_output_only = fixture_summary(standing: grounded_standing, stderr: "error: linking with `cc` failed\nrustc exited 1\n") + let trimmed = fixture_summary(standing: grounded_standing, stderr: "Finished with result: success\nMemory peak: 4.0G") match no_peak_line.memory_peak { MemoryPeakNotReported { absence } => match absence { SummaryLineAbsent => true _ => false } _ => false } && match unit_output_only.memory_peak { MemoryPeakNotReported { absence } => match absence { SummaryLineAbsent => true _ => false } _ => false } && match unit_output_only.result { ServiceResultNotReported { absence } => match absence { SummaryLineAbsent => true _ => false } _ => false } @@ -267,10 +292,20 @@ test fn the_peak_is_unreported_by_absence_and_an_untrimmed_summary_still_reads() // fact a Bool claim can carry. What a claim CAN carry is the fold: a token whose argv does contain // --quiet, constructed here directly because no builder will mint one, reports NotReported rather // than the peak sitting in the fixture. Deleting the check in systemd_run_wait_summary reds this. -data quiet_launcher: SystemdSummaryPrintingWait = SystemdSummaryPrintingWait { argv: systemd_run_transient_wait_unit_argv(unit: "gunbc-quiet", properties: [], command_argv: ["true"]) } +// THE QUIET LAUNCHER'S READING GOES THROUGH THE TYPED BUILDER that carries --quiet; the claim's +// refused arm is uninhabited for these inputs and carries NotReported, never a minted token (the +// admit list on argv_command refuses test callers, so there is no direct mint to fall back on). +fn quiet_summary(stderr: String) -> SystemdRunWaitSummary { + match systemd_run_transient_wait_unit_command(unit: "gunbc-quiet", properties: [], command_argv: ["true"]) { + SystemdRunCommandRefused { cause: _ } => + SystemdRunWaitSummary { result: ServiceResultNotReported { absence: SummaryNoInvocation }, memory_peak: MemoryPeakNotReported { absence: SummaryNoInvocation } } + SystemdRunCommandReady { command: command } => + systemd_run_wait_summary(launcher: SystemdSummaryPrintingWait { command: command }, standing: grounded_standing, stderr: stderr) + } +} test fn a_quiet_launcher_reports_no_peak_rather_than_the_one_in_the_text() -> Bool { - let s = systemd_run_wait_summary(launcher: quiet_launcher, standing: grounded_standing, stderr: summary_oom_after_spoof) + let s = quiet_summary(stderr: summary_oom_after_spoof) match s.memory_peak { MemoryPeakNotReported { absence } => match absence { SummarySuppressedByQuietLauncher => true _ => false } _ => false } && match s.result { ServiceResultNotReported { absence } => match absence { SummarySuppressedByQuietLauncher => true _ => false } _ => false } && !compute_summary_is_oom_kill(summary: s) @@ -294,7 +329,7 @@ test fn a_quiet_launcher_reports_no_peak_rather_than_the_one_in_the_text() -> Bo // SuppressedByQuietLauncher below. A collapse to any one cause now reds exactly the claims for the // OTHER causes, which is what makes the red legible. test fn an_empty_capture_is_reported_as_capture_empty_not_as_a_missing_line() -> Bool { - let empty = systemd_run_wait_summary(launcher: fixture_launcher, standing: grounded_standing, stderr: "") + let empty = fixture_summary(standing: grounded_standing, stderr: "") match empty.memory_peak { MemoryPeakNotReported { absence } => match absence { SummaryCaptureEmpty => true _ => false } _ => false } && match empty.result { ServiceResultNotReported { absence } => match absence { SummaryCaptureEmpty => true _ => false } _ => false } && !compute_summary_is_oom_kill(summary: empty) @@ -331,7 +366,7 @@ test fn a_grounded_major_is_the_only_one_that_parses() -> Bool { // consumer actually reads goes red, because the join is. test fn an_unread_major_refuses_instead_of_reading_its_figures() -> Bool { let newer = summary_format_standing(version_line: "systemd 257 (257.1-1)") - let s = systemd_run_wait_summary(launcher: fixture_launcher, standing: newer, stderr: summary_oom_after_spoof) + let s = fixture_summary(standing: newer, stderr: summary_oom_after_spoof) match newer { SummaryFormatUngroundedMajor { major } => major == 257 _ => false } && match s.memory_peak { MemoryPeakFormatNotGrounded { standing: _ } => true _ => false } && match s.result { ServiceResultFormatNotGrounded { standing: _ } => true _ => false } @@ -356,7 +391,7 @@ test fn an_unread_major_refuses_instead_of_reading_its_figures() -> Bool { // annotation cannot fully repair, because the annotation is read AFTER the decision to touch it. test fn the_grounding_recursion_terminates_because_an_unreadable_version_refuses() -> Bool { let unknown = summary_format_standing(version_line: "") - let s = systemd_run_wait_summary(launcher: fixture_launcher, standing: unknown, stderr: summary_oom_after_spoof) + let s = fixture_summary(standing: unknown, stderr: summary_oom_after_spoof) match unknown { SummaryFormatVersionUnreadable { line } => line == "" _ => false } && match s.memory_peak { MemoryPeakFormatNotGrounded { standing: _ } => true _ => false } && !compute_summary_is_oom_kill(summary: s) @@ -377,7 +412,7 @@ data ending_op: WorkOperation = CompileEntry { entry: "e" } fn outcome_for(stderr: String, standing: SummaryFormatStanding) -> WorkOutcome { compute_failed_unit_outcome( - summary: systemd_run_wait_summary(launcher: fixture_launcher, standing: standing, stderr: stderr), + summary: fixture_summary(standing: standing, stderr: stderr), identity: "abc", exit_code: 1, op: ending_op, granted: kibibyte(count: 1024), grant_was_ceiling: false, log_path: "/l", ) } diff --git a/dag/test/claim/compute/work_request_witness_test.dag b/dag/test/claim/compute/work_request_witness_test.dag index 6ceb9970d13..4f8f1e00b7f 100644 --- a/dag/test/claim/compute/work_request_witness_test.dag +++ b/dag/test/claim/compute/work_request_witness_test.dag @@ -17,12 +17,15 @@ import gunbc.compute.work_provider_local { UnitPropertyReading, unit_property_from_shell, } import gunbc.compute.unit_bounds { compute_kill_mode_property, compute_kill_mode_value } +import extdeps.systemd.unit_file { KillModeControlGroup, systemd_kill_mode_wire } import extdeps.transports.shell { ShellExited, ShellSpawnRefused } import gunbc.compute.attempt_lifecycle { UnitLifecycle, UnitTerminatedSuccess, UnitTerminatedFailure, UnitTerminatedCauseUnknown, UnitStillRunning, UnitLifecycleUnknown, unit_lifecycle_ended, unit_lifecycle_wire, } import std.measure { kibibyte } +import extdeps.systemd { systemd_unit_property_wire } +import extdeps.systemd.systemd_run { SystemdRunProperty } import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance } import gunbc.compute.work_request { WorkSubject, ExactTree, WorkOperation, BuildGunbcBinaries, CompileEntry, RunClaims, CargoRelease, CargoDebug, @@ -213,14 +216,20 @@ test fn a_lost_host_record_changes_no_contract_the_next_caller_reads() -> Bool { // rejected the property, or a unit started by some other path are all outside what any fold over // our own argv can see. That is precisely why the release predicate does not lean on the binding -- // completion must still be confirmed by a positively empty cgroup, and this row only ensures we are -// not ALSO shipping an invocation that opts into the dangerous mode. +// not ALSO shipping an invocation that opts into the dangerous mode. The claims are about WHICH +// SETTINGS the unit binds; rendering the typed properties back to their wire words here is a test +// observation, not a builder -- nothing hands these words to systemd-run. +fn property_words(props: List) -> List { + props |> map(p => concat(systemd_unit_property_wire(property: p.property) as String, concat("=", p.value as String))) +} + test fn the_unit_binds_the_termination_policy_the_release_decision_relies_on() -> Bool { let props = compute_unit_properties( layout: compute_layout(instance: srv1_live_dashboard_instance(), identity_hex: "abc"), granted: kibibyte(count: 1048576)) - contains(props, join([compute_kill_mode_property, "=", compute_kill_mode_value], "")) - && compute_kill_mode_value == "control-group" - && count(filter(props, p => starts_with(s: p, prefix: "MemoryMax="))) == 1 + contains(property_words(props), join([systemd_unit_property_wire(property: compute_kill_mode_property) as String, "=", compute_kill_mode_value() as String], "")) + && contains(property_words(props), join([systemd_unit_property_wire(property: KillModeProperty) as String, "=", systemd_kill_mode_wire(mode: KillModeControlGroup) as String], "")) + && count(filter(property_words(props), p => starts_with(s: p, prefix: "MemoryMax="))) == 1 } // A LOSS ANYWHERE IN THE CHAIN REACHES A TERMINAL CONSUMER, which is the property three separate diff --git a/dag/test/claim/fabric/systemd_run_transient_wait_witness_test.dag b/dag/test/claim/fabric/systemd_run_transient_wait_witness_test.dag index 4bdcd70830b..27b4f382fe5 100644 --- a/dag/test/claim/fabric/systemd_run_transient_wait_witness_test.dag +++ b/dag/test/claim/fabric/systemd_run_transient_wait_witness_test.dag @@ -2,7 +2,15 @@ module test.claim.fabric.systemd_run_transient_wait_witness_test import std.types { Bool, List } import extdeps.systemd { WorkingDirectoryProperty, MemoryMax } -import extdeps.systemd.systemd_run { SystemdRunProperty, systemd_run_transient_wait_unit_argv } +import extdeps.exec.command { argv_words } +import extdeps.systemd.systemd_run { + SystemdRunProperty, SystemdRunCommandReady, SystemdRunCommandRefused, + SystemdRunOptionWordsProjected, SystemdRunOptionWordsRefused, + SystemdSetenvBinding, + RunUserManager, RunScope, Wait, Quiet, Pipe, Collect, EndOfOptions, SetEnv, + systemd_run_option_words, systemd_run_transient_wait_unit_command, systemd_run_user_wait_command, + systemd_run_transient_unit_command, systemd_run_transient_retained_unit_command, systemd_run_system_scope_command, +} import gunbc.systemd_run_transient { systemd_run_transient_wait_operation_argv_matches_authority, systemd_run_transient_wait_outcome_from_result, @@ -16,25 +24,199 @@ data wait_properties: List = [ SystemdRunProperty { property: MemoryMax, value: "17179869184" }, ] -test fn systemd_wait_route_owns_wait_quiet_collect_and_preserves_tokens() -> Bool { - let argv = systemd_run_transient_wait_unit_argv( - unit: "fci1-bounded-driver.service", - properties: wait_properties, - command_argv: ["/bin/sh", "-c", "exit 86"], - ) - systemd_run_transient_wait_operation_argv_matches_authority( +// THE POSITIVE CONTROL FOR THE TYPED CUTOVER: the words the typed option model projects are +// byte-identical to the words the string-built builder this replaces rendered for the same +// arguments, including the space inside the working directory's value, and the operation's +// materialized transport words are the same words the typed command projects. +fn wait_control_command_reading() -> SystemdRunCommandReading { + systemd_run_transient_wait_unit_command( unit: "fci1-bounded-driver.service", properties: wait_properties, command_argv: ["/bin/sh", "-c", "exit 86"], ) - && argv_exact_token_equal( - left: argv, - right: [ - "systemd-run", "--unit=fci1-bounded-driver.service", "--wait", "--quiet", "--collect", - "--property=WorkingDirectory=/tmp/path with space", "--property=MemoryMax=17179869184", - "--", "/bin/sh", "-c", "exit 86", - ], - ) +} + +// THE POSITIVE CONTROL, WORDS HALF: the projection's words are the old builder's words, byte for byte. +test fn systemd_wait_route_projects_the_old_words() -> Bool { + match wait_control_command_reading() { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + argv_exact_token_equal( + left: argv_words(command: command), + right: [ + "systemd-run", "--unit=fci1-bounded-driver.service", "--wait", "--quiet", "--collect", + "--property=WorkingDirectory=/tmp/path with space", "--property=MemoryMax=17179869184", + "--", "/bin/sh", "-c", "exit 86", + ], + ) + } +} + +// THE POSITIVE CONTROL, MATERIALIZATION HALF: the operation's declared transport materializes the +// same words the typed command projects. +test fn systemd_wait_route_materializes_the_projected_words() -> Bool { + match wait_control_command_reading() { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + systemd_run_transient_wait_operation_argv_matches_authority(command: command) + } +} + +// THE RED WITNESS: a property value carrying a newline is REFUSED at the projection -- not trimmed, +// not split into two words, not passed through. The operation that would have run does not exist, +// and the reason names the wire fact the value violates. +test fn a_property_value_the_wire_cannot_carry_is_refused() -> Bool { + match systemd_run_transient_wait_unit_command( + unit: "fci1-refused.service", + properties: [SystemdRunProperty { property: WorkingDirectoryProperty, value: "/tmp/a\nb" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: UnitSettingValueMultiline { property: p } } => systemd_unit_property_wire(property: p) == "WorkingDirectory" + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: _ } => false + } +} + +// ── BYTE IDENTITY, EVERY BUILDER: one control per builder shape, each pinning the words the +// retired glued builders emitted, in the order they emitted them. A control per builder is the +// point: the wait fixture alone proved one shape and the other builders drifted silently. + +// The collecting transient: --unit, --collect, the properties, then -- (the base order the +// retired glued builder and its transport both emitted; the side-chat review corrected this +// control -- main's property-projection witness never pinned the full order). +test fn the_collecting_transient_builder_emits_unit_collect_properties() -> Bool { + match systemd_run_transient_unit_command( + unit: "fci1-order.service", + properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + argv_exact_token_equal( + left: argv_words(command: command), + right: [ + "systemd-run", "--unit=fci1-order.service", "--collect", "--property=MemoryMax=17179869184", "--", "/bin/true", + ], + ) + } +} + +// The retained transient: --unit, the properties, then --, with NO --collect — the terminal state +// stays observable, which is why the retained variant exists. +test fn the_retained_transient_builder_omits_collect() -> Bool { + match systemd_run_transient_retained_unit_command( + unit: "fci1-retained.service", + properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + argv_exact_token_equal( + left: argv_words(command: command), + right: ["systemd-run", "--unit=fci1-retained.service", "--property=MemoryMax=17179869184", "--", "/bin/true"], + ) + } +} + +// The user wait builder: --wait --pipe --collect lead, then --user --unit, short properties, the +// setenv bindings, then --. +test fn the_user_wait_builder_renders_the_old_order() -> Bool { + match systemd_run_user_wait_command( + unit: "fci1-user.service", + properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], + setenv_bindings: [SystemdSetenvBinding { name: "A" as NonEmptyStr, value: "c" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdSummaryPrintingWaitRefused { cause: _ } => false + SystemdSummaryPrintingWaitReady { wait: launcher } => + argv_exact_token_equal( + left: argv_words(command: launcher.command), + right: [ + "systemd-run", "--wait", "--pipe", "--collect", "--user", "--unit=fci1-user.service", + "-p", "MemoryMax=17179869184", "--setenv=A=c", "--", "/bin/true", + ], + ) + } +} + +// The scope builder: --user --scope lead (the account drop runs under the user manager), long +// properties, then --. +test fn the_scope_builder_leads_with_scope_only() -> Bool { + match systemd_run_system_scope_command( + properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + argv_exact_token_equal( + left: argv_words(command: command), + right: ["systemd-run", "--scope", "--property=MemoryMax=17179869184", "--", "/bin/true"], + ) + } +} + +// A unit name is one line and never carries a slash (systemd.unit(5)); the projection refuses both. +test fn a_unit_name_the_wire_cannot_carry_is_refused() -> Bool { + match systemd_run_transient_wait_unit_command( + unit: "wrong/name" as NonEmptyStr, + properties: [], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: UnitNameContainsSlash } => true + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: _ } => false + } +} + +// --setenv= takes NAME=VALUE, so the NAME side carries no = of its own: a binding that would render +// an ambiguous word is refused, not passed to systemd-run to misparse. +test fn a_setenv_name_with_an_equals_is_refused() -> Bool { + match systemd_run_user_wait_command( + unit: "fci1-setenv.service", + properties: [], + setenv_bindings: [SystemdSetenvBinding { name: "A=b" as NonEmptyStr, value: "c" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdSummaryPrintingWaitRefused { cause: SetenvNameContainsEquals } => true + SystemdSummaryPrintingWaitRefused { cause: _ } => false + SystemdSummaryPrintingWaitReady { wait: _ } => false + } +} + +test fn a_setenv_binding_with_an_equals_in_the_name_is_refused() -> Bool { + match systemd_run_option_words(options: [SetEnv { binding: SystemdSetenvBinding { name: "A=b" as NonEmptyStr, value: "c" as NonEmptyStr } }]) { + SystemdRunOptionWordsRefused { cause: SetenvNameContainsEquals } => true + SystemdRunOptionWordsRefused { cause: _ } => false + SystemdRunOptionWordsProjected { words: _ } => false + } +} + +// The cause sum discriminates: two different wire defects yield two different constructors, so a +// caller can branch on the cause without matching on prose. Tags 0 and 1 must differ. +test fn refusal_causes_discriminate_two_wire_defects() -> Bool { + let slash_tag = match systemd_run_option_words(options: [RunUnit { unit: "wrong/name" as NonEmptyStr }]) { + SystemdRunOptionWordsRefused { cause: UnitNameContainsSlash } => 1 + SystemdRunOptionWordsRefused { cause: _ } => 0 - 1 + SystemdRunOptionWordsProjected { words: _ } => 0 - 1 + } + let equals_tag = match systemd_run_option_words(options: [SetEnv { binding: SystemdSetenvBinding { name: "A=b" as NonEmptyStr, value: "c" as NonEmptyStr } }]) { + SystemdRunOptionWordsRefused { cause: SetenvNameContainsEquals } => 2 + SystemdRunOptionWordsRefused { cause: _ } => 0 - 1 + SystemdRunOptionWordsProjected { words: _ } => 0 - 1 + } + (slash_tag == 1) && (equals_tag == 2) && (slash_tag != equals_tag) +} + +// THE TYPED OPTION IS A SEALED SUM: there is no arm an unknown flag could ride. This fold walks the +// variants the module renders today and pins each one's man-page spelling at systemd 255; a variant +// added to the sum updates this fold or compilation fails, which is how an unknown option stays +// unwritable. +test fn every_option_variant_renders_its_man_page_spelling() -> Bool { + match systemd_run_option_words(options: [RunUserManager, RunScope, Wait, Quiet, Pipe, Collect, EndOfOptions]) { + SystemdRunOptionWordsRefused { cause: _ } => false + SystemdRunOptionWordsProjected { words: words } => + join(words, separator: " ") == "--user --scope --wait --quiet --pipe --collect --" + } } test fn systemd_wait_route_rejects_split_working_directory_token() -> Bool { @@ -54,3 +236,37 @@ test fn systemd_wait_normal_exit_86_remains_86() -> Bool { SystemdRunTransientWaitTransportRefused { reason: _ } => false } } + + +fn materialized_cardinality(reading: SystemdRunCommandReading) -> Int { + match reading { + SystemdRunCommandRefused { cause: _ } => 0 - 2 + SystemdRunCommandReady { command: command } => + match systemd_run_transient_wait_operation_argv(command: command) { + ArgvMaterialized { argv: words } => count(words) + ArgvMaterializationRefused { at: _, cause: _ } => 0 - 1 + } + } +} + +// THE CARDINALITY CONTROLS, ALL TRUE-RETURNING. Every floor claim answers TRUE; the negative +// controls are folded into assertions of absence rather than designed-false claims, so the +// discrimination survives without a claim that reads as a floor red: the exact cardinality is +// pinned at 11 (the materialized shape: the leading literal plus ten projected words), the two +// refusal sentinels are asserted absent, and the degenerate counts (empty, singleton, the +// option-word-only 10) are asserted absent. materialized_cardinality above is a plain fn, so a +// refusal would move the answer to its sentinel and these claims would go red, not silent. +test fn probe_materialized_cardinality_is_exactly_11() -> Bool { + materialized_cardinality(reading: wait_control_command_reading()) == 11 +} + +test fn probe_cardinality_refusal_arms_are_not_produced() -> Bool { + let c = materialized_cardinality(reading: wait_control_command_reading()) + (c != 0 - 1) && (c != 0 - 2) +} + +test fn probe_cardinality_degenerate_counts_are_not_produced() -> Bool { + let c = materialized_cardinality(reading: wait_control_command_reading()) + (c != 0) && (c != 1) && (c != 10) +} + diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index 44068994dbd..2c6568c0a91 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -63,7 +63,8 @@ import gunbc.managed_host { ManagedHost, managed_hosts, managed_host_binding, Ma import extdeps.bmc.endpoint { BmcControllerEndpoint } import extdeps.github.actions_runner { ActionsRunnerLinuxArm64 } import gunbc.runner_slot_desired { gunbc_runner_slot_desired } -import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_labels, ephemeral_slot_argv } +import extdeps.systemd.systemd_run { SystemdRunCommandReady, SystemdRunCommandRefused } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_labels, ephemeral_slot_command } import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of, readback_subject_refusal, DeregistrationReadbackForAnotherSubject } import gunbc.runner.runner_host_unit_slot { HostUnitSlotStanding, HostUnitSlotRefused, HostUnitSlotHostNotBound, host_unit_slot_for, host_unit_slot_listener_argv, @@ -1376,7 +1377,10 @@ test fn witness_jit_mint_authorizes_a_managed_host_unit_slot_under_the_routes_ow (req.name.value as String) == (ephemeral_slot_unit(host: h.host, attempt: "q-1") as String) && req.runner_group_id.value == 3 && runner_label_sets_equal(a: map(req.labels, l => l.value as String), b: ephemeral_slot_labels(host: h.host, attempt: "q-1")) - && u.argv == ephemeral_slot_argv(unit: u.unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()) + && match ephemeral_slot_command(unit: u.unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()) { + SystemdRunCommandReady { command: c } => u.argv == c + SystemdRunCommandRefused { cause: _ } => false + } && match u.runner.arch { ActionsRunnerLinuxArm64 => true _ => false } _ => false }) diff --git a/dag/test/claim/nbd_proxy_serve_transport_witness_test.dag b/dag/test/claim/nbd_proxy_serve_transport_witness_test.dag index 14b877ea7f4..d65fadfb662 100644 --- a/dag/test/claim/nbd_proxy_serve_transport_witness_test.dag +++ b/dag/test/claim/nbd_proxy_serve_transport_witness_test.dag @@ -5,6 +5,9 @@ import extdeps.filesystem.filesystem_io { filesystem_listing_observation, filesystem_file_observation, } +import extdeps.exec.command { argv_words } +import extdeps.systemd { MemoryMax, WorkingDirectoryProperty } +import extdeps.systemd.systemd_run { SystemdRunProperty, SystemdRunCommandReady, SystemdRunCommandRefused, systemd_run_transient_unit_command } import gunbc.host_effect_nbd_proxy_serve { BmcwebSessionTokenObservation, BmcwebSessionTokenUsable, @@ -217,11 +220,14 @@ test fn nbd_proxy_observe_port_local_shell_is_active_active_wires_through() -> B } test fn witness_systemd_run_transient_operation_argv_matches_authority() -> Bool { - systemd_run_transient_operation_argv_matches_authority( + match systemd_run_transient_unit_command( unit: "nbd-proxy-srv3-nbd-proxy-10809" as NonEmptyStr, properties: [], command_argv: ["nbdkit", "-p", "10809", "file", "/var/lib/gunbc/artifacts/x.iso"], - ) + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => systemd_run_transient_operation_argv_matches_authority(command: command) + } } // THE EMPTY POPULATION ABOVE CANNOT SEE A PROPERTY THAT NEVER REACHES THE EXECUTED WORDS, which is @@ -230,11 +236,14 @@ test fn witness_systemd_run_transient_operation_argv_matches_authority() -> Bool // is launched unbounded. This control carries a real population so the materialized argv and the // extdeps authority must agree on the property word itself. test fn witness_systemd_run_transient_property_reaches_materialized_argv() -> Bool { - systemd_run_transient_operation_argv_matches_authority( + match systemd_run_transient_unit_command( unit: "nbd-proxy-srv3-nbd-proxy-10809" as NonEmptyStr, properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], command_argv: ["nbdkit", "-p", "10809", "file", "/var/lib/gunbc/artifacts/x.iso"], - ) + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => systemd_run_transient_operation_argv_matches_authority(command: command) + } } // THE WHITESPACE ARM IS THE ONE THE OLD COMPARISON COULD NOT SEE. join(a," ")==join(b," ") cannot @@ -243,11 +252,14 @@ test fn witness_systemd_run_transient_property_reaches_materialized_argv() -> Bo // such a value here means the control fails if argv agreement is ever weakened back to a text // comparison: the words still join to the same text, and only cardinality separates them. test fn witness_systemd_run_property_value_with_space_keeps_token_identity() -> Bool { - systemd_run_transient_operation_argv_matches_authority( + match systemd_run_transient_unit_command( unit: "nbd-proxy-srv3-nbd-proxy-10809" as NonEmptyStr, properties: [SystemdRunProperty { property: WorkingDirectoryProperty, value: "/srv/path with space" as NonEmptyStr }], command_argv: ["nbdkit", "-p", "10809"], - ) + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => systemd_run_transient_operation_argv_matches_authority(command: command) + } } // THE ENCODING'S OWN PRECONDITION IS ASSERTED, not assumed. argv_exact_token_equal is injective @@ -262,13 +274,19 @@ test fn witness_argv_exact_token_equal_separates_split_words() -> Bool { // authority against one materialization; if the transport template silently dropped the property // words, BOTH sides would drop them together only if the authority also stopped rendering them -- // which this asserts it does not. The property word must be present in the authority's own output. +// POSITIVE CONTROL FOR THE CUTOVER: the wire name reaches the projected words untransformed, in +// position, in the non-wait start the nbd proxy's serve path actually runs. test fn witness_systemd_run_property_argv_renders_the_wire_name() -> Bool { - join( - systemd_run_property_argv( - properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], - ), - " ", - ) == "--property=MemoryMax=17179869184" + match systemd_run_transient_unit_command( + unit: "nbd-proxy-witness.service", + properties: [SystemdRunProperty { property: MemoryMax, value: "17179869184" as NonEmptyStr }], + command_argv: ["/bin/true"], + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + join(argv_words(command: command), " ") + == "systemd-run --unit=nbd-proxy-witness.service --collect --property=MemoryMax=17179869184 -- /bin/true" + } } test fn witness_systemctl_stop_operation_argv_matches_authority() -> Bool { diff --git a/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag b/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag index 00af84b6015..70241f2e6f0 100644 --- a/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag +++ b/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag @@ -2,13 +2,21 @@ module test.claim.roadmap.dispatch_worker_confinement_witness_test import std.types { Bool, String, List } import gunbc.roadmap_dispatch_actuator { dispatch_worker_unit_properties, attempt_state_prepare_argv_for_instance, dispatch_attempt_verification_dir_for_instance, attempt_unit_substate_has_exited, dispatch_review_unit_properties } +import extdeps.systemd { SystemdUnitProperty, systemd_unit_property_wire } +import extdeps.systemd.systemd_run { SystemdRunProperty } import gunbc.roadmap_model { RoadmapNodeId } import std.measure { kibibyte } import gunbc.roadmap_session_placement { session_process_bounds } import gunbc.roadmap_dashboard_instance { srv2_deploy_dashboard_instance } -fn has_property(props: List, needle: String) -> Bool { - any(props, p => string_contains(s: p, pattern: needle)) +// The claims are about WHICH SETTINGS the unit binds; rendering the typed properties back to their +// wire words here is a test observation, not a builder -- nothing hands these words to systemd-run. +fn property_words(props: List) -> List { + props |> map(p => concat(systemd_unit_property_wire(property: p.property) as String, concat("=", p.value as String))) +} + +fn has_property(props: List, needle: String) -> Bool { + any(property_words(props: props), p => string_contains(s: p, pattern: needle)) } test fn the_worker_unit_confines_writes_to_the_instance_root() -> Bool { diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index e895320488e..dcccbf697a5 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -6,7 +6,13 @@ import gunbc.roadmap_sizing { import test.claim.roadmap.roadmap_node_fixture { fx_authored, fx_authored_wi, fx_derivable_wi, fx_ticket_row, fx_ticket_wi, fx_superseded, fx_signed, fx_sign } import std.optional { Present } +import extdeps.systemd.systemd_run { SystemdRunCommandReady, SystemdRunCommandRefused } +import extdeps.exec.command { argv_words } +import v2.std.collection { List } +import v2.std.algebra { fold_list } +import std.types { GitRef } import std.types { NonEmptyStr, GitRef, FilePath, path_segment_is_safe } +import std.measure { byte_size_count } import std.disposition { Scaffold, Terminal } import gunbc.roadmap_model { RoadmapNodeId, @@ -53,6 +59,10 @@ import extdeps.llm.cli { reasoning_effort_label, } import gunbc.roadmap_dispatch_actuator { + dispatch_audit_unit_command, + dispatch_supervisor_unit_command, + dispatch_attempt_audit_events_path_for_instance, + dispatch_attempt_supervisor_events_path_for_instance, attempt_state_prepare_argv_for_instance, dispatch_reviewer_attempt_identity, dispatch_attempt_publication_dir_for_instance, @@ -90,7 +100,8 @@ import gunbc.roadmap_dispatch_actuator { dispatch_brief_fields_for_environment, dispatch_brief_with_centering, dispatch_continuation_alignment_preamble, - dispatch_supervisor_unit_argv, dispatch_supervisor_unit_name, dispatch_supervisor_attempt_identity, + dispatch_review_unit_command, dispatch_supervisor_unit_command, dispatch_unit_exec_argv, + dispatch_supervisor_unit_name, dispatch_supervisor_attempt_identity, DispatchSpawnCommands, HostExecArgv, SystemdUnitContainer, @@ -121,7 +132,6 @@ import gunbc.roadmap_dispatch_actuator { SpawnSession, dispatch_current_attempt_events_projection_argv_for_instance, gunbc_harness_review_argv, - dispatch_review_unit_argv, } import gunbc.dispatch_selection { dispatch_selection_standing_ineligible_reason, @@ -632,8 +642,11 @@ test fn witness_tmux_ls_parse_filters_prefix() -> Bool { TmuxLsParsed { entries } => { let filtered = filter_dispatch_tmux_sessions(entries: entries) count(filtered) == 1 - && filtered.first().session_name == "gunbc-dispatch-node-4" - && filtered.first().windows == 2 + && match filtered.first() { + Present { value: entry } => + entry.session_name == "gunbc-dispatch-node-4" && entry.windows == 2 + Absent => false + } } TmuxLsParseRefused { line_number: _, raw: _, reason: _ } => false } @@ -1162,14 +1175,13 @@ test fn witness_review_argv_carries_spawner_minted_attempt_identity() -> Bool { string_contains(s: join(argv, separator: "\0"), pattern: "\0--arg\0attempt_identity=reviewer:node-1-att-20260920-c") } -// THE SPAWN SITE, not just the argv row: dispatch_review_unit_argv is where the mint lives, and it +// THE SPAWN SITE, not just the argv row: dispatch_review_unit_command is where the mint lives, and it // is the fold the belt's review pass actually spawns through. This witness fails if a future edit // re-drops the argument between the unit fold and the argv row, which compilation alone would not // catch once the parameter exists and could be ignored. test fn witness_review_unit_argv_mints_per_criterion_attempt_identity() -> Bool { - let cmd = dispatch_review_unit_argv( - systemd_run_program: "/usr/bin/systemd-run" as FilePath, + match dispatch_review_unit_command( instance: srv1_lab_dashboard_instance(), node_id: roadmap_dispatch_actuator_nid(s: "node-1"), attempt_key: "att-20260920", @@ -1177,8 +1189,13 @@ test fn witness_review_unit_argv_mints_per_criterion_attempt_identity() -> Bool brief: "brief", head_sha: "abc123", criterion_key: "conformance-external-facts", - ) - string_contains(s: join(cmd.args, separator: "\0"), pattern: "attempt_identity=reviewer:6.node-1.12.att-20260920.26.conformance-external-facts.6.abc123") + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => { + let cmd = dispatch_unit_exec_argv(command: command, resolved_program: "/usr/bin/systemd-run" as FilePath) + string_contains(s: join(cmd.args, separator: "\0"), pattern: "attempt_identity=reviewer:6.node-1.12.att-20260920.26.conformance-external-facts.6.abc123") + } + } } // THE ROW THAT WOULD HAVE CAUGHT THE ORIGINAL BUG: an attempt identity built from a path-bearing @@ -1356,27 +1373,83 @@ test fn an_unresolved_alignment_refuses_the_real_spawn_fold_with_zero_commands() // reads. The session invokes the supervisor's own CLI function, never the worker's. test fn witness_supervisor_unit_argv_shapes_one_session_per_escalation_record() -> Bool { let lab = srv1_lab_dashboard_instance() - let argv = dispatch_supervisor_unit_argv( - systemd_run_program: "/usr/bin/systemd-run" as FilePath, + match dispatch_supervisor_unit_command( instance: lab, node_id: roadmap_dispatch_actuator_nid(s: "shell-dag-cron-entry-line-builder"), attempt_key: "cd172fe81b806160", turn: 3, worktree_path: "/x/attempts/wt", brief: "BRIEF-MARKER", - ) - let wire = join(argv.args, separator: "\0") - wire.contains("gunbc-supervisor-shell-dag-cron-entry-line-builder-cd172fe81b806160-t3.service") - && wire.contains("\0--function\0harness_supervisor_cli\0") - && wire.contains("\0worktree=/x/attempts/wt\0") - && wire.contains("BRIEF-MARKER") - && wire.contains("\0verdict_name=supervisor-verdict.json\0") - && wire.contains("/supervisor/harness-request.json") - && wire.contains("/supervisor/t3.provider-events.jsonl") - && wire.contains("ReadWritePaths=") - && wire.contains("/supervisor\0") - && !wire.contains("harness_worker_cli") - && dispatch_supervisor_attempt_identity(node_id: "shell-dag-cron-entry-line-builder", attempt_key: "cd172fe81b806160", turn: 3) - == "supervisor:shell-dag-cron-entry-line-builder-cd172fe81b806160-t3" - && wire.contains("attempt_identity=supervisor:shell-dag-cron-entry-line-builder-cd172fe81b806160-t3") + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => { + let cmd = dispatch_unit_exec_argv(command: command, resolved_program: "/usr/bin/systemd-run" as FilePath) + let wire = join(cmd.args, separator: "\0") + wire.contains("gunbc-supervisor-shell-dag-cron-entry-line-builder-cd172fe81b806160-t3.service") + && wire.contains("\0--function\0harness_supervisor_cli\0") + && wire.contains("\0worktree=/x/attempts/wt\0") + && wire.contains("BRIEF-MARKER") + && wire.contains("\0verdict_name=supervisor-verdict.json\0") + && wire.contains("/supervisor/harness-request.json") + && wire.contains("/supervisor/t3.provider-events.jsonl") + && wire.contains("ReadWritePaths=") + && wire.contains("/supervisor\0") + && !wire.contains("harness_worker_cli") + && dispatch_supervisor_attempt_identity(node_id: "shell-dag-cron-entry-line-builder", attempt_key: "cd172fe81b806160", turn: 3) + == "supervisor:shell-dag-cron-entry-line-builder-cd172fe81b806160-t3" + && wire.contains("attempt_identity=supervisor:shell-dag-cron-entry-line-builder-cd172fe81b806160-t3") + } + } +} + +// The writer/reader pairing for token metering: each launch's argv must name the SAME per-launch +// events path the metering reader opens — a path shared across launches let a later launch +// rewrite an earlier one's stream and erase its token usage (see the comment above +// dispatch_audit_unit_command in the actuator). +fn witness_argv_member(xs: List, x: String) -> Bool { + fold_list(xs: xs, empty: false, cons: fn(acc, y) { acc || (y == x) }) +} + +test fn witness_auditor_launch_writes_the_events_path_metering_reads() -> Bool { + let instance = srv1_live_dashboard_instance() + match dispatch_audit_unit_command( + instance: instance, + node_id: roadmap_dispatch_actuator_nid(s: "witness-node"), + attempt_key: "witness-attempt", + worktree_path: "/tmp/fci-witness-worktree", + brief: "witness brief", + head_sha: "fci1witnesssha", + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + witness_argv_member( + xs: argv_words(command: command), + x: join( + ["events_path=", dispatch_attempt_audit_events_path_for_instance(instance: instance, node_id: roadmap_dispatch_actuator_nid(s: "witness-node"), attempt_key: "witness-attempt", head_sha: "fci1witnesssha")], + "", + ), + ) + } +} + +test fn witness_supervisor_launch_writes_the_events_path_metering_reads() -> Bool { + let instance = srv1_live_dashboard_instance() + match dispatch_supervisor_unit_command( + instance: instance, + node_id: roadmap_dispatch_actuator_nid(s: "witness-node"), + attempt_key: "witness-attempt", + turn: 7, + worktree_path: "/tmp/fci-witness-worktree", + brief: "witness brief", + ) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + witness_argv_member( + xs: argv_words(command: command), + x: join( + ["events_path=", dispatch_attempt_supervisor_events_path_for_instance(instance: instance, node_id: roadmap_dispatch_actuator_nid(s: "witness-node"), attempt_key: "witness-attempt", turn: 7)], + "", + ), + ) + } } diff --git a/dag/test/claim/roadmap/session_placement_witness_test.dag b/dag/test/claim/roadmap/session_placement_witness_test.dag index 883a0ce527d..633db54e91a 100644 --- a/dag/test/claim/roadmap/session_placement_witness_test.dag +++ b/dag/test/claim/roadmap/session_placement_witness_test.dag @@ -17,6 +17,9 @@ import gunbc.compute.attempt_lifecycle { } import gunbc.roadmap_dashboard_instance { srv2_deploy_dashboard_instance } import gunbc.roadmap_dispatch_actuator { dispatch_worker_unit_properties } +import extdeps.systemd { systemd_unit_property_wire } +import extdeps.systemd.systemd_run { SystemdRunProperty } + import gunbc.roadmap_session_placement { SessionPlacementGrant, SessionPlacementStores, session_process_bounds, session_unit, session_attempt_record, session_placement_record_of, session_placement_record_json, session_placement_record_write, @@ -43,6 +46,11 @@ import extdeps.languages.json.emit { serialize_json } // The inhabitance claim: the bytes the real writer produces (session_placement_record_write) are what // the real reader and the real settlement read back as this attempt's grant. +// The claims are about WHICH SETTINGS the unit binds; rendering the typed properties back to their +// wire words here is a test observation, not a builder -- nothing hands these words to systemd-run. +fn property_words(props: List) -> List { + props |> map(p => concat(systemd_unit_property_wire(property: p.property) as String, concat("=", p.value as String))) +} data node_a: String = "g5-node-a" data key_a: String = "feedfacefeedface" data node_b: String = "g5-node-b" @@ -215,8 +223,8 @@ test fn settlement_releases_only_a_closed_window_or_an_ended_unit() -> Bool { // same fold, carries no process bounds. test fn the_worker_unit_is_capped_at_its_grant_by_the_compute_fold() -> Bool { let inst = srv2_deploy_dashboard_instance() - let props = dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1024), process_bounds: session_process_bounds) - let compute = compute_unit_properties(layout: compute_layout(instance: inst, identity_hex: "abc"), granted: kibibyte(count: 1024)) + let props = property_words(props: dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1024), process_bounds: session_process_bounds)) + let compute = property_words(props: compute_unit_properties(layout: compute_layout(instance: inst, identity_hex: "abc"), granted: kibibyte(count: 1024))) contains(props, "MemoryMax=1048576") && contains(props, "CPUQuota=800%") && contains(props, "TasksMax=4096") diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 987150c31c3..58a8cd86699 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.runner.runner_microvm_slot_controller_witness_test import v2.std.algebra { any } +import std.algebra { list_append } import std.types { String, NonEmptyStr, Bool, Int, List } import std.nat { Nat } import std.measure { byte_size_count, byte_size, gibibyte, gibibyte_to_byte_size, second_count } @@ -27,7 +28,10 @@ import gunbc.runner_microvm_attempt { MicroVmAttempt, microvm_attempt, MicroVmAttemptAccepted, MicroVmAttemptRefused, runner_microvm_attempt_slice_property, runner_microvm_attempt_part_of_property, } -import extdeps.systemd.systemd_run { systemd_run_property_argv } +import extdeps.exec.command { argv_words } +import extdeps.systemd.systemd_run { + SystemdRunCommandReady, SystemdRunCommandRefused, systemd_run_transient_unit_command, +} import gunbc.runner_microvm_shakedown { microvm_shakedown_work, microvm_shakedown_demand, microvm_shakedown_offer } import gunbc.runner_attempt_launch { ReservationAuthorization, ReservationAuthorizes, ReservationArmRequiresInternalFleetTrust, @@ -318,16 +322,34 @@ test fn only_a_delivered_mint_carries_a_registration_into_the_controller() -> Bo // (external review of gunbc#12011). A witness over the slot unit's own directives could not have // caught it -- those directives were all present and correct. // -// So this reads the ARGV: the exact `--property=` words systemd_run_property_argv builds from the -// same list run_jailer_in_cell passes, and requires PartOf to name this slot's supervising unit. +// So this reads the ARGV: the exact `--property=` words systemd_run_transient_unit_command +// projects (through systemd_run_option_words) from the same list run_jailer_in_cell passes, and +// requires PartOf to name this slot's supervising unit. // The Slice word is required in the same breath because the two are a pair -- the cell placement is // what makes the control-group kill insufficient, so a future edit that dropped Slice would make // PartOf redundant rather than load-bearing, and one that dropped PartOf leaks the guest. fn attempt_launch_property_words(attempt: MicroVmAttempt) -> List { - systemd_run_property_argv(properties: [ - runner_microvm_attempt_slice_property(attempt: attempt), - runner_microvm_attempt_part_of_property(attempt: attempt), - ]) + match systemd_run_transient_unit_command( + unit: "w-42.service", + properties: [ + runner_microvm_attempt_slice_property(attempt: attempt), + runner_microvm_attempt_part_of_property(attempt: attempt), + ], + command_argv: [], + ) { + SystemdRunCommandRefused { cause: _ } => [] + SystemdRunCommandReady { command: command } => property_words_of(argv: argv_words(command: command)) + } +} + +fn property_words_of(argv: List) -> List { + argv |> fold([], (acc, w) => + if substring(s: w, start: 0, end: 11) == "--property=" { + list_append(left: acc, right: [w]) + } else { + acc + } + ) } test fn the_attempt_unit_is_bound_to_the_slot_unit_that_supervises_it() -> Bool { diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index 9465908b8aa..21a65bf2872 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -11,6 +11,8 @@ import std.measure { } import std.decl_ref { decl_ref } import extdeps.currency.currency { Usd } +import extdeps.exec.command { argv_words } +import extdeps.systemd.systemd_run { SystemdRunCommandReading, SystemdRunCommandReady, SystemdRunCommandRefused } import extdeps.toolchain.types { Architecture, Aarch64, X86_64 } import product.host_identity { HostIdentity } import product.fabric.work { ControlPlaneCapacity, CustomerExecutableCapacity, Shape, HardRequirements } @@ -74,7 +76,7 @@ import gunbc.runner_throughput_qualification_route { QualificationStage, BootHostUnderApprovalGate, RegisterEphemeralRunnerSlot, DispatchFloorToSlot, CollectInstruments, DeregisterRunnerSlot, BmcWrite, GitHubControlPlaneWrite, ObservationOnly, stage_effect, stage_egress, - mtcollins1_qualification_route, ephemeral_slot_argv, ephemeral_slot_labels, ephemeral_slot_unit, + mtcollins1_qualification_route, ephemeral_slot_command, ephemeral_slot_labels, ephemeral_slot_unit, route_bmc_writes_are_gated, route_bmc_write_count, route_control_plane_write_count, route_effectful_stage_count, route_is_executable, route_registers_one_slot, route_deregisters_what_it_registered, route_dispatch_selector_names_the_attempt, @@ -866,14 +868,24 @@ test fn the_collect_stage_owes_the_rate_specimen_and_the_cgroup_rows() -> Bool { // THE EPHEMERAL SLOT CARRIES THE FLEET SLOT SHAPE, spelled through the systemd-run authority; under // SlotCpuQuotaUnbounded no CPUQuota word is emitted, which is the transient unit's own "no ceiling". test fn the_ephemeral_slot_argv_carries_the_fleet_memory_max() -> Bool { - let argv = ephemeral_slot_argv(unit: slot_unit(), desired: gunbc_runner_slot_desired(), command_argv: ["/mnt/runner/run.sh"]) - let has_quota_word = argv |> any(w => string_contains(s: w, pattern: "--property=CPUQuota=")) - (argv |> any(w => w == concat("--property=MemoryMax=", byte_size_count(b: gunbc_runner_slot_desired().memory_max) as String))) - && (match gunbc_runner_slot_desired().cpu_quota { - SlotCpuQuotaUnbounded => !has_quota_word - CpuQuotaResolved { threads: t } => - argv |> any(w => w == concat("--property=CPUQuota=", concat((hardware_thread_count_value(t: t) * 100) as String, "%"))) - }) + let reading = ephemeral_slot_command(unit: slot_unit(), desired: gunbc_runner_slot_desired(), command_argv: ["/mnt/runner/run.sh"]) + ephemeral_slot_words_carry_fleet_shape(reading: reading) +} + +fn ephemeral_slot_words_carry_fleet_shape(reading: SystemdRunCommandReading) -> Bool { + match reading { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => { + let argv = argv_words(command: command) + let has_quota_word = argv |> any(w => string_contains(s: w, pattern: "--property=CPUQuota=")) + (argv |> any(w => w == concat("--property=MemoryMax=", byte_size_count(b: gunbc_runner_slot_desired().memory_max) as String))) + && (match gunbc_runner_slot_desired().cpu_quota { + SlotCpuQuotaUnbounded => !has_quota_word + CpuQuotaResolved { threads: t } => + argv |> any(w => w == concat("--property=CPUQuota=", concat((hardware_thread_count_value(t: t) * 100) as String, "%"))) + }) + } + } } // THE CLASS LABEL IS READ OFF THE SLOT AUTHORITY, not written here (review 68972 -- the class this diff --git a/dag/test/manual/runner_microvm_lifecycle_wet_receipt_test.dag b/dag/test/manual/runner_microvm_lifecycle_wet_receipt_test.dag index 685327a42e7..ad96255995a 100644 --- a/dag/test/manual/runner_microvm_lifecycle_wet_receipt_test.dag +++ b/dag/test/manual/runner_microvm_lifecycle_wet_receipt_test.dag @@ -6,7 +6,10 @@ import std.resources { Network, Filesystem } import std.measure { Second, second } import extdeps.tools.sleep { sleep_delay_seconds_second_carrier_projection } import extdeps.systemd { SliceProperty, SystemdUnitProperty, ActiveState, systemd_unit_property_wire, SystemdSliceCgroupPath, SliceCgroupPath, SliceNameNotASlice, systemd_slice_cgroup_path } -import extdeps.systemd.systemd_run { SystemdRunProperty, systemd_run_property_argv } + +import extdeps.systemd.systemd_run { + SystemdRunProperty, SystemdRunCommandReady, SystemdRunCommandRefused, systemd_run_transient_unit_command, systemd_run_transient_retained_unit_command, systemd_run_command_argument_words, +} import extdeps.linux.cgroup_v2 { cgroup_v2_mount_point, cgroup_v2_child_path } import gunbc.runner_microvm_lifecycle { @@ -66,11 +69,11 @@ fn wet_slice_property() -> SystemdRunProperty { fn start_payload(unit: NonEmptyStr, argv: List) -> Bool uses net: Network { - systemd.SystemdRun.RunTransient( - unit: unit, - property_argv: systemd_run_property_argv(properties: [wet_slice_property()]), - command_argv: argv, - ).success + match systemd_run_transient_unit_command(unit: unit, properties: [wet_slice_property()], command_argv: argv) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + systemd.SystemdRun.RunTransient(command_argv: systemd_run_command_argument_words(command: command)).success + } } // EVERY CLAIM TEARS ITS OWN SLICE DOWN FIRST AND THEN WAITS FOR IT TO BE OBSERVABLY EMPTY. @@ -272,7 +275,6 @@ test fn a_missing_cgroup_events_is_unobservable_and_never_empty() -> Bool readback_is_read_failure(r: observe_cgroup_subtree(slice: "-.slice" as NonEmptyStr)) } - // THE SHOW-PROPERTY PAIR, AND IT IS A DISCRIMINATOR RATHER THAN TWO READINGS. // // systemd.Systemctl.ShowProperty takes a SystemdUnitProperty and interpolates it into @@ -402,14 +404,24 @@ fn incarnation_unit() -> NonEmptyStr { fn start_with_invocation(unit: NonEmptyStr, invocation: String, sleep_for: String) -> Bool uses net: Network { - systemd.SystemdRun.RunTransientRetained( + start_retained_payload( unit: unit, - property_argv: systemd_run_property_argv(properties: [ + properties: [ wet_slice_property(), attempt_invocation_env_setting(invocation_id: invocation as NonEmptyStr), - ]), + ], command_argv: ["/bin/sleep", sleep_for], - ).success + ) +} + +// Retained means the terminal state stays observable: the retained builder, and the argument +// tail only — the transport owns the "systemd-run" head. +fn start_retained_payload(unit: NonEmptyStr, properties: List, command_argv: List) -> Bool { + match systemd_run_transient_retained_unit_command(unit: unit, properties: properties, command_argv: command_argv) { + SystemdRunCommandRefused { cause: _ } => false + SystemdRunCommandReady { command: command } => + systemd.SystemdRun.RunTransient(command_argv: systemd_run_command_argument_words(command: command)).success + } } // THE STOP TAKES THE FIRST READING AS AN ARGUMENT SO IT CANNOT BE HOISTED ABOVE IT. Order in this