Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a467384
fix(onboard): restore lifecycle E2E qualification
sandl99 Aug 24, 2026
7d65f9c
fix(e2e): require exact lifecycle cleanup evidence
cv Aug 24, 2026
df68482
fix(e2e): reject incomplete policy cleanup evidence
cv Aug 24, 2026
a025a90
fix(e2e): repair deterministic state failures
sandl99 Aug 24, 2026
06c958a
Merge branch 'main' into fix/top3-e2e-state-errors
sandl99 Aug 24, 2026
98b6a93
fix(security): preserve messaging credential boundaries
apurvvkumaria Aug 24, 2026
1dfce9d
fix(e2e): stabilize credential-bound workflows
sandl99 Aug 24, 2026
eeb5f1f
fix(e2e): refresh Hermes validator integrity pin
sandl99 Aug 24, 2026
a562ca8
fix(e2e): align credential lifecycle checks
sandl99 Aug 24, 2026
02eb135
fix(security): enforce credential lifecycle boundaries
apurvvkumaria Aug 24, 2026
a46e9ec
fix(e2e): migrate credential-bound provider state
sandl99 Aug 24, 2026
f1d8504
Merge branch 'main' into fix/top3-e2e-state-errors
cv Aug 24, 2026
e6737b2
fix(security): preserve sealed provider runtime config
sandl99 Aug 24, 2026
de3e463
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 Aug 24, 2026
9a77bef
fix(e2e): read runtime provider config in proof
sandl99 Aug 24, 2026
5542a68
fix(e2e): bind proof to exec credential revision
sandl99 Aug 24, 2026
50e9af9
fix(e2e): bind fake Telegram provider endpoint
sandl99 Aug 24, 2026
64ecc98
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 Aug 24, 2026
fd3fcb3
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 Aug 24, 2026
8a7e4e4
fix(test): repair grouped integration checks
sandl99 Aug 24, 2026
9dbce7d
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 Aug 24, 2026
5a8e381
fix(test): remove grouped move artifacts
sandl99 Aug 25, 2026
2fcd211
fix(hermes): refresh wrapper integrity pin
sandl99 Aug 25, 2026
39e60f7
fix(hermes): refresh profile patch integrity pin
sandl99 Aug 25, 2026
1bedba3
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 Aug 25, 2026
c9b54eb
fix(test): repair grouped runtime imports
sandl99 Aug 25, 2026
c9e2401
Merge remote-tracking branch 'origin/main' into fix/top3-e2e-state-er…
sandl99 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
6 changes: 3 additions & 3 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -701,10 +701,10 @@ RUN node --experimental-strip-types \
# silent supply-chain tampering of the build context (an attacker rewriting a
# file has to also rewrite the Dockerfile-committed hash, which reviewers gate).
# Regenerate with `sha256sum agents/hermes/{hermes-wrapper.py,hermes-cli-adapter-v1.json,validate-cli-adapter.py,validate-env-secret-boundary.py,finalize-tirith-marker.py,cron-restore-control.py}`.
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=f4276e9833638b7a620176c88bd329d6b6d4948538a3227b727a1397146a0e0e
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=4db45043f45d8296dd39228315b721ee19b0a4e0591579ec0ceeec2777bbb40d
ARG NEMOCLAW_HERMES_CLI_ADAPTER_SHA256=989edf54a8c09c6efb348600a8aa2f264c0b71408eb9d7bcd579b92cbeccf9b1
ARG NEMOCLAW_HERMES_CLI_ADAPTER_VALIDATOR_SHA256=db4046e79e513eab67b069a8eda20167b8b65529cf26842531d2ad673c670330
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=b355d1365fb1d15475e327f312ceb854ae96f9ebed28cf96bc8817f550df2688
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=4121dfcc56cff35278795ce8482fd892480d0e179f4221f391f9935816db0623
ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819
ARG NEMOCLAW_HERMES_CRON_RESTORE_CONTROLLER_SHA256=e8593cf1580bffa4663e91c079ba0ce31c3d26391f5b1718872701138ce250b0
# hadolint ignore=DL4006
Expand Down Expand Up @@ -909,7 +909,7 @@ RUN install -o root -g root -m 0444 \

