Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ export const chatCapabilitiesSchema = z.object({
scope: z.literal("owner_global"),
model: z.string(),
reasoning_effort: z.string(),
channel_binding: z.object({
schema_version: z.string(),
executor_endpoint: z.string(),
executor_endpoint_source: z.string(),
executor_transport_reason: z.string(),
model: z.string(),
model_source: z.string(),
credential_env_var: z.string(),
operator_credential_configured: z.boolean(),
}).optional(),
runtime: z.object({
schema_version: z.literal("manager_runtime_effective_profile_v0"),
runtime_profile: z.enum(["restricted", "trusted_owner"]),
Expand Down
24 changes: 21 additions & 3 deletions docs/reference/protocols/manager-evidence-and-continuity-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,28 @@ in a `manager.context` event, and supplies it to the executor. Chat prose is
never the inventory. Normal manager questions no longer silently use a limited
frontend projection; explicitly choosing status-only still uses that projection.

For Codex, manager defaults are `gpt-6-astra` with `high` reasoning. Set
Manager defaults follow the operator credential (`DEEPSEEK_API_KEY`, or the
endpoint in `DEEPSEEK_BASE_URL`): with a credential configured the steward
channel defaults to the operator model (`deepseek-flash`) so its model work does
not depend on an individual CLI login, and without one it keeps the vendor
default `gpt-6-astra`. Reasoning effort defaults to `high` either way. The
steward channel also needs a transport that can hold an interactive session:
the default executor endpoint resolves from the same credential, and when the
managed host has no chat transport the resolution reports
`dsh_chat_transport_unsupported` instead of silently downgrading. Set
`LOOPX_MANAGER_MODEL` and `LOOPX_MANAGER_REASONING_EFFORT` on the Chat service to
override them. Thread start, resume and turn start explicitly carry the settings;
worker configuration is unchanged. Capabilities expose the manager defaults.
override the defaults; an explicit override always wins. Thread start, resume
and turn start explicitly carry the settings; worker configuration is unchanged.
Capabilities expose the manager defaults and their source. A session request that
names a managed host without a Chat transport fails as the typed
`managed_host_chat_transport_unsupported` host-tool gate instead of an unknown
endpoint error, in both the Chat service and Lark routing.

The Chat capabilities payload carries the same binding in its `manager` block
(`channel_binding`): resolved executor endpoint, endpoint source, transport
reason, resolved model, model source, and whether an operator credential is
configured. It reports the credential variable name, never its value, so a
frontend can show which executor and model the steward channel resolved and why.
Legacy managed manager sessions retain their logical identity and bounded chat
history but start a fresh executor thread in the same Codex home on first
restore. This removes inherited project instructions without importing sessions
Expand Down
180 changes: 180 additions & 0 deletions examples/loopx-steward-channel-binding-smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Prove the steward channel binds its executor and model to the operator credential."""

from __future__ import annotations

import json
import os
import sys
import tempfile
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))

from loopx.chat_manager import ( # noqa: E402
MANAGER_ENDPOINT_TRANSPORT_UNSUPPORTED,
MANAGER_MODEL_SOURCE_ENV_OVERRIDE,
MANAGER_MODEL_SOURCE_OPERATOR_CREDENTIAL,
MANAGER_MODEL_SOURCE_VENDOR_DEFAULT,
manager_channel_binding,
manager_model_config,
open_manager_session,
)
from loopx.chat_agent import ( # noqa: E402
MANAGED_HOST_CHAT_TRANSPORT_UNSUPPORTED,
CodexChatAgentError,
)
from loopx.chat_runtime import ChatRuntimeController # noqa: E402
from loopx.chat_store import ChatSessionStore # noqa: E402


CREDENTIAL_ENV = "DEEPSEEK_API_KEY"
CREDENTIAL_VALUE = "fixture-operator-credential"


def _assert(condition: bool, message: str) -> None:
if not condition:
raise SystemExit(f"steward channel binding smoke failed: {message}")


def main() -> int:
without_credential = manager_channel_binding({})
_assert(
without_credential["executor_endpoint"] == "codex",
"no credential must keep the shipped chat endpoint",
)
_assert(
without_credential["model"] == "gpt-6-astra"
and without_credential["model_source"] == MANAGER_MODEL_SOURCE_VENDOR_DEFAULT,
"no credential must keep the vendor model default",
)

with_credential = manager_channel_binding({CREDENTIAL_ENV: CREDENTIAL_VALUE})
_assert(
with_credential["model"] == "deepseek-flash"
and with_credential["model_source"] == MANAGER_MODEL_SOURCE_OPERATOR_CREDENTIAL,
"a configured credential must bind the steward model to the operator provider",
)
_assert(
with_credential["credential_env_var"] == CREDENTIAL_ENV,
"the binding must report the credential variable name",
)
_assert(
CREDENTIAL_VALUE not in json.dumps(with_credential),
"the binding must never echo a credential value",
)
_assert(
with_credential["executor_endpoint_source"] == "operator_credential"
and with_credential["executor_transport_reason"]
== MANAGER_ENDPOINT_TRANSPORT_UNSUPPORTED,
"the chat transport limit must be reported as a typed reason",
)
_assert(
manager_model_config(
{CREDENTIAL_ENV: CREDENTIAL_VALUE, "LOOPX_MANAGER_MODEL": "fixture-model"}
)["model"]
== "fixture-model"
and manager_channel_binding(
{CREDENTIAL_ENV: CREDENTIAL_VALUE, "LOOPX_MANAGER_MODEL": "fixture-model"}
)["model_source"]
== MANAGER_MODEL_SOURCE_ENV_OVERRIDE,
"an explicit model override must win over the credential default",
)

opened: list[dict[str, object]] = []

class _Controller:
def open_session(self, **kwargs):
opened.append(kwargs)
return {"session_id": "fixture-session"}, False

controller = _Controller()
with tempfile.TemporaryDirectory() as work_dir:
ambient = os.environ.pop(CREDENTIAL_ENV, None)
try:
open_manager_session(
controller=controller,
goal_id="loopx-steward-binding-fixture",
work_dir=Path(work_dir),
)
finally:
if ambient is not None:
os.environ[CREDENTIAL_ENV] = ambient
_assert(
opened[-1]["agent_id"] == without_credential["executor_endpoint"],
"without a configured credential the manager session must open the shipped endpoint",
)

os.environ[CREDENTIAL_ENV] = CREDENTIAL_VALUE
try:
open_manager_session(
controller=controller,
goal_id="loopx-steward-binding-fixture",
work_dir=Path(work_dir),
)
finally:
os.environ.pop(CREDENTIAL_ENV, None)
_assert(
opened[-1]["agent_id"] == with_credential["executor_endpoint"],
"with a configured credential the manager session must open the resolved endpoint",
)
_assert(
opened[-1]["agent_id"] != "dsh"
or with_credential["executor_transport_reason"] == "",
"the manager session must not open an endpoint without a chat transport",
)

print(
json.dumps(
{
"ok": True,
"without_credential": without_credential,
"with_credential": with_credential,
"opened_endpoint": opened[-1]["agent_id"],
},
ensure_ascii=False,
indent=2,
)
)
return 0


def _assert_typed_transport_gate(root: Path) -> None:
"""A managed host without a chat transport must fail as a typed gate."""

runtime = ChatRuntimeController(
store=ChatSessionStore(root / "store"), codex_bin="fixture-codex"
)
try:
try:
runtime.open_session(
goal_id="loopx-steward-binding-fixture",
agent_id="dsh",
work_dir=root,
objective="fixture",
mode="new",
)
except CodexChatAgentError as exc:
_assert(
exc.error_code == MANAGED_HOST_CHAT_TRANSPORT_UNSUPPORTED,
"the managed host without a chat transport must report its own error code",
)
_assert(
exc.gate.get("kind") == "host_tool_gate",
"the managed host gate must stay a host tool gate",
)
else:
raise SystemExit(
"steward channel binding smoke failed: dsh opened an interactive session"
)
finally:
runtime.close()


if __name__ == "__main__":
exit_code = main()
with tempfile.TemporaryDirectory() as gate_root:
_assert_typed_transport_gate(Path(gate_root))
raise SystemExit(exit_code)
23 changes: 21 additions & 2 deletions loopx/capabilities/manager_runtime/machine_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,32 @@ def manager_runtime_session_fields(profile: Mapping[str, Any]) -> dict[str, Any]
def manager_runtime_capability_projection(
runtime_controller: object,
model_configuration: Mapping[str, Any],
*,
channel_binding: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the manager section of the shared chat capabilities projection."""
"""Build the manager section of the shared chat capabilities projection.

``channel_binding`` is the resolved steward-channel executor and model
binding. The caller owns it; this projection only carries it into readback so
a frontend can show which executor and model the manager channel resolved
and why, without re-deriving the rule.
"""

resolver = getattr(runtime_controller, "manager_runtime_profile", None)
runtime = (
resolver()
if callable(resolver)
else {**effective_manager_runtime_profile(None), "status": "ready"}
)
return {"scope": "owner_global", **dict(model_configuration), "runtime": runtime}
projection: dict[str, Any] = {
"scope": "owner_global",
**dict(model_configuration),
"runtime": runtime,
}
if channel_binding is not None:
binding = dict(channel_binding)
binding["operator_credential_configured"] = bool(
str(binding.get("credential_env_var") or "").strip()
)
projection["channel_binding"] = binding
return projection
29 changes: 29 additions & 0 deletions loopx/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,35 @@ def _host_tool_gate(summary: str, next_action: str) -> dict[str, str]:
}


# The managed Turn host runs one bounded work segment per request and has no
# interactive Chat transport, so a session request for it is a known outcome
# rather than an unknown endpoint.
MANAGED_TURN_HOST_IDS = frozenset({"dsh"})
MANAGED_HOST_CHAT_TRANSPORT_UNSUPPORTED = "managed_host_chat_transport_unsupported"


def agent_endpoint_error(agent_id: str) -> ValueError:
"""Return the typed error for an Agent id this runtime cannot hold.

A managed Turn host keeps a typed host-tool gate and an actionable next
step, so the steward channel never half-connects to a host it cannot hold.
Every other unknown id keeps the existing untyped fallback.
"""

if agent_id in MANAGED_TURN_HOST_IDS:
return CodexChatAgentError(
f"The managed host '{agent_id}' runs bounded LoopX Turns and cannot "
"hold an interactive Chat session yet.",
error_code=MANAGED_HOST_CHAT_TRANSPORT_UNSUPPORTED,
gate=_host_tool_gate(
f"'{agent_id}' has no LoopX Chat transport; it is a bounded Turn host.",
"Select a chat-capable Agent endpoint for this session, or run "
"the managed host through `loopx turn`.",
),
)
return ValueError(f"unknown Agent endpoint: {agent_id}")


def _approval_gate(summary: str) -> dict[str, str]:
return {
"kind": "approval_gate",
Expand Down
15 changes: 13 additions & 2 deletions loopx/chat_lark_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
CommandRunner,
default_subprocess_runner,
)
from .chat_manager import manager_channel, open_manager_session
from .chat_agent import CodexChatAgentError
from .chat_manager import (
manager_channel,
manager_executor_endpoint_default,
open_manager_session,
)
from .extensions.lark.goal_channel_contracts import binding_for_goal, goal_from_registry
from .extensions.lark.goal_channel_targets import goal_channel_target_for_name
from .history import load_registry
Expand Down Expand Up @@ -480,8 +485,9 @@ def _lark_connect(self) -> None:
executor_endpoint_id = (
_compact_text(body.get("executor_endpoint_id"), limit=100)
or stored_routing.get("executor_endpoint_id")
or "codex"
)
if conversation_kind == "manager" and not executor_endpoint_id:
executor_endpoint_id = manager_executor_endpoint_default()
session_id: str | None = None
session_ids_by_agent: dict[str, str] = {}
if conversation_kind == "manager":
Expand Down Expand Up @@ -591,6 +597,11 @@ def _lark_connect(self) -> None:
else None,
session_id=session_id,
)
except CodexChatAgentError as exc:
self._send_error(
str(exc), status=400, gate=exc.gate, error_code=exc.error_code
)
return
except ValueError as exc:
self._send_error(str(exc), status=400, error_code="invalid_lark_connection")
return
Expand Down
Loading