gabriel-slack 0.1.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.
- gabriel_slack-0.1.0/.gitignore +32 -0
- gabriel_slack-0.1.0/CHANGELOG.md +20 -0
- gabriel_slack-0.1.0/LICENSE +21 -0
- gabriel_slack-0.1.0/PKG-INFO +324 -0
- gabriel_slack-0.1.0/README.md +299 -0
- gabriel_slack-0.1.0/examples/mention_stream_reply.py +62 -0
- gabriel_slack-0.1.0/examples/raw_core.py +40 -0
- gabriel_slack-0.1.0/examples/smoke_readonly.py +126 -0
- gabriel_slack-0.1.0/pyproject.toml +69 -0
- gabriel_slack-0.1.0/src/gabriel/__init__.py +139 -0
- gabriel_slack-0.1.0/src/gabriel/__main__.py +7 -0
- gabriel_slack-0.1.0/src/gabriel/cli/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/cli/browser.py +200 -0
- gabriel_slack-0.1.0/src/gabriel/cli/main.py +130 -0
- gabriel_slack-0.1.0/src/gabriel/cli/session_store.py +132 -0
- gabriel_slack-0.1.0/src/gabriel/client/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/client/apis.py +711 -0
- gabriel_slack-0.1.0/src/gabriel/client/chat.py +462 -0
- gabriel_slack-0.1.0/src/gabriel/client/client.py +1464 -0
- gabriel_slack-0.1.0/src/gabriel/client/context.py +246 -0
- gabriel_slack-0.1.0/src/gabriel/client/executor.py +209 -0
- gabriel_slack-0.1.0/src/gabriel/client/files.py +273 -0
- gabriel_slack-0.1.0/src/gabriel/client/hydrator.py +363 -0
- gabriel_slack-0.1.0/src/gabriel/client/llm_context.py +66 -0
- gabriel_slack-0.1.0/src/gabriel/client/loader.py +142 -0
- gabriel_slack-0.1.0/src/gabriel/client/origin.py +137 -0
- gabriel_slack-0.1.0/src/gabriel/client/presence.py +498 -0
- gabriel_slack-0.1.0/src/gabriel/client/recovery.py +356 -0
- gabriel_slack-0.1.0/src/gabriel/client/scope.py +544 -0
- gabriel_slack-0.1.0/src/gabriel/client/stream.py +301 -0
- gabriel_slack-0.1.0/src/gabriel/client/unread.py +115 -0
- gabriel_slack-0.1.0/src/gabriel/core/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/core/auth.py +434 -0
- gabriel_slack-0.1.0/src/gabriel/core/core.py +341 -0
- gabriel_slack-0.1.0/src/gabriel/core/http.py +416 -0
- gabriel_slack-0.1.0/src/gabriel/core/metadata.py +67 -0
- gabriel_slack-0.1.0/src/gabriel/core/types.py +63 -0
- gabriel_slack-0.1.0/src/gabriel/core/ws.py +763 -0
- gabriel_slack-0.1.0/src/gabriel/errors.py +140 -0
- gabriel_slack-0.1.0/src/gabriel/events/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/events/normalize.py +1031 -0
- gabriel_slack-0.1.0/src/gabriel/events/types.py +616 -0
- gabriel_slack-0.1.0/src/gabriel/markdown/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/markdown/blocks_to_markdown.py +403 -0
- gabriel_slack-0.1.0/src/gabriel/markdown/chunker.py +310 -0
- gabriel_slack-0.1.0/src/gabriel/py.typed +0 -0
- gabriel_slack-0.1.0/src/gabriel/state/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/state/file.py +98 -0
- gabriel_slack-0.1.0/src/gabriel/state/store.py +91 -0
- gabriel_slack-0.1.0/src/gabriel/util/__init__.py +0 -0
- gabriel_slack-0.1.0/src/gabriel/util/aio.py +260 -0
- gabriel_slack-0.1.0/src/gabriel/util/logger.py +98 -0
- gabriel_slack-0.1.0/src/gabriel/util/lru.py +69 -0
- gabriel_slack-0.1.0/src/gabriel/util/redact.py +31 -0
- gabriel_slack-0.1.0/src/gabriel/util/semaphore.py +155 -0
- gabriel_slack-0.1.0/src/gabriel/util/validate.py +110 -0
- gabriel_slack-0.1.0/tests/conftest.py +50 -0
- gabriel_slack-0.1.0/tests/harness.py +406 -0
- gabriel_slack-0.1.0/tests/test_cli.py +82 -0
- gabriel_slack-0.1.0/tests/test_client.py +887 -0
- gabriel_slack-0.1.0/tests/test_client_primitives.py +251 -0
- gabriel_slack-0.1.0/tests/test_conformance.py +213 -0
- gabriel_slack-0.1.0/tests/test_core.py +721 -0
- gabriel_slack-0.1.0/tests/test_markdown.py +279 -0
- gabriel_slack-0.1.0/tests/test_primitives.py +89 -0
- gabriel_slack-0.1.0/uv.lock +820 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
coverage/
|
|
4
|
+
*.log
|
|
5
|
+
.DS_Store
|
|
6
|
+
|
|
7
|
+
# Credentials and local state — never commit
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
!.env.example
|
|
11
|
+
!.env.sample
|
|
12
|
+
!.env.template
|
|
13
|
+
.gabriel/
|
|
14
|
+
*.pem
|
|
15
|
+
*.key
|
|
16
|
+
|
|
17
|
+
# Local-only leak-check word list (contains the very identifiers it guards against)
|
|
18
|
+
.oss-denylist.txt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Private research repository (Easy-Ez/gabriel-slack-internal), never part of the public repo
|
|
22
|
+
internal/
|
|
23
|
+
# Legacy location of the local phase-1 credentials file
|
|
24
|
+
mvp-demo/
|
|
25
|
+
|
|
26
|
+
# Python
|
|
27
|
+
__pycache__/
|
|
28
|
+
*.py[cod]
|
|
29
|
+
.venv/
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
.ruff_cache/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Python SDK are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Initial Python / asyncio implementation of the Gabriel contract, at parity with the TypeScript
|
|
14
|
+
reference SDK: `GabrielCore` (cookie exchange, token refresh / `AUTH_DEAD`, Web API + EdgeAPI,
|
|
15
|
+
WebSocket state machine) and `GabrielClient` (typed events, hydration, ordering, executor,
|
|
16
|
+
`wait_for_reply`, chat with chunking and streams, threads, files, search, presence, gap
|
|
17
|
+
recovery, `scope()` sandbox, `to_llm_context()`), `MemoryStateStore` / `FileStateStore` and the
|
|
18
|
+
`gabriel auth` CLI.
|
|
19
|
+
- Every conformance fixture in `spec/fixtures/normalize` passes; every event validates against
|
|
20
|
+
`spec/schema/events.schema.json`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EzEasy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gabriel-slack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed, AI-friendly real-time SDK for the Slack Web client protocol (session token + cookie)
|
|
5
|
+
Project-URL: Homepage, https://github.com/Easy-Ez/gabriel-slack/tree/main/sdks/python#readme
|
|
6
|
+
Project-URL: Repository, https://github.com/Easy-Ez/gabriel-slack
|
|
7
|
+
Project-URL: Issues, https://github.com/Easy-Ez/gabriel-slack/issues
|
|
8
|
+
Author: EzEasy
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,llm,realtime,sdk,slack,websocket
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: websockets>=14
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# gabriel-slack (Python)
|
|
27
|
+
|
|
28
|
+
Python / asyncio implementation of [Gabriel](https://github.com/Easy-Ez/gabriel-slack/blob/main/README.md): a typed,
|
|
29
|
+
AI-friendly real-time SDK for the Slack Web client protocol. It implements the same contract as
|
|
30
|
+
the TypeScript reference SDK.
|
|
31
|
+
|
|
32
|
+
> Unofficial and not affiliated with Slack. It acts as the signed-in person: use it only
|
|
33
|
+
> where you are authorized to automate, and treat the session cookie like a password.
|
|
34
|
+
|
|
35
|
+
* Python **≥ 3.11**, asyncio, fully typed (`py.typed`, `mypy --strict` clean).
|
|
36
|
+
* Runtime dependencies: [`httpx`](https://www.python-httpx.org/) and [`websockets`](https://websockets.readthedocs.io/).
|
|
37
|
+
* Behavior contract: [`spec/behavior.md`](https://github.com/Easy-Ez/gabriel-slack/blob/main/spec/behavior.md); every
|
|
38
|
+
[conformance fixture](https://github.com/Easy-Ez/gabriel-slack/tree/main/spec/fixtures/normalize) passes and every
|
|
39
|
+
event validates against [`events.schema.json`](https://github.com/Easy-Ez/gabriel-slack/blob/main/spec/schema/events.schema.json).
|
|
40
|
+
|
|
41
|
+
Install from PyPI (the distribution is `gabriel-slack`, the import package `gabriel`):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install gabriel-slack # or: uv add gabriel-slack
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Contents
|
|
48
|
+
|
|
49
|
+
1. [Credentials](#credentials)
|
|
50
|
+
2. [Quick start](#quick-start)
|
|
51
|
+
3. [Events](#events)
|
|
52
|
+
4. [Handler context](#handler-context)
|
|
53
|
+
5. [Actions](#actions)
|
|
54
|
+
6. [AI safety: `scope()` and `to_llm_context()`](#ai-safety)
|
|
55
|
+
7. [Reliability](#reliability)
|
|
56
|
+
8. [Errors](#errors)
|
|
57
|
+
9. [Low-level core](#low-level-core)
|
|
58
|
+
10. [Configuration reference](#configuration-reference)
|
|
59
|
+
11. [CLI](#cli)
|
|
60
|
+
12. [Python conventions](#python-conventions)
|
|
61
|
+
13. [Development](#development)
|
|
62
|
+
|
|
63
|
+
## Credentials
|
|
64
|
+
|
|
65
|
+
The only required credential is the `d` cookie (`xoxd-…`) of a signed-in Slack web session.
|
|
66
|
+
The SDK exchanges it for the workspace token (`xoxc-…`), team ID and user ID, refreshes the
|
|
67
|
+
token on `invalid_auth` (concurrent failures share one refresh), and stops with `auth.dead`
|
|
68
|
+
when the cookie itself expires.
|
|
69
|
+
|
|
70
|
+
* Load it from the environment or a secret manager: `GABRIEL_COOKIE_D` (and optionally
|
|
71
|
+
`GABRIEL_WORKSPACE`).
|
|
72
|
+
* For local development, `gabriel auth parse-curl --save` stores it in
|
|
73
|
+
`~/.config/gabriel/auth.json` (mode 0600, shared with the Node CLI); `load_session_cookie()`
|
|
74
|
+
reads the environment first, then that file.
|
|
75
|
+
* Use `allowed_teams` to refuse connecting to any other workspace.
|
|
76
|
+
|
|
77
|
+
The SDK never logs, emits or returns tokens or cookies; reprs of credential objects are redacted.
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import asyncio
|
|
83
|
+
from gabriel import GabrielClient, MessageCreatedEvent, load_session_cookie
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
session = load_session_cookie()
|
|
87
|
+
client = GabrielClient.from_cookie(
|
|
88
|
+
cookie_d=session.cookie_d, workspace=session.workspace, allowed_teams=["T0123456789"]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@client.on_message(mentions_me=True)
|
|
92
|
+
async def greet(evt: MessageCreatedEvent, ctx) -> None:
|
|
93
|
+
if evt.origin != "other_user":
|
|
94
|
+
return # skip bots and your own messages typed elsewhere
|
|
95
|
+
await ctx.reply(f"Hi {evt.sender.display_name or 'there'} 👋")
|
|
96
|
+
|
|
97
|
+
@client.on("auth.dead")
|
|
98
|
+
def dead(evt, ctx) -> None:
|
|
99
|
+
alert(f"Gabriel session expired: {evt.reason}")
|
|
100
|
+
|
|
101
|
+
client.on_error(lambda err, evt: print(evt and evt.event_id, err))
|
|
102
|
+
|
|
103
|
+
async with client: # start() … stop(drain=10.0)
|
|
104
|
+
await asyncio.Event().wait()
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
More in [`examples/`](https://github.com/Easy-Ez/gabriel-slack/tree/main/sdks/python/examples): a streamed reply to mentions, a read-only live smoke test and
|
|
110
|
+
raw frames. Run one with `GABRIEL_COOKIE_D=… uv run python examples/mention_stream_reply.py`.
|
|
111
|
+
|
|
112
|
+
## Events
|
|
113
|
+
|
|
114
|
+
Subscribe with `client.on(type_or_class, handler)`, `client.on_message(matcher, handler)`,
|
|
115
|
+
`client.on_thread_reply(handler)` — each also works as a decorator — or pull with
|
|
116
|
+
`async for evt in client.events()`. Handlers receive `(evt, ctx)` and may be sync or async.
|
|
117
|
+
|
|
118
|
+
Every event is a dataclass with snake_case attributes mapping 1:1 to the schema's camelCase
|
|
119
|
+
fields; `evt.to_dict()` / `evt.to_json()` produce the schema form. Select events by type string
|
|
120
|
+
(`"message.created"`) or by class (`MessageCreatedEvent`).
|
|
121
|
+
|
|
122
|
+
| Event | Class | When |
|
|
123
|
+
| :--- | :--- | :--- |
|
|
124
|
+
| `message.created` | `MessageCreatedEvent` | Any new message: channel, thread reply, broadcast, file share, bot. |
|
|
125
|
+
| `message.updated` | `MessageUpdatedEvent` | The body actually changed (metadata-only updates are filtered). |
|
|
126
|
+
| `message.deleted` | `MessageDeletedEvent` | A message was deleted (`deleted_ts`). |
|
|
127
|
+
| `message.thread_meta_updated` | `MessageThreadMetaUpdatedEvent` | A thread's reply count / latest reply changed. |
|
|
128
|
+
| `message.other` | `MessageOtherEvent` | Message subtypes without a dedicated model. |
|
|
129
|
+
| `ai.stream_updated` / `ai.stream_completed` | `AiStreamUpdatedEvent` / `AiStreamCompletedEvent` | Another AI app streams / finishes a reply. |
|
|
130
|
+
| `presence.changed` | `PresenceChangedEvent` | A watched user switched `active` ⇄ `away` (`is_initial_snapshot` for the first value). |
|
|
131
|
+
| `user.status_changed` / `user.dnd_changed` | `UserStatusChangedEvent` / `UserDndChangedEvent` | A watched user's custom status / DND changed. |
|
|
132
|
+
| `user.subteam_updated` | `UserSubteamUpdatedEvent` | A user group's members changed. |
|
|
133
|
+
| `interaction.typing` | `TypingEvent` | Someone is typing. |
|
|
134
|
+
| `interaction.reaction_added` / `_removed` | `ReactionAddedEvent` / `ReactionRemovedEvent` | Reaction on a message (`target.thread_root_ts` resolved) or file. |
|
|
135
|
+
| `channel.lifecycle` | `ChannelLifecycleEvent` | Joined / left / DM opened / renamed / archived. |
|
|
136
|
+
| `channel.marked` | `ChannelMarkedEvent` | Your read cursor moved (the SDK's own cursor changes are hidden). |
|
|
137
|
+
| `unread.activity` / `unread.badge_counts` | `UnreadActivityEvent` / `UnreadBadgeCountsEvent` | Activity item / badge snapshot (deduplicated). |
|
|
138
|
+
| `connection.ready` / `.reconnecting` / `.gap` | `Connection*Event` | Connection lifecycle and backfill reports. |
|
|
139
|
+
| `auth.refreshed` / `auth.dead` | `AuthRefreshedEvent` / `AuthDeadEvent` | Token refreshed / session expired (terminal). |
|
|
140
|
+
| `guard.loop_detected` / `.protocol_drift` / `.overflow` | `LoopDetectedEvent` / `ProtocolDriftEvent` / `OverflowEvent` | Circuit breaker / unknown frame / event dropped under load. |
|
|
141
|
+
|
|
142
|
+
Key fields of `MessageCreatedEvent`: `ts`, `channel` (`id`, `name`, `is_private`, `is_im`,
|
|
143
|
+
`is_mpim`), `sender` (`id`, `display_name`, `handle`, `is_bot`, `is_external`), `origin`, `text`
|
|
144
|
+
(raw mrkdwn), `markdown` (standard Markdown, mentions rendered as `@Name`), `thread`
|
|
145
|
+
(`root_ts`, `is_reply`, `broadcast` or `None`), `is_thread_reply`, `thread_ts`, `mentions`,
|
|
146
|
+
`is_mentioning_me`, `files`, `delivery` (`realtime` | `backfill`), `raw`.
|
|
147
|
+
|
|
148
|
+
**`origin`** distinguishes `this_client` (an echo of this SDK instance's own writes, dropped
|
|
149
|
+
unless `LoopGuardOptions(include_self_messages=True)`), `self_other_client`, `other_user` and `bot`.
|
|
150
|
+
|
|
151
|
+
## Handler context
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
@client.on(MessageCreatedEvent)
|
|
155
|
+
async def handle(evt, ctx):
|
|
156
|
+
ctx.signal # AbortSignal: aborted on delete, timeout or stop()
|
|
157
|
+
await ctx.reply("text") # in the trigger's thread (in_thread=False for top level)
|
|
158
|
+
stream = await ctx.reply_stream() # streaming reply in the trigger's thread
|
|
159
|
+
await ctx.react("eyes")
|
|
160
|
+
answer = await ctx.wait_for_reply(lambda r: r.thread_ts == evt.ts, timeout=60) # None on timeout
|
|
161
|
+
await ctx.hydrate_remaining() # finish name resolution if the 300 ms budget ran out
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`await ctx.signal.wait()` blocks until the signal fires; `ctx.signal.aborted` checks it.
|
|
165
|
+
`wait_for_reply` never misses a reply that arrived before it was called, releases the handler's
|
|
166
|
+
concurrency slot while waiting, and (with `consume=True`, the default) keeps the reply from
|
|
167
|
+
reaching other handlers. Handlers for one channel run one at a time in arrival order
|
|
168
|
+
(`ConcurrencyOptions(executor_key="thread")` switches to per-thread). Exceptions go to
|
|
169
|
+
`client.on_error` and never block later events. Middleware:
|
|
170
|
+
`client.use(async def mw(evt, ctx, next): …; await next())`.
|
|
171
|
+
|
|
172
|
+
## Actions
|
|
173
|
+
|
|
174
|
+
| Namespace | Methods |
|
|
175
|
+
| :--- | :--- |
|
|
176
|
+
| `client.chat` | `send(channel, markdown, *, blocks, thread_ts, broadcast, priority, preserve_unread_cursor, auto_chunk)`, `update`, `delete`, `send_typing`, `create_stream`, `stream` (async context manager) |
|
|
177
|
+
| `client.threads` | `get_context(channel, thread_ts, limit=50)` → messages, participants, `formatted_transcript`; `iterate_replies`, `mark_read`, `subscribe`, `unsubscribe` |
|
|
178
|
+
| `client.files` | `upload(filename, content, …)` (3-step, streaming), `download_stream` / `download_bytes` (`max_bytes`, sign-in page detection), `get_info`, `delete` |
|
|
179
|
+
| `client.search` | `messages`, `files`, `channels`, `people`, `iterate_messages`, `iterate_files` |
|
|
180
|
+
| `client.presence` | `watch(user_ids, track_profile_and_dnd=False)` → handle (`await h.unsubscribe()` / `async with`), `get_status(user_id)` |
|
|
181
|
+
| `client.reactions` | `add(channel, ts, emoji)`, `remove` (idempotent) |
|
|
182
|
+
| `client.users` | `resolve(id or ids)` (batched, cached), `get_dnd` |
|
|
183
|
+
| `client.channels` | `resolve`, `get_history`, `iterate_history` |
|
|
184
|
+
|
|
185
|
+
Streams: `append()` is synchronous; at most one update per stream is in flight and the final
|
|
186
|
+
message equals the buffer at `finish()`. Deleting the message in Slack aborts `stream.signal`.
|
|
187
|
+
Long output continues in a new message (`continuation_ts`).
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
async with client.chat.stream("C0123456789", thread_ts=ts) as s: # finish on exit, abort on error
|
|
191
|
+
async for chunk in llm.stream(prompt):
|
|
192
|
+
s.append(chunk)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Sending behavior (chunking at 11,500 effective characters, `client_msg_id` idempotency, per-channel
|
|
196
|
+
FIFO, a reserved slot for interactive replies, `preserve_unread_cursor`) and client-side limits
|
|
197
|
+
(presence frame ≤ 16,000 bytes, profile/DND ≤ 900 users) are identical to the Node SDK.
|
|
198
|
+
|
|
199
|
+
## AI safety
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from gabriel import ReadPolicy, ScopePolicy, WritePolicy
|
|
203
|
+
|
|
204
|
+
agent = client.scope(ScopePolicy(
|
|
205
|
+
read=ReadPolicy(channels=["C0123456789"], allow_search=False, max_file_download_bytes=5 * 1024 * 1024),
|
|
206
|
+
write=WritePolicy(
|
|
207
|
+
target_scope="trigger_thread_only",
|
|
208
|
+
allowed_actions=["chat.send", "chat.create_stream", "reactions.add"],
|
|
209
|
+
disclosure_suffix="\n_— generated by an AI assistant_",
|
|
210
|
+
),
|
|
211
|
+
approval=ask_a_human, # async (action, params) -> bool
|
|
212
|
+
))
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The scoped view has the same API. Anything outside the policy raises `SCOPE_POLICY_VIOLATION`
|
|
216
|
+
and is logged. Give LLM tools the scoped view, not the client.
|
|
217
|
+
|
|
218
|
+
`evt.to_llm_context()` renders metadata separately from content and wraps the content in
|
|
219
|
+
`<slack_message untrusted="true">`. Tell the model that `<content>` is data, not instructions.
|
|
220
|
+
|
|
221
|
+
## Reliability
|
|
222
|
+
|
|
223
|
+
* **Ordering**: per-channel dispatch in arrival order while entity lookups run in parallel.
|
|
224
|
+
* **De-duplication** by `event_id`.
|
|
225
|
+
* **Recovery**: after a cold reconnect, or a restart with a persistent store, missed channel
|
|
226
|
+
messages and watched-thread replies are delivered as `delivery="backfill"` before newer realtime
|
|
227
|
+
messages, followed by a `connection.gap` report.
|
|
228
|
+
* **State stores**: `MemoryStateStore` (default) and `FileStateStore(dir)` (single host; same file
|
|
229
|
+
format as the Node SDK). Implement the `StateStore` protocol for other backends.
|
|
230
|
+
* **Shutdown**: `await client.stop(drain=10.0)` (or leaving `async with`) drains handlers, interrupts
|
|
231
|
+
open streams, flushes sends and persists state.
|
|
232
|
+
* **Health**: `client.health()` → `HealthReport(status, ws_state, auth_state, last_frame_age, queue_depth)`.
|
|
233
|
+
|
|
234
|
+
## Errors
|
|
235
|
+
|
|
236
|
+
Every API raises `GabrielError` with `code`, `retryable`, `raw_slack_error` and `retry_after`
|
|
237
|
+
(seconds). Retryable errors were already retried — don't add another retry loop.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from gabriel import GabrielError
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
await agent.chat.send(channel, markdown, thread_ts=ts)
|
|
244
|
+
except GabrielError as err:
|
|
245
|
+
if err.code == "SCOPE_POLICY_VIOLATION":
|
|
246
|
+
return "not allowed"
|
|
247
|
+
raise
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Codes: see [`spec/behavior.md` §12](https://github.com/Easy-Ez/gabriel-slack/blob/main/spec/behavior.md#12-errors).
|
|
251
|
+
|
|
252
|
+
## Low-level core
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
client.core.on_event("ai_assistant_status", lambda frame: ...) # unmodified frames
|
|
256
|
+
client.core.on_message_subtype("channel_topic", lambda frame: ...)
|
|
257
|
+
await client.core.call_web_api("reminders.add", {"text": "Review", "time": "in 30 minutes"})
|
|
258
|
+
await client.core.call_edge_api("users/search", {"query": "alice", "count": 5})
|
|
259
|
+
await client.core.send_ws_command({"type": "tickle", "reason": "keepalive"})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`GabrielCore` also works on its own (`async with GabrielCore(auth=AuthOptions(cookie_d=…)) as core:`).
|
|
263
|
+
It bypasses loop guards, origin detection and the sandbox. Never send `presence_sub` yourself.
|
|
264
|
+
|
|
265
|
+
## Configuration reference
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
GabrielClient(
|
|
269
|
+
auth=AuthOptions(cookie_d=..., token=None, workspace=None, allowed_teams=None),
|
|
270
|
+
concurrency=ConcurrencyOptions(global_concurrency=32, per_key_concurrency=1, max_queue=5000,
|
|
271
|
+
max_inflight_events=10_000, executor_key="channel", handler_timeout=300.0),
|
|
272
|
+
hydration=HydrationOptions(hydrate_budget=0.3, preload_usergroups=True),
|
|
273
|
+
loop_guard=LoopGuardOptions(include_self_messages=False, max_auto_replies_per_minute=5),
|
|
274
|
+
chat=ChatSchedulerOptions(global_send_concurrency=4, item_deadline=30.0, stream_updates_per_second=2,
|
|
275
|
+
stream_min_interval=0.5, interrupted_notice="\n\n_(interrupted)_"),
|
|
276
|
+
recovery=RecoveryOptions(max_pages_per_channel=10, watched_threads_limit=100, resync_buffer_limit=10_000),
|
|
277
|
+
state_store=FileStateStore("/var/lib/my-agent/gabriel"),
|
|
278
|
+
logger=console_logger("info"), # default: stdlib logging, logger name "gabriel"
|
|
279
|
+
metrics=my_metrics, # increment(name, labels) / observe(name, value, labels)
|
|
280
|
+
advanced=AdvancedOptions(http_client=httpx.AsyncClient(proxy=...), ws_factory=..., metadata={"user_agent": ...},
|
|
281
|
+
endpoints=..., gateway_server=None, start_timeout=60.0),
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`GabrielClient.from_cookie(cookie_d=…, workspace=…, allowed_teams=…, **options)` takes the same
|
|
286
|
+
options as keyword arguments.
|
|
287
|
+
|
|
288
|
+
## CLI
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
gabriel auth login your-workspace # sign in in a dedicated Chrome window; saved with mode 0600
|
|
292
|
+
gabriel auth extract --port 9222 # read from a Chrome started with --remote-debugging-port
|
|
293
|
+
pbpaste | gabriel auth parse-curl --save # "Copy as cURL" of any request from your signed-in Slack tab
|
|
294
|
+
gabriel auth status # verify and list workspaces (secrets redacted)
|
|
295
|
+
gabriel auth logout --yes
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`python -m gabriel …` works too. The session file is shared with the Node CLI.
|
|
299
|
+
|
|
300
|
+
## Python conventions
|
|
301
|
+
|
|
302
|
+
* Durations are **float seconds** (`handler_timeout=300.0`, `retry_after`), except event payload
|
|
303
|
+
fields that the schema defines in milliseconds (`ConnectionReconnectingEvent.delay_ms`,
|
|
304
|
+
`AuthRefreshedEvent.refreshed_at`).
|
|
305
|
+
* Cancellation: methods accept `signal=AbortSignal`; ordinary `asyncio` task cancellation works too.
|
|
306
|
+
SDK-initiated cancellation surfaces as `GabrielError("CANCELLED")`.
|
|
307
|
+
* Logging: any object with `debug/info/warn/error(fields, msg)`; `stdlib_logger()` adapts `logging`.
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
cd sdks/python
|
|
313
|
+
uv sync
|
|
314
|
+
uv run pytest # unit, fake-Slack integration, conformance and schema tests
|
|
315
|
+
uv run mypy # strict
|
|
316
|
+
uv run ruff check src tests examples && uv run ruff format --check src tests examples
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Tests never touch a real workspace: [`tests/harness.py`](https://github.com/Easy-Ez/gabriel-slack/blob/main/sdks/python/tests/harness.py) is an in-memory Slack
|
|
320
|
+
(Web API, EdgeAPI, cookie exchange, WebSocket gateway).
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT © 2026 EzEasy
|