@twinsearth/w2m-dsh-plugin 1.8.0 → 1.8.2

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.2"
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.2",
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.2"
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.2",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.8.2] — 2026-10-26
11
+
12
+ **GPW 超时策略:黄类(名单外)100 秒无人应答自动同意;黑白名单规则不变;已覆盖父目录的子文件夹拦截记录折叠。**
13
+
14
+ - **只读工具绿名单扩展**:`read/read_file/search/grep/glob/list/…`、`job_list/job_output/job_status/job_wait`、`w2m_status/w2m_devices/w2m_tasks/w2m_history/w2m_report/w2m_memory/w2m_stats` 判为绿色(无打扰放行);`job_kill/job_start/w2m_run/w2m_cancel/w2m_intent` 仍为黄色(可变更,需人工)。
15
+ - 超时默认值 120s → **100s**;超时结果由「拒绝」改为「自动同意」(仅黄类 / 名单外路径调用)。
16
+ - 红线(危险命令、敏感/系统路径、显式 deny)仍然立即拒绝,不进入待审批队列;白名单命中仍然立即放行。
17
+ 因此超时规则不会扩大任何名单的授权范围。
18
+ - 自动同意的调用会记录为会话授权(指纹),同一操作的后续出现以蓝色(已授权常态)直接放行。
19
+ - 「最近被拦截」折叠逻辑确认覆盖父目录下的子文件夹:名单条目是根目录,其任意深度子文件夹的拦截记录都会进入
20
+ 「已在名单中 N 条」折叠组(`folder_in` 双向包含匹配)。
21
+ - 新增 `test/gpw-auto-approve.test.mjs`(4 项:超时自动同意→下次变蓝;红线仍拒;白名单立即放行;100s 默认值)。
22
+ - 证据:`docs/acceptance/1.8.2/GPW-AUTO-APPROVE.json`。
23
+ ## [1.8.1] — 2026-10-26
24
+
25
+ **GPW「最近被拦截」去噪:已在名单中的条目折叠显示;加入名单后条目立即消失。**
26
+
27
+ - 状态接口为每条 recent 增加 `folder_in`(whitelist / blacklist / null):文件夹已被名单覆盖的拦截记录不再占用主列表。
28
+ - 面板把已覆盖条目折叠为「已在名单中 N 条 · 展开/收起」,展开后只读展示(不重复给加入按钮)。
29
+ - 从最近拦截一键加入白/黑名单时,服务端同步裁剪该条目(以及被同一父目录覆盖的条目),下一次轮询不再出现。
30
+ - 新增 `test/gpw-recent-fold.test.mjs`(4 项:覆盖归属、嵌套覆盖、客户端折叠静态断言、状态接口字段)。
31
+ - 证据:`docs/acceptance/1.8.1/GPW-RECENT-FOLD.json`。
10
32
  ## [1.8.0] — 2026-10-26
11
33
 
12
34
  **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.2", "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
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": "1.8.2",
3
+ "case": "GPW-AUTO-APPROVE",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-26T07:41:28.057Z",
9
+ "executor": "lead",
10
+ "task_id": "local-gpw-timeout-20261026074128",
11
+ "requirement": "a class-C (yellow) call with no operator answer is approved after 100 s, while blacklist/whitelist rules and red denials are unchanged; subfolders of a listed parent folder are folded in recently-intercepted",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "timeout": "DEFAULT_PENDING_TIMEOUT_MS = 100_000; on expiry the pending call resolves allow and records a session grant",
16
+ "lists_unchanged": "red denies immediately (never queued); whitelist hits allow immediately (never queued)",
17
+ "next_call_blue": "the auto-approved fingerprint is added to session grants, so the identical next call runs immediately",
18
+ "fold_parents": "folder_in uses bidirectional containment: a child folder under a listed parent maps to that list and is collapsed",
19
+ "tests": "test/gpw-auto-approve.test.mjs 4/4",
20
+ "readonly_green": "read-only tools are green (read/grep/glob/web_*/status/health/metrics/inspect, job_list/job_output/job_status/job_wait, w2m_status/devices/tasks/history/report/memory/stats); mutating tools (job_kill/job_start/w2m_run/w2m_cancel/w2m_intent) stay yellow"
21
+ }
22
+ }
23
+ }
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.2",
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 test/gpw-auto-approve.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 test/gpw-auto-approve.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.2",
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": [
@@ -27,7 +27,18 @@ export const GPW_COLOR_LABELS = Object.freeze({
27
27
  });
28
28
 
29
29
  /** Tools that only read: green by their nature. */
30
- const READONLY_TOOLS = /^(read|search|grep|glob|list|dir|ls|web_search|web_fetch|fetch|cat|head|tail|status|health|metrics|inspect)$/i;
30
+ const READONLY_TOOLS = new RegExp(
31
+ '^(' +
32
+ // local read-only tools
33
+ 'read|read_file|search|grep|glob|glob_files|list|list_files|dir|ls|cat|head|tail|view|' +
34
+ 'web_search|web_fetch|fetch|status|health|metrics|inspect|' +
35
+ // background-job observation is read-only; kill/spawn are not in this list on purpose
36
+ 'job_list|job_output|job_status|job_wait|' +
37
+ // read-only fleet queries (w2m_run / w2m_cancel / w2m_intent mutate, so they stay yellow)
38
+ 'w2m_status|w2m_devices|w2m_tasks|w2m_history|w2m_report|w2m_memory|w2m_wait|w2m_stats' +
39
+ ')$',
40
+ 'i',
41
+ );
31
42
 
32
43
  /** Command tools: their argv decides, not the tool name. */
33
44
  const COMMAND_TOOLS = /^(bash|pwsh|powershell|shell|terminal|run_command|exec)$/i;
@@ -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
 
@@ -55,7 +95,7 @@ export const GPW_PICK_ROUTE = '/api/w2m/gpw/pick';
55
95
  export const GPW_RESET_ROUTE = '/api/w2m/gpw/reset';
56
96
 
57
97
  const DEFAULT_BUDGET = { maxPerWindow: 10, windowMs: 60_000, minGapMs: 3_000 };
58
- const DEFAULT_PENDING_TIMEOUT_MS = 120_000;
98
+ const DEFAULT_PENDING_TIMEOUT_MS = 100_000;
59
99
  const RECENT_LIMIT = 20;
60
100
  const NO_STORE = { 'cache-control': 'no-store' };
61
101
  /** v1.7.9: lists are MAC-signed (v2) so an external edit is detectable instead of silently trusted. */
@@ -426,16 +466,22 @@ 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;
432
473
  const timer = setTimeout(() => {
433
474
  if (!pending.has(id)) return;
434
475
  pending.delete(id);
435
- deniedTotal += 1;
436
- resolveDecision({ kind: 'deny', reason: `GPW: no operator answer within ${Math.round(pendingTimeoutMs / 1000)} s for ${entry.tool}` });
476
+ // v1.8.2 timeout policy: a yellow (class-C) call that nobody answered is approved after 100 s. Red never reaches
477
+ // this queue (it is denied outright) and black/white list hits never become pending, so this rule cannot widen
478
+ // either list's authority. The approval is recorded as a session grant, which is what turns its next occurrence
479
+ // blue.
480
+ allowedTotal += 1;
481
+ colorCounts.blue += 1;
482
+ if (typeof entry.fingerprint === 'string' && entry.fingerprint !== '') sessionGrants.add(entry.fingerprint);
483
+ resolveDecision({ kind: 'allow', reason: `GPW: auto-approved after ${Math.round(pendingTimeoutMs / 1000)} s with no operator answer (yellow path; black/white lists keep their own rules)` });
437
484
  }, pendingTimeoutMs);
438
- timer.unref?.();
439
485
  entry.timer = timer;
440
486
  });
441
487
 
@@ -608,15 +654,19 @@ export function apply(ctx, config = {}) {
608
654
  tampered: lists.tampered,
609
655
  tampered_reason: lists.reason,
610
656
  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
- })),
657
+ recent: recent.map((entry) => {
658
+ const folder = entry.paths.length > 0 ? folderForPath(entry.paths[0]) : null;
659
+ return {
660
+ at: entry.at,
661
+ tool: entry.tool,
662
+ decision: entry.decision,
663
+ color: entry.color ?? null,
664
+ reason: entry.reason,
665
+ paths: entry.paths,
666
+ folder,
667
+ folder_in: folderInLists(folder, lists),
668
+ };
669
+ }),
620
670
  pending: [...pending.entries()].map(([id, entry]) => ({
621
671
  id,
622
672
  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.2';
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;