@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
|
@@ -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
|
|
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
|
|
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)
|
|
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
|
};
|