Skip to content

feat(kanban): expose privacy-safe lifecycle activity - #83348

Open
moonweave wants to merge 2 commits into
NousResearch:mainfrom
moonweave:codex/activity-v1-standalone-20260811
Open

feat(kanban): expose privacy-safe lifecycle activity#83348
moonweave wants to merge 2 commits into
NousResearch:mainfrom
moonweave:codex/activity-v1-standalone-20260811

Conversation

@moonweave

Copy link
Copy Markdown

Summary

Add a bounded read-only kanban activity projection for operator dashboards.

The new command is:

hermes kanban activity --json --limit 80

It returns only pseudonymous event/work references, lifecycle kind, timestamp, and event-time profile when available. It never returns raw task/event/run IDs, task title/body/result/comments, payloads, claim locks, worker PIDs, paths, sessions, or error text.

Contract notes

  • allowlisted lifecycle kinds only
  • limit is bounded to 1..200
  • created and assigned profiles come from event-time payload projection
  • run-scoped events use the historical task_run profile
  • assigned exposes previous_profile only when a different prior projected profile exists
  • output is read-only and does not change kanban state

Tests

  • uv run --extra dev ruff check on the four changed files
  • uv run --extra dev pytest tests/hermes_cli/test_kanban_core_functionality.py tests/hermes_cli/test_kanban_cli.py
    • 28 passed, 1 skipped

A separate consumer integration is tracked in moonweave/hermes-agent-team-dashboard#6.

@moonweave

Copy link
Copy Markdown
Author

Pushed bf94f16 to address a weakness in the pseudonymization this PR introduces.

_dashboard_activity_ref derived work_ref/event_ref from plain SHA-256 over the database file path. That path is not a secret — the module header documents the default location — and _new_task_id draws from 32 bits (secrets.token_hex(4), ~4.3B values, per its own docstring). Anyone who knew the layout could enumerate the whole id space offline and invert a work_ref back to a task id in well under a minute, which defeats the point of the projection.

Refs are now HMAC-SHA256 under a 256-bit secret generated once per board and stored write-once in a new kanban_meta table, so the search space is the key rather than the id. The prefix moved inside the MAC'd message for domain separation, and the path-derived namespace is gone — it also drifted when the same board was reached through a different path.

Two details worth a reviewer's attention:

  • The salt is re-read after INSERT OR IGNORE rather than trusting the locally generated candidate. That is what makes concurrent processes converge on one value, and ref stability matters because consumers dedupe on these refs and keep bounded retention windows. Racing 12 processes on a fresh board yields one salt and one ref set.
  • A malformed salt raises instead of keying HMAC with nothing. bytes.fromhex("") returns an empty key, not an error, which would put the original attack back within reach.

Migration is additive and idempotent, in the style of _migrate_add_optional_columns. Existing boards pick up the table and seed on first use.

Verified on this branch: tests/hermes_cli/test_kanban_core_functionality.py and test_kanban_cli.py → 35 passed, 1 skipped. The two test_gateway_dispatcher_disables_corrupt_board_without_traceback failures in my environment are missing optional gateway dependencies and are unrelated to this change.

@alt-glitch alt-glitch added type/feature New feature or request comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 11, 2026
moonweave and others added 2 commits August 13, 2026 20:42
The activity projection pseudonymized task and event ids with plain SHA-256
over the database file path. That path is not a secret — the module header
documents the default — and `_new_task_id` draws from only 32 bits, so anyone
who knew the layout could enumerate the whole id space and invert work_ref
back to a task id in under a minute. A projection whose stated purpose is to
withhold identifiers was not withholding them.

Refs are now HMAC-SHA256 under a 256-bit secret generated once per board and
stored write-once in a new `kanban_meta` table, so the search space is the key
rather than the id. The prefix moves inside the MAC'd message for domain
separation, and the path-derived namespace is gone — it also drifted when the
same board was reached by a different path.

The salt is read back after `INSERT OR IGNORE` rather than trusting the
locally generated candidate, which is what makes racing processes converge on
one value; refs must stay stable because the consumer dedupes on them and
keeps a bounded retention window. A malformed salt raises instead of keying
HMAC with nothing: `bytes.fromhex("")` returns an empty key, which would put
the original attack back within reach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit aa2c423)
@moonweave

Copy link
Copy Markdown
Author

This branch is 45 commits behind main. A rebased version is on moonweave:codex/activity-v1-rebased-20260813 — same two commits, same content (verified: zero kanban_db.py definitions differ), applied to current main with no conflicts.

Two follow-ups build on it and are open now: #85266 (report why work stopped and whether it is moving) and #85267 (report that person-driven work exists, without reporting what it is). Both were opened against main rather than against this branch, because this branch's stale base produced 20 conflicts that current main does not — their diffs therefore include these two commits until this one merges.

Happy to force-push the rebase here if that is preferred.

@moonweave
moonweave force-pushed the codex/activity-v1-standalone-20260811 branch from bf94f16 to b4865d8 Compare August 13, 2026 12:16
@moonweave

Copy link
Copy Markdown
Author

Rebased onto current main and force-pushed — this is now mergeable. Same two commits, same content.

#85266 and #85267 build on this and are open. Both were opened against main while this one was conflicting, so their diffs still include these two commits; once this merges, theirs reduce to their own changes.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

feat(kanban): expose privacy-safe lifecycle activity

  1. hermes_cli/kanban_db.py board_activity calls _activity_ref_salt(conn), which can write (INSERT OR IGNORE) on a function documented as a "read-only source contract" — e.g. the test_activity_salt_reprovisions_after_row_loss case where the salt row is lost. A consumer that opens the DB ?mode=ro (the pattern fix(kanban): count board list entries through read-only DB paths #83337 just adopted for board counts) would get "attempt to write a readonly database". The eager seeding in _migrate_add_optional_columns covers fresh/legacy migrations, so this only bites when the row is lost — but the docstring should state the write-on-fallback behavior explicitly, or seed a placeholder at connect() time.
  2. The HMAC key lives in the same DB file it protects (kanban_meta), and a copied board carries the key — the docstring acknowledges this ("key must be treated as board-identifying material"), but the "privacy-safe" framing would benefit from stating plainly that refs are pseudonymous only to observers who cannot read the DB file; anyone with the file can invert the 32-bit id space or read raw ids directly. The current text gestures at it; making it explicit avoids overclaiming.
  3. previous_profile is computed within the bounded window only — an assigned event whose prior assignee sits outside the --limit window reports previous_profile: null. Fine for a bounded projection, but consumers should know the field is window-relative; a one-line note in the docstring/CLI help would help.
  4. Minor: int(row["created_at"]) is safe given the current schema (created_at INTEGER NOT NULL), but the file already has a _to_epoch normalizer used elsewhere — using it here would make board_activity resilient to any future timestamp-representation change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants