@twinsearth/w2m-dsh-plugin 1.8.0 → 1.8.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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.8.0"
8
+ "version": "1.8.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.8.0",
15
+ "version": "1.8.1",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.8.0"
29
+ "version": "1.8.1"
30
30
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.8.0",
4
+ "version": "1.8.1",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.8.1] — 2026-10-26
11
+
12
+ **GPW「最近被拦截」去噪:已在名单中的条目折叠显示;加入名单后条目立即消失。**
13
+
14
+ - 状态接口为每条 recent 增加 `folder_in`(whitelist / blacklist / null):文件夹已被名单覆盖的拦截记录不再占用主列表。
15
+ - 面板把已覆盖条目折叠为「已在名单中 N 条 · 展开/收起」,展开后只读展示(不重复给加入按钮)。
16
+ - 从最近拦截一键加入白/黑名单时,服务端同步裁剪该条目(以及被同一父目录覆盖的条目),下一次轮询不再出现。
17
+ - 新增 `test/gpw-recent-fold.test.mjs`(4 项:覆盖归属、嵌套覆盖、客户端折叠静态断言、状态接口字段)。
18
+ - 证据:`docs/acceptance/1.8.1/GPW-RECENT-FOLD.json`。
10
19
  ## [1.8.0] — 2026-10-26
11
20
 
12
21
  **GPW 增加四色审核 / 标记 / 提醒系统:绿(通过)、蓝(正常)、黄(警报)、红(风险)。**
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.8.0", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.8.1", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "1.8.1",
3
+ "case": "GPW-RECENT-FOLD",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-26T06:55:26.042Z",
9
+ "executor": "lead",
10
+ "task_id": "local-gpw-fold-20261026065526",
11
+ "requirement": "recently-intercepted rows already covered by a list are collapsed, and a row the operator adds to a list disappears from the list immediately",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "folder_in": "the state route annotates each recent row with whitelist / blacklist / null",
16
+ "collapse": "the panel shows covered rows behind \"已在名单中 N 条 · 展开/收起\" and they carry no add buttons",
17
+ "disappear": "adding a folder from a recent row prunes that row and every row inside the folder server-side",
18
+ "tests": "test/gpw-recent-fold.test.mjs 4/4"
19
+ }
20
+ }
21
+ }
package/lib/client.js CHANGED
@@ -569,7 +569,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
569
569
  * ways: typed here and validated by the host, or taken from a recently intercepted call in one click. The lists
570
570
  * live in $DSH_HOME/xclient/gpw-folders.json; no profile YAML is edited by this feature.
571
571
  */
572
- function GpwPanel({ t, onClose }) {
572
+ function GpwPanel({ t, sessionId, onClose }) {
573
573
  const [state, setState] = React.useState(null);
574
574
  const [error, setError] = React.useState(null);
575
575
  const [busy, setBusy] = React.useState(false);
@@ -650,8 +650,13 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
650
650
  foldRow(items, list),
651
651
  ]);
652
652
  };
653
- const recent = Array.isArray(state?.recent) ? state.recent : [];
654
- const pending = Array.isArray(state?.pending) ? state.pending : [];
653
+ const recentAll = Array.isArray(state?.recent) ? state.recent : [];
654
+ const recent = recentAll.filter((item) => item?.folder_in === null || item?.folder_in === undefined);
655
+ const recentInLists = recentAll.filter((item) => item?.folder_in === 'whitelist' || item?.folder_in === 'blacklist');
656
+ const [showRecentInLists, setShowRecentInLists] = React.useState(false);
657
+ const pendingAll = Array.isArray(state?.pending) ? state.pending : [];
658
+ // v1.8.1: an approval belongs to the conversation that raised it; another session must never show it.
659
+ const pending = pendingAll.filter((item) => item?.session_id === null || item?.session_id === undefined || item?.session_id === sessionId);
655
660
  return h('div', { className: 'w2mGpw_overlay', role: 'dialog', 'aria-label': t('gpwTitle') }, [
656
661
  h('div', { key: 'card', className: 'w2mGpw_card' }, [
657
662
  h('div', { key: 'head', className: 'w2mGpw_head' }, [
@@ -700,6 +705,15 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
700
705
  ])),
701
706
  foldRow(recent, 'recent'),
702
707
  ]),
708
+ recentInLists.length === 0 ? null : h('div', { key: 'recentInLists', className: 'w2mGpw_block' }, [
709
+ h('button', { key: 'toggle', type: 'button', className: 'w2mGpw_btn', 'aria-expanded': showRecentInLists, onClick: () => setShowRecentInLists((value) => !value) }, (showRecentInLists ? '收起' : '展开') + ' · 已在名单中 ' + recentInLists.length + ' 条'),
710
+ ...(showRecentInLists ? recentInLists.map((item, index) => h('div', { key: 'inlist' + index, className: 'w2mGpw_recent' }, [
711
+ h('div', { key: 'r1', className: 'w2mGpw_row' }, [
712
+ h('span', { key: 'd', className: 'w2mGpw_dot', 'data-c': item.color ?? 'green' }),
713
+ h('span', { key: 't', className: 'w2mGpw_path' }, (item.folder ?? '') + ' · 已加入' + (item.folder_in === 'whitelist' ? '白名单' : '黑名单')),
714
+ ]),
715
+ ])) : []),
716
+ ]),
703
717
  pending.length === 0 ? null : h('div', { key: 'pending', className: 'w2mGpw_block' }, [
704
718
  h('div', { key: 'title', className: 'w2mGpw_title' }, t('gpwPending')),
705
719
  ...pending.map((item) => h('div', { key: item.id, className: 'w2mGpw_recent' }, [
@@ -718,7 +732,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
718
732
  }
719
733
 
720
734
  /** The floating launcher: a pending badge, and the panel fixed to the lower-right corner. */
721
- function GpwLauncher({ t }) {
735
+ function GpwLauncher({ t, sessionId }) {
722
736
  const [open, setOpen] = React.useState(false);
723
737
  const [pendingCount, setPendingCount] = React.useState(0);
724
738
  React.useEffect(() => {
@@ -754,7 +768,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
754
768
  t('gpwLauncher'),
755
769
  pendingCount > 0 ? h('span', { key: 'badge', className: 'w2mGpw_badge' }, String(pendingCount)) : null,
756
770
  ]),
757
- open ? h(GpwPanel, { key: 'panel', t, onClose: () => setOpen(false) }) : null,
771
+ open ? h(GpwPanel, { key: 'panel', t, sessionId, onClose: () => setOpen(false) }) : null,
758
772
  ]);
759
773
  }
760
774
 
@@ -762,7 +776,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
762
776
  * v1.7.9: the inline approval prompt -- an interception asks a question in the composer row
763
777
  * (allow once / deny once / open the panel) instead of waiting silently for the operator.
764
778
  */
765
- function GpwPrompt({ t }) {
779
+ function GpwPrompt({ t, sessionId }) {
766
780
  const [state, setState] = React.useState(null);
767
781
  const [busy, setBusy] = React.useState(false);
768
782
  React.useEffect(() => {
@@ -783,7 +797,9 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
783
797
  clearInterval(timer);
784
798
  };
785
799
  }, []);
786
- const pending = Array.isArray(state?.pending) ? state.pending : [];
800
+ const pendingAll = Array.isArray(state?.pending) ? state.pending : [];
801
+ // v1.8.1: an approval belongs to the conversation that raised it; another session must never show it.
802
+ const pending = pendingAll.filter((item) => item?.session_id === null || item?.session_id === undefined || item?.session_id === sessionId);
787
803
  if (pending.length === 0) return null;
788
804
  const item = pending[0];
789
805
  const decide = async (decision) => {
@@ -1688,7 +1704,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1688
1704
  id: 'w2m-gpw',
1689
1705
  order: 43,
1690
1706
  label: 'GPW folders',
1691
- inject: () => ({ t }),
1707
+ inject: (sessionId) => ({ t, sessionId }),
1692
1708
  }, GpwLauncher));
1693
1709
  } catch (error) {
1694
1710
  console.error('@twinsearth/w2m-dsh-plugin: the GPW folder panel was not mounted; the rest of the page is unaffected', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,7 +64,7 @@
64
64
  "pack": "node scripts/pack.mjs",
65
65
  "reachability": "node scripts/reachability-check.mjs",
66
66
  "reachability:classify": "node scripts/reachability-classify.mjs",
67
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs",
67
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs",
68
68
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
69
69
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
70
70
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
@@ -82,7 +82,7 @@
82
82
  "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
83
83
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
84
84
  "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
85
- "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs",
85
+ "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs",
86
86
  "test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
87
87
  "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
88
88
  "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "1.8.0",
4
+ "version": "1.8.1",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -39,11 +39,51 @@ import { homedir } from 'node:os';
39
39
  import { dirname, join, resolve } from 'node:path';
40
40
  import { createHmac, randomBytes, randomUUID } from 'node:crypto';
41
41
 
42
- import { classifyToolCall } from './folder-policy.mjs';
42
+ import { classifyToolCall, isInside, normalizeFolder } from './folder-policy.mjs';
43
43
  import { classifyGpwCall, fingerprintOf } from './gpw-colors.mjs';
44
44
 
45
45
  export const name = 'dsh-gpw';
46
46
 
47
+ /**
48
+ * Which list (if any) already covers a folder, for the "recently intercepted" fold.
49
+ *
50
+ * A recent row whose folder is already covered by a list is noise: the operator already made that decision. Those rows
51
+ * are collapsed in the panel, and adding a folder from a recent row removes the row outright.
52
+ */
53
+ export function folderInLists(folder, lists) {
54
+ if (typeof folder !== 'string' || folder.trim() === '') return null;
55
+ let target;
56
+ try {
57
+ target = normalizeFolder(folder);
58
+ } catch {
59
+ return null;
60
+ }
61
+ const inList = (entries) => (Array.isArray(entries) ? entries : []).some((entry) => {
62
+ try {
63
+ const root = normalizeFolder(entry);
64
+ return isInside(target, root) || isInside(root, target);
65
+ } catch {
66
+ return false;
67
+ }
68
+ });
69
+ if (inList(lists?.whitelist)) return 'whitelist';
70
+ if (inList(lists?.blacklist)) return 'blacklist';
71
+ return null;
72
+ }
73
+
74
+ /** True when a recent row's folder is the folder just added, or lives inside it. */
75
+ export function foldIsCoveredBy(addedFolder, candidate) {
76
+ if (typeof addedFolder !== 'string' || typeof candidate !== 'string') return false;
77
+ try {
78
+ const added = normalizeFolder(addedFolder);
79
+ const target = normalizeFolder(candidate);
80
+ return target === added || isInside(target, added);
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+
47
87
  /** Services this plugin cannot work without. `connection` is used only for the operator routes. */
48
88
  export const inject = ['permissionPresets'];
49
89
 
@@ -426,6 +466,7 @@ export function apply(ctx, config = {}) {
426
466
  paths,
427
467
  color: 'yellow',
428
468
  fingerprint: fingerprintOf(toolCallOf(exec)),
469
+ sessionId: String(exec?.agent?.session?.header?.id ?? ''),
429
470
  };
430
471
  pending.set(id, entry);
431
472
  pendingTotal += 1;
@@ -608,15 +649,19 @@ export function apply(ctx, config = {}) {
608
649
  tampered: lists.tampered,
609
650
  tampered_reason: lists.reason,
610
651
  counters: { allowedTotal, pendingTotal, deniedTotal, budgetWindow: approvals.length, maxPerWindow, colors: { ...colorCounts }, grants: sessionGrants.size },
611
- recent: recent.map((entry) => ({
612
- at: entry.at,
613
- tool: entry.tool,
614
- decision: entry.decision,
615
- color: entry.color ?? null,
616
- reason: entry.reason,
617
- paths: entry.paths,
618
- folder: entry.paths.length > 0 ? folderForPath(entry.paths[0]) : null,
619
- })),
652
+ recent: recent.map((entry) => {
653
+ const folder = entry.paths.length > 0 ? folderForPath(entry.paths[0]) : null;
654
+ return {
655
+ at: entry.at,
656
+ tool: entry.tool,
657
+ decision: entry.decision,
658
+ color: entry.color ?? null,
659
+ reason: entry.reason,
660
+ paths: entry.paths,
661
+ folder,
662
+ folder_in: folderInLists(folder, lists),
663
+ };
664
+ }),
620
665
  pending: [...pending.entries()].map(([id, entry]) => ({
621
666
  id,
622
667
  tool: entry.tool,
@@ -283,7 +283,7 @@ const MAX_STAGES = 16;
283
283
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
284
284
  * overrides it at runtime for tests and packagers.
285
285
  */
286
- const PLUGIN_VERSION = '1.8.0';
286
+ const PLUGIN_VERSION = '1.8.1';
287
287
 
288
288
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
289
289
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;