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.
Files changed (81) hide show
  1. autoforge/__init__.py +5 -0
  2. autoforge/agent_worktree.py +653 -0
  3. autoforge/certificate.py +495 -0
  4. autoforge/child_env.py +871 -0
  5. autoforge/claims.py +726 -0
  6. autoforge/claude_stream.py +501 -0
  7. autoforge/cli.py +2884 -0
  8. autoforge/config.py +2178 -0
  9. autoforge/config_fingerprint.py +374 -0
  10. autoforge/doctor.py +1551 -0
  11. autoforge/doctor_sandbox.py +691 -0
  12. autoforge/effect_ops.py +1272 -0
  13. autoforge/effects.py +3210 -0
  14. autoforge/engine.py +16637 -0
  15. autoforge/epic_commands.py +1486 -0
  16. autoforge/epic_graph.py +852 -0
  17. autoforge/errors.py +536 -0
  18. autoforge/evidence.py +1722 -0
  19. autoforge/executor.py +1601 -0
  20. autoforge/executor_duplex.py +985 -0
  21. autoforge/executor_supervised.py +144 -0
  22. autoforge/git_transport.py +970 -0
  23. autoforge/github.py +3402 -0
  24. autoforge/interaction.py +1027 -0
  25. autoforge/local_workspace.py +1253 -0
  26. autoforge/locking.py +1050 -0
  27. autoforge/loop_detect.py +660 -0
  28. autoforge/loop_guard.py +404 -0
  29. autoforge/materialize.py +722 -0
  30. autoforge/ownership.py +240 -0
  31. autoforge/pi_rpc.py +713 -0
  32. autoforge/premerge.py +181 -0
  33. autoforge/profiles.py +106 -0
  34. autoforge/progress.py +392 -0
  35. autoforge/prompts/__init__.py +173 -0
  36. autoforge/prompts/acceptance_actions.md +39 -0
  37. autoforge/prompts/analyze_execute.md +128 -0
  38. autoforge/prompts/common.md +132 -0
  39. autoforge/prompts/correction.md +12 -0
  40. autoforge/prompts/fix.md +252 -0
  41. autoforge/prompts/local_analyze_execute.md +69 -0
  42. autoforge/prompts/local_common.md +149 -0
  43. autoforge/prompts/local_fix.md +96 -0
  44. autoforge/prompts/local_review.md +103 -0
  45. autoforge/prompts/operator_acceptance.md +61 -0
  46. autoforge/prompts/replan_reexecute.md +991 -0
  47. autoforge/prompts/review.md +312 -0
  48. autoforge/prompts/update_epic.md +120 -0
  49. autoforge/prompts/update_epic_rerequest.md +85 -0
  50. autoforge/prompts/validation_evidence.md +26 -0
  51. autoforge/prompts/validation_failure.md +22 -0
  52. autoforge/prompts/worktree_state.md +45 -0
  53. autoforge/providers.py +2893 -0
  54. autoforge/read_context.py +389 -0
  55. autoforge/redaction.py +408 -0
  56. autoforge/replan.py +208 -0
  57. autoforge/replan_txn.py +1930 -0
  58. autoforge/result_parser.py +2234 -0
  59. autoforge/roadmap.py +151 -0
  60. autoforge/run_contract.py +408 -0
  61. autoforge/run_namespace.py +839 -0
  62. autoforge/runlog.py +764 -0
  63. autoforge/safefs.py +1717 -0
  64. autoforge/sandbox.py +1313 -0
  65. autoforge/sandbox_bwrap.py +815 -0
  66. autoforge/sandbox_pin.py +170 -0
  67. autoforge/sandbox_policy.py +1107 -0
  68. autoforge/scheduler.py +1143 -0
  69. autoforge/scheduler_state.py +2316 -0
  70. autoforge/scheduler_stop.py +421 -0
  71. autoforge/sdk_policy.py +988 -0
  72. autoforge/state.py +2417 -0
  73. autoforge/transitions.py +388 -0
  74. autoforge/validation.py +324 -0
  75. autoforge/worker.py +568 -0
  76. autoforge_cli-0.1.0a0.dist-info/METADATA +525 -0
  77. autoforge_cli-0.1.0a0.dist-info/RECORD +81 -0
  78. autoforge_cli-0.1.0a0.dist-info/WHEEL +5 -0
  79. autoforge_cli-0.1.0a0.dist-info/entry_points.txt +2 -0
  80. autoforge_cli-0.1.0a0.dist-info/licenses/LICENSE +21 -0
  81. autoforge_cli-0.1.0a0.dist-info/top_level.txt +1 -0
@@ -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)