ai-code-engineer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_code_engineer/__init__.py +2 -0
- ai_code_engineer/catalog.py +143 -0
- ai_code_engineer/chat.py +181 -0
- ai_code_engineer/cli.py +384 -0
- ai_code_engineer/config.py +405 -0
- ai_code_engineer/engine.py +1282 -0
- ai_code_engineer/errors.py +27 -0
- ai_code_engineer/git_integration.py +443 -0
- ai_code_engineer/gui.py +2646 -0
- ai_code_engineer/host.py +81 -0
- ai_code_engineer/ignore.py +269 -0
- ai_code_engineer/intent.py +222 -0
- ai_code_engineer/labels.py +871 -0
- ai_code_engineer/memory.py +91 -0
- ai_code_engineer/modes.py +156 -0
- ai_code_engineer/overrides.py +540 -0
- ai_code_engineer/planbook.py +192 -0
- ai_code_engineer/providers.py +404 -0
- ai_code_engineer/redaction.py +54 -0
- ai_code_engineer/repair.py +564 -0
- ai_code_engineer/report.py +352 -0
- ai_code_engineer/runner.py +854 -0
- ai_code_engineer/setup.py +386 -0
- ai_code_engineer/symbols.py +1286 -0
- ai_code_engineer/verification.py +218 -0
- ai_code_engineer/webapp/__init__.py +1 -0
- ai_code_engineer/webapp/__main__.py +45 -0
- ai_code_engineer/webapp/contract.py +36 -0
- ai_code_engineer/webapp/controller.py +3556 -0
- ai_code_engineer/webapp/fake.py +1141 -0
- ai_code_engineer/webapp/launch.py +108 -0
- ai_code_engineer/webapp/server.py +349 -0
- ai_code_engineer/webapp/static/app.css +780 -0
- ai_code_engineer/webapp/static/app.js +2118 -0
- ai_code_engineer/webapp/static/boot.js +19 -0
- ai_code_engineer/webapp/static/index.html +89 -0
- ai_code_engineer/webapp/static/tokens.css +173 -0
- ai_code_engineer/workspace.py +385 -0
- ai_code_engineer-0.1.0.dist-info/METADATA +7 -0
- ai_code_engineer-0.1.0.dist-info/RECORD +44 -0
- ai_code_engineer-0.1.0.dist-info/WHEEL +5 -0
- ai_code_engineer-0.1.0.dist-info/entry_points.txt +2 -0
- ai_code_engineer-0.1.0.dist-info/licenses/LICENSE +21 -0
- ai_code_engineer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""Configuration the program signs, so an edit made outside it is seen rather than followed.
|
|
2
|
+
|
|
3
|
+
The request was a file a person can change without touching a profile or a window's code — and
|
|
4
|
+
"encrypted, so nobody can edit it except from inside the program". Encryption is the part this tool
|
|
5
|
+
cannot deliver honestly: the standard library has no cipher, the key would have to sit on the same
|
|
6
|
+
account as the file it protects, and the contents are not secret — a credential *value* is refused on
|
|
7
|
+
the way in, so what remains is a model name, an address and four numbers.
|
|
8
|
+
|
|
9
|
+
What does answer the fear is integrity. Every row is signed with a per-install secret, and a file
|
|
10
|
+
whose rows were added, reordered, retyped or deleted by hand is **not obeyed**: the tool names the rows
|
|
11
|
+
it refused and carries on with the profile. Honest scope, in the words the file itself prints — this is
|
|
12
|
+
tamper evidence, not access control. The owner of this account can rewrite both files, and nothing on a
|
|
13
|
+
local filesystem stops them. What an edit from outside the program cannot do is change behaviour
|
|
14
|
+
quietly.
|
|
15
|
+
|
|
16
|
+
Nothing here decides what a value *means*: ``config.py`` owns the provider table, the ranges and the
|
|
17
|
+
one ``validate``. This module owns the file, the signature and the refusal.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import hmac
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import secrets
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
from . import redaction
|
|
30
|
+
from .labels import say
|
|
31
|
+
|
|
32
|
+
FILE = ".agent-overrides.json"
|
|
33
|
+
KEY_FILE = ".agent-overrides.key"
|
|
34
|
+
|
|
35
|
+
# The target that means "for every provider". A row naming one provider beats it.
|
|
36
|
+
EVERY = "*"
|
|
37
|
+
|
|
38
|
+
# The keys a row may carry, and the JSON type each is stored as. `provider` is deliberately absent:
|
|
39
|
+
# which vendor answers a task is decided by the row on screen or the profile being read, and a side
|
|
40
|
+
# file able to swap it is the same failure ``providers.py`` refuses upstream fallbacks for — the model
|
|
41
|
+
# that answered would not be the one that was reviewed.
|
|
42
|
+
TYPES = {"model": str, "endpoint": str, "api_key_env": str, "max_turns": int,
|
|
43
|
+
"timeout_seconds": int, "context_chars": int, "output_tokens": int}
|
|
44
|
+
|
|
45
|
+
# A row with target `EVERY` states a preference that holds for every provider. An address and a key
|
|
46
|
+
# variable belong to one provider, so a wildcard row for either would silently aim a different vendor's
|
|
47
|
+
# traffic at it.
|
|
48
|
+
PER_PROVIDER = ("endpoint", "api_key_env")
|
|
49
|
+
|
|
50
|
+
# normcase(path) -> ((mtime_ns, size, key bytes), (accepted rows, refusals)). A snapshot goes out on
|
|
51
|
+
# every streamed log line, so a read must not re-HMAC the disk each time — the same idiom `modes` uses.
|
|
52
|
+
_cache: dict[str, tuple[tuple, tuple[list, list]]] = {}
|
|
53
|
+
# normcase(key path) -> (mtime_ns, size, bytes). The key is read on every snapshot's cache check, so it
|
|
54
|
+
# is cached too: a hot path that stats and reads a file per call is a hot path that shifts a race.
|
|
55
|
+
_keys: dict[str, tuple[int, int, bytes]] = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def path(app_dir) -> Path:
|
|
59
|
+
return Path(app_dir) / FILE
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def key_path(app_dir) -> Path:
|
|
63
|
+
return Path(app_dir) / KEY_FILE
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------- the signature ----------------------------------------------------------------
|
|
67
|
+
def _secret(app_dir, create: bool = False) -> bytes:
|
|
68
|
+
"""This install's signing key, or ``b""`` when there is none and none was asked for.
|
|
69
|
+
|
|
70
|
+
Read from a file the program creates and never ships. A key written in the code would be a key in
|
|
71
|
+
the repository, which is the mistake the profiles already refuse to make about API keys.
|
|
72
|
+
|
|
73
|
+
Cached by the file's stamp because ``config`` consults this on the resolution order, and the
|
|
74
|
+
resolution order runs on every snapshot a window pushes — a per-push read of a file nobody writes
|
|
75
|
+
is a cost with no answer behind it.
|
|
76
|
+
"""
|
|
77
|
+
file = key_path(app_dir)
|
|
78
|
+
stamp = os.path.normcase(str(file))
|
|
79
|
+
try:
|
|
80
|
+
info = file.stat()
|
|
81
|
+
except OSError:
|
|
82
|
+
_keys.pop(stamp, None)
|
|
83
|
+
return _create_secret(app_dir, stamp) if create else b""
|
|
84
|
+
held = _keys.get(stamp)
|
|
85
|
+
if held and held[0] == (info.st_mtime_ns, info.st_size):
|
|
86
|
+
return held[2]
|
|
87
|
+
try:
|
|
88
|
+
secret = bytes.fromhex(file.read_text(encoding="utf-8").strip())
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
_keys.pop(stamp, None)
|
|
91
|
+
return _create_secret(app_dir, stamp) if create else b""
|
|
92
|
+
_keys[stamp] = (info.st_mtime_ns, info.st_size, secret)
|
|
93
|
+
return secret
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _create_secret(app_dir, stamp: str) -> bytes:
|
|
97
|
+
secret = secrets.token_hex(32)
|
|
98
|
+
file = key_path(app_dir)
|
|
99
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
file.write_text(secret + "\n", encoding="utf-8")
|
|
101
|
+
try:
|
|
102
|
+
os.chmod(file, 0o600) # Windows has no per-user mode; the attempt is the whole promise there
|
|
103
|
+
except OSError:
|
|
104
|
+
pass
|
|
105
|
+
raw = bytes.fromhex(secret)
|
|
106
|
+
_keys[stamp] = (file.stat().st_mtime_ns, file.stat().st_size, raw)
|
|
107
|
+
return raw
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _canonical(rows: list) -> bytes:
|
|
111
|
+
"""The bytes that get signed: the list as stored, one key order per row, no whitespace.
|
|
112
|
+
|
|
113
|
+
Not sorted here. ``_write`` already stores the rows in one order it computes itself, so signing
|
|
114
|
+
the stored list means a hand-edit that only *reordered* them changes the bytes and breaks the
|
|
115
|
+
signature — which is an edit, and the whole reason the file is signed.
|
|
116
|
+
"""
|
|
117
|
+
return json.dumps(rows, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _signature(secret: bytes, rows: list) -> str:
|
|
121
|
+
return hmac.new(secret, _canonical(rows), hashlib.sha256).hexdigest()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------- the document ----------------------------------------------------------------
|
|
125
|
+
def document() -> dict:
|
|
126
|
+
"""The block the file carries beside its rows: what may be set, and to what.
|
|
127
|
+
|
|
128
|
+
Every limit here is *read* from ``config`` instead of written out again. A file that stated its own
|
|
129
|
+
ranges would be a second table, and the second table is what eventually disagrees with the
|
|
130
|
+
validator.
|
|
131
|
+
"""
|
|
132
|
+
from . import config # `config` reads this module; one of the two directions has to wait
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"what": ("Configuration rows the program signs. Change them from inside the program - "
|
|
136
|
+
"Settings then Overrides, or `agent overrides` - never by editing this file. Rows the "
|
|
137
|
+
"program cannot verify are refused and named, and it runs on the profile instead."),
|
|
138
|
+
"why not encrypted": ("These contents are not secret and a cipher that this install can open "
|
|
139
|
+
"on its own cannot keep a person out of their own file, so the file is "
|
|
140
|
+
"signed instead: an edit made outside the program is seen and refused "
|
|
141
|
+
"rather than followed."),
|
|
142
|
+
"order": ["what you typed into the window for this run",
|
|
143
|
+
"a signed row in this file",
|
|
144
|
+
"the profile being read",
|
|
145
|
+
"the provider's environment variable",
|
|
146
|
+
"the provider's own row inside the program"],
|
|
147
|
+
"keys": {key: (f"a number, {config.LIMITS[key][0]} to {config.LIMITS[key][1]}"
|
|
148
|
+
if key in config.LIMITS else "text, and never a credential value")
|
|
149
|
+
for key in sorted(TYPES)},
|
|
150
|
+
"targets": {key: f"applies when the provider being used is {key}"
|
|
151
|
+
for key in sorted(config.BY_KEY)},
|
|
152
|
+
EVERY: "applies to every provider, and loses to a row that names one",
|
|
153
|
+
"not here": ["the provider - the choice on the screen or in the profile, not a preference",
|
|
154
|
+
"an API key - only the name of the variable holding one, exactly as a profile does",
|
|
155
|
+
"an address for a provider whose profile already spells one out: a row can supply "
|
|
156
|
+
"an address nobody wrote down, but it never redirects one somebody did"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------- the rules ----------------------------------------------------------------
|
|
161
|
+
def _refusable(key: str, value, target: str) -> str:
|
|
162
|
+
"""Why this row cannot be used, or "" when it can. Keys and types — ranges are `validate`'s."""
|
|
163
|
+
if key not in TYPES:
|
|
164
|
+
return "unknown"
|
|
165
|
+
if target == EVERY and key in PER_PROVIDER:
|
|
166
|
+
return "per-provider"
|
|
167
|
+
wanted = TYPES[key]
|
|
168
|
+
if wanted is int:
|
|
169
|
+
if isinstance(value, bool) or not isinstance(value, (int, str)):
|
|
170
|
+
return "not a number"
|
|
171
|
+
try:
|
|
172
|
+
int(str(value).strip())
|
|
173
|
+
except ValueError:
|
|
174
|
+
return "not a number"
|
|
175
|
+
return ""
|
|
176
|
+
if not isinstance(value, str):
|
|
177
|
+
return "not text"
|
|
178
|
+
return "empty" if not value.strip() else ""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------- reading ----------------------------------------------------------------
|
|
182
|
+
def _read(app_dir) -> tuple[list, list]:
|
|
183
|
+
"""The rows this file signs, and the rows it refused with the reason for each.
|
|
184
|
+
|
|
185
|
+
A bad signature refuses the whole file rather than one row: the signature covers the row set, so
|
|
186
|
+
deleting or reordering a row is an edit too, and the safe reading of a file nobody can account for
|
|
187
|
+
is "this one is not mine".
|
|
188
|
+
"""
|
|
189
|
+
file = path(app_dir)
|
|
190
|
+
stamp = os.path.normcase(str(file))
|
|
191
|
+
try:
|
|
192
|
+
info = file.stat()
|
|
193
|
+
except OSError:
|
|
194
|
+
_cache.pop(stamp, None)
|
|
195
|
+
return [], []
|
|
196
|
+
secret = _secret(app_dir)
|
|
197
|
+
now = ((info.st_mtime_ns, info.st_size), secret)
|
|
198
|
+
cached = _cache.get(stamp)
|
|
199
|
+
if cached and cached[0] == now:
|
|
200
|
+
return cached[1]
|
|
201
|
+
try:
|
|
202
|
+
text = file.read_text(encoding="utf-8")
|
|
203
|
+
except OSError:
|
|
204
|
+
# A file the operating system will not hand over is not a statement about the configuration,
|
|
205
|
+
# so nothing is refused and nothing is applied — the same direction `modes` reads an unreadable
|
|
206
|
+
# declaration in.
|
|
207
|
+
_cache[stamp] = (now, ([], []))
|
|
208
|
+
return [], []
|
|
209
|
+
accepted: list = []
|
|
210
|
+
refused: list = []
|
|
211
|
+
try:
|
|
212
|
+
raw = json.loads(text)
|
|
213
|
+
except ValueError:
|
|
214
|
+
raw = None
|
|
215
|
+
refused.append(_refused(EVERY, "*", "unreadable"))
|
|
216
|
+
if isinstance(raw, dict):
|
|
217
|
+
stored = raw.get("rows")
|
|
218
|
+
if not isinstance(stored, list):
|
|
219
|
+
refused.append(_refused(EVERY, "*", "no-rows"))
|
|
220
|
+
elif not secret or not hmac.compare_digest(str(raw.get("signature") or ""),
|
|
221
|
+
_signature(secret, stored)):
|
|
222
|
+
refused.append(_refused(EVERY, "*", "changed-outside"))
|
|
223
|
+
else:
|
|
224
|
+
for row in stored:
|
|
225
|
+
if not isinstance(row, dict):
|
|
226
|
+
continue
|
|
227
|
+
target = str(row.get("target") or EVERY).strip().casefold() or EVERY
|
|
228
|
+
key = str(row.get("key") or "").strip().casefold()
|
|
229
|
+
why = _refusable(key, row.get("value"), target)
|
|
230
|
+
if why:
|
|
231
|
+
refused.append(_refused(target, key, why))
|
|
232
|
+
else:
|
|
233
|
+
accepted.append({"target": target, "key": key,
|
|
234
|
+
"value": int(row["value"]) if TYPES[key] is int
|
|
235
|
+
else str(row["value"]).strip(),
|
|
236
|
+
"at": str(row.get("at") or "")[:32],
|
|
237
|
+
"by": str(row.get("by") or "")[:40]})
|
|
238
|
+
elif raw is not None:
|
|
239
|
+
refused.append(_refused(EVERY, "*", "wrong-shape"))
|
|
240
|
+
_cache[stamp] = (now, (accepted, refused))
|
|
241
|
+
return accepted, refused
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _refused(target: str, key: str, why: str) -> dict:
|
|
245
|
+
return {"target": target, "key": key, "why": why}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def rows(app_dir) -> list:
|
|
249
|
+
"""Every row the file signs and the schema still knows."""
|
|
250
|
+
return [dict(row) for row in _read(app_dir)[0]]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def refusals(app_dir) -> list:
|
|
254
|
+
"""What this file asked for that the program will not do — the named half of refuse-and-report."""
|
|
255
|
+
return [dict(row) for row in _read(app_dir)[1]]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def note(app_dir, key: str, why: str, target: str = EVERY) -> None:
|
|
259
|
+
"""Record a refusal only the merge could find, so one list still tells the whole story.
|
|
260
|
+
|
|
261
|
+
The rows are individually valid, but a read can still refuse them as a set — a schema that shrank
|
|
262
|
+
between versions is the way. Dropping them silently would be the exact failure this file exists to
|
|
263
|
+
prevent, and raising would stop a task that has nothing to do with the row.
|
|
264
|
+
"""
|
|
265
|
+
cached = _cache.get(os.path.normcase(str(path(app_dir))))
|
|
266
|
+
if cached:
|
|
267
|
+
cached[1][1].append(_refused(target, key, why))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def values(app_dir, provider: str) -> dict:
|
|
271
|
+
"""The rows that speak for this provider: the wildcard first, so a row naming it wins."""
|
|
272
|
+
merged: dict = {}
|
|
273
|
+
for row in sorted(rows(app_dir), key=lambda item: (item["target"] != EVERY, item["at"])):
|
|
274
|
+
if row["target"] == EVERY or row["target"] == provider:
|
|
275
|
+
merged[row["key"]] = row["value"]
|
|
276
|
+
return merged
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def listed(app_dir) -> list[dict]:
|
|
280
|
+
"""Rows and refusals together, newest first, for a person asking what is in force."""
|
|
281
|
+
kept = [{**row, "state": "in force", "why": ""} for row in rows(app_dir)]
|
|
282
|
+
refused = [{**row, "state": "refused", "value": ""} for row in refusals(app_dir)]
|
|
283
|
+
return sorted(kept + refused, key=lambda row: row.get("at", ""), reverse=True)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------- writing ----------------------------------------------------------------
|
|
287
|
+
def _write(app_dir, kept: list) -> None:
|
|
288
|
+
from .engine import atomic_json # `engine` reads `config`, which reads this module
|
|
289
|
+
|
|
290
|
+
# One order, decided here and signed here: signing a list and then storing a re-ordered copy of it
|
|
291
|
+
# would leave the program unable to verify its own file.
|
|
292
|
+
ordered = sorted(kept, key=lambda row: (row["target"], row["key"]))
|
|
293
|
+
atomic_json(path(app_dir), {"read first": document(), "rows": ordered,
|
|
294
|
+
"signature": _signature(_secret(app_dir, create=True), ordered)})
|
|
295
|
+
_cache.pop(os.path.normcase(str(path(app_dir))), None)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def ensure(app_dir) -> list[str]:
|
|
299
|
+
"""Create the key and the file on first run. An existing file is never rewritten.
|
|
300
|
+
|
|
301
|
+
Born as the schema rather than as an empty object: `{}` documents nothing and would be rewritten
|
|
302
|
+
every launch, while a file that lists its own keys, their ranges and the resolution order is the
|
|
303
|
+
documentation a person actually opens.
|
|
304
|
+
"""
|
|
305
|
+
if path(app_dir).exists():
|
|
306
|
+
return []
|
|
307
|
+
key_exists = key_path(app_dir).exists()
|
|
308
|
+
_secret(app_dir, create=True)
|
|
309
|
+
_write(app_dir, [])
|
|
310
|
+
return [KEY_FILE, FILE] if not key_exists else [FILE]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def put(app_dir, target: str, key: str, value, by: str = "", *, arabic: bool = False) -> dict:
|
|
314
|
+
"""Sign one row into the file, or raise with the reason this value was refused.
|
|
315
|
+
|
|
316
|
+
The value is judged by the program's own ``validate``, so a row cannot state a limit the tool would
|
|
317
|
+
then refuse at run time — and the sentence that comes back is the validator's, not a second copy of
|
|
318
|
+
the rules written here. The refusals this module writes itself are written in the asking window's
|
|
319
|
+
language; the validator's are English everywhere in this tool, and one more half-translation would
|
|
320
|
+
only make the two windows disagree about a range.
|
|
321
|
+
"""
|
|
322
|
+
from . import config
|
|
323
|
+
|
|
324
|
+
target = str(target or EVERY).strip().casefold() or EVERY
|
|
325
|
+
key = str(key or "").strip().casefold()
|
|
326
|
+
if target != EVERY and config.kind_for(target) is None:
|
|
327
|
+
raise config.AgentError(unknown_target(target, arabic=arabic))
|
|
328
|
+
why = _refusable(key, value, target)
|
|
329
|
+
if why:
|
|
330
|
+
raise config.AgentError(bad_value(key, why, arabic=arabic))
|
|
331
|
+
cleaned = {"target": target, "key": key, "at": time.strftime("%Y-%m-%d %H:%M"),
|
|
332
|
+
"by": str(by or "")[:40],
|
|
333
|
+
"value": int(str(value).strip()) if TYPES[key] is int else str(value).strip()}
|
|
334
|
+
if TYPES[key] is str and redaction.redact(cleaned["value"]) != cleaned["value"]:
|
|
335
|
+
raise config.AgentError(credential_refused(key, arabic=arabic))
|
|
336
|
+
kind = config.kind_for(target) or config.DEFAULT_KIND
|
|
337
|
+
config.validate(_candidate(config, kind, key, cleaned["value"]))
|
|
338
|
+
kept = [row for row in rows(app_dir)
|
|
339
|
+
if not (row["target"] == target and row["key"] == key)]
|
|
340
|
+
kept.append(cleaned)
|
|
341
|
+
_write(app_dir, kept)
|
|
342
|
+
return cleaned
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _candidate(config, kind, key: str, value):
|
|
346
|
+
"""A Settings for one provider row with this one value in it, for the validator to judge.
|
|
347
|
+
|
|
348
|
+
The provider comes from the row's own target, because that is the only rule set the address has to
|
|
349
|
+
satisfy: a row for Groq is judged by Groq's https rule, not by whichever row the window is on.
|
|
350
|
+
"""
|
|
351
|
+
from dataclasses import replace
|
|
352
|
+
|
|
353
|
+
return replace(config.Settings(), provider=kind.key, **{key: value})
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def delete(app_dir, target: str, key: str) -> bool:
|
|
357
|
+
"""Take one row out of the file; whether it was there is the answer."""
|
|
358
|
+
target = str(target or EVERY).strip().casefold() or EVERY
|
|
359
|
+
key = str(key or "").strip().casefold()
|
|
360
|
+
before = rows(app_dir)
|
|
361
|
+
kept = [row for row in before if not (row["target"] == target and row["key"] == key)]
|
|
362
|
+
if len(kept) == len(before):
|
|
363
|
+
return False
|
|
364
|
+
_write(app_dir, kept)
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------- the sentences ----------------------------------------------------------------
|
|
369
|
+
def fields() -> list[dict]:
|
|
370
|
+
"""What the row form offers: every key, whether it takes a number, and the range it is judged by.
|
|
371
|
+
|
|
372
|
+
The ranges are read out of ``config.LIMITS`` rather than repeated here, so a drawer cannot advertise
|
|
373
|
+
a number the validator then refuses — the failure the request timeout already had to be fixed for.
|
|
374
|
+
"""
|
|
375
|
+
from . import config
|
|
376
|
+
|
|
377
|
+
out = []
|
|
378
|
+
for key in sorted(TYPES):
|
|
379
|
+
low, high = config.LIMITS.get(key, (None, None))
|
|
380
|
+
out.append({"key": key, "number": TYPES[key] is int, "low": low, "high": high,
|
|
381
|
+
"per_provider": key in PER_PROVIDER})
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def unknown_target(target: str, *, arabic: bool) -> str:
|
|
386
|
+
from . import config
|
|
387
|
+
|
|
388
|
+
return say(arabic,
|
|
389
|
+
en=f"A target is a provider — {', '.join(sorted(config.BY_KEY))} — or {EVERY} for "
|
|
390
|
+
f"all of them, not {target}.",
|
|
391
|
+
ar=f"الهدف يكون اسم بروفايدر — {'، '.join(sorted(config.BY_KEY))} — أو {EVERY} لكل "
|
|
392
|
+
f"البروفايدرات، مش {target}.")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def bad_value(key: str, why: str, *, arabic: bool) -> str:
|
|
396
|
+
from . import config
|
|
397
|
+
|
|
398
|
+
reasons = {"unknown": (f"There is no configuration field named {key} to override.",
|
|
399
|
+
f"مفيش خانة إعداد اسمها {key} تتغيّر."),
|
|
400
|
+
"per-provider": (f"{key} belongs to one provider, so it cannot be set for all of them "
|
|
401
|
+
"at once. Name the provider.",
|
|
402
|
+
f"{key} بتاع بروفايدر واحد، فمش ممكن يتظبط لكل البروفايدرات مع بعض. "
|
|
403
|
+
"اكتب اسم البروفايدر."),
|
|
404
|
+
"not a number": (f"{key} is a whole number.", f"{key} لازم يكون رقم كامل."),
|
|
405
|
+
"not text": (f"{key} is a piece of text.", f"{key} لازم يكون نص."),
|
|
406
|
+
"empty": (f"{key} cannot be blank. Remove the row instead of emptying it.",
|
|
407
|
+
f"{key} ما ينفعش فاضي. امسح السطر بدل ما تسيّبه فاضي.")}
|
|
408
|
+
en, ar = reasons.get(why, (f"{key} cannot be stored: {why}.", f"{key} ما يتخزنش: {why}."))
|
|
409
|
+
if why == "unknown":
|
|
410
|
+
en += " The file lists what may be set: " + ", ".join(sorted(TYPES)) + "."
|
|
411
|
+
ar += " الملف بيقارن اللي ممكن يتغيّر: " + "، ".join(sorted(TYPES)) + "."
|
|
412
|
+
if why == "not a number":
|
|
413
|
+
low, high = config.LIMITS.get(key, (None, None))
|
|
414
|
+
if low is not None:
|
|
415
|
+
en += f" Between {low} and {high}."
|
|
416
|
+
ar += f" بين {low} و {high}."
|
|
417
|
+
return say(arabic, en=en, ar=ar)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def credential_refused(key: str, *, arabic: bool) -> str:
|
|
421
|
+
return say(arabic,
|
|
422
|
+
en=f"{key} looks like a credential, and no key goes in this file. Name the environment "
|
|
423
|
+
f"variable that holds it, exactly as a profile does.",
|
|
424
|
+
ar=f"{key} بيشبه مفتاح، ومفيش مفتاح بيتكتب في الملف ده. اكتب اسم متغير البيئة اللي "
|
|
425
|
+
f"فيه، زي البروفايدر بالظبط.")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def written(row: dict, *, arabic: bool) -> str:
|
|
429
|
+
"""What a save costs the operator to believe: the row, what it now overrides, and where it stops."""
|
|
430
|
+
return say(arabic,
|
|
431
|
+
en=f"{row['key']} = {row['value']} for "
|
|
432
|
+
+ ("every provider" if row["target"] == EVERY else row["target"])
|
|
433
|
+
+ ". Signed, and read by both windows and the terminal from now on.",
|
|
434
|
+
ar=f"{row['key']} = {row['value']} لـ "
|
|
435
|
+
+ ("كل البروفايدرات" if row["target"] == EVERY else row["target"])
|
|
436
|
+
+ ". اتوقع واتقرأ من النافذتين ومن التيرمنال من دلوقتي.")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def removed(key: str, *, arabic: bool) -> str:
|
|
440
|
+
return say(arabic,
|
|
441
|
+
en=f"{key} is no longer overridden. The profile, the environment variable and the "
|
|
442
|
+
f"provider's own row answer in its place, in that order.",
|
|
443
|
+
ar=f"{key} بقى مش متغيّر. البروفايدر، وبعده متغير البيئة، وبعده صف البروفايدر نفسه "
|
|
444
|
+
f"هي اللي ترد، بالترتيب ده.")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def absent(key: str, *, arabic: bool) -> str:
|
|
448
|
+
return say(arabic, en=f"There was no row for {key} to remove.",
|
|
449
|
+
ar=f"ما كانش في سطر لـ {key} يتشال.")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def nothing_yet(*, arabic: bool) -> str:
|
|
453
|
+
return say(arabic,
|
|
454
|
+
en="Nothing is overridden. Every value comes from its profile, its provider's "
|
|
455
|
+
"environment variable, or the provider's own row.",
|
|
456
|
+
ar="مفيش حاجة متغيّرة. كل قيمة جايّة من البروفايدر، أو من متغير البيئة تبعه، أو من صفه "
|
|
457
|
+
"نفسه.")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def spoken(rows: list, *, arabic: bool) -> list[dict]:
|
|
461
|
+
"""One mapping from a row to what a surface may print — used by the file and by the preview.
|
|
462
|
+
|
|
463
|
+
A refusal code is internal; the sentence is ``why_text``'s, in the language the operator asked in,
|
|
464
|
+
and a drawer that drew the code would show an English identifier inside an Arabic window. A refused
|
|
465
|
+
row is not drawn with its value, because the program is not using that value.
|
|
466
|
+
"""
|
|
467
|
+
out = []
|
|
468
|
+
for row in rows:
|
|
469
|
+
item = dict(row)
|
|
470
|
+
if item["state"] != "in force":
|
|
471
|
+
item["why"] = why_text(item.get("why", ""), arabic=arabic)
|
|
472
|
+
item["display"] = says(item, arabic=arabic)
|
|
473
|
+
else:
|
|
474
|
+
item["display"] = f"{row['key']} = {row['value']}"
|
|
475
|
+
out.append(item)
|
|
476
|
+
return out
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def reported(app_dir, *, arabic: bool) -> list[dict]:
|
|
480
|
+
"""The rows on disk, spoken."""
|
|
481
|
+
return spoken(listed(app_dir), arabic=arabic)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def says(row: dict, *, arabic: bool) -> str:
|
|
485
|
+
"""What a refused row is called: a row-level one names its key, a file-level one names the file.
|
|
486
|
+
|
|
487
|
+
A wildcard target is spelled out because `*` alone in a sentence reads like a mistake rather than
|
|
488
|
+
like "this row cannot apply to every provider at once".
|
|
489
|
+
"""
|
|
490
|
+
if row["key"] == EVERY:
|
|
491
|
+
return say(arabic, en="the whole file", ar="الملف كله")
|
|
492
|
+
target = (say(arabic, en="every provider", ar="كل البروفايدرات")
|
|
493
|
+
if row["target"] == EVERY else row["target"])
|
|
494
|
+
return f"{row['key']} for {target}"
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def refused_line(refused: list, *, arabic: bool) -> str:
|
|
498
|
+
"""One line naming every row the program would not obey — a refusal without a name reads as a bug."""
|
|
499
|
+
if not refused:
|
|
500
|
+
return ""
|
|
501
|
+
parts = [f"{says(row, arabic=arabic)}: {why_text(row['why'], arabic=arabic)}"
|
|
502
|
+
for row in refused]
|
|
503
|
+
head = say(arabic, en="Overrides not used:", ar="تعديلات ما اتطبقتش:")
|
|
504
|
+
return head + " " + ("؛ " if arabic else "; ").join(parts)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def why_text(why: str, *, arabic: bool) -> str:
|
|
508
|
+
from . import config
|
|
509
|
+
|
|
510
|
+
known = {"changed-outside": (f"the rows were changed outside the program, so none of them are "
|
|
511
|
+
f"trusted — re-save what you want from Settings or `agent overrides`",
|
|
512
|
+
f"السطور اتغيرت من برة البرنامج، فولا واحد فيهم محل ثقة — أعد الحفظ "
|
|
513
|
+
f"من Settings أو `agent overrides`"),
|
|
514
|
+
"unreadable": ("the file could not be read as the JSON the program writes",
|
|
515
|
+
"الملف ما اتقرش كـ JSON اللي البرنامج بيكتبه"),
|
|
516
|
+
"wrong-shape": ("the file is not the shape the program writes",
|
|
517
|
+
"الملف مش بالشكل اللي البرنامج بيكتبه"),
|
|
518
|
+
"no-rows": ("the file holds no row list", "الملف مفيهوش قائمة سطور"),
|
|
519
|
+
"no-longer-valid": ("the rows fit a version of the rules that is not this one, so none of "
|
|
520
|
+
"them were applied",
|
|
521
|
+
"السطور دي ماشية مع قواعد مش قواعد النسخة دي، فولا واحد اتطبق"),
|
|
522
|
+
"unknown": ("no configuration field by that name", "مفيش خانة إعداد بالاسم ده"),
|
|
523
|
+
"per-provider": ("it belongs to one provider", "هي بتاعة بروفايدر واحد"),
|
|
524
|
+
"not a number": ("not a whole number", "مش رقم كامل"),
|
|
525
|
+
"not text": ("not text", "مش نص"),
|
|
526
|
+
"empty": ("empty", "فاضي")}
|
|
527
|
+
en, ar = known.get(why, (why, why))
|
|
528
|
+
return say(arabic, en=en, ar=ar)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def scope(*, arabic: bool) -> str:
|
|
532
|
+
"""What the file is, in the words the settings drawer and `agent overrides --list` both print."""
|
|
533
|
+
return say(arabic,
|
|
534
|
+
en="These rows are written only by the program and signed, so an edit made in another "
|
|
535
|
+
"editor is seen and refused rather than followed. It is tamper evidence, not a "
|
|
536
|
+
"password: the owner of this account can still rewrite the file by hand, and what "
|
|
537
|
+
"they lose by doing it is every override in it.",
|
|
538
|
+
ar="السطور دي البرنامج لوحده اللي بيكتبها وبيوقعها، فتعديل أي محرر تاني ليها بيظهر "
|
|
539
|
+
"ويترفض بدل ما ينفذ. دي علامة على التغيير مش كلمة سر: صاحب الحساب ده لسه يقدر "
|
|
540
|
+
"يعيد كتابة الملف بإيده، واللي بيخسره لما يعمل كده هو كل تعديل في الملف.")
|