chrono-agent 0.3.0__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.
- chrono_agent-0.3.0/LICENSE +21 -0
- chrono_agent-0.3.0/PKG-INFO +185 -0
- chrono_agent-0.3.0/README.md +176 -0
- chrono_agent-0.3.0/chrono_agent/__init__.py +6 -0
- chrono_agent-0.3.0/chrono_agent/__main__.py +4 -0
- chrono_agent-0.3.0/chrono_agent/audit.py +205 -0
- chrono_agent-0.3.0/chrono_agent/cli.py +494 -0
- chrono_agent-0.3.0/chrono_agent/dashboard.py +87 -0
- chrono_agent-0.3.0/chrono_agent/db.py +157 -0
- chrono_agent-0.3.0/chrono_agent/gitlink.py +90 -0
- chrono_agent-0.3.0/chrono_agent/interceptor.py +33 -0
- chrono_agent-0.3.0/chrono_agent/mcp_server.py +160 -0
- chrono_agent-0.3.0/chrono_agent/policy.py +164 -0
- chrono_agent-0.3.0/chrono_agent/remote_audit.py +90 -0
- chrono_agent-0.3.0/chrono_agent/snapshot.py +433 -0
- chrono_agent-0.3.0/chrono_agent/team.py +99 -0
- chrono_agent-0.3.0/chrono_agent/transaction.py +74 -0
- chrono_agent-0.3.0/chrono_agent.egg-info/PKG-INFO +185 -0
- chrono_agent-0.3.0/chrono_agent.egg-info/SOURCES.txt +25 -0
- chrono_agent-0.3.0/chrono_agent.egg-info/dependency_links.txt +1 -0
- chrono_agent-0.3.0/chrono_agent.egg-info/entry_points.txt +2 -0
- chrono_agent-0.3.0/chrono_agent.egg-info/top_level.txt +1 -0
- chrono_agent-0.3.0/pyproject.toml +16 -0
- chrono_agent-0.3.0/setup.cfg +4 -0
- chrono_agent-0.3.0/tests/test_audit.py +252 -0
- chrono_agent-0.3.0/tests/test_runtime.py +430 -0
- chrono_agent-0.3.0/tests/test_v030.py +330 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ChronoAgent 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.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chrono-agent
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Local filesystem snapshots, savepoints, rollback, branches, advisory command scanning and a tamper-evident audit trail.
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Dynamic: license-file
|
|
9
|
+
|
|
10
|
+
# ChronoAgent 0.3.0 — local file recovery CLI with a tamper-evident audit trail
|
|
11
|
+
|
|
12
|
+
A dependency-free Python tool for snapshots, granular savepoints, rollback, branches, conservative three-way merges, advisory command scanning and an append-only audit log of every action. The website is a separate simulator. This package does not connect to Llama, Claude, GPT or a database.
|
|
13
|
+
|
|
14
|
+
## Install / upgrade in Windows CMD
|
|
15
|
+
Download the ZIP without extracting it. In its download folder:
|
|
16
|
+
|
|
17
|
+
```cmd
|
|
18
|
+
python -m pip install --user --upgrade chrono-agent-0.2.0.zip
|
|
19
|
+
python -m chrono_agent --version
|
|
20
|
+
```
|
|
21
|
+
Expected: `chrono-agent 0.2.0`. If `chrono` is not on PATH, all examples also work by replacing `chrono` with `python -m chrono_agent`. Restart CMD after a permanent PATH change. Python 3.8 or newer is required. This distribution is not published to PyPI.
|
|
22
|
+
|
|
23
|
+
## Continue the test folder from 0.1.0
|
|
24
|
+
```cmd
|
|
25
|
+
cd /d "%USERPROFILE%\chrono-dev-test-01"
|
|
26
|
+
chrono run python success_agent.py
|
|
27
|
+
python app.py
|
|
28
|
+
chrono status
|
|
29
|
+
chrono log
|
|
30
|
+
```
|
|
31
|
+
Expected: `COMMIT sp_...`, `VERSION_3`, `(no changes)`, and a new `COMMIT: python success_agent.py` snapshot at HEAD. Existing 0.1.0 snapshots are readable; do not delete `.chrono`.
|
|
32
|
+
|
|
33
|
+
## Command reference (one command at a time, never a pipeline)
|
|
34
|
+
```cmd
|
|
35
|
+
chrono init
|
|
36
|
+
chrono snapshot -m "before agent"
|
|
37
|
+
chrono run python agent.py
|
|
38
|
+
chrono status
|
|
39
|
+
chrono diff
|
|
40
|
+
chrono log
|
|
41
|
+
chrono rollback
|
|
42
|
+
chrono branch feature
|
|
43
|
+
chrono checkout main
|
|
44
|
+
chrono merge feature
|
|
45
|
+
chrono scan "rm -rf /"
|
|
46
|
+
chrono audit
|
|
47
|
+
chrono audit --verify
|
|
48
|
+
chrono audit --csv --out audit-report.csv
|
|
49
|
+
chrono audit --push https://audit.example.com/ingest --verify-remote
|
|
50
|
+
chrono policy
|
|
51
|
+
chrono team add sara --role developer
|
|
52
|
+
chrono team list
|
|
53
|
+
chrono git install-hook
|
|
54
|
+
chrono git status
|
|
55
|
+
chrono db add appdb --engine sqlite --path app.db
|
|
56
|
+
chrono db snapshot appdb
|
|
57
|
+
chrono db restore appdb
|
|
58
|
+
chrono dashboard --out report.html
|
|
59
|
+
chrono mcp
|
|
60
|
+
chrono --help
|
|
61
|
+
```
|
|
62
|
+
Use these only inside a disposable test project initially. `init` is idempotent. Default rollback restores the current branch HEAD, not automatically its parent; choose an ID from `log` to restore an older version. It asks `[y/n]`; enter `y` before typing any further command. `rollback -y` skips confirmation and discards unsaved tracked changes.
|
|
63
|
+
|
|
64
|
+
`branch NAME` creates **and switches** to that branch. Branch creation and checkout save dirty source work before leaving it. Merge requires clean work, preserves independent changes, records both parents, and stops without changes if both branches alter the same file differently. It is file-level merge, not automatic line-level conflict resolution.
|
|
65
|
+
|
|
66
|
+
## Run / rescue behavior
|
|
67
|
+
Programs run with exact argument boundaries, without a shell by default. Use `--` if needed:
|
|
68
|
+
|
|
69
|
+
```cmd
|
|
70
|
+
chrono run -- python "my agent.py"
|
|
71
|
+
```
|
|
72
|
+
A successful run saves a post-command snapshot **before** printing COMMIT. On failure:
|
|
73
|
+
- `r`: rollback tracked files to the pre-command snapshot; the CLI still returns the child's failure code.
|
|
74
|
+
- `d`: inspect diff only; the program is NOT rerun.
|
|
75
|
+
- `t`: explicitly retry the program against the current changed files; non-idempotent actions can repeat.
|
|
76
|
+
- `k`: keep changes without committing; returns the failure code.
|
|
77
|
+
- EOF / redirected input: rollback by default.
|
|
78
|
+
|
|
79
|
+
Intentional shell execution must pass one quoted string. In Windows CMD:
|
|
80
|
+
```cmd
|
|
81
|
+
chrono run --shell "echo hello"
|
|
82
|
+
```
|
|
83
|
+
In Unix, the shell is `/bin/sh`; in Windows it is CMD/COMSPEC, not the invoking PowerShell. Batch files require explicit shell mode. `--force` bypasses the advisory detector, NOT OS permissions; never use it just to suppress a warning on an untrusted command.
|
|
84
|
+
|
|
85
|
+
## Audit trail
|
|
86
|
+
Every action that changes state, plus every risk scan, is appended to `.chrono/audit.jsonl`: who ran it, when, from which host and folder, which snapshot it produced, the risk score of any command, and the outcome (`ok`, `blocked`, `aborted`, `uncommitted`, `rolled back`).
|
|
87
|
+
|
|
88
|
+
```cmd
|
|
89
|
+
chrono audit " newest 25 records
|
|
90
|
+
chrono audit -n 0 " all records
|
|
91
|
+
chrono audit --action run.commit
|
|
92
|
+
chrono audit --verify " check the chain for tampering
|
|
93
|
+
chrono audit --csv --out audit-report.csv
|
|
94
|
+
chrono audit --json --out audit-report.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each record stores the digest of the record before it, so editing, deleting or reordering an entry breaks the chain and `--verify` reports the first bad record. Every snapshot also stores the log head that existed when it was taken, so a log that was rewritten *and* re-chained still fails the cross-check (`snapshot anchors do not match the log`).
|
|
98
|
+
|
|
99
|
+
To make records unforgeable for anyone who only has the files, set a signing key before the first action and keep it somewhere else (a secret manager, not this folder):
|
|
100
|
+
|
|
101
|
+
```cmd
|
|
102
|
+
set CHRONO_AUDIT_KEY=copy-the-long-secret-from-your-vault
|
|
103
|
+
```
|
|
104
|
+
Digests then become HMACs; without the key, `--verify` fails loudly instead of reporting a clean chain. Attribution for AI use is taken from the environment:
|
|
105
|
+
|
|
106
|
+
```cmd
|
|
107
|
+
set CHRONO_MODEL=claude-3.5-sonnet
|
|
108
|
+
set CHRONO_AGENT=cursor
|
|
109
|
+
```
|
|
110
|
+
Those two fields are written into every later record, so a report shows which model and which client did what. They are self-reported labels, not proof.
|
|
111
|
+
|
|
112
|
+
## Demo
|
|
113
|
+
```cmd
|
|
114
|
+
chrono demo "fix login page and update database"
|
|
115
|
+
```
|
|
116
|
+
The older quoted-task shorthand still works. Demo runs in an isolated temporary folder and is fully scripted: there is no real AI, login test or database migration. Demo menu letters differ from real `run`: `u` undo failed step, `d` diff, `r` scripted retry, `f` full rollback. Demo files remain in the printed temporary path for inspection.
|
|
117
|
+
|
|
118
|
+
## Python SDK
|
|
119
|
+
```python
|
|
120
|
+
from chrono_agent import Transaction
|
|
121
|
+
|
|
122
|
+
with Transaction(".", "trusted agent") as tx:
|
|
123
|
+
# make first set of local file edits
|
|
124
|
+
tx.savepoint("models")
|
|
125
|
+
# make another set of edits
|
|
126
|
+
# tx.rollback_to("models") preserves the first set
|
|
127
|
+
# exception: restore BEGIN; successful exit: save COMMIT
|
|
128
|
+
```
|
|
129
|
+
Later named savepoints are invalidated after rollback to an earlier one. Duplicate names are rejected. A failed commit triggers recovery; if recovery also fails, the error preserves the original exception as its cause. Locks prevent another cooperating Chrono process from changing the same project's state during a transaction.
|
|
130
|
+
|
|
131
|
+
## What 0.3.0 adds
|
|
132
|
+
- **Policy file** (`chrono-policy.yaml` in the project root): your own command rules with scores and actions (`block` / `approve` / `allow`), custom risk thresholds, a named approver list, and role gates (`rollback: admin` etc.). `chrono policy` validates and shows the active policy. A broken policy file fails closed with a clear error.
|
|
133
|
+
- **Approval flow**: when a rule says `approve`, `chrono run` stops until a named human approves — interactively, or with `chrono run --approver manan --approval-reason "release fix" -- <cmd>`. Approvals and denials are written to the audit log.
|
|
134
|
+
- **Team roster** (`chrono team add/list/remove`): local per-project names and roles (viewer/developer/admin). The first member must be an admin; the last admin cannot be removed. Role gates come from the policy file; the acting user is `CHRONO_USER` or the OS account. This is a local file, not company accounts — anyone with write access to the folder can edit it.
|
|
135
|
+
- **Git integration**: every snapshot records the current Git commit (`git_head` in the snapshot metadata). `chrono git status` shows whether the last savepoint matches the checked-out commit; `chrono git install-hook` adds a post-commit hook that snapshots after every commit (it refuses to touch a hook it did not install).
|
|
136
|
+
- **Database rollback** (`chrono db add/snapshot/restore/list/remove`): sqlite is fully supported via SQLite's backup API with an integrity check before restore. Postgres (`pg_dump`/`psql`) and MySQL (`mysqldump`/`mysql`) are implemented against their standard client tools but were NOT tested against a live server in the build environment — verify on your own database before relying on them. A restore overwrites current data and asks `[y/n]` unless `-y` is given.
|
|
137
|
+
- **MCP connector** (`chrono mcp`): a Model Context Protocol server over stdio exposing six tools (`chrono_scan`, `chrono_snapshot`, `chrono_status`, `chrono_log`, `chrono_rollback`, `chrono_audit_verify`) to AI clients such as Claude Desktop or Cursor. Client config: `{ "mcpServers": { "chrono": { "command": "chrono", "args": ["mcp"] } } }`. Rollback requires `confirm: true`. Actions are recorded in the audit log with `CHRONO_AGENT`/`CHRONO_MODEL` attribution.
|
|
138
|
+
- **Remote audit push** (`chrono audit --push URL [--verify-remote]`): sends only the records the remote has not seen to an append-only HTTP(S) store and can compare chain heads. This closes the "newest records deleted locally" gap — but the remote service itself is not included; the expected request/response contract is documented in `chrono_agent/remote_audit.py`.
|
|
139
|
+
- **Local dashboard** (`chrono dashboard --out report.html`): a self-contained HTML report of the audit chain status, actions, outcomes, risk levels, branches, savepoints and records. Single project, offline, no tracking — not the multi-team company dashboard from the roadmap.
|
|
140
|
+
- **MIT license** (`LICENSE`) and a step-by-step `PUBLISHING.md` for releasing to PyPI with your own account.
|
|
141
|
+
|
|
142
|
+
## What 0.2.0 adds
|
|
143
|
+
- Append-only, hash-chained audit log of every state-changing action and risk scan (`.chrono/audit.jsonl`).
|
|
144
|
+
- `chrono audit` report with `--verify`, `--json`, `--csv`, `--out`, `-n` and `--action`.
|
|
145
|
+
- Snapshot anchors: each snapshot stores the audit log head, so a rewritten and re-chained log is still detected.
|
|
146
|
+
- Optional HMAC signing with `CHRONO_AUDIT_KEY` so a writer without the key cannot forge records.
|
|
147
|
+
- Model and client attribution from `CHRONO_MODEL` / `CHRONO_AGENT` on every record.
|
|
148
|
+
- Audit storage is checked for symlinks and is never included in a snapshot.
|
|
149
|
+
|
|
150
|
+
## What 0.1.1 fixes
|
|
151
|
+
- Real successful-run COMMIT snapshots and clean HEAD/status.
|
|
152
|
+
- Diff viewing no longer executes a failed program again.
|
|
153
|
+
- Exact subprocess arguments, empty-command rejection and explicit shell mode.
|
|
154
|
+
- File-level three-way merges instead of wholesale source-tree replacement.
|
|
155
|
+
- Conflict detection, source ancestry, clean-work checks and safer checkout.
|
|
156
|
+
- Atomic individual object/state writes, verified hashes and UUID snapshot IDs.
|
|
157
|
+
- Interprocess operation locks and snapshot mutation checks.
|
|
158
|
+
- Traversal/symlink rejection and restore object preflight before deleting files.
|
|
159
|
+
- Best-effort in-process recovery on restore write errors and file/directory transitions.
|
|
160
|
+
- Per-file POSIX mode restoration; identical bytes need not share permissions.
|
|
161
|
+
- Commit-failure, launch-failure, EOF and interrupt recovery paths.
|
|
162
|
+
- Idempotent init, readable expected errors, typo rejection and module execution fallback.
|
|
163
|
+
- More command-risk variants and removal of misleading ACID/security guarantees.
|
|
164
|
+
|
|
165
|
+
## Important boundaries — do not call this production ACID or a sandbox
|
|
166
|
+
- Only regular files under the selected root are tracked. Run commands from the initialized root; parent discovery is not implemented.
|
|
167
|
+
- Symlinks and special files are rejected; symlink-based project layouts need another tool.
|
|
168
|
+
- Empty directories, ownership, ACLs, timestamps, hard-link relationships and Windows alternate streams are not preserved. POSIX file mode preservation is supported, not a Windows ACL backup.
|
|
169
|
+
- `.chrono`, `.git`, `node_modules`, `__pycache__`, `.venv`, `venv`, `.idea`, `.vscode` are excluded. Changes in these locations cannot be rolled back. Secret files elsewhere are NOT automatically excluded; do not share snapshot storage.
|
|
170
|
+
- Files outside the root, network calls, messages, payments, detached processes and other external effects cannot be undone. Database rollback is limited to databases you explicitly register with `chrono db add`, and only to the point of the last `chrono db snapshot` — it is a dump restore, not a transaction log, so writes after the snapshot are lost on restore.
|
|
171
|
+
- The detector is a text heuristic with false positives and bypasses; a Python script can be dangerous despite a LOW score. No real quarantine or OS containment is provided.
|
|
172
|
+
- Cooperating CLI processes are serialized. Editors/untrusted children can still alter files or `.chrono`; stop other writers. Checks are not resistant to malicious concurrent symlink replacement.
|
|
173
|
+
- Atomic replacement of individual files is not an atomic entire-tree transaction. Power loss, kill -9, disk failure/fullness or a second recovery failure can leave a partial restore. Keep independent backups outside the project. Files are buffered in memory during restore; very large projects are not the target.
|
|
174
|
+
- Snapshots inside `.chrono` are not tamper-proof or independent backups. Hard adversarial isolation, crash-safe journals, external backups, real model routing, multi-agent orchestration and database transactions are NOT implemented.
|
|
175
|
+
- The audit log is a local file. It detects records that were edited, deleted, reordered or re-chained, but **deleting the newest records leaves no local gap** — use `chrono audit --push` to a store you do not control to close that. The push itself is only as trustworthy as the remote service.
|
|
176
|
+
- Team roles and approvals are enforced only inside this CLI on this machine. A developer can bypass them by editing files directly; they are guardrails for cooperating users, not security boundaries. Real company accounts and a central dashboard need a server product that does not exist yet.
|
|
177
|
+
- With `CHRONO_AUDIT_KEY`, anyone holding the key can still forge the whole log; the key must live outside the project folder, and losing it makes a signed log unverifiable.
|
|
178
|
+
- `CHRONO_MODEL` / `CHRONO_AGENT` are self-reported labels taken from the environment, not verified identity.
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
After extracting the ZIP, in the extracted package folder:
|
|
182
|
+
```cmd
|
|
183
|
+
python -m unittest discover -s tests -v
|
|
184
|
+
```
|
|
185
|
+
Tests use temporary projects and safe child scripts, not your real project. Test results and verification environment are in `VERIFICATION.md`.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# ChronoAgent 0.3.0 — local file recovery CLI with a tamper-evident audit trail
|
|
2
|
+
|
|
3
|
+
A dependency-free Python tool for snapshots, granular savepoints, rollback, branches, conservative three-way merges, advisory command scanning and an append-only audit log of every action. The website is a separate simulator. This package does not connect to Llama, Claude, GPT or a database.
|
|
4
|
+
|
|
5
|
+
## Install / upgrade in Windows CMD
|
|
6
|
+
Download the ZIP without extracting it. In its download folder:
|
|
7
|
+
|
|
8
|
+
```cmd
|
|
9
|
+
python -m pip install --user --upgrade chrono-agent-0.2.0.zip
|
|
10
|
+
python -m chrono_agent --version
|
|
11
|
+
```
|
|
12
|
+
Expected: `chrono-agent 0.2.0`. If `chrono` is not on PATH, all examples also work by replacing `chrono` with `python -m chrono_agent`. Restart CMD after a permanent PATH change. Python 3.8 or newer is required. This distribution is not published to PyPI.
|
|
13
|
+
|
|
14
|
+
## Continue the test folder from 0.1.0
|
|
15
|
+
```cmd
|
|
16
|
+
cd /d "%USERPROFILE%\chrono-dev-test-01"
|
|
17
|
+
chrono run python success_agent.py
|
|
18
|
+
python app.py
|
|
19
|
+
chrono status
|
|
20
|
+
chrono log
|
|
21
|
+
```
|
|
22
|
+
Expected: `COMMIT sp_...`, `VERSION_3`, `(no changes)`, and a new `COMMIT: python success_agent.py` snapshot at HEAD. Existing 0.1.0 snapshots are readable; do not delete `.chrono`.
|
|
23
|
+
|
|
24
|
+
## Command reference (one command at a time, never a pipeline)
|
|
25
|
+
```cmd
|
|
26
|
+
chrono init
|
|
27
|
+
chrono snapshot -m "before agent"
|
|
28
|
+
chrono run python agent.py
|
|
29
|
+
chrono status
|
|
30
|
+
chrono diff
|
|
31
|
+
chrono log
|
|
32
|
+
chrono rollback
|
|
33
|
+
chrono branch feature
|
|
34
|
+
chrono checkout main
|
|
35
|
+
chrono merge feature
|
|
36
|
+
chrono scan "rm -rf /"
|
|
37
|
+
chrono audit
|
|
38
|
+
chrono audit --verify
|
|
39
|
+
chrono audit --csv --out audit-report.csv
|
|
40
|
+
chrono audit --push https://audit.example.com/ingest --verify-remote
|
|
41
|
+
chrono policy
|
|
42
|
+
chrono team add sara --role developer
|
|
43
|
+
chrono team list
|
|
44
|
+
chrono git install-hook
|
|
45
|
+
chrono git status
|
|
46
|
+
chrono db add appdb --engine sqlite --path app.db
|
|
47
|
+
chrono db snapshot appdb
|
|
48
|
+
chrono db restore appdb
|
|
49
|
+
chrono dashboard --out report.html
|
|
50
|
+
chrono mcp
|
|
51
|
+
chrono --help
|
|
52
|
+
```
|
|
53
|
+
Use these only inside a disposable test project initially. `init` is idempotent. Default rollback restores the current branch HEAD, not automatically its parent; choose an ID from `log` to restore an older version. It asks `[y/n]`; enter `y` before typing any further command. `rollback -y` skips confirmation and discards unsaved tracked changes.
|
|
54
|
+
|
|
55
|
+
`branch NAME` creates **and switches** to that branch. Branch creation and checkout save dirty source work before leaving it. Merge requires clean work, preserves independent changes, records both parents, and stops without changes if both branches alter the same file differently. It is file-level merge, not automatic line-level conflict resolution.
|
|
56
|
+
|
|
57
|
+
## Run / rescue behavior
|
|
58
|
+
Programs run with exact argument boundaries, without a shell by default. Use `--` if needed:
|
|
59
|
+
|
|
60
|
+
```cmd
|
|
61
|
+
chrono run -- python "my agent.py"
|
|
62
|
+
```
|
|
63
|
+
A successful run saves a post-command snapshot **before** printing COMMIT. On failure:
|
|
64
|
+
- `r`: rollback tracked files to the pre-command snapshot; the CLI still returns the child's failure code.
|
|
65
|
+
- `d`: inspect diff only; the program is NOT rerun.
|
|
66
|
+
- `t`: explicitly retry the program against the current changed files; non-idempotent actions can repeat.
|
|
67
|
+
- `k`: keep changes without committing; returns the failure code.
|
|
68
|
+
- EOF / redirected input: rollback by default.
|
|
69
|
+
|
|
70
|
+
Intentional shell execution must pass one quoted string. In Windows CMD:
|
|
71
|
+
```cmd
|
|
72
|
+
chrono run --shell "echo hello"
|
|
73
|
+
```
|
|
74
|
+
In Unix, the shell is `/bin/sh`; in Windows it is CMD/COMSPEC, not the invoking PowerShell. Batch files require explicit shell mode. `--force` bypasses the advisory detector, NOT OS permissions; never use it just to suppress a warning on an untrusted command.
|
|
75
|
+
|
|
76
|
+
## Audit trail
|
|
77
|
+
Every action that changes state, plus every risk scan, is appended to `.chrono/audit.jsonl`: who ran it, when, from which host and folder, which snapshot it produced, the risk score of any command, and the outcome (`ok`, `blocked`, `aborted`, `uncommitted`, `rolled back`).
|
|
78
|
+
|
|
79
|
+
```cmd
|
|
80
|
+
chrono audit " newest 25 records
|
|
81
|
+
chrono audit -n 0 " all records
|
|
82
|
+
chrono audit --action run.commit
|
|
83
|
+
chrono audit --verify " check the chain for tampering
|
|
84
|
+
chrono audit --csv --out audit-report.csv
|
|
85
|
+
chrono audit --json --out audit-report.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Each record stores the digest of the record before it, so editing, deleting or reordering an entry breaks the chain and `--verify` reports the first bad record. Every snapshot also stores the log head that existed when it was taken, so a log that was rewritten *and* re-chained still fails the cross-check (`snapshot anchors do not match the log`).
|
|
89
|
+
|
|
90
|
+
To make records unforgeable for anyone who only has the files, set a signing key before the first action and keep it somewhere else (a secret manager, not this folder):
|
|
91
|
+
|
|
92
|
+
```cmd
|
|
93
|
+
set CHRONO_AUDIT_KEY=copy-the-long-secret-from-your-vault
|
|
94
|
+
```
|
|
95
|
+
Digests then become HMACs; without the key, `--verify` fails loudly instead of reporting a clean chain. Attribution for AI use is taken from the environment:
|
|
96
|
+
|
|
97
|
+
```cmd
|
|
98
|
+
set CHRONO_MODEL=claude-3.5-sonnet
|
|
99
|
+
set CHRONO_AGENT=cursor
|
|
100
|
+
```
|
|
101
|
+
Those two fields are written into every later record, so a report shows which model and which client did what. They are self-reported labels, not proof.
|
|
102
|
+
|
|
103
|
+
## Demo
|
|
104
|
+
```cmd
|
|
105
|
+
chrono demo "fix login page and update database"
|
|
106
|
+
```
|
|
107
|
+
The older quoted-task shorthand still works. Demo runs in an isolated temporary folder and is fully scripted: there is no real AI, login test or database migration. Demo menu letters differ from real `run`: `u` undo failed step, `d` diff, `r` scripted retry, `f` full rollback. Demo files remain in the printed temporary path for inspection.
|
|
108
|
+
|
|
109
|
+
## Python SDK
|
|
110
|
+
```python
|
|
111
|
+
from chrono_agent import Transaction
|
|
112
|
+
|
|
113
|
+
with Transaction(".", "trusted agent") as tx:
|
|
114
|
+
# make first set of local file edits
|
|
115
|
+
tx.savepoint("models")
|
|
116
|
+
# make another set of edits
|
|
117
|
+
# tx.rollback_to("models") preserves the first set
|
|
118
|
+
# exception: restore BEGIN; successful exit: save COMMIT
|
|
119
|
+
```
|
|
120
|
+
Later named savepoints are invalidated after rollback to an earlier one. Duplicate names are rejected. A failed commit triggers recovery; if recovery also fails, the error preserves the original exception as its cause. Locks prevent another cooperating Chrono process from changing the same project's state during a transaction.
|
|
121
|
+
|
|
122
|
+
## What 0.3.0 adds
|
|
123
|
+
- **Policy file** (`chrono-policy.yaml` in the project root): your own command rules with scores and actions (`block` / `approve` / `allow`), custom risk thresholds, a named approver list, and role gates (`rollback: admin` etc.). `chrono policy` validates and shows the active policy. A broken policy file fails closed with a clear error.
|
|
124
|
+
- **Approval flow**: when a rule says `approve`, `chrono run` stops until a named human approves — interactively, or with `chrono run --approver manan --approval-reason "release fix" -- <cmd>`. Approvals and denials are written to the audit log.
|
|
125
|
+
- **Team roster** (`chrono team add/list/remove`): local per-project names and roles (viewer/developer/admin). The first member must be an admin; the last admin cannot be removed. Role gates come from the policy file; the acting user is `CHRONO_USER` or the OS account. This is a local file, not company accounts — anyone with write access to the folder can edit it.
|
|
126
|
+
- **Git integration**: every snapshot records the current Git commit (`git_head` in the snapshot metadata). `chrono git status` shows whether the last savepoint matches the checked-out commit; `chrono git install-hook` adds a post-commit hook that snapshots after every commit (it refuses to touch a hook it did not install).
|
|
127
|
+
- **Database rollback** (`chrono db add/snapshot/restore/list/remove`): sqlite is fully supported via SQLite's backup API with an integrity check before restore. Postgres (`pg_dump`/`psql`) and MySQL (`mysqldump`/`mysql`) are implemented against their standard client tools but were NOT tested against a live server in the build environment — verify on your own database before relying on them. A restore overwrites current data and asks `[y/n]` unless `-y` is given.
|
|
128
|
+
- **MCP connector** (`chrono mcp`): a Model Context Protocol server over stdio exposing six tools (`chrono_scan`, `chrono_snapshot`, `chrono_status`, `chrono_log`, `chrono_rollback`, `chrono_audit_verify`) to AI clients such as Claude Desktop or Cursor. Client config: `{ "mcpServers": { "chrono": { "command": "chrono", "args": ["mcp"] } } }`. Rollback requires `confirm: true`. Actions are recorded in the audit log with `CHRONO_AGENT`/`CHRONO_MODEL` attribution.
|
|
129
|
+
- **Remote audit push** (`chrono audit --push URL [--verify-remote]`): sends only the records the remote has not seen to an append-only HTTP(S) store and can compare chain heads. This closes the "newest records deleted locally" gap — but the remote service itself is not included; the expected request/response contract is documented in `chrono_agent/remote_audit.py`.
|
|
130
|
+
- **Local dashboard** (`chrono dashboard --out report.html`): a self-contained HTML report of the audit chain status, actions, outcomes, risk levels, branches, savepoints and records. Single project, offline, no tracking — not the multi-team company dashboard from the roadmap.
|
|
131
|
+
- **MIT license** (`LICENSE`) and a step-by-step `PUBLISHING.md` for releasing to PyPI with your own account.
|
|
132
|
+
|
|
133
|
+
## What 0.2.0 adds
|
|
134
|
+
- Append-only, hash-chained audit log of every state-changing action and risk scan (`.chrono/audit.jsonl`).
|
|
135
|
+
- `chrono audit` report with `--verify`, `--json`, `--csv`, `--out`, `-n` and `--action`.
|
|
136
|
+
- Snapshot anchors: each snapshot stores the audit log head, so a rewritten and re-chained log is still detected.
|
|
137
|
+
- Optional HMAC signing with `CHRONO_AUDIT_KEY` so a writer without the key cannot forge records.
|
|
138
|
+
- Model and client attribution from `CHRONO_MODEL` / `CHRONO_AGENT` on every record.
|
|
139
|
+
- Audit storage is checked for symlinks and is never included in a snapshot.
|
|
140
|
+
|
|
141
|
+
## What 0.1.1 fixes
|
|
142
|
+
- Real successful-run COMMIT snapshots and clean HEAD/status.
|
|
143
|
+
- Diff viewing no longer executes a failed program again.
|
|
144
|
+
- Exact subprocess arguments, empty-command rejection and explicit shell mode.
|
|
145
|
+
- File-level three-way merges instead of wholesale source-tree replacement.
|
|
146
|
+
- Conflict detection, source ancestry, clean-work checks and safer checkout.
|
|
147
|
+
- Atomic individual object/state writes, verified hashes and UUID snapshot IDs.
|
|
148
|
+
- Interprocess operation locks and snapshot mutation checks.
|
|
149
|
+
- Traversal/symlink rejection and restore object preflight before deleting files.
|
|
150
|
+
- Best-effort in-process recovery on restore write errors and file/directory transitions.
|
|
151
|
+
- Per-file POSIX mode restoration; identical bytes need not share permissions.
|
|
152
|
+
- Commit-failure, launch-failure, EOF and interrupt recovery paths.
|
|
153
|
+
- Idempotent init, readable expected errors, typo rejection and module execution fallback.
|
|
154
|
+
- More command-risk variants and removal of misleading ACID/security guarantees.
|
|
155
|
+
|
|
156
|
+
## Important boundaries — do not call this production ACID or a sandbox
|
|
157
|
+
- Only regular files under the selected root are tracked. Run commands from the initialized root; parent discovery is not implemented.
|
|
158
|
+
- Symlinks and special files are rejected; symlink-based project layouts need another tool.
|
|
159
|
+
- Empty directories, ownership, ACLs, timestamps, hard-link relationships and Windows alternate streams are not preserved. POSIX file mode preservation is supported, not a Windows ACL backup.
|
|
160
|
+
- `.chrono`, `.git`, `node_modules`, `__pycache__`, `.venv`, `venv`, `.idea`, `.vscode` are excluded. Changes in these locations cannot be rolled back. Secret files elsewhere are NOT automatically excluded; do not share snapshot storage.
|
|
161
|
+
- Files outside the root, network calls, messages, payments, detached processes and other external effects cannot be undone. Database rollback is limited to databases you explicitly register with `chrono db add`, and only to the point of the last `chrono db snapshot` — it is a dump restore, not a transaction log, so writes after the snapshot are lost on restore.
|
|
162
|
+
- The detector is a text heuristic with false positives and bypasses; a Python script can be dangerous despite a LOW score. No real quarantine or OS containment is provided.
|
|
163
|
+
- Cooperating CLI processes are serialized. Editors/untrusted children can still alter files or `.chrono`; stop other writers. Checks are not resistant to malicious concurrent symlink replacement.
|
|
164
|
+
- Atomic replacement of individual files is not an atomic entire-tree transaction. Power loss, kill -9, disk failure/fullness or a second recovery failure can leave a partial restore. Keep independent backups outside the project. Files are buffered in memory during restore; very large projects are not the target.
|
|
165
|
+
- Snapshots inside `.chrono` are not tamper-proof or independent backups. Hard adversarial isolation, crash-safe journals, external backups, real model routing, multi-agent orchestration and database transactions are NOT implemented.
|
|
166
|
+
- The audit log is a local file. It detects records that were edited, deleted, reordered or re-chained, but **deleting the newest records leaves no local gap** — use `chrono audit --push` to a store you do not control to close that. The push itself is only as trustworthy as the remote service.
|
|
167
|
+
- Team roles and approvals are enforced only inside this CLI on this machine. A developer can bypass them by editing files directly; they are guardrails for cooperating users, not security boundaries. Real company accounts and a central dashboard need a server product that does not exist yet.
|
|
168
|
+
- With `CHRONO_AUDIT_KEY`, anyone holding the key can still forge the whole log; the key must live outside the project folder, and losing it makes a signed log unverifiable.
|
|
169
|
+
- `CHRONO_MODEL` / `CHRONO_AGENT` are self-reported labels taken from the environment, not verified identity.
|
|
170
|
+
|
|
171
|
+
## Tests
|
|
172
|
+
After extracting the ZIP, in the extracted package folder:
|
|
173
|
+
```cmd
|
|
174
|
+
python -m unittest discover -s tests -v
|
|
175
|
+
```
|
|
176
|
+
Tests use temporary projects and safe child scripts, not your real project. Test results and verification environment are in `VERIFICATION.md`.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Append-only, hash-chained audit log of every Chrono action.
|
|
2
|
+
|
|
3
|
+
Every record stores the digest of the record before it, so editing, deleting or
|
|
4
|
+
reordering an entry breaks the chain (verify() reports where). With
|
|
5
|
+
CHRONO_AUDIT_KEY set the digests are HMACs, so a writer who does not hold the
|
|
6
|
+
key cannot forge a valid chain.
|
|
7
|
+
|
|
8
|
+
This is still a local file: it proves a record was not quietly altered in place,
|
|
9
|
+
but it is not a legal-grade ledger, a remote write-once store, or proof of what a
|
|
10
|
+
program did outside the tracked files.
|
|
11
|
+
"""
|
|
12
|
+
import getpass
|
|
13
|
+
import hashlib
|
|
14
|
+
import hmac
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import socket
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
GENESIS = "GENESIS"
|
|
22
|
+
KEY_ENV = "CHRONO_AUDIT_KEY"
|
|
23
|
+
FIELDS = ("seq", "ts", "t", "actor", "host", "root", "action", "outcome", "kind",
|
|
24
|
+
"detail", "command", "snapshot", "model", "agent", "risk_score",
|
|
25
|
+
"risk_level", "files", "prev", "hash")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _actor():
|
|
29
|
+
try:
|
|
30
|
+
name = getpass.getuser()
|
|
31
|
+
except Exception:
|
|
32
|
+
name = ""
|
|
33
|
+
return (name or os.environ.get("USER") or os.environ.get("USERNAME")
|
|
34
|
+
or os.environ.get("LOGNAME") or "unknown")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _host():
|
|
38
|
+
raw = os.environ.get("CHRONO_HOST")
|
|
39
|
+
if raw:
|
|
40
|
+
return raw
|
|
41
|
+
try:
|
|
42
|
+
return socket.gethostname() or "unknown"
|
|
43
|
+
except Exception:
|
|
44
|
+
return "unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _key():
|
|
48
|
+
raw = os.environ.get(KEY_ENV)
|
|
49
|
+
return raw.encode("utf-8") if raw else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _digest(payload, key):
|
|
53
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":"),
|
|
54
|
+
ensure_ascii=True).encode("utf-8")
|
|
55
|
+
return (hmac.new(key, body, hashlib.sha256) if key
|
|
56
|
+
else hashlib.sha256(body)).hexdigest()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AuditLog:
|
|
60
|
+
"""One append-only log per project, stored at .chrono/audit.jsonl."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, root="."):
|
|
63
|
+
self.dir = Path(root).resolve() / ".chrono"
|
|
64
|
+
self.path = self.dir / "audit.jsonl"
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def enabled(self):
|
|
68
|
+
return self.dir.is_dir() and not self.path.is_symlink()
|
|
69
|
+
|
|
70
|
+
def _raw_lines(self):
|
|
71
|
+
if not self.path.is_file():
|
|
72
|
+
return []
|
|
73
|
+
with open(self.path, "rb") as f:
|
|
74
|
+
return f.read().decode("utf-8", errors="replace").splitlines()
|
|
75
|
+
|
|
76
|
+
def read(self):
|
|
77
|
+
"""Parsed records; unreadable lines come back marked, never dropped."""
|
|
78
|
+
out = []
|
|
79
|
+
for n, line in enumerate(self._raw_lines(), 1):
|
|
80
|
+
if not line.strip():
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
rec = json.loads(line)
|
|
84
|
+
if not isinstance(rec, dict):
|
|
85
|
+
raise ValueError("not an object")
|
|
86
|
+
except (ValueError, TypeError):
|
|
87
|
+
rec = {"seq": None, "action": "CORRUPT", "outcome": "unreadable",
|
|
88
|
+
"detail": "line " + str(n)}
|
|
89
|
+
out.append(rec)
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
def record(self, action, outcome="ok", **fields):
|
|
93
|
+
"""Append one event. Returns None when the project is not initialised."""
|
|
94
|
+
if not self.enabled:
|
|
95
|
+
return None
|
|
96
|
+
if self.path.is_symlink():
|
|
97
|
+
raise RuntimeError("Unsafe symlink in audit storage: " + str(self.path))
|
|
98
|
+
key = _key()
|
|
99
|
+
fields.setdefault("model", os.environ.get("CHRONO_MODEL"))
|
|
100
|
+
fields.setdefault("agent", os.environ.get("CHRONO_AGENT"))
|
|
101
|
+
lines = self._raw_lines()
|
|
102
|
+
prev = GENESIS
|
|
103
|
+
for line in reversed(lines):
|
|
104
|
+
if not line.strip():
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
last = json.loads(line)
|
|
108
|
+
except ValueError:
|
|
109
|
+
last = None
|
|
110
|
+
if isinstance(last, dict) and isinstance(last.get("hash"), str):
|
|
111
|
+
prev = last["hash"]
|
|
112
|
+
break
|
|
113
|
+
rec = {"seq": len([x for x in lines if x.strip()]) + 1,
|
|
114
|
+
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
115
|
+
"t": time.time(),
|
|
116
|
+
"actor": _actor(),
|
|
117
|
+
"host": _host(),
|
|
118
|
+
"root": str(self.dir.parent),
|
|
119
|
+
"action": action,
|
|
120
|
+
"outcome": outcome}
|
|
121
|
+
for name in ("detail", "command", "snapshot", "model", "agent", "kind",
|
|
122
|
+
"risk_score", "risk_level", "files"):
|
|
123
|
+
value = fields.get(name)
|
|
124
|
+
if value is not None:
|
|
125
|
+
rec[name] = value
|
|
126
|
+
rec["prev"] = prev
|
|
127
|
+
rec["hash"] = _digest({k: v for k, v in rec.items() if k != "hash"}, key)
|
|
128
|
+
body = (json.dumps(rec, sort_keys=True, ensure_ascii=True) + "\n").encode("utf-8")
|
|
129
|
+
with open(self.path, "ab") as f:
|
|
130
|
+
f.write(body)
|
|
131
|
+
f.flush()
|
|
132
|
+
os.fsync(f.fileno())
|
|
133
|
+
return rec
|
|
134
|
+
|
|
135
|
+
def head(self):
|
|
136
|
+
"""(seq, hash) of the newest record; (0, GENESIS) when the log is empty."""
|
|
137
|
+
for line in reversed(self._raw_lines()):
|
|
138
|
+
if not line.strip():
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
rec = json.loads(line)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return (None, None)
|
|
144
|
+
if isinstance(rec, dict) and isinstance(rec.get("hash"), str):
|
|
145
|
+
return (rec.get("seq"), rec["hash"])
|
|
146
|
+
return (None, None)
|
|
147
|
+
return (0, GENESIS)
|
|
148
|
+
|
|
149
|
+
def verify(self):
|
|
150
|
+
"""Walk the chain and report the first record that does not add up."""
|
|
151
|
+
key = _key()
|
|
152
|
+
lines = [x for x in self._raw_lines() if x.strip()]
|
|
153
|
+
if not lines:
|
|
154
|
+
return {"ok": True, "records": 0, "broken_at": None, "reason": "no audit records"}
|
|
155
|
+
if key is None and os.environ.get("CHRONO_AUDIT_REQUIRE_KEY"):
|
|
156
|
+
return {"ok": False, "records": len(lines), "broken_at": None,
|
|
157
|
+
"reason": "signed log but " + KEY_ENV + " is not set"}
|
|
158
|
+
prev = GENESIS
|
|
159
|
+
for n, line in enumerate(lines, 1):
|
|
160
|
+
try:
|
|
161
|
+
rec = json.loads(line)
|
|
162
|
+
except ValueError:
|
|
163
|
+
return {"ok": False, "records": len(lines), "broken_at": n,
|
|
164
|
+
"reason": "record is not valid JSON"}
|
|
165
|
+
if not isinstance(rec, dict):
|
|
166
|
+
return {"ok": False, "records": len(lines), "broken_at": n,
|
|
167
|
+
"reason": "record is not an object"}
|
|
168
|
+
stored = rec.get("hash")
|
|
169
|
+
if not isinstance(stored, str):
|
|
170
|
+
return {"ok": False, "records": len(lines), "broken_at": n,
|
|
171
|
+
"reason": "record has no digest"}
|
|
172
|
+
if rec.get("prev") != prev:
|
|
173
|
+
return {"ok": False, "records": len(lines), "broken_at": n,
|
|
174
|
+
"reason": "previous link does not match (record deleted or reordered)"}
|
|
175
|
+
if _digest({k: v for k, v in rec.items() if k != "hash"}, key) != stored:
|
|
176
|
+
why = "content does not match its digest (record edited)"
|
|
177
|
+
if key is None:
|
|
178
|
+
why += "; if this log is HMAC-signed, set " + KEY_ENV
|
|
179
|
+
return {"ok": False, "records": len(lines), "broken_at": n,
|
|
180
|
+
"reason": why}
|
|
181
|
+
prev = stored
|
|
182
|
+
return {"ok": True, "records": len(lines), "broken_at": None,
|
|
183
|
+
"reason": "chain intact" + (" (HMAC-signed)" if key else "")}
|
|
184
|
+
|
|
185
|
+
def rows(self, limit=None, action=None):
|
|
186
|
+
recs = self.read()
|
|
187
|
+
if action:
|
|
188
|
+
recs = [r for r in recs if r.get("action") == action]
|
|
189
|
+
return recs[-limit:] if limit else recs
|
|
190
|
+
|
|
191
|
+
def to_csv(self, records):
|
|
192
|
+
head = ["seq", "ts", "actor", "host", "action", "outcome", "snapshot",
|
|
193
|
+
"model", "agent", "risk_score", "risk_level", "files", "detail",
|
|
194
|
+
"command", "hash"]
|
|
195
|
+
import csv, io
|
|
196
|
+
buf = io.StringIO()
|
|
197
|
+
writer = csv.DictWriter(buf, fieldnames=head, extrasaction="ignore",
|
|
198
|
+
lineterminator="\n")
|
|
199
|
+
writer.writeheader()
|
|
200
|
+
for rec in records:
|
|
201
|
+
row = {k: rec.get(k, "") for k in head}
|
|
202
|
+
if isinstance(row.get("files"), dict):
|
|
203
|
+
row["files"] = " ".join("%s=%s" % kv for kv in sorted(row["files"].items()))
|
|
204
|
+
writer.writerow(row)
|
|
205
|
+
return buf.getvalue()
|