@skrr-ai/cli 0.1.103 → 0.1.104
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/dist/commands/commitments/create.js +2 -0
- package/dist/commands/daemon/logs.d.ts +69 -2
- package/dist/commands/daemon/logs.js +202 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7370 -7361
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
|
@@ -164,6 +164,8 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
|
|
|
164
164
|
'<%= config.bin %> commitments create --agent <agent-id> --title "AWS cost watch" --outcome "Cost regressions are investigated" --agent-directive-file ./aws-cost-policy.md',
|
|
165
165
|
'<%= config.bin %> commitments create --from-json commitment.json --json',
|
|
166
166
|
`# Replace <agent-id> with your Agent ID; this creates a draft, not a running loop.
|
|
167
|
+
# In "policy", 0 is a ZERO budget (nothing may run or be spent), not "no limit".
|
|
168
|
+
# To remove a limit set it to null (e.g. "maxCostUsdPerDay": null) or leave the key out.
|
|
167
169
|
cat > commitment.json <<'JSON'
|
|
168
170
|
{
|
|
169
171
|
"agentId": "<agent-id>",
|
|
@@ -61,13 +61,25 @@ export declare function resolveDaemonLogsProfile(flags: Pick<DaemonLogsFlags, 'p
|
|
|
61
61
|
home?: string;
|
|
62
62
|
env?: NodeJS.ProcessEnv;
|
|
63
63
|
}): string;
|
|
64
|
+
/** Output destinations the installed launchd definition for a profile declares. */
|
|
65
|
+
export interface InstalledServiceLogDestinations {
|
|
66
|
+
/** The `--log-file` in ProgramArguments, when present. */
|
|
67
|
+
logFile: string | null;
|
|
68
|
+
stdoutPath: string | null;
|
|
69
|
+
stderrPath: string | null;
|
|
70
|
+
}
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
72
|
+
* Read the installed launchd definition's log destinations (OSK-15993). The
|
|
73
|
+
* desktop supervisor starts the daemon with
|
|
67
74
|
* `--log-file ~/Library/Logs/Skrr/daemon.app.log` and redirects stderr to
|
|
68
75
|
* `daemon.stderr.log`, so the profile's `logs/` directory can hold a file
|
|
69
76
|
* nobody has written for a week. Read-only: the plist is parsed, never loaded.
|
|
70
77
|
*/
|
|
78
|
+
export declare function installedServiceLogDestinations(profile: string, options?: {
|
|
79
|
+
home?: string;
|
|
80
|
+
platform?: NodeJS.Platform;
|
|
81
|
+
}): InstalledServiceLogDestinations | null;
|
|
82
|
+
/** The installed service's log files for the main log, or for `--stderr`. */
|
|
71
83
|
export declare function installedServiceLogCandidates(profile: string, stderr: boolean, options?: {
|
|
72
84
|
home?: string;
|
|
73
85
|
platform?: NodeJS.Platform;
|
|
@@ -82,6 +94,8 @@ export declare function chooseLegacyFallbackLog(args: {
|
|
|
82
94
|
profilePath: string;
|
|
83
95
|
serviceCandidates: readonly string[];
|
|
84
96
|
mtimeOf: (filePath: string) => number | null;
|
|
97
|
+
/** Why the runtime's own answer is not used; ends the note. */
|
|
98
|
+
reason?: string;
|
|
85
99
|
}): {
|
|
86
100
|
path: string;
|
|
87
101
|
note: string | null;
|
|
@@ -93,6 +107,59 @@ export declare function chooseLegacyFallbackLog(args: {
|
|
|
93
107
|
* rather than printed about a file that may not be the one shown.
|
|
94
108
|
*/
|
|
95
109
|
export declare function resolveForwardedLogPath(flags: Pick<DaemonLogsFlags, 'stderr' | 'profile'>, capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
|
|
110
|
+
/**
|
|
111
|
+
* The first runtime whose own `logs` reads the LIVE daemon's `--log-file`
|
|
112
|
+
* (OSK-15993, `daemon/src/daemon-log-target.ts`). 0.8.112 through 0.8.118 have
|
|
113
|
+
* a `logs` subcommand, so the subcommand probe says "supported", but it reads
|
|
114
|
+
* the profile's `logs/daemon.log` unconditionally — with a desktop-managed
|
|
115
|
+
* daemon, a file nobody has written for a week. Forwarding to one of those is
|
|
116
|
+
* forwarding to the bug, so the CLI forwards only at or above this version.
|
|
117
|
+
*/
|
|
118
|
+
export declare const LIVE_LOG_RESOLUTION_RUNTIME_VERSION = "0.8.119";
|
|
119
|
+
/** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
|
|
120
|
+
export declare function parseRuntimeVersion(text: string): [number, number, number] | null;
|
|
121
|
+
/** Whether `version` is at least `floor`; null when either is not a version. */
|
|
122
|
+
export declare function runtimeVersionAtLeast(version: string, floor: string): boolean | null;
|
|
123
|
+
/** The installed runtime's `--version`, or null when it cannot say in time. */
|
|
124
|
+
export declare function probeRuntimeVersion(capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
|
|
125
|
+
/**
|
|
126
|
+
* The live daemon's log as the runtime's `status --json` reports it
|
|
127
|
+
* (`service.logs`). Status has named the live `--log-file` since before `logs`
|
|
128
|
+
* did, so on 0.8.117 status is right while logs is wrong. Null when status
|
|
129
|
+
* cannot answer, or answers with anything but an absolute path.
|
|
130
|
+
*/
|
|
131
|
+
export declare function probeStatusLogPath(flags: Pick<DaemonLogsFlags, 'profile'>, capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
|
|
132
|
+
/** What `skrr daemon logs` does: forward to the runtime, or read a file itself. */
|
|
133
|
+
export type DaemonLogsPlan = {
|
|
134
|
+
kind: 'forward';
|
|
135
|
+
}
|
|
136
|
+
/** `note` goes to stderr before any log content; never printed for `--path`. */
|
|
137
|
+
| {
|
|
138
|
+
kind: 'read';
|
|
139
|
+
path: string;
|
|
140
|
+
note: string | null;
|
|
141
|
+
missingHint: string;
|
|
142
|
+
};
|
|
143
|
+
export interface DaemonLogsPlanDeps {
|
|
144
|
+
supportsLogs(): Promise<boolean | null>;
|
|
145
|
+
runtimeVersion(): Promise<string | null>;
|
|
146
|
+
statusLogPath(): Promise<string | null>;
|
|
147
|
+
/** The profile log for this invocation (`daemon.err.log` under `--stderr`). */
|
|
148
|
+
profilePath: string;
|
|
149
|
+
service: InstalledServiceLogDestinations | null;
|
|
150
|
+
mtimeOf(filePath: string): number | null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decide which runtime, if any, `skrr daemon logs` may trust (OSK-15993).
|
|
154
|
+
*
|
|
155
|
+
* - No `logs` subcommand: the legacy fallback, as before.
|
|
156
|
+
* - A runtime at or above {@link LIVE_LOG_RESOLUTION_RUNTIME_VERSION}: forward.
|
|
157
|
+
* - An older runtime, or one whose version cannot be read: ask its `status`
|
|
158
|
+
* which file the live daemon writes and read that file here. When status
|
|
159
|
+
* cannot answer, an older runtime gets the legacy fallback (the newest of the
|
|
160
|
+
* profile log and the installed service's logs); an unknown one is forwarded.
|
|
161
|
+
*/
|
|
162
|
+
export declare function planDaemonLogs(flags: Pick<DaemonLogsFlags, 'stderr'>, deps: DaemonLogsPlanDeps): Promise<DaemonLogsPlan>;
|
|
96
163
|
/** Read a bounded trailing window, so an unrotated legacy err log is safe. */
|
|
97
164
|
export declare function readDaemonLogTail(filePath: string, maxLines: number): string[];
|
|
98
165
|
/**
|
|
@@ -3,12 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = void 0;
|
|
6
7
|
exports.describeLogAge = describeLogAge;
|
|
7
8
|
exports.resolveLegacyDaemonLogPath = resolveLegacyDaemonLogPath;
|
|
8
9
|
exports.resolveDaemonLogsProfile = resolveDaemonLogsProfile;
|
|
10
|
+
exports.installedServiceLogDestinations = installedServiceLogDestinations;
|
|
9
11
|
exports.installedServiceLogCandidates = installedServiceLogCandidates;
|
|
10
12
|
exports.chooseLegacyFallbackLog = chooseLegacyFallbackLog;
|
|
11
13
|
exports.resolveForwardedLogPath = resolveForwardedLogPath;
|
|
14
|
+
exports.parseRuntimeVersion = parseRuntimeVersion;
|
|
15
|
+
exports.runtimeVersionAtLeast = runtimeVersionAtLeast;
|
|
16
|
+
exports.probeRuntimeVersion = probeRuntimeVersion;
|
|
17
|
+
exports.probeStatusLogPath = probeStatusLogPath;
|
|
18
|
+
exports.planDaemonLogs = planDaemonLogs;
|
|
12
19
|
exports.readDaemonLogTail = readDaemonLogTail;
|
|
13
20
|
exports.buildDaemonLogsArgs = buildDaemonLogsArgs;
|
|
14
21
|
const core_1 = require("@oclif/core");
|
|
@@ -93,22 +100,22 @@ function resolveDaemonLogsProfile(flags, options = {}) {
|
|
|
93
100
|
return profile ?? 'default';
|
|
94
101
|
}
|
|
95
102
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
103
|
+
* Read the installed launchd definition's log destinations (OSK-15993). The
|
|
104
|
+
* desktop supervisor starts the daemon with
|
|
98
105
|
* `--log-file ~/Library/Logs/Skrr/daemon.app.log` and redirects stderr to
|
|
99
106
|
* `daemon.stderr.log`, so the profile's `logs/` directory can hold a file
|
|
100
107
|
* nobody has written for a week. Read-only: the plist is parsed, never loaded.
|
|
101
108
|
*/
|
|
102
|
-
function
|
|
109
|
+
function installedServiceLogDestinations(profile, options = {}) {
|
|
103
110
|
if ((options.platform ?? process.platform) !== 'darwin')
|
|
104
|
-
return
|
|
111
|
+
return null;
|
|
105
112
|
const plist = node_path_1.default.join(options.home ?? (0, node_os_1.homedir)(), 'Library', 'LaunchAgents', `ai.skrr.daemon.${profile}.plist`);
|
|
106
113
|
let xml;
|
|
107
114
|
try {
|
|
108
115
|
xml = (0, node_fs_1.readFileSync)(plist, 'utf8');
|
|
109
116
|
}
|
|
110
117
|
catch {
|
|
111
|
-
return
|
|
118
|
+
return null;
|
|
112
119
|
}
|
|
113
120
|
const unescape = (v) => v
|
|
114
121
|
.replace(/</g, '<')
|
|
@@ -120,22 +127,28 @@ function installedServiceLogCandidates(profile, stderr, options = {}) {
|
|
|
120
127
|
const m = new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`).exec(xml);
|
|
121
128
|
return m?.[1]?.trim() ? unescape(m[1].trim()) : null;
|
|
122
129
|
};
|
|
123
|
-
|
|
124
|
-
const errPath = stringFor('StandardErrorPath');
|
|
125
|
-
return errPath ? [errPath] : [];
|
|
126
|
-
}
|
|
127
|
-
const out = [];
|
|
130
|
+
let logFile = null;
|
|
128
131
|
const array = /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml);
|
|
129
132
|
if (array?.[1]) {
|
|
130
133
|
const args = [...array[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unescape(m[1].trim()));
|
|
131
134
|
const at = args.lastIndexOf('--log-file');
|
|
132
135
|
if (at >= 0 && args[at + 1] && !args[at + 1].startsWith('-'))
|
|
133
|
-
|
|
136
|
+
logFile = args[at + 1];
|
|
134
137
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
return {
|
|
139
|
+
logFile,
|
|
140
|
+
stdoutPath: stringFor('StandardOutPath'),
|
|
141
|
+
stderrPath: stringFor('StandardErrorPath'),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** The installed service's log files for the main log, or for `--stderr`. */
|
|
145
|
+
function installedServiceLogCandidates(profile, stderr, options = {}) {
|
|
146
|
+
const dest = installedServiceLogDestinations(profile, options);
|
|
147
|
+
if (!dest)
|
|
148
|
+
return [];
|
|
149
|
+
if (stderr)
|
|
150
|
+
return dest.stderrPath ? [dest.stderrPath] : [];
|
|
151
|
+
return [dest.logFile, dest.stdoutPath].filter((p) => Boolean(p));
|
|
139
152
|
}
|
|
140
153
|
/**
|
|
141
154
|
* Pick the file the legacy fallback reads. An old daemon cannot say which file
|
|
@@ -164,7 +177,7 @@ function chooseLegacyFallbackLog(args) {
|
|
|
164
177
|
(profileMtime === null
|
|
165
178
|
? `the profile log ${args.profilePath}, which does not exist.`
|
|
166
179
|
: `the profile log ${args.profilePath}, which has not been written since.`) +
|
|
167
|
-
'
|
|
180
|
+
` ${args.reason ?? 'This daemon predates `logs`, so it cannot name its live log itself.'}`,
|
|
168
181
|
};
|
|
169
182
|
}
|
|
170
183
|
/**
|
|
@@ -191,6 +204,153 @@ async function resolveForwardedLogPath(flags, capture = exec_runtime_binary_1.ex
|
|
|
191
204
|
return null;
|
|
192
205
|
}
|
|
193
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The first runtime whose own `logs` reads the LIVE daemon's `--log-file`
|
|
209
|
+
* (OSK-15993, `daemon/src/daemon-log-target.ts`). 0.8.112 through 0.8.118 have
|
|
210
|
+
* a `logs` subcommand, so the subcommand probe says "supported", but it reads
|
|
211
|
+
* the profile's `logs/daemon.log` unconditionally — with a desktop-managed
|
|
212
|
+
* daemon, a file nobody has written for a week. Forwarding to one of those is
|
|
213
|
+
* forwarding to the bug, so the CLI forwards only at or above this version.
|
|
214
|
+
*/
|
|
215
|
+
exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = '0.8.119';
|
|
216
|
+
/** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
|
|
217
|
+
function parseRuntimeVersion(text) {
|
|
218
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
219
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
220
|
+
}
|
|
221
|
+
/** Whether `version` is at least `floor`; null when either is not a version. */
|
|
222
|
+
function runtimeVersionAtLeast(version, floor) {
|
|
223
|
+
const a = parseRuntimeVersion(version);
|
|
224
|
+
const b = parseRuntimeVersion(floor);
|
|
225
|
+
if (!a || !b)
|
|
226
|
+
return null;
|
|
227
|
+
for (let i = 0; i < 3; i += 1) {
|
|
228
|
+
if (a[i] !== b[i])
|
|
229
|
+
return a[i] > b[i];
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
/** The installed runtime's `--version`, or null when it cannot say in time. */
|
|
234
|
+
async function probeRuntimeVersion(capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
|
|
235
|
+
try {
|
|
236
|
+
const out = await capture(['--version'], { timeoutMs: 5_000 });
|
|
237
|
+
if (out.code !== 0)
|
|
238
|
+
return null;
|
|
239
|
+
const parsed = parseRuntimeVersion(out.stdout);
|
|
240
|
+
return parsed ? parsed.join('.') : null;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The live daemon's log as the runtime's `status --json` reports it
|
|
248
|
+
* (`service.logs`). Status has named the live `--log-file` since before `logs`
|
|
249
|
+
* did, so on 0.8.117 status is right while logs is wrong. Null when status
|
|
250
|
+
* cannot answer, or answers with anything but an absolute path.
|
|
251
|
+
*/
|
|
252
|
+
async function probeStatusLogPath(flags, capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
|
|
253
|
+
try {
|
|
254
|
+
const out = await capture([...(flags.profile ? ['--profile', flags.profile] : []), 'status', '--json'], { timeoutMs: 15_000 });
|
|
255
|
+
// `status` exits non-zero when it reports problems; the JSON is still the answer.
|
|
256
|
+
const start = out.stdout.indexOf('{');
|
|
257
|
+
if (start < 0)
|
|
258
|
+
return null;
|
|
259
|
+
const parsed = JSON.parse(out.stdout.slice(start));
|
|
260
|
+
const logs = parsed?.service?.logs;
|
|
261
|
+
return typeof logs === 'string' && node_path_1.default.isAbsolute(logs) ? logs : null;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function sameLogFile(a, b) {
|
|
268
|
+
return Boolean(a && b && node_path_1.default.resolve(a) === node_path_1.default.resolve(b));
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Decide which runtime, if any, `skrr daemon logs` may trust (OSK-15993).
|
|
272
|
+
*
|
|
273
|
+
* - No `logs` subcommand: the legacy fallback, as before.
|
|
274
|
+
* - A runtime at or above {@link LIVE_LOG_RESOLUTION_RUNTIME_VERSION}: forward.
|
|
275
|
+
* - An older runtime, or one whose version cannot be read: ask its `status`
|
|
276
|
+
* which file the live daemon writes and read that file here. When status
|
|
277
|
+
* cannot answer, an older runtime gets the legacy fallback (the newest of the
|
|
278
|
+
* profile log and the installed service's logs); an unknown one is forwarded.
|
|
279
|
+
*/
|
|
280
|
+
async function planDaemonLogs(flags, deps) {
|
|
281
|
+
const legacy = (reason) => {
|
|
282
|
+
const chosen = chooseLegacyFallbackLog({
|
|
283
|
+
profilePath: deps.profilePath,
|
|
284
|
+
serviceCandidates: (flags.stderr
|
|
285
|
+
? [deps.service?.stderrPath]
|
|
286
|
+
: [deps.service?.logFile, deps.service?.stdoutPath]).filter((p) => Boolean(p)),
|
|
287
|
+
mtimeOf: deps.mtimeOf,
|
|
288
|
+
reason,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
kind: 'read',
|
|
292
|
+
path: chosen.path,
|
|
293
|
+
note: chosen.note,
|
|
294
|
+
missingHint: 'The installed daemon predates `oversky logs`; start it or upgrade it, then retry.',
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
const supports = await deps.supportsLogs();
|
|
298
|
+
if (supports === false) {
|
|
299
|
+
return legacy('This daemon predates `logs`, so it cannot name its live log itself.');
|
|
300
|
+
}
|
|
301
|
+
// Help could not be read: nothing to go on, keep the old behaviour.
|
|
302
|
+
if (supports === null)
|
|
303
|
+
return { kind: 'forward' };
|
|
304
|
+
const version = await deps.runtimeVersion();
|
|
305
|
+
const current = version
|
|
306
|
+
? runtimeVersionAtLeast(version, exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION)
|
|
307
|
+
: null;
|
|
308
|
+
if (current === true)
|
|
309
|
+
return { kind: 'forward' };
|
|
310
|
+
const live = await deps.statusLogPath();
|
|
311
|
+
const predates = version !== null
|
|
312
|
+
? `The installed runtime (${version}) predates \`logs\` following the live daemon's ` +
|
|
313
|
+
`--log-file (${exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION}), so it would read ${deps.profilePath}.`
|
|
314
|
+
: `The installed runtime did not report its version, so its \`logs\` may read ` +
|
|
315
|
+
`${deps.profilePath} instead of the live log.`;
|
|
316
|
+
if (!live) {
|
|
317
|
+
if (current === null)
|
|
318
|
+
return { kind: 'forward' };
|
|
319
|
+
return legacy(`${predates} Its \`status\` did not name the live log either.`);
|
|
320
|
+
}
|
|
321
|
+
// The live daemon writes the profile log itself: the runtime's answer is right.
|
|
322
|
+
if (sameLogFile(live, node_path_1.default.join(node_path_1.default.dirname(deps.profilePath), 'daemon.log'))) {
|
|
323
|
+
return { kind: 'forward' };
|
|
324
|
+
}
|
|
325
|
+
const missingHint = 'The live daemon has not written it yet; `skrr daemon status` shows whether one is running.';
|
|
326
|
+
if (!flags.stderr) {
|
|
327
|
+
return {
|
|
328
|
+
kind: 'read',
|
|
329
|
+
path: live,
|
|
330
|
+
note: `Showing ${live}, the live daemon's log as \`skrrd status\` reports it. ${predates}`,
|
|
331
|
+
missingHint,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// --stderr: trust the installed definition's redirect only when it is the
|
|
335
|
+
// one that launched this daemon (its --log-file is the live one) — the same
|
|
336
|
+
// rule the runtime's own resolution applies.
|
|
337
|
+
if (deps.service?.stderrPath && sameLogFile(deps.service.logFile, live)) {
|
|
338
|
+
return {
|
|
339
|
+
kind: 'read',
|
|
340
|
+
path: deps.service.stderrPath,
|
|
341
|
+
note: `Showing ${deps.service.stderrPath}, the live daemon's stderr as its service definition ` +
|
|
342
|
+
`redirects it; its full log, WARN and ERROR included, is ${live}. ${predates}`,
|
|
343
|
+
missingHint,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
kind: 'read',
|
|
348
|
+
path: live,
|
|
349
|
+
note: `Showing ${live}, the live daemon's log as \`skrrd status\` reports it; it carries every ` +
|
|
350
|
+
`WARN and ERROR line. ${predates}`,
|
|
351
|
+
missingHint,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
194
354
|
/** Read a bounded trailing window, so an unrotated legacy err log is safe. */
|
|
195
355
|
function readDaemonLogTail(filePath, maxLines) {
|
|
196
356
|
if (maxLines === 0)
|
|
@@ -301,29 +461,38 @@ class DaemonLogs extends core_1.Command {
|
|
|
301
461
|
// `oversky 0.2.x` has profiles and writes the same files, but has no
|
|
302
462
|
// `logs` subcommand. Probe top-level help first so forwarding does not
|
|
303
463
|
// print an unrelated "unknown option" error before the useful output.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
464
|
+
//
|
|
465
|
+
// OSK-15993: a runtime that HAS `logs` is not necessarily one to trust
|
|
466
|
+
// with it — 0.8.112–0.8.118 read the profile log even when the live
|
|
467
|
+
// daemon writes elsewhere. `planDaemonLogs` forwards only to a runtime
|
|
468
|
+
// that resolves the live log itself.
|
|
469
|
+
const plan = await planDaemonLogs(flags, {
|
|
470
|
+
supportsLogs: () => (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('logs'),
|
|
471
|
+
runtimeVersion: () => probeRuntimeVersion(),
|
|
472
|
+
statusLogPath: () => probeStatusLogPath(flags),
|
|
473
|
+
profilePath: resolveLegacyDaemonLogPath(flags),
|
|
474
|
+
service: installedServiceLogDestinations(resolveDaemonLogsProfile(flags)),
|
|
475
|
+
mtimeOf: (filePath) => {
|
|
476
|
+
try {
|
|
477
|
+
return (0, node_fs_1.statSync)(filePath).mtimeMs;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
if (plan.kind === 'read') {
|
|
485
|
+
const logPath = plan.path;
|
|
486
|
+
// `--path` prints the one path and nothing else, on either stream.
|
|
320
487
|
if (flags.path) {
|
|
321
488
|
this.log(logPath);
|
|
322
489
|
return;
|
|
323
490
|
}
|
|
491
|
+
if (plan.note)
|
|
492
|
+
this.logToStderr(plan.note);
|
|
324
493
|
showAge(logPath);
|
|
325
494
|
if (!(0, node_fs_1.existsSync)(logPath)) {
|
|
326
|
-
this.error(`No daemon log at ${logPath}.
|
|
495
|
+
this.error(`No daemon log at ${logPath}. ${plan.missingHint}`, { exit: 1 });
|
|
327
496
|
}
|
|
328
497
|
const lines = readDaemonLogTail(logPath, flags.lines ?? 200);
|
|
329
498
|
if (lines.length > 0)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
|
|
3
|
+
*
|
|
4
|
+
* `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
|
|
5
|
+
* capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
|
|
6
|
+
* report or a log line that carries it can open the preview, so every sink that records a URL has to run its
|
|
7
|
+
* strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
|
|
8
|
+
* exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
|
|
11
|
+
* than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
|
|
12
|
+
* characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
|
|
13
|
+
* only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
|
|
14
|
+
* written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
|
|
15
|
+
* string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
|
|
16
|
+
* and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
|
|
17
|
+
* mints, so a change to either side fails there rather than in a trace nobody reads.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
|
|
20
|
+
* repository writes one, and a scan that decoded every string first would cost every sink on every record. The
|
|
21
|
+
* request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
|
|
22
|
+
* stays as it is.
|
|
23
|
+
*
|
|
24
|
+
* COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
|
|
25
|
+
* cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
|
|
26
|
+
*
|
|
27
|
+
* NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
|
|
28
|
+
* with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
|
|
29
|
+
* record, because a record with the token still in it is exactly what this exists to prevent.
|
|
30
|
+
*/
|
|
31
|
+
/** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
|
|
32
|
+
export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
|
|
33
|
+
/**
|
|
34
|
+
* Replace every preview token in a string with {@link PREVIEW_TOKEN_REDACTION}. The rest of the string is
|
|
35
|
+
* untouched, so a trace is still readable. Anything that is not a string comes back as it came.
|
|
36
|
+
*/
|
|
37
|
+
export declare function redactPreviewTokens(value: string): string;
|
|
38
|
+
export declare function redactPreviewTokens<T>(value: T): T;
|
|
39
|
+
export interface PreviewTokenWalkLimits {
|
|
40
|
+
/** Objects and arrays visited before the walk gives up. Default 50 000: a Sentry transaction of a thousand spans. */
|
|
41
|
+
maxNodes?: number;
|
|
42
|
+
/** Nesting followed before the walk gives up. Default 32. */
|
|
43
|
+
maxDepth?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
|
|
47
|
+
* copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
|
|
48
|
+
* the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
|
|
49
|
+
* be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
|
|
50
|
+
* not enumerable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function redactPreviewTokensDeep(root: unknown, limits?: PreviewTokenWalkLimits): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
|
|
55
|
+
* are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
|
|
56
|
+
* `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
|
|
57
|
+
* An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
|
|
58
|
+
* what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
|
|
59
|
+
* Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
|
|
60
|
+
*/
|
|
61
|
+
export declare function withPreviewTokenRedaction<T extends object>(options: T): T;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
|
|
4
|
+
*
|
|
5
|
+
* `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
|
|
6
|
+
* capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
|
|
7
|
+
* report or a log line that carries it can open the preview, so every sink that records a URL has to run its
|
|
8
|
+
* strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
|
|
9
|
+
* exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
|
|
12
|
+
* than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
|
|
13
|
+
* characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
|
|
14
|
+
* only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
|
|
15
|
+
* written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
|
|
16
|
+
* string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
|
|
17
|
+
* and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
|
|
18
|
+
* mints, so a change to either side fails there rather than in a trace nobody reads.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
|
|
21
|
+
* repository writes one, and a scan that decoded every string first would cost every sink on every record. The
|
|
22
|
+
* request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
|
|
23
|
+
* stays as it is.
|
|
24
|
+
*
|
|
25
|
+
* COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
|
|
26
|
+
* cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
|
|
27
|
+
*
|
|
28
|
+
* NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
|
|
29
|
+
* with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
|
|
30
|
+
* record, because a record with the token still in it is exactly what this exists to prevent.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.PREVIEW_TOKEN_REDACTION = void 0;
|
|
34
|
+
exports.redactPreviewTokens = redactPreviewTokens;
|
|
35
|
+
exports.redactPreviewTokensDeep = redactPreviewTokensDeep;
|
|
36
|
+
exports.withPreviewTokenRedaction = withPreviewTokenRedaction;
|
|
37
|
+
/** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
|
|
38
|
+
exports.PREVIEW_TOKEN_REDACTION = 'pvw_[redacted]';
|
|
39
|
+
/**
|
|
40
|
+
* One character of a token: base64url, a dot, or any of those written as a percent-escape. An escape that was
|
|
41
|
+
* escaped again is `%25` followed by the original two digits (`_` is `%5F`, then `%255F`, then `%25255F`), which is
|
|
42
|
+
* what the `(?:25)*` is for. `25` is not itself a digit pair of a token character, so a run of them can only be
|
|
43
|
+
* read one way.
|
|
44
|
+
*/
|
|
45
|
+
const TOKEN_CHAR = '(?:[A-Za-z0-9._-]|%(?:25)*(?:2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[0-9AaFf]|6[1-9A-Fa-f]|7[0-9Aa]))';
|
|
46
|
+
/**
|
|
47
|
+
* `pvw` then an underscore, which may be written `%5F` (or `%255F`, ...); then at least ONE token character. Not
|
|
48
|
+
* zero: the marker is `pvw_[`, and `[` is not a token character, so a string that was already redacted matches
|
|
49
|
+
* nothing and redacting twice changes nothing.
|
|
50
|
+
*/
|
|
51
|
+
const PREVIEW_TOKEN = new RegExp(`pvw(?:_|%(?:25)*5[Ff])${TOKEN_CHAR}+`, 'g');
|
|
52
|
+
function redactPreviewTokens(value) {
|
|
53
|
+
if (typeof value !== 'string' || value.indexOf('pvw') === -1) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
return value.replace(PREVIEW_TOKEN, exports.PREVIEW_TOKEN_REDACTION);
|
|
57
|
+
}
|
|
58
|
+
const DEFAULT_MAX_NODES = 50_000;
|
|
59
|
+
const DEFAULT_MAX_DEPTH = 32;
|
|
60
|
+
/**
|
|
61
|
+
* Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
|
|
62
|
+
* copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
|
|
63
|
+
* the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
|
|
64
|
+
* be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
|
|
65
|
+
* not enumerable.
|
|
66
|
+
*/
|
|
67
|
+
function redactPreviewTokensDeep(root, limits = {}) {
|
|
68
|
+
if (root === null || typeof root !== 'object') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const maxNodes = limits.maxNodes ?? DEFAULT_MAX_NODES;
|
|
72
|
+
const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
73
|
+
try {
|
|
74
|
+
const seen = new WeakSet();
|
|
75
|
+
const stack = [[root, 0]];
|
|
76
|
+
let visited = 0;
|
|
77
|
+
let complete = true;
|
|
78
|
+
const rewrite = (holder, key, current) => {
|
|
79
|
+
const next = redactPreviewTokens(current);
|
|
80
|
+
if (next === current) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
holder[key] = next;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// a frozen object throws in strict code
|
|
88
|
+
}
|
|
89
|
+
if (holder[key] !== next) {
|
|
90
|
+
// and silently keeps the old value in sloppy code
|
|
91
|
+
complete = false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const [node, depth] = stack.pop();
|
|
96
|
+
if (seen.has(node) || ArrayBuffer.isView(node)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seen.add(node);
|
|
100
|
+
visited += 1;
|
|
101
|
+
if (visited > maxNodes) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const holder = node;
|
|
105
|
+
if (node instanceof Error) {
|
|
106
|
+
for (const key of ['message', 'stack']) {
|
|
107
|
+
if (typeof holder[key] === 'string') {
|
|
108
|
+
rewrite(holder, key, holder[key]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const key of Object.keys(node)) {
|
|
113
|
+
let child;
|
|
114
|
+
try {
|
|
115
|
+
child = holder[key];
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
complete = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (typeof child === 'string') {
|
|
122
|
+
rewrite(holder, key, child);
|
|
123
|
+
}
|
|
124
|
+
else if (child !== null && typeof child === 'object') {
|
|
125
|
+
if (depth + 1 > maxDepth) {
|
|
126
|
+
complete = false;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
stack.push([child, depth + 1]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return complete;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isThenable(value) {
|
|
141
|
+
return (value !== null &&
|
|
142
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
143
|
+
typeof value.then === 'function');
|
|
144
|
+
}
|
|
145
|
+
/** The record with its tokens redacted, or `null` (dropped) when that could not be done. */
|
|
146
|
+
function scrubRecord(record) {
|
|
147
|
+
if (record === null || record === undefined) {
|
|
148
|
+
return record;
|
|
149
|
+
}
|
|
150
|
+
if (typeof record !== 'object') {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return redactPreviewTokensDeep(record) ? record : null;
|
|
154
|
+
}
|
|
155
|
+
function guard(existing) {
|
|
156
|
+
return (value, hint) => {
|
|
157
|
+
const result = existing ? existing(value, hint) : value;
|
|
158
|
+
return isThenable(result) ? result.then(scrubRecord) : scrubRecord(result);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
|
|
163
|
+
* are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
|
|
164
|
+
* `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
|
|
165
|
+
* An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
|
|
166
|
+
* what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
|
|
167
|
+
* Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
|
|
168
|
+
*/
|
|
169
|
+
function withPreviewTokenRedaction(options) {
|
|
170
|
+
const hooks = options;
|
|
171
|
+
return {
|
|
172
|
+
...options,
|
|
173
|
+
beforeSend: guard(hooks.beforeSend),
|
|
174
|
+
beforeSendTransaction: guard(hooks.beforeSendTransaction),
|
|
175
|
+
beforeBreadcrumb: guard(hooks.beforeBreadcrumb),
|
|
176
|
+
};
|
|
177
|
+
}
|