@retasc/cli 1.39.0 → 1.39.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,25 @@ 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.39.1 (2026-09-02)
10
+
11
+ - **RTSC-800** — (security) a keystore-resolved key now travels only to the keystore's own
12
+ url, whether the competing url arrives in the `.mcp.json` entry or in the environment.
13
+ 1.6.0 made this promise for the entry, which covered `claim`/`tidy`/`done` but not the
14
+ proxy: the proxy is the one caller that ever reads a committed marker, and it passes no
15
+ entry at all. The harness copies the marker's `env` block into the spawned process, so a
16
+ `RETASC_MCP_URL` planted next to `RETASC_WORKSPACE` in a repo's committed `.mcp.json`
17
+ arrived as an environment url, outranked the keystore, and every teammate's proxy sent its
18
+ real workspace key to that host on the next start, during session-key minting, with no
19
+ prompt. An environment url is now honored only alongside an explicit `RETASC_MCP_KEY`,
20
+ where the key and the url share a trust domain.
21
+ Nothing legitimate loses a path: `bind` already stores the deployment's url into the
22
+ binding, so a self-hosted url is in the keystore before the proxy asks for it.
23
+ This closes the url, and only the url. A committed marker also chooses the `command` that
24
+ runs and can set any other variable, so a marker you do not trust is code execution by the
25
+ MCP config format's own design, and the harness's project-trust prompt is the boundary for
26
+ that. What this removes is the one variant that reads as ordinary configuration in review.
27
+
9
28
  ## 1.39.0 (2026-08-30)
10
29
 
11
30
  - **RTSC-793** — `retasc setup` now wires Cursor, OpenCode and Gemini CLI too, bringing it
@@ -136,18 +136,41 @@ export function findBindingByPath(dir) {
136
136
  * workspace `.mcp.json`'s `mcpServers.retasc` entry, or undefined when only the
137
137
  * environment carries the binding (the spawned proxy).
138
138
  *
139
- * URL trust pairs with key source (RTSC-262 review): a keystore-resolved key
140
- * travels ONLY to the keystore's url (or explicit env url) never to a url
141
- * from the entry. The marker is secret-free and committed by design, so a
142
- * hostile edit adding RETASC_MCP_URL next to RETASC_WORKSPACE must not be able
143
- * to redirect a teammate's real key. Legacy inline entries keep their own url:
144
- * there the key and url are co-located, same trust domain.
139
+ * URL trust pairs with key source (RTSC-262 review, tightened by RTSC-800): a url
140
+ * is honored only when it arrived WITH the key actually used. A keystore-resolved
141
+ * key therefore travels only to the keystore's url never to one from the entry,
142
+ * and never to one from the environment. Co-located pairs keep their own url,
143
+ * because there the key and url share a trust domain: a legacy inline entry, and
144
+ * an explicit `RETASC_MCP_KEY` + `RETASC_MCP_URL` in the environment.
145
+ *
146
+ * RTSC-800 is why the env url is PAIRED rather than absolute. RTSC-262 made this
147
+ * promise but implemented it on the `mcpEntry` branch only, and the proxy — the
148
+ * one caller that ever reads a committed marker — passes no entry. It hands us
149
+ * `process.env`, which the harness populates from the marker's own `env` block
150
+ * (`mcpMarkerEntry` / `autoMarkerEntry`). So a `RETASC_MCP_URL` planted next to
151
+ * `RETASC_WORKSPACE` in a committed `.mcp.json` arrived as an env url, won, and
152
+ * every teammate's proxy posted its real keystore key to that host on the next
153
+ * harness start. Nothing legitimate loses a path: `bind` stores the deployment's
154
+ * url INTO the binding, so a self-hosted url is already in the keystore by the
155
+ * time the proxy asks for it.
156
+ *
157
+ * This closes the url, and only the url. A hostile committed marker can set any
158
+ * variable for the spawned proxy (`RETASC_DIR` relocates the keystore outright —
159
+ * see `keystoreDir` above — and `NODE_OPTIONS` is arbitrary code) and it chooses
160
+ * the `command` that runs, so a marker you do not trust is
161
+ * code execution by the MCP config format's own design — the harness's project-
162
+ * trust prompt is the boundary for that, not this function. What this removes is
163
+ * the one variant that survives review, because a url string in a config file
164
+ * looks like configuration.
145
165
  */
146
166
  export function resolveConn(opts) {
147
167
  const env = opts.env ?? process.env;
148
168
  const entry = opts.mcpEntry;
149
169
  let key = env.RETASC_MCP_KEY || "";
150
- let url = env.RETASC_MCP_URL || "";
170
+ // Paired with the env KEY, not read unconditionally (RTSC-800): without a key
171
+ // beside it, an env url is just a string the committed marker chose, and the key
172
+ // it would redirect belongs to the keystore.
173
+ let url = key ? env.RETASC_MCP_URL || "" : "";
151
174
  // Entry-supplied key/url are read together — the url is honored only when
152
175
  // the key it's paired with is the one actually used.
153
176
  let entryKey = "";
@@ -171,7 +194,8 @@ export function resolveConn(opts) {
171
194
  url = entryUrl;
172
195
  }
173
196
  // Canonical (RTSC-92): secret-free marker → workspace id → home keystore.
174
- // The entry's url is deliberately NOT consulted on this branch.
197
+ // Neither the entry's url nor the environment's is consulted on this branch
198
+ // the key came from the keystore, so the url does too (RTSC-800).
175
199
  //
176
200
  // RTSC-780 adds one more step in front of the id: `auto` names no workspace, so
177
201
  // the folder we were spawned in picks it. Same trust rule either way — the key
@@ -187,8 +211,12 @@ export function resolveConn(opts) {
187
211
  : undefined;
188
212
  if (b) {
189
213
  key = b.key;
190
- if (!url)
191
- url = b.url;
214
+ // Unconditional, where this used to be `if (!url)`. Equivalent today — reaching
215
+ // here means `key` was empty, and both writes to `url` above are key-gated, so it
216
+ // is empty too — and that is the point: the rule "a keystore key rides the keystore
217
+ // url" is stated here outright rather than left resting on that invariant, so
218
+ // restoring an unpaired url above cannot quietly resurrect the RTSC-800 redirect.
219
+ url = b.url;
192
220
  }
193
221
  }
194
222
  return { key, url: url || opts.defaultUrl || "" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.39.0",
3
+ "version": "1.39.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": {