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.
@@ -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,6 @@
1
+ from .snapshot import SnapshotStore
2
+ from .transaction import Transaction
3
+ from .interceptor import assess
4
+ from .audit import AuditLog
5
+ __all__ = ["SnapshotStore", "Transaction", "assess", "AuditLog"]
6
+ __version__ = "0.3.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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()