Skip to content

Add orynq-ai-auditability community ability (supersedes #249)#257

Closed
realdecimalist wants to merge 4 commits intoopenhome-dev:devfrom
realdecimalist:orynq-passive-daemon
Closed

Add orynq-ai-auditability community ability (supersedes #249)#257
realdecimalist wants to merge 4 commits intoopenhome-dev:devfrom
realdecimalist:orynq-passive-daemon

Conversation

@realdecimalist
Copy link
Copy Markdown

@realdecimalist realdecimalist commented Apr 24, 2026

Summary

New community ability: Orynq — Passive AI Auditability. Pure Background Daemon that hashes every conversation turn into a SHA-256 rolling chain and auto-anchors to Materios + Cardano with zero user action. Supersedes #249 (closed with CHANGES_REQUESTED, never reopened).

End-to-end proven on Preprod v5 — 2026-04-24

Deployed to OpenHome agent 578906 as ability id 3789, category background_daemon. Full cycle (capture → upload → on-chain receipt → cert-daemon attestation) confirmed twice during a single voice session.

Verify the receipts live: fluxpointstudios.com/materios/explorer#reciepts — paste any receipt ID or content hash below to view on Preprod v5.

Session-open anchor

  • Content hash: 0xc7a028f035145908df39c5fcee837b8acebdd40288f063764c204ec3237de359
  • Receipt ID: 0xa95eb1d2875d81dddf573352974373e2b6df306d2fc43eed4bfa3c8a60778782
  • Availability cert hash: 0xbb5f1a1c11f0231c394c64b0bc6ac70cb25234e472bcf2fe460119b087c42d72

Weather Q&A anchor

  • Content hash: 0x2bfad36760aaa5992b658df86bed6217c020cf16d5dde719c03d2b5f8eaf16a7
  • Receipt ID: 0xa560d10cc433223d112439517243426863e6e503f107bbda39d191a10b8df6e7
  • Availability cert hash: 0x7beff7f944bd937b077968dcd858c1e2bc5bb648234fb3f01664077568266ff2

Submitter: 15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5 (Materios Preprod v5 sponsored operator). Zero user action, zero signing key on the device.

What changed beyond #249

Design

  • Pure Background Daemon (was Interactive Combined). No hotwords. main.py is a non-functional stub required only by the CLI validator's REQUIRED_FILES check; all capture + anchoring runs silently in background.py.
  • Auto-anchor gating: after every poll that grew the chain, four gates evaluated in order — no_growth / rate_limit (60s) / backoff (10min after 3 fails) / api_key. Fails open to local-only.
  • Sponsored-receipt flow: gateway fires fire-and-forget callback to a sponsored submitter — no device-side signing key.
  • Capture coverage expanded to any non-empty role + text OR structured dict/list content, canonicalised with sort_keys+compact separators. Regression: tool_call dicts and capability invocations were previously dropped.

Sandbox + deploy-cycle hardening (learned on real hardware)

  • Removed getattr (forbidden builtin) in favour of hasattr + direct access.
  • Renamed journal from orynq_audit_chain.json.tmp to orynq_audit_chain_tmp.json (SDK silently drops .tmp extensions).
  • Dropped mode="w" on write_file (silently dropped on real DevKit; default "a+" works).
  • Bounded-retry journal verify (5 attempts, 50→100→200→400ms) for the write/read eventual-consistency window.
  • Static templated confirmation strings whenever speak() carries structured data — text_to_text_response() is not safe for factual confirmations (LLM safety layer refuses with "I can't confirm specific transactions…").
  • Auth routing helper: Authorization: Bearer when token is matra_-prefixed, else legacy x-api-key.
  • Gateway URL confirmed as verified double-prefix /preprod-blobs/blobs/{hash}/… (nginx strips the outer prefix; backend expects /blobs/{hash}/…).

Verification + diagnostics

  • _verify_chain(chain) now lives in background.py as a pure helper (was in main.py). Locally test-covered for content-hash tamper, previous-hash tamper, direct chain-hash tamper, partial-verify-after-compaction.
  • Per-poll verbose logs for diagnosability:
    [OrynqAudit] poll: hist_len=N seen_prev=M new_msgs=K added=X chain_len=Y
    [OrynqAudit] anchor skip: no_growth|rate_limit|backoff|no_api_key (…)
    [OrynqAudit] anchor start: chain_len=N last_len=M
    [OrynqAudit] upload OK hash=<64-hex>
    

uzair401's review (#249) — status

All six axes of the voice-UX audit prompt are now addressed by the passive refactor:

  1. Hardcoded string matching — no string matching remains (no user-input routing path).
  2. LLM classifier prompt examples — no classifier in the daemon path; retired with the interactive flow.
  3. Exit / confirmation word lists — none; no conversational surface.
  4. Voice output problems — daemon never calls speak(). Silent by design.
  5. Response length — n/a (no speak path).
  6. Menu-driven flow — eliminated entirely.

Test plan

  • Repo validator passes: python3 validate_ability.py community/orynq-ai-auditability/
  • Deploy via openhome-cli deploy --category background_daemon attaches cleanly to a test personality
  • On session start, [OrynqAudit] daemon started appears in logs; subsequent polls log hist_len=… new_msgs=… added=…
  • With no Bearer token set, daemon runs silently in local-only mode (anchor-skip: no_api_key)
  • With Bearer token in iOS Settings → API Keys → materios_gateway_api_key, first new-turn poll triggers an upload and a receipt lands on chain within ~6s

Known follow-ups (not blocking)

  • background.py uses sync requests.post()/put() inside an async loop — should migrate to aiohttp / asyncio.to_thread() for worst-case event-loop starvation. Not urgent; uploads typically 1–2s and rate-limit caps at 1/min.
  • OpenHome CLI validator requires main.py + at least one --triggers string even for --category background_daemon. Stub + dummy-trigger-which-the-server-clears works but is a platform contradiction worth a separate upstream issue.

🤖 Generated with Claude Code

github-actions Bot and others added 2 commits April 21, 2026 05:10
…o-end on-chain certification proven

Supersedes openhome-dev#249 (which remains closed, CHANGES_REQUESTED). Major
architectural and sandbox-compatibility improvements validated end-to-end
on Preprod v5 on 2026-04-24.

Design changes
--------------
- Pure Background Daemon category. No matching_hotwords; main.py is a
  non-functional stub required only by the CLI validator's REQUIRED_FILES
  check. All capture + anchoring runs silently in background.py.
- Auto-anchor: after every poll that grew the chain, _maybe_anchor_auto()
  evaluates four gates (no_growth / rate_limit / backoff / api_key) and
  uploads the canonical v2 envelope to the Materios gateway. Rate-limited
  to once per minute; fails open to local-only mode if no Bearer token
  is configured. User takes no action after one-time token setup.
- Sponsored-receipt flow: gateway fires fire-and-forget callback to the
  sponsored submitter, which signs submit_receipt_v2 on-chain. No
  signing key on the device.
- Capture coverage expanded to ANY non-empty message (user / assistant /
  tool / function roles, text OR structured dict/list content) via
  canonical JSON (sort_keys, compact separators). Regression: previously
  tool_call dicts and capability invocations were silently dropped.

Sandbox + deploy-cycle hardening
--------------------------------
- Removed 'getattr' (forbidden builtin) in favour of hasattr + direct access.
- Renamed journal file from orynq_audit_chain.json.tmp to
  orynq_audit_chain_tmp.json (the SDK silently drops .tmp extensions).
- Omitted mode='w' on write_file (silently dropped on real hardware;
  default 'a+' works).
- Bounded-retry journal verify: 5 attempts with exponential backoff
  (50->100->200->400ms) accommodates the write/read eventual-consistency
  window; a single read returns empty.
- Static templated confirmation strings in any speak() path that carries
  structured data (hashes, receipt IDs). text_to_text_response() is not
  safe for factual confirmations — the LLM safety layer refuses.
- Auth routing helper selects Authorization: Bearer when the key is
  prefixed matra_, else falls back to legacy x-api-key.
- Gateway URL uses the verified double-prefix /preprod-blobs/blobs/{hash}/...
  (nginx strips the outer prefix; backend routes expect /blobs/{hash}/...).

Verification + diagnostics
--------------------------
- _verify_chain(chain) ported from main.py into background.py as a pure
  helper. Tests (kept out of the PR, local-dev only) cover content-hash
  tamper, previous-hash tamper, direct chain-hash tamper,
  partial-verify-after-compaction, and empty-chain success.
- Per-poll verbose logs added for diagnosability:
  [OrynqAudit] poll: hist_len=N seen_prev=M new_msgs=K added=X chain_len=Y
  [OrynqAudit] anchor skip: no_growth|rate_limit|backoff|no_api_key (...)
  [OrynqAudit] anchor start: chain_len=N last_len=M
  [OrynqAudit] upload OK hash=<64-hex>
  [OrynqAudit] history shrunk, resetting pointer

End-to-end proof 2026-04-24
---------------------------
Deployed as OpenHome ability 3789 (Background Daemon, agent 578906).
Two full cycles captured during a single voice session, both certified:

- Session open — content 0xc7a028f0... -> receipt 0xa95eb1d2...8782,
  cert 0xbb5f1a1c...
- Weather Q&A  — content 0x2bfad367... -> receipt 0xa560d10c...df6e7,
  cert 0x7beff7f944bd937b...

Submitter 15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5 (Materios
Preprod v5 sponsored operator).

Known follow-ups
----------------
- background.py uses requests.post()/put() synchronously from an async
  path; should migrate to aiohttp or asyncio.to_thread() to avoid
  worst-case event-loop starvation on slow networks. Not urgent —
  uploads typically complete ~1-2s and rate-limit caps to 1/min.
- OpenHome CLI validator still insists on main.py + at least one
  --triggers string even for --category background_daemon. The stub
  main.py + dummy-trigger-which-the-server-clears workaround is a
  platform contradiction worth a separate upstream issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@realdecimalist realdecimalist requested a review from a team as a code owner April 24, 2026 17:08
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

🔀 Branch Merge Check

PR direction: orynq-passive-daemondev

Passedorynq-passive-daemondev is a valid merge direction

@github-actions github-actions Bot added the community-ability Community-contributed ability label Apr 24, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

✅ Community PR Path Check — Passed

All changed files are inside the community/ folder. Looks good!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

✅ Ability Validation Passed

📋 Validating: community/orynq-ai-auditability
  ✅ All checks passed!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

🔍 Lint Results

__init__.py — Empty as expected

Files linted: community/orynq-ai-auditability/background.py community/orynq-ai-auditability/main.py

✅ Flake8 — Passed

✅ All checks passed!

…ostics — verified end-to-end on DevKit voice

Since the initial PR commit, live testing on a DevKit revealed three
hardening issues. Fixed in this commit, confirmed in a real voice
session 2026-04-24 with the Background Daemon category:

1. Cross-session state corruption — `last_seen_index` was user-scoped
   but indexed session-scoped `get_full_message_history()`. Each new
   session reset the pointer via the "history shrunk" branch and
   nothing was captured. Replaced with `session_pointers: {session_id:
   int}` keyed on `worker.session_id` (fallback to synthesized id if
   unexposed). State bumped to `state_version: 2` with tolerant
   migration from the old schema.

2. `call(worker, background_daemon_mode)` lacked a default on the
   second arg. For category=skill invocations the platform calls
   `call(worker)` and hits TypeError, preventing the daemon from
   starting. Added `background_daemon_mode: bool = False` so the
   signature works for both skill and background_daemon paths.

3. No observability when the daemon silently fails to load. Added:
   - Step-by-step `[OrynqAudit] call() STEP 1-5` logs in the entry
     point, wrapped per-step in try/except so a failure before log openhome-dev#1
     still surfaces (to the extent logging is reachable)
   - `[OrynqAudit] watch_loop STEP A-D` logs at the start of the
     coroutine including resolved session_id + fallback source
   - `orynq_audit_heartbeat.json` written on step C — its existence in
     user-data file storage is a persistent non-log signal that
     `call()` fired + watch_loop started
   - Per-message `extend[i/total]: ADD/skip role=<x> reason=<r>`
     logging with skip counters and content-hash prefix
   - Per-poll `poll#N: hist_len=M prev_seen=P shrunk=<bool> …` dump
   - Per-gate anchor-skip reasons (`no_growth|rate_limit|backoff|
     no_api_key`) and an `anchor start` line before uploads

End-to-end verification (2026-04-24, OpenHome agent 578906, DevKit):
- BD ability auto-loaded on session init (bd_mode=True), no hotword
- 47-turn conversation captured on first poll (chain 0 → 47)
- Forward-journal save succeeded (17522-byte .tmp verified)
- Auto-anchor uploaded content_hash 0x3b83d1e816494b0161015880dd17bb56
  5a463a8c3b2168614fb502c4aaee39a4 (chain_len=335, 103596 bytes)
- On-chain receipt 0x463c3b1c730b6ef7821c45a70139bb6faf1b98b303d29b00b
  983195b715efb32 landed in block ~22:00:06 UTC, cert-daemon attested
  within ~4s (certHash 0x75fe3b4bbb8d64bb361c3a60e4145554e8760957c348d
  e677ebe9ad936bfe1f0)
- Per-session-pointer fix proven across 8 distinct session_ids tracked
  in the same file without cross-session contamination

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@realdecimalist
Copy link
Copy Markdown
Author

Update: per-session pointers + step-by-step diagnostics, verified end-to-end on DevKit voice

Pushed commit 0800324 with three hardening fixes discovered through live DevKit testing today:

  1. Fixed: cross-session state corruption
    The prior last_seen_index was user-scoped but indexed session-scoped get_full_message_history(). Every new session tripped "history shrunk" and reset the pointer without capturing anything. Replaced with session_pointers: {session_id: int} keyed on worker.session_id (fallback to a synthesized id when unexposed). State bumped to state_version: 2 with tolerant migration from the v1 schema — old journal files still load, the legacy field is ignored at load time.

  2. Fixed: call(worker, background_daemon_mode) TypeError when invoked as a skill
    Added background_daemon_mode: bool = False default so the entry point works for both category=skill (platform calls call(worker)) and category=background_daemon (platform calls call(worker, True)) invocations without a signature mismatch.

  3. Added: step-by-step diagnostics

    • [OrynqAudit] call() STEP 1-5 in the entry point (each step in try/except so a failure before the first log still surfaces where possible)
    • [OrynqAudit] watch_loop STEP A-D at coroutine entry, including resolved session_id + fallback source
    • orynq_audit_heartbeat.json written in step C — its presence in user-data file storage is a persistent non-log signal that the daemon actually started (useful when editor_logging_handler output isn't reachable for post-mortem)
    • Per-message extend[i/total]: ADD/skip role=<x> reason=<r> with skip counters and content_hash prefix
    • Per-poll poll#N: hist_len=M prev_seen=P shrunk=<bool> new_msgs=X added=Y chain_len=Z head=<hex> sessions_tracked=N
    • Per-gate anchor skip: no_growth|rate_limit|backoff|no_api_key (…) lines and an anchor start: chain_len=N last_len=M line before uploads

End-to-end verification (2026-04-24, OpenHome agent 578906, DevKit)

Stage Evidence
BD auto-loaded on session init (no hotword) Capability Worker initialized by: orynqbackgrounddaemon + call() STEP 1: entered, bd_mode=True
Heartbeat file written watch_loop STEP C: heartbeat file written OK + orynq_audit_heartbeat.json visible in Persistent Memory's file-list snapshot
Cross-session state loaded correctly loaded state: version=2 chain_len=286 session_pointers={6 sessions} last_anchor_ts=…
49-turn conversation captured on first poll extend[0-48/49]: ADD role=user/assistant seq=286..334
Forward-journal save succeeded save: writing .tmp (123660 bytes) → verify → success (chain_len=335)
Auto-anchor upload auto-anchor OK hash=0x3b83d1e816494b0161015880dd17bb565a463a8c3b2168614fb502c4aaee39a4 chain_len=335 size=103596
On-chain receipt landed 0x463c3b1c730b6ef7821c45a70139bb6faf1b98b303d29b00b983195b715efb32 at 22:00:06 UTC
Cert-daemon attestation certHash=0x75fe3b4bbb8d64bb361c3a60e4145554e8760957c348de677ebe9ad936bfe1f0 ✅ CERTIFIED
Per-session pointer fix validated 8 distinct session_ids tracked in the same state file without cross-session contamination

All three green bot checks (branch direction, community path, ability validation, lint/flake8) pass on the new commit.

@uzair401
Copy link
Copy Markdown
Contributor

Hey @Ju-usc, the rest of the code looks good! One thing to fix before we can approve — for all JSON file writes, please use the canonical delete + write pattern instead of writing directly.

The SDK's write_file defaults to a+ (append mode), which corrupts JSON on subsequent writes by producing invalid concatenated objects like {...}{...}.

Anywhere you write a JSON file, replace:

await self.capability_worker.write_file(filename, json.dumps(data), False)

with:

if await self.capability_worker.check_if_file_exists(filename, False):
    await self.capability_worker.delete_file(filename, False)
await self.capability_worker.write_file(filename, json.dumps(data), False)

Please address this and push your fix — we'll take another look.

@uzair401 uzair401 closed this Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-ability Community-contributed ability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants