@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.
@@ -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
- if (typeof effective.mode === 'string' && effective.mode) {
58
- const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
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.log(`Ping ${result.status}${result.error ? `: ${result.error}` : ''}.`);
102
+ this.error(describePingFailure(result.status, result.error), { exit: 1 });
94
103
  }
95
104
  return;
96
105
  }
@@ -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)}.`);
@@ -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)
@@ -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
- answers[question.header ?? question.question] = await this.ask(`${question.header ?? 'Question'}: ${question.question}\n> `);
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
@@ -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)) {
@@ -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
@@ -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,
@@ -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
- { serverOrigin: requestedServerUrl, clearReauth: true });
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,