@twinsearth/w2m-dsh-plugin 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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 test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.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 test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.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",
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": "2.0.2",
4
+ "version": "2.0.4",
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": [
@@ -95,7 +95,21 @@ export const GPW_PICK_ROUTE = '/api/w2m/gpw/pick';
95
95
  export const GPW_RESET_ROUTE = '/api/w2m/gpw/reset';
96
96
 
97
97
  const DEFAULT_BUDGET = { maxPerWindow: 10, windowMs: 60_000, minGapMs: 3_000 };
98
- const DEFAULT_PENDING_TIMEOUT_MS = 100_000;
98
+ const DEFAULT_PENDING_TIMEOUT_MS = 10_000;
99
+
100
+ /** Two policy strategies; novice is the product default. */
101
+ export const GPW_MODES = Object.freeze(['novice', 'advanced']);
102
+ export const DEFAULT_GPW_MODE = 'novice';
103
+ /** At most one path-naming auto-pass per 3 s in novice mode. */
104
+ export const NOVICE_APPROVE_GAP_MS = 3000;
105
+ /** Always refused in novice mode, even with an empty operator blacklist. */
106
+ export const DEFAULT_PROTECTED_FOLDERS = Object.freeze([
107
+ 'C:\\Windows',
108
+ 'C:\\Program Files',
109
+ 'D:\\Program Files',
110
+ 'C:\\Program Files (x86)',
111
+ 'D:\\Program Files (x86)',
112
+ ]);
99
113
  const RECENT_LIMIT = 20;
100
114
  const NO_STORE = { 'cache-control': 'no-store' };
101
115
  /** v1.7.9: lists are MAC-signed (v2) so an external edit is detectable instead of silently trusted. */
@@ -164,29 +178,30 @@ export function readFolders(file, key = null, options = {}) {
164
178
  raw = readFileSync(file, 'utf8');
165
179
  } catch (error) {
166
180
  if (error?.code === 'ENOENT' && key !== null && options.allowMissing !== true) {
167
- return { whitelist: [], blacklist: [], tampered: true, reason: 'folder lists file is missing' };
181
+ return { whitelist: [], blacklist: [], mode: 'novice', tampered: true, reason: 'folder lists file is missing' };
168
182
  }
169
183
  if (error?.code !== 'ENOENT') {
170
184
  return { whitelist: [], blacklist: [], tampered: true, reason: `folder lists unreadable: ${error?.message ?? error}` };
171
185
  }
172
- return { whitelist: [], blacklist: [], tampered: false, reason: null, legacy: false };
186
+ return { whitelist: [], blacklist: [], mode: 'novice', tampered: false, reason: null, legacy: false };
173
187
  }
174
188
  let parsed = null;
175
189
  try {
176
190
  parsed = JSON.parse(raw);
177
191
  } catch {
178
- return { whitelist: [], blacklist: [], tampered: true, reason: 'folder lists file is not valid JSON' };
192
+ return { whitelist: [], blacklist: [], mode: 'novice', tampered: true, reason: 'folder lists file is not valid JSON' };
179
193
  }
180
194
  const whitelist = Array.isArray(parsed?.whitelist) ? parsed.whitelist.filter((v) => typeof v === 'string' && v !== '') : [];
181
195
  const blacklist = Array.isArray(parsed?.blacklist) ? parsed.blacklist.filter((v) => typeof v === 'string' && v !== '') : [];
182
196
  if (parsed?.version !== FOLDERS_VERSION || typeof parsed?.mac !== 'string') {
183
- return { whitelist, blacklist, tampered: false, reason: null, legacy: true };
197
+ return { whitelist, blacklist, mode: 'novice', tampered: false, reason: null, legacy: true };
184
198
  }
185
- if (key === null) return { whitelist, blacklist, tampered: false, reason: null, legacy: false };
199
+ const storedMode = parsed?.mode === 'advanced' ? 'advanced' : 'novice';
200
+ if (key === null) return { whitelist, blacklist, mode: storedMode, tampered: false, reason: null, legacy: false };
186
201
  if (macOfFolders(whitelist, blacklist, key) !== parsed.mac) {
187
- return { whitelist, blacklist, tampered: true, reason: 'folder lists were modified outside GPW (MAC mismatch)' };
202
+ return { whitelist, blacklist, mode: storedMode, tampered: true, reason: 'folder lists were modified outside GPW (MAC mismatch)' };
188
203
  }
