dockerhub-api 1.3.10__tar.gz → 1.4.0__tar.gz
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.
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/PKG-INFO +4 -10
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/README.md +2 -8
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_access_tokens.py +3 -1
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_base.py +1 -20
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_groups.py +4 -2
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_org_access_tokens.py +3 -1
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_orgs.py +5 -3
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_registry.py +11 -9
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_registry_base.py +0 -2
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/auth.py +4 -17
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/mcp/__init__.py +0 -3
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/certification.json +3 -3
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/dockerhub.ttl +4 -1
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/shapes/connector.shacl.ttl +2 -2
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/PKG-INFO +4 -10
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/SOURCES.txt +1 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/requires.txt +1 -1
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/pyproject.toml +2 -2
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_org.py +6 -16
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_registry.py +11 -24
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_teams.py +4 -9
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_tokens.py +4 -12
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_wrapper.py +2 -8
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_auth.py +0 -6
- dockerhub_api-1.4.0/tests/test_destructive_declaration.py +98 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/LICENSE +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/__init__.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/__main__.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/__init__.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_audit_logs.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_auth.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_repositories.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_scim.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_scout.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api_client.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/connector_contracts.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/connector_sources.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/dockerhub_input_models.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/dockerhub_response_models.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/kg_ingest.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/mcp/mcp_admin.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/mcp/mcp_kg.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/mcp_config.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/mcp_server.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/__init__.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/fixtures/records.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/mappings/source.yaml +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/migrations/manifest.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/prompts/dockerhub-mcp-operator.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/prompts/dockerhub-mcp-organization-administrator.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/prompts/dockerhub-mcp-release-engineer.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/prompts/dockerhub-mcp-security-analyst.json +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-api-development/SKILL.md +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-api-operations/SKILL.md +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-image-registry-scout/SKILL.md +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-org-administration/SKILL.md +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-repository-management/SKILL.md +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/dependency_links.txt +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/entry_points.txt +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api.egg-info/top_level.txt +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/setup.cfg +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_audit.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_repositories.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_scim.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_api_scout.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_dockerhub_a2a_validation.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_dockerhub_mcp_validation.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_fleet_name.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_identity_namespace_scoping.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_init_dynamics.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_kg_ingest.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_models.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_rate_limit.py +0 -0
- {dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/tests/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dockerhub-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Docker Hub API + MCP Server
|
|
5
5
|
Author-email: Repository Maintainers <maintainers@knuckles.team>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: <3.15,>=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: scion-sdk[mcp]
|
|
15
|
+
Requires-Dist: scion-sdk[mcp]<0.11,>=0.10.2
|
|
16
16
|
Requires-Dist: httpx>=0.28.1
|
|
17
17
|
Requires-Dist: python-dotenv>=1.2.4
|
|
18
18
|
Provides-Extra: test
|
|
@@ -84,8 +84,7 @@ multi-arch inspection, OCI referrers, and gated push/remove) and **Docker Scout*
|
|
|
84
84
|
expiry, with one transparent re-mint on 401.
|
|
85
85
|
- **Rate-Limit Telemetry:** `X-RateLimit-*` headers surfaced in every result;
|
|
86
86
|
HTTP 429 retried with bounded `Retry-After` backoff.
|
|
87
|
-
- **Safety by Default:** Removes and org-settings writes are
|
|
88
|
-
`DOCKERHUB_ALLOW_DESTRUCTIVE` (default `False`); secrets are redacted from tool
|
|
87
|
+
- **Safety by Default:** Removes and org-settings writes are declared destructive; the SDK asks the caller to confirm (`confirm=true`) before any request. Secrets are redacted from tool
|
|
89
88
|
results (plaintext tokens appear exactly once — on creation). Repository creation
|
|
90
89
|
stays enabled: it is the primary release-provisioning use case.
|
|
91
90
|
- **Integrated Graph Agent:** Built-in Pydantic AI agent (`dockerhub-agent`) with
|
|
@@ -279,7 +278,6 @@ _9 action-routed tool(s) (default) · 54 verbose 1:1 tool(s). Each is enabled un
|
|
|
279
278
|
"ADMINTOOL": "True",
|
|
280
279
|
"AUDITTOOL": "True",
|
|
281
280
|
"AUTHTOOL": "True",
|
|
282
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
283
281
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
284
282
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
285
283
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -328,7 +326,6 @@ own runtime secret boundary.
|
|
|
328
326
|
"ADMINTOOL": "True",
|
|
329
327
|
"AUDITTOOL": "True",
|
|
330
328
|
"AUTHTOOL": "True",
|
|
331
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
332
329
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
333
330
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
334
331
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -376,7 +373,6 @@ docker run -i --rm \
|
|
|
376
373
|
-e ADMINTOOL=True \
|
|
377
374
|
-e AUDITTOOL=True \
|
|
378
375
|
-e AUTHTOOL=True \
|
|
379
|
-
-e DOCKERHUB_ALLOW_DESTRUCTIVE=False \
|
|
380
376
|
-e DOCKERHUB_TLS_PROFILE=system \
|
|
381
377
|
-e DOCKERHUB_TOKEN=dckr_pat_your_personal_access_token \
|
|
382
378
|
-e DOCKERHUB_URL=https://hub.docker.com \
|
|
@@ -423,8 +419,7 @@ the detailed transport contract.
|
|
|
423
419
|
|---|---|---|
|
|
424
420
|
| Reads (repos, tags, members, logs, SCIM) | allowed | — |
|
|
425
421
|
| Repository create / immutable-tag config / invites / role updates | allowed | — |
|
|
426
|
-
| Removes (PATs, OATs, groups, members, invites) |
|
|
427
|
-
| Org-settings writes (`PUT /v2/orgs/{org}/settings`) | **blocked** | `DOCKERHUB_ALLOW_DESTRUCTIVE=True` |
|
|
422
|
+
| Removes (PATs, OATs, groups, members, invites) and org-settings writes | confirmation required | `confirm=true` per call, or operator setting `SCION_ALLOW_DESTRUCTIVE` |
|
|
428
423
|
|
|
429
424
|
---
|
|
430
425
|
|
|
@@ -488,7 +483,6 @@ network transport, connector ingestion, Arbora delegation, or trace export.
|
|
|
488
483
|
| `DOCKER_REGISTRY_URL` | `https://registry-1.docker.io` | |
|
|
489
484
|
| `DOCKER_REGISTRY_AUTH_URL` | `https://auth.docker.io/token` | token-service realm; auto-discovered from the 401 challenge |
|
|
490
485
|
| `DOCKER_SCOUT_URL` | `https://api.scout.docker.com` | |
|
|
491
|
-
| `DOCKERHUB_ALLOW_DESTRUCTIVE` | `False` | |
|
|
492
486
|
| `AUTHTOOL` | `True` | |
|
|
493
487
|
| `REPOSTOOL` | `True` | |
|
|
494
488
|
| `ORGTOOL` | `True` | |
|
|
@@ -60,8 +60,7 @@ multi-arch inspection, OCI referrers, and gated push/remove) and **Docker Scout*
|
|
|
60
60
|
expiry, with one transparent re-mint on 401.
|
|
61
61
|
- **Rate-Limit Telemetry:** `X-RateLimit-*` headers surfaced in every result;
|
|
62
62
|
HTTP 429 retried with bounded `Retry-After` backoff.
|
|
63
|
-
- **Safety by Default:** Removes and org-settings writes are
|
|
64
|
-
`DOCKERHUB_ALLOW_DESTRUCTIVE` (default `False`); secrets are redacted from tool
|
|
63
|
+
- **Safety by Default:** Removes and org-settings writes are declared destructive; the SDK asks the caller to confirm (`confirm=true`) before any request. Secrets are redacted from tool
|
|
65
64
|
results (plaintext tokens appear exactly once — on creation). Repository creation
|
|
66
65
|
stays enabled: it is the primary release-provisioning use case.
|
|
67
66
|
- **Integrated Graph Agent:** Built-in Pydantic AI agent (`dockerhub-agent`) with
|
|
@@ -255,7 +254,6 @@ _9 action-routed tool(s) (default) · 54 verbose 1:1 tool(s). Each is enabled un
|
|
|
255
254
|
"ADMINTOOL": "True",
|
|
256
255
|
"AUDITTOOL": "True",
|
|
257
256
|
"AUTHTOOL": "True",
|
|
258
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
259
257
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
260
258
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
261
259
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -304,7 +302,6 @@ own runtime secret boundary.
|
|
|
304
302
|
"ADMINTOOL": "True",
|
|
305
303
|
"AUDITTOOL": "True",
|
|
306
304
|
"AUTHTOOL": "True",
|
|
307
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
308
305
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
309
306
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
310
307
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -352,7 +349,6 @@ docker run -i --rm \
|
|
|
352
349
|
-e ADMINTOOL=True \
|
|
353
350
|
-e AUDITTOOL=True \
|
|
354
351
|
-e AUTHTOOL=True \
|
|
355
|
-
-e DOCKERHUB_ALLOW_DESTRUCTIVE=False \
|
|
356
352
|
-e DOCKERHUB_TLS_PROFILE=system \
|
|
357
353
|
-e DOCKERHUB_TOKEN=dckr_pat_your_personal_access_token \
|
|
358
354
|
-e DOCKERHUB_URL=https://hub.docker.com \
|
|
@@ -399,8 +395,7 @@ the detailed transport contract.
|
|
|
399
395
|
|---|---|---|
|
|
400
396
|
| Reads (repos, tags, members, logs, SCIM) | allowed | — |
|
|
401
397
|
| Repository create / immutable-tag config / invites / role updates | allowed | — |
|
|
402
|
-
| Removes (PATs, OATs, groups, members, invites) |
|
|
403
|
-
| Org-settings writes (`PUT /v2/orgs/{org}/settings`) | **blocked** | `DOCKERHUB_ALLOW_DESTRUCTIVE=True` |
|
|
398
|
+
| Removes (PATs, OATs, groups, members, invites) and org-settings writes | confirmation required | `confirm=true` per call, or operator setting `SCION_ALLOW_DESTRUCTIVE` |
|
|
404
399
|
|
|
405
400
|
---
|
|
406
401
|
|
|
@@ -464,7 +459,6 @@ network transport, connector ingestion, Arbora delegation, or trace export.
|
|
|
464
459
|
| `DOCKER_REGISTRY_URL` | `https://registry-1.docker.io` | |
|
|
465
460
|
| `DOCKER_REGISTRY_AUTH_URL` | `https://auth.docker.io/token` | token-service realm; auto-discovered from the 401 challenge |
|
|
466
461
|
| `DOCKER_SCOUT_URL` | `https://api.scout.docker.com` | |
|
|
467
|
-
| `DOCKERHUB_ALLOW_DESTRUCTIVE` | `False` | |
|
|
468
462
|
| `AUTHTOOL` | `True` | |
|
|
469
463
|
| `REPOSTOOL` | `True` | |
|
|
470
464
|
| `ORGTOOL` | `True` | |
|
|
@@ -5,6 +5,8 @@ CONCEPT:DH-OS.identity.jwt-auth-lifecycle-endpoint — JWT auth lifecycle (PAT m
|
|
|
5
5
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from scion_sdk.mcp.destructive import destructive_operation
|
|
9
|
+
|
|
8
10
|
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
9
11
|
from dockerhub_api.dockerhub_input_models import (
|
|
10
12
|
AccessTokenCreateModel,
|
|
@@ -70,8 +72,8 @@ class DockerHubApiAccessTokens(DockerHubApiBase):
|
|
|
70
72
|
envelope["data"] = validate_lenient(AccessToken, envelope["data"])
|
|
71
73
|
return envelope
|
|
72
74
|
|
|
75
|
+
@destructive_operation
|
|
73
76
|
def delete_access_token(self, uuid: str) -> dict[str, Any]:
|
|
74
77
|
"""Delete a personal access token. Destructive — gated."""
|
|
75
|
-
self._guard_destructive("delete_access_token")
|
|
76
78
|
model = AccessTokenModel(uuid=uuid)
|
|
77
79
|
return self._request("DELETE", f"/v2/access-tokens/{model.uuid}")
|
|
@@ -10,8 +10,7 @@ headers are captured into the result envelope (and kept on the client as
|
|
|
10
10
|
``Retry-After`` backoff.
|
|
11
11
|
|
|
12
12
|
CONCEPT:DH-OS.identity.destructive-action-gating-member — destructive-action gating. Deletes and org-settings writes
|
|
13
|
-
|
|
14
|
-
``allow_destructive=True``.
|
|
13
|
+
are declared with the SDK ``destructive_operation`` decorator; the SDK asks the caller to confirm.
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
import logging
|
|
@@ -40,10 +39,6 @@ DEFAULT_MAX_RETRIES = 3
|
|
|
40
39
|
DEFAULT_TIMEOUT = 30.0
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class DestructiveOperationError(PermissionError):
|
|
44
|
-
"""Raised when a destructive operation is attempted while gated off."""
|
|
45
|
-
|
|
46
|
-
|
|
47
42
|
class DockerHubApiBase:
|
|
48
43
|
"""Shared transport, auth header, retry, and envelope logic."""
|
|
49
44
|
|
|
@@ -58,7 +53,6 @@ class DockerHubApiBase:
|
|
|
58
53
|
timeout: float = DEFAULT_TIMEOUT,
|
|
59
54
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
60
55
|
retry_after_cap: float = DEFAULT_RETRY_AFTER_CAP,
|
|
61
|
-
allow_destructive: bool = False,
|
|
62
56
|
debug: bool = False,
|
|
63
57
|
transport: httpx.BaseTransport | None = None,
|
|
64
58
|
):
|
|
@@ -75,7 +69,6 @@ class DockerHubApiBase:
|
|
|
75
69
|
self.timeout = timeout
|
|
76
70
|
self.max_retries = max_retries
|
|
77
71
|
self.retry_after_cap = retry_after_cap
|
|
78
|
-
self.allow_destructive = allow_destructive
|
|
79
72
|
self.debug = debug
|
|
80
73
|
#: Last rate-limit snapshot seen on any response.
|
|
81
74
|
self.rate_limit: dict[str, Any] = {}
|
|
@@ -188,18 +181,6 @@ class DockerHubApiBase:
|
|
|
188
181
|
"rate_limit": dict(self.rate_limit) if self.rate_limit else {},
|
|
189
182
|
}
|
|
190
183
|
|
|
191
|
-
# ------------------------------------------------------------------ #
|
|
192
|
-
# Destructive gating
|
|
193
|
-
# ------------------------------------------------------------------ #
|
|
194
|
-
|
|
195
|
-
def _guard_destructive(self, operation: str) -> None:
|
|
196
|
-
if not self.allow_destructive:
|
|
197
|
-
raise DestructiveOperationError(
|
|
198
|
-
f"Destructive operation '{operation}' is disabled. "
|
|
199
|
-
"Build the client with allow_destructive=True or set "
|
|
200
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE=True to enable it."
|
|
201
|
-
)
|
|
202
|
-
|
|
203
184
|
# ------------------------------------------------------------------ #
|
|
204
185
|
# Request engine
|
|
205
186
|
# ------------------------------------------------------------------ #
|
|
@@ -6,6 +6,8 @@ CONCEPT:DH-OS.identity.destructive-action-gating-member — destructive-action g
|
|
|
6
6
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from scion_sdk.mcp.destructive import destructive_operation
|
|
10
|
+
|
|
9
11
|
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
10
12
|
from dockerhub_api.dockerhub_input_models import (
|
|
11
13
|
GroupCreateModel,
|
|
@@ -99,9 +101,9 @@ class DockerHubApiGroups(DockerHubApiBase):
|
|
|
99
101
|
envelope["data"] = validate_lenient(Group, envelope["data"])
|
|
100
102
|
return envelope
|
|
101
103
|
|
|
104
|
+
@destructive_operation
|
|
102
105
|
def delete_group(self, org: str, group_name: str) -> dict[str, Any]:
|
|
103
106
|
"""Delete a group. Destructive — gated."""
|
|
104
|
-
self._guard_destructive("delete_group")
|
|
105
107
|
model = GroupModel(org=org, group_name=group_name)
|
|
106
108
|
return self._request(
|
|
107
109
|
"DELETE", f"/v2/orgs/{model.org}/groups/{model.group_name}"
|
|
@@ -146,11 +148,11 @@ class DockerHubApiGroups(DockerHubApiBase):
|
|
|
146
148
|
envelope["data"] = validate_lenient(OrgMember, envelope["data"])
|
|
147
149
|
return envelope
|
|
148
150
|
|
|
151
|
+
@destructive_operation
|
|
149
152
|
def remove_group_member(
|
|
150
153
|
self, org: str, group_name: str, username: str
|
|
151
154
|
) -> dict[str, Any]:
|
|
152
155
|
"""Remove a username from a group. Destructive — gated."""
|
|
153
|
-
self._guard_destructive("remove_group_member")
|
|
154
156
|
model = GroupMemberModel(org=org, group_name=group_name, username=username)
|
|
155
157
|
return self._request(
|
|
156
158
|
"DELETE",
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/api/api_client_org_access_tokens.py
RENAMED
|
@@ -5,6 +5,8 @@ CONCEPT:DH-OS.identity.jwt-auth-lifecycle-endpoint — JWT auth lifecycle (OAT m
|
|
|
5
5
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from scion_sdk.mcp.destructive import destructive_operation
|
|
9
|
+
|
|
8
10
|
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
9
11
|
from dockerhub_api.dockerhub_input_models import (
|
|
10
12
|
OrgAccessTokenCreateModel,
|
|
@@ -97,9 +99,9 @@ class DockerHubApiOrgAccessTokens(DockerHubApiBase):
|
|
|
97
99
|
envelope["data"] = validate_lenient(OrgAccessToken, envelope["data"])
|
|
98
100
|
return envelope
|
|
99
101
|
|
|
102
|
+
@destructive_operation
|
|
100
103
|
def delete_org_access_token(self, org: str, token_id: str) -> dict[str, Any]:
|
|
101
104
|
"""Delete an organization access token. Destructive — gated."""
|
|
102
|
-
self._guard_destructive("delete_org_access_token")
|
|
103
105
|
model = OrgAccessTokenModel(org=org, token_id=token_id)
|
|
104
106
|
return self._request(
|
|
105
107
|
"DELETE", f"/v2/orgs/{model.org}/access-tokens/{model.token_id}"
|
|
@@ -7,6 +7,8 @@ org-settings writes).
|
|
|
7
7
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
from scion_sdk.mcp.destructive import destructive_operation
|
|
11
|
+
|
|
10
12
|
from dockerhub_api.api.api_client_base import DockerHubApiBase
|
|
11
13
|
from dockerhub_api.dockerhub_input_models import (
|
|
12
14
|
BulkInviteModel,
|
|
@@ -37,6 +39,7 @@ class DockerHubApiOrgs(DockerHubApiBase):
|
|
|
37
39
|
envelope["data"] = validate_lenient(OrgSettings, envelope["data"])
|
|
38
40
|
return envelope
|
|
39
41
|
|
|
42
|
+
@destructive_operation
|
|
40
43
|
def update_org_settings(
|
|
41
44
|
self,
|
|
42
45
|
org: str,
|
|
@@ -50,7 +53,6 @@ class DockerHubApiOrgs(DockerHubApiBase):
|
|
|
50
53
|
org-approved images, with carve-outs for Docker Official Images and
|
|
51
54
|
Verified Publisher images.
|
|
52
55
|
"""
|
|
53
|
-
self._guard_destructive("update_org_settings")
|
|
54
56
|
model = OrgSettingsModel(
|
|
55
57
|
org=org,
|
|
56
58
|
restricted_images_enabled=restricted_images_enabled,
|
|
@@ -104,9 +106,9 @@ class DockerHubApiOrgs(DockerHubApiBase):
|
|
|
104
106
|
envelope["data"] = validate_lenient(OrgMember, envelope["data"])
|
|
105
107
|
return envelope
|
|
106
108
|
|
|
109
|
+
@destructive_operation
|
|
107
110
|
def remove_org_member(self, org: str, username: str) -> dict[str, Any]:
|
|
108
111
|
"""Remove a member from the organization. Destructive — gated."""
|
|
109
|
-
self._guard_destructive("remove_org_member")
|
|
110
112
|
model = OrgMemberModel(org=org, username=username)
|
|
111
113
|
return self._request("DELETE", f"/v2/orgs/{model.org}/members/{model.username}")
|
|
112
114
|
|
|
@@ -118,9 +120,9 @@ class DockerHubApiOrgs(DockerHubApiBase):
|
|
|
118
120
|
envelope["data"] = validate_lenient(InvitePage, envelope["data"])
|
|
119
121
|
return envelope
|
|
120
122
|
|
|
123
|
+
@destructive_operation
|
|
121
124
|
def delete_invite(self, invite_id: str) -> dict[str, Any]:
|
|
122
125
|
"""Cancel an invite. Destructive — gated."""
|
|
123
|
-
self._guard_destructive("delete_invite")
|
|
124
126
|
model = InviteModel(invite_id=invite_id)
|
|
125
127
|
return self._request("DELETE", f"/v2/invites/{model.invite_id}")
|
|
126
128
|
|
|
@@ -6,8 +6,8 @@ multi-arch platform resolution, digest resolution, and (gated) deletes.
|
|
|
6
6
|
CONCEPT:DH-OS.governance.oci-referrers-attestation-discovery — OCI Referrers / attestation discovery.
|
|
7
7
|
CONCEPT:DH-OS.governance.chunked-blob-push-upload — chunked blob push (upload session + manifest put).
|
|
8
8
|
|
|
9
|
-
Pushes, deletes, and blob uploads are destructive
|
|
10
|
-
|
|
9
|
+
Pushes, deletes, and blob uploads are destructive: the SDK
|
|
10
|
+
asks the caller to confirm each. ``_catalog`` is
|
|
11
11
|
intentionally omitted: Docker Hub does not support registry-wide catalog
|
|
12
12
|
listing (it requires a registry-scoped token the Hub token service won't issue).
|
|
13
13
|
"""
|
|
@@ -15,6 +15,8 @@ listing (it requires a registry-scoped token the Hub token service won't issue).
|
|
|
15
15
|
import json as _json
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from scion_sdk.mcp.destructive import destructive_operation
|
|
19
|
+
|
|
18
20
|
from dockerhub_api.api.api_client_registry_base import (
|
|
19
21
|
ACCEPT_MANIFESTS,
|
|
20
22
|
OCTET_STREAM,
|
|
@@ -96,6 +98,7 @@ class RegistryApi(RegistryApiBase):
|
|
|
96
98
|
}
|
|
97
99
|
return envelope
|
|
98
100
|
|
|
101
|
+
@destructive_operation
|
|
99
102
|
def put_manifest(
|
|
100
103
|
self,
|
|
101
104
|
repo: str,
|
|
@@ -104,7 +107,6 @@ class RegistryApi(RegistryApiBase):
|
|
|
104
107
|
media_type: str,
|
|
105
108
|
) -> dict[str, Any]:
|
|
106
109
|
"""``PUT /v2/{repo}/manifests/{reference}`` — push a manifest (gated)."""
|
|
107
|
-
self._guard_destructive("put_manifest")
|
|
108
110
|
body = manifest if isinstance(manifest, str) else _json.dumps(manifest)
|
|
109
111
|
return self._registry_request(
|
|
110
112
|
"PUT",
|
|
@@ -115,13 +117,13 @@ class RegistryApi(RegistryApiBase):
|
|
|
115
117
|
content_type=media_type,
|
|
116
118
|
)
|
|
117
119
|
|
|
120
|
+
@destructive_operation
|
|
118
121
|
def delete_manifest(self, repo: str, reference: str) -> dict[str, Any]:
|
|
119
122
|
"""``DELETE /v2/{repo}/manifests/{digest}`` — delete a tag/manifest (gated).
|
|
120
123
|
|
|
121
124
|
Docker Hub requires deletion by digest; pass a tag and it is resolved
|
|
122
125
|
to its digest first.
|
|
123
126
|
"""
|
|
124
|
-
self._guard_destructive("delete_manifest")
|
|
125
127
|
target = reference
|
|
126
128
|
if not reference.startswith("sha256:"):
|
|
127
129
|
resolved = self.resolve_digest(repo, reference)
|
|
@@ -140,13 +142,13 @@ class RegistryApi(RegistryApiBase):
|
|
|
140
142
|
"""``HEAD /v2/{repo}/blobs/{digest}`` — blob existence check."""
|
|
141
143
|
return self._registry_exists(repo, f"/blobs/{digest}")
|
|
142
144
|
|
|
145
|
+
@destructive_operation
|
|
143
146
|
def delete_blob(self, repo: str, digest: str) -> dict[str, Any]:
|
|
144
147
|
"""``DELETE /v2/{repo}/blobs/{digest}`` — delete a blob (gated).
|
|
145
148
|
|
|
146
149
|
Often returns ``405`` on Docker Hub (blob delete disabled); the status
|
|
147
150
|
is surfaced cleanly rather than masked.
|
|
148
151
|
"""
|
|
149
|
-
self._guard_destructive("delete_blob")
|
|
150
152
|
return self._registry_request(
|
|
151
153
|
"DELETE",
|
|
152
154
|
repo,
|
|
@@ -293,13 +295,14 @@ class RegistryApi(RegistryApiBase):
|
|
|
293
295
|
|
|
294
296
|
# ------------------------------- push -------------------------------- #
|
|
295
297
|
|
|
298
|
+
@destructive_operation
|
|
296
299
|
def start_upload(self, repo: str) -> dict[str, Any]:
|
|
297
300
|
"""``POST /v2/{repo}/blobs/uploads/`` — open a blob upload session (gated)."""
|
|
298
|
-
self._guard_destructive("start_upload")
|
|
299
301
|
return self._registry_request(
|
|
300
302
|
"POST", repo, "/blobs/uploads/", scope_actions="pull,push"
|
|
301
303
|
)
|
|
302
304
|
|
|
305
|
+
@destructive_operation
|
|
303
306
|
def upload_chunk(
|
|
304
307
|
self,
|
|
305
308
|
repo: str,
|
|
@@ -308,7 +311,6 @@ class RegistryApi(RegistryApiBase):
|
|
|
308
311
|
content_range: str | None = None,
|
|
309
312
|
) -> dict[str, Any]:
|
|
310
313
|
"""``PATCH {location}`` — upload one blob chunk (gated)."""
|
|
311
|
-
self._guard_destructive("upload_chunk")
|
|
312
314
|
extra = {"Content-Range": content_range} if content_range else None
|
|
313
315
|
return self._registry_request(
|
|
314
316
|
"PATCH",
|
|
@@ -320,6 +322,7 @@ class RegistryApi(RegistryApiBase):
|
|
|
320
322
|
extra_headers=extra,
|
|
321
323
|
)
|
|
322
324
|
|
|
325
|
+
@destructive_operation
|
|
323
326
|
def complete_upload(
|
|
324
327
|
self,
|
|
325
328
|
repo: str,
|
|
@@ -328,7 +331,6 @@ class RegistryApi(RegistryApiBase):
|
|
|
328
331
|
final_chunk: bytes | None = None,
|
|
329
332
|
) -> dict[str, Any]:
|
|
330
333
|
"""``PUT {location}?digest=`` — finalize a blob upload (gated)."""
|
|
331
|
-
self._guard_destructive("complete_upload")
|
|
332
334
|
return self._registry_request(
|
|
333
335
|
"PUT",
|
|
334
336
|
repo,
|
|
@@ -339,9 +341,9 @@ class RegistryApi(RegistryApiBase):
|
|
|
339
341
|
content_type=OCTET_STREAM,
|
|
340
342
|
)
|
|
341
343
|
|
|
344
|
+
@destructive_operation
|
|
342
345
|
def mount_blob(self, repo: str, digest: str, from_repo: str) -> dict[str, Any]:
|
|
343
346
|
"""``POST /v2/{repo}/blobs/uploads/?mount=&from=`` — cross-repo mount (gated)."""
|
|
344
|
-
self._guard_destructive("mount_blob")
|
|
345
347
|
return self._registry_request(
|
|
346
348
|
"POST",
|
|
347
349
|
repo,
|
|
@@ -74,7 +74,6 @@ class RegistryApiBase(DockerHubApiBase):
|
|
|
74
74
|
registry_token_manager: Any | None = None,
|
|
75
75
|
tls_profile: ResolvedTLSProfile | None = None,
|
|
76
76
|
timeout: float | None = None,
|
|
77
|
-
allow_destructive: bool = False,
|
|
78
77
|
debug: bool = False,
|
|
79
78
|
transport: httpx.BaseTransport | None = None,
|
|
80
79
|
**kwargs: Any,
|
|
@@ -85,7 +84,6 @@ class RegistryApiBase(DockerHubApiBase):
|
|
|
85
84
|
url=url or DEFAULT_REGISTRY_URL,
|
|
86
85
|
tls_profile=tls_profile,
|
|
87
86
|
timeout=timeout if timeout is not None else 30.0,
|
|
88
|
-
allow_destructive=allow_destructive,
|
|
89
87
|
debug=debug,
|
|
90
88
|
transport=transport,
|
|
91
89
|
**kwargs,
|
|
@@ -339,12 +339,6 @@ def _resolve_tls_profile(
|
|
|
339
339
|
)
|
|
340
340
|
|
|
341
341
|
|
|
342
|
-
def _resolve_allow_destructive(allow_destructive: bool | None) -> bool:
|
|
343
|
-
if allow_destructive is None:
|
|
344
|
-
return setting("DOCKERHUB_ALLOW_DESTRUCTIVE", False)
|
|
345
|
-
return allow_destructive
|
|
346
|
-
|
|
347
|
-
|
|
348
342
|
@dataclass
|
|
349
343
|
class _HubCredentials:
|
|
350
344
|
url: str
|
|
@@ -407,7 +401,6 @@ def get_client(
|
|
|
407
401
|
token: str | None = None,
|
|
408
402
|
jwt: str | None = None,
|
|
409
403
|
tls_profile: ResolvedTLSProfile | None = None,
|
|
410
|
-
allow_destructive: bool | None = None,
|
|
411
404
|
config: dict | None = None,
|
|
412
405
|
) -> Any:
|
|
413
406
|
"""Factory for the Docker Hub :class:`~dockerhub_api.api_client.Api` client.
|
|
@@ -422,8 +415,8 @@ def get_client(
|
|
|
422
415
|
password, a PAT ``dckr_pat_*``, or an org token).
|
|
423
416
|
3. Anonymous — public, unauthenticated endpoints only.
|
|
424
417
|
|
|
425
|
-
|
|
426
|
-
|
|
418
|
+
Deletes and org-settings writes are declared destructive with the SDK and ask the caller to confirm.
|
|
419
|
+
CONCEPT:DH-OS.identity.destructive-action-gating-member — destructive-action gating.
|
|
427
420
|
|
|
428
421
|
A resolved ``username`` (personal account or org) the caller's identity is
|
|
429
422
|
not entitled to is denied before any credential exchange happens.
|
|
@@ -433,7 +426,6 @@ def get_client(
|
|
|
433
426
|
creds = _resolve_hub_credentials(url, username, token, jwt, config)
|
|
434
427
|
_deny_unless_entitled(creds.username)
|
|
435
428
|
profile = _resolve_tls_profile(tls_profile)
|
|
436
|
-
allow_destructive = _resolve_allow_destructive(allow_destructive)
|
|
437
429
|
|
|
438
430
|
if creds.jwt:
|
|
439
431
|
logger.info("Using pre-minted Docker Hub JWT")
|
|
@@ -441,7 +433,6 @@ def get_client(
|
|
|
441
433
|
url=creds.url,
|
|
442
434
|
token=creds.jwt,
|
|
443
435
|
tls_profile=profile,
|
|
444
|
-
allow_destructive=allow_destructive,
|
|
445
436
|
)
|
|
446
437
|
|
|
447
438
|
if creds.username and creds.token:
|
|
@@ -459,14 +450,13 @@ def get_client(
|
|
|
459
450
|
url=creds.url,
|
|
460
451
|
token_manager=manager,
|
|
461
452
|
tls_profile=profile,
|
|
462
|
-
allow_destructive=allow_destructive,
|
|
463
453
|
)
|
|
464
454
|
|
|
465
455
|
logger.warning(
|
|
466
456
|
"No Docker Hub credentials configured — anonymous client "
|
|
467
457
|
"(public endpoints only). Set DOCKERHUB_USERNAME and DOCKERHUB_TOKEN."
|
|
468
458
|
)
|
|
469
|
-
return Api(url=creds.url, tls_profile=profile
|
|
459
|
+
return Api(url=creds.url, tls_profile=profile)
|
|
470
460
|
|
|
471
461
|
|
|
472
462
|
def _resolve_credentials(
|
|
@@ -509,7 +499,6 @@ def get_registry_client(
|
|
|
509
499
|
username: str | None = None,
|
|
510
500
|
token: str | None = None,
|
|
511
501
|
tls_profile: ResolvedTLSProfile | None = None,
|
|
512
|
-
allow_destructive: bool | None = None,
|
|
513
502
|
config: dict | None = None,
|
|
514
503
|
transport: Any | None = None,
|
|
515
504
|
) -> Any:
|
|
@@ -519,7 +508,7 @@ def get_registry_client(
|
|
|
519
508
|
scoped tokens via :class:`RegistryTokenManager` using the same
|
|
520
509
|
``DOCKER_HUB_USER`` / ``DOCKER_HUB_TOKEN`` credentials (anonymous when
|
|
521
510
|
unset, which still works for public pulls). Pushes, deletes, and blob
|
|
522
|
-
uploads are
|
|
511
|
+
uploads are declared destructive with the SDK.
|
|
523
512
|
|
|
524
513
|
A resolved ``username`` (personal account or org) the caller's identity is
|
|
525
514
|
not entitled to is denied before any token is minted.
|
|
@@ -541,7 +530,6 @@ def get_registry_client(
|
|
|
541
530
|
username, token = _resolve_credentials(username, token, config)
|
|
542
531
|
_deny_unless_entitled(username)
|
|
543
532
|
profile = _resolve_tls_profile(tls_profile)
|
|
544
|
-
allow_destructive = _resolve_allow_destructive(allow_destructive)
|
|
545
533
|
|
|
546
534
|
token_manager = RegistryTokenManager(
|
|
547
535
|
username=username,
|
|
@@ -555,7 +543,6 @@ def get_registry_client(
|
|
|
555
543
|
url=url,
|
|
556
544
|
registry_token_manager=token_manager,
|
|
557
545
|
tls_profile=profile,
|
|
558
|
-
allow_destructive=allow_destructive,
|
|
559
546
|
transport=transport,
|
|
560
547
|
)
|
|
561
548
|
|
|
@@ -15,8 +15,6 @@ from scion_sdk.exceptions import (
|
|
|
15
15
|
UnauthorizedError,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
19
|
-
|
|
20
18
|
#: Keys whose values are always masked in MCP tool results.
|
|
21
19
|
SECRET_KEYS = {"secret", "password", "access_token", "refresh_token", "jwt"}
|
|
22
20
|
REDACTED = "***redacted***"
|
|
@@ -76,7 +74,6 @@ HANDLED_ERRORS = (
|
|
|
76
74
|
UnauthorizedError,
|
|
77
75
|
ParameterError,
|
|
78
76
|
MissingParameterError,
|
|
79
|
-
DestructiveOperationError,
|
|
80
77
|
ValueError,
|
|
81
78
|
)
|
|
82
79
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"api_version": "graphos.io/v1",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"dockerhub_api/ontology/dockerhub.ttl": "
|
|
4
|
+
"dockerhub_api/ontology/dockerhub.ttl": "d498f4e75964cdb27a76fddccb23e42556e7eee994d8d91ba773c0ed2cc9dc5e",
|
|
5
5
|
"dockerhub_api/ontology/fixtures/records.json": "cdf24bdcd53b75daef789c05f93d55ced4ce4a138360a2ff9f1e34c55f8f6da8",
|
|
6
6
|
"dockerhub_api/ontology/mappings/source.yaml": "84985121b0fe97a045b04a5c40183d434d3d7ca1bca29202080cb390955545a6",
|
|
7
7
|
"dockerhub_api/ontology/migrations/manifest.json": "81a24f182615e83dc844c6e7f3a211d47705d2bfce4ee45b5c4b24134e64909f",
|
|
8
|
-
"dockerhub_api/ontology/shapes/connector.shacl.ttl": "
|
|
8
|
+
"dockerhub_api/ontology/shapes/connector.shacl.ttl": "1417340d744e6cf9488ab231ff0ff21ed79cf126f54a901966da4827aa41d05b"
|
|
9
9
|
},
|
|
10
10
|
"checks": {
|
|
11
11
|
"declared_tool_schema": "passed",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"signer": "UNSIGNED-PREVIEW",
|
|
30
30
|
"signing_public_key": "",
|
|
31
31
|
"status": "source-validated",
|
|
32
|
-
"validated_at": "2026-11-
|
|
32
|
+
"validated_at": "2026-11-12T19:24:12Z"
|
|
33
33
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
3
3
|
@prefix dc: <http://purl.org/dc/terms/> .
|
|
4
4
|
@prefix : <http://knuckles.team/kg#> .
|
|
5
|
+
@prefix core: <https://knuckles-team.github.io/ontology/core#> .
|
|
5
6
|
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
6
7
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
7
8
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
@@ -12,12 +13,14 @@
|
|
|
12
13
|
# per-package IRI. Backs the dockerhub-api MCP tool surface and its native ingestion (dockerhub:repository / :image / …).
|
|
13
14
|
|
|
14
15
|
:Namespace a owl:Class ;
|
|
16
|
+
rdfs:subClassOf core:Namespace ;
|
|
15
17
|
rdfs:label "Docker Hub Namespace" ;
|
|
16
18
|
rdfs:comment "A Docker Hub account/organization namespace that owns image repositories (the {namespace} in namespaces/{ns}/repositories)." .
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
:ContainerImage a owl:Class ;
|
|
23
|
+
rdfs:subClassOf core:ContainerImage ;
|
|
21
24
|
rdfs:label "Container Image" ;
|
|
22
25
|
rdfs:comment "A concrete, content-addressable OCI/Docker image (a repository tag resolving to a digest, size, and platform) — the repository-tag and registry-manifest tools." .
|
|
23
26
|
|
|
@@ -25,7 +28,7 @@
|
|
|
25
28
|
:AccessToken a owl:Class ;
|
|
26
29
|
rdfs:label "Access Token" ;
|
|
27
30
|
rdfs:comment "A personal or organization access token (PAT / OAT) scoped for registry and API access (the access-token and org-access-token tools). Secret material is never ingested." ;
|
|
28
|
-
rdfs:subClassOf :Credential .
|
|
31
|
+
rdfs:subClassOf core:Credential .
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
# --- links (owl:ObjectProperty) ---
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/shapes/connector.shacl.ttl
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
@prefix : <
|
|
2
|
-
@prefix shape: <
|
|
1
|
+
@prefix : <https://knuckles-team.github.io/ontology/core#> .
|
|
2
|
+
@prefix shape: <https://knuckles-team.github.io/ontology/connector/dockerhub-shapes#> .
|
|
3
3
|
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
4
4
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
5
5
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dockerhub-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Docker Hub API + MCP Server
|
|
5
5
|
Author-email: Repository Maintainers <maintainers@knuckles.team>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: <3.15,>=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: scion-sdk[mcp]
|
|
15
|
+
Requires-Dist: scion-sdk[mcp]<0.11,>=0.10.2
|
|
16
16
|
Requires-Dist: httpx>=0.28.1
|
|
17
17
|
Requires-Dist: python-dotenv>=1.2.4
|
|
18
18
|
Provides-Extra: test
|
|
@@ -84,8 +84,7 @@ multi-arch inspection, OCI referrers, and gated push/remove) and **Docker Scout*
|
|
|
84
84
|
expiry, with one transparent re-mint on 401.
|
|
85
85
|
- **Rate-Limit Telemetry:** `X-RateLimit-*` headers surfaced in every result;
|
|
86
86
|
HTTP 429 retried with bounded `Retry-After` backoff.
|
|
87
|
-
- **Safety by Default:** Removes and org-settings writes are
|
|
88
|
-
`DOCKERHUB_ALLOW_DESTRUCTIVE` (default `False`); secrets are redacted from tool
|
|
87
|
+
- **Safety by Default:** Removes and org-settings writes are declared destructive; the SDK asks the caller to confirm (`confirm=true`) before any request. Secrets are redacted from tool
|
|
89
88
|
results (plaintext tokens appear exactly once — on creation). Repository creation
|
|
90
89
|
stays enabled: it is the primary release-provisioning use case.
|
|
91
90
|
- **Integrated Graph Agent:** Built-in Pydantic AI agent (`dockerhub-agent`) with
|
|
@@ -279,7 +278,6 @@ _9 action-routed tool(s) (default) · 54 verbose 1:1 tool(s). Each is enabled un
|
|
|
279
278
|
"ADMINTOOL": "True",
|
|
280
279
|
"AUDITTOOL": "True",
|
|
281
280
|
"AUTHTOOL": "True",
|
|
282
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
283
281
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
284
282
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
285
283
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -328,7 +326,6 @@ own runtime secret boundary.
|
|
|
328
326
|
"ADMINTOOL": "True",
|
|
329
327
|
"AUDITTOOL": "True",
|
|
330
328
|
"AUTHTOOL": "True",
|
|
331
|
-
"DOCKERHUB_ALLOW_DESTRUCTIVE": "False",
|
|
332
329
|
"DOCKERHUB_TLS_PROFILE": "system",
|
|
333
330
|
"DOCKERHUB_TOKEN": "dckr_pat_your_personal_access_token",
|
|
334
331
|
"DOCKERHUB_URL": "https://hub.docker.com",
|
|
@@ -376,7 +373,6 @@ docker run -i --rm \
|
|
|
376
373
|
-e ADMINTOOL=True \
|
|
377
374
|
-e AUDITTOOL=True \
|
|
378
375
|
-e AUTHTOOL=True \
|
|
379
|
-
-e DOCKERHUB_ALLOW_DESTRUCTIVE=False \
|
|
380
376
|
-e DOCKERHUB_TLS_PROFILE=system \
|
|
381
377
|
-e DOCKERHUB_TOKEN=dckr_pat_your_personal_access_token \
|
|
382
378
|
-e DOCKERHUB_URL=https://hub.docker.com \
|
|
@@ -423,8 +419,7 @@ the detailed transport contract.
|
|
|
423
419
|
|---|---|---|
|
|
424
420
|
| Reads (repos, tags, members, logs, SCIM) | allowed | — |
|
|
425
421
|
| Repository create / immutable-tag config / invites / role updates | allowed | — |
|
|
426
|
-
| Removes (PATs, OATs, groups, members, invites) |
|
|
427
|
-
| Org-settings writes (`PUT /v2/orgs/{org}/settings`) | **blocked** | `DOCKERHUB_ALLOW_DESTRUCTIVE=True` |
|
|
422
|
+
| Removes (PATs, OATs, groups, members, invites) and org-settings writes | confirmation required | `confirm=true` per call, or operator setting `SCION_ALLOW_DESTRUCTIVE` |
|
|
428
423
|
|
|
429
424
|
---
|
|
430
425
|
|
|
@@ -488,7 +483,6 @@ network transport, connector ingestion, Arbora delegation, or trace export.
|
|
|
488
483
|
| `DOCKER_REGISTRY_URL` | `https://registry-1.docker.io` | |
|
|
489
484
|
| `DOCKER_REGISTRY_AUTH_URL` | `https://auth.docker.io/token` | token-service realm; auto-discovered from the 401 challenge |
|
|
490
485
|
| `DOCKER_SCOUT_URL` | `https://api.scout.docker.com` | |
|
|
491
|
-
| `DOCKERHUB_ALLOW_DESTRUCTIVE` | `False` | |
|
|
492
486
|
| `AUTHTOOL` | `True` | |
|
|
493
487
|
| `REPOSTOOL` | `True` | |
|
|
494
488
|
| `ORGTOOL` | `True` | |
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dockerhub-api"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "Docker Hub API + MCP Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Environment :: Console", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3",]
|
|
11
11
|
requires-python = ">=3.12, <3.15"
|
|
12
|
-
dependencies = [ "scion-sdk[mcp]>=0.
|
|
12
|
+
dependencies = [ "scion-sdk[mcp]>=0.10.2,<0.11", "httpx>=0.28.1", "python-dotenv>=1.2.4",]
|
|
13
13
|
[[project.authors]]
|
|
14
14
|
name = "Repository Maintainers"
|
|
15
15
|
email = "maintainers@knuckles.team"
|
|
@@ -4,7 +4,6 @@ import json
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def test_get_org_settings(api):
|
|
@@ -12,13 +11,8 @@ def test_get_org_settings(api):
|
|
|
12
11
|
assert result["data"]["restricted_images"]["enabled"] is False
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
api.update_org_settings(org="acme", restricted_images_enabled=True)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_update_org_settings_payload(hub, api_destructive):
|
|
21
|
-
result = api_destructive.update_org_settings(
|
|
14
|
+
def test_update_org_settings_payload(hub, api):
|
|
15
|
+
result = api.update_org_settings(
|
|
22
16
|
org="acme",
|
|
23
17
|
restricted_images_enabled=True,
|
|
24
18
|
allow_official_images=True,
|
|
@@ -72,11 +66,9 @@ def test_update_member_role(hub, api):
|
|
|
72
66
|
assert hub.requests[-1].method == "PUT"
|
|
73
67
|
|
|
74
68
|
|
|
75
|
-
def test_remove_member_gated(api
|
|
76
|
-
with pytest.raises(DestructiveOperationError):
|
|
77
|
-
api.remove_org_member(org="acme", username="dev")
|
|
69
|
+
def test_remove_member_gated(api):
|
|
78
70
|
assert (
|
|
79
|
-
|
|
71
|
+
api.remove_org_member(org="acme", username="dev")["status_code"]
|
|
80
72
|
== 204
|
|
81
73
|
)
|
|
82
74
|
|
|
@@ -86,10 +78,8 @@ def test_list_invites(api):
|
|
|
86
78
|
assert result["data"]["results"][0]["id"] == "inv-1"
|
|
87
79
|
|
|
88
80
|
|
|
89
|
-
def test_delete_invite_gated(api
|
|
90
|
-
|
|
91
|
-
api.delete_invite(invite_id="inv-1")
|
|
92
|
-
assert api_destructive.delete_invite(invite_id="inv-1")["status_code"] == 204
|
|
81
|
+
def test_delete_invite_gated(api):
|
|
82
|
+
assert api.delete_invite(invite_id="inv-1")["status_code"] == 204
|
|
93
83
|
|
|
94
84
|
|
|
95
85
|
def test_resend_invite(hub, api):
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import httpx
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
7
6
|
from tests.conftest import (
|
|
8
7
|
AMD64_DIGEST,
|
|
9
8
|
CONFIG_DIGEST,
|
|
@@ -86,45 +85,33 @@ def test_check_manifest_and_blob(registry_api):
|
|
|
86
85
|
# --------------------------- destructive gating --------------------------- #
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
registry_api.delete_manifest("nginx", "latest")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_delete_manifest_resolves_tag_then_deletes(registry, registry_api_destructive):
|
|
95
|
-
result = registry_api_destructive.delete_manifest("nginx", "latest")
|
|
88
|
+
def test_delete_manifest_resolves_tag_then_deletes(registry, registry_api):
|
|
89
|
+
result = registry_api.delete_manifest("nginx", "latest")
|
|
96
90
|
assert result["status_code"] == 202
|
|
97
91
|
paths = _registry_paths(registry)
|
|
98
92
|
# tag resolved to a digest, then DELETE issued against the digest
|
|
99
93
|
assert f"/v2/library/nginx/manifests/{INDEX_DIGEST}" in paths
|
|
100
94
|
|
|
101
95
|
|
|
102
|
-
def test_delete_blob_surfaces_405_cleanly(
|
|
103
|
-
result =
|
|
96
|
+
def test_delete_blob_surfaces_405_cleanly(registry_api):
|
|
97
|
+
result = registry_api.delete_blob("nginx", CONFIG_DIGEST)
|
|
104
98
|
assert result["status_code"] == 405
|
|
105
99
|
|
|
106
100
|
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
registry_api.start_upload("myorg/app")
|
|
110
|
-
with pytest.raises(DestructiveOperationError):
|
|
111
|
-
registry_api.put_manifest("myorg/app", "v1", {"schemaVersion": 2}, "media")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_full_push_session(registry, registry_api_destructive):
|
|
115
|
-
started = registry_api_destructive.start_upload("myorg/app")
|
|
101
|
+
def test_full_push_session(registry, registry_api):
|
|
102
|
+
started = registry_api.start_upload("myorg/app")
|
|
116
103
|
location = started["headers"]["Location"]
|
|
117
104
|
assert started["status_code"] == 202
|
|
118
105
|
|
|
119
|
-
patched =
|
|
106
|
+
patched = registry_api.upload_chunk("myorg/app", location, b"layerbytes")
|
|
120
107
|
assert patched["status_code"] == 202
|
|
121
108
|
|
|
122
|
-
completed =
|
|
109
|
+
completed = registry_api.complete_upload(
|
|
123
110
|
"myorg/app", location, AMD64_DIGEST, final_chunk=b""
|
|
124
111
|
)
|
|
125
112
|
assert completed["status_code"] == 201
|
|
126
113
|
|
|
127
|
-
put =
|
|
114
|
+
put = registry_api.put_manifest(
|
|
128
115
|
"myorg/app",
|
|
129
116
|
"v1",
|
|
130
117
|
{"schemaVersion": 2, "mediaType": "x", "config": {}, "layers": []},
|
|
@@ -133,8 +120,8 @@ def test_full_push_session(registry, registry_api_destructive):
|
|
|
133
120
|
assert put["status_code"] == 201
|
|
134
121
|
|
|
135
122
|
|
|
136
|
-
def test_mount_blob(
|
|
137
|
-
result =
|
|
123
|
+
def test_mount_blob(registry_api):
|
|
124
|
+
result = registry_api.mount_blob("myorg/app", CONFIG_DIGEST, "library/nginx")
|
|
138
125
|
assert result["status_code"] == 201
|
|
139
126
|
|
|
140
127
|
|
|
@@ -4,7 +4,6 @@ import json
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def test_list_groups(hub, api):
|
|
@@ -44,25 +43,21 @@ def test_update_group_requires_change(api):
|
|
|
44
43
|
api.update_group(org="acme", group_name="platform")
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def test_delete_group_gated(api
|
|
48
|
-
with pytest.raises(DestructiveOperationError):
|
|
49
|
-
api.delete_group(org="acme", group_name="platform")
|
|
46
|
+
def test_delete_group_gated(api):
|
|
50
47
|
assert (
|
|
51
|
-
|
|
48
|
+
api.delete_group(org="acme", group_name="platform")["status_code"]
|
|
52
49
|
== 204
|
|
53
50
|
)
|
|
54
51
|
|
|
55
52
|
|
|
56
|
-
def test_group_members_roundtrip(hub, api
|
|
53
|
+
def test_group_members_roundtrip(hub, api):
|
|
57
54
|
members = api.get_group_members(org="acme", group_name="platform")
|
|
58
55
|
assert members["data"]["results"][0]["username"] == "tester"
|
|
59
56
|
|
|
60
57
|
added = api.add_group_member(org="acme", group_name="platform", member="dev")
|
|
61
58
|
assert added["data"]["username"] == "dev"
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
api.remove_group_member(org="acme", group_name="platform", username="dev")
|
|
65
|
-
removed = api_destructive.remove_group_member(
|
|
60
|
+
removed = api.remove_group_member(
|
|
66
61
|
org="acme", group_name="platform", username="dev"
|
|
67
62
|
)
|
|
68
63
|
assert removed["status_code"] == 204
|
|
@@ -4,7 +4,6 @@ import json
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def test_list_pats_pagination(hub, api):
|
|
@@ -40,13 +39,8 @@ def test_update_pat_requires_a_change(api):
|
|
|
40
39
|
api.update_access_token(uuid="pat-1")
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
api.delete_access_token(uuid="pat-1")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_delete_pat_allowed_when_enabled(hub, api_destructive):
|
|
49
|
-
result = api_destructive.delete_access_token(uuid="pat-1")
|
|
42
|
+
def test_delete_pat_allowed_when_enabled(hub, api):
|
|
43
|
+
result = api.delete_access_token(uuid="pat-1")
|
|
50
44
|
assert result["status_code"] == 204
|
|
51
45
|
assert hub.requests[-1].method == "DELETE"
|
|
52
46
|
|
|
@@ -91,11 +85,9 @@ def test_get_update_oat(hub, api):
|
|
|
91
85
|
assert result["data"]["is_active"] is False
|
|
92
86
|
|
|
93
87
|
|
|
94
|
-
def test_delete_oat_gated(api,
|
|
95
|
-
with pytest.raises(DestructiveOperationError):
|
|
96
|
-
api.delete_org_access_token(org="acme", token_id="oat-1") # nosec B105 B106 — fake test credential
|
|
88
|
+
def test_delete_oat_gated(api, hub):
|
|
97
89
|
assert (
|
|
98
|
-
|
|
90
|
+
api.delete_org_access_token(org="acme", token_id="oat-1")[ # nosec B105 B106 — fake test credential
|
|
99
91
|
"status_code"
|
|
100
92
|
]
|
|
101
93
|
== 204
|
|
@@ -4,7 +4,6 @@ and destructive gating at the facade level."""
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
8
7
|
from dockerhub_api.api_client import Api
|
|
9
8
|
|
|
10
9
|
|
|
@@ -46,11 +45,6 @@ def test_facade_composes_all_domain_mixins():
|
|
|
46
45
|
assert callable(getattr(Api, method, None)), f"missing {method}"
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
api.delete_access_token(uuid="uuid-1")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_destructive_call_allowed_when_enabled(api_destructive):
|
|
55
|
-
result = api_destructive.delete_access_token(uuid="uuid-1")
|
|
48
|
+
def test_destructive_call_allowed_when_enabled(api):
|
|
49
|
+
result = api.delete_access_token(uuid="uuid-1")
|
|
56
50
|
assert result["status_code"] in (200, 202, 204)
|
|
@@ -167,12 +167,6 @@ def test_get_client_uses_token_manager(monkeypatch):
|
|
|
167
167
|
assert client._token_manager.identifier == "tester"
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
def test_get_client_destructive_env(monkeypatch):
|
|
171
|
-
monkeypatch.setenv("DOCKERHUB_ALLOW_DESTRUCTIVE", "True")
|
|
172
|
-
client = get_client()
|
|
173
|
-
assert client.allow_destructive is True
|
|
174
|
-
|
|
175
|
-
|
|
176
170
|
def test_get_client_anonymous(monkeypatch):
|
|
177
171
|
monkeypatch.delenv("DOCKERHUB_USERNAME", raising=False)
|
|
178
172
|
monkeypatch.delenv("DOCKERHUB_TOKEN", raising=False)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""CONN-DOCKERHUB-API-CONNECTOR-CONTENT-R003: destructive operations use the SDK's one declaration and guard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
from fastmcp import Client
|
|
11
|
+
from scion_sdk.connector import build_server
|
|
12
|
+
|
|
13
|
+
pytestmark = pytest.mark.spec("CONN-DOCKERHUB-API-CONNECTOR-CONTENT-R003")
|
|
14
|
+
|
|
15
|
+
DESTRUCTIVE_TOOLS = {
|
|
16
|
+
"dockerhub_delete_access_token": {"uuid": "u1"},
|
|
17
|
+
"dockerhub_delete_group": {"org": "acme", "group_name": "g"},
|
|
18
|
+
"dockerhub_delete_invite": {"invite_id": "i1"},
|
|
19
|
+
"dockerhub_delete_org_access_token": {"org": "acme", "token_id": "t1"},
|
|
20
|
+
"dockerhub_remove_group_member": {
|
|
21
|
+
"org": "acme",
|
|
22
|
+
"group_name": "g",
|
|
23
|
+
"username": "u",
|
|
24
|
+
},
|
|
25
|
+
"dockerhub_remove_org_member": {"org": "acme", "username": "u"},
|
|
26
|
+
"dockerhub_update_org_settings": {"org": "acme", "restricted_images_enabled": True},
|
|
27
|
+
"dockerhub_registry_delete_blob": {
|
|
28
|
+
"repo": "acme/app",
|
|
29
|
+
"digest": "sha256:" + "a" * 64,
|
|
30
|
+
},
|
|
31
|
+
"dockerhub_registry_delete_manifest": {"repo": "acme/app", "reference": "latest"},
|
|
32
|
+
"dockerhub_registry_start_upload": {"repo": "acme/app"},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _server(requests: list[httpx.Request], monkeypatch):
|
|
37
|
+
real = httpx.Client
|
|
38
|
+
|
|
39
|
+
def record(request: httpx.Request) -> httpx.Response:
|
|
40
|
+
requests.append(request)
|
|
41
|
+
return httpx.Response(200, json={})
|
|
42
|
+
|
|
43
|
+
def client(*args, **kwargs):
|
|
44
|
+
kwargs["transport"] = httpx.MockTransport(record)
|
|
45
|
+
return real(*args, **kwargs)
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr("dockerhub_api.api.api_client_base.httpx.Client", client)
|
|
48
|
+
monkeypatch.setenv("DOCKERHUB_JWT", "jwt")
|
|
49
|
+
monkeypatch.delenv("SCION_ALLOW_DESTRUCTIVE", raising=False)
|
|
50
|
+
with patch("sys.argv", ["dockerhub-mcp"]):
|
|
51
|
+
return build_server("dockerhub_api", command_args=[])[1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_declared_operations_are_destructive_tools_that_ask_first(monkeypatch):
|
|
55
|
+
mcp = _server([], monkeypatch)
|
|
56
|
+
|
|
57
|
+
async def _run():
|
|
58
|
+
async with Client(mcp) as client:
|
|
59
|
+
tools = {t.name: t for t in await client.list_tools()}
|
|
60
|
+
for name in DESTRUCTIVE_TOOLS:
|
|
61
|
+
assert tools[name].annotations.destructive_hint is True, name
|
|
62
|
+
for name, tool in tools.items():
|
|
63
|
+
properties = tool.input_schema["properties"]
|
|
64
|
+
assert "allow_destructive" not in properties, name
|
|
65
|
+
if tool.annotations and tool.annotations.destructive_hint:
|
|
66
|
+
assert "confirm" in properties, name
|
|
67
|
+
|
|
68
|
+
asyncio.run(_run())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("name", sorted(DESTRUCTIVE_TOOLS))
|
|
72
|
+
def test_unconfirmed_call_sends_no_request(name, monkeypatch):
|
|
73
|
+
requests: list[httpx.Request] = []
|
|
74
|
+
mcp = _server(requests, monkeypatch)
|
|
75
|
+
|
|
76
|
+
async def _run():
|
|
77
|
+
async with Client(mcp) as client:
|
|
78
|
+
refused = await client.call_tool(
|
|
79
|
+
name, DESTRUCTIVE_TOOLS[name], raise_on_error=False
|
|
80
|
+
)
|
|
81
|
+
assert refused.structured_content["cancelled"] is True
|
|
82
|
+
|
|
83
|
+
asyncio.run(_run())
|
|
84
|
+
assert requests == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_confirmed_call_sends_one_request(monkeypatch):
|
|
88
|
+
requests: list[httpx.Request] = []
|
|
89
|
+
mcp = _server(requests, monkeypatch)
|
|
90
|
+
|
|
91
|
+
async def _run():
|
|
92
|
+
async with Client(mcp) as client:
|
|
93
|
+
await client.call_tool(
|
|
94
|
+
"dockerhub_delete_access_token", {"uuid": "u1", "confirm": True}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
asyncio.run(_run())
|
|
98
|
+
assert [r.method for r in requests] == ["DELETE"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/ontology/migrations/manifest.json
RENAMED
|
File without changes
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/prompts/dockerhub-mcp-operator.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-api-development/SKILL.md
RENAMED
|
File without changes
|
{dockerhub_api-1.3.10 → dockerhub_api-1.4.0}/dockerhub_api/skills/dockerhub-api-operations/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|