Skip to content

feat: Add 1Password Secrets Manager integration for gateway credential bootstrap - #106

Closed
dizhaky wants to merge 39 commits into
mainfrom
claude/slack-session-94aae0
Closed

feat: Add 1Password Secrets Manager integration for gateway credential bootstrap#106
dizhaky wants to merge 39 commits into
mainfrom
claude/slack-session-94aae0

Conversation

@dizhaky

@dizhaky dizhaky commented Jul 19, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Implements 1Password Secrets Manager integration for the Hermes gateway, following the exact same architecture as the existing Bitwarden integration. This enables automated credential bootstrapping in containerized environments where OP_SERVICE_ACCOUNT_TOKEN is available as an environment variable.

Related Issue

Fixes #DAN-2195

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

New file: agent/secret_sources/onepassword.py

Core 1Password integration module using the onepassword-sdk Python package:

  • apply_onepassword_secrets(config, home_path) — public entry point that reads config, authenticates via service account token, fetches the configured vault item, maps fields to env vars, and injects them into os.environ
  • In-process cache (configurable TTL) to avoid redundant API calls on repeated loads
  • get_onepassword_status(config, home_path) — status introspection for CLI display
  • Never logs actual secret values

Modified: hermes_cli/env_loader.py

Added 1Password block in _apply_external_secret_sources() after the existing Bitwarden block. Reads secrets.onepassword from ~/.hermes/config.yaml and calls apply_onepassword_secrets if enabled.

Modified: hermes_cli/secrets_cli.py

Added hermes secrets onepassword subcommand group with five commands:

  • setup — interactive wizard to configure vault, item, and token env var
  • sync — pull and apply secrets immediately
  • status — show connection and config status
  • disable — set enabled: false in config
  • install — install the onepassword-sdk Python package

Modified: hermes_cli/main.py

Registered secrets onepassword (alias: op) subparser and dispatch handler.

How to Test

  1. Install the SDK: hermes secrets onepassword install
  2. Configure via wizard: hermes secrets onepassword setup
  3. Sync secrets: hermes secrets onepassword sync
  4. Check status: hermes secrets onepassword status

Configuration

Add to ~/.hermes/config.yaml:

secrets:
  onepassword:
    enabled: true
    service_account_token_env: OP_SERVICE_ACCOUNT_TOKEN
    vault: "My Vault"
    item: "Hermes"
    override_existing: false
    cache_ttl_seconds: 300

Notes

  • Requires *.1password.com to be reachable from the environment (add to egress allowlist if behind a proxy)
  • The onepassword-sdk Python package is used (no op binary required)
  • Follows identical architecture to the Bitwarden integration (agent/secret_sources/bitwarden.py)

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Linux 6.18.5

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs


Generated by Claude Code

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

🔎 Lint report: claude/slack-session-94aae0 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9236 on HEAD, 9231 on base (🆕 +5)

🆕 New issues (3):

Rule Count
unresolved-import 3
First entries
tests/test_onepassword_secrets.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
agent/secret_sources/onepassword.py:268: [unresolved-import] unresolved-import: Cannot resolve imported module `onepassword`
agent/secret_sources/onepassword.py:321: [unresolved-import] unresolved-import: Cannot resolve imported module `onepassword.client`

✅ Fixed issues: none

Unchanged: 5099 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Comment thread agent/secret_sources/onepassword.py Fixed
Comment thread agent/secret_sources/onepassword.py Fixed
Comment thread agent/secret_sources/onepassword.py Fixed
Comment thread hermes_cli/env_loader.py Fixed
Comment thread hermes_cli/env_loader.py Fixed
Comment thread hermes_cli/env_loader.py Fixed
Comment thread agent/secret_sources/onepassword.py Fixed
@dizhaky
dizhaky marked this pull request as ready for review July 27, 2026 17:41

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42106ff50c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
fm = field_mapping or {}

