Skip to content

feat(hermes): support explicit Nowledge Mem agent identity#324

Merged
wey-gu merged 19 commits into
nowledge-co:mainfrom
KingBoyAndGirl:feat/agent-identity-fallback-from-config
Jun 24, 2026
Merged

feat(hermes): support explicit Nowledge Mem agent identity#324
wey-gu merged 19 commits into
nowledge-co:mainfrom
KingBoyAndGirl:feat/agent-identity-fallback-from-config

Conversation

@KingBoyAndGirl

@KingBoyAndGirl KingBoyAndGirl commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Thanks @KingBoyAndGirl for surfacing the missing agent-identity propagation path for multi-agent Hermes/Codex/Claude/Copilot setups. The underlying issue was real: integrations need a reliable way to pass identity context into Nowledge Mem instead of collapsing every long-running agent into the default profile.

This PR now keeps the contribution under the product design we settled on:

  • agent_id / NMEM_AGENT_ID is the normal portable Nowledge AI Identity.
  • host_agent_id / NMEM_HOST_AGENT_ID is an advanced external alias for integration authors, not a second user-facing identity.
  • Machine/container fingerprints are runtime provenance only and must not silently become an AI Identity or select a space.

Final behavior

Hermes now resolves identity in this order:

  1. A named Hermes agent_identity from runtime kwargs, unless it is default.
  2. An explicit agent_identity in nowledge-mem.json.
  3. Empty/default identity, with no generated fingerprint.

The resolved identity is used for Context Bundle and space_by_identity / space_template resolution. The provider does not auto-generate or persist machine/container fingerprints.

Codex, Claude Code, and Copilot hook-side fingerprinting was removed from the final diff. The CLI now provides the right shared path for thread writes: nmem t save/import/sync can accept explicit identity context and also honor NMEM_AGENT_ID / NMEM_HOST_AGENT_ID.

Verification

python -m py_compile \
  nowledge-mem-hermes/provider.py \
  nowledge-mem-claude-code-plugin/scripts/nmem-hook-save.py \
  nowledge-mem-codex-plugin/hooks/nmem-stop-save.py \
  nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py

python nowledge-mem-hermes/tests/test_space_resolution.py
# 15 tests OK

uv run --with pytest pytest tests/plugin_e2e -q
# 65 passed, 6 skipped

git diff --check
# passed

Co-authored-by: mo 48237066+KingBoyAndGirl@users.noreply.github.com

Summary by CodeRabbit

  • New Features

    • Improved initialization to consistently use a resolved agent identity when determining workspace context.
    • Added support for clearer identity-based space selection, with fallback behavior when identity is missing or empty.
  • Bug Fixes

    • Context bundle retrieval now uses the same resolved identity as space resolution, helping ensure more consistent results.
    • Initialization logs now include agent identity alongside space for easier troubleshooting.

…file is default

When Hermes uses the default profile, Hermes core passes
agent_identity="default" to the memory provider. This makes it
impossible to distinguish clients on the Nowledge Mem server
when the same server is used by multiple machines.

This change adds a fallback: when agent_identity is "default",
check nowledge-mem.json for an explicit agent_identity field.
This lets each machine set its own agent identity in the
plugin config file without changing the Hermes profile.

