@twinsearth/w2m-dsh-plugin 0.5.8 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,32 @@ export const DEFAULT_PAIR_RATE_LIMIT_PER_MINUTE = 5; // v0.1.2 §6
36
36
  export const RATE_LIMIT_WINDOW_MS = 60_000;
37
37
  export const DEFAULT_STATE_DIR = path.join(os.homedir(), '.w2m');
38
38
 
39
+ /**
40
+ * The environment variable that opts in to printing the operator token on a start that did not
41
+ * generate it (MEDIUM-6 of the v0.5.9 review).
42
+ *
43
+ * The value is printed **once, on the start that generated it**, because that is the only moment at
44
+ * which it exists nowhere else yet; every other start has `<state>/operator-token.txt` to read it
45
+ * from, and a relay's stdout ends up in journald, a container log or a CI transcript — where a
46
+ * credential that authorises dispatching to the whole fleet does not belong. `W2M_PRINT_OPERATOR_TOKEN=1`
47
+ * (or `printOperatorToken: true`, which is the same switch for an embedder) restores the old
48
+ * behaviour for an operator who genuinely needs it in the log, and says so in the banner when it does.
49
+ */
50
+ export const PRINT_OPERATOR_TOKEN_ENV = 'W2M_PRINT_OPERATOR_TOKEN';
51
+
52
+ /**
53
+ * Resolve that opt-in, so the relay and the CLI agree on one answer.
54
+ *
55
+ * @param {object} [options] Relay options; `printOperatorToken: true` is the programmatic spelling.
56
+ * @returns {'option'|'env'|null} Why printing was asked for, or null.
57
+ */
58
+ export function printOperatorTokenOptIn(options = {}) {
59
+ if (options.printOperatorToken === true) return 'option';
60
+ const raw = process.env[PRINT_OPERATOR_TOKEN_ENV];
61
+ if (raw === '1' || raw === 'true') return 'env';
62
+ return null;
63
+ }
64
+
39
65
  /* ------------------------------------------------------------------ */
40
66
  /* helpers */
41
67
  /* ------------------------------------------------------------------ */
@@ -459,11 +485,19 @@ export class RelayServer {
459
485
  const wantsRequired = options.operatorTokenRequired !== false;
460
486
  const explicit = options.operatorToken;
461
487
  const persisted = this.persistence.enabled ? this.persistence.readOperatorToken() : null;
488
+ /**
489
+ * Why the token may be printed, when it may be: `'generated'` is the one start whose token exists
490
+ * nowhere else until this process writes it, and `'option'`/`'env'` are the documented opt-ins.
491
+ * @type {'generated'|'option'|'env'|null}
492
+ */
493
+ this.operatorTokenPrintReason = printOperatorTokenOptIn(options);
462
494
 
463
495
  if (!wantsRequired || explicit === '') {
464
496
  this.operatorToken = null;
465
497
  this.operatorTokenRequired = false;
466
498
  this.operatorTokenSource = 'disabled';
499
+ this.operatorTokenPrintable = false;
500
+ this.operatorTokenPrintReason = null;
467
501
  return;
468
502
  }
469
503
  this.operatorTokenRequired = true;
@@ -471,18 +505,22 @@ export class RelayServer {
471
505
  this.operatorToken = explicit;
472
506
  this.operatorTokenSource = 'option';
473
507
  this.persistence.writeOperatorToken(explicit);
474
- return;
475
- }
476
- if (persisted) {
508
+ } else if (persisted) {
477
509
  // §5.1 + §7: keep the token stable across restarts, otherwise every restart
478
510
  // would silently invalidate the plugin's configured operatorToken.
479
511
  this.operatorToken = persisted;
480
512
  this.operatorTokenSource = 'restored';
481
- return;
513
+ } else {
514
+ this.operatorToken = randomBytes(32).toString('hex');
515
+ this.operatorTokenSource = 'generated';
516
+ this.persistence.writeOperatorToken(this.operatorToken);
482
517
  }
483
- this.operatorToken = randomBytes(32).toString('hex');
484
- this.operatorTokenSource = 'generated';
485
- this.persistence.writeOperatorToken(this.operatorToken);
518
+ // MEDIUM-6: the material is printed only by the start that generated it, or when the operator has
519
+ // explicitly asked for it. "The posture, never the material" still holds everywhere else.
520
+ if (this.operatorTokenSource === 'generated') {
521
+ this.operatorTokenPrintReason = this.operatorTokenPrintReason ?? 'generated';
522
+ }
523
+ this.operatorTokenPrintable = this.operatorTokenPrintReason !== null;
486
524
  }