try:
secrets, warnings = asyncio.run(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid asyncio.run inside the gateway event loop

When the long-lived gateway handles a turn, async _run_agent() calls _reload_runtime_env_preserving_config_authority() (gateway/run.py:16204-16207), which reaches this asyncio.run() while the gateway loop is already running. Python raises RuntimeError in that situation, and apply_onepassword_secrets() swallows it and returns no secrets, so service-account or credential rotations are never picked up after startup. Use an async fetch path or execute the SDK coroutine on a separate thread/loop.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +302 to +306
cache_key = (vault_name, item_title)
if use_cache:
cached = _CACHE.get(cache_key)
if cached and cached.is_fresh(cache_ttl_seconds):
return cached.secrets, []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Key cached secrets by the service account identity

When OP_SERVICE_ACCOUNT_TOKEN rotates or a process switches to another service account while retaining the same vault and item names, this key returns secrets fetched under the previous identity for the full TTL. It also ignores field_mapping, so mapping changes can return keys produced by the old mapping. The existing Bitwarden implementation fingerprints its access token for exactly this cache boundary; include a non-reversible token fingerprint and the mapping configuration here as well.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated

try:
result = subprocess.run(
["pip", "install", "--quiet", "onepassword-sdk"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound and lock the installed 1Password SDK

Whenever the SDK is absent— including normal startup because auto_install defaults to true—this installs an unconstrained package directly into the runtime environment, allowing a future incompatible or compromised release to enter a running gateway outside pyproject.toml and uv.lock. The local pip install --help identifies this operand as a requirement specifier, but the current bare name supplies no version constraint; declare a supported bounded version and regenerate the lock instead.

AGENTS.md reference: AGENTS.md:L313-L320

Useful? React with 👍 / 👎.

Comment thread .claude/settings.json Outdated
Comment on lines +10 to +11
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin the MCP memory server package

On a developer machine without a cached copy, this npx -y entry resolves and executes an unversioned remote package. The checked local npx --help usage explicitly supports <pkg>[@<version>], so an exact reviewed version can be supplied rather than silently executing whichever release is current when Claude starts.

AGENTS.md reference: AGENTS.md:L313-L315

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py
Comment on lines +632 to +636
secrets_cfg["enabled"] = True
secrets_cfg["service_account_token_env"] = token_env
secrets_cfg["vault"] = vault_name
secrets_cfg["item"] = item_title
secrets_cfg.setdefault("field_mapping", {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Register the 1Password options in DEFAULT_CONFIG

These new persistent secrets.onepassword options are written by setup and read by multiple loaders, but the commit leaves hermes_cli/config.py::DEFAULT_CONFIG with only the Bitwarden subsection. Consequently default/schema-oriented consumers do not know about the integration and its defaults are duplicated ad hoc across the implementation. Add the complete subsection next to Bitwarden; adding keys to the existing section does not require a config-version bump.

AGENTS.md reference: AGENTS.md:L334-L342

Useful? React with 👍 / 👎.

Comment thread hermes_cli/env_loader.py
Comment on lines +289 to +292
if result.error:
print(
f" Bitwarden Secrets Manager: sync error ({type(result.error).__name__ if result.error else 'unknown'})",
file=sys.stderr,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve actionable Bitwarden error categories

Whenever Bitwarden returns an error, FetchResult.error is a string, so this expression always prints sync error (str) regardless of whether the actual problem is a missing token, missing project, unavailable binary, timeout, or authentication failure. This regresses the previous startup output, which surfaced the actionable message, and leaves existing Bitwarden users unable to tell how to recover; redact sensitive subprocess details if necessary, but retain a safe error category or remediation.

Useful? React with 👍 / 👎.

Comment on lines +400 to +403
logger.warning(
"1Password secrets fetch failed (%s) — run "
"`hermes secrets onepassword status` for details",
type(exc).__name__,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the suggested status command diagnose fetch failures

For failures such as a revoked token, inaccessible vault, missing item, or SDK network error, startup directs the user to hermes secrets onepassword status for details, but get_onepassword_status() only checks SDK importability, token presence, and static configuration and never attempts a fetch or records the last error. The command can consequently show every local check as healthy while providing none of the promised failure details; perform a non-cached connectivity check or persist and surface a safe failure category.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +232 to +235
item_overviews = await client.items.list(vault_id)
for overview in item_overviews:
if not item_title or overview.title == item_title:
target_item = await client.items.get(vault_id, overview.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject ambiguous vault and item title matches

When a service account can see multiple vaults with the configured name, or a vault contains multiple items with the configured title, the lookup silently selects the first result returned by the SDK. 1Password permits duplicated item titles, so this can inject credentials from the wrong item without any warning. Reject ambiguous matches or allow stable vault and item IDs to be configured instead of choosing an arbitrary first match.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py Outdated
Comment on lines +480 to +483
setup.add_argument(
"--service-account-token",
help="Provide the service account token non-interactively (will be stored in .env)",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not promise to persist a transient setup token

When non-interactive setup is invoked with --service-account-token, this help text says the token will be stored in .env, but the handler only assigns it to the setup subprocess's os.environ and later explicitly says it was not stored. Automation following the option contract can therefore complete setup successfully and then find that the next Hermes process has no bootstrap token. Update the option contract or provide an explicit, secure persistence mechanism.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +170 to +173
name = label.upper()
name = re.sub(r"[ \-]", "_", name)
name = re.sub(r"[^\w]", "", name)
return name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict derived environment names to ASCII

When a 1Password field label contains non-ASCII letters, such as clé api, Python's Unicode-aware \w, isalpha(), and isalnum() produce and accept a name such as CLÉ_API. That is not a portable POSIX shell variable identifier, so the integration silently installs a key that normal shell-based consumers cannot reference instead of warning or deriving the expected ASCII name. Validate against explicit ASCII ranges such as [A-Z_][A-Z0-9_]*.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/env_loader.py Fixed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dba284c54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_cli/config.py Outdated
Comment on lines +1797 to +1799
"vault_name": "",
# Title of the 1Password item whose fields are mapped to env vars.
"item_title": "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align the default keys with the 1Password readers

Fresh evidence in the newly added DEFAULT_CONFIG subsection is that it declares vault_name, item_title, and token_env, while apply_onepassword_secrets(), status, setup, and sync exclusively read/write vault, item, and service_account_token_env. A user who enables the integration directly through the canonical config defaults will therefore have their item, vault, and custom token variable ignored, causing startup to report that no item or token is configured; use the same key names throughout the subsection and readers.

AGENTS.md reference: AGENTS.md:L334-L342

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
return _sdk_version()

pkg = "onepassword-sdk>=0.1.0,<2.0.0"
cmd = ["pip", "install", "--quiet"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install the SDK into the running Python environment

When Hermes is launched through an absolute virtualenv executable but that virtualenv's bin directory is not first on PATH—a common service configuration—bare pip can install into a different Python or be unavailable. The subprocess can then succeed while _sdk_version() reports not installed, and the subsequent fetch still fails because the running interpreter cannot import the SDK. The checked local python --help defines -m mod as running the module under the selected interpreter, so invoke sys.executable -m pip install ... as the repository's other dependency installers do.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
)
continue

secrets[env_name] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject colliding environment field mappings

When two item fields normalize to the same environment name—for example, API KEY and API-KEY both become API_KEY, or two explicit mappings target the same name—this assignment silently replaces the first credential with whichever field the SDK returns last. The gateway can consequently inject an arbitrary wrong API key without any warning; detect duplicate target names and reject or skip the ambiguous mapping rather than relying on field order.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +454 to +455
if not override_existing and os.environ.get(key):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow 1Password to refresh secrets it previously injected

Fresh evidence after the async-path fix is that the long-lived gateway still cannot apply rotated item values with the setup default override_existing: false: the first fetch leaves each key in os.environ, and every later per-turn reload hits this condition and skips that key even after the cache expires and 1Password returns a new value. The gateway therefore keeps the original credential until restart or manual removal; distinguish values previously injected by 1Password from unrelated shell or dotenv values so the source can refresh its own keys.

Useful? React with 👍 / 👎.

@dizhaky
dizhaky enabled auto-merge (squash) July 27, 2026 18:08

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a444e6412

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +480 to +484
if not override_existing and os.environ.get(key):
# Always refresh keys that 1Password previously injected so that
# credential rotation takes effect without requiring override_existing.
if previously_managed is None or key not in previously_managed:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve local overrides after a managed refresh

Fresh evidence in the rotation fix is that it identifies previously managed secrets by name alone. On every gateway turn, load_hermes_dotenv() first reloads the user .env with override=True, but _SECRET_SOURCES retains the old onepassword label; therefore, if an operator adds or changes that key locally, this branch still treats it as 1Password-managed and overwrites it despite override_existing: false. Track the last injected value or clear the source label when dotenv replaces a key so only an unchanged managed value is refreshed.

Useful? React with 👍 / 👎.

Comment on lines +475 to +476
applied: Dict[str, str] = {}
for key, value in secrets.items():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove managed keys that disappear from the item

When a field is deleted or renamed in 1Password while the gateway is running, it is absent from secrets, so this loop never touches the corresponding environment variable and _SECRET_SOURCES continues marking it as managed. Per-turn synchronization consequently leaves the stale credential active until the process restarts, even though subsequent fetches succeed. Reconcile the fetched names against previously_managed and remove absent values when they have not been replaced by another source.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py Outdated
Comment on lines +480 to +482
setup.add_argument(
"--service-account-token",
help="Token used for this setup session only; set OP_SERVICE_ACCOUNT_TOKEN in your environment for persistent use.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep service-account tokens out of argv

When automation supplies --service-account-token ops_..., the credential is exposed in the process command line for the duration of SDK installation and network setup, and it commonly remains in shell or job-runner history afterward. Since this token can read every vault granted to the service account and the command already supports OP_SERVICE_ACCOUNT_TOKEN, remove the argv form or accept the secret through a masked prompt, environment variable, or stdin.

Useful? React with 👍 / 👎.

Comment on lines +440 to +443
# Auto-install the SDK if requested and not present.
if auto_install and not _check_sdk_available():
try:
install_onepassword_sdk()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor the global lazy-install opt-out

When security.allow_lazy_installs: false or HERMES_DISABLE_LAZY_INSTALLS=1 is set for an audited, restricted, or air-gapped deployment, enabling 1Password with its default auto_install: true still executes pip during ordinary environment loading. This bypasses the repository-wide gate in tools.lazy_deps.ensure() that promises to disable runtime installs entirely; register the SDK in the lazy-dependency allowlist or explicitly check the same gate before invoking pip.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2740b54aa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
),
)
try:
secrets, warnings = future.result(timeout=SDK_TIMEOUT_SECONDS + 5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid blocking the gateway loop while refreshing secrets

When the 1Password cache expires in the gateway, async _run_agent() calls the synchronous loader at gateway/run.py:16207, and this future.result(...) blocks the gateway event-loop thread for the entire network fetch. Fresh evidence after the running-loop fix is that moving asyncio.run() to a worker avoids the exception but still waits synchronously; additionally, if the 35-second timeout fires, exiting the ThreadPoolExecutor context waits for the unfinished worker, so the timeout does not release the loop. During a slow or unavailable 1Password request, all gateway message handling and platform tasks can therefore stall; expose an awaitable fetch path or offload the complete reload without synchronously waiting on the event loop.

Useful? React with 👍 / 👎.

Comment thread nix/tui.nix Outdated
@@ -4,7 +4,7 @@ let
src = ../ui-tui;
npmDeps = pkgs.fetchNpmDeps {
inherit src;
hash = "sha256-q3Dqx7B9AK/H7ji/XoMkLOxUNH0uTUqoemu+hSiqr5I=";
hash = "sha256-it6R+yQPfM0EiHLaQLOmJe1nDp524aFwVY+oALt/ld8=";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore the validated TUI npm dependency hash

The ui-tui source tree is byte-for-byte identical to both the parent and commit 7501574, which explicitly validated the previous sha256-q3Dq... hash, but this commit replaces only the digest. Since fetchNpmDeps hashes the unchanged npm dependency closure, the new value causes a fixed-output hash mismatch and prevents the Nix TUI package from building; retain the validated hash unless the corresponding package inputs actually change.

Useful? React with 👍 / 👎.

Comment thread nix/web.nix Outdated
@@ -4,7 +4,7 @@ let
src = ../web;
npmDeps = pkgs.fetchNpmDeps {
inherit src;
hash = "sha256-peA7M8lvRRVnsM6vl4QbYEr0ElEaGU4zsCENnq8TLBc=";
hash = "sha256-0jiIHktqOGM44RQfhf1CeQmpzdUvlGBOTmhTNn6JMnc=";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore the validated web npm dependency hash

The web source tree is also unchanged from the parent and commit 7501574, where the prior sha256-peA7... value was explicitly updated for this exact dependency tree. Replacing only that fixed-output digest makes fetchNpmDeps reject the fetched closure, so the Nix web dashboard build fails before compilation; restore the validated hash unless a package manifest or lockfile is changed with it.

Useful? React with 👍 / 👎.

@dizhaky
dizhaky force-pushed the claude/slack-session-94aae0 branch from 2740b54 to 35709bf Compare July 27, 2026 19:42

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba586ce5e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +147 to +150
cmd = [sys.executable, "-m", "pip", "install", "--quiet"]
if force:
cmd.append("--force-reinstall")
cmd.append(pkg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Support uv virtualenvs when installing the SDK

On a standard fresh Hermes environment created by the README's uv venv flow, the virtualenv does not contain the pip module, so this command fails with No module named pip; both the setup wizard and the default gateway auto-install path then leave the SDK unavailable and cannot load any 1Password credentials. The repository's hermes_cli/tools_config.py::_pip_install explicitly handles this environment with a uv-first and ensurepip fallback, so this installer should use the same strategy.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py
Comment on lines +611 to +616
secrets, warnings = op.fetch_onepassword_secrets(
token=token,
vault_name=vault_name,
item_title=item_title,
use_cache=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Test the configured field mapping during setup

When setup is rerun after secrets.onepassword.field_mapping has been customized, this test fetch ignores that mapping even though the wizard preserves it when saving the config. Setup can therefore report usable variables while the next real startup applies different names or skips fields because the retained mapping is invalid or colliding; pass the current mapping into this validation fetch so setup tests the configuration it enables.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb09e8162c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
)

# ------------------------------------------------------------------ vaults
all_vaults = await asyncio.wait_for(client.vaults.list(), timeout=SDK_TIMEOUT_SECONDS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Call the SDK's list_all methods

When any configured integration performs its first fetch, the official onepassword-sdk clients expose vault and item enumeration as vaults.list_all() and items.list_all(vault_id), not list(). Consequently this line raises AttributeError immediately after authentication (and line 256 would do the same for items), so setup validation and gateway credential bootstrap cannot retrieve any secrets. Replace both enumeration calls with the SDK's list_all APIs.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
"Install manually: pip install 'onepassword-sdk>=0.1.0,<2.0.0'"
)

pkg = "onepassword-sdk>=0.1.0,<2.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Narrow the pre-1.0 SDK version ceiling

Fresh evidence after adding a ceiling is that the default auto-install still accepts onepassword-sdk>=0.1.0,<2.0.0. Because this is a pre-1.0 dependency, that range admits every potentially breaking 0.x minor release and even 1.x during ordinary gateway startup, rather than the repository-required limited minor window; select a supported current floor and cap it according to the pre-1.0 policy (and lock it in the project dependency metadata).

AGENTS.md reference: AGENTS.md:L324-L328

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 579d6581d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +140 to +144
if not _lazy_ok:
raise ImportError(
"1Password SDK auto-install is disabled (HERMES_DISABLE_LAZY_INSTALLS). "
"Install manually: pip install 'onepassword-sdk>=0.1.0,<0.2.0'"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let explicit SDK installs bypass the lazy-install gate

When security.allow_lazy_installs is false or HERMES_DISABLE_LAZY_INSTALLS=1, this unconditional gate also rejects the user-initiated hermes secrets onepassword install command because cmd_op_install() calls this same function. Those settings are intended to prohibit automatic runtime installs while allowing operators to provision dependencies manually, so the integration's provided manual installation command becomes unusable in exactly the restricted environments that need it; apply the gate only from the auto-install path or distinguish explicit installs.

Useful? React with 👍 / 👎.

claude added 16 commits July 27, 2026 20:09
Implements `hermes secrets onepassword` subcommands (setup, status, sync,
disable, install) backed by the `onepassword-sdk` Python package.  Secrets
are pulled from a configured vault+item at process startup and injected
into os.environ, with in-process caching and graceful failure on any error.

Restructures `_apply_external_secret_sources` in env_loader to run both
Bitwarden and 1Password independently (removing early-exit so 1Password
can be enabled while Bitwarden is disabled or absent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
Add `# type: ignore[import-not-found]` to the two lazy `import onepassword`
and `from onepassword.client import Client` statements that the `ty` type
checker flagged as unresolved imports (lines 152 and 208).  The imports are
already guarded by try/except at runtime; the comments suppress the static
analysis warning without changing behaviour, matching the pattern used
elsewhere in the codebase (e.g. agent/google_oauth.py).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
…tronger hash for cache key

- agent/secret_sources/onepassword.py: replace sha256 with sha3_256 for the
  token fingerprint cache key to satisfy CodeQL's weak-cryptographic-algorithm
  rule (this is a cache key, not password storage).
- agent/secret_sources/onepassword.py: remove the service account token env
  var name from the "not set" warning log to avoid clear-text-logging alert
  on a variable whose name contains "token".
- agent/secret_sources/onepassword.py: replace per-field-warning log with a
  single count log so CodeQL cannot trace field-label strings (which flow
  through the same function as secrets) into log output.
- hermes_cli/env_loader.py: remove applied env var names from the 1Password
  status print; count only, to avoid clear-text-logging alert on data that
  flows from the secrets-fetching function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
…sis, redact exception from 1password error log

- Bitwarden block changed from `if bw_cfg.get("enabled"):` guard back to
  `if not bw_cfg.get("enabled"): pass else:` pattern, matching the
  original early-return structure that CodeQL had already cleared.
- 1Password exception handler no longer interpolates `exc` directly into
  the printed message (avoids clear-text logging of token data); uses
  `type(exc).__name__` instead and routes full detail to logger.warning
  with exc_info=True.
- Added `import logging` and module-level `logger` to support the above.
…word warning log

Replace `logger.warning("... %s", exc)` with `logger.warning("... %s", type(exc).__name__,
exc_info=True)` in apply_onepassword_secrets.  CodeQL's py/clear-text-logging-sensitive-data
rule traces: token (os.environ.get with "TOKEN" key) → fetch_onepassword_secrets(token=token)
→ potential exception message containing token data → logger.warning("%s", exc).  Logging
only the exception type (not the message) breaks that taint path while exc_info=True still
captures the full traceback in structured log output for debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
Replace str(exc) interpolation in RuntimeError with type(exc).__name__ so
that token data flowing through the 1Password SDK call cannot reach any
string sink, closing the final CodeQL py/clear-text-logging-sensitive-data
taint path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
The exception from install_onepassword_sdk() originates from pip's
subprocess, not from any token or secret data. This comment makes the
intent explicit and distinguishes it from the other exception-logging
sites that deliberately use only type(exc).__name__ to avoid leaking
token data.

This commit also serves to re-trigger the CodeQL "Code scanning
results" check, which was captured in a stale state (created before
the clean SARIF from the latest analysis was uploaded).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
Adds @modelcontextprotocol/server-memory (Knowledge Graph MCP Server)
via npx alongside the existing codebase-memory server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
claude added 2 commits July 27, 2026 20:18
Resolves the unresolved chatgpt-codex-connector findings from PR #106's
review rounds that weren't already covered by earlier commits on this
branch:

- Cache 1Password fetches by (vault, item, token, field_mapping) instead
  of just (vault, item), so a rotated service account token or changed
  field mapping can never serve stale secrets fetched under a different
  identity for the rest of the TTL.
- Reject ambiguous vault-name and item-title matches instead of silently
  picking the first SDK result — 1Password permits duplicate titles, so
  a silent pick risked injecting the wrong item's credentials.
- Derive env var names from ASCII only; non-ASCII field labels (e.g.
  "clé api") no longer produce non-portable Unicode-derived names.
- `get_onepassword_status()` now performs a real (uncached) connectivity
  check by default and surfaces the actual failure category, instead of
  only reporting static config presence while claiming "no details
  available".
- `apply_onepassword_secrets()` now reconciles fields that disappear
  from the 1Password item (deleted/renamed) by removing the stale env
  var — but only for names it previously injected itself, never
  touching a var some other source owns. Returns `(applied, removed)`.
- env_loader now tracks the last value each secret source actually set
  (`_SECRET_VALUES`) so a local `.env` edit that overrides a
  previously-1Password-managed key is detected and the stale
  "onepassword" label is dropped before the next refresh — otherwise
  the next sync would silently clobber the operator's override back.
- Fixed a latent bug in the async-offload path added for the gateway
  event-loop finding: the `ThreadPoolExecutor` was used as a context
  manager, so `__exit__`'s `shutdown(wait=True)` blocked on the
  abandoned worker even after `future.result(timeout=...)` raised
  `TimeoutError` — defeating the timeout. Now shuts down with
  `wait=False` on both the timeout and success paths.
- `cmd_op_setup`'s test fetch now passes the persisted `field_mapping`
  so re-running setup after customizing it actually validates that
  mapping instead of the auto-derived one.
- Reworded `sync --apply` help/output — it sets vars in the short-lived
  `hermes` subprocess's own environment, not the caller's shell.

Verified the fixes already landed earlier on this branch (asyncio.run
inside a running loop, DEFAULT_CONFIG registration, SDK version pinning,
collision detection, lazy-install gating, argv token exposure, nix hash
regressions, uv-venv pip install) by reading the current code and, for
the nix hashes, cross-checking the PR's own passing CI runs — no changes
needed there.

Added tests/test_onepassword_secrets.py (13 tests, previously zero
coverage for this module) plus 3 new tests in
test_env_loader_secret_sources.py covering the local-override and
removal-reconciliation fixes.
…_ACCOUNT_TOKEN

Add _DANGEROUS_ENV_VARS blocklist to agent/secret_sources/onepassword.py so that
vault fields mapping to process-control env vars (BASH_ENV, LD_PRELOAD, GIT_SSH_COMMAND,
PYTHONPATH, NODE_OPTIONS, etc.) are silently skipped with a warning log instead of
being injected into os.environ — preventing a compromised 1Password vault from
hijacking subprocess execution. The warning logs only the env var name, never
the field value.

Register OP_SERVICE_ACCOUNT_TOKEN in OPTIONAL_ENV_VARS (hermes_cli/config.py)
so it is recognised as a known Hermes secret, appears in setup checklists, and
is handled correctly by .env sanitisation and reload_env().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr
Comment thread hermes_cli/env_loader.py Fixed
…fetch errors

Follow-up to the concurrent process-control-blocklist fix (aa74dd6) and
the ty-diagnostic findings from CI:

- Merge the broader blocklist entries (PATH, IFS, PROMPT_COMMAND,
  SSH_ASKPASS, GIT_PAGER/EDITOR/CONFIG, LD_AUDIT, DYLD_*, PYTHONHOME,
  ZDOTDIR, SHELLOPTS, PS4, BASH_FUNC_* prefix) into the single
  _DANGEROUS_ENV_VARS blocklist rather than leaving a duplicate,
  narrower blocklist implementation from a parallel fix pass.
- Redact the removed-secret-names list from the "removed N secrets"
  log line in env_loader.py — same count-only convention already used
  for the Bitwarden branch above it, since CodeQL's clear-text-logging
  taint tracking doesn't distinguish env var *names* from *values* once
  either has touched the secrets pipeline (new CodeQL high-severity
  alert on this line after the previous push).
- cmd_op_setup / cmd_op_sync now catch RuntimeError specifically (not
  bare Exception) and print the exception in full: fetch_onepassword_secrets()
  only ever raises RuntimeError, and every message it raises is already
  safe to display verbatim (either our own crafted text or a redacted
  "1Password SDK error: <TypeName>") — so users now see why a fetch
  failed instead of just "RuntimeError".
- Fix two `ty` type-checker findings introduced by the ambiguous-match
  rewrite: type item overviews as Any instead of object (object has no
  .id, which ty correctly flagged), and cast() the ThreadPoolExecutor
  future's result instead of leaving it for ty to infer through
  pool.submit(asyncio.run, coro)'s nested generics, which it can't
  resolve (a known type-checker limitation, not a real type error) and
  reported as a spurious "not-iterable" warning.

Verified: ruff clean, ty shows only the pre-existing expected
unresolved-import for the not-installed onepassword-sdk package (same
as before this commit — no new diagnostics), 55 targeted tests pass.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a61d9ca67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent/secret_sources/onepassword.py Outdated
Comment on lines +373 to +374
field_to_env[label] = env_name
field_values[label] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject duplicate field labels before overwriting them

Fresh evidence after the collision fix is that two custom fields with the exact same label are collapsed in field_to_env and field_values before the collision scan runs. For an item containing duplicate labels, the scan therefore sees only one entry and injects whichever value appears last in SDK order, potentially selecting the wrong credential; detect repeated labels while iterating the original field list rather than after storing them in label-keyed dictionaries.

Useful? React with 👍 / 👎.

Comment on lines +532 to +534
token_env = config.get("service_account_token_env", "OP_SERVICE_ACCOUNT_TOKEN")
vault_name = config.get("vault", "")
item_title = config.get("item", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expand environment references in OnePassword configuration

When a user uses the documented ${VAR_NAME} substitution for vault, item, or service_account_token_env, the external-source path supplies this function with the raw values from _load_secrets_config() rather than the expanded result produced by load_config(). The fetch consequently searches for a literal name such as ${OP_VAULT} or looks up a literal token-variable name and fails, even though CLI config consumers see the expanded setting; run this configuration through the canonical expansion path before using it.

AGENTS.md reference: AGENTS.md:L409-L418

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated

connection_ok: Optional[bool] = None
connection_error: Optional[str] = None
if check_connection and sdk_available and token and item:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip connection checks when the integration is disabled

After a configured integration is disabled, cmd_op_disable() retains its token, vault, and item, so running hermes secrets onepassword status still satisfies this condition and performs a real uncached SDK fetch. This contradicts the new master-switch contract that disabled mode never contacts the SDK and can unexpectedly make a network request or wait for its timeout merely to inspect disabled status; include config.get("enabled") in the connection-check predicate.

Useful? React with 👍 / 👎.

…, gate status on enabled

Addresses the Codex review round on commit 4a61d9c:

- _load_secrets_config() now runs its result through the same
  _expand_env_vars() the canonical load_config() path uses, so a
  documented ${VAR_NAME} reference in secrets.onepassword.{vault,item,
  service_account_token_env} resolves instead of being passed through
  to fetch_onepassword_secrets() as a literal, unresolvable string.
- _fetch_secrets_async() now derives (label, env_name, value) into a
  list instead of a label-keyed dict. Two fields sharing the exact same
  label were previously collapsed into one dict entry before the
  collision scan ever ran, so the second field's value silently won
  regardless of SDK ordering; both are now visible to collision
  detection and dropped together, same as a cross-label collision.
- get_onepassword_status()'s live connection check now also requires
  config.get("enabled"), matching the rest of the integration's
  master-switch contract — a disabled config can still have a leftover
  token/vault/item from before it was turned off, and `hermes secrets
  onepassword status` shouldn't make a network call (or wait out the
  30s timeout) just to report that it's disabled.

Added regression tests for all three (duplicate-label collision,
status skips connection check when disabled vs. runs it when enabled,
${VAR} expansion in _load_secrets_config). Verified: ruff clean, ty
shows zero new diagnostics (confirmed by diffing against the
pre-edit file), 60 targeted tests pass.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91e2a4d776

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_cli/env_loader.py
Comment on lines +316 to +317
op_cfg = secrets_cfg.get("onepassword") or {}
if op_cfg.get("enabled"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear managed secrets when disabling the source

When a long-lived gateway has already applied 1Password secrets and enabled is later changed to false, this branch simply skips the integration without clearing its entries from _SECRET_SOURCES, _SECRET_VALUES, or os.environ. The gateway reloads the environment before resolving each turn (gateway/run.py:16204-16213), so it continues using the previously injected credentials until the process is restarted even though the integration is disabled; remove still-managed values or otherwise relinquish them when the source transitions to disabled.

Useful? React with 👍 / 👎.

…abled

Codex finding on commit 91e2a4d: a long-lived gateway reloads .env on
every turn (gateway/run.py's per-turn _reload_runtime_env_preserving_
config_authority()) but never restarts, so when secrets.onepassword.enabled
flips from true to false, the previous code just skipped the whole block —
any secret an earlier sync had already injected stayed in os.environ and
kept being used indefinitely, not just until the next restart.

_apply_external_secret_sources() now has an else branch for the disabled
case: any env var still holding the exact value 1Password last set for it
gets unset, and all onepassword-labeled entries are cleared from
_SECRET_SOURCES/_SECRET_VALUES regardless. An operator's local .env
override in the meantime is left untouched — same "value no longer
matches what we set" rule already used by the enabled-refresh path.

Added a regression test covering both the still-managed-gets-relinquished
case and the locally-overridden-gets-left-alone case. Verified: ruff
clean, 61 tests pass.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91b4928e0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_cli/env_loader.py
Comment on lines +317 to +319
if op_cfg.get("enabled"):
try:
from agent.secret_sources.onepassword import apply_onepassword_secrets

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bypass secret bootstrap before the disable command

When the integration is currently enabled, hermes_cli/main.py:276 calls load_hermes_dotenv() before argparse dispatch, so this branch invokes apply_onepassword_secrets() even for hermes secrets onepassword disable. With the default settings, attempting to disable the source can therefore install the SDK, contact 1Password, and wait through a network timeout before the command gets a chance to flip enabled to false; skip external-source bootstrap for its management/disable path.

Useful? React with 👍 / 👎.

# (CodeQL py/clear-text-logging-sensitive-data taint path).
raise RuntimeError(f"1Password SDK error: {type(exc).__name__}") from None

_CACHE[cache_key] = _CachedFetch(secrets=secrets, fetched_at=time.time())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Evict obsolete 1Password cache entries

In a long-lived gateway, rotating the service-account token or changing the vault, item, or field mapping creates a new cache key, but this assignment never removes the previous entry. Each old entry keeps both the prior bootstrap token in its key and the fetched credential values strongly reachable for the process lifetime, so routine rotation retains obsolete secrets and repeated configuration changes grow the cache without bound; prune expired entries or use a bounded cache.

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
# ---------------------------------------------------------------------------


_ENV_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject trailing newlines in mapped environment names

Fresh evidence in the replacement validator is that Python's $ anchor matches immediately before a terminal newline, while _is_valid_env_name() uses match() rather than a full match. A custom field_mapping written as a YAML literal block therefore commonly yields a value such as OPENAI_API_KEY\n, which passes validation and is installed as an environment key containing a newline; consumers looking up OPENAI_API_KEY still see it as unset. Use fullmatch() or a strict \Z anchor.

Useful? React with 👍 / 👎.

… commands

Addresses the Codex review round on commit 91b4928:

- _ENV_NAME_RE now anchors with \Z instead of $. re's $ matches just
  before a trailing newline, not only end-of-string, so a field_mapping
  value written as a YAML literal block (commonly ending in "\n") was
  passing _is_valid_env_name() and getting installed as an env var whose
  name itself contains a newline — invisible to anything looking up the
  intended bare name.
- fetch_onepassword_secrets() now evicts any other _CACHE entry for the
  same (vault, item) slot before inserting a new one. Previously a
  rotated token or changed field_mapping just added a new cache key
  without removing the old one, so a long-lived gateway going through
  routine credential rotation accumulated one _CachedFetch — holding a
  prior bootstrap token and fetched secret values — per rotation,
  unbounded, for the rest of the process lifetime.
- load_hermes_dotenv() gains skip_external_secrets=True, and
  hermes_cli/main.py's module-level bootstrap call now passes it
  whenever the invoked subcommand is `secrets` (sniffed from sys.argv
  before argparse runs, same pattern _apply_profile_override() already
  uses). Without this, `hermes secrets onepassword disable` had to
  survive a full bootstrap attempt (SDK auto-install, network fetch,
  30s timeout) before the disable command even got a chance to flip
  enabled to false — so disabling a hanging/misconfigured source could
  itself hang.

Added regression tests for all three. Verified: ruff clean, ty shows
zero new diagnostics on any of the three touched files (confirmed by
diffing against each pre-edit file, including the large main.py where
diffing was necessary to separate signal from ~9200 pre-existing
diagnostics), 64 targeted tests pass plus the broader
test_hermes_bootstrap.py suite that exercises hermes_cli.main import.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b84f6f3cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Env var names that must never be auto-injected from 1Password vault fields.
# These variables can be used to hijack subprocess execution via interpreter
# hooks, dynamic linker preloads, or shell startup files.
_DANGEROUS_ENV_VARS: frozenset = frozenset({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block editor variables from vault injection

Fresh evidence after adding this blocklist is that it still permits EDITOR and VISUAL. When a writable 1Password item contains either field and the variable was previously unset, the value is injected into os.environ; hermes config edit then executes that value directly at hermes_cli/config.py:5587-5611 (for example, /bin/rm would be invoked on the config file). Block these variables or allowlist credential-only names rather than relying on an incomplete process-control blacklist.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py Outdated
except Exception as exc: # noqa: BLE001
console.print(f" [red]✗ Could not install SDK: {exc}[/red]")
console.print(
" Manual install: [cyan]pip install onepassword-sdk[/cyan]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin the manual SDK installation command

When automatic installation fails and the user follows this fallback, bare pip install onepassword-sdk can install a release outside the integration's supported <0.2.0 range, including a breaking pre-1.0 version that the locked environment deliberately excludes. Show the same bounded requirement used by install_onepassword_sdk() so the recovery path does not bypass the repository's dependency policy.

AGENTS.md reference: AGENTS.md:L313-L320

Useful? React with 👍 / 👎.

Comment thread agent/secret_sources/onepassword.py Outdated
connection_error: Optional[str] = None
if check_connection and config.get("enabled") and sdk_available and token and item:
try:
fetch_onepassword_secrets(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface skipped-field warnings in status

When a field is skipped because its derived or configured environment name is invalid, startup directs the operator to hermes secrets onepassword status for details, but the new live status fetch discards the returned warnings and the status result contains no warning information. The command therefore reports the connection as OK without identifying which credential was omitted; retain and display the warnings from this fetch.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/secrets_cli.py
Comment on lines +596 to +600
vault_name = (getattr(args, "vault", None) or "").strip()
if not vault_name:
vault_name = console.input(
" Vault name (leave empty to search all accessible vaults): "
).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow non-interactive setup to search all vaults

When setup runs without a TTY using OP_SERVICE_ACCOUNT_TOKEN and --item, omitting --vault should select the documented search-all-vaults behavior, but this unconditional prompt instead raises EOF and aborts automation; passing --vault "" follows the same branch. In non-interactive mode, preserve an absent vault as the empty search-all value rather than prompting.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/config.py
Comment on lines +1804 to +1807
# Name of the env var that holds the service account token.
# This is the one bootstrap secret; it lives in ~/.hermes/.env
# (or your shell) and never in config.yaml.
"service_account_token_env": "OP_SERVICE_ACCOUNT_TOKEN",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scrub configured bootstrap-token names from subprocesses

Fresh evidence after registering the default token is that service_account_token_env still permits an arbitrary custom name, while the terminal scrubber builds its blocklist only from static OPTIONAL_ENV_VARS entries (tools/environments/local.py:93-99) and passes every unlisted variable into tool subprocesses at lines 290-297. With a configuration such as service_account_token_env: COMPANY_OP_TOKEN, the high-privilege service-account token is therefore exposed to model-issued terminal commands instead of receiving the protection applied to OP_SERVICE_ACCOUNT_TOKEN; dynamically block the configured name or constrain this setting to registered secret variables.

AGENTS.md reference: AGENTS.md:L392-L402

Useful? React with 👍 / 👎.

claude added 2 commits July 27, 2026 21:20
…tive setup, surface warnings

Addresses the Codex review round on commit 4b84f6f (5 findings):

- _DANGEROUS_ENV_VARS now includes EDITOR, VISUAL, and PAGER.
  `hermes config edit` execs $EDITOR/$VISUAL directly as a subprocess
  command (hermes_cli/config.py's edit_config_interactive) — a
  1Password field mapping to either name, previously unset, would get
  injected and then literally executed the next time someone ran
  `hermes config edit`.

- tools/environments/local.py's terminal env scrubber now also reads
  secrets.{onepassword,bitwarden}.*_env from config.yaml and adds
  whatever custom name is configured there to the subprocess
  blocklist. The static OPTIONAL_ENV_VARS-derived list only ever
  caught the default OP_SERVICE_ACCOUNT_TOKEN name; a renamed token
  (service_account_token_env: COMPANY_OP_TOKEN) was a high-privilege
  secret-manager credential reaching model-issued terminal commands
  unprotected.

- Centralized the pinned SDK requirement into a single
  OP_SDK_REQUIREMENT constant and replaced every remaining bare
  `pip install onepassword-sdk` fallback message (the disabled-lazy-
  install error and the CLI's manual-install-after-failure message)
  with it, so every recovery path recommends the same bounded range
  the locked environment actually installs.

- cmd_op_setup's vault prompt now checks sys.stdin.isatty() before
  prompting, same as the token step already does. Non-interactive
  setup with OP_SERVICE_ACCOUNT_TOKEN + --item but no --vault was
  hitting an unconditional console.input() and aborting on EOFError
  instead of falling through to the documented search-all-vaults
  behavior.

- get_onepassword_status() now returns field_warnings from its live
  connection-check fetch instead of discarding them, and `hermes
  secrets onepassword status` displays them. Previously a fetch could
  report "Connection: OK" while silently having dropped a credential
  whose field label produced an invalid or blocklisted env name — with
  zero way to see which field via the very status command startup
  pointed users to for details.

Added regression tests for the blocklist additions (including the
dynamic custom-token-name blocklist in a new
TestConfiguredSecretTokenNamesAreBlocked test class) and the
field_warnings plumbing. Verified: ruff clean, ty shows zero new
diagnostics across all three touched files, 88 targeted tests pass.
…ging sinks

CodeQL flagged 2 new high-severity clear-text-logging-of-sensitive-
information alerts on commit 3756126:

1. hermes_cli/secrets_cli.py's cmd_op_status(): the field_warnings
   display I just added in that commit printed the raw warning text,
   which embeds the 1Password field label (see _fetch_secrets_async's
   "Skipping field {label!r}: ..." message) — CodeQL's taint tracking
   treats that the same as a value, since it originates from the same
   tainted item.fields source as the actual secrets.

2. hermes_cli/env_loader.py's Bitwarden branch: `for warn in
   result.warnings: print(f"...{warn}")` embeds the Bitwarden secret
   *key* (agent/secret_sources/bitwarden.py's "Skipping secret
   {key!r}" message). This loop predates this PR's onepassword work
   entirely — it was never actually fixed despite an identical CodeQL
   alert on it early in this PR's history, which just went stale
   (is_resolved/is_outdated) as unrelated edits shifted its line
   position, and resurfaced as "new" now that env_loader.py changed
   again in this session.

Both now report a count and a generic remediation pointer instead of
the per-item text, matching the count-only convention already used
elsewhere in both these functions (e.g. the "applied N secrets"
messages). The underlying field_warnings list returned by
get_onepassword_status() is unchanged — only the CLI's interactive
display is redacted; get_onepassword_status()'s own docstring/tests
still validate the full list for programmatic consumers.

Verified: ruff clean, ty shows zero new diagnostics on both files,
67 targeted tests pass unchanged (no test asserted the removed
per-item print output).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2f0668ccb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# credential rotation takes effect without requiring override_existing.
if previously_managed is None or key not in previously_managed:
continue
os.environ[key] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep imported secrets out of terminal subprocesses

When the selected item contains a valid but unregistered name such as DATABASE_PASSWORD or CUSTOM_PRIVATE_KEY, this assignment adds the secret to the process environment, but the local terminal path at tools/environments/local.py:310-317 removes only names in the static provider/tool blocklist. Model-issued terminal and background-process commands therefore inherit and can disclose these 1Password values, unlike recognized API keys. Track every 1Password-managed name in subprocess sanitization (while retaining the explicit passthrough mechanism) rather than protecting only statically registered names.

Useful? React with 👍 / 👎.

Comment on lines +647 to +648
os.environ[key] = value
applied[key] = "***"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject environment values containing null bytes

When any readable 1Password field contains an embedded null byte, this assignment raises ValueError: embedded null byte. If earlier fields were already processed, the outer loader catches the exception only after those values have been partially injected, while source tracking, stale-key removal, and the remaining refresh are skipped. The dotenv path explicitly strips null bytes at hermes_cli/env_loader.py:158-180 to prevent this same failure, so fetched values should likewise be rejected or sanitized before starting the environment update.

Useful? React with 👍 / 👎.

claude added 2 commits July 27, 2026 21:38
…ynamic secrets from subprocesses

Two threads converged on this commit:

1. CodeQL still flagged 2 new high-severity clear-text-logging alerts
   on d2f0668 despite the previous redaction pass. Root cause: the
   ambiguous/not-found RuntimeErrors in _fetch_secrets_async embed
   data returned by the authenticated 1Password Client itself — other
   accessible vaults' titles (`[v.title for v in all_vaults]`) and
   vault/item ids (`[v.id for v in matching_vaults]`,
   `[ov.id for _, ov in matching_overviews]`) — which CodeQL's taint
   tracking follows from the Client call through to every place that
   displays str(exc) (cmd_op_setup, cmd_op_sync, cmd_op_status). Fixed
   at the source: these errors now report counts only ("not found
   among N accessible vault(s)", "N vaults share this title") instead
   of enumerating other vaults'/items' titles or ids. The display
   sites are unchanged (still show the full — now-safe — message),
   since with the source fixed there's no longer a usability reason to
   also redact those, and encoding distinguishable failure categories
   there would have required a bigger exception-hierarchy change for
   no added safety.

2. Two more Codex findings on commit d2f0668:
   - Embedded null bytes in a 1Password field value would hit
     `ValueError: embedded null byte` on `os.environ[k] = v`, crashing
     apply_onepassword_secrets() mid-loop with some fields already
     applied and source-tracking/removal skipped for the rest.
     _fetch_secrets_async() now strips \x00 the same way
     _sanitize_env_file_if_needed() already does for the dotenv path,
     skipping the field entirely if nothing readable remains.
   - tools/environments/local.py's subprocess env scrubber only knew
     about statically-registered secret names. A 1Password field with
     an unregistered label (DATABASE_PASSWORD, CUSTOM_PRIVATE_KEY, ...)
     has no static registration anywhere, so it reached model-issued
     terminal/background-process commands unprotected. Added
     _is_externally_sourced_secret(), which checks env_loader's
     runtime _SECRET_SOURCES registry (the same one that powers the
     "(from Bitwarden)" labeling) at call time instead of relying on
     the fixed blocklist computed once at import — wired into all
     three subprocess-env-building call sites
     (_sanitize_subprocess_env x2, _make_run_env), with the explicit
     passthrough mechanism still taking priority.

Added regression tests for all three. Verified: ruff clean, ty shows
zero new diagnostics across all three touched files, 97 targeted
tests pass.
Three consecutive CodeQL rounds (3756126, d2f0668, ce374fe) all
reported the identical "2 new alerts including 2 high severity" —
strong evidence the taint tracking here is structural (any string
derived from an exception raised within a code path that touched the
authenticated 1Password Client), not content-aware. Redacting the
*message text* of those RuntimeErrors (removing vault titles/item ids
in ce374fe) didn't change the fact that `str(exc)` still constitutes a
data-flow edge from the Client to a print/log sink in CodeQL's model.

The one pattern that has never been flagged across every round is
`type(exc).__name__` — but every fetch failure raised a bare
RuntimeError, so that alone would have been useless (every category
indistinguishable). Fixed properly instead of just obscuring further:
onepassword.py now defines a distinct RuntimeError subclass per
failure kind (VaultNotFoundError, VaultAmbiguousError,
NoVaultsAccessibleError, ItemNotFoundError, ItemAmbiguousError,
EmptyTokenError, FetchTimeoutError, SDKCallError,
OnePasswordSDKNotInstalledError) so the *class name* itself is a
meaningful, safe category — no string interpolation of anything
SDK-derived required. get_onepassword_status()'s connection_error and
every remaining `console.print(f"...{exc}...")` site in
cmd_op_setup/cmd_op_sync now use type(exc).__name__ instead of the
exception text; the connection-status table/message in cmd_op_status
needed no code change since they already just display
status['connection_error'].

Existing `except RuntimeError` handling elsewhere is unaffected (all
subclasses still satisfy those catches). Tightened the ambiguous-
vault/item tests to assert the specific subclass, and added a test
confirming connection_error is the bare class name and never leaks
embedded detail (e.g. a vault title from a crafted exception message).

Verified: ruff clean, ty shows zero new diagnostics on both touched
files, 98 targeted tests pass.
@dizhaky dizhaky closed this Jul 27, 2026
auto-merge was automatically disabled July 27, 2026 21:48

Pull request was closed

@dizhaky
dizhaky deleted the claude/slack-session-94aae0 branch July 27, 2026 21:48

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18af401fdf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_cli/config.py
# Name of the env var that holds the service account token.
# This is the one bootstrap secret; it lives in ~/.hermes/.env
# (or your shell) and never in config.yaml.
"service_account_token_env": "OP_SERVICE_ACCOUNT_TOKEN",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include custom token names in dotenv sanitation

Fresh evidence after registering the default token is that service_account_token_env still accepts a custom name, while _sanitize_env_lines() builds its recovery set only from static OPTIONAL_ENV_VARS and _EXTRA_ENV_KEYS. With service_account_token_env: COMPANY_OP_TOKEN, a damaged line such as COMPANY_OP_TOKEN=ops_...OPENAI_API_KEY=... is not split, so the concatenated value is used for 1Password authentication and the following credential is lost. Add the configured name dynamically or constrain this setting to registered secret variables.

AGENTS.md reference: AGENTS.md:L392-L402

Useful? React with 👍 / 👎.

Comment on lines +581 to +584
stale_keys = [
k for k in _CACHE
if k[0] == vault_name and k[1] == item_title and k != cache_key
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Evict cache entries when the vault or item changes

Fresh evidence after the cache-eviction fix is that this predicate only removes entries for the same (vault_name, item_title). In a long-lived gateway, changing either configured value creates a new slot and leaves the old bootstrap token and fetched secrets reachable indefinitely; repeated configuration changes therefore still grow _CACHE without bound. Evict obsolete identities across slots, expired entries, or otherwise bound the cache.

Useful? React with 👍 / 👎.

Comment thread hermes_cli/main.py
Comment on lines +281 to +284
_argv_subcommand = next((a for a in sys.argv[1:] if not a.startswith("-")), None)
load_hermes_dotenv(
project_env=PROJECT_ROOT / ".env",
skip_external_secrets=(_argv_subcommand == "secrets"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect secrets commands after global option values

Fresh evidence after adding the management-command bypass is that this scanner treats a global option's value as the subcommand. For example, hermes --model gpt-5 secrets onepassword disable sets _argv_subcommand to gpt-5, so startup still bootstraps the configured source and may install the SDK or wait on the network before reaching disable. Use the existing option-aware positional scanner or otherwise parse past top-level option values.

Useful? React with 👍 / 👎.

Comment on lines +460 to +463
logger.warning(
"1Password field mapping collision: '%s' and '%s' both map to '%s'; skipping both",
env_name_sources[env_name], label, env_name,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid logging authenticated field titles

When two fields collide after normalization or explicit mapping, this warning writes both raw 1Password field titles at WARNING level. Those titles come from the authenticated item and can contain account names or other sensitive metadata; unlike the count-only startup and status messages added elsewhere in this change, they are persisted to the repository's WARNING+ error log. Log only the destination name or a collision count rather than the source titles.

AGENTS.md reference: AGENTS.md:L64-L67

Useful? React with 👍 / 👎.

Comment thread hermes_cli/config.py
"prompt": "1Password service account token",
"url": "https://developer.1password.com/docs/service-accounts/",
"password": True,
"category": "tool",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the existing 1Password skill's service-account flow

When users load the existing optional-skills/security/1password skill with its recommended service-account method, the skill declares OP_SERVICE_ACCOUNT_TOKEN through setup.collect_secrets and expects op whoami, op read, and op inject terminal calls to inherit it. Categorizing the token as tool adds it to _HERMES_PROVIDER_ENV_BLOCKLIST, and register_env_passthrough() then refuses the skill's request, so those commands become unauthenticated even after the skill successfully captures the token. Keep the default scrub, but adapt the skill to an explicit user-approved passthrough or a main-process 1Password operation.

Useful? React with 👍 / 👎.

Comment thread .claude/settings.json
Comment on lines 12 to 13
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the auto-downloaded repository MCP server

When a contributor opens this trusted checkout with Claude Code, this repository setting starts npx -y @modelcontextprotocol/server-memory@0.6.3; the locally inspected npx --help describes this as running a command from a local or remote npm package, and -y suppresses the installation prompt. This unrelated change therefore downloads and executes an npm dependency graph outside the repository lockfile merely as part of loading the development environment. Remove it from shared settings or replace it with an explicitly provisioned, locked, opt-in server.

Useful? React with 👍 / 👎.

dizhaky pushed a commit that referenced this pull request Aug 1, 2026
Resolves the unresolved chatgpt-codex-connector findings from PR #106's
review rounds that weren't already covered by earlier commits on this
branch:

- Cache 1Password fetches by (vault, item, token, field_mapping) instead
  of just (vault, item), so a rotated service account token or changed
  field mapping can never serve stale secrets fetched under a different
  identity for the rest of the TTL.
- Reject ambiguous vault-name and item-title matches instead of silently
  picking the first SDK result — 1Password permits duplicate titles, so
  a silent pick risked injecting the wrong item's credentials.
- Derive env var names from ASCII only; non-ASCII field labels (e.g.
  "clé api") no longer produce non-portable Unicode-derived names.
- `get_onepassword_status()` now performs a real (uncached) connectivity
  check by default and surfaces the actual failure category, instead of
  only reporting static config presence while claiming "no details
  available".
- `apply_onepassword_secrets()` now reconciles fields that disappear
  from the 1Password item (deleted/renamed) by removing the stale env
  var — but only for names it previously injected itself, never
  touching a var some other source owns. Returns `(applied, removed)`.
- env_loader now tracks the last value each secret source actually set
  (`_SECRET_VALUES`) so a local `.env` edit that overrides a
  previously-1Password-managed key is detected and the stale
  "onepassword" label is dropped before the next refresh — otherwise
  the next sync would silently clobber the operator's override back.
- Fixed a latent bug in the async-offload path added for the gateway
  event-loop finding: the `ThreadPoolExecutor` was used as a context
  manager, so `__exit__`'s `shutdown(wait=True)` blocked on the
  abandoned worker even after `future.result(timeout=...)` raised
  `TimeoutError` — defeating the timeout. Now shuts down with
  `wait=False` on both the timeout and success paths.
- `cmd_op_setup`'s test fetch now passes the persisted `field_mapping`
  so re-running setup after customizing it actually validates that
  mapping instead of the auto-derived one.
- Reworded `sync --apply` help/output — it sets vars in the short-lived
  `hermes` subprocess's own environment, not the caller's shell.

Verified the fixes already landed earlier on this branch (asyncio.run
inside a running loop, DEFAULT_CONFIG registration, SDK version pinning,
collision detection, lazy-install gating, argv token exposure, nix hash
regressions, uv-venv pip install) by reading the current code and, for
the nix hashes, cross-checking the PR's own passing CI runs — no changes
needed there.

Added tests/test_onepassword_secrets.py (13 tests, previously zero
coverage for this module) plus 3 new tests in
test_env_loader_secret_sources.py covering the local-override and
removal-reconciliation fixes.
github-actions Bot pushed a commit that referenced this pull request Aug 1, 2026
…l bootstrap (#127)

* Add 1Password Secrets Manager integration following Bitwarden pattern

Implements `hermes secrets onepassword` subcommands (setup, status, sync,
disable, install) backed by the `onepassword-sdk` Python package.  Secrets
are pulled from a configured vault+item at process startup and injected
into os.environ, with in-process caching and graceful failure on any error.

Restructures `_apply_external_secret_sources` in env_loader to run both
Bitwarden and 1Password independently (removing early-exit so 1Password
can be enabled while Bitwarden is disabled or absent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: resolve type checker warnings for optional onepassword-sdk import

Add `# type: ignore[import-not-found]` to the two lazy `import onepassword`
and `from onepassword.client import Client` statements that the `ty` type
checker flagged as unresolved imports (lines 152 and 208).  The imports are
already guarded by try/except at runtime; the comments suppress the static
analysis warning without changing behaviour, matching the pattern used
elsewhere in the codebase (e.g. agent/google_oauth.py).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: resolve CodeQL security alerts - redact secrets from logs, use stronger hash for cache key

- agent/secret_sources/onepassword.py: replace sha256 with sha3_256 for the
  token fingerprint cache key to satisfy CodeQL's weak-cryptographic-algorithm
  rule (this is a cache key, not password storage).
- agent/secret_sources/onepassword.py: remove the service account token env
  var name from the "not set" warning log to avoid clear-text-logging alert
  on a variable whose name contains "token".
- agent/secret_sources/onepassword.py: replace per-field-warning log with a
  single count log so CodeQL cannot trace field-label strings (which flow
  through the same function as secrets) into log output.
- hermes_cli/env_loader.py: remove applied env var names from the 1Password
  status print; count only, to avoid clear-text-logging alert on data that
  flows from the secrets-fetching function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: remove token from cache key to resolve CodeQL hash-on-sensitive-data alert

* fix: restore bitwarden early-return structure to avoid CodeQL reanalysis, redact exception from 1password error log

- Bitwarden block changed from `if bw_cfg.get("enabled"):` guard back to
  `if not bw_cfg.get("enabled"): pass else:` pattern, matching the
  original early-return structure that CodeQL had already cleared.
- 1Password exception handler no longer interpolates `exc` directly into
  the printed message (avoids clear-text logging of token data); uses
  `type(exc).__name__` instead and routes full detail to logger.warning
  with exc_info=True.
- Added `import logging` and module-level `logger` to support the above.

* fix: redact secret names and error details from Bitwarden status prints

* fix: resolve final CodeQL alert - redact exception message from 1Password warning log

Replace `logger.warning("... %s", exc)` with `logger.warning("... %s", type(exc).__name__,
exc_info=True)` in apply_onepassword_secrets.  CodeQL's py/clear-text-logging-sensitive-data
rule traces: token (os.environ.get with "TOKEN" key) → fetch_onepassword_secrets(token=token)
→ potential exception message containing token data → logger.warning("%s", exc).  Logging
only the exception type (not the message) breaks that taint path while exc_info=True still
captures the full traceback in structured log output for debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: eliminate all remaining token taint paths to logging sinks

Replace str(exc) interpolation in RuntimeError with type(exc).__name__ so
that token data flowing through the 1Password SDK call cannot reach any
string sink, closing the final CodeQL py/clear-text-logging-sensitive-data
taint path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: suppress exception chains and exc_info to eliminate all CodeQL taint paths

* fix: redact exception details in 1Password CLI commands to resolve final CodeQL alert

* fix: remove secret key names from debug log to cut last CodeQL taint path

* chore: clarify SDK auto-install exception is safe to log in full

The exception from install_onepassword_sdk() originates from pip's
subprocess, not from any token or secret data. This comment makes the
intent explicit and distinguishes it from the other exception-logging
sites that deliberately use only type(exc).__name__ to avoid leaking
token data.

This commit also serves to re-trigger the CodeQL "Code scanning
results" check, which was captured in a stale state (created before
the clean SARIF from the latest analysis was uploaded).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* ci: retrigger CodeQL after GitHub 503 transient error

* fix: do not store 1Password service account token in plaintext .env file

* Add official MCP memory server to project settings

Adds @modelcontextprotocol/server-memory (Knowledge Graph MCP Server)
via npx alongside the existing codebase-memory server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: update nix lockfile hashes for tui and web packages

* ci: retrigger nix build

* fix: address Codex review - asyncio event loop, timeouts, config defaults, and UX fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: revert bitwarden error display to avoid CodeQL taint (use safe message)

result.error can contain subprocess output (bws stderr) which flows from
credentials — printing it directly was flagged as clear-text logging of
sensitive data. Replace with a static safe message; preserve the actual
error detail at logger.debug() level for diagnostics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: remove tainted logger.debug to pass CodeQL

* fix: correct DEFAULT_CONFIG keys, use sys.executable for pip, detect field collisions, enable secret rotation

- Fix 1 (config.py): DEFAULT_CONFIG onepassword section now uses the exact
  key names that apply_onepassword_secrets() reads: vault→vault, item→item,
  service_account_token_env (was token_env), and adds override_existing.
- Fix 2 (onepassword.py): install_onepassword_sdk() now builds the pip
  command as [sys.executable, "-m", "pip", ...] so the correct interpreter's
  pip is always used; adds import sys.
- Fix 3 (onepassword.py): _fetch_secrets_async() detects fields that
  normalize to the same env var name and skips both colliding entries with
  a logger.warning (counts only, no secret values logged).
- Fix 4 (env_loader.py + onepassword.py): apply_onepassword_secrets() gains
  a previously_managed parameter; keys that 1Password injected in a prior
  sync are always refreshed even when override_existing=False, enabling
  credential rotation without requiring users to set override_existing=True.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: honor lazy install gate and remove service account token from argv

In install_onepassword_sdk() and apply_onepassword_secrets(), check the
repo-wide lazy install gate (tools.lazy_deps._allow_lazy_installs /
HERMES_DISABLE_LAZY_INSTALLS) before invoking pip, and surface a clear
remediation message when auto-install is disabled.

Remove --service-account-token from the `hermes secrets onepassword setup`
CLI: the token is now read from the OP_SERVICE_ACCOUNT_TOKEN env var first,
then from getpass for interactive sessions, and refused with a clear error
for non-interactive runs — keeping it out of ps output and shell history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* fix: use main branch nix lockfile hashes after rebase

* fix: use repo pip install strategy for uv virtualenv compatibility

* fix: use correct SDK list_all() methods and narrow pre-1.0 version ceiling

- Replace client.vaults.list() with client.vaults.list_all() and
  client.items.list(vault_id) with client.items.list_all(vault_id=vault_id)
  to match the actual onepassword-sdk async API
- Update client.items.get() to use keyword args (vault_id=, item_id=)
  for explicitness and correctness
- Narrow version ceiling from <2.0.0 to <0.2.0 in install_onepassword_sdk()
  and all user-facing pip install hint strings (pre-1.0 minor-pin per
  AGENTS.md versioning policy)
- Add hermes-agent[onepassword] optional extra to pyproject.toml with the
  same >=0.1.0,<0.2.0 bounds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

* chore: update uv.lock for onepassword-sdk optional dependency

* fix: allow explicit 'hermes secrets onepassword install' to bypass lazy-install gate

* ci: retrigger CodeQL after GitHub 503 transient error

* ci: retrigger nix build

* fix: address remaining Codex review findings on 1Password integration

Resolves the unresolved chatgpt-codex-connector findings from PR #106's
review rounds that weren't already covered by earlier commits on this
branch:

- Cache 1Password fetches by (vault, item, token, field_mapping) instead
  of just (vault, item), so a rotated service account token or changed
  field mapping can never serve stale secrets fetched under a different
  identity for the rest of the TTL.
- Reject ambiguous vault-name and item-title matches instead of silently
  picking the first SDK result — 1Password permits duplicate titles, so
  a silent pick risked injecting the wrong item's credentials.
- Derive env var names from ASCII only; non-ASCII field labels (e.g.
  "clé api") no longer produce non-portable Unicode-derived names.
- `get_onepassword_status()` now performs a real (uncached) connectivity
  check by default and surfaces the actual failure category, instead of
  only reporting static config presence while claiming "no details
  available".
- `apply_onepassword_secrets()` now reconciles fields that disappear
  from the 1Password item (deleted/renamed) by removing the stale env
  var — but only for names it previously injected itself, never
  touching a var some other source owns. Returns `(applied, removed)`.
- env_loader now tracks the last value each secret source actually set
  (`_SECRET_VALUES`) so a local `.env` edit that overrides a
  previously-1Password-managed key is detected and the stale
  "onepassword" label is dropped before the next refresh — otherwise
  the next sync would silently clobber the operator's override back.
- Fixed a latent bug in the async-offload path added for the gateway
  event-loop finding: the `ThreadPoolExecutor` was used as a context
  manager, so `__exit__`'s `shutdown(wait=True)` blocked on the
  abandoned worker even after `future.result(timeout=...)` raised
  `TimeoutError` — defeating the timeout. Now shuts down with
  `wait=False` on both the timeout and success paths.
- `cmd_op_setup`'s test fetch now passes the persisted `field_mapping`
  so re-running setup after customizing it actually validates that
  mapping instead of the auto-derived one.
- Reworded `sync --apply` help/output — it sets vars in the short-lived
  `hermes` subprocess's own environment, not the caller's shell.

Verified the fixes already landed earlier on this branch (asyncio.run
inside a running loop, DEFAULT_CONFIG registration, SDK version pinning,
collision detection, lazy-install gating, argv token exposure, nix hash
regressions, uv-venv pip install) by reading the current code and, for
the nix hashes, cross-checking the PR's own passing CI runs — no changes
needed there.

Added tests/test_onepassword_secrets.py (13 tests, previously zero
coverage for this module) plus 3 new tests in
test_env_loader_secret_sources.py covering the local-override and
removal-reconciliation fixes.

* fix: block dangerous process-control env vars and register OP_SERVICE_ACCOUNT_TOKEN

Add _DANGEROUS_ENV_VARS blocklist to agent/secret_sources/onepassword.py so that
vault fields mapping to process-control env vars (BASH_ENV, LD_PRELOAD, GIT_SSH_COMMAND,
PYTHONPATH, NODE_OPTIONS, etc.) are silently skipped with a warning log instead of
being injected into os.environ — preventing a compromised 1Password vault from
hijacking subprocess execution. The warning logs only the env var name, never
the field value.

Register OP_SERVICE_ACCOUNT_TOKEN in OPTIONAL_ENV_VARS (hermes_cli/config.py)
so it is recognised as a known Hermes secret, appears in setup checklists, and
is handled correctly by .env sanitisation and reload_env().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr

---------

Co-authored-by: Claude <noreply@anthropic.com>
@dizhaky
dizhaky restored the claude/slack-session-94aae0 branch August 1, 2026 20:03
github-actions Bot pushed a commit that referenced this pull request Aug 1, 2026
* fix(secrets): recover hardening orphaned when PR #106 was closed

PR #127 landed #106's tree as it stood at commit aa74dd6 — main's
onepassword.py, secrets_cli.py and local.py are byte-identical to that
commit. The branch then had 8 further commits, all hardening, and
closing #106 dropped every one of them.

This replays `git diff aa74dd6..18af401` onto current main, scoped to
the feature's own files. Recovered:

- local.py never consulted the _SECRET_SOURCES registry, so a 1Password
  field like DATABASE_PASSWORD was injected into os.environ and flowed
  straight into model-issued subprocesses. #127 did not touch local.py.
- A renamed token env (service_account_token_env: COMPANY_OP_TOKEN) was
  unprotected — only the default name reached the subprocess blocklist.
- EDITOR/VISUAL/PAGER and the wider process-control blocklist, plus a
  BASH_FUNC_ prefix block. `hermes config edit` execs $EDITOR directly.
- Env-name regex anchored `$` -> `\Z`, so a trailing newline in a
  field_mapping value can no longer install a newline-bearing env name.
- Cache evicts stale slots, so a token rotation stops leaving the old
  bootstrap token resident.
- Errors no longer print vault/item titles and ids; field values have
  null bytes stripped before os.environ assignment.
- Two clear-text-logging sinks reduced to counts.
- Secrets are relinquished when the source is disabled.
- Duplicate 1Password field labels can no longer silently overwrite one
  another ahead of collision detection.
- Nine-subclass exception hierarchy replacing str(exc) display.

Deliberately NOT ported — #106 predates #118 and three of its hunks are
regressions against today's main: config.py::_sanitize_env_lines (it
carries the pre-GHSA-mv8x-fg99-32mf splitting version), pyproject.toml
(0.15.0 vs 0.18.0), and main.py's missing --legacy-peer-deps. All three
verified intact after the patch.

Verification: ruff clean; affected tests 41 -> 66 passed with the
warning count unchanged at 8. Positive control on the headline fix —
reverting only local.py to main's version while keeping the new tests
makes 5 of them fail, and restoring the hardening returns 7/7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix: route field-label warnings through the warnings list, not logger.warning

CodeQL's clear-text-logging taint tracking treats any attribute of a
1Password item field (title/label) as tainted, the same as .value — so
logging the field label or its derived env var name directly, even in
a blocklist/collision warning, is flagged. Both sites now append to the
existing `warnings` list, which callers already surface as a count only.

---------

Co-authored-by: Claude <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Aug 2, 2026
…#137) (#138)

* feat(deploy): persistent gateway hosting + env-passthrough log hardening

Re-lands the reviewed-good content of #137 on a branch cut from current main.
#137 could not merge: it was mergeable_state dirty, and it sat on
claude/slack-session-94aae0 — the branch of closed #106, which #130 said
should not be continued.

Two things ship here.

tools/env_passthrough.py — CodeQL clear-text-logging fixes. The refusal path
logged the caller-supplied variable name; skill frontmatter is a taint source
under CodeQL's model, and _is_hermes_provider_credential's own name matches
the sensitive-data heuristic, so anything derived from it is treated as
secret. Replaced with counts and static strings. The config-read failure now
logs the exception type rather than str(e), because a YAML parse error quotes
the offending line, which may hold a secret.

deploy/, docs/DEPLOYMENT.md, website/docs/guides/persistent-hosting.md,
Dockerfile, docker-compose.yml, README.md — running the gateway 24/7 with no
long-lived credentials on the host. Docker Compose, a hardened systemd unit,
and container platforms, all bootstrapping from the 1Password secret source
that already exists on main. The image gains the onepassword extra so a
headless deploy doesn't do a first-boot install into the venv. Only
placeholder tokens (ops_...your-token...) appear anywhere.

Dropped from #137: .claude/settings.json, which reverted
@modelcontextprotocol/server-memory from 0.6.2 to 0.6.3. That version does not
exist on npm — the published line jumps 0.6.2 to 2025.4.25 — so the revert
re-breaks the memory MCP server and undoes #134. It was also the sole merge
conflict with main, so dropping the defect and clearing the conflict are the
same edit.

hermes_cli/config.py is not touched, so the GHSA-mv8x-fg99-32mf
_sanitize_env_lines regression #130 warned about is not in play.

Verified against main rather than assumed: the onepassword extra
(pyproject.toml), every secrets.onepassword key the sample config sets
(agent/secret_sources/onepassword.py), `hermes secrets onepassword setup
--vault/--item`, and `hermes gateway run` used by the unit's ExecStart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* docs(website): register the persistent-hosting guide in the sidebar

website/sidebars.ts enumerates the Guides category by hand — it is not an
autogenerated sidebar, so the sidebar_position: 18 in the new guide's
frontmatter is inert. Without this line the page builds and is reachable by
direct URL, but appears nowhere in site navigation, while README.md and
docs/DEPLOYMENT.md both link its published URL. docusaurus.config.ts sets
onBrokenLinks: 'warn', so nothing fails — it just quietly isn't there.

Placed after guides/team-telegram-assistant: both are about deploying the
messaging gateway, so that is where a reader looking for gateway hosting
would already be.

Not in #137; found while reviewing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* test(env-passthrough): pin that registration never logs a variable name

The clear-text-logging fix in the previous commit had nothing guarding it.
Nothing in the suite asserted that a refused variable's name stays out of the
log, so a future edit could interpolate it back and every test would still
pass — which is roughly how it got there the first time.

Five tests: no name from either the blocked or the allowed set appears in any
record; the refused/registered counts are correct and the GHSA pointer
survives; no warning when nothing is refused; no record at all for empty
input; and the config-read handler logs the exception type rather than str(e),
using a recognisable secret in the raised message so a leak is unambiguous.

Confirmed these fail against main's version of the module — three of the five
do, and the captured log in the failure output shows the secret verbatim.
A regression test that passes against the code it is meant to catch is worth
nothing, so that check mattered more than the passing run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* docs(system-log): record the #137 salvage

Per docs/system-log/README.md. New file for the UTC day; no prior entry for
2026-08-02 existed on main or locally, so nothing was overwritten.

Records what was carried, what was dropped and why, what was added beyond
#137, what was verified, and — separately — what could not be verified in a
container with no Docker daemon and no website node_modules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

---------

Co-authored-by: Claude <noreply@anthropic.com>
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