Skip to content

feat(local-agent): serve MCP and models to OpenClaw, Hermes and DSH - #1809

Merged
SaulMoro merged 8 commits into
Tencent:mainfrom
plzfgme:feat/openclaw-hermes-dsh-mcp-models
Nov 10, 2026
Merged

SaulMoro merged 8 commits into
Tencent:mainfrom
plzfgme:feat/openclaw-hermes-dsh-mcp-models

Conversation

@plzfgme

@plzfgme plzfgme commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

OpenClaw, Hermes and DSH keep their MCP servers and models in the single config each tool boots from — they have no project-scope file. This delivers a workspace's MCP server into that user config while recording ownership against the workspace, applies delivered models to each tool's provider store, and makes a re-delivery at either scope idempotent. Two behaviours outside the three tools change too: restoreMcpDoc restores a failed write's exact bytes for every JSON MCP tool, and parseYaml no longer warns about an unresolved !!js tag in any YAML it reads.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes (includes the generated tool-support docs check)
  • npx vitest run src/__tests__/local-agent-mcp.test.ts src/__tests__/local-agent-model-config.test.ts src/__tests__/mcp-reconcile.test.ts src/__tests__/locate-mcp.test.ts → 4 files, 1341 tests passed
  • Added/updated tests for the change

Manual end-to-end check against real clients (Docker, node:24, real openclaw@2026.11.2 / hermes-agent@0.23.0 / @deepseek-ai/dsh@0.2.2-rc.1), mock backend sending workspace-scope install_mcp and apply_model_config:

  • each tool's own file received the server (~/.openclaw/openclaw.json, ~/.hermes/config.yaml, ~/.dsh/cordis.patch.yml) and the model provider; nothing was written under the workspace directory
  • install_mcp(user) then install_mcp(workspace) for the same server both succeed and each scope keeps its record; uninstalling one scope leaves the entry while the other still records it
  • teamai hooks list reports dsh installed

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Allow rotation of teamai-owned provider credentials — src/local-agent.ts:6971. After installing with key A, a full snapshot containing key B fails with “a different API key,” even when the provider still matches its ownership record. Hermes has the same problem; URL changes are rejected for all three tools. A normal credential rotation therefore leaves clients using obsolete settings. Permit replacement of unchanged, recorded provider fields.

  • [P1 blocking] Handle workspace-to-user MCP re-delivery — src/local-agent.ts:5124. Cross-scope ownership lookup runs only for project-scoped commands. Install a server at workspace scope, then deliver the same server at user scope: its record remains in the workspace manifest, so the user install rejects teamai’s own entry as unmanaged. Resolve ownership in both directions before transferring the record.

  • [P1 blocking] Include manifest persistence in the model transaction — src/local-agent.ts:4591. All three command branches omit the reconciliation commit callback and save the manifest afterward. If that save fails after updating an existing model, the config retains the new entry while the manifest retains its old hash; subsequent cleanup treats the entry as member-owned and leaves it behind. Pass manifest persistence through the existing callback so failure rolls back the config writes.

Testing: The description documents a representative manual end-to-end record, so no missing-E2E finding. I only read the diff; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Reviewed at a7e7e2cf. A single pull delivers well: the servers land in openclaw.json, config.yaml and cordis.patch.yml, and the member's own entries and comments survive. A second delivery of the same server adds no duplicate. These three files are now shared by every workspace on the machine and by user scope, though, and two paths still treat them as one workspace's own. Both reproduce with the built CLI.

