@delorenj/pjangler 1.2.21 → 1.2.25
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.
- package/dist/index.js +1852 -368
- package/dist/mcp-server.js +1782 -296
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +3 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -2
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import stat
|
|
6
|
+
import subprocess
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).parents[1]
|
|
14
|
+
SLACK_SCRIPT = ROOT / "template" / ".scripts" / "31-slack.sh"
|
|
15
|
+
LIB_SCRIPT = ROOT / "template" / ".scripts" / "_lib.sh"
|
|
16
|
+
REGISTRY_SCRIPT = ROOT / "template" / ".scripts" / "80-registry.sh"
|
|
17
|
+
|
|
18
|
+
BOT_TOKEN = "xoxb-profile-only-secret"
|
|
19
|
+
APP_TOKEN = "xapp-profile-only-secret"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _make_role(tmp_path: Path) -> tuple[Path, Path, Path]:
|
|
23
|
+
role = tmp_path / "role"
|
|
24
|
+
scripts = role / ".scripts"
|
|
25
|
+
runtime = role / "runtime"
|
|
26
|
+
scripts.mkdir(parents=True)
|
|
27
|
+
runtime.mkdir()
|
|
28
|
+
shutil.copy2(SLACK_SCRIPT, scripts / SLACK_SCRIPT.name)
|
|
29
|
+
shutil.copy2(LIB_SCRIPT, scripts / LIB_SCRIPT.name)
|
|
30
|
+
(role / "role.yaml").write_text(
|
|
31
|
+
"""repo: demo
|
|
32
|
+
role: pm
|
|
33
|
+
agent_id: demo-pm
|
|
34
|
+
display_name: "Demo PM"
|
|
35
|
+
profile: demo-pm
|
|
36
|
+
telegram:
|
|
37
|
+
bot_username: "demo_pm_bot"
|
|
38
|
+
slack:
|
|
39
|
+
provisioning_status: "deferred"
|
|
40
|
+
team_id: ""
|
|
41
|
+
team_name: ""
|
|
42
|
+
bot_user_id: ""
|
|
43
|
+
bot_id: ""
|
|
44
|
+
bot_username: ""
|
|
45
|
+
plane:
|
|
46
|
+
workspace: "test"
|
|
47
|
+
identifier: ""
|
|
48
|
+
runtime:
|
|
49
|
+
github_owner: "test"
|
|
50
|
+
github_repo: "agent-hm-demo-pm"
|
|
51
|
+
""",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
)
|
|
54
|
+
registry = tmp_path / "agents-registry.yaml"
|
|
55
|
+
registry.write_text("schema_version: 1\nagents: {}\n", encoding="utf-8")
|
|
56
|
+
return role, runtime, registry
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _fake_curl(tmp_path: Path) -> Path:
|
|
60
|
+
bindir = tmp_path / "bin"
|
|
61
|
+
bindir.mkdir(exist_ok=True)
|
|
62
|
+
curl = bindir / "curl"
|
|
63
|
+
curl.write_text(
|
|
64
|
+
"""#!/usr/bin/env python3
|
|
65
|
+
import json
|
|
66
|
+
import sys
|
|
67
|
+
|
|
68
|
+
args = sys.argv[1:]
|
|
69
|
+
assert "https://slack.com/api/auth.test" in args
|
|
70
|
+
header = args[args.index("-H") + 1]
|
|
71
|
+
assert header.startswith("Authorization: Bearer xoxb-")
|
|
72
|
+
print(json.dumps({
|
|
73
|
+
"ok": True,
|
|
74
|
+
"team_id": "T123",
|
|
75
|
+
"team": "Example Workspace",
|
|
76
|
+
"user_id": "U123BOT",
|
|
77
|
+
"bot_id": "B123BOT",
|
|
78
|
+
"user": "demo-pm",
|
|
79
|
+
}))
|
|
80
|
+
""",
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
)
|
|
83
|
+
curl.chmod(0o755)
|
|
84
|
+
return bindir
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _run(
|
|
88
|
+
role: Path,
|
|
89
|
+
registry: Path,
|
|
90
|
+
home: Path,
|
|
91
|
+
bindir: Path,
|
|
92
|
+
overrides: dict[str, str] | None = None,
|
|
93
|
+
) -> subprocess.CompletedProcess[str]:
|
|
94
|
+
env = {
|
|
95
|
+
key: value
|
|
96
|
+
for key, value in os.environ.items()
|
|
97
|
+
if not key.startswith("SLACK_") and key not in {"ENABLE_SLACK", "WIRE_SLACK"}
|
|
98
|
+
}
|
|
99
|
+
env.update(
|
|
100
|
+
{
|
|
101
|
+
"HOME": str(home),
|
|
102
|
+
"PATH": f"{bindir}:{env['PATH']}",
|
|
103
|
+
"HERMES_FLEET_ENV": str(home / ".hermes" / "fleet.env"),
|
|
104
|
+
"REGISTRY_FILE": str(registry),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
env.update(overrides or {})
|
|
108
|
+
return subprocess.run(
|
|
109
|
+
["bash", str(role / ".scripts" / "31-slack.sh")],
|
|
110
|
+
env=env,
|
|
111
|
+
input="",
|
|
112
|
+
text=True,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_slack_is_deferred_by_default_and_ignores_shared_fleet_tokens(tmp_path: Path) -> None:
|
|
119
|
+
role, runtime, registry = _make_role(tmp_path)
|
|
120
|
+
home = tmp_path / "home"
|
|
121
|
+
fleet = home / ".hermes" / "fleet.env"
|
|
122
|
+
fleet.parent.mkdir(parents=True)
|
|
123
|
+
fleet.write_text(
|
|
124
|
+
"SLACK_BOT_TOKEN=xoxb-shared\nSLACK_APP_TOKEN=xapp-shared\n"
|
|
125
|
+
"SLACK_ALLOWED_USERS=U999\n",
|
|
126
|
+
encoding="utf-8",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = _run(role, registry, home, _fake_curl(tmp_path))
|
|
130
|
+
|
|
131
|
+
assert result.returncode == 0, result.stderr
|
|
132
|
+
assert "deferred" in result.stderr
|
|
133
|
+
assert not (runtime / ".env").exists()
|
|
134
|
+
assert not (role / ".scripts" / ".done-31-slack").exists()
|
|
135
|
+
assert "xoxb-shared" in fleet.read_text(encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_explicit_noninteractive_enable_requires_both_tokens(tmp_path: Path) -> None:
|
|
139
|
+
role, runtime, registry = _make_role(tmp_path)
|
|
140
|
+
home = tmp_path / "home"
|
|
141
|
+
home.mkdir()
|
|
142
|
+
|
|
143
|
+
result = _run(
|
|
144
|
+
role,
|
|
145
|
+
registry,
|
|
146
|
+
home,
|
|
147
|
+
_fake_curl(tmp_path),
|
|
148
|
+
{"ENABLE_SLACK": "1", "SLACK_BOT_TOKEN": BOT_TOKEN},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert result.returncode != 0
|
|
152
|
+
assert "requires both SLACK_BOT_TOKEN and SLACK_APP_TOKEN" in result.stderr
|
|
153
|
+
assert BOT_TOKEN not in result.stdout + result.stderr
|
|
154
|
+
assert not (runtime / ".env").exists()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_both_tokens_verify_and_write_only_private_runtime_env(tmp_path: Path) -> None:
|
|
158
|
+
role, runtime, registry = _make_role(tmp_path)
|
|
159
|
+
home = tmp_path / "home"
|
|
160
|
+
fleet = home / ".hermes" / "fleet.env"
|
|
161
|
+
fleet.parent.mkdir(parents=True)
|
|
162
|
+
fleet.write_text("SLACK_ALLOWED_USERS=U111,U222\n", encoding="utf-8")
|
|
163
|
+
shared = home / ".hermes" / ".env"
|
|
164
|
+
shared.write_text("PROVIDER_KEY=keep-me\n", encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
result = _run(
|
|
167
|
+
role,
|
|
168
|
+
registry,
|
|
169
|
+
home,
|
|
170
|
+
_fake_curl(tmp_path),
|
|
171
|
+
{"SLACK_BOT_TOKEN": BOT_TOKEN, "SLACK_APP_TOKEN": APP_TOKEN},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert result.returncode == 0, result.stderr
|
|
175
|
+
env_file = runtime / ".env"
|
|
176
|
+
env_text = env_file.read_text(encoding="utf-8")
|
|
177
|
+
assert f'SLACK_BOT_TOKEN="{BOT_TOKEN}"' in env_text
|
|
178
|
+
assert f'SLACK_APP_TOKEN="{APP_TOKEN}"' in env_text
|
|
179
|
+
assert 'SLACK_ALLOWED_USERS="U111,U222"' in env_text
|
|
180
|
+
assert stat.S_IMODE(env_file.stat().st_mode) == 0o600
|
|
181
|
+
assert shared.read_text(encoding="utf-8") == "PROVIDER_KEY=keep-me\n"
|
|
182
|
+
assert fleet.read_text(encoding="utf-8") == "SLACK_ALLOWED_USERS=U111,U222\n"
|
|
183
|
+
assert BOT_TOKEN not in result.stdout + result.stderr
|
|
184
|
+
assert APP_TOKEN not in result.stdout + result.stderr
|
|
185
|
+
|
|
186
|
+
slack = yaml.safe_load((role / "role.yaml").read_text(encoding="utf-8"))["slack"]
|
|
187
|
+
assert slack == {
|
|
188
|
+
"provisioning_status": "verified",
|
|
189
|
+
"team_id": "T123",
|
|
190
|
+
"team_name": "Example Workspace",
|
|
191
|
+
"bot_user_id": "U123BOT",
|
|
192
|
+
"bot_id": "B123BOT",
|
|
193
|
+
"bot_username": "demo-pm",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_rejects_reused_pair_without_disclosing_tokens(tmp_path: Path) -> None:
|
|
198
|
+
role, runtime, registry = _make_role(tmp_path)
|
|
199
|
+
home = tmp_path / "home"
|
|
200
|
+
home.mkdir()
|
|
201
|
+
other_role = tmp_path / "other-role"
|
|
202
|
+
other_runtime = other_role / "runtime"
|
|
203
|
+
other_runtime.mkdir(parents=True)
|
|
204
|
+
(other_runtime / ".env").write_text(
|
|
205
|
+
f'SLACK_BOT_TOKEN="{BOT_TOKEN}"\nSLACK_APP_TOKEN="{APP_TOKEN}"\n',
|
|
206
|
+
encoding="utf-8",
|
|
207
|
+
)
|
|
208
|
+
registry.write_text(
|
|
209
|
+
yaml.safe_dump(
|
|
210
|
+
{
|
|
211
|
+
"schema_version": 1,
|
|
212
|
+
"agents": {
|
|
213
|
+
"other-pm": {
|
|
214
|
+
"role_dir": str(other_role),
|
|
215
|
+
"slack": {"provisioning_status": "verified"},
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
sort_keys=False,
|
|
220
|
+
),
|
|
221
|
+
encoding="utf-8",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
result = _run(
|
|
225
|
+
role,
|
|
226
|
+
registry,
|
|
227
|
+
home,
|
|
228
|
+
_fake_curl(tmp_path),
|
|
229
|
+
{"SLACK_BOT_TOKEN": BOT_TOKEN, "SLACK_APP_TOKEN": APP_TOKEN},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
assert result.returncode != 0
|
|
233
|
+
assert "already assigned to agent other-pm" in result.stderr
|
|
234
|
+
assert BOT_TOKEN not in result.stdout + result.stderr
|
|
235
|
+
assert APP_TOKEN not in result.stdout + result.stderr
|
|
236
|
+
assert not (runtime / ".env").exists()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_refuses_runtime_env_symlink_before_auth_test(tmp_path: Path) -> None:
|
|
240
|
+
role, runtime, registry = _make_role(tmp_path)
|
|
241
|
+
home = tmp_path / "home"
|
|
242
|
+
shared = home / ".hermes" / ".env"
|
|
243
|
+
shared.parent.mkdir(parents=True)
|
|
244
|
+
shared.write_text("PROVIDER_KEY=keep-me\n", encoding="utf-8")
|
|
245
|
+
(runtime / ".env").symlink_to(shared)
|
|
246
|
+
|
|
247
|
+
result = _run(
|
|
248
|
+
role,
|
|
249
|
+
registry,
|
|
250
|
+
home,
|
|
251
|
+
_fake_curl(tmp_path),
|
|
252
|
+
{"SLACK_BOT_TOKEN": BOT_TOKEN, "SLACK_APP_TOKEN": APP_TOKEN},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
assert result.returncode != 0
|
|
256
|
+
assert "refusing to write Slack credentials through symlink" in result.stderr
|
|
257
|
+
assert shared.read_text(encoding="utf-8") == "PROVIDER_KEY=keep-me\n"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_registry_persists_identity_metadata_without_tokens(tmp_path: Path) -> None:
|
|
261
|
+
role, _, registry = _make_role(tmp_path)
|
|
262
|
+
home = tmp_path / "home"
|
|
263
|
+
home.mkdir()
|
|
264
|
+
shutil.copy2(REGISTRY_SCRIPT, role / ".scripts" / REGISTRY_SCRIPT.name)
|
|
265
|
+
role_yaml = role / "role.yaml"
|
|
266
|
+
role_yaml.write_text(
|
|
267
|
+
role_yaml.read_text(encoding="utf-8")
|
|
268
|
+
.replace('provisioning_status: "deferred"', 'provisioning_status: "verified"')
|
|
269
|
+
.replace('team_id: ""', 'team_id: "T123"')
|
|
270
|
+
.replace('team_name: ""', 'team_name: "Example Workspace"')
|
|
271
|
+
.replace('bot_user_id: ""', 'bot_user_id: "U123BOT"')
|
|
272
|
+
.replace('bot_id: ""', 'bot_id: "B123BOT"')
|
|
273
|
+
.replace('bot_username: ""', 'bot_username: "demo-pm"'),
|
|
274
|
+
encoding="utf-8",
|
|
275
|
+
)
|
|
276
|
+
env = {
|
|
277
|
+
key: value
|
|
278
|
+
for key, value in os.environ.items()
|
|
279
|
+
if not key.startswith("SLACK_")
|
|
280
|
+
}
|
|
281
|
+
env.update(
|
|
282
|
+
{
|
|
283
|
+
"HOME": str(home),
|
|
284
|
+
"HERMES_FLEET_ENV": str(home / ".hermes" / "fleet.env"),
|
|
285
|
+
"REGISTRY_FILE": str(registry),
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
result = subprocess.run(
|
|
290
|
+
["bash", str(role / ".scripts" / "80-registry.sh")],
|
|
291
|
+
env=env,
|
|
292
|
+
text=True,
|
|
293
|
+
capture_output=True,
|
|
294
|
+
check=False,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
assert result.returncode == 0, result.stderr
|
|
298
|
+
entry = yaml.safe_load(registry.read_text(encoding="utf-8"))["agents"]["demo-pm"]
|
|
299
|
+
assert entry["slack"] == {
|
|
300
|
+
"provisioning_status": "verified",
|
|
301
|
+
"team_id": "T123",
|
|
302
|
+
"team_name": "Example Workspace",
|
|
303
|
+
"bot_user_id": "U123BOT",
|
|
304
|
+
"bot_id": "B123BOT",
|
|
305
|
+
"bot_username": "demo-pm",
|
|
306
|
+
}
|
|
307
|
+
serialized = registry.read_text(encoding="utf-8")
|
|
308
|
+
assert "SLACK_BOT_TOKEN" not in serialized
|
|
309
|
+
assert "SLACK_APP_TOKEN" not in serialized
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_concurrent_profiles_cannot_claim_same_slack_identity(tmp_path: Path) -> None:
|
|
313
|
+
role_a, _, _ = _make_role(tmp_path / "a")
|
|
314
|
+
role_b, _, _ = _make_role(tmp_path / "b")
|
|
315
|
+
role_b_yaml = role_b / "role.yaml"
|
|
316
|
+
role_b_yaml.write_text(
|
|
317
|
+
role_b_yaml.read_text(encoding="utf-8").replace("demo-pm", "demo-reviewer"),
|
|
318
|
+
encoding="utf-8",
|
|
319
|
+
)
|
|
320
|
+
registry = tmp_path / "agents-registry.yaml"
|
|
321
|
+
registry.write_text("schema_version: 1\nagents: {}\n", encoding="utf-8")
|
|
322
|
+
home = tmp_path / "home"
|
|
323
|
+
home.mkdir()
|
|
324
|
+
bindir = _fake_curl(tmp_path)
|
|
325
|
+
overrides = {"SLACK_BOT_TOKEN": BOT_TOKEN, "SLACK_APP_TOKEN": APP_TOKEN}
|
|
326
|
+
|
|
327
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
328
|
+
results = list(
|
|
329
|
+
pool.map(
|
|
330
|
+
lambda role: _run(role, registry, home, bindir, overrides),
|
|
331
|
+
(role_a, role_b),
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
assert sorted(result.returncode for result in results) == [0, 1]
|
|
336
|
+
combined = "".join(result.stdout + result.stderr for result in results)
|
|
337
|
+
assert "bot identity is already assigned" in combined
|
|
338
|
+
assert BOT_TOKEN not in combined
|
|
339
|
+
assert APP_TOKEN not in combined
|
|
340
|
+
agents = yaml.safe_load(registry.read_text(encoding="utf-8"))["agents"]
|
|
341
|
+
assert len(agents) == 1
|
|
342
|
+
assert next(iter(agents.values()))["slack"]["bot_id"] == "B123BOT"
|
|
343
|
+
assert stat.S_IMODE(registry.stat().st_mode) == 0o600
|
|
344
|
+
assert stat.S_IMODE(Path(f"{registry}.lock").stat().st_mode) == 0o600
|