487
525
 
488
526
  _buildStartupMessages() {
@@ -517,10 +555,30 @@ export class RelayServer {
517
555
  m.push(`[w2m-rabbit] !! ${w.file}: ${w.message}`);
518
556
  }
519
557
  if (this.operatorTokenRequired) {
520
- m.push(`[w2m-rabbit] operator token (${this.operatorTokenSource}) - required for POST /v1/task:`);
521
- m.push(`[w2m-rabbit] ${this.operatorToken}`);
522
- if (this.persistence.operatorTokenFile) {
523
- m.push(`[w2m-rabbit] stored at ${this.persistence.operatorTokenFile} (0600)`);
558
+ // MEDIUM-6: the token is material, not posture, so it is printed as rarely as it can be — on the
559
+ // start that generated it (`operatorTokenSource === 'generated'`) or when `printOperatorToken` /
560
+ // `W2M_PRINT_OPERATOR_TOKEN=1` asked for it — and once, never twice. Where it is stored is said in
561
+ // every case, including the case where there is nowhere to store it.
562
+ //
563
+ // DELIBERATE, do not "fix": on that one start the CLI prints the value a second time, in its own
564
+ // stdout banner. Two sinks for one start is not a leak — the defect was printing it on *every*
565
+ // start, restored ones included — and the box is the documented first-start handoff a human reads
566
+ // (docs/DEPLOY.md §2.4, test/crossnetwork.test.mjs). Collapsing the pair would trade a documented
567
+ // behaviour for one saved line on one start.
568
+ const tokenFile = this.persistence.operatorTokenFile ?? null;
569
+ const stored = tokenFile === null
570
+ ? 'persistence is off, so this token exists only in this process and cannot be read back later'
571
+ : `stored at ${tokenFile} (0600)`;
572
+ if (this.operatorTokenPrintable) {
573
+ m.push(`[w2m-rabbit] operator token (${this.operatorTokenSource}) - required for POST /v1/task:`);
574
+ m.push(`[w2m-rabbit] ${this.operatorToken}`);
575
+ m.push(`[w2m-rabbit] printed once because ${this.operatorTokenPrintReason === 'generated'
576
+ ? 'this start generated it' : `${this.operatorTokenPrintReason} asked for it (${PRINT_OPERATOR_TOKEN_ENV})`}`
577
+ + ` - and ${stored}`);
578
+ } else {
579
+ m.push(`[w2m-rabbit] operator token (${this.operatorTokenSource}) - required for POST /v1/task, NOT printed`);
580
+ m.push(`[w2m-rabbit] read it from ${tokenFile ?? '<state>/operator-token.txt'} - ${stored}`);
581
+ m.push(`[w2m-rabbit] set ${PRINT_OPERATOR_TOKEN_ENV}=1 to print it on this start as well`);
524
582
  }
525
583
  } else {
526
584
  m.push('[w2m-rabbit] !! WARNING: OPERATOR TOKEN DISABLED (--operator-token "") - '
@@ -562,6 +620,11 @@ export class RelayServer {
562
620
  basePath: this.basePath,
563
621
  operatorToken: this.operatorToken,
564
622
  operatorTokenRequired: this.operatorTokenRequired,
623
+ // The CLI prints its own banner, so it has to reach the same conclusion this class did rather
624
+ // than re-deriving one from the token's presence (MEDIUM-6).
625
+ operatorTokenSource: this.operatorTokenSource,
626
+ operatorTokenPrintable: this.operatorTokenPrintable === true,
627
+ operatorTokenPrintReason: this.operatorTokenPrintReason ?? null,
565
628
  scheme: this.scheme,
566
629
  });
567
630
  });