@wirecat/cli-base 0.65.3 → 0.65.5
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 +40 -0
- package/README.md +2 -2
- package/dist/credentials.d.ts +14 -10
- package/dist/credentials.js +31 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +27 -1
- package/docs/standards/accounts.md +0 -1
- package/docs/standards/lifecycle.md +5 -5
- package/docs/standards/mcp.md +2 -3
- package/docs/standards/permissions.md +1 -2
- package/docs/standards/releases.md +3 -2
- package/docs/standards/security.md +36 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.65.5 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`movedRunService(app, configDir)` names a moved run's temporary keyring entry**, so a test or an agent can delete
|
|
16
|
+
it by name.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- **A moved run no longer replaces the owner's keyring entry.** `session start` in a worktree or with
|
|
21
|
+
`HOME=/tmp/x` overwrote the owner's real sign-in. With the system keyring, `Credentials` in a moved run (`isolated`,
|
|
22
|
+
or a moved `HOME` or XDG folder) now writes a temporary entry, `<app>:<absolute config folder>`; reads it first and
|
|
23
|
+
the owner's `<app>` entry when it has none; and `remove` deletes only the temporary entry, so `session end` cleans up.
|
|
24
|
+
A token refreshed in a moved run is saved in the temporary entry too, so a run that never ends its session leaves
|
|
25
|
+
one behind ([WC-SECR-24](docs/standards/security.md#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
26
|
+
A run with the default folders, and a test with `memoryKeyring()`, are unchanged.
|
|
27
|
+
|
|
28
|
+
## 0.65.4 — 31.10.2026
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **A run with moved folders uses the app's own keyring entry again** ([WC-SECR-24](docs/standards/security.md#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it),
|
|
33
|
+
rewritten). `Credentials` with `isolated` reads and writes service `<app>`, not `<app>:<config folder>`, and no
|
|
34
|
+
longer saves to the moved folder's file first (the 0.65.1 redirect is gone). `keyringService` returns the app's
|
|
35
|
+
service whatever its other arguments say, so cli-core's sign-in key names the shared entry. Per-folder entries made
|
|
36
|
+
before are no longer read; they were copies made by throwaway runs. A secret 0.65.1 saved in a moved folder's file
|
|
37
|
+
is still read when the keyring has none.
|
|
38
|
+
- **The handbook's statements about gcal match gcal-cli 0.2.0:** it has `session list`, its `api` checks
|
|
39
|
+
`events.notify` and `events.delete.series`, its `api` answers drop the passcode lines of a description and a
|
|
40
|
+
location, it serves MCP as `mcp serve`, it sets `requestMs` to 1.5 s, and it packs 144 files.
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
|
|
44
|
+
- **A moved run never deletes the owner's keyring entry.** A security review found that
|
|
45
|
+
`HOME=/tmp/x gcal personal session end` deleted the owner's real Google sign-in. With the system keyring,
|
|
46
|
+
`Credentials.remove` from a moved run keeps the entry, warns on stderr and leaves `"keyring"` out of what it
|
|
47
|
+
returns. A run counts as moved with `isolated`, or when the `env` it is given moves `HOME` or an XDG folder away from
|
|
48
|
+
the account's own home. The new `isMovedRun({ appName, prefix, env })` says the same for a CLI: pass it as
|
|
49
|
+
`isolated`, and check it before revoking a sign-in at the provider. A test with `memoryKeyring()` is unchanged.
|
|
50
|
+
|
|
11
51
|
## 0.65.3 — 30.10.2026
|
|
12
52
|
|
|
13
53
|
### Added
|
package/README.md
CHANGED
|
@@ -150,8 +150,8 @@ on macOS and Windows through `node-ci.yml` with `platforms: true`; here, Full va
|
|
|
150
150
|
spawning `npm` and `tsc`). In every test the keyring is a fake, so no test reaches a real Keychain or Credential
|
|
151
151
|
Manager.
|
|
152
152
|
|
|
153
|
-
Two traps worth designing for: the keyring is global, so a throwaway config directory
|
|
154
|
-
|
|
153
|
+
Two traps worth designing for: the keyring is global, so a throwaway config directory shares the owner's entry (pass
|
|
154
|
+
`isolated: isMovedRun(…)` so it never deletes it); and a locked Linux keyring can block on a prompt rather than fail.
|
|
155
155
|
|
|
156
156
|
### Time, retries and deadlines
|
|
157
157
|
|
package/dist/credentials.d.ts
CHANGED
|
@@ -12,16 +12,11 @@ export interface CredentialsOptions {
|
|
|
12
12
|
/** The keyring service name — the CLI's app name, e.g. `max-cli`, not its command word. */
|
|
13
13
|
service: string;
|
|
14
14
|
/**
|
|
15
|
-
* **True when the
|
|
15
|
+
* **True when the run's folders are not the owner's own** — pass `isMovedRun(…)`.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* keys on 2026-09-14, and a keyring entry cannot be read back out. Passing `true` here scopes the
|
|
21
|
-
* service name to the directory, making the isolation people already assume they have real.
|
|
22
|
-
*
|
|
23
|
-
* With the system keyring it also keeps new secrets out of it: they go to the file in the moved directory, so a test
|
|
24
|
-
* or worktree run leaves nothing in the owner's keyring (WC-SECR-24). A secret saved there before is still read.
|
|
17
|
+
* With the system keyring the run reads the owner's entry (`service`) but never replaces or deletes it: `write`
|
|
18
|
+
* goes to a temporary entry, `movedRunService(service, configDir)`, which `read` prefers and `remove` deletes
|
|
19
|
+
* (WC-SECR-24). A moved `HOME` or XDG folder in `env` counts as moved even when this is false.
|
|
25
20
|
*/
|
|
26
21
|
isolated?: boolean;
|
|
27
22
|
/** Read before the keyring, for CI. */
|
|
@@ -33,7 +28,16 @@ export interface CredentialsOptions {
|
|
|
33
28
|
/** Where the one-line warning goes when the keyring is unavailable. Never stdout. */
|
|
34
29
|
warn?: (message: string) => void;
|
|
35
30
|
}
|
|
36
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The app's shared keyring service, which a moved run reads too. The other two arguments are ignored and stay only so
|
|
33
|
+
* that callers written against 0.65.0 still compile.
|
|
34
|
+
*/
|
|
35
|
+
export declare const keyringService: (service: string, _configDir?: string, _isolated?: boolean) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The temporary keyring service a moved run writes instead of the owner's entry: `<app>:<absolute config folder>`, the
|
|
38
|
+
* shape `bin/keyring-dedupe` already finds and removes.
|
|
39
|
+
*/
|
|
40
|
+
export declare const movedRunService: (service: string, configDir: string) => string;
|
|
37
41
|
/**
|
|
38
42
|
* Environment first, then the OS keyring, then a file.
|
|
39
43
|
*
|
package/dist/credentials.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { writeSecurely } from "./config.js";
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
5
|
import { systemKeyring } from "./keyring.js";
|
|
6
|
+
import { homeIsMoved } from "./paths.js";
|
|
6
7
|
export const CREDENTIAL_STORAGE = ["auto", "keyring", "file"];
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The app's shared keyring service, which a moved run reads too. The other two arguments are ignored and stay only so
|
|
10
|
+
* that callers written against 0.65.0 still compile.
|
|
11
|
+
*/
|
|
12
|
+
export const keyringService = (service, _configDir, _isolated) => service;
|
|
13
|
+
/**
|
|
14
|
+
* The temporary keyring service a moved run writes instead of the owner's entry: `<app>:<absolute config folder>`, the
|
|
15
|
+
* shape `bin/keyring-dedupe` already finds and removes.
|
|
16
|
+
*/
|
|
17
|
+
export const movedRunService = (service, configDir) => `${service}:${resolve(configDir)}`;
|
|
8
18
|
/**
|
|
9
19
|
* Environment first, then the OS keyring, then a file.
|
|
10
20
|
*
|
|
@@ -21,7 +31,8 @@ export class Credentials {
|
|
|
21
31
|
#envVar;
|
|
22
32
|
#fileName;
|
|
23
33
|
#warn;
|
|
24
|
-
|
|
34
|
+
/** Set only for a moved run with the system keyring. */
|
|
35
|
+
#temporaryService;
|
|
25
36
|
#warned = false;
|
|
26
37
|
constructor(options) {
|
|
27
38
|
this.#configDir = options.configDir;
|
|
@@ -31,33 +42,27 @@ export class Credentials {
|
|
|
31
42
|
this.#envVar = options.envVar;
|
|
32
43
|
this.#fileName = options.fileName ?? "credentials.json";
|
|
33
44
|
this.#warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
34
|
-
this.#service =
|
|
35
|
-
this.#
|
|
45
|
+
this.#service = options.service;
|
|
46
|
+
const moved = this.#keyring === systemKeyring && (options.isolated === true || homeIsMoved({ env: this.#env }));
|
|
47
|
+
this.#temporaryService = moved ? movedRunService(options.service, options.configDir) : undefined;
|
|
36
48
|
}
|
|
37
49
|
read(account) {
|
|
38
50
|
const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
|
|
39
51
|
if (fromEnv)
|
|
40
52
|
return { secret: fromEnv, source: "environment" };
|
|
41
|
-
const fromFile = () => {
|
|
42
|
-
const secret = this.#readFile()?.[account]?.secret;
|
|
43
|
-
return secret ? { secret, source: "file" } : undefined;
|
|
44
|
-
};
|
|
45
|
-
if (this.#fileFirst) {
|
|
46
|
-
const found = fromFile();
|
|
47
|
-
if (found)
|
|
48
|
-
return found;
|
|
49
|
-
}
|
|
50
53
|
if (this.#storage !== "file") {
|
|
51
|
-
const
|
|
54
|
+
const temporary = this.#temporaryService;
|
|
55
|
+
const fromKeyring = this.#tryKeyring(() => (temporary && this.#keyring.get(temporary, account)) || this.#keyring.get(this.#service, account));
|
|
52
56
|
if (fromKeyring)
|
|
53
57
|
return { secret: fromKeyring, source: "keyring" };
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
const secret = this.#readFile()?.[account]?.secret;
|
|
60
|
+
return secret ? { secret, source: "file" } : undefined;
|
|
56
61
|
}
|
|
57
62
|
write(account, secret) {
|
|
58
|
-
if (this.#storage !== "file"
|
|
63
|
+
if (this.#storage !== "file") {
|
|
59
64
|
const stored = this.#tryKeyring(() => {
|
|
60
|
-
this.#keyring.set(this.#service, account, secret);
|
|
65
|
+
this.#keyring.set(this.#temporaryService ?? this.#service, account, secret);
|
|
61
66
|
return true;
|
|
62
67
|
});
|
|
63
68
|
if (stored)
|
|
@@ -74,8 +79,14 @@ export class Credentials {
|
|
|
74
79
|
}
|
|
75
80
|
remove(account) {
|
|
76
81
|
const removed = [];
|
|
77
|
-
if (this.#storage !== "file"
|
|
78
|
-
|
|
82
|
+
if (this.#storage !== "file") {
|
|
83
|
+
if (this.#tryKeyring(() => this.#keyring.delete(this.#temporaryService ?? this.#service, account))) {
|
|
84
|
+
removed.push("keyring");
|
|
85
|
+
}
|
|
86
|
+
if (this.#temporaryService) {
|
|
87
|
+
this.#warn(`kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with moved ` +
|
|
88
|
+
"folders never removes it");
|
|
89
|
+
}
|
|
79
90
|
}
|
|
80
91
|
const store = this.#readFile();
|
|
81
92
|
if (store?.[account] !== undefined) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { BATCH_LIMITS, type BatchContext, type BatchHandler, type BatchOperation, type BatchReport, type BatchResult, type PreparedBatchRequest, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
3
|
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential } from "./credentials.js";
|
|
4
|
-
export { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
|
|
4
|
+
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
5
5
|
export { createDeadline, type Deadline, type DeadlineOptions } from "./deadline.js";
|
|
6
6
|
export { CliError, type CliErrorDetails, type ErrorCode, errorCodes, isCliError } from "./errors.js";
|
|
7
7
|
export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
|
|
@@ -10,7 +10,7 @@ export { brokenKeyring, type KeyringStore, memoryKeyring, systemKeyring } from "
|
|
|
10
10
|
export type { Logger } from "./logger.js";
|
|
11
11
|
export { createRecordLog, logRecordOf, NOT_ALLOWED, REDACTED_FIELDS, type RecordInput, type RecordLogOptions, } from "./logging.js";
|
|
12
12
|
export { createOutputBuffer, type OutputBuffer, type OutputBufferOptions } from "./output-buffer.js";
|
|
13
|
-
export { configFilePath, type Paths, type PathsOptions, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
13
|
+
export { configFilePath, isMovedRun, type Paths, type PathsOptions, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
14
14
|
export { indent, type PrettyOptions, renderPretty } from "./pretty.js";
|
|
15
15
|
export { createRenderer, RENDER_FORMATS, type Renderer, type RendererOptions, type RenderFormat, } from "./renderer.js";
|
|
16
16
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
-
export { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
|
|
3
|
+
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
4
4
|
export { createDeadline } from "./deadline.js";
|
|
5
5
|
export { CliError, errorCodes, isCliError } from "./errors.js";
|
|
6
6
|
export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
|
|
@@ -8,7 +8,7 @@ export { catchCrashes, errorObjectOf, errorRecordOf, exitCodeOfAnswer, exitCodeO
|
|
|
8
8
|
export { brokenKeyring, memoryKeyring, systemKeyring } from "./keyring.js";
|
|
9
9
|
export { createRecordLog, logRecordOf, NOT_ALLOWED, REDACTED_FIELDS, } from "./logging.js";
|
|
10
10
|
export { createOutputBuffer } from "./output-buffer.js";
|
|
11
|
-
export { configFilePath, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
11
|
+
export { configFilePath, isMovedRun, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
12
12
|
export { indent, renderPretty } from "./pretty.js";
|
|
13
13
|
export { createRenderer, RENDER_FORMATS, } from "./renderer.js";
|
|
14
14
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
package/dist/paths.d.ts
CHANGED
|
@@ -31,4 +31,21 @@ export declare const defaultPaths: ({ appName, env, platform }: PathsOptions) =>
|
|
|
31
31
|
export declare const resolvePaths: (options: PathsOptions) => Paths;
|
|
32
32
|
/** Whether any of the three directories came from the environment rather than the OS convention. */
|
|
33
33
|
export declare const pathsAreOverridden: ({ appName, prefix, env }: PathsOptions) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `HOME` or an XDG (or Windows) folder in `env` puts the default folders anywhere but under the account's own
|
|
36
|
+
* home, which comes from the user database: `os.homedir()` returns `$HOME` and would miss exactly this. An account with
|
|
37
|
+
* no home entry counts as moved.
|
|
38
|
+
*/
|
|
39
|
+
export declare const homeIsMoved: ({ env, platform, home, }?: {
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
home?: string | undefined;
|
|
43
|
+
}) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* **A run whose folders are not the owner's own**: a `<PREFIX>_*_DIR` override, or a moved `HOME` or XDG folder.
|
|
46
|
+
* It still shares the owner's keyring entry, so it must not delete or revoke it (WC-SECR-24).
|
|
47
|
+
*/
|
|
48
|
+
export declare const isMovedRun: (options: PathsOptions & {
|
|
49
|
+
home?: string | undefined;
|
|
50
|
+
}) => boolean;
|
|
34
51
|
export declare const configFilePath: (configDir: string, fileName?: string) => string;
|
package/dist/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, userInfo } from "node:os";
|
|
2
2
|
import { join, posix, win32 } from "node:path";
|
|
3
3
|
export const PATH_KINDS = ["config", "state", "cache"];
|
|
4
4
|
const OVERRIDES = { config: "CONFIG_DIR", state: "STATE_DIR", cache: "CACHE_DIR" };
|
|
@@ -41,4 +41,30 @@ export const resolvePaths = (options) => {
|
|
|
41
41
|
};
|
|
42
42
|
/** Whether any of the three directories came from the environment rather than the OS convention. */
|
|
43
43
|
export const pathsAreOverridden = ({ appName, prefix, env = process.env }) => PATH_KINDS.some((kind) => env[overrideName(appName, prefix, kind)] !== undefined);
|
|
44
|
+
const accountHome = () => {
|
|
45
|
+
try {
|
|
46
|
+
return userInfo().homedir;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Whether `HOME` or an XDG (or Windows) folder in `env` puts the default folders anywhere but under the account's own
|
|
54
|
+
* home, which comes from the user database: `os.homedir()` returns `$HOME` and would miss exactly this. An account with
|
|
55
|
+
* no home entry counts as moved.
|
|
56
|
+
*/
|
|
57
|
+
export const homeIsMoved = ({ env = process.env, platform = process.platform, home = accountHome(), } = {}) => {
|
|
58
|
+
if (home === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
const appName = "app";
|
|
61
|
+
const actual = defaultPaths({ appName, env, platform });
|
|
62
|
+
const owners = defaultPaths({ appName, env: { HOME: home, USERPROFILE: home }, platform });
|
|
63
|
+
return PATH_KINDS.some((kind) => actual[kind] !== owners[kind]);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* **A run whose folders are not the owner's own**: a `<PREFIX>_*_DIR` override, or a moved `HOME` or XDG folder.
|
|
67
|
+
* It still shares the owner's keyring entry, so it must not delete or revoke it (WC-SECR-24).
|
|
68
|
+
*/
|
|
69
|
+
export const isMovedRun = (options) => pathsAreOverridden(options) || homeIsMoved(options);
|
|
44
70
|
export const configFilePath = (configDir, fileName = "config.json") => join(configDir, fileName);
|
|
@@ -180,7 +180,6 @@ Every CLI that signs in has four commands under `session`:
|
|
|
180
180
|
- tg, max and wa have `session start` and `session end`, no `session status`, and list their profiles with
|
|
181
181
|
`account list` instead of `session list`.
|
|
182
182
|
- drive has `session start`, `session list` and `session end`, and no `session status`.
|
|
183
|
-
- gcal has `session start`, `session status` and `session end`, and no `session list`.
|
|
184
183
|
- zm has `session start` and `session status`, and no `session end` or `session list`.
|
|
185
184
|
- memo signs in per mail account with `mail add` and has no `session` group.
|
|
186
185
|
|
|
@@ -23,10 +23,10 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
23
23
|
- **WC-LIFE-15:** tg, max, wa, gcal and the template. drive, memo and zm do not keep runs yet; each is adding them on a
|
|
24
24
|
`feat/runs` branch (2026-10-25). zm's `resolveSettings` also returns `keepFailedRuns: false`, which turns off the
|
|
25
25
|
failure log cli-core's `run` would keep.
|
|
26
|
-
- **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal has it. The
|
|
27
|
+
deadlines of WC-LIFE-5 come with the same release. The other syncs (`messages sync` in tg, max and wa, drive's
|
|
28
|
+
`sync`, memo's mail import, zm's `recordings sync`) are cooperative but spend no request budget yet. gcal sets
|
|
29
|
+
`requestMs` to 1.5 s.
|
|
30
30
|
|
|
31
31
|
## One-shot commands
|
|
32
32
|
|
|
@@ -271,7 +271,7 @@ status command answers how far it is
|
|
|
271
271
|
and the run loses its round; with one, every run ends with an answer, saves its work and says how to go on. One
|
|
272
272
|
`requestMs` per CLI, in one place, keeps the arithmetic the same in every sync of that CLI.
|
|
273
273
|
- **Enforced by:** `cli-core: src/background/budget.test.ts` · `cli-core: src/calendar/testing/sync-contract.ts`.
|
|
274
|
-
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with
|
|
274
|
+
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with gcal's 1.5 s about 25 requests. It
|
|
275
275
|
answers `state: "partial"`, `stopped: "requests"`, `progress: { calendars, calendarsDone, requests, saved }`, and
|
|
276
276
|
`next` with `gcal personal sync`; `gcal personal status` shows each calendar's progress. cli-core builds the budget
|
|
277
277
|
with `runBudgetOf(command, maxPages)` from `@wirecat/cli-core/cli` over `runBudget` in
|
package/docs/standards/mcp.md
CHANGED
|
@@ -19,9 +19,8 @@ tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) an
|
|
|
19
19
|
`mcp serve --profiles` ([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) is not
|
|
20
20
|
built yet: cli-core serves one named profile.
|
|
21
21
|
zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
|
|
22
|
-
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp
|
|
23
|
-
|
|
24
|
-
drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
22
|
+
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp serve`). memo and drive have their own servers: memo has hand-written
|
|
23
|
+
tools, and drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
25
24
|
gaps against these rules.
|
|
26
25
|
|
|
27
26
|
WC-MCP-2 and WC-MCP-8 need cli-core 0.321.0, which tg, max and wa pin.
|
|
@@ -346,8 +346,7 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
|
|
|
346
346
|
- **Exceptions:** known gaps, as of 2026-10-27:
|
|
347
347
|
- cli-core's `CALENDAR_WRITE_DEFAULTS` (`src/calendar/writer.ts`) still has `events create`, `update` and
|
|
348
348
|
`respond` at `ask`.
|
|
349
|
-
- gcal's generated `api` commands default every write to `ask`, ordinary changes included
|
|
350
|
-
`events.notify` nor `events.delete.series`.
|
|
349
|
+
- gcal's generated `api` commands default every write to `ask`, ordinary changes included.
|
|
351
350
|
- drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
|
|
352
351
|
memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
|
|
353
352
|
- **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
|
|
@@ -196,8 +196,9 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
|
|
|
196
196
|
`@wirecat/cli-base/release` fails on any path in `npm pack --dry-run` outside `allowed`. It checks the pack against
|
|
197
197
|
the list it is given, not against this rule: a CLI that passes its own `files` list as `allowed`, as the template's
|
|
198
198
|
`scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
|
|
199
|
-
`npm pack --dry-run` in gcal-cli
|
|
200
|
-
`contracts/`, `lint/` and `template.json` among them, because its `files` list
|
|
199
|
+
`npm pack --dry-run` in gcal-cli packed 256 files on 2026-10-27, `src/`, `scripts/`, `.github/`, `config/`,
|
|
200
|
+
`contracts/`, `lint/` and `template.json` among them, because its `files` list named them; its check could not see
|
|
201
|
+
it. On 2026-10-31 (gcal-cli 0.2.0) it packs 144. The check this rule needs
|
|
201
202
|
refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
|
|
202
203
|
`upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
|
|
203
204
|
- **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
|
|
@@ -68,11 +68,11 @@ name. Any other top-level key is written as `[not allowed]`. A key that names co
|
|
|
68
68
|
|
|
69
69
|
cli-base's `Credentials` reads the environment variable first (for CI), then the OS keyring, then a file with mode
|
|
70
70
|
`0600`. The keyring service is the CLI's app name, `appName` in its identity (`max-cli`, `gcal-cli`, `cli-memo`), not
|
|
71
|
-
the command word people type (`max`, `gcal`).
|
|
72
|
-
|
|
71
|
+
the command word people type (`max`, `gcal`). A run with moved folders reads the same entry, writes a temporary one,
|
|
72
|
+
and never replaces or deletes the owner's
|
|
73
|
+
([WC-SECR-24](#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
73
74
|
|
|
74
|
-
- **Why:** the keyring is the safest store on the computer
|
|
75
|
-
owner's real keyring entry.
|
|
75
|
+
- **Why:** the keyring is the safest store on the computer.
|
|
76
76
|
- **Enforced by:** `src/credentials.test.ts`.
|
|
77
77
|
- **Example:** a bot token is read from `MAX_BOT_TOKEN`, then the keyring entry `bot:<profile>`, then the file.
|
|
78
78
|
- **Exceptions:** `auto` storage tries the keyring and, when it fails, warns once on stderr and uses the file. A
|
|
@@ -322,28 +322,42 @@ store and runs no search. A CLI declares its provider and may replace any step.
|
|
|
322
322
|
the provider and never touched the store: a generic API command removes the provider's passcode fields (Google's
|
|
323
323
|
`conferenceData.entryPoints[].passcode`, `pin`, `accessCode`, `password`), the query of every join link and the
|
|
324
324
|
dial sequence after a phone number, and the passcode lines of a description or a location.
|
|
325
|
-
- **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
|
|
326
|
-
2026-10-27: gcal's `api` answers drop the conference entry points' secrets (`src/conference.ts`) but keep the
|
|
327
|
-
description and location as Google sent them.
|
|
325
|
+
- **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
|
|
328
326
|
- **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
|
|
329
327
|
(output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
|
|
330
328
|
|
|
331
|
-
### WC-SECR-24 · A run with moved folders
|
|
332
|
-
|
|
333
|
-
- **Why:**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
329
|
+
### WC-SECR-24 · A run with moved folders shares the owner's keyring entry and never deletes it
|
|
330
|
+
|
|
331
|
+
- **Why:** a per-folder keyring entry (`<app>:<config folder>`) for every run left one copy of a sign-in for every test
|
|
332
|
+
run, agent check or worktree, about 1,500 of them in the owner's keyring, and nothing removed them. One entry per
|
|
333
|
+
computer is enough. But a moved run then reaches the owner's real sign-in: `HOME=/tmp/x gcal personal session end`
|
|
334
|
+
deleted the owner's Google sign-in, because only `<PREFIX>_*_DIR` counted as moved (a security review, 2026-10-30),
|
|
335
|
+
and `session start` in a worktree replaced it.
|
|
336
|
+
- **Enforced by:** `src/credentials.test.ts` · `src/paths.test.ts`.
|
|
337
|
+
- **Example:** a run is moved when `isMovedRun({ appName, prefix, env })` is true: a `<PREFIX>_CONFIG_DIR`,
|
|
338
|
+
`_STATE_DIR` or `_CACHE_DIR` override, or a `HOME` or XDG folder that puts the default folders anywhere but under
|
|
339
|
+
the account's own home. A CLI passes that as `isolated`; a moved `HOME` or XDG folder in `env` counts even when
|
|
340
|
+
`isolated` is false. With the system keyring, `Credentials` in a moved run:
|
|
341
|
+
- **writes** only a temporary entry, `movedRunService(app, configDir)` = `<app>:<absolute config folder>`, never the
|
|
342
|
+
owner's `<app>` entry;
|
|
343
|
+
- **reads** that temporary entry first and the owner's `<app>` entry when it has none, so a moved run still uses
|
|
344
|
+
the owner's sign-in;
|
|
345
|
+
- **removes** only the temporary entry, warns on stderr that the owner's was kept, and returns `"keyring"` only when
|
|
346
|
+
a temporary entry was there. `session end` in a moved run therefore cleans up after `session start`.
|
|
347
|
+
|
|
348
|
+
A CLI never revokes a sign-in at the provider in a moved run, even one read from the temporary entry: a refreshed
|
|
349
|
+
token is still the owner's grant. A test passes `memoryKeyring()`; a run that must not see the keyring at all also
|
|
350
|
+
runs with `DBUS_SESSION_BUS_ADDRESS` removed, since the keyring is reached over D-Bus.
|
|
351
|
+
- **Cleanup:** a moved run that never reaches `session end` leaves its temporary entry, and so does one that refreshes
|
|
352
|
+
a token, since the refreshed token is written there. `bin/keyring-dedupe --delete` in the projects folder removes
|
|
353
|
+
temporary entries that sit beside the owner's real one; a test or agent can also delete one by name with
|
|
354
|
+
`movedRunService`. When only `<PREFIX>_STATE_DIR` or `_CACHE_DIR` is moved, the temporary entry is named after the
|
|
355
|
+
owner's own config folder, which `keyring-dedupe` treats as a real entry: remove it with `session end`.
|
|
344
356
|
- **Exceptions:** none.
|
|
345
|
-
- **Applies to:** every CLI, its tests and every agent that runs a CLI. **Since:**
|
|
346
|
-
2026-10-29 (sign-ins copied per work folder should have reused the existing entry)
|
|
357
|
+
- **Applies to:** every CLI, its tests and every agent that runs a CLI. **Since:** cli-base 0.65.4 (temporary entry:
|
|
358
|
+
0.65.5). **Rulings:** owner, 2026-10-29 (sign-ins copied per work folder should have reused the existing entry);
|
|
359
|
+
owner, 2026-10-30 (share the keyring entry, do not multiply it); owner, 2026-10-31 (do not replace it: a test of a
|
|
360
|
+
new session writes a temporary entry and cleans it up).
|
|
347
361
|
|
|
348
362
|
## Sources
|
|
349
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.5",
|
|
4
4
|
"description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|