-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsentinel.example.toml
More file actions
252 lines (226 loc) · 11.6 KB
/
Copy pathsentinel.example.toml
File metadata and controls
252 lines (226 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# Sentinel's configuration. Copy to sentinel.toml and edit.
#
# Everything defaults to the harmless answer: no custom rules, nothing armed.
# A missing file is exactly these defaults, so Sentinel starts safe and does
# nothing until you tell it what you care about.
[bot]
nsec_env = "SENTINEL_NSEC" # the env var holding the key, never the key
communities = ["*"] # or explicit community ids
# mod_channel = "mod-log" # channel NAME for the audit trail; omit to stay silent
# Named, every armed sentence is announced there BEFORE it is carried out, and
# a channel that cannot be reached holds the sentence rather than acting with
# no record of it.
poll_secs = 120 # floor of 90: the report is memoised that long
# How Sentinel introduces itself. Published as a kind-0 at boot, and only when
# it differs from what is already out there — a member who has just been warned
# looks up the npub that warned them, and an empty profile tells them nothing.
[bot.profile]
name = "Sentinel"
# about = "The watchtower for your Community"
# avatar = "https://..."
# banner = "https://..."
# Nothing is armed until you say so, and each class arms separately. A bot that
# warns is not thereby a bot that bans.
# An unarmed rung is still rehearsed and recorded, so the ladder climbs past it.
# Arming `kick` with `warn` off is a real configuration: the warning is
# rehearsed, and the kick is delivered.
# Changing any of these clears that community's slate: a rehearsal records
# nothing, so nobody carries a backlog of sentences that were never delivered
# into the run that would deliver them.
[arm]
warn = false
delete = false
kick = false
ban = false
raid = false
[limits]
max_actions_per_run = 25
max_actions_per_hour = 100
# The blast radius, NOT a rate limit — every member has their own strikes and
# their own rung on the ladder. This caps how many DISTINCT people Sentinel may
# answer for in one community in an hour before it stops and asks a person, so
# a misconfigured rule or a bad raid call cannot walk the whole memberlist.
# The percentage scales with the community; the floor stops a small one
# deadlocking on its second offender. Whichever is larger wins.
halt_if_over_pct = 10 # % of the roster, per hour
halt_floor = 3 # ...but never fewer than this many people
# The rules Sentinel compiles into an engine policy. Vector's built-in raid
# detection runs regardless; everything here is yours.
[rules]
window_hours = 168
window_messages = 4000
# [[rules.words]]
# id = "slurs"
# patterns = ["…"] # bare = whole word; *word* matches inside longer ones
# gravity = "grave"
# [[rules.links]]
# id = "shorteners"
# domains = ["bit.ly", "tinyurl.com"]
# gravity = "serious"
# [rules.rate] # too many messages too fast
# enabled = true
# per_secs = 60 # the window
# messages = 10 # how many inside it convict; ONE is a member talking
# gravity = "minor"
# [rules.mass_tagging] # one message naming a crowd; counts DISTINCT people
# enabled = true
# times = 15 # how many p-tags convict
# gravity = "serious"
# [rules.repetition] # the same line over and over, inside a burst
# enabled = true
# times = 4 # how many times; one is not a repeat
# gravity = "minor"
# What an offense costs, and what a running total answers to. Strikes halve
# every half-life, so forgiveness is built in rather than being a pardon
# somebody has to remember to issue.
[ladder]
strikes = { note = 1, minor = 2, serious = 4, grave = 12 }
decay_half_life_hours = 168
# The rungs, against a decayed running total. A rung is what happens to the
# MEMBER; what happens to the post is not a rung at all. Whatever a conviction
# cites is removed the first time it is answered, so content a community does
# not host never outlives the warning that says so.
#
# A member sitting inside a band is answered at that band again on a fresh
# offence, rather than climbing. With `grave` worth 12 that reads as: warned,
# warned again, kicked, banned. Nobody is banned without having been told twice.
[[ladder.steps]]
at = 1
response = "warn"
[[ladder.steps]]
at = 36
response = "kick"
[[ladder.steps]]
at = 48
response = "ban"
# A raid is one event, not forty members each earning strikes, so it skips the
# ladder entirely and answers to whatever you set here. This is also the ONLY
# place Sentinel acts on inference: a cohort reads high confidence and zero
# proven, nobody can replay it, and arming it is you overriding that on purpose.
[raid]
min_confidence = 75 # the Alert band's floor
response = "kick" # report | kick | ban
max_batch = 100 # per ban call; the wire rejects an over-cap banlist whole
# The live tripwire. The engine's verdict is memoised for 90 seconds, which is
# right for a background pass and far too slow for a wave in progress. When this
# many DISTINCT accounts speak or join inside the window, Sentinel drops the
# cache and evaluates immediately. It decides WHEN to ask, never who is guilty.
tripwire_accounts = 5
tripwire_secs = 30
tripwire_cooldown_secs = 60 # an evaluation is a full corpus read
# How long containing a member binds. Long enough that a wave arriving over many
# sweeps is one event; short enough that the same accounts raiding next week are
# a new one.
claim_ttl_secs = 21600 # 6 hours
# The media lane. Off by default: it decrypts attachments and ships the bytes to
# a model, and that is a decision an operator makes rather than inherits.
#
# llama.cpp's llama-server speaks this shape out of the box. A hosted endpoint
# is the same block with a different base_url and an api_key_env — but note that
# an attachment is end-to-end encrypted right up until Sentinel decrypts it and
# posts it to somebody else's server, so allow_remote must be set on purpose.
#
# `provider` picks the wire. "openai" is any OpenAI-compatible endpoint, local or
# hosted. "tee" is a confidential-computing enclave: the request body is sealed
# with HPKE to a key the enclave proved it holds, so neither the billing proxy
# nor the operator of the machine running the model can read the attachment.
#
# For "tee", name the enclave-internal model (gemma4-31b, not private/gemma4-31b)
# and point api_key_env at your proxy key. The enclave_host, enclave_repo and
# enclave_proxy keys default to Tinfoil's inference router, which is what PPQ's
# private models sit behind, and base_url goes unused.
#
# What is attested is that ROUTER, which terminates the encrypted channel and
# forwards to a model enclave it verified itself. So the guarantee is that
# measured, publicly audited code handles the plaintext — not that nothing does.
# Worth knowing before describing it to a community. The verified release is
# printed at boot.
[vision]
enabled = false
provider = "openai"
base_url = "http://127.0.0.1:8080/v1"
model = "llava"
api_key_env = "" # empty = local, no auth
allow_remote = false # required for any host that is not loopback
timeout_secs = 60
max_bytes = 8388608
max_per_min = 20
# A ceiling on the model's ANSWER, not its input. The reply is a small JSON map;
# anything longer is a model talking to itself, and a loop that runs to
# timeout_secs holds that community's one blob slot for the whole of it.
max_answer_tokens = 256
# A model that answers in prose, fences its JSON, or drops a label is asked
# again with the fault named. Bounded, not "until it complies": a model that
# never complies would hold the blob slot and spend the budget forever, and
# unjudged-then-escalated is the safe end of that.
max_attempts = 3
# Local reasoning models otherwise spend the whole budget deliberating about a
# picture and answer with truncated JSON, which reads as unjudged. Empty omits
# the field; an endpoint that rejects it is retried once without it.
reasoning_effort = "none"
# Whatever your model can read. Sentinel sends the bytes; an endpoint that
# cannot answer for a video says so, and an unanswered attachment goes to a
# person rather than being treated as clean.
mimes = ["image/png", "image/jpeg", "image/webp", "image/gif", "video/mp4", "video/webm"]
# Clips are not sent to the model whole — no vision endpoint reads an mp4.
# ffmpeg samples frames evenly across the WHOLE clip and tiles them into one
# contact sheet, which the model judges in a single call. Animated GIF and WebP
# go the same way: content in frame fifty is invisible to a model shown only the
# first. Needs ffmpeg and ffprobe on PATH; without them clips reach a person
# unjudged, and still images are unaffected.
[vision.video]
enabled = true
ffmpeg = "ffmpeg" # or an absolute path
ffprobe = "ffprobe"
cols = 3 # the grid. More tiles buy resolution in time
rows = 2 # and cost the model pixels.
tile_width = 512 # per tile; height follows the source aspect
timeout_secs = 30 # a wall clock on ffmpeg itself
max_duration_secs = 600 # longest clip to sample across; 0 = no cap
# Each label carries YOUR definition of it, which is sent to the model with the
# name. A bare name is the model's guess at what your community means by a word;
# a sentence is your answer. `describe` is optional and badly missed.
#
# The model returns a score for every label AND one sentence describing what the
# media shows. That sentence goes on the strike record, so a moderator reading it
# months later knows what was posted without reopening it.
# [[vision.labels]]
# name = "sexual_content"
# describe = "Unclothed or barely-clothed bodies, suggestive posing, or sexual activity."
# threshold = 0.90
# gravity = "grave"
# [[vision.labels]]
# name = "gore"
# describe = "Real injury, blood, mutilation or death. Not fictional or cartoon violence."
# threshold = 0.90
# gravity = "grave"
[shields]
respect_trusted = true # a trusted regular is queued for a human, never actioned
respect_protected = true # the owner and moderators are never Sentinel's to judge
# ── Per-community overrides ─────────────────────────────────────────────────
#
# Everything above is the default. A community can override the blocks that
# describe how it is judged: arm, limits, ladder, shields, raid and rules. The
# bot and vision blocks are process-wide and have no per-community form.
# Nothing about how Sentinel judges one community reaches another: separate
# rulebooks, ladders, arming, tripwires and strike history.
#
# A key Sentinel does not know is refused at boot rather than ignored, so a
# typo names itself instead of quietly leaving the default in place.
#
# Sentinel also reads what each community actually PERMITS it to do. Being a
# member is not being a moderator: a community can grant MANAGE_MESSAGES and
# withhold BAN, and a sentence it cannot carry out is reported as such rather
# than attempted. The boot line says which powers it has where.
#
# [community."fe4abeb3fd227a67fc59d8a4363420649bb970436dc3b14d51c2b66fee334dea"]
# arm = { warn = true, delete = true, kick = true, ban = false, raid = false }
#
# Overrides fold FIELD by field, so naming one thing leaves the rest of your
# settings alone:
#
# [community."fe4abeb3fd227a67fc59d8a4363420649bb970436dc3b14d51c2b66fee334dea".limits]
# halt_if_over_pct = 20 # your other limits are untouched
#
# [community."fe4abeb3fd227a67fc59d8a4363420649bb970436dc3b14d51c2b66fee334dea".ladder]
# decay_half_life_hours = 72 # your steps and strike worths are untouched