dockerhub-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ """Pydantic response models for the Docker Hub API client.
2
+
3
+ CONCEPT:HUB-1.0 — core wrapper.
4
+
5
+ Models are intentionally lenient (``extra="allow"``, optional fields) so they
6
+ survive Docker Hub schema evolution: known fields get typed, unknown fields
7
+ ride along. Client methods validate response data against these models when
8
+ the shape is known and fall back to raw JSON when validation fails.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class HubModel(BaseModel):
17
+ model_config = ConfigDict(extra="allow")
18
+
19
+
20
+ class RateLimit(HubModel):
21
+ """Snapshot of the ``X-RateLimit-*`` headers from the latest response."""
22
+
23
+ limit: int | None = Field(default=None, description="Request ceiling")
24
+ remaining: int | None = Field(default=None, description="Requests remaining")
25
+ reset: int | None = Field(default=None, description="Window reset (epoch s)")
26
+
27
+
28
+ class Page(HubModel):
29
+ """Standard Docker Hub page envelope (``count``/``next``/``previous``)."""
30
+
31
+ count: int | None = None
32
+ next: str | None = None
33
+ previous: str | None = None
34
+ results: list[Any] | None = None
35
+
36
+
37
+ class JwtToken(HubModel):
38
+ """``POST /v2/auth/token`` response."""
39
+
40
+ access_token: str | None = None
41
+ token: str | None = None
42
+
43
+
44
+ class LoginResult(HubModel):
45
+ """``POST /v2/users/login`` / ``POST /v2/users/2fa-login`` response."""
46
+
47
+ token: str | None = None
48
+ refresh_token: str | None = None
49
+ detail: str | None = None
50
+ login_2fa_token: str | None = None
51
+
52
+
53
+ class AccessToken(HubModel):
54
+ """A personal access token record."""
55
+
56
+ uuid: str | None = None
57
+ token_label: str | None = None
58
+ token: str | None = Field(
59
+ default=None, description="Plaintext token — only present on creation"
60
+ )
61
+ scopes: list[str] | None = None
62
+ is_active: bool | None = None
63
+ client_id: str | None = None
64
+ creator_ip: str | None = None
65
+ creator_ua: str | None = None
66
+ created_at: str | None = None
67
+ last_used: str | None = None
68
+ generated_by: str | None = None
69
+
70
+
71
+ class AccessTokenPage(Page):
72
+ active_count: int | None = None
73
+ results: list[AccessToken] | None = None # type: ignore[assignment]
74
+
75
+
76
+ class OrgAccessToken(HubModel):
77
+ """An organization access token record."""
78
+
79
+ id: str | None = None
80
+ uuid: str | None = None
81
+ label: str | None = None
82
+ description: str | None = None
83
+ token: str | None = Field(
84
+ default=None, description="Plaintext token — only present on creation"
85
+ )
86
+ scopes: list[str] | None = None
87
+ resources: list[dict] | None = None
88
+ is_active: bool | None = None
89
+ created_at: str | None = None
90
+ expires_at: str | None = None
91
+
92
+
93
+ class OrgAccessTokenPage(Page):
94
+ results: list[OrgAccessToken] | None = None # type: ignore[assignment]
95
+
96
+
97
+ class AuditLogEvent(HubModel):
98
+ """One audit-log entry."""
99
+
100
+ account: str | None = None
101
+ action: str | None = None
102
+ name: str | None = None
103
+ actor: str | None = None
104
+ actor_ip: str | None = None
105
+ data: dict | None = None
106
+ timestamp: str | None = None
107
+
108
+
109
+ class AuditLogPage(HubModel):
110
+ logs: list[AuditLogEvent] | None = None
111
+ count: int | None = None
112
+
113
+
114
+ class AuditLogActions(HubModel):
115
+ """``GET /v2/auditlogs/{account}/actions`` response."""
116
+
117
+ actions: dict | list | None = None
118
+
119
+
120
+ class RestrictedImages(HubModel):
121
+ enabled: bool | None = None
122
+ allow_official_images: bool | None = None
123
+ allow_verified_publishers: bool | None = None
124
+
125
+
126
+ class OrgSettings(HubModel):
127
+ """``GET|PUT /v2/orgs/{name}/settings`` response."""
128
+
129
+ restricted_images: RestrictedImages | None = None
130
+
131
+
132
+ class Repository(HubModel):
133
+ """A Docker Hub image repository."""
134
+
135
+ name: str | None = None
136
+ namespace: str | None = None
137
+ repository_type: str | None = None
138
+ description: str | None = None
139
+ full_description: str | None = None
140
+ registry: str | None = None
141
+ is_private: bool | None = None
142
+ status: int | str | None = None
143
+ star_count: int | None = None
144
+ pull_count: int | None = None
145
+ last_updated: str | None = None
146
+ date_registered: str | None = None
147
+ affiliation: str | None = None
148
+ media_types: list[str] | None = None
149
+
150
+
151
+ class RepositoryPage(Page):
152
+ results: list[Repository] | None = None # type: ignore[assignment]
153
+
154
+
155
+ class TagImage(HubModel):
156
+ architecture: str | None = None
157
+ os: str | None = None
158
+ digest: str | None = None
159
+ size: int | None = None
160
+ status: str | None = None
161
+ last_pushed: str | None = None
162
+ last_pulled: str | None = None
163
+
164
+
165
+ class Tag(HubModel):
166
+ """A repository tag."""
167
+
168
+ id: int | None = None
169
+ name: str | None = None
170
+ repository: int | str | None = None
171
+ full_size: int | None = None
172
+ digest: str | None = None
173
+ images: list[TagImage] | None = None
174
+ last_updated: str | None = None
175
+ last_updater_username: str | None = None
176
+ tag_status: str | None = None
177
+ tag_last_pulled: str | None = None
178
+ tag_last_pushed: str | None = None
179
+ v2: bool | str | None = None
180
+ content_type: str | None = None
181
+ media_type: str | None = None
182
+
183
+
184
+ class TagPage(Page):
185
+ results: list[Tag] | None = None # type: ignore[assignment]
186
+
187
+
188
+ class ImmutableTagsSettings(HubModel):
189
+ """``PATCH .../immutabletags`` response."""
190
+
191
+ enabled: bool | None = None
192
+ rules: list[str] | None = None
193
+
194
+
195
+ class ImmutableTagsVerification(HubModel):
196
+ """``POST .../immutabletags/verify`` response."""
197
+
198
+ enabled: bool | None = None
199
+ rules: list[str] | None = None
200
+ results: list[dict] | None = None
201
+ errors: list[dict] | None = None
202
+
203
+
204
+ class RepositoryGroup(HubModel):
205
+ """``POST /v2/repositories/{ns}/{repo}/groups`` response."""
206
+
207
+ group_id: int | str | None = None
208
+ group_name: str | None = None
209
+ permission: str | None = None
210
+
211
+
212
+ class OrgMember(HubModel):
213
+ """An organization member."""
214
+
215
+ id: str | None = None
216
+ username: str | None = None
217
+ full_name: str | None = None
218
+ email: str | None = None
219
+ role: str | None = None
220
+ type: str | None = None
221
+ groups: list[str] | None = None
222
+ is_guest: bool | None = None
223
+ last_logged_in_date: str | None = None
224
+ joined_at: str | None = None
225
+
226
+
227
+ class OrgMemberPage(Page):
228
+ results: list[OrgMember] | None = None # type: ignore[assignment]
229
+
230
+
231
+ class Invite(HubModel):
232
+ """An organization invite."""
233
+
234
+ id: str | None = None
235
+ invitee: str | None = None
236
+ inviter: str | None = None
237
+ org: str | None = None
238
+ team: str | None = None
239
+ role: str | None = None
240
+ created_at: str | None = None
241
+ status: str | None = None
242
+
243
+
244
+ class InvitePage(Page):
245
+ data: list[Invite] | None = None
246
+ results: list[Invite] | None = None # type: ignore[assignment]
247
+
248
+
249
+ class BulkInviteResult(HubModel):
250
+ """``POST /v2/invites/bulk`` response."""
251
+
252
+ org: str | None = None
253
+ team: str | None = None
254
+ role: str | None = None
255
+ dry_run: bool | None = None
256
+ invitees: list[dict] | list[str] | None = None
257
+ valid: list[Any] | None = None
258
+ invalid: list[Any] | None = None
259
+
260
+
261
+ class Group(HubModel):
262
+ """An organization group (team)."""
263
+
264
+ id: int | str | None = None
265
+ name: str | None = None
266
+ description: str | None = None
267
+ member_count: int | None = None
268
+
269
+
270
+ class GroupPage(Page):
271
+ results: list[Group] | None = None # type: ignore[assignment]
272
+
273
+
274
+ class ScimName(HubModel):
275
+ givenName: str | None = None
276
+ familyName: str | None = None
277
+
278
+
279
+ class ScimEmail(HubModel):
280
+ value: str | None = None
281
+ primary: bool | None = None
282
+
283
+
284
+ class ScimUser(HubModel):
285
+ """A SCIM 2.0 User resource."""
286
+
287
+ schemas: list[str] | None = None
288
+ id: str | None = None
289
+ userName: str | None = None
290
+ name: ScimName | None = None
291
+ emails: list[ScimEmail] | None = None
292
+ active: bool | None = None
293
+ groups: list[dict] | None = None
294
+ meta: dict | None = None
295
+
296
+
297
+ class ScimListResponse(HubModel):
298
+ """SCIM 2.0 list envelope (``ListResponse``)."""
299
+
300
+ schemas: list[str] | None = None
301
+ totalResults: int | None = None
302
+ startIndex: int | None = None
303
+ itemsPerPage: int | None = None
304
+ Resources: list[dict] | None = None
305
+
306
+
307
+ class ScimServiceProviderConfig(HubModel):
308
+ schemas: list[str] | None = None
309
+ patch: dict | None = None
310
+ bulk: dict | None = None
311
+ filter: dict | None = None
312
+ changePassword: dict | None = None
313
+ sort: dict | None = None
314
+ etag: dict | None = None
315
+ authenticationSchemes: list[dict] | None = None
316
+
317
+
318
+ class ScimResourceType(HubModel):
319
+ schemas: list[str] | None = None
320
+ id: str | None = None
321
+ name: str | None = None
322
+ endpoint: str | None = None
323
+ schema_uri: str | None = Field(default=None, alias="schema")
324
+
325
+
326
+ class ScimSchema(HubModel):
327
+ id: str | None = None
328
+ name: str | None = None
329
+ description: str | None = None
330
+ attributes: list[dict] | None = None
331
+
332
+
333
+ def validate_lenient(model: type[BaseModel], data: Any) -> Any:
334
+ """Validate ``data`` against ``model``; fall back to the raw data.
335
+
336
+ Keeps client results JSON-serializable while still exercising the typed
337
+ models whenever the payload matches the documented shape.
338
+ """
339
+ if data is None:
340
+ return None
341
+ try:
342
+ return model.model_validate(data).model_dump(mode="json", exclude_none=True)
343
+ except Exception: # pragma: no cover - lenient by design
344
+ return data
@@ -0,0 +1,14 @@
1
+ {
2
+ "task": "main-agent",
3
+ "input": "# main-agent\n\nYou are the Docker Hub operations agent for this workspace. Your goal is to help the user manage Docker Hub repositories, tags, access tokens, organizations, teams, audit logs, and SCIM provisioning.\n\n### Core Principles\n* Be concise and efficient.\n* Prefer read-only inspection before any write.\n* Destructive operations (deletes, org-settings writes) are gated behind DOCKERHUB_ALLOW_DESTRUCTIVE — confirm intent before enabling.\n* Verify your work before concluding.\n\nYour personality:\n* **Emoji:** 🐳\n* **Vibe:** Professional, efficient, helpful",
4
+ "type": "prompt",
5
+ "description": "The Docker Hub operations agent for this workspace.",
6
+ "tools": [
7
+ "workspace-manager",
8
+ "agent-workflows"
9
+ ],
10
+ "topic": "Docker Hub Administration",
11
+ "tone": "technical and precise",
12
+ "style": "professional assistant",
13
+ "goal": "Manage Docker Hub repositories, tokens, organizations, teams, and provisioning."
14
+ }
@@ -0,0 +1,120 @@
1
+ """MCP tool registration modules for dockerhub-api.
2
+
3
+ CONCEPT:HUB-1.4 — action-routed MCP surface. Each module registers one
4
+ consolidated, action-routed tool; this package provides the shared client
5
+ resolution, parameter parsing, secret redaction, and error-envelope helpers.
6
+ """
7
+
8
+ import json
9
+ from typing import Any
10
+
11
+ from agent_utilities.core.exceptions import (
12
+ ApiError,
13
+ AuthError,
14
+ MissingParameterError,
15
+ ParameterError,
16
+ UnauthorizedError,
17
+ )
18
+
19
+ from dockerhub_api.api.api_client_base import DestructiveOperationError
20
+
21
+ #: Keys whose values are always masked in MCP tool results.
22
+ SECRET_KEYS = {"secret", "password", "access_token", "refresh_token", "jwt"}
23
+ REDACTED = "***redacted***"
24
+
25
+
26
+ def get_hub_client():
27
+ """Resolve the Docker Hub client late so tests/deployments can rebind
28
+ ``dockerhub_api.auth.get_client``."""
29
+ from dockerhub_api.auth import get_client
30
+
31
+ return get_client()
32
+
33
+
34
+ def parse_params(params_json: str) -> dict[str, Any]:
35
+ """Parse the ``params_json`` tool argument, dropping ``None`` values."""
36
+ kwargs = json.loads(params_json) if params_json else {}
37
+ if not isinstance(kwargs, dict):
38
+ raise ValueError("params_json must decode to a JSON object")
39
+ return {k: v for k, v in kwargs.items() if v is not None}
40
+
41
+
42
+ def redact_secrets(data: Any, allow_keys: set[str] | None = None) -> Any:
43
+ """Recursively mask secret-bearing fields in a tool result.
44
+
45
+ ``allow_keys`` whitelists keys that must stay visible for this one call
46
+ (e.g. the plaintext ``token`` returned exactly once on PAT creation).
47
+ """
48
+ allow = allow_keys or set()
49
+ if isinstance(data, dict):
50
+ return {
51
+ key: (
52
+ REDACTED
53
+ if key in SECRET_KEYS and key not in allow and value
54
+ else redact_secrets(value, allow)
55
+ )
56
+ for key, value in data.items()
57
+ }
58
+ if isinstance(data, list):
59
+ return [redact_secrets(item, allow) for item in data]
60
+ return data
61
+
62
+
63
+ def error_envelope(error: Exception) -> dict[str, Any]:
64
+ """Map a client exception to the MCP error envelope."""
65
+ return {"error": {"type": type(error).__name__, "message": str(error)}}
66
+
67
+
68
+ HANDLED_ERRORS = (
69
+ ApiError,
70
+ AuthError,
71
+ UnauthorizedError,
72
+ ParameterError,
73
+ MissingParameterError,
74
+ DestructiveOperationError,
75
+ ValueError,
76
+ )
77
+
78
+
79
+ def run_action(handlers: dict, action: str, kwargs: dict[str, Any]) -> Any:
80
+ """Dispatch ``action`` to its handler, mapping errors to the envelope."""
81
+ handler = handlers.get(action)
82
+ if handler is None:
83
+ return {
84
+ "error": {
85
+ "type": "UnknownAction",
86
+ "message": f"Unknown action: {action!r}. "
87
+ f"Valid actions: {sorted(handlers)}",
88
+ }
89
+ }
90
+ try:
91
+ return handler(**kwargs)
92
+ except HANDLED_ERRORS as error:
93
+ return error_envelope(error)
94
+
95
+
96
+ from dockerhub_api.mcp.mcp_admin import register_admin_tools
97
+ from dockerhub_api.mcp.mcp_audit import register_audit_tools
98
+ from dockerhub_api.mcp.mcp_auth import register_auth_tools
99
+ from dockerhub_api.mcp.mcp_org import register_org_tools
100
+ from dockerhub_api.mcp.mcp_repos import register_repos_tools
101
+ from dockerhub_api.mcp.mcp_scim import register_scim_tools
102
+ from dockerhub_api.mcp.mcp_teams import register_teams_tools
103
+
104
+ __all__ = [
105
+ "HANDLED_ERRORS",
106
+ "REDACTED",
107
+ "SECRET_KEYS",
108
+ "error_envelope",
109
+ "get_hub_client",
110
+ "parse_params",
111
+ "redact_secrets",
112
+ "register_admin_tools",
113
+ "register_audit_tools",
114
+ "register_auth_tools",
115
+ "register_org_tools",
116
+ "register_repos_tools",
117
+ "register_scim_tools",
118
+ "register_teams_tools",
119
+ "run_action",
120
+ ]
@@ -0,0 +1,45 @@
1
+ """MCP tool for Docker Hub client telemetry and identity introspection.
2
+
3
+ CONCEPT:HUB-1.4 — action-routed MCP surface.
4
+ CONCEPT:HUB-1.2 — rate-limit telemetry.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from fastmcp import Context, FastMCP
10
+ from fastmcp.dependencies import Depends
11
+ from pydantic import Field
12
+
13
+ from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
14
+
15
+
16
+ def register_admin_tools(mcp: FastMCP):
17
+ @mcp.tool(tags={"admin"})
18
+ async def hub_admin(
19
+ action: str = Field(
20
+ description="Action to perform. Must be one of: 'rate_limit', 'whoami'"
21
+ ),
22
+ params_json: str = Field(
23
+ default="{}", description="JSON string of parameters to pass to the action."
24
+ ),
25
+ client=Depends(get_hub_client),
26
+ ctx: Context | None = Field(
27
+ default=None, description="MCP context for progress reporting"
28
+ ),
29
+ ) -> Any:
30
+ """Client diagnostics: 'rate_limit' returns the latest
31
+ X-RateLimit-* snapshot observed by the client; 'whoami' introspects
32
+ the active credential locally (decoded JWT claims, no network call).
33
+ """
34
+ if ctx:
35
+ await ctx.info("Executing tool...")
36
+ try:
37
+ kwargs = parse_params(params_json)
38
+ except Exception as e:
39
+ return {"error": f"Invalid params_json: {e}"}
40
+
41
+ handlers = {
42
+ "rate_limit": client.get_rate_limit,
43
+ "whoami": client.whoami,
44
+ }
45
+ return redact_secrets(run_action(handlers, action, kwargs))
@@ -0,0 +1,44 @@
1
+ """MCP tool for Docker Hub audit logs.
2
+
3
+ CONCEPT:HUB-1.4 — action-routed MCP surface.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from fastmcp import Context, FastMCP
9
+ from fastmcp.dependencies import Depends
10
+ from pydantic import Field
11
+
12
+ from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
13
+
14
+
15
+ def register_audit_tools(mcp: FastMCP):
16
+ @mcp.tool(tags={"audit"})
17
+ async def hub_audit(
18
+ action: str = Field(
19
+ description="Action to perform. Must be one of: 'logs', 'actions'"
20
+ ),
21
+ params_json: str = Field(
22
+ default="{}", description="JSON string of parameters to pass to the action."
23
+ ),
24
+ client=Depends(get_hub_client),
25
+ ctx: Context | None = Field(
26
+ default=None, description="MCP context for progress reporting"
27
+ ),
28
+ ) -> Any:
29
+ """Read a Docker Hub account's audit trail: 'logs' lists events
30
+ (filters: action, name, actor, from_date, to_date, page, page_size),
31
+ 'actions' lists the available action names.
32
+ """
33
+ if ctx:
34
+ await ctx.info("Executing tool...")
35
+ try:
36
+ kwargs = parse_params(params_json)
37
+ except Exception as e:
38
+ return {"error": f"Invalid params_json: {e}"}
39
+
40
+ handlers = {
41
+ "logs": client.get_audit_logs,
42
+ "actions": client.get_audit_log_actions,
43
+ }
44
+ return redact_secrets(run_action(handlers, action, kwargs))
@@ -0,0 +1,66 @@
1
+ """MCP tool for Docker Hub auth and token lifecycle.
2
+
3
+ CONCEPT:HUB-1.4 — action-routed MCP surface.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from fastmcp import Context, FastMCP
9
+ from fastmcp.dependencies import Depends
10
+ from pydantic import Field
11
+
12
+ from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
13
+
14
+
15
+ def register_auth_tools(mcp: FastMCP):
16
+ @mcp.tool(tags={"auth"})
17
+ async def hub_auth(
18
+ action: str = Field(
19
+ description=(
20
+ "Action to perform. Must be one of: 'create_token', 'login' "
21
+ "(deprecated), 'two_factor_login', 'list_pats', 'create_pat', "
22
+ "'get_pat', 'update_pat', 'delete_pat', 'list_oats', "
23
+ "'create_oat', 'get_oat', 'update_oat', 'delete_oat'"
24
+ )
25
+ ),
26
+ params_json: str = Field(
27
+ default="{}", description="JSON string of parameters to pass to the action."
28
+ ),
29
+ client=Depends(get_hub_client),
30
+ ctx: Context | None = Field(
31
+ default=None, description="MCP context for progress reporting"
32
+ ),
33
+ ) -> Any:
34
+ """Manage Docker Hub authentication, personal access tokens (PATs),
35
+ and organization access tokens (OATs).
36
+
37
+ Bearer JWTs are minted/cached client-side and redacted from results.
38
+ Plaintext PAT/OAT values appear exactly once, on creation. Token
39
+ deletion requires DOCKERHUB_ALLOW_DESTRUCTIVE=True.
40
+ """
41
+ if ctx:
42
+ await ctx.info("Executing tool...")
43
+ try:
44
+ kwargs = parse_params(params_json)
45
+ except Exception as e:
46
+ return {"error": f"Invalid params_json: {e}"}
47
+
48
+ handlers = {
49
+ "create_token": client.create_auth_token,
50
+ "login": client.login,
51
+ "two_factor_login": client.two_factor_login,
52
+ "list_pats": client.get_access_tokens,
53
+ "create_pat": client.create_access_token,
54
+ "get_pat": client.get_access_token,
55
+ "update_pat": client.update_access_token,
56
+ "delete_pat": client.delete_access_token,
57
+ "list_oats": client.get_org_access_tokens,
58
+ "create_oat": client.create_org_access_token,
59
+ "get_oat": client.get_org_access_token,
60
+ "update_oat": client.update_org_access_token,
61
+ "delete_oat": client.delete_org_access_token,
62
+ }
63
+ result = run_action(handlers, action, kwargs)
64
+ # Plaintext tokens are shown exactly once — on creation.
65
+ allow_keys = {"token"} if action in ("create_pat", "create_oat") else set()
66
+ return redact_secrets(result, allow_keys=allow_keys)
@@ -0,0 +1,58 @@
1
+ """MCP tool for Docker Hub organization members, settings, and invites.
2
+
3
+ CONCEPT:HUB-1.4 — action-routed MCP surface.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from fastmcp import Context, FastMCP
9
+ from fastmcp.dependencies import Depends
10
+ from pydantic import Field
11
+
12
+ from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
13
+
14
+
15
+ def register_org_tools(mcp: FastMCP):
16
+ @mcp.tool(tags={"org"})
17
+ async def hub_org(
18
+ action: str = Field(
19
+ description=(
20
+ "Action to perform. Must be one of: 'get_settings', "
21
+ "'update_settings', 'list_members', 'export_members', "
22
+ "'update_member', 'remove_member', 'list_invites', "
23
+ "'delete_invite', 'resend_invite', 'bulk_invite'"
24
+ )
25
+ ),
26
+ params_json: str = Field(
27
+ default="{}", description="JSON string of parameters to pass to the action."
28
+ ),
29
+ client=Depends(get_hub_client),
30
+ ctx: Context | None = Field(
31
+ default=None, description="MCP context for progress reporting"
32
+ ),
33
+ ) -> Any:
34
+ """Manage a Docker Hub organization: settings (restricted images),
35
+ member roster and roles, CSV export, and invites (single, resend,
36
+ bulk with dry_run). Member removal, invite deletion, and settings
37
+ writes require DOCKERHUB_ALLOW_DESTRUCTIVE=True.
38
+ """
39
+ if ctx:
40
+ await ctx.info("Executing tool...")
41
+ try:
42
+ kwargs = parse_params(params_json)
43
+ except Exception as e:
44
+ return {"error": f"Invalid params_json: {e}"}
45
+
46
+ handlers = {
47
+ "get_settings": client.get_org_settings,
48
+ "update_settings": client.update_org_settings,
49
+ "list_members": client.get_org_members,
50
+ "export_members": client.export_org_members,
51
+ "update_member": client.update_org_member,
52
+ "remove_member": client.remove_org_member,
53
+ "list_invites": client.get_org_invites,
54
+ "delete_invite": client.delete_invite,
55
+ "resend_invite": client.resend_invite,
56
+ "bulk_invite": client.bulk_invite,
57
+ }
58
+ return redact_secrets(run_action(handlers, action, kwargs))