Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,39 @@ It merges the previously separate policy/plugin package, Discord gateway sidecar
- `@openclaw-agent-mesh/runtime-wrapper` — OpenClaw runtime wrapper/plugin integration for sidecar rollout and dry-run safety.
- `@openclaw-agent-mesh/tmux-bridge` — agnostic tmux bridge for CLI-to-CLI agent intercommunication, surfaced in the gateway as the `tmux-transport` adapter (a peer of the Discord adapter). See `docs/transports.md`.

## Agentwheel install
## AgentWheel / OpenPack install

This repo can be installed as an agentwheel package:
This repo is an OpenPack package for AgentWheel. `openpack.json` is the
canonical package manifest; the older `agentwheel.json` manifest is not used.

Install the shared runtime-neutral instructions and tmux skills:

```bash
npm i -g agentwheel
agentwheel registry update
agentwheel add nestdev-mesh --adapter openclaw
agentwheel sync --dry-run
agentwheel sync
agentwheel add nestdev-mesh --adapter codex
agentwheel install --dry-run
agentwheel install
```

OpenClaw runtimes that need the optional mesh plugin should select it
explicitly instead of installing it everywhere:

```bash
agentwheel install nestdev-mesh \
--adapter openclaw \
--select plugins/openclaw-agent-mesh \
--dry-run

# after reviewing the plan:
agentwheel install nestdev-mesh \
--adapter openclaw \
--select plugins/openclaw-agent-mesh
```

The package currently provides the `claude-tmux` and `codex-tmux` skills from
`skills/`. With agentwheel asset-includes, each installed skill also receives
the bridge scripts and agent configs it needs under its own `bin/` and `agents/`
The package provides the `claude-tmux` and `codex-tmux` skills from `skills/`.
With AgentWheel asset-includes, each installed skill also receives the bridge
scripts and agent configs it needs under its own `bin/` and `agents/`
directories. The canonical script source remains `packages/tmux-bridge/bin` in
this repo; no generated script copies are committed.