What I ran (built a7e7e2cf and the merge-base d54415d6, isolated HOME, empty GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM=1; team with root mcp/mcp.yaml (docs-mcp, local-tools) and mcp/be/mcp.yaml (be-only), roles fe and be; each tool's home seeded with a member entry mine and comments):

1. workspace app1, role fe: teamai init <url> --agent openclaw --agent hermes --agent dsh --role fe; teamai pull   exit 0
   -> docs-mcp and local-tools in all three files; mine and the comments kept. Merge-base build: nothing written.
2. workspace app2, role be, same steps                                                                           exit 0
   -> be-only added to all three files.
3. team drops docs-mcp; teamai pull in app1                                                                      exit 0
   -> "MCP: 6 change(s) across 2 server(s)": docs-mcp and be-only removed from all three files;
      app2's managed-mcp.json still names be-only.
4. user-scope install and pull, then workspace app1 with the same servers; teamai uninstall --force outside it
   -> "Removed 6 teamai-managed MCP server(s)": docs-mcp and local-tools gone from all three files, while
      app1's record still names them; the "Kept for the projects still set up" list does not mention them.

Blocking

  • [P1 blocking] A pull in one workspace deletes another workspace's server (step 3). The removal loop in src/mcp-reconcile.ts treats an unrecorded entry that matches a render from the team history as teamai's, and deletes it unless this workspace's own record (ownedNames) names it. Now that a project's servers go to a user file (mcpScopeFor, src/tools.ts), that file has other owners, and the loop never reads their records. Until app2 pulls again, an agent started there runs without be-only. Counting a server as owned when any record of the file names it, whatever scope or workspace wrote that record, would fix this. shadowedMcpRecords already reads the other records.
  • [P1 blocking] A user-scope uninstall removes servers a set-up workspace still delivers (step 4). src/uninstall.ts calls reconcileMcpForConfig(..., { removeAll: true }), which reaches the same loop for entries app1 also records. The same rule (keep what another live record names) would cover it. The message would then be true when it says the project is kept.

Non-blocking

  • [P2 non-blocking] The OpenClaw warning says teamai left openclaw.json as it is, in the same pull that writes MCP servers into it (src/openclaw-hooks.ts:458). On main the sentence was true, but with this PR it only holds for the hook entry.
  • [P3 nit] Changes outside the three tools that the body does not mention.
    • restoreMcpDoc now restores the exact bytes on a failed write for every JSON MCP tool. This is an improvement, but the body says no other tool's behaviour changes.
    • parseYaml no longer warns about !!js in any YAML file it reads. The value still stays a string.

CI review

The CI review at this head reports three [P1 blocking] items. I read each one in the code and agree with all three:

  • A changed API key or base URL is refused for teamai's own provider (src/local-agent.ts:6971). planOpenclaw compares the provider's baseUrl and apiKey with the new delivery and throws "a different API key" or "a different base URL" before it looks at the record. A team that changes its key or endpoint therefore leaves every member on the old one. The Hermes path does the same. When the record shows teamai wrote those fields, replacing them should be allowed.
  • Re-delivery from workspace to user scope is refused (src/local-agent.ts:5124). The lookup of the other scope's records (cross) runs only when projectScope is set. A server installed at workspace scope and then delivered at user scope is still recorded only under the workspace key, so the user install treats teamai's own entry as the member's and refuses it. The body's idempotence check covers the other order only. The lookup needs to run in both directions.
  • Model files are written before their record is saved (src/local-agent.ts:4591). This matches what I found. applyModelConfig calls the three reconcile*Models functions without the commit argument they accept, then writes the manifest. If that write fails, the member's file keeps the new models while the record keeps the old hashes, and a later delivery leaves them behind as the member's own. Saving the record through commit would roll the file back on failure.

Before CI can run

The branch conflicts with main in CHANGELOG.md, both tool-support.md pages and src/tools.ts, so no CI run exists for this head. A rebase is needed before lint, the unit suite and the E2E shards can say anything. The body's real-client run in Docker is useful, but it has no commands, exit codes or commit. The record above can go in the body, with the real-client lines added to it.

Merge risk

Merge risk: high.

  • Alignment: decent. The pull path matches the body. The two scope paths above do not, and the body does not mention the changes outside the three tools.
  • Impact: large. The PR rewrites three tools' user configs, which every workspace on the machine shares, in a 3.6k-line diff.
  • Verification: poor. The record has no commands, and neither it nor the tests cover two workspaces or uninstall. CI cannot run until the branch is rebased. install_mcp and apply_model_config through the local-agent backend were not run here.
  • Rollback cost: medium. The configs are restored by the next pull or uninstall, but those are the two paths that delete live entries.
  • Error detection: decent. A file that cannot be parsed is left unchanged with a warning. Gaps are the OpenClaw warning above and the model record.

@jeff-r2026 your call on this one. It is a new capability that writes into files every workspace on a machine shares, so the ownership rule for those files is a design decision as much as a fix.

@SaulMoro
SaulMoro requested a review from jeff-r2026 November 10, 2026 02:07
@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from a7e7e2c to 1797833 Compare November 10, 2026 03:00
@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

All five blocking items are fixed on 17978333, rebased onto 53ad0e66. tsc --noEmit, lint and the four affected suites pass (1341 tests).

  • P1: a pull in one workspace deleted another workspace's server. The delete loop now counts a server as owned when any record names it for that file: the user manifest when the run is in a workspace, every workspace shard when it is at user scope (namesRecordedElsewhere, src/mcp-reconcile.ts). owned still decides what may be rewritten, so this only stops deletions.
  • P1: user-scope uninstall removed servers a live workspace delivers. Same loop, same fix.
  • P1: re-delivery from workspace to user scope was refused. The other-scope lookup now runs in both directions: at user scope it reads every workspace's managed-mcp.json (loadWorkspaceScopeMcpManifests), adopts the record and clears it there after the write.
  • P1: a changed API key or base URL was refused for teamai's own provider. planOpenclaw and planHermes allow the replacement when every model of the managed provider still matches its record, which is what proves teamai wrote it.
  • P1: model files were written before their record. applyModelConfig now passes the record through commit, so a failed manifest write rolls the files back.
  • P2: the OpenClaw warning says teamai did not add or remove the team hooks plugin, instead of claiming the file was left untouched.
  • P3: the PR body now says the other tool changes: restoreMcpDoc restores exact bytes on a failed write for every JSON MCP tool, and parseYaml stops warning about !!js in any YAML it reads.

The branch is rebased on the current main, so CI can run.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] DSH endpoint rotation still fails — src/local-agent.ts:7714. Install models using URL A, then deliver a full snapshot using URL B: the existing baseURL is rejected even when its provider matches the ownership record. DSH remains on the obsolete endpoint. This part of the earlier rotation finding remains unresolved.

  • [P1 blocking] Check provider ownership before replacing credentials — src/local-agent.ts:7009 and src/local-agent.ts:7213. After installation, a member can edit the provider’s URL or key without changing its models. teamaiProvider still evaluates true because it checks only model hashes, so re-delivery silently overwrites those member edits. Verify the recorded provider fields before permitting replacement.

  • [P1 blocking] Preserve credentials for a member-edited DSH model — src/local-agent.ts:7774. Install one model, edit its name in the profile, then deliver an empty full snapshot. Its hash no longer matches, so this early return leaves the model/provider intact without reporting retainsProvider; credential cleanup then deletes its unchanged API-key reference. The preserved model becomes unusable.

  • [P1 blocking] Protect MCP entries jointly owned by another scope — src/mcp-reconcile.ts:3377. Install a workspace server, then user-scope pull the identical team definition, which adopts it into the user manifest. User-scope uninstall deletes the entry despite the surviving workspace ownership. Adding other owners to ownedNames only protects the unrecorded sweep; the owned-entry removal loop still deletes it. Exclude externally owned entries from removal.

Earlier findings: Workspace-to-user MCP re-delivery and transactional model-manifest persistence are resolved. Credential rotation is partially resolved, with the DSH endpoint issue remaining above.

Testing: The description records representative real-CLI end-to-end verification, so no missing-E2E finding. I only read the diff; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Re-reviewed at 17978333. Thanks for the quick round. These are now resolved:

  • the OpenClaw warning;
  • the body's list of changes outside the three tools;
  • re-delivery from workspace to user scope through install_mcp;
  • the model record saved through commit;
  • the key and URL change for OpenClaw and Hermes.

I checked each one in the code. The two shared-file P1s still reproduce with the built CLI, though, and the provider change has two gaps.

Still blocking

  • [P1 blocking] A pull in one workspace still deletes another workspace's server, and a user-scope uninstall still removes servers a workspace records. I reran both setups from the first round on this head: a role-fe pull removes be-only from all three files while the be workspace still records it, and uninstall --force outside the workspace prints "Removed 6 teamai-managed MCP server(s)" for servers the workspace still records. The cause is the record key:

    • These three tools have no project file, so a workspace's pull records its servers under the plain tool key, {"openclaw": [...]}, in that workspace's managed-mcp.json.
    • namesRecordedElsewhere (src/mcp-reconcile.ts) reads ${tool}:project from each workspace shard, which is the key install_mcp writes, and so it never sees a record a pull wrote.

    A reader that takes both keys would close the unrecorded sweep. The CI review also points out that the loop over this run's own recorded entries removes them whatever other records say. So a server that user scope adopted, and that a workspace also records, is still deleted on user uninstall. That loop needs the same check.

New blocking

  • [P1 blocking] A member's own URL or key on the teamai provider is now overwritten without a word.
    • teamaiProvider (src/local-agent.ts:7009, and the Hermes counterpart at :7213) counts the provider as teamai's when every model matches its record. It never looks at the provider's own baseUrl or apiKey.
    • The write then sets both from the delivery.
    • So a member who replaced the key in ~/.openclaw/openclaw.json and left the models alone gets the team's key back on the next delivery, with no message. Main refused this case with "a different API key".
    • A containerHash is already recorded for the provider, but only the removal path reads it. Checking it here too would let a team's change through and still stop at a member's.
  • [P1 blocking] DSH still refuses a changed base URL (src/local-agent.ts:7714). planDsh throws "a different baseURL" with no ownership check, so a team that moves its endpoint leaves DSH members on the old one. It needs the same rule as the other two tools, including the check above.
  • [P1 blocking] An empty delivery drops the API-key reference of a model the member edited (CI review, src/local-agent.ts:7774). I confirmed this in the code. When nothing is delivered and no managed model was removed, planDsh returns { write: null } before it sets retainsProvider. The cleanup at :8206 keeps the key store only for a plan that retains its provider, so the kept model loses the reference it uses. Setting retainsProvider whenever the provider and a kept model remain would fix it. I did not run this one.

CI review at 17978333

It reports four P1s, and I agree with all four. The joint-ownership one reproduces in the order I ran: user install, workspace pull, then user uninstall. The DSH URL, provider-edit and empty-delivery items are from reading the code.

Not run

  • install_mcp and apply_model_config through the local-agent backend, so every model-path item above comes from the code.
  • CI has not run on this head yet: the fork's workflow runs are waiting for approval.

Merge risk

Merge risk: high. The PR still deletes live entries in a file every workspace shares. The new provider rule gives up a refusal that main had.

@jeff-r2026 your call on this one. The shared-file ownership rule is the design question, and this round shows it needs both record keys and both removal loops to agree.

@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from 1797833 to 081348d Compare November 10, 2026 03:17
@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Second round, on 081348d6.

  • DSH endpoint rotation. planDsh now allows a different baseURL when the container still matches its record, as OpenClaw and Hermes do. All three replaced the model-hash heuristic with authoredFieldsIntact(...) against previousRecord.containerHash, which is what actually proves teamai wrote the URL and key — so a member who edits either still blocks the rewrite (previous finding).
  • Member-edited DSH model kept its credential. When a full snapshot is empty and the provider holds a model whose hash no longer matches, the plan now reports retainsProvider, so cleanup keeps the API-key reference the model still points at.
  • MCP entries owned by two scopes. The record is no longer moved. Installing at the other scope now keeps both records, and reconcile treats a server another scope records as not this scope's to remove: it stays out of owned, so the pull and uninstall --force loops leave the entry while the other owner still names it. That covers the user-scope uninstall you reproduced.

tsc --noEmit, lint and local-agent-mcp / local-agent-model-config / mcp-reconcile pass (492 tests).

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Recognize ownership records created by workspace pull — src/local-agent.ts:5016, src/mcp-reconcile.ts:2753. Workspace pull produces fallback targets with projectScope: false, so their records use keys such as dsh, not dsh:project. Both cross-scope readers look only for the latter. After workspace pull installs a server, user-scope install_mcp therefore rejects teamai’s unchanged entry as unmanaged. Read the keys actually used by both delivery paths.

  • [P1 blocking] Protect other scopes during command-driven MCP uninstall — src/local-agent.ts:5452. Install the same server first at workspace scope, then at user scope. Installation adds the user record without removing the workspace record. User-scope uninstall_mcp then deletes the shared config entry while the workspace still owns it. The reconciliation guard does not cover this command path; check surviving owners before deleting the entry.

Earlier findings: Model credential/endpoint rotation, provider-field ownership checks, model-manifest transaction persistence, and member-edited DSH model credential retention are resolved. Command-to-command workspace/user re-delivery is also resolved. Shared-scope MCP cleanup remains incomplete as described above.

Testing: The description documents representative real-CLI end-to-end verification, so no missing-E2E finding. I only read the diff; I did not run, build, or install PR code.

@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from 081348d to 16154a4 Compare November 10, 2026 03:34
@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Both remaining items are fixed on 16154a4a.

  • Workspace-pull records were invisible to the cross-scope readers. A pull's fallback target carries projectScope: false, so its record sits under tool in the workspace manifest, while the command channel writes tool:project. Both readers now take the two keys: loadWorkspaceScopeMcpManifests returns the keys a shard actually holds, and namesRecordedElsewhere collects from tool and tool:project (src/local-agent.ts, src/mcp-reconcile.ts). A user-scope install_mcp after a workspace pull now adopts that record instead of refusing its own entry.
  • Command-driven uninstall deleted a shared entry. uninstallMcpServer now checks the other scope before touching the file (slugOwnedInOtherScope): when a workspace, or the user manifest on the workspace side, still records the server, only this scope's record is dropped and the config entry stays. uninstall --force already keeps it through the reconcile guard.

tsc --noEmit, lint, local-agent-mcp / mcp-reconcile / local-agent-model-config pass (492 tests).

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Re-reviewed at 16154a4a. The provider fixes hold:

  • a member's own key on teamai's provider is now refused ("already has a different API key; teamai does not overwrite it");
  • a team key or URL change still goes through;
  • the DSH baseURL moves to the new URL.

The shared-file ownership still fails in the runs below. One root cause explains three of them, and this head adds a regression.

A correction to my last review: I wrote that main refused a member's own key with "a different API key". Main has no model delivery for these three tools, so that comparison was wrong. The finding stands on its own: the member's key must not be overwritten, and this head now refuses that case.

Blocking

  • [P1 blocking] The cross-scope readers look in a directory that does not exist. namesRecordedElsewhere reads getDataHome(localConfig)/workspaces (src/mcp-reconcile.ts:2741), and loadWorkspaceScopeMcpManifests reads resolveDataHomeForScope('user')/workspaces, which is ~/.teamai/workspaces (src/local-agent.ts:5015). A workspace's record lives under its project, at ~/.teamai/projects/<slug>/workspaces/<id>/managed-mcp.json. So no other project's record is read, and ~/.teamai/workspaces is never there. This one cause still produces all of these:

    • A pull in one workspace removes another workspace's server. After the role-fe pull, be-only is gone from openclaw.json while the be workspace still records it.
    • A user-scope uninstall --force removes servers a workspace records. It prints "Removed 6 teamai-managed MCP server(s)" while the workspace record still names docs-mcp and local-tools.
    • A user-scope uninstall_mcp removes an entry a workspace still records (CI review). Install docs-mcp at user scope, then at workspace scope, then run uninstall_mcp at user scope: the entry is gone. slugOwnedInOtherScope finds no shard, so it returns false.
    • A user-scope install_mcp refuses teamai's own entry when a workspace records the server (CI review). It says "exists in openclaw config and is not managed by teamai". This happens both after a workspace pull and after a workspace install_mcp.

    projectsRootDir() already exists in src/mcp-reconcile.ts. Scanning projects/*/workspaces/*/managed-mcp.json from there, with both the plain and the :project key, would reach every record.

  • [P1 blocking] New in this head: a server the team drops stays forever once its only owner pulls. One workspace with role fe pulls docs-mcp, the team drops it, and that workspace pulls again. openclaw.json still has docs-mcp, and no record names it, so uninstall cannot remove it either. At 081348d6 the entry is removed. The plain-key read in namesRecordedElsewhere includes the workspace's own shard, so its own record counts as another owner and leaves owned. Skipping the current workspace's shard in that read would fix it. The two-workspace drop keeps docs-mcp with no record for the same reason.

Non-blocking

  • [P2 non-blocking] The body's real-CLI record says a second-scope install moves the record. This head keeps both. The record should be rerun once the items above are fixed.

CI review

The last CI review comment was written before this push. It reported two P1s:

  • records written by a workspace pull use the plain key;
  • uninstall_mcp at user scope ignores the workspace's record.

This head reads the plain key, but both still reproduce here because of the directory above. I agree with both.

Not run

CI has not run on this head: the fork's workflow runs are waiting for approval.

Merge risk: high.

