Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
node: 24
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: ${{ matrix.node }}
cache: npm
Expand All @@ -40,8 +40,8 @@ jobs:
name: Package validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
cache: npm
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to AgentTX are documented here. The project follows Semantic Versioning.

## Unreleased

### Added

- Successful rollback now emits path-free, hash-linked evidence recording whether the Git-visible original workspace status changed during rollback.
- `agenttx evidence <transaction-id>` regenerates rollback evidence from the terminal ledger when the initial atomic artifact write is unavailable.
- `agenttx verify-evidence <file>` checks the canonical outer receipt digest and every offline-derivable invariant without claiming authentication.
- Workspace evidence now binds tracked diffs and untracked content fingerprints, and rollback metadata is bound into the terminal ledger event.

### Changed

- Bump the unreleased package identity to `0.2.0`.
- Pin CI checkout and Node setup actions to reviewed immutable commits.

## [0.1.0] - 2026-08-08

### Added
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ agenttx rollback
```

`agenttx commit` applies files to your working tree; it does **not** create or stage a Git commit.
`agenttx rollback` also writes a redacted `rollback-evidence.json` with discarded-change counts, a bound terminal event, and content-sensitive before/after digests recording whether the Git-visible original workspace stayed unchanged. Verify its unsigned integrity offline with `agenttx verify-evidence <file>`.

Try the real, deterministic demo with no model, credentials, remote, or network write:

Expand All @@ -54,7 +55,7 @@ agenttx demo

AgentTX runs the child command inside an independent local Git clone, from the equivalent repository directory. Your original working tree stays available and unchanged until you explicitly accept the transaction. After the child exits, inspect its diff, verification results, detected side effects, and risk; then commit or roll back.

> **Security boundary:** AgentTX v0.1.0 isolates supported repository changes, not the operating system. Child processes retain your normal user permissions, and external-action detection is heuristic. Read the [security model](docs/SECURITY_MODEL.md).
> **Security boundary:** AgentTX v0.2.0 isolates supported repository changes, not the operating system. Child processes retain your normal user permissions, and external-action detection is heuristic. Read the [security model](docs/SECURITY_MODEL.md).

## Why AgentTX?

Expand All @@ -76,9 +77,11 @@ AgentTX captures the repository baseline, builds an independent local clone, ove
| `agenttx inspect [id] [--json]` | Show changes, side effects, risk, and checks |
| `agenttx verify [id] [--run]` | Discover checks; run them only with `--run` |
| `agenttx commit [id]` | Accept transaction files after conflict checks |
| `agenttx rollback [id]` | Discard the isolated transaction |
| `agenttx rollback [id]` | Discard the isolated transaction and write rollback evidence |
| `agenttx history [--json]` | List local transaction history |
| `agenttx replay <id> [--json]` | Read recorded events; it does not re-execute |
| `agenttx evidence <id> [--output path]` | Regenerate redacted rollback evidence from the terminal ledger |
| `agenttx verify-evidence <file>` | Offline-check receipt integrity and derivable invariants; it does not authenticate the artifact |
| `agenttx report [id] --html` | Write a standalone redacted HTML report |
| `agenttx doctor [--json]` | Check Node, Git, repository state, storage, and agent CLIs |
| `agenttx demo [--keep]` | Run the offline seven-file demo |
Expand All @@ -101,6 +104,8 @@ Named adapters identify common CLIs; they do not depend on private agent hooks.

Using AgentTX with Claude Code, Codex, Gemini CLI, OpenCode, or another coding agent? [Open a compatibility report](https://github.com/aliengineering-byte/agenttx/issues/new?template=agent-compatibility.yml). Real reports determine which agent-specific workflows receive deeper testing.

Found another problem or workflow gap? Open a [sanitized bug report](https://github.com/aliengineering-byte/agenttx/issues/new?template=bug.yml) or a [focused feature request](https://github.com/aliengineering-byte/agenttx/issues/new?template=feature.yml).

## Safety model

AgentTX gives a strong, narrow repository guarantee: before acceptance, rollback removes only the isolated transaction workspace; acceptance refuses overlapping changes in the original repository and restores from a recovery backup if file application fails.
Expand All @@ -109,6 +114,8 @@ AgentTX is **not an OS security boundary**. The child retains your normal user p

No telemetry, account, API key, Docker daemon, or cloud service is required. AgentTX does not upload code, paths, prompts, commands, diffs, or transaction metadata.

Rollback receipts are unsigned and recomputable. Their outer hash detects accidental or partial modification, while the offline verifier also checks the bound terminal event, metadata/diff references, and derived workspace result. It is integrity checking, not authentication against someone able to rewrite the complete local receipt and ledger.

Read [SECURITY.md](SECURITY.md), [the exact security model](docs/SECURITY_MODEL.md), and [the threat model](docs/THREAT_MODEL.md) before relying on AgentTX around untrusted code.

## Honest V0 limitations
Expand Down
2 changes: 1 addition & 1 deletion docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dependency and license record

AgentTX v0.1.0 has **zero runtime dependencies**.
AgentTX v0.2.0 has **zero runtime dependencies**.

The release environment resolved these direct development dependencies from `package-lock.json`:

Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Run from the repository root. Record evidence in the release notes or release ha

- [ ] GitHub CI green on the final release commit
- [ ] release commit SHA recorded
- [ ] annotated `v0.1.0` tag prepared from that exact commit
- [ ] annotated `v0.2.0` tag prepared from that exact commit
- [ ] package tarball SHA-256 recorded
- [ ] GitHub repository publication verified
- [ ] npm publication verified from the public registry
Expand Down
71 changes: 71 additions & 0 deletions docs/SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,74 @@ File `kind` is `added`, `modified`, `deleted`, or `renamed`. Binary line counts
Risk `level` is `LOW`, `MEDIUM`, `HIGH`, or `CRITICAL`. Every score contribution appears in `reasons`.

Secret finding values are always the literal `[REDACTED]`.

## Rollback evidence

A successful `agenttx rollback` writes `rollback-evidence.json` in the transaction
directory and prints its location. If persistence fails, rollback remains complete
and the CLI prints `agenttx evidence <transaction-id>` as the regeneration path.
Evidence export accepts an existing byte-identical artifact but refuses to replace
different content, including at an explicit `--output` path.

The schema has an unsigned receipt plus an outer integrity record:

```json
{
"receipt": {
"schemaVersion": 1,
"evidenceType": "agenttx.rollback",
"producer": { "version": "0.2.0", "...": "..." },
"transaction": { "state": "ROLLED_BACK", "...": "..." },
"result": { "originalWorkspaceStatusUnchanged": true, "...": "..." },
"workspaceStatusEvidence": {
"algorithm": "sha256(agenttx-git-visible-content-v1)",
"before": "<sha256>",
"after": "<sha256>"
},
"eventChain": {
"algorithm": "sha256(JSON.stringify(event))",
"events": 7,
"finalHash": "<sha256>",
"terminalEvent": { "type": "rollback.completed", "...": "..." }
},
"artifacts": {
"transactionDiff": { "sha256": "<sha256>", "...": "..." },
"transactionMetadata": { "sha256": "<sha256>", "...": "..." }
},
"redaction": {
"filePathsIncluded": false,
"commandArgumentsIncluded": false,
"privatePathsIncluded": false,
"secrets": "redacted"
},
"limitations": ["..."]
},
"integrity": {
"algorithm": "sha256",
"canonicalization": "agenttx-canonical-json-v1",
"scope": "receipt",
"authentication": "none",
"digest": "<sha256>"
}
}
```

`agenttx-canonical-json-v1` serializes primitives with JSON rules, preserves array
order, and sorts object keys by explicit UTF-16 code-unit comparison. The outer
digest covers every field in `receipt`. `agenttx verify-evidence <file>` recomputes
that digest and checks strict field sets, formats and counts, the transaction
metadata digest, the terminal ledger event hash and cross-references, and the
workspace result derived from the before/after digests.

`originalWorkspaceStatusUnchanged` is never trusted as an independent assertion.
It is `null` if either workspace digest is unavailable; otherwise it is exactly
the equality result for those digests. `agenttx-git-visible-content-v1` commits to
Git `HEAD`, porcelain-v2 status, the full binary tracked diff relative to `HEAD`,
and sorted untracked path/content fingerprints. Only the digests are exported;
ignored files and external systems remain outside the receipt.

The terminal event binds the transaction ID, baseline/base object IDs, completion
time, discarded diff, and workspace digests without exporting commands, private
paths, or file names. The receipt is deliberately unsigned and recomputable. Its
hash detects accidental or partial alteration, but is not authentication and is
not proof against a party able to rewrite the complete local receipt and ledger.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agenttx",
"version": "0.1.0",
"version": "0.2.0",
"description": "Git-style transactions for AI coding agents. Inspect, commit, or roll back agent changes.",
"type": "module",
"bin": {
Expand Down
9 changes: 7 additions & 2 deletions scripts/release-verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ async function initializeRepository(root) {
}

const project = resolve(fileURLToPath(new URL("..", import.meta.url)));
const manifest = JSON.parse(await readFile(join(project, "package.json"), "utf8"));
if (manifest.name !== "agenttx" || typeof manifest.version !== "string") {
throw new Error("Unexpected package identity in package.json");
}
const expectedVersion = manifest.version;
const temporary = await mkdtemp(join(tmpdir(), "agenttx-release-verify-"));
const packDirectory = join(temporary, "pack");
const prefix = join(temporary, "prefix");
Expand All @@ -82,7 +87,7 @@ try {
} else {
await execute(packageManager, ["pack", "--pack-destination", packDirectory], { cwd: project });
}
const tarball = join(packDirectory, "agenttx-0.1.0.tgz");
const tarball = join(packDirectory, `${manifest.name}-${expectedVersion}.tgz`);
await access(tarball);
if (hasNpm) {
await execute(packageManager, ["install", tarball, "--prefix", prefix]);
Expand All @@ -93,7 +98,7 @@ try {
const shebang = (await readFile(cli, "utf8")).split("\n")[0];
if (shebang !== "#!/usr/bin/env node") throw new Error(`Unexpected CLI shebang: ${shebang}`);
const version = (await execute(process.execPath, [cli, "--version"], { capture: true })).stdout.trim();
if (version !== "0.1.0") throw new Error(`Unexpected installed version: ${version}`);
if (version !== expectedVersion) throw new Error(`Unexpected installed version: ${version}`);

await initializeRepository(repository);
const env = { ...process.env, AGENTTX_HOME: transactionHome };
Expand Down
46 changes: 43 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { resolveAdapter } from "./adapters/agent.js";
import { runDoctor, renderDoctor } from "./cli/doctor.js";
import { verifyRollbackEvidenceFile, writeRollbackEvidence } from "./core/evidence.js";
import { inspectTransaction } from "./core/inspection.js";
import { EventLedger } from "./core/ledger.js";
import { redactText } from "./core/redaction.js";
Expand All @@ -30,6 +31,7 @@ import {
renderTransactionReport,
renderVerification
} from "./reporters/terminal.js";
import { VERSION } from "./version.js";

const execFileAsync = promisify(execFile);
const cliPath = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -62,7 +64,7 @@ function flagValue(args: readonly string[], flag: string): string | undefined {
}

function help(): string {
return `AgentTX 0.1.0 — Make AI agents undoable.
return `AgentTX ${VERSION} — Make AI agents undoable.

Usage:
agenttx run [--allow-external] [--] <command...>
Expand All @@ -74,6 +76,8 @@ Usage:
agenttx rollback [transaction-id]
agenttx history [--json]
agenttx replay <transaction-id> [--json]
agenttx evidence <transaction-id> [--output path]
agenttx verify-evidence <file>
agenttx report [transaction-id] --html [--output path]
agenttx doctor [--json]
agenttx demo [--keep]
Expand Down Expand Up @@ -167,7 +171,21 @@ async function handleRollback(args: string[]): Promise<void> {
const result = await rollbackTransaction(metadata);
print(`Transaction ${result.metadata.transactionId} rolled back.\n`);
print(`Discarded ${result.diff.filesChanged} file change${result.diff.filesChanged === 1 ? "" : "s"}.`);
print("Original workspace unchanged.");
if (result.originalWorkspaceStatusUnchanged === true) {
print("Git-visible original workspace status unchanged (digest matched).");
} else if (result.originalWorkspaceStatusUnchanged === false) {
print("Warning: Git-visible original workspace status changed during rollback; inspect the evidence artifact.");
} else {
print("Warning: original workspace status could not be verified; inspect the evidence artifact.");
}
if (result.evidencePath) {
print(`Rollback evidence: ${result.evidencePath}`);
} else {
process.stderr.write(
`Rollback completed, but evidence could not be written: ${result.evidenceWarning ?? "unknown error"}\n` +
`Regenerate it with: agenttx evidence ${result.metadata.transactionId}\n`
);
}
}

async function handleHistory(args: string[]): Promise<void> {
Expand All @@ -188,6 +206,23 @@ async function handleReplay(args: string[]): Promise<void> {
for (const event of events) print(` ${String(event.seq).padStart(3)} ${event.timestamp} ${event.type}`);
}

async function handleEvidence(args: string[]): Promise<void> {
const id = positional(args)[0];
if (!id) throw new Error("agenttx evidence requires a transaction ID.");
const metadata = await resolveTransaction(id, process.cwd());
const path = await writeRollbackEvidence(metadata, flagValue(args, "--output"));
print(`Rollback evidence written to ${path}`);
}

async function handleVerifyEvidence(args: string[]): Promise<void> {
const path = positional(args)[0];
if (!path) throw new Error("agenttx verify-evidence requires an evidence file.");
const verification = await verifyRollbackEvidenceFile(path);
print(`Evidence integrity verified for ${verification.transactionId}.`);
print(`Receipt SHA-256: ${verification.digest}`);
print("Authentication: none — this is unsigned, recomputable integrity, not authentication.");
}

async function handleReport(args: string[]): Promise<void> {
if (!hasFlag(args, "--html")) throw new Error("V0 report output requires --html.");
const id = positional(args)[0];
Expand Down Expand Up @@ -250,6 +285,10 @@ async function main(): Promise<void> {
process.exitCode = await runShim(transactionId, tool, executable, args.slice(separator + 1));
return;
}
if (command === "verify-evidence") {
await handleVerifyEvidence(args);
return;
}
const recovered = await recoverInterruptedTransactions();
if (recovered.length && !hasFlag(args, "--json")) {
for (const item of recovered) {
Expand All @@ -266,11 +305,12 @@ async function main(): Promise<void> {
case "rollback": await handleRollback(args); break;
case "history": await handleHistory(args); break;
case "replay": await handleReplay(args); break;
case "evidence": await handleEvidence(args); break;
case "report": await handleReport(args); break;
case "doctor": await handleDoctor(args); break;
case "demo": await handleDemo(args); break;
case "--version":
case "-v": print("0.1.0"); break;
case "-v": print(VERSION); break;
case "help":
case "--help":
case "-h": print(help()); break;
Expand Down
Loading