beeos-claw 0.6.49 → 0.6.50-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/acp-gateway/mcp-desired-state-controller.js +1 -1
- package/dist/acp-gateway/mcp-tool-bridge.js +1 -1
- package/dist/acp-gateway/openclaw-native-mcp-projection.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -206,9 +206,9 @@ Logs are written to rolling files in `log.dir` with automatic rotation and sensi
|
|
|
206
206
|
The publish workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml) is triggered only by protected version tags:
|
|
207
207
|
|
|
208
208
|
- `vX.Y.Z-staging.N` must point at the current `staging` head and exactly match `package.json`; it publishes npm `staging` and invokes the frozen-SHA OpenClaw `staging` image workflow.
|
|
209
|
-
- `vX.Y.Z` must point at the current `main` head
|
|
209
|
+
- `vX.Y.Z` must point at the current `main` head, exactly match `package.json`, and contain exactly one committed matching `evidence/releases/X.Y.Z-staging.N.json`; it publishes the immutable npm version and promotes the already-verified staging OCIR digests without rebuilding.
|
|
210
210
|
|
|
211
|
-
A normal branch push never publishes. Both lightweight and annotated tags are resolved to an immutable commit and rechecked before publish; moved tags and stale branch heads fail closed. OpenClaw
|
|
211
|
+
A normal branch push never publishes. Both lightweight and annotated tags are resolved to an immutable commit and rechecked before publish; moved tags and stale branch heads fail closed. OpenClaw reads back and verifies the OCIR manifest, config digest, revision/source labels, and requested aliases. Only then does Claw dispatch Backend with immutable receipt selectors; Backend independently re-verifies them before its receipt-bound release controller can promote a pin or roll out.
|
|
212
212
|
|
|
213
213
|
### Release to staging
|
|
214
214
|
|
|
@@ -222,7 +222,7 @@ git tag v$(node -p "require('./package.json').version")
|
|
|
222
222
|
git push origin v$(node -p "require('./package.json').version")
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
The tag workflow publishes `beeos-claw@0.X.Y-staging.N` with `dist-tag=staging`, then
|
|
225
|
+
The tag workflow publishes `beeos-claw@0.X.Y-staging.N` with `dist-tag=staging`, then builds and verifies `openclaw-desktop:staging-<full-claw-sha>` in OCIR. Staging never uses `staging-latest`; consumers pulling `beeos-claw@latest` or the OCIR `latest` image are **not** affected.
|
|
226
226
|
|
|
227
227
|
### Promote to production
|
|
228
228
|
|
|
@@ -232,13 +232,16 @@ After staging validation:
|
|
|
232
232
|
git checkout main
|
|
233
233
|
git merge staging # brings pre-release commits into main
|
|
234
234
|
npm version 0.X.Y --no-git-tag-version # strip the -staging.N suffix
|
|
235
|
-
|
|
235
|
+
# Add the finalized staging evidence receipt before tagging. The stable tag
|
|
236
|
+
# resolver requires exactly one evidence/releases/0.X.Y-staging.N.json.
|
|
237
|
+
git add package.json package-lock.json evidence/releases/0.X.Y-staging.N.json
|
|
238
|
+
git commit -m "chore: release $(node -p "require('./package.json').version")"
|
|
236
239
|
git push origin main
|
|
237
240
|
git tag v$(node -p "require('./package.json').version")
|
|
238
241
|
git push origin v$(node -p "require('./package.json').version")
|
|
239
242
|
```
|
|
240
243
|
|
|
241
|
-
The tag workflow publishes `beeos-claw@0.X.Y`
|
|
244
|
+
The tag workflow first publishes `beeos-claw@0.X.Y` under `production-candidate`. OpenClaw verifies the exact staging run and OCIR authority, then promotes the same Base/Desktop digests to the version, `production-<sha>`, and `latest` aliases without rebuilding. npm `latest` moves only after that promotion succeeds.
|
|
242
245
|
|
|
243
246
|
### Install from a specific channel (for local dev / debugging)
|
|
244
247
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;projection;credentials;references;releases;receiptHistory;logger;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;authorityCloseBlocked=!1;constructor(e,t,i,r,s,n,a){this.store=e,this.projection=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=n,this.logger=a,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0)})}async reconnect(e){await this.exclusive(async()=>{await this.reconcileAll(e)})}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.projection.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,...void 0!==r?.connected?{connected:r.connected}:{},tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.projection.unset(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r}))}async unset(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r}))}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const n=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,n,i))return blocked(s.revision);const a=i?await this.findAttemptActive(s,n,i):void 0;if((i||this.releases)&&!a){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&n?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!n){this.activeCredentialRevisions.delete(e),await this.projection.unset(e);const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,void 0,i,a)}const o=a?releaseKey(a):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.projection.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,n,i,a)}const c=await this.reconcileServer(n,s,t,a,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),withOperationReceipt(c,s,n,i,a)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear(),this.authorityEpoch=null,await this.projection.close()})}nudgeNativeProjection(){this.liveReleaseKeys.clear(),this.reconnect().catch(e=>this.logger?.warn?.(`[mcp-desired] native projection nudge failed: ${e instanceof Error?e.message:e}`))}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));const i=this.projection.detachMany(this.credentialServers.keys()).then(()=>({ok:!0}),e=>({ok:!1,error:e}));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t,i))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t,i){const r=await i;if(!r.ok)throw this.authorityCloseBlocked=!0,r.error;const s=await this.store.load();if(this.rememberDesired(s),t!==this.authorityGeneration||null===e||this.authorityCloseBlocked)return;const n=this.credentials?.currentAuthority();n&&n.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,s))}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.projection.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,blocked(r.revision)),await this.projection.unset(e.name)}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())t.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),n=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())n.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers){let n=r.find(e=>releaseMatchesDesired(e,i,t));if(!n){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.projection.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}n=r}if(t.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==n.runtimeEpoch){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.projection.unset(t.name),this.projections.set(t.name,blocked(i.revision));continue}const a=releaseKey(n);if(this.liveReleaseKeys.get(t.name)===a&&this.projection.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,n)).state&&this.liveReleaseKeys.set(t.name,releaseKey(n))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,n){try{e.credentialRef&&this.projection.list().some(t=>t.name===e.name)&&await this.projection.detach(e.name),await this.installServer(e,t,i,r,s,n);const a=synced(t.revision);return this.projections.set(e.name,a),a}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.projection.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}this.logger?.warn?.(`[mcp-desired] projection failed name=${e.name}: ${i instanceof Error?i.message:i}`);const r=pending(t.revision);return this.projections.set(e.name,r),r}}async installServer(e,t,i,r,s,n){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(n){if(!s||n.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,n.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const a={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&n.runtimeEpoch===s.runtimeEpoch},o=await this.projection.set(e.name,n.config,n.credential,a);if(!a.isCurrent())throw await this.projection.detach(e.name),new Error("MCP operation-scoped activation lost authority during OpenClaw installation");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.projection.set(e.name,e.config);if(i?.aborted)throw await this.projection.detach(e.name),new Error("MCP projection attempt lost authority during OpenClaw installation");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const a=this.credentials.currentAuthority();if(!a||null===this.authorityEpoch||a.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),u=this.revoked.get(e.credentialRef);if(void 0!==u&&l<=u)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const p=await this.projection.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw await this.projection.detach(e.name),new Error("runtime-scoped MCP credential authority changed during OpenClaw installation");return this.activeCredentialRevisions.set(e.name,l),p}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef)}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const n=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:n})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,n=s.payload,a=i.effectCommitted,o=a?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||n.operationId!==e.operationId||n.requestMessageId!==r.requestMessageId||n.requestHash!==r.requestHash||n.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==n.method:"mcp/unset"!==n.method)||n.command.name!==e.method||n.dispatchEpoch!==e.runtimeEpoch||!a||!o||a.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||a.channel_id!==e.operationId||a.in_reply_to!==r.requestMessageId||a.publisher_id!==n.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==n.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==n.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===n.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===n.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
1
|
+
import{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;projection;credentials;references;releases;receiptHistory;logger;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;constructor(e,t,i,r,s,n,a){this.store=e,this.projection=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=n,this.logger=a,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0)})}async reconnect(e){await this.exclusive(async()=>{await this.reconcileAll(e)})}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.projection.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,...void 0!==r?.connected?{connected:r.connected}:{},tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r}))}async unset(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.projection.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r}))}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const n=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,n,i))return blocked(s.revision);const a=i?await this.findAttemptActive(s,n,i):void 0;if((i||this.releases)&&!a){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&n?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!n){this.activeCredentialRevisions.delete(e),await this.projection.unset(e);const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,void 0,i,a)}const o=a?releaseKey(a):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.projection.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,n,i,a)}const c=await this.reconcileServer(n,s,t,a,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),withOperationReceipt(c,s,n,i,a)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear(),this.authorityEpoch=null,await this.projection.close()})}nudgeNativeProjection(){this.liveReleaseKeys.clear(),this.reconnect().catch(e=>this.logger?.warn?.(`[mcp-desired] native projection nudge failed: ${e instanceof Error?e.message:e}`))}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t){const i=await this.store.load();if(this.rememberDesired(i),t!==this.authorityGeneration||null===e)return;const r=this.credentials?.currentAuthority();r&&r.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,i))}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.projection.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,blocked(r.revision)),await this.projection.unset(e.name)}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())t.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),n=new Set(i.servers.map(e=>e.name));for(const e of this.projection.list())n.has(e.name)||await this.projection.unset(e.name);for(const t of i.servers){let n=r.find(e=>releaseMatchesDesired(e,i,t));if(!n){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),t.credentialRef||await this.projection.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}n=r}if(t.credentialRef){const e=await this.projection.has(t.name),r=e?synced(i.revision):pending(i.revision);this.projections.set(t.name,r),e&&this.liveReleaseKeys.set(t.name,releaseKey(n));continue}const a=releaseKey(n);if(this.liveReleaseKeys.get(t.name)===a&&this.projection.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,n)).state&&this.liveReleaseKeys.set(t.name,releaseKey(n))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,n){try{await this.installServer(e,t,i,r,s,n);const a=synced(t.revision);return this.projections.set(e.name,a),a}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.projection.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}this.logger?.warn?.(`[mcp-desired] projection failed name=${e.name}: ${i instanceof Error?i.message:i}`);const r=pending(t.revision);return this.projections.set(e.name,r),r}}async installServer(e,t,i,r,s,n){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(n){if(!s||n.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,n.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const a={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&n.runtimeEpoch===s.runtimeEpoch},o=await this.projection.set(e.name,n.config,n.credential,a);if(!a.isCurrent())throw new Error("MCP operation-scoped activation lost authority during OpenClaw installation");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.projection.set(e.name,e.config);if(i?.aborted)throw new Error("MCP projection attempt lost authority during OpenClaw installation");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const a=this.credentials.currentAuthority();if(!a||null===this.authorityEpoch||a.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),u=this.revoked.get(e.credentialRef);if(void 0!==u&&l<=u)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const p=await this.projection.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during OpenClaw installation");return this.activeCredentialRevisions.set(e.name,l),p}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef)}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const n=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:n})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,n=s.payload,a=i.effectCommitted,o=a?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||n.operationId!==e.operationId||n.requestMessageId!==r.requestMessageId||n.requestHash!==r.requestHash||n.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==n.method:"mcp/unset"!==n.method)||n.command.name!==e.method||n.dispatchEpoch!==e.runtimeEpoch||!a||!o||a.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||a.channel_id!==e.operationId||a.in_reply_to!==r.requestMessageId||a.publisher_id!==n.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==n.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==n.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===n.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===n.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{Client}from"@modelcontextprotocol/sdk/client";import{SSEClientTransport}from"@modelcontextprotocol/sdk/client/sse.js";import{StdioClientTransport,getDefaultEnvironment}from"@modelcontextprotocol/sdk/client/stdio.js";import{StreamableHTTPClientTransport}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{normalizeMcpPublicServerConfig}from"./mcp-desired-state-store.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";const DEFAULT_TIMEOUT_MS=15e3;export const MCP_BRIDGE_ACTIVATION=Object.freeze({status:"ready",blockers:Object.freeze([])});export class InProcessMcpToolBridge{connect;servers=new Map;generation=0;constructor(e=connectMcpServer){this.connect=e}registerTools(e){e.registerTool(()=>[...this.servers.values()].flatMap(e=>e.tools),{optional:!0})}list(){return[...this.servers.values()].map(e=>e.view)}async set(e,r,t,o){const n=requireServerName(e),i=normalizeMcpPublicServerConfig(r);requireAuthority(o);const s=await this.connect(i,t);let a,c,l;try{requireAuthority(o),a=await s.listTools(),requireAuthority(o);const e=new Set,r=++this.generation;c=a.map(t=>{if(!t.name||e.has(t.name))throw new Error("MCP server returned an invalid or duplicate tool name");return e.add(t.name),bridgeTool(n,t,s,()=>{const e=this.servers.get(n);if(!e||e!==l||e.generation!==r||!isAuthorityCurrent(o))throw new Error("MCP tool authority is no longer current")})}),l={view:void 0,connection:s,tools:Object.freeze([...c]),generation:r,...o?{authority:o}:{}}}catch{throw await s.close().catch(()=>{}),new Error("MCP server tool discovery failed")}const u=Object.freeze({name:n,transport:transportKind(i),connected:!0,tools:Object.freeze(a.map(e=>e.name))});l.view=u;try{requireAuthority(o)}catch{throw await s.close().catch(()=>{}),new Error("MCP server authority changed during connection")}const p=this.servers.get(n);return this.servers.set(n,l),await(p?.connection.close().catch(()=>{})),u}async unset(e){return await this.detach(e)}detach(e){const r=requireServerName(e),t=this.servers.get(r);return t?(this.servers.delete(r),t.connection.close().then(()=>!0)):Promise.resolve(!1)}detachMany(e){const r=[];for(const t of new Set(e))r.push(this.detach(t));return Promise.allSettled(r).then(e=>{const r=e.filter(e=>"rejected"===e.status);if(r.length)throw new AggregateError(r.map(e=>e.reason),"one or more MCP transports failed to close")})}async close(){await this.detachMany([...this.servers.keys()])}}export async function connectMcpServer(e,r){return await connectMcpTransport(createTransport(e,r),e.connectionTimeoutMs)}export async function connectMcpTransport(e,r=15e3){const t=new Client({name:"beeos-claw-mcp-bridge",version:"0.1.0"},{capabilities:{}}),o=validTimeout(r);try{await t.connect(e,{timeout:o})}catch{throw await t.close().catch(()=>{}),new Error("MCP server connection failed")}return{listTools:async()=>(await t.listTools({},{timeout:o})).tools.map(e=>({name:e.name,...e.description?{description:e.description}:{},...isObject(e.inputSchema)?{inputSchema:e.inputSchema}:{}})),callTool:async(e,r,n)=>await t.callTool({name:e,arguments:r},void 0,{timeout:o,...n?{signal:n}:{}}),async close(){await t.close()}}}function createTransport(e,r){const t=transportKind(e=normalizeMcpPublicServerConfig(e));if("stdio"===t){if(r?.headers&&Object.keys(r.headers).length)throw new Error("MCP stdio cannot inject HTTP headers");if("string"!=typeof e.command||!e.command.trim())throw new Error("MCP stdio command is required");const t=stringArray(e.args,"MCP stdio args"),o=stringMap(r?.env,"MCP environment");return new StdioClientTransport({command:e.command,...t?{args:t}:{},env:{...getDefaultEnvironment(),...o},stderr:"pipe",..."string"==typeof e.cwd&&e.cwd?{cwd:e.cwd}:{}})}if("string"!=typeof e.url||!e.url)throw new Error("MCP server URL is required");if(r?.env&&Object.keys(r.env).length)throw new Error("MCP HTTP transport cannot inject process environment");const o=new URL(e.url);if("https:"!==o.protocol||o.username||o.password||o.search||o.hash)throw new Error("MCP server URL must be credential-free HTTPS");const n=stringMap(r?.headers,"MCP headers");return"sse"===t?new SSEClientTransport(o,{requestInit:{headers:n},eventSourceInit:{fetch:(e,r)=>fetch(e,{...r,headers:n})}}):new StreamableHTTPClientTransport(o,{requestInit:{headers:n}})}function bridgeTool(e,r,t,o){return Object.freeze({name:publicToolName(e,r.name),description:safeDescription(e,r),parameters:normalizeSchema(r.inputSchema),async execute(e,n,i){if(!isObject(n))throw new Error("MCP tool arguments must be an object");try{return o(),await t.callTool(r.name,n,i)}catch{throw new Error("MCP tool invocation failed")}}})}function isAuthorityCurrent(e){return!e||e.isCurrent()}function requireAuthority(e){if(!isAuthorityCurrent(e))throw new Error("MCP bridge authority is no longer current")}function publicToolName(e,r){return`${`beeos_mcp_${identifier(e)}_${identifier(r)}`.slice(0,54).replace(/_+$/g,"")}_${createHash("sha256").update(`${e}\0${r}`).digest("hex").slice(0,8)}`}function identifier(e){return e.toLowerCase().replace(/[^a-z0-9_]+/g,"_").replace(/^_+|_+$/g,"")||"tool"}function safeDescription(e,r){return`MCP ${e}: ${r.description||r.name}`.replace(/[\u0000-\u001f\u007f]/g," ").slice(0,512)}function normalizeSchema(e){return!e||void 0!==e.type&&"object"!==e.type?{type:"object",properties:{},additionalProperties:!0}:void 0===e.type?{...e,type:"object"}:e}function transportKind(e){return"sse"===e.transport||"streamable-http"===e.transport||"stdio"===e.transport?e.transport:e.command?"stdio":"streamable-http"}function requireServerName(e){return requireSecretFreeMcpServerName(e)}function stringArray(e,r){if(void 0!==e){if(!Array.isArray(e)||e.some(e=>"string"!=typeof e))throw new Error(`${r} invalid`);return[...e]}}function stringMap(e,r){if(void 0===e)return{};if(!isObject(e)||Object.values(e).some(e=>"string"!=typeof e))throw new Error(`${r} invalid`);return{...e}}function validTimeout(e){return"number"==typeof e&&Number.isSafeInteger(e)&&e>0&&e<=12e4?e:15e3}function isObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}
|
|
1
|
+
import{createHash}from"node:crypto";import{Client}from"@modelcontextprotocol/sdk/client";import{SSEClientTransport}from"@modelcontextprotocol/sdk/client/sse.js";import{StdioClientTransport,getDefaultEnvironment}from"@modelcontextprotocol/sdk/client/stdio.js";import{StreamableHTTPClientTransport}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{normalizeMcpPublicServerConfig}from"./mcp-desired-state-store.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";const DEFAULT_TIMEOUT_MS=15e3;export const MCP_BRIDGE_ACTIVATION=Object.freeze({status:"ready",blockers:Object.freeze([])});export class InProcessMcpToolBridge{connect;servers=new Map;generation=0;constructor(e=connectMcpServer){this.connect=e}registerTools(e){e.registerTool(()=>[...this.servers.values()].flatMap(e=>e.tools),{optional:!0})}list(){return[...this.servers.values()].map(e=>e.view)}async has(e){return this.servers.has(requireServerName(e))}async set(e,r,t,o){const n=requireServerName(e),i=normalizeMcpPublicServerConfig(r);requireAuthority(o);const s=await this.connect(i,t);let a,c,l;try{requireAuthority(o),a=await s.listTools(),requireAuthority(o);const e=new Set,r=++this.generation;c=a.map(t=>{if(!t.name||e.has(t.name))throw new Error("MCP server returned an invalid or duplicate tool name");return e.add(t.name),bridgeTool(n,t,s,()=>{const e=this.servers.get(n);if(!e||e!==l||e.generation!==r||!isAuthorityCurrent(o))throw new Error("MCP tool authority is no longer current")})}),l={view:void 0,connection:s,tools:Object.freeze([...c]),generation:r,...o?{authority:o}:{}}}catch{throw await s.close().catch(()=>{}),new Error("MCP server tool discovery failed")}const u=Object.freeze({name:n,transport:transportKind(i),connected:!0,tools:Object.freeze(a.map(e=>e.name))});l.view=u;try{requireAuthority(o)}catch{throw await s.close().catch(()=>{}),new Error("MCP server authority changed during connection")}const p=this.servers.get(n);return this.servers.set(n,l),await(p?.connection.close().catch(()=>{})),u}async unset(e){return await this.detach(e)}detach(e){const r=requireServerName(e),t=this.servers.get(r);return t?(this.servers.delete(r),t.connection.close().then(()=>!0)):Promise.resolve(!1)}detachMany(e){const r=[];for(const t of new Set(e))r.push(this.detach(t));return Promise.allSettled(r).then(e=>{const r=e.filter(e=>"rejected"===e.status);if(r.length)throw new AggregateError(r.map(e=>e.reason),"one or more MCP transports failed to close")})}async close(){await this.detachMany([...this.servers.keys()])}}export async function connectMcpServer(e,r){return await connectMcpTransport(createTransport(e,r),e.connectionTimeoutMs)}export async function connectMcpTransport(e,r=15e3){const t=new Client({name:"beeos-claw-mcp-bridge",version:"0.1.0"},{capabilities:{}}),o=validTimeout(r);try{await t.connect(e,{timeout:o})}catch{throw await t.close().catch(()=>{}),new Error("MCP server connection failed")}return{listTools:async()=>(await t.listTools({},{timeout:o})).tools.map(e=>({name:e.name,...e.description?{description:e.description}:{},...isObject(e.inputSchema)?{inputSchema:e.inputSchema}:{}})),callTool:async(e,r,n)=>await t.callTool({name:e,arguments:r},void 0,{timeout:o,...n?{signal:n}:{}}),async close(){await t.close()}}}function createTransport(e,r){const t=transportKind(e=normalizeMcpPublicServerConfig(e));if("stdio"===t){if(r?.headers&&Object.keys(r.headers).length)throw new Error("MCP stdio cannot inject HTTP headers");if("string"!=typeof e.command||!e.command.trim())throw new Error("MCP stdio command is required");const t=stringArray(e.args,"MCP stdio args"),o=stringMap(r?.env,"MCP environment");return new StdioClientTransport({command:e.command,...t?{args:t}:{},env:{...getDefaultEnvironment(),...o},stderr:"pipe",..."string"==typeof e.cwd&&e.cwd?{cwd:e.cwd}:{}})}if("string"!=typeof e.url||!e.url)throw new Error("MCP server URL is required");if(r?.env&&Object.keys(r.env).length)throw new Error("MCP HTTP transport cannot inject process environment");const o=new URL(e.url);if("https:"!==o.protocol||o.username||o.password||o.search||o.hash)throw new Error("MCP server URL must be credential-free HTTPS");const n=stringMap(r?.headers,"MCP headers");return"sse"===t?new SSEClientTransport(o,{requestInit:{headers:n},eventSourceInit:{fetch:(e,r)=>fetch(e,{...r,headers:n})}}):new StreamableHTTPClientTransport(o,{requestInit:{headers:n}})}function bridgeTool(e,r,t,o){return Object.freeze({name:publicToolName(e,r.name),description:safeDescription(e,r),parameters:normalizeSchema(r.inputSchema),async execute(e,n,i){if(!isObject(n))throw new Error("MCP tool arguments must be an object");try{return o(),await t.callTool(r.name,n,i)}catch{throw new Error("MCP tool invocation failed")}}})}function isAuthorityCurrent(e){return!e||e.isCurrent()}function requireAuthority(e){if(!isAuthorityCurrent(e))throw new Error("MCP bridge authority is no longer current")}function publicToolName(e,r){return`${`beeos_mcp_${identifier(e)}_${identifier(r)}`.slice(0,54).replace(/_+$/g,"")}_${createHash("sha256").update(`${e}\0${r}`).digest("hex").slice(0,8)}`}function identifier(e){return e.toLowerCase().replace(/[^a-z0-9_]+/g,"_").replace(/^_+|_+$/g,"")||"tool"}function safeDescription(e,r){return`MCP ${e}: ${r.description||r.name}`.replace(/[\u0000-\u001f\u007f]/g," ").slice(0,512)}function normalizeSchema(e){return!e||void 0!==e.type&&"object"!==e.type?{type:"object",properties:{},additionalProperties:!0}:void 0===e.type?{...e,type:"object"}:e}function transportKind(e){return"sse"===e.transport||"streamable-http"===e.transport||"stdio"===e.transport?e.transport:e.command?"stdio":"streamable-http"}function requireServerName(e){return requireSecretFreeMcpServerName(e)}function stringArray(e,r){if(void 0!==e){if(!Array.isArray(e)||e.some(e=>"string"!=typeof e))throw new Error(`${r} invalid`);return[...e]}}function stringMap(e,r){if(void 0===e)return{};if(!isObject(e)||Object.values(e).some(e=>"string"!=typeof e))throw new Error(`${r} invalid`);return{...e}}function validTimeout(e){return"number"==typeof e&&Number.isSafeInteger(e)&&e>0&&e<=12e4?e:15e3}function isObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{listMcpServers,setMcpServer,unsetMcpServer}from"../openclaw-cli.js";export class OpenClawNativeMcpProjection{installed=new Map;cli;constructor(t={}){this.cli={list:t.cli?.list??listMcpServers,set:t.cli?.set??setMcpServer,unset:t.cli?.unset??unsetMcpServer}}list(){return[...this.installed.values()]}async set(t,e,r,n){
|
|
1
|
+
import{listMcpServers,setMcpServer,unsetMcpServer}from"../openclaw-cli.js";export class OpenClawNativeMcpProjection{installed=new Map;cli;constructor(t={}){this.cli={list:t.cli?.list??listMcpServers,set:t.cli?.set??setMcpServer,unset:t.cli?.unset??unsetMcpServer}}list(){return[...this.installed.values()]}async has(t){const e=(await this.cli.list())[t];return e?(this.installed.set(t,nativeView(t,e)),!0):(this.installed.delete(t),!1)}async set(t,e,r,n){requireCurrent(n),await this.cli.set(t,renderOpenClawMcpServer(e,r));const s=nativeView(t,e);if(this.installed.set(t,s),!isCurrent(n))throw new Error("MCP projection authority changed during OpenClaw installation");return s}async unset(t){return await this.detach(t)}async detach(t){const e=await this.removeIfPresent(t);return this.installed.delete(t),e}async detachMany(t){const e=[];for(const r of new Set(t))try{await this.detach(r)}catch(t){e.push(t)}if(e.length)throw new AggregateError(e,"one or more native MCP entries failed to uninstall")}async close(){this.installed.clear()}async removeIfPresent(t){const e=await this.cli.list();return!!Object.prototype.hasOwnProperty.call(e,t)&&(await this.cli.unset(t),!0)}}export function renderOpenClawMcpServer(t,e){const r=e?.headers&&Object.keys(e.headers).length?{...e.headers}:void 0,n=e?.env&&Object.keys(e.env).length?{...e.env}:void 0;return{..."stdio"!==t.transport?{transport:t.transport}:{},..."string"==typeof t.url?{url:t.url}:{},..."string"==typeof t.command?{command:t.command}:{},...Array.isArray(t.args)?{args:[...t.args]}:{},..."string"==typeof t.cwd?{cwd:t.cwd}:{},..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{},...r?{headers:r}:{},...n?{env:n}:{}}}function transportKind(t){return"sse"===t.transport||"streamable-http"===t.transport||"stdio"===t.transport?t.transport:t.command?"stdio":"streamable-http"}function nativeView(t,e){return Object.freeze({name:t,transport:transportKind(e),tools:Object.freeze([])})}function isCurrent(t){return!t||t.isCurrent()}function requireCurrent(t){if(!isCurrent(t))throw new Error("MCP projection authority is no longer current")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toMessagePart}from"../message-client-types.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,isPlainRecord=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function pickRawResultFromPayload(e){if(void 0!==e.rawResult)return e.rawResult;if(void 0!==e.result)return e.result;if(void 0!==e.details)return e.details;if(void 0!==e.toolResult)return e.toolResult;if(isPlainRecord(e.channelData)){const t=e.channelData;if(void 0!==t.rawResult)return t.rawResult;if(void 0!==t.result)return t.result;if(void 0!==t.details)return t.details}}const extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const r=t.slice(e.text.length);return e.text=t,{kind:"append",text:r}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let r,n="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0,m="";const g=new Set,c=(t,r,n)=>{const o=u;return e.stream.addPart(toMessagePart({type:"tool_use",id:t,name:r,arguments:n,state:"streaming",_seq:o+1})),u+=1,o},p=()=>{const r=t.text;if(!r||r===m)return;const n=r.startsWith(m)?r.slice(m.length):r;if(!n)return void(m=r);const o=u;e.stream.addPart(toMessagePart({type:"text",text:n,state:"done",_seq:o+1})),u+=1,m=r},f=()=>(d+=1,`tool_${e.requestId}_${d}`),y=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},I=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},h=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:r,index:n}]=t;return l.splice(n,1),r},v=(e,t)=>{i.set(e,t)},$=e=>i.get(e)??e,x=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),k=(t,r,n,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==r?r:i.name,l=!x(i.arguments)&&x(n)?n:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,toMessagePart({type:"tool_use",id:i.id,name:s,arguments:l,state:i.state,_seq:i.partIndex+1})),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}p();const l={id:t,name:r,arguments:n,partIndex:c(t,r,n),state:"streaming",itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},b=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done",_seq:t.partIndex+1})),t.state="done")},_=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(_).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${_(t)}`).join(",")}}`},R=(t,r,n)=>{const o=`${n?"1":"0"}:${_(r)}`;if(t.progressSignature===o)return;const s=toMessagePart({type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:r,...n?{is_error:!0}:{}}});if(void 0===t.progressPartIndex){const r=u;return e.stream.addPart(s),t.progressPartIndex=r,t.progressSignature=o,void(u+=1)}e.stream.replacePart(t.progressPartIndex,s),t.progressSignature=o},S=e=>[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e)),q=(e,t=[])=>{const r=[...new Set([...S(e),...t])];return isNonEmptyString(e.text)&&r.length>0?{text:e.text,media_urls:r}:isNonEmptyString(e.text)?e.text:1===r.length?r[0]:r.length>1?{media_urls:r}:!0===e.isError?"Tool execution failed.":""},T=(t,r)=>{if(t.durableUploadFilesEmitted)return;e.logger.info?.(`[im-sink] schedule tool artifact enrichment requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`);const n=pickRawResultFromPayload(r),o=(async()=>{let o=[];try{o=await resolveToolArtifactLinks({toolCallId:t.id,toolName:t.name,rawResult:n,rawText:"string"==typeof r.text?r.text:void 0,payloadMediaUrls:S(r)})}catch(r){const n=r instanceof Error?r.message:String(r);return void e.logger.warn?.(`[im-sink] resolveToolArtifactLinks failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)}0!==o.length?(e.logger.info?.(`[im-sink] tool artifact enrichment resolved ${o.length} link(s) requestId=${e.requestId} toolUseId=${t.id}`),await new Promise(n=>{e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;const n=o.map(e=>e.uri);R(t,q(r,n),!0===r.isError),((t,r,n)=>{if(0!==r.length&&!t.durableUploadFilesEmitted){t.durableUploadFilesEmitted=!0,e.stream.appendToolResult(t.id,r.map(e=>({type:"resource_link",uri:e.uri,name:e.name,...e.mimeType?{mimeType:e.mimeType}:{}})),n),u+=1;for(const t of r)e.stream.addPart(toMessagePart({type:"resource_link",uri:t.uri,name:t.name,...t.mimeType?{mimeType:t.mimeType}:{}})),u+=1}})(t,o,!0===r.isError)}catch(r){const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment apply failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)}finally{n()}})})):e.logger.debug?.(`[im-sink] tool artifact enrichment found no links requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`)})().catch(r=>{const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)});g.add(o),o.finally(()=>{g.delete(o)})},w=(e,t,r)=>{const n=i.get(e);if(n)return k(n,t??"tool",r??{},!1);if(s.has(e))return k(e,t??"tool",r??{},!1);const o=I(t);return o?(v(e,o),k(o,t??"tool",r??{},!1)):k(e,t??"tool",r??{},!1)},E=(e,t)=>{const r=i.get(e);if(r)return k(r,t,{},!1);if(s.has(e))return k(e,t,{},!1);const n=(e=>{const t=a.filter(t=>{const r=s.get(t);return!(void 0===r||e&&r.name!==e||r.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(n)return v(e,n),k(n,t,{},!1);const o=h(t);return k(e,t,o?.arguments??{},!1)},P={tool:0,block:0,final:0};let A,M=!1;const N=(t,r)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!M||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{r()}catch(r){const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${n}`)}})};return{dispatcher:{sendToolResult:t=>(N("tool_progress",()=>{const r=resolveRuntimeToolCallId(t),n=resolveRuntimeToolName(t),o=r?void 0:n?I(n):I(),i=r||o?void 0:h(n);if(!r&&!o&&!i)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${n??"unknown"} pending=${a.length}`);const l=r?w(r,n,resolveRuntimeToolArguments(t)):k(o??f(),n??s.get(o??"")?.name??i.name,x(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):i?.arguments??{},!1);P.tool+=1,R(l,q(t),!0===t.isError),b(l),y(l.id),T(l,t)}),!0),sendBlockReply:r=>(N("block_reply",()=>{P.block+=1,isNonEmptyString(r.text)&&(!0!==r.isError?(A=void 0,e.stream.setBody(r.text),t.text=r.text,s.size>0&&p()):A=r.text)}),!0),sendFinalReply:r=>(N("final_reply",()=>{P.final+=1,isNonEmptyString(r.text)&&(!0!==r.isError?(A=void 0,e.stream.setBody(r.text),t.text=r.text,s.size>0&&p()):A=r.text)}),!0),waitForIdle:async()=>{for(let t=0;t<16;t++){if(await e.callbackQueue.waitForIdle(),0===g.size)return;await Promise.allSettled([...g])}await e.callbackQueue.waitForIdle()},getQueuedCounts:()=>({...P}),getAccumulatedBody:()=>t.text,getTerminalErrorText:()=>A,markComplete:t=>{if(1===l.length){const e=l.shift();k(f(),e.name,e.arguments,!1)}s.size>0&&p();for(const r of s.values())try{"success"===t&&b(r),y(r.id)}catch(t){const n=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${r.id} error=${n}`)}l.length=0,M=!0}},replyOptions:{...e.runId?{runId:e.runId}:{},abortSignal:e.abortSignal,onAgentRunStart:t=>{e.onAgentRunStart?.(t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:r=>{N("partial",()=>{const n=extractBodyUpdate(t,r.text);switch(n.kind){case"append":e.stream.appendBody(n.text);break;case"replace":e.stream.setBody(n.text)}})},onReasoningStream:t=>{N("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!r&&(r=e.openReasoningStream?.(),!r))return;const s=t.text;""===o&&""!==n&&(r.appendBody("\n"),n+="\n"),s!==o&&(s.startsWith(o)?(r.appendBody(s.slice(o.length)),o=s):o.startsWith(s)||(r.setBody(n+s),o=s))})},onReasoningEnd:()=>{N("reasoning_end",()=>{var t;""!==o&&(t=o,e.stream.appendThinking(t,"done"),u+=1,n+=o,o="")})},onAssistantMessageStart:()=>{e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)},onToolStart:t=>{N("tool_start",()=>{const r="string"==typeof t.phase?t.phase:"start",n=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===r){if(o){const e=$(o);s.has(e)&&k(e,n,resolveRuntimeToolArguments(t),!0)}return}const i=resolveRuntimeToolArguments(t);if(o){const e=w(o,n,i);return e.toolStartObserved=!0,void(a.includes(e.id)||"streaming"!==e.state||a.push(e.id))}const d=(u=n,a.map(e=>s.get(e)).filter(e=>void 0!==e&&(!u||e.name===u)&&e.itemEventObserved));var u;if(1===d.length){return void(k(d[0].id,n,i,!0).toolStartObserved=!0)}d.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${n} candidates=${d.length}`):l.push({name:n,arguments:i})})},onItemEvent:e=>{N("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const r="string"==typeof e.phase?e.phase.toLowerCase():"update",n=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!n||!o)return;if("update"===r){const e=$(n),t=s.get(e);return void(t&&(k(t.id,o,{},!0).itemEventObserved=!0))}const i=E(n,o);i.itemEventObserved=!0,a.includes(i.id)||"streaming"!==i.state||a.push(i.id)})}}}}
|
|
1
|
+
import{toMessagePart}from"../message-client-types.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,isPlainRecord=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function pickRawResultFromPayload(e){if(void 0!==e.rawResult)return e.rawResult;if(void 0!==e.result)return e.result;if(void 0!==e.details)return e.details;if(void 0!==e.toolResult)return e.toolResult;if(isPlainRecord(e.channelData)){const t=e.channelData;if(void 0!==t.rawResult)return t.rawResult;if(void 0!==t.result)return t.result;if(void 0!==t.details)return t.details}}export function createImReplySink(e){const t={text:""};let r="",n="",o=!1,s=!1;const i=[];let a,l="",d="";const u=new Map,m=new Map,g=[],c=[];let p=0,f=0,y=1;const I=new Set,h=r=>{r!==t.text&&(r.startsWith(t.text)?e.stream.appendBody(r.slice(t.text.length)):e.stream.setBody(r),t.text=r)},v=t=>{if(void 0!==t.partIndex)return;const r=f,n=y;e.stream.addPart(toMessagePart({type:"text",text:t.text,state:"done",_seq:n})),t.partIndex=r,f+=1,y+=1},$=()=>{n&&(n="",h(r))},b=e=>{if(o)return;const t={text:e};i.push(t),r+=e,n="",o=!0,s&&v(t),h(r)},x=()=>(p+=1,`tool_${e.requestId}_${p}`),k=e=>{const t=g.indexOf(e);t>=0&&g.splice(t,1)},S=e=>{const t=e?g.filter(t=>u.get(t)?.name===e):g;return 1===t.length?t[0]:void 0},_=e=>{const t=c.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:r,index:n}]=t;return c.splice(n,1),r},q=(e,t)=>{m.set(e,t)},R=e=>m.get(e)??e,T=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),E=(t,r,n,o)=>{const a=u.get(t);if(a){const s="tool"===a.name&&"tool"!==r?r:a.name,i=!T(a.arguments)&&T(n)?n:a.arguments;return s===a.name&&i===a.arguments||(e.stream.replacePart(a.partIndex,toMessagePart({type:"tool_use",id:a.id,name:s,arguments:i,state:a.state,_seq:a.displaySeq})),a.name=s,a.arguments=i),o&&!g.includes(t)&&"streaming"===a.state&&g.push(t),a}$(),(()=>{if(!s){s=!0;for(const e of i)v(e)}})();const l=((t,r,n)=>{const o=f,s=y;return e.stream.addPart(toMessagePart({type:"tool_use",id:t,name:r,arguments:n,state:"streaming",_seq:s})),f+=1,y+=1,{partIndex:o,displaySeq:s}})(t,r,n),d={id:t,name:r,arguments:n,partIndex:l.partIndex,displaySeq:l.displaySeq,state:"streaming",itemEventObserved:!1,toolStartObserved:!1};return u.set(t,d),o&&g.push(t),d},w=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done",_seq:t.displaySeq})),t.state="done")},P=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(P).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${P(t)}`).join(",")}}`},A=(t,r,n)=>{const o=`${n?"1":"0"}:${P(r)}`;if(t.progressSignature===o)return;const s=toMessagePart({type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:r,...n?{is_error:!0}:{}}});if(void 0===t.progressPartIndex){const r=f;return e.stream.addPart(s),t.progressPartIndex=r,t.progressSignature=o,void(f+=1)}e.stream.replacePart(t.progressPartIndex,s),t.progressSignature=o},M=e=>[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e)),N=(e,t=[])=>{const r=[...new Set([...M(e),...t])];return isNonEmptyString(e.text)&&r.length>0?{text:e.text,media_urls:r}:isNonEmptyString(e.text)?e.text:1===r.length?r[0]:r.length>1?{media_urls:r}:!0===e.isError?"Tool execution failed.":""},O=(t,r)=>{if(t.durableUploadFilesEmitted)return;e.logger.info?.(`[im-sink] schedule tool artifact enrichment requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`);const n=pickRawResultFromPayload(r),o=(async()=>{let o=[];try{o=await resolveToolArtifactLinks({toolCallId:t.id,toolName:t.name,rawResult:n,rawText:"string"==typeof r.text?r.text:void 0,payloadMediaUrls:M(r)})}catch(r){const n=r instanceof Error?r.message:String(r);return void e.logger.warn?.(`[im-sink] resolveToolArtifactLinks failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)}0!==o.length?(e.logger.info?.(`[im-sink] tool artifact enrichment resolved ${o.length} link(s) requestId=${e.requestId} toolUseId=${t.id}`),await new Promise(n=>{e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;const n=o.map(e=>e.uri);A(t,N(r,n),!0===r.isError),((t,r,n)=>{if(0!==r.length&&!t.durableUploadFilesEmitted){t.durableUploadFilesEmitted=!0,e.stream.appendToolResult(t.id,r.map(e=>({type:"resource_link",uri:e.uri,name:e.name,...e.mimeType?{mimeType:e.mimeType}:{}})),n),f+=1;for(const t of r)e.stream.addPart(toMessagePart({type:"resource_link",uri:t.uri,name:t.name,...t.mimeType?{mimeType:t.mimeType}:{}})),f+=1}})(t,o,!0===r.isError)}catch(r){const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment apply failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)}finally{n()}})})):e.logger.debug?.(`[im-sink] tool artifact enrichment found no links requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`)})().catch(r=>{const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment failed requestId=${e.requestId} toolUseId=${t.id} error=${n}`)});I.add(o),o.finally(()=>{I.delete(o)})},U=(e,t,r)=>{const n=m.get(e);if(n)return E(n,t??"tool",r??{},!1);if(u.has(e))return E(e,t??"tool",r??{},!1);const o=S(t);return o?(q(e,o),E(o,t??"tool",r??{},!1)):E(e,t??"tool",r??{},!1)},j=(e,t)=>{const r=m.get(e);if(r)return E(r,t,{},!1);if(u.has(e))return E(e,t,{},!1);const n=(e=>{const t=g.filter(t=>{const r=u.get(t);return!(void 0===r||e&&r.name!==e||r.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(n)return q(e,n),E(n,t,{},!1);const o=_(t);return E(e,t,o?.arguments??{},!1)},C={tool:0,block:0,final:0};let B,F=!1;const Q=(t,r)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!F||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{r()}catch(r){const n=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${n}`)}})};return{dispatcher:{sendToolResult:t=>(Q("tool_progress",()=>{const r=resolveRuntimeToolCallId(t),n=resolveRuntimeToolName(t),o=r?void 0:n?S(n):S(),s=r||o?void 0:_(n);if(!r&&!o&&!s)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${n??"unknown"} pending=${g.length}`);const i=r?U(r,n,resolveRuntimeToolArguments(t)):E(o??x(),n??u.get(o??"")?.name??s.name,T(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):s?.arguments??{},!1);C.tool+=1,A(i,N(t),!0===t.isError),w(i),k(i.id),O(i,t)}),!0),sendBlockReply:e=>(Q("block_reply",()=>{C.block+=1,isNonEmptyString(e.text)&&(B=!0!==e.isError?void 0:e.text)}),!0),sendFinalReply:e=>(Q("final_reply",()=>{C.final+=1,isNonEmptyString(e.text)&&(!0!==e.isError?(B=void 0,b(e.text)):B=e.text)}),!0),waitForIdle:async()=>{for(let t=0;t<16;t++){if(await e.callbackQueue.waitForIdle(),0===I.size)return;await Promise.allSettled([...I])}await e.callbackQueue.waitForIdle()},getQueuedCounts:()=>({...C}),getAccumulatedBody:()=>t.text,getTerminalErrorText:()=>B,markComplete:t=>{if(1===c.length){const e=c.shift();E(x(),e.name,e.arguments,!1)}$();for(const r of u.values())try{"success"===t&&w(r),k(r.id)}catch(t){const n=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${r.id} error=${n}`)}c.length=0,F=!0}},replyOptions:{...e.runId?{runId:e.runId}:{},abortSignal:e.abortSignal,disableBlockStreaming:!0,onAgentRunStart:t=>{e.onAgentRunStart?.(t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:e=>{Q("partial",()=>{"string"==typeof e.text&&(n=e.text,h(r+n))})},onReasoningStream:t=>{Q("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!a&&(a=e.openReasoningStream?.(),!a))return;const r=t.text;""===d&&""!==l&&(a.appendBody("\n"),l+="\n"),r!==d&&(r.startsWith(d)?(a.appendBody(r.slice(d.length)),d=r):d.startsWith(r)||(a.setBody(l+r),d=r))})},onReasoningEnd:()=>{Q("reasoning_end",()=>{var t;""!==d&&(t=d,e.stream.appendThinking(t,"done"),f+=1,l+=d,d="")})},onAssistantMessageStart:()=>{Q("assistant_message_start",()=>{n="",o=!1,h(r),e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)})},onBlockReplyQueued:e=>{Q("block_reply_queued",()=>{isNonEmptyString(e.text)&&!0!==e.isError&&(B=void 0,b(e.text))})},onToolStart:t=>{Q("tool_start",()=>{const r="string"==typeof t.phase?t.phase:"start",n=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===r){if(o){const e=R(o);u.has(e)&&E(e,n,resolveRuntimeToolArguments(t),!0)}return}const s=resolveRuntimeToolArguments(t);if(o){const e=U(o,n,s);return e.toolStartObserved=!0,void(g.includes(e.id)||"streaming"!==e.state||g.push(e.id))}const i=(a=n,g.map(e=>u.get(e)).filter(e=>void 0!==e&&(!a||e.name===a)&&e.itemEventObserved));var a;if(1===i.length){return void(E(i[0].id,n,s,!0).toolStartObserved=!0)}i.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${n} candidates=${i.length}`):c.push({name:n,arguments:s})})},onItemEvent:e=>{Q("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const r="string"==typeof e.phase?e.phase.toLowerCase():"update",n=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!n||!o)return;if("update"===r){const e=R(n),t=u.get(e);return void(t&&(E(t.id,o,{},!0).itemEventObserved=!0))}const s=j(n,o);s.itemEventObserved=!0,g.includes(s.id)||"streaming"!==s.state||g.push(s.id)})}}}}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "beeos-claw",
|
|
3
3
|
"name": "beeos-claw",
|
|
4
4
|
"description": "BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.6.48-staging.1",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true
|
|
8
8
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.50-staging.1",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"fast-uri": ">=3.1.4",
|
|
88
88
|
"brace-expansion": ">=5.0.9"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "790bdd22ec976d68688f3f076e5337391fbc31b5"
|
|
91
91
|
}
|