@jeff-r2026 your call on this one. The provider side is in good shape. The open question is still who owns an entry in a file every workspace shares, and the code needs to read records where they are written.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Search project partitions for workspace owners — src/local-agent.ts:5015, src/mcp-reconcile.ts:2741. User-scope discovery scans ~/.teamai/workspaces, but partitioned git workspaces record ownership under ~/.teamai/projects/<slug>/workspaces/<id>. After workspace installation, user-scope install_mcp still rejects the unchanged entry as unmanaged; user-scope uninstall also misses surviving workspace owners. Discover the actual partition manifests.

  • [P1 blocking] Exclude the current workspace from external ownership — src/mcp-reconcile.ts:2750. Workspace fallback targets have projectScope: false, so this scan includes the uninstalling workspace’s own manifest. Pull a server into one workspace, then uninstall that workspace: its own record protects the entry from deletion, while reconciliation drops the record. The server remains installed without ownership.

  • [P1 blocking] Read pull-created ownership during workspace commands — src/local-agent.ts:5150, src/local-agent.ts:5446. Workspace pull records native servers under dsh/hermes/openclaw, but workspace commands read only <tool>:project from their current manifest. After workspace pull installs a server, same-workspace install_mcp rejects it as unmanaged and uninstall_mcp silently leaves it installed. Read both current-workspace key forms.

  • [P1 blocking] Do not erase the user owner during workspace uninstall — src/local-agent.ts:5447, src/local-agent.ts:5462. Install the same server at user scope and then workspace scope, then uninstall it from the workspace. The loop removes both manifests’ records, but each iteration sees the user record as a surviving owner and skips config deletion. The entry becomes unmanaged. Remove only the uninstalling scope’s record.

  • [P1 blocking] Protect owners in other workspaces during command uninstall — src/local-agent.ts:5398. Install a server through workspace A’s command, then pull the identical definition into workspace B. With no user-scope owner, uninstall_mcp(workspace A) checks only the user manifest and deletes the shared entry despite B’s surviving ownership. Check other workspace owners before deletion.

Earlier findings: Model rotation, provider-field ownership checks, model-manifest transactions, and member-edited DSH credential retention are resolved. Both workspace key forms are recognized by cross-scope readers, but MCP ownership remains incomplete in the cases above.

Testing: The description documents representative real-CLI end-to-end verification; no missing-E2E finding. I only read the diff and its context; I did not run, build, or install PR code.

@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from 16154a4 to a8fcbe9 Compare November 10, 2026 03:47
@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed on a8fcbe99.

  • The readers looked in a directory that does not exist. loadManagedMcpShards now enumerates the manifests where they are actually written: ~/.teamai/managed-mcp.json plus, for every slug under projectsRootDir(), ~/.teamai/projects/<slug>/managed-mcp.json and ~/.teamai/projects/<slug>/workspaces/<id>/managed-mcp.json. Both readers use it — namesRecordedElsewhere (pull, uninstall --force) and loadWorkspaceScopeMcpManifests / slugOwnedInOtherScope (the command paths) — and both take the plain and the :project key, so a record from either delivery path is seen.
  • New regression: a dropped server stayed once its only owner pulled. That was the same read counting this run's own shard as a second owner. namesRecordedElsewhere now takes the manifest path the run writes and skips it, so its own records no longer protect an entry from removal.

tsc --noEmit, lint, local-agent-mcp / mcp-reconcile pass (412 tests). The body's real-CLI record still says the second-scope install moves the record; that sentence is stale and I will rerun it once CI is green.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Read pull-created ownership during workspace commands — src/local-agent.ts:5144, src/local-agent.ts:5440. Workspace pull records native servers under <tool>, but workspace commands still read only <tool>:project from their current manifest. After workspace pull installs a server, same-workspace install_mcp rejects it as unmanaged and uninstall_mcp leaves it installed. Read both current-workspace key forms.

  • [P1 blocking] Preserve the user owner during workspace uninstall — src/local-agent.ts:5441. Install the same server at user scope and then workspace scope, then uninstall it from the workspace. This loop removes both scopes’ records, while the shared-owner check prevents config deletion. The server becomes unmanaged. Remove only the uninstalling scope’s record.

  • [P1 blocking] Check other workspaces during command uninstall — src/local-agent.ts:5392. Install a server through workspace A’s command, then pull the identical definition into workspace B. With no user owner, uninstalling from A deletes the shared entry despite B’s surviving ownership: the project branch checks only the user manifest. Include other workspace owners.

  • [P1 blocking] Keep shared ownership valid after an entry changes — src/local-agent.ts:5152. Install a workspace server using URL A, then user-scope install the same name using URL B. The config and user record change, but the workspace hash remains A. A subsequent workspace install of B discards the matching user record because that name already exists locally, then rejects teamai’s entry as unmanaged. Preserve valid ownership across shared updates.

Resolved: Project-partition discovery and exclusion of reconciliation’s own manifest are fixed. The earlier model rotation, provider-field ownership, transaction, and credential-retention findings remain resolved.

Testing: The description documents representative real-client end-to-end verification; no missing-E2E finding. I only read the diff and context, without running, building, or installing PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Read a8fcbe99. loadManagedMcpShards now reads records where they are written, under both key forms, and namesRecordedElsewhere skips the run's own manifest. That covers both items from my last review. I'll rerun the two-workspace and user-uninstall setups on the head that settles the points below.

I checked the four P1s in the CI review at this head in the code, and I agree with them:

  • Workspace commands read only <tool>:project from their own manifest (src/local-agent.ts:5150, records = manifest[manifestKey]). A server a workspace pull recorded under the plain key is invisible to install_mcp and uninstall_mcp in that same workspace: the install refuses teamai's own entry, and the uninstall leaves it. The current manifest needs both keys, as the cross-scope readers now have.
  • A workspace uninstall_mcp also removes the user owner's record. The loop over locations includes the user manifest (cross). Each pass sees the other record as a surviving owner and keeps the entry, then drops that pass's record. With both records gone, the entry is no longer managed. Only the uninstalling scope's record should go.
  • A workspace uninstall_mcp checks only the user manifest for other owners (slugOwnedInOtherScope, the projectScope branch). Suppose workspace A installed a server by command and workspace B pulled the same one. Uninstalling it from A then deletes the entry that B still records. That branch needs the other workspaces' shards too, without A's own.
  • A record goes stale when another scope changes the shared entry (src/local-agent.ts:5152). After a user-scope install rewrites the entry, the workspace record keeps the old hash. A later workspace install then treats the matching user record as replaced by its own, and refuses the entry. I followed this one in the code less fully than the other three.

These four come from reading the code; I have not run them. The first three are one rule applied in three places: every path that adds or removes a record of a shared file must read all owners' records, by both key forms, without its own.

CI has not run on this head yet: the fork's workflow runs are waiting for approval.

@jeff-r2026 your call on this one. These four points are still about who owns an entry in the shared file.

@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from a8fcbe9 to 0834ae0 Compare November 10, 2026 04:01
@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

All four are fixed on 0834ae07, and this time I ran the two-shard cases before pushing.

  • Pull-written records were invisible to workspace commands. A workspace command now reads its own manifest's plain key as well as tool:project (install_mcp and uninstall_mcp), so a server a pull recorded there is owned.
  • A workspace uninstall removed the user owner. Uninstall only removes the uninstalling scope's record now; the other scope's manifest is read for ownership and never written by it.
  • A command uninstall ignored other workspaces. slugOwnedInOtherScope scans every enrolled workspace's manifest, skipping the one this uninstall writes.
  • Shared ownership after an entry changed. Cross-scope records are no longer dropped because this scope also records the name: after one scope rewrites the entry, the other scope's record is the one that matches what is installed, and it now counts.
  • The shard enumeration also covers the in-workspace data home: ~/.teamai/projects/<slug>/… for a partitioned install, and <workspace>/.teamai/workspaces/<id>/… for one that keeps its data home in the workspace, reached through the local agent's workspaceBindings.

