managed-deepagents 0.6.2.dev6__py3-none-win_amd64.whl → 0.6.2.dev7__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,12 +9,19 @@ This package provides two things:
9
9
 
10
10
  Resources use ``define_*`` factories while provider collections use namespaces:
11
11
  ``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
12
- and ``auth.supabase(...)``. Contributing a *new*
12
+ ``credentials.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
13
13
  primitive is not part of the public surface.
14
14
  """
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ from managed_deepagents._credentials import (
19
+ CredentialAuthorizationRequiredError,
20
+ CredentialNotFoundError,
21
+ CredentialOwner,
22
+ CredentialStoreError,
23
+ credentials,
24
+ )
18
25
  from managed_deepagents._managed_tools import (
19
26
  ManagedDeepAgentRuntime,
20
27
  )
@@ -31,6 +38,10 @@ from managed_deepagents.sandbox import SandboxDefinition
31
38
  from managed_deepagents.schedules import define_schedule
32
39
 
33
40
  __all__ = [
41
+ "CredentialAuthorizationRequiredError",
42
+ "CredentialNotFoundError",
43
+ "CredentialOwner",
44
+ "CredentialStoreError",
34
45
  "DeepAgentDefinition",
35
46
  "ManagedDeepAgentRuntime",
36
47
  "MemoryDefinition",
@@ -38,6 +49,7 @@ __all__ = [
38
49
  "auth",
39
50
  "channels",
40
51
  "connectors",
52
+ "credentials",
41
53
  "define_deep_agent",
42
54
  "define_identity",
43
55
  "define_memory",
@@ -0,0 +1,260 @@
1
+ """Agent Auth client used by ``credentials.get`` and the pre-run credential gate.
2
+
3
+ Talks to the shipped Agent Auth APIs (credentials + oauth-authorization-sessions),
4
+ not the aspirational ``credentials/mint`` endpoint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import urllib.parse
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass
12
+ from typing import Protocol, TypedDict
13
+
14
+ from managed_deepagents._agent_auth_http import (
15
+ AGENT_AUTH_TIMEOUT_SECONDS,
16
+ CredentialStoreError,
17
+ agent_auth_credential_query,
18
+ agent_auth_headers,
19
+ request_agent_auth_json,
20
+ )
21
+
22
+ AGENT_AUTH_PROVIDER_PAGE_SIZE = 100
23
+ AGENT_AUTH_MAX_PROVIDER_PAGES = 20
24
+
25
+ _HTTP_BAD_REQUEST = 400
26
+ _HTTP_NOT_FOUND = 404
27
+ _HTTP_CONFLICT = 409
28
+
29
+ __all__ = [
30
+ "AGENT_AUTH_TIMEOUT_SECONDS",
31
+ "AgentAuth",
32
+ "AgentAuthContext",
33
+ "AgentAuthUnknownProviderError",
34
+ "CredentialAuthorizationRequiredError",
35
+ "PendingAuthorization",
36
+ ]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class AgentAuthContext:
41
+ base_url: str
42
+ #: Deployment id, passed to oauth-authorization-sessions as ``agent_id``.
43
+ agent_id: str
44
+ #: Authenticated caller, used as ``owner_id`` for user credentials.
45
+ principal_id: str
46
+ api_key: str | None = None
47
+ workspace_id: str | None = None
48
+
49
+
50
+ class PendingAuthorization(TypedDict):
51
+ """One credential slug the caller has not granted, with its connect URL."""
52
+
53
+ slug: str
54
+ connect_url: str
55
+ auth_id: str
56
+
57
+
58
+ class CredentialAuthorizationRequiredError(RuntimeError):
59
+ def __init__(self, slugs: list[str]) -> None:
60
+ super().__init__(
61
+ f"the caller has not connected: {', '.join(slugs)}. Declare the credential "
62
+ "on a connector so the pre-run gate can request it."
63
+ )
64
+ self.slugs = slugs
65
+
66
+
67
+ class AgentAuthUnknownProviderError(RuntimeError):
68
+ def __init__(self, slug: str) -> None:
69
+ super().__init__(
70
+ f'Agent Auth has no OAuth provider registered for slug "{slug}". '
71
+ "Register the provider or fix the credential slug."
72
+ )
73
+ self.slug = slug
74
+
75
+
76
+ class AgentAuthRequest(Protocol):
77
+ """HTTP surface bound onto :class:`AgentAuth` (method, path, optional JSON body)."""
78
+
79
+ def __call__(
80
+ self,
81
+ method: str,
82
+ path: str,
83
+ *,
84
+ body: dict[str, object] | None = None,
85
+ ) -> tuple[int, dict[str, object]]: ...
86
+
87
+
88
+ class AgentAuth:
89
+ """Bound Agent Auth HTTP client. Headers, origin, and timeout live on the instance."""
90
+
91
+ def __init__(
92
+ self,
93
+ context: AgentAuthContext,
94
+ *,
95
+ request: AgentAuthRequest | None = None,
96
+ timeout: float = AGENT_AUTH_TIMEOUT_SECONDS,
97
+ ) -> None:
98
+ """Bind this client to a caller.
99
+
100
+ Args:
101
+ context: Caller/run identity and Agent Auth origin.
102
+ request: Testing only. Substitute for the default HTTP transport.
103
+ timeout: Testing only. Override the request budget in seconds.
104
+ """
105
+ self.context = context
106
+ self._timeout = timeout
107
+ self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
108
+
109
+ def read_user_access_token(self, slug: str) -> str | None:
110
+ """Read the caller's OAuth access token without starting a session.
111
+
112
+ Reads never mint, so a caller can be probed repeatedly — on every replay
113
+ of an interrupted node, for instance — without leaving authorization
114
+ sessions behind.
115
+
116
+ Returns:
117
+ The access token, or ``None`` when the caller must connect.
118
+ """
119
+ credential_id = self.find_credential_id(
120
+ owner_type="user",
121
+ owner_id=self.context.principal_id,
122
+ kind="oauth2",
123
+ slug=slug,
124
+ )
125
+ if credential_id is None:
126
+ return None
127
+ return self._fetch_access_token(credential_id)
128
+
129
+ def start_authorization_session(self, slug: str) -> PendingAuthorization:
130
+ """Start an OAuth session for a slug the caller has not granted."""
131
+ provider_id = self.resolve_oauth_provider_id(slug)
132
+ status, body = self._request(
133
+ "POST",
134
+ "/v1/agent-auth/oauth-authorization-sessions",
135
+ body={
136
+ "provider_id": provider_id,
137
+ "principal_id": self.context.principal_id,
138
+ "agent_id": self.context.agent_id,
139
+ },
140
+ )
141
+ if status >= _HTTP_BAD_REQUEST:
142
+ raise CredentialStoreError(
143
+ "Agent Auth create oauth-authorization-session failed", status
144
+ )
145
+ auth_id = str(body.get("id") or "")
146
+ connect_url = str(body.get("verification_url") or "")
147
+ if not auth_id or not connect_url:
148
+ raise CredentialStoreError(
149
+ "Agent Auth oauth-authorization-session missing id or verification_url"
150
+ )
151
+ return {"slug": slug, "connect_url": connect_url, "auth_id": auth_id}
152
+
153
+ def resolve_oauth_provider_id(self, slug: str) -> str:
154
+ """oauth-providers has no slug filter, so the match is made across pages."""
155
+ cursor: str | None = None
156
+ for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
157
+ query = {"page_size": str(AGENT_AUTH_PROVIDER_PAGE_SIZE)}
158
+ if cursor:
159
+ query["cursor"] = cursor
160
+ status, body = self._request(
161
+ "GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
162
+ )
163
+ if status >= _HTTP_BAD_REQUEST:
164
+ raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
165
+ raw_items = body.get("items")
166
+ for item in raw_items if isinstance(raw_items, list) else []:
167
+ if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
168
+ return str(item["id"])
169
+ next_cursor = body.get("next_cursor")
170
+ if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
171
+ raise AgentAuthUnknownProviderError(slug)
172
+ cursor = next_cursor
173
+ raise CredentialStoreError("Agent Auth oauth-provider list returned too many pages")
174
+
175
+ def find_credential_id(
176
+ self,
177
+ *,
178
+ owner_type: str,
179
+ owner_id: str,
180
+ kind: str,
181
+ slug: str,
182
+ ) -> str | None:
183
+ """Find a credential id by slug.
184
+
185
+ The credential key is ``credential_id``, never ``id``.
186
+ """
187
+ query = agent_auth_credential_query(
188
+ owner_type=owner_type, owner_id=owner_id, kind=kind, slug=slug
189
+ )
190
+ status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
191
+ if status >= _HTTP_BAD_REQUEST:
192
+ raise CredentialStoreError("Agent Auth list credentials failed", status)
193
+ items = body.get("items")
194
+ row = items[0] if isinstance(items, list) and items else None
195
+ credential_id = row.get("credential_id") if isinstance(row, Mapping) else None
196
+ return credential_id if isinstance(credential_id, str) else None
197
+
198
+ def read_secret(self, credential_id: str) -> str | None:
199
+ status, body = self._request(
200
+ "GET",
201
+ f"/v1/agent-auth/credentials/{_path_segment(credential_id)}/secret",
202
+ )
203
+ if status == _HTTP_NOT_FOUND:
204
+ return None
205
+ if status >= _HTTP_BAD_REQUEST:
206
+ raise CredentialStoreError("Agent Auth get secret failed", status)
207
+ secret = body.get("secret")
208
+ return secret if isinstance(secret, str) and secret else None
209
+
210
+ def _fetch_access_token(self, credential_id: str) -> str | None:
211
+ status, body = self._request(
212
+ "GET",
213
+ f"/v1/agent-auth/credentials/{_path_segment(credential_id)}/oauth2",
214
+ )
215
+ if _is_authorization_required(status, body):
216
+ return None
217
+ if status >= _HTTP_BAD_REQUEST:
218
+ raise CredentialStoreError("Agent Auth get oauth2 failed", status)
219
+ oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
220
+ token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
221
+ return token if isinstance(token, str) and token else None
222
+
223
+ def _urlopen_json(
224
+ self,
225
+ method: str,
226
+ path: str,
227
+ *,
228
+ body: dict[str, object] | None = None,
229
+ ) -> tuple[int, dict[str, object]]:
230
+ if not self.context.base_url.startswith(("http://", "https://")):
231
+ raise ValueError("Agent Auth URL must be an http(s) URL")
232
+ return request_agent_auth_json(
233
+ method,
234
+ f"{self.context.base_url}{path}",
235
+ body=body,
236
+ headers=self._headers(),
237
+ timeout=self._timeout,
238
+ )
239
+
240
+ def _headers(self) -> dict[str, str]:
241
+ return agent_auth_headers(
242
+ api_key=self.context.api_key,
243
+ workspace_id=self.context.workspace_id,
244
+ )
245
+
246
+
247
+ def _path_segment(value: str) -> str:
248
+ return urllib.parse.quote(value, safe="")
249
+
250
+
251
+ def _is_authorization_required(status: int, body: dict[str, object]) -> bool:
252
+ """Agent Auth answers 409 when a stored grant has lapsed and needs re-auth."""
253
+ if status != _HTTP_CONFLICT:
254
+ return False
255
+ detail = str(body.get("detail") or body.get("title") or body.get("error") or "").lower()
256
+ return (
257
+ body.get("error") == "authorization_required"
258
+ or "authorization is required" in detail
259
+ or "oauth authorization" in detail
260
+ )
@@ -0,0 +1,119 @@
1
+ """Shared Agent Auth HTTP transport and credential-list paging.
2
+
3
+ Used by ``credentials.get`` and the credential gate so both talk to the same
4
+ API with the same headers, redirect policy, and page walker.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from collections.abc import Mapping
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from http.client import HTTPMessage
18
+ from typing import IO
19
+
20
+ AGENT_AUTH_TIMEOUT_SECONDS = 10.0
21
+
22
+
23
+ class CredentialStoreError(RuntimeError):
24
+ """Agent Auth could not answer.
25
+
26
+ Callers pass a fixed message: a response body can quote secret material, so
27
+ it never reaches this error, the logs, or the traceback chain.
28
+ """
29
+
30
+ def __init__(self, message: str, status: int | None = None) -> None:
31
+ self.status = status
32
+ super().__init__(message if status is None else f"{message} (status {status})")
33
+
34
+
35
+ __all__ = [
36
+ "AGENT_AUTH_TIMEOUT_SECONDS",
37
+ "CredentialStoreError",
38
+ "agent_auth_credential_query",
39
+ "agent_auth_headers",
40
+ "request_agent_auth_json",
41
+ ]
42
+
43
+
44
+ def agent_auth_credential_query(
45
+ *,
46
+ owner_type: str,
47
+ owner_id: str,
48
+ kind: str,
49
+ slug: str,
50
+ ) -> str:
51
+ """Query string that addresses one credential by its natural key."""
52
+ return urllib.parse.urlencode(
53
+ {"owner_type": owner_type, "owner_id": owner_id, "kind": kind, "slug": slug}
54
+ )
55
+
56
+
57
+ def agent_auth_headers(
58
+ *,
59
+ api_key: str | None = None,
60
+ workspace_id: str | None = None,
61
+ ) -> dict[str, str]:
62
+ headers = {
63
+ "accept": "application/json",
64
+ "content-type": "application/json",
65
+ "x-langsmith-product": "mda",
66
+ }
67
+ resolved_api_key = (api_key or "").strip()
68
+ if resolved_api_key:
69
+ headers["x-api-key"] = resolved_api_key
70
+ resolved_workspace = (workspace_id or "").strip()
71
+ if resolved_workspace:
72
+ headers["x-tenant-id"] = resolved_workspace
73
+ return headers
74
+
75
+
76
+ def _decode_json_object(raw: bytes) -> dict[str, object]:
77
+ try:
78
+ payload = json.loads(raw.decode("utf-8") or "{}")
79
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
80
+ return {}
81
+ return payload if isinstance(payload, dict) else {}
82
+
83
+
84
+ class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
85
+ def redirect_request(
86
+ self,
87
+ req: urllib.request.Request,
88
+ fp: IO[bytes],
89
+ code: int,
90
+ msg: str,
91
+ headers: HTTPMessage,
92
+ newurl: str,
93
+ ) -> urllib.request.Request | None:
94
+ del req, fp, code, msg, headers, newurl
95
+ return None
96
+
97
+
98
+ def request_agent_auth_json(
99
+ method: str,
100
+ url: str,
101
+ *,
102
+ body: Mapping[str, object] | None = None,
103
+ headers: Mapping[str, str],
104
+ timeout: float,
105
+ ) -> tuple[int, dict[str, object]]:
106
+ """Issue one Agent Auth HTTP call: no redirects, bounded timeout, JSON body."""
107
+ data = None if body is None else json.dumps(dict(body)).encode("utf-8")
108
+ request = urllib.request.Request( # noqa: S310
109
+ url,
110
+ data=data,
111
+ headers=dict(headers),
112
+ method=method,
113
+ )
114
+ try:
115
+ opener = urllib.request.build_opener(_NoRedirectHandler())
116
+ with opener.open(request, timeout=timeout) as response:
117
+ return int(response.status), _decode_json_object(response.read())
118
+ except urllib.error.HTTPError as exc:
119
+ return int(exc.code), _decode_json_object(exc.read())
Binary file
@@ -2,31 +2,30 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import os
6
- import uuid
7
7
  from collections.abc import Awaitable, Callable, Mapping
8
8
  from importlib import import_module
9
9
  from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
10
10
 
11
11
  from managed_deepagents._channels.assistant_text import extract_assistant_text
12
12
  from managed_deepagents._channels.binding import SLACK_BOT_TOKEN_ENV
13
- from managed_deepagents._channels.trigger.client import create_trigger_client
14
13
  from managed_deepagents._identity_runtime import (
15
14
  _ambient_configurable,
16
15
  _configurable_from_runtime,
17
16
  )
18
- from managed_deepagents._log import logger
19
17
  from managed_deepagents._memory.async_util import await_if_needed
20
18
 
21
19
  if TYPE_CHECKING:
22
20
  from langchain.agents.middleware.types import AgentMiddleware
23
21
 
24
- from managed_deepagents._channels.trigger.types import TriggerChannelActionRequest
25
22
  from managed_deepagents._channels.types import ChannelAddress
26
23
  else:
27
24
  # Authoring-light installs may lack langchain; annotations stay deferred.
28
25
  AgentMiddleware = object
29
26
 
27
+ _LOG = logging.getLogger("mda")
28
+
30
29
  _schedule_metadata_for_tests: Callable[[], Mapping[str, object] | None] | None = None
31
30
 
32
31
 
@@ -59,16 +58,6 @@ class NormalizedScheduleDeliverTo(TypedDict):
59
58
  auto_post: bool
60
59
 
61
60
 
62
- def get_deployment_id(env: Mapping[str, str] | None = None) -> str | None:
63
- """Return the deployment ID supplied by the managed runtime."""
64
- resolved_env = os.environ if env is None else env
65
- for key in ("LANGSMITH_HOST_PROJECT_ID", "LANGGRAPH_CLOUD_ID"):
66
- value = (resolved_env.get(key) or "").strip()
67
- if value:
68
- return value
69
- return None
70
-
71
-
72
61
  def read_schedule_deliver_to( # noqa: PLR0911 - validation short-circuits
73
62
  configurable: Mapping[str, object] | None,
74
63
  ) -> NormalizedScheduleDeliverTo | None:
@@ -133,7 +122,7 @@ def address_from_deliver_to(to: _ProviderConversation | _ProviderThread) -> Chan
133
122
  return {"conversation_id": to["conversation_id"]}
134
123
 
135
124
 
136
- async def deliver_schedule_result( # noqa: C901, PLR0911 - soft-fail outcomes
125
+ async def deliver_schedule_result( # noqa: PLR0911 - soft-fail outcomes
137
126
  *,
138
127
  deliver_to: NormalizedScheduleDeliverTo,
139
128
  reply_text: str,
@@ -145,9 +134,9 @@ async def deliver_schedule_result( # noqa: C901, PLR0911 - soft-fail outcomes
145
134
  if not text or not deliver_to["auto_post"]:
146
135
  return "skipped"
147
136
  if deliver_to["channel"] != "slack":
148
- logger.error(
149
- 'schedule deliver_to channel "%s" is not supported yet',
150
- logger.log_identifier(deliver_to["channel"]),
137
+ _LOG.error(
138
+ "schedule deliver_to channel %r is not supported yet",
139
+ deliver_to["channel"],
151
140
  )
152
141
  return "failed"
153
142
 
@@ -162,47 +151,15 @@ async def deliver_schedule_result( # noqa: C901, PLR0911 - soft-fail outcomes
162
151
  if hasattr(result, "__await__"):
163
152
  await cast("Awaitable[object]", result)
164
153
  return "posted"
165
- except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
166
- logger.error(
167
- "schedule Slack delivery failed (%s)",
168
- logger.safe_error_label(error),
169
- )
170
- return "failed"
171
-
172
- deployment_id = get_deployment_id()
173
- try:
174
- client = create_trigger_client()
175
- except ValueError:
176
- client = None
177
- if client is not None and deployment_id:
178
- action: TriggerChannelActionRequest = {
179
- "type": "post",
180
- "action_id": str(uuid.uuid4()),
181
- "address": dict(address),
182
- "message": {"text": text},
183
- }
184
- try:
185
- await client.post_deployment_action(
186
- deployment_id,
187
- deliver_to["channel"],
188
- action,
189
- )
190
- return "posted"
191
- except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
192
- logger.error(
193
- "schedule Trigger channel delivery failed (%s)",
194
- logger.safe_error_label(error),
195
- )
154
+ except Exception: # noqa: BLE001 - soft-fail schedule delivery
155
+ _LOG.exception("schedule Slack delivery failed")
196
156
  return "failed"
197
157
 
198
158
  token = (
199
159
  bot_token if bot_token is not None else os.environ.get(SLACK_BOT_TOKEN_ENV) or ""
200
160
  ).strip()
201
161
  if not token:
202
- logger.error(
203
- "schedule deliver_to requires Trigger channel configuration or "
204
- "SLACK_BOT_TOKEN; skipping auto-post"
205
- )
162
+ _LOG.error("schedule deliver_to requires SLACK_BOT_TOKEN; skipping auto-post")
206
163
  return "failed"
207
164
 
208
165
  try:
@@ -219,11 +176,8 @@ async def deliver_schedule_result( # noqa: C901, PLR0911 - soft-fail outcomes
219
176
  if hasattr(posted, "__await__"):
220
177
  await cast("Awaitable[object]", posted)
221
178
  return "posted"
222
- except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
223
- logger.error(
224
- "schedule Slack delivery failed (%s)",
225
- logger.safe_error_label(error),
226
- )
179
+ except Exception: # noqa: BLE001 - soft-fail schedule delivery
180
+ _LOG.exception("schedule Slack delivery failed")
227
181
  return "failed"
228
182
 
229
183
 
@@ -343,7 +297,6 @@ __all__ = [
343
297
  "address_from_deliver_to",
344
298
  "create_schedule_delivery_middleware",
345
299
  "deliver_schedule_result",
346
- "get_deployment_id",
347
300
  "is_trusted_schedule_delivery_run",
348
301
  "read_schedule_deliver_to",
349
302
  ]
@@ -93,41 +93,18 @@ class TriggerClient(TriggerClientLike):
93
93
  channel_id: str,
94
94
  body: TriggerChannelActionRequest,
95
95
  ) -> TriggerChannelActionResponse:
96
- encoded_channel_id = urllib.parse.quote(channel_id, safe="")
97
- response = await self._post(
98
- "actions",
99
- f"{self._api_base}/channels/{encoded_channel_id}/actions",
100
- body,
101
- )
102
- return _parse_action_response(response)
103
-
104
- async def post_deployment_action(
105
- self,
106
- deployment_id: str,
107
- channel_name: str,
108
- body: TriggerChannelActionRequest,
109
- ) -> TriggerChannelActionResponse:
110
- query = urllib.parse.urlencode(
111
- {"deployment_id": deployment_id, "channel_name": channel_name}
112
- )
113
- response = await self._post(
114
- "actions",
115
- f"{self._api_base}/channels/actions?{query}",
116
- body,
117
- )
118
- return _parse_action_response(response)
96
+ response = await self._post("actions", channel_id, body)
97
+ response_id = response.get("id") if isinstance(response, dict) else None
98
+ if not isinstance(response_id, str) or not response_id.strip():
99
+ raise TriggerClientError("actions")
100
+ return {"id": response_id}
119
101
 
120
102
  async def post_runtime_event(
121
103
  self,
122
104
  channel_id: str,
123
105
  body: TriggerChannelRuntimeEventRequest,
124
106
  ) -> TriggerChannelRuntimeEventResponse:
125
- encoded_channel_id = urllib.parse.quote(channel_id, safe="")
126
- response = await self._post(
127
- "events",
128
- f"{self._api_base}/channels/{encoded_channel_id}/events",
129
- body,
130
- )
107
+ response = await self._post("events", channel_id, body)
131
108
  status = response.get("status") if isinstance(response, dict) else None
132
109
  if not isinstance(status, str):
133
110
  raise TriggerClientError("events")
@@ -136,9 +113,11 @@ class TriggerClient(TriggerClientLike):
136
113
  async def _post(
137
114
  self,
138
115
  operation: TriggerClientOperation,
139
- url: str,
116
+ channel_id: str,
140
117
  body: TriggerChannelActionRequest | TriggerChannelRuntimeEventRequest,
141
118
  ) -> object:
119
+ encoded_channel_id = urllib.parse.quote(channel_id, safe="")
120
+ url = f"{self._api_base}/channels/{encoded_channel_id}/{operation}"
142
121
  headers = {
143
122
  "content-type": "application/json",
144
123
  "x-api-key": self._api_key,
@@ -165,13 +144,6 @@ class TriggerClient(TriggerClientLike):
165
144
  raise TriggerClientError(operation) from None
166
145
 
167
146
 
168
- def _parse_action_response(response: object) -> TriggerChannelActionResponse:
169
- response_id = response.get("id") if isinstance(response, dict) else None
170
- if not isinstance(response_id, str) or not response_id.strip():
171
- raise TriggerClientError("actions")
172
- return {"id": response_id}
173
-
174
-
175
147
  def create_trigger_client(
176
148
  *,
177
149
  user_id: str | None = None,
@@ -66,15 +66,12 @@ class TriggerChannelActionMessage(TypedDict):
66
66
  text: str
67
67
 
68
68
 
69
- class TriggerChannelActionRequestRequired(TypedDict):
69
+ class TriggerChannelActionRequest(TypedDict):
70
70
  type: Literal["post"]
71
71
  action_id: str
72
72
  address: TriggerChannelAddress
73
- message: TriggerChannelActionMessage
74
-
75
-
76
- class TriggerChannelActionRequest(TriggerChannelActionRequestRequired, total=False):
77
73
  source_thread_key: str
74
+ message: TriggerChannelActionMessage
78
75
 
79
76
 
80
77
  class TriggerChannelActionResponse(TypedDict):