Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b5f067a
refactor(messaging): migrate channel lifecycle hooks
sandl99 Jun 12, 2026
b99378a
refactor(messaging): finish manifest channel migration
sandl99 Jun 12, 2026
067ad60
fix(messaging): address manifest hook review feedback
sandl99 Jun 12, 2026
f761d0b
refactor(messaging): simplify channel hook manifests
sandl99 Jun 13, 2026
34ab674
Merge remote-tracking branch 'origin/main' into refactor/messaging-ch…
sandl99 Jun 13, 2026
0de062b
refactor(messaging): colocate wechat host qr login
sandl99 Jun 13, 2026
59cf532
fix(messaging): address metadata review comments
sandl99 Jun 13, 2026
5c71c1f
refactor(messaging): compact persisted plan state
sandl99 Jun 13, 2026
f37ff32
Merge remote-tracking branch 'origin/main' into refactor/messaging-ch…
sandl99 Jun 14, 2026
cbe09ef
fix(messaging): preserve persisted channel plan fields
sandl99 Jun 14, 2026
72ccad9
fix(messaging): bake runtime setup artifact
sandl99 Jun 14, 2026
c02fedb
test(e2e): align WhatsApp preload guard assertion
sandl99 Jun 14, 2026
f7c0b68
Merge branch 'main' into refactor/messaging-channel-manifest-stragglers
sandl99 Jun 14, 2026
75075b0
refactor(messaging): move Slack deny feedback into runtime preload
sandl99 Jun 14, 2026
e497815
refactor(messaging): install plugins from package plan
sandl99 Jun 14, 2026
a465123
fix(messaging): validate plan object array entries
sandl99 Jun 14, 2026
3b46a32
Merge remote-tracking branch 'origin/refactor/messaging-channel-manif…
sandl99 Jun 14, 2026
848c9ec
refactor(messaging): move runtime preload sources to ts
sandl99 Jun 14, 2026
9fc3141
fix(messaging): patch Slack ESM helper imports
sandl99 Jun 15, 2026
f7ba9f7
fix(messaging): type channel diagnostics preloads
sandl99 Jun 15, 2026
c535a6e
fix(messaging): compile runtime preloads for sandbox image
sandl99 Jun 15, 2026
9b85f89
fix(messaging): compile runtime preloads narrowly
sandl99 Jun 15, 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
26 changes: 13 additions & 13 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
allowed_jobs=""
free_standing_scenarios_csv=""
free_standing_scenario_jobs_csv=""
declare -A seen_inventory_keys=()
seen_inventory_keys=","
while IFS= read -r line || [ -n "${line}" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
Expand All @@ -73,11 +73,11 @@ jobs:
fi
inventory_key="${BASH_REMATCH[1]}"
inventory_value="${BASH_REMATCH[2]}"
if [[ -n "${seen_inventory_keys[${inventory_key}]:-}" ]]; then
if [[ "${seen_inventory_keys}" == *",${inventory_key},"* ]]; then
echo "::error::free-standing workflow inventory must not redefine ${inventory_key}" >&2
exit 1
fi
seen_inventory_keys["${inventory_key}"]=1
seen_inventory_keys="${seen_inventory_keys}${inventory_key},"
case "${inventory_key}" in
allowed_jobs) allowed_jobs="${inventory_value}" ;;
free_standing_scenarios_csv) free_standing_scenarios_csv="${inventory_value}" ;;
Expand All @@ -90,34 +90,34 @@ jobs:
exit 1
fi
done
declare -A seen_allowed_jobs=()
seen_allowed_jobs=","
IFS=',' read -r -a allowed_job_entries <<< "${allowed_jobs}"
for job in "${allowed_job_entries[@]}"; do
if [[ ! "${job}" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "::error::free-standing workflow inventory contains invalid job id" >&2
exit 1
fi
if [[ -n "${seen_allowed_jobs[${job}]:-}" ]]; then
if [[ "${seen_allowed_jobs}" == *",${job},"* ]]; then
echo "::error::free-standing workflow inventory repeats job id" >&2
exit 1
fi
seen_allowed_jobs["${job}"]=1
seen_allowed_jobs="${seen_allowed_jobs}${job},"
done
declare -A seen_free_standing_scenarios=()
seen_free_standing_scenarios=","
IFS=',' read -r -a free_standing_scenario_entries <<< "${free_standing_scenarios_csv}"
for scenario in "${free_standing_scenario_entries[@]}"; do
if [[ ! "${scenario}" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "::error::free-standing workflow inventory contains invalid scenario id" >&2
exit 1
fi
if [[ -n "${seen_free_standing_scenarios[${scenario}]:-}" ]]; then
if [[ "${seen_free_standing_scenarios}" == *",${scenario},"* ]]; then
echo "::error::free-standing workflow inventory repeats scenario id" >&2
exit 1
fi
seen_free_standing_scenarios["${scenario}"]=1
seen_free_standing_scenarios="${seen_free_standing_scenarios}${scenario},"
done
IFS=',' read -r -a scenario_job_entries <<< "${free_standing_scenario_jobs_csv}"
declare -A seen_scenario_mappings=()
seen_scenario_mappings=","
derived_free_standing_scenarios=()
for entry in "${scenario_job_entries[@]}"; do
if [[ ! "${entry}" =~ ^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$ ]]; then
Expand All @@ -126,12 +126,12 @@ jobs:
fi
scenario="${entry%%:*}"
job="${entry#*:}"
if [[ -n "${seen_scenario_mappings[${scenario}]:-}" ]]; then
if [[ "${seen_scenario_mappings}" == *",${scenario},"* ]]; then
echo "::error::free-standing workflow inventory repeats scenario mapping" >&2
exit 1
fi
seen_scenario_mappings["${scenario}"]=1
if [[ -z "${seen_allowed_jobs[${job}]:-}" ]]; then
seen_scenario_mappings="${seen_scenario_mappings}${scenario},"
if [[ "${seen_allowed_jobs}" != *",${job},"* ]]; then
echo "::error::Free-standing scenario maps to unknown job" >&2
exit 1
fi
Expand Down
46 changes: 26 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ COPY nemoclaw/src/ /opt/nemoclaw/src/
WORKDIR /opt/nemoclaw
RUN npm ci && npm run build

# Stage 2: Runtime image — pull cached base from GHCR
# Stage 2: Build TypeScript messaging runtime preloads.
FROM builder AS runtime-preload-builder
WORKDIR /opt/nemoclaw-root
COPY tsconfig.runtime-preloads.json /opt/nemoclaw-root/
COPY src/lib/messaging/channels/ /opt/nemoclaw-root/src/lib/messaging/channels/
RUN ln -s /opt/nemoclaw/node_modules /opt/nemoclaw-root/node_modules \
&& /opt/nemoclaw/node_modules/.bin/tsc -p tsconfig.runtime-preloads.json

# Stage 3: Runtime image — pull cached base from GHCR
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
ARG OPENCLAW_VERSION=2026.5.27
Expand Down Expand Up @@ -92,10 +100,8 @@ ENV NPM_CONFIG_AUDIT=false \
RUN npm ci --omit=dev
COPY scripts/patch-openclaw-tool-catalog.js /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.js
COPY scripts/patch-openclaw-chat-send.js /usr/local/lib/nemoclaw/patch-openclaw-chat-send.js
COPY scripts/patch-openclaw-slack-deny-feedback.mts /usr/local/lib/nemoclaw/patch-openclaw-slack-deny-feedback.mts
RUN chmod 755 /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.js \
/usr/local/lib/nemoclaw/patch-openclaw-chat-send.js \
/usr/local/lib/nemoclaw/patch-openclaw-slack-deny-feedback.mts
/usr/local/lib/nemoclaw/patch-openclaw-chat-send.js

# Upgrade OpenClaw if the base image is stale.
#
Expand Down Expand Up @@ -528,8 +534,11 @@ COPY scripts/lib/clean_runtime_shell_env_shim.py /usr/local/lib/nemoclaw/clean_r
COPY scripts/nemoclaw-start.sh /usr/local/bin/nemoclaw-start
# Copy NODE_OPTIONS preload modules to a Landlock-accessible path. OpenShell ≥0.0.36
# blocks /opt/nemoclaw-blueprint/ from non-root users, but the entrypoint
# needs to read these files to install runtime preloads under /tmp.
# needs to read these files to install Node runtime preloads under /tmp.
# Channel runtime preloads are authored as TypeScript and compiled in the
# runtime-preload-builder stage before being flattened by filename for --require.
COPY nemoclaw-blueprint/scripts/*.js /usr/local/lib/nemoclaw/preloads/
COPY --from=runtime-preload-builder /opt/nemoclaw-root/dist/lib/messaging/channels/ /usr/local/lib/nemoclaw/preloads-compiled-channels/
COPY scripts/codex-acp-wrapper.sh /usr/local/bin/nemoclaw-codex-acp
COPY scripts/generate-openclaw-config.mts /scripts/generate-openclaw-config.mts
COPY src/lib/messaging/ /src/lib/messaging/
Expand All @@ -541,6 +550,11 @@ RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-codex-acp \
&& chmod -R a+rX /src/lib/messaging \
&& chmod 644 /usr/local/lib/nemoclaw/openclaw_device_approval_policy.py \
/usr/local/lib/nemoclaw/clean_runtime_shell_env_shim.py \
&& if [ -d /usr/local/lib/nemoclaw/preloads-compiled-channels ]; then \
find /usr/local/lib/nemoclaw/preloads-compiled-channels -path '*/runtime/*.js' -type f \
-exec sh -c 'for file do cp "$file" "/usr/local/lib/nemoclaw/preloads/$(basename "$file")"; done' sh {} +; \
fi \
&& rm -rf /usr/local/lib/nemoclaw/preloads-compiled-channels \
&& if [ -d /usr/local/lib/nemoclaw/preloads ]; then find /usr/local/lib/nemoclaw/preloads -type f -name '*.js' -exec chmod 644 {} +; fi \
&& chmod 755 /usr/local/share/nemoclaw \
/usr/local/share/nemoclaw/openclaw-plugins \
Expand Down Expand Up @@ -644,6 +658,13 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME=${NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME} \
NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE=${NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE}

# Bake reduced messaging runtime metadata for the entrypoint. The full
# NEMOCLAW_MESSAGING_PLAN_B64 is a build input; OpenShell sandbox create only
# forwards explicit runtime env, so nemoclaw-start reads this generic artifact
# when the env plan is absent.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent openclaw --phase runtime-setup

WORKDIR /sandbox
USER sandbox

Expand Down Expand Up @@ -687,21 +708,6 @@ RUN set -eu; \
# hadolint ignore=DL3059,DL4006
RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent openclaw --phase agent-install

# Patch the OpenClaw Slack channel (@openclaw/slack) so a denied explicit
# @-mention still blocks the command but sends one bounded sender-facing
# feedback message instead of dropping silently (NemoClaw #4752). The script
# classifies the installed Slack dist by content signature, fails the build if
# a @openclaw/slack package is present but the deny path shape is unrecognized,
# and is a no-op when the Slack channel is not enabled for this image.
# Scoped to the sandbox-writable OpenClaw config dir: `openclaw plugins install`
# stages external channel packages under $HOME/.openclaw/npm, and this step runs
# as the sandbox user, so do not scan the root-owned global node_modules tree.
# Removal criteria: drop when upstream OpenClaw notifies the sender on a denied
# explicit Slack @-mention, or when NemoClaw no longer ships @openclaw/slack.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /usr/local/lib/nemoclaw/patch-openclaw-slack-deny-feedback.mts \
/sandbox/.openclaw

# Lock down npm for the next RUN: the local OpenClaw plugin install must
# resolve from /opt/nemoclaw and the staged plugin-runtime-deps tree without
# touching the registry. Reset to false after that RUN so the runtime image
Expand Down
4 changes: 4 additions & 0 deletions ci/env-var-doc-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"name": "NEMOCLAW_TEST_NO_SLEEP",
"reason": "Test sentinel that bypasses real-time sleep() calls in onboard inference probes. Set to '1' only by Vitest tests; never user-set."
},
{
"name": "NEMOCLAW_TELEGRAM_STARTUP_GRACE_MS",
"reason": "Internal Vitest-only override that shortens the Telegram diagnostics startup-grace timer. Production uses the built-in default."
},
{
"name": "NEMOCLAW_E2E_FAILURE_INJECTION",
"reason": "Internal E2E-only sentinel that enables deterministic onboarding fault injection for resume/repair scripts. Never user-set in production."
Expand Down
4 changes: 2 additions & 2 deletions ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"test/channels-add-preset.test.ts": 1871,
"test/generate-openclaw-config.test.ts": 1989,
"test/install-preflight.test.ts": 4207,
"test/nemoclaw-start.test.ts": 5230,
"test/nemoclaw-start.test.ts": 5162,
"test/onboard-messaging.test.ts": 2063,
"test/onboard-selection.test.ts": 6891,
"test/onboard-selection.test.ts": 6888,
"test/onboard.test.ts": 4774,
"test/policies.test.ts": 2753
}
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw-blueprint/policies/presets/wechat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# IDC host either bridge can hit must be listed explicitly here.
#
# Known hosts (extend when an operator observes a new IDC redirect):
# - ilinkai.weixin.qq.com bootstrap; hard-coded in src/ext/wechat/qr.ts
# - ilinkai.weixin.qq.com bootstrap; hard-coded in src/lib/messaging/channels/wechat/qr.ts
# and Hermes' WEIXIN_BASE_URL default per
# hermes-agent docs/messaging/weixin
# - ilinkai.wechat.com per-account baseUrl returned after QR confirm
Expand Down
116 changes: 0 additions & 116 deletions nemoclaw-blueprint/scripts/slack-channel-guard.js

This file was deleted.

Loading
Loading