Checked with the built CLI in Docker, dsh, two workspaces:

  • workspace install_mcp (record under tool:project), then the record moved to the plain tool key as a pull writes it, then user-scope install_mcp: success (it failed before this change).
  • user-scope install_mcp then workspace uninstall_mcp: success, and the entry stays in cordis.patch.yml because the user record still owns it.
  • user-scope install_mcp then workspace install_mcp: both success, both records kept.

tsc --noEmit, lint, local-agent-mcp (107) pass.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Consult other workspaces during workspace installation — src/local-agent.ts:5146. Install a native MCP server through workspace A’s install_mcp, then install the identical server through workspace B, without a user-scope owner. workspaceCrosses is empty for project-scoped commands, so B rejects A’s unchanged entry as unmanaged. Workspace installs must also consult other workspace owners.

  • [P1 blocking] Preserve valid ownership when the updating scope leaves — src/local-agent.ts:5459. Workspace-install a server at URL A, user-install that name at URL B, then user-uninstall it. The shared-entry guard preserves B but removes the user record, leaving only the workspace’s hash of A. A subsequent workspace teamai uninstall treats B as member-edited and leaves it installed after dropping ownership. Preserve matching ownership evidence when relinquishing the updating scope.

  • [P1 blocking] Discover non-enrolled in-workspace ownership — src/mcp-reconcile.ts:2749. In-workspace manifests are discovered only through the local agent’s workspaceBindings. A self-mode workspace that uses teamai pull without local-agent enrollment is therefore invisible. After it delivers a native server, user-scope install_mcp rejects that unchanged entry as unmanaged. Ownership discovery must include these installations without requiring enrollment.

Earlier findings: The previously reported model issues, workspace-key handling, and uninstall protection scenarios are resolved. The shared-update cleanup case above remains distinct from the fixed re-install scenario.

Testing: The description documents representative real-client end-to-end verification; no missing-E2E finding. I only read the diff and its context—no PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Re-reviewed at 0834ae07, with the built CLI. The four command-path P1s from the CI review at a8fcbe99 are fixed. So are the earlier shared-file items: a pull no longer removes another workspace's server, a server the team drops goes once its last owner pulls, and both user-scope install_mcp and uninstall_mcp respect a workspace's record. Two cases still fail in the runs below, and the CI review at this head adds two more.

Blocking

  • [P1 blocking] A deleted workspace keeps a dropped server forever. Setup:

    • app1 and app2 pull docs-mcp into OpenClaw.
    • The team drops it.
    • app2 is deleted without teamai uninstall, as a removed worktree is.
    • app1 pulls.

    openclaw.json still has docs-mcp, and teamai uninstall --force in app1 keeps it too. The cause is loadManagedMcpShards: it lists every projects/<slug>/workspaces/<id>/managed-mcp.json without checking that the workspace still exists. namesRecordedElsewhere, the pull guard and slugOwnedInOtherScope all count that record as a live owner, so the entry never loses its last owner. Counting a shard only while its workspace path exists, or pruning it when the workspace goes, would give it an end.

  • [P1 blocking] A workspace install_mcp refuses a server another workspace's pull recorded (the CI review's first P1 at this head names the same gap). After app2 pulls docs-mcp, install_mcp docs-mcp for app1 fails with "server "docs-mcp" exists in openclaw config and is not managed by teamai". On the workspace path, cross reads only the user manifest, and workspaceCrosses is built only when !projectScope (src/local-agent.ts:5146). So no other workspace's record reaches owned. The install path needs the same all-owners read that uninstall now has.

  • [P1 blocking] A workspace that keeps its data home inside the workspace is found only when the local agent is enrolled in it (CI review). loadManagedMcpShards reaches <workspace>/.teamai/workspaces/<id>/ only through workspaceBindings in the local agent's config. A self-mode workspace that runs teamai pull without enrolling is invisible, so its servers have no owner seen from anywhere else. I confirmed this in the code; I did not run it.

  • [P1 blocking] A scope's record can go stale when another scope's entry wins (CI review, src/local-agent.ts:5459). Setup:

    • A server is installed at URL A at workspace scope, then at URL B at user scope.
    • The user scope uninstalls it.

    The guard keeps entry B but drops the user record. Only the workspace's hash of A is left, so the workspace's own uninstall then treats B as a member edit and leaves it. This follows from the code as the CI review describes; I did not trace every branch.

Non-blocking

  • [P2 non-blocking] "Removed N teamai-managed MCP server(s)" counts records, not entries that left the config: it says "Removed 2" while docs-mcp stays. The line predates this PR, but this PR makes the two numbers differ.
  • [P3 nit] A shard that cannot be read counts as "no owner" (readJson(...).catch(() => null)). That error leads to a delete, not a keep. I found this by reading the code.

Before CI can run

CI has not run on this head, because the fork's workflow runs are waiting for approval. The branch is on 53ad0e66, and main has moved since. It merges cleanly, but a rebase would let CI run on what will be merged.

Merge risk: high. Each round closes the cases I listed and finds the next one: an entry in the shared file still has no single place that decides who owns it.

@jeff-r2026 your call on this one. After five rounds, the open items are all about one invariant: one owner set per shared entry, read the same way by every path that adds or removes a record. Whether that shape fits the local-agent design is the maintainer's decision.

OpenClaw, Hermes and DSH have no project MCP file and no project model
store. Deliver a workspace's MCP server to the config each tool reads
(its user config) while keeping the workspace's ownership record, and
apply delivered models to the provider store each tool boots from.

- tools: add `mcpScopeFor`, so a tool with no project MCP channel takes
  a workspace's server in its user config instead of skipping it
- locate/mcp-reconcile: resolve those servers against the user channel,
  with the user channel's syntax (OpenClaw's JSON5 config)
- local-agent: install/uninstall read the other scope's records for the
  same file and adopt them, so a re-delivery at another scope no longer
  fails with "exists ... and is not managed by teamai"
- local-agent: apply_model_config writes the OpenClaw, Hermes and DSH
  model stores; workspace scope is ignored for them, as none has one
- hooks list: report dsh from its adapter files instead of "not
  configured"
- yaml: read DSH's `!!js` tag as its source text, without the warning
@plzfgme
plzfgme force-pushed the feat/openclaw-hermes-dsh-mcp-models branch from 0834ae0 to 7743ace Compare November 10, 2026 06:54
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Read command-created workspace records during reconciliation — src/mcp-reconcile.ts:700, src/mcp-reconcile.ts:3477. Workspace install_mcp records native servers under <tool>:project, but reconciliation reads only <tool> because the fallback target has projectScope: false. In an HTTP-mode workspace, install a server through the command, then run teamai uninstall: reconciliation finds no ownership or team history and leaves the server installed. Read both workspace key forms.

  • [P1 blocking] Preserve matching ownership when reconciliation removes a scope — src/mcp-reconcile.ts:3486. Workspace-pull a server at URL A, user-install it at URL B, then run user-scope teamai uninstall. The shared-owner guard preserves B but drops its matching user record without updating the workspace’s A hash. Subsequent workspace uninstall leaves B installed as an unowned entry. The command-path fix needs an equivalent reconciliation fix.

  • [P1 blocking] Do not adopt member edits while relinquishing ownership — src/local-agent.ts:5434, src/local-agent.ts:5486. Install an identical server at both scopes, manually change its URL, then user-scope uninstall_mcp. This code hashes the member-edited entry and assigns that hash to the surviving workspace record. Workspace uninstall subsequently deletes the member’s edit. Transfer ownership evidence only when the current entry matches an existing ownership record.

  • [P3 nit] Fail closed when ownership discovery is incomplete — src/local-agent.ts:5160, src/mcp-reconcile.ts:3486. An unreadable external manifest sets complete: false. Installation then bypasses the unmanaged-entry check and can overwrite a member’s server; reconciliation retains scope-owned entries in its deletion candidates despite the unknown surviving owner. Leave entries unchanged when ownership cannot be established.

Earlier findings: Cross-workspace installation and non-enrolled self-mode discovery are resolved. Command-driven shared-update cleanup is fixed, but its reconciliation counterpart remains above. The previously resolved model and scope-handling findings remain resolved.

Testing: The description documents representative real-client end-to-end verification; no missing-E2E finding. I only read the specified diff and its context—no PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Re-reviewed at 7743ace7 with the built CLI. The client configs were stand-ins. Two of the four blocking items from my last review are fixed:

  • A deleted workspace no longer protects a dropped server. With app2 removed, app1's pull takes docs-mcp out.
  • A workspace install_mcp now accepts a server another workspace's pull recorded.

I traced a third in the code without running it: a data home inside the workspace is now found without enrollment, through managed-mcp-homes.json. The fourth, the stale hash, is fixed on the command path but not on the reconcile path (first item below). The CI review at this head raised the three blocking items below, and each one reproduced here.

Blocking

  • [P1 blocking] A user-scope teamai uninstall --force drops the user's record for a name a workspace also records, and the workspace then treats the entry as a member edit. Setup:

    • a workspace pull installs docs-mcp at URL A;
    • a user-scope install puts it at URL B;
    • user-scope uninstall --force runs.

    B stays, which is right, but the user record goes and the workspace keeps hash A. The workspace's uninstall --force then prints "Kept MCP server docs-mcp … you changed it since teamai wrote it", and B is left with no owner. In reconcileTargets, the shared name is kept out of owned (src/mcp-reconcile.ts:3486), and no remaining owner's hash is updated. The command path handles this with adoptKeptMcpEntry; reconcile needs the same step.

  • [P1 blocking] A server a workspace installs with install_mcp survives that workspace's own uninstall --force. Setup:

    • http mode, bind-project;
    • workspace init;
    • workspace install_mcp docs-mcp;
    • workspace teamai uninstall --force.

    It prints "Removed 1 teamai-managed MCP server(s)" and removes the record, but openclaw.json still has docs-mcp. The record is under openclaw:project, which matches the CI review's point that reconcile reads only <tool>. I did not trace the write.

  • [P1 blocking] A workspace uninstall deletes a member's edit after the user scope leaves. Setup:

    • an identical docs-mcp is installed at workspace and user scope;
    • the member changes its URL in openclaw.json;
    • user-scope uninstall_mcp runs, then workspace uninstall_mcp.

    The first uninstall keeps the edit. The second removes it. adoptKeptMcpEntry (src/local-agent.ts:5349) copies the current entry's hash into the other owners' records whatever it is. Copying it only when the entry matches a record teamai wrote would keep the edit.

Non-blocking

  • [P2 non-blocking] "Removed N teamai-managed MCP server(s)" still counts records, not entries that left the config. In the first case above it says "Removed 2" while docs-mcp stays.
  • [P3 nit] complete is read inverted, so an unreadable record fails open. The McpOwners.complete comment means false is "an owner may be missing, keep everything". The install refusal fires only when complete is true (src/local-agent.ts:5140, :5160). Reconcile's filter puts records in owned when it is false (src/mcp-reconcile.ts:3486), and owned is what gets removed. With an unparseable managed-mcp.json, a user-scope install_mcp mine overwrote the member's own mine entry, which main refuses. A corrupt record is unlikely, but the fix is to flip the condition at these three sites.

Before merge

CI has not run at this head, because the fork's workflow runs are waiting for approval. The branch is on 53ad0e66 and merges cleanly with main (b8c811db).

Merge risk: high. The command paths now read every owner. The reconcile path that pull and uninstall use still has gaps in how it updates the other owners.

@jeff-r2026 your call on this one. This round shows the same pattern as the last: each fix to how the command path updates other scopes' records needs a matching change in reconcile. One function that both paths call to update the shared owner set would close this class instead of each case.

@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the latest review in 9664282 (fix(mcp): preserve shared ownership across delivery and uninstall), now pushed to this PR.

Findings addressed

  • P1: stale hashes after user-scope full uninstall. Command teardown and reconcile now call the same adoptMcpEntriesForOwners function. Authorship is checked by server name + hash, using the departing/current records. Reconcile includes other-owner writes in its existing rollback boundary; dry runs do not update those manifests.
  • P1: workspace command installs surviving that workspace's full uninstall. Installs, reports and reconcile use the resolved target's canonical key. Reconcile also consumes the <tool>:project records written by earlier PR heads. The manifest remains in the workspace's shard: a user config file does not turn workspace ownership/reporting into user ownership.
  • P1: member edits deleted after another scope leaves. A current hash is adopted only when a same-name record proves teamai wrote that content. Uninstall preserves edited entries instead of certifying the edit as teamai-owned. Added a regression for two different server names whose rendered values happen to match.
  • P2: phantom removal counts. JSON and Codex reconciliation report removed only when an entry/block actually leaves the config, not merely when a scope relinquishes a shared record.
  • P3: unreadable ownership fails open. An unreadable shard no longer permits overwriting a member entry. Reconcile preserves the config and records; full uninstall retains the ownership home for retry. This shared-home owner scan is limited to the fallback tools, so an unrelated corrupt shard does not block Claude/Cursor or change existing tools' ownership semantics.

