@skrr-ai/cli 0.1.57 → 0.1.58
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/agents/show.js +13 -2
- package/dist/commands/harnesses/ping.d.ts +2 -0
- package/dist/commands/harnesses/ping.js +10 -1
- package/dist/commands/login.js +6 -1
- package/dist/commands/logout.js +8 -0
- package/dist/commands/pair.js +1 -1
- package/dist/lib/agentic-stream.d.ts +12 -0
- package/dist/lib/agentic-stream.js +50 -1
- package/dist/lib/auth-storage.d.ts +27 -0
- package/dist/lib/auth-storage.js +42 -0
- package/dist/lib/commitments.js +1 -0
- package/dist/lib/daemonBroker.d.ts +5 -1
- package/dist/lib/daemonBroker.js +13 -2
- package/dist/lib/daemonHandoff.d.ts +1 -1
- package/dist/lib/daemonHandoff.js +18 -0
- package/dist/lib/login.js +2 -2
- package/dist/lib/oauthLogin.js +3 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3200 -3189
- package/oclif.manifest.json +28891 -28891
- package/package.json +1 -1
|
@@ -54,8 +54,19 @@ function agentPermissionLines(agent, harness) {
|
|
|
54
54
|
return [`Permission: ${stored} — enforced by the ${harness} harness`];
|
|
55
55
|
}
|
|
56
56
|
const lines = [];
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
|
|
58
|
+
if (typeof effective.refusedMode === 'string' && effective.refusedMode) {
|
|
59
|
+
// OSK-12316: a mode the harness could only honour by WIDENING it is refused
|
|
60
|
+
// at dispatch (OSK-12154); it does not "run as" anything.
|
|
61
|
+
const required = typeof effective.requiredMode === 'string' && effective.requiredMode
|
|
62
|
+
? effective.requiredMode
|
|
63
|
+
: null;
|
|
64
|
+
lines.push(`Permission: ${effective.refusedMode} (from ${from}) — turns are REFUSED: the ${harness} harness cannot run it without allowing more`);
|
|
65
|
+
if (required) {
|
|
66
|
+
lines.push(`${pad}Set ${required} to run (skrr agents update <agent> --default-permission-mode ${required}), or pass --permission-mode for a turn.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (typeof effective.mode === 'string' && effective.mode) {
|
|
59
70
|
lines.push(`Permission: ${effective.mode} (from ${from}) — enforced by the ${harness} harness`);
|
|
60
71
|
}
|
|
61
72
|
else {
|
|
@@ -8,6 +8,8 @@ import { BaseCommand } from '../../base-command';
|
|
|
8
8
|
* sends, and testing for them alone is what made the success branch dead code.
|
|
9
9
|
*/
|
|
10
10
|
export declare function isPingSuccess(status: string | undefined): boolean;
|
|
11
|
+
/** The server declared a completed probe failure, not a still-pending poll. */
|
|
12
|
+
export declare function describePingFailure(status: string, error?: string | null): string;
|
|
11
13
|
export default class HarnessesPing extends BaseCommand {
|
|
12
14
|
static description: string;
|
|
13
15
|
static examples: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isPingSuccess = isPingSuccess;
|
|
4
|
+
exports.describePingFailure = describePingFailure;
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const base_command_1 = require("../../base-command");
|
|
6
7
|
const harnesses_1 = require("../../lib/harnesses");
|
|
@@ -31,6 +32,14 @@ const POLL_INTERVAL_MS = 300;
|
|
|
31
32
|
function isPingSuccess(status) {
|
|
32
33
|
return status === 'completed' || status === 'ok' || status === 'success';
|
|
33
34
|
}
|
|
35
|
+
/** The server declared a completed probe failure, not a still-pending poll. */
|
|
36
|
+
function describePingFailure(status, error) {
|
|
37
|
+
const detail = typeof error === 'string' && error.trim() ? `: ${error.trim()}` : '.';
|
|
38
|
+
if (status === 'timeout') {
|
|
39
|
+
return `Ping timed out${detail} The daemon is registered but may be offline or not responding.`;
|
|
40
|
+
}
|
|
41
|
+
return `Ping ${status}${detail} The daemon is registered but may be offline or not responding.`;
|
|
42
|
+
}
|
|
34
43
|
class HarnessesPing extends base_command_1.BaseCommand {
|
|
35
44
|
static description = 'Ping a harness and wait for its daemon to answer (local only)';
|
|
36
45
|
static examples = [
|
|
@@ -90,7 +99,7 @@ class HarnessesPing extends base_command_1.BaseCommand {
|
|
|
90
99
|
await this.logReadiness(args.harness);
|
|
91
100
|
}
|
|
92
101
|
else {
|
|
93
|
-
this.
|
|
102
|
+
this.error(describePingFailure(result.status, result.error), { exit: 1 });
|
|
94
103
|
}
|
|
95
104
|
return;
|
|
96
105
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -234,6 +234,11 @@ class Login extends base_command_1.BaseCommand {
|
|
|
234
234
|
if (handoff.status === 'delivered') {
|
|
235
235
|
this.log(`The local daemon was signed in too — ${handoff.detail}.`);
|
|
236
236
|
}
|
|
237
|
+
else if (handoff.reason === 'platform-managed') {
|
|
238
|
+
// A Dedicated guest: its daemon is the platform's, so there is
|
|
239
|
+
// nothing to install and nothing to hand over (OSK-12296).
|
|
240
|
+
this.log("This machine's daemon is managed by the platform; there is nothing to set up here.");
|
|
241
|
+
}
|
|
237
242
|
else if (handoff.reason === 'foreign-root') {
|
|
238
243
|
// Not a failure and not a hint to re-login anything: the daemon is
|
|
239
244
|
// the default root's, and this is a second root (OSK-12189).
|
|
@@ -391,7 +396,7 @@ class Login extends base_command_1.BaseCommand {
|
|
|
391
396
|
refreshExpiresAt: result.refreshExpiresAt
|
|
392
397
|
? new Date(result.refreshExpiresAt).getTime()
|
|
393
398
|
: undefined,
|
|
394
|
-
}, { serverOrigin: this.cliConfig.baseURL, clearReauth: true });
|
|
399
|
+
}, { serverOrigin: this.cliConfig.baseURL, clearReauth: true, provenance: 'user_login' });
|
|
395
400
|
this.log('');
|
|
396
401
|
this.log(`Signed in as ${result.user.email} via recovery code on ` +
|
|
397
402
|
`${(0, config_1.describeDeployment)(this.cliConfig.baseURL)}.`);
|
package/dist/commands/logout.js
CHANGED
|
@@ -257,6 +257,14 @@ class Logout extends base_command_1.BaseCommand {
|
|
|
257
257
|
: 'Signed out from all devices.'
|
|
258
258
|
: 'Signed out.';
|
|
259
259
|
this.log(finalMessage);
|
|
260
|
+
// On a brokered Dedicated guest the sign-in just removed was the person's
|
|
261
|
+
// OWN (an explicit `skrr login`); the machine's platform-managed access is
|
|
262
|
+
// untouched and skrr keeps working through it. Say both, so "Signed out."
|
|
263
|
+
// is not read as "this machine can no longer reach skrr" (OSK-12043).
|
|
264
|
+
if ((0, daemonBroker_1.findBrokeredHandoffDescriptor)()) {
|
|
265
|
+
this.log('Removed your own sign-in from this machine. ' +
|
|
266
|
+
"skrr keeps working here through the platform's managed hand-off.");
|
|
267
|
+
}
|
|
260
268
|
if (sweptLegacy.length > 0) {
|
|
261
269
|
this.log(`Also removed ${sweptLegacy.length} credential${sweptLegacy.length === 1 ? '' : 's'} left by the previous product identity:`);
|
|
262
270
|
for (const service of sweptLegacy)
|
package/dist/commands/pair.js
CHANGED
|
@@ -273,7 +273,7 @@ function persistPairCredentials(plaintext, serverOrigin) {
|
|
|
273
273
|
(0, auth_storage_1.writeToBackend)({
|
|
274
274
|
token: plaintext.token,
|
|
275
275
|
...(plaintext.refreshToken ? { refreshToken: plaintext.refreshToken } : {}),
|
|
276
|
-
}, { serverOrigin, clearReauth: true });
|
|
276
|
+
}, { serverOrigin, clearReauth: true, provenance: 'user_login' });
|
|
277
277
|
if (plaintext.dataEncryptionKey) {
|
|
278
278
|
// The unified `writeToBackend` API only carries token+refresh
|
|
279
279
|
// today. For now, persist the DEK as a sibling field on the next
|
|
@@ -371,6 +371,12 @@ export declare class AgenticStreamClient {
|
|
|
371
371
|
* (OSK-12197) instead of leaving `[y/N]` open after the run stopped asking.
|
|
372
372
|
*/
|
|
373
373
|
private readonly openPermissionPrompts;
|
|
374
|
+
/**
|
|
375
|
+
* Question prompts on the terminal still waiting for an answer, by
|
|
376
|
+
* questionId, so a server `question:cancel` can close the one it names
|
|
377
|
+
* (OSK-12208) the same way `permission:cancel` closes a permission prompt.
|
|
378
|
+
*/
|
|
379
|
+
private readonly openQuestionPrompts;
|
|
374
380
|
/** Text already written to the terminal, for suffix-only rendering. */
|
|
375
381
|
private renderedText;
|
|
376
382
|
private idleTimer;
|
|
@@ -455,6 +461,12 @@ export declare class AgenticStreamClient {
|
|
|
455
461
|
* and close the terminal prompt if this client is still showing it.
|
|
456
462
|
*/
|
|
457
463
|
private handlePermissionCancel;
|
|
464
|
+
/**
|
|
465
|
+
* The server closed a question (OSK-12208): nobody answered it in time, or
|
|
466
|
+
* the turn ended first. Say so, and close the terminal prompt if this
|
|
467
|
+
* client is still showing it — the question twin of handlePermissionCancel.
|
|
468
|
+
*/
|
|
469
|
+
private handleQuestionCancel;
|
|
458
470
|
private ask;
|
|
459
471
|
/**
|
|
460
472
|
* True the first time this (kind, identity) pair is seen; false afterwards.
|
|
@@ -563,6 +563,12 @@ class AgenticStreamClient {
|
|
|
563
563
|
* (OSK-12197) instead of leaving `[y/N]` open after the run stopped asking.
|
|
564
564
|
*/
|
|
565
565
|
openPermissionPrompts = new Map();
|
|
566
|
+
/**
|
|
567
|
+
* Question prompts on the terminal still waiting for an answer, by
|
|
568
|
+
* questionId, so a server `question:cancel` can close the one it names
|
|
569
|
+
* (OSK-12208) the same way `permission:cancel` closes a permission prompt.
|
|
570
|
+
*/
|
|
571
|
+
openQuestionPrompts = new Map();
|
|
566
572
|
/** Text already written to the terminal, for suffix-only rendering. */
|
|
567
573
|
renderedText = '';
|
|
568
574
|
idleTimer = null;
|
|
@@ -961,6 +967,11 @@ class AgenticStreamClient {
|
|
|
961
967
|
if (match(event))
|
|
962
968
|
void this.handleQuestion(event);
|
|
963
969
|
});
|
|
970
|
+
socket.on('question:cancel', (event) => {
|
|
971
|
+
if (!match(event))
|
|
972
|
+
return;
|
|
973
|
+
this.handleQuestionCancel(event);
|
|
974
|
+
});
|
|
964
975
|
socket.on('error:unauthorized', (event) => {
|
|
965
976
|
if (match(event))
|
|
966
977
|
this.finish('failed', event);
|
|
@@ -1063,9 +1074,20 @@ class AgenticStreamClient {
|
|
|
1063
1074
|
const interactive = this.options.input !== undefined || Boolean(process.stdin.isTTY);
|
|
1064
1075
|
const answers = {};
|
|
1065
1076
|
if (interactive) {
|
|
1077
|
+
const prompt = new AbortController();
|
|
1078
|
+
if (event.questionId)
|
|
1079
|
+
this.openQuestionPrompts.set(event.questionId, prompt);
|
|
1066
1080
|
for (const question of event.questions ?? []) {
|
|
1067
|
-
|
|
1081
|
+
if (prompt.signal.aborted)
|
|
1082
|
+
break;
|
|
1083
|
+
answers[question.header ?? question.question] = await this.ask(`${question.header ?? 'Question'}: ${question.question}\n> `, prompt.signal);
|
|
1068
1084
|
}
|
|
1085
|
+
if (event.questionId)
|
|
1086
|
+
this.openQuestionPrompts.delete(event.questionId);
|
|
1087
|
+
// Closed by the server (it stopped waiting, or the turn ended): an answer
|
|
1088
|
+
// sent now would be read by nobody, and would look like a person's.
|
|
1089
|
+
if (prompt.signal.aborted)
|
|
1090
|
+
return;
|
|
1069
1091
|
}
|
|
1070
1092
|
else {
|
|
1071
1093
|
answers._dismissed = exports.NON_INTERACTIVE_QUESTION_DISMISSAL;
|
|
@@ -1110,6 +1132,25 @@ class AgenticStreamClient {
|
|
|
1110
1132
|
: {}),
|
|
1111
1133
|
});
|
|
1112
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* The server closed a question (OSK-12208): nobody answered it in time, or
|
|
1137
|
+
* the turn ended first. Say so, and close the terminal prompt if this
|
|
1138
|
+
* client is still showing it — the question twin of handlePermissionCancel.
|
|
1139
|
+
*/
|
|
1140
|
+
handleQuestionCancel(event) {
|
|
1141
|
+
const questionId = typeof event.questionId === 'string' ? event.questionId : undefined;
|
|
1142
|
+
if (!this.firstTimeSeen('question-cancel', questionId))
|
|
1143
|
+
return;
|
|
1144
|
+
const open = questionId ? this.openQuestionPrompts.get(questionId) : undefined;
|
|
1145
|
+
if (questionId)
|
|
1146
|
+
this.openQuestionPrompts.delete(questionId);
|
|
1147
|
+
open?.abort();
|
|
1148
|
+
this.emit({
|
|
1149
|
+
...event,
|
|
1150
|
+
type: 'question.cancelled',
|
|
1151
|
+
promptClosed: Boolean(open),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1113
1154
|
ask(prompt, signal) {
|
|
1114
1155
|
// A prompt raised after close has nobody to answer it, and re-creating the
|
|
1115
1156
|
// interface here would resurrect stdin for a run that is already over.
|
|
@@ -1243,6 +1284,14 @@ class AgenticStreamClient {
|
|
|
1243
1284
|
output.write(`\n[${event.type}]\n`);
|
|
1244
1285
|
this.renderedText = this.accumulated;
|
|
1245
1286
|
}
|
|
1287
|
+
else if (event.type === 'question.cancelled') {
|
|
1288
|
+
// A question the run stopped waiting for (OSK-12208).
|
|
1289
|
+
const why = typeof event.message === 'string' && event.message
|
|
1290
|
+
? event.message
|
|
1291
|
+
: `closed (${String(event.reason ?? 'cancelled')})`;
|
|
1292
|
+
output.write(`\n[question] ${why}\n`);
|
|
1293
|
+
this.renderedText = this.accumulated;
|
|
1294
|
+
}
|
|
1246
1295
|
else if (event.type === 'permission.approved' && event.auto === true) {
|
|
1247
1296
|
// Said out loud, because nobody was asked: the flag approved it, and a
|
|
1248
1297
|
// reader of this log should see which tools that covered (OSK-10323).
|
|
@@ -11,7 +11,26 @@ export interface AuthBundle {
|
|
|
11
11
|
* current config; callers never need to set it.
|
|
12
12
|
*/
|
|
13
13
|
owner?: CredentialOwner;
|
|
14
|
+
/**
|
|
15
|
+
* Who created the refresh family this bundle carries (OSK-12293). Recorded
|
|
16
|
+
* in the file in plaintext, like `owner`, so it can be read without the key.
|
|
17
|
+
* Absent on everything an older CLI wrote.
|
|
18
|
+
*/
|
|
19
|
+
provenance?: CredentialProvenance;
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How a stored refresh family came to exist.
|
|
23
|
+
*
|
|
24
|
+
* - `user_login`: a person signed in on purpose (`skrr login` in any of its
|
|
25
|
+
* flows, `skrr pair`). On a Dedicated guest this is the one stored family
|
|
26
|
+
* the broker-mode retirement must never touch: the person asked for it.
|
|
27
|
+
* - `daemon_broker`: the local daemon minted it through its hand-off.
|
|
28
|
+
*
|
|
29
|
+
* An absent value means an older CLI wrote the file and the family's origin
|
|
30
|
+
* is unknown, which on a brokered guest is the pre-broker leftover the
|
|
31
|
+
* retirement exists for (OSK-12194).
|
|
32
|
+
*/
|
|
33
|
+
export type CredentialProvenance = 'user_login' | 'daemon_broker';
|
|
15
34
|
/** Which skrr config a credential belongs to. */
|
|
16
35
|
export interface CredentialOwner {
|
|
17
36
|
/** Resolved config root (`~/.skrr`, or the `SKRR_CONFIG_DIR` override). */
|
|
@@ -39,6 +58,13 @@ export interface WriteOptions {
|
|
|
39
58
|
serverOrigin?: string;
|
|
40
59
|
/** Clear a stale needs-reauth latch after a server has issued fresh credentials. */
|
|
41
60
|
clearReauth?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Who created the family being written. A write that names none is a
|
|
63
|
+
* ROTATION of the stored family (a refresh), and keeps the provenance the
|
|
64
|
+
* stored file records for the same server — so a refresh never erases the
|
|
65
|
+
* marker a login wrote, and never invents one an older CLI did not.
|
|
66
|
+
*/
|
|
67
|
+
provenance?: CredentialProvenance;
|
|
42
68
|
}
|
|
43
69
|
/**
|
|
44
70
|
* File-backend-only read. Exported for the brokered-hand-off migration
|
|
@@ -62,6 +88,7 @@ export declare function readFromFile(): AuthBundle | null;
|
|
|
62
88
|
export declare function readRefreshFamilyFromFile(): {
|
|
63
89
|
refreshToken: string;
|
|
64
90
|
serverOrigin?: string;
|
|
91
|
+
provenance?: CredentialProvenance;
|
|
65
92
|
} | null;
|
|
66
93
|
/**
|
|
67
94
|
* Delete `~/.skrr/cli-auth.json` only (the keychain is untouched). Exported
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -106,6 +106,12 @@ const cli_id_1 = require("./cli-id");
|
|
|
106
106
|
* `auth.json`, deliberately distinct so the two binaries never clobber
|
|
107
107
|
* each other's tokens (CLI holds scope=cli, daemon holds scope=daemon). */
|
|
108
108
|
const AUTH_FILENAME = 'cli-auth.json';
|
|
109
|
+
const CREDENTIAL_PROVENANCES = ['user_login', 'daemon_broker'];
|
|
110
|
+
function parseProvenance(raw) {
|
|
111
|
+
return CREDENTIAL_PROVENANCES.includes(raw)
|
|
112
|
+
? raw
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
109
115
|
/**
|
|
110
116
|
* The config root — the same one `cli-config.json` (and so `cliId`) lives in.
|
|
111
117
|
*
|
|
@@ -253,8 +259,29 @@ function readRefreshFamilyFromFile() {
|
|
|
253
259
|
return {
|
|
254
260
|
refreshToken: bundle.refreshToken,
|
|
255
261
|
...(bundle.serverOrigin ? { serverOrigin: bundle.serverOrigin } : {}),
|
|
262
|
+
...(bundle.provenance ? { provenance: bundle.provenance } : {}),
|
|
256
263
|
};
|
|
257
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* The provenance this root's `cli-auth.json` records for `serverOrigin`, read
|
|
267
|
+
* from the plaintext field alone — no decrypt, no ownership rewrite. Used by a
|
|
268
|
+
* rotation to carry the marker forward.
|
|
269
|
+
*/
|
|
270
|
+
function storedFileProvenance(serverOrigin) {
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(fs.readFileSync(authFilePath(), 'utf-8'));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const provenance = parseProvenance(parsed?.provenance);
|
|
279
|
+
if (!provenance)
|
|
280
|
+
return undefined;
|
|
281
|
+
const stored = typeof parsed.serverOrigin === 'string' ? normalizeServerOrigin(parsed.serverOrigin) : null;
|
|
282
|
+
const next = serverOrigin ? normalizeServerOrigin(serverOrigin) : null;
|
|
283
|
+
return stored && next && stored === next ? provenance : undefined;
|
|
284
|
+
}
|
|
258
285
|
/**
|
|
259
286
|
* This root's file, refused when it belongs to another config; else, under a
|
|
260
287
|
* root override, the pre-override `$HOME/.skrr` file when it is positively ours.
|
|
@@ -349,6 +376,9 @@ function readFileAt(p, upgradeInPlace, opts = {}) {
|
|
|
349
376
|
}
|
|
350
377
|
if (owner)
|
|
351
378
|
bundle.owner = owner;
|
|
379
|
+
const provenance = parseProvenance(parsed.provenance);
|
|
380
|
+
if (provenance)
|
|
381
|
+
bundle.provenance = provenance;
|
|
352
382
|
// Best-effort upgrade: re-write the bundle so legacy plaintext fields
|
|
353
383
|
// become wrapped on the next read. A failure here doesn't break the
|
|
354
384
|
// read path — the caller already has the plaintext bundle.
|
|
@@ -413,6 +443,8 @@ function writeToFile(bundle) {
|
|
|
413
443
|
// Rewrites of an existing bundle (the envelope upgrade) keep its recorded
|
|
414
444
|
// owner; everything else is stamped with this config.
|
|
415
445
|
wireBundle.owner = bundle.owner ?? ownerFor(bundle);
|
|
446
|
+
if (bundle.provenance)
|
|
447
|
+
wireBundle.provenance = bundle.provenance;
|
|
416
448
|
// tmp + rename so no racing reader ever sees a partial file; chmod 0600
|
|
417
449
|
// on the final file so the same-UID blast-radius is as narrow as the
|
|
418
450
|
// file backend can offer.
|
|
@@ -772,6 +804,16 @@ function writeToBackend(bundle, options = {}) {
|
|
|
772
804
|
// Always stamp THIS config as the owner — a bundle read back and re-written
|
|
773
805
|
// (e.g. `pair`) must not carry a record forward from wherever it came from.
|
|
774
806
|
persistedBundle = { ...persistedBundle, owner: ownerFor(persistedBundle) };
|
|
807
|
+
// Provenance: an explicit one wins (a login or a broker mint creates a new
|
|
808
|
+
// family); otherwise this is a rotation of the stored family and keeps what
|
|
809
|
+
// the file recorded for the same server (OSK-12293).
|
|
810
|
+
const provenance = options.provenance ??
|
|
811
|
+
persistedBundle.provenance ??
|
|
812
|
+
storedFileProvenance(persistedBundle.serverOrigin);
|
|
813
|
+
if (provenance)
|
|
814
|
+
persistedBundle = { ...persistedBundle, provenance };
|
|
815
|
+
else
|
|
816
|
+
delete persistedBundle.provenance;
|
|
775
817
|
let backend;
|
|
776
818
|
if (keychain.isAvailable()) {
|
|
777
819
|
if (writeToKeychain(persistedBundle)) {
|
package/dist/lib/commitments.js
CHANGED
|
@@ -660,6 +660,7 @@ const STALLED_DIAGNOSES = {
|
|
|
660
660
|
observation_unreadable: 'the latest check could not read every configured source',
|
|
661
661
|
execution_budget_exhausted: 'recent planned work was skipped — the execution budget is exhausted',
|
|
662
662
|
no_advancement: 'no server-recorded advancement exists inside the declared window',
|
|
663
|
+
owner_action_blocked: 'a completed work run is blocked pending owner action',
|
|
663
664
|
contract_version_mismatch: 'the running contract is older than the authored one; its triggers have not resynced',
|
|
664
665
|
};
|
|
665
666
|
/**
|
|
@@ -329,7 +329,11 @@ export declare function findBrokeredHandoffDescriptor(profile?: string): LocalBo
|
|
|
329
329
|
* 1. the descriptor that just redeemed IS the Dedicated Runtime guest
|
|
330
330
|
* hand-off file (`/run/skrr-dedicated-runtime/...`),
|
|
331
331
|
* 2. `~/.skrr/cli-auth.json` reads cleanly and carries a refresh token,
|
|
332
|
-
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl
|
|
332
|
+
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl`,
|
|
333
|
+
* 4. it is not marked `provenance: 'user_login'`. Since OSK-12293 every
|
|
334
|
+
* explicit sign-in (`skrr login`, `skrr pair`) records that marker in
|
|
335
|
+
* the file, so a login the person made seconds ago is kept and used;
|
|
336
|
+
* an unmarked file was written by a CLI that predates the marker.
|
|
333
337
|
*
|
|
334
338
|
* When provenance can't be proven the file is KEPT: an explicit `skrr login`
|
|
335
339
|
* on a guest is rare but real, and a stale extra family is bounded by the
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -826,7 +826,11 @@ function findBrokeredHandoffDescriptor(profile = 'default') {
|
|
|
826
826
|
* 1. the descriptor that just redeemed IS the Dedicated Runtime guest
|
|
827
827
|
* hand-off file (`/run/skrr-dedicated-runtime/...`),
|
|
828
828
|
* 2. `~/.skrr/cli-auth.json` reads cleanly and carries a refresh token,
|
|
829
|
-
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl
|
|
829
|
+
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl`,
|
|
830
|
+
* 4. it is not marked `provenance: 'user_login'`. Since OSK-12293 every
|
|
831
|
+
* explicit sign-in (`skrr login`, `skrr pair`) records that marker in
|
|
832
|
+
* the file, so a login the person made seconds ago is kept and used;
|
|
833
|
+
* an unmarked file was written by a CLI that predates the marker.
|
|
830
834
|
*
|
|
831
835
|
* When provenance can't be proven the file is KEPT: an explicit `skrr login`
|
|
832
836
|
* on a guest is rare but real, and a stale extra family is bounded by the
|
|
@@ -869,6 +873,13 @@ function retirableCliAuthFamily(descriptorServerUrl) {
|
|
|
869
873
|
}
|
|
870
874
|
if (!stored?.refreshToken)
|
|
871
875
|
return null;
|
|
876
|
+
// A family a person signed in for on purpose is never a pre-broker
|
|
877
|
+
// leftover: `skrr login` / `skrr pair` stamp it, and a refresh carries the
|
|
878
|
+
// stamp forward. Retiring it undid an explicit guest login on the very next
|
|
879
|
+
// command (OSK-12293). Only an UNMARKED file — written by a CLI that
|
|
880
|
+
// predates the marker — or a daemon-minted one is retired.
|
|
881
|
+
if (stored.provenance === 'user_login')
|
|
882
|
+
return null;
|
|
872
883
|
const storedOrigin = stored.serverOrigin ? (0, auth_storage_1.normalizeServerOrigin)(stored.serverOrigin) : null;
|
|
873
884
|
const descriptorOrigin = descriptorServerUrl ? (0, auth_storage_1.normalizeServerOrigin)(descriptorServerUrl) : null;
|
|
874
885
|
if (!storedOrigin || !descriptorOrigin || storedOrigin !== descriptorOrigin)
|
|
@@ -987,7 +998,7 @@ async function attemptDaemonBrokerLoginAndPersist(opts) {
|
|
|
987
998
|
...(expiryToEpochMs(outcome.refreshExpiresAt) !== undefined
|
|
988
999
|
? { refreshExpiresAt: expiryToEpochMs(outcome.refreshExpiresAt) }
|
|
989
1000
|
: {}),
|
|
990
|
-
}, { serverOrigin, clearReauth: true });
|
|
1001
|
+
}, { serverOrigin, clearReauth: true, provenance: 'daemon_broker' });
|
|
991
1002
|
// Only now: the credential is on disk, so "the predecessor dies iff the
|
|
992
1003
|
// replacement is in place" holds. A server that predates the split ships no
|
|
993
1004
|
// `familyId` and has already revoked eagerly — nothing to do there.
|
|
@@ -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' | 'foreign-root' | 'mint-refused';
|
|
43
|
+
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused' | 'platform-managed';
|
|
44
44
|
}
|
|
45
45
|
export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
|
|
46
46
|
loginFlow?: string;
|
|
@@ -5,6 +5,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
5
5
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const daemon_handoff_opt_out_1 = require("./daemon-handoff-opt-out");
|
|
8
|
+
const dedicated_service_1 = require("./dedicated-service");
|
|
8
9
|
const exec_runtime_binary_1 = require("./exec-runtime-binary");
|
|
9
10
|
const keychain_1 = require("./keychain");
|
|
10
11
|
/**
|
|
@@ -196,6 +197,23 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
196
197
|
detail: 'the local daemon brokered this login, so it already has a working credential',
|
|
197
198
|
};
|
|
198
199
|
}
|
|
200
|
+
// A Dedicated Runtime guest's daemon belongs to the platform: the image
|
|
201
|
+
// installs it, the platform signs it in, and it runs as a uid the workload
|
|
202
|
+
// cannot touch. A person's login is theirs alone and is never handed to it,
|
|
203
|
+
// and there is nothing for them to install. Without this the workload found
|
|
204
|
+
// no daemon it could use and login ended by telling them to run
|
|
205
|
+
// `skrr daemon install` on a machine where that is wrong (OSK-12296).
|
|
206
|
+
//
|
|
207
|
+
// Detected the way the rest of the CLI detects a guest — the workload uid
|
|
208
|
+
// stamp, or the guest markers the image and daemon own, among them the
|
|
209
|
+
// hand-off descriptor directory under /run/skrr-dedicated-runtime.
|
|
210
|
+
if ((0, dedicated_service_1.runningOnDedicatedRuntimeGuest)(env)) {
|
|
211
|
+
return {
|
|
212
|
+
status: 'skipped',
|
|
213
|
+
reason: 'platform-managed',
|
|
214
|
+
detail: "this machine's daemon is managed by the platform",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
199
217
|
// A relocated config root (`SKRR_CONFIG_DIR`) is a second sign-in on this
|
|
200
218
|
// machine, not the machine's sign-in. The installed daemon service carries no
|
|
201
219
|
// config-root override, so it reads the DEFAULT root — its credential, its
|
package/dist/lib/login.js
CHANGED
|
@@ -332,7 +332,7 @@ async function cliLoginWithRefreshToken(opts) {
|
|
|
332
332
|
...(pair.accessExpiresAt !== undefined ? { expiresAt: pair.accessExpiresAt } : {}),
|
|
333
333
|
refreshToken: pair.refreshToken,
|
|
334
334
|
...(pair.refreshExpiresAt !== undefined ? { refreshExpiresAt: pair.refreshExpiresAt } : {}),
|
|
335
|
-
}, { serverOrigin: serverUrl, clearReauth: true });
|
|
335
|
+
}, { serverOrigin: serverUrl, clearReauth: true, provenance: 'user_login' });
|
|
336
336
|
return {
|
|
337
337
|
token: accessToken,
|
|
338
338
|
refreshToken: pair.refreshToken,
|
|
@@ -490,7 +490,7 @@ async function runDeviceCode(serverUrl, cliId, reason, ssh, errorMessage) {
|
|
|
490
490
|
: {}),
|
|
491
491
|
}
|
|
492
492
|
: {}),
|
|
493
|
-
}, { serverOrigin: serverUrl, clearReauth: true });
|
|
493
|
+
}, { serverOrigin: serverUrl, clearReauth: true, provenance: 'user_login' });
|
|
494
494
|
console.log(' ✓ Authorized.');
|
|
495
495
|
return {
|
|
496
496
|
token: approved.token,
|
package/dist/lib/oauthLogin.js
CHANGED
|
@@ -366,7 +366,9 @@ async function runOAuthLogin(opts) {
|
|
|
366
366
|
// server URL — daemonBroker.ts:543, login.ts:322, refresh.ts:282 —
|
|
367
367
|
// so this was the lone outlier, and it only bites a deployment
|
|
368
368
|
// whose web and API origins differ.
|
|
369
|
-
|
|
369
|
+
// A person signed in on purpose: a brokered Dedicated guest must
|
|
370
|
+
// keep this family rather than retire it as pre-broker (OSK-12293).
|
|
371
|
+
{ serverOrigin: requestedServerUrl, clearReauth: true, provenance: 'user_login' });
|
|
370
372
|
console.log(' ✓ Login successful! Token saved.');
|
|
371
373
|
finish({
|
|
372
374
|
ok: true,
|