-
Notifications
You must be signed in to change notification settings - Fork 0
Migration
The legacy schema (integrations: / notify: / handoffs: / controllers:
/ control: / paseo_bin) still loads and runs unchanged — both schemas
coexist. Migration converts a legacy file to the connectors model, and it is
automatic, total, and fail-safe.
Deployed boxes auto-update; a schema change requiring a manual edit would crash-loop them. So on boot the daemon:
- detects legacy constructs (per file —
imports:are walked too), - transforms each legacy file,
- backs the original up alongside it (
<file>.pre-connectors, first backup wins), - swaps the file in and re-validates the whole config — on any failure
the original is restored, the daemon keeps running on it, and a
config needs manual migration: …notification names what failed, - logs a mapping summary.
Idempotent: a migrated file has no legacy constructs, so the next boot is a no-op. Because both schemas coexist, every intermediate state (some files migrated, some not) still loads.
Manual: conductor config migrate (same flow), --dry-run prints the
transformed YAML and the mapping summary, then runs the transform through
the SAME full validation as the real path — without writing anything.
The transform maps every legacy construct; a construct it cannot map (an
unknown integration type, control.enabled: false, a mixed-schema file) is
a hard error that names it and refuses to commit. Legacy keys the schema
no longer knows — retired options, inert fields, blocks from configs that
predate the schema — are dropped with a summary note instead: the
migration is one-time and must produce a loadable config from any legacy
file (the strict runtime loader would otherwise crash-loop the box on
auto-update). The output is checked against the strict runtime decode before
it is written. Fields the legacy engine never read (rule workspace:,
action project:/method:, match.project/match.status) and notify
events with no delivery sink drop with notes the same way. ${VAR}
references survive verbatim (the transform masks them around parsing;
secrets are never inlined). Carried blocks (store:, update:, …) keep
their original YAML, comments included; agents: is not carried — it is
converted (see the table below).
If the migration still cannot produce a loadable config, boot holds degraded instead of crash-looping: the process stays alive, logs the blocker, and retries migrate+load every minute until an edit (or a newer binary) unblocks it.
| legacy | connectors model |
|---|---|
integrations: - type: github (app/webhook/sweep/identity/retry/project_map/project_rewrite/me) |
a connectors: entry, fields carried |
github app.webhook_secret / app.verify_signature
|
webhook.secret / webhook.verify_signature — verification describes the RECEIVER, not App auth, and an App-less config parked them under app: with nothing else in it. The old keys are refused at load with a message naming the new ones; app: keeps only app_id/private_key_path, and disappears entirely when it held nothing else |
github rules:/defaults: (most-specific repo wins) |
a per-trigger filter: {repo: …} + computed not_repo:, the same winner per repo; the defaults merge is flattened into each trigger |
every github kind + its action filters (labels_any/labels_all/authors/assignee/sole_assignee/reviewer/from_users/ignore_users/ignore_checks/require_label/include_prereleases/gates/exclude) and variants |
on: <conn>.<kind> triggers, name: = variant; the predicate keys map into one filter: under their unified names (label_any, author, comment_author/not_comment_author, not_branch/not_label_any/not_title, not_draft), and reviewer/assignee/ignore_checks/include_prereleases into options:
|
flaky_rerun / stuck_after / poll_interval / max_attempts_per_head
|
trigger options:
|
action steps: (id/if/type/agent/prompt/checkout/workdir/env/output_schema/background/handoff/retry/backend) |
steps: carried field-for-field (the legacy rerequest_review: field is retired — use a uses: <conn>.rerequest_review step) |
slack triggers: (on/reaction/command) |
on: <conn>.<event> + a filter:; a multi-variant rule merges into ONE trigger whose step is parallel branches (ids variant-prefixed, intra-variant references rewritten), so the feedback aggregation point is the join |
slack ack / on_done / on_fail
|
hooks at: start/done/fail using <conn>.react / <conn>.post — on_done fires once after ALL variants complete and on_fail once when any failed, identical to the legacy aggregation |
cron schedules:
|
connection schedules: + one trigger per schedule |
webhook sources: (path/sign/match/title/dedup/repo) |
connection sources: + one trigger per source (repo: on the trigger) |
sentry / pagerduty rules: (match, repo) |
one trigger per rule; later triggers carry exclude: maps of every earlier rule's match, so the legacy first-match winner is preserved under independent triggers (a rule behind a catch-all was unreachable and is skipped with a note) |
rss feeds: (url/interval/match/repo) |
connection feeds: + one trigger per feed (match as its filter) |
handoffs: (web + tunnels, slack/discord dm/thread) |
ask-capable connectors; the default entry's name is stamped onto background steps that named none |
controllers: |
runtimes: (same fields; agent controller: refs stay valid) |
paseo_bin |
the paseo runtime's bin:
|
control: (shadow/pause_label/max_concurrent_agents/max_agents_per_hour) |
the global policy:; an explicit enabled: false refuses to migrate (the kill switch is now only the runtime conductor pause) |
notify: (on/via/sinks/digest/push) |
triggers on the conductor.* lifecycle events, one per enabled event (legacy escalate → conductor.escalate + conductor.failed), whose steps are the sink verbs — generated connectors (notify-slack, notify-ntfy, …) with byte-identical wire payloads; digest → a grouped conductor.complete trigger (group: { window }); the inert push is dropped with a note. The block itself is retired (a standalone pass also rewrites it on already-migrated files) |
agents: |
removed — the block no longer exists. Each of the five jobs agents.<name> was doing moves to a home that is not an agent: provider+model → model: on the step (an exact pin; the migration never invents a fleet), budget → the runtime the agent ran on, and the behavior/memory/session/outcome fields → onto each step that referenced the profile. There is no registry to move a profile into, so the behavior is INLINED at every site that named it; sites in one file share a YAML anchor parked under x-migrated:, sites in different files each get a copy (anchors do not cross imports:). The profile table is gathered from the whole import tree first, so a conf.d/*.yaml file with references but no agents: block of its own still gets them inlined. Track record survives: the template is emitted under the OLD agent name, and every step extending it inherits that name as its identity — so outcome stats, engagements and agent:<name> memory scopes keep matching what is already on disk |
store:, update:, imports:, dry_run, adopt_open_workspaces
|
carried through unchanged |
agent_guidance |
folded into policy.guidance (the global scope of the guidance cascade) by a standalone pass — see Reuse; the top-level alias stays accepted |
Secret references migrate too — on legacy files AND on connectors-schema files that still carry the pre-vaults model (the pass runs standalone at boot, and is idempotent):
| pre-vaults | vaults model |
|---|---|
vault:entry |
{{ vault "local" "entry" }} + vaults: local (conductor) — the existing vault.json keeps working at its default path |
op://Item/field |
{{ vault "op" "Item/field" }} + an onepassword entry |
pass:name |
{{ vault "pass" "name" }} + a pass entry |
file:/dir/name |
{{ vault "files" "name" }} + one file entry per directory |
secrets: {x: <ref>} |
block removed; every {{.secrets.x}} usage rewritten inline (vault call / ${VAR} / the literal); an unrewritable usage is a hard error |
oauth2 refresh_token: vault:x
|
kept as the {{ vault … }} seed + token_vault: added, so rotation keeps persisting |
Existing vaults: entries are reused when they match; fresh names dodge
collisions with a numeric suffix. After migration the old forms are
rejected: an unmigrated scheme ref or secrets: block fails loudly with a
pointer at conductor config migrate — never silently treated as a literal.
Behavioral-equivalence golden tests feed identical webhook payloads through the legacy integration and the migrated-then-lowered one and assert the same triggers fire the same work (kind, variant, dedup signature, step content). The shipped legacy example transforms and passes full semantic validation; the e2e suite boots a daemon on a legacy config and asserts the backup, the swap, and that the same fixture still produces the same commit — plus the fail-safe refusal on an unmappable file.
Legacy removal is a later release, after deployed boxes have auto-migrated.
Related: Configuration · Connectors · Commands
Setup
The model
- Connectors
- Workflows
- Reuse
- Settings-and-Templating
- Packs
- Verbs
- Code-Steps
- Stores
- Runtimes
- Model-Selection
- Model-Discovery
- Steps
- Decide-Steps
- Grouping
- Memory
- Binary-Data
- Agent-Skill
- Policy
- Gates
- Teams
- Outcomes
- Cost-Accounting
- Secrets
- Hosts
- Isolation
- Trust-and-Isolation
Connectors
Operations
- One-Shot
- Callable-Service
- Runs
- Hand-offs
- Notifications
- Migration
- Controllers (legacy name → Runtimes)