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
22 changes: 22 additions & 0 deletions .github/workflows/seal-memory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# C-287 HIVE mesh execution — forward recent OAA journal entries to Civic ledger ingest
name: Seal OAA Memory

on:
schedule:
- cron: "*/10 * * * *"
workflow_dispatch:

jobs:
seal-memory:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Seal recent memory entries
env:
CIVIC_LEDGER_URL: ${{ secrets.CIVIC_LEDGER_URL }}
LEDGER_BASE_URL: ${{ secrets.LEDGER_BASE_URL }}
CIVIC_LEDGER_TOKEN: ${{ secrets.CIVIC_LEDGER_TOKEN }}
LEDGER_ADMIN_TOKEN: ${{ secrets.LEDGER_ADMIN_TOKEN }}
OAA_SEAL_LOOKBACK: "50"
run: node scripts/oaa/seal-memory.mjs
10 changes: 7 additions & 3 deletions docs/mobius_yaml_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ KV / runtime state
|--------|--------|
| `mesh` | Node identity, tier, role, repository, discovery |
| `pulse` | What the node exposes for health, feeds, snapshots; lanes; `authoritative_for`; `emits` flags |
| `ingest` | How the node participates in writes: `mode`, optional `write_url` / `auth` / `accepts`, and/or `targets[]` for client forwarding |
| `ingest` | How the node participates in writes: `mode`, optional `write_url` / `auth` / `accepts`, `targets[]` for client forwarding, and optional **`sources`** (read URLs other nodes use when composing world / pulse) |
| `jobs` | **Declaration only** — maps workflow ids to `.github/workflows/*.yml` plus `trigger` / `cron` (execution is GitHub Actions, not this file) |
| `governance` | Agent PR rules, merge gates, reviewer handles (policy text; enforce in branch protection + CODEOWNERS) |
| `mcp` | MCP server and discovery URLs when the node exposes tools |
| `policy` | Canonical ledger node, hashing, mirroring, local truth |

Expand Down Expand Up @@ -76,6 +78,8 @@ Declare under `ingest.targets[].accepts` or ledger `ingest.accepts` as appropria

## This repository (`OAA-API-Library`)

Root [`mobius.yaml`](../mobius.yaml) declares **OAA** as a **`service_node`**: append-only memory (`/api/oaa/kv`), optional warm **KV bridge** (`/api/kv-bridge/*`), and **client** forwarding of proofs to **`civic-protocol-core`** (`/mesh/ingest`). Relative URLs in `pulse` are resolved against the deployed hub base URL.
Root [`mobius.yaml`](../mobius.yaml) declares **OAA** as a **`service_node`**: append-only memory (`/api/oaa/kv`), latest journal slices (**`GET /api/oaa/kv/latest`** for HIVE / mesh consumers), optional warm **KV bridge** (`/api/kv-bridge/*`), **`ingest.sources`** pointers for cross-node reads, **`jobs`** listing **`seal-memory`** (`.github/workflows/seal-memory.yml`), and **client** forwarding of proofs to **`civic-protocol-core`** (`/mesh/ingest`). The CI script [`scripts/oaa/seal-memory.mjs`](../scripts/oaa/seal-memory.mjs) re-forwards recent `OAA_MEMORY_ENTRY_V1` rows when secrets are present.

For the full cross-repo rollout (Terminal writer + Substrate spec authority + Civic `mobius.yaml`), apply the same schema in those repositories on their own branches.
Relative URLs in `pulse` / `ingest.sources.sovereign_memory` are resolved against the deployed hub base URL.

For the full cross-repo rollout (Substrate `mesh-sync`, Terminal `publish-cycle-state`, HIVE `world-update`, browser-shell renderers), apply the same schema and workflows in those repositories on their own branches.
30 changes: 26 additions & 4 deletions lib/kv/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { OaaMemoryEntry } from "../../types/oaa-kv";
import { canonicalJson } from "../crypto/canonicalJson";
import { sha256Hex } from "../crypto/hmac";

const MEMORY_PATH =
process.env.OAA_MEMORY_PATH || path.join(process.cwd(), "OAA_MEMORY.json");
function getMemoryPath(): string {
return process.env.OAA_MEMORY_PATH || path.join(process.cwd(), "OAA_MEMORY.json");
}

