Repository navigation
Security
-
Generated DAGs write embedded scripts into a folder private to the worker's user
(<tmp>/airflow-scripts-<uid>, mode 0700, checked: not a symlink, owned by that user, no
group or other access; otherwise a fresh private folder). An existing script is used only
when it is a regular file of that user with exactly the expected content; anything else,
including a symlink planted at the content-addressed path, is replaced atomically. -
Standalone token verification accepts only Airflow's own answer: a
200with the DAG list.
A redirect (an SSO proxy or a wrongairflow_urlsending the check to a login page), another
2xx or an unexpected body used to count as a verified token, so a forged JWT's role claims
could be trusted. Such answers now fail closed with a 502 that names the URL. -
Processes that run DAG code (import checks, the validator's workers, Python imports, blueprint previews, test runs, language servers) no longer inherit Airflow's secrets or cloud credentials:
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN,AIRFLOW__CORE__FERNET_KEY,AIRFLOW__API_AUTH__JWT_SECRET, everyAIRFLOW__*SECRET*/*_CMDsetting,AIRFLOW_CONN_*,AIRFLOW_VAR_*,AWS_*keys,GOOGLE_APPLICATION_CREDENTIALS,AZURE_*secrets and other credential-like names are removed (sandbox.credential_env_pattern, new). Before, onlyAUTOAIR_*secrets were. Variables whose value is a URL with a password are removed whatever their name. DAG test runs withmodedb/autokeep the metadata-DB settings and Fernet keydag.test()needs, with a random per-run JWT secret instead of Airflow's. Upgrade note: DAGs that read a variable or connection from the environment at parse time, and task tests that need one, get it back withsandbox.env_passthrough(new, fnmatch patterns, e.g.["AIRFLOW_VAR_ENVIRONMENT"]). -
Git commands of
provider=local/giteatargets and repository bulk imports inherit only what git, ssh and curl read (PATH,HOME, locale,TMPDIR,SSH_AUTH_SOCK, CA and proxy variables,GIT_*), not the whole server environment;storage.git.env_passthrough(new) adds names such asKRB5CCNAME. SSH private keys from Airflow connections were cached in oneid_autoairfile per clone cache and never rewritten, so a user whose connection shared that cache (Gitea per-user credentials, a rotated key) pushed with the first user's key; each key now gets its own owner-only file named after its hash. -
Recycling a merged pull-request branch moved it with
git push --force, discarding a commit pushed to it meanwhile; it now pushes with--force-with-leaseon the head it saw (Gitea / local git; Azure DevOps uses the ref update's compare-and-swap, GitHub, GitLab and Bitbucket re-check the head first) and starts over when the branch moved. -
Deploy targets fail closed when AutoAir's state record (
.autoair/state/<dag_id>.json) is unreadable: no file of that DAG is replaced or deleted (409 naming the file and how to recover) instead of the record being ignored, which disabled the hand-written and hand-edited (ejected) file protection. Without a state record, an existing DAG file that differs from the new one counts as hand-edited and is replaced or deleted only with force (an untouched loader stub excepted). -
The hard-coded secret scan (
policy.secret_assignment_pattern) also finds secrets passed inline, such as keyword arguments (Connection(password="...")), statements after;and quoted command-line options; it was anchored to the start of a line. Deployments that override the pattern keep their own. -
The hard-coded secret scan also finds secrets in dict literals and
dict()calls in Python code (inline functions,@taskbodies, raw code DAGs, hand-written parts of source-preserving deploys): an entry whose key matchespolicy.secret_param_pattern(and not the exclude pattern) and whose value is a string literal, such as{"token": "ghp_..."}ordict(password="..."), is asecretwarning on the key's line. Values that are not plain literals (variables,os.environ[...],Variable.get(...), f-strings), Jinja or format placeholders, empty or short strings, environment variable names (GITHUB_TOKEN) and placeholders (<your-token>,changeme,********) are not reported. -
Standalone
autoair servewith the defaultairflow_permissionsstrategy: Airflow's DAG and configuration permissions cannot be checked there, and an unchecked permission counted as granted, so every user with a valid Airflow token (an Airflow Viewer included) got the Author, Deployer and Admin roles. An unchecked permission now grants onlyiam.airflow_permission_roles.unverified(new, default["Viewer"]); plugin mode, where Airflow answers these checks, is unchanged. Upgrade note: standalone deployments that relied on this grant roles explicitly withiam.strategies=["role_mapping"](the verified token's role claim, alone or together withairflow_permissions) oriam.default_roles. -
Servers without authentication (
autoair serve --no-auth,autoair mcp-serve --http --no-auth,autoair mcp --http/autoair-mcp --http,autoair ai-serve --no-auth,autoair local --no-token) refuse to bind a non-loopback address and answer only requests whoseHost(andOrigin, when sent) is a loopback address or the host ofserver.public_url, against DNS rebinding and cross-site pages.iam.anonymous_rolesdefaults to["Viewer"](was["Admin"]). Upgrade note: development setups that let anonymous callers edit and deploy setAUTOAIR_IAM__ANONYMOUS_ROLES='["Admin"]'. -
autoair localis authenticated by default: it creates a random access token on every start, prints a sign-in link and opens the UI with it (/ui/#token=..., kept in the URL fragment, which never reaches a server). The token holder getslocal.roles(new, default["Admin"]), so the local user keeps editing and deploying; other programs and web pages on the machine can no longer use the API.--token/AUTOAIR_LOCAL__TOKENstill set the token; the new--no-tokenserves without authentication withiam.anonymous_roles(Viewer). -
Test runs (
POST /runner/test-task,/runner/test-dag) execute the DAG's Python on the server and now needcode:executein addition torun:test, like every other code-running route. The built-in roles are unaffected (every role withrun:testalso holdscode:execute); custom roles grantingrun:testalone lose test runs. -
Plugin mode: state-changing requests authenticated by Airflow's
_tokencookie (Airflow 3.1+) now need theX-AutoAir-CSRFheader and a same-origin or allowedOrigin, as standalone cookie sessions already did.sso.require_csrf_headercontrols it in both modes. The bundled UI already sends the header; scripts that use the browser cookie instead of a bearer token must add it. -
Saving a template with hand-written DAG code (a raw code DAG,
spec.raw_code, or a kept Python file,spec.source.code) throughPOST /api/templatesor a marketplace install now needscode:raw, like saving that spec. Upgrade note: TemplateManager alone can no longer publish such templates. -
Collaboration tickets are bound to the requesting user and the room, open a single connection (claimed in the shared state store, so this holds across replicas) and are valid for 60 s by default (
collab_ticket_ttl, was 600). -
Language-server WebSocket tickets (
POST /api/lsp/session) could open any number of sockets within their 60 s TTL; like collaboration tickets they now name the user and open one socket, claimed in the shared state store (documentscollectionlsp_tickets, or<state>/lsp-tickets/), so a leaked ticket cannot be replayed on any replica. -
autoair mcp --http(andautoair-mcp --http) served every tool without authentication and without the loopback and DNS-rebinding protections of the other unauthenticated servers. It now refuses a non-loopback--host, checksHostandOriginlikemcp-serve --http --no-auth, and gives callersiam.anonymous_roles(default Viewer, read-only). Upgrade note: a local agent that saves or deploys through it needsAUTOAIR_IAM__ANONYMOUS_ROLES='["Admin"]'(or use stdio,autoair mcp, which acts as the local user). -
Storing hand-written Python in a spec (
source.code, the file a DAG was imported from or edited as in the code mode) needscode:rawwhen the code is new or differs from the stored spec's, whatever the source policy:PUT /api/specs/{dag_id}answers 403, the code mode apply (POST /api/interchange/python/sync) answers 403 when the applied file differs from the document's code, and MCPsave_dag_specrefuses the save. Before, only a deploy that wrote that code as the DAG file needed it, so a user withoutcode:rawcould store code that a later deploy by someone else wrote. This replaces the deploy'ssource_refusedanswer for such users (the Deployer role included) with the save-time 403; deploying the generated file instead now means saving with the source policygenerateand withoutsource.code. Upgrade note: custom roles that edit in the code mode or import Python DAGs and deploy them needcode:raw(Author, CodeDeployer and Admin have it). -
Four-eyes approvals (
iam.require_approval): AutoAir now records who saved each stored spec version (shared state:documentscollectionspec_version_authors, or<proposals URL>-authors/), so the saver of a version can no longer approve it (403) and their approval never counts towards putting it live, whoever deploys it; a version saved anonymously never satisfies four-eyes. Before, a version stored without a proposal (a pull request, a save made before approvals were required) could be approved by its own author and deployed by someone else. Versions saved before this release have no record and keep the previous rule (the deploying user counts as the author). -
MCP
validate_dag_specimported (executed) the generated DAG withimport_check=truewhile annotated read-only, so MCP clients ran it without asking. The import check is now its own tool,import_check_dag_spec(RUNS_CODE, needscode:execute);validate_dag_speconly checks statically and ignoresimport_check(its result'snotesays so). Upgrade note: agents that passedimport_check=truecallimport_check_dag_spec. -
Live-editing connections are closed after
collab.session_seconds(new, default 300); the browser reconnects with a new ticket, which re-checks DAG edit permission, so revoked permissions end co-editing within that interval. -
Messages relayed between replicas (
collab.syncdatabase or redis) are signed with HMAC-SHA256 under a key derived from the collaboration secret; unsigned or foreign messages are dropped. Upgrade note: every replica must share the collaboration secret (as tickets already required); during a rolling upgrade, messages from replicas still on the old version are dropped until they restart. -
DAG deletes follow the deploy governance. On git targets they follow the target's mode and
iam.force_pr_without_direct_push(a pull request removes the file); a delete that goes live needsdeployas well asspec:delete, and withiam.require_approvalanother user's approval: without one it is stored as a delete proposal (HTTP 202,status: "pending_approval"), executed withPOST /specs/{dag_id}/proposals/{etag}/delete. Before, a delete went straight to the branch. Upgrade note: custom roles withspec:deletebut withoutdeploycan no longer delete live; they still can through a pull request. -
A deploy proposal is approved, deployed or executed only while the stored spec is still the version it was made from: otherwise
POST /specs/{dag_id}/reviews(approve),POST .../proposals/{etag}/deployandPOST .../proposals/{etag}/deleteanswer409naming both etags, andGET .../proposals/{etag}reportsstale. A proposal without abase_etagused to overwrite the DAG blindly once it existed (a proposal for a then-new DAG, or one stored by 0.2.2 or earlier); it is now refused the same way. Upgrade note: pending proposals of existing DAGs from 0.2.2 must be proposed again. -
MCP writes follow the same rules as the REST deploy and delete routes: a confirmed
save_dag_spec,save_raw_dag_codeordelete_dag_specover HTTP opens a pull request where REST would (a PR-mode target, oriam.force_pr_without_direct_pushfor a caller withoutdeploy:direct_push); a write that goes live needsdeploy, and underiam.require_approvalit needs another user's approval of that version, else the tool stores a deploy or delete proposal like REST (status: pending_approval,proposal_etag). Results carrystatus(saved,deleted,pr_opened,pending_approval). LikePUT /specs, opening a pull request or a proposal needs onlyspec:write(spec:deletefor deletes), so these tools now show for Authors. -
AI agents: tools that run Python on the AutoAir server (
import_python_dag,deploy_dry_run) were read-only tools and ran without approval, so prompt-injected text in a log or document could make the agent execute code. They form a new tool classexecute: their calls wait for approval (thewriteclass gates them too, so existingai.approvalsrules apply), and they are offered only to callers withcode:execute, whateverai.guardrails.tool_allowlistsays. The MCP toolimport_python_dagis no longer annotated read-only, so MCP clients ask before calling it. -
External MCP servers'
readOnlyHintis no longer trusted: a compromised server could mark a destructive tool read-only to skip the approval. Only tools an admin lists in the server's newread_only_tools(fnmatch, configuration and the MCP servers tab) run without approval or reach the admin assistant. Upgrade note: tools that ran directly because the server marked them read-only now wait for approval until you add them toread_only_tools. -
AI memory: global and team memories reach every user's prompts, so adding them (
rememberwith scopeglobalorteam, MCP and the assistants) and deleting them (forget) needadmin:config;forgetdeletes a personal memory only for its owner (before, anyone withai:usecould delete any memory). Everyremembercall of AutoAir's assistants waits for approval;ai.approvals.*.exemptno longer applies to it. Upgrade note: rules that exemptedremembernow get an approval card. -
SSH private keys from git connections are cached as
<cache_dir>/keys/id_autoair-<hash>(mode 0600) for the git CLI; files of keys nobody used forstorage.git.ssh_key_cache_days(new, default 30; 0 keeps them) are removed whenever a new key file is written. Only AutoAir's own key files owned by the server's user are removed, never the key being used; using a key refreshes its age, and a removed key is written again on its next use. -
Untrusted text wrapped for the model (documents, logs, tool results) could escape the
<untrusted-data>block with a closing tag that had no>(the 0.2.2 ReDoS fix left unclosed tag starts in place). Every remaining<untrusted-data/</untrusted-datastart is now neutralised, still in linear time. -
Tool-call approval is bound to the exact arguments: the card shows how many characters are cut and offers Show full arguments (
GET /api/ai/approvals/{id}/calls/{call_id}/arguments, secrets masked), and an approval carries the arguments' SHA-256 (arguments_sha256); the run resumes only when the stored call still has exactly those arguments (409 and<feature>:tool_approval_mismatchin the audit log otherwise). API clients that approve calls must sendarguments_sha256from the pending call; approvals paused by 0.2.2 can only be denied. -
AI answers, troubleshooting reports, knowledge-base and documentation previews and other rendered Markdown no longer load remote images; they are shown as links, so a prompt-injected image URL cannot send conversation data to another server (inline raster
data:images still display). -
Plugin mode: DAG code in a test run could read the Airflow API server's environment (its database URL, Fernet and JWT secrets, connections) through
/proc/$PPID/environ, because the API server's process stays dumpable and AutoAir does not change it. Test runs now (1) run on the validator service whenvalidator.url(or the newvalidator.run_url) is set, through a new signedPOST /v1/runwith the same request and response signing, body limit, timeouts, metrics (autoair_validator_runs_total,autoair_validator_run_seconds) and a per-run log line as/v1/check, at mostvalidator.max_concurrent_runs(new, 2) at a time andvalidator.max_run_seconds(new, 600) long; and (2) locally, start in their own PID, user and mount namespaces with a fresh/procwhere the kernel allows (runner.process_isolation, new,auto/required/off), withPR_SET_NO_NEW_PRIVS, no core dumps, non-dumpable, and an audit hook that denies reading other processes'/proc/<pid>entries and other runs' folders (runner.audit_hook, new, on). Subprocesses, network and writes stay allowed in test runs (the "run" profile ofvalidation/sandbox.py; import checks keep the stricter "check" profile). Where unprivileged user namespaces are restricted (Ubuntu 24.04+, most containers)autofalls back to the audit hook, which does not cover subprocesses, and each run's log says so: use the validator in plugin mode. Upgrade notes: upgrade the validator together with the server (an older validator answers 404 for/v1/runand test runs fail with "upgrade it"); with a validator configured, test runs go there by default (validator.test_runs, new,true), where they have network unlessvalidator.run_network(new) isfalse, but the shipped NetworkPolicies deny all egress, so tasks that call services fail until you deploy a second validator for runs with its own egress policy (HelmnetworkPolicy.egress, new) and setvalidator.run_url; when the validator cannot be asked,fail_modeopenandclosedboth fail the test run (it never falls back to the local server) and onlylocalruns it locally. -
Local test runs no longer see Airflow's secrets in
airflow.cfgor write into DAG folders: each run gets a privateAIRFLOW_HOMEwith a copy ofairflow.cfgwithout its secrets (Airflow'ssensitive_config_valuesand options named like passwords, secrets or_cmdindirections), reading the realairflow.cfg,airflow.db,webserver_config.pyand the simple auth manager's password file is denied, and Airflow's DAG and plugin folders and AutoAir's DAG folder are read-only (audit hook; in the PID namespace also mounts, for subprocesses too, together with a mask over other runs' folders). Before, a test could read the Fernet key fromairflow.cfgor drop a DAG file that Airflow parsed without review.runner.process_isolationdefaults toautoin every mode, the Airflow plugin included: where the host does not allow the namespaces the run still executes with the audit hook, read-only DAG folders and the secret-freeairflow.cfgcopy, its result shows a non-blocking Not fully isolated notice ("configure validator.url for isolated test runs"), and the server logs one warning per process;requiredstays available and refuses such runs. Thetest_runaudit record and the run result (newwhereandprocess_isolatedfields ofTaskTestResult/DagTestResult) carry where the code ran and whether it had its own PID namespace. Validator test runs are retried only when they surely did not run (no second run after a timeout, a lost connection or a proxy 502/504, and no local fallback then),RunRequestids must match the spec's key pattern, andvalidator.runs_enabled(new) lets an import-check-only validator refuse/v1/run. Upgrade note: on hosts without unprivileged user namespaces (Ubuntu 24.04+, most containers) local test runs, in the plugin too, are only partly isolated and say so: configurevalidator.urlfor isolated test runs, or setAUTOAIR_RUNNER__PROCESS_ISOLATION=requiredto refuse them; tests that relied on secrets fromairflow.cfgor wrote into the DAG folder now fail. -
Local import checks (the warm DagBag pool used without
validator.url) could read the server's environment through/proc/$PPID/environ(in the plugin, the Airflow API server's database URL, Fernet and JWT secrets) and Airflow's secrets inairflow.cfg, and write into DAG folders. Pool workers now get the protections of local test runs that cannot break parsing a DAG file: the run profile of the audit hook (runner.audit_hook: no reading other processes'/proc/<pid>entries, other workers' and runs' folders or Airflow's secret files, no writes into Airflow's DAG and plugin folders or AutoAir's output folder), non-dumpable workers, and a privateAIRFLOW_HOMEwith a secret-free copy ofairflow.cfg(Airflow's DAG and plugin folders are kept). The pool stays warm; it gets no PID namespace. Upgrade note: a DAG file that readsairflow.cfgsecrets, the metadata DB or another process's/procentry at parse time now fails the local import check. -
DAG test runs with
mode=db(dag.test()) hand the DAG code the metadata DB connection and the Fernet key. They are now off by default (runner.allow_db_mode, new,false, read per request) and, when allowed, needadmin:configin addition torun:testandcode:execute(audited 403 otherwise);mode=autopicksdag.test()only for those callers and otherwise runs in memory.GET /api/metareportstest_runs(on_validator,db_mode_unavailable) and the test dialog disables dag.test() (writes a run) with the reason. Every test run is recorded in the audit trail (test_run). Upgrade note: setAUTOAIR_RUNNER__ALLOW_DB_MODE=trueto keepdag.test()for administrators; API clients that sendmode=dbotherwise get 403, andmode=autonow runs in memory for them.
Changed
-
Cron and interval schedules with
data_interval: falseare generated as explicit
CronTriggerTimetable/DeltaTriggerTimetableinstead of a bare cron string or
timedelta, whose meaning Airflow's[scheduler] create_cron_data_intervals/
create_delta_data_intervalsdecided. Regenerated DAG files change accordingly. -
Spec reads (
GET /api/specs,GET /api/specs/{dag_id}, version history and diffs) take
anenvironmentquery parameter and default todefault_environment, so git targets read
that environment's branch.server.airflow_list_max_entries(50000) bounds paged Airflow
lists. -
Object-storage targets (
storage.mode=object) serialise writers of one DAG with a lease in AutoAir's state backend (storage.object_lock_seconds,storage.object_lock_wait_seconds, new): two replicas that loaded the same version can no longer both save; the second gets a conflict. The lease is shared across hosts on a SQLdatabase.backend. Onfile://URLs objects are written to a temporary file and renamed into place. -
DELETE /api/specs/{dag_id}returnsstatus(deleted,not_found,pending_approval,pr_opened),proposal_etagandmessage; deploy proposals carryaction(deployordelete). -
Overwriting an existing DAG needs the etag it was loaded with:
PUT /api/specs/{dag_id}withoutbase_etagorIf-Matchanswers428 Precondition Required(theETagheader and the message carry the current etag) instead of silently replacing a concurrent edit; creating a new DAG needs none. MCPsave_dag_specandsave_raw_dag_codetakebase_etag(the dry run returnscurrent_etag, a save the newetag) and refuse an overwrite without it, returningcurrent_etagso the agent can retry. The editor sends the etag as before; deploying a new document onto a dag_id that already exists opens the merge dialog. Upgrade note: API clients that overwrite without an etag send the one fromGET /api/specs/{dag_id}, or setAUTOAIR_STORAGE__REQUIRE_BASE_ETAG=false(newstorage.require_base_etag, defaulttrue). -
Deleting an existing DAG needs the etag it was loaded with as well:
DELETE /api/specs/{dag_id}withoutIf-Matchanswers428 Precondition Required(theETagheader and the message carry the current etag) and a stale one409, after the permission checks and before a delete proposal is stored. MCPdelete_dag_spectakesbase_etag(its dry run returnscurrent_etag) and refuses a delete without it or with a stale one, returningcurrent_etag. The editor sends the etag of the loaded version, also when a renamed DAG's old files are removed. Upgrade note: API clients that delete without an etag sendIf-Matchwith the etag fromGET /api/specs/{dag_id}, or setAUTOAIR_STORAGE__REQUIRE_BASE_ETAG=false, which now also relaxes deletes. -
MCP saves to a git target in PR mode are allowed under
iam.require_approval: the pull request is the review. MCP writes that would go live under four-eyes, or push while pull requests are forced, are no longer refused: they store a proposal or open a pull request (see Security). -
The production Kustomize overlay (
overlays/production/settings/settings.env) and the Helm example values (deploy/helm/values-autoair.yaml) setvalidator.fail_mode=closed: deploys are refused while the validator service cannot be asked. The application default staysopen. Upgrade note: copies of the production overlay that should keep deploying while the validator is down setAUTOAIR_VALIDATOR__FAIL_MODE=openin theirsettings.env.
Fixed
-
autoair serve/autoair localover HTTPS: TLS 1.2 clients with current cipher policies
(Python's, for one) could not connect, because uvicorn's default cipher string left TLS 1.2
only SHA-1 CBC suites. Withoutserver.ssl_ciphersthe server now uses Python's default
set, as the setting describes. With uvicorn older than 0.47 (the version Airflow 3.0 and 3.1
constraints install) HTTPS failed to start with aTypeError; it now starts, and
server.ssl_min_version=TLSv1.3there names the uvicorn version it needs. -
Python DAG import reads
rerun_with_latest_version(Airflow 3.3+), which code generation
already wrote. -
Paged Airflow lists stop on a short page or a repeated page, so a server or proxy that
ignoresoffsetdoes not make AutoAir read the same page up to the entry limit. -
Python DAG import: positional TaskFlow arguments (
multiply(7)) are bound to their
parameters instead of falling back to the defaults; keyword-only and positional-only
parameters and their literal defaults are kept;*args/**kwargsand non-literal
defaults are reported instead of being dropped silently. -
Python DAG import: TaskFlow bodies keep the DAG file's imports they use (
import math
at the top of the file), so the regenerated task no longer fails with NameError and the
imported spec validates without F821; other module-level names are reported. -
Python DAG import:
CronTriggerTimetable(interval=..., run_immediately=True)keeps its
data window andrun_immediately(read from the Task SDK's private attribute where
needed). -
Inline functions that bind the same import alias to different objects (
floor as calc,
ceil as calc) no longer share one module-level import: the later function imports inside
its own body. -
Version diffs tell an absent key from an explicit null (
execution_timeout: null
overrides a DAG default):FieldChangegainsbefore_present/after_present, and the
diff view shows "(not set)". -
A DAG saved only to the default environment's git branch is listed and opened (it was 404
and missing from the list). -
Airflow connections, variables, pools and import errors are read page by page until
total_entries(Airflow clamps the page size), not only the first page. -
Object storage deploys: a writer whose write lease expired mid-save no longer undoes the
save of the replica that took the lease over (its spec could revert while the DAG file kept
the newer version). It takes the lease back, waits for that save to finish, and restores
only paths that still hold its own content; undo after any other failure is conditional the
same way. -
DAG test runs with
mode=db(dag.test()) failed on every Airflow version: on 3.1+ with "the dag is not serialized" (the generated DAG lives outside every configured DAG bundle, sodag.test()never found it), on 3.0 every task with an execution-API validation error (Airflow 3.0's Task SDK client sends JSON without aContent-Type, which current FastAPI rejects), and on 3.2 every task with "'State' object has no attribute 'svcs_registry'" (the in-process execution API's lifespan ended when its object was garbage-collected). The run now writes the DAG to the metadata DB under the first configured bundle when it is not serialized yet (a deployed DAG of the same id is left alone; the DAG processor later marks the test DAG stale) and works around both execution-API issues on the versions that have them. -
Test selected task… / Test whole DAG…, Trigger DAG (raw JSON) and Backfill: with invalid conf JSON the run buttons stayed enabled and sent the last valid conf; they are now disabled while the JSON does not parse or is not an object, with the reason next to the field (and next to the run buttons in the test dialog).
-
A deploy to a local folder or object storage whose DAG file write failed left the new spec, etag and history entry in place with the old DAG file; the files already written are now restored, so nothing advances.
-
A rendering error outside the builder (start-up, sign-in, providers) now shows a recoverable message with Try again and Reload page in the plugin and the standalone UI, instead of an empty page.
-
The four-eyes rule is computed in one place (
autoair.iam.proposals); the deploy route, the proposal routes, the admin overview and MCP used to compute it differently. Proposals without an author, and anonymous callers, never satisfy an approval, and the admin overview showsrequire_approvalas off whenfeatures.iamis off. -
Backup and restore docs: the PostgreSQL examples used an undefined
$AUTOAIR_DB_URL; they now derive the URL fromAUTOAIR_DATABASE__URL(andAUTOAIR_DATABASE__PASSWORD). -
Python import: task
retry_delayandexecution_timeout,retries=0(or any value equal to Airflow's default) overridingdefault_args, edge labels (a >> Label("x") >> b, also on edges from or to task groups),dagrun_timeout,is_paused_upon_creation,fail_fast,max_consecutive_failed_dag_runs,render_template_as_native_obj,owner_links, taskinlets,executor,executor_config,retry_exponential_backoff, a param'sdescriptionand length/pattern keywords, and XCom references nested in a list or dict argument were dropped; code-mode apply then lost them too. Constructor arguments of an operator whose class lists a mixin before the operator base were dropped as well. Arguments the spec has no field for (email,do_xcom_push, task callbacks, ...) and operator classes defined in the DAG file are now import warnings. -
Generated code for a target Airflow before 3.2 turned a
retry_exponential_backoffmultiplier other than 2 silently intoTrue(base-2 backoff, the only one those versions have); validation now reports it as anairflow_versionwarning on the task's field. -
Generated code: a Python task whose function name equals a module the file imports (
os,hashlib,tempfile,pendulum,datetime, or a name an inline function imports) shadowed the module, which broke parsing or the task at run time; the function is renamed in the code and validation warns (function_name_reserved). Durations that are infinite, not a number or larger thandatetime.timedeltaholds are adurationvalidation error instead of a DAG that fails withOverflowError. Start and end dates in a DAG timezone lost their microseconds.retry_exponential_backoffis generated as a bool for target Airflow versions before 3.2, which reject a number. -
Airflow YAML export wrote
default_args, which Airflow's YAML importer rejects; they are now adefault_argstask template that every task extends. Taskretry_delay/execution_timeout,dagrun_timeoutandtimedeltaarguments ofHITLOperatorand the external task sensor are written as duration strings (5m) instead of being refused;retry_exponential_backoff,executor_config,render_template_as_native_objandrerun_with_latest_versionare exported, OpenLineage settings are reported, and importing restores the DAG timezone, the duration fields andrun_immediately. -
dag-factory export: dependencies on task groups are exported as dependencies of the group's tasks,
end_dateand the remaining task settings are written, disabled tasks are left out, and every setting the file cannot carry (edge labels, params, callbacks, deadlines, unsupported schedules, setup/teardown,expand_kwargs, ...) is a warning instead of being dropped silently.
Install: pip install autoair==0.2.3 ·
Image: ghcr.io/ctolon/autoair:0.2.3