tested 1pass backend and vars (#2690)

Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
Prakash Maheshwaran 2025-06-12 04:20:27 -04:00 committed by GitHub
parent b5bf9d291f
commit 9868750de3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 590 additions and 25 deletions

View file

@ -72,8 +72,12 @@ DATABASE_STRING="postgresql+psycopg://skyvern@localhost/skyvern"
PORT=8000
# Analytics configuration:
# Distinct analytics ID (a UUID is generated if left blank).
# ANALYTICS_ID: Distinct analytics ID (a UUID is generated if left blank).
ANALYTICS_ID="anonymous"
# 1Password Integration
# OP_SERVICE_ACCOUNT_TOKEN: API token for 1Password integration
OP_SERVICE_ACCOUNT_TOKEN=""
# Enable recording skyvern logs as artifacts
ENABLE_LOG_ARTIFACTS=false
ENABLE_LOG_ARTIFACTS=false

View file

@ -0,0 +1,62 @@
"""db script for 1password integration
Revision ID: 1517a4ba63fa
Revises: add_run_timestamps
Create Date: 2025-06-12 08:06:13.439802+00:00
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "1517a4ba63fa"
down_revision: Union[str, None] = "add_run_timestamps"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"onepassword_credential_parameters",
sa.Column("onepassword_credential_parameter_id", sa.String(), nullable=False),
sa.Column("workflow_id", sa.String(), nullable=False),
sa.Column("key", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=True),
sa.Column("vault_id", sa.String(), nullable=False),
sa.Column("item_id", sa.String(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("modified_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("onepassword_credential_parameter_id"),
)
op.create_index(
op.f("ix_onepassword_credential_parameters_onepassword_credential_parameter_id"),
"onepassword_credential_parameters",
["onepassword_credential_parameter_id"],
unique=False,
)
op.create_index(
op.f("ix_onepassword_credential_parameters_workflow_id"),
"onepassword_credential_parameters",
["workflow_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_onepassword_credential_parameters_workflow_id"), table_name="onepassword_credential_parameters"
)
op.drop_index(
op.f("ix_onepassword_credential_parameters_onepassword_credential_parameter_id"),
table_name="onepassword_credential_parameters",
)
op.drop_table("onepassword_credential_parameters")
# ### end Alembic commands ###

View file

@ -118,6 +118,10 @@ services:
# - BITWARDEN_CLIENT_ID=FILL_ME_IN_PLEASE
# - BITWARDEN_CLIENT_SECRET=FILL_ME_IN_PLEASE
# - BITWARDEN_MASTER_PASSWORD=FILL_ME_IN_PLEASE
# 1Password Integration
# If you are looking to integrate Skyvern with 1Password, you can use the following environment variables.
# OP_SERVICE_ACCOUNT_TOKEN=""
depends_on:
postgres:
condition: service_healthy

43
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "about-time"
@ -4496,6 +4496,45 @@ files = [
{file = "numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291"},
]
[[package]]
name = "onepassword-sdk"
version = "0.3.0"
description = "The 1Password Python SDK offers programmatic read access to your secrets in 1Password in an interface native to Python."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "onepassword_sdk-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cde57e9c2b07b420a8d4844f2260d0353defd8e008a690700a9c8ae1a8d53881"},
{file = "onepassword_sdk-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75cf002de86a0401838b635622d1a9f34c99f8c3eda86c7f35bb4ccfde534290"},
{file = "onepassword_sdk-0.3.0-cp310-cp310-manylinux_2_32_aarch64.whl", hash = "sha256:607d6147294037f8a790120850d54880892f69c22b215a8240b83ee92776676f"},
{file = "onepassword_sdk-0.3.0-cp310-cp310-manylinux_2_32_x86_64.whl", hash = "sha256:6d907d584cf8e8db04d4047566cc7378faa99264722d18b982636f3db5ffd54a"},
{file = "onepassword_sdk-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:deceb924e1dd9f9c05046a6e48d7bb99653d07284dc2b50932da06ece9f412ff"},
{file = "onepassword_sdk-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6510af9498efc76f1df18eda9cdc78a65a3143350dc59679fe3cf8bf8ad8982f"},
{file = "onepassword_sdk-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07482e5bd416eb0eaa87faf15307fe1529db46247a532ace45f373d3d220b7c2"},
{file = "onepassword_sdk-0.3.0-cp311-cp311-manylinux_2_32_aarch64.whl", hash = "sha256:cac7e9229e47e0f03e9c30ca65616daf8818acd3aec4de8933a4a819bdbd9e0d"},
{file = "onepassword_sdk-0.3.0-cp311-cp311-manylinux_2_32_x86_64.whl", hash = "sha256:d2f626f7b9bd8101d292e1faa017f94f48fe46d78895db50a5f8e77ca5e66633"},
{file = "onepassword_sdk-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:2dec1f199e6db5b671411f2f0f7eed0b308ec922ce0985e061ea8433a5203d94"},
{file = "onepassword_sdk-0.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f5a023212483f0936f9a2fff687221c8b2bdeb467fadba89b0f198de51956f21"},
{file = "onepassword_sdk-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ef97d00310e277e974be18f57b8a76661789a20afe5392138172a9ae827c3a1"},
{file = "onepassword_sdk-0.3.0-cp312-cp312-manylinux_2_32_aarch64.whl", hash = "sha256:02bb56039392c78990f868e7f116be17f119d7c81004cbd7808669989a02f3f6"},
{file = "onepassword_sdk-0.3.0-cp312-cp312-manylinux_2_32_x86_64.whl", hash = "sha256:21e748c7d8bdc8216dc19f0fb35dcee3148ddbcbcef45e86bcefc50cecc98ca0"},
{file = "onepassword_sdk-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c212e4ddebdd18845bf0974950cf5072f3174083b6b498756fc8713e82cd1a"},
{file = "onepassword_sdk-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1105e3f5c33a3086d038df910cb65a230d482b98fcb3f71cef155d1832c8f19"},
{file = "onepassword_sdk-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:18b3a1f49eabf30e7b6de4389e3181d41802bb37ce2182c9162a0591c3705d77"},
{file = "onepassword_sdk-0.3.0-cp313-cp313-manylinux_2_32_aarch64.whl", hash = "sha256:85c3a66fbe086d22ee6126dde3505859100ceb58129e2aca8a7d4b38aca7e03d"},
{file = "onepassword_sdk-0.3.0-cp313-cp313-manylinux_2_32_x86_64.whl", hash = "sha256:d45a64c6142250cfcb42a3342b339de50f3c51882a3a46c9fafbb3ade1fccadc"},
{file = "onepassword_sdk-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:6d892d1e6ca891f34328864fb1b5cfe2c28d1b9db95001a3c6468eb09befafc2"},
{file = "onepassword_sdk-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1758497e111cca1c69a6eec52cc74971fb12a95744ca426dfed8a143f9a19a30"},
{file = "onepassword_sdk-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:44c8bd2764d650ea0d4bbf05d0ff9f67b27fb8378f82e14569792ab0c8981902"},
{file = "onepassword_sdk-0.3.0-cp39-cp39-manylinux_2_32_aarch64.whl", hash = "sha256:2510c3d569d0d087677507d934d77b740eb8150fa0a81c434fcd02964ff2f7c0"},
{file = "onepassword_sdk-0.3.0-cp39-cp39-manylinux_2_32_x86_64.whl", hash = "sha256:eef5676c615c5fe987d26e29e7d60e08c3362ee3a4ec8ef2994f3275c6f3ed64"},
{file = "onepassword_sdk-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:83bb1c3e80ad1c6d216247dc51f3d6ae1afdef33bb31671267237ee6fbc2c297"},
{file = "onepassword_sdk-0.3.0.tar.gz", hash = "sha256:f6e2223cf67cdd07e15f06b61818386d8dcd8a1b54d20e8bf08ed48306479865"},
]
[package.dependencies]
pydantic = ">=2.5"
[[package]]
name = "onnxruntime"
version = "1.16.3"
@ -8720,4 +8759,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.14"
content-hash = "21275cab7e1d76046abc646983184af8c9d02afae1c7950eaf1c2592b2cca129"
content-hash = "42bde1cb27d171911f966360781659a19ef5df896a220161c7d780c05a6a4758"

View file

@ -73,6 +73,7 @@ anthropic = "^0.50.0"
google-cloud-aiplatform = "^1.90.0"
alive-progress = "^3.2.0"
colorama = "^0.4.6"
onepassword-sdk = "0.3.0"
types-boto3 = {extras = ["full"], version = "^1.38.31"}
[tool.poetry.group.dev.dependencies]

View file

@ -242,6 +242,7 @@ class Settings(BaseSettings):
BITWARDEN_CLIENT_ID: str | None = None
BITWARDEN_CLIENT_SECRET: str | None = None
BITWARDEN_MASTER_PASSWORD: str | None = None
OP_SERVICE_ACCOUNT_TOKEN: str | None = None
# Skyvern Auth Bitwarden Settings
SKYVERN_AUTH_BITWARDEN_CLIENT_ID: str | None = None

View file

@ -22,6 +22,7 @@ from skyvern.forge.sdk.db.models import (
BitwardenSensitiveInformationParameterModel,
CredentialModel,
CredentialParameterModel,
OnePasswordCredentialParameterModel,
OrganizationAuthTokenModel,
OrganizationBitwardenCollectionModel,
OrganizationModel,
@ -80,6 +81,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
BitwardenLoginCredentialParameter,
BitwardenSensitiveInformationParameter,
CredentialParameter,
OnePasswordCredentialParameter,
OutputParameter,
WorkflowParameter,
WorkflowParameterType,
@ -1904,6 +1906,32 @@ class AgentDB:
deleted_at=credential_parameter.deleted_at,
)
async def create_onepassword_credential_parameter(
self, workflow_id: str, key: str, vault_id: str, item_id: str, description: str | None = None
) -> OnePasswordCredentialParameter:
async with self.Session() as session:
parameter = OnePasswordCredentialParameterModel(
workflow_id=workflow_id,
key=key,
description=description,
vault_id=vault_id,
item_id=item_id,
)
session.add(parameter)
await session.commit()
await session.refresh(parameter)
return OnePasswordCredentialParameter(
onepassword_credential_parameter_id=parameter.onepassword_credential_parameter_id,
workflow_id=parameter.workflow_id,
key=parameter.key,
description=parameter.description,
vault_id=parameter.vault_id,
item_id=parameter.item_id,
created_at=parameter.created_at,
modified_at=parameter.modified_at,
deleted_at=parameter.deleted_at,
)
async def get_workflow_run_output_parameters(self, workflow_run_id: str) -> list[WorkflowRunOutputParameter]:
try:
async with self.Session() as session:

View file

@ -34,6 +34,7 @@ AWS_SECRET_PARAMETER_PREFIX = "asp"
BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX = "bccd"
BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc"
BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi"
CREDENTIAL_ONEPASSWORD_PARAMETER_PREFIX = "opp"
CREDENTIAL_PARAMETER_PREFIX = "cp"
CREDENTIAL_PREFIX = "cred"
ORGANIZATION_BITWARDEN_COLLECTION_PREFIX = "obc"
@ -106,6 +107,11 @@ def generate_bitwarden_credit_card_data_parameter_id() -> str:
return f"{BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX}_{int_id}"
def generate_onepassword_credential_parameter_id() -> str:
int_id = generate_id()
return f"{CREDENTIAL_ONEPASSWORD_PARAMETER_PREFIX}_{int_id}"
def generate_organization_auth_token_id() -> str:
int_id = generate_id()
return f"{ORGANIZATION_AUTH_TOKEN_PREFIX}_{int_id}"

View file

@ -29,6 +29,7 @@ from skyvern.forge.sdk.db.id import (
generate_bitwarden_sensitive_information_parameter_id,
generate_credential_id,
generate_credential_parameter_id,
generate_onepassword_credential_parameter_id,
generate_org_id,
generate_organization_auth_token_id,
generate_organization_bitwarden_collection_id,
@ -416,6 +417,28 @@ class CredentialParameterModel(Base):
deleted_at = Column(DateTime, nullable=True)
class OnePasswordCredentialParameterModel(Base):
__tablename__ = "onepassword_credential_parameters"
onepassword_credential_parameter_id = Column(
String, primary_key=True, index=True, default=generate_onepassword_credential_parameter_id
)
workflow_id = Column(String, index=True, nullable=False)
key = Column(String, nullable=False)
description = Column(String, nullable=True)
vault_id = Column(String, nullable=False)
item_id = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(
DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
nullable=False,
)
deleted_at = Column(DateTime, nullable=True)
class WorkflowRunParameterModel(Base):
__tablename__ = "workflow_run_parameters"

View file

@ -0,0 +1,191 @@
import json
import logging
from enum import StrEnum
from typing import Optional
from onepassword.client import Client as OnePasswordClient
from skyvern.config import settings
LOG = logging.getLogger(__name__)
class OnePasswordConstants(StrEnum):
"""Constants for 1Password integration."""
TOTP = "OP_TOTP" # Special value to indicate a TOTP code
async def resolve_secret(vault_id: str, item_id: str) -> str:
"""
Resolve a 1Password secret using vault_id and item_id directly.
Args:
vault_id: The 1Password vault ID
item_id: The 1Password item ID
Returns:
The resolved secret value
"""
token = settings.OP_SERVICE_ACCOUNT_TOKEN
if not token:
raise ValueError("OP_SERVICE_ACCOUNT_TOKEN not configured in settings")
client = await OnePasswordClient.authenticate(
auth=token,
integration_name="Skyvern 1Password",
integration_version="v1.0.0",
)
result = await get_1password_item_details(client, vault_id, item_id)
return result
async def get_1password_item_details(client: OnePasswordClient, vault_id: str, item_id: str) -> str:
"""
Get details of a 1Password item.
Args:
client: Authenticated 1Password client
vault_id: The vault ID
item_id: The item ID
Returns:
JSON string containing item fields and their values
"""
try:
item = await client.items.get(vault_id, item_id)
# Check if item is None
if item is None:
LOG.error(f"No item found for vault_id:{vault_id}, item_id:{item_id}")
raise ValueError(f"1Password item not found: vault_id:{vault_id}, item_id:{item_id}")
# Create a dictionary of all fields
result = {}
# Debug: Log the structure of the item and fields
LOG.info(
f"1Password item structure: {dir(item)}"
+ (f"\nFirst field structure: {dir(item.fields[0])}" if hasattr(item, "fields") and item.fields else "")
)
# We don't log field values as they may contain sensitive credentials
# Add all fields with proper attribute checking
for i, field in enumerate(item.fields):
# Debug: Log each field's structure
LOG.debug(f"Field {i} structure: {dir(field)}")
if hasattr(field, "value") and field.value is not None:
# Safely get field identifier - use id attribute or fallback to a default
try:
# Try different possible attribute names for the field identifier
field_id = None
# Check all available attributes on the field object
field_attrs = dir(field)
LOG.debug(f"Field {i} attributes: {field_attrs}")
# Try to get the most appropriate identifier
if hasattr(field, "id") and field.id:
field_id = field.id
LOG.debug(f"Using field.id: {field_id}")
elif hasattr(field, "name") and field.name:
field_id = field.name
LOG.debug(f"Using field.name: {field_id}")
elif hasattr(field, "label") and field.label:
field_id = field.label
LOG.debug(f"Using field.label: {field_id}")
elif hasattr(field, "type") and field.type:
field_id = f"{field.type}_{i}"
LOG.debug(f"Using field.type: {field_id}")
else:
# If no identifier found, generate one based on index
field_id = f"field_{i}"
LOG.debug(f"Using generated id: {field_id}")
# Create a safe key name
key = str(field_id).lower().replace(" ", "_")
result[key] = field.value
LOG.debug(f"Added field with key '{key}' and value type: {type(field.value).__name__}")
except Exception as field_err:
LOG.warning(f"Error processing field {i}: {field_err}")
# Still try to capture the value with a generic key
result[f"field_{i}"] = field.value
# Explicitly look for username and password fields
for i, field in enumerate(item.fields):
try:
# Check for username field using various possible attributes
if "username" not in result:
if hasattr(field, "id") and field.id == "username" and hasattr(field, "value") and field.value:
result["username"] = field.value
LOG.debug(f"Found username field at index {i}")
elif (
hasattr(field, "purpose")
and field.purpose == "USERNAME"
and hasattr(field, "value")
and field.value
):
result["username"] = field.value
LOG.debug(f"Found username field by purpose at index {i}")
elif (
hasattr(field, "type") and field.type == "USERNAME" and hasattr(field, "value") and field.value
):
result["username"] = field.value
LOG.debug(f"Found username field by type at index {i}")
# Check for password field using various possible attributes
if "password" not in result:
if hasattr(field, "id") and field.id == "password" and hasattr(field, "value") and field.value:
result["password"] = field.value
LOG.debug(f"Found password field at index {i}")
elif (
hasattr(field, "purpose")
and field.purpose == "PASSWORD"
and hasattr(field, "value")
and field.value
):
result["password"] = field.value
LOG.debug(f"Found password field by purpose at index {i}")
elif (
hasattr(field, "type") and field.type == "PASSWORD" and hasattr(field, "value") and field.value
):
result["password"] = field.value
LOG.debug(f"Found password field by type at index {i}")
except Exception as field_err:
LOG.warning(f"Error processing username/password field at index {i}: {field_err}")
# Add TOTP if available
try:
totp = await get_totp_for_item(client, vault_id, item_id)
if totp:
result["totp"] = totp
except Exception as totp_err:
LOG.warning(f"Error getting TOTP: {totp_err}")
return json.dumps(result)
except Exception as e:
LOG.error(f"Error retrieving 1Password item {vault_id}:{item_id}: {str(e)}")
raise
async def get_totp_for_item(client: OnePasswordClient, vault_id: str, item_id: str) -> Optional[str]:
"""
Get the TOTP code for a 1Password item if available.
Args:
client: Authenticated 1Password client
vault_id: The vault ID
item_id: The item ID
Returns:
TOTP code if available, None otherwise
"""
try:
totp = await client.items.get_totp(vault_id, item_id)
return totp
except Exception:
# TOTP might not be available for this item
return None

View file

@ -1,8 +1,10 @@
import copy
import json
import uuid
from typing import TYPE_CHECKING, Any, Self
import structlog
from onepassword.client import Client as OnePasswordClient
from skyvern.config import settings
from skyvern.exceptions import (
@ -17,6 +19,7 @@ from skyvern.forge.sdk.schemas.credentials import PasswordCredential
from skyvern.forge.sdk.schemas.organizations import Organization
from skyvern.forge.sdk.schemas.tasks import TaskStatus
from skyvern.forge.sdk.services.bitwarden import BitwardenConstants, BitwardenService
from skyvern.forge.sdk.services.credentials import OnePasswordConstants, resolve_secret
from skyvern.forge.sdk.workflow.exceptions import OutputParameterKeyCollisionError
from skyvern.forge.sdk.workflow.models.parameter import (
PARAMETER_TYPE,
@ -26,6 +29,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
BitwardenSensitiveInformationParameter,
ContextParameter,
CredentialParameter,
OnePasswordCredentialParameter,
OutputParameter,
Parameter,
ParameterType,
@ -86,6 +90,8 @@ class WorkflowRunContext:
await workflow_run_context.register_aws_secret_parameter_value(aws_client, secrete_parameter)
elif isinstance(secrete_parameter, CredentialParameter):
await workflow_run_context.register_credential_parameter_value(secrete_parameter, organization)
elif isinstance(secrete_parameter, OnePasswordCredentialParameter):
await workflow_run_context.register_onepassword_credential_parameter_value(secrete_parameter)
elif isinstance(secrete_parameter, BitwardenLoginCredentialParameter):
await workflow_run_context.register_bitwarden_login_credential_parameter_value(
aws_client, secrete_parameter, organization
@ -180,6 +186,29 @@ class WorkflowRunContext:
def generate_random_secret_id() -> str:
return f"secret_{uuid.uuid4()}"
async def _get_credential_vault_and_item_ids(self, credential_id: str) -> tuple[str, str]:
"""
Extract vault_id and item_id from the credential_id.
This method handles the legacy format vault_id:item_id.
Args:
credential_id: The credential identifier in the format vault_id:item_id
Returns:
A tuple of (vault_id, item_id)
Raises:
ValueError: If the credential format is invalid
"""
# Check if it's in the format vault_id:item_id
if ":" in credential_id:
LOG.info(f"Processing credential in vault_id:item_id format: {credential_id}")
vault_id, item_id = credential_id.split(":", 1)
return vault_id, item_id
# If we can't parse the credential_id, raise an error
raise ValueError(f"Invalid credential format: {credential_id}. Expected format: vault_id:item_id")
async def register_secret_workflow_parameter_value(
self,
parameter: WorkflowParameter,
@ -195,30 +224,108 @@ class WorkflowRunContext:
LOG.info(f"Fetching credential parameter value for credential: {credential_id}")
db_credential = await app.DATABASE.get_credential(credential_id, organization_id=organization.organization_id)
if db_credential is None:
raise CredentialParameterNotFoundError(credential_id)
try:
# Extract vault_id and item_id from the database
vault_id, item_id = await self._get_credential_vault_and_item_ids(credential_id)
bitwarden_credential = await BitwardenService.get_credential_item(db_credential.item_id)
# Use the 1Password SDK to resolve the reference using vault_id and item_id directly
secret_value_json = await resolve_secret(vault_id, item_id)
credential_item = bitwarden_credential.credential
# Validate the JSON response
if not secret_value_json:
LOG.error(f"Empty response from 1Password for credential: {credential_id}")
raise ValueError(f"Empty response from 1Password for credential: {credential_id}")
self.parameters[parameter.key] = parameter
self.values[parameter.key] = {}
credential_dict = credential_item.model_dump()
for key, value in credential_dict.items():
random_secret_id = self.generate_random_secret_id()
secret_id = f"{random_secret_id}_{key}"
self.secrets[secret_id] = value
self.values[parameter.key][key] = secret_id
try:
secret_values = json.loads(secret_value_json)
except json.JSONDecodeError as json_err:
LOG.error(f"Invalid JSON response from 1Password: {secret_value_json[:100]}... Error: {json_err}")
raise ValueError(f"Invalid JSON response from 1Password: {json_err}")
if isinstance(credential_item, PasswordCredential) and credential_item.totp is not None:
random_secret_id = self.generate_random_secret_id()
totp_secret_id = f"{random_secret_id}_totp"
self.secrets[totp_secret_id] = BitwardenConstants.TOTP
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
self.secrets[totp_secret_value] = credential_item.totp
self.values[parameter.key]["totp"] = totp_secret_id
if not secret_values:
LOG.warning(f"No values found in 1Password item: {credential_id}")
# Still continue with empty values
self.parameters[parameter.key] = parameter
self.values[parameter.key] = {}
# Process fields from the 1Password item
if "fields" in secret_values and isinstance(secret_values["fields"], list):
for field in secret_values["fields"]:
if not isinstance(field, dict) or "id" not in field or "value" not in field:
continue
field_id = field.get("id")
field_type = field.get("field_type")
field_value = field.get("value")
# Store the field value
random_secret_id = self.generate_random_secret_id()
secret_id = f"{random_secret_id}_{field_id}"
self.secrets[secret_id] = field_value
self.values[parameter.key][field_id] = secret_id
# For TOTP fields, also store the current code
if field_type == "Totp" and isinstance(field.get("details"), dict):
details = field.get("details")
# Explicitly check that details is a dict before accessing get method
if isinstance(details, dict):
content = details.get("content")
if isinstance(content, dict) and "code" in content:
totp_code = content["code"]
random_secret_id = self.generate_random_secret_id()
totp_secret_id = f"{random_secret_id}_totp"
self.secrets[totp_secret_id] = totp_code
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
self.secrets[totp_secret_value] = field_value # Store the TOTP secret
self.values[parameter.key]["totp"] = totp_secret_id
else:
# Process each field in the 1Password item (old format or custom format)
for key, value in secret_values.items():
random_secret_id = self.generate_random_secret_id()
secret_id = f"{random_secret_id}_{key}"
self.secrets[secret_id] = value
self.values[parameter.key][key] = secret_id
LOG.info("Successfully processed 1Password credential")
return
except Exception as e:
LOG.error(f"Failed to process 1Password credential: {credential_id}. Error: {str(e)}")
# Add more context to the error
raise ValueError(f"Failed to process 1Password credential {credential_id}: {str(e)}") from e
# Handle regular credentials from the database
try:
db_credential = await app.DATABASE.get_credential(
credential_id, organization_id=organization.organization_id
)
if db_credential is None:
raise CredentialParameterNotFoundError(credential_id)
bitwarden_credential = await BitwardenService.get_credential_item(db_credential.item_id)
credential_item = bitwarden_credential.credential
self.parameters[parameter.key] = parameter
self.values[parameter.key] = {}
credential_dict = credential_item.model_dump()
for key, value in credential_dict.items():
random_secret_id = self.generate_random_secret_id()
secret_id = f"{random_secret_id}_{key}"
self.secrets[secret_id] = value
self.values[parameter.key][key] = secret_id
if isinstance(credential_item, PasswordCredential) and credential_item.totp is not None:
random_secret_id = self.generate_random_secret_id()
totp_secret_id = f"{random_secret_id}_totp"
self.secrets[totp_secret_id] = BitwardenConstants.TOTP
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
self.secrets[totp_secret_value] = credential_item.totp
self.values[parameter.key]["totp"] = totp_secret_id
except Exception as e:
LOG.error(f"Failed to get credential from database: {credential_id}. Error: {e}")
raise e
async def register_credential_parameter_value(
self,
@ -278,6 +385,56 @@ class WorkflowRunContext:
self.values[parameter.key] = random_secret_id
self.parameters[parameter.key] = parameter
async def register_onepassword_credential_parameter_value(self, parameter: OnePasswordCredentialParameter) -> None:
token = settings.OP_SERVICE_ACCOUNT_TOKEN
if not token:
raise ValueError("OP_SERVICE_ACCOUNT_TOKEN environment variable not set")
client = await OnePasswordClient.authenticate(
auth=token,
integration_name="Skyvern",
integration_version="v1.0.0",
)
item = await client.items.get(parameter.vault_id, parameter.item_id)
# Check if item is None
if item is None:
LOG.error(f"No item found for vault_id:{parameter.vault_id}, item_id:{parameter.item_id}")
raise ValueError(f"1Password item not found: vault_id:{parameter.vault_id}, item_id:{parameter.item_id}")
self.parameters[parameter.key] = parameter
self.values[parameter.key] = {}
# Process all fields
for field in item.fields:
if field.value is None:
continue
random_secret_id = self.generate_random_secret_id()
secret_id = f"{random_secret_id}_{field.id}"
self.secrets[secret_id] = field.value
key = (field.label or field.id).lower().replace(" ", "_")
self.values[parameter.key][key] = secret_id
# Try to get TOTP if available
try:
totp = await client.items.get_totp(parameter.vault_id, parameter.item_id)
if totp:
# Store the actual TOTP value in a separate secret for internal use
random_secret_id = self.generate_random_secret_id()
totp_value_id = f"{random_secret_id}_totp_value"
self.secrets[totp_value_id] = totp
# Store the special TOTP constant that the agent will recognize
totp_secret_id = f"{random_secret_id}_totp"
self.secrets[totp_secret_id] = OnePasswordConstants.TOTP
self.values[parameter.key]["totp"] = totp_secret_id
LOG.info(f"TOTP code available for item {parameter.item_id}")
except Exception as e:
# TOTP might not be available for this item, just log and continue
LOG.debug(f"TOTP not available for item {parameter.item_id}: {str(e)}")
async def register_bitwarden_login_credential_parameter_value(
self,
aws_client: AsyncAWSClient,

View file

@ -18,6 +18,7 @@ class ParameterType(StrEnum):
BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential"
BITWARDEN_SENSITIVE_INFORMATION = "bitwarden_sensitive_information"
BITWARDEN_CREDIT_CARD_DATA = "bitwarden_credit_card_data"
ONEPASSWORD = "onepassword"
OUTPUT = "output"
CREDENTIAL = "credential"
@ -127,6 +128,19 @@ class BitwardenCreditCardDataParameter(Parameter):
deleted_at: datetime | None = None
class OnePasswordCredentialParameter(Parameter):
parameter_type: Literal[ParameterType.ONEPASSWORD] = ParameterType.ONEPASSWORD
onepassword_credential_parameter_id: str
workflow_id: str
vault_id: str
item_id: str
created_at: datetime
modified_at: datetime
deleted_at: datetime | None = None
class WorkflowParameterType(StrEnum):
STRING = "string"
INTEGER = "integer"
@ -203,6 +217,7 @@ ParameterSubclasses = Union[
BitwardenLoginCredentialParameter,
BitwardenSensitiveInformationParameter,
BitwardenCreditCardDataParameter,
OnePasswordCredentialParameter,
OutputParameter,
CredentialParameter,
]

View file

@ -86,6 +86,12 @@ class BitwardenCreditCardDataParameterYAML(ParameterYAML):
bitwarden_item_id: str
class OnePasswordCredentialParameterYAML(ParameterYAML):
parameter_type: Literal[ParameterType.ONEPASSWORD] = ParameterType.ONEPASSWORD # type: ignore
vault_id: str
item_id: str
class WorkflowParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
# Parameter 1 of Literal[...] cannot be of type "Any"
@ -370,6 +376,7 @@ PARAMETER_YAML_SUBCLASSES = (
| BitwardenLoginCredentialParameterYAML
| BitwardenSensitiveInformationParameterYAML
| BitwardenCreditCardDataParameterYAML
| OnePasswordCredentialParameterYAML
| WorkflowParameterYAML
| ContextParameterYAML
| OutputParameterYAML

View file

@ -68,6 +68,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
BitwardenSensitiveInformationParameter,
ContextParameter,
CredentialParameter,
OnePasswordCredentialParameter,
OutputParameter,
Parameter,
ParameterType,
@ -239,6 +240,7 @@ class WorkflowService:
BitwardenLoginCredentialParameter,
BitwardenCreditCardDataParameter,
BitwardenSensitiveInformationParameter,
OnePasswordCredentialParameter,
CredentialParameter,
),
)
@ -883,6 +885,22 @@ class WorkflowService:
description=description,
)
async def create_onepassword_credential_parameter(
self,
workflow_id: str,
key: str,
vault_id: str,
item_id: str,
description: str | None = None,
) -> OnePasswordCredentialParameter:
return await app.DATABASE.create_onepassword_credential_parameter(
workflow_id=workflow_id,
key=key,
vault_id=vault_id,
item_id=item_id,
description=description,
)
async def create_bitwarden_sensitive_information_parameter(
self,
workflow_id: str,
@ -1490,6 +1508,14 @@ class WorkflowService:
description=parameter.description,
credential_id=parameter.credential_id,
)
elif parameter.parameter_type == ParameterType.ONEPASSWORD:
parameters[parameter.key] = await self.create_onepassword_credential_parameter(
workflow_id=workflow.workflow_id,
key=parameter.key,
description=parameter.description,
vault_id=parameter.vault_id,
item_id=parameter.item_id,
)
elif parameter.parameter_type == ParameterType.BITWARDEN_LOGIN_CREDENTIAL:
if not parameter.bitwarden_collection_id and not parameter.bitwarden_item_id:
raise WorkflowParameterMissingRequiredValue(

View file

@ -70,6 +70,7 @@ from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.models import Step
from skyvern.forge.sdk.schemas.tasks import Task
from skyvern.forge.sdk.services.bitwarden import BitwardenConstants
from skyvern.forge.sdk.services.credentials import OnePasswordConstants
from skyvern.schemas.runs import CUA_RUN_TYPES
from skyvern.utils.prompt_engine import CheckPhoneNumberFormatResponse, load_prompt_with_elements
from skyvern.webeye.actions import actions
@ -821,7 +822,7 @@ async def handle_input_text_action(
if text is None:
return [ActionFailure(FailedToFetchSecret())]
is_totp_value = text == BitwardenConstants.TOTP
is_totp_value = text == BitwardenConstants.TOTP or text == OnePasswordConstants.TOTP
is_secret_value = text != action.text
# dynamically validate the attr, since it could change into enabled after the previous actions