Skip to content

template import creates LLC agents without the adapter checks the hire path applies #14800

Description

@mrveiss

The company-template import creates agents with an unchecked adapter_type

autobot-backend/llc/services/portability.py:621-660 (_import_agent, reached from
:251) inserts straight into agent_org_nodes with adapter_type taken from an
imported JSON template:

INSERT INTO agent_org_nodes
   (... company_id, adapter_type, adapter_config, heartbeat_cron, ...)

There is no registration check and no availability check on that path.

Why it matters

POST /companies/{id}/agent-hires rejects an unregistered adapter_type, and now
also rejects one whose adapter cannot run on this deployment (#14797). The import
path creates the same kind of row — a real, heartbeat-dispatched agent — with
neither check.

So a template naming claude_code on a host without the CLI, or naming an adapter
type that does not exist at all, produces exactly the symptom the hire-time checks
exist to prevent: the agent is created, every heartbeat is skipped, and it looks
degraded forever with no indication why.

This is pre-existing — the import path never had these checks, and #14797 did
not introduce the asymmetry. Filing it so the gate is not mistaken for complete:
right now it covers one of the two paths that create runnable agents.

Fix

Route _import_agent through the same two checks before insert. The import response
schema already carries warnings and skipped, so an unrunnable adapter can be
reported rather than silently created — which is likely better than failing the
whole import, since a template may legitimately be imported onto a host that will be
provisioned afterwards.

Acceptance criteria

  • Importing a template whose adapter_type is unregistered does not silently
    create an agent
  • Importing one whose adapter cannot run here is surfaced in warnings or
    skipped rather than created silently
  • A test imports a template with a bad adapter_type and asserts the outcome at
    the API boundary, not at the helper
  • The existing hire-time checks and this path agree on what "runnable" means —
    one definition, not two

Found during review of #14797. Refs #12681.

Activity

  1. mrveiss commented on Aug 22, 2026

    @mrveiss
    OwnerAuthor

    Fixed — verified on base

    Merged via #14808 with 10/10 required checks and 12/12 python-suite shards.
    Verified against origin/Dev_new_gui:

    check result
    adapter_unavailable_reason in llc/adapters/base.py 1
    _import_agent gated 2 references
    a resolved or "claude_code" default remaining 0

    What it was

    _import_agent inserted into agent_org_nodes with an adapter_type from an
    imported template and asked neither of the questions the hire route asks. So the
    gate added in #14797 covered one of the two paths that create runnable agents.

    Skipped with a warning rather than failing: a template may legitimately be imported
    onto a host provisioned afterwards, and the response already carries warnings and
    skipped.

    The part the review caught, worth recording

    My first version resolved agent.get("adapter_type") or "claude_code" and validated
    that, while the INSERT bound the raw field. Wrong twice over:

    • the value checked was not the value persisted — an omitted type was checked as
      claude_code and written as NULL
    • claude_code was the wrong default anyway; the scheduler reads a missing
      adapter_type as autobot_agent, which runs in-process and is deliberately
      absent from the registry
    ordinary in-process agent (no adapter_type)  OLD=SKIP   NEW=import   <-- regression
    declared claude_code, CLI absent             OLD=SKIP   NEW=SKIP
    declared unknown adapter                     OLD=SKIP   NEW=SKIP
    

    A gate that breaks the ordinary case to catch the unusual one is a bad trade, and I
    made it by copying the hire route's default into a place where the runtime default
    differs. Only a declared type is checked now, and the gate reads the same
    expression the INSERT binds.

    The test asserting the omitted case is gated was asserting the regression. It now
    asserts the opposite, plus one that reads the persisted bind value rather than the
    source text.

    Not covered

    #14811 — a skipped manager leaves imported subordinates with a reports_to
    pointing at a never-inserted UUID, and the column has no FK. Pre-existing, but this
    change widens it: a CLI-less host can now skip an entire manager subtree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions