Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d60ab8b
feat(messaging): apply provider lifecycle through adapter
rsliter Sep 2, 2026
b6982da
fix(messaging): tighten provider lifecycle boundaries
rsliter Sep 2, 2026
60ac1b0
merge: integrate main for messaging lifecycle
rsliter Sep 3, 2026
5c93fde
fix(messaging): complete provider lifecycle recovery
rsliter Sep 3, 2026
93c228a
fix(messaging): reconcile lifecycle failures
rsliter Sep 3, 2026
9d15de6
merge: integrate latest main for messaging lifecycle
rsliter Sep 3, 2026
ebf5412
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
c9ffb24
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
f7de128
fix(messaging): finish typed provider lifecycle cutover
rsliter Sep 4, 2026
35937fc
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
28ee5e3
fix(messaging): finish typed lifecycle migration
rsliter Sep 4, 2026
91f9d56
fix(messaging): address exact-head review
rsliter Sep 4, 2026
dc008ef
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
afbbe33
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
d4256c2
docs(messaging): clarify re-add recovery
rsliter Sep 4, 2026
6cc2dac
fix(messaging): redact rejected refresh errors
rsliter Sep 4, 2026
118eb30
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
ae6628b
docs(messaging): align provider recovery guidance
rsliter Sep 4, 2026
c5c741a
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
82892e1
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
8e98b67
docs(messaging): qualify channel add idempotency
rsliter Sep 4, 2026
887229c
fix(messaging): preserve typed replacement receipts
rsliter Sep 4, 2026
32ebf04
fix(messaging): remove inert lifecycle options
rsliter Sep 4, 2026
a20fa6e
fix(messaging): authorize channel provider replacement
rsliter Sep 4, 2026
1eef94f
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
7636fbd
docs(messaging): document provider replacement authority
rsliter Sep 4, 2026
92677c0
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
70852d7
fix(messaging): remove unreachable cleanup receipt
rsliter Sep 4, 2026
9a21307
docs(messaging): clarify inspection recovery
rsliter Sep 4, 2026
1560442
docs(messaging): render recovery launcher
rsliter Sep 4, 2026
309226c
test(messaging): reject invalid refresh material
rsliter Sep 4, 2026
7aaeffe
fix(messaging): preserve replacement recovery evidence
rsliter Sep 4, 2026
2afc29f
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
a07537e
merge: refresh main for messaging provider lifecycle
rsliter Sep 4, 2026
ea4eab1
docs(messaging): qualify credential rotation
rsliter Sep 4, 2026
65da334
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 4, 2026
249cc66
merge: resolve conflicts with main
github-actions[bot] Sep 4, 2026
76ecb58
fix(messaging): preserve lifecycle ownership
rsliter Sep 4, 2026
5a87303
docs(messaging): complete residual-provider recovery
rsliter Sep 4, 2026
1259d50
fix(messaging): restore provider attachment on cleanup failure
rsliter Sep 5, 2026
b10001d
merge: main into messaging provider lifecycle
rsliter Sep 5, 2026
389a506
test(onboard): isolate messaging lifecycle fixtures
rsliter Sep 5, 2026
352e059
docs(channels): clarify force credential limits
rsliter Sep 5, 2026
cf493b3
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 6, 2026
b60d3a3
Merge remote-tracking branch 'origin/main' into codex/9806-messaging-…
rsliter Sep 7, 2026
fc28484
fix(messaging): continue attachment recovery after rejection
rsliter Sep 7, 2026
28b1059
fix(messaging): restore replacement attachments
rsliter Sep 7, 2026
472733d
docs(messaging): clarify Google Chat cleanup state
rsliter Sep 7, 2026
fdc1e45
merge: resolve conflicts with main
github-actions[bot] Sep 7, 2026
7356767
merge: refresh canonical main
rsliter Sep 7, 2026
c141412
fix(messaging): reconcile refresh and detach failures
rsliter Sep 7, 2026
379ab01
merge: refresh canonical main
rsliter Sep 7, 2026
26ba549
merge: refresh canonical main
rsliter Sep 7, 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 ci/e2e-assertion-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
"nodeAssertions": 119,
"namedAssertionHelpers": 930,
"failCalls": 38,
"throwGuards": 638,
"throwGuards": 633,
"objectFieldAssertions": 348,
"assertionPoints": 4411,
"assertionPoints": 4406,
"generatedProbeBlocks": 290,
"generatedProbeConditions": 975
},
Expand All @@ -52,7 +52,7 @@
"test/e2e/live/brave-search.test.ts": [19,22,27,32,1],
"test/e2e/live/brev-workspace-cleanup.test.ts": [0,0,0,1,0],
"test/e2e/live/channels-add-remove.test.ts": [40,93,40,93,0],
"test/e2e/live/channels-stop-start.test.ts": [0,0,36,135,9],
"test/e2e/live/channels-stop-start.test.ts": [0,0,36,130,9],
"test/e2e/live/cloud-inference.test.ts": [12,18,12,18,1],
"test/e2e/live/cloud-onboard.test.ts": [23,25,23,25,1],
"test/e2e/live/common-egress-agent.test.ts": [26,66,27,74,5],
Expand Down
4 changes: 2 additions & 2 deletions ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"src/lib/core/json-types.ts": 34,
"src/lib/core/ports.ts": 89,
"src/lib/core/shell-quote.ts": 27,
"src/lib/core/url-utils.ts": 29,
"src/lib/core/url-utils.ts": 27,
"src/lib/core/wait.ts": 30,
"src/lib/credentials/store.ts": 45,
"src/lib/inference/config.ts": 30,
Expand Down Expand Up @@ -60,7 +60,7 @@
},
"allowedCycles": [],
"maxRootFiles": {
"src/lib/onboard": 301,
"src/lib/onboard": 300,
"src/lib/actions": 18,
"src/lib/actions/sandbox": 177,
"src/lib/state": 38,
Expand Down
25 changes: 19 additions & 6 deletions docs/manage-sandboxes/add-channels-after-onboarding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ List the supported channel names:
$$nemoclaw my-assistant channels list
```

<Warning>
`channels add` can replace a mismatched provider when complete credentials are available.
Replacement is allowed only when the provider is unattached or attached only to the selected sandbox.
An attachment to another sandbox stops the operation.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
The `--force` flag does not override this provider attachment authority.
</Warning>

Add the channel you want:

```bash
Expand Down Expand Up @@ -64,15 +71,21 @@ It uses Pub/Sub REST pull and does not create or configure a public webhook endp
A missing or malformed preset aborts before token prompts, registry writes, or the rebuild prompt, so the sandbox never advertises a channel without matching policy.
With the preset present, the command applies it before rebuild.

When policy application fails after a fresh add writes registry state, NemoClaw attempts to roll back the bridge providers, `messagingChannels` entry, and staged environment credentials.
OpenShell provider inspection uncertainty stops the operation before provider mutation.
On a fresh add, NemoClaw may already have applied the credential-free policy preset and attempts to remove it before exiting.
If that policy cleanup fails, rebuild the sandbox first, then run the `$$nemoclaw <name> policy remove <channel>` command printed by the CLI.

When final policy application fails after provider registration during a fresh add, NemoClaw attempts to roll back the bridge providers, preset, and staged environment credentials.
The registry has not been updated at that point.
It exits without prompting for a rebuild.
If a gateway-side cleanup step fails, rollback continues and prints `Rollback could not fully clean <surfaces>` so you can clean up manually.

When the same failure happens while re-adding an enabled channel, NemoClaw restores the prior `messagingChannels` entry, staged environment credentials when available, registry credential hashes, and prior bridge providers.
It flags `gateway-providers` as residual because the in-flight upsert can leave the gateway with the new token.

Verify the gateway bridge before relying on the channel.
Restore the preset YAML and rerun the original command.
When the same failure happens while re-adding an enabled channel, NemoClaw restores the prior `messagingChannels` entry, staged environment credentials when available, and registry credential hashes.
An existing provider can be changed during re-add.
NemoClaw attempts to restore stored provider credentials, but it cannot restore prior gateway refresh material that was available only during the earlier setup command.
NemoClaw flags `gateway-providers` as residual.
Inspect the channel's gateway providers, restore the preset YAML, and correct the gateway failure.
When the gateway is reachable, run the `$$nemoclaw <name> channels remove <channel>` command printed by the CLI, then rerun `$$nemoclaw <name> channels add <channel>`.

Choose the rebuild so the running sandbox image picks up the new channel.
For Telegram, Discord, and Slack, `channels add` checks the rebuilt runtime for the selected bridge and reports startup, credential, or missing-plugin warnings before returning.
Expand Down
13 changes: 10 additions & 3 deletions docs/manage-sandboxes/manage-messaging-channels.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ Use host-side channel commands to change a configured messaging channel.

## Rotate Credentials

Running `channels add` for a channel that is already configured overwrites stored tokens and registers the updated bridge provider. Rebuild the sandbox after the update so the image reflects the current channel set.
A successful `channels add` for a channel that is already configured updates the stored tokens and bridge provider.
Provider replacement requires complete credentials and is refused when the provider is attached to another sandbox.
If re-add fails after changing the provider, follow the [channel-add recovery guidance](add-channels-after-onboarding#apply-policy-and-rebuild) for residual gateway-provider state.
Rebuild the sandbox after a successful update so the image reflects the current channel set.

For WeChat, the cached-token shortcut applies. Remove WeChat first when you intend to acquire a fresh account through a new QR scan.

For Google Chat, re-add the channel and paste the replacement service-account JSON. NemoClaw updates the gateway-side refresh material. The sandbox keeps only the OpenShell credential placeholder. OpenShell keeps refreshed access tokens at the gateway and substitutes them at approved egress boundaries.
For Google Chat, re-add the channel and paste the replacement service-account JSON. On success, NemoClaw updates the gateway-side refresh material. The sandbox keeps only the OpenShell credential placeholder. OpenShell keeps refreshed access tokens at the gateway and substitutes them at approved egress boundaries.

<AgentOnly variant="hermes">
Re-adding Google Chat prompts again for the project ID, complete Pub/Sub subscription name, and
Expand All @@ -46,7 +49,11 @@ $$nemoclaw my-assistant channels remove teams

`channels remove wechat` clears the bot token, deletes the `<sandbox>-wechat-bridge` provider, and removes `wechat` from the enabled-channel set. The next rebuild omits WeChat configuration and per-account state files.

`channels remove googlechat` detaches and deletes the `<sandbox>-googlechat-bridge` provider before the rebuild removes Google Chat configuration and the matching policy preset.
`channels remove googlechat` disables the channel, removes its agent configuration and matching policy preset, and then detaches and deletes the `<sandbox>-googlechat-bridge` provider.
If deletion fails after detachment, NemoClaw attempts to restore the provider attachment.
If restoration also fails, the command names the provider and sandbox and warns that delivery may remain interrupted.
The credentials remain, but the channel stays disabled with provider cleanup pending and its policy already removed.
Resolve the gateway error, then rerun `$$nemoclaw <name> channels remove googlechat`.

<AgentOnly variant="openclaw">
It also stops the dedicated cloudflared process and webhook proxy for that sandbox.
Expand Down
35 changes: 32 additions & 3 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,36 @@ Register a messaging channel with the sandbox and rebuild so the image picks up

Google Chat uses an experimental token-paste flow for the service-account JSON on both agents. OpenClaw also requires interactive public-webhook confirmation and prompts for optional app-principal configuration during enrollment. Hermes instead prompts for the Google Cloud project ID, complete Pub/Sub subscription name, and email sender allowlist. It pulls inbound events from Pub/Sub over REST and does not create a public webhook endpoint.

After registering the channel, NemoClaw asks whether to rebuild immediately. Running `add` for an already-configured channel overwrites the stored credentials where applicable. The operation is idempotent. Static channel credentials use a validated endpointless OpenShell provider profile so the gateway can replace their sandbox placeholders. NemoClaw validates the existing profile, provider type, and credential keys before it reuses a provider. A missing, malformed, conflicting, or incompatible provider state stops the operation before reuse. Hermes Discord uses its dedicated static provider type because its policy binds the Discord API and Gateway endpoints. Channel names are trimmed and lowercased before NemoClaw stores credentials, names bridge providers, or prints rebuild messages. NemoClaw requires the matching built-in network policy preset YAML to be present. A missing or malformed preset YAML (no `network_policies:` section) aborts `channels add` before any token prompt, registry write, or rebuild prompt. After validating that preset, NemoClaw discloses its effect before prompting for credentials or changing gateway or registry state. It prints the exact effective egress scope when the preset would open or replace access, or reports that no new egress would be opened when the preset is already effective. The `--dry-run` path prints the same disclosure without collecting credentials or applying changes. With the preset file in place, NemoClaw applies it to the sandbox before the rebuild so the bridge has egress to its upstream API. When the apply step itself fails after the registry write on a fresh add, NemoClaw attempts to roll back the bridge providers, the `messagingChannels` entry, and any staged environment credentials, then exits without prompting for a rebuild; if any gateway-side step (provider detach or delete) fails the rollback continues and prints a `Rollback could not fully clean <surfaces>` warning so the operator can clean up manually. When the same failure happens on a re-add of an already-enabled channel, NemoClaw restores the prior `messagingChannels` entry, restores staged environment credentials when available, restores registry credential hashes, and attempts to re-upsert the prior bridge providers, but flags `gateway-providers` as residual because the in-flight upsert may have left the gateway with the new token; verify the gateway bridge before relying on the channel. Restore the preset YAML and re-run `$$nemoclaw <name> channels add <channel>`. For Telegram, Discord, and Slack, a rebuild triggered by `channels add` also verifies that the selected bridge starts and reports credential, startup, or plugin discovery warnings.
After registering the channel, NemoClaw asks whether to rebuild immediately.
Running `add` for an already-configured channel overwrites the stored credentials where applicable.
Repeating a successful add with identical inputs is idempotent.
A failed re-add can leave residual gateway-provider state when prior refresh material cannot be restored.
Static channel credentials use a validated endpointless OpenShell provider profile so the gateway can replace their sandbox placeholders.
NemoClaw validates the existing profile, provider type, and credential keys before it reuses a provider.
A missing provider is created when complete credentials are available.
OpenShell provider inspection uncertainty stops the operation before provider mutation.
On a fresh add, NemoClaw may already have applied the credential-free policy preset and attempts to remove it before exiting.
If that policy cleanup fails, rebuild the sandbox first, then run the `$$nemoclaw <name> policy remove <channel>` command printed by the CLI.
With complete credentials, NemoClaw replaces a mismatched provider only when it is unattached or attached exclusively to the selected sandbox.
An attachment to another sandbox stops replacement.
The `--force` flag does not override this provider attachment authority.
Hermes Discord uses its dedicated static provider type because its policy binds the Discord API and Gateway endpoints.
Channel names are trimmed and lowercased before NemoClaw stores credentials, names bridge providers, or prints rebuild messages.
NemoClaw requires the matching built-in network policy preset YAML to be present.
A missing or malformed preset YAML (no `network_policies:` section) aborts `channels add` before any token prompt, registry write, or rebuild prompt.
After validating that preset, NemoClaw discloses its effect before prompting for credentials or changing gateway or registry state.
It prints the exact effective egress scope when the preset would open or replace access, or reports that no new egress would be opened when the preset is already effective.
The `--dry-run` path prints the same disclosure without collecting credentials or applying changes.
With the preset file in place, NemoClaw applies it to the sandbox before the rebuild so the bridge has egress to its upstream API.
When final policy application fails after provider registration during a fresh add, NemoClaw attempts to roll back the bridge providers, preset, and staged environment credentials before registry state is written, then exits without prompting for a rebuild.
If any gateway-side step fails, rollback continues and prints a `Rollback could not fully clean <surfaces>` warning so the operator can clean up manually.
When the same failure happens on a re-add of an already-enabled channel, NemoClaw restores the prior `messagingChannels` entry, staged environment credentials when available, and registry credential hashes.
An existing provider can be changed during re-add.
NemoClaw attempts to restore stored provider credentials, but it cannot restore prior gateway refresh material that was available only during the earlier setup command.
It flags `gateway-providers` as residual.
Inspect the channel's gateway providers, restore the preset YAML, and correct the gateway failure.
When the gateway is reachable, run the `$$nemoclaw <name> channels remove <channel>` command printed by the CLI, then rerun `$$nemoclaw <name> channels add <channel>`.
For Telegram, Discord, and Slack, a rebuild triggered by `channels add` also verifies that the selected bridge starts and reports credential, startup, or plugin discovery warnings.

```bash
$$nemoclaw my-assistant channels add telegram
Expand All @@ -2168,13 +2197,13 @@ $$nemoclaw my-assistant channels add telegram
| Flag | Description |
| --- | --- |
| `--dry-run` | Validate the channel name and matching policy preset without prompting for credentials, contacting the gateway, or rebuilding |
| `--force` | Add the channel despite a credential conflict, shared-resource conflict, or incomplete required check. This flag is the only conflict override. |
| `--force` | Add the channel despite a credential conflict, shared-resource conflict, or another forceable check. This flag does not bypass missing or partial credentials or authorize replacing a provider attached to another sandbox. |

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn. The conflict check compares only sandboxes in the selected OpenShell gateway's sandbox registry. It cannot detect Slack token reuse across independent OpenShell gateways. Run only one active Slack sandbox on each OpenShell gateway, and use distinct Slack bot and app tokens across gateways. Optional Slack allowlists come from `SLACK_ALLOWED_USERS` and `SLACK_ALLOWED_CHANNELS` at rebuild time. Telegram and Discord mention mode default to `1` when no environment, session, or saved state value exists for that setting. Discord applies that default only when a server ID is configured. A run with `NEMOCLAW_NON_INTERACTIVE=1`, or a run without a terminal on stdin, fails fast on any missing token and shows no rebuild prompt. Instead, the change is queued and you are told to run `$$nemoclaw <name> rebuild` manually. An SSH command without `-t`, a service unit, or a CI job has no terminal on stdin, so it does not need `NEMOCLAW_NON_INTERACTIVE=1` to reach this path. If you omit the required `<channel>` argument, the CLI prints the `channels add <channel>` usage with the supported channel list instead of falling back to top-level help.

### `$$nemoclaw <name> channels remove <channel>`

Clear the stored credentials for a messaging channel and rebuild the sandbox so the image drops the channel. Running `remove` for a channel that was never configured is a no-op against the credentials file and still triggers the rebuild prompt. When the bridge provider is attached to a live sandbox, NemoClaw detaches it before deleting the provider from the OpenShell gateway. If the matching built-in policy preset is applied, NemoClaw also removes that preset from the current OpenShell policy so the upstream API is no longer allow-listed after the channel is gone. No session preset list exists to synchronize.
Clear the stored credentials for a messaging channel and rebuild the sandbox so the image drops the channel. Running `remove` for a channel that was never configured is a no-op against the credentials file and still triggers the rebuild prompt. When the bridge provider is attached to a live sandbox, NemoClaw detaches it before deleting the provider from the OpenShell gateway. If deletion fails, NemoClaw attempts to restore the attachment before returning the failure. If restoration also fails, the diagnostic names the provider and sandbox and warns that channel delivery may remain interrupted. If the matching built-in policy preset is applied, NemoClaw also removes that preset from the current OpenShell policy so the upstream API is no longer allow-listed after the channel is gone. No session preset list exists to synchronize.

For QR-paired channels (today: WhatsApp), NemoClaw destructively clears the in-sandbox session directories before the rebuild so the `state_dirs` backup does not restore the auth blob and let the channel reconnect:

Expand Down
8 changes: 8 additions & 0 deletions src/lib/actions/credentials-provider-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ function providerAdapter(
const detachProvider: OpenShellProviderAdapter["detachProvider"] = async () => ({
ok: true,
});
const attachProvider: OpenShellProviderAdapter["attachProvider"] = async () => ({ ok: true });
const configureProviderRefresh: OpenShellProviderAdapter["configureProviderRefresh"] =
async () => ({ ok: true });
const getProviderRefreshStatus: OpenShellProviderAdapter["getProviderRefreshStatus"] =
async () => ({ ok: true, value: { status: "refreshed" } });
return {
listProviders: vi.fn(listProviders),
createProvider: vi.fn(createProvider),
Expand All @@ -65,6 +70,9 @@ function providerAdapter(
inspectProviderProfile: vi.fn(inspectProviderProfile),
deleteProvider: vi.fn(deleteProvider),
detachProvider: vi.fn(detachProvider),
attachProvider: vi.fn(attachProvider),
configureProviderRefresh: vi.fn(configureProviderRefresh),
getProviderRefreshStatus: vi.fn(getProviderRefreshStatus),
...overrides,
};
}
Expand Down
Loading
Loading