@skrr-ai/cli 0.1.53 → 0.1.55
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/tasks/approvals/list.d.ts +38 -2
- package/dist/commands/tasks/approvals/list.js +112 -20
- package/dist/lib/auth-core-init.d.ts +19 -0
- package/dist/lib/auth-core-init.js +38 -1
- package/dist/lib/cli-identity.js +1 -2
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.js +29 -0
- package/dist/lib/device-id.d.ts +23 -0
- package/dist/lib/device-id.js +68 -0
- package/dist/lib/first-party-harness-broker.js +4 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +9 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +10 -6
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +182 -175
- package/oclif.manifest.json +27307 -27307
- package/package.json +1 -1
|
@@ -30,10 +30,46 @@ export declare function approvalRow(a: Record<string, unknown>): {
|
|
|
30
30
|
* on an older run that a later run got past is history, not the current state.
|
|
31
31
|
*/
|
|
32
32
|
export declare function permissionBlockedRun(runs: Array<Record<string, unknown>>): Record<string, unknown> | null;
|
|
33
|
+
/** One refused call: the tool and its input, as the run recorded them. */
|
|
34
|
+
export type PermissionRefusal = {
|
|
35
|
+
tool: string | null;
|
|
36
|
+
input: string;
|
|
37
|
+
};
|
|
38
|
+
/** One way forward, as a command a person or a script can run. */
|
|
39
|
+
export type PermissionBlockRemedy = {
|
|
40
|
+
action: 'run_interactively' | 'allow_unattended' | 'restart';
|
|
41
|
+
command: string;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The structured answer `--json` carries as `blockedRun` (OSK-12173). Before
|
|
46
|
+
* this the explanation existed only as prose, and with `--json` only on
|
|
47
|
+
* stderr, so a script reading the JSON saw `{"data":[]}` and nothing to act on.
|
|
48
|
+
*/
|
|
49
|
+
export type BlockedRunSummary = {
|
|
50
|
+
runId: string;
|
|
51
|
+
failureCode: string;
|
|
52
|
+
/** The cause, from the run's own sentence, without its generic remedy. */
|
|
53
|
+
reason: string | null;
|
|
54
|
+
agentId: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* The refused calls with their inputs. Empty when the run recorded none
|
|
57
|
+
* (runs finalized before the server kept them, or evidence without an input
|
|
58
|
+
* preview). Never inferred.
|
|
59
|
+
*/
|
|
60
|
+
refused: PermissionRefusal[];
|
|
61
|
+
remedies: PermissionBlockRemedy[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The block as data: which run, why, what was refused, and the ways forward
|
|
65
|
+
* with the agent id filled in (`<agent>` only when the run recorded no agent).
|
|
66
|
+
*/
|
|
67
|
+
export declare function blockedRunSummary(run: Record<string, unknown>, bin?: string, taskRef?: string): BlockedRunSummary;
|
|
33
68
|
/**
|
|
34
69
|
* What to print for such a run: why it is blocked, what needed approval (the
|
|
35
|
-
* run's own
|
|
36
|
-
*
|
|
70
|
+
* run's own sentence, plus each refused input it recorded), that an unattended
|
|
71
|
+
* run cannot prompt, and the ways forward, each stated once with the real
|
|
72
|
+
* agent id.
|
|
37
73
|
*/
|
|
38
74
|
export declare function permissionBlockLines(run: Record<string, unknown>, bin?: string, taskRef?: string): string[];
|
|
39
75
|
export default class TasksApprovalsList extends BaseCommand {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.approvalRow = approvalRow;
|
|
4
4
|
exports.permissionBlockedRun = permissionBlockedRun;
|
|
5
|
+
exports.blockedRunSummary = blockedRunSummary;
|
|
5
6
|
exports.permissionBlockLines = permissionBlockLines;
|
|
6
7
|
const core_1 = require("@oclif/core");
|
|
7
8
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
@@ -46,28 +47,107 @@ function permissionBlockedRun(runs) {
|
|
|
46
47
|
const code = latest.failureCode ?? latest.failureClass;
|
|
47
48
|
return code === data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE ? latest : null;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Where the run's own fix begins in its recorded sentence.
|
|
52
|
+
*
|
|
53
|
+
* The server writes `failureReason` as the cause followed by the fix
|
|
54
|
+
* (`permissionBlockedRun.PERMISSION_BLOCK_GUIDANCE`), and that fix names the
|
|
55
|
+
* agent only as `<agent>` because the sentence is shared by every surface.
|
|
56
|
+
* This command knows the agent, so it states the fix itself, with the real id,
|
|
57
|
+
* and keeps only the cause from the sentence (OSK-12173): printing both showed
|
|
58
|
+
* the remedy twice, once with a placeholder.
|
|
59
|
+
*/
|
|
60
|
+
const RECORDED_REMEDY_MARKER = 'Re-running will be refused';
|
|
61
|
+
function refusalsOf(run) {
|
|
62
|
+
const raw = run.permissionRefusals;
|
|
63
|
+
if (!Array.isArray(raw))
|
|
64
|
+
return [];
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const entry of raw) {
|
|
67
|
+
if (!entry || typeof entry !== 'object')
|
|
68
|
+
continue;
|
|
69
|
+
const { tool, input } = entry;
|
|
70
|
+
if (typeof input !== 'string' || !input.trim())
|
|
71
|
+
continue;
|
|
72
|
+
out.push({ tool: typeof tool === 'string' && tool ? tool : null, input: input.trim() });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** The cause, cut before the recorded remedy; `<agent>` filled in if any survives. */
|
|
77
|
+
function blockReason(run, agentId) {
|
|
78
|
+
const full = typeof run.failureReason === 'string' ? run.failureReason.trim() : '';
|
|
79
|
+
if (!full)
|
|
80
|
+
return null;
|
|
81
|
+
const at = full.indexOf(RECORDED_REMEDY_MARKER);
|
|
82
|
+
const cause = at > 0 ? full.slice(0, at).trim() : full;
|
|
83
|
+
return agentId ? cause.split('<agent>').join(agentId) : cause;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The block as data: which run, why, what was refused, and the ways forward
|
|
87
|
+
* with the agent id filled in (`<agent>` only when the run recorded no agent).
|
|
88
|
+
*/
|
|
89
|
+
function blockedRunSummary(run, bin = 'skrr', taskRef = '<task-id>') {
|
|
90
|
+
const executor = run.executor;
|
|
91
|
+
const agentId = executor && typeof executor.agentId === 'string' && executor.agentId ? executor.agentId : null;
|
|
92
|
+
const agent = agentId ?? '<agent>';
|
|
93
|
+
const code = run.failureCode ?? run.failureClass;
|
|
94
|
+
return {
|
|
95
|
+
runId: String(run.runId ?? run.id ?? '-'),
|
|
96
|
+
failureCode: typeof code === 'string' ? code : data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE,
|
|
97
|
+
reason: blockReason(run, agentId),
|
|
98
|
+
agentId,
|
|
99
|
+
refused: refusalsOf(run),
|
|
100
|
+
remedies: [
|
|
101
|
+
{
|
|
102
|
+
action: 'run_interactively',
|
|
103
|
+
command: `${bin} agents chat ${agent} --approve-tools --prompt "..."`,
|
|
104
|
+
description: 'Do the work interactively, approving each call.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
action: 'allow_unattended',
|
|
108
|
+
command: `${bin} agents update ${agent} --default-permission-mode bypassPermissions`,
|
|
109
|
+
description: 'Let unattended runs use the tool without asking.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
action: 'restart',
|
|
113
|
+
command: `${bin} tasks start ${taskRef}`,
|
|
114
|
+
description: 'Start the task again once its agent may use the tool (or pick an agent/harness whose mode allows it).',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const SHELL_TOOL = /^(bash|shell|local_shell|exec_command|commandexecution|command_execution)$/i;
|
|
120
|
+
function toolLabel(tool) {
|
|
121
|
+
if (!tool)
|
|
122
|
+
return '';
|
|
123
|
+
return SHELL_TOOL.test(tool) ? ' (shell)' : ` (${tool})`;
|
|
124
|
+
}
|
|
49
125
|
/**
|
|
50
126
|
* What to print for such a run: why it is blocked, what needed approval (the
|
|
51
|
-
* run's own
|
|
52
|
-
*
|
|
127
|
+
* run's own sentence, plus each refused input it recorded), that an unattended
|
|
128
|
+
* run cannot prompt, and the ways forward, each stated once with the real
|
|
129
|
+
* agent id.
|
|
53
130
|
*/
|
|
54
131
|
function permissionBlockLines(run, bin = 'skrr', taskRef = '<task-id>') {
|
|
55
|
-
const
|
|
56
|
-
const reason =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
const summary = blockedRunSummary(run, bin, taskRef);
|
|
133
|
+
const reason = summary.reason ?? '';
|
|
134
|
+
// The sentence already quotes a lone refused input (shortened); list an
|
|
135
|
+
// input only when the sentence does not already show it.
|
|
136
|
+
const refusedLines = summary.refused
|
|
137
|
+
.filter((refusal) => !reason.includes(refusal.input.slice(0, 60)))
|
|
138
|
+
.map((refusal) => ` Refused: ${refusal.input}${toolLabel(refusal.tool)}`);
|
|
139
|
+
const [chat, update, restart] = summary.remedies;
|
|
61
140
|
return [
|
|
62
|
-
`The latest run (${runId}) is blocked on a permission, not on an approval request.`,
|
|
141
|
+
`The latest run (${summary.runId}) is blocked on a permission, not on an approval request.`,
|
|
63
142
|
...(reason ? [` ${reason}`] : []),
|
|
143
|
+
...refusedLines,
|
|
64
144
|
'It ran unattended (`tasks start`), and an unattended run cannot prompt anyone to approve a',
|
|
65
145
|
'tool call, so the ask was refused on the spot and no approval request was recorded here.',
|
|
66
146
|
'',
|
|
67
147
|
'To continue, either:',
|
|
68
|
-
` ${
|
|
69
|
-
` ${
|
|
70
|
-
` then: ${
|
|
148
|
+
` ${chat.command} do the work interactively, approving each call`,
|
|
149
|
+
` ${update.command} let unattended runs use the tool`,
|
|
150
|
+
` then: ${restart.command} (or pick an agent/harness whose mode allows it)`,
|
|
71
151
|
];
|
|
72
152
|
}
|
|
73
153
|
class TasksApprovalsList extends base_command_1.BaseCommand {
|
|
@@ -98,25 +178,37 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
|
|
|
98
178
|
// row, so the empty list alone would say "nothing to act on" about a task
|
|
99
179
|
// that is waiting on exactly that. Read the latest run, best-effort: the
|
|
100
180
|
// approvals list is still the answer if the runs read fails.
|
|
181
|
+
// `runsRead` separates "the latest run is not blocked" from "we could not
|
|
182
|
+
// look": only the first may be reported to a script as `blockedRun: null`.
|
|
101
183
|
let blockedRun = null;
|
|
184
|
+
let runsRead = false;
|
|
102
185
|
try {
|
|
103
186
|
const runs = (await data_provider_1.dataService.getTaskRuns(args.id));
|
|
104
187
|
blockedRun = permissionBlockedRun(runs?.data ?? []);
|
|
188
|
+
runsRead = true;
|
|
105
189
|
}
|
|
106
190
|
catch {
|
|
107
191
|
blockedRun = null;
|
|
108
192
|
}
|
|
109
|
-
const blockLines = blockedRun ? permissionBlockLines(blockedRun, this.config.bin, args.id) : [];
|
|
110
193
|
if (flags.json) {
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
194
|
+
// OSK-12173 — the server payload is kept as it was (`data` unchanged)
|
|
195
|
+
// and the block is added beside it as data a script can act on:
|
|
196
|
+
// `blockedRun` is the summary, or null when the latest run is not
|
|
197
|
+
// blocked. It is omitted when the runs could not be read, because null
|
|
198
|
+
// would claim a state nobody checked.
|
|
199
|
+
const payload = response && typeof response === 'object' && !Array.isArray(response)
|
|
200
|
+
? response
|
|
201
|
+
: { data: items };
|
|
202
|
+
const out = runsRead
|
|
203
|
+
? {
|
|
204
|
+
...payload,
|
|
205
|
+
blockedRun: blockedRun ? blockedRunSummary(blockedRun, this.config.bin, args.id) : null,
|
|
206
|
+
}
|
|
207
|
+
: payload;
|
|
208
|
+
this.log(JSON.stringify(out, null, 2));
|
|
118
209
|
return;
|
|
119
210
|
}
|
|
211
|
+
const blockLines = blockedRun ? permissionBlockLines(blockedRun, this.config.bin, args.id) : [];
|
|
120
212
|
if (items.length === 0) {
|
|
121
213
|
this.log('No approval requests.');
|
|
122
214
|
if (blockLines.length > 0) {
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential-envelope failure that carries a hint, as ONE plain line
|
|
3
|
+
* (OSK-12180) — or null when the warning is anything else.
|
|
4
|
+
*
|
|
5
|
+
* An unreadable stored credential is reported on every command until someone
|
|
6
|
+
* signs in again, and auth-core's structured form is six lines of object dump
|
|
7
|
+
* ending in the raw CryptoKit/OpenSSL string (`Unsupported state or unable to
|
|
8
|
+
* authenticate data`). The person reading it needs the hint auth-core already
|
|
9
|
+
* wrote for exactly this, and what to do; the key id and the crypto message are
|
|
10
|
+
* for whoever is debugging, which is what `DEBUG` is for everywhere else in
|
|
11
|
+
* this logger. The daemon logs through its own logger and keeps the structured
|
|
12
|
+
* record.
|
|
13
|
+
*
|
|
14
|
+
* The remedy is added only to the DEFAULT hint, which is about this config's
|
|
15
|
+
* own credential. A caller that supplied its own hint knows the case better —
|
|
16
|
+
* another config's credential is not fixed by `skrr login` here, and its hint
|
|
17
|
+
* says so (OSK-12135).
|
|
18
|
+
*/
|
|
19
|
+
export declare function plainCredentialEnvelopeWarning(args: readonly unknown[]): string | null;
|
|
1
20
|
type DedicatedGenerationPreparationOptions = {
|
|
2
21
|
machineUuid?: string;
|
|
3
22
|
configDir?: string;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.CliCredentialInitializationError = void 0;
|
|
7
|
+
exports.plainCredentialEnvelopeWarning = plainCredentialEnvelopeWarning;
|
|
7
8
|
exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
|
|
8
9
|
exports.ensureAuthCoreConfigured = ensureAuthCoreConfigured;
|
|
9
10
|
exports.prefetchCliAuth = prefetchCliAuth;
|
|
@@ -44,6 +45,36 @@ function envFlag(name) {
|
|
|
44
45
|
const v = process.env[name];
|
|
45
46
|
return v === '1' || v === 'true' || v === 'TRUE';
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A credential-envelope failure that carries a hint, as ONE plain line
|
|
50
|
+
* (OSK-12180) — or null when the warning is anything else.
|
|
51
|
+
*
|
|
52
|
+
* An unreadable stored credential is reported on every command until someone
|
|
53
|
+
* signs in again, and auth-core's structured form is six lines of object dump
|
|
54
|
+
* ending in the raw CryptoKit/OpenSSL string (`Unsupported state or unable to
|
|
55
|
+
* authenticate data`). The person reading it needs the hint auth-core already
|
|
56
|
+
* wrote for exactly this, and what to do; the key id and the crypto message are
|
|
57
|
+
* for whoever is debugging, which is what `DEBUG` is for everywhere else in
|
|
58
|
+
* this logger. The daemon logs through its own logger and keeps the structured
|
|
59
|
+
* record.
|
|
60
|
+
*
|
|
61
|
+
* The remedy is added only to the DEFAULT hint, which is about this config's
|
|
62
|
+
* own credential. A caller that supplied its own hint knows the case better —
|
|
63
|
+
* another config's credential is not fixed by `skrr login` here, and its hint
|
|
64
|
+
* says so (OSK-12135).
|
|
65
|
+
*/
|
|
66
|
+
function plainCredentialEnvelopeWarning(args) {
|
|
67
|
+
const [message, metadata] = args;
|
|
68
|
+
if (typeof message !== 'string' || !message.startsWith('[credEnvelope] '))
|
|
69
|
+
return null;
|
|
70
|
+
if (!metadata || typeof metadata !== 'object')
|
|
71
|
+
return null;
|
|
72
|
+
const hint = metadata.hint;
|
|
73
|
+
if (typeof hint !== 'string' || !hint.trim())
|
|
74
|
+
return null;
|
|
75
|
+
const remedy = hint === auth_core_1.DEFAULT_UNREADABLE_CREDENTIAL_HINT ? ' Run `skrr login`.' : '';
|
|
76
|
+
return `warning: ${hint.trim()}.${remedy} (DEBUG=1 shows the underlying error.)`;
|
|
77
|
+
}
|
|
47
78
|
const cliLogger = {
|
|
48
79
|
// The CLI is a short-lived foreground process. Trace/debug get dropped
|
|
49
80
|
// unless DEBUG is set so we don't spam terminal output.
|
|
@@ -55,7 +86,13 @@ const cliLogger = {
|
|
|
55
86
|
if (process.env.DEBUG)
|
|
56
87
|
console.error('[auth-core info]', ...args);
|
|
57
88
|
},
|
|
58
|
-
warn: (...args) =>
|
|
89
|
+
warn: (...args) => {
|
|
90
|
+
const plain = process.env.DEBUG ? null : plainCredentialEnvelopeWarning(args);
|
|
91
|
+
if (plain)
|
|
92
|
+
console.error(plain);
|
|
93
|
+
else
|
|
94
|
+
console.error('[auth-core warn]', ...args);
|
|
95
|
+
},
|
|
59
96
|
error: (...args) => console.error('[auth-core error]', ...args),
|
|
60
97
|
};
|
|
61
98
|
/**
|
package/dist/lib/cli-identity.js
CHANGED
|
@@ -74,7 +74,6 @@ const path = __importStar(require("node:path"));
|
|
|
74
74
|
const auth_storage_1 = require("./auth-storage");
|
|
75
75
|
const config_1 = require("./config");
|
|
76
76
|
const cli_id_1 = require("./cli-id");
|
|
77
|
-
const device_id_1 = require("./device-id");
|
|
78
77
|
const keychain_1 = require("./keychain");
|
|
79
78
|
/**
|
|
80
79
|
* A named profile's own identity. Deterministic in (host, user, config root,
|
|
@@ -127,7 +126,7 @@ function planLoginCliId(cfg, deps = {}) {
|
|
|
127
126
|
const machine = cfg.cliId?.trim();
|
|
128
127
|
if (machine)
|
|
129
128
|
return { cliId: machine, source: 'machine' };
|
|
130
|
-
const cliId = (0,
|
|
129
|
+
const cliId = (0, config_1.machineCliId)();
|
|
131
130
|
return { cliId, source: 'machine', updatedConfig: { ...cfg, cliId } };
|
|
132
131
|
}
|
|
133
132
|
/**
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -190,6 +190,16 @@ export declare function isEnvAuthOverride(): boolean;
|
|
|
190
190
|
* resolver and every existing install read it.
|
|
191
191
|
*/
|
|
192
192
|
export declare function configRoot(env?: NodeJS.ProcessEnv): string;
|
|
193
|
+
/**
|
|
194
|
+
* The machine cliId THIS config root mints when it has none stored
|
|
195
|
+
* (OSK-12179). The primary root keeps the historical (host, user) id; any other
|
|
196
|
+
* root gets one of its own, so two roots of one user are two server-side
|
|
197
|
+
* families and one root's hand-off confirm cannot retire the other's session.
|
|
198
|
+
*
|
|
199
|
+
* Mints nothing and reads nothing stored: callers persist it (`cli-identity.ts`)
|
|
200
|
+
* or present it only when nothing is stored (the daemon broker).
|
|
201
|
+
*/
|
|
202
|
+
export declare function machineCliId(): string;
|
|
193
203
|
/**
|
|
194
204
|
* The PRE-RENAME config root — the generation between `.sky` and `.skrr`.
|
|
195
205
|
*
|
package/dist/lib/config.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.describeDeployment = describeDeployment;
|
|
|
39
39
|
exports.canonicalizeKnownBaseURL = canonicalizeKnownBaseURL;
|
|
40
40
|
exports.isEnvAuthOverride = isEnvAuthOverride;
|
|
41
41
|
exports.configRoot = configRoot;
|
|
42
|
+
exports.machineCliId = machineCliId;
|
|
42
43
|
exports.priorConfigDir = priorConfigDir;
|
|
43
44
|
exports.loadConfig = loadConfig;
|
|
44
45
|
exports.saveConfig = saveConfig;
|
|
@@ -54,6 +55,7 @@ const path = __importStar(require("node:path"));
|
|
|
54
55
|
const keychain_1 = require("./keychain");
|
|
55
56
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
56
57
|
const cli_id_1 = require("./cli-id");
|
|
58
|
+
const device_id_1 = require("./device-id");
|
|
57
59
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
58
60
|
// Both from config/public-endpoints.json, the one domain contract, rather than a
|
|
59
61
|
// fourth hand-copied pair. The CLI previously knew only about dev — there was no
|
|
@@ -269,6 +271,26 @@ function configRoot(env = process.env) {
|
|
|
269
271
|
function newConfigDir() {
|
|
270
272
|
return configRoot();
|
|
271
273
|
}
|
|
274
|
+
/** The root this machine uses when nothing relocates it (`~/.skrr`). */
|
|
275
|
+
function defaultConfigRoot() {
|
|
276
|
+
return (0, auth_core_1.resolveConfigRoot)({});
|
|
277
|
+
}
|
|
278
|
+
/** Is this process on the primary root, however it was reached? */
|
|
279
|
+
function isDefaultConfigRoot() {
|
|
280
|
+
return path.resolve(configRoot()) === path.resolve(defaultConfigRoot());
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The machine cliId THIS config root mints when it has none stored
|
|
284
|
+
* (OSK-12179). The primary root keeps the historical (host, user) id; any other
|
|
285
|
+
* root gets one of its own, so two roots of one user are two server-side
|
|
286
|
+
* families and one root's hand-off confirm cannot retire the other's session.
|
|
287
|
+
*
|
|
288
|
+
* Mints nothing and reads nothing stored: callers persist it (`cli-identity.ts`)
|
|
289
|
+
* or present it only when nothing is stored (the daemon broker).
|
|
290
|
+
*/
|
|
291
|
+
function machineCliId() {
|
|
292
|
+
return (0, device_id_1.machineCliIdForRoot)(configRoot(), defaultConfigRoot());
|
|
293
|
+
}
|
|
272
294
|
/**
|
|
273
295
|
* Fields that belong to the MACHINE, not to a profile.
|
|
274
296
|
*
|
|
@@ -472,6 +494,13 @@ function loadConfig() {
|
|
|
472
494
|
if (!fs.existsSync(carriedPath))
|
|
473
495
|
continue;
|
|
474
496
|
const carried = migrateLegacyBaseURL(readJsonOrThrow(carriedPath));
|
|
497
|
+
// The ancestor lives under $HOME whatever root this is, so its `cliId`
|
|
498
|
+
// belongs to the PRIMARY root's sessions. A relocated root that adopted
|
|
499
|
+
// it would present the primary root's identity, and a hand-off confirm
|
|
500
|
+
// there retires the primary root's families (OSK-12179). It mints its own
|
|
501
|
+
// at first login instead (`machineCliId`).
|
|
502
|
+
if (!isDefaultConfigRoot())
|
|
503
|
+
delete carried.cliId;
|
|
475
504
|
try {
|
|
476
505
|
saveConfig(carried);
|
|
477
506
|
}
|
package/dist/lib/device-id.d.ts
CHANGED
|
@@ -12,3 +12,26 @@
|
|
|
12
12
|
* 128-char + printable-ASCII guard.
|
|
13
13
|
*/
|
|
14
14
|
export declare function generateCliId(): string;
|
|
15
|
+
/**
|
|
16
|
+
* The machine cliId for ONE config root (OSK-12179).
|
|
17
|
+
*
|
|
18
|
+
* `generateCliId()` hashes only (host, user), so every root on the machine got
|
|
19
|
+
* the same id: a fresh `SKRR_CONFIG_DIR` root's default profile minted exactly
|
|
20
|
+
* the id the real `~/.skrr` root had been presenting. The server's
|
|
21
|
+
* `cli-handoff/confirm` retires every OTHER live family of (user, cliId), so a
|
|
22
|
+
* sign-in in the second root could end the first root's session. The Keychain
|
|
23
|
+
* slots were already separated by root (OSK-12135), and a named profile's own
|
|
24
|
+
* id already hashes its root (`profileScopedCliId`); this was the one identity
|
|
25
|
+
* that did not.
|
|
26
|
+
*
|
|
27
|
+
* The DEFAULT root keeps `generateCliId()` byte for byte. Every install already
|
|
28
|
+
* stores or re-derives that value, every live session is bound to it, and a
|
|
29
|
+
* refresh presenting another id is refused — so changing it would sign the
|
|
30
|
+
* primary root out. Only a root somewhere else gets its own id, deterministic in
|
|
31
|
+
* (host, user, root) so a root that loses its config file and signs in again
|
|
32
|
+
* presents the same id rather than scattering families.
|
|
33
|
+
*
|
|
34
|
+
* A root is the default one when it resolves to the default path, however it
|
|
35
|
+
* was reached: `SKRR_CONFIG_DIR=~/.skrr` is the primary root, not a second one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function machineCliIdForRoot(root: string, defaultRoot: string): string;
|
package/dist/lib/device-id.js
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.generateCliId = generateCliId;
|
|
40
|
+
exports.machineCliIdForRoot = machineCliIdForRoot;
|
|
7
41
|
const node_crypto_1 = require("node:crypto");
|
|
8
42
|
const node_os_1 = __importDefault(require("node:os"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
9
44
|
/**
|
|
10
45
|
* Derive a stable per-machine identifier the server will treat as a
|
|
11
46
|
* "device" for the device-code flow. Mirrors the daemon's `generateDaemonId`
|
|
@@ -25,3 +60,36 @@ function generateCliId() {
|
|
|
25
60
|
const digest = (0, node_crypto_1.createHash)('sha256').update(`${hostname}|${username}|cli`).digest('hex');
|
|
26
61
|
return `cli_${digest.slice(0, 12)}`;
|
|
27
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The machine cliId for ONE config root (OSK-12179).
|
|
65
|
+
*
|
|
66
|
+
* `generateCliId()` hashes only (host, user), so every root on the machine got
|
|
67
|
+
* the same id: a fresh `SKRR_CONFIG_DIR` root's default profile minted exactly
|
|
68
|
+
* the id the real `~/.skrr` root had been presenting. The server's
|
|
69
|
+
* `cli-handoff/confirm` retires every OTHER live family of (user, cliId), so a
|
|
70
|
+
* sign-in in the second root could end the first root's session. The Keychain
|
|
71
|
+
* slots were already separated by root (OSK-12135), and a named profile's own
|
|
72
|
+
* id already hashes its root (`profileScopedCliId`); this was the one identity
|
|
73
|
+
* that did not.
|
|
74
|
+
*
|
|
75
|
+
* The DEFAULT root keeps `generateCliId()` byte for byte. Every install already
|
|
76
|
+
* stores or re-derives that value, every live session is bound to it, and a
|
|
77
|
+
* refresh presenting another id is refused — so changing it would sign the
|
|
78
|
+
* primary root out. Only a root somewhere else gets its own id, deterministic in
|
|
79
|
+
* (host, user, root) so a root that loses its config file and signs in again
|
|
80
|
+
* presents the same id rather than scattering families.
|
|
81
|
+
*
|
|
82
|
+
* A root is the default one when it resolves to the default path, however it
|
|
83
|
+
* was reached: `SKRR_CONFIG_DIR=~/.skrr` is the primary root, not a second one.
|
|
84
|
+
*/
|
|
85
|
+
function machineCliIdForRoot(root, defaultRoot) {
|
|
86
|
+
const resolved = path.resolve(root);
|
|
87
|
+
if (resolved === path.resolve(defaultRoot))
|
|
88
|
+
return generateCliId();
|
|
89
|
+
const hostname = node_os_1.default.hostname();
|
|
90
|
+
const username = node_os_1.default.userInfo().username || 'unknown';
|
|
91
|
+
const digest = (0, node_crypto_1.createHash)('sha256')
|
|
92
|
+
.update(`${hostname}|${username}|cli|root:${resolved}`)
|
|
93
|
+
.digest('hex');
|
|
94
|
+
return `cli_${digest.slice(0, 12)}`;
|
|
95
|
+
}
|
|
@@ -75,7 +75,6 @@ const auth_core_init_1 = require("./auth-core-init");
|
|
|
75
75
|
const config_1 = require("./config");
|
|
76
76
|
const cli_id_1 = require("./cli-id");
|
|
77
77
|
const cred_envelope_1 = require("./cred-envelope");
|
|
78
|
-
const device_id_1 = require("./device-id");
|
|
79
78
|
const first_party_harness_1 = require("./first-party-harness");
|
|
80
79
|
const first_party_harness_agent_1 = require("./first-party-harness-agent");
|
|
81
80
|
const first_party_harness_managed_1 = require("./first-party-harness-managed");
|
|
@@ -862,8 +861,9 @@ function hasLocalCredential() {
|
|
|
862
861
|
*
|
|
863
862
|
* The id login and refresh present, read through `resolveCliId` (a named
|
|
864
863
|
* profile may have its own, OSK-12156); regenerated deterministically as the
|
|
865
|
-
* machine id when nothing is stored (same
|
|
866
|
-
*
|
|
864
|
+
* machine id of THIS config root when nothing is stored (the same host, user
|
|
865
|
+
* and root always yield the same value; a relocated root does not share the
|
|
866
|
+
* primary root's id, OSK-12179). Normalized to the `cli_` namespace the server requires,
|
|
867
867
|
* because `OVERSKY_CLI_ID` is overlaid verbatim with no format check and a
|
|
868
868
|
* hand-set value would otherwise come back as an opaque
|
|
869
869
|
* `runtime-id-namespace-mismatch`.
|
|
@@ -879,7 +879,7 @@ function localRuntimeId() {
|
|
|
879
879
|
})();
|
|
880
880
|
if (configured?.startsWith('cli_'))
|
|
881
881
|
return configured;
|
|
882
|
-
return (0,
|
|
882
|
+
return (0, config_1.machineCliId)();
|
|
883
883
|
}
|
|
884
884
|
function errorCodeOf(err) {
|
|
885
885
|
const code = err?.code;
|
|
@@ -329,14 +329,18 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
329
329
|
};
|
|
330
330
|
// The paths go in the MESSAGE, not only the metadata: a log pipeline that
|
|
331
331
|
// drops structured fields would otherwise render this as an unactionable
|
|
332
|
-
// noun. Naming the remedy matters too
|
|
333
|
-
//
|
|
332
|
+
// noun. Naming the remedy matters too, and it has to be one that converges
|
|
333
|
+
// (OSK-12172). Only the CLI's logout deletes a key, and only the canonical
|
|
334
|
+
// one; a daemon logout deletes neither. So the CLI logs out, the daemon
|
|
335
|
+
// BINARY restarts and adopts its legacy key into the empty canonical slot,
|
|
336
|
+
// and the CLI logs back in on it. Not `skrr daemon restart`: any `skrr`
|
|
337
|
+
// command mints a fresh key into an empty slot before doing anything else.
|
|
334
338
|
(0, runtime_js_1.getAuthLogger)().warn(`[credEnvelope] profile "${profile}" has two different usable wrapped DEKs; ` +
|
|
335
339
|
`refusing to choose. Keeping ${_pathConflict.legacyPath} and leaving ` +
|
|
336
340
|
`${_pathConflict.canonicalPath} untouched. Each side can still read its own ` +
|
|
337
|
-
"credentials, and neither can read the other's.
|
|
338
|
-
|
|
339
|
-
'
|
|
341
|
+
"credentials, and neither can read the other's. Do not delete " +
|
|
342
|
+
`${_pathConflict.legacyPath}: it is the key the daemon reads. To converge on it, ` +
|
|
343
|
+
'run `skrr logout && skrrd restart && skrr login`.');
|
|
340
344
|
}
|
|
341
345
|
/**
|
|
342
346
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
@@ -47,7 +47,7 @@ import lockfile from 'proper-lockfile';
|
|
|
47
47
|
import { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, isEnvelopeString as isCredEnvelopeString, generateDek, serialize as credEnvelopeSerialize, } from './credentialEnvelope.js';
|
|
48
48
|
import { getKekStrategy, zeroizeKekCaches } from './kek/index.js';
|
|
49
49
|
import { consolidateProfileState, profileStateDir, profileStateFilePathsToClear, resolveProfileStateFile, } from './profileStateDir.js';
|
|
50
|
-
import { emitAuthTelemetry,
|
|
50
|
+
import { emitAuthTelemetry, getAuthLogger, getCredEnvelopeFailClosed } from './runtime.js';
|
|
51
51
|
/**
|
|
52
52
|
* Account name the wrapped DEK lives under (file fallback). Keeping the
|
|
53
53
|
* literal here so a future rename surfaces as a single grep target.
|
|
@@ -309,14 +309,18 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
309
309
|
};
|
|
310
310
|
// The paths go in the MESSAGE, not only the metadata: a log pipeline that
|
|
311
311
|
// drops structured fields would otherwise render this as an unactionable
|
|
312
|
-
// noun. Naming the remedy matters too
|
|
313
|
-
//
|
|
312
|
+
// noun. Naming the remedy matters too, and it has to be one that converges
|
|
313
|
+
// (OSK-12172). Only the CLI's logout deletes a key, and only the canonical
|
|
314
|
+
// one; a daemon logout deletes neither. So the CLI logs out, the daemon
|
|
315
|
+
// BINARY restarts and adopts its legacy key into the empty canonical slot,
|
|
316
|
+
// and the CLI logs back in on it. Not `skrr daemon restart`: any `skrr`
|
|
317
|
+
// command mints a fresh key into an empty slot before doing anything else.
|
|
314
318
|
getAuthLogger().warn(`[credEnvelope] profile "${profile}" has two different usable wrapped DEKs; ` +
|
|
315
319
|
`refusing to choose. Keeping ${_pathConflict.legacyPath} and leaving ` +
|
|
316
320
|
`${_pathConflict.canonicalPath} untouched. Each side can still read its own ` +
|
|
317
|
-
"credentials, and neither can read the other's.
|
|
318
|
-
|
|
319
|
-
'
|
|
321
|
+
"credentials, and neither can read the other's. Do not delete " +
|
|
322
|
+
`${_pathConflict.legacyPath}: it is the key the daemon reads. To converge on it, ` +
|
|
323
|
+
'run `skrr logout && skrrd restart && skrr login`.');
|
|
320
324
|
}
|
|
321
325
|
/**
|
|
322
326
|
* Initialize the envelope path. Idempotent — second call when state is
|