Backward compatible: if nowledge-mem.json has no agent_identity
field, behavior is unchanged (remains "default").

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nowledge-mem-hermes/provider.py`:
- Around line 810-811: The fallback logic that assigns identity from
config.get("agent_identity", identity) at lines 810-811 can assign a non-string
value, which will cause a TypeError when template.replace("{identity}",
identity) is called later. After the identity assignment from the config
fallback, validate that the assigned value is a string. Either convert
non-string values to string using str() conversion or skip the assignment if the
value is not a string, ensuring that only string identities are used for
template substitution.
- Around line 286-289: The config.get("agent_identity", identity) call on line
288 can return non-string JSON values, which then get assigned to host_agent_id
without type normalization, causing inconsistent identity handling. Apply the
same string normalization to the configured agent_identity value that is already
being applied to raw_identity at the beginning of the block: wrap the
config.get("agent_identity", identity) result with str() and .strip() to ensure
the value is always converted to a string before being assigned to
host_agent_id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3988f4f0-7389-4a3d-bc86-7df20d604b77

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab3aac and 4d24015.

📒 Files selected for processing (1)
  • nowledge-mem-hermes/provider.py

Comment thread nowledge-mem-hermes/provider.py Outdated
Comment thread nowledge-mem-hermes/provider.py Outdated
Replaces the static config-fallback approach with automatic
fingerprint generation:

1. /etc/machine-id — standard on systemd/Linux hosts
2. /proc/1/mountinfo — overlay upperdir layer ID for
   Docker/LazyCat containers

On first run the fingerprint is persisted to nowledge-mem.json
so it survives restarts without being regenerated.

Resolution order:
- Hermes named profile identity → use as-is
- nowledge-mem.json agent_identity → use explicit override
- Auto-generate fingerprint → save to nowledge-mem.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
nowledge-mem-hermes/provider.py (1)

886-889: 💤 Low value

Narrow the exception clause to specific expected exceptions.

The bare except Exception: could mask unexpected errors. Since you're reading JSON from a file, catching (OSError, json.JSONDecodeError) would be more precise while still handling all expected failure modes.

Suggested narrower exception handling
             try:
                 cfg = json.loads(config_path.read_text(encoding="utf-8"))
-            except Exception:
+            except (OSError, json.JSONDecodeError):
                 cfg = {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nowledge-mem-hermes/provider.py` around lines 886 - 889, Replace the bare
except Exception clause with a more specific exception handler that catches only
the expected exceptions. Since the code calls config_path.read_text() and
json.loads(), modify the except statement to catch (OSError,
json.JSONDecodeError) instead of Exception. This will allow unexpected errors to
propagate while still handling the two specific failure modes: file reading
errors from read_text and JSON parsing errors from json.loads.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nowledge-mem-hermes/provider.py`:
- Around line 931-937: The _resolve_space function signature has changed to
accept identity as a string parameter instead of a dictionary, but the tests in
test_space_resolution.py are still passing dictionaries as the second argument.
Update all test calls to _resolve_space throughout test_space_resolution.py to
extract the identity string value from the dictionary argument (such as using
dictionary.get("agent_identity")) and pass that string directly to
_resolve_space instead of passing the entire dictionary. This will align the
test calls with the new function signature that expects a string parameter which
it immediately calls .strip() on.

---

Nitpick comments:
In `@nowledge-mem-hermes/provider.py`:
- Around line 886-889: Replace the bare except Exception clause with a more
specific exception handler that catches only the expected exceptions. Since the
code calls config_path.read_text() and json.loads(), modify the except statement
to catch (OSError, json.JSONDecodeError) instead of Exception. This will allow
unexpected errors to propagate while still handling the two specific failure
modes: file reading errors from read_text and JSON parsing errors from
json.loads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 492679f5-b82d-49d7-b96e-c86f6fa8772b

📥 Commits

Reviewing files that changed from the base of the PR and between 4d24015 and d7dc4f0.

📒 Files selected for processing (1)
  • nowledge-mem-hermes/provider.py

Comment thread nowledge-mem-hermes/provider.py Outdated
_resolve_space now accepts (config, identity: str) instead of
(config, kwargs: dict). Update all test call sites to pass the
identity string directly.
@KingBoyAndGirl KingBoyAndGirl changed the title feat: allow nowledge-mem.json agent_identity override when Hermes profile is default feat: auto agent identity fingerprint for Hermes, Codex, and Claude Code plugins Jun 21, 2026
@KingBoyAndGirl KingBoyAndGirl changed the title feat: auto agent identity fingerprint for Hermes, Codex, and Claude Code plugins feat: auto agent identity fingerprint from system sources for Hermes plugin Jun 21, 2026
…oks (CLI not yet supported)"

This reverts commit f39e50b.
…oks (pending nmem CLI support for --host-agent-id on t save)
@KingBoyAndGirl KingBoyAndGirl changed the title feat: auto agent identity fingerprint from system sources for Hermes plugin feat: auto agent identity fingerprint for Hermes, Codex, and Claude Code plugins Jun 21, 2026
…g nmem CLI support for --host-agent-id on t import)
Replace the overlay-only fallback with a universal chain that works
across bare metal, VMs, Docker, and LPK:

1. /etc/machine-id - systemd hosts (gold standard)
2. MAC address - all Linux environments (Docker assigns per-host
   IP-to-MAC, unique across machines in practice)
3. overlay - /proc/1/mountinfo layer hash (last resort)

Adds _read_primary_mac() that scans /sys/class/net/*/address,
skipping loopback. Different prefixes (hermes-/overlay-) identify
which source was used.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
nowledge-mem-codex-plugin/hooks/nmem-stop-save.py (1)

205-222: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Overlay ID extraction returns non-unique value.

Same bug as in nmem-hook-save.py: Path(upperdir).name returns "diff" for all containers instead of the unique layer ID.

🐛 Fix to extract the unique layer ID
-        return Path(upperdir).name
+        return Path(upperdir).parent.name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nowledge-mem-codex-plugin/hooks/nmem-stop-save.py` around lines 205 - 222,
The _extract_overlay_id function incorrectly extracts the overlay ID by using
Path(upperdir).name which returns only the final directory component ("diff")
for all containers instead of the unique layer ID. Replace the line that uses
Path(upperdir).name with a path traversal that extracts the unique layer
identifier from a parent directory in the path hierarchy, likely using
.parent.name or navigating up multiple levels to get the actual layer ID rather
than just the final directory name.
nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py (1)

