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
ai_code_engineer/cli.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .config import load_settings, validate
|
|
12
|
+
from .engine import apply_proposal, load_session, plan, review, rollback
|
|
13
|
+
from .errors import AgentError
|
|
14
|
+
from .providers import make_provider
|
|
15
|
+
from .report import export_file, find_session
|
|
16
|
+
from . import config, intent, modes, overrides, setup
|
|
17
|
+
from .verification import RECIPES, verify
|
|
18
|
+
from .workspace import Workspace
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def safe_print(value: str) -> None:
|
|
22
|
+
# Strip terminal control characters in untrusted model/repository content.
|
|
23
|
+
value = "".join(c if c in "\n\t" or ord(c) >= 32 and not 127 <= ord(c) <= 159 else "?" for c in value)
|
|
24
|
+
try:
|
|
25
|
+
print(value)
|
|
26
|
+
except UnicodeEncodeError:
|
|
27
|
+
# A cp1252 console cannot carry every script this tool answers in. A half sentence that
|
|
28
|
+
# survives is better than a traceback where the answer should have been.
|
|
29
|
+
encoding = getattr(sys.stdout, "encoding", None) or "ascii"
|
|
30
|
+
print(value.encode(encoding, "replace").decode(encoding, "replace"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parser() -> argparse.ArgumentParser:
|
|
34
|
+
root = argparse.ArgumentParser(prog="agent", description="Review-first Python developer agent (MVP)")
|
|
35
|
+
sub = root.add_subparsers(dest="command", required=True)
|
|
36
|
+
sub.add_parser("doctor", help="Check Python, local Ollama, Docker and key presence")
|
|
37
|
+
first = sub.add_parser("setup", help="Run the first checks in order, and prove the red line offline")
|
|
38
|
+
first.add_argument("--repo", type=Path, help="Check this project folder for a runnable command")
|
|
39
|
+
first.add_argument("--provider", default="", help="Provider to check instead of Ollama")
|
|
40
|
+
first.add_argument("--endpoint", default="", help="Endpoint for that provider")
|
|
41
|
+
first.add_argument("--model", default="", help="Model the windows should land on")
|
|
42
|
+
first.add_argument("--arabic", action="store_true", help="Write the rows in Arabic")
|
|
43
|
+
first.add_argument("--yes", action="store_true", help="Answer every offer yes, for a script")
|
|
44
|
+
first.add_argument("--no-demo", action="store_true", help="Do not offer the offline proof")
|
|
45
|
+
repo = sub.add_parser("map", help="Read-only repository map: files with their parsed declarations")
|
|
46
|
+
repo.add_argument("--repo", type=Path, required=True)
|
|
47
|
+
seal = sub.add_parser("read-only",
|
|
48
|
+
help="Declare a folder Read-only for every surface, lift it, or list what is declared")
|
|
49
|
+
seal.add_argument("--repo", type=Path, help="The folder to declare or lift; with no folder, list")
|
|
50
|
+
seal.add_argument("--off", action="store_true", help="Lift the declaration instead of making it")
|
|
51
|
+
seal.add_argument("--arabic", action="store_true", help="Write the rows in Arabic")
|
|
52
|
+
over = sub.add_parser("overrides",
|
|
53
|
+
help="Read, set or remove the configuration rows the program signs")
|
|
54
|
+
over.add_argument("--list", action="store_true", help="Every row and every refusal, newest first")
|
|
55
|
+
over.add_argument("--set", nargs=3, metavar=("TARGET", "KEY", "VALUE"),
|
|
56
|
+
help=f"A provider key or {overrides.EVERY}, one of "
|
|
57
|
+
f"{', '.join(sorted(overrides.TYPES))}, and the new value")
|
|
58
|
+
over.add_argument("--unset", nargs=2, metavar=("TARGET", "KEY"), help="Remove one row")
|
|
59
|
+
over.add_argument("--arabic", action="store_true", help="Write the rows in Arabic")
|
|
60
|
+
draft = sub.add_parser("plan", help="Explore a repository and save a proposed diff; never writes code")
|
|
61
|
+
draft.add_argument("task")
|
|
62
|
+
draft.add_argument("--repo", type=Path, required=True)
|
|
63
|
+
draft.add_argument("--config", type=Path)
|
|
64
|
+
draft.add_argument("--model", help="Override model ID (provider is selected in config)")
|
|
65
|
+
draft.add_argument("--plan-file", help="Markdown/text plan inside the selected project")
|
|
66
|
+
draft.add_argument("--runs", type=Path, default=Path(".agent-runs"))
|
|
67
|
+
draft.add_argument("--allow-cloud", action="store_true")
|
|
68
|
+
draft.add_argument("--data-class", choices=["restricted", "public", "synthetic"], default="restricted")
|
|
69
|
+
for name in ("review", "apply", "rollback", "verify", "status"):
|
|
70
|
+
command = sub.add_parser(name)
|
|
71
|
+
command.add_argument("session", type=Path)
|
|
72
|
+
if name in {"apply", "rollback"}:
|
|
73
|
+
command.add_argument("--approve", help="Full proposal SHA256; otherwise asks interactively")
|
|
74
|
+
if name == "verify":
|
|
75
|
+
command.add_argument("--recipe", choices=sorted(RECIPES))
|
|
76
|
+
command.add_argument("--image", help="Preloaded approved Linux image@sha256:digest")
|
|
77
|
+
sub.add_parser("demo", help="Run deterministic, offline synthetic demo without changing your repository")
|
|
78
|
+
export = sub.add_parser("export-session",
|
|
79
|
+
help="Write one session's record as JSON or as a readable report")
|
|
80
|
+
export.add_argument("session", help="Session id, a unique id prefix, or its folder/file")
|
|
81
|
+
export.add_argument("--format", choices=("json", "markdown"), default="markdown")
|
|
82
|
+
export.add_argument("--runs", type=Path, default=Path(".agent-runs"))
|
|
83
|
+
export.add_argument("--out", type=Path, help="Write here instead of standard output")
|
|
84
|
+
return root
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def doctor() -> dict:
|
|
88
|
+
result = {"python": sys.version.split()[0], "runtime_dependencies": "standard library only",
|
|
89
|
+
"docker": bool(shutil.which("docker")),
|
|
90
|
+
"openrouter_key_present": bool(os.environ.get("OPENROUTER_API_KEY")),
|
|
91
|
+
"local_models": [], "ollama_models": []}
|
|
92
|
+
# The same probe `agent setup` reads, so the two cannot disagree about whether Ollama answered.
|
|
93
|
+
entries, _source, error = setup.reach(config.OLLAMA)
|
|
94
|
+
if error:
|
|
95
|
+
result["ollama"] = "unreachable"
|
|
96
|
+
else:
|
|
97
|
+
result["ollama_models"] = [item["id"] for item in entries]
|
|
98
|
+
result["local_models"] = [item["id"] for item in entries if not item["cloud"]]
|
|
99
|
+
result["ollama"] = "reachable"
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def demo() -> dict:
|
|
104
|
+
"""The offline proof, in `setup` because the wizard and the window offer it too."""
|
|
105
|
+
return setup.run_demo()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def app_dir() -> Path:
|
|
109
|
+
"""Where this tool keeps its own records — the folder both windows write to.
|
|
110
|
+
|
|
111
|
+
Computed the way the launcher computes it (`webapp/launch.py`), not from the working directory: the
|
|
112
|
+
declaration file is only worth having if `agent read-only` and a window opened from another folder
|
|
113
|
+
are reading the same one.
|
|
114
|
+
"""
|
|
115
|
+
return Path(__file__).resolve().parents[2]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def refuses_sealed(folder, what: str) -> None:
|
|
119
|
+
"""Stop a terminal write on a folder somebody told to stay read-only.
|
|
120
|
+
|
|
121
|
+
The answer comes off the file the windows use rather than from anything this process remembers: a
|
|
122
|
+
position a second process could ignore would only protect the window that set it. Naming the
|
|
123
|
+
command that lifts it is part of the refusal — an error that says "not allowed" and nothing else
|
|
124
|
+
sends the operator hunting through config files for a switch that is not there.
|
|
125
|
+
"""
|
|
126
|
+
if modes.sealed(app_dir(), folder):
|
|
127
|
+
raise AgentError(modes.refusal(app_dir(), folder, what) + " " + intent.lift(str(folder)))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def speak_arabic() -> None:
|
|
131
|
+
"""Give the console an encoding that can carry Arabic at all.
|
|
132
|
+
|
|
133
|
+
A cp1252 terminal cannot print one Arabic letter, and the person who asked for Arabic rows is
|
|
134
|
+
exactly the one who needs to read them rather than see a line of question marks.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
138
|
+
except (AttributeError, OSError, ValueError):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_read_only(args) -> int:
|
|
143
|
+
"""Declare, lift, or list — the position a folder keeps after this terminal closes."""
|
|
144
|
+
folder = str(args.repo) if args.repo else ""
|
|
145
|
+
if args.arabic:
|
|
146
|
+
speak_arabic()
|
|
147
|
+
if not folder:
|
|
148
|
+
rows = modes.listed(app_dir())
|
|
149
|
+
if not rows:
|
|
150
|
+
safe_print("Nothing is declared. A folder with no declaration opens on Change mode.")
|
|
151
|
+
return 0
|
|
152
|
+
for row in rows:
|
|
153
|
+
safe_print(f"{row.get('path') or row['folder']} {intent.label(row['mode'])} "
|
|
154
|
+
f"{intent.source(row.get('by', ''), arabic=args.arabic)} {row.get('at', '')}")
|
|
155
|
+
return 0
|
|
156
|
+
if not Path(folder).is_dir():
|
|
157
|
+
raise AgentError("No such folder to declare: " + folder)
|
|
158
|
+
if args.off:
|
|
159
|
+
modes.forget(app_dir(), folder)
|
|
160
|
+
safe_print("Nothing is declared for " + folder
|
|
161
|
+
+ ". It opens on Change mode again, like any folder nobody has told otherwise.")
|
|
162
|
+
return 0
|
|
163
|
+
row = modes.declare(app_dir(), folder, intent.READ, by=modes.TERMINAL)
|
|
164
|
+
safe_print(intent.declared(by=row["by"], at=row["at"], arabic=args.arabic,
|
|
165
|
+
project=Path(folder).name))
|
|
166
|
+
safe_print(intent.lift(folder, arabic=args.arabic))
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_overrides(args) -> int:
|
|
171
|
+
"""Read, set or remove the rows the program signs.
|
|
172
|
+
|
|
173
|
+
The file is the terminal's half of the same store both windows write, so a row set here changes a
|
|
174
|
+
profile read here — and one set in a window changes this listing. Writing it through ``overrides``
|
|
175
|
+
rather than editing the JSON is what makes it take effect: a row the signature does not cover is
|
|
176
|
+
refused on the next read, and this command is the only routine that signs.
|
|
177
|
+
"""
|
|
178
|
+
where = app_dir()
|
|
179
|
+
if args.arabic:
|
|
180
|
+
speak_arabic()
|
|
181
|
+
created = overrides.ensure(where)
|
|
182
|
+
if created:
|
|
183
|
+
safe_print("Created " + str(overrides.path(where)) + " and its signing key "
|
|
184
|
+
+ str(overrides.key_path(where)) + " — the rows live beside the other records this "
|
|
185
|
+
"tool keeps, and never inside a project it is working on.")
|
|
186
|
+
if args.set:
|
|
187
|
+
target, key, value = args.set
|
|
188
|
+
row = overrides.put(where, target, key, value, by=modes.TERMINAL, arabic=args.arabic)
|
|
189
|
+
safe_print(f"{row['key']} = {row['value']} for "
|
|
190
|
+
+ (f"every provider" if row["target"] == overrides.EVERY else row["target"])
|
|
191
|
+
+ ". Signed, and read by every surface from now on.")
|
|
192
|
+
return 0
|
|
193
|
+
if args.unset:
|
|
194
|
+
target, key = args.unset
|
|
195
|
+
safe_print(("Removed " if overrides.delete(where, target, key) else "Nothing to remove: no row for ")
|
|
196
|
+
+ f"{key} for {target or overrides.EVERY}.")
|
|
197
|
+
return 0
|
|
198
|
+
listed = overrides.listed(where)
|
|
199
|
+
if not listed:
|
|
200
|
+
safe_print("Nothing is overridden yet. The file lists its own keys, ranges and order.")
|
|
201
|
+
for row in listed:
|
|
202
|
+
if row["state"] == "in force":
|
|
203
|
+
safe_print(f"{row['key']} = {row['value']} [{row['target']}] {row['by']} {row['at']}")
|
|
204
|
+
else:
|
|
205
|
+
safe_print(overrides.says(row, arabic=args.arabic) + " refused: "
|
|
206
|
+
+ overrides.why_text(row["why"], arabic=args.arabic))
|
|
207
|
+
refused = overrides.refusals(where)
|
|
208
|
+
if refused:
|
|
209
|
+
safe_print(overrides.refused_line(refused, arabic=args.arabic))
|
|
210
|
+
safe_print("")
|
|
211
|
+
safe_print(overrides.scope(arabic=args.arabic))
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def ask_line(prompt: str, ask=input) -> str:
|
|
216
|
+
"""One line from the operator. Ctrl-D is an empty answer, not a traceback."""
|
|
217
|
+
try:
|
|
218
|
+
return str(ask(prompt) or "").strip()
|
|
219
|
+
except EOFError:
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def wants(prompt: str, args, ask=input, interactive=True) -> bool:
|
|
224
|
+
"""One yes/no. A pipe gets a no and a line saying so, not a hang.
|
|
225
|
+
|
|
226
|
+
`apply` already refuses to block on a pipe; a wizard that waited on `input()` in a CI job would be
|
|
227
|
+
the same mistake with a friendlier name.
|
|
228
|
+
"""
|
|
229
|
+
if getattr(args, "yes", False):
|
|
230
|
+
return True
|
|
231
|
+
if not interactive:
|
|
232
|
+
safe_print("(not an interactive terminal, so that question is answered no)")
|
|
233
|
+
return False
|
|
234
|
+
return ask_line(prompt, ask).lower() in {"y", "yes", "ok", "نعم", "ايه", "أيوه"}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def run_setup(args, ask=input, interactive=None) -> int:
|
|
238
|
+
"""The first-run wizard: check, prove, explain. It writes nothing outside a temporary folder.
|
|
239
|
+
|
|
240
|
+
Choosing the model, granting the folder and picking the write position all happen in the window,
|
|
241
|
+
because that is where those preferences are stored — a second copy of the same choices kept by the
|
|
242
|
+
terminal is exactly how two surfaces begin to disagree. So this run reports, offers the proof, and
|
|
243
|
+
then names the three things to do there.
|
|
244
|
+
"""
|
|
245
|
+
if interactive is None:
|
|
246
|
+
interactive = sys.stdin.isatty()
|
|
247
|
+
if args.arabic:
|
|
248
|
+
# A console that defaults to cp1252 cannot print Arabic at all, and the person who asked for
|
|
249
|
+
# Arabic rows is exactly the one who needs to read them rather than see question marks.
|
|
250
|
+
speak_arabic()
|
|
251
|
+
# The wizard is the first thing a new operator runs, so it is where the file the windows and the
|
|
252
|
+
# terminal later share appears — with its own keys and ranges printed inside it.
|
|
253
|
+
overrides.ensure(app_dir())
|
|
254
|
+
repo = str(args.repo) if args.repo else ""
|
|
255
|
+
rows = setup.audit(repo=repo, provider=args.provider, endpoint=args.endpoint,
|
|
256
|
+
model=args.model, arabic=args.arabic)
|
|
257
|
+
safe_print(setup.render(rows))
|
|
258
|
+
|
|
259
|
+
if args.repo is None and not args.yes and wants(
|
|
260
|
+
"Name a project folder to check it for a runnable command? (y/n): ",
|
|
261
|
+
args, ask, interactive):
|
|
262
|
+
typed = ask_line("Project folder: ", ask).strip('"')
|
|
263
|
+
if typed:
|
|
264
|
+
rows = setup.audit(repo=typed, provider=args.provider, endpoint=args.endpoint,
|
|
265
|
+
model=args.model, arabic=args.arabic)
|
|
266
|
+
safe_print(setup.render([row for row in rows if row["id"] == "project"]))
|
|
267
|
+
else:
|
|
268
|
+
safe_print("No folder named, so nothing was checked against one.")
|
|
269
|
+
elif args.repo is None:
|
|
270
|
+
# A scripted run answered yes to everything, and there is nothing to guess here: the folder is
|
|
271
|
+
# the one question a yes cannot answer.
|
|
272
|
+
safe_print("No folder named with --repo, so nothing was checked against one.")
|
|
273
|
+
|
|
274
|
+
if not args.no_demo and wants("Run the offline proof? It writes only to a temporary folder, and "
|
|
275
|
+
"rolls it back. (y/n): ", args, ask, interactive):
|
|
276
|
+
safe_print("Running it…")
|
|
277
|
+
safe_print(setup.render([setup.demo_row(setup.run_demo(), arabic=args.arabic)]))
|
|
278
|
+
|
|
279
|
+
# The promises are in the audit already; printing them again here put the same paragraph on the
|
|
280
|
+
# screen twice three lines apart. The question points at the copy the operator has just read.
|
|
281
|
+
safe_print("")
|
|
282
|
+
accepted = wants("Accept the five promises printed above, before the window offers you a write? "
|
|
283
|
+
"(y/n): ", args, ask, interactive)
|
|
284
|
+
|
|
285
|
+
counts = setup.counts(rows)
|
|
286
|
+
safe_print("")
|
|
287
|
+
if args.arabic:
|
|
288
|
+
lines = [
|
|
289
|
+
"الخطوات الجاية في النافذة: افتح `agent ui`، اختار الموديل من Settings، افتح مجلد من "
|
|
290
|
+
"الشريط الجانبي، وبعدها حدد الشارة: Chat أو Read-only أو Change.",
|
|
291
|
+
"الكتابة التلقائية مفتاح فوق Change، ومتشغلش غير لو كنت مستعد ترجع بـ git.",
|
|
292
|
+
"لو عايز المجلد يفضل للقراءة فقط من كل السطوح ومن غير ما تعتمد على النافذة: "
|
|
293
|
+
"`agent read-only --repo PATH`، والرفع بـ `--off` بنفس المسار.",
|
|
294
|
+
]
|
|
295
|
+
if not accepted:
|
|
296
|
+
lines.insert(0, "لم تتم الموافقة على السياسة: الأداة لسه هتشتغل، بس مش هتكتب في ملفاتك.")
|
|
297
|
+
else:
|
|
298
|
+
lines = [
|
|
299
|
+
"Next, in the window: start it with `agent ui`, pick the model in Settings, open a folder "
|
|
300
|
+
"from the sidebar, then set the badge to Chat, Read-only or Change.",
|
|
301
|
+
"Auto-Apply is the switch on top of Change — leave it off unless you are ready to reverse "
|
|
302
|
+
"the writes with git.",
|
|
303
|
+
"A folder can also be sealed from here, where no window has to stay open for it to hold: "
|
|
304
|
+
"`agent read-only --repo PATH`, lifted by the same line with `--off`.",
|
|
305
|
+
]
|
|
306
|
+
if not accepted:
|
|
307
|
+
lines.insert(0, "The policy was not accepted. The tool still runs, but no write should be "
|
|
308
|
+
"approved until you have read those lines.")
|
|
309
|
+
safe_print("\n".join(lines))
|
|
310
|
+
safe_print("\n" + setup.tally(counts, arabic=args.arabic) + ".")
|
|
311
|
+
return 1 if counts["bad"] else 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def main(argv: list[str] | None = None) -> int:
|
|
315
|
+
args = parser().parse_args(argv)
|
|
316
|
+
try:
|
|
317
|
+
if args.command == "doctor":
|
|
318
|
+
safe_print(json.dumps(doctor(), indent=2))
|
|
319
|
+
elif args.command == "setup":
|
|
320
|
+
return run_setup(args)
|
|
321
|
+
elif args.command == "map":
|
|
322
|
+
safe_print(Workspace(args.repo).repo_map())
|
|
323
|
+
elif args.command == "read-only":
|
|
324
|
+
return run_read_only(args)
|
|
325
|
+
elif args.command == "overrides":
|
|
326
|
+
return run_overrides(args)
|
|
327
|
+
elif args.command == "plan":
|
|
328
|
+
refuses_sealed(args.repo, "A proposal")
|
|
329
|
+
settings = load_settings(args.config, app_dir())
|
|
330
|
+
if args.model:
|
|
331
|
+
settings = replace(settings, model=args.model)
|
|
332
|
+
validate(settings)
|
|
333
|
+
ws = Workspace(args.repo)
|
|
334
|
+
provider = make_provider(settings, allow_cloud=args.allow_cloud, data_class=args.data_class)
|
|
335
|
+
path = plan(ws, args.task, provider, settings, args.runs, progress=safe_print, plan_file=args.plan_file)
|
|
336
|
+
safe_print(review(load_session(path)))
|
|
337
|
+
safe_print("\nSession: " + str(path))
|
|
338
|
+
safe_print("No project files changed. Review this proposal before applying it.")
|
|
339
|
+
elif args.command == "demo":
|
|
340
|
+
result = demo()
|
|
341
|
+
safe_print(json.dumps(result, indent=2))
|
|
342
|
+
return 0 if result["proposal_apply_rollback"] == "passed" else 1
|
|
343
|
+
elif args.command == "export-session":
|
|
344
|
+
text = export_file(find_session(args.runs, args.session), args.format)
|
|
345
|
+
if args.out:
|
|
346
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
args.out.write_text(text, encoding="utf-8", newline="\n")
|
|
348
|
+
safe_print("Wrote " + str(args.out) + " (" + str(len(text)) + " characters)")
|
|
349
|
+
else:
|
|
350
|
+
safe_print(text)
|
|
351
|
+
else:
|
|
352
|
+
session = load_session(args.session)
|
|
353
|
+
if args.command == "review":
|
|
354
|
+
safe_print(review(session))
|
|
355
|
+
elif args.command == "status":
|
|
356
|
+
safe_print(json.dumps({"id": session["id"], "state": session["state"],
|
|
357
|
+
"model": session["model"], "events": session["events"]}, indent=2))
|
|
358
|
+
elif args.command in {"apply", "rollback"}:
|
|
359
|
+
# Checked before anything is asked, because a hash typed for a write that was never
|
|
360
|
+
# going to happen teaches the operator that the prompts do not mean anything.
|
|
361
|
+
refuses_sealed(session.get("root", ""), args.command)
|
|
362
|
+
approved = args.approve
|
|
363
|
+
if not approved:
|
|
364
|
+
safe_print(review(session))
|
|
365
|
+
if not sys.stdin.isatty():
|
|
366
|
+
raise AgentError("Non-interactive mode requires --approve with the full proposal SHA256.")
|
|
367
|
+
approved = input("Type the full proposal SHA256 to " + args.command + ": ").strip()
|
|
368
|
+
operation = apply_proposal if args.command == "apply" else rollback
|
|
369
|
+
safe_print("State: " + operation(args.session, approved)["state"])
|
|
370
|
+
elif args.command == "verify":
|
|
371
|
+
result = verify(args.session, args.recipe, args.image)
|
|
372
|
+
safe_print(json.dumps(result, indent=2))
|
|
373
|
+
return 0 if result["status"] == "passed" else 2
|
|
374
|
+
return 0
|
|
375
|
+
except KeyboardInterrupt:
|
|
376
|
+
safe_print("Cancelled. Inspect session status before retrying an interrupted apply.")
|
|
377
|
+
return 130
|
|
378
|
+
except (AgentError, OSError) as exc:
|
|
379
|
+
safe_print("Error: " + str(exc))
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
raise SystemExit(main())
|