Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b4d5ba4
test: repair grouped integration paths
jyaunches Aug 24, 2026
6efcb5a
fix(mcp): retain Deep Agents credential revisions
jyaunches Aug 25, 2026
7382e17
fix(mcp): wait for revision-bound credential readiness
jyaunches Aug 25, 2026
d015b2b
fix(mcp): bind resolution probes to credential revisions
jyaunches Aug 25, 2026
7c03ae3
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
1a1fb0c
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
7e1ef25
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
9bfe29d
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
a029e33
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
1955db7
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
4001b04
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
424bcd4
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
da93629
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
af02636
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
c19278b
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
8548808
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
2ba945f
Merge branch 'main' into codex/10079-dcode-credential-revision
rsliter Aug 25, 2026
7688223
merge(main): refresh Deep Agents credential revisions
rsliter Aug 25, 2026
96db460
fix(mcp): preserve credential revisions across recovery
rsliter Aug 25, 2026
7ea88b5
merge(main): refresh Deep Agents credential revisions
rsliter Aug 25, 2026
a057ce6
merge(main): include provider profile reconciliation
rsliter Aug 25, 2026
57c4ef0
fix(mcp): validate Hermes credential revisions
rsliter Aug 25, 2026
681da99
merge(main): refresh MCP revision recovery
rsliter Aug 25, 2026
7266b5f
merge(main): refresh MCP revision recovery
rsliter Aug 25, 2026
d3adccb
fix(mcp): bind status to credential revisions
ericksoa Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 81 additions & 8 deletions agents/hermes/mcp-config-transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
ENV_PLACEHOLDER_RE = re.compile(
r"^Bearer openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]{0,127})$"
)
REVISIONED_ENV_PLACEHOLDER_RE = re.compile(
r"^Bearer openshell:resolve:env:(v[0-9]{1,20})_([A-Za-z_][A-Za-z0-9_]{0,127})$"
)
OPENSHELL_CREDENTIAL_REVISION_RE = re.compile(r"^v[0-9]{1,20}$")
OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE = re.compile(r"^v[0-9]+_[A-Za-z0-9_]+$")
BOUNDARY_MANIFEST_NAME = "openshell-child-visible-credentials.v0.0.106.json"
ANSI_ESCAPE_RE = re.compile(
Expand Down Expand Up @@ -361,6 +365,8 @@ def _validate_payload(action: str, payload: dict[str, object]) -> None:
raise ValueError("Unsupported MCP config action")
allowed = {"server", "url", "headers"}
allowed.add("replace_existing" if action == "add" else "force")
if action == "add":
allowed.update({"credential_name", "credential_revision"})
unexpected = sorted(set(payload) - allowed)
if unexpected:
raise ValueError(
Expand Down Expand Up @@ -453,16 +459,38 @@ def _validate_payload(action: str, payload: dict[str, object]) -> None:
if not isinstance(headers, dict) or set(headers) != {"Authorization"}:
raise ValueError("MCP mutation payload must contain one Authorization header")
authorization = headers.get("Authorization")
authorization_match = (
ENV_PLACEHOLDER_RE.fullmatch(authorization)
if isinstance(authorization, str)
else None
)
declared_credential_name = payload.get("credential_name")
credential_revision = payload.get("credential_revision")
if credential_revision is not None and (
not isinstance(credential_revision, str)
or OPENSHELL_CREDENTIAL_REVISION_RE.fullmatch(credential_revision) is None
):
raise ValueError("Hermes MCP credential revision is invalid")
authorization_match = None
credential_name = None
if isinstance(authorization, str) and credential_revision is not None:
authorization_match = REVISIONED_ENV_PLACEHOLDER_RE.fullmatch(authorization)
if (
authorization_match is not None
and authorization_match.group(1) == credential_revision
and isinstance(declared_credential_name, str)
and authorization_match.group(2) == declared_credential_name
):
credential_name = authorization_match.group(2)
else:
authorization_match = None
elif isinstance(authorization, str) and declared_credential_name is None:
authorization_match = ENV_PLACEHOLDER_RE.fullmatch(authorization)
if authorization_match is not None:
credential_name = authorization_match.group(1)
if authorization_match is None:
raise ValueError(
"Hermes MCP Authorization must contain an OpenShell environment placeholder"
)
if action == "add" and _credential_name_is_reserved(authorization_match.group(1)):
if action == "add" and (
not isinstance(credential_name, str)
or _credential_name_is_reserved(credential_name)
):
raise ValueError(
"Hermes MCP Authorization uses a reserved credential environment name"
)
Expand All @@ -484,6 +512,48 @@ def _managed_candidate(payload: dict[str, object]) -> dict[str, object]:
return candidate


def _managed_candidate_matches(
actual: object, expected: dict[str, object], allow_revisioned: bool
) -> bool:
if actual == expected:
return True
if not allow_revisioned or not isinstance(actual, dict):
return False
if set(actual) != set(expected):
return False
for name, value in expected.items():
if name != "headers" and actual.get(name) != value:
return False
actual_headers = actual.get("headers")
expected_headers = expected.get("headers")
if not isinstance(actual_headers, dict) or not isinstance(expected_headers, dict):
return False
if set(actual_headers) != {"Authorization"} or set(expected_headers) != {
"Authorization"
}:
return False
expected_authorization = expected_headers.get("Authorization")
actual_authorization = actual_headers.get("Authorization")
expected_match = (
ENV_PLACEHOLDER_RE.fullmatch(expected_authorization)
if isinstance(expected_authorization, str)
else None
)
if expected_match is None:
return False
expected_name = expected_match.group(1)
if OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE.fullmatch(expected_name):
return False
if not isinstance(actual_authorization, str):
return False
prefix = "Bearer openshell:resolve:env:v"
suffix = f"_{expected_name}"
if not actual_authorization.startswith(prefix) or not actual_authorization.endswith(suffix):
return False
revision = actual_authorization[len(prefix) : -len(suffix)]
return revision.isdigit() and 1 <= len(revision) <= 20


_MANAGED_CANDIDATE_FIELDS = frozenset(
{"url", "enabled", "timeout", "connect_timeout", "tools", "headers"}
)
Expand Down Expand Up @@ -555,7 +625,10 @@ def inspect_managed_config(payload: dict[str, object]) -> dict[str, object]:
absent = payload["absent"]
if not isinstance(present, dict) or not isinstance(absent, list):
raise RuntimeError("Hermes MCP config does not match persisted managed intent")
matches = all(servers.get(name) == expected for name, expected in present.items())
matches = all(
_managed_candidate_matches(servers.get(name), expected, True)
for name, expected in present.items()
)
matches = matches and all(name not in servers for name in absent)
if not matches:
raise RuntimeError("Hermes MCP config does not match persisted managed intent")
Expand Down Expand Up @@ -595,7 +668,7 @@ def _mutate(data: object, action: str, payload: dict[str, object]) -> tuple[dict
return data, False
if payload.get("force") is not True:
current = servers.get(server_name)
if current != _managed_candidate(payload):
if not _managed_candidate_matches(current, _managed_candidate(payload), True):
raise ValueError(
f"Refusing to remove modified Hermes MCP server '{server_name}'. Use --force to remove it."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _write_config(url: str, *, ambiguous: bool = False) -> None:
"type": "http",
"url": url,
"headers": {
"Authorization": "Bearer openshell:resolve:env:VALIDATION_MCP_TOKEN"
"Authorization": "Bearer openshell:resolve:env:v12_VALIDATION_MCP_TOKEN"
},
}
if ambiguous:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { McpBridgeEntry } from "../../state/registry";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";
import {
type AdapterRegistrationInspection,
inspectAdapterRegistrationCommand,
Expand All @@ -11,10 +12,11 @@ import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
export function inspectDeepAgentsAdapterRegistration(
sandboxName: string,
entry: McpBridgeEntry,
credentialRevision?: McpAttachedCredentialRevision,
): AdapterRegistrationInspection {
return inspectAdapterRegistrationCommand(
sandboxName,
entry,
buildDeepAgentsMcpStatusCommand(entry),
buildDeepAgentsMcpStatusCommand(entry, credentialRevision),
);
}
67 changes: 17 additions & 50 deletions src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
DEEPAGENTS_STRICT_JSON_HELPERS,
} from "./mcp-bridge-adapter-deepagents-projection";
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
} from "./mcp-bridge/deepagents-legacy-config";
import {
MANAGED_HTTP_SERVER_MATCH_HELPERS,
DEEPAGENTS_MCP_CONFIG_PATH,
deepAgentsManagedServerConfig,
pythonJsonLiteral,
Expand All @@ -24,59 +29,13 @@ import {
// and runtime-generation suites execute the rendered helper against real files.
// removalCondition: delete this compatibility module after supported releases can
// no longer contain registry-owned v1 entries and the migration window has ended.
export const DEEPAGENTS_LEGACY_MCP_CONFIG_PATH = "/sandbox/.deepagents/.mcp.json";

export const DEEPAGENTS_LEGACY_CONFIG_HELPERS = [
"LEGACY_MCP_MAX_BYTES = 262144",
"def legacy_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
"def read_legacy_config(path):",
" flags = os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" descriptor = os.open(path, flags)",
" try:",
" before = os.fstat(descriptor)",
" linked = os.stat(path, follow_symlinks=False)",
" safe = (stat.S_ISREG(before.st_mode) and before.st_uid == os.getuid() and stat.S_IMODE(before.st_mode) == 0o600 and before.st_nlink == 1 and (before.st_dev, before.st_ino) == (linked.st_dev, linked.st_ino))",
" if not safe:",
" raise ValueError('legacy MCP config has unsafe ownership, mode, type, or links')",
" if before.st_size <= 0 or before.st_size > LEGACY_MCP_MAX_BYTES:",
" raise ValueError('legacy MCP config has invalid size')",
" chunks = []",
" remaining = before.st_size",
" while remaining:",
" chunk = os.read(descriptor, remaining)",
" if not chunk:",
" break",
" chunks.append(chunk)",
" remaining -= len(chunk)",
" after = os.fstat(descriptor)",
" linked_after = os.stat(path, follow_symlinks=False)",
" stable = (legacy_fingerprint(before) == legacy_fingerprint(after) and legacy_fingerprint(after) == legacy_fingerprint(linked_after))",
" if remaining or not stable:",
" raise ValueError('legacy MCP config changed while reading')",
" finally:",
" os.close(descriptor)",
" raw = b''.join(chunks).decode('utf-8')",
" data = strict_json_loads(raw)",
" return data, legacy_fingerprint(before)",
"def assert_legacy_source_stable(path, identity):",
" if identity is None:",
" if os.path.lexists(path):",
" raise ValueError('legacy MCP config appeared during mutation')",
" return",
" current = os.stat(path, follow_symlinks=False)",
" safe = (stat.S_ISREG(current.st_mode) and current.st_uid == os.getuid() and stat.S_IMODE(current.st_mode) == 0o600 and current.st_nlink == 1 and legacy_fingerprint(current) == identity)",
" if not safe:",
" raise ValueError('legacy MCP config changed before mutation')",
];

export function buildDeepAgentsMcpRollbackRegisterCommand(
entry: McpBridgeEntry,
expectedServers: Record<string, Record<string, unknown>>,
): string {
const payload = {
server: entry.server,
expected: deepAgentsManagedServerConfig(entry),
expected: expectedServers[entry.server] ?? deepAgentsManagedServerConfig(entry),
expectedServers,
};
return [
Expand All @@ -88,6 +47,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
`runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
"if runtime_kind == 'auto':",
" runtime_kind = 'unknown'",
Expand Down Expand Up @@ -131,9 +91,15 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" servers = data.get('mcpServers', {})",
" if not isinstance(servers, dict):",
" fail_rollback(f'Invalid managed MCP v2 server map at {config_path}')",
" if any(payload['expectedServers'].get(name) != current for name, current in servers.items()):",
" if any(not managed_http_server_matches(current, payload['expectedServers'].get(name), True) for name, current in servers.items()):",
" fail_rollback(f'Refusing to overwrite drifted managed MCP v2 projection at {config_path}')",
" data = {'mcpServers': payload['expectedServers']}",
" next_servers = {}",
" for name, expected in payload['expectedServers'].items():",
" if name != payload['server'] and name in servers:",
" next_servers[name] = servers[name]",
" else:",
" next_servers[name] = expected",
" data = {'mcpServers': next_servers}",
"else:",
" servers = data.setdefault('mcpServers', {})",
" if not isinstance(servers, dict):",
Expand Down Expand Up @@ -173,7 +139,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
" fail_rollback(f'Could not verify managed MCP rollback state at {config_path}: {exc}')",
"if is_v2:",
" restored = persisted == {'mcpServers': payload['expectedServers']}",
" persisted_servers = persisted.get('mcpServers') if isinstance(persisted, dict) else None",
" restored = isinstance(persisted_servers, dict) and set(persisted_servers) == set(payload['expectedServers']) and all(managed_http_server_matches(persisted_servers.get(name), expected, True) for name, expected in payload['expectedServers'].items())",
"else:",
" persisted_servers = persisted.get('mcpServers') if isinstance(persisted, dict) else None",
" restored = isinstance(persisted_servers, dict) and persisted_servers.get(payload['server']) == payload['expected']",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Deep Agents managed MCP projection safety", () => {
);
});

it("keeps status inspection nonblocking and no-follow for hostile projection paths", () => {
it("fails status inspection closed without following hostile projection paths", () => {
const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
expect(statusCommand).toContain("os.O_NONBLOCK | os.O_NOFOLLOW");
expect(statusCommand).not.toContain("config_path.read_text");
Expand All @@ -86,15 +86,17 @@ describe("Deep Agents managed MCP projection safety", () => {
0o600,
{ symlink: true },
);
expect(symlink.status, symlink.stderr).toBe(0);
expect(symlink.stdout.trim()).toBe("absent");
expect(symlink.status).toBe(2);
expect(symlink.stdout.trim()).toBe("");
expect(symlink.stderr).toContain("Could not inspect managed Deep Agents MCP state");
expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);

const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
fifo: true,
});
expect(fifo.status, fifo.stderr).toBe(0);
expect(fifo.stdout.trim()).toBe("absent");
expect(fifo.status).toBe(2);
expect(fifo.stdout.trim()).toBe("");
expect(fifo.stderr).toContain("Could not inspect managed Deep Agents MCP state");
});

it.each([
Expand Down
Loading
Loading