gumloop 0.4.0__tar.gz → 0.4.2__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.
- {gumloop-0.4.0 → gumloop-0.4.2}/PKG-INFO +21 -1
- {gumloop-0.4.0 → gumloop-0.4.2}/README.md +20 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/pyproject.toml +1 -1
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/_client.py +9 -1
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/_http.py +50 -2
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/_version.py +1 -1
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/agents.py +10 -4
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/mcp.py +8 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/skills.py +4 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_client.py +121 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/uv.lock +1 -1
- {gumloop-0.4.0 → gumloop-0.4.2}/.env.example +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/.gitignore +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/.pre-commit-config.yaml +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/.python-version +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/LICENSE +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/scripts/check +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/scripts/fmt +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/scripts/sdk_examples.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/_auth.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/_sync_output.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/agents.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/brain.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/mcp.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/sessions.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/commands/sync.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/main.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/errors.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/oauth.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/py.typed +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/brain.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/sessions.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/sync.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/archive.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/bundle.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/bundle_validation.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/errors.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/hashing.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/installed.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/local_state.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/lock.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/markers.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/reconcile.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/reporting.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/results.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/run.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/scheduler.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/target_files.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/targets.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/sync/wire.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/src/gumloop/types.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/conftest.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/sync_scenario_helpers.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/sync_test_fakes.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_agents.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_brain.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_chat.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_context.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_home_isolation.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_login.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_main.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_mcp.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sessions.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_skills.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_bundle.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_command.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_command_safety.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_contract_fixtures.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_enrollment.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_fixture_corpus.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_hashing.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_installed.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_reconciliation_safety.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_scheduler_macos.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/cli/test_sync_targets.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/conftest.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/README.md +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/integration/conftest.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/integration/test_live.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/__init__.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/conftest.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/helpers.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_agents.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_brain.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_sessions.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_sync.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/skill_sync_fixtures.py +0 -0
- {gumloop-0.4.0 → gumloop-0.4.2}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gumloop
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Python client for the Gumloop API
|
|
5
5
|
Project-URL: Homepage, https://github.com/gumloop/gumloop-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/gumloop/gumloop-py/issues
|
|
@@ -57,6 +57,26 @@ output = client.run_flow(
|
|
|
57
57
|
print(output)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Authenticate with a team API key
|
|
61
|
+
|
|
62
|
+
Team (workspace) API keys are scoped to a single team. Pass the team's ID and
|
|
63
|
+
the acting member's user ID — every request is validated against that team and
|
|
64
|
+
only reaches resources the team owns.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from gumloop import Gumloop
|
|
68
|
+
|
|
69
|
+
client = Gumloop(
|
|
70
|
+
api_key="your_team_api_key",
|
|
71
|
+
user_id="your_user_id", # must be a member of the team
|
|
72
|
+
team_id="your_team_id",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
agents = client.agents.list() # scoped to the team
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`team_id` can also be provided via the `GUMLOOP_TEAM_ID` environment variable.
|
|
79
|
+
|
|
60
80
|
## Chat with an agent (streaming)
|
|
61
81
|
|
|
62
82
|
```python
|
|
@@ -32,6 +32,26 @@ output = client.run_flow(
|
|
|
32
32
|
print(output)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Authenticate with a team API key
|
|
36
|
+
|
|
37
|
+
Team (workspace) API keys are scoped to a single team. Pass the team's ID and
|
|
38
|
+
the acting member's user ID — every request is validated against that team and
|
|
39
|
+
only reaches resources the team owns.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from gumloop import Gumloop
|
|
43
|
+
|
|
44
|
+
client = Gumloop(
|
|
45
|
+
api_key="your_team_api_key",
|
|
46
|
+
user_id="your_user_id", # must be a member of the team
|
|
47
|
+
team_id="your_team_id",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
agents = client.agents.list() # scoped to the team
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`team_id` can also be provided via the `GUMLOOP_TEAM_ID` environment variable.
|
|
54
|
+
|
|
35
55
|
## Chat with an agent (streaming)
|
|
36
56
|
|
|
37
57
|
```python
|
|
@@ -32,7 +32,7 @@ from gumloop.resources import Sync
|
|
|
32
32
|
from gumloop.resources import Teams
|
|
33
33
|
|
|
34
34
|
DEFAULT_BASE_URL = "https://api.gumloop.com/api/v1"
|
|
35
|
-
DEFAULT_TIMEOUT =
|
|
35
|
+
DEFAULT_TIMEOUT = 90.0
|
|
36
36
|
DEFAULT_STREAM_TIMEOUT = 3600.0
|
|
37
37
|
|
|
38
38
|
|
|
@@ -55,6 +55,7 @@ class Gumloop:
|
|
|
55
55
|
*,
|
|
56
56
|
access_token: str | None = None,
|
|
57
57
|
user_id: str | None = None,
|
|
58
|
+
team_id: str | None = None,
|
|
58
59
|
base_url: str | None = None,
|
|
59
60
|
stream_base_url: str | None = None,
|
|
60
61
|
timeout: float = DEFAULT_TIMEOUT,
|
|
@@ -66,6 +67,8 @@ class Gumloop:
|
|
|
66
67
|
# x-auth-key header to look up the owner's stored secret. OAuth
|
|
67
68
|
# callers omit it because the bearer token already identifies them.
|
|
68
69
|
self.user_id = user_id or os.environ.get("GUMLOOP_USER_ID")
|
|
70
|
+
# Team API keys are validated against ``team_id``; ``user_id`` stays the acting member.
|
|
71
|
+
self.team_id = team_id or os.environ.get("GUMLOOP_TEAM_ID")
|
|
69
72
|
# Defaults to production; GUMLOOP_BASE_URL is an optional override.
|
|
70
73
|
self.base_url = (base_url or os.environ.get("GUMLOOP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
71
74
|
self.stream_base_url = (stream_base_url or _derive_stream_base_url(self.base_url)).rstrip("/")
|
|
@@ -77,6 +80,7 @@ class Gumloop:
|
|
|
77
80
|
stream_base_url=self.stream_base_url,
|
|
78
81
|
access_token=self.access_token,
|
|
79
82
|
user_id=self.user_id,
|
|
83
|
+
team_id=self.team_id,
|
|
80
84
|
timeout=self.timeout,
|
|
81
85
|
stream_timeout=self.stream_timeout,
|
|
82
86
|
)
|
|
@@ -112,6 +116,7 @@ class AsyncGumloop:
|
|
|
112
116
|
*,
|
|
113
117
|
access_token: str | None = None,
|
|
114
118
|
user_id: str | None = None,
|
|
119
|
+
team_id: str | None = None,
|
|
115
120
|
base_url: str | None = None,
|
|
116
121
|
stream_base_url: str | None = None,
|
|
117
122
|
timeout: float = DEFAULT_TIMEOUT,
|
|
@@ -120,6 +125,8 @@ class AsyncGumloop:
|
|
|
120
125
|
self.api_key = api_key
|
|
121
126
|
self.access_token = access_token or api_key or EnvToken.from_env()
|
|
122
127
|
self.user_id = user_id or os.environ.get("GUMLOOP_USER_ID")
|
|
128
|
+
# Team API keys are validated against ``team_id``; ``user_id`` stays the acting member.
|
|
129
|
+
self.team_id = team_id or os.environ.get("GUMLOOP_TEAM_ID")
|
|
123
130
|
# Defaults to production; GUMLOOP_BASE_URL is an optional override.
|
|
124
131
|
self.base_url = (base_url or os.environ.get("GUMLOOP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
125
132
|
self.stream_base_url = (stream_base_url or _derive_stream_base_url(self.base_url)).rstrip("/")
|
|
@@ -131,6 +138,7 @@ class AsyncGumloop:
|
|
|
131
138
|
stream_base_url=self.stream_base_url,
|
|
132
139
|
access_token=self.access_token,
|
|
133
140
|
user_id=self.user_id,
|
|
141
|
+
team_id=self.team_id,
|
|
134
142
|
timeout=self.timeout,
|
|
135
143
|
stream_timeout=self.stream_timeout,
|
|
136
144
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
from collections.abc import AsyncIterator
|
|
5
6
|
from collections.abc import Iterator
|
|
6
7
|
from collections.abc import Mapping
|
|
@@ -19,6 +20,29 @@ from gumloop.types import StreamEvent
|
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
def _inject_trace_context(request: httpx.Request) -> None:
|
|
25
|
+
"""Request event hook: attach W3C trace context when the host sets TRACEPARENT.
|
|
26
|
+
|
|
27
|
+
Read per request: sandbox runtimes rotate env between calls. setdefault so
|
|
28
|
+
an explicitly provided header wins.
|
|
29
|
+
"""
|
|
30
|
+
traceparent = os.environ.get("TRACEPARENT")
|
|
31
|
+
if not traceparent:
|
|
32
|
+
return
|
|
33
|
+
request.headers.setdefault("traceparent", traceparent)
|
|
34
|
+
tracestate = os.environ.get("TRACESTATE")
|
|
35
|
+
if tracestate:
|
|
36
|
+
request.headers.setdefault("tracestate", tracestate)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _inject_trace_context_async(request: httpx.Request) -> None:
|
|
40
|
+
_inject_trace_context(request)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_TRACE_EVENT_HOOKS = {"request": [_inject_trace_context]}
|
|
44
|
+
_ASYNC_TRACE_EVENT_HOOKS = {"request": [_inject_trace_context_async]}
|
|
45
|
+
|
|
22
46
|
_DONE_SENTINEL = "[DONE]"
|
|
23
47
|
_T = TypeVar("_T", bound=BaseModel)
|
|
24
48
|
|
|
@@ -64,14 +88,24 @@ class HttpClient:
|
|
|
64
88
|
stream_base_url: str,
|
|
65
89
|
access_token: str | None,
|
|
66
90
|
user_id: str | None,
|
|
91
|
+
team_id: str | None = None,
|
|
67
92
|
timeout: float,
|
|
68
93
|
stream_timeout: float | None,
|
|
69
94
|
) -> None:
|
|
70
95
|
self.access_token = access_token
|
|
71
96
|
self.user_id = user_id
|
|
97
|
+
self.team_id = team_id
|
|
72
98
|
self._stream_base_url = stream_base_url.rstrip("/")
|
|
73
99
|
self._stream_timeout = stream_timeout
|
|
74
|
-
self._client = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout)
|
|
100
|
+
self._client = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, event_hooks=_TRACE_EVENT_HOOKS)
|
|
101
|
+
|
|
102
|
+
def _scoped_params(self, params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
103
|
+
# Team keys are validated against ``team_id``, so it rides on every request.
|
|
104
|
+
if not self.team_id:
|
|
105
|
+
return params
|
|
106
|
+
merged = dict(params or {})
|
|
107
|
+
merged.setdefault("team_id", self.team_id)
|
|
108
|
+
return merged
|
|
75
109
|
|
|
76
110
|
def close(self) -> None:
|
|
77
111
|
self._client.close()
|
|
@@ -234,6 +268,7 @@ class HttpClient:
|
|
|
234
268
|
headers["Content-Type"] = "application/json"
|
|
235
269
|
if extra_headers:
|
|
236
270
|
headers.update(extra_headers)
|
|
271
|
+
kwargs["params"] = self._scoped_params(kwargs.get("params"))
|
|
237
272
|
response = self._client.request(method, path, headers=headers, **kwargs)
|
|
238
273
|
if response.status_code >= 400:
|
|
239
274
|
raise to_api_error(response)
|
|
@@ -250,14 +285,26 @@ class AsyncHttpClient:
|
|
|
250
285
|
stream_base_url: str,
|
|
251
286
|
access_token: str | None,
|
|
252
287
|
user_id: str | None,
|
|
288
|
+
team_id: str | None = None,
|
|
253
289
|
timeout: float,
|
|
254
290
|
stream_timeout: float | None,
|
|
255
291
|
) -> None:
|
|
256
292
|
self.access_token = access_token
|
|
257
293
|
self.user_id = user_id
|
|
294
|
+
self.team_id = team_id
|
|
258
295
|
self._stream_base_url = stream_base_url.rstrip("/")
|
|
259
296
|
self._stream_timeout = stream_timeout
|
|
260
|
-
self._client = httpx.AsyncClient(
|
|
297
|
+
self._client = httpx.AsyncClient(
|
|
298
|
+
base_url=base_url.rstrip("/"), timeout=timeout, event_hooks=_ASYNC_TRACE_EVENT_HOOKS
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _scoped_params(self, params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
302
|
+
# Team keys are validated against ``team_id``, so it rides on every request.
|
|
303
|
+
if not self.team_id:
|
|
304
|
+
return params
|
|
305
|
+
merged = dict(params or {})
|
|
306
|
+
merged.setdefault("team_id", self.team_id)
|
|
307
|
+
return merged
|
|
261
308
|
|
|
262
309
|
async def aclose(self) -> None:
|
|
263
310
|
await self._client.aclose()
|
|
@@ -371,6 +418,7 @@ class AsyncHttpClient:
|
|
|
371
418
|
headers = auth_headers(self.access_token, self.user_id)
|
|
372
419
|
if not kwargs.get("files"):
|
|
373
420
|
headers["Content-Type"] = "application/json"
|
|
421
|
+
kwargs["params"] = self._scoped_params(kwargs.get("params"))
|
|
374
422
|
response = await self._client.request(method, path, headers=headers, **kwargs)
|
|
375
423
|
if response.status_code >= 400:
|
|
376
424
|
raise to_api_error(response)
|
|
@@ -46,9 +46,11 @@ class Agents:
|
|
|
46
46
|
request: AgentCreateRequest | Mapping[str, Any] | None = None,
|
|
47
47
|
**kwargs: Any,
|
|
48
48
|
) -> AgentResponse:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
body = AgentCreateRequest.build(request, **kwargs)
|
|
50
|
+
# The create workspace is read from the body, not the query.
|
|
51
|
+
if self._client.team_id is not None:
|
|
52
|
+
body.setdefault("team_id", self._client.team_id)
|
|
53
|
+
return AgentResponse.model_validate(self._client.post("agents", json=body))
|
|
52
54
|
|
|
53
55
|
def retrieve(self, agent_id: str) -> AgentResponse:
|
|
54
56
|
return AgentResponse.model_validate(self._client.get(f"agents/{agent_id}"))
|
|
@@ -169,7 +171,11 @@ class AsyncAgents:
|
|
|
169
171
|
request: AgentCreateRequest | Mapping[str, Any] | None = None,
|
|
170
172
|
**kwargs: Any,
|
|
171
173
|
) -> AgentResponse:
|
|
172
|
-
|
|
174
|
+
body = AgentCreateRequest.build(request, **kwargs)
|
|
175
|
+
# The create workspace is read from the body, not the query.
|
|
176
|
+
if self._client.team_id is not None:
|
|
177
|
+
body.setdefault("team_id", self._client.team_id)
|
|
178
|
+
data = await self._client.post("agents", json=body)
|
|
173
179
|
return AgentResponse.model_validate(data)
|
|
174
180
|
|
|
175
181
|
async def retrieve(self, agent_id: str) -> AgentResponse:
|
|
@@ -79,6 +79,8 @@ class MCP:
|
|
|
79
79
|
*,
|
|
80
80
|
team_id: str | None = None,
|
|
81
81
|
) -> McpPromptResponse:
|
|
82
|
+
# team_id is read from the body here; the query-level default doesn't reach it.
|
|
83
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
82
84
|
body: dict[str, Any] = {"name": name, "arguments": arguments or {}}
|
|
83
85
|
if team_id is not None:
|
|
84
86
|
body["team_id"] = team_id
|
|
@@ -93,6 +95,7 @@ class MCP:
|
|
|
93
95
|
ref: str | None = None,
|
|
94
96
|
team_id: str | None = None,
|
|
95
97
|
) -> McpExecuteResponse:
|
|
98
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
96
99
|
call = McpToolCallRequest(
|
|
97
100
|
server_id=server_id,
|
|
98
101
|
tool_name=tool_name,
|
|
@@ -107,6 +110,7 @@ class MCP:
|
|
|
107
110
|
*,
|
|
108
111
|
team_id: str | None = None,
|
|
109
112
|
) -> McpExecuteResponse:
|
|
113
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
110
114
|
return McpExecuteResponse.model_validate(self._client.post("mcp/tools/call", json=_call_body(calls, team_id)))
|
|
111
115
|
|
|
112
116
|
|
|
@@ -156,6 +160,8 @@ class AsyncMCP:
|
|
|
156
160
|
*,
|
|
157
161
|
team_id: str | None = None,
|
|
158
162
|
) -> McpPromptResponse:
|
|
163
|
+
# team_id is read from the body here; the query-level default doesn't reach it.
|
|
164
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
159
165
|
body: dict[str, Any] = {"name": name, "arguments": arguments or {}}
|
|
160
166
|
if team_id is not None:
|
|
161
167
|
body["team_id"] = team_id
|
|
@@ -171,6 +177,7 @@ class AsyncMCP:
|
|
|
171
177
|
ref: str | None = None,
|
|
172
178
|
team_id: str | None = None,
|
|
173
179
|
) -> McpExecuteResponse:
|
|
180
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
174
181
|
call = McpToolCallRequest(
|
|
175
182
|
server_id=server_id,
|
|
176
183
|
tool_name=tool_name,
|
|
@@ -186,5 +193,6 @@ class AsyncMCP:
|
|
|
186
193
|
*,
|
|
187
194
|
team_id: str | None = None,
|
|
188
195
|
) -> McpExecuteResponse:
|
|
196
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
189
197
|
data = await self._client.post("mcp/tools/call", json=_call_body(calls, team_id))
|
|
190
198
|
return McpExecuteResponse.model_validate(data)
|
|
@@ -70,6 +70,8 @@ class Skills:
|
|
|
70
70
|
team_id: str | None = None,
|
|
71
71
|
**kwargs: Any,
|
|
72
72
|
) -> SkillResponse:
|
|
73
|
+
# The create workspace is read from the form body, not the query.
|
|
74
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
73
75
|
return SkillResponse.model_validate(
|
|
74
76
|
self._client.post(
|
|
75
77
|
"skills",
|
|
@@ -134,6 +136,8 @@ class AsyncSkills:
|
|
|
134
136
|
team_id: str | None = None,
|
|
135
137
|
**kwargs: Any,
|
|
136
138
|
) -> SkillResponse:
|
|
139
|
+
# The create workspace is read from the form body, not the query.
|
|
140
|
+
team_id = team_id if team_id is not None else self._client.team_id
|
|
137
141
|
data = await self._client.post(
|
|
138
142
|
"skills",
|
|
139
143
|
data={"team_id": team_id, **kwargs},
|
|
@@ -13,6 +13,7 @@ from gumloop.errors import APIStatusError
|
|
|
13
13
|
from gumloop.errors import AuthenticationError
|
|
14
14
|
from tests.sdk.helpers import API_BASE
|
|
15
15
|
from tests.sdk.helpers import auth_header
|
|
16
|
+
from tests.sdk.helpers import request_json
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@respx.mock
|
|
@@ -202,3 +203,123 @@ def test_async_user_id_sets_x_auth_key_header() -> None:
|
|
|
202
203
|
asyncio.run(run())
|
|
203
204
|
|
|
204
205
|
assert route.calls[0].request.headers["x-auth-key"] == "user_123"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_AGENT_JSON = {"agent": {"id": "agent_1", "name": "Agent"}}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@respx.mock
|
|
212
|
+
def test_team_id_is_sent_as_query_param_on_every_request() -> None:
|
|
213
|
+
# Team keys are validated against ``team_id`` — it must be on every request.
|
|
214
|
+
route = respx.get(f"{API_BASE}/agents/agent_1").mock(return_value=httpx.Response(200, json=_AGENT_JSON))
|
|
215
|
+
|
|
216
|
+
Gumloop(api_key="team_key", user_id="user_123", team_id="team_1").agents.retrieve("agent_1")
|
|
217
|
+
|
|
218
|
+
assert route.calls[0].request.url.params["team_id"] == "team_1"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@respx.mock
|
|
222
|
+
def test_explicit_team_id_wins_over_client_team_id() -> None:
|
|
223
|
+
route = respx.get(f"{API_BASE}/agents").mock(return_value=httpx.Response(200, json={"agents": []}))
|
|
224
|
+
|
|
225
|
+
Gumloop(api_key="team_key", user_id="user_123", team_id="team_1").agents.list(team_id="team_2")
|
|
226
|
+
|
|
227
|
+
assert route.calls[0].request.url.params["team_id"] == "team_2"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@respx.mock
|
|
231
|
+
def test_no_team_id_omits_query_param() -> None:
|
|
232
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
233
|
+
|
|
234
|
+
Gumloop(api_key="gum_xxx", user_id="user_123").models.list()
|
|
235
|
+
|
|
236
|
+
assert "team_id" not in route.calls[0].request.url.params
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@respx.mock
|
|
240
|
+
def test_env_team_id_is_used_when_not_passed_explicitly(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
241
|
+
monkeypatch.setenv("GUMLOOP_TEAM_ID", "env_team")
|
|
242
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
243
|
+
|
|
244
|
+
Gumloop(api_key="team_key", user_id="user_123").models.list()
|
|
245
|
+
|
|
246
|
+
assert route.calls[0].request.url.params["team_id"] == "env_team"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@respx.mock
|
|
250
|
+
def test_team_id_defaults_into_mcp_execute_body() -> None:
|
|
251
|
+
# /mcp/tools/call reads team_id from the body — a query-only default would
|
|
252
|
+
# authenticate as the team but silently execute with personal credentials.
|
|
253
|
+
route = respx.post(f"{API_BASE}/mcp/tools/call").mock(return_value=httpx.Response(200, json={"results": []}))
|
|
254
|
+
|
|
255
|
+
Gumloop(api_key="team_key", user_id="user_123", team_id="team_1").mcp.execute("server_1", "tool_1")
|
|
256
|
+
|
|
257
|
+
assert request_json(route.calls[0].request)["team_id"] == "team_1"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@respx.mock
|
|
261
|
+
def test_team_id_defaults_into_agent_create_body() -> None:
|
|
262
|
+
route = respx.post(f"{API_BASE}/agents").mock(return_value=httpx.Response(201, json=_AGENT_JSON))
|
|
263
|
+
|
|
264
|
+
client = Gumloop(api_key="team_key", user_id="user_123", team_id="team_1")
|
|
265
|
+
client.agents.create(name="Agent", model_name="model")
|
|
266
|
+
client.agents.create(name="Agent", model_name="model", team_id="team_2")
|
|
267
|
+
|
|
268
|
+
assert request_json(route.calls[0].request)["team_id"] == "team_1"
|
|
269
|
+
assert request_json(route.calls[1].request)["team_id"] == "team_2"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@respx.mock
|
|
273
|
+
def test_async_team_id_is_sent_as_query_param() -> None:
|
|
274
|
+
route = respx.get(f"{API_BASE}/agents/agent_1").mock(return_value=httpx.Response(200, json=_AGENT_JSON))
|
|
275
|
+
|
|
276
|
+
async def run() -> None:
|
|
277
|
+
async with AsyncGumloop(api_key="team_key", user_id="user_123", team_id="team_1") as client:
|
|
278
|
+
await client.agents.retrieve("agent_1")
|
|
279
|
+
|
|
280
|
+
asyncio.run(run())
|
|
281
|
+
|
|
282
|
+
assert route.calls[0].request.url.params["team_id"] == "team_1"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@respx.mock
|
|
286
|
+
def test_traceparent_env_is_forwarded_as_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
287
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "env-token")
|
|
288
|
+
monkeypatch.setenv("TRACEPARENT", "00-11111111111111111111111111111111-2222222222222222-01")
|
|
289
|
+
monkeypatch.setenv("TRACESTATE", "gumloop=1")
|
|
290
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
291
|
+
|
|
292
|
+
Gumloop().models.list()
|
|
293
|
+
|
|
294
|
+
request = route.calls[0].request
|
|
295
|
+
assert request.headers["traceparent"] == "00-11111111111111111111111111111111-2222222222222222-01"
|
|
296
|
+
assert request.headers["tracestate"] == "gumloop=1"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@respx.mock
|
|
300
|
+
def test_no_trace_headers_without_traceparent_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
301
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "env-token")
|
|
302
|
+
monkeypatch.delenv("TRACEPARENT", raising=False)
|
|
303
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
304
|
+
|
|
305
|
+
Gumloop().models.list()
|
|
306
|
+
|
|
307
|
+
request = route.calls[0].request
|
|
308
|
+
assert "traceparent" not in request.headers
|
|
309
|
+
assert "tracestate" not in request.headers
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@respx.mock
|
|
313
|
+
def test_traceparent_env_is_forwarded_on_async_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
314
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "env-token")
|
|
315
|
+
monkeypatch.setenv("TRACEPARENT", "00-11111111111111111111111111111111-2222222222222222-01")
|
|
316
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
317
|
+
|
|
318
|
+
async def run() -> None:
|
|
319
|
+
async with AsyncGumloop() as client:
|
|
320
|
+
await client.models.list()
|
|
321
|
+
|
|
322
|
+
asyncio.run(run())
|
|
323
|
+
|
|
324
|
+
request = route.calls[0].request
|
|
325
|
+
assert request.headers["traceparent"] == "00-11111111111111111111111111111111-2222222222222222-01"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip
RENAMED
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip
RENAMED
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip
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
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json
RENAMED
|
File without changes
|
|
File without changes
|
{gumloop-0.4.0 → gumloop-0.4.2}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json
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
|