type MemoryFile = {
version?: string;
Expand All @@ -19,7 +20,7 @@ type MemoryFile = {

async function readMemoryFile(): Promise<MemoryFile> {
try {
const raw = await fs.readFile(MEMORY_PATH, "utf8");
const raw = await fs.readFile(getMemoryPath(), "utf8");
return JSON.parse(raw) as MemoryFile;
} catch {
return {
Expand All @@ -36,7 +37,7 @@ async function readMemoryFile(): Promise<MemoryFile> {

async function writeMemoryFile(data: MemoryFile): Promise<void> {
data.updatedAt = new Date().toISOString();
await fs.writeFile(MEMORY_PATH, JSON.stringify(data, null, 2), "utf8");
await fs.writeFile(getMemoryPath(), JSON.stringify(data, null, 2), "utf8");
}

function isOaaMemoryEntry(n: unknown): n is OaaMemoryEntry {
Expand Down Expand Up @@ -100,3 +101,24 @@ export async function getLatestHash(): Promise<string | null> {
}
return null;
}

/** Latest `OAA_MEMORY_ENTRY_V1` per `data.key` (newest by `ts` wins). */
export async function getLatestJournalEntriesByKeys(
keys: string[]
): Promise<Record<string, OaaMemoryEntry | null>> {
const db = await readMemoryFile();
const notes = Array.isArray(db.notes) ? db.notes : [];
const journal = notes.filter(isOaaMemoryEntry).sort((a, b) => b.ts - a.ts);

const out: Record<string, OaaMemoryEntry | null> = {};
for (const k of keys) {
out[k] = null;
}
for (const e of journal) {
const dk = e.data.key;
if (keys.includes(dk) && out[dk] === null) {
out[dk] = e;
}
}
return out;
}
12 changes: 2 additions & 10 deletions lib/ledger/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OaaMemoryEntry } from "../../types/oaa-kv";
import { buildOaaMemoryIngestBody } from "./oaaProof";

const LEDGER_URL =
process.env.CIVIC_LEDGER_URL || process.env.LEDGER_BASE_URL || "";
Expand All @@ -20,15 +21,6 @@ export async function sealToLedger(entry: OaaMemoryEntry): Promise<void> {
Authorization: `Bearer ${LEDGER_TOKEN}`,
"X-MNS-Node": "oaa-api-library"
},
body: JSON.stringify({
type: "OAA_MEMORY_ENTRY_V1",
agent: entry.agent,
cycle: entry.cycle,
key: entry.data.key,
intent: entry.intent ?? null,
hash: entry.hash,
previous_hash: entry.previous_hash,
timestamp: new Date(entry.ts).toISOString()
})
body: JSON.stringify(buildOaaMemoryIngestBody(entry))
});
}
15 changes: 15 additions & 0 deletions lib/ledger/oaaProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { OaaMemoryEntry } from "../../types/oaa-kv";

/** Civic Core /mesh/ingest body for OAA journal entries. */
export function buildOaaMemoryIngestBody(entry: OaaMemoryEntry): Record<string, unknown> {
return {
type: "OAA_MEMORY_ENTRY_V1",
agent: entry.agent,
cycle: entry.cycle,
key: entry.data.key,
intent: entry.intent ?? null,
hash: entry.hash,
previous_hash: entry.previous_hash,
timestamp: new Date(entry.ts).toISOString()
};
}
28 changes: 28 additions & 0 deletions mobius.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ ingest:
enabled: true
mode: "client_of_other_node"

# Read-only sources other nodes use when composing world / pulse (HIVE mesh contract)
sources:
terminal_snapshot:
node_id: "mobius-terminal"
read_url: "https://mobius-civic-ai-terminal.vercel.app/api/terminal/snapshot-lite"
cycle_state:
node_id: "mobius-substrate"
read_url: "https://raw.githubusercontent.com/kaizencycle/Mobius-Substrate/main/ledger/mobius-pulse.json"
sovereign_memory:
node_id: "oaa-api-library"
read_url: "/api/oaa/kv/latest"

targets:
- node_id: "civic-protocol-core"
purpose: "durable_ledger"
Expand All @@ -65,6 +77,22 @@ ingest:
- MOBIUS_PULSE_V1
- EPICON_ENTRY_V1

# Execution fabric: declared jobs (GitHub Actions / cron) — this repo runs seal-memory only
jobs:
enabled: true
workflows:
- id: "seal-memory"
file: ".github/workflows/seal-memory.yml"
trigger: "schedule"
cron: "*/10 * * * *"

governance:
agent_prs_allowed: true
auto_merge_allowed: false
required_reviewers:
- "ZEUS"
- "ATLAS"

mcp:
enabled: false

Expand Down
37 changes: 37 additions & 0 deletions pages/api/oaa/kv/latest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getLatestJournalEntriesByKeys } from "../../../lib/kv/store";

const DEFAULT_KEYS = [
"vault:status",
"mic:readiness",
"heartbeat:terminal",
"GI_STATE",
"SIGNAL_SNAPSHOT"
];

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
return res.status(405).json({ ok: false, error: "Method not allowed" });
}

const raw = req.query.keys;
let keys: string[];
if (typeof raw === "string" && raw.trim()) {
keys = raw
.split(",")
.map((k) => k.trim())
.filter(Boolean);
} else {
keys = [...DEFAULT_KEYS];
}

const entries = await getLatestJournalEntriesByKeys(keys);

return res.status(200).json({
ok: true,
schema: "OAA_KV_LATEST_V1",
keys_requested: keys,
latest: entries,
timestamp: new Date().toISOString()
});
}
97 changes: 97 additions & 0 deletions scripts/oaa/seal-memory.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* C-287 / mesh workflow: forward recent OAA_MEMORY_ENTRY_V1 rows to Civic Core /mesh/ingest.
* Uses OAA_MEMORY.json on disk (CI checkout). Set CIVIC_LEDGER_URL + CIVIC_LEDGER_TOKEN (or LEDGER_*).
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.join(__dirname, "..", "..");
const MEMORY_PATH = process.env.OAA_MEMORY_PATH || path.join(repoRoot, "OAA_MEMORY.json");

const LEDGER_URL = (process.env.CIVIC_LEDGER_URL || process.env.LEDGER_BASE_URL || "").replace(
/\/$/,
""
);
const LEDGER_TOKEN = process.env.CIVIC_LEDGER_TOKEN || process.env.LEDGER_ADMIN_TOKEN || "";
const LOOKBACK = Math.max(1, parseInt(process.env.OAA_SEAL_LOOKBACK || "50", 10) || 50);

function isOaaMemoryEntry(n) {
return (
n &&
typeof n === "object" &&
n.type === "OAA_MEMORY_ENTRY_V1" &&
typeof n.hash === "string" &&
typeof n.ts === "number" &&
n.data &&
typeof n.data === "object" &&
typeof n.data.key === "string"
);
}

function buildBody(entry) {
return {
type: "OAA_MEMORY_ENTRY_V1",
agent: entry.agent,
cycle: entry.cycle,
key: entry.data.key,
intent: entry.intent ?? null,
hash: entry.hash,
previous_hash: entry.previous_hash,
timestamp: new Date(entry.ts).toISOString()
};
}

async function main() {
if (!LEDGER_URL || !LEDGER_TOKEN) {
console.log("[seal-memory] skip: CIVIC_LEDGER_URL/LEDGER_BASE_URL or token not set");
process.exit(0);
}

let mem;
try {
mem = JSON.parse(fs.readFileSync(MEMORY_PATH, "utf8"));
} catch (e) {
console.error("[seal-memory] cannot read memory:", e.message);
process.exit(1);
}

const notes = Array.isArray(mem.notes) ? mem.notes : [];
const journal = notes.filter(isOaaMemoryEntry);
const slice = journal.slice(-LOOKBACK);

let ok = 0;
let fail = 0;
const ingestUrl = `${LEDGER_URL}/mesh/ingest`;

for (const entry of slice) {
try {
const res = await fetch(ingestUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${LEDGER_TOKEN}`,
"X-MNS-Node": "oaa-api-library"
},
body: JSON.stringify(buildBody(entry))
});
if (res.ok) ok += 1;
else {
fail += 1;
const t = await res.text();
console.warn(`[seal-memory] ${entry.hash.slice(0, 8)}… HTTP ${res.status}: ${t.slice(0, 200)}`);
}
} catch (e) {
fail += 1;
console.warn(`[seal-memory] ${entry.hash.slice(0, 8)}…`, e.message);
}
}

console.log(`[seal-memory] forwarded ${ok}/${slice.length} entries (${fail} failed)`);
// Do not fail the workflow on partial ingest errors (ledger may be down); operators use logs.
process.exit(0);
}

main();
61 changes: 61 additions & 0 deletions tests/oaa-kv-latest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getLatestJournalEntriesByKeys } from "../lib/kv/store";

describe("getLatestJournalEntriesByKeys", () => {
let tmp: string;
let prev: string | undefined;

beforeEach(() => {
tmp = path.join(os.tmpdir(), `oaa-memory-${Date.now()}.json`);
prev = process.env.OAA_MEMORY_PATH;
process.env.OAA_MEMORY_PATH = tmp;
});

afterEach(() => {
if (prev === undefined) delete process.env.OAA_MEMORY_PATH;
else process.env.OAA_MEMORY_PATH = prev;
try {
fs.unlinkSync(tmp);
} catch {
/* ignore */
}
});

it("returns newest entry per key", async () => {
const mem = {
version: "v1",
notes: [
{
ts: 100,
note: "old",
tag: "t",
agent: "A",
cycle: "C-1",
data: { key: "k1", value: 1 },
hash: "h1",
previous_hash: null,
type: "OAA_MEMORY_ENTRY_V1"
},
{
ts: 200,
note: "new",
tag: "t",
agent: "A",
cycle: "C-1",
data: { key: "k1", value: 2 },
hash: "h2",
previous_hash: "h1",
type: "OAA_MEMORY_ENTRY_V1"
}
]
};
fs.writeFileSync(tmp, JSON.stringify(mem), "utf8");

const r = await getLatestJournalEntriesByKeys(["k1", "missing"]);
expect(r.k1?.data.value).toBe(2);
expect(r.missing).toBeNull();
});
});
Loading