@commonlyai/cli 0.1.64 → 0.1.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.64",
3
+ "version": "0.1.65",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -17,13 +17,15 @@
17
17
  * fixed path would widen the window to "since the first spawn" and would let two
18
18
  * concurrent seats share one credential by accident.
19
19
  *
20
+ * The caller names the root; this module never invents one, because the only
21
+ * root it could invent is a directory no adapter cleans up.
22
+ *
20
23
  * The reader side is `readToken` in `commonly-mcp/src/client.js`, which resolves
21
24
  * fd, then file, then environment — by declaration, and refuses to fall through
22
25
  * from a declared source that cannot be read.
23
26
  */
24
27
  import { chmodSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
25
28
  import { dirname, join } from 'node:path';
26
- import { homedir } from 'node:os';
27
29
  import { randomBytes } from 'node:crypto';
28
30
 
29
31
  /** The variable a child reads to find the credential. Never carries the token. */
@@ -36,25 +38,37 @@ export const CREDENTIAL_FILE_VAR = 'COMMONLY_TOKEN_FILE';
36
38
  */
37
39
  export const CREDENTIAL_KEY = 'COMMONLY_AGENT_TOKEN';
38
40
 
39
- /** Default root: inside the CLI's own state directory, not a world-readable /tmp. */
40
- export const credentialRoot = () => join(homedir(), '.commonly', 'credentials');
41
-
42
41
  /**
43
- * Write `token` to a fresh 0600 file and return its path, or null when there is
44
- * no token to write (a seat bootstrapping without one).
42
+ * Write `token` to a fresh 0600 file under `root` and return its path, or null
43
+ * when there is no token to write (a seat bootstrapping without one).
44
+ *
45
+ * `root` is REQUIRED. It used to default to the CLI's own state directory, which
46
+ * meant a caller that forgot it wrote a live credential into
47
+ * `~/.commonly/credentials` — a directory no adapter cleans, because that is not
48
+ * where adapters put theirs. Measured 2026-09-20: 32 such directories on the
49
+ * fleet host, 24 of them written by this repo's own harnesses, all holding a
50
+ * seat credential. A missing root is now an error instead of a silent write into
51
+ * the operator's home. The token check stays first, so "no token, nothing
52
+ * written" remains true without a root.
45
53
  *
46
54
  * `fs` is injectable so tests can assert the mode and the path shape without
47
55
  * writing into the operator's home.
48
56
  */
49
57
  export const writeCredentialFile = (token, {
50
58
  agentName = 'agent',
51
- root = credentialRoot(),
59
+ root,
52
60
  fs = { mkdirSync, writeFileSync, chmodSync, rmSync },
53
61
  now = () => Date.now(),
54
62
  random = () => randomBytes(4).toString('hex'),
55
63
  } = {}) => {
56
64
  const value = typeof token === 'string' ? token.trim() : '';
57
65
  if (!value) return null;
66
+ if (typeof root !== 'string' || !root.trim()) {
67
+ throw new Error(
68
+ 'writeCredentialFile requires an explicit root: an omitted root used to fall back to '
69
+ + '~/.commonly/credentials, where nothing sweeps the credential files it writes',
70
+ );
71
+ }
58
72
  const dir = join(root, `${agentName}-${now()}-${random()}`);
59
73
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
60
74
  const path = join(dir, 'token');
@@ -44,9 +44,20 @@ export const CREDENTIAL_FILE_PLACEHOLDER = '${COMMONLY_TOKEN_FILE}';
44
44
  * a field the adapter substitutes LITERALLY has no file channel, so `keepsValue`
45
45
  * is passed in by the adapter rather than inferred, and the spawn that keeps a
46
46
  * secret says so in its own warning.
47
+ *
48
+ * The PATH comes out too when there is no file for THIS spawn. The runtime
49
+ * environment is derived from `process.env`, so a launcher whose own process was
50
+ * spawned by another seat inherits that seat's `COMMONLY_TOKEN_FILE` and hands
51
+ * it to the runtime and every MCP child below it — a path to a credential this
52
+ * launcher did not mint. The value was already deleted here for that reason; the
53
+ * path is the same leak with a smaller blast radius.
47
54
  */
48
55
  export const withholdRuntimeCredential = (env, { credentialFile = null, keepsValue = false } = {}) => {
49
- if (credentialFile) env[CREDENTIAL_FILE_VAR] = credentialFile;
56
+ if (credentialFile) {
57
+ env[CREDENTIAL_FILE_VAR] = credentialFile;
58
+ } else {
59
+ delete env[CREDENTIAL_FILE_VAR];
60
+ }
50
61
  if (!keepsValue) delete env[CREDENTIAL_KEY];
51
62
  return env;
52
63
  };