228-244: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Overlay ID extraction returns non-unique value.

Same bug as in the other hook files: Path(value[:end]).name returns "diff" for all containers instead of the unique layer ID.

🐛 Fix to extract the unique layer ID
-        return Path(value[:end]).name
+        return Path(value[:end]).parent.name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py` around lines 228
- 244, The _extract_overlay_id function has a critical bug in its return
statement where Path(value[:end]).name extracts only the filename portion (which
is "diff") instead of the unique layer ID. Instead of using .name to get the
final path component, extract the parent directory of the "diff" path to get the
actual unique layer identifier. You can achieve this by using Path operations to
access the parent directory and then getting its name, which will give you the
unique layer hash instead of the generic "diff" string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nowledge-mem-claude-code-plugin/scripts/nmem-hook-save.py`:
- Around line 137-154: The function `_extract_overlay_id` returns the final path
component (which is always "diff" for Docker overlay2 containers) instead of the
unique layer ID, causing all containers to have identical fingerprints. In the
return statement, instead of using `Path(upperdir).name` which returns the final
component, access the parent directory and then get its name to extract the
unique layer ID from the Docker overlay2 structure. This will return the actual
unique identifier between the `/overlay2/` and `/diff` directories.

In `@nowledge-mem-codex-plugin/hooks/nmem-stop-save.py`:
- Around line 187-224: The functions _host_agent_fingerprint and
_extract_overlay_id are duplicated with nearly identical implementations across
three hook files (nmem-hook-save.py, nmem-stop-save.py, and
copilot-stop-save.py). Create a new shared utility module for fingerprinting
logic and move both _host_agent_fingerprint and _extract_overlay_id functions
into this shared module. Then update all three hook files to import and use
these functions from the shared utility module instead of maintaining duplicate
copies.

---

Duplicate comments:
In `@nowledge-mem-codex-plugin/hooks/nmem-stop-save.py`:
- Around line 205-222: The _extract_overlay_id function incorrectly extracts the
overlay ID by using Path(upperdir).name which returns only the final directory
component ("diff") for all containers instead of the unique layer ID. Replace
the line that uses Path(upperdir).name with a path traversal that extracts the
unique layer identifier from a parent directory in the path hierarchy, likely
using .parent.name or navigating up multiple levels to get the actual layer ID
rather than just the final directory name.