Verification

  • 21 test files / 2115 tests passed, with retries disabled:
    ./node_modules/.bin/vitest run --retry=0 --maxWorkers=2 \
      src/__tests__/*mcp*.test.ts \
      src/__tests__/uninstall.test.ts \
      src/__tests__/local-agent-model-config.test.ts \
      src/__tests__/tools-channels.test.ts
    Includes existing Copilot bare configs, Muse content ownership, Claude/CodeBuddy/WorkBuddy/Codex MCP behavior, model delivery and uninstall regressions. An incidental warning-text assertion was removed; its config/script preservation assertions remain.
  • npm run typecheck passed; type-aware oxlint passed on all changed TypeScript files; npm run build passed.
  • Docker: 21 asserted scenarios passed, seven each for openclaw@2026.11.2, hermes-agent@0.23.0, and @deepseek-ai/dsh@0.2.2-rc.1, with the rebuilt package installed:
    1. Workspace command install → same workspace uninstall --force, including workspace—not user—inventory reporting.
    2. Actual self-mode workspace pull at URL A → user command install at URL B → both full uninstalls.
    3. Two actual workspace pulls → first owner leaves → last owner removes the entry.
    4. Member edit survives command uninstalls, user first.
    5. Member edit survives command uninstalls, workspace first.
    6. Corrupt ownership refuses a member-config overwrite.
    7. Workspace model snapshot adds two candidates without changing MCP configuration (and preserves the seeded Hermes current-model setting).

Docker package SHA-256: cc8792e9b0ac3f21620129b7b8d721c10b0bf9c3c87816be7515f5e20f8fe8b3.

Verification boundary: these are isolated Docker simulations with real agent packages and a mock HTTP backend, not live external MCP/LLM calls. Pull, init, binding and full uninstall use the installed CLI. Command report/sync/ack uses an awaited bundle of the production reportAndSyncLocalAgent from the same source revision; the session hook's 4.5-second foreground deadline was reached in the container, so this is not claimed as a passing session-hook timing test. No hook timeout or retry behavior was changed.

Please re-review this head; the successful local checks are not a claim that GitHub review/CI has approved it.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Use external ownership when updating shared MCP entries during pull — src/mcp-reconcile.ts:3564. User-install a server at URL A, then workspace-pull an HTTP team definition for the same name at URL B, with no history containing A. External ownership adds only to ownedNames, not owned; applyJson therefore treats A as unrecorded, judges it member-owned, and skips B. Include matching external ownership evidence when deciding whether delivery may replace the entry.

  • [P1 blocking] Protect shared owners when cleaning previous config files — src/mcp-reconcile.ts:3545. Install a Hermes server at both scopes in the default home, then switch the workspace run to another initialized HERMES_HOME and run workspace teamai uninstall. Ownership discovery checks only the new target file; removeFromOtherFiles deletes the original server despite its surviving user-scope owner. Apply ownership protection to every recorded file being cleaned.

Earlier findings: The findings from the last pass are resolved, and previously resolved findings remain resolved. These are distinct remaining reconciliation paths.

Testing: The description documents representative real-client end-to-end verification, so no missing-E2E finding. I read the specified diff and its context without running, building, or installing PR code.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Re-reviewed at 96642829 with the built CLI, with stand-in client configs. The same reproduction cases as last round were rerun at this head:

  • Fixed: all three blocking items from my last review.
    • A user-scope uninstall --force keeps the workspace's ownership consistent, and the workspace's own uninstall then removes the entry.
    • A workspace install_mcp is removed by that workspace's uninstall --force.
    • A member's edit survives both scopes' uninstalls.
  • Fixed: "Removed N" now counts entries that left the config, and an unreadable record no longer lets an install overwrite a member's entry.
  • Still pass: the earlier cases — orphaned workspaces, cross-workspace install, and dropped servers.
  • Merges: the branch merges cleanly with main.

Nothing blocking is left from my side. Four non-blocking points:

  • [P2 non-blocking] A command uninstall that keeps the entry because a record is unreadable still reports success. Setup:

    • a workspace and the user scope both install docs-mcp;
    • another project's managed-mcp.json is unreadable;
    • user uninstall_mcp, then workspace uninstall_mcp.

    docs-mcp stays, which is the safe choice. But the ack is success, and the only output is "Failed to parse JSON: …". The cause is the continue at src/local-agent.ts:5423. Failing the command with the unreadable file's path and "fix or remove it, then run this again" would tell the member why the server is still there.

  • [P2 non-blocking] One unreadable record freezes the whole target.

    • A pull then delivers no new server for that tool and exits 0 with a failed check.
    • uninstall --force exits 1 and removes nothing from that config.

    Limiting the freeze to removals and overwrites would keep new deliveries working. Whether that is worth it is a design call.

  • [P2 non-blocking] The uninstall preview still says "will be removed" for an entry another scope keeps (src/uninstall.ts:1765). The plan lists every record in this scope's manifest. That code predates this PR, but this PR adds the case where the two differ.

  • [P2 non-blocking] An assertion was removed from an unrelated test. In uninstall.test.ts, "keeps the MCP server folder whose entry it cannot remove ([feat] Resources as folders: workflows, hooks, rules and MCP servers carry their own files #1787)" lost its check that the kept-folder warning names the folder, and its title lost "and names it". No other test covers that warning, and this PR does not change it. Please restore the hunk. If it fails, it is likely the path form (other cases compare realpaths), and the fix is in the comparison, not removing the check.

The real-CLI record in the body is for an earlier head. CI has not run at this head yet.

Merge risk: high. It writes into three tools' user configs that every workspace shares. The ownership model now has one function for both paths, which is the shape that was missing.

@jeff-r2026 your call on this one. From our side the blocking items are fixed. What is left is the four points above, and whether the "one unreadable record freezes the target" behaviour is what you want.

@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the two remaining reconciliation P1s in 70e6402, pushed to this PR.

  • Pull accepts external ownership evidence only when the same server name and installed content hash match another scope's record. Member edits still use the normal ownership judge. Regression covers A -> B with no matching team history, shared retention, and last-owner removal.
  • Previous-file cleanup discovers owners for each recorded file, excludes the departing scope's manifest, preserves surviving owners, and transfers matching ownership evidence within the existing rollback boundary. Regression covers switching HERMES_HOME before workspace full uninstall, then user full uninstall. That case failed with the original code and passes after the fix.

Scope: the two P1s only; non-blocking P2 suggestions are unchanged. HTTP-mode reconcile skips delivery in this codebase, so the pull regression uses delivery-enabled git mode without matching history rather than claiming an HTTP delivery scenario.

Verification: typecheck, changed-file type-aware oxlint and build passed. Affected compatibility regressions: 21 files / 2117 tests passed, with retries disabled (MCP suites, uninstall, local-agent model config and tool channels).

Real-CLI limitation: attempted an isolated verify world (build 9664282+dirty, package sha256 prefix 9503743b8d90). init/pull/uninstall executed, but OpenClaw was not installed in that world and pull delivered nothing. This is NOT passing E2E evidence. Ledger reported 3 leaks (checkout snapshot changes while the world was running and project/.openclaw remaining); world cleaned afterward. Earlier-head Docker evidence is not claimed to verify this change.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Read 70e64026 against 96642829. It is a small change, so I read it rather than rerunning the scripts. Both blocking items from the CI review at 96642829 are addressed in the code:

  • Pull and another scope's record. sharedJudge treats an entry as teamai's when another scope's record has the same name and the entry's current hash. After the write, adoptMcpEntriesForOwners moves those owners' hashes to the new entry. The A → B update therefore no longer reads A as a member edit, and the other scope's record follows.
  • Previously used files. removeFromOtherFiles now looks up owners for each recorded file and leaves names another scope records. It fails, keeping the records, when a record cannot be read, and moves matching ownership to the surviving owners. Switching HERMES_HOME before a workspace uninstall no longer takes the user scope's server.

One design point, not a defect in this diff:

  • Two scopes that deliver different values for one name will alternate. A user scope delivers docs-mcp at URL A and a workspace at URL B, into the same user config file. With this change, each scope's pull recognises the other's value as teamai's and writes its own. The entry then flips between A and B on every pull. Before, the second writer stopped at "member-owned" instead. Neither is wrong for a single owner, but a shared file with two desired values needs a rule for which one wins: the workspace, the user scope, or a refusal that names both. That is a call for the design, not for this patch.

The four non-blocking points from my last comment are unchanged, as the author says. The body notes there is no passing real-CLI run at this head: the world had no OpenClaw installed. The Docker record is from an earlier head. CI has not run here.

@jeff-r2026 your call on this one. The blocking items from both reviews are fixed in the code. What is left is the four non-blocking points, a real-CLI run at the final head, and which scope wins when two deliver different values for one shared entry.

@github-actions

Copy link
Copy Markdown

No new findings confidently established in the specified diff.

Resolved: Pull now consults matching external MCP ownership when updating shared entries, and previous-config cleanup protects surviving scope owners. The earlier resolved findings remain resolved.

Testing: The PR description documents representative real-client end-to-end verification, satisfying the required testing record. No full tool × provider matrix is required.

I only read the specified diff and its context; I did not run, build, or install PR code.

@plzfgme

plzfgme commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Final-head Docker verification completed for 70e6402: 10 asserted scenarios passed using the freshly built, packed and globally installed CLI.

Real installed client packages in the image: openclaw@2026.11.2, hermes-agent@0.23.0, @deepseek-ai/dsh@0.2.2-rc.1.

For each of OpenClaw, Hermes and DSH, the installed teamai init, teamai pull and teamai uninstall --force exercised:

  1. User-scope pull delivers URL A; workspace pull delivers same-name URL B. Before workspace init, the team repo history is replaced with a B-only root commit, so workspace history cannot certify A. Assert shared native config changes to B.
  2. Workspace full uninstall preserves B for the user owner; user full uninstall removes it.
  3. Reinstall both scopes, manually edit the native server URL, then uninstall both scopes: member URL survives.

Hermes additional scenario: deliver to both scopes in default home; initialize a different HERMES_HOME and workspace full-uninstall using it; original config must remain byte-identical. User full-uninstall from original home then removes the server.

Package SHA-256: 206e71950293eaff5357c1b9fb131559c072cfe36c24fd0aec854ab2a4971bbe.

Boundary: real installed CLI and real client packages, isolated homes and local Git repos via a sandbox Git URL rewrite; no external MCP/LLM calls or session-hook timing claims. This run uses git-mode delivery, not HTTP command sync. No host configuration mounted into the container. Verification container removed; logs and JSON results retained locally as head70-cli.log / head70-results.json.

This supplements (does not relabel) the earlier unsuccessful verify-world attempt. No production code changes in this verification run.

@plzfgme
plzfgme requested a review from SaulMoro November 10, 2026 12:29
…locks

The entry and every record stay as before, but the command now fails and
names the record that could not be read, so the member knows what to fix
instead of reading success while the server is still delivered.
OpenClaw, Hermes and DSH keep every scope's servers in one user config, so
an uninstall keeps an entry another scope still records. The preview listed
it as removed; it now marks it as kept.
A failed MCP removal marked every MCP file unread, so uninstall kept the
whole delivered/mcp folder with a warning that a file could not be read,
instead of naming the server folder the readable file still starts. The
data home is still kept for the retry. Restores the Tencent#1787 assertion.
@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

To get this merged, I pushed four commits on top of 70e64026 for the open review points and the README. Your commits are unchanged.

Commit What it does
0d171aec fix(mcp) A command uninstall_mcp whose only other possible owner is an unreadable ownership record now fails. The error names that file and says "fix or remove it, then run this again". The entry and records stay as before.
da2f385b fix(uninstall) The uninstall preview marks a server another scope still records as (kept: another scope may still deliver it) instead of "will be removed".
98e1dec6 fix(uninstall) Restores the #1787 assertion and title. The cause was not a path form: a failed MCP removal was pushed into the list of unreadable MCP files, so uninstall kept the whole delivered/mcp folder instead of naming jira.<hash>. That case now has its own list, and the data home is still kept for the retry.
a82050b0 docs(readme) All five READMEs mark mcp for OpenClaw, Hermes and DeepSeek Harness.

Tests. Each fix has a test that failed first. The 2a and 2b tests cover OpenClaw, Hermes and DSH in user and workspace scope. npm run build, typecheck and lint pass, and the MCP and uninstall suites pass 2016/2016.

README models column. It stays —. That column lists the tools teamai models switches, and the docs check (scripts/doc-checks/readme-matrix.ts) holds it to that. These three tools receive models through the HTTP apply_model_config command instead.

Real-CLI run of the first fix, built from a82050b0, isolated HOME, OpenClaw in HTTP mode, user and workspace scope:

$ teamai init --http <server> --token … --agent openclaw --scope user --force          exit 0
$ (app1) teamai bind-project --project-id 7                                             exit 0
$ (app1) teamai init --http <server> --token … --agent openclaw --scope workspace --force   exit 0
install_mcp docs-mcp (workspace)    -> success
install_mcp docs-mcp (user)         -> success
planted an unreadable ~/.teamai/projects/<other>/managed-mcp.json
uninstall_mcp docs-mcp (user)       -> success  (the workspace still owns it: entry kept)
uninstall_mcp docs-mcp (workspace)  -> failed:
  uninstall_mcp: kept "docs-mcp" in ~/.openclaw/openclaw.json: the MCP ownership record
  ~/.teamai/projects/<other>/managed-mcp.json could not be read, so another scope may still deliver it;
  fix or remove it, then run this again.
openclaw.json still has docs-mcp; the workspace record is kept

Before these commits, the last ack was success with the entry still delivered.

Two behaviours are unchanged, as before:

  • one unreadable record holds every change to that tool's config file;
  • two scopes that deliver different values for one name write over each other on each pull.

I'll merge once CI is green at a82050b0.

@github-actions

Copy link
Copy Markdown
  • [P3 nit] Respect DSH credential ownership during rotation — src/local-agent.ts:7952. If TEAMAI_MODEL_API_KEY already contains key A, delivering A records refOwned: false. A later full snapshot with key B nevertheless treats the reference as owned because its hash matches, overwriting the pre-existing credential. Require ownership as well as a matching hash before allowing rotation.

Earlier findings: The previously reported issues are resolved in the current diff.

Testing: The description documents representative real-client end-to-end verification; no missing-E2E finding.

I only read the specified diff and its context. I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Approving at a82050b0. All blocking items from both reviews are fixed, and the open review points are closed by the commits on top. The CI review at this head raises one nit: a DSH key teamai did not create can be rotated once its hash matches. That can follow on its own. Two behaviours stay as described: an unreadable record holds every change to that config, and two scopes that deliver different values for one name write over each other. I'll merge once CI is green.

@github-actions

Copy link
Copy Markdown
  • [P3 nit] Respect DSH credential ownership during rotation — src/local-agent.ts:7952. This earlier finding remains unresolved: when TEAMAI_MODEL_API_KEY already contains key A, delivering A records refOwned: false. A subsequent full snapshot with key B still treats the reference as owned because its hash matches, overwriting the pre-existing credential. Check ownership as well as the hash before permitting rotation.

Earlier findings: The other previously reported issues are resolved. No additional findings confidently established.

Testing: The description documents representative real-client end-to-end verification, satisfying the required testing record.

I only read the specified diff and its context; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Approving at 66c710a9, which is a82050b0 with main merged in. #1810 touched src/uninstall.ts as well, and the merge had no conflicts. The CI review at this head repeats the one nit on DSH key rotation, which can follow on its own. I'll merge once CI is green.

@SaulMoro
SaulMoro merged commit 3107efd into Tencent:main Nov 10, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants