airbyte_ops_mcp.cli.devin
CLI commands for Devin session operations.
Commands:
airbyte-ops devin secret-request - Deliver a secret after Slack approval airbyte-ops devin secret-list - List available secrets in the vault
CLI reference
The commands below are regenerated by poe docs-generate via cyclopts's
programmatic docs API; see docs/generate_cli.py.
airbyte-ops devin COMMAND
Devin session operations.
Commands:
secret-list: List available secrets in the 1Password vault.secret-request: Deliver a Devin secret after Slack approval.
airbyte-ops devin secret-list
airbyte-ops devin secret-list
List available secrets in the 1Password vault.
Lists all item titles in the 'devin-on-demand-secrets' vault and outputs them as compact single-line JSON. The MCP tool reads this output from the workflow job logs to return the list to the caller.
NOTE: We intentionally use print(json.dumps(...)) instead of
print_json() (Rich) so the output stays on one log line, which
the MCP log parser can reliably extract.
airbyte-ops devin secret-request
airbyte-ops devin secret-request SESSION SECRET-ALIAS APPROVAL-EVIDENCE-URL [ARGS]
Deliver a Devin secret after Slack approval.
Validates the approval record, creates a 1Password share link, and sends it to the Devin session.
NOTE: The approval request (Phase 1) is handled entirely by the GitHub Actions workflow (bash + reusable HITL workflow), not by this CLI command.
Parameters:
SESSION, --session: Devin session ID or URL (e.g. 'abc123...' or 'https://appairbyte_ops_mcp.cli.devin.ai/sessions/abc123...'). [required]SECRET-ALIAS, --secret-alias: Secret name matching an item title in the 'devin-on-demand-secrets' 1Password vault. [required]APPROVAL-EVIDENCE-URL, --approval-evidence-url: Slack approval record URL. Validates the approval and delivers the secret to the Devin session. [required]EXPECTED-REQUEST-ID, --expected-request-id: Request ID from Phase 1. When provided, the approval record is validated against this ID for replay protection.
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""CLI commands for Devin session operations. 3 4Commands: 5 airbyte-ops devin secret-request - Deliver a secret after Slack approval 6 airbyte-ops devin secret-list - List available secrets in the vault 7 8## CLI reference 9 10The commands below are regenerated by `poe docs-generate` via cyclopts's 11programmatic docs API; see `docs/generate_cli.py`. 12 13.. include:: ../../../docs/generated/cli/devin.md 14 :start-line: 2 15""" 16 17from __future__ import annotations 18 19# Hide Python-level members from the pdoc page for this module; the rendered 20# docs for this CLI group come entirely from the grafted `.. include::` in 21# the module docstring above. 22__all__: list[str] = [] 23 24import json 25import logging 26import os 27import subprocess 28from typing import Annotated 29 30import requests 31from cyclopts import Parameter 32 33from airbyte_ops_mcp.cli._base import App, app 34from airbyte_ops_mcp.cli._shared import exit_with_error, print_json, print_success 35from airbyte_ops_mcp.devin_api import ( 36 extract_session_id, 37 send_session_message, 38) 39from airbyte_ops_mcp.onepassword import ( 40 DEFAULT_SHARE_EXPIRY, 41 create_share_link, 42 list_vault_items, 43) 44from airbyte_ops_mcp.slack_api import ( 45 SlackAPIError, 46 SlackApprovalRecordError, 47 SlackURLParseError, 48 validate_slack_approval_record, 49) 50 51logger = logging.getLogger(__name__) 52 53# --------------------------------------------------------------------------- 54# Sub-app registration 55# --------------------------------------------------------------------------- 56 57devin_app = App(name="devin", help="Devin session operations.") 58app.command(devin_app) 59 60 61# --------------------------------------------------------------------------- 62# CLI commands 63# --------------------------------------------------------------------------- 64 65 66@devin_app.command(name="secret-list") 67def secret_list() -> None: 68 """List available secrets in the 1Password vault. 69 70 Lists all item titles in the 'devin-on-demand-secrets' vault and 71 outputs them as compact single-line JSON. The MCP tool reads this 72 output from the workflow job logs to return the list to the caller. 73 74 NOTE: We intentionally use `print(json.dumps(...))` instead of 75 `print_json()` (Rich) so the output stays on one log line, which 76 the MCP log parser can reliably extract. 77 """ 78 titles = list_vault_items() 79 print(json.dumps({"available_secrets": titles, "available_count": len(titles)})) 80 81 82@devin_app.command(name="secret-request") 83def secret_request( 84 session: Annotated[ 85 str, 86 Parameter( 87 help="Devin session ID or URL " 88 "(e.g. 'abc123...' or 'https://app.devin.ai/sessions/abc123...').", 89 ), 90 ], 91 secret_alias: Annotated[ 92 str, 93 Parameter( 94 help="Secret name matching an item title in the " 95 "'devin-on-demand-secrets' 1Password vault.", 96 ), 97 ], 98 approval_evidence_url: Annotated[ 99 str, 100 Parameter( 101 help="Slack approval record URL. Validates the approval " 102 "and delivers the secret to the Devin session.", 103 ), 104 ], 105 expected_request_id: Annotated[ 106 str | None, 107 Parameter( 108 help="Request ID from Phase 1. When provided, the " 109 "approval record is validated against this ID for replay " 110 "protection.", 111 ), 112 ] = None, 113) -> None: 114 """Deliver a Devin secret after Slack approval. 115 116 Validates the approval record, creates a 1Password share link, 117 and sends it to the Devin session. 118 119 NOTE: The approval *request* (Phase 1) is handled entirely by the 120 GitHub Actions workflow (bash + reusable HITL workflow), not by 121 this CLI command. 122 """ 123 try: 124 session_id = extract_session_id(session) 125 except ValueError as exc: 126 exit_with_error(str(exc)) 127 return # unreachable, but keeps the type checker happy 128 129 _handle_secret_delivery( 130 session_id=session_id, 131 secret_alias=secret_alias, 132 approval_evidence_url=approval_evidence_url, 133 expected_request_id=expected_request_id, 134 ) 135 136 137# --------------------------------------------------------------------------- 138# Phase handlers 139# --------------------------------------------------------------------------- 140 141 142def _handle_secret_delivery( 143 session_id: str, 144 secret_alias: str, 145 approval_evidence_url: str, 146 *, 147 expected_request_id: str | None = None, 148) -> None: 149 """Phase 2: Validate approval, create share link, deliver to session.""" 150 # Step 1: Validate the Slack approval record 151 logger.info("Validating approval evidence: %s", approval_evidence_url) 152 try: 153 record = validate_slack_approval_record( 154 approval_evidence_url, 155 expected_secret_alias=secret_alias, 156 expected_request_id=expected_request_id, 157 ) 158 except (SlackURLParseError, SlackAPIError, SlackApprovalRecordError) as exc: 159 exit_with_error(str(exc)) 160 return # unreachable 161 162 approver = record.user_name or record.user_id 163 logger.info("Approval validated: approved by %s", approver) 164 165 # Step 2: Create 1Password share link 166 logger.info("Creating 1Password share link for '%s'...", secret_alias) 167 try: 168 share_link = create_share_link(secret_alias) 169 except (RuntimeError, subprocess.TimeoutExpired) as exc: 170 _handle_share_link_failure( 171 secret_alias=secret_alias, session_id=session_id, error=exc 172 ) 173 return # unreachable 174 175 # Mask the share link in GitHub Actions logs 176 if os.environ.get("GITHUB_ACTIONS"): 177 print(f"::add-mask::{share_link}") 178 179 logger.info("Share link created (expires in %s)", DEFAULT_SHARE_EXPIRY) 180 181 # Step 3: Send to Devin session 182 logger.info("Sending share link to session %s...", session_id[:8]) 183 message = ( 184 f"Your requested secret `{secret_alias}` is ready.\n\n" 185 f"Open this link in your browser to view and copy the secret values:\n" 186 f"{share_link}\n\n" 187 f"This link expires in {DEFAULT_SHARE_EXPIRY}. " 188 f"Retrieve and store the value immediately in your secure local storage." 189 ) 190 try: 191 send_session_message(session_id, message) 192 except (requests.RequestException, RuntimeError) as exc: 193 exit_with_error(f"Failed to send message to session: {exc}") 194 return # unreachable 195 196 print_success("Secret delivered successfully!") 197 print_json( 198 { 199 "phase": "delivered", 200 "secret_alias": secret_alias, 201 "session_id": session_id, 202 "approver": approver, 203 } 204 ) 205 206 207def _handle_share_link_failure( 208 secret_alias: str, 209 session_id: str, 210 error: Exception, 211) -> None: 212 """Handle a failed share-link creation by listing available secrets. 213 214 Attempts to list all available secret names in the vault so the agent 215 (and approver) can see valid options. This helps recover from typos 216 or incorrect secret names. 217 """ 218 logger.warning("Share link creation failed for '%s': %s", secret_alias, error) 219 220 available_hint = "" 221 try: 222 titles = list_vault_items() 223 if titles: 224 formatted = ", ".join(f"`{t}`" for t in titles) 225 available_hint = f" Available secrets: {formatted}" 226 except (RuntimeError, subprocess.TimeoutExpired, OSError): 227 logger.warning("Could not list vault items to suggest alternatives.") 228 229 error_message = ( 230 f"Failed to create share link for `{secret_alias}`: {error}{available_hint}" 231 ) 232 # Best-effort: notify the session so the agent can self-correct 233 try: 234 send_session_message(session_id, error_message) 235 except (requests.RequestException, RuntimeError): 236 logger.warning( 237 "Could not send failure notification to session %s", session_id[:8] 238 ) 239 240 exit_with_error(f"Failed to create share link for '{secret_alias}': {error}")