From ec9d3d5a4e78742591c5742ffebd7e74a4700ccc Mon Sep 17 00:00:00 2001 From: shadowmo Date: Tue, 17 Feb 2026 21:34:16 -0600 Subject: [PATCH 1/6] asdf --- REVIEW.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 REVIEW.md diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..4e00ab7 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,39 @@ +Cognition-OS — Review & Onboarding Report + +Summary +- Purpose: spec + reference design for a "cognitive kernel" (deterministic control plane) and userland services that schedule probabilistic workers. Focus: capability-based security, provenance, deterministic routing. +- Main components: docs/ (design + RFCs), spec/ (contracts), kernel/ (supervisor stub in Go), runtime/ (placeholder services), linux/ & freebsd/ mappings, examples/ for flows. + +Sanity checks +- Repo is largely design/spec. A runnable Go stub exists at kernel/supervisor-go/main.go and builds with module present (go.mod included). +- README accurately describes intent and recommended reading order. +- No CI, no tests, no dependency vulnerability tooling detected. Build steps not documented for the Go stub. + +Top actionable issues (priority order) +1) Missing CI (tests/build): no GitHub/Gitea CI config found. Add pipeline to `go build ./...` for kernel/supervisor-go. (kernel/supervisor-go/main.go) +2) No CONTRIBUTING checklist for local dev build/test of Go stub — add build/run steps. (CONTRIBUTING.md) +3) No unit/integration tests: kernel/supervisor-go has logic (hash, ledger append) that should be covered by tests. Add tests for computeHash, Append, and ledger integrity. (kernel/supervisor-go/*.go) +4) Logger / error handling: Append() returns errors but main ignores them; surface errors to caller and fail-fast during startup. (kernel/supervisor-go/main.go, Append call sites) +5) Use of map[string]any canonicalization: current canonicalJSON is a best-effort; document risks and add tests for deterministic hashing across Go versions. (kernel/supervisor-go/canonicalJSON) +6) File permissions for ledger files: open with 0644; consider user/umask and possible sensitive data (use 0600 or configurable). (kernel/supervisor-go/Ledger.Append) +7) Missing LICENSE clarity for contribution process — LICENSE exists but recommend adding contributor CLA or short note in CONTRIBUTING.md. (CONTRIBUTING.md) +8) Empty/runtime placeholders under runtime/ — mark TODOs and minimal interface docs so contributors know the intended contracts. (runtime/*) +9) .DS_Store remnants exist under freebsd/prototypes — remove these artifacts. (freebsd/prototypes/.DS_Store) +10) Lack of automated formatting/linting configuration (gofmt, go vet) — add Makefile/CI step. (repo root) + +Recommended first 3 tasks for a new contributor +1) Add CI build for Go stub (effort: 1–2 hours) + - Create simple pipeline that runs `go test ./...` and `go build ./kernel/supervisor-go`. +2) Write unit tests for computeHash & Ledger.Append (effort: 4–6 hours) + - Test deterministic outputs for canonical inputs and ledger append behavior. Include temp files and cleanup. +3) Document local dev steps (effort: 1 hour) + - Update CONTRIBUTING.md with build/run/test commands, Go version, and how to run the supervisor stub. + +Quick wins (low effort) +- Remove .DS_Store, add .gitattributes and .editorconfig, add Makefile with build/test targets. + +Next steps I can take +- Create branch work/review, commit this REVIEW.md, push branch, and open a merge request draft. (ready to run) +- Optionally add a basic GitLab/Gitea CI config for Go build/test. + +Prepared by: lab-code-heavy (automated review) — tell me to push this review to work/review and open the MR. From 9eb6b6506d2e4550af3e97623b421eaf628ca060 Mon Sep 17 00:00:00 2001 From: lab-code-heavy Date: Mon, 23 Feb 2026 21:08:47 -0600 Subject: [PATCH 2/6] CI: add Gitea-compatible workflow wrapper --- .gitea/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..13e370a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI +on: + push: + branches: [ main, work/review ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.20' + - name: Build + run: make build + - name: Test + run: make test From d4e3b37e912aadb975148c7cd6247835477a5db1 Mon Sep 17 00:00:00 2001 From: lab-code-heavy Date: Mon, 23 Feb 2026 21:10:03 -0600 Subject: [PATCH 3/6] CI: improve workflow (checkout v3, setup-go v4, cache, fmt/vet) --- .gitea/workflows/ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 13e370a..ff4f37a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,11 +7,27 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: '1.20' + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Tidy modules + run: go mod tidy + - name: Format check + run: | + gofmt_out=$(gofmt -l . | wc -l); if [ "$gofmt_out" -ne "0" ]; then echo "gofmt needs to be run"; gofmt -w .; exit 1; fi + - name: Vet + run: go vet ./... - name: Build run: make build - name: Test From bca500623b3c6b08802ab105d4458238b3d58f59 Mon Sep 17 00:00:00 2001 From: lab-code-heavy Date: Mon, 23 Feb 2026 21:10:35 -0600 Subject: [PATCH 4/6] CI: add coverage, static analysis, cross-builds --- .gitea/workflows/ci.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ff4f37a..5e611c7 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,16 +2,21 @@ name: CI on: push: branches: [ main, work/review ] + jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.20'] + os: ['ubuntu-latest'] steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: ${{ matrix.go }} - name: Cache Go modules uses: actions/cache@v4 with: @@ -28,7 +33,26 @@ jobs: gofmt_out=$(gofmt -l . | wc -l); if [ "$gofmt_out" -ne "0" ]; then echo "gofmt needs to be run"; gofmt -w .; exit 1; fi - name: Vet run: go vet ./... - - name: Build + - name: Static analysis (golangci-lint) + uses: golangci/golangci-lint-action@v4 + with: + version: 'v1.59.0' + - name: Build (amd64) run: make build + - name: Cross build (linux/arm64) + run: | + GOOS=linux GOARCH=arm64 go build -o bin/arm64 ./... - name: Test run: make test + - name: Coverage + run: | + go test -coverprofile=coverage.out ./... || true + go tool cover -func=coverage.out | sed -n '1,200p' + + release-checks: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v3 + - name: Echo OK + run: echo "build and tests passed" From 6f2a1aa4b1e6df534c255bdc1d4e799d7b940443 Mon Sep 17 00:00:00 2001 From: shadowmo Date: Mon, 23 Feb 2026 21:28:28 -0600 Subject: [PATCH 5/6] supervisor-go: add ledger package with Append/Read APIs, tests; refactor CLI to use package; update README --- kernel/supervisor-go/README.md | 28 +++- kernel/supervisor-go/ledger/ledger.go | 144 +++++++++++++++++++++ kernel/supervisor-go/ledger/ledger_test.go | 56 ++++++++ kernel/supervisor-go/main.go | 129 ++---------------- 4 files changed, 235 insertions(+), 122 deletions(-) create mode 100644 kernel/supervisor-go/ledger/ledger.go create mode 100644 kernel/supervisor-go/ledger/ledger_test.go diff --git a/kernel/supervisor-go/README.md b/kernel/supervisor-go/README.md index 1443ca7..11dc7a3 100644 --- a/kernel/supervisor-go/README.md +++ b/kernel/supervisor-go/README.md @@ -3,14 +3,30 @@ Runnable stub demonstrating CognitiveOS control-plane concepts without an actual LLM. -## Run +This directory now contains a small ledger package (ledger/) with Append +and Read APIs and unit tests, plus a tiny CLI demo (main.go) that +appends route/session/output events to a JSONL ledger. -go run . -ledger ./ledger.jsonl +## Run (demo) -Then type requests and observe: - deterministic route decision - -session_spawn + child_output_received - final_output_emitted +Build or run the CLI: + + go run . -ledger ./ledger.jsonl + +Then type requests and observe the deterministic route decision, +spawn/output events, and final output. The ledger file will be created +as JSONL at the path you provide. + +## Tests + +Run the unit tests for the ledger package: + + go test ./ledger + +The tests create a temporary ledger file, append events, and validate +read-back and hash chaining. ## Notes -- This is intentionally minimal and deterministic. -- Replace `childWorkerStub` with real model calls later. +- This is intentionally minimal and deterministic. +- Replace `childWorkerStub` with real model calls later. diff --git a/kernel/supervisor-go/ledger/ledger.go b/kernel/supervisor-go/ledger/ledger.go new file mode 100644 index 0000000..487f800 --- /dev/null +++ b/kernel/supervisor-go/ledger/ledger.go @@ -0,0 +1,144 @@ +package ledger + +import ( + "bufio" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "sort" + "time" +) + +// Event is a single ledger entry stored as JSON per-line (JSONL). +type Event struct { + ID string `json:"id"` + TS string `json:"ts"` + StreamID string `json:"stream_id"` + Type string `json:"type"` + Body map[string]any `json:"body"` + PrevHash *string `json:"prev_hash"` + Hash string `json:"hash"` +} + +// Ledger manages append-only JSONL ledger for a single stream. +// It is intentionally minimal and safe for the demo (not highly concurrent). +type Ledger struct { + Path string + StreamID string + LastHash *string +} + +// New returns a Ledger instance. It does not create the file yet. +func New(path, streamID string) *Ledger { + return &Ledger{Path: path, StreamID: streamID, LastHash: nil} +} + +func sha256Hex(b []byte) string { + h := sha256.Sum256(b) + return hex.EncodeToString(h[:]) +} + +func computeHash(id, ts, stream, typ string, body map[string]any, prev *string) (string, error) { + env := map[string]any{ + "id": id, + "ts": ts, + "stream_id": stream, + "type": typ, + "body": body, + "prev_hash": prev, + } + // Best-effort deterministic ordering for body keys. + if body != nil { + keys := make([]string, 0, len(body)) + for k := range body { + keys = append(keys, k) + } + sort.Strings(keys) + ordered := make(map[string]any, len(body)) + for _, k := range keys { + ordered[k] = body[k] + } + env["body"] = ordered + } + b, err := json.Marshal(env) + if err != nil { + return "", err + } + return sha256Hex(b), nil +} + +// Append writes a new event to the ledger file and updates LastHash. +func (l *Ledger) Append(typ string, body map[string]any) (*Event, error) { + if l == nil { + return nil, errors.New("nil ledger") + } + id := fmt.Sprintf("%d", time.Now().UnixNano()) + ts := time.Now().UTC().Format(time.RFC3339Nano) + h, err := computeHash(id, ts, l.StreamID, typ, body, l.LastHash) + if err != nil { + return nil, err + } + ev := &Event{ + ID: id, + TS: ts, + StreamID: l.StreamID, + Type: typ, + Body: body, + PrevHash: l.LastHash, + Hash: h, + } + f, err := os.OpenFile(l.Path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return nil, err + } + defer f.Close() + enc := json.NewEncoder(f) + if err := enc.Encode(ev); err != nil { + return nil, err + } + l.LastHash = &ev.Hash + return ev, nil +} + +// ReadAll reads all events from the ledger file. If the file does not exist, returns empty slice. +func (l *Ledger) ReadAll() ([]*Event, error) { + f, err := os.Open(l.Path) + if err != nil { + if os.IsNotExist(err) { + return []*Event{}, nil + } + return nil, err + } + defer f.Close() + var out []*Event + sc := bufio.NewScanner(f) + for sc.Scan() { + line := sc.Bytes() + var ev Event + if err := json.Unmarshal(line, &ev); err != nil { + return nil, err + } + out = append(out, &ev) + } + if err := sc.Err(); err != nil { + return nil, err + } + return out, nil +} + +// LoadLastHash sets the ledger LastHash to the last entry in the file (if any). +func (l *Ledger) LoadLastHash() error { + evs, err := l.ReadAll() + if err != nil { + return err + } + if len(evs) == 0 { + l.LastHash = nil + return nil + } + l.LastHash = &evs[len(evs)-1].Hash + return nil +} diff --git a/kernel/supervisor-go/ledger/ledger_test.go b/kernel/supervisor-go/ledger/ledger_test.go new file mode 100644 index 0000000..a0a40a2 --- /dev/null +++ b/kernel/supervisor-go/ledger/ledger_test.go @@ -0,0 +1,56 @@ +package ledger + +import ( + "os" + "testing" +) + +func TestAppendAndRead(t *testing.T) { + f, err := os.CreateTemp("", "ledger-test-*.jsonl") + if err != nil { + t.Fatalf("tempfile: %v", err) + } + path := f.Name() + f.Close() + defer os.Remove(path) + + l := New(path, "stream-1") + // ensure empty read + evs, err := l.ReadAll() + if err != nil { + t.Fatalf("read empty: %v", err) + } + if len(evs) != 0 { + t.Fatalf("expected 0 events, got %d", len(evs)) + } + + e1, err := l.Append("route_decision", map[string]any{"tier": "fast"}) + if err != nil { + t.Fatalf("append1: %v", err) + } + if l.LastHash == nil || *l.LastHash != e1.Hash { + t.Fatalf("lasthash not set after append1") + } + + e2, err := l.Append("final_output_emitted", map[string]any{"result": "ok"}) + if err != nil { + t.Fatalf("append2: %v", err) + } + if l.LastHash == nil || *l.LastHash != e2.Hash { + t.Fatalf("lasthash not set after append2") + } + if e2.PrevHash == nil || *e2.PrevHash != e1.Hash { + t.Fatalf("prevhash chain broken") + } + + evs, err = l.ReadAll() + if err != nil { + t.Fatalf("read: %v", err) + } + if len(evs) != 2 { + t.Fatalf("expected 2 events, got %d", len(evs)) + } + if evs[0].Hash != e1.Hash || evs[1].Hash != e2.Hash { + t.Fatalf("readback hashes mismatch") + } +} diff --git a/kernel/supervisor-go/main.go b/kernel/supervisor-go/main.go index 4dfa28a..1c2f9af 100644 --- a/kernel/supervisor-go/main.go +++ b/kernel/supervisor-go/main.go @@ -4,121 +4,20 @@ import ( "bufio" "crypto/sha256" "encoding/hex" - "encoding/json" "flag" "fmt" "os" - "sort" "strings" "time" + + "cognitiveos/supervisor/ledger" ) -// This is a runnable *stub* for the deterministic control plane. -// It does NOT call real LLMs. Instead it demonstrates: -// - deterministic routing (fast/default/heavy) -// - append-only, hash-chained ledger per "stream" -// - must-delegate enforcement: router cannot emit without a spawn+child output proof - -type Event struct { - ID string `json:"id"` - TS string `json:"ts"` - StreamID string `json:"stream_id"` - Type string `json:"type"` - Body map[string]any `json:"body"` - PrevHash *string `json:"prev_hash"` - Hash string `json:"hash"` -} - -func canonicalJSON(v any) ([]byte, error) { - // Simple canonicalization: marshal with sorted keys for map[string]any at top-level envelope. - // For a real system, use a stricter canonical JSON spec. - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - return b, nil -} - func sha256Hex(b []byte) string { h := sha256.Sum256(b) return hex.EncodeToString(h[:]) } -func newID() string { - // Not cryptographically strong; good enough for a stub. - return fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid()) -} - -func computeHash(id, ts, stream, typ string, body map[string]any, prev *string) (string, error) { - env := map[string]any{ - "id": id, - "ts": ts, - "stream_id": stream, - "type": typ, - "body": body, - "prev_hash": prev, - } - // Sort body keys deterministically (best effort). - if body != nil { - keys := make([]string, 0, len(body)) - for k := range body { - keys = append(keys, k) - } - sort.Strings(keys) - ordered := make(map[string]any, len(body)) - for _, k := range keys { - ordered[k] = body[k] - } - env["body"] = ordered - } - b, err := canonicalJSON(env) - if err != nil { - return "", err - } - return sha256Hex(b), nil -} - -type Ledger struct { - Path string - StreamID string - LastHash *string -} - -func (l *Ledger) Append(typ string, body map[string]any) (*Event, error) { - id := newID() - ts := time.Now().UTC().Format(time.RFC3339Nano) - h, err := computeHash(id, ts, l.StreamID, typ, body, l.LastHash) - if err != nil { - return nil, err - } - ev := &Event{ - ID: id, - TS: ts, - StreamID: l.StreamID, - Type: typ, - Body: body, - PrevHash: l.LastHash, - Hash: h, - } - f, err := os.OpenFile(l.Path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) - if err != nil { - return nil, err - } - defer f.Close() - - enc := json.NewEncoder(f) - if err := enc.Encode(ev); err != nil { - return nil, err - } - l.LastHash = &ev.Hash - return ev, nil -} - -// Deterministic routing rules (reference): -// - explicit @fast/@heavy -// - keywords: one-liner, regex, jq, bash, quick => FAST -// - multi-file/architecture/restructure/rewrite => HEAVY -// - else DEFAULT func decideRoute(req string) (tier string, agent string, reason string) { l := strings.ToLower(req) if strings.Contains(l, "@fast") { @@ -141,7 +40,7 @@ func decideRoute(req string) (tier string, agent string, reason string) { } func childWorkerStub(agent string, req string) string { - // Placeholder for actual specialist call. This returns deterministic output for demo. + // Deterministic placeholder for demo. return fmt.Sprintf("[stub:%s] Received request (%d chars).", agent, len(req)) } @@ -150,11 +49,12 @@ func main() { flag.Parse() routerStream := "router-session-1" - ledger := &Ledger{Path: *ledgerPath, StreamID: routerStream, LastHash: nil} + l := ledger.New(*ledgerPath, routerStream) + // initialize last-hash from existing file if present + _ = l.LoadLastHash() - fmt.Println("CognitiveOS Supervisor (stub). Type a request, then press Enter. Ctrl-D to exit.") + fmt.Println("CognitiveOS Supervisor (demo). Type a request, then press Enter. Ctrl-D to exit.") sc := bufio.NewScanner(os.Stdin) - for sc.Scan() { req := sc.Text() if strings.TrimSpace(req) == "" { @@ -162,35 +62,32 @@ func main() { } tier, agent, reason := decideRoute(req) - _, _ = ledger.Append("route_decision", map[string]any{ + _, _ = l.Append("route_decision", map[string]any{ "tier": tier, "target_agent": agent, "reason": reason, }) - // session_spawn (must-delegate proof step) - childSession := "child-" + newID() + childSession := "child-" + fmt.Sprintf("%d", time.Now().UnixNano()) payloadHash := sha256Hex([]byte(req)) - _, _ = ledger.Append("session_spawn", map[string]any{ + _, _ = l.Append("session_spawn", map[string]any{ "child_session": childSession, "child_agent": agent, "payload_hash": payloadHash, }) - // child_output_received childOut := childWorkerStub(agent, req) outHash := sha256Hex([]byte(childOut)) - _, _ = ledger.Append("child_output_received", map[string]any{ + _, _ = l.Append("child_output_received", map[string]any{ "child_session": childSession, "output_hash": outHash, "output_len": len(childOut), }) - // emit_guarded (enforced) preamble := fmt.Sprintf("Delegating to %s because %s.", agent, reason) final := preamble + "\n" + childOut - _, _ = ledger.Append("final_output_emitted", map[string]any{ - "proof_ref": "proof-" + newID(), + _, _ = l.Append("final_output_emitted", map[string]any{ + "proof_ref": "proof-" + fmt.Sprintf("%d", time.Now().UnixNano()), "output_hash": sha256Hex([]byte(final)), }) From a24aa1623783eaade66412ddf02e34635f6501a1 Mon Sep 17 00:00:00 2001 From: shadowmo Date: Sat, 28 Feb 2026 00:06:32 -0600 Subject: [PATCH 6/6] feat(ledger): add file-backed append-only Event Ledger prototype (Go) with verify --- runtime/ledger/ledger.go | 124 ++++++++++++++++++++++++++++++++++ runtime/ledger/ledger_test.go | 18 +++++ 2 files changed, 142 insertions(+) create mode 100644 runtime/ledger/ledger.go create mode 100644 runtime/ledger/ledger_test.go diff --git a/runtime/ledger/ledger.go b/runtime/ledger/ledger.go new file mode 100644 index 0000000..4c00752 --- /dev/null +++ b/runtime/ledger/ledger.go @@ -0,0 +1,124 @@ +package ledger + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "time" +) + +type Entry struct { + Timestamp int64 `json:"timestamp"` + Type string `json:"type"` + Payload json.RawMessage `json:"payload"` + PrevHash string `json:"prev_hash"` + Hash string `json:"hash"` +} + +type Ledger struct { + path string + file *os.File + last string +} + +func sha256Hex(b []byte) string { + h := sha256.Sum256(b) + return hex.EncodeToString(h[:]) +} + +func Open(path string) (*Ledger, error) { + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600) + if err != nil { + return nil, err + } + l := &Ledger{path: path, file: f} + if err := l.loadLastHash(); err != nil { + return nil, err + } + return l, nil +} + +func (l *Ledger) loadLastHash() error { + // read file line by line, keep last hash + f, err := os.Open(l.path) + if err != nil { + return err + } + defer f.Close() + dec := json.NewDecoder(f) + var last string + for { + var e Entry + if err := dec.Decode(&e); err != nil { + break + } + last = e.Hash + } + l.last = last + return nil +} + +func (l *Ledger) Append(eventType string, payload interface{}) (string, error) { + pbytes, err := json.Marshal(payload) + if err != nil { + return "", err + } + e := Entry{ + Timestamp: time.Now().UnixNano(), + Type: eventType, + Payload: json.RawMessage(pbytes), + PrevHash: l.last, + } + // compute hash + hb, _ := json.Marshal(struct { + Timestamp int64 `json:"timestamp"` + Type string `json:"type"` + Payload json.RawMessage `json:"payload"` + PrevHash string `json:"prev_hash"` + }{e.Timestamp, e.Type, e.Payload, e.PrevHash}) + e.Hash = sha256Hex(hb) + enc, err := json.Marshal(e) + if err != nil { + return "", err + } + if _, err := l.file.Write(append(enc, '\n')); err != nil { + return "", err + } + l.last = e.Hash + return e.Hash, nil +} + +func (l *Ledger) Verify() error { + f, err := os.Open(l.path) + if err != nil { + return err + } + defer f.Close() + dec := json.NewDecoder(f) + var prev string + for { + var e Entry + if err := dec.Decode(&e); err != nil { + break + } + if e.PrevHash != prev { + return fmt.Errorf("broken chain: prev %s != entry.PrevHash %s", prev, e.PrevHash) + } + // recompute + hb, _ := json.Marshal(struct { + Timestamp int64 `json:"timestamp"` + Type string `json:"type"` + Payload json.RawMessage `json:"payload"` + PrevHash string `json:"prev_hash"` + }{e.Timestamp, e.Type, e.Payload, e.PrevHash}) + if sha256Hex(hb) != e.Hash { + return fmt.Errorf("hash mismatch for entry at %d", e.Timestamp) + } + prev = e.Hash + } + return nil +} + +func (l *Ledger) Close() error { return l.file.Close() } diff --git a/runtime/ledger/ledger_test.go b/runtime/ledger/ledger_test.go new file mode 100644 index 0000000..c562ab4 --- /dev/null +++ b/runtime/ledger/ledger_test.go @@ -0,0 +1,18 @@ +package ledger + +import ( + "os" + "testing" +) + +func TestAppendAndVerify(t *testing.T) { + p := "test_ledger.jsonl" + os.Remove(p) + l, err := Open(p) + if err != nil { t.Fatal(err) } + defer os.Remove(p) + defer l.Close() + if _, err := l.Append("test_event", map[string]string{"k":"v"}); err != nil { t.Fatal(err) } + if _, err := l.Append("test_event2", map[string]string{"a":"b"}); err != nil { t.Fatal(err) } + if err := l.Verify(); err != nil { t.Fatal(err) } +}