189
- return { whitelist, blacklist, tampered: false, reason: null, legacy: false };
204
+ return { whitelist, blacklist, mode: storedMode, tampered: false, reason: null, legacy: false };
190
205
  }
191
206
 
192
207
  /**
@@ -200,7 +215,8 @@ export function writeFolders(file, folders, key = null) {
200
215
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
201
216
  const whitelist = [...new Set((folders?.whitelist ?? []).filter((v) => typeof v === 'string' && v.trim() !== ''))];
202
217
  const blacklist = [...new Set((folders?.blacklist ?? []).filter((v) => typeof v === 'string' && v.trim() !== ''))];
203
- const next = { version: FOLDERS_VERSION, whitelist, blacklist };
218
+ const mode = folders?.mode === 'advanced' ? 'advanced' : 'novice';
219
+ const next = { version: FOLDERS_VERSION, whitelist, blacklist, mode };
204
220
  if (key !== null) {
205
221
  next.algo = FOLDERS_MAC_ALGO;
206
222
  next.mac = macOfFolders(whitelist, blacklist, key);
@@ -213,12 +229,12 @@ export function writeFolders(file, folders, key = null) {
213
229
  try { chmodSync(tmp, 0o600); } catch { /* best effort */ }
214
230
  renameSync(tmp, file);
215
231
  try { chmodSync(file, 0o444); } catch { /* read-only is best effort on Windows, MAC still verifies */ }
216
- return { whitelist, blacklist };
232
+ return { whitelist, blacklist, mode };
217
233
  }
218
234
 
219
235
  /** Operator reset: drop every entry and re-sign. Used only after a tamper report. */
220
236
  export function resetFolders(file, key) {
221
- return writeFolders(file, { whitelist: [], blacklist: [] }, key);
237
+ return writeFolders(file, { whitelist: [], blacklist: [], mode: 'novice' }, key);
222
238
  }
223
239
 
224
240
  /**
@@ -363,6 +379,7 @@ export function apply(ctx, config = {}) {
363
379
  const minGapMs = positiveInt(config.minGapMs, DEFAULT_BUDGET.minGapMs);
364
380
  const pendingTimeoutMs = positiveInt(config.pendingTimeoutMs, DEFAULT_PENDING_TIMEOUT_MS);
365
381
  const requireFullAccess = config.requireFullAccess !== false;
382
+ const configuredMode = config.gpwMode === 'advanced' ? 'advanced' : DEFAULT_GPW_MODE;
366
383
  const foldersFile = foldersFileOf(config);
367
384
  const keyInfo = ensureFoldersKey(foldersFile, config);
368
385
  const foldersKey = keyInfo.key;
@@ -384,6 +401,8 @@ export function apply(ctx, config = {}) {
384
401
  noteTamper(initialFolders.reason);
385
402
  } else {
386
403
  // Always leave a signed v2 file behind (first run creates the empty one; later runs re-sign it).
404
+ // A configured mode is the bootstrap choice when the file carries none yet (tests and first run).
405
+ if (config.gpwMode !== undefined) initialFolders.mode = configuredMode;
387
406
  try {
388
407
  writeFolders(foldersFile, initialFolders, foldersKey);
389
408
  foldersSeen = true;
@@ -398,6 +417,7 @@ export function apply(ctx, config = {}) {
398
417
  let allowedTotal = 0;
399
418
  let pendingTotal = 0;
400
419
  let deniedTotal = 0;
420
+ let lastNovicePassAt = 0;
401
421
  /** id -> { resolve, createdAt, tool, reason, paths } */
402
422
  const pending = new Map();
403
423
  /** Newest-first record of calls the gate saw, so the panel can offer "add this folder" in one click. */
@@ -422,9 +442,13 @@ export function apply(ctx, config = {}) {
422
442
  foldersSeen = true;
423
443
  }
424
444
  const merge = (a, b) => [...new Set([...(Array.isArray(a) ? a : []), ...b])];
