Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 91 additions & 6 deletions src/xagent/web/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
parse_user_sandbox_key,
)
from ..schemas.chat import TaskCreateRequest, TaskCreateResponse
from ..schemas.connector_runtime import ConnectorRuntimeRequirementsModel
from ..services.agent_access import list_accessible_published_agents
from ..services.agent_team_scope import (
get_agent_team_scope,
Expand All @@ -89,7 +90,8 @@
)
from ..services.connector_runtime import (
bind_connector_runtime_selection_snapshot,
prepare_connector_runtime_selection_snapshot,
build_task_runtime_requirements,
resolve_agent_runtime_requirements,
)
from ..services.db_runtime import (
drain_async_task_cancellation_safe,
Expand Down Expand Up @@ -4488,10 +4490,12 @@ def _get_default_internal_model_ids() -> Dict[str, Optional[str]]:
agent_id=request.agent_id, # Set agent_id if provided
is_visible=False if request.is_preview else request.is_visible,
)
selected_refs = prepare_connector_runtime_selection_snapshot(
db=db,
agent=selected_agent,
connector_user_id=int(user.id),
selected_refs, connector_runtime_requirements = (
resolve_agent_runtime_requirements(
db=db,
agent=selected_agent,
connector_user_id=int(user.id),
)
)
bind_connector_runtime_selection_snapshot(
task=task, selected_refs=selected_refs
Expand Down Expand Up @@ -4668,6 +4672,7 @@ def _get_default_internal_model_ids() -> Dict[str, Optional[str]]:
runtime_extensions=runtime_extensions,
runtime_extensions_status=runtime_extensions_status,
runtime_extensions_omitted=runtime_extensions_omitted,
connector_runtime_requirements=connector_runtime_requirements,
)

except HTTPException:
Expand All @@ -4694,7 +4699,7 @@ def _get_default_internal_model_ids() -> Dict[str, Optional[str]]:
#
# The asymmetry with the ConnectorRuntimeError arm above is real
# and deliberate: that arm has a live producer inside this endpoint
# (``prepare_connector_runtime_selection_snapshot``), this one has
# (``resolve_agent_runtime_requirements``), this one has
# none. It is kept because what the two arms share is the
# failure-path contract, not the producer: both errors carry their
# own status and a caller-safe message, and the blanket handler
Expand Down Expand Up @@ -5288,6 +5293,86 @@ async def get_task_runtime_extensions(
}


@chat_router.get(
"/agent/{agent_id}/connector-runtime-requirements",
response_model=ConnectorRuntimeRequirementsModel,
)
async def get_agent_connector_runtime_requirements(
agent_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> ConnectorRuntimeRequirementsModel:
"""Report the runtime inputs a prospective task would need, before one
exists.

Reuses the same predicate ``POST /task/create`` applies to an agent id
(``_load_agent_for_task_create``) rather than a second authorization
path for the same resource, so a caller who could create a task with
this agent sees exactly the same "not found" boundary here that they
would hit on that call. Lives on the chat router, next to the
task-keyed sibling below and the existing task-keyed
``/task/{task_id}/runtime-extensions``, rather than under
``/api/agents`` -- this endpoint has no consumer outside chat.

There is no task yet, so every reported input is unsatisfied and the
connector team scope is whatever ``resolve_agent_selected_connectors``
derives from the agent's own team, never a value this endpoint passes
in itself.
"""

agent = _load_agent_for_task_create(db, user, agent_id)
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found or access denied")
try:
_refs, requirements = resolve_agent_runtime_requirements(
db=db, agent=agent, connector_user_id=int(user.id)
)
except ConnectorRuntimeError as exc:
raise HTTPException(
status_code=exc.status_code, detail=exc.safe_message
) from exc
return requirements


@chat_router.get(
"/task/{task_id}/connector-runtime-requirements",
response_model=ConnectorRuntimeRequirementsModel,
)
async def get_task_connector_runtime_requirements(
task_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> ConnectorRuntimeRequirementsModel:
"""Report which of a task's declared connector runtime inputs already
have a value.

Access is plain task ownership -- ``Task.user_id == current_user.id`` in
the same query that loads the task, unlike
``/task/{task_id}/runtime-extensions`` above, which additionally lets an
admin read any task; this endpoint does not extend that exception.
A task that does not exist or is not the caller's own is a uniform 404.

Pure read: never writes, and never asserts that a required value is
present -- that assertion belongs to the per-turn gate that runs later,
not to this report.
"""

task = db.query(Task).filter(Task.id == task_id, Task.user_id == user.id).first()
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
agent = (
db.query(Agent).filter(Agent.id == task.agent_id).first()
Comment thread
AlexLiu190625 marked this conversation as resolved.
Outdated
if task.agent_id is not None
else None
)
try:
return build_task_runtime_requirements(db=db, task=task, agent=agent)
except ConnectorRuntimeError as exc:
raise HTTPException(
status_code=exc.status_code, detail=exc.safe_message
) from exc


@chat_router.delete("/task/{task_id}")
async def delete_task(
task_id: int,
Expand Down
16 changes: 16 additions & 0 deletions src/xagent/web/api/public_chat_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,10 @@ async def _create_workforce_widget_chat_task(
else None,
channel_id=task.channel_id,
channel_name=task.channel_name,
# This path never resolves connector-runtime requirements for
Comment thread
AlexLiu190625 marked this conversation as resolved.
Outdated
# its caller: the widget/share guest never sees connector key
# names.
connector_runtime_requirements=None,
)


Expand Down Expand Up @@ -1140,6 +1144,10 @@ async def create_public_chat_task(
else None,
channel_id=task.channel_id,
channel_name=task.channel_name,
# This path never resolves connector-runtime requirements for
# its caller: the widget/share guest never sees connector key
# names.
connector_runtime_requirements=None,
)


Expand Down Expand Up @@ -1209,6 +1217,10 @@ async def _create_workforce_share_chat_task(
else None,
channel_id=task.channel_id,
channel_name=task.channel_name,
# This path never resolves connector-runtime requirements for
# its caller: the widget/share guest never sees connector key
# names.
connector_runtime_requirements=None,
)


Expand Down Expand Up @@ -1297,6 +1309,10 @@ async def create_share_chat_task(
else None,
channel_id=task.channel_id,
channel_name=task.channel_name,
# This path never resolves connector-runtime requirements for
# its caller: the widget/share guest never sees connector key
# names.
connector_runtime_requirements=None,
)


Expand Down
15 changes: 15 additions & 0 deletions src/xagent/web/schemas/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic import BaseModel, Field, model_validator

from ...core.task_runtime import MAX_TASK_RUNTIME_EXTENSIONS
from .connector_runtime import ConnectorRuntimeRequirementsModel

# Only ever read here (TaskCreateRequest.seed_interactions is passed straight
# through to create_task_with_message as-is - see api/chat.py), unlike
Expand Down Expand Up @@ -194,6 +195,20 @@ class TaskCreateResponse(BaseModel):
"the status is `truncated`. Empty otherwise."
),
)
connector_runtime_requirements: ConnectorRuntimeRequirementsModel | None = Field(
...,
description=(
"Which runtime inputs this task's connectors still need, and "
"which of them already have a value. Always present in the "
"response body -- a client must not treat its absence as "
"meaning anything. Never includes a stored value itself, only "
"whether one exists. Present with a report on the web chat "
"create path (`POST /api/chat/task/create`); `null` on the "
"public chat and share-link create paths, meaning the "
"requirements were not evaluated there -- those visitors never "
"receive connector key names."
),
)


class ExecutionStatus(BaseModel):
Expand Down
72 changes: 72 additions & 0 deletions src/xagent/web/schemas/connector_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Connector runtime requirements: the shared response shape both read
endpoints return.

The agent-keyed and task-keyed read endpoints both return this same
requirements report -- which runtime inputs a task's (or a prospective
task's) connectors declare, and whether each one already has a value --
never a stored value itself, and never a connector's transport or
authentication configuration.

Placed in its own module rather than ``schemas/chat.py`` because a values-
submission endpoint lands on top of it shortly and will share this same
response shape as its own 200 body; putting it here now avoids a later
move that would touch every existing importer.
"""

from __future__ import annotations

from pydantic import BaseModel


class ConnectorRuntimeRefModel(BaseModel):
"""Wire identity of a connector, as returned in a requirements report."""

connector_type: str
connector_id: int


class ConnectorRuntimeInputModel(BaseModel):
"""One declared runtime input and whether it is currently satisfied.

``key`` is the raw key name a connector owner wrote when declaring the
input -- there is no human-readable label anywhere in the declaration.
``type`` is already normalized server-side to ``"string"`` or
``"object"``; a client must not normalize it again or expect any other
value. ``satisfied`` and ``expired`` are constants in this phase for the
``secrets``/``auth_selector`` sections: no secret store exists yet, so
both are always ``False``.
"""

section: str
key: str
type: str
required: bool
satisfied: bool
expired: bool = False


class ConnectorRuntimeConnectorModel(BaseModel):
"""One connector's declared runtime inputs.

``name`` is the only piece of connector identity beyond the ref that is
ever included -- never the connector's URL, headers, environment, or
authentication configuration.
"""

connector_ref: ConnectorRuntimeRefModel
name: str
inputs: list[ConnectorRuntimeInputModel]


class ConnectorRuntimeRequirementsModel(BaseModel):
"""A requirements report. Every field always appears.

``connectors`` is empty, never omitted, when nothing is selected or
declares a runtime input. ``secrets_expires_at`` is a constant ``null``
in this phase; a later phase gives it a real value without changing
its meaning or making it optional.
"""

satisfied: bool
secrets_expires_at: str | None
connectors: list[ConnectorRuntimeConnectorModel]
Loading