Expand Down
20 changes: 16 additions & 4 deletions agentwheel.json → openpack.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
{
"schemaVersion": 1,
"name": "nestdev-mesh",
"schemaVersion": 2,
"name": "NestDevLab/agent-mesh",
"version": "0.9.0",
"provides": [
{
"type": "instructions",
"path": "AGENTS.md"
},
{
"type": "skills",
"path": "skills",
"assets": [
{
"from": "packages/tmux-bridge/bin",
"into": "bin",
"include": ["*.sh"],
"include": [
"*.sh"
],
"mode": "preserve"
},
{
"from": "packages/tmux-bridge/agents",
"into": "agents",
"include": ["*.conf"],
"include": [
"*.conf"
],
"mode": "preserve"
}
]
},
{
"type": "plugins",
"path": "plugins"
}
]
}
26 changes: 26 additions & 0 deletions packages/core/src/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,17 @@ export function planDiscordMentionCorrection(config, action, inheritedBase) {
const normalizedTurn = normalizeBridgeTurn(request);
const text = normalizedTurn.text;

if (/^\s*(?:<@!?\d+>\s*)*Controller:/i.test(text)) {
return {
...base,
accepted: true,
reason: "mention_correction_skipped_controller_message",
nextAction: "none",
sideEffectsAllowed: false,
normalizedTurn
};
}

if (cfg.mode === "observe") {
return {
...base,
Expand Down Expand Up @@ -452,6 +463,21 @@ export function planDiscordMentionCorrection(config, action, inheritedBase) {
};
}

// Anti ping-loop guard: the mention-correction bridge used to derive
// Discord pings from free-form bot text. In mesh mode the body is human
// content, not routing. Bot/controller output must never generate fresh
// Discord mentions; only a separately validated routing/envelope layer may.
if (sourceBotId) {
return {
...base,
accepted: true,
reason: "mention_correction_skipped_bot_source",
nextAction: "none",
sideEffectsAllowed: false,
normalizedTurn
};
}

const references = findUntaggedParticipantReferences(cfg.bridge.participants, text, sourceBotId);
if (!references.length) {
return {
Expand Down
41 changes: 33 additions & 8 deletions packages/core/test/policy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ test("address book includes labels, mentions, and aliases", () => {
assert.match(formatAgentAddressBook(entries), /ControllerBot: <@222>/);
});

test("mention correction planner detects natural agent names without real Discord mention", () => {
test("mention correction skips bot-authored natural agent names without creating pings", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
Expand All @@ -277,13 +277,13 @@ test("mention correction planner detects natural agent names without real Discor
}
});
assert.equal(plan.accepted, true);
assert.equal(plan.reason, "mention_correction_required");
assert.equal(plan.nextAction, "send_correction_dry_run");
assert.deepEqual(plan.references, [{ botId: "222", mention: "<@222>", label: "ControllerBot", matchedAlias: "ControllerBot" }]);
assert.match(plan.correctionMessage, /^<@222> Controller:/);
assert.equal(plan.reason, "mention_correction_skipped_bot_source");
assert.equal(plan.nextAction, "none");
assert.equal(plan.references, undefined);
assert.equal(plan.correctionMessage, undefined);
});

test("mention correction planner does not correct already valid mentions or self references", () => {
test("mention correction skips bot-authored mentions and self references", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
Expand All @@ -301,7 +301,7 @@ test("mention correction planner does not correct already valid mentions or self
messageText: "Passo a ControllerBot <@222>."
}
});
assert.equal(alreadyMentioned.reason, "mention_correction_not_needed");
assert.equal(alreadyMentioned.reason, "mention_correction_skipped_bot_source");
assert.equal(alreadyMentioned.nextAction, "none");

const selfReference = planDiscordMentionCorrection(cfg, {
Expand All @@ -311,7 +311,32 @@ test("mention correction planner does not correct already valid mentions or self
messageText: "WorkerAlpha can continue alone."
}
});
assert.equal(selfReference.reason, "mention_correction_not_needed");
assert.equal(selfReference.reason, "mention_correction_skipped_bot_source");
});

test("mention correction skips controller service text without creating pings", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
bridge: {
participants: [
{ botId: "111", mention: "<@111>", label: "Agent Alpha", aliases: ["Agent Alpha"] },
{ botId: "222", mention: "<@222>", label: "Agent Beta", aliases: ["Agent Beta"] }
]
}
};

const plan = planDiscordMentionCorrection(cfg, {
request: {
target: { guildId: "g1", channelId: "c1" },
messageText: "Controller: Agent Gamma named Agent Alpha, Agent Beta without a valid Discord tag. The controller is applying the canonical tag so the turn can continue."
}
});

assert.equal(plan.accepted, true);
assert.equal(plan.reason, "mention_correction_skipped_controller_message");
assert.equal(plan.nextAction, "none");
assert.equal(plan.references, undefined);
});

test("mention correction skips structured event task messages", () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/runtime-wrapper/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,17 @@ export function planDiscordMentionCorrection(config, action, inheritedBase) {
const normalizedTurn = normalizeBridgeTurn(request);
const text = normalizedTurn.text;

if (/^\s*(?:<@!?\d+>\s*)*Controller:/i.test(text)) {
return {
...base,
accepted: true,
reason: "mention_correction_skipped_controller_message",
nextAction: "none",
sideEffectsAllowed: false,
normalizedTurn
};
}

if (cfg.mode === "observe") {
return {
...base,
Expand Down Expand Up @@ -448,6 +459,21 @@ export function planDiscordMentionCorrection(config, action, inheritedBase) {
};
}

// Anti ping-loop guard: the mention-correction bridge used to derive
// Discord pings from free-form bot text. In mesh mode the body is human
// content, not routing. Bot/controller output must never generate fresh
// Discord mentions; only a separately validated routing/envelope layer may.
if (sourceBotId) {
return {
...base,
accepted: true,
reason: "mention_correction_skipped_bot_source",
nextAction: "none",
sideEffectsAllowed: false,
normalizedTurn
};
}

const references = findUntaggedParticipantReferences(cfg.bridge.participants, text, sourceBotId);
if (!references.length) {
return {
Expand Down
41 changes: 33 additions & 8 deletions packages/runtime-wrapper/test/policy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ test("address book includes labels, mentions, and aliases", () => {
assert.match(formatAgentAddressBook(entries), /Agent Beta: <@222>/);
});

test("mention correction planner detects natural agent names without real Discord mention", () => {
test("mention correction skips bot-authored natural agent names without creating pings", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
Expand All @@ -309,13 +309,13 @@ test("mention correction planner detects natural agent names without real Discor
}
});
assert.equal(plan.accepted, true);
assert.equal(plan.reason, "mention_correction_required");
assert.equal(plan.nextAction, "send_correction_dry_run");
assert.deepEqual(plan.references, [{ botId: "222", mention: "<@222>", label: "Agent Beta", matchedAlias: "Agent Beta" }]);
assert.match(plan.correctionMessage, /^<@222> Controller:/);
assert.equal(plan.reason, "mention_correction_skipped_bot_source");
assert.equal(plan.nextAction, "none");
assert.equal(plan.references, undefined);
assert.equal(plan.correctionMessage, undefined);
});

test("mention correction planner does not correct already valid mentions or self references", () => {
test("mention correction skips bot-authored mentions and self references", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
Expand All @@ -333,7 +333,7 @@ test("mention correction planner does not correct already valid mentions or self
messageText: "Passo a Agent Beta <@222>."
}
});
assert.equal(alreadyMentioned.reason, "mention_correction_not_needed");
assert.equal(alreadyMentioned.reason, "mention_correction_skipped_bot_source");
assert.equal(alreadyMentioned.nextAction, "none");

const selfReference = planDiscordMentionCorrection(cfg, {
Expand All @@ -343,7 +343,32 @@ test("mention correction planner does not correct already valid mentions or self
messageText: "Agent Alpha può continuare da solo."
}
});
assert.equal(selfReference.reason, "mention_correction_not_needed");
assert.equal(selfReference.reason, "mention_correction_skipped_bot_source");
});

test("mention correction skips controller service text without creating pings", () => {
const cfg = {
mode: "plan",
discordAllowlist: [{ guildId: "g1", channelId: "c1" }],
bridge: {
participants: [
{ botId: "111", mention: "<@111>", label: "Agent Alpha", aliases: ["Agent Alpha"] },
{ botId: "222", mention: "<@222>", label: "Agent Beta", aliases: ["Agent Beta"] }
]
}
};

const plan = planDiscordMentionCorrection(cfg, {
request: {
target: { guildId: "g1", channelId: "c1" },
messageText: "Controller: Agent Gamma named Agent Alpha, Agent Beta without a valid Discord tag. The controller is applying the canonical tag so the turn can continue."
}
});

assert.equal(plan.accepted, true);
assert.equal(plan.reason, "mention_correction_skipped_controller_message");
assert.equal(plan.nextAction, "none");
assert.equal(plan.references, undefined);
});

test("mention correction skips structured event task messages", () => {
Expand Down
49 changes: 49 additions & 0 deletions plugins/openclaw-agent-mesh/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Agent Mesh OpenClaw Plugin

OpenClaw adapter package for Agent Mesh planning tools.

This package wires the runtime-agnostic core into OpenClaw plugin tools. It is intentionally thin: real deployment-specific participants, channel ids, guild ids, paths, and feature flags should live in private host configuration, not in this package.

## Current boundary

Implemented here:

- OpenClaw plugin registration as `agent-mesh-wrapper`.
- Dry-run planning tools for runtime actions, Discord bridge turns, event-task turns, and Mesh v1 pre-dispatch handling.
- Audit JSONL writes for tool invocations when audit is enabled.
- Generic, config-driven OpenClaw example config.

Not implemented here yet:

- A live inbound Discord interception hook before OpenClaw agent dispatch.
- Durable cross-message partial/final state persistence owned by OpenClaw runtime hooks.
- Automatic pre-dispatch rewrite/injection of assembled Mesh v1 content.
- Live send/forward side effects from the plugin itself.

Until those hooks are wired, treat this package as the safe OpenClaw planning/adapter layer, not as the full live runtime path.

## Mesh v1 finality rule

Complete peer handoffs must use `final=1` in compact `ccm:v1` envelopes. Use `final=0` only for partial context chunks that should be buffered and must not dispatch the receiving agent yet.

The runtime-neutral hydrator defaults compact messages to `final=1`; pass `--final 0` only when deliberately sending a partial chunk:

```bash
node ../../scripts/mesh-hydrate.mjs \
--compact \
--to next-peer \
--from current-peer \
--id smoke-run \
--body "Complete handoff body."
```

## Scripts

```bash
npm run build
```

## Example Config

See `examples/openclaw.config.example.jsonc`.

Loading