@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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +187 -462
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.3/RELEASE-2030.json +22 -0
- package/docs/acceptance/2.0.4/RELEASE-2040.json +22 -0
- package/lib/client.js +2616 -2552
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/plugin/gpw.mjs +99 -15
- package/src/plugin/tools.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
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": [
|
package/src/plugin/gpw.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
533
|
-
const result = classifyToolCall({ ...call, blacklist: lists.
|
|
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
|
}
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -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.
|
|
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;
|