@twinsearth/w2m-dsh-plugin 1.9.14 → 1.9.15

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.9.14"
8
+ "version": "1.9.15"
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.9.14",
15
+ "version": "1.9.15",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.9.14"
29
+ "version": "1.9.15"
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.9.14",
4
+ "version": "1.9.15",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.9.15] - 2026-10-28
11
+
12
+ - G0-1:operator token 安全存储。新增 `~/.w2m/operator-token`(0600,可用 W2M_OPERATOR_TOKEN_FILE 覆盖)与 `w2m-operator` CLI(set 从 stdin/文件读取、status 只显示指纹、clear);w2m_status 新增 operatorToken_fingerprint/operatorToken_source,token 值不进日志/状态。
13
+ - 解析顺序:profile config(兼容)→ 安全文件(推荐)→ 环境变量(兼容)。
14
+ - 新增 test/operator-token.test.mjs(6 项)。
15
+
10
16
  ## [1.9.14] - 2026-10-28
11
17
 
12
18
  - 修复「立即更新」失败(fetch failed):GitHub Release 下载被网络阻断时,自动回退到 npm registry 的同一版本 tarball;下载前校验 registry 的 sha512 integrity(缺失时 sha1 shasum),再由现有安装器做 sha256 校验、备份与回滚。
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Operator token CLI (v2 G0-1).
4
+ *
5
+ * The token is read from stdin, never from argv, and stored in a 0600 file. Output is JSON with a
6
+ * fingerprint and the file path only — the value is never printed back.
7
+ *
8
+ * Usage:
9
+ * echo <token> | node bin/w2m-operator.mjs set
10
+ * node bin/w2m-operator.mjs set --file <path> (reads the file, still never echoes it)
11
+ * node bin/w2m-operator.mjs status
12
+ * node bin/w2m-operator.mjs clear
13
+ *
14
+ * Exit codes: 0 ok, 1 refusal, 2 usage.
15
+ */
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ import {
19
+ clearOperatorToken,
20
+ fingerprintOf,
21
+ operatorTokenPath,
22
+ readOperatorToken,
23
+ writeOperatorToken,
24
+ } from '../src/plugin/operator-token.mjs';
25
+
26
+ const args = process.argv.slice(2);
27
+ const command = args[0];
28
+ const flag = (name) => {
29
+ const index = args.indexOf(name);
30
+ return index >= 0 ? args[index + 1] : undefined;
31
+ };
32
+ const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
33
+
34
+ async function readStdin() {
35
+ const chunks = [];
36
+ for await (const chunk of process.stdin) chunks.push(chunk);
37
+ return Buffer.concat(chunks).toString('utf8');
38
+ }
39
+
40
+ if (command === 'set') {
41
+ const file = flag('--file');
42
+ let token;
43
+ try { token = file !== undefined ? readFileSync(file, 'utf8') : await readStdin(); } catch (error) {
44
+ print({ ok: false, code: 'TOKEN_READ_FAILED', error: error instanceof Error ? error.message : String(error) });
45
+ process.exit(1);
46
+ }
47
+ const written = writeOperatorToken(token);
48
+ if (written.ok !== true) {
49
+ print({ ok: false, code: written.code, detail: written.detail ?? null });
50
+ process.exit(1);
51
+ }
52
+ print({ ok: true, path: written.path, fingerprint: written.fingerprint, source: 'file' });
53
+ process.exit(0);
54
+ }
55
+
56
+ if (command === 'status') {
57
+ const read = readOperatorToken();
58
+ if (read.configured !== true) {
59
+ print({ ok: true, configured: false, path: read.path, error: read.error ?? null });
60
+ process.exit(0);
61
+ }
62
+ print({ ok: true, configured: true, path: read.path, fingerprint: fingerprintOf(read.token) });
63
+ process.exit(0);
64
+ }
65
+
66
+ if (command === 'clear') {
67
+ const cleared = clearOperatorToken();
68
+ print({ ok: cleared.ok === true, path: cleared.path, code: cleared.code ?? null });
69
+ process.exit(cleared.ok === true ? 0 : 1);
70
+ }
71
+
72
+ print({ ok: false, code: 'USAGE', path: operatorTokenPath(), usage: 'set [--file <path>] | status | clear' });
73
+ process.exit(2);
@@ -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.9.14", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.15", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "1.9.15",
3
+ "case": "RELEASE-1915",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T01:02:47.935Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-1915-1793149367936",
11
+ "requirement": "V1.9.15:G0-1 operator token 安全存储与指纹状态",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "storage": "~/.w2m/operator-token 0600 or W2M_OPERATOR_TOKEN_FILE",
16
+ "cli": "set/status/clear, stdin only, fingerprint only",
17
+ "order": "config -> file -> env",
18
+ "status": "operatorToken_fingerprint/operatorToken_source, never value",
19
+ "tests": "operator-token 6/6"
20
+ }
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.9.14",
3
+ "version": "1.9.15",
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",
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",
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",
@@ -207,7 +207,8 @@
207
207
  "w2m-bootstrap-sign": "bin/w2m-bootstrap-sign.mjs",
208
208
  "w2m-hidden-task": "bin/w2m-hidden-task.mjs",
209
209
  "w2m-self-update": "bin/w2m-self-update.mjs",
210
- "w2m-joint": "bin/w2m-joint.mjs"
210
+ "w2m-joint": "bin/w2m-joint.mjs",
211
+ "w2m-operator": "bin/w2m-operator.mjs"
211
212
  },
212
213
  "disclosure": {
213
214
  "cloud": true,
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.9.14",
4
+ "version": "1.9.15",
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": [
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Secure storage for the relay operator token (v2 G0-1).
3
+ *
4
+ * The token must not travel through chat, argv, logs or environment dumps. This module stores it in
5
+ * a single 0600 file under the per-user W2M directory (or W2M_OPERATOR_TOKEN_FILE), and every read
6
+ * returns metadata plus at most a short fingerprint — never the value in logs.
7
+ *
8
+ * Resolution order used by the plugin: profile config (legacy) → this file → W2M_OPERATOR_TOKEN
9
+ * (legacy compatibility; discouraged). The file is the recommended source: it is not visible in the
10
+ * process environment and not echoed by any status output.
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+
17
+ const MIN_LENGTH = 16;
18
+ const MAX_LENGTH = 4096;
19
+
20
+ /** Where the token file lives. */
21
+ export function operatorTokenPath(env = process.env) {
22
+ const explicit = typeof env.W2M_OPERATOR_TOKEN_FILE === 'string' ? env.W2M_OPERATOR_TOKEN_FILE.trim() : '';
23
+ if (explicit !== '') return explicit;
24
+ return join(homedir(), '.w2m', 'operator-token');
25
+ }
26
+
27
+ /** A stable, non-reversible fingerprint for display ("configured, id 3f2a1c..."). */
28
+ export function fingerprintOf(token) {
29
+ if (typeof token !== 'string' || token === '') return null;
30
+ return createHash('sha256').update(token, 'utf8').digest('hex').slice(0, 12);
31
+ }
32
+
33
+ export function validateOperatorToken(token) {
34
+ if (typeof token !== 'string') return { ok: false, code: 'OPERATOR_TOKEN_INVALID' };
35
+ const value = token.trim();
36
+ if (value.length < MIN_LENGTH) return { ok: false, code: 'OPERATOR_TOKEN_TOO_SHORT', detail: { min: MIN_LENGTH } };
37
+ if (value.length > MAX_LENGTH) return { ok: false, code: 'OPERATOR_TOKEN_TOO_LONG', detail: { max: MAX_LENGTH } };
38
+ if (/[\r\n]/.test(value)) return { ok: false, code: 'OPERATOR_TOKEN_INVALID', detail: 'token must be a single line' };
39
+ return { ok: true, token: value };
40
+ }
41
+
42
+ /**
43
+ * Read the token file.
44
+ *
45
+ * @returns {{configured: false, path: string}
46
+ * | {configured: true, token: string, path: string, fingerprint: string, mode: number|null}}
47
+ */
48
+ export function readOperatorToken(options = {}) {
49
+ const env = options.env ?? process.env;
50
+ const path = options.path ?? operatorTokenPath(env);
51
+ if (!existsSync(path)) return { configured: false, path };
52
+ let raw;
53
+ try { raw = readFileSync(path, 'utf8'); } catch (error) {
54
+ return { configured: false, path, error: error instanceof Error ? error.message : String(error) };
55
+ }
56
+ const checked = validateOperatorToken(raw);
57
+ if (checked.ok !== true) return { configured: false, path, error: checked.code };
58
+ return { configured: true, token: checked.token, path, fingerprint: fingerprintOf(checked.token) };
59
+ }
60
+
61
+ export function writeOperatorToken(token, options = {}) {
62
+ const checked = validateOperatorToken(token);
63
+ if (checked.ok !== true) return checked;
64
+ const env = options.env ?? process.env;
65
+ const path = options.path ?? operatorTokenPath(env);
66
+ try {
67
+ mkdirSync(dirname(path), { recursive: true });
68
+ writeFileSync(path, checked.token + '\n', { encoding: 'utf8', mode: 0o600 });
69
+ try { chmodSync(path, 0o600); } catch { /* Windows: the file ACL carries the protection */ }
70
+ } catch (error) {
71
+ return { ok: false, code: 'OPERATOR_TOKEN_WRITE_FAILED', detail: error instanceof Error ? error.message : String(error) };
72
+ }
73
+ return { ok: true, path, fingerprint: fingerprintOf(checked.token) };
74
+ }
75
+
76
+ export function clearOperatorToken(options = {}) {
77
+ const env = options.env ?? process.env;
78
+ const path = options.path ?? operatorTokenPath(env);
79
+ try { rmSync(path, { force: true }); } catch (error) {
80
+ return { ok: false, code: 'OPERATOR_TOKEN_CLEAR_FAILED', detail: error instanceof Error ? error.message : String(error) };
81
+ }
82
+ return { ok: true, path };
83
+ }
@@ -191,6 +191,7 @@ import {
191
191
  import { registerChatRoutes } from './fleet-chat-route.mjs';
192
192
  import { registerExecCapabilityRoutes } from './exec-capability-route.mjs';
193
193
  import { registerPluginUpdateRoutes } from './update-route.mjs';
194
+ import { fingerprintOf, readOperatorToken } from './operator-token.mjs';
194
195
  import { registerCaptureRoutes } from './capture-route.mjs';
195
196
  import { defaultExecCapabilityFile } from '../util/exec-capability.mjs';
196
197
  import { createFleetCouncil, fleetCouncilThread } from './council-fleet.mjs';
@@ -283,7 +284,7 @@ const MAX_STAGES = 16;
283
284
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
284
285
  * overrides it at runtime for tests and packagers.
285
286
  */
286
- const PLUGIN_VERSION = '1.9.14';
287
+ const PLUGIN_VERSION = '1.9.15';
287
288
 
288
289
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
289
290
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -687,6 +688,7 @@ async function loadDefineTool() {
687
688
  * p2pMode: string, stunServers: string[]}}
688
689
  */
689
690
  function readConfig(config = {}) {
691
+ let operatorTokenSource = null;
690
692
  const cwd = process.cwd();
691
693
 
692
694
  /**
@@ -872,11 +874,23 @@ function readConfig(config = {}) {
872
874
  ? process.env.W2M_PAIRING_CODE.trim()
873
875
  : null),
874
876
  // §5: the operator credential. Only `w2m_run` uses it.
875
- operatorToken: typeof config.operatorToken === 'string' && config.operatorToken.trim() !== ''
876
- ? config.operatorToken.trim()
877
- : (typeof process.env.W2M_OPERATOR_TOKEN === 'string' && process.env.W2M_OPERATOR_TOKEN.trim() !== ''
877
+ // v2 G0-1: the token file (0600) is the recommended source; profile config stays for legacy,
878
+ // env last. The value never reaches a status output - only the source and a fingerprint do.
879
+ operatorToken: (() => {
880
+ const fromConfig = typeof config.operatorToken === 'string' && config.operatorToken.trim() !== ''
881
+ ? config.operatorToken.trim()
882
+ : null;
883
+ if (fromConfig !== null) { operatorTokenSource = 'config'; return fromConfig; }
884
+ const secure = readOperatorToken();
885
+ if (secure.configured === true) { operatorTokenSource = 'file'; return secure.token; }
886
+ const fromEnv = typeof process.env.W2M_OPERATOR_TOKEN === 'string' && process.env.W2M_OPERATOR_TOKEN.trim() !== ''
878
887
  ? process.env.W2M_OPERATOR_TOKEN.trim()
879
- : null),
888
+ : null;
889
+ if (fromEnv !== null) { operatorTokenSource = 'env'; return fromEnv; }
890
+ operatorTokenSource = null;
891
+ return null;
892
+ })(),
893
+ operatorTokenSource,
880
894
  // ---------------------------------------------------------------------------------------------
881
895
  // v0.2.3: daily self-update
882
896
  // ---------------------------------------------------------------------------------------------
@@ -4996,6 +5010,7 @@ const result = await request({
4996
5010
  // wrong one without spending a dispatch, and `required_by_relay` is what makes the two
4997
5011
  // halves of the mismatch (client has none / relay demands one) visible side by side.
4998
5012
  operatorToken_configured: cfg.operatorToken !== null,
5013
+ operatorToken_fingerprint: cfg.operatorToken === null ? null : fingerprintOf(cfg.operatorToken),
4999
5014
  operatorToken_source: cfg.operatorToken !== null
5000
5015
  ? (typeof config?.operatorToken === 'string' && config.operatorToken.trim() !== ''
5001
5016
  ? 'config'