445
+ const mode = config.gpwMode !== undefined ? configuredMode : (stored.mode ?? configuredMode);
446
+ const storedBlacklist = merge(config.blacklist, stored.blacklist);
425
447
  return {
448
+ mode,
426
449
  whitelist: merge(config.whitelist, stored.whitelist),
427
- blacklist: merge(config.blacklist, stored.blacklist),
450
+ blacklist: storedBlacklist,
451
+ effectiveBlacklist: mode === 'novice' ? [...new Set([...DEFAULT_PROTECTED_FOLDERS, ...storedBlacklist])] : storedBlacklist,
428
452
  tampered: tamperedReason !== null,
429
453
  reason: tamperedReason,
430
454
  };
@@ -529,8 +553,8 @@ export function apply(ctx, config = {}) {
529
553
  ctx.logger?.warn?.(`dsh-gpw: refused ${String(call.name ?? 'tool')} -- lists locked (${lists.reason})`);
530
554
  return `GPW: folder lists failed integrity check (${lists.reason}); reset them in the GPW panel`;
531
555
  }
532
- if (lists.blacklist.length === 0) return undefined;
533
- const result = classifyToolCall({ ...call, blacklist: lists.blacklist });
556
+ if (lists.effectiveBlacklist.length === 0) return undefined;
557
+ const result = classifyToolCall({ ...call, blacklist: lists.effectiveBlacklist });
534
558
  if (result.decision !== 'deny') return undefined;
535
559
  deniedTotal += 1;
536
560
  noteRecent({ tool: String(call.name ?? 'tool'), decision: 'deny', reason: result.reason, paths: result.paths });
@@ -575,6 +599,34 @@ export function apply(ctx, config = {}) {
575
599
  ctx.logger?.warn?.(`dsh-gpw: refused ${String(call.name ?? 'tool')} -- lists locked (${lists.reason})`);
576
600
  return { kind: 'deny', reason: `GPW: folder lists failed integrity check (${lists.reason}); reset them in the GPW panel` };
577
601
  }
602
+ if (lists.mode === 'novice') {
603
+ // 小白模式: only the blacklist restricts anything; every other call passes.
604
+ // At most one path-naming call per 3 s is auto-passed, so a burst cannot do unbounded damage.
605
+ let noviceCwd = null;
606
+ try {
607
+ noviceCwd = await ctx.workingDirectory?.ensure?.(exec.agent, exec.signal);
608
+ } catch {
609
+ noviceCwd = null;
610
+ }
611
+ const noviceCall = toolCallOf(exec);
612
+ const noviceClassification = classifyToolCall({ ...noviceCall, blacklist: lists.effectiveBlacklist, workspace: noviceCwd ?? null, base: noviceCwd ?? undefined });
613
+ if (noviceClassification.decision === 'deny') {
614
+ deniedTotal += 1;
615
+ colorCounts.red += 1;
616
+ noteRecent({ tool: String(noviceCall.name ?? 'tool'), decision: 'deny', color: 'red', reason: noviceClassification.reason, paths: noviceClassification.paths });
617
+ ctx.logger?.warn?.('dsh-gpw: novice -- refused ' + String(noviceCall.name ?? 'tool') + ' -- ' + noviceClassification.reason);
618
+ return { kind: 'deny', reason: 'GPW: ' + noviceClassification.reason + '; the call was not executed' };
619
+ }
620
+ if (noviceClassification.paths.length > 0) {
621
+ const waitMs = NOVICE_APPROVE_GAP_MS - (Date.now() - lastNovicePassAt);
622
+ if (waitMs > 0) await new Promise((resolveSlot) => { setTimeout(resolveSlot, Math.min(waitMs, NOVICE_APPROVE_GAP_MS)); });
623
+ lastNovicePassAt = Date.now();
624
+ }
625
+ colorCounts.green += 1;
626
+ const noviceReason = '小白模式:黑名单外全部放行(每 3 秒最多一次路径操作)';
627
+ noteRecent({ tool: String(noviceCall.name ?? 'tool'), decision: 'allow', color: 'green', reason: noviceReason, paths: noviceClassification.paths });
628
+ return { kind: 'allow', reason: noviceReason };
629
+ }
578
630
  let cwd = null;
