Skip to content
Open
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
46 changes: 46 additions & 0 deletions .github/workflows/upstream-compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Upstream Compatibility

on:
workflow_dispatch:
schedule:
- cron: '17 9 * * 1'

permissions:
contents: read

jobs:
hermes-latest:
name: Hermes latest isolated plugin gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.10'

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Validate against latest Hermes Agent
run: python scripts/compat-hermes-latest.py

openclaw-latest:
name: OpenClaw latest isolated plugin gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '22'

- name: Install latest OpenClaw
run: npm install -g openclaw@latest

- name: Validate against latest OpenClaw
run: node scripts/compat-openclaw-latest.mjs
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.0] - 2026-05-28

### Added

- **Hosted approval API foundation for host-owned approval flows** — Rampart can return hosted approval metadata without creating a hidden Rampart pending queue item, preserving the single approval-owner boundary for hosts such as Hermes.
- **Hermes audit/tool-call correlation** — The experimental Hermes policy gate passes Hermes tool-call metadata through Rampart so audit entries can be correlated with the originating Hermes tool call.

### Changed

- **Bundled plugin metadata is aligned for v1.2.0** — The OpenClaw and experimental Hermes plugin manifests, runtime exports, and public examples now report `1.2.0` with the Rampart release.
- **Release-facing docs and install examples now point at v1.2.0** — Current-version markers, troubleshooting examples, and container tag examples are refreshed for the release.

### Fixed

- **Audit chain recovery now resumes from the latest valid JSONL event** — Startup reconstruction recovers both the event count and chain head from existing audit logs instead of trusting absent, stale, or tampered anchors as the next `prev_hash` source.
- **Partial audit verification is safer** — `rampart audit verify --since` accepts intentionally truncated history while continuing to verify the included hash chain and anchor data that is present in the selected window.

## [1.1.1] - 2026-05-26

### Added
Expand Down
18 changes: 11 additions & 7 deletions cmd/rampart/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ Example:
return fmt.Errorf("audit: no .jsonl files found in %s", auditDir)
}

// Filter files by --since date if provided
// Filter files by --since date if provided. Size-rotated files use
// YYYY-MM-DD.pN.jsonl names, so compare only the leading date.
if since != "" {
sinceDate, parseErr := time.Parse("2006-01-02", since)
if parseErr != nil {
Expand All @@ -161,11 +162,13 @@ Example:
filtered := files[:0]
for _, f := range files {
base := filepath.Base(f)
// Filename format: YYYY-MM-DD.jsonl
datePart := strings.TrimSuffix(base, ".jsonl")
if len(datePart) >= len("2006-01-02") {
datePart = datePart[:len("2006-01-02")]
}
fileDate, dateErr := time.Parse("2006-01-02", datePart)
if dateErr != nil {
// Can't parse date from filename — include it to be safe
// Can't parse date from filename — include it to be safe.
filtered = append(filtered, f)
continue
}
Expand All @@ -179,12 +182,12 @@ Example:
}
}

count, hashesByID, err := verifyAuditChain(files)
count, hashesByID, err := verifyAuditChain(files, since != "")
if err != nil {
return err
}

if err := verifyAnchors(auditDir, hashesByID); err != nil {
if err := verifyAnchors(auditDir, hashesByID, since == ""); err != nil {
return err
}

Expand All @@ -200,15 +203,16 @@ Example:
return cmd
}

func verifyAuditChain(files []string) (int, map[string]string, error) {
func verifyAuditChain(files []string, allowInitialPrev ...bool) (int, map[string]string, error) {
partialChain := len(allowInitialPrev) > 0 && allowInitialPrev[0]
prevHash := ""
eventCount := 0
hashesByID := map[string]string{}

for _, file := range files {
scanErr := scanAuditEvents(file, func(event audit.Event) error {
eventCount++
if eventCount == 1 && event.PrevHash != "" {
if eventCount == 1 && event.PrevHash != "" && !partialChain {
return fmt.Errorf("audit: CHAIN BROKEN at event %s in file %s: first event has non-empty prev_hash", event.ID, filepath.Base(file))
}
if eventCount > 1 && event.PrevHash != prevHash {
Expand Down
9 changes: 8 additions & 1 deletion cmd/rampart/cli/audit_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ func eventMatchesQuery(event audit.Event, query string) bool {
return false
}

func verifyAnchors(auditDir string, hashesByID map[string]string) error {
func verifyAnchors(auditDir string, hashesByID map[string]string, strictOpt ...bool) error {
strict := true
if len(strictOpt) > 0 {
strict = strictOpt[0]
}
anchors, err := listAnchorFiles(auditDir)
if err != nil {
return err
Expand All @@ -342,6 +346,9 @@ func verifyAnchors(auditDir string, hashesByID map[string]string) error {

hash, ok := hashesByID[anchor.EventID]
if !ok {
if !strict {
continue
}
return fmt.Errorf("audit: CHAIN BROKEN at event %s in file %s: anchor event not found", anchor.EventID, filepath.Base(anchorFile))
}
if hash != anchor.Hash {
Expand Down
40 changes: 40 additions & 0 deletions cmd/rampart/cli/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,46 @@ func TestAuditVerify_ValidChain(t *testing.T) {
assert.Contains(t, stdout, "10 events")
}

func TestAuditVerifySince_AllowsPartialChainAndSkippedAnchor(t *testing.T) {
dir := t.TempDir()

first := makeEvent("exec", "old", "main", "allow", "ok")
first.ID = audit.NewEventID()
first.Timestamp = time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
require.NoError(t, first.ComputeHash())

second := makeEvent("exec", "new", "main", "allow", "ok")
second.ID = audit.NewEventID()
second.Timestamp = time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)
second.PrevHash = first.Hash
require.NoError(t, second.ComputeHash())

writeSingleAuditEvent := func(name string, event audit.Event) {
t.Helper()
data, err := json.Marshal(event)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(dir, name), append(data, '\n'), 0o644))
}
writeSingleAuditEvent("2026-02-08.jsonl", first)
writeSingleAuditEvent("2026-02-09.jsonl", second)

anchor := audit.ChainAnchor{
EventID: first.ID,
Hash: first.Hash,
EventCount: 1,
Timestamp: first.Timestamp,
File: "2026-02-08.jsonl",
}
anchorData, err := json.Marshal(anchor)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(dir, "audit-anchor.json"), anchorData, 0o644))

stdout, _, err := runCLI(t, "audit", "verify", "--audit-dir", dir, "--since", "2026-02-09")
require.NoError(t, err)
assert.Contains(t, stdout, "1 events")
assert.Contains(t, stdout, "no tampering detected")
}

func TestAuditVerify_BrokenChain(t *testing.T) {
dir := t.TempDir()
events := make([]audit.Event, 5)
Expand Down
Loading