Skip to content

AutoAir 0.2.3

Latest

Choose a tag to compare

@github-actions github-actions released this 22 Oct 16:28

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 200 with the DAG list.
    A redirect (an SSO proxy or a wrong airflow_url sending 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, every AIRFLOW__*SECRET*/*_CMD setting, AIRFLOW_CONN_*, AIRFLOW_VAR_*, AWS_* keys, GOOGLE_APPLICATION_CREDENTIALS, AZURE_* secrets and other credential-like names are removed (sandbox.credential_env_pattern, new). Before, only AUTOAIR_* secrets were. Variables whose value is a URL with a password are removed whatever their name. DAG test runs with mode db/auto keep the metadata-DB settings and Fernet key dag.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 with sandbox.env_passthrough (new, fnmatch patterns, e.g. ["AIRFLOW_VAR_ENVIRONMENT"]).

  • Git commands of provider=local/gitea targets 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 as KRB5CCNAME. SSH private keys from Airflow connections were cached in one id_autoair file 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-lease on 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, @task bodies, raw code DAGs, hand-written parts of source-preserving deploys): an entry whose key matches policy.secret_param_pattern (and not the exclude pattern) and whose value is a string literal, such as {"token": "ghp_..."} or dict(password="..."), is a secret warning 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 serve with the default airflow_permissions strategy: 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 only iam.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 with iam.strategies=["role_mapping"] (the verified token's role claim, alone or together with airflow_permissions) or iam.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 whose Host (and Origin, when sent) is a loopback address or the host of server.public_url, against DNS rebinding and cross-site pages. iam.anonymous_roles defaults to ["Viewer"] (was ["Admin"]). Upgrade note: development setups that let anonymous callers edit and deploy set AUTOAIR_IAM__ANONYMOUS_ROLES='["Admin"]'.

  • autoair local is 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 gets local.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__TOKEN still set the token; the new --no-token serves without authentication with iam.anonymous_roles (Viewer).

  • Test runs (POST /runner/test-task, /runner/test-dag) execute the DAG's Python on the server and now need code:execute in addition to run:test, like every other code-running route. The built-in roles are unaffected (every role with run:test also holds code:execute); custom roles granting run:test alone lose test runs.

  • Plugin mode: state-changing requests authenticated by Airflow's _token cookie (Airflow 3.1+) now need the X-AutoAir-CSRF header and a same-origin or allowed Origin, as standalone cookie sessions already did. sso.require_csrf_header controls 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) through POST /api/templates or a marketplace install now needs code: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 (documents collection lsp_tickets, or <state>/lsp-tickets/), so a leaked ticket cannot be replayed on any replica.

  • autoair mcp --http (and autoair-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, checks Host and Origin like mcp-serve --http --no-auth, and gives callers iam.anonymous_roles (default Viewer, read-only). Upgrade note: a local agent that saves or deploys through it needs AUTOAIR_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) needs code:raw when 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 MCP save_dag_spec refuses the save. Before, only a deploy that wrote that code as the DAG file needed it, so a user without code:raw could store code that a later deploy by someone else wrote. This replaces the deploy's source_refused answer for such users (the Deployer role included) with the save-time 403; deploying the generated file instead now means saving with the source policy generate and without source.code. Upgrade note: custom roles that edit in the code mode or import Python DAGs and deploy them need code:raw (Author, CodeDeployer and Admin have it).

  • Four-eyes approvals (iam.require_approval): AutoAir now records who saved each stored spec version (shared state: documents collection spec_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_spec imported (executed) the generated DAG with import_check=true while annotated read-only, so MCP clients ran it without asking. The import check is now its own tool, import_check_dag_spec (RUNS_CODE, needs code:execute); validate_dag_spec only checks statically and ignores import_check (its result's note says so). Upgrade note: agents that passed import_check=true call import_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.sync database 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 needs deploy as well as spec:delete, and with iam.require_approval another user's approval: without one it is stored as a delete proposal (HTTP 202, status: "pending_approval"), executed with POST /specs/{dag_id}/proposals/{etag}/delete. Before, a delete went straight to the branch. Upgrade note: custom roles with spec:delete but without deploy can 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}/deploy and POST .../proposals/{etag}/delete answer 409 naming both etags, and GET .../proposals/{etag} reports stale. A proposal without a base_etag used 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_code or delete_dag_spec over HTTP opens a pull request where REST would (a PR-mode target, or iam.force_pr_without_direct_push for a caller without deploy:direct_push); a write that goes live needs deploy, and under iam.require_approval it 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 carry status (saved, deleted, pr_opened, pending_approval). Like PUT /specs, opening a pull request or a proposal needs only spec:write (spec:delete for 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 class execute: their calls wait for approval (the write class gates them too, so existing ai.approvals rules apply), and they are offered only to callers with code:execute, whatever ai.guardrails.tool_allowlist says. The MCP tool import_python_dag is no longer annotated read-only, so MCP clients ask before calling it.

  • External MCP servers' readOnlyHint is 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 new read_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 to read_only_tools.

  • AI memory: global and team memories reach every user's prompts, so adding them (remember with scope global or team, MCP and the assistants) and deleting them (forget) need admin:config; forget deletes a personal memory only for its owner (before, anyone with ai:use could delete any memory). Every remember call of AutoAir's assistants waits for approval; ai.approvals.*.exempt no longer applies to it. Upgrade note: rules that exempted remember now 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 for storage.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-data start 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_mismatch in the audit log otherwise). API clients that approve calls must send arguments_sha256 from 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 when validator.url (or the new validator.run_url) is set, through a new signed POST /v1/run with 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 most validator.max_concurrent_runs (new, 2) at a time and validator.max_run_seconds (new, 600) long; and (2) locally, start in their own PID, user and mount namespaces with a fresh /proc where the kernel allows (runner.process_isolation, new, auto/required/off), with PR_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 of validation/sandbox.py; import checks keep the stricter "check" profile). Where unprivileged user namespaces are restricted (Ubuntu 24.04+, most containers) auto falls 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/run and 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 unless validator.run_network (new) is false, 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 (Helm networkPolicy.egress, new) and set validator.run_url; when the validator cannot be asked, fail_mode open and closed both fail the test run (it never falls back to the local server) and only local runs it locally.

  • Local test runs no longer see Airflow's secrets in airflow.cfg or write into DAG folders: each run gets a private AIRFLOW_HOME with a copy of airflow.cfg without its secrets (Airflow's sensitive_config_values and options named like passwords, secrets or _cmd indirections), reading the real airflow.cfg, airflow.db, webserver_config.py and 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 from airflow.cfg or drop a DAG file that Airflow parsed without review. runner.process_isolation defaults to auto in 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-free airflow.cfg copy, 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; required stays available and refuses such runs. The test_run audit record and the run result (new where and process_isolated fields of TaskTestResult / 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), RunRequest ids must match the spec's key pattern, and validator.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: configure validator.url for isolated test runs, or set AUTOAIR_RUNNER__PROCESS_ISOLATION=required to refuse them; tests that relied on secrets from airflow.cfg or 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 in airflow.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 private AIRFLOW_HOME with a secret-free copy of airflow.cfg (Airflow's DAG and plugin folders are kept). The pool stays warm; it gets no PID namespace. Upgrade note: a DAG file that reads airflow.cfg secrets, the metadata DB or another process's /proc entry 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, need admin:config in addition to run:test and code:execute (audited 403 otherwise); mode=auto picks dag.test() only for those callers and otherwise runs in memory. GET /api/meta reports test_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: set AUTOAIR_RUNNER__ALLOW_DB_MODE=true to keep dag.test() for administrators; API clients that send mode=db otherwise get 403, and mode=auto now runs in memory for them.

Changed

  • Cron and interval schedules with data_interval: false are generated as explicit
    CronTriggerTimetable / DeltaTriggerTimetable instead of a bare cron string or
    timedelta, whose meaning Airflow's [scheduler] create_cron_data_intervals /
    create_delta_data_intervals decided. Regenerated DAG files change accordingly.

  • Spec reads (GET /api/specs, GET /api/specs/{dag_id}, version history and diffs) take
    an environment query parameter and default to default_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 SQL database.backend. On file:// URLs objects are written to a temporary file and renamed into place.

  • DELETE /api/specs/{dag_id} returns status (deleted, not_found, pending_approval, pr_opened), proposal_etag and message; deploy proposals carry action (deploy or delete).

  • Overwriting an existing DAG needs the etag it was loaded with: PUT /api/specs/{dag_id} without base_etag or If-Match answers 428 Precondition Required (the ETag header and the message carry the current etag) instead of silently replacing a concurrent edit; creating a new DAG needs none. MCP save_dag_spec and save_raw_dag_code take base_etag (the dry run returns current_etag, a save the new etag) and refuse an overwrite without it, returning current_etag so 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 from GET /api/specs/{dag_id}, or set AUTOAIR_STORAGE__REQUIRE_BASE_ETAG=false (new storage.require_base_etag, default true).

  • Deleting an existing DAG needs the etag it was loaded with as well: DELETE /api/specs/{dag_id} without If-Match answers 428 Precondition Required (the ETag header and the message carry the current etag) and a stale one 409, after the permission checks and before a delete proposal is stored. MCP delete_dag_spec takes base_etag (its dry run returns current_etag) and refuses a delete without it or with a stale one, returning current_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 send If-Match with the etag from GET /api/specs/{dag_id}, or set AUTOAIR_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) set validator.fail_mode=closed: deploys are refused while the validator service cannot be asked. The application default stays open. Upgrade note: copies of the production overlay that should keep deploying while the validator is down set AUTOAIR_VALIDATOR__FAIL_MODE=open in their settings.env.

Fixed

  • autoair serve / autoair local over 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. Without server.ssl_ciphers the 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 a TypeError; it now starts, and
    server.ssl_min_version=TLSv1.3 there 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
    ignores offset does 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 / **kwargs and 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 and run_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): FieldChange gains before_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, so dag.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 a Content-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 shows require_approval as off when features.iam is off.

  • Backup and restore docs: the PostgreSQL examples used an undefined $AUTOAIR_DB_URL; they now derive the URL from AUTOAIR_DATABASE__URL (and AUTOAIR_DATABASE__PASSWORD).

  • Python import: task retry_delay and execution_timeout, retries=0 (or any value equal to Airflow's default) overriding default_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, task inlets, executor, executor_config, retry_exponential_backoff, a param's description and 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_backoff multiplier other than 2 silently into True (base-2 backoff, the only one those versions have); validation now reports it as an airflow_version warning 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 than datetime.timedelta holds are a duration validation error instead of a DAG that fails with OverflowError. Start and end dates in a DAG timezone lost their microseconds. retry_exponential_backoff is 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 a default_args task template that every task extends. Task retry_delay / execution_timeout, dagrun_timeout and timedelta arguments of HITLOperator and the external task sensor are written as duration strings (5m) instead of being refused; retry_exponential_backoff, executor_config, render_template_as_native_obj and rerun_with_latest_version are exported, OpenLineage settings are reported, and importing restores the DAG timezone, the duration fields and run_immediately.

  • dag-factory export: dependencies on task groups are exported as dependencies of the group's tasks, end_date and 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