579
631
  try {
580
632
  cwd = await ctx.workingDirectory?.ensure?.(exec.agent, exec.signal);
@@ -651,6 +703,11 @@ export function apply(ctx, config = {}) {
651
703
  enabled: true,
652
704
  require_full_access: requireFullAccess,
653
705
  folders: { whitelist: lists.whitelist, blacklist: lists.blacklist },
706
+ mode: lists.mode,
707
+ modes: [...GPW_MODES],
708
+ protected_folders: lists.mode === 'novice' ? [...DEFAULT_PROTECTED_FOLDERS, ...lists.blacklist] : [...lists.blacklist],
709
+ pending_timeout_ms: pendingTimeoutMs,
710
+ novice_approve_gap_ms: NOVICE_APPROVE_GAP_MS,
654
711
  tampered: lists.tampered,
655
712
  tampered_reason: lists.reason,
656
713
  counters: { allowedTotal, pendingTotal, deniedTotal, budgetWindow: approvals.length, maxPerWindow, colors: { ...colorCounts }, grants: sessionGrants.size },
@@ -690,6 +747,22 @@ export function apply(ctx, config = {}) {
690
747
  } catch {
691
748
  /* refused below */
692
749
  }
750
+ if (body?.action === 'set-mode') {
751
+ const current = folderLists();
752
+ if (current.tampered) {
753
+ return json({ ok: false, error: 'folder lists are locked after an integrity failure (' + current.reason + '); reset them first' }, 409);
754
+ }
755
+ const mode = body?.mode;
756
+ if (mode !== 'novice' && mode !== 'advanced') return json({ ok: false, error: 'mode must be novice or advanced' }, 400);
757
+ const stored = readFolders(foldersFile, foldersKey, { allowMissing: true });
758
+ try {
759
+ writeFolders(foldersFile, { ...stored, mode }, foldersKey);
760
+ } catch (error) {
761
+ return json({ ok: false, error: 'could not write the policy mode: ' + (error?.message ?? error) }, 500);
762
+ }
763
+ ctx.logger?.info?.('dsh-gpw: policy mode -> ' + mode);
764
+ return json({ ok: true, mode, folders: folderLists() });
765
+ }
693
766
  const list = body?.list;
694
767
  if (list !== 'whitelist' && list !== 'blacklist') return json({ ok: false, error: 'list must be whitelist or blacklist' }, 400);
695
768
  if (body?.action !== 'add' && body?.action !== 'remove') return json({ ok: false, error: 'action must be add or remove' }, 400);
@@ -698,6 +771,17 @@ export function apply(ctx, config = {}) {
698
771
  return json({ ok: false, error: `folder lists are locked after an integrity failure (${current.reason}); reset them first` }, 409);
699
772
  }
700
773
  const stored = readFolders(foldersFile, foldersKey, { allowMissing: true });
774
+ if (body?.action === 'set-mode') {
775
+ const mode = body?.mode;
776
+ if (mode !== 'novice' && mode !== 'advanced') return json({ ok: false, error: 'mode must be novice or advanced' }, 400);
777
+ try {
778
+ writeFolders(foldersFile, { ...stored, mode }, foldersKey);
779
+ } catch (error) {
780
+ return json({ ok: false, error: 'could not write the policy mode: ' + (error?.message ?? error) }, 500);
781
+ }
782
+ ctx.logger?.info?.('dsh-gpw: policy mode -> ' + mode);
783
+ return json({ ok: true, mode, folders: folderLists() });
784
+ }
701
785
  if (body.action === 'remove') {
702
786
  if (typeof body?.path !== 'string' || body.path.trim() === '') return json({ ok: false, error: 'path must be a non-empty string' }, 400);
703
787
  const next = { ...stored, [list]: stored[list].filter((entry) => entry !== body.path) };
@@ -734,7 +818,7 @@ export function apply(ctx, config = {}) {
734
818
  }
735
819
  if (body?.confirm !== 'reset') return json({ ok: false, error: "confirm must be the string 'reset'" }, 400);
736
820
  try {
737
- resetFolders(foldersFile, foldersKey);
821
+ resetFolders(foldersFile, foldersKey, folderLists().mode);
738
822
  } catch (error) {
739
823
  return json({ ok: false, error: `could not reset folder lists: ${error?.message ?? error}` }, 500);
740
824
  }
@@ -286,7 +286,7 @@ const MAX_STAGES = 16;
286
286
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
287
287
  * overrides it at runtime for tests and packagers.
288
288
  */
289
- const PLUGIN_VERSION = '2.0.2';
289
+ const PLUGIN_VERSION = '2.0.4';
290
290
 
291
291
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
292
292
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;