# Fresh named profiles do not receive config.yaml. Patch the pinned Hermes
# fallback readers from the generated manifest, then validate a real profile.
ARG NEMOCLAW_HERMES_PROFILE_POLICY_PATCHER_SHA256=7468555c7596b3b95732fb98aec6152537778d8519a4409c3da8aa6a76c9a3f7
ARG NEMOCLAW_HERMES_PROFILE_POLICY_PATCHER_SHA256=424336d2ee3a12b4fb979ed84401ef105bf9c70e36dc3aa27a70f2a46b46def9
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
"$NEMOCLAW_HERMES_PROFILE_POLICY_PATCHER_SHA256" /usr/local/lib/nemoclaw/patch-hermes-profile-policy-defaults.py \
Expand Down
7 changes: 7 additions & 0 deletions agents/hermes/runtime-config-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4962,6 +4962,13 @@ def _runtime_plan_replacements_and_provider_keys(
if not _placeholder_suffix_matches_env_key(suffix, env_key):
continue
if compiled.search(runtime_value):
revision = re.fullmatch(
rf"openshell:resolve:env:(v[0-9]{{1,20}}_){re.escape(env_key)}",
runtime_value,
)
marker = f"-OPENSHELL-RESOLVE-ENV-{env_key}"
if revision and value.endswith(marker):
value = value[: -len(env_key)] + revision.group(1) + env_key
replacements[env_key] = (value, message)
return replacements, provider_env_keys, True

Expand Down
4 changes: 3 additions & 1 deletion agents/hermes/validate-env-secret-boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
from typing import Iterable, TextIO

SECRET_KEY_RE = re.compile(r"(^|_)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)(_|$)")
PLACEHOLDER_RE = re.compile(r"^(xoxb|xapp)-OPENSHELL-RESOLVE-ENV-[A-Z0-9_]+$")
PLACEHOLDER_RE = re.compile(
r"^(xoxb|xapp)-OPENSHELL-RESOLVE-ENV-(?:v[0-9]{1,20}_)?[A-Z][A-Z0-9_]*$"
)
KEY_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
API_SERVER_KEY_RE = re.compile(r"^[0-9a-f]{64}$")
HERMES_API_PORT_RANGE_START = 8642
Expand Down
2 changes: 1 addition & 1 deletion ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"test/installer-integration/install-preflight.test.ts": 3025,
"test/agents/openclaw/runtime/nemoclaw-start.test.ts": 4671,
"test/onboarding/onboard-messaging.test.ts": 2023,
"test/onboarding/onboard-selection.test.ts": 4177
"test/onboarding/onboard-selection.test.ts": 4176
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,7 @@ No real Microsoft Teams tenant proof is included in this PR. The work remains tr
Pull requests execute that WeChat audit action from the PR base SHA.
If the PR base SHA does not contain the action, the pull request workflow fails.
The production installer routes registry metadata lookup, archive packing, and installation through the disposable writable-cache boundary so retrieval cannot fall back to `HOME/.npm`; the trusted source cache remains read-only and the disposable copy is removed in the same image layer.
<<<<<<< HEAD
- The stale nonterminal rebuild-resume repair in `src/lib/actions/sandbox/rebuild-resume-session.ts` remains a migration compatibility shim tracked against #4533's onboard FSM/resume compatibility boundary. Its removal condition is to delete it after a session-version migration proves recreate sessions are always persisted at a resumable pre-sandbox boundary; `src/lib/actions/sandbox/rebuild-resume-session.test.ts` covers the helper directly, `test/onboarding/onboard-resume-provider-recovery.test.ts` carries the onboard-suite producer-level regression for `machine.state='openclaw'`, and `src/lib/actions/sandbox/rebuild-resume-snapshot.test.ts` owns the rebuild handoff regression.
=======
- The stale nonterminal rebuild-resume repair in `src/lib/actions/sandbox/rebuild-resume-session.ts` remains a migration compatibility shim tracked against #4533's onboard FSM/resume compatibility boundary. Its removal condition is to delete it after a session-version migration proves recreate sessions are always persisted at a resumable pre-sandbox boundary; `src/lib/actions/sandbox/rebuild-resume-session.test.ts` covers the helper directly, `test/onboard-resume-provider-recovery.test.ts` carries the onboard-suite producer-level regression for `machine.state='openclaw'`, and `src/lib/actions/sandbox/rebuild-resume-snapshot.test.ts` owns the rebuild handoff regression.
>>>>>>> origin/main
- Production OpenClaw image build paths call `scripts/check-production-build-args.sh` before production `docker build` or `docker/build-push-action` use. `test/agents/openclaw/openclaw-dependency-review.test.ts` keeps that workflow contract documented.
- The rebuild-reasoning cases added by this PR live in the focused `rebuild-resume-reasoning.test.ts` file; the smaller route-provenance additions remain with their `rebuild-resume-config.ts` boundary tests.
- `src/lib/state/sandbox.ts` is 100 lines smaller than current `main` in this PR. Managed-extension policy, restore exclusions, symlink predicates, and cleanup construction now live in `openclaw-managed-extensions.ts`; further decomposition of unrelated snapshot orchestration is outside this dependency bump.
Expand Down
2 changes: 1 addition & 1 deletion scripts/find-source-shape-tests.mts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function hasDirectProductionPathHint(text: string): boolean {
/["'`]\.\.\/["'`]\s*,\s*["'`](?:\.github|agents|bin|dist|nemoclaw|nemoclaw-blueprint|scripts|src|Dockerfile(?:\.base)?|install\.sh|package\.json)["'`]/.test(
text,
) ||
/["'`]\.\.["'`]\s*,\s*["'`](?:\.github|agents|bin|dist|nemoclaw|nemoclaw-blueprint|scripts|src|Dockerfile(?:\.base)?|install\.sh|package\.json)["'`]/.test(
/["'`](?:\.\.\/)*\.\.["'`]\s*,\s*["'`](?:\.github|agents|bin|dist|nemoclaw|nemoclaw-blueprint|scripts|src|Dockerfile(?:\.base)?|install\.sh|package\.json)["'`]/.test(
text,
) ||
/join\(\s*["'`]\.\.["'`]\s*,\s*["'`](?:\.github|agents|bin|dist|nemoclaw|nemoclaw-blueprint|scripts|src|Dockerfile(?:\.base)?|install\.sh|package\.json)["'`]\s*\)/.test(
Expand Down
97 changes: 86 additions & 11 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1509,10 +1509,18 @@ refresh_openclaw_provider_placeholders() {
local config_file="/sandbox/.openclaw/openclaw.json"
local hash_file="/sandbox/.openclaw/.config-hash"
[ -f "$config_file" ] || return 0
if [ -L "$config_file" ] || [ -L "$hash_file" ]; then
printf '[SECURITY] Refusing provider placeholder refresh — config or hash path is a symlink\n' >&2
return 1
fi

local sealed_config=0
if [ "$(openclaw_config_dir_owner "$(dirname "$config_file")")" = "root" ]; then
printf '[config] Shields are up; preserving sealed provider placeholders unchanged\n' >&2
return 0
if [ "$(id -u)" -ne 0 ]; then
printf '[config] Shields are up; preserving sealed provider placeholders unchanged\n' >&2
return 0
fi
sealed_config=1
fi

local keys
Expand Down Expand Up @@ -1676,9 +1684,37 @@ PYPLACEHOLDERKEYS
"$_extras_accepted" "$_accepted_extra_keys" >&2
fi

if [ -L "$config_file" ] || [ -L "$hash_file" ]; then
printf '[SECURITY] Refusing provider placeholder refresh — config or hash path is a symlink\n' >&2
return 1
local runtime_config=0
if [ "$sealed_config" -eq 1 ]; then
local key runtime_value needs_runtime_config=0
for key in $keys; do
runtime_value="${!key-}"
if [[ "$runtime_value" = openshell:resolve:env:* ]] \
&& [ "$runtime_value" != "openshell:resolve:env:$key" ]; then
needs_runtime_config=1
break
fi
done
[ "$needs_runtime_config" -eq 1 ] || return 0

# Keep the persistent seal unchanged. OpenClaw reads this root-owned,
# read-only copy through OPENCLAW_CONFIG_PATH for the current process.
local sealed_config_file="$config_file"
local runtime_dir="/run/nemoclaw/openclaw-provider-config"
if [ -L /run/nemoclaw ] || [ -L "$runtime_dir" ]; then
printf '[SECURITY] Refusing provider placeholder refresh — runtime config path is a symlink\n' >&2
return 1
fi
install -d -o root -g root -m 755 /run/nemoclaw "$runtime_dir" || return 1
config_file="$runtime_dir/openclaw.json"
hash_file="$runtime_dir/.config-hash"
if [ -L "$config_file" ] || [ -L "$hash_file" ]; then
printf '[SECURITY] Refusing provider placeholder refresh — runtime config or hash path is a symlink\n' >&2
return 1
fi
emit_sandbox_sourced_file "$config_file" <"$sealed_config_file" || return 1
export OPENCLAW_CONFIG_PATH="$config_file"
runtime_config=1
fi

prepare_openclaw_config_for_write "$config_file" "$hash_file"
Expand All @@ -1698,12 +1734,16 @@ prefix = "openshell:resolve:env:"
alias_marker = "-OPENSHELL-RESOLVE-ENV-"
keys = os.environ.get("NEMOCLAW_PROVIDER_PLACEHOLDER_KEYS", "").split()
replacements = {}
alias_replacements = {}
warnings = []

for key in keys:
value = os.environ.get(key, "")
if value.startswith(prefix) and value != f"{prefix}{key}":
replacements[f"{prefix}{key}"] = (key, value)
suffix = value[len(prefix) :]
if re.fullmatch(rf"v[0-9]+_{re.escape(key)}", suffix):
alias_replacements[key] = suffix

with open(config_file, encoding="utf-8") as f:
config = json.load(f)
Expand All @@ -1722,6 +1762,17 @@ replacement_patterns = [
(re.compile(re.escape(old) + r"(?![A-Za-z0-9_])"), key, new)
for old, (key, new) in sorted(replacements.items(), key=lambda kv: -len(kv[0]))
]
alias_replacement_patterns = [
(
re.compile(
re.escape(alias_marker)
+ rf"(?:v[0-9]+_)?{re.escape(key)}(?![A-Za-z0-9_])"
),
key,
f"{alias_marker}{suffix}",
)
for key, suffix in alias_replacements.items()
]


def rewrite(value):
Expand All @@ -1731,6 +1782,11 @@ def rewrite(value):
if count:
refreshed.add(key)
value = updated
for pattern, key, new in alias_replacement_patterns:
updated, count = pattern.subn(new, value)
if count:
refreshed.add(key)
value = updated
return value
if isinstance(value, list):
return [rewrite(item) for item in value]
Expand Down Expand Up @@ -1784,7 +1840,7 @@ def walk_for_warnings(value, path):
alias_env_key = value[alias_index + len(alias_marker) :]
token_scheme = value[:alias_index] + "-"
for env_key in keys:
if env_key != alias_env_key:
if not placeholder_suffix_matches_env_key(alias_env_key, env_key):
continue
label = path_label(path)
env_value = os.environ.get(env_key, "")
Expand All @@ -1795,7 +1851,16 @@ def walk_for_warnings(value, path):
warnings.append(
f"[channels] {label} expects the {env_key} provider placeholder but it is missing from the runtime environment"
)
elif not placeholder_re.match(env_value) and not env_value.startswith(token_scheme):
elif placeholder_re.match(env_value):
expected = (
f"{token_scheme}OPENSHELL-RESOLVE-ENV-"
f"{env_value[len(prefix):]}"
)
if value != expected:
warnings.append(
f"[channels] {label} placeholder does not match the OpenShell runtime placeholder for {env_key}"
)
elif not env_value.startswith(token_scheme):
warnings.append(
f"[channels] {label} runtime {env_key} is neither the {env_key} OpenShell placeholder nor a {token_scheme} token; runtime may reject it"
)
Expand Down Expand Up @@ -1828,7 +1893,8 @@ PYPLACEHOLDERS
local _refreshed_keys
_refreshed_keys="$(printf '%s\n' "$_placeholder_report" | sed -n 's/^refreshed=//p' | tail -n 1)"
if [ -n "$_refreshed_keys" ]; then
if (cd /sandbox/.openclaw && sha256sum openclaw.json >"$hash_file"); then
if [ "$runtime_config" -eq 1 ] \
|| (cd /sandbox/.openclaw && sha256sum openclaw.json >"$hash_file"); then
printf '[config] Refreshed provider placeholders from OpenShell runtime env: %s\n' "$_refreshed_keys" >&2
else
_write_rc=$?
Expand Down Expand Up @@ -2067,11 +2133,20 @@ import sys
with open(sys.argv[1], encoding="utf-8") as handle:
plan = json.load(handle)
for alias in plan.get("envAliases", []):
if not re.search(alias["match"], os.environ.get(alias["envKey"], "")):
env_key = alias["envKey"]
runtime_value = os.environ.get(env_key, "")
if not re.search(alias["match"], runtime_value):
continue
value = alias["value"]
revision = re.fullmatch(
rf"openshell:resolve:env:(v[0-9]+_){re.escape(env_key)}", runtime_value
)
marker = f"-OPENSHELL-RESOLVE-ENV-{env_key}"
if revision and value.endswith(marker):
value = value[: -len(env_key)] + revision.group(1) + env_key
print("\t".join([
alias["envKey"],
alias["value"],
env_key,
value,
alias.get("message", ""),
]))
PYMESSAGINGALIASES
Expand Down
Loading
Loading