autoforge-cli 0.1.0a0__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.
- autoforge/__init__.py +5 -0
- autoforge/agent_worktree.py +653 -0
- autoforge/certificate.py +495 -0
- autoforge/child_env.py +871 -0
- autoforge/claims.py +726 -0
- autoforge/claude_stream.py +501 -0
- autoforge/cli.py +2884 -0
- autoforge/config.py +2178 -0
- autoforge/config_fingerprint.py +374 -0
- autoforge/doctor.py +1551 -0
- autoforge/doctor_sandbox.py +691 -0
- autoforge/effect_ops.py +1272 -0
- autoforge/effects.py +3210 -0
- autoforge/engine.py +16637 -0
- autoforge/epic_commands.py +1486 -0
- autoforge/epic_graph.py +852 -0
- autoforge/errors.py +536 -0
- autoforge/evidence.py +1722 -0
- autoforge/executor.py +1601 -0
- autoforge/executor_duplex.py +985 -0
- autoforge/executor_supervised.py +144 -0
- autoforge/git_transport.py +970 -0
- autoforge/github.py +3402 -0
- autoforge/interaction.py +1027 -0
- autoforge/local_workspace.py +1253 -0
- autoforge/locking.py +1050 -0
- autoforge/loop_detect.py +660 -0
- autoforge/loop_guard.py +404 -0
- autoforge/materialize.py +722 -0
- autoforge/ownership.py +240 -0
- autoforge/pi_rpc.py +713 -0
- autoforge/premerge.py +181 -0
- autoforge/profiles.py +106 -0
- autoforge/progress.py +392 -0
- autoforge/prompts/__init__.py +173 -0
- autoforge/prompts/acceptance_actions.md +39 -0
- autoforge/prompts/analyze_execute.md +128 -0
- autoforge/prompts/common.md +132 -0
- autoforge/prompts/correction.md +12 -0
- autoforge/prompts/fix.md +252 -0
- autoforge/prompts/local_analyze_execute.md +69 -0
- autoforge/prompts/local_common.md +149 -0
- autoforge/prompts/local_fix.md +96 -0
- autoforge/prompts/local_review.md +103 -0
- autoforge/prompts/operator_acceptance.md +61 -0
- autoforge/prompts/replan_reexecute.md +991 -0
- autoforge/prompts/review.md +312 -0
- autoforge/prompts/update_epic.md +120 -0
- autoforge/prompts/update_epic_rerequest.md +85 -0
- autoforge/prompts/validation_evidence.md +26 -0
- autoforge/prompts/validation_failure.md +22 -0
- autoforge/prompts/worktree_state.md +45 -0
- autoforge/providers.py +2893 -0
- autoforge/read_context.py +389 -0
- autoforge/redaction.py +408 -0
- autoforge/replan.py +208 -0
- autoforge/replan_txn.py +1930 -0
- autoforge/result_parser.py +2234 -0
- autoforge/roadmap.py +151 -0
- autoforge/run_contract.py +408 -0
- autoforge/run_namespace.py +839 -0
- autoforge/runlog.py +764 -0
- autoforge/safefs.py +1717 -0
- autoforge/sandbox.py +1313 -0
- autoforge/sandbox_bwrap.py +815 -0
- autoforge/sandbox_pin.py +170 -0
- autoforge/sandbox_policy.py +1107 -0
- autoforge/scheduler.py +1143 -0
- autoforge/scheduler_state.py +2316 -0
- autoforge/scheduler_stop.py +421 -0
- autoforge/sdk_policy.py +988 -0
- autoforge/state.py +2417 -0
- autoforge/transitions.py +388 -0
- autoforge/validation.py +324 -0
- autoforge/worker.py +568 -0
- autoforge_cli-0.1.0a0.dist-info/METADATA +525 -0
- autoforge_cli-0.1.0a0.dist-info/RECORD +81 -0
- autoforge_cli-0.1.0a0.dist-info/WHEEL +5 -0
- autoforge_cli-0.1.0a0.dist-info/entry_points.txt +2 -0
- autoforge_cli-0.1.0a0.dist-info/licenses/LICENSE +21 -0
- autoforge_cli-0.1.0a0.dist-info/top_level.txt +1 -0
autoforge/sandbox_pin.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""The outer sandbox's mode a run is pinned to across its commands (#280).
|
|
2
|
+
|
|
3
|
+
Why this module exists
|
|
4
|
+
----------------------
|
|
5
|
+
|
|
6
|
+
Every command reads the configuration again, and ``sandbox.mode`` used to
|
|
7
|
+
be read at the step that used it: a run started under ``required`` resumed
|
|
8
|
+
under ``disabled`` with no notice (ADR 0005 D12.3 and D15.3 before #280).
|
|
9
|
+
Two things made that a hole rather than a convenience. A command run
|
|
10
|
+
without ``--config`` falls back to the built-in default, ``disabled``. And
|
|
11
|
+
a configuration file a sandboxed agent could rewrite let it switch the
|
|
12
|
+
sandbox off for every later launch, while the controller's own GitHub
|
|
13
|
+
command and ``doctor``'s agent checks ran on what the file said. The
|
|
14
|
+
placement rule (:func:`autoforge.sandbox_policy.config_file_problem`)
|
|
15
|
+
keeps such a file out of every tree a launch writes; this pin makes a
|
|
16
|
+
weaker mode, however it came about, a decision the operator takes in the
|
|
17
|
+
open.
|
|
18
|
+
|
|
19
|
+
The rule
|
|
20
|
+
--------
|
|
21
|
+
|
|
22
|
+
A run records the mode it is created under (``state.sandbox_pin``). Each
|
|
23
|
+
``step``, ``resume`` and ``unblock``, and a scheduler worker resuming an
|
|
24
|
+
issue, compares the mode it would run under with the pin before any phase
|
|
25
|
+
executes (:meth:`autoforge.engine.ControllerEngine.check_run_configuration`):
|
|
26
|
+
|
|
27
|
+
- **weaker:** refused, with nothing written, unless the operator passes
|
|
28
|
+
``--accept-config-change``; the change is then written to the run log
|
|
29
|
+
first, and the run is pinned to the weaker mode (``accepted``);
|
|
30
|
+
- **stronger:** the run is pinned to it (``strengthened``) with the
|
|
31
|
+
command's next save, so going back is a downgrade like any other;
|
|
32
|
+
- **as strong:** nothing changes.
|
|
33
|
+
|
|
34
|
+
Strength is what a launch of the run gets (:func:`mode_strength`):
|
|
35
|
+
``disabled`` is logical isolation only, LOCAL's ``preferred`` falls back to
|
|
36
|
+
it where the host cannot enforce, and ``required`` never runs a launch
|
|
37
|
+
unenforced, which is also what REMOTE's ``preferred`` does (ADR 0005
|
|
38
|
+
D12.2). In a REMOTE run ``required`` and ``preferred`` are therefore one
|
|
39
|
+
strength, and moving between them changes nothing.
|
|
40
|
+
|
|
41
|
+
The pin holds configuration, never enforcement. Whether a launch ran inside
|
|
42
|
+
the sandbox stays the run log's (ADR 0005 D16.1), and the host's capability
|
|
43
|
+
may change between commands without touching the pin: under ``required``
|
|
44
|
+
an incapable host is refused at the gate, as before.
|
|
45
|
+
|
|
46
|
+
It applies to LOCAL and REMOTE runs alike, beside LOCAL's run contract and
|
|
47
|
+
REMOTE's recorded configuration rather than inside either. The contract
|
|
48
|
+
refuses any drift and an older contract is never migrated; the recorded
|
|
49
|
+
configuration is REMOTE's alone and refuses any schema but its own. The pin
|
|
50
|
+
refuses only a weaker mode, and it must load in every run created before
|
|
51
|
+
it existed.
|
|
52
|
+
|
|
53
|
+
The record
|
|
54
|
+
----------
|
|
55
|
+
|
|
56
|
+
``{"schema": 1, "mode": ..., "source": ..., "recorded_at": ...}``, or
|
|
57
|
+
``{}`` for a run loaded from a state file written before the pin (protocol
|
|
58
|
+
11 or older): such a run is not checked, and its first ``step``, ``resume``
|
|
59
|
+
or ``unblock`` pins the mode it runs under (``upgraded``), saved with that
|
|
60
|
+
command's next save.
|
|
61
|
+
|
|
62
|
+
A later sandbox fact (the backend of a REMOTE run, #273) is added to this
|
|
63
|
+
record under a new schema whose parser still reads schema 1, the new fact
|
|
64
|
+
absent and pinned by the next command (``upgraded``), so that a run created
|
|
65
|
+
before it stays loadable. That is the rule a fact added here follows; a
|
|
66
|
+
schema the parser does not know is refused.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
from dataclasses import dataclass
|
|
72
|
+
from typing import Any
|
|
73
|
+
|
|
74
|
+
from .config import SANDBOX_MODES, SANDBOX_PREFERRED, SANDBOX_REQUIRED
|
|
75
|
+
from .errors import ConfigurationError, StateError
|
|
76
|
+
|
|
77
|
+
#: Schema of the persisted pin. A sandbox fact added later takes a new
|
|
78
|
+
#: schema whose parser also reads this one (see "The record" above).
|
|
79
|
+
PIN_SCHEMA = 1
|
|
80
|
+
|
|
81
|
+
#: How the pin came to be: ``created`` by ``run`` or ``local run``;
|
|
82
|
+
#: ``upgraded`` by the first command that found a state file written before
|
|
83
|
+
#: #280 without one; ``strengthened`` by a command under a stronger mode;
|
|
84
|
+
#: ``accepted`` by an operator's ``--accept-config-change`` under a weaker one.
|
|
85
|
+
SOURCE_CREATED = "created"
|
|
86
|
+
SOURCE_UPGRADED = "upgraded"
|
|
87
|
+
SOURCE_STRENGTHENED = "strengthened"
|
|
88
|
+
SOURCE_ACCEPTED = "accepted"
|
|
89
|
+
SOURCES = (SOURCE_CREATED, SOURCE_UPGRADED, SOURCE_STRENGTHENED, SOURCE_ACCEPTED)
|
|
90
|
+
|
|
91
|
+
#: What a refusal, a notice and the run log call the pinned value.
|
|
92
|
+
MODE_KEY = "sandbox.mode"
|
|
93
|
+
|
|
94
|
+
_RECORD_KEYS = ("schema", "mode", "source", "recorded_at")
|
|
95
|
+
_MAX_TIMESTAMP_CHARS = 64
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mode_strength(mode: str, *, local: bool) -> int:
|
|
99
|
+
"""What ``mode`` gives a launch of a LOCAL or REMOTE run, as an order.
|
|
100
|
+
|
|
101
|
+
0: logical isolation only (``disabled``). 1: enforced where the host can
|
|
102
|
+
be, logical isolation only elsewhere (LOCAL's ``preferred``). 2: never
|
|
103
|
+
run unenforced (``required``, and REMOTE's ``preferred``, D12.2).
|
|
104
|
+
"""
|
|
105
|
+
if mode == SANDBOX_REQUIRED:
|
|
106
|
+
return 2
|
|
107
|
+
if mode == SANDBOX_PREFERRED:
|
|
108
|
+
return 1 if local else 2
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class SandboxPin:
|
|
114
|
+
"""The mode a run is pinned to, and where and when that was decided."""
|
|
115
|
+
|
|
116
|
+
mode: str
|
|
117
|
+
source: str
|
|
118
|
+
recorded_at: str
|
|
119
|
+
|
|
120
|
+
def direction(self, mode: str, *, local: bool) -> int:
|
|
121
|
+
"""-1 when ``mode`` is weaker than the pin, 1 when stronger, 0 when as strong."""
|
|
122
|
+
now, pinned = mode_strength(mode, local=local), mode_strength(self.mode, local=local)
|
|
123
|
+
return (now > pinned) - (now < pinned)
|
|
124
|
+
|
|
125
|
+
def describe(self) -> str:
|
|
126
|
+
"""The pin as ``status`` and a refusal show it."""
|
|
127
|
+
return f"{self.mode} ({self.source} {self.recorded_at})"
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"schema": PIN_SCHEMA,
|
|
132
|
+
"mode": self.mode,
|
|
133
|
+
"source": self.source,
|
|
134
|
+
"recorded_at": self.recorded_at,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def persisted(self) -> dict:
|
|
138
|
+
""":meth:`to_dict`, for the state file: ConfigurationError unless :meth:`from_dict`
|
|
139
|
+
reads it back, so no pin that would make the run unloadable is written."""
|
|
140
|
+
data = self.to_dict()
|
|
141
|
+
try:
|
|
142
|
+
SandboxPin.from_dict(data)
|
|
143
|
+
except StateError as exc:
|
|
144
|
+
raise ConfigurationError(
|
|
145
|
+
f"{exc}: a run pins its sandbox.mode in its state file, which could not be "
|
|
146
|
+
"loaded again otherwise (#280)"
|
|
147
|
+
) from None
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dict(cls, data: Any) -> SandboxPin:
|
|
152
|
+
"""Parse a persisted pin strictly; StateError on any other shape."""
|
|
153
|
+
if isinstance(data, dict) and data.get("schema") != PIN_SCHEMA:
|
|
154
|
+
raise StateError(
|
|
155
|
+
f"schema {data.get('schema')!r:.40} is not {PIN_SCHEMA}, the one this "
|
|
156
|
+
"controller reads"
|
|
157
|
+
)
|
|
158
|
+
if not isinstance(data, dict) or sorted(data) != sorted(_RECORD_KEYS):
|
|
159
|
+
raise StateError(f"the sandbox pin must hold exactly {', '.join(_RECORD_KEYS)}")
|
|
160
|
+
mode, source, at = data["mode"], data["source"], data["recorded_at"]
|
|
161
|
+
if mode not in SANDBOX_MODES:
|
|
162
|
+
raise StateError(f"mode must be one of {', '.join(SANDBOX_MODES)}")
|
|
163
|
+
if source not in SOURCES:
|
|
164
|
+
raise StateError(f"source must be one of {', '.join(SOURCES)}")
|
|
165
|
+
if not isinstance(at, str) or not at or len(at) > _MAX_TIMESTAMP_CHARS:
|
|
166
|
+
raise StateError(
|
|
167
|
+
f"recorded_at must be a non-empty string of at most {_MAX_TIMESTAMP_CHARS} "
|
|
168
|
+
"characters"
|
|
169
|
+
)
|
|
170
|
+
return cls(mode=mode, source=source, recorded_at=at)
|