@skrr-ai/cli 0.1.55 → 0.1.57
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 +19 -1
- package/dist/base-command.js +104 -7
- package/dist/commands/agents/chat.js +5 -3
- package/dist/commands/agents/create.js +7 -0
- package/dist/commands/agents/update.js +7 -0
- package/dist/commands/login.js +14 -0
- package/dist/commands/tasks/move.js +11 -0
- package/dist/lib/agent-model-check.d.ts +19 -0
- package/dist/lib/agent-model-check.js +68 -0
- package/dist/lib/agentic-stream.d.ts +22 -0
- package/dist/lib/agentic-stream.js +91 -5
- package/dist/lib/auth-core-init.d.ts +20 -0
- package/dist/lib/auth-core-init.js +72 -2
- package/dist/lib/auth-storage.d.ts +15 -0
- package/dist/lib/auth-storage.js +33 -7
- package/dist/lib/commitments.js +3 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +4 -2
- package/dist/lib/credential-resolver.d.ts +27 -0
- package/dist/lib/credential-resolver.js +19 -0
- package/dist/lib/daemonBroker.d.ts +25 -0
- package/dist/lib/daemonBroker.js +54 -15
- package/dist/lib/daemonHandoff.d.ts +1 -1
- package/dist/lib/daemonHandoff.js +21 -1
- package/dist/lib/internal-id.d.ts +28 -0
- package/dist/lib/internal-id.js +41 -0
- package/dist/lib/workflow-state-hint.d.ts +10 -0
- package/dist/lib/workflow-state-hint.js +27 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1896 -1893
- package/oclif.manifest.json +30434 -30434
- package/package.json +1 -1
|
@@ -3,8 +3,9 @@ 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.CliCredentialInitializationError = void 0;
|
|
6
|
+
exports.CliCredentialInitializationError = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
|
|
7
7
|
exports.plainCredentialEnvelopeWarning = plainCredentialEnvelopeWarning;
|
|
8
|
+
exports.createCredentialEnvelopeWarningPresenter = createCredentialEnvelopeWarningPresenter;
|
|
8
9
|
exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
|
|
9
10
|
exports.ensureAuthCoreConfigured = ensureAuthCoreConfigured;
|
|
10
11
|
exports.prefetchCliAuth = prefetchCliAuth;
|
|
@@ -75,6 +76,73 @@ function plainCredentialEnvelopeWarning(args) {
|
|
|
75
76
|
const remedy = hint === auth_core_1.DEFAULT_UNREADABLE_CREDENTIAL_HINT ? ' Run `skrr login`.' : '';
|
|
76
77
|
return `warning: ${hint.trim()}.${remedy} (DEBUG=1 shows the underlying error.)`;
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The one line an EXPECTED credential-key change prints (OSK-12186).
|
|
81
|
+
*
|
|
82
|
+
* When the key that wraps this profile's credential key changes underneath it
|
|
83
|
+
* — a Dedicated guest moved to a new image gets a new `/etc/machine-id`, which
|
|
84
|
+
* the Linux key is derived from; a new Mac or a reset Keychain does the same —
|
|
85
|
+
* auth-core cannot unwrap the old key, enrolls a new one and carries on. That
|
|
86
|
+
* is recovery, not damage, but it reached the terminal as four warnings: the
|
|
87
|
+
* unwrap failure, the rotation notice, and one decrypt failure per credential
|
|
88
|
+
* still sealed under the old key, each ending in a raw crypto string. The
|
|
89
|
+
* second run was clean, which is exactly what corruption does not look like.
|
|
90
|
+
*
|
|
91
|
+
* It is distinct from a GENUINELY unreadable credential (OSK-12180): there the
|
|
92
|
+
* key unwraps and one stored value still cannot be opened, nothing will fix it
|
|
93
|
+
* by itself, and that line names `skrr login`. Here the old values are
|
|
94
|
+
* unreadable BECAUSE the key was replaced, and the next step happens on its
|
|
95
|
+
* own when a local daemon can hand a credential over.
|
|
96
|
+
*/
|
|
97
|
+
exports.CREDENTIAL_KEY_CHANGED_NOTICE = "note: this machine's credential key changed (a new machine or image), so credentials " +
|
|
98
|
+
'saved under the old key can no longer be read. skrr gets a new one from the local daemon when ' +
|
|
99
|
+
'one is running; otherwise run `skrr login`. (DEBUG=1 shows the underlying error.)';
|
|
100
|
+
/**
|
|
101
|
+
* What the CLI prints for one auth-core warning: a replacement line, nothing
|
|
102
|
+
* (`''`), or null for "print it as auth-core wrote it".
|
|
103
|
+
*
|
|
104
|
+
* Stateful by design — the process that detected a key change has already
|
|
105
|
+
* told the person why every old credential is unreadable, so the per-value
|
|
106
|
+
* decrypt failures that follow add nothing. A fresh process starts clean, and
|
|
107
|
+
* a later genuinely unreadable credential still gets its own OSK-12180 line.
|
|
108
|
+
*/
|
|
109
|
+
/** auth-core's device-identity warning for a private key it could not open. */
|
|
110
|
+
const DEVICE_KEY_DECRYPT_FAILED = '[deviceIdentity] private_key_decrypt_failed';
|
|
111
|
+
function createCredentialEnvelopeWarningPresenter() {
|
|
112
|
+
let keyChanged = false;
|
|
113
|
+
return (args) => {
|
|
114
|
+
const [message, metadata] = args;
|
|
115
|
+
const event = typeof message === 'string' && message.startsWith('[credEnvelope] ')
|
|
116
|
+
? message.slice('[credEnvelope] '.length)
|
|
117
|
+
: null;
|
|
118
|
+
const meta = (metadata && typeof metadata === 'object' ? metadata : {});
|
|
119
|
+
// auth-core emits this and then, from the same branch, `kek_rotated`: the
|
|
120
|
+
// wrapped key was present and would not unwrap. The notice below is the
|
|
121
|
+
// one line for both.
|
|
122
|
+
if (event === 'cred_envelope.unwrap.failed' && meta.reason === 'wrapped_dek_unwrap_threw') {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
if (event === 'cred_envelope.kek_rotated') {
|
|
126
|
+
if (keyChanged)
|
|
127
|
+
return '';
|
|
128
|
+
keyChanged = true;
|
|
129
|
+
return exports.CREDENTIAL_KEY_CHANGED_NOTICE;
|
|
130
|
+
}
|
|
131
|
+
// Every value sealed under the old key fails here now — the notice has
|
|
132
|
+
// already said why. A MALFORMED envelope is a different fact and still prints.
|
|
133
|
+
if (keyChanged && event === 'cred_envelope.read.decrypt_failed')
|
|
134
|
+
return '';
|
|
135
|
+
// The device private key is one of those values: auth-core's device
|
|
136
|
+
// identity loads it next, cannot open it, and mints a new keypair. It
|
|
137
|
+
// logs that through its own `[deviceIdentity]` prefix, which reached the
|
|
138
|
+
// terminal as a raw `[auth-core warn]` line right under the notice. The
|
|
139
|
+
// notice covers it; without a key change it is a real fault and prints.
|
|
140
|
+
if (keyChanged && message === DEVICE_KEY_DECRYPT_FAILED)
|
|
141
|
+
return '';
|
|
142
|
+
return plainCredentialEnvelopeWarning(args);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const presentCredentialEnvelopeWarning = createCredentialEnvelopeWarningPresenter();
|
|
78
146
|
const cliLogger = {
|
|
79
147
|
// The CLI is a short-lived foreground process. Trace/debug get dropped
|
|
80
148
|
// unless DEBUG is set so we don't spam terminal output.
|
|
@@ -87,7 +155,9 @@ const cliLogger = {
|
|
|
87
155
|
console.error('[auth-core info]', ...args);
|
|
88
156
|
},
|
|
89
157
|
warn: (...args) => {
|
|
90
|
-
const plain = process.env.DEBUG ? null :
|
|
158
|
+
const plain = process.env.DEBUG ? null : presentCredentialEnvelopeWarning(args);
|
|
159
|
+
if (plain === '')
|
|
160
|
+
return;
|
|
91
161
|
if (plain)
|
|
92
162
|
console.error(plain);
|
|
93
163
|
else
|
|
@@ -48,6 +48,21 @@ export interface WriteOptions {
|
|
|
48
48
|
* retiring it. Everything else should keep using `readFromBackend()`.
|
|
49
49
|
*/
|
|
50
50
|
export declare function readFromFile(): AuthBundle | null;
|
|
51
|
+
/**
|
|
52
|
+
* The refresh family a stored `cli-auth.json` carries, whether or not it also
|
|
53
|
+
* carries an access token — for the Dedicated guest's retirement of a
|
|
54
|
+
* pre-broker file only (OSK-12194).
|
|
55
|
+
*
|
|
56
|
+
* `readFromFile()` treats a file with an empty `token` as no credential, which
|
|
57
|
+
* is right for signing in and wrong here: the realistic pre-broker shape is
|
|
58
|
+
* `{"token":"", "refreshToken":…, "serverOrigin":…}` once its access token
|
|
59
|
+
* lapsed, and a reader that returns nothing for it retires nothing. Same
|
|
60
|
+
* ownership rules as `readFromFile()`; never rewrites the file.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readRefreshFamilyFromFile(): {
|
|
63
|
+
refreshToken: string;
|
|
64
|
+
serverOrigin?: string;
|
|
65
|
+
} | null;
|
|
51
66
|
/**
|
|
52
67
|
* Delete `~/.skrr/cli-auth.json` only (the keychain is untouched). Exported
|
|
53
68
|
* for the brokered-hand-off migration cleanup; `skrr logout` continues to
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.backends = void 0;
|
|
37
37
|
exports.readFromFile = readFromFile;
|
|
38
|
+
exports.readRefreshFamilyFromFile = readRefreshFamilyFromFile;
|
|
38
39
|
exports.deleteFileBackend = deleteFileBackend;
|
|
39
40
|
exports.readFromBackend = readFromBackend;
|
|
40
41
|
exports.describeForeignCredential = describeForeignCredential;
|
|
@@ -234,13 +235,33 @@ function judgeOwnership(bundle, me, ownSlot) {
|
|
|
234
235
|
function readFromFile() {
|
|
235
236
|
return readFileAssessed().bundle;
|
|
236
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The refresh family a stored `cli-auth.json` carries, whether or not it also
|
|
240
|
+
* carries an access token — for the Dedicated guest's retirement of a
|
|
241
|
+
* pre-broker file only (OSK-12194).
|
|
242
|
+
*
|
|
243
|
+
* `readFromFile()` treats a file with an empty `token` as no credential, which
|
|
244
|
+
* is right for signing in and wrong here: the realistic pre-broker shape is
|
|
245
|
+
* `{"token":"", "refreshToken":…, "serverOrigin":…}` once its access token
|
|
246
|
+
* lapsed, and a reader that returns nothing for it retires nothing. Same
|
|
247
|
+
* ownership rules as `readFromFile()`; never rewrites the file.
|
|
248
|
+
*/
|
|
249
|
+
function readRefreshFamilyFromFile() {
|
|
250
|
+
const bundle = readFileAssessed({ requireAccessToken: false }).bundle;
|
|
251
|
+
if (!bundle?.refreshToken)
|
|
252
|
+
return null;
|
|
253
|
+
return {
|
|
254
|
+
refreshToken: bundle.refreshToken,
|
|
255
|
+
...(bundle.serverOrigin ? { serverOrigin: bundle.serverOrigin } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
237
258
|
/**
|
|
238
259
|
* This root's file, refused when it belongs to another config; else, under a
|
|
239
260
|
* root override, the pre-override `$HOME/.skrr` file when it is positively ours.
|
|
240
261
|
*/
|
|
241
|
-
function readFileAssessed() {
|
|
262
|
+
function readFileAssessed(opts = {}) {
|
|
242
263
|
const me = currentOwner();
|
|
243
|
-
const own = readFileAt(authFilePath(), true);
|
|
264
|
+
const own = readFileAt(authFilePath(), true, opts);
|
|
244
265
|
if (own) {
|
|
245
266
|
const judged = judgeOwnership(own, me, true);
|
|
246
267
|
return judged.ours ? { bundle: own } : { bundle: null, foreign: judged.stored };
|
|
@@ -248,7 +269,7 @@ function readFileAssessed() {
|
|
|
248
269
|
const legacy = legacyAuthFilePath();
|
|
249
270
|
if (path.resolve(legacy) === path.resolve(authFilePath()))
|
|
250
271
|
return { bundle: null };
|
|
251
|
-
const adopted = readFileAt(legacy, false);
|
|
272
|
+
const adopted = readFileAt(legacy, false, opts);
|
|
252
273
|
if (adopted && judgeOwnership(adopted, me, false).ours)
|
|
253
274
|
return { bundle: adopted };
|
|
254
275
|
return { bundle: null };
|
|
@@ -265,7 +286,7 @@ function legacyFileIsOurs() {
|
|
|
265
286
|
* Parse one credential file. `upgradeInPlace` lets the envelope migration
|
|
266
287
|
* rewrite it — true only for this root's own file, never a migration source.
|
|
267
288
|
*/
|
|
268
|
-
function readFileAt(p, upgradeInPlace) {
|
|
289
|
+
function readFileAt(p, upgradeInPlace, opts = {}) {
|
|
269
290
|
if (!fs.existsSync(p))
|
|
270
291
|
return null;
|
|
271
292
|
let raw;
|
|
@@ -282,7 +303,9 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
282
303
|
catch {
|
|
283
304
|
return null;
|
|
284
305
|
}
|
|
285
|
-
|
|
306
|
+
const requireAccessToken = opts.requireAccessToken !== false;
|
|
307
|
+
const hasAccessToken = typeof parsed.token === 'string' && parsed.token.length > 0;
|
|
308
|
+
if (!hasAccessToken && requireAccessToken) {
|
|
286
309
|
return null;
|
|
287
310
|
}
|
|
288
311
|
// L12 — route the secret string fields through the envelope. Mixed
|
|
@@ -293,13 +316,16 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
293
316
|
const owner = parseOwner(parsed.owner);
|
|
294
317
|
const ownerRoot = owner?.configRoot ? path.resolve(owner.configRoot) : undefined;
|
|
295
318
|
const decryptOptions = ownerRoot && ownerRoot !== path.resolve((0, config_1.configRoot)()) ? { foreignOwnerRoot: ownerRoot } : {};
|
|
296
|
-
const tokenResult =
|
|
319
|
+
const tokenResult = hasAccessToken
|
|
320
|
+
? (0, cred_envelope_1.maybeDecryptOnRead)(parsed.token, decryptOptions)
|
|
321
|
+
: { plaintext: '', needsMigration: false };
|
|
297
322
|
if (tokenResult.plaintext === null) {
|
|
298
323
|
// Envelope on disk but no DEK to unwrap → treat as missing so the
|
|
299
324
|
// caller falls through to a clean `skrr login`.
|
|
300
325
|
return null;
|
|
301
326
|
}
|
|
302
|
-
|
|
327
|
+
// A token-less read is never written back: there is no access token to seal.
|
|
328
|
+
let needsMigration = hasAccessToken && tokenResult.needsMigration;
|
|
303
329
|
const bundle = { token: tokenResult.plaintext };
|
|
304
330
|
if (typeof parsed.expiresAt === 'number' && Number.isFinite(parsed.expiresAt)) {
|
|
305
331
|
bundle.expiresAt = parsed.expiresAt;
|
package/dist/lib/commitments.js
CHANGED
|
@@ -1334,6 +1334,9 @@ function formatCommitmentExplanation(explanation) {
|
|
|
1334
1334
|
: explanation.verdict === 'quiet'
|
|
1335
1335
|
? `Chose to stay quiet at: ${explanation.stoppedAt}`
|
|
1336
1336
|
: `Stopped at: ${explanation.stoppedAt}`);
|
|
1337
|
+
// What the next Run may do without asking — an Ask-mode Agent can ask
|
|
1338
|
+
// nobody on an unattended Run (OSK-12201). Same lines as show/preflight.
|
|
1339
|
+
lines.push(...permissionLines(explanation.permissions));
|
|
1337
1340
|
return lines;
|
|
1338
1341
|
}
|
|
1339
1342
|
/**
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -190,6 +190,10 @@ 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
|
+
/** The root this machine uses when nothing relocates it (`~/.skrr`). */
|
|
194
|
+
export declare function defaultConfigRoot(): string;
|
|
195
|
+
/** Is this process on the primary root, however it was reached? */
|
|
196
|
+
export declare function isDefaultConfigRoot(env?: NodeJS.ProcessEnv): boolean;
|
|
193
197
|
/**
|
|
194
198
|
* The machine cliId THIS config root mints when it has none stored
|
|
195
199
|
* (OSK-12179). The primary root keeps the historical (host, user) id; any other
|
package/dist/lib/config.js
CHANGED
|
@@ -39,6 +39,8 @@ exports.describeDeployment = describeDeployment;
|
|
|
39
39
|
exports.canonicalizeKnownBaseURL = canonicalizeKnownBaseURL;
|
|
40
40
|
exports.isEnvAuthOverride = isEnvAuthOverride;
|
|
41
41
|
exports.configRoot = configRoot;
|
|
42
|
+
exports.defaultConfigRoot = defaultConfigRoot;
|
|
43
|
+
exports.isDefaultConfigRoot = isDefaultConfigRoot;
|
|
42
44
|
exports.machineCliId = machineCliId;
|
|
43
45
|
exports.priorConfigDir = priorConfigDir;
|
|
44
46
|
exports.loadConfig = loadConfig;
|
|
@@ -276,8 +278,8 @@ function defaultConfigRoot() {
|
|
|
276
278
|
return (0, auth_core_1.resolveConfigRoot)({});
|
|
277
279
|
}
|
|
278
280
|
/** Is this process on the primary root, however it was reached? */
|
|
279
|
-
function isDefaultConfigRoot() {
|
|
280
|
-
return path.resolve(configRoot()) === path.resolve(defaultConfigRoot());
|
|
281
|
+
function isDefaultConfigRoot(env = process.env) {
|
|
282
|
+
return path.resolve(configRoot(env)) === path.resolve(defaultConfigRoot());
|
|
281
283
|
}
|
|
282
284
|
/**
|
|
283
285
|
* The machine cliId THIS config root mints when it has none stored
|
|
@@ -64,6 +64,33 @@ export declare function resolveCredential(opts?: ResolveOptions): Promise<Resolv
|
|
|
64
64
|
* a token (e.g., just post-login) don't need to re-resolve.
|
|
65
65
|
*/
|
|
66
66
|
export declare function kindOf(token: string | null | undefined): TokenKind;
|
|
67
|
+
/**
|
|
68
|
+
* Where an in-memory credential ACTUALLY came from, when the shared
|
|
69
|
+
* `CredentialSource` union has no slot for it.
|
|
70
|
+
*
|
|
71
|
+
* Two CLI paths hold a token that was never read from `OVERSKY_TOKEN` yet
|
|
72
|
+
* are recorded as `env-token` — the union's only "a token this process holds
|
|
73
|
+
* in memory" slot, and the one `ensureFreshCliCredential` correctly skips: a
|
|
74
|
+
* daemon hand-off redeem (renewed by re-redeeming its descriptor) and a
|
|
75
|
+
* daemon-launched Agent session (`OVERSKY_SESSION_TOKEN`). Labelling either
|
|
76
|
+
* by the slot alone printed `Token source: OVERSKY_TOKEN` for a variable
|
|
77
|
+
* that was not set, which sends a debugging user to the wrong place
|
|
78
|
+
* (OSK-12185). The provenance travels BESIDE the credential so the slot keeps
|
|
79
|
+
* its refresh semantics and the label tells the truth.
|
|
80
|
+
*/
|
|
81
|
+
export type CredentialProvenance = {
|
|
82
|
+
kind: 'daemon-handoff';
|
|
83
|
+
descriptorPath?: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'daemon-delegated';
|
|
86
|
+
} | {
|
|
87
|
+
kind: 'agent-session';
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The label `whoami`, `context` and `doctor` print — text and `--json` read
|
|
91
|
+
* the same value. A recorded provenance wins over the source slot.
|
|
92
|
+
*/
|
|
93
|
+
export declare function describeCredentialSource(source: CredentialSource, provenance?: CredentialProvenance | null): string;
|
|
67
94
|
/**
|
|
68
95
|
* Human-readable label for a `CredentialSource`. Used by `skrr whoami`
|
|
69
96
|
* and (Phase H) `skrr doctor`.
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.resolveTrustedAutonomousCredential = resolveTrustedAutonomousCredential;
|
|
4
4
|
exports.resolveCredential = resolveCredential;
|
|
5
5
|
exports.kindOf = kindOf;
|
|
6
|
+
exports.describeCredentialSource = describeCredentialSource;
|
|
6
7
|
exports.formatCredentialSource = formatCredentialSource;
|
|
7
8
|
/**
|
|
8
9
|
* credential-resolver.ts — Phase H.1 precedence for the Sky CLI.
|
|
@@ -118,6 +119,24 @@ async function resolveCredential(opts = {}) {
|
|
|
118
119
|
function kindOf(token) {
|
|
119
120
|
return (0, auth_core_1.classifyTokenKind)(token);
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The label `whoami`, `context` and `doctor` print — text and `--json` read
|
|
124
|
+
* the same value. A recorded provenance wins over the source slot.
|
|
125
|
+
*/
|
|
126
|
+
function describeCredentialSource(source, provenance) {
|
|
127
|
+
if (provenance?.kind === 'daemon-handoff') {
|
|
128
|
+
return provenance.descriptorPath
|
|
129
|
+
? `daemon hand-off (${provenance.descriptorPath})`
|
|
130
|
+
: 'daemon hand-off';
|
|
131
|
+
}
|
|
132
|
+
if (provenance?.kind === 'daemon-delegated') {
|
|
133
|
+
return 'daemon delegated credential (OVERSKY_DELEGATED_CLI_BROKER_URL)';
|
|
134
|
+
}
|
|
135
|
+
if (provenance?.kind === 'agent-session') {
|
|
136
|
+
return 'agent session (OVERSKY_SESSION_TOKEN)';
|
|
137
|
+
}
|
|
138
|
+
return formatCredentialSource(source);
|
|
139
|
+
}
|
|
121
140
|
/**
|
|
122
141
|
* Human-readable label for a `CredentialSource`. Used by `skrr whoami`
|
|
123
142
|
* and (Phase H) `skrr doctor`.
|
|
@@ -115,6 +115,13 @@ export interface DaemonBrokeredOutcome {
|
|
|
115
115
|
/** Server the minted credential belongs to (daemon-reported, else the
|
|
116
116
|
* descriptor's own `serverUrl`). */
|
|
117
117
|
serverUrl?: string;
|
|
118
|
+
/**
|
|
119
|
+
* The descriptor file the token was redeemed from. The token lives only in
|
|
120
|
+
* this process's memory, so this path is the only honest answer to "where
|
|
121
|
+
* did this credential come from" — `skrr whoami` prints it as the token
|
|
122
|
+
* source (OSK-12185).
|
|
123
|
+
*/
|
|
124
|
+
descriptorPath?: string;
|
|
118
125
|
}
|
|
119
126
|
/**
|
|
120
127
|
* Explicit reasons the broker path could fail. These exist to keep the
|
|
@@ -335,6 +342,24 @@ export declare function maybeRetireStoredCliAuthFile(opts: {
|
|
|
335
342
|
descriptorPath?: string;
|
|
336
343
|
descriptorServerUrl?: string;
|
|
337
344
|
}): Promise<void>;
|
|
345
|
+
/**
|
|
346
|
+
* Should this process broker BEFORE it reads the stored credential?
|
|
347
|
+
* (OSK-12194)
|
|
348
|
+
*
|
|
349
|
+
* True only on a brokered Dedicated guest: the guest hand-off descriptor is
|
|
350
|
+
* live and offers the access-token mode and NOT the durable mint (its secret
|
|
351
|
+
* can only redeem, so any `cli-auth.json` here predates broker mode), and that
|
|
352
|
+
* file carries a refresh family for the descriptor's own server — the same
|
|
353
|
+
* provenance guard the retirement applies.
|
|
354
|
+
*
|
|
355
|
+
* Without this, the retirement could never run: it runs after a successful
|
|
356
|
+
* redeem, the auto-broker runs only when the resolver found no token, and the
|
|
357
|
+
* resolver adopts a file that carries one — so a LIVE pre-broker family was
|
|
358
|
+
* used and kept forever, which is exactly the case the cleanup exists for.
|
|
359
|
+
* A laptop has no guest descriptor and is untouched: there `cli-auth.json` is
|
|
360
|
+
* a legitimate login.
|
|
361
|
+
*/
|
|
362
|
+
export declare function shouldBrokerBeforeStoredCredential(descriptorPath?: string): boolean;
|
|
338
363
|
/**
|
|
339
364
|
* Convenience wrapper: run the broker call and, on a durable-mint success,
|
|
340
365
|
* persist the returned bundle through the canonical Sky CLI auth backend
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -86,6 +86,7 @@ exports.redeemBrokeredHandoffToken = redeemBrokeredHandoffToken;
|
|
|
86
86
|
exports.__resetBrokeredHandoffForTest = __resetBrokeredHandoffForTest;
|
|
87
87
|
exports.findBrokeredHandoffDescriptor = findBrokeredHandoffDescriptor;
|
|
88
88
|
exports.maybeRetireStoredCliAuthFile = maybeRetireStoredCliAuthFile;
|
|
89
|
+
exports.shouldBrokerBeforeStoredCredential = shouldBrokerBeforeStoredCredential;
|
|
89
90
|
exports.attemptDaemonBrokerLoginAndPersist = attemptDaemonBrokerLoginAndPersist;
|
|
90
91
|
exports.maybeAutoBroker = maybeAutoBroker;
|
|
91
92
|
const fs = __importStar(require("node:fs"));
|
|
@@ -463,6 +464,7 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
|
463
464
|
...((response.serverUrl ?? bootstrap.serverUrl)
|
|
464
465
|
? { serverUrl: response.serverUrl ?? bootstrap.serverUrl }
|
|
465
466
|
: {}),
|
|
467
|
+
...(sourcePath ? { descriptorPath: sourcePath } : {}),
|
|
466
468
|
};
|
|
467
469
|
}
|
|
468
470
|
// The descriptor ALSO advertises the durable mint (a laptop island during
|
|
@@ -836,32 +838,69 @@ function findBrokeredHandoffDescriptor(profile = 'default') {
|
|
|
836
838
|
async function maybeRetireStoredCliAuthFile(opts) {
|
|
837
839
|
if (opts.descriptorPath !== exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR)
|
|
838
840
|
return;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
stored = (0, auth_storage_1.readFromFile)();
|
|
842
|
-
}
|
|
843
|
-
catch {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (!stored?.refreshToken)
|
|
847
|
-
return;
|
|
848
|
-
const storedOrigin = stored.serverOrigin ? (0, auth_storage_1.normalizeServerOrigin)(stored.serverOrigin) : null;
|
|
849
|
-
const descriptorOrigin = opts.descriptorServerUrl
|
|
850
|
-
? (0, auth_storage_1.normalizeServerOrigin)(opts.descriptorServerUrl)
|
|
851
|
-
: null;
|
|
852
|
-
if (!storedOrigin || !descriptorOrigin || storedOrigin !== descriptorOrigin)
|
|
841
|
+
const stale = retirableCliAuthFamily(opts.descriptorServerUrl);
|
|
842
|
+
if (!stale)
|
|
853
843
|
return;
|
|
854
844
|
// The revoke is authenticated by the refresh token itself — presenting it
|
|
855
845
|
// is the proof of ownership. `revokeDaemonRefreshSession` swallows its own
|
|
856
846
|
// network/HTTP failures (best-effort by contract); the delete still runs.
|
|
857
847
|
try {
|
|
858
|
-
await (0, auth_core_1.revokeDaemonRefreshSession)(
|
|
848
|
+
await (0, auth_core_1.revokeDaemonRefreshSession)(stale.origin, stale.refreshToken);
|
|
859
849
|
}
|
|
860
850
|
catch {
|
|
861
851
|
/* best-effort — delete proceeds regardless */
|
|
862
852
|
}
|
|
863
853
|
(0, auth_storage_1.deleteFileBackend)();
|
|
864
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Guards 2 and 3 of the retirement above: the stored file's refresh family,
|
|
857
|
+
* when its origin matches the descriptor's. Read through
|
|
858
|
+
* `readRefreshFamilyFromFile`, which accepts a file whose access token is
|
|
859
|
+
* empty — the realistic pre-broker shape once that token lapsed, and the one
|
|
860
|
+
* `readFromFile` reports as no credential at all (OSK-12194).
|
|
861
|
+
*/
|
|
862
|
+
function retirableCliAuthFamily(descriptorServerUrl) {
|
|
863
|
+
let stored;
|
|
864
|
+
try {
|
|
865
|
+
stored = (0, auth_storage_1.readRefreshFamilyFromFile)();
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
if (!stored?.refreshToken)
|
|
871
|
+
return null;
|
|
872
|
+
const storedOrigin = stored.serverOrigin ? (0, auth_storage_1.normalizeServerOrigin)(stored.serverOrigin) : null;
|
|
873
|
+
const descriptorOrigin = descriptorServerUrl ? (0, auth_storage_1.normalizeServerOrigin)(descriptorServerUrl) : null;
|
|
874
|
+
if (!storedOrigin || !descriptorOrigin || storedOrigin !== descriptorOrigin)
|
|
875
|
+
return null;
|
|
876
|
+
return { origin: descriptorOrigin, refreshToken: stored.refreshToken };
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Should this process broker BEFORE it reads the stored credential?
|
|
880
|
+
* (OSK-12194)
|
|
881
|
+
*
|
|
882
|
+
* True only on a brokered Dedicated guest: the guest hand-off descriptor is
|
|
883
|
+
* live and offers the access-token mode and NOT the durable mint (its secret
|
|
884
|
+
* can only redeem, so any `cli-auth.json` here predates broker mode), and that
|
|
885
|
+
* file carries a refresh family for the descriptor's own server — the same
|
|
886
|
+
* provenance guard the retirement applies.
|
|
887
|
+
*
|
|
888
|
+
* Without this, the retirement could never run: it runs after a successful
|
|
889
|
+
* redeem, the auto-broker runs only when the resolver found no token, and the
|
|
890
|
+
* resolver adopts a file that carries one — so a LIVE pre-broker family was
|
|
891
|
+
* used and kept forever, which is exactly the case the cleanup exists for.
|
|
892
|
+
* A laptop has no guest descriptor and is untouched: there `cli-auth.json` is
|
|
893
|
+
* a legitimate login.
|
|
894
|
+
*/
|
|
895
|
+
function shouldBrokerBeforeStoredCredential(descriptorPath = exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR) {
|
|
896
|
+
const bootstrap = readBootstrap(descriptorPath);
|
|
897
|
+
if (!bootstrap)
|
|
898
|
+
return false;
|
|
899
|
+
const modes = (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap);
|
|
900
|
+
if (!modes.includes('access_token') || modes.includes('refresh_family'))
|
|
901
|
+
return false;
|
|
902
|
+
return retirableCliAuthFamily(bootstrap.serverUrl) !== null;
|
|
903
|
+
}
|
|
865
904
|
/** Bound the confirm so a slow server cannot stall a login that already worked. */
|
|
866
905
|
const CONFIRM_TIMEOUT_MS = 10_000;
|
|
867
906
|
/**
|
|
@@ -40,7 +40,7 @@ export interface DaemonHandoffOutcome {
|
|
|
40
40
|
* The two absent cases are separated because they need different work — one
|
|
41
41
|
* needs a binary fetched, the other only needs the service registered.
|
|
42
42
|
*/
|
|
43
|
-
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service';
|
|
43
|
+
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused';
|
|
44
44
|
}
|
|
45
45
|
export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
|
|
46
46
|
loginFlow?: string;
|
|
@@ -196,6 +196,24 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
196
196
|
detail: 'the local daemon brokered this login, so it already has a working credential',
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
+
// A relocated config root (`SKRR_CONFIG_DIR`) is a second sign-in on this
|
|
200
|
+
// machine, not the machine's sign-in. The installed daemon service carries no
|
|
201
|
+
// config-root override, so it reads the DEFAULT root — its credential, its
|
|
202
|
+
// deployment, its account belong there. Offering this root's login to it would
|
|
203
|
+
// re-point the machine at whatever this root signed in as, and when the server
|
|
204
|
+
// refused the offer the CLI used to report a healthy, signed-in daemon as
|
|
205
|
+
// "not signed in" and send the user to re-login it (OSK-12189).
|
|
206
|
+
//
|
|
207
|
+
// Decided on the root alone, before anything is spawned or minted: nothing
|
|
208
|
+
// about this machine's daemon should change because a second root signed in.
|
|
209
|
+
if (!(0, config_1.isDefaultConfigRoot)(env)) {
|
|
210
|
+
return {
|
|
211
|
+
status: 'skipped',
|
|
212
|
+
reason: 'foreign-root',
|
|
213
|
+
detail: `this login is in the config root ${(0, config_1.configRoot)(env)}; the local daemon ` +
|
|
214
|
+
`belongs to ${(0, config_1.defaultConfigRoot)()} and keeps its own sign-in`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
199
217
|
const binary = (0, exec_runtime_binary_1.findRuntimeBinary)();
|
|
200
218
|
if (!binary) {
|
|
201
219
|
return { status: 'skipped', reason: 'no-binary', detail: 'no local daemon installed' };
|
|
@@ -264,7 +282,9 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
264
282
|
}));
|
|
265
283
|
}
|
|
266
284
|
catch (err) {
|
|
267
|
-
|
|
285
|
+
// Refused BEFORE the daemon was touched: it still holds exactly the
|
|
286
|
+
// credential it had, so this is "not handed this login", never "signed out".
|
|
287
|
+
return { status: 'failed', reason: 'mint-refused', detail: err.message };
|
|
268
288
|
}
|
|
269
289
|
const payload = JSON.stringify({
|
|
270
290
|
refreshToken: bundle.refreshToken,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The internal Mongo `_id` handed back where a command wants the `id`.
|
|
3
|
+
*
|
|
4
|
+
* `--json` prints every row's `_id` FIRST, so it is the value a person or a
|
|
5
|
+
* script copies. Spaces and Tasks are addressed by `id` (a UUID) everywhere in
|
|
6
|
+
* the API, and no resolver accepts both forms, so `spaces show <_id>`,
|
|
7
|
+
* `tasks show <_id>` and `tasks create --space <_id>` answered with not-found
|
|
8
|
+
* or a raw HTTP 404 — true, and no help: nothing said which field to use
|
|
9
|
+
* (OSK-12205).
|
|
10
|
+
*
|
|
11
|
+
* Only the refusal lives here. Finding the `id` that goes with an `_id` needs
|
|
12
|
+
* a listing, so the caller does that (it holds the API) and passes the answer
|
|
13
|
+
* in when it has one.
|
|
14
|
+
*/
|
|
15
|
+
/** A 24-hex Mongo ObjectId, the shape of `_id`. */
|
|
16
|
+
export declare function looksLikeInternalObjectId(value: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The sentence for an `_id` passed where the `id` belongs.
|
|
19
|
+
*
|
|
20
|
+
* `canonicalId` is the row's `id` when the caller could find it — then the
|
|
21
|
+
* remedy is the exact value to use. Without it the remedy names the field.
|
|
22
|
+
*/
|
|
23
|
+
export declare function describeInternalIdMisuse(opts: {
|
|
24
|
+
value: string;
|
|
25
|
+
noun: 'space' | 'task';
|
|
26
|
+
canonicalId?: string | null;
|
|
27
|
+
bin: string;
|
|
28
|
+
}): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The internal Mongo `_id` handed back where a command wants the `id`.
|
|
4
|
+
*
|
|
5
|
+
* `--json` prints every row's `_id` FIRST, so it is the value a person or a
|
|
6
|
+
* script copies. Spaces and Tasks are addressed by `id` (a UUID) everywhere in
|
|
7
|
+
* the API, and no resolver accepts both forms, so `spaces show <_id>`,
|
|
8
|
+
* `tasks show <_id>` and `tasks create --space <_id>` answered with not-found
|
|
9
|
+
* or a raw HTTP 404 — true, and no help: nothing said which field to use
|
|
10
|
+
* (OSK-12205).
|
|
11
|
+
*
|
|
12
|
+
* Only the refusal lives here. Finding the `id` that goes with an `_id` needs
|
|
13
|
+
* a listing, so the caller does that (it holds the API) and passes the answer
|
|
14
|
+
* in when it has one.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.looksLikeInternalObjectId = looksLikeInternalObjectId;
|
|
18
|
+
exports.describeInternalIdMisuse = describeInternalIdMisuse;
|
|
19
|
+
const OBJECT_ID_RE = /^[0-9a-fA-F]{24}$/;
|
|
20
|
+
/** A 24-hex Mongo ObjectId, the shape of `_id`. */
|
|
21
|
+
function looksLikeInternalObjectId(value) {
|
|
22
|
+
return OBJECT_ID_RE.test(value.trim());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The sentence for an `_id` passed where the `id` belongs.
|
|
26
|
+
*
|
|
27
|
+
* `canonicalId` is the row's `id` when the caller could find it — then the
|
|
28
|
+
* remedy is the exact value to use. Without it the remedy names the field.
|
|
29
|
+
*/
|
|
30
|
+
function describeInternalIdMisuse(opts) {
|
|
31
|
+
const { value, noun, canonicalId, bin } = opts;
|
|
32
|
+
const head = `"${value.trim()}" is the internal _id of a ${noun}; commands take its \`id\``;
|
|
33
|
+
if (canonicalId) {
|
|
34
|
+
return `${head}. Use \`id\` ${canonicalId}.`;
|
|
35
|
+
}
|
|
36
|
+
const alternatives = noun === 'task'
|
|
37
|
+
? 'the `id` field (a UUID) or the `identifier` (OSK-123) from the same --json output'
|
|
38
|
+
: 'the `id` field (a UUID) or the `identifier` (OSK-S-12) from the same --json output';
|
|
39
|
+
return (`${head}.\n` +
|
|
40
|
+
` → use ${alternatives}; \`${bin} ${noun === 'task' ? 'tasks' : 'spaces'} list --json\` prints both.`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remedy for `tasks move --status <unknown>` (OSK-12205).
|
|
3
|
+
*
|
|
4
|
+
* Workflow states are Space-local, so the CLI cannot declare them as flag
|
|
5
|
+
* options and cannot list them ahead of the refusal. The server now names the
|
|
6
|
+
* accepted states in its message (and in `details.validStates`); this adds the
|
|
7
|
+
* command that lists them with their types, and quotes the states itself when
|
|
8
|
+
* a server sent them only in `details`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function describeUnknownWorkflowState(body: Record<string, unknown> | undefined, bin: string): string | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeUnknownWorkflowState = describeUnknownWorkflowState;
|
|
4
|
+
/**
|
|
5
|
+
* The remedy for `tasks move --status <unknown>` (OSK-12205).
|
|
6
|
+
*
|
|
7
|
+
* Workflow states are Space-local, so the CLI cannot declare them as flag
|
|
8
|
+
* options and cannot list them ahead of the refusal. The server now names the
|
|
9
|
+
* accepted states in its message (and in `details.validStates`); this adds the
|
|
10
|
+
* command that lists them with their types, and quotes the states itself when
|
|
11
|
+
* a server sent them only in `details`.
|
|
12
|
+
*/
|
|
13
|
+
function describeUnknownWorkflowState(body, bin) {
|
|
14
|
+
if (!body || body.code !== 'WORKFLOW_STATE_UNKNOWN')
|
|
15
|
+
return null;
|
|
16
|
+
const details = (body.details ?? {});
|
|
17
|
+
const valid = Array.isArray(details.validStates)
|
|
18
|
+
? details.validStates.filter((v) => typeof v === 'string')
|
|
19
|
+
: [];
|
|
20
|
+
let message = typeof body.error === 'string' && body.error ? body.error : 'Unknown workflow state';
|
|
21
|
+
if (valid.length > 0 && !message.includes(valid[0])) {
|
|
22
|
+
message += `. This Space accepts: ${valid.join(', ')}.`;
|
|
23
|
+
}
|
|
24
|
+
return (`${message}\n` +
|
|
25
|
+
` → \`${bin} spaces states list <space-id>\` lists this Space's states with their types; ` +
|
|
26
|
+
'`--status` takes a state id or its display name.');
|
|
27
|
+
}
|