-
Notifications
You must be signed in to change notification settings - Fork 62
fix(copilot): add permission handler, event logging, and idle detection race fix #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -411,6 +411,9 @@ async def _execute_sdk_call( | |
| # Build session config with MCP servers from workflow configuration | ||
| session_config: dict[str, Any] = { | ||
| "model": model, | ||
| # Auto-approve all permission requests (shell, write, mcp, read, url) | ||
| # since Conductor workflows run non-interactively. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 No audit logging on permission approvals. Since this handler silently auto-approves dangerous categories (shell, write, mcp), consider adding a logger.debug("auto-approved permission request: %s", request)
return {"kind": "approved"} |
||
| "on_permission_request": lambda _req, _ctx: {"kind": "approved"}, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Lint failure (CI blocker): This inline lambda is causing the ruff format check to fail in CI. Extract it to a named static method with proper type hints and a docstring. Suggestion: @staticmethod
def _default_permission_handler(
request: dict[str, Any],
invocation: dict[str, str],
) -> dict[str, Any]:
"""Default permission handler that approves all requests.
SDK v0.1.28+ requires a permission handler on session creation.
In orchestration mode, we approve all tool permissions since the
workflow author controls which tools are available to each agent.
"""
return {"kind": "approved"}Then reference it here as |
||
| } | ||
|
|
||
| # Add temperature if configured | ||
|
|
@@ -621,6 +624,16 @@ def on_event(event: Any) -> None: | |
| nonlocal response_content, error_message | ||
| event_type = event.type.value if hasattr(event.type, "value") else str(event.type) | ||
|
|
||
| # Log every SDK event for debugging stalls (visible via --log-file) | ||
| if logger.isEnabledFor(logging.DEBUG): | ||
| tool_info = "" | ||
| if event_type == "tool.execution_start": | ||
| tn = getattr(event.data, "tool_name", None) or getattr( | ||
| event.data, "name", "?" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Minor: The if event_type == "tool.execution_start" and hasattr(event, "data") and event.data is not None:Not a regression (existing code has the same pattern), but since this is new code it's worth hardening. |
||
| ) | ||
| tool_info = f" tool={tn}" | ||
| logger.debug("sdk_event: %s%s", event_type, tool_info) | ||
|
|
||
| # Update last activity on EVERY event (this is key for idle detection!) | ||
| last_activity_ref[0] = event_type | ||
| last_activity_ref[2] = time.monotonic() | ||
|
|
@@ -1267,6 +1280,11 @@ async def _wait_with_idle_detection( | |
| idle_timeout = self._idle_recovery_config.idle_timeout_seconds | ||
|
|
||
| while True: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Good fix. This early-return guard closes a real race window where |
||
| # Check if done was already set (avoids race where session.idle | ||
| # arrived between a previous done.clear() and the next wait). | ||
| if done.is_set(): | ||
| return | ||
|
|
||
| try: | ||
| # Wait for done with idle timeout | ||
| await asyncio.wait_for( | ||
|
|
@@ -1288,7 +1306,11 @@ async def _wait_with_idle_detection( | |
| # just hasn't finished yet. Reset recovery counter (new task) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Good fix. Without this guard, if |
||
| # and keep waiting. | ||
| recovery_attempts = 0 | ||
| done.clear() | ||
| # Only clear if done hasn't been set in the meantime | ||
| # (prevents race where session.idle arrives right as we | ||
| # check time_since_last_event). | ||
| if not done.is_set(): | ||
| done.clear() | ||
| continue | ||
|
|
||
| # Genuinely idle — no events for the full timeout period | ||
|
|
@@ -1321,9 +1343,10 @@ async def _wait_with_idle_detection( | |
| recovery_prompt = self._build_recovery_prompt(last_event_type, last_tool_call) | ||
| await session.send({"prompt": recovery_prompt}) | ||
|
|
||
| # Reset the done event to wait again | ||
| # (it may have been set by a previous partial response) | ||
| done.clear() | ||
| # Reset the done event to wait again — but only if it hasn't | ||
| # been set since the recovery prompt was sent. | ||
| if not done.is_set(): | ||
| done.clear() | ||
|
|
||
| async def _ensure_client_started(self) -> None: | ||
| """Ensure the Copilot client is started.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Missing
resume_sessionhandler: SDK v0.1.28+ also requireson_permission_requestwhen callingresume_session(). Without it, resuming workflows will still raise the permission denied error.Around line 429 where
resume_sessionis called, pass a config dict: