heinzel-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.
- heinzel_slack-0.1.0/.copier-answers.yml +5 -0
- heinzel_slack-0.1.0/.gitignore +10 -0
- heinzel_slack-0.1.0/AGENTS.md +39 -0
- heinzel_slack-0.1.0/CHANGELOG.md +9 -0
- heinzel_slack-0.1.0/CLAUDE.md +1 -0
- heinzel_slack-0.1.0/LICENSE +21 -0
- heinzel_slack-0.1.0/Makefile +19 -0
- heinzel_slack-0.1.0/PKG-INFO +215 -0
- heinzel_slack-0.1.0/README.md +203 -0
- heinzel_slack-0.1.0/docs/SCREENSHOT_STORY.md +38 -0
- heinzel_slack-0.1.0/docs/VENDORING.md +86 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/capture.json +84 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/index.html +1 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/review-slack-notice.png +0 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/review-slack-notice.txt +73 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-cli.png +0 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-cli.txt +15 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-decision-cli.png +0 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-decision-cli.txt +12 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-notice-delivered.png +0 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/slack-notice-delivered.txt +30 -0
- heinzel_slack-0.1.0/docs/assets/screenshots/source-story.md +38 -0
- heinzel_slack-0.1.0/docs/screenshot-definition.json +7 -0
- heinzel_slack-0.1.0/docs/screenshot_fixture.py +487 -0
- heinzel_slack-0.1.0/docs/screenshots.toml +51 -0
- heinzel_slack-0.1.0/plugin-lock.json +41 -0
- heinzel_slack-0.1.0/pyproject.toml +86 -0
- heinzel_slack-0.1.0/requirements.lock +1175 -0
- heinzel_slack-0.1.0/src/heinzel_slack/__init__.py +181 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_blocks.py +125 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_channel.py +67 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_decide.py +108 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_digest.py +40 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_models.py +34 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_module.py +5 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_sdk.py +64 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_service.py +455 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_settings.py +63 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_slack.py +297 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_store.py +333 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/__init__.py +1 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/LICENSE +21 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/__init__.py +54 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/errors/__init__.py +56 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/__init__.py +48 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/builtin_handlers.py +110 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/builtin_interval_calculators.py +45 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/handler.py +82 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/interval_calculator.py +13 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/jitter.py +25 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/request.py +36 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/response.py +23 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/state.py +21 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/__init__.py +57 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/attachments/__init__.py +590 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/basic_objects.py +132 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/__init__.py +162 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/basic_components.py +742 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/block_elements.py +2335 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/blocks.py +1302 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/messages/__init__.py +91 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/messages/chunk.py +159 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/metadata/__init__.py +1255 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/views/__init__.py +232 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/proxy_env_variable_loader.py +25 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/py.typed +0 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/__init__.py +13 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/__init__.py +5 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/client.py +294 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/connection.py +449 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/frame_header.py +47 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/internals.py +413 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/client.py +159 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/interval_runner.py +33 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/listeners.py +17 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/logger/__init__.py +0 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/logger/messages.py +6 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/request.py +57 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/response.py +28 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/version.py +3 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/__init__.py +12 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/base_client.py +640 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/chat_stream.py +243 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/client.py +6649 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/deprecation.py +52 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/file_upload_v2_result.py +7 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/internal_utils.py +440 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/slack_response.py +196 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/__init__.py +10 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/client.py +279 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/internal_utils.py +45 -0
- heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/webhook_response.py +16 -0
- heinzel_slack-0.1.0/src/heinzel_slack/links/__init__.py +15 -0
- heinzel_slack-0.1.0/src/heinzel_slack/links/_cli_process.py +61 -0
- heinzel_slack-0.1.0/src/heinzel_slack/links/_reviewed_link.py +366 -0
- heinzel_slack-0.1.0/src/heinzel_slack/manifest.json +25 -0
- heinzel_slack-0.1.0/src/heinzel_slack/py.typed +0 -0
- heinzel_slack-0.1.0/tests/acceptance/channel_host_test.py +98 -0
- heinzel_slack-0.1.0/tests/acceptance/install_test.py +51 -0
- heinzel_slack-0.1.0/tests/acceptance/links_registration_test.py +63 -0
- heinzel_slack-0.1.0/tests/conftest.py +165 -0
- heinzel_slack-0.1.0/tests/e2e/live_test.py +83 -0
- heinzel_slack-0.1.0/tests/fixtures/captured_writer/pyproject.toml +18 -0
- heinzel_slack-0.1.0/tests/fixtures/captured_writer/src/fixture_captured_writer/__init__.py +66 -0
- heinzel_slack-0.1.0/tests/fixtures/channel_host_probe.py +246 -0
- heinzel_slack-0.1.0/tests/integration/approval_test.py +352 -0
- heinzel_slack-0.1.0/tests/integration/cli_process_test.py +31 -0
- heinzel_slack-0.1.0/tests/integration/manifest_test.py +25 -0
- heinzel_slack-0.1.0/tests/integration/notify_test.py +57 -0
- heinzel_slack-0.1.0/tests/integration/operations_test.py +179 -0
- heinzel_slack-0.1.0/tests/integration/optional_bot_test.py +124 -0
- heinzel_slack-0.1.0/tests/integration/reviewed_link_test.py +273 -0
- heinzel_slack-0.1.0/tests/integration/reviewed_target_test.py +108 -0
- heinzel_slack-0.1.0/tests/integration/sdk_boundary_test.py +196 -0
- heinzel_slack-0.1.0/tests/integration/socket_mask_test.py +76 -0
- heinzel_slack-0.1.0/tests/integration/socket_transport_test.py +252 -0
- heinzel_slack-0.1.0/tests/integration/store_security_test.py +161 -0
- heinzel_slack-0.1.0/tests/integration/vendor_typing_test.py +73 -0
- heinzel_slack-0.1.0/tests/integration/web_api_test.py +221 -0
- heinzel_slack-0.1.0/tests/unit/blocks_test.py +47 -0
- heinzel_slack-0.1.0/tests/unit/digest_test.py +26 -0
- heinzel_slack-0.1.0/vendor-manifest.json +326 -0
- heinzel_slack-0.1.0/vendor-provenance/slack_sdk-3.46.0-py2.py3-none-any.whl +0 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Generated by Copier. Keep this file so the template can be updated.
|
|
2
|
+
_commit: v0.9.8
|
|
3
|
+
_src_path: gh:helmut-hoffer-von-ankershoffen/heinzel
|
|
4
|
+
plugin_name: slack
|
|
5
|
+
plugin_summary: Tell the owner what needs them, and ask for approval of an exact draft, in their own Slack DM.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# heinzel-slack
|
|
2
|
+
|
|
3
|
+
Read [README.md](README.md). Edit the feature in `src/heinzel_slack/` and its tests; use the public `heinzel`
|
|
4
|
+
and `heinzel.kernel` API. One `@module.operation` supplies CLI, MCP and REST. Add a service or model when the
|
|
5
|
+
feature needs it; neither is required for a small operation. Never import a plugin implementation or core internals.
|
|
6
|
+
|
|
7
|
+
Run from this plugin's directory:
|
|
8
|
+
```sh
|
|
9
|
+
make setup
|
|
10
|
+
make check-fast
|
|
11
|
+
make check
|
|
12
|
+
make build
|
|
13
|
+
make acceptance
|
|
14
|
+
make preview
|
|
15
|
+
make lock # after changing dependencies
|
|
16
|
+
make release # preview only
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keep all feature changes in this directory. Setup never initializes Git for a collection plugin. The owning
|
|
20
|
+
Makefile works even when the parent repository builds something else. A parent that provides collection commands
|
|
21
|
+
also accepts `NAME=heinzel-slack` from its root. Publication uses the parent's generic plugin release workflow;
|
|
22
|
+
do not copy a nested `.github/` or edit a parent registry.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Each pytest file declares exactly one level: `unit`, `integration` or `e2e`. Write a behavior test for a change.
|
|
26
|
+
Run the fast loop while editing and the owning full/installed-wheel checks before release. These commands install
|
|
27
|
+
a compatible core wheel; they never run the core suite. `CORE=<wheel>` explicitly selects a candidate SDK.
|
|
28
|
+
|
|
29
|
+
The source operation in the starter shows the minimum abstraction. Use the contributions preset or
|
|
30
|
+
[extension guide](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PLUGINS.md) for views,
|
|
31
|
+
priorities, Today widgets, notifications and shared contracts. Ordinary dependencies belong in this project's
|
|
32
|
+
`pyproject.toml`; regenerate its own lock. Use conventional commits and review nontrivial changes.
|
|
33
|
+
|
|
34
|
+
Publishing requires a merged commit, this plugin's version and configured GitHub/PyPI publishing. Local
|
|
35
|
+
`release` only checks/builds and previews; `publish` dispatches the protected workflow. Never upload a different
|
|
36
|
+
rebuild under an existing version. See the README's release steps.
|
|
37
|
+
Private sources use private GitHub releases and the installing person's `gh` login. PyPI is public; do not publish
|
|
38
|
+
private sources there. The [custom plugin guide](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PLUGIN_AUTHORING.md)
|
|
39
|
+
documents separate repositories, catalog discovery and both release paths.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0
|
|
6
|
+
|
|
7
|
+
First version: notices and shared drafts in the owner's own Slack direct message, through the owner's private Slack
|
|
8
|
+
app (official Web API and Socket Mode). Approve and Decline are bound to the exact draft bytes and to the owner;
|
|
9
|
+
a reply, a reaction, another person, a replaced or expired draft and a replayed click never approve.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Plugin authors
|
|
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,19 @@
|
|
|
1
|
+
.DEFAULT_GOAL := help
|
|
2
|
+
TOOLING ?= heinzel-plugin-tools @ https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/releases/download/tools-v0.3.7/heinzel_plugin_tools-0.3.7-py3-none-any.whl\#sha256=ac959c164e3726f86311c86fa9d3b4a3cbc82a6b4f81fae8bf462ee8103bd359
|
|
3
|
+
unexport TOOLING CORE TAG REGISTRY
|
|
4
|
+
export HZ_PLUGIN_TOOLING := $(value TOOLING)
|
|
5
|
+
export PLUGIN_CORE := $(value CORE)
|
|
6
|
+
export PLUGIN_TAG := $(value TAG)
|
|
7
|
+
export PLUGIN_REGISTRY := $(value REGISTRY)
|
|
8
|
+
MAKEOVERRIDES =
|
|
9
|
+
PLUGIN := uv run --quiet --no-project --with "$$HZ_PLUGIN_TOOLING" hz-plugin
|
|
10
|
+
|
|
11
|
+
.PHONY: help setup fix check-fast check test lock preview build acceptance audit release preflight publish
|
|
12
|
+
help:
|
|
13
|
+
@echo 'make setup; make check-fast; make check; make preview; make build; make acceptance'
|
|
14
|
+
@echo 'make lock refreshes this plugin dependency lock; CORE=<wheel> selects a candidate SDK.'
|
|
15
|
+
@echo 'make release previews; make publish dispatches the protected release workflow.'
|
|
16
|
+
setup fix check-fast check test lock preview build acceptance audit release preflight:
|
|
17
|
+
@$(PLUGIN) $@
|
|
18
|
+
publish:
|
|
19
|
+
@$(PLUGIN) publish
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: heinzel-slack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tell the owner what needs them, and ask for approval of an exact draft, in their own Slack DM.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: src/heinzel_slack/_vendor/slack_sdk/LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: heinzel<0.10,>=0.9.20
|
|
10
|
+
Requires-Dist: slack-sdk==3.46.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
> This plugin is in development. Its offline fixtures use fictional Slack transports and accounts.
|
|
14
|
+
> Installed acceptance against the released SDK and a new gallery review are pending. The existing
|
|
15
|
+
> captures illustrate earlier behavior.
|
|
16
|
+
|
|
17
|
+
# heinzel-slack
|
|
18
|
+
|
|
19
|
+
Slack as an extra notification channel and as a place to share a draft and get a yes or no back: a
|
|
20
|
+
[Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin, released on its own.
|
|
21
|
+
|
|
22
|
+
- **A notice says why.** Every message carries "Why you are seeing this", because a nudge that cannot say why is noise.
|
|
23
|
+
- **A draft is shared with Approve and Decline.** The answer is for the exact text you saw, and only you can give it.
|
|
24
|
+
- **Your DM only.** It writes to your direct message with your own app. Never a channel, never anyone else.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
Lotte has a draft for you
|
|
28
|
+
Weekly report
|
|
29
|
+
Why you are seeing this: It is Friday and you asked for a report.
|
|
30
|
+
| Weekly report ... |
|
|
31
|
+
Nothing goes out until you say so. Fingerprint 3fa9c1d2b4e0 · answer by 16:00 · only your click counts.
|
|
32
|
+
[ Approve ] [ Decline ]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
The bot channel needs all of these. The separate reviewed-link adapter does not use the bot or app tokens.
|
|
38
|
+
For links-only use, explicitly run `hz system config set slack.bot_enabled false` and configure the SlackCLI
|
|
39
|
+
path/profile described below. The bot's local and live health checks then report it disabled without contacting
|
|
40
|
+
Slack; reviewed links still require their own exact account, target, grant and human approval checks.
|
|
41
|
+
|
|
42
|
+
`slack.bot_enabled true` enables the bot and reports every missing prerequisite. When unset, any configured
|
|
43
|
+
owner/workspace or stored bot/app token retains the existing bot setup and its missing-prerequisite checks,
|
|
44
|
+
even when SlackCLI selectors are also configured. An entirely unconfigured bot is optional and neutral.
|
|
45
|
+
`hz setup slack` explicitly enables the bot. Disabling it prevents bot sends, listening and approval consumption;
|
|
46
|
+
it does not grant reviewed-link delivery or remove its independent safeguards.
|
|
47
|
+
|
|
48
|
+
1. **Heinzelmännchen 0.9.20 or newer within the 0.9 series.** The development lock uses 0.9.20.
|
|
49
|
+
2. **A Slack workspace where you may install an app.** You create a *private app that only you use*, in your own
|
|
50
|
+
workspace, from the manifest this plugin ships. If your workspace lets only admins install apps, ask for approval
|
|
51
|
+
of that one app (the manifest lists exactly what it can do) or use a workspace of your own. The plugin does not
|
|
52
|
+
work around that setting.
|
|
53
|
+
3. **Your Slack member ID**: your profile, the three dots, "Copy member ID" (like `U01ABCDEF`), and your
|
|
54
|
+
**workspace ID** (like `T01ABCDEF`), configured as `slack.owner` and `slack.team`. Both are required.
|
|
55
|
+
4. **Two tokens from your app**, kept in the OS keychain by `hz`, never in a file or a repository:
|
|
56
|
+
- the **bot token** (`xoxb-...`, on *Install App*), scopes `chat:write`, `im:write`, `users:read`;
|
|
57
|
+
- an **app-level token** (`xapp-...`, on *Basic Information*, scope `connections:write`) for Socket Mode.
|
|
58
|
+
5. **Python 3.12+** (supplied by uv) and an internet connection to `slack.com`. No public address, no port to open.
|
|
59
|
+
|
|
60
|
+
## Set up (about three minutes)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
hz slack manifest # copy the JSON
|
|
64
|
+
# api.slack.com/apps, Create New App, From a manifest, paste it, Create, Install to Workspace.
|
|
65
|
+
# Basic Information, App-Level Tokens, Generate Token with connections:write.
|
|
66
|
+
hz system credential set bot-token --service slack
|
|
67
|
+
hz system credential set app-token --service slack
|
|
68
|
+
hz system config set slack.owner U01ABCDEF # or: hz setup slack
|
|
69
|
+
hz system config set slack.team T01ABCDEF
|
|
70
|
+
hz system doctor # then: hz system connector test slack
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The adapter registers notification and approval ports for the core channel consumer. Registration
|
|
74
|
+
alone does not route notices or grant consent. In the installed offline acceptance flow, the source-bound host
|
|
75
|
+
reviews each exact posting target and message through its normal human confirmation surface before sending.
|
|
76
|
+
Consenting to post a draft does not approve that draft: an owner click is a separate claim the host verifies.
|
|
77
|
+
The plugin operations can be used explicitly:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
hz slack notify "Build done" "You asked to know when it finishes." --helper Otto
|
|
81
|
+
hz slack ask "Send the weekly report" "$(cat report.md)" --helper Lotte
|
|
82
|
+
hz slack wait <request-id> # listens for your click, up to --seconds; or: hz slack status <request-id>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Clicks are only registered while something listens** (`wait`, `listen`, or the five-minute `listen` job while a draft is waiting). A button pressed with
|
|
86
|
+
nobody listening shows Slack's "not responding" notice and the draft stays waiting; press it again once `hz` listens.
|
|
87
|
+
The message says nothing false in the meantime: it still shows the buttons because it is still unanswered.
|
|
88
|
+
|
|
89
|
+
## What approval means here
|
|
90
|
+
|
|
91
|
+
Approval follows a typed code path (T3). `DraftStanding.approves(draft)` checks an approved state and the exact
|
|
92
|
+
draft digest. A host consuming `ChannelApprovalReply` must separately validate the request, actor, argument digest
|
|
93
|
+
and expiry before acting. The plugin records a decision only if all of this held:
|
|
94
|
+
|
|
95
|
+
| Held at click time | How |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| It is a **button** press on our message | Replies, reactions and slash commands are not even subscribed to, and any such payload is refused |
|
|
98
|
+
| The person is **you** | The clicker's Slack ID equals the owner recorded *before* posting; a project `hz.toml` cannot set it |
|
|
99
|
+
| Right **workspace, conversation and message** | Compared with what was recorded when the message was posted |
|
|
100
|
+
| The **exact bytes** | The button carries the host argument digest; the stored preview is hashed again under the lock; `DraftStanding.approves(draft)` hashes what you pass |
|
|
101
|
+
| Still **fresh** | Pending, not expired (default 4 hours), click time plausible; replacing a draft requires an explicit host `settle(reference, "superseded")` call |
|
|
102
|
+
| **Once** | The move from pending to decided is a single atomic update, so a second click, a Slack retry or a replay is refused |
|
|
103
|
+
|
|
104
|
+
Everything else from Slack is data. Only identifiers are read; message text, names and replies are never parsed for
|
|
105
|
+
meaning. A draft containing control, zero-width or bidirectional characters is refused (it could not be reviewed as
|
|
106
|
+
it is), and a draft over 4,000 characters is refused (you could not read it all in Slack). The draft is shown in a
|
|
107
|
+
literal preformatted block, so mentions and links in it are inert. Refused clicks are recorded in the audit trail
|
|
108
|
+
(`approvals.sqlite3` in the state folder, readable by you only) with a code of ours, never Slack's text.
|
|
109
|
+
|
|
110
|
+
What this does **not** prove: that a human, rather than software running as you in Slack, pressed the button. It is an
|
|
111
|
+
application-level decision by the owner's Slack account, as in `hz` core's own confirmations. Anyone who controls
|
|
112
|
+
your Slack account or the machine controls your approvals.
|
|
113
|
+
|
|
114
|
+
## Why your own app and not a browser session or the Slack CLI
|
|
115
|
+
|
|
116
|
+
| Approach | Verdict |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| **Browser session tokens** (community CLIs such as `slackcli` with `xoxc`/`xoxd`) | Acts as you, nothing to install, but it is an unofficial use of your login that Slack can break or forbid at any time, tokens die when you sign out, and it cannot receive button clicks. Approval would be a reaction or a typed reply, which cannot be bound to the exact bytes. Not a dependency for a product. |
|
|
119
|
+
| **The official Slack CLI** (`slack`) | Builds and runs apps on Slack's platform and manages manifests; it has no way to post a message or receive a click for another tool. Useful for creating and installing the app during development. |
|
|
120
|
+
| **A private Slack app** (this plugin): bot token for the Web API, Socket Mode for clicks, official `slack-sdk` | Supported by Slack, least privilege (three bot scopes, no user scope, no event subscriptions), signed identity of the clicker in every click, and no inbound address, so nothing is hosted. **Chosen.** |
|
|
121
|
+
|
|
122
|
+
Tenets weighed: P1 ("your identity over a bot identity") and ADR 0035 ("only what an ordinary employee may do, no admin-registered
|
|
123
|
+
apps") against T3 and P2 (approval must be bound and verifiable) and distributability. The app here is yours alone, in
|
|
124
|
+
your workspace, created by you, and it only ever writes to your own DM as a mailbox: it never posts as you and never to
|
|
125
|
+
a shared channel. Anything published under your name later is a separate step with your identity. If you cannot install an
|
|
126
|
+
app, the plugin reports so; it does not fall back to a browser session.
|
|
127
|
+
|
|
128
|
+
## Ports
|
|
129
|
+
|
|
130
|
+
The plugin registers one service, `SlackChannel`, a `NotificationChannel` and an `ApprovalChannel` as the candidate
|
|
131
|
+
SDK defines them; core imports nothing from this plugin and names no Slack. A channel carries words and never decides:
|
|
132
|
+
|
|
133
|
+
- `delivery_target()` observes a `ChannelTarget`: authenticated workspace/bot account, exact owner DM, and
|
|
134
|
+
versioned owner-bound posting scope. This observation posts no message and grants no authority.
|
|
135
|
+
- `deliver_reviewed(notice, target)` and `post_reviewed(request, target)` require that exact target. They verify it
|
|
136
|
+
again at mutation using the same immutable credential selection and SDK client that will post, with retries
|
|
137
|
+
disabled. A changed account, DM or scope refuses without a post; legacy methods are never a host fallback.
|
|
138
|
+
- `deliver(notice)` is the explicit plugin operation path and posts at most once per `key`. The receipt says what
|
|
139
|
+
was observed: `delivered`, `refused` (Slack said no, safe to retry) or `unknown` (the answer was lost, so it may have been sent and is never retried by itself).
|
|
140
|
+
- `post(request)` shows the exact preview with Approve and Decline, bound to the request's `arguments_sha256` and expiry,
|
|
141
|
+
idempotent by `request_id`. `replies(references)` returns only clicks that passed every check above, with the owner's
|
|
142
|
+
workspace and member ID as the actor; the host must check that actor, digest and expiry again before it
|
|
143
|
+
consumes any approval.
|
|
144
|
+
- `settle(reference, outcome)` is an explicit host call to close the message once the host has acted, expired it or
|
|
145
|
+
replaced it. Posting another request never supersedes an earlier one automatically. Every message update first
|
|
146
|
+
verifies that current credentials address the recorded workspace and the owner's recorded DM.
|
|
147
|
+
|
|
148
|
+
Credential selection is pinned for each conditional call. Rotating credentials after the client is acquired cannot
|
|
149
|
+
redirect that call; it does not transactionally cancel an already pinned, correctly reviewed operation. Revocation
|
|
150
|
+
can make Slack refuse it, while a lost acknowledgement remains `unknown` and is never retried automatically.
|
|
151
|
+
|
|
152
|
+
For development against a different compatible core artifact, select its wheel with `CORE=<wheel>`.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
make check-fast
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The closed SDK subset, upstream wheel pin, license and regeneration policy are described in
|
|
161
|
+
[the vendoring notes](docs/VENDORING.md). Vendor-aware tooling verifies exact upstream and projected bytes before
|
|
162
|
+
formatting, checks or builds, and verifies the built wheel again. Only verified upstream implementation files receive
|
|
163
|
+
dependency treatment; first-party lint, strict typing and the 90% coverage floor remain unchanged.
|
|
164
|
+
|
|
165
|
+
The in-memory Slack in `tests/conftest.py` is a labelled fake. `tests/e2e/live_test.py` talks to a real workspace when
|
|
166
|
+
`SLACK_PROOF=1`; the tokens come from the environment (the official `slack run` injects them without printing them).
|
|
167
|
+
|
|
168
|
+
<!-- screenshots:start -->
|
|
169
|
+
## Screenshots
|
|
170
|
+
|
|
171
|
+
<img src="docs/assets/screenshots/review-slack-notice.png" alt="Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery." width="960">
|
|
172
|
+
|
|
173
|
+
Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery.
|
|
174
|
+
|
|
175
|
+
<img src="docs/assets/screenshots/slack-notice-delivered.png" alt="Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write." width="960">
|
|
176
|
+
|
|
177
|
+
Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write.
|
|
178
|
+
|
|
179
|
+
<details>
|
|
180
|
+
<summary>Command line</summary>
|
|
181
|
+
|
|
182
|
+
<img src="docs/assets/screenshots/slack-draft-cli.png" alt="Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks." width="960">
|
|
183
|
+
|
|
184
|
+
Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks.
|
|
185
|
+
|
|
186
|
+
<img src="docs/assets/screenshots/slack-draft-decision-cli.png" alt="Demo data: fictional clicks exercise the real decision code; Mira's exact approval records one reply while invalid attempts are refused." width="960">
|
|
187
|
+
|
|
188
|
+
Demo data: fictional clicks exercise the real decision code; Mira's exact approval records one reply while invalid attempts are refused.
|
|
189
|
+
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
[Open the screenshot gallery](docs/assets/screenshots/index.html).
|
|
193
|
+
<!-- screenshots:end -->
|
|
194
|
+
|
|
195
|
+
### Approved document links through SlackCLI
|
|
196
|
+
|
|
197
|
+
The separate source-only `SlackReviewedLinks` companion uses an existing SlackCLI 0.18.0 browser profile.
|
|
198
|
+
Its workflow channel name is `slack-reviewed-links`, registered in the separate `slack.links` module record.
|
|
199
|
+
The existing `slack` bot channel retains its own unique ordinary notification and approval service. Select
|
|
200
|
+
an absolute native executable with `slack.cli_path` and the exact stored profile key with `slack.cli_profile`.
|
|
201
|
+
These owner settings are ignored in project configuration. It resolves the authenticated user's existing self DM
|
|
202
|
+
before the host asks for approval, and binds that account, destination and captured source into the reviewed target.
|
|
203
|
+
It sends only the exact approved Google Doc URL. Private review notices use the host's ordinary app notifications.
|
|
204
|
+
|
|
205
|
+
No bot or app token is required for this companion. File-backed browser profiles are privately pinned along with the
|
|
206
|
+
native binary; keychain-backed profiles and standard-token SDK profiles refuse. The standard SDK transport may retry
|
|
207
|
+
uncertain writes, so that mode cannot satisfy this companion's acknowledgement contract. Missing or ambiguous self
|
|
208
|
+
DMs refuse without creating a conversation. The exact selected CLI source and destination are checked again after
|
|
209
|
+
the durable delivery intent. Timeout, malformed acknowledgement and replay retain uncertainty and never resend.
|
|
210
|
+
The existing bot-backed notice/approval channel and its verified SDK projection remain separate.
|
|
211
|
+
|
|
212
|
+
The reviewed CLI behavior comes from [SlackCLI v0.18.0](https://github.com/shaharia-lab/slackcli/tree/v0.18.0),
|
|
213
|
+
including its fixed home-relative profile store, explicit workspace selector, browser-write retry policy and structured
|
|
214
|
+
message acknowledgement. Local contract tests use fictional profiles and provider replies. Live account delivery
|
|
215
|
+
and installed acceptance against the released SDK still need verification.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
> This plugin is in development. Its offline fixtures use fictional Slack transports and accounts.
|
|
2
|
+
> Installed acceptance against the released SDK and a new gallery review are pending. The existing
|
|
3
|
+
> captures illustrate earlier behavior.
|
|
4
|
+
|
|
5
|
+
# heinzel-slack
|
|
6
|
+
|
|
7
|
+
Slack as an extra notification channel and as a place to share a draft and get a yes or no back: a
|
|
8
|
+
[Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin, released on its own.
|
|
9
|
+
|
|
10
|
+
- **A notice says why.** Every message carries "Why you are seeing this", because a nudge that cannot say why is noise.
|
|
11
|
+
- **A draft is shared with Approve and Decline.** The answer is for the exact text you saw, and only you can give it.
|
|
12
|
+
- **Your DM only.** It writes to your direct message with your own app. Never a channel, never anyone else.
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
Lotte has a draft for you
|
|
16
|
+
Weekly report
|
|
17
|
+
Why you are seeing this: It is Friday and you asked for a report.
|
|
18
|
+
| Weekly report ... |
|
|
19
|
+
Nothing goes out until you say so. Fingerprint 3fa9c1d2b4e0 · answer by 16:00 · only your click counts.
|
|
20
|
+
[ Approve ] [ Decline ]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
The bot channel needs all of these. The separate reviewed-link adapter does not use the bot or app tokens.
|
|
26
|
+
For links-only use, explicitly run `hz system config set slack.bot_enabled false` and configure the SlackCLI
|
|
27
|
+
path/profile described below. The bot's local and live health checks then report it disabled without contacting
|
|
28
|
+
Slack; reviewed links still require their own exact account, target, grant and human approval checks.
|
|
29
|
+
|
|
30
|
+
`slack.bot_enabled true` enables the bot and reports every missing prerequisite. When unset, any configured
|
|
31
|
+
owner/workspace or stored bot/app token retains the existing bot setup and its missing-prerequisite checks,
|
|
32
|
+
even when SlackCLI selectors are also configured. An entirely unconfigured bot is optional and neutral.
|
|
33
|
+
`hz setup slack` explicitly enables the bot. Disabling it prevents bot sends, listening and approval consumption;
|
|
34
|
+
it does not grant reviewed-link delivery or remove its independent safeguards.
|
|
35
|
+
|
|
36
|
+
1. **Heinzelmännchen 0.9.20 or newer within the 0.9 series.** The development lock uses 0.9.20.
|
|
37
|
+
2. **A Slack workspace where you may install an app.** You create a *private app that only you use*, in your own
|
|
38
|
+
workspace, from the manifest this plugin ships. If your workspace lets only admins install apps, ask for approval
|
|
39
|
+
of that one app (the manifest lists exactly what it can do) or use a workspace of your own. The plugin does not
|
|
40
|
+
work around that setting.
|
|
41
|
+
3. **Your Slack member ID**: your profile, the three dots, "Copy member ID" (like `U01ABCDEF`), and your
|
|
42
|
+
**workspace ID** (like `T01ABCDEF`), configured as `slack.owner` and `slack.team`. Both are required.
|
|
43
|
+
4. **Two tokens from your app**, kept in the OS keychain by `hz`, never in a file or a repository:
|
|
44
|
+
- the **bot token** (`xoxb-...`, on *Install App*), scopes `chat:write`, `im:write`, `users:read`;
|
|
45
|
+
- an **app-level token** (`xapp-...`, on *Basic Information*, scope `connections:write`) for Socket Mode.
|
|
46
|
+
5. **Python 3.12+** (supplied by uv) and an internet connection to `slack.com`. No public address, no port to open.
|
|
47
|
+
|
|
48
|
+
## Set up (about three minutes)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
hz slack manifest # copy the JSON
|
|
52
|
+
# api.slack.com/apps, Create New App, From a manifest, paste it, Create, Install to Workspace.
|
|
53
|
+
# Basic Information, App-Level Tokens, Generate Token with connections:write.
|
|
54
|
+
hz system credential set bot-token --service slack
|
|
55
|
+
hz system credential set app-token --service slack
|
|
56
|
+
hz system config set slack.owner U01ABCDEF # or: hz setup slack
|
|
57
|
+
hz system config set slack.team T01ABCDEF
|
|
58
|
+
hz system doctor # then: hz system connector test slack
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The adapter registers notification and approval ports for the core channel consumer. Registration
|
|
62
|
+
alone does not route notices or grant consent. In the installed offline acceptance flow, the source-bound host
|
|
63
|
+
reviews each exact posting target and message through its normal human confirmation surface before sending.
|
|
64
|
+
Consenting to post a draft does not approve that draft: an owner click is a separate claim the host verifies.
|
|
65
|
+
The plugin operations can be used explicitly:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
hz slack notify "Build done" "You asked to know when it finishes." --helper Otto
|
|
69
|
+
hz slack ask "Send the weekly report" "$(cat report.md)" --helper Lotte
|
|
70
|
+
hz slack wait <request-id> # listens for your click, up to --seconds; or: hz slack status <request-id>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Clicks are only registered while something listens** (`wait`, `listen`, or the five-minute `listen` job while a draft is waiting). A button pressed with
|
|
74
|
+
nobody listening shows Slack's "not responding" notice and the draft stays waiting; press it again once `hz` listens.
|
|
75
|
+
The message says nothing false in the meantime: it still shows the buttons because it is still unanswered.
|
|
76
|
+
|
|
77
|
+
## What approval means here
|
|
78
|
+
|
|
79
|
+
Approval follows a typed code path (T3). `DraftStanding.approves(draft)` checks an approved state and the exact
|
|
80
|
+
draft digest. A host consuming `ChannelApprovalReply` must separately validate the request, actor, argument digest
|
|
81
|
+
and expiry before acting. The plugin records a decision only if all of this held:
|
|
82
|
+
|
|
83
|
+
| Held at click time | How |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| It is a **button** press on our message | Replies, reactions and slash commands are not even subscribed to, and any such payload is refused |
|
|
86
|
+
| The person is **you** | The clicker's Slack ID equals the owner recorded *before* posting; a project `hz.toml` cannot set it |
|
|
87
|
+
| Right **workspace, conversation and message** | Compared with what was recorded when the message was posted |
|
|
88
|
+
| The **exact bytes** | The button carries the host argument digest; the stored preview is hashed again under the lock; `DraftStanding.approves(draft)` hashes what you pass |
|
|
89
|
+
| Still **fresh** | Pending, not expired (default 4 hours), click time plausible; replacing a draft requires an explicit host `settle(reference, "superseded")` call |
|
|
90
|
+
| **Once** | The move from pending to decided is a single atomic update, so a second click, a Slack retry or a replay is refused |
|
|
91
|
+
|
|
92
|
+
Everything else from Slack is data. Only identifiers are read; message text, names and replies are never parsed for
|
|
93
|
+
meaning. A draft containing control, zero-width or bidirectional characters is refused (it could not be reviewed as
|
|
94
|
+
it is), and a draft over 4,000 characters is refused (you could not read it all in Slack). The draft is shown in a
|
|
95
|
+
literal preformatted block, so mentions and links in it are inert. Refused clicks are recorded in the audit trail
|
|
96
|
+
(`approvals.sqlite3` in the state folder, readable by you only) with a code of ours, never Slack's text.
|
|
97
|
+
|
|
98
|
+
What this does **not** prove: that a human, rather than software running as you in Slack, pressed the button. It is an
|
|
99
|
+
application-level decision by the owner's Slack account, as in `hz` core's own confirmations. Anyone who controls
|
|
100
|
+
your Slack account or the machine controls your approvals.
|
|
101
|
+
|
|
102
|
+
## Why your own app and not a browser session or the Slack CLI
|
|
103
|
+
|
|
104
|
+
| Approach | Verdict |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| **Browser session tokens** (community CLIs such as `slackcli` with `xoxc`/`xoxd`) | Acts as you, nothing to install, but it is an unofficial use of your login that Slack can break or forbid at any time, tokens die when you sign out, and it cannot receive button clicks. Approval would be a reaction or a typed reply, which cannot be bound to the exact bytes. Not a dependency for a product. |
|
|
107
|
+
| **The official Slack CLI** (`slack`) | Builds and runs apps on Slack's platform and manages manifests; it has no way to post a message or receive a click for another tool. Useful for creating and installing the app during development. |
|
|
108
|
+
| **A private Slack app** (this plugin): bot token for the Web API, Socket Mode for clicks, official `slack-sdk` | Supported by Slack, least privilege (three bot scopes, no user scope, no event subscriptions), signed identity of the clicker in every click, and no inbound address, so nothing is hosted. **Chosen.** |
|
|
109
|
+
|
|
110
|
+
Tenets weighed: P1 ("your identity over a bot identity") and ADR 0035 ("only what an ordinary employee may do, no admin-registered
|
|
111
|
+
apps") against T3 and P2 (approval must be bound and verifiable) and distributability. The app here is yours alone, in
|
|
112
|
+
your workspace, created by you, and it only ever writes to your own DM as a mailbox: it never posts as you and never to
|
|
113
|
+
a shared channel. Anything published under your name later is a separate step with your identity. If you cannot install an
|
|
114
|
+
app, the plugin reports so; it does not fall back to a browser session.
|
|
115
|
+
|
|
116
|
+
## Ports
|
|
117
|
+
|
|
118
|
+
The plugin registers one service, `SlackChannel`, a `NotificationChannel` and an `ApprovalChannel` as the candidate
|
|
119
|
+
SDK defines them; core imports nothing from this plugin and names no Slack. A channel carries words and never decides:
|
|
120
|
+
|
|
121
|
+
- `delivery_target()` observes a `ChannelTarget`: authenticated workspace/bot account, exact owner DM, and
|
|
122
|
+
versioned owner-bound posting scope. This observation posts no message and grants no authority.
|
|
123
|
+
- `deliver_reviewed(notice, target)` and `post_reviewed(request, target)` require that exact target. They verify it
|
|
124
|
+
again at mutation using the same immutable credential selection and SDK client that will post, with retries
|
|
125
|
+
disabled. A changed account, DM or scope refuses without a post; legacy methods are never a host fallback.
|
|
126
|
+
- `deliver(notice)` is the explicit plugin operation path and posts at most once per `key`. The receipt says what
|
|
127
|
+
was observed: `delivered`, `refused` (Slack said no, safe to retry) or `unknown` (the answer was lost, so it may have been sent and is never retried by itself).
|
|
128
|
+
- `post(request)` shows the exact preview with Approve and Decline, bound to the request's `arguments_sha256` and expiry,
|
|
129
|
+
idempotent by `request_id`. `replies(references)` returns only clicks that passed every check above, with the owner's
|
|
130
|
+
workspace and member ID as the actor; the host must check that actor, digest and expiry again before it
|
|
131
|
+
consumes any approval.
|
|
132
|
+
- `settle(reference, outcome)` is an explicit host call to close the message once the host has acted, expired it or
|
|
133
|
+
replaced it. Posting another request never supersedes an earlier one automatically. Every message update first
|
|
134
|
+
verifies that current credentials address the recorded workspace and the owner's recorded DM.
|
|
135
|
+
|
|
136
|
+
Credential selection is pinned for each conditional call. Rotating credentials after the client is acquired cannot
|
|
137
|
+
redirect that call; it does not transactionally cancel an already pinned, correctly reviewed operation. Revocation
|
|
138
|
+
can make Slack refuse it, while a lost acknowledgement remains `unknown` and is never retried automatically.
|
|
139
|
+
|
|
140
|
+
For development against a different compatible core artifact, select its wheel with `CORE=<wheel>`.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
make check-fast
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The closed SDK subset, upstream wheel pin, license and regeneration policy are described in
|
|
149
|
+
[the vendoring notes](docs/VENDORING.md). Vendor-aware tooling verifies exact upstream and projected bytes before
|
|
150
|
+
formatting, checks or builds, and verifies the built wheel again. Only verified upstream implementation files receive
|
|
151
|
+
dependency treatment; first-party lint, strict typing and the 90% coverage floor remain unchanged.
|
|
152
|
+
|
|
153
|
+
The in-memory Slack in `tests/conftest.py` is a labelled fake. `tests/e2e/live_test.py` talks to a real workspace when
|
|
154
|
+
`SLACK_PROOF=1`; the tokens come from the environment (the official `slack run` injects them without printing them).
|
|
155
|
+
|
|
156
|
+
<!-- screenshots:start -->
|
|
157
|
+
## Screenshots
|
|
158
|
+
|
|
159
|
+
<img src="docs/assets/screenshots/review-slack-notice.png" alt="Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery." width="960">
|
|
160
|
+
|
|
161
|
+
Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery.
|
|
162
|
+
|
|
163
|
+
<img src="docs/assets/screenshots/slack-notice-delivered.png" alt="Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write." width="960">
|
|
164
|
+
|
|
165
|
+
Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write.
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary>Command line</summary>
|
|
169
|
+
|
|
170
|
+
<img src="docs/assets/screenshots/slack-draft-cli.png" alt="Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks." width="960">
|
|
171
|
+
|
|
172
|
+
Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks.
|
|
173
|
+
|
|
174
|
+
<img src="docs/assets/screenshots/slack-draft-decision-cli.png" alt="Demo data: fictional clicks exercise the real decision code; Mira's exact approval records one reply while invalid attempts are refused." width="960">
|
|
175
|
+
|
|
176
|
+
Demo data: fictional clicks exercise the real decision code; Mira's exact approval records one reply while invalid attempts are refused.
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
[Open the screenshot gallery](docs/assets/screenshots/index.html).
|
|
181
|
+
<!-- screenshots:end -->
|
|
182
|
+
|
|
183
|
+
### Approved document links through SlackCLI
|
|
184
|
+
|
|
185
|
+
The separate source-only `SlackReviewedLinks` companion uses an existing SlackCLI 0.18.0 browser profile.
|
|
186
|
+
Its workflow channel name is `slack-reviewed-links`, registered in the separate `slack.links` module record.
|
|
187
|
+
The existing `slack` bot channel retains its own unique ordinary notification and approval service. Select
|
|
188
|
+
an absolute native executable with `slack.cli_path` and the exact stored profile key with `slack.cli_profile`.
|
|
189
|
+
These owner settings are ignored in project configuration. It resolves the authenticated user's existing self DM
|
|
190
|
+
before the host asks for approval, and binds that account, destination and captured source into the reviewed target.
|
|
191
|
+
It sends only the exact approved Google Doc URL. Private review notices use the host's ordinary app notifications.
|
|
192
|
+
|
|
193
|
+
No bot or app token is required for this companion. File-backed browser profiles are privately pinned along with the
|
|
194
|
+
native binary; keychain-backed profiles and standard-token SDK profiles refuse. The standard SDK transport may retry
|
|
195
|
+
uncertain writes, so that mode cannot satisfy this companion's acknowledgement contract. Missing or ambiguous self
|
|
196
|
+
DMs refuse without creating a conversation. The exact selected CLI source and destination are checked again after
|
|
197
|
+
the durable delivery intent. Timeout, malformed acknowledgement and replay retain uncertainty and never resend.
|
|
198
|
+
The existing bot-backed notice/approval channel and its verified SDK projection remain separate.
|
|
199
|
+
|
|
200
|
+
The reviewed CLI behavior comes from [SlackCLI v0.18.0](https://github.com/shaharia-lab/slackcli/tree/v0.18.0),
|
|
201
|
+
including its fixed home-relative profile store, explicit workspace selector, browser-write retry policy and structured
|
|
202
|
+
message acknowledgement. Local contract tests use fictional profiles and provider replies. Live account delivery
|
|
203
|
+
and installed acceptance against the released SDK still need verification.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Audience
|
|
2
|
+
|
|
3
|
+
Someone considering a private Slack notification and draft-approval channel for Heinzelmännchen, who wants to understand what is reviewed, where decisions happen, and what setup requires.
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
Show a fictional weekly-report notice at the point where the owner can inspect its exact posting account, destination, scope, and message, then show the Core receipt after confirmation. Complement that real Core flow with terminal demonstrations of the Slack draft blocks and decision checks. The receipt establishes delivery to the fixture's local stand-in, not delivery to a real Slack workspace or approval of a separate captured write.
|
|
8
|
+
|
|
9
|
+
## Scenes
|
|
10
|
+
|
|
11
|
+
1. **Review the notice — light UI.** Mira sees the pending Core confirmation with the fictional account, owner DM, scope, title, and full reason. The existing fixture stages this through the channel proposal and Core chat APIs (`docs/screenshot_fixture.py`, `stage_notice`).
|
|
12
|
+
2. **See the result — dark UI.** After confirmation, Core reports that the channel delivered the notice to the local stand-in. This is a meaningful later state of the same flow, not a duplicate image or a claim that a draft was approved (`docs/screenshot_fixture.py`, `StandIn` and `RECEIPT`).
|
|
13
|
+
3. **Inspect a draft — CLI.** The fixture's `showcase share` renders the actual posted Slack message blocks as a terminal illustration, including the fictional draft and button labels. It is not a screenshot of Slack or an interactive feature UI (`docs/screenshot_fixture.py`, `share`, `draw`).
|
|
14
|
+
4. **Inspect decisions — CLI.** `showcase answers` exercises the real decision code with fictional owner, stranger, stale draft, replay, and expiry cases, and reports the host's replies. These are fixture operations, not declared production `hz slack` commands (`docs/screenshot_fixture.py`, `answers`).
|
|
15
|
+
|
|
16
|
+
## Fictional fixtures
|
|
17
|
+
|
|
18
|
+
Use the existing `docs/screenshot_fixture.py` bootstrap and its private synthetic profile. Lotte, Mira, Jonas, the workspace and DM identifiers, and the reading-list report are fictional. The UI fixture binds a loopback Slack stand-in and stages one notice; the CLI fixture records posts in memory. No live Slack delivery or installed acceptance is established by this plan. The README explicitly says released-SDK acceptance and a new gallery review are pending (`README.md`, opening notice).
|
|
19
|
+
|
|
20
|
+
## Source-bound UI surface inventory
|
|
21
|
+
|
|
22
|
+
- **Core feature UI:** The actual Core chat confirmation and delivery result are evidenced by the fixture's proposal operations, pending action, review terms, confirmation, and receipt (`docs/screenshot_fixture.py`, `stage_notice`). The plan uses `/`, an existing generic Core route. This is sufficient evidence for two useful feature UI states even though this plugin declares no dedicated Page or View.
|
|
23
|
+
- **External provider UI:** Slack itself displays the posted notice or draft and its Approve and Decline buttons. The plugin constructs message blocks and handles button decisions (`src/heinzel_slack/_blocks.py`, `src/heinzel_slack/_service.py`, `src/heinzel_slack/_decide.py`). The fixture's terminal rendering is an illustration of those blocks, not genuine Slack UI. No provider screenshot is claimed.
|
|
24
|
+
- **Operations and generic Core contributions:** Both declared entry points in `pyproject.toml`, `heinzel.modules:slack` and `heinzel.connected_sources:slack`, target `heinzel_slack`. Its `module` is declared in `src/heinzel_slack/_module.py`; `src/heinzel_slack/__init__.py` registers settings, services, `info`, `manifest`, `notify`, `ask`, `status`, `wait`, and `listen`, plus a scheduled listener, local/live health, optional setup, and system-info metadata. These can contribute to generic Core operation, setup, health, and information surfaces; their precise rendered screens are not supplied here and are not invented in the plan. Settings are declared in `src/heinzel_slack/_settings.py`.
|
|
25
|
+
- **Channel and approval surfaces:** `src/heinzel_slack/_channel.py` exposes the notification and approval channel, while Core owns the demonstrated review and confirmation. The separate `Module("links", ...)` conditionally registers `SlackReviewedLinks` only when the captured-source SDK contracts are available (`src/heinzel_slack/links/__init__.py`); its reviewed Google Doc link flow and self-DM target are described in `README.md` and `src/heinzel_slack/links/_reviewed_link.py`. This fixture does not demonstrate that flow. Its module record is described by the README, but no separate packaging entry point for it is declared in the supplied `pyproject.toml`; how Core discovers it remains unresolved from these sources.
|
|
26
|
+
- **Terminal and unresolved evidence:** The fixture's two `showcase` operations are documentation-only native terminal results. Production operations are declared in source, but absence of a dedicated view does not make them terminal-only. No plugin navigation or footer declaration is evident in the inspected first-party declarations; this does not rule out generic Core rendering. The manifest operation reads `src/heinzel_slack/manifest.json`, which is not present in the provided file inventory, so its actual output cannot be verified here. The README's development-status warning qualifies any existing screenshot claims.
|
|
27
|
+
|
|
28
|
+
## Privacy pitfalls
|
|
29
|
+
|
|
30
|
+
Normal presentation fictionalization is explicitly limited: verified owning-interpreter paths become `/demo-profile/.venv`, and only the verified whole hostname detail in the genuine Doctor row becomes `demo-host`. These mappings change no SDK health status or remaining detail and establish no privacy acceptance. Unknown identities still fail, and independent review must inspect every image and transcript. Inspect account IDs, paths, timestamps, tokens, hostnames, terminal history, and any unexpected Core content; do not publish if their provenance is unknown. The local stand-in and fictional account identifiers must remain visibly distinguishable from real delivery.
|
|
31
|
+
|
|
32
|
+
## Acceptance criteria
|
|
33
|
+
|
|
34
|
+
- Capture the existing four distinct scenes with applied light and dark appearances, using the declared bootstrap and demo data. Confirm the review contains the complete terms and the later state shows the actual Core receipt.
|
|
35
|
+
- Treat the two terminal outputs as labeled fixture demonstrations, never as Slack UI or production commands. Reject duplicate captures, caption-only changes, terminal output presented as feature UI, and forged HTML.
|
|
36
|
+
- Independently inspect every image and transcript for privacy and truthfulness, including the narrowly permitted path and hostname mappings. Unknown identities fail review.
|
|
37
|
+
- Verify that the local stand-in received only the intended notice and that the receipt does not imply draft approval or live Slack acceptance. Preserve any failure or changed SDK health status rather than masking it.
|
|
38
|
+
- The existing plan is preserved because it already demonstrates the useful Core review and delivery states plus two distinct decision-related CLI results. Its TOML structure was reviewed against the supplied schema requirements without executing code or performing a capture; runtime behavior and accessibility labels remain to be verified during explicit capture.
|