fullstop 0.2.1__tar.gz
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.
- fullstop-0.2.1/LICENSE +21 -0
- fullstop-0.2.1/PKG-INFO +445 -0
- fullstop-0.2.1/README.md +435 -0
- fullstop-0.2.1/fullstop/__init__.py +8 -0
- fullstop-0.2.1/fullstop/__main__.py +8 -0
- fullstop-0.2.1/fullstop/activity.py +241 -0
- fullstop-0.2.1/fullstop/agent.py +463 -0
- fullstop-0.2.1/fullstop/cli.py +262 -0
- fullstop-0.2.1/fullstop/gate.py +230 -0
- fullstop-0.2.1/fullstop/manifest.py +305 -0
- fullstop-0.2.1/fullstop/policy.py +184 -0
- fullstop-0.2.1/fullstop/protocol.py +212 -0
- fullstop-0.2.1/fullstop/provider.py +188 -0
- fullstop-0.2.1/fullstop/redact.py +52 -0
- fullstop-0.2.1/fullstop/resolve.py +98 -0
- fullstop-0.2.1/fullstop/sandbox.py +66 -0
- fullstop-0.2.1/fullstop/state.py +92 -0
- fullstop-0.2.1/fullstop/tools/__init__.py +41 -0
- fullstop-0.2.1/fullstop/tools/base.py +65 -0
- fullstop-0.2.1/fullstop/tools/browser.py +25 -0
- fullstop-0.2.1/fullstop/tools/file.py +178 -0
- fullstop-0.2.1/fullstop/tools/note.py +59 -0
- fullstop-0.2.1/fullstop/tools/shell.py +82 -0
- fullstop-0.2.1/fullstop/tools/web.py +135 -0
- fullstop-0.2.1/fullstop/types.py +63 -0
- fullstop-0.2.1/fullstop/ui_approver.py +258 -0
- fullstop-0.2.1/fullstop/ui_assets.py +955 -0
- fullstop-0.2.1/fullstop/ui_model.py +187 -0
- fullstop-0.2.1/fullstop/ui_server.py +310 -0
- fullstop-0.2.1/fullstop/ui_strings.py +327 -0
- fullstop-0.2.1/fullstop/ui_wizard.py +302 -0
- fullstop-0.2.1/fullstop.egg-info/PKG-INFO +445 -0
- fullstop-0.2.1/fullstop.egg-info/SOURCES.txt +80 -0
- fullstop-0.2.1/fullstop.egg-info/dependency_links.txt +1 -0
- fullstop-0.2.1/fullstop.egg-info/top_level.txt +1 -0
- fullstop-0.2.1/pyproject.toml +8 -0
- fullstop-0.2.1/setup.cfg +4 -0
- fullstop-0.2.1/tests/test_activity_log.py +144 -0
- fullstop-0.2.1/tests/test_agent_loop.py +278 -0
- fullstop-0.2.1/tests/test_cli.py +169 -0
- fullstop-0.2.1/tests/test_credentials.py +160 -0
- fullstop-0.2.1/tests/test_examples.py +183 -0
- fullstop-0.2.1/tests/test_fix_01_crash_resume.py +319 -0
- fullstop-0.2.1/tests/test_fix_02_config_tamper.py +215 -0
- fullstop-0.2.1/tests/test_fix_03_context_exhaustion.py +120 -0
- fullstop-0.2.1/tests/test_fix_04_goal_override.py +97 -0
- fullstop-0.2.1/tests/test_fix_05_redirect_chain.py +145 -0
- fullstop-0.2.1/tests/test_fix_06_approval_display.py +191 -0
- fullstop-0.2.1/tests/test_fix_07_shell_allowlist.py +178 -0
- fullstop-0.2.1/tests/test_fix_08_activity_log_rotation.py +98 -0
- fullstop-0.2.1/tests/test_fix_09_file_read_cap.py +47 -0
- fullstop-0.2.1/tests/test_fix_10_unattended_stop.py +109 -0
- fullstop-0.2.1/tests/test_fix_11_redactor_call_time.py +49 -0
- fullstop-0.2.1/tests/test_fix_12_note_symlink.py +58 -0
- fullstop-0.2.1/tests/test_fix_13_file_list_protected.py +71 -0
- fullstop-0.2.1/tests/test_fix_14_gate_classification_label.py +65 -0
- fullstop-0.2.1/tests/test_fix_15_observation_delimiters.py +126 -0
- fullstop-0.2.1/tests/test_fix_16_log_truncate_wiring.py +65 -0
- fullstop-0.2.1/tests/test_fix_17_19_readme_honesty.py +85 -0
- fullstop-0.2.1/tests/test_fix_20_live_user_seed.py +84 -0
- fullstop-0.2.1/tests/test_fix_21_fence_not_line_anchored.py +59 -0
- fullstop-0.2.1/tests/test_fix_22_resume_seed_guard.py +127 -0
- fullstop-0.2.1/tests/test_fix_23_parser_stray_marker_cliffs.py +89 -0
- fullstop-0.2.1/tests/test_imports.py +80 -0
- fullstop-0.2.1/tests/test_manifest_policy.py +332 -0
- fullstop-0.2.1/tests/test_policy_gate.py +357 -0
- fullstop-0.2.1/tests/test_protocol.py +137 -0
- fullstop-0.2.1/tests/test_provider.py +190 -0
- fullstop-0.2.1/tests/test_r2_01_replay_dedup_keys.py +311 -0
- fullstop-0.2.1/tests/test_r2_02_provider_context_body.py +253 -0
- fullstop-0.2.1/tests/test_r2_03_log_oversize_wedge.py +163 -0
- fullstop-0.2.1/tests/test_r2_04_readme_allow_config_change.py +99 -0
- fullstop-0.2.1/tests/test_r2_05_error_observation_frame.py +173 -0
- fullstop-0.2.1/tests/test_sandbox.py +184 -0
- fullstop-0.2.1/tests/test_state.py +80 -0
- fullstop-0.2.1/tests/test_tools.py +362 -0
- fullstop-0.2.1/tests/test_ui_approver.py +339 -0
- fullstop-0.2.1/tests/test_ui_assets.py +49 -0
- fullstop-0.2.1/tests/test_ui_cli.py +332 -0
- fullstop-0.2.1/tests/test_ui_model.py +212 -0
- fullstop-0.2.1/tests/test_ui_server.py +307 -0
- fullstop-0.2.1/tests/test_ui_wizard.py +214 -0
fullstop-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the fullstop contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fullstop-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fullstop
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A zero-dependency Python runtime for always-on agents: identity manifests, a default-deny policy gate with human approval, a fenced tool-call protocol, a hash-chained activity log, and crash-safe checkpoint/resume with replay
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# fullstop
|
|
12
|
+
|
|
13
|
+
**Your agent. Your machine. Full stop.**
|
|
14
|
+
|
|
15
|
+
A self-hosted, model-agnostic, **zero-dependency** Python runtime for always-on
|
|
16
|
+
agents: each agent has a persistent workspace, an identity with scoped
|
|
17
|
+
credentials, a policy gate that reviews every consequential action, and a
|
|
18
|
+
tamper-evident append-only activity log.
|
|
19
|
+
|
|
20
|
+
The free, open-source counterpart to OpenAI's Dots announcement (DevDay 2026,
|
|
21
|
+
Sept 29), built the week it was announced. `fullstop` is the final name
|
|
22
|
+
(verified free on PyPI 2026-09-30); the project was developed under the
|
|
23
|
+
working name `hearth`, which is why early git history says so. No OpenAI
|
|
24
|
+
marks are used in the repo, package, or CLI names.
|
|
25
|
+
|
|
26
|
+
- Runtime: Python 3.11+, **stdlib only** (no third-party imports anywhere —
|
|
27
|
+
proven by `tests/test_imports.py`)
|
|
28
|
+
- `pathlib` everywhere, no shellisms; the suite runs green on Windows (CI
|
|
29
|
+
is configured to add `ubuntu-latest`)
|
|
30
|
+
- Synchronous only; JSON everywhere, no YAML
|
|
31
|
+
|
|
32
|
+
## What it is, honestly
|
|
33
|
+
|
|
34
|
+
An agent that keeps working toward a goal across sessions: you give it a goal
|
|
35
|
+
and a manifest, it runs a plan → act → observe loop against an
|
|
36
|
+
OpenAI-compatible model API, checkpoints its state as JSON after every step,
|
|
37
|
+
and can resume after a crash — including mid-step. The crash-mid-run resume
|
|
38
|
+
without double execution is demonstrated by
|
|
39
|
+
`tests/test_fix_01_crash_resume.py`, not just guard-stop resumes.
|
|
40
|
+
|
|
41
|
+
It is **not**: a hosted cloud product; a real browser in v0.1 (`web_fetch`
|
|
42
|
+
only — the `browser` tool interface is defined but unimplemented, and saying
|
|
43
|
+
so is enforced by `tests/test_tools.py::BrowserToolTests`); a multi-agent
|
|
44
|
+
orchestrator; a plugin marketplace; streaming output or a desktop GUI (the
|
|
45
|
+
loopback activity view documented below is a local dashboard served to your
|
|
46
|
+
browser, not either); wired to
|
|
47
|
+
any non-OpenAI-compatible provider protocol (Anthropic native etc. — only
|
|
48
|
+
OpenAI-compatible chat-completions endpoints, plus the scripted mock).
|
|
49
|
+
|
|
50
|
+
## fullstop vs Dots — as announced
|
|
51
|
+
|
|
52
|
+
OpenAI announced Dots and Specialist Dots at DevDay 2026 (Sept 29). The
|
|
53
|
+
comparison below is scoped to the capabilities **as announced** that day —
|
|
54
|
+
not to press speculation — and is dated for exactly that reason: OpenAI may
|
|
55
|
+
have shipped more since. OpenAI and Dots are trademarks of OpenAI; fullstop
|
|
56
|
+
is an independent open-source project with no affiliation. What fullstop
|
|
57
|
+
**has**, **has not**, and **will not** have:
|
|
58
|
+
|
|
59
|
+
| Capability | fullstop v0.1 | Dots (as announced 2026-09-29) |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| Hosting | self-hosted, your machine | OpenAI cloud |
|
|
62
|
+
| Model | any endpoint that speaks the OpenAI chat-completions API (unit-tested against scripted mock endpoints; you supply the base URL; live endpoints untested — see Limits) | OpenAI models |
|
|
63
|
+
| Always-on loop, checkpoints, resume | has — incl. crash-mid-run with no double execution (`tests/test_fix_01_crash_resume.py`) | has (managed) |
|
|
64
|
+
| Policy gate over every consequential action | has — code between model and tools, default-deny (`tests/test_policy_gate.py`) | has (product-managed review) |
|
|
65
|
+
| Human approval showing the full request | has — at the console (`tests/test_fix_06_approval_display.py`) | has (in-product) |
|
|
66
|
+
| Tamper-evident activity log you can read and verify | has — with rotation (`tests/test_fix_08_activity_log_rotation.py`) | not disclosed |
|
|
67
|
+
| Config integrity anchored in the log | has — `config_loaded` hashes; resume refuses swapped gate rules (`tests/test_fix_02_config_tamper.py`) | not disclosed |
|
|
68
|
+
| Browser automation | **has not** — v0.1 is `web_fetch` only, redirect hops re-validated per hop (`tests/test_fix_05_redirect_chain.py`) | has (built-in) |
|
|
69
|
+
| Multi-agent orchestration / specialist dots | **has not** | has |
|
|
70
|
+
| Streaming output | **has not** | has |
|
|
71
|
+
|
|
72
|
+
fullstop **will not** have — permanent non-goals, not a todo list: cloud
|
|
73
|
+
hosting; a plugin marketplace; non-OpenAI-compatible provider protocols
|
|
74
|
+
(Anthropic native etc.); multi-agent orchestration.
|
|
75
|
+
|
|
76
|
+
fullstop **has not** yet, and may gain later: real browser automation;
|
|
77
|
+
streaming; message-history compaction (context exhaustion stops the run
|
|
78
|
+
honestly — see Limits below).
|
|
79
|
+
|
|
80
|
+
## Quickstart (keyless, exactly ONE approval prompt)
|
|
81
|
+
|
|
82
|
+
From the repo root:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m fullstop run --manifest examples/scripted-demo.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The scripted demo needs **no API key and no network**. It performs, in order:
|
|
89
|
+
|
|
90
|
+
1. a `note` write to `memory.md` — pre-approved by the demo policy, **no prompt**;
|
|
91
|
+
2. a pre-approved `notes/intro.md` write + read — **no prompt**;
|
|
92
|
+
3. a `summary.md` write that matches nothing pre-approved — **THE one prompt**:
|
|
93
|
+
- answer `y` → the write executes and `summary.md` appears;
|
|
94
|
+
- answer `n` (or just Enter) → the write is denied, logged, and **the run
|
|
95
|
+
still completes**;
|
|
96
|
+
- note: on a NON-interactive console (piped stdin) no one can answer, so
|
|
97
|
+
the run pauses in `stopped_approval` instead — resume it from a real
|
|
98
|
+
terminal (`tests/test_fix_10_unattended_stop.py`);
|
|
99
|
+
4. a glob-disguised escape write `notes/../../escape.txt` — the glob
|
|
100
|
+
`notes/**` pre-approves it at the gate (no prompt), and the **sandbox
|
|
101
|
+
blocks it** — you watch the backstop work without being asked;
|
|
102
|
+
5. a final summary turn — run completes.
|
|
103
|
+
|
|
104
|
+
Then inspect the evidence:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m fullstop log --manifest examples/scripted-demo.json --verify
|
|
108
|
+
python -m fullstop status --manifest examples/scripted-demo.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The quickstart's exact behavior (exactly one prompt, the artifact set, the
|
|
112
|
+
sandbox block, the verified hash chain) is asserted by
|
|
113
|
+
`tests/test_examples.py`, which loads the shipped files from disk.
|
|
114
|
+
|
|
115
|
+
## The example manifests
|
|
116
|
+
|
|
117
|
+
| Manifest | What it demonstrates | Prompts |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `examples/scripted-demo.json` | the full loop: pre-approved writes, one approval, a blocked escape | exactly one |
|
|
120
|
+
| `examples/research-readonly.json` | a read-only specialist: listings are free, `notes/**` and `memory.md` are protected, so both write attempts (`file_write`, `note`) are hard-denied with no human present — the workspace ends untouched | zero |
|
|
121
|
+
| `examples/writer-approval.json` | an approval-gated writer: `drafts/**` writes freely, the `report.md` write and the memory note each ask once (answer `y`, then `n`), and a `secret-*.txt` write is unapprovable at any price | exactly two |
|
|
122
|
+
| `examples/research-assistant.json` | the real-provider template (needs `FULLSTOP_API_KEY` and your model id; validated by load in `tests/test_examples.py`) | — |
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m fullstop run --manifest examples/research-readonly.json
|
|
126
|
+
python -m fullstop run --manifest examples/writer-approval.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The first completes with zero prompts on any console. The second needs a
|
|
130
|
+
human for its two prompts; where no one can answer, the same attended-only
|
|
131
|
+
rules as the quickstart apply — a genuinely non-tty console pauses in
|
|
132
|
+
`stopped_approval` for a later `resume`. That unattended stop is tested
|
|
133
|
+
generically (`tests/test_fix_10_unattended_stop.py`), not against this
|
|
134
|
+
example — in the suite the writer-approval run is exercised with an
|
|
135
|
+
approver armed. Both runs' exact behavior — prompt counts, decision trail,
|
|
136
|
+
artifact set, verified chain — is asserted by `tests/test_examples.py`.
|
|
137
|
+
|
|
138
|
+
Any run can override its manifest's goal for that one run with `--goal`
|
|
139
|
+
(`python -m fullstop run --manifest examples/research-readonly.json --goal
|
|
140
|
+
"Inventory only"`); the override is applied before the loop — and thus the
|
|
141
|
+
system prompt — is built (`tests/test_fix_04_goal_override.py`).
|
|
142
|
+
|
|
143
|
+
## Resuming, editing the config, and `--allow-config-change`
|
|
144
|
+
|
|
145
|
+
`fullstop resume` continues from the checkpoint. Two facts govern every
|
|
146
|
+
resume:
|
|
147
|
+
|
|
148
|
+
- **Guard-stopped runs are resumable.** When a run stops at a limit
|
|
149
|
+
(`stopped_max_steps`, `stopped_max_cost`, `stopped_context`,
|
|
150
|
+
`stopped_approval`), the operator edits the manifest — the canonical
|
|
151
|
+
case is to raise `max_steps` (or lift `max_context_chars` / remove a
|
|
152
|
+
deny rule) — and resumes from there
|
|
153
|
+
(`tests/test_agent_loop.py`, `tests/test_fix_10_unattended_stop.py`).
|
|
154
|
+
- **Resume refuses a changed config by default.** Every start and resume
|
|
155
|
+
appends a `config_loaded` event with sha256 hashes of the manifest and
|
|
156
|
+
policy bytes; `resume` compares against the last one and refuses on a
|
|
157
|
+
mismatch, because one approved write must never silently swap the gate's
|
|
158
|
+
rules (`tests/test_fix_02_config_tamper.py`).
|
|
159
|
+
|
|
160
|
+
Editing the manifest to raise `max_steps` changes the manifest bytes, so
|
|
161
|
+
the standard resume-with-edit flow is:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python -m fullstop resume --manifest <path.json> --allow-config-change
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`--allow-config-change` is the explicit operator override: you assert the
|
|
168
|
+
config change is yours and intentional, and the new `config_loaded` event
|
|
169
|
+
anchors the run to the edited rules from that resume onward.
|
|
170
|
+
|
|
171
|
+
**Failed runs have no supported retry.** Only guard-stopped
|
|
172
|
+
(`stopped_*`) and crash-mid-run (`running`) checkpoints continue;
|
|
173
|
+
`completed`, `failed`, and `script_exhausted` runs are terminal — `resume`
|
|
174
|
+
on them does nothing except re-log and exit (nonzero for `failed`). There
|
|
175
|
+
is no supported retry of a failed run; start a new run (`run`) instead.
|
|
176
|
+
|
|
177
|
+
## The activity view (loopback dashboard)
|
|
178
|
+
|
|
179
|
+
`python -m fullstop ui --manifest <path.json>` serves a dashboard for that
|
|
180
|
+
manifest's workspace and opens it in a browser (`--no-browser` prints the
|
|
181
|
+
URL instead). `run --ui` and `resume --ui` serve the same dashboard while a
|
|
182
|
+
run executes: the run stays foreground and the dashboard lives on daemon
|
|
183
|
+
threads, polling the checkpoint and log files the loop already writes; the
|
|
184
|
+
server never imports the agent loop (`tests/test_ui_cli.py`).
|
|
185
|
+
|
|
186
|
+
What it shows:
|
|
187
|
+
|
|
188
|
+
- the current run: goal, status, steps, cost, tokens, and the checkpointed
|
|
189
|
+
failure line if any;
|
|
190
|
+
- the activity log as written: narration (model replies), tool calls, gate
|
|
191
|
+
decisions, approvals, sandbox blocks, guard trips, run end
|
|
192
|
+
(`tests/test_ui_model.py`);
|
|
193
|
+
- a history panel of the runs recorded in this workspace, and a verify
|
|
194
|
+
button that re-checks the hash chain and reports the first broken entry
|
|
195
|
+
(`tests/test_ui_server.py`);
|
|
196
|
+
- approval cards. With `--ui`, an APPROVAL_REQUIRED decision renders the
|
|
197
|
+
complete request (the full `render_request` output, never a summary) in
|
|
198
|
+
the browser. Approvals use a decision-file protocol: the pending card is
|
|
199
|
+
written to a rendezvous directory under the OS temp dir, deliberately
|
|
200
|
+
OUTSIDE the workspace sandbox so the agent's own file tools can never
|
|
201
|
+
reach it, let alone forge an approval; each prompt carries a 64-bit
|
|
202
|
+
random id bound into both filenames; a wrong-id or forged decision file
|
|
203
|
+
is ignored; and on timeout (default 300 s, `--approval-timeout`) the run
|
|
204
|
+
fails closed into `stopped_approval`, exactly like an unattended console
|
|
205
|
+
(`tests/test_ui_approver.py`, `tests/test_ui_cli.py`);
|
|
206
|
+
- a new-run wizard: builds ONE manifest file with an inline policy,
|
|
207
|
+
validated by the same strict validators the runtime uses (errors surface
|
|
208
|
+
field-level and verbatim), saves to new files only (an existing path is
|
|
209
|
+
refused and never modified; the no-clobber write is atomic), and carries
|
|
210
|
+
credential env var NAMES only; the written bytes are scrubbed through
|
|
211
|
+
`Redactor.from_env`, so an environment value can never be persisted even
|
|
212
|
+
if pasted into a text field (`tests/test_ui_wizard.py`).
|
|
213
|
+
|
|
214
|
+
Trust boundaries, stated plainly:
|
|
215
|
+
|
|
216
|
+
- the server binds 127.0.0.1 only and there is NO host flag anywhere; a
|
|
217
|
+
`--host` argument is rejected by the CLI parser
|
|
218
|
+
(`tests/test_ui_server.py`);
|
|
219
|
+
- requests whose Host header is not this loopback origin are refused
|
|
220
|
+
(DNS-rebinding guard), and every POST must carry the `X-Fullstop-UI`
|
|
221
|
+
header that cross-site form posts cannot set (CSRF guard)
|
|
222
|
+
(`tests/test_ui_server.py`);
|
|
223
|
+
- the dashboard deliberately carries NO token. This is a deliberate
|
|
224
|
+
contrast with keysmith's token-gated dashboard, not an oversight: any
|
|
225
|
+
process running as the local user can read the page and answer an
|
|
226
|
+
approval card. If that is not acceptable on your machine, do not use
|
|
227
|
+
`--ui`;
|
|
228
|
+
- the only writes the dashboard can perform are the decision file and,
|
|
229
|
+
when explicitly asked through the wizard, one new manifest file; it
|
|
230
|
+
holds no code path that opens checkpoints, manifests, or the log for
|
|
231
|
+
writing (`tests/test_ui_server.py`).
|
|
232
|
+
|
|
233
|
+
## Manifest schema (strict; unknown keys rejected)
|
|
234
|
+
|
|
235
|
+
```jsonc
|
|
236
|
+
{
|
|
237
|
+
"identity": {"name": str, "role": str, "home": str}, // home = workspace dir
|
|
238
|
+
"goal": str,
|
|
239
|
+
"policy": "path.json" | { "inline policy object" },
|
|
240
|
+
"credentials": ["ENV_VAR_NAME", ...], // optional; NAMES only
|
|
241
|
+
"provider": {"type": "openai_compat", "base_url": str, "api_key_env": str,
|
|
242
|
+
"model": str, "usd_per_1k_input": num?, "usd_per_1k_output": num?,
|
|
243
|
+
"timeout_s": num?}
|
|
244
|
+
| {"type": "scripted", "script_path": str},
|
|
245
|
+
"limits": {"max_steps": int, "max_cost_usd": num?, "max_tool_calls_per_step": int?,
|
|
246
|
+
"max_context_chars": int?}, // near-limit estimate -> stopped_context
|
|
247
|
+
"log_truncate_chars": int?
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`identity.home`, the policy path form, and `provider.script_path` all resolve
|
|
252
|
+
**relative to the manifest file's directory** (validated from any CWD by
|
|
253
|
+
`tests/test_manifest_policy.py`). `max_cost_usd` requires both pricing fields
|
|
254
|
+
(they are illustrative placeholders in `examples/research-assistant.json` —
|
|
255
|
+
set them to your provider's real prices).
|
|
256
|
+
|
|
257
|
+
**Model config note — the markers are load-bearing.** The
|
|
258
|
+
`<<<TOOL_CALL>>>` / `<<<END_TOOL_CALL>>>` fences are the only way a model can
|
|
259
|
+
act. A model that *mentions* a marker in prose (quoted or not) hijacks the
|
|
260
|
+
scan: by quote parity, the next real block surfaces as a loud
|
|
261
|
+
`malformed_call` error (`invalid JSON` / `unterminated`) instead of
|
|
262
|
+
executing, and case-mismatched markers (e.g. `<<<tool_call>>>`) are plain
|
|
263
|
+
prose — the reply completes silently with no call. When choosing or prompting
|
|
264
|
+
a model, tell it to **never echo the markers outside an actual call**
|
|
265
|
+
(`tests/test_fix_23_parser_stray_marker_cliffs.py` pins the exact cliffs).
|
|
266
|
+
|
|
267
|
+
## Policy schema (default-deny)
|
|
268
|
+
|
|
269
|
+
```jsonc
|
|
270
|
+
{
|
|
271
|
+
"protected_paths": ["secrets/**", "*.pem", ".env"], // + non-removable .fullstop/**
|
|
272
|
+
"write_preapproved": ["notes/**", "memory.md"],
|
|
273
|
+
"shell": {"allow": ["git", // bare name: ONLY the bare invocation
|
|
274
|
+
{"program": "git", // constrained entry: program
|
|
275
|
+
"args": ["status"]}], // + exact/pattern ("*") arguments
|
|
276
|
+
"deny": ["rm"], "timeout_s": 30},
|
|
277
|
+
"web": {"allow_domains": ["arxiv.org"], "deny_domains": [], "timeout_s": 10,
|
|
278
|
+
"max_bytes": 1048576}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**The note-is-a-write law:** `note` is a WRITE, gated exactly like
|
|
283
|
+
`file_write` against `memory.md`. It is DENIED when that file is protected,
|
|
284
|
+
pre-approved when it is listed in `write_preapproved`, and otherwise
|
|
285
|
+
APPROVAL_REQUIRED. Operators who want frictionless memory put `"memory.md"` in
|
|
286
|
+
`write_preapproved` — of the four shipped example policies, `scripted-demo`
|
|
287
|
+
and `research-assistant` preapprove it, `writer-approval` routes the memory
|
|
288
|
+
note to human approval (it asks once), and `research-readonly` protects
|
|
289
|
+
`memory.md` outright so the note is hard-denied. Proven by
|
|
290
|
+
`tests/test_policy_gate.py`.
|
|
291
|
+
|
|
292
|
+
**The no-arbitrary-arguments law:** an allowlist entry pre-approves a
|
|
293
|
+
program ONLY together with its argument constraints — a bare string entry
|
|
294
|
+
pre-approves just the bare invocation, a `{"program", "args"}` entry
|
|
295
|
+
pre-approves exactly matching argument patterns; anything else routes to
|
|
296
|
+
human approval. Programs are matched as resolved absolute paths (PATH-only,
|
|
297
|
+
`.exe`-pinned on Windows; the workspace cwd is never searched, and
|
|
298
|
+
`.bat`/`.cmd` targets are refused outright). Proven by
|
|
299
|
+
`tests/test_fix_07_shell_allowlist.py`.
|
|
300
|
+
|
|
301
|
+
## The gate (the differentiator)
|
|
302
|
+
|
|
303
|
+
Every tool call is classified read-only vs consequential and decided by the
|
|
304
|
+
table in `fullstop/gate.py`:
|
|
305
|
+
|
|
306
|
+
| call | decision |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `file_read` unprotected / `file_list` of an ordinary directory | ALLOW (read-only; protected entries such as `.fullstop/` are hidden from the listing) |
|
|
309
|
+
| `file_list` of a protected directory | **operator-approvable** |
|
|
310
|
+
| `file_read` protected (incl. `.fullstop/**`) | **operator-approvable** |
|
|
311
|
+
| `file_write` protected / denylisted shell / denylisted domain / `.fullstop` write | **hard DENY — approval never upgrades it** |
|
|
312
|
+
| `file_write` / `note` / `shell` / `web_fetch` / `browser` matching a pre-approval | ALLOW |
|
|
313
|
+
| everything else consequential | APPROVAL_REQUIRED → human prompt |
|
|
314
|
+
|
|
315
|
+
The gate matches the **literal** path spelling (a glob can pre-approve
|
|
316
|
+
`notes/../../escape.txt`); the sandbox chokepoint inside the tool is the
|
|
317
|
+
ordered backstop that kills it. Approvals only unlock APPROVAL_REQUIRED
|
|
318
|
+
outcomes; hard denials stay hard (`tests/test_policy_gate.py`). ALLOW decisions
|
|
319
|
+
carry a per-call token minted with a per-Gate secret; the tool registry
|
|
320
|
+
refuses to execute anything else (`fullstop/tools/base.py`), so a forged
|
|
321
|
+
decision from model output cannot reach a tool.
|
|
322
|
+
|
|
323
|
+
## Security model — and its limits
|
|
324
|
+
|
|
325
|
+
Model:
|
|
326
|
+
|
|
327
|
+
- **Sandbox**: all file tools pass through `fullstop/sandbox.py` — absolute,
|
|
328
|
+
drive/UNC/ADS/colon, NUL, and `..` spellings are rejected; realpath +
|
|
329
|
+
commonpath containment; symlink aliases are blocked
|
|
330
|
+
(`tests/test_sandbox.py`).
|
|
331
|
+
- **Anti-aliasing**: writes additionally require resolved == literal path, so
|
|
332
|
+
symlinked/short-name/trailing-dot redirects of writes fail closed
|
|
333
|
+
(`tests/test_tools.py`).
|
|
334
|
+
- **Credentials**: manifests carry env var NAMES only; values are read at call
|
|
335
|
+
time, scrubbed (`[REDACTED:NAME]`) at every write layer, and removed from
|
|
336
|
+
child process environments (`tests/test_credentials.py`).
|
|
337
|
+
- **Tamper-evident log**: every line chains
|
|
338
|
+
`sha256(prev:canonical_json(line))`; appending onto a tampered tail fails
|
|
339
|
+
loudly (`tests/test_activity_log.py`). Tamper-evident, **not** tamper-proof —
|
|
340
|
+
an attacker with write access to the file can rebuild the whole chain.
|
|
341
|
+
|
|
342
|
+
Limits, stated plainly ("not yet" — untested or unimplemented):
|
|
343
|
+
|
|
344
|
+
- Shell children inherit the environment **minus the credential names** — full
|
|
345
|
+
environment isolation is not yet implemented.
|
|
346
|
+
- `web_fetch` has no JavaScript and no cookies. Redirects ARE followed, but
|
|
347
|
+
every hop is re-validated against the full domain allow/deny predicate
|
|
348
|
+
before it is requested (proven against a live loopback redirect chain in
|
|
349
|
+
`tests/test_fix_05_redirect_chain.py`); the ordinary live-network path is
|
|
350
|
+
otherwise exercised only by that test.
|
|
351
|
+
- **Context exhaustion**: message history grows without compaction in v0.1.
|
|
352
|
+
When the provider rejects a call for context length, or the estimated
|
|
353
|
+
context reaches `limits.max_context_chars`, the run stops in
|
|
354
|
+
`stopped_context` — surfaced again on resume, honestly, rather than
|
|
355
|
+
looping `failed` → resume → `failed`
|
|
356
|
+
(`tests/test_fix_03_context_exhaustion.py`).
|
|
357
|
+
- **Attended-only approvals**: when a consequential action needs approval
|
|
358
|
+
and no human can answer (no approver configured, or a non-tty console),
|
|
359
|
+
the run pauses in `stopped_approval` instead of spinning to `max_steps`;
|
|
360
|
+
it resumes once a human answers (`tests/test_fix_10_unattended_stop.py`).
|
|
361
|
+
- Live-model behavior (real internet model endpoints) is untested by the
|
|
362
|
+
suite — the suite is keyless by design, and its only network I/O is
|
|
363
|
+
loopback; the provider
|
|
364
|
+
adapter is tested against a fake opener plus a loopback mock HTTP
|
|
365
|
+
endpoint serving realistic error bodies (`tests/test_provider.py`,
|
|
366
|
+
`tests/test_r2_02_provider_context_body.py`).
|
|
367
|
+
- The write-path equality rule intentionally **fails closed** on
|
|
368
|
+
legal-but-unusual Windows spellings whose realpath differs from the literal
|
|
369
|
+
(8.3 short names, trailing dots and spaces). Blocking them is deliberate;
|
|
370
|
+
do not "repair" this.
|
|
371
|
+
- Protected **reads** are operator-approvable and, once approved, execute;
|
|
372
|
+
protected **writes** are unapprovable, full stop.
|
|
373
|
+
|
|
374
|
+
## Deliberate deviation from the charter
|
|
375
|
+
|
|
376
|
+
`ScriptedModel` ships **in the package** (`fullstop/provider.py`), not only in
|
|
377
|
+
the test suite as CHARTER.md:15 imagined, because the keyless quickstart and
|
|
378
|
+
the smoke script need it. Tests import it from there. This is the only stated
|
|
379
|
+
deviation.
|
|
380
|
+
|
|
381
|
+
## Development
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
python -m unittest discover -s tests # the whole suite: zero keys, loopback-only tests
|
|
385
|
+
python scripts/smoke.py # DoD#2 smoke (keyless, socketless)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
CI (`.github/workflows/ci.yml`) is configured to run the same suite on
|
|
389
|
+
`ubuntu-latest` and `windows-latest` on every push — with no install step,
|
|
390
|
+
because the repo is stdlib-only.
|
|
391
|
+
|
|
392
|
+
## Claim → demonstrating test
|
|
393
|
+
|
|
394
|
+
| Claim | Test |
|
|
395
|
+
| --- | --- |
|
|
396
|
+
| Zero third-party imports | `tests/test_imports.py` |
|
|
397
|
+
| Sandbox escapes blocked + logged | `tests/test_sandbox.py` |
|
|
398
|
+
| Credentials never logged/written | `tests/test_credentials.py` |
|
|
399
|
+
| Gate not bypassable by model output; approvals execute; hard denials stick | `tests/test_policy_gate.py` |
|
|
400
|
+
| Tamper-evident chained log | `tests/test_activity_log.py` |
|
|
401
|
+
| Strict protocol parsing | `tests/test_protocol.py` |
|
|
402
|
+
| Checkpoints atomic + scrubbed | `tests/test_state.py` |
|
|
403
|
+
| Tool semantics (incl. alias rules) | `tests/test_tools.py` |
|
|
404
|
+
| Provider adapter (key only in header) | `tests/test_provider.py` |
|
|
405
|
+
| Loop: guards, resume, event sequence | `tests/test_agent_loop.py` |
|
|
406
|
+
| CLI exit codes / JSON output | `tests/test_cli.py` |
|
|
407
|
+
| Shipped examples work as documented | `tests/test_examples.py` |
|
|
408
|
+
| Read-only example: zero prompts; both writes hard-denied; workspace untouched | `tests/test_examples.py` |
|
|
409
|
+
| Approval-gated writer example: exactly two prompts; protected write unapprovable | `tests/test_examples.py` |
|
|
410
|
+
| Quickstart: exactly one prompt, sandbox block, verified chain | `tests/test_examples.py` |
|
|
411
|
+
| Manifest/policy validation incl. relative resolution | `tests/test_manifest_policy.py` |
|
|
412
|
+
| Crash-mid-run resume: no double execution, no re-prompt of approved calls | `tests/test_fix_01_crash_resume.py` |
|
|
413
|
+
| Config anchored: in-sandbox config refused; hash mismatch blocks resume | `tests/test_fix_02_config_tamper.py` |
|
|
414
|
+
| Context exhaustion stops in `stopped_context` | `tests/test_fix_03_context_exhaustion.py` |
|
|
415
|
+
| `--goal` override reaches the system prompt | `tests/test_fix_04_goal_override.py` |
|
|
416
|
+
| Redirect hops re-validated per hop | `tests/test_fix_05_redirect_chain.py` |
|
|
417
|
+
| Approver prompts show the complete request | `tests/test_fix_06_approval_display.py` |
|
|
418
|
+
| Shell allowlist constrains arguments; PATH-resolved programs; batch refused | `tests/test_fix_07_shell_allowlist.py` |
|
|
419
|
+
| Log appends never re-read the file; rotation keeps the chain verifiable across segments | `tests/test_fix_08_activity_log_rotation.py` |
|
|
420
|
+
| `file_read` caps the read itself | `tests/test_fix_09_file_read_cap.py` |
|
|
421
|
+
| Unattended approval pauses in `stopped_approval` | `tests/test_fix_10_unattended_stop.py` |
|
|
422
|
+
| Redactor reads credentials at call time | `tests/test_fix_11_redactor_call_time.py` |
|
|
423
|
+
| `note` write protected like `file_write` (symlink-safe target) | `tests/test_fix_12_note_symlink.py` |
|
|
424
|
+
| `file_list` hides protected metadata without approval | `tests/test_fix_13_file_list_protected.py` |
|
|
425
|
+
| Approval-gated `web_fetch` logged as consequential | `tests/test_fix_14_gate_classification_label.py` |
|
|
426
|
+
| Tool output delimited per session (no forged observations) | `tests/test_fix_15_observation_delimiters.py` |
|
|
427
|
+
| `log_truncate_chars` reaches the activity log | `tests/test_fix_16_log_truncate_wiring.py` |
|
|
428
|
+
| README honesty (comparison vs announced Dots capabilities, non-goals, limits) | `tests/test_fix_17_19_readme_honesty.py` |
|
|
429
|
+
| Crash-resume replay-dedup holds for oversized and redacted args (raw-args digest) | `tests/test_r2_01_replay_dedup_keys.py` |
|
|
430
|
+
| Provider error bodies carried (size-capped); context 400 -> `stopped_context` via a mock HTTP endpoint | `tests/test_r2_02_provider_context_body.py` |
|
|
431
|
+
| Oversize log lines never wedge the tail scan; `log_truncate_chars` bounded below the tail window | `tests/test_r2_03_log_oversize_wedge.py` |
|
|
432
|
+
| `--allow-config-change` and the resume-with-edit flow documented | `tests/test_r2_04_readme_allow_config_change.py` |
|
|
433
|
+
| Error observations delimited per session, capped like ok-output | `tests/test_r2_05_error_observation_frame.py` |
|
|
434
|
+
| Resume of a pre-v0.1.2 system-only checkpoint seeds the goal from the persisted goal; no double seed | `tests/test_fix_22_resume_seed_guard.py` |
|
|
435
|
+
| Stray-marker cliffs pinned: bare mention → malformed + later block parses; quoted mention → unterminated; forged block inside an args string inert | `tests/test_fix_23_parser_stray_marker_cliffs.py` |
|
|
436
|
+
| Activity view: loopback-only bind, no host flag, Host allowlist + X-Fullstop-UI POST guards, decision-file-only write surface | `tests/test_ui_server.py` |
|
|
437
|
+
| Browser approvals: decision-file protocol outside the sandbox, full request on the card, forged id ignored, timeout → `stopped_approval` | `tests/test_ui_approver.py` |
|
|
438
|
+
| `run --ui` / `resume --ui` end-to-end over real loopback HTTP; `ui` subcommand serves until stopped | `tests/test_ui_cli.py` |
|
|
439
|
+
| UI read model: snapshot/history views and chain verify over the log | `tests/test_ui_model.py` |
|
|
440
|
+
| Wizard: real strict validators, verbatim field errors, new-file-only atomic save, env values never persisted | `tests/test_ui_wizard.py` |
|
|
441
|
+
| Front-end naming law (no prose in the assets) and inline-script breakout guard | `tests/test_ui_assets.py` |
|
|
442
|
+
|
|
443
|
+
## License
|
|
444
|
+
|
|
445
|
+
MIT — see `LICENSE`.
|