@skrr-ai/cli 0.1.51 → 0.1.53
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/base-command.d.ts +7 -0
- package/dist/base-command.js +38 -8
- package/dist/commands/agents/chat.js +2 -1
- package/dist/commands/agents/create.js +21 -8
- package/dist/commands/agents/update.d.ts +55 -0
- package/dist/commands/agents/update.js +119 -23
- package/dist/commands/commitments/next-tick.js +9 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.js +16 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/tasks/approvals/list.d.ts +22 -0
- package/dist/commands/tasks/approvals/list.js +80 -3
- package/dist/commands/tasks/create.d.ts +5 -0
- package/dist/commands/tasks/create.js +28 -7
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.js +30 -3
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +15 -0
- package/dist/lib/agent-config.js +46 -10
- package/dist/lib/agentic-stream.d.ts +21 -1
- package/dist/lib/agentic-stream.js +40 -6
- package/dist/lib/auth-failure.js +4 -1
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +348 -13
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +4 -0
- package/dist/lib/commitments.js +12 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/cred-envelope.d.ts +12 -1
- package/dist/lib/cred-envelope.js +20 -3
- package/dist/lib/daemonBroker.d.ts +26 -4
- package/dist/lib/daemonBroker.js +82 -10
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +53 -4
- package/dist/lib/dedicated-machines.js +95 -5
- package/dist/lib/delegated-cli.js +16 -0
- package/dist/lib/first-party-harness-broker.js +6 -4
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +254 -40
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +72 -1
- package/dist/lib/login.js +24 -12
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.js +33 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +9 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3138 -3100
- package/oclif.manifest.json +26470 -26449
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.profileScopedCliId = profileScopedCliId;
|
|
40
|
+
exports.hasStoredCredential = hasStoredCredential;
|
|
41
|
+
exports.planLoginCliId = planLoginCliId;
|
|
42
|
+
exports.cliIdForLogin = cliIdForLogin;
|
|
43
|
+
exports.persistPresentedCliId = persistPresentedCliId;
|
|
44
|
+
/**
|
|
45
|
+
* cli-identity.ts — which cliId a LOGIN presents, and when a profile gets its own.
|
|
46
|
+
*
|
|
47
|
+
* Reading the identity is `resolveCliId()` in `config.ts` (it has to live there:
|
|
48
|
+
* `auth-storage.ts` judges credential ownership by it, and this module reads
|
|
49
|
+
* auth-storage). This module owns the one decision a read must never make —
|
|
50
|
+
* issuing an identity — and the one write that restores a backed-up pair.
|
|
51
|
+
*
|
|
52
|
+
* Why a profile needs its own (OSK-12156): the server's `cli-handoff/confirm`
|
|
53
|
+
* retires every OTHER live cli family of (user, cliId) once a hand-off lands.
|
|
54
|
+
* That is right for one config replacing its own previous session, and wrong
|
|
55
|
+
* when every profile on the machine presents the same machine-scoped `cliId`:
|
|
56
|
+
* profile B's daemon-brokered login then revoked profile A's session, silently.
|
|
57
|
+
* The server scoping is already per cliId, so the fix is the id, not the route.
|
|
58
|
+
*
|
|
59
|
+
* Why only a profile with NO stored credential: an existing session was minted
|
|
60
|
+
* under the machine id, and the refresh route refuses a rotation that presents
|
|
61
|
+
* another. Re-identifying such a profile would log it out on its next refresh —
|
|
62
|
+
* and a re-login under a new id would leave its old family live (the confirm
|
|
63
|
+
* only sweeps the id it was minted for) in a bucket the server evicts
|
|
64
|
+
* oldest-first. So a profile keeps presenting the id its credential is bound
|
|
65
|
+
* to, and only a profile starting from nothing is issued one. That is also why
|
|
66
|
+
* an upgrade forces no re-login: nothing that already works changes id.
|
|
67
|
+
*
|
|
68
|
+
* The default profile keeps the machine id. It IS the machine file, and it is
|
|
69
|
+
* the identity every pre-profile install already presents.
|
|
70
|
+
*/
|
|
71
|
+
const node_crypto_1 = require("node:crypto");
|
|
72
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
73
|
+
const path = __importStar(require("node:path"));
|
|
74
|
+
const auth_storage_1 = require("./auth-storage");
|
|
75
|
+
const config_1 = require("./config");
|
|
76
|
+
const cli_id_1 = require("./cli-id");
|
|
77
|
+
const device_id_1 = require("./device-id");
|
|
78
|
+
const keychain_1 = require("./keychain");
|
|
79
|
+
/**
|
|
80
|
+
* A named profile's own identity. Deterministic in (host, user, config root,
|
|
81
|
+
* profile), so a profile that loses its config file and signs in again is
|
|
82
|
+
* issued the same id rather than scattering families, and two profiles — or the
|
|
83
|
+
* same profile name under two config roots — never share one. Same `cli_<12hex>`
|
|
84
|
+
* shape as the machine id: the server requires the `cli_` namespace.
|
|
85
|
+
*/
|
|
86
|
+
function profileScopedCliId(profile = (0, keychain_1.getActiveProfile)(), root = (0, config_1.configRoot)()) {
|
|
87
|
+
const hostname = node_os_1.default.hostname();
|
|
88
|
+
const username = node_os_1.default.userInfo().username || 'unknown';
|
|
89
|
+
const digest = (0, node_crypto_1.createHash)('sha256')
|
|
90
|
+
.update(`${hostname}|${username}|cli|profile:${profile}|root:${path.resolve(root)}`)
|
|
91
|
+
.digest('hex');
|
|
92
|
+
return `cli_${digest.slice(0, 12)}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Does this profile hold a credential it would present? Its own backend slot
|
|
96
|
+
* (ownership-checked, any origin) or a pre-migration token still in the config.
|
|
97
|
+
*
|
|
98
|
+
* A read that FAILS answers true: "keep the id you already present" is the
|
|
99
|
+
* outcome that cannot log anyone out, so doubt falls on that side.
|
|
100
|
+
*/
|
|
101
|
+
function hasStoredCredential(cfg) {
|
|
102
|
+
if (cfg.token || cfg.refreshToken)
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const read = (0, auth_storage_1.readFromBackend)();
|
|
106
|
+
return Boolean(read.bundle?.token || read.bundle?.refreshToken);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Decide the id a login is about to present. Pure apart from the credential
|
|
114
|
+
* read; the caller persists `updatedConfig` (see `cliIdForLogin`).
|
|
115
|
+
*/
|
|
116
|
+
function planLoginCliId(cfg, deps = {}) {
|
|
117
|
+
const env = (0, cli_id_1.envCliIdOverride)();
|
|
118
|
+
if (env)
|
|
119
|
+
return { cliId: env, source: 'env' };
|
|
120
|
+
const issued = cfg.profileCliId?.trim();
|
|
121
|
+
if (issued)
|
|
122
|
+
return { cliId: issued, source: 'profile' };
|
|
123
|
+
if ((0, cli_id_1.isNamedProfile)() && !(deps.hasStoredCredential ?? hasStoredCredential)(cfg)) {
|
|
124
|
+
const cliId = profileScopedCliId();
|
|
125
|
+
return { cliId, source: 'issued', updatedConfig: { ...cfg, profileCliId: cliId } };
|
|
126
|
+
}
|
|
127
|
+
const machine = cfg.cliId?.trim();
|
|
128
|
+
if (machine)
|
|
129
|
+
return { cliId: machine, source: 'machine' };
|
|
130
|
+
const cliId = (0, device_id_1.generateCliId)();
|
|
131
|
+
return { cliId, source: 'machine', updatedConfig: { ...cfg, cliId } };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The id an interactive login presents, persisted BEFORE the exchange so a
|
|
135
|
+
* retry — or a sibling process — presents the same one.
|
|
136
|
+
*/
|
|
137
|
+
function cliIdForLogin(cfg = (0, config_1.loadConfig)()) {
|
|
138
|
+
const plan = planLoginCliId(cfg);
|
|
139
|
+
if (plan.updatedConfig)
|
|
140
|
+
(0, config_1.saveConfig)(plan.updatedConfig);
|
|
141
|
+
return plan.cliId;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Record that this profile now presents `cliId` — a restored (refresh token,
|
|
145
|
+
* cliId) pair. A named profile records it as its own identity; writing the
|
|
146
|
+
* machine field from a named profile would re-identify the default profile and
|
|
147
|
+
* every profile still on the machine id, whose next refresh would be refused.
|
|
148
|
+
*/
|
|
149
|
+
function persistPresentedCliId(cfg, cliId) {
|
|
150
|
+
if ((0, cli_id_1.isNamedProfile)()) {
|
|
151
|
+
const current = cfg.profileCliId?.trim() || cfg.cliId?.trim();
|
|
152
|
+
if (current === cliId)
|
|
153
|
+
return;
|
|
154
|
+
(0, config_1.saveConfig)({ ...cfg, profileCliId: cliId });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (cfg.cliId !== cliId)
|
|
158
|
+
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
159
|
+
}
|
|
@@ -486,6 +486,10 @@ export interface CommitmentNextTickProjection {
|
|
|
486
486
|
diagnosisKind?: string;
|
|
487
487
|
terminalDispositionCensus?: {
|
|
488
488
|
workFailures?: number;
|
|
489
|
+
/** Failed runs the runtime never started (`classifyRunFailure` machine). */
|
|
490
|
+
machineFailures?: number;
|
|
491
|
+
/** Failed runs whose result was rejected (`classifyRunFailure` contract). */
|
|
492
|
+
contractFailures?: number;
|
|
489
493
|
budgetRefusals?: number;
|
|
490
494
|
terminal?: number;
|
|
491
495
|
};
|
package/dist/lib/commitments.js
CHANGED
|
@@ -655,6 +655,8 @@ function describeEvidenceHealth(health, criteriaCount) {
|
|
|
655
655
|
const STALLED_DIAGNOSES = {
|
|
656
656
|
scheduler_stalled: 'its expected checks are no longer arriving',
|
|
657
657
|
work_failing: 'every recent run reached the agent and failed while doing the work',
|
|
658
|
+
runtime_failing: 'recent runs never started — the runtime refused them',
|
|
659
|
+
result_rejected: 'every recent run reached the agent, but its result was rejected',
|
|
658
660
|
observation_unreadable: 'the latest check could not read every configured source',
|
|
659
661
|
execution_budget_exhausted: 'recent planned work was skipped — the execution budget is exhausted',
|
|
660
662
|
no_advancement: 'no server-recorded advancement exists inside the declared window',
|
|
@@ -674,8 +676,17 @@ function describeStalledHealth(commitment) {
|
|
|
674
676
|
const kind = typeof liveness.diagnosisKind === 'string' ? liveness.diagnosisKind : undefined;
|
|
675
677
|
const clause = (kind && STALLED_DIAGNOSES[kind]) ||
|
|
676
678
|
'this loop has stopped producing; the reconciler did not record which check failed';
|
|
679
|
+
// What refused and what to change, when the reconciler recorded them
|
|
680
|
+
// (OSK-12137). A machine failure has a specific fix; printing only the
|
|
681
|
+
// verdict would send the owner to read run history for it.
|
|
682
|
+
const code = typeof liveness.diagnosisCode === 'string' && liveness.diagnosisCode
|
|
683
|
+
? ` (${liveness.diagnosisCode})`
|
|
684
|
+
: '';
|
|
677
685
|
const since = typeof liveness.unhealthySinceAt === 'string' ? ` (since ${liveness.unhealthySinceAt})` : '';
|
|
678
|
-
|
|
686
|
+
const fix = typeof liveness.diagnosisFix === 'string' && liveness.diagnosisFix
|
|
687
|
+
? ` — ${liveness.diagnosisFix}`
|
|
688
|
+
: '';
|
|
689
|
+
return `${clause}${code}${since}${fix}`;
|
|
679
690
|
}
|
|
680
691
|
/** Render an interval back as a compact human string. */
|
|
681
692
|
function describeIntervalMs(ms) {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -29,6 +29,23 @@ export interface CliConfig {
|
|
|
29
29
|
* different daemonId.
|
|
30
30
|
*/
|
|
31
31
|
cliId?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The CLI identity of ONE named profile (OSK-12156), stored in that profile's
|
|
34
|
+
* own file and never in the machine file.
|
|
35
|
+
*
|
|
36
|
+
* `cliId` above is machine-scoped, so every profile used to present it — and
|
|
37
|
+
* the server's `cli-handoff/confirm` retires every OTHER live family of
|
|
38
|
+
* (user, cliId), so one profile's daemon-brokered login silently ended
|
|
39
|
+
* another's session. This is issued ONLY to a named profile that holds no
|
|
40
|
+
* stored credential at the moment it signs in (`cli-identity.ts`), because
|
|
41
|
+
* an existing session was minted under the machine id and a refresh that
|
|
42
|
+
* presents another is refused: a profile that already has a session keeps
|
|
43
|
+
* presenting the id it was minted under, so an upgrade forces no re-login.
|
|
44
|
+
*
|
|
45
|
+
* Never read this field, or `cliId`, to decide what to PRESENT — call
|
|
46
|
+
* `resolveCliId()` (`cli-id.ts`), the one resolver every caller goes through.
|
|
47
|
+
*/
|
|
48
|
+
profileCliId?: string;
|
|
32
49
|
/**
|
|
33
50
|
* OSK-3892 — the agent a locally-hosted `skrr code` session is attributed to.
|
|
34
51
|
*
|
package/dist/lib/config.js
CHANGED
|
@@ -53,6 +53,7 @@ const os = __importStar(require("node:os"));
|
|
|
53
53
|
const path = __importStar(require("node:path"));
|
|
54
54
|
const keychain_1 = require("./keychain");
|
|
55
55
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
56
|
+
const cli_id_1 = require("./cli-id");
|
|
56
57
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
57
58
|
// Both from config/public-endpoints.json, the one domain contract, rather than a
|
|
58
59
|
// fourth hand-copied pair. The CLI previously knew only about dev — there was no
|
|
@@ -174,7 +175,8 @@ const ENV_VAR = {
|
|
|
174
175
|
serverUrl: 'OVERSKY_SERVER_URL',
|
|
175
176
|
token: 'OVERSKY_TOKEN',
|
|
176
177
|
refreshToken: 'OVERSKY_REFRESH_TOKEN',
|
|
177
|
-
|
|
178
|
+
// OVERSKY_CLI_ID / SKRR_CLI_ID: `envCliIdOverride` (cli-id.ts), which
|
|
179
|
+
// `resolveCliId` shares, so the overlay and the resolver cannot disagree.
|
|
178
180
|
};
|
|
179
181
|
/**
|
|
180
182
|
* Read one overlay variable under either prefix.
|
|
@@ -215,8 +217,8 @@ function overlayEnv(cfg) {
|
|
|
215
217
|
const rt = envOverlayValue('refreshToken');
|
|
216
218
|
if (rt && rt.length > 0)
|
|
217
219
|
out.refreshToken = rt;
|
|
218
|
-
const cid =
|
|
219
|
-
if (cid
|
|
220
|
+
const cid = (0, cli_id_1.envCliIdOverride)();
|
|
221
|
+
if (cid)
|
|
220
222
|
out.cliId = cid;
|
|
221
223
|
return out;
|
|
222
224
|
}
|
|
@@ -275,6 +277,16 @@ function newConfigDir() {
|
|
|
275
277
|
* and rejects a rotation against a different one. Split it per profile and a
|
|
276
278
|
* second profile does not degrade — it fails to stay logged in.
|
|
277
279
|
*
|
|
280
|
+
* So `cliId` stays here, and a profile's OWN identity is a different key,
|
|
281
|
+
* `profileCliId`, which is profile-scoped (OSK-12156). Sharing the machine id
|
|
282
|
+
* meant the server's cli-handoff confirm — which retires the other families of
|
|
283
|
+
* (user, cliId) — could not tell two profiles of one user apart, so one
|
|
284
|
+
* profile's daemon-brokered login ended another's session. A named profile that
|
|
285
|
+
* signs in holding no credential is now issued its own id; a profile that
|
|
286
|
+
* already has a session keeps the machine id it was minted under, because a
|
|
287
|
+
* refresh presenting another is refused. `resolveCliId()` (`cli-id.ts`) is the
|
|
288
|
+
* one reader.
|
|
289
|
+
*
|
|
278
290
|
* `credentials.store` is a machine posture (keychain / file / env-only), and
|
|
279
291
|
* `migrated` is a breadcrumb about this machine's DISK LAYOUT. Neither is a
|
|
280
292
|
* property of which deployment you are addressing.
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
* and file backends and avoids a detect-then-read TOCTOU race.
|
|
9
9
|
*/
|
|
10
10
|
import { type ReadTransformResult } from '@skrr-ai/auth-core';
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
13
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
14
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
15
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
16
|
+
*/
|
|
17
|
+
export declare function foreignCredentialHint(ownerConfigRoot: string): string;
|
|
18
|
+
export interface CliDecryptOnReadOptions {
|
|
19
|
+
/** The config root the stored value's owner record names, when not ours. */
|
|
20
|
+
foreignOwnerRoot?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function maybeDecryptOnRead(stored: string, options?: CliDecryptOnReadOptions): ReadTransformResult;
|
|
12
23
|
/** Consume and clear the per-process blocked-read observation. */
|
|
13
24
|
export declare function consumeEncryptedCredentialReadWhileInactive(): boolean;
|
|
14
25
|
/** Consume any unreadable envelope observation, including tamper/decrypt failure. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.__wrappedDekFilePathForTest = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = void 0;
|
|
4
|
+
exports.foreignCredentialHint = foreignCredentialHint;
|
|
4
5
|
exports.maybeDecryptOnRead = maybeDecryptOnRead;
|
|
5
6
|
exports.consumeEncryptedCredentialReadWhileInactive = consumeEncryptedCredentialReadWhileInactive;
|
|
6
7
|
exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCredentialRead;
|
|
@@ -16,10 +17,26 @@ exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCre
|
|
|
16
17
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
17
18
|
let encryptedReadWhileInactive = false;
|
|
18
19
|
let unreadableEncryptedRead = false;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
22
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
23
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
24
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
25
|
+
*/
|
|
26
|
+
function foreignCredentialHint(ownerConfigRoot) {
|
|
27
|
+
return (`stored credential belongs to another skrr config (config root ${ownerConfigRoot}) and is ` +
|
|
28
|
+
"sealed under that config's key, so this config cannot read it. Nothing is wrong with " +
|
|
29
|
+
'either credential and it was left untouched; signing in here does not change it');
|
|
30
|
+
}
|
|
31
|
+
function maybeDecryptOnRead(stored, options = {}) {
|
|
32
|
+
const result = (0, auth_core_1.maybeDecryptOnRead)(stored, options.foreignOwnerRoot
|
|
33
|
+
? { unreadableHint: foreignCredentialHint(options.foreignOwnerRoot) }
|
|
34
|
+
: {});
|
|
21
35
|
if (result.plaintext === null && (0, auth_core_1.isCredEnvelopeString)(stored)) {
|
|
22
|
-
|
|
36
|
+
// Another config's value is not an unreadable credential of OURS, so it
|
|
37
|
+
// must not tell `logout` there is local encrypted state to erase.
|
|
38
|
+
if (!options.foreignOwnerRoot)
|
|
39
|
+
unreadableEncryptedRead = true;
|
|
23
40
|
if ((0, auth_core_1.describeCredEnvelopeState)().kind !== 'active') {
|
|
24
41
|
encryptedReadWhileInactive = true;
|
|
25
42
|
}
|
|
@@ -122,7 +122,7 @@ export interface DaemonBrokeredOutcome {
|
|
|
122
122
|
* fall through to PKCE; `ci_token_refused` is the one we surface
|
|
123
123
|
* because re-driving it via PKCE would not change the outcome.
|
|
124
124
|
*/
|
|
125
|
-
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'handoff_broker_only' | 'rate_limited' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
125
|
+
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'handoff_broker_only' | 'rate_limited' | 'upstream_refused' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
126
126
|
export interface DaemonBrokerFailure {
|
|
127
127
|
ok: false;
|
|
128
128
|
reason: DaemonBrokerErrorReason;
|
|
@@ -132,6 +132,16 @@ export interface DaemonBrokerFailure {
|
|
|
132
132
|
status?: number;
|
|
133
133
|
/** Server-supplied error code when present (e.g. 'CI_TOKEN_REFUSED'). */
|
|
134
134
|
code?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Who refused, when the daemon answered at all: `daemon` for a refusal the
|
|
137
|
+
* local service made itself (not signed in, broker-only, its own outage),
|
|
138
|
+
* `server` for the skrr server's refusal of the daemon's request, carried
|
|
139
|
+
* through the loopback route (`upstreamCode` on the token route, the
|
|
140
|
+
* verbatim body on the legacy one). The two have different repairs, and a
|
|
141
|
+
* caller that cannot tell them apart can only say "Not signed in"
|
|
142
|
+
* (OSK-12149).
|
|
143
|
+
*/
|
|
144
|
+
refusedBy?: 'daemon' | 'server';
|
|
135
145
|
/**
|
|
136
146
|
* For `base_url_mismatch`: the daemon's serverUrl, so the caller can
|
|
137
147
|
* surface a helpful hint like "run `skrr login --base-url <X>` to use
|
|
@@ -139,11 +149,23 @@ export interface DaemonBrokerFailure {
|
|
|
139
149
|
*/
|
|
140
150
|
daemonServerUrl?: string;
|
|
141
151
|
/**
|
|
142
|
-
*
|
|
143
|
-
* `daemonServerUrl`, so a message can say
|
|
144
|
-
* where "the" daemon is logged in
|
|
152
|
+
* The descriptor file the outcome came from. For `base_url_mismatch` found
|
|
153
|
+
* locally it is the file that named `daemonServerUrl`, so a message can say
|
|
154
|
+
* what was read rather than assert where "the" daemon is logged in
|
|
155
|
+
* (OSK-12117). For a refusal it is the descriptor whose daemon answered —
|
|
156
|
+
* which is how a caller knows the answer came from a Dedicated Runtime
|
|
157
|
+
* guest's daemon (`DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR`), where nobody
|
|
158
|
+
* signs in and the repair is the machine, not a login.
|
|
145
159
|
*/
|
|
146
160
|
descriptorPath?: string;
|
|
161
|
+
/** The answering daemon's id, from its descriptor, when it published one. */
|
|
162
|
+
daemonId?: string;
|
|
163
|
+
/**
|
|
164
|
+
* True when the answering descriptor is a Dedicated Runtime guest's
|
|
165
|
+
* (`DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR`): nobody signs in there, so
|
|
166
|
+
* the repair for a refusal is the machine, never a login.
|
|
167
|
+
*/
|
|
168
|
+
dedicatedGuest?: boolean;
|
|
147
169
|
}
|
|
148
170
|
export type DaemonBrokerOutcome = (DaemonBrokerResult & {
|
|
149
171
|
ok: true;
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -96,7 +96,7 @@ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
|
96
96
|
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
97
97
|
const auth_storage_1 = require("./auth-storage");
|
|
98
98
|
const config_1 = require("./config");
|
|
99
|
-
const
|
|
99
|
+
const cli_identity_1 = require("./cli-identity");
|
|
100
100
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
101
101
|
/**
|
|
102
102
|
* Normalize a base URL for equality comparison. Mirrors the daemon-side
|
|
@@ -120,6 +120,19 @@ function normalizeBaseUrl(u) {
|
|
|
120
120
|
return (0, config_1.canonicalizeKnownBaseURL)(trimmed.toLowerCase());
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Codes the daemon's legacy `/v1/auth/cli-handoff` route writes itself. Every
|
|
125
|
+
* other coded body on that route is the server's, passed through verbatim.
|
|
126
|
+
*/
|
|
127
|
+
const DAEMON_OWN_HANDOFF_CODES = new Set([
|
|
128
|
+
cli_handoff_wire_1.CLI_HANDOFF_ERROR_BROKER_ONLY,
|
|
129
|
+
'BODY_TOO_LARGE',
|
|
130
|
+
'BODY_PARSE_FAILED',
|
|
131
|
+
'CLI_ID_INVALID',
|
|
132
|
+
'BASE_URL_MISMATCH',
|
|
133
|
+
'DAEMON_NOT_AUTHED',
|
|
134
|
+
'UPSTREAM_UNREACHABLE',
|
|
135
|
+
]);
|
|
123
136
|
/**
|
|
124
137
|
* Compute the bootstrap file path. Exported for tests so they can assert
|
|
125
138
|
* the platform fork without monkeypatching `os.platform()`.
|
|
@@ -394,6 +407,17 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
394
407
|
let outcome = null;
|
|
395
408
|
for (const { bootstrap, sourcePath } of usable) {
|
|
396
409
|
outcome = await brokerThrough(bootstrap, opts, sourcePath);
|
|
410
|
+
if (!outcome.ok) {
|
|
411
|
+
// Say which daemon answered: a guest's refusal has a different repair.
|
|
412
|
+
outcome = {
|
|
413
|
+
...outcome,
|
|
414
|
+
descriptorPath: outcome.descriptorPath ?? sourcePath,
|
|
415
|
+
...(bootstrap.daemonId ? { daemonId: bootstrap.daemonId } : {}),
|
|
416
|
+
...(sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR
|
|
417
|
+
? { dedicatedGuest: true }
|
|
418
|
+
: {}),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
397
421
|
if (outcome.ok || outcome.reason !== 'network')
|
|
398
422
|
return outcome;
|
|
399
423
|
}
|
|
@@ -448,6 +472,13 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
|
448
472
|
// 403 HANDOFF_BROKER_ONLY anyway.
|
|
449
473
|
if (!offersRefreshFamily)
|
|
450
474
|
return redeem;
|
|
475
|
+
const minted = await mintRefreshFamilyThrough(bootstrap, opts);
|
|
476
|
+
// Both doors refused: report the one that says more. The legacy route
|
|
477
|
+
// usually carries the server's code itself, but an older daemon's token
|
|
478
|
+
// route may be the only one that named it.
|
|
479
|
+
if (!minted.ok && !minted.code && redeem.code && redeem.reason !== 'network')
|
|
480
|
+
return redeem;
|
|
481
|
+
return minted;
|
|
451
482
|
}
|
|
452
483
|
return mintRefreshFamilyThrough(bootstrap, opts);
|
|
453
484
|
}
|
|
@@ -517,6 +548,28 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
|
|
|
517
548
|
...(code ? { code } : {}),
|
|
518
549
|
};
|
|
519
550
|
}
|
|
551
|
+
// A daemon that names the SERVER's refusal of its own mint carries
|
|
552
|
+
// `upstreamCode` / `upstreamStatus` beside the 503 (older daemons omit them). Surface the
|
|
553
|
+
// server's code — `DEVICE_PROOF_PIN_MISMATCH` has a specific repair that
|
|
554
|
+
// `HANDOFF_TOKEN_UNAVAILABLE` cannot point at (OSK-12149).
|
|
555
|
+
const upstreamCode = typeof parsedBody?.upstreamCode === 'string'
|
|
556
|
+
? parsedBody.upstreamCode
|
|
557
|
+
: undefined;
|
|
558
|
+
const upstreamStatus = typeof parsedBody?.upstreamStatus === 'number'
|
|
559
|
+
? parsedBody.upstreamStatus
|
|
560
|
+
: undefined;
|
|
561
|
+
if (upstreamCode || upstreamStatus !== undefined) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
reason: 'upstream_refused',
|
|
565
|
+
refusedBy: 'server',
|
|
566
|
+
detail: message,
|
|
567
|
+
...(upstreamStatus !== undefined
|
|
568
|
+
? { status: upstreamStatus }
|
|
569
|
+
: { status: response.status }),
|
|
570
|
+
...(upstreamCode ? { code: upstreamCode } : code ? { code } : {}),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
520
573
|
return {
|
|
521
574
|
ok: false,
|
|
522
575
|
// 503 HANDOFF_TOKEN_UNAVAILABLE lands in server_error with its code
|
|
@@ -524,6 +577,7 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
|
|
|
524
577
|
// upstream unreachable, mint cool-down). That is a daemon outage, not a
|
|
525
578
|
// decline, and the code keeps it diagnosable.
|
|
526
579
|
reason: response.status >= 500 ? 'server_error' : 'unknown',
|
|
580
|
+
refusedBy: 'daemon',
|
|
527
581
|
detail: message,
|
|
528
582
|
status: response.status,
|
|
529
583
|
...(code ? { code } : {}),
|
|
@@ -610,7 +664,14 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
|
|
|
610
664
|
};
|
|
611
665
|
}
|
|
612
666
|
if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
|
|
613
|
-
return {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
reason: 'daemon_not_authed',
|
|
670
|
+
refusedBy: 'daemon',
|
|
671
|
+
detail: message,
|
|
672
|
+
status: 503,
|
|
673
|
+
code,
|
|
674
|
+
};
|
|
614
675
|
}
|
|
615
676
|
// 429 is TRANSIENT and its remedy is time, which makes it the one failure
|
|
616
677
|
// here that a fall-through cannot substitute for. Left in the `unknown`
|
|
@@ -644,12 +705,26 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
|
|
|
644
705
|
...(daemonServerUrl ? { daemonServerUrl } : {}),
|
|
645
706
|
};
|
|
646
707
|
}
|
|
708
|
+
// A coded body the daemon did not write is the server's refusal of the
|
|
709
|
+
// daemon's mint, passed through verbatim (`cliHandoff.ts`). A 4xx one —
|
|
710
|
+
// observed: 401 DEVICE_PROOF_PIN_MISMATCH — is a decision with a named
|
|
711
|
+
// repair, and used to land in `unknown`, where no caller could classify it.
|
|
712
|
+
const refusedBy = code
|
|
713
|
+
? DAEMON_OWN_HANDOFF_CODES.has(code)
|
|
714
|
+
? 'daemon'
|
|
715
|
+
: 'server'
|
|
716
|
+
: undefined;
|
|
647
717
|
return {
|
|
648
718
|
ok: false,
|
|
649
|
-
reason: response.status >= 500
|
|
719
|
+
reason: response.status >= 500
|
|
720
|
+
? 'server_error'
|
|
721
|
+
: refusedBy === 'server'
|
|
722
|
+
? 'upstream_refused'
|
|
723
|
+
: 'unknown',
|
|
650
724
|
detail: message,
|
|
651
725
|
status: response.status,
|
|
652
726
|
...(code ? { code } : {}),
|
|
727
|
+
...(refusedBy ? { refusedBy } : {}),
|
|
653
728
|
};
|
|
654
729
|
}
|
|
655
730
|
// Happy path — validate the response shape minimally.
|
|
@@ -946,14 +1021,11 @@ async function maybeAutoBroker(opts) {
|
|
|
946
1021
|
if (opts.commandId && AUTH_SELF_MANAGED_COMMANDS.has(opts.commandId)) {
|
|
947
1022
|
return { triggered: false };
|
|
948
1023
|
}
|
|
949
|
-
//
|
|
1024
|
+
// The same decision an interactive login makes (OSK-12156): a named profile
|
|
1025
|
+
// with no stored credential is issued its own id, so a durable hand-off's
|
|
1026
|
+
// confirm cannot retire another profile's session. Persisted by the caller on
|
|
950
1027
|
// success so the rotation row stays bound across processes.
|
|
951
|
-
|
|
952
|
-
let updatedConfig;
|
|
953
|
-
if (!cliId) {
|
|
954
|
-
cliId = (0, device_id_1.generateCliId)();
|
|
955
|
-
updatedConfig = { ...opts.cliConfig, cliId };
|
|
956
|
-
}
|
|
1028
|
+
const { cliId, updatedConfig } = (0, cli_identity_1.planLoginCliId)(opts.cliConfig);
|
|
957
1029
|
const outcome = await attemptDaemonBrokerLoginAndPersist({
|
|
958
1030
|
cliId,
|
|
959
1031
|
baseURL: opts.cliConfig.baseURL,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to tell a person whose local daemon ANSWERED the CLI hand-off and
|
|
3
|
+
* refused it (OSK-12149, OSK-12151).
|
|
4
|
+
*
|
|
5
|
+
* The auto-broker used to be "fail-quiet": every refusal except a rate limit
|
|
6
|
+
* and a base-URL mismatch was dropped, and the command then said only "Not
|
|
7
|
+
* signed in. Run `skrr login` first." That sentence is true and points at the
|
|
8
|
+
* wrong repair whenever the daemon's refusal is specific — observed: the server
|
|
9
|
+
* refused the daemon's own mint with `401 DEVICE_PROOF_PIN_MISMATCH`, which
|
|
10
|
+
* `skrr daemon login` repairs (an interactive daemon login re-pins its device
|
|
11
|
+
* key), and which on a Dedicated Runtime guest no login the tenant can run
|
|
12
|
+
* repairs at all.
|
|
13
|
+
*
|
|
14
|
+
* One function decides the headline and the remedy so the NOT_SIGNED_IN error,
|
|
15
|
+
* `skrr login`'s progress line and its headless refusal cannot drift apart.
|
|
16
|
+
*/
|
|
17
|
+
import type { DaemonBrokerFailure } from './daemonBroker';
|
|
18
|
+
/**
|
|
19
|
+
* True when a daemon ANSWERED the hand-off and refused it — as opposed to no
|
|
20
|
+
* daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
|
|
21
|
+
* behind). This is the case the CLI must explain rather than reduce to "Not
|
|
22
|
+
* signed in": the person has a daemon, it said something specific, and that
|
|
23
|
+
* decides whether `skrr login` would even help.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAnsweredBrokerRefusal(outcome: DaemonBrokerFailure): boolean;
|
|
26
|
+
export interface BrokerRefusalDescription {
|
|
27
|
+
/** One sentence: who refused and what they said, with status and code. */
|
|
28
|
+
headline: string;
|
|
29
|
+
/** What to do about it — never a login the reader cannot perform. */
|
|
30
|
+
remedy: string;
|
|
31
|
+
/** True when waiting and retrying is the repair. */
|
|
32
|
+
transient: boolean;
|
|
33
|
+
/** True when the refusing daemon is a Dedicated Runtime guest's. */
|
|
34
|
+
dedicatedGuest: boolean;
|
|
35
|
+
/** Machine-readable twin for `--json` error details. */
|
|
36
|
+
broker: {
|
|
37
|
+
reason: DaemonBrokerFailure['reason'];
|
|
38
|
+
refusedBy?: 'daemon' | 'server';
|
|
39
|
+
status?: number;
|
|
40
|
+
code?: string;
|
|
41
|
+
detail?: string;
|
|
42
|
+
descriptorPath?: string;
|
|
43
|
+
daemonId?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Describe a broker failure for a person, or null when there is nothing to
|
|
48
|
+
* say: no daemon was there to ask, or the failure already has its own message
|
|
49
|
+
* (`base_url_mismatch`, `rate_limited`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function describeBrokerRefusal(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
|