In `@nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py`:
- Around line 228-244: The _extract_overlay_id function has a critical bug in
its return statement where Path(value[:end]).name extracts only the filename
portion (which is "diff") instead of the unique layer ID. Instead of using .name
to get the final path component, extract the parent directory of the "diff" path
to get the actual unique layer identifier. You can achieve this by using Path
operations to access the parent directory and then getting its name, which will
give you the unique layer hash instead of the generic "diff" string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b7949a1-df2d-423c-8015-1b5fc1ece88d

📥 Commits

Reviewing files that changed from the base of the PR and between d7dc4f0 and e570ebf.

📒 Files selected for processing (5)
  • nowledge-mem-claude-code-plugin/scripts/nmem-hook-save.py
  • nowledge-mem-codex-plugin/hooks/nmem-stop-save.py
  • nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py
  • nowledge-mem-hermes/provider.py
  • nowledge-mem-hermes/tests/test_space_resolution.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • nowledge-mem-hermes/provider.py

Comment thread nowledge-mem-claude-code-plugin/scripts/nmem-hook-save.py Outdated
Comment thread nowledge-mem-codex-plugin/hooks/nmem-stop-save.py Outdated
@KingBoyAndGirl

Copy link
Copy Markdown
Contributor Author

变更摘要

本 PR 为 nowledge-mem 的四个客户端插件(Hermes、Codex、Claude Code/Grok、Copilot CLI)增加了自动 agent 身份指纹识别功能。

核心逻辑

三级 fallback 指纹生成:

  1. /etc/machine-id
  2. MAC 地址(首个非 loopback、非零)
  3. overlay upperdir 哈希

指纹前缀规则:

  • machine-id / MAC → {runtime}-XXXXXXXX
  • overlay → overlay-XXXXXXXX(提示用户非机器唯一)

行为

  • 首次运行时自动计算指纹并保存到本地 nowledge-mem.json
  • 后续运行读取已保存的指纹,不再重新计算
  • 用户手动设置 agent_identity 后不会被自动覆盖

端到端验证(脱敏)

在两台 Docker 容器部署的 Hermes Agent 上验证:

机器 MAC(脱敏) 指纹 Context Bundle 返回
02 机器 02:42:xx:xx:02:04 hermes-d58f6f7a agent_id 正确返回 ✅
03 机器 02:42:xx:xx:05:04 hermes-53293a78 agent_id 正确返回 ✅

验证要点:

  • ✅ 两个容器 MAC 不同 → 指纹不同 → 服务端识别为两个独立 agent
  • ✅ Context Bundle 根据 --host-agent-id 返回对应的 agent 身份、描述、标签
  • ✅ 指纹计算可跨进程复现(MAC SHA256 前 8 位 hex)
  • ✅ 用户手动设置的 agent_identity 不会被自动计算覆盖

已知限制

nmem t save / nmem t import 暂不支持 --host-agent-id 参数,当前 hook 脚本中传入的该参数会被 CLI 静默忽略。待 CLI 侧支持后即可端到端受益。

@KingBoyAndGirl

Copy link
Copy Markdown
Contributor Author

补充说明一下这里的实现原因:

这里的问题不在于“开源/闭源”本身,而在于当前 nmem CLI 还没有提供统一的 host-agent-id 传递接口,尤其是在 thread save / import 这类写入路径上。

当前验证结果(nmem 0.9.19):

  • Hermes:已可端到端工作,因为 nmem context --host-agent-id 已支持
  • Codex / Claude Code / Grok:仍受限于 nmem t save --host-agent-id 缺失
  • Copilot CLI:仍受限于 nmem t import --host-agent-id 缺失

因此,这个 PR 里多个插件分别实现指纹逻辑,并不是因为更喜欢分散实现,而是因为目前还没有一个 CLI 侧的统一入口可复用。等 CLI 后续补上 thread save/import 的 host-agent-id 支持后,这部分逻辑可以进一步统一和收敛。


Clarification on the implementation rationale:

The issue here is not really “open source vs closed source” by itself. The practical constraint is that the current nmem CLI does not yet provide a unified host-agent-id propagation interface, especially on thread save / import write paths.

Current verified status on nmem 0.9.19:

  • Hermes: works end-to-end, because nmem context --host-agent-id is already supported
  • Codex / Claude Code / Grok: still blocked by the lack of nmem t save --host-agent-id
  • Copilot CLI: still blocked by the lack of nmem t import --host-agent-id

So the fingerprint logic is currently duplicated across several plugins not because this is the preferred design, but because there is not yet a reusable CLI-level identity propagation entry point for these flows. Once the CLI adds host-agent-id support for thread save/import operations, this logic can be consolidated further.

@wey-gu wey-gu requested a review from Copilot June 22, 2026 04:47

This comment was marked as resolved.

@nowledge-co nowledge-co deleted a comment from coderabbitai Bot Jun 24, 2026
Co-authored-by: mo <48237066+KingBoyAndGirl@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e9d9666-6bc2-401e-a877-1699b3c20e2f

📥 Commits

Reviewing files that changed from the base of the PR and between e570ebf and a5aff1c.

📒 Files selected for processing (2)
  • nowledge-mem-hermes/provider.py
  • nowledge-mem-hermes/tests/test_space_resolution.py

📝 Walkthrough

Walkthrough

NowledgeMemProvider gains a new _resolve_agent_identity(config, kwargs) method that selects the agent identity from Hermes kwargs, config file, or falls back to an empty string. _resolve_space is refactored to accept an explicit identity: str instead of the full kwargs dict. initialize stores the resolved identity in self._agent_identity and uses it for space resolution, host_agent_id derivation, and logging. Tests are updated to match the new signatures and three new tests cover _resolve_agent_identity precedence.

Changes

Agent Identity Resolution Refactor

Layer / File(s) Summary
_resolve_agent_identity and _resolve_space refactor
nowledge-mem-hermes/provider.py
Introduces _resolve_agent_identity(config, kwargs) with simplified three-step fallback (non-empty/non-default Hermes input → config file identity → empty string). Adds self._agent_identity instance field. Refactors _resolve_space to accept identity: str instead of a kwargs dict.
initialize wires identity into space resolution and logging
nowledge-mem-hermes/provider.py
initialize calls _resolve_agent_identity and stores the result in self._agent_identity, passes it to _resolve_space, derives host_agent_id from self._agent_identity instead of raw kwargs, and extends the initialization log payload with agent_identity.
Updated and new tests
nowledge-mem-hermes/tests/test_space_resolution.py
Existing _resolve_space test calls updated to pass identity as a plain string instead of a dict. Three new _resolve_agent_identity tests verify Hermes-named identity override, default-identity fallback to config, and empty-input behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop hop, the identity's clear,
No more dicts to dig through, I hear!
A plain string it shall be,
Resolved by steps of three,
And logged so the humans can cheer! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the Hermes-focused agent identity change in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wey-gu wey-gu changed the title feat: auto agent identity fingerprint for Hermes, Codex, and Claude Code plugins feat(hermes): support explicit Nowledge Mem agent identity Jun 24, 2026
@wey-gu

wey-gu commented Jun 24, 2026

Copy link
Copy Markdown
Member

Thanks @KingBoyAndGirl for digging into the multi-agent identity gap and validating the Hermes case in real container setups. I reshaped the implementation to match Mem's identity model: explicit AI Identity first, advanced external aliases when needed, and no silent machine-fingerprint identity. Your PR is still the one that surfaced and carries this fix.

@wey-gu wey-gu merged commit 8c38b59 into nowledge-co:main Jun 24, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants