diff --git a/CHANGELOG.md b/CHANGELOG.md index 822cc9d..9fd58b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Added + +- `GetClawSpace`: `client.spaces.get` and `arkclaw space get` for retrieving + ArkClaw space detail (endpoints, auth type, status, APM ID). +- `ListUsersModelConfig`: `client.spaces.list_users_model_config` and + `arkclaw space list-users-model-config` for paginated user model + configuration lookup with optional `UserIds` filter. +- `ListUsers`: `client.users.list` and `arkclaw user list` for listing users + in a space with department, group, email, phone, name, and user-ID filters. +- `CreateClawInstance`: new optional parameters `EnableHeadless`, + `ClientToken`, `DryRun` on `client.instances.create` and + `arkclaw instance create`. +- `UpdateClawInstance`: reassign or unbind the owning user via + `client.instances.update(user_id=...)` / `arkclaw instance update --user-id`, + which populates `Patch.UserId.Value` and `FieldMask.Paths=[Patch.UserId]` + under the hood. Omitting `user_id` leaves the binding untouched; passing an + empty string or `None` unbinds the current user. + +### Changed + +- `CreateClawInstance`: `UserId` is now optional (was required). +- `CreateUsers`: spec explicitly declares `Users` as a required `object[]`; + user element fields remain the same. + +### Fixed + +- Parameter spec `.N` list-marker stripping now uses a positional regex so + legitimate field names such as `Filter.Name` are no longer mangled into + `Filterame`. + ## [0.1.0] - 2026-06-23 ### Added diff --git a/README.md b/README.md index 13e5d2d..fd97dab 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ Spaces: | OpenAPI action | SDK method | CLI command | |---|---|---| | `ListClawSpaces` | `client.spaces.list` | `arkclaw space list` | +| `GetClawSpace` | `client.spaces.get` | `arkclaw space get` | | `UpdateUsersModelConfig` | `client.spaces.update_users_model_config` | `arkclaw space update-users-model-config` | +| `ListUsersModelConfig` | `client.spaces.list_users_model_config` | `arkclaw space list-users-model-config` | Instances: @@ -79,6 +81,7 @@ Users: | `CreateUser` | `client.users.create` | `arkclaw user create` | | `UpdateUser` | `client.users.update` | `arkclaw user update` | | `DeleteUser` | `client.users.delete` | `arkclaw user delete` | +| `ListUsers` | `client.users.list` | `arkclaw user list` | Additional SDK workflows include `wait_for_instance`, `provision_instance`, and `prepare_chat_access` under `client.workflows`. @@ -92,6 +95,20 @@ client = ArkClawClient.from_env() spaces = client.spaces.list() +space = client.spaces.get(space_id="csi-xxx") + +user_configs = client.spaces.list_users_model_config( + space_id="csi-xxx", + user_ids=["user-xxx"], + max_results=20, +) + +users_page = client.users.list( + space_id="csi-xxx", + filter={"name": "zhang", "user_ids": ["user-xxx"]}, + max_results=20, +) + model_config_result = client.spaces.update_users_model_config( space_id="csi-xxx", user_ids=["user-xxx"], @@ -128,6 +145,12 @@ client.instances.update( instance_name="demo-claw-renamed", ) +client.instances.update( + space_id="csi-xxx", + instance_id=instance["InstanceId"], + user_id="user-yyy", # pass "" to unbind the current owner +) + terminal = client.instances.get_terminal_token( space_id="csi-xxx", instance_id=instance["InstanceId"], @@ -221,8 +244,8 @@ Supported command groups and subcommands: | Group | Subcommands | |---|---| -| `space` | `list`, `update-users-model-config` | -| `user` | `create`, `create-many`, `update`, `delete` | +| `space` | `list`, `get`, `update-users-model-config`, `list-users-model-config` | +| `user` | `create`, `create-many`, `update`, `delete`, `list` | | `instance` | `create`, `update-model`, `chat-token`, `get`, `update-channel`, `list`, `start`, `stop`, `reset`, `update`, `delete`, `terminal-token`, `wait` | | `message` | `send`, `shell` | @@ -231,11 +254,24 @@ Selected examples: ```bash arkclaw space list +arkclaw space get --space-id csi-xxx + +arkclaw space list-users-model-config \ + --space-id csi-xxx \ + --user-id user-xxx \ + --max-results 20 + arkclaw space update-users-model-config \ --space-id csi-xxx \ --user-id user-xxx \ --coding-plan-seat-type Lite +arkclaw user list \ + --space-id csi-xxx \ + --filter-name zhang \ + --filter-user-id user-xxx \ + --max-results 20 + arkclaw user create \ --space-id csi-xxx \ --email alice@example.com \ @@ -266,6 +302,11 @@ arkclaw instance update \ --instance-id ci-xxx \ --instance-name demo-claw-renamed +arkclaw instance update \ + --space-id csi-xxx \ + --instance-id ci-xxx \ + --user-id user-yyy # pass "" to unbind the current owner + arkclaw instance terminal-token \ --space-id csi-xxx \ --instance-id ci-xxx diff --git a/arkclaw/cli/instances.py b/arkclaw/cli/instances.py index 8e94e59..8991f47 100644 --- a/arkclaw/cli/instances.py +++ b/arkclaw/cli/instances.py @@ -21,6 +21,9 @@ from ._common import build_client, emit, info +_CLI_USER_ID_UNSET = object() + + def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg] group = subparsers.add_parser("instance", help="ClawInstance management") sub = group.add_subparsers(dest="instance_action") @@ -28,12 +31,15 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty p = sub.add_parser("create", help="Create a ClawInstance") p.add_argument("--space-id", required=True) - p.add_argument("--user-id", required=True) + p.add_argument("--user-id", default=None) p.add_argument("--instance-name", required=True) p.add_argument("--seat-type", required=True, choices=["Starter", "Standard", "Premium", "Ultimate"]) p.add_argument("--description", default=None) p.add_argument("--model-api-key", default=None) p.add_argument("--template-id", default=None) + p.add_argument("--enable-headless", choices=["true", "false"], default=None) + p.add_argument("--client-token", default=None) + p.add_argument("--dry-run", choices=["true", "false"], default=None) p.add_argument("--wait", action="store_true", default=False) p.add_argument("--wait-timeout", type=float, default=600) p.add_argument("--interval", type=float, default=5) @@ -101,6 +107,11 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty p.add_argument("--space-id", required=True) p.add_argument("--instance-id", required=True) p.add_argument("--instance-name", default=None) + p.add_argument( + "--user-id", + default=_CLI_USER_ID_UNSET, + help="Reassign the instance to this user ID; pass an empty string to unbind. Omit to leave the binding unchanged.", + ) p.add_argument("--client-token", default=None) p.add_argument("--dry-run", choices=["true", "false"], default=None) p.set_defaults(func=_update) @@ -129,6 +140,8 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty def _create(args: argparse.Namespace) -> None: client = build_client(args) + enable_headless = None if args.enable_headless is None else args.enable_headless == "true" + dry_run = None if args.dry_run is None else args.dry_run == "true" if args.wait: info(f"Creating {args.instance_name} and waiting for Running...") result = client.workflows.provision_instance( @@ -151,6 +164,9 @@ def _create(args: argparse.Namespace) -> None: description=args.description, model_api_key=args.model_api_key, template_id=args.template_id, + enable_headless=enable_headless, + client_token=args.client_token, + dry_run=dry_run, ) emit(result) @@ -258,15 +274,16 @@ def _reset(args: argparse.Namespace) -> None: def _update(args: argparse.Namespace) -> None: client = build_client(args) dry_run = None if args.dry_run is None else args.dry_run == "true" - emit( - client.instances.update( - space_id=args.space_id, - instance_id=args.instance_id, - instance_name=None if args.instance_name in (None, "") else args.instance_name, - client_token=args.client_token, - dry_run=dry_run, - ) + update_kwargs: dict[str, Any] = dict( + space_id=args.space_id, + instance_id=args.instance_id, + instance_name=None if args.instance_name in (None, "") else args.instance_name, + client_token=args.client_token, + dry_run=dry_run, ) + if args.user_id is not _CLI_USER_ID_UNSET: + update_kwargs["user_id"] = args.user_id + emit(client.instances.update(**update_kwargs)) def _delete(args: argparse.Namespace) -> None: diff --git a/arkclaw/cli/spaces.py b/arkclaw/cli/spaces.py index 38cc694..2ec16f5 100644 --- a/arkclaw/cli/spaces.py +++ b/arkclaw/cli/spaces.py @@ -30,6 +30,10 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty p.add_argument("--space-name", default=None) p.set_defaults(func=_list) + p = sub.add_parser("get", help="Get ClawSpace detail") + p.add_argument("--space-id", required=True) + p.set_defaults(func=_get) + p = sub.add_parser("update-users-model-config", help="Update codingPlan seat type and token limits for users") p.add_argument("--space-id", required=True) p.add_argument("--user-id", dest="user_ids", action="append", required=True) @@ -38,6 +42,13 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty p.add_argument("--token-rate-limit-per-day", type=int, default=None) p.set_defaults(func=_update_users_model_config) + p = sub.add_parser("list-users-model-config", help="List user model configurations in a space") + p.add_argument("--space-id", required=True) + p.add_argument("--user-id", dest="user_ids", action="append", default=None) + p.add_argument("--max-results", type=int, default=None) + p.add_argument("--next-token", default=None) + p.set_defaults(func=_list_users_model_config) + def _model_config_kwargs(args: argparse.Namespace) -> dict[str, Any]: return { @@ -52,6 +63,11 @@ def _list(args: argparse.Namespace) -> None: emit(client.spaces.list(project_name=args.project_name, space_name=args.space_name)) +def _get(args: argparse.Namespace) -> None: + client = build_client(args) + emit(client.spaces.get(space_id=args.space_id)) + + def _update_users_model_config(args: argparse.Namespace) -> None: client = build_client(args) emit( @@ -61,3 +77,15 @@ def _update_users_model_config(args: argparse.Namespace) -> None: model_config=_model_config_kwargs(args), ) ) + + +def _list_users_model_config(args: argparse.Namespace) -> None: + client = build_client(args) + emit( + client.spaces.list_users_model_config( + space_id=args.space_id, + user_ids=args.user_ids, + max_results=args.max_results, + next_token=args.next_token, + ) + ) diff --git a/arkclaw/cli/users.py b/arkclaw/cli/users.py index 4d8cd3b..92b05c6 100644 --- a/arkclaw/cli/users.py +++ b/arkclaw/cli/users.py @@ -47,6 +47,48 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty p.add_argument("--user-id", required=True) p.set_defaults(func=_delete) + p = sub.add_parser("list", help="List users in a space with optional filters") + p.add_argument("--space-id", required=True) + p.add_argument("--max-results", type=int, default=None) + p.add_argument("--next-token", default=None) + p.add_argument("--filter-department-uid", default=None) + p.add_argument( + "--filter-department-uid-recursive", + dest="filter_department_uid_recursive", + action="store_true", + default=None, + ) + p.add_argument("--filter-email", default=None) + p.add_argument( + "--filter-email-phone-name-is-null-or-empty", + dest="filter_email_phone_name_is_null_or_empty", + action="store_true", + default=None, + ) + p.add_argument("--filter-group-uid", default=None) + p.add_argument("--filter-name", default=None) + p.add_argument( + "--filter-not-in-any-department", + dest="filter_not_in_any_department", + action="store_true", + default=None, + ) + p.add_argument( + "--filter-not-in-any-group", + dest="filter_not_in_any_group", + action="store_true", + default=None, + ) + p.add_argument("--filter-phone-number", default=None) + p.add_argument( + "--filter-user-id", + dest="filter_user_ids", + action="append", + default=None, + help="User ID filter; may be given multiple times", + ) + p.set_defaults(func=_list) + def _add_user_fields(parser: argparse.ArgumentParser) -> None: parser.add_argument("--email", default=None) @@ -90,3 +132,28 @@ def _delete(args: argparse.Namespace) -> None: client = build_client(args) emit(client.users.delete(space_id=args.space_id, user_id=args.user_id)) + +def _list(args: argparse.Namespace) -> None: + client = build_client(args) + filter_kwargs: dict[str, Any] = { + "department_uid": args.filter_department_uid, + "department_uid_recursive": args.filter_department_uid_recursive, + "email": args.filter_email, + "email_phone_name_is_null_or_empty": args.filter_email_phone_name_is_null_or_empty, + "group_uid": args.filter_group_uid, + "name": args.filter_name, + "not_in_any_department": args.filter_not_in_any_department, + "not_in_any_group": args.filter_not_in_any_group, + "phone_number": args.filter_phone_number, + "user_ids": args.filter_user_ids, + } + filter_payload = {k: v for k, v in filter_kwargs.items() if v is not None} + emit( + client.users.list( + space_id=args.space_id, + filter=filter_payload or None, + max_results=args.max_results, + next_token=args.next_token, + ) + ) + diff --git a/arkclaw/client.py b/arkclaw/client.py index f51b2d0..3a69d9b 100644 --- a/arkclaw/client.py +++ b/arkclaw/client.py @@ -29,7 +29,14 @@ from .config import RetryConfig, RuntimeOptions, TimeoutConfig, TransportConfig from .exceptions import ApiError, ValidationError from .signer import sign_request -from .spec import ACTION_SPECS, DEFAULT_VERSION, GROUP_TO_ACTIONS, ActionSpec, ParameterSpec +from .spec import ( + ACTION_SPECS, + DEFAULT_VERSION, + GROUP_TO_ACTIONS, + ActionSpec, + ParameterSpec, + _strip_list_markers, +) from .transport import HttpTransport, Urllib3Transport if TYPE_CHECKING: @@ -38,6 +45,8 @@ LOGGER = logging.getLogger("arkclaw") +_UNSET: Any = object() + def _set_nested(target: dict[str, Any], path: tuple[str, ...], value: Any) -> None: current = target @@ -72,12 +81,14 @@ def _flatten_input(mapping: dict[str, Any], prefix: tuple[str, ...] = ()) -> lis def _candidate_aliases(name: str) -> list[str]: lowered = name.lower() - collapsed = lowered.replace(".n", "").replace(".", "_") + stripped = _strip_list_markers(name) + stripped_lower = _strip_list_markers(lowered) + collapsed = stripped_lower.replace(".", "_") return [ name, lowered, - name.replace(".N", ""), - lowered.replace(".n", ""), + stripped, + stripped_lower, collapsed, ] @@ -135,6 +146,29 @@ def _pascalize_tag_filter(tag_filter: dict[str, Any]) -> dict[str, Any]: } +_USER_FILTER_KEY_MAP = { + "department_uid": "DepartmentUid", + "department_uid_recursive": "DepartmentUidRecursive", + "email": "Email", + "email_phone_name_is_null_or_empty": "EmailPhoneNameIsNullOrEmpty", + "group_uid": "GroupUid", + "name": "Name", + "not_in_any_department": "NotInAnyDepartment", + "not_in_any_group": "NotInAnyGroup", + "phone_number": "PhoneNumber", + "user_ids": "UserIds", +} + + +def _pascalize_user_filter(user_filter: dict[str, Any]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in user_filter.items(): + if value is None: + continue + result[_USER_FILTER_KEY_MAP.get(key, key)] = value + return result + + def _normalize_special_payload_value(raw_key: str, value: Any) -> Any: lowered = raw_key.lower() if lowered == "tag_filters" and isinstance(value, list): @@ -253,6 +287,18 @@ class SpaceOperations(ResourceBase): def list(self, *, runtime_options: Optional[RuntimeOptions] = None, **kwargs: Any) -> dict[str, Any]: return self.invoke("ListClawSpaces", runtime_options=runtime_options, **kwargs) + def get( + self, + *, + space_id: str, + runtime_options: Optional[RuntimeOptions] = None, + ) -> dict[str, Any]: + return self.invoke( + "GetClawSpace", + space_id=space_id, + runtime_options=runtime_options, + ) + def update_users_model_config( self, *, @@ -270,6 +316,25 @@ def update_users_model_config( ) return self.invoke("UpdateUsersModelConfig", payload=payload, runtime_options=runtime_options) + def list_users_model_config( + self, + *, + space_id: str, + user_ids: Optional[List[str]] = None, + max_results: Optional[int] = None, + next_token: Optional[str] = None, + runtime_options: Optional[RuntimeOptions] = None, + ) -> dict[str, Any]: + payload = _compact_dict( + { + "space_id": space_id, + "user_ids": user_ids, + "max_results": max_results, + "next_token": next_token, + } + ) + return self.invoke("ListUsersModelConfig", payload=payload, runtime_options=runtime_options) + class UserOperations(ResourceBase): actions = GROUP_TO_ACTIONS["users"] @@ -329,6 +394,25 @@ def delete( runtime_options=runtime_options, ) + def list( + self, + *, + space_id: str, + filter: Optional[dict[str, Any]] = None, + max_results: Optional[int] = None, + next_token: Optional[str] = None, + runtime_options: Optional[RuntimeOptions] = None, + ) -> dict[str, Any]: + payload = _compact_dict( + { + "space_id": space_id, + "Filter": _pascalize_user_filter(filter) if filter else None, + "max_results": max_results, + "next_token": next_token, + } + ) + return self.invoke("ListUsers", payload=payload, runtime_options=runtime_options) + class InstanceOperations(ResourceBase): actions = GROUP_TO_ACTIONS["instances"] @@ -336,10 +420,13 @@ def create( self, *, space_id: str, - user_id: str, instance_name: str, seat_type: str, + user_id: Optional[str] = None, template_id: Optional[str] = None, + enable_headless: Optional[bool] = None, + client_token: Optional[str] = None, + dry_run: Optional[bool] = None, runtime_options: Optional[RuntimeOptions] = None, **kwargs: Any, ) -> dict[str, Any]: @@ -350,6 +437,9 @@ def create( instance_name=instance_name, seat_type=seat_type, template_id=template_id, + enable_headless=enable_headless, + client_token=client_token, + dry_run=dry_run, runtime_options=runtime_options, **kwargs, ) @@ -493,11 +583,12 @@ def update( space_id: str, instance_id: str, instance_name: Optional[str] = None, + user_id: Any = _UNSET, client_token: Optional[str] = None, dry_run: Optional[bool] = None, runtime_options: Optional[RuntimeOptions] = None, ) -> dict[str, Any]: - payload = _compact_dict( + payload: dict[str, Any] = _compact_dict( { "space_id": space_id, "instance_id": instance_id, @@ -506,6 +597,13 @@ def update( "dry_run": dry_run, } ) + if user_id is not _UNSET: + payload["Patch"] = {"UserId": "" if user_id is None else user_id} + existing_paths = payload.get("FieldMask", {}).get("Paths", []) + paths = list(existing_paths) + if "Patch.UserId" not in paths: + paths.append("Patch.UserId") + payload["FieldMask"] = {"Paths": paths} return self.invoke("UpdateClawInstance", payload=payload, runtime_options=runtime_options) def delete( diff --git a/arkclaw/spec.py b/arkclaw/spec.py index 74937f0..6466939 100644 --- a/arkclaw/spec.py +++ b/arkclaw/spec.py @@ -22,8 +22,15 @@ DEFAULT_VERSION = "2026-05-01" +_LIST_MARKER_RE = re.compile(r"\.[Nn](?=\.|$)") + + +def _strip_list_markers(name: str) -> str: + return _LIST_MARKER_RE.sub("", name) + + def _to_snake(name: str) -> str: - cleaned = name.replace(".N", "") + cleaned = _strip_list_markers(name) cleaned = cleaned.replace(".", "_") cleaned = re.sub(r"(? bool: - return ".N" in self.raw_name + return bool(_LIST_MARKER_RE.search(self.raw_name)) @property def body_name(self) -> str: - return self.raw_name.replace(".N", "") + return _strip_list_markers(self.raw_name) @property def path(self) -> tuple[str, ...]: @@ -162,18 +169,27 @@ class RawActionSpec(TypedDict): ("SpaceName", False, "string"), ], }, + "GetClawSpace": { + "group": "spaces", + "method": "GET", + "summary": "Get ArkClaw space detail.", + "params": [ + ("SpaceId", True, "string"), + ], + }, "CreateUsers": { "group": "users", "method": "POST", "summary": "Create multiple users in an ArkClaw space.", "params": [ ("SpaceId", True, "string"), - ("Users.N.Email", False, "string[]"), - ("Users.N.ExternalProviderUserIdentifier", False, "string[]"), - ("Users.N.Name", False, "string[]"), - ("Users.N.Password", False, "string[]"), - ("Users.N.PhoneNumber", False, "string[]"), - ("Users.N.PreferredUsername", False, "string[]"), + ("Users", True, "object[]"), + ("Users.N.Email", False, "string"), + ("Users.N.ExternalProviderUserIdentifier", False, "string"), + ("Users.N.Name", False, "string"), + ("Users.N.Password", False, "string"), + ("Users.N.PhoneNumber", False, "string"), + ("Users.N.PreferredUsername", False, "string"), ], }, "DeleteUser": { @@ -212,6 +228,26 @@ class RawActionSpec(TypedDict): ("UserId", True, "string"), ], }, + "ListUsers": { + "group": "users", + "method": "GET", + "summary": "List users in an ArkClaw space with optional filters.", + "params": [ + ("Filter.DepartmentUid", False, "string"), + ("Filter.DepartmentUidRecursive", False, "boolean"), + ("Filter.Email", False, "string"), + ("Filter.EmailPhoneNameIsNullOrEmpty", False, "boolean"), + ("Filter.GroupUid", False, "string"), + ("Filter.Name", False, "string"), + ("Filter.NotInAnyDepartment", False, "boolean"), + ("Filter.NotInAnyGroup", False, "boolean"), + ("Filter.PhoneNumber", False, "string"), + ("Filter.UserIds.N", False, "string[]"), + ("MaxResults", False, "integer"), + ("NextToken", False, "string"), + ("SpaceId", True, "string"), + ], + }, "UpdateUsersModelConfig": { "group": "spaces", "method": "POST", @@ -225,18 +261,32 @@ class RawActionSpec(TypedDict): ("ModelConfig", True, "object"), ], }, + "ListUsersModelConfig": { + "group": "spaces", + "method": "GET", + "summary": "List user model configurations in an ArkClaw space.", + "params": [ + ("MaxResults", False, "integer"), + ("NextToken", False, "string"), + ("SpaceId", True, "string"), + ("UserIds.N", False, "string[]"), + ], + }, "CreateClawInstance": { "group": "instances", "method": "POST", "summary": "Create an ArkClaw instance.", "params": [ + ("ClientToken", False, "string"), ("Description", False, "string"), + ("DryRun", False, "boolean"), + ("EnableHeadless", False, "boolean"), ("InstanceName", True, "string"), ("ModelApiKey", False, "string"), ("SeatType", True, "string"), ("SpaceId", True, "string"), ("TemplateId", False, "string"), - ("UserId", True, "string"), + ("UserId", False, "string"), ], }, "UpdateClawInstanceModel": { @@ -335,6 +385,8 @@ class RawActionSpec(TypedDict): ("SpaceId", True, "string"), ("InstanceId", True, "string"), ("InstanceName", False, "string"), + ("Patch.UserId", False, "string"), + ("FieldMask.Paths.N", False, "string[]"), ("ClientToken", False, "string"), ("DryRun", False, "boolean"), ], diff --git a/arkclaw/workflows.py b/arkclaw/workflows.py index 8f5da25..8cba459 100644 --- a/arkclaw/workflows.py +++ b/arkclaw/workflows.py @@ -72,9 +72,9 @@ def provision_instance( self, *, space_id: str, - user_id: str, instance_name: str, seat_type: str, + user_id: Optional[str] = None, template_id: Optional[str] = None, wait: bool = True, timeout: float = 600.0, diff --git a/tests/test_cli.py b/tests/test_cli.py index 8e81d6d..24ea05b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -329,6 +329,9 @@ def test_instance_create_dispatches_template_id(self, mock_build) -> None: description=None, model_api_key=None, template_id="ctpl-test", + enable_headless=None, + client_token=None, + dry_run=None, ) @patch("arkclaw.cli.instances.build_client")