@retasc/cli 1.48.0 → 1.48.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ release commits and the issues they reference.
6
6
 
7
7
  Dates are the npm publish date. Each entry names the RTSC issue behind it.
8
8
 
9
+ ## 1.48.1 (2026-09-08)
10
+
11
+ - **RTSC-878** — `retasc doctor` no longer condemns the entry `retasc setup` writes. Its
12
+ machine-global check dated from RTSC-91, when every top-level `mcpServers.retasc` carried
13
+ a key or a workspace id and so routed every unbound folder to one org; presence alone was
14
+ a correct test for that. RTSC-780 then made one global entry mandatory, the keyless `auto`
15
+ marker, which carries no identity at all, and the check was never taught the difference.
16
+ Doctor therefore printed `✗ GLOBAL Retasc server registered` at the marker and offered
17
+ `claude mcp remove -s user retasc` as the fix, which strips Retasc from every folder
18
+ without a binding of its own. It cost a customer a day.
19
+ - The check now reads the entry's identity: `RETASC_WORKSPACE=auto` with no key resolved is
20
+ reported as correct and named, so someone who just ran `setup` can see that what it wrote
21
+ is what belongs there. Every other shape stays illegal, including a keyless entry that
22
+ names a real `ws_` workspace, which still routes every unbound folder to one binding.
23
+ - The exemption is not granted on the sentinel alone. `parseServerEntry` reads
24
+ `RETASC_WORKSPACE` before it looks for a key in an `Authorization` header or in
25
+ `RETASC_MCP_KEY`, so an entry carrying both the `auto` sentinel and a real credential
26
+ parses as an innocent keyless marker; judging the parse alone would have exempted a live
27
+ machine-global registration pointing every unbound folder at that key's org. The raw
28
+ entry is examined too, and anything carrying credential material or a `url` of its own
29
+ stays illegal whatever its `env` claims.
30
+
9
31
  ## 1.48.0 (2026-09-08)
10
32
 
11
33
  - **RTSC-859** — `retasc key mint --hosted`: a key now says at mint what it is for.
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { claudeConfigPath, isNetworkError, readGlobalBinding, readLocalBinding, readShadowedBinding, resolveBinding, sameIdentity, } from "../lib/binding.js";
3
- import { getBinding } from "../lib/keystore.js";
3
+ import { AUTO_WORKSPACE, getBinding } from "../lib/keystore.js";
4
4
  import { gitCommonDir } from "../lib/gitRepo.js";
5
5
  import { detectHarnesses } from "../lib/harness.js";
6
6
  import { runsOk } from "../lib/launcher.js";
@@ -16,8 +16,9 @@ import { clean } from "../lib/text.js";
16
16
  // binding, which teaches people to distrust everything else it says. When both
17
17
  // exist, we report the claude-local one (what Claude Code actually runs;
18
18
  // local > project) and warn if a shadowed folder marker disagrees. The
19
- // illegal-global check is presence-based and never prints an all-clear it
20
- // can't prove (unreadable config ≠ no global server).
19
+ // illegal-global check reads the entry's IDENTITY, not merely its presence
20
+ // (RTSC-878), and never prints an all-clear it can't prove (unreadable config
21
+ // ≠ no global server).
21
22
  const ok = (m) => console.log(` ✓ ${m}`);
22
23
  const warn = (m) => console.log(` ! ${m}`);
23
24
  const bad = (m) => console.log(` ✗ ${m}`);
@@ -48,6 +49,65 @@ export function launcherVerdict(local, probe = runsOk) {
48
49
  shown: clean([local.command, ...probeArgs].join(" ")),
49
50
  };
50
51
  }
52
+ /**
53
+ * RTSC-878: is the machine-global entry a real registration, or the marker `setup` writes?
54
+ *
55
+ * The old rule was presence alone, and it was correct when it was written (RTSC-91): every
56
+ * top-level `mcpServers.retasc` then carried a key or a workspace id, so any of them routed
57
+ * every unbound folder on the disk to one org. RTSC-780 changed that world by making ONE
58
+ * global entry mandatory, the keyless `auto` marker, which carries no identity at all: the
59
+ * proxy resolves the folder it was spawned in and the org still comes from the folder.
60
+ *
61
+ * Doctor kept the old rule and so told people to delete what `retasc setup` had just
62
+ * installed. Following that advice strips Retasc from every folder without a binding of its
63
+ * own, and it happened to a real customer before this was fixed.
64
+ *
65
+ * The test is the marker's OWN sentinel rather than a shape heuristic: `RETASC_WORKSPACE`
66
+ * is `auto` and no key resolved from it. Workspace ids are `ws_`-prefixed uuids
67
+ * (`keystore.ts`), so `auto` can never be one, and an entry naming a real workspace stays
68
+ * illegal exactly as before.
69
+ *
70
+ * The sentinel alone is NOT enough, and this is the trap the exemption has to survive.
71
+ * `parseServerEntry` reads `RETASC_WORKSPACE` BEFORE it looks for a key in an
72
+ * `Authorization` header or in `RETASC_MCP_KEY`, so an entry that carries BOTH the `auto`
73
+ * sentinel and a real credential parses as an innocent keyless marker. That shape is a
74
+ * live machine-global registration pointing every unbound folder at whatever org its key
75
+ * names, which is the exact fault this check exists to catch, and it is not theoretical:
76
+ * `lib/harness.ts` records an agent hand-writing a machine-global entry with an inline key.
77
+ * So the raw entry is examined too, and anything carrying credential material or its own
78
+ * endpoint is illegal no matter what its `env` claims.
79
+ */
80
+ export function globalVerdict(global) {
81
+ if (global.status === "none")
82
+ return "clear";
83
+ if (global.status === "unreadable")
84
+ return "unreadable";
85
+ const b = global.binding;
86
+ if (b && b.workspaceId === AUTO_WORKSPACE && !b.key && isBareMarker(global.raw))
87
+ return "auto";
88
+ return "illegal";
89
+ }
90
+ /**
91
+ * Does the entry as written carry nothing but the `auto` sentinel?
92
+ *
93
+ * Deliberately a DENYLIST of credential-bearing and endpoint-bearing fields rather than an
94
+ * exact-shape match: `setup` may add a field to the marker later (it already grew `env`
95
+ * once), and an exemption that broke on every such addition would be reverted rather than
96
+ * updated. What must never be exempt is an entry that can reach a server on its own terms:
97
+ * a key in `env`, a key in a header, or a `url` of its own.
98
+ */
99
+ function isBareMarker(raw) {
100
+ if (!raw || typeof raw !== "object")
101
+ return false;
102
+ const e = raw;
103
+ if (e.url !== undefined)
104
+ return false;
105
+ if (e.headers !== undefined)
106
+ return false;
107
+ if (e.env && typeof e.env === "object" && e.env.RETASC_MCP_KEY !== undefined)
108
+ return false;
109
+ return true;
110
+ }
51
111
  /**
52
112
  * RTSC-498: macOS is the only platform this CLI is tested on. That was true and written
53
113
  * down nowhere, which left someone on Linux or Windows unable to tell their own mistake
@@ -192,22 +252,34 @@ export async function doctorAction() {
192
252
  }
193
253
  // 2) The safety check: any illegal machine-global Retasc server?
194
254
  const global = readGlobalBinding();
195
- if (global.status === "none") {
255
+ // Pulled out once: the verdict below is computed from the same entry, and reading it
256
+ // through the union again inside a branch loses the `present` narrowing.
257
+ const entry = global.status === "present" ? global.binding : undefined;
258
+ const verdict = globalVerdict(global);
259
+ if (verdict === "clear") {
196
260
  ok("no illegal global Retasc server.");
197
261
  }
198
- else if (global.status === "unreadable") {
262
+ else if (verdict === "auto") {
263
+ // RTSC-878: the entry `retasc setup` writes, and the one state doctor used to
264
+ // condemn. Named rather than passed over in silence: someone who just ran `setup`
265
+ // should see that the thing it wrote is the thing that is supposed to be there.
266
+ ok("machine-wide Retasc entry is the keyless `auto` marker `retasc setup` writes;\n" +
267
+ " it names no org, so every folder still resolves its own binding.");
268
+ }
269
+ else if (verdict === "unreadable") {
199
270
  warn("could not parse Claude Code's config — cannot verify there is no global Retasc server\n" +
200
271
  " (a local-scope binding for this folder would be invisible too).");
201
272
  }
202
273
  else {
203
274
  // Name the org it points at when we can: "leaks across orgs" is abstract,
204
- // "every unbound folder gets org X" is what makes someone act. Presence
205
- // alone is the finding a keyless entry is still an illegal registration
206
- // and if the network already failed above, don't burn another timeout.
275
+ // "every unbound folder gets org X" is what makes someone act. A keyless entry
276
+ // that names a WORKSPACE is still illegal (it routes every unbound folder to that
277
+ // one binding); only the identity-free `auto` marker above is not. If the network
278
+ // already failed above, don't burn another timeout.
207
279
  let points = "";
208
- if (global.binding?.key && !networkDown) {
280
+ if (entry?.key && !networkDown) {
209
281
  try {
210
- const b = await resolveBinding(global.binding.url || cfg.mcpUrl, global.binding.key);
282
+ const b = await resolveBinding(entry.url || cfg.mcpUrl, entry.key);
211
283
  points = ` It points at org "${clean(b.org.name)}".`;
212
284
  }
213
285
  catch {
@@ -213,7 +213,7 @@ export function readGlobalBinding() {
213
213
  const raw = doc?.mcpServers?.retasc;
214
214
  if (!raw)
215
215
  return { status: "none" };
216
- return { status: "present", binding: parseServerEntry(raw, "global") };
216
+ return { status: "present", binding: parseServerEntry(raw, "global"), raw };
217
217
  }
218
218
  /** Two per-folder bindings are the SAME binding if they agree on workspace id
219
219
  * (canonical form) or on the literal key (legacy forms). Anything else — or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.48.0",
3
+ "version": "1.48.1",
4
4
  "description": "Retasc CLI — the issue tracker AI agents pull work from. Sign in with GitHub or Google, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {