@skrr-ai/cli 0.1.50 → 0.1.52

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.
Files changed (131) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +95 -8
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +8 -4
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +32 -0
  9. package/dist/commands/agents/update.js +81 -9
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/commitments/doctor.d.ts +1 -0
  13. package/dist/commands/commitments/doctor.js +19 -4
  14. package/dist/commands/commitments/effective-policy.js +15 -1
  15. package/dist/commands/commitments/next-tick.js +12 -2
  16. package/dist/commands/commitments/preflight.js +10 -0
  17. package/dist/commands/commitments/show.js +11 -1
  18. package/dist/commands/context.js +2 -1
  19. package/dist/commands/daemon/config.d.ts +15 -0
  20. package/dist/commands/daemon/config.js +53 -0
  21. package/dist/commands/daemon/index.js +1 -0
  22. package/dist/commands/daemon/status.js +15 -1
  23. package/dist/commands/doctor.js +8 -1
  24. package/dist/commands/harnesses/models.d.ts +54 -0
  25. package/dist/commands/harnesses/models.js +100 -7
  26. package/dist/commands/harnesses/show.d.ts +11 -0
  27. package/dist/commands/harnesses/show.js +35 -0
  28. package/dist/commands/harnesses/update.d.ts +28 -1
  29. package/dist/commands/harnesses/update.js +73 -10
  30. package/dist/commands/login.d.ts +2 -1
  31. package/dist/commands/login.js +26 -7
  32. package/dist/commands/logout.js +3 -0
  33. package/dist/commands/machines/dedicated/show.js +25 -2
  34. package/dist/commands/outbox/list.d.ts +12 -1
  35. package/dist/commands/outbox/list.js +31 -2
  36. package/dist/commands/revoke-daemon.js +9 -0
  37. package/dist/commands/spaces/create.d.ts +1 -0
  38. package/dist/commands/spaces/create.js +17 -0
  39. package/dist/commands/spaces/show.js +5 -0
  40. package/dist/commands/spaces/update.d.ts +1 -0
  41. package/dist/commands/spaces/update.js +16 -0
  42. package/dist/commands/tasks/create.d.ts +6 -0
  43. package/dist/commands/tasks/create.js +31 -6
  44. package/dist/commands/tasks/show.js +45 -0
  45. package/dist/commands/tasks/start.d.ts +14 -0
  46. package/dist/commands/tasks/start.js +31 -1
  47. package/dist/commands/tasks/update.d.ts +1 -0
  48. package/dist/commands/tasks/update.js +29 -1
  49. package/dist/commands/tasks/upsert.d.ts +1 -0
  50. package/dist/commands/tasks/upsert.js +16 -0
  51. package/dist/commands/whoami.js +5 -3
  52. package/dist/lib/agent-config.d.ts +2 -0
  53. package/dist/lib/agent-config.js +14 -10
  54. package/dist/lib/agentic-stream.d.ts +88 -1
  55. package/dist/lib/agentic-stream.js +330 -9
  56. package/dist/lib/auth-core-init.d.ts +2 -10
  57. package/dist/lib/auth-core-init.js +20 -67
  58. package/dist/lib/auth-failure.d.ts +33 -0
  59. package/dist/lib/auth-failure.js +68 -0
  60. package/dist/lib/auth-storage.d.ts +24 -0
  61. package/dist/lib/auth-storage.js +342 -11
  62. package/dist/lib/cli-id.d.ts +22 -0
  63. package/dist/lib/cli-id.js +56 -0
  64. package/dist/lib/cli-identity.d.ts +49 -0
  65. package/dist/lib/cli-identity.js +159 -0
  66. package/dist/lib/commitments.d.ts +25 -1
  67. package/dist/lib/commitments.js +46 -1
  68. package/dist/lib/config.d.ts +17 -0
  69. package/dist/lib/config.js +15 -3
  70. package/dist/lib/daemonBroker.d.ts +37 -1
  71. package/dist/lib/daemonBroker.js +146 -22
  72. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  73. package/dist/lib/daemonBrokerRefusal.js +127 -0
  74. package/dist/lib/dedicated-machines.d.ts +33 -2
  75. package/dist/lib/dedicated-machines.js +50 -1
  76. package/dist/lib/delegated-cli.js +20 -1
  77. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  78. package/dist/lib/exec-runtime-binary.js +4 -2
  79. package/dist/lib/first-party-harness-broker.js +10 -5
  80. package/dist/lib/harnesses.d.ts +10 -0
  81. package/dist/lib/keychain.d.ts +39 -0
  82. package/dist/lib/keychain.js +228 -39
  83. package/dist/lib/label-ref.d.ts +34 -0
  84. package/dist/lib/label-ref.js +68 -1
  85. package/dist/lib/live-runtime-binary.d.ts +40 -0
  86. package/dist/lib/live-runtime-binary.js +193 -0
  87. package/dist/lib/login.d.ts +23 -0
  88. package/dist/lib/login.js +67 -21
  89. package/dist/lib/message-intent.js +3 -1
  90. package/dist/lib/oauthLogin.js +2 -8
  91. package/dist/lib/refresh.js +6 -2
  92. package/dist/lib/tasks.d.ts +20 -0
  93. package/dist/lib/tasks.js +87 -8
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  117. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  118. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  119. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  120. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  128. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  129. package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
  130. package/oclif.manifest.json +28591 -28438
  131. package/package.json +1 -1
@@ -212,9 +212,11 @@ function notInstalledMessage() {
212
212
  * preserving "exit code = the daemon's exit code" semantics that scripts
213
213
  * may depend on.
214
214
  */
215
- function execRuntimeBinary(args) {
215
+ function execRuntimeBinary(args, opts = {}) {
216
216
  return new Promise((resolve, reject) => {
217
- const bin = findRuntimeBinary();
217
+ // `binary` lets `status` render from the LIVE daemon's program rather than
218
+ // the first one on PATH (OSK-12102); every other verb forwards to PATH.
219
+ const bin = opts.binary ?? findRuntimeBinary();
218
220
  if (!bin) {
219
221
  reject(new Error('runtime-binary-not-installed'));
220
222
  return;
@@ -73,6 +73,7 @@ const api_fetch_1 = require("./api-fetch");
73
73
  const auth_storage_1 = require("./auth-storage");
74
74
  const auth_core_init_1 = require("./auth-core-init");
75
75
  const config_1 = require("./config");
76
+ const cli_id_1 = require("./cli-id");
76
77
  const cred_envelope_1 = require("./cred-envelope");
77
78
  const device_id_1 = require("./device-id");
78
79
  const first_party_harness_1 = require("./first-party-harness");
@@ -259,7 +260,10 @@ async function acquireManagedSession(input = {}, deps = {}) {
259
260
  sessionToken,
260
261
  model: input.model?.trim() || AUTOMATIC_MODEL,
261
262
  localRuntimeId: (deps.localRuntimeId ?? localRuntimeId)(),
262
- machineUuid: (deps.machineUuid ?? auth_core_1.ensureMachineDaemonId)(),
263
+ // The hardware id every Daemon row is keyed on — the same function
264
+ // the daemon calls — not the minted `~/.skrr/daemon.id` UUIDv7,
265
+ // so the reservation's attribution joins to this machine (OSK-12063).
266
+ machineUuid: (deps.machineUuid ?? auth_core_1.getMachineUuid)(),
263
267
  }, fetchImpl, acquisition.signal);
264
268
  }
265
269
  catch (err) {
@@ -856,9 +860,10 @@ function hasLocalCredential() {
856
860
  /**
857
861
  * This installation's runtime id.
858
862
  *
859
- * Persisted at first login as `cliId`; regenerated deterministically when absent
860
- * (same hostname+user always yields the same value), matching `daemonBroker.ts`
861
- * and `refresh.ts`. Normalized to the `cli_` namespace the server requires,
863
+ * The id login and refresh present, read through `resolveCliId` (a named
864
+ * profile may have its own, OSK-12156); regenerated deterministically as the
865
+ * machine id when nothing is stored (same hostname+user always yields the same
866
+ * value). Normalized to the `cli_` namespace the server requires,
862
867
  * because `OVERSKY_CLI_ID` is overlaid verbatim with no format check and a
863
868
  * hand-set value would otherwise come back as an opaque
864
869
  * `runtime-id-namespace-mismatch`.
@@ -866,7 +871,7 @@ function hasLocalCredential() {
866
871
  function localRuntimeId() {
867
872
  const configured = (() => {
868
873
  try {
869
- return (0, config_1.loadConfig)().cliId?.trim();
874
+ return (0, cli_id_1.resolveCliId)((0, config_1.loadConfig)());
870
875
  }
871
876
  catch {
872
877
  return undefined;
@@ -64,6 +64,16 @@ export interface Harness {
64
64
  version?: number;
65
65
  claudeEngine?: 'cli' | 'sdk';
66
66
  lastSeenAt?: string;
67
+ /** This harness's own stored permission-mode fallback, when set. */
68
+ defaultPermissionMode?: string | null;
69
+ /**
70
+ * The permission modes this harness can honour, and where the answer came
71
+ * from — sent by `GET /api/harnesses/:id` for a local row (OSK-12130).
72
+ */
73
+ permissionModes?: {
74
+ modes: string[];
75
+ source: 'daemon' | 'daemon_record' | 'static' | 'vocabulary' | string;
76
+ };
67
77
  /**
68
78
  * Registration metadata. A managed machine's rows carry `machineProduct` and
69
79
  * `machineLeaseId`, which is the only link from a lease to the harness rows its
@@ -11,6 +11,19 @@ export declare function setActiveProfile(profile: string): void;
11
11
  */
12
12
  export declare function isProfileResolved(): boolean;
13
13
  export declare function getActiveProfile(): string;
14
+ /** The resolved config root this process's credentials belong to. */
15
+ export declare function resolvedConfigRoot(): string;
16
+ /** Is `root` this account's default skrr root, the owner of the legacy slot? */
17
+ export declare function isDefaultConfigRoot(root?: string): boolean;
18
+ /** A slot other than this root's own. */
19
+ export type ForeignSlot = {
20
+ kind: 'legacy';
21
+ } | {
22
+ kind: 'displaced';
23
+ cliId: string;
24
+ };
25
+ /** True when this root's own slot IS the historical profile-only slot. */
26
+ export declare function isLegacySlotCurrent(): boolean;
14
27
  /** @internal exposed for tests / diagnostics. */
15
28
  export declare function __currentServiceForTest(): string;
16
29
  /**
@@ -47,6 +60,32 @@ export declare function readServerOrigin(): string | null;
47
60
  export declare function writeServerOrigin(origin: string): boolean;
48
61
  /** Remove the issuing-origin companion slot. */
49
62
  export declare function deleteServerOrigin(): void;
63
+ /** Raw owner record (`{configRoot, cliId}` JSON) of this root's own slot. */
64
+ export declare function readOwner(): string | null;
65
+ export declare function writeOwner(owner: string): boolean;
66
+ export declare function deleteOwner(): void;
67
+ /** Every value a CLI bundle keeps in the Keychain, read from one service. */
68
+ export interface KeychainBundleSlots {
69
+ token: KeychainEntry;
70
+ refresh: KeychainEntry | null;
71
+ serverOrigin: string | null;
72
+ owner: string | null;
73
+ }
74
+ /**
75
+ * Read a whole bundle from a slot this root does not own (the legacy slot, or a
76
+ * displaced one). The caller decides whether it is ours; nothing here rewrites
77
+ * a slot on another config's behalf.
78
+ */
79
+ export declare function readBundleAt(slot: ForeignSlot): KeychainBundleSlots | null;
80
+ /**
81
+ * Copy every item of one slot to another BYTE FOR BYTE (the displacement
82
+ * rescue). Raw on purpose: the secret items may be sealed under the owning
83
+ * config's envelope key, and re-sealing them under ours would make the copy
84
+ * unreadable to the one config it exists for.
85
+ */
86
+ export declare function copyBundle(from: ForeignSlot, to: ForeignSlot): boolean;
87
+ /** Remove every CLI item under a slot this root does not own. The caller proves ownership. */
88
+ export declare function deleteAllAt(slot: ForeignSlot): void;
50
89
  export declare function deleteToken(): void;
51
90
  export declare function deleteRefreshToken(): void;
52
91
  /**
@@ -1,9 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.DEFAULT_PROFILE = void 0;
4
37
  exports.setActiveProfile = setActiveProfile;
5
38
  exports.isProfileResolved = isProfileResolved;
6
39
  exports.getActiveProfile = getActiveProfile;
40
+ exports.resolvedConfigRoot = resolvedConfigRoot;
41
+ exports.isDefaultConfigRoot = isDefaultConfigRoot;
42
+ exports.isLegacySlotCurrent = isLegacySlotCurrent;
7
43
  exports.__currentServiceForTest = __currentServiceForTest;
8
44
  exports.isAvailable = isAvailable;
9
45
  exports.resetAvailability = resetAvailability;
@@ -14,6 +50,12 @@ exports.writeRefreshToken = writeRefreshToken;
14
50
  exports.readServerOrigin = readServerOrigin;
15
51
  exports.writeServerOrigin = writeServerOrigin;
16
52
  exports.deleteServerOrigin = deleteServerOrigin;
53
+ exports.readOwner = readOwner;
54
+ exports.writeOwner = writeOwner;
55
+ exports.deleteOwner = deleteOwner;
56
+ exports.readBundleAt = readBundleAt;
57
+ exports.copyBundle = copyBundle;
58
+ exports.deleteAllAt = deleteAllAt;
17
59
  exports.deleteToken = deleteToken;
18
60
  exports.deleteRefreshToken = deleteRefreshToken;
19
61
  exports.deleteLegacyBrandEntries = deleteLegacyBrandEntries;
@@ -42,11 +84,39 @@ exports.resolveProfileFromArgv = resolveProfileFromArgv;
42
84
  * account = "cli-refresh" → refresh JWT
43
85
  * account = "cli-refresh-expires" → epoch ms expiry, optional
44
86
  * account = "cli-server-origin" → issuing API origin (non-secret)
87
+ * account = "cli-owner" → JSON {configRoot, cliId} of the config
88
+ * that wrote this bundle (non-secret)
89
+ *
90
+ * OSK-12135 — the slot is namespaced by CONFIG ROOT as well as profile. The
91
+ * login Keychain is one per macOS user, but the CLI identity (`cliId`) lives in
92
+ * the config root, so a second skrr setup on the same Mac (a different HOME or
93
+ * `SKRR_CONFIG_DIR`) with the same profile name used to write into this
94
+ * profile's slot, and the next refresh presented a token bound to the other
95
+ * setup's `cliId` (`stored=cli_devin_dogfood presented=cli_81ff3efd4ff8`).
96
+ *
97
+ * - The account's DEFAULT root (`<passwd home>/.skrr`) keeps the historical
98
+ * service name `ai.skrr.cli.<profile>`, so a normal single-config machine
99
+ * reads exactly the slot it always has — no migration, no re-login, and an
100
+ * older skrr binary sharing that config keeps interoperating.
101
+ * - Any other root writes `ai.skrr.cli.<profile>.root-<sha256(root)[:12]>`,
102
+ * a region no other root can name. It may READ the legacy slot as a
103
+ * migration, and only when that bundle positively belongs to it (see
104
+ * `auth-storage.ts`).
105
+ * - `ai.skrr.cli.<profile>.cli-<cliId>` holds a credential the default root
106
+ * DISPLACED from its own slot because an older binary of another config
107
+ * wrote it there. It is keyed by the identity the server bound it to, so
108
+ * only that config can adopt it, and a login destroys nobody's credential.
109
+ *
110
+ * The passwd home (not `$HOME`) defines "default", because a setup that swaps
111
+ * `$HOME` is exactly the second config this protects against.
45
112
  *
46
113
  * On non-darwin (Linux/CI) every function is a no-op / null-return so the
47
114
  * caller cleanly falls back to the file-based `cli-auth.json` backend.
48
115
  */
49
116
  const node_child_process_1 = require("node:child_process");
117
+ const node_crypto_1 = require("node:crypto");
118
+ const os = __importStar(require("node:os"));
119
+ const path = __importStar(require("node:path"));
50
120
  const cred_envelope_1 = require("./cred-envelope");
51
121
  const auth_core_1 = require("@skrr-ai/auth-core");
52
122
  /** Service-name prefix. Always followed by `.<profile>`. */
@@ -58,6 +128,7 @@ const ACCOUNT_EXPIRES = 'cli-token-expires';
58
128
  const ACCOUNT_REFRESH = 'cli-refresh';
59
129
  const ACCOUNT_REFRESH_EXPIRES = 'cli-refresh-expires';
60
130
  const ACCOUNT_SERVER_ORIGIN = 'cli-server-origin';
131
+ const ACCOUNT_OWNER = 'cli-owner';
61
132
  /** macOS `security` exit code when the requested item does not exist. */
62
133
  const SECURITY_ITEM_NOT_FOUND = 44;
63
134
  /** Cached availability — probed once at first call. */
@@ -95,9 +166,55 @@ function isProfileResolved() {
95
166
  function getActiveProfile() {
96
167
  return _activeProfile;
97
168
  }
98
- function currentService() {
169
+ /** The resolved config root this process's credentials belong to. */
170
+ function resolvedConfigRoot() {
171
+ return path.resolve((0, auth_core_1.resolveConfigRoot)());
172
+ }
173
+ /**
174
+ * The account's home from the user database, NOT `$HOME`. A second skrr setup
175
+ * that swaps `$HOME` must not count as the default root, or it would share the
176
+ * historical slot with the real one — the incident this namespacing exists for.
177
+ */
178
+ function accountHome() {
179
+ try {
180
+ const home = os.userInfo().homedir;
181
+ if (home)
182
+ return home;
183
+ }
184
+ catch {
185
+ /* no passwd entry (some containers) — fall through */
186
+ }
187
+ return os.homedir();
188
+ }
189
+ /** Is `root` this account's default skrr root, the owner of the legacy slot? */
190
+ function isDefaultConfigRoot(root = resolvedConfigRoot()) {
191
+ return path.resolve(root) === path.resolve(accountHome(), '.skrr');
192
+ }
193
+ function legacyService() {
99
194
  return `${SERVICE_BASE}.${_activeProfile}`;
100
195
  }
196
+ function currentService() {
197
+ const root = resolvedConfigRoot();
198
+ if (isDefaultConfigRoot(root))
199
+ return legacyService();
200
+ const digest = (0, node_crypto_1.createHash)('sha256').update(root).digest('hex').slice(0, 12);
201
+ return `${legacyService()}.root-${digest}`;
202
+ }
203
+ function displacedService(cliId) {
204
+ // A cliId is `cli_<hex>` in practice; anything else is hashed so the service
205
+ // name never carries characters `security -i` quoting has to worry about.
206
+ const safe = /^[A-Za-z0-9_-]{1,64}$/.test(cliId)
207
+ ? cliId
208
+ : (0, node_crypto_1.createHash)('sha256').update(cliId).digest('hex').slice(0, 16);
209
+ return `${legacyService()}.cli-${safe}`;
210
+ }
211
+ function serviceForSlot(slot) {
212
+ return slot.kind === 'legacy' ? legacyService() : displacedService(slot.cliId);
213
+ }
214
+ /** True when this root's own slot IS the historical profile-only slot. */
215
+ function isLegacySlotCurrent() {
216
+ return currentService() === legacyService();
217
+ }
101
218
  /** @internal exposed for tests / diagnostics. */
102
219
  function __currentServiceForTest() {
103
220
  return currentService();
@@ -140,7 +257,11 @@ function resetAvailability() {
140
257
  function isSecretAccount(account) {
141
258
  return account === ACCOUNT_TOKEN || account === ACCOUNT_REFRESH;
142
259
  }
143
- function readSlot(account, service = currentService()) {
260
+ /**
261
+ * `raw` returns the stored string exactly, with no envelope transform — for
262
+ * copying another config's item, which may be sealed under a key we lack.
263
+ */
264
+ function readSlot(account, service = currentService(), raw_ = false) {
144
265
  let raw;
145
266
  try {
146
267
  const out = (0, node_child_process_1.execFileSync)('security', ['find-generic-password', '-s', service, '-a', account, '-w'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
@@ -161,7 +282,7 @@ function readSlot(account, service = currentService()) {
161
282
  }
162
283
  return null;
163
284
  }
164
- if (!isSecretAccount(account))
285
+ if (raw_ || !isSecretAccount(account))
165
286
  return raw;
166
287
  // L12 — route secret slots through the envelope.
167
288
  // - Envelope on disk + path active → returns plaintext.
@@ -172,7 +293,10 @@ function readSlot(account, service = currentService()) {
172
293
  const result = (0, cred_envelope_1.maybeDecryptOnRead)(raw);
173
294
  if (result.plaintext === null)
174
295
  return null;
175
- if (result.needsMigration) {
296
+ // Only this root's OWN slot is upgraded. A slot read as a migration source may
297
+ // belong to another config, whose envelope key this process does not hold —
298
+ // sealing it under ours would make it unreadable to its owner (OSK-12135).
299
+ if (result.needsMigration && service === currentService()) {
176
300
  // Best-effort upgrade — a failure here doesn't break the read; the
177
301
  // slot just stays plaintext and we'll try again on the next read.
178
302
  try {
@@ -191,11 +315,11 @@ function readSlot(account, service = currentService()) {
191
315
  function quoteForSecurityInteractive(v) {
192
316
  return '"' + v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
193
317
  }
194
- function writeSlot(account, value, service = currentService()) {
318
+ function writeSlot(account, value, service = currentService(), raw = false) {
195
319
  try {
196
320
  // L12 — wrap secret-bearing slots through the envelope when active.
197
- // Empty / non-secret slots are passthrough.
198
- const wireValue = isSecretAccount(account) ? (0, cred_envelope_1.maybeEncryptForWrite)(value) : value;
321
+ // Empty / non-secret slots are passthrough, and so is a raw copy.
322
+ const wireValue = !raw && isSecretAccount(account) ? (0, cred_envelope_1.maybeEncryptForWrite)(value) : value;
199
323
  // -i reads commands from stdin so the JWT never appears in argv (it
200
324
  // would otherwise be visible to same-UID `ps` observers). -U updates
201
325
  // in place instead of failing with "item already exists".
@@ -231,13 +355,11 @@ function deleteSlot(account, service = currentService()) {
231
355
  }
232
356
  }
233
357
  }
234
- function readToken() {
235
- if (!isAvailable())
236
- return null;
237
- const token = readSlot(ACCOUNT_TOKEN);
358
+ function readEntryAt(service, account, expiresAccount) {
359
+ const token = readSlot(account, service);
238
360
  if (!token)
239
361
  return null;
240
- const expiresRaw = readSlot(ACCOUNT_EXPIRES);
362
+ const expiresRaw = readSlot(expiresAccount, service);
241
363
  let expiresAt;
242
364
  if (expiresRaw) {
243
365
  const parsed = Number.parseInt(expiresRaw, 10);
@@ -246,51 +368,41 @@ function readToken() {
246
368
  }
247
369
  return expiresAt !== undefined ? { token, expiresAt } : { token };
248
370
  }
249
- function writeToken(token, expiresAt) {
250
- if (!isAvailable())
251
- return false;
252
- if (!token)
253
- return false;
254
- if (!writeSlot(ACCOUNT_TOKEN, token))
371
+ function writeEntryAt(service, account, expiresAccount, token, expiresAt) {
372
+ if (!writeSlot(account, token, service))
255
373
  return false;
256
374
  if (expiresAt && Number.isFinite(expiresAt)) {
257
- writeSlot(ACCOUNT_EXPIRES, String(Math.trunc(expiresAt)));
375
+ writeSlot(expiresAccount, String(Math.trunc(expiresAt)), service);
258
376
  }
259
377
  else {
260
378
  // Stale expiry from a prior write would lie about the token's lifetime.
261
- deleteSlot(ACCOUNT_EXPIRES);
379
+ deleteSlot(expiresAccount, service);
262
380
  }
263
381
  return true;
264
382
  }
265
- function readRefreshToken() {
383
+ function readToken() {
266
384
  if (!isAvailable())
267
385
  return null;
268
- const token = readSlot(ACCOUNT_REFRESH);
386
+ return readEntryAt(currentService(), ACCOUNT_TOKEN, ACCOUNT_EXPIRES);
387
+ }
388
+ function writeToken(token, expiresAt) {
389
+ if (!isAvailable())
390
+ return false;
269
391
  if (!token)
392
+ return false;
393
+ return writeEntryAt(currentService(), ACCOUNT_TOKEN, ACCOUNT_EXPIRES, token, expiresAt);
394
+ }
395
+ function readRefreshToken() {
396
+ if (!isAvailable())
270
397
  return null;
271
- const expiresRaw = readSlot(ACCOUNT_REFRESH_EXPIRES);
272
- let expiresAt;
273
- if (expiresRaw) {
274
- const parsed = Number.parseInt(expiresRaw, 10);
275
- if (Number.isFinite(parsed) && parsed > 0)
276
- expiresAt = parsed;
277
- }
278
- return expiresAt !== undefined ? { token, expiresAt } : { token };
398
+ return readEntryAt(currentService(), ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES);
279
399
  }
280
400
  function writeRefreshToken(token, expiresAt) {
281
401
  if (!isAvailable())
282
402
  return false;
283
403
  if (!token)
284
404
  return false;
285
- if (!writeSlot(ACCOUNT_REFRESH, token))
286
- return false;
287
- if (expiresAt && Number.isFinite(expiresAt)) {
288
- writeSlot(ACCOUNT_REFRESH_EXPIRES, String(Math.trunc(expiresAt)));
289
- }
290
- else {
291
- deleteSlot(ACCOUNT_REFRESH_EXPIRES);
292
- }
293
- return true;
405
+ return writeEntryAt(currentService(), ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES, token, expiresAt);
294
406
  }
295
407
  /**
296
408
  * Read the API origin that issued the active token bundle.
@@ -321,6 +433,82 @@ function deleteServerOrigin() {
321
433
  return;
322
434
  deleteSlot(ACCOUNT_SERVER_ORIGIN);
323
435
  }
436
+ /** Raw owner record (`{configRoot, cliId}` JSON) of this root's own slot. */
437
+ function readOwner() {
438
+ if (!isAvailable())
439
+ return null;
440
+ const value = readSlot(ACCOUNT_OWNER);
441
+ return value && value.length > 0 ? value : null;
442
+ }
443
+ function writeOwner(owner) {
444
+ if (!isAvailable())
445
+ return false;
446
+ if (!owner)
447
+ return false;
448
+ return writeSlot(ACCOUNT_OWNER, owner);
449
+ }
450
+ function deleteOwner() {
451
+ if (!isAvailable())
452
+ return;
453
+ deleteSlot(ACCOUNT_OWNER);
454
+ }
455
+ /**
456
+ * Read a whole bundle from a slot this root does not own (the legacy slot, or a
457
+ * displaced one). The caller decides whether it is ours; nothing here rewrites
458
+ * a slot on another config's behalf.
459
+ */
460
+ function readBundleAt(slot) {
461
+ if (!isAvailable())
462
+ return null;
463
+ const service = serviceForSlot(slot);
464
+ const token = readEntryAt(service, ACCOUNT_TOKEN, ACCOUNT_EXPIRES);
465
+ if (!token)
466
+ return null;
467
+ return {
468
+ token,
469
+ refresh: readEntryAt(service, ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES),
470
+ serverOrigin: readSlot(ACCOUNT_SERVER_ORIGIN, service) || null,
471
+ owner: readSlot(ACCOUNT_OWNER, service) || null,
472
+ };
473
+ }
474
+ const BUNDLE_ACCOUNTS = [
475
+ ACCOUNT_TOKEN,
476
+ ACCOUNT_EXPIRES,
477
+ ACCOUNT_REFRESH,
478
+ ACCOUNT_REFRESH_EXPIRES,
479
+ ACCOUNT_SERVER_ORIGIN,
480
+ ACCOUNT_OWNER,
481
+ ];
482
+ /**
483
+ * Copy every item of one slot to another BYTE FOR BYTE (the displacement
484
+ * rescue). Raw on purpose: the secret items may be sealed under the owning
485
+ * config's envelope key, and re-sealing them under ours would make the copy
486
+ * unreadable to the one config it exists for.
487
+ */
488
+ function copyBundle(from, to) {
489
+ if (!isAvailable())
490
+ return false;
491
+ const source = serviceForSlot(from);
492
+ const target = serviceForSlot(to);
493
+ if (readSlot(ACCOUNT_TOKEN, source, true) === null)
494
+ return false;
495
+ for (const account of BUNDLE_ACCOUNTS) {
496
+ const value = readSlot(account, source, true);
497
+ if (value === null)
498
+ deleteSlot(account, target);
499
+ else if (!writeSlot(account, value, target, true))
500
+ return false;
501
+ }
502
+ return true;
503
+ }
504
+ /** Remove every CLI item under a slot this root does not own. The caller proves ownership. */
505
+ function deleteAllAt(slot) {
506
+ if (!isAvailable())
507
+ return;
508
+ const service = serviceForSlot(slot);
509
+ for (const account of BUNDLE_ACCOUNTS)
510
+ deleteSlot(account, service);
511
+ }
324
512
  function deleteToken() {
325
513
  if (!isAvailable())
326
514
  return;
@@ -423,6 +611,7 @@ function deleteAll() {
423
611
  deleteToken();
424
612
  deleteRefreshToken();
425
613
  deleteServerOrigin();
614
+ deleteOwner();
426
615
  }
427
616
  /**
428
617
  * Pull `--profile` out of raw argv.
@@ -102,3 +102,37 @@ export declare class LabelRefError extends Error {
102
102
  * label ends up attached to a year of work.
103
103
  */
104
104
  export declare function resolveLabelRefs(refs: string[] | undefined, context: LabelRefContext): Promise<string[]>;
105
+ /**
106
+ * Help for a `--tag` flag that sits beside `--label` (OSK-12053).
107
+ *
108
+ * Tags and labels are two DIFFERENT vocabularies: a tag is any free string,
109
+ * stored as typed on `task.tags`; a label is declared (`skrr labels`) and is
110
+ * what `--label` filters, the board and the label guidance read. The flags used
111
+ * to be described as "Tag (repeatable)" and "Attach a task label", so a person
112
+ * wanting the `dogfood` label typed `--tag dogfood` on every task of a program
113
+ * and none of them was labelled.
114
+ */
115
+ export declare const TAG_FLAG_HELP: string;
116
+ export interface TagNamingLabel {
117
+ tag: string;
118
+ label: LabelRow;
119
+ }
120
+ /**
121
+ * The `--tag` values that equal a declared label's name, each paired with that
122
+ * label — the one case where the mistake is certain enough to say so.
123
+ *
124
+ * Best-effort by construction: it only ever feeds a WARNING, so a vocabulary
125
+ * that cannot be read yields no matches rather than an error, and the tags are
126
+ * stored as typed either way. Nothing is converted into a label implicitly —
127
+ * which one a person meant is theirs to say, and a warning that names the exact
128
+ * flag costs one retry.
129
+ */
130
+ export declare function tagsNamingLabels(tags: string[] | undefined, context: LabelVocabularyContext): Promise<TagNamingLabel[]>;
131
+ /** The stderr line for `tagsNamingLabels` hits; an empty string when there are none. */
132
+ export declare function describeTagsNamingLabels(hits: TagNamingLabel[]): string;
133
+ /**
134
+ * The whole `--tag` check a write command runs: the warning for tags that equal
135
+ * a declared label AND were not also attached through `--label` (passing both
136
+ * is someone doing it right). An empty string means say nothing.
137
+ */
138
+ export declare function tagLabelWarning(tags: string[] | undefined, attachedLabelIds: string[] | undefined, context: LabelVocabularyContext): Promise<string>;
@@ -33,11 +33,14 @@
33
33
  * their path.
34
34
  */
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.LabelRefError = void 0;
36
+ exports.TAG_FLAG_HELP = exports.LabelRefError = void 0;
37
37
  exports.looksLikeLabelId = looksLikeLabelId;
38
38
  exports.closestNames = closestNames;
39
39
  exports.collectLabelVocabulary = collectLabelVocabulary;
40
40
  exports.resolveLabelRefs = resolveLabelRefs;
41
+ exports.tagsNamingLabels = tagsNamingLabels;
42
+ exports.describeTagsNamingLabels = describeTagsNamingLabels;
43
+ exports.tagLabelWarning = tagLabelWarning;
41
44
  const data_provider_1 = require("@skrr-ai/data-provider");
42
45
  const workspaces_1 = require("./workspaces");
43
46
  /** A canonical v4 uuid, which is what every label id is. */
@@ -171,3 +174,67 @@ async function resolveLabelRefs(refs, context) {
171
174
  }
172
175
  return resolved;
173
176
  }
177
+ /**
178
+ * Help for a `--tag` flag that sits beside `--label` (OSK-12053).
179
+ *
180
+ * Tags and labels are two DIFFERENT vocabularies: a tag is any free string,
181
+ * stored as typed on `task.tags`; a label is declared (`skrr labels`) and is
182
+ * what `--label` filters, the board and the label guidance read. The flags used
183
+ * to be described as "Tag (repeatable)" and "Attach a task label", so a person
184
+ * wanting the `dogfood` label typed `--tag dogfood` on every task of a program
185
+ * and none of them was labelled.
186
+ */
187
+ exports.TAG_FLAG_HELP = 'Free-form tag (repeatable), stored exactly as typed and never checked against anything. ' +
188
+ 'NOT a label: to classify the task under a declared label (`labels list`), use --label.';
189
+ /**
190
+ * The `--tag` values that equal a declared label's name, each paired with that
191
+ * label — the one case where the mistake is certain enough to say so.
192
+ *
193
+ * Best-effort by construction: it only ever feeds a WARNING, so a vocabulary
194
+ * that cannot be read yields no matches rather than an error, and the tags are
195
+ * stored as typed either way. Nothing is converted into a label implicitly —
196
+ * which one a person meant is theirs to say, and a warning that names the exact
197
+ * flag costs one retry.
198
+ */
199
+ async function tagsNamingLabels(tags, context) {
200
+ const input = (tags ?? []).map((tag) => tag.trim()).filter(Boolean);
201
+ if (input.length === 0)
202
+ return [];
203
+ let vocabulary;
204
+ try {
205
+ vocabulary = await collectLabelVocabulary(context);
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ const hits = [];
211
+ for (const tag of input) {
212
+ const needle = tag.toLowerCase();
213
+ const label = vocabulary.find((row) => (row.name ?? '').toLowerCase() === needle);
214
+ if (label)
215
+ hits.push({ tag, label });
216
+ }
217
+ return hits;
218
+ }
219
+ /** The stderr line for `tagsNamingLabels` hits; an empty string when there are none. */
220
+ function describeTagsNamingLabels(hits) {
221
+ if (hits.length === 0)
222
+ return '';
223
+ const names = hits.map((hit) => `"${hit.tag}"`).join(', ');
224
+ const flags = hits.map((hit) => `--label ${hit.label.name}`).join(' ');
225
+ return (`--tag ${names} ${hits.length === 1 ? 'was' : 'were'} stored as free-form ` +
226
+ `${hits.length === 1 ? 'tag' : 'tags'}, NOT as the declared label of the same name, ` +
227
+ `so a label filter will not find this task. To attach the label, pass ${flags}.`);
228
+ }
229
+ /**
230
+ * The whole `--tag` check a write command runs: the warning for tags that equal
231
+ * a declared label AND were not also attached through `--label` (passing both
232
+ * is someone doing it right). An empty string means say nothing.
233
+ */
234
+ async function tagLabelWarning(tags, attachedLabelIds, context) {
235
+ if (!tags || tags.length === 0)
236
+ return '';
237
+ const attached = new Set(attachedLabelIds ?? []);
238
+ const hits = await tagsNamingLabels(tags, context);
239
+ return describeTagsNamingLabels(hits.filter((hit) => !attached.has(hit.label.id)));
240
+ }
@@ -0,0 +1,40 @@
1
+ export interface LiveProgram {
2
+ /** The executable the OS reports for the live pid. */
3
+ exec: string;
4
+ /** For an interpreter-launched daemon, the script it runs. */
5
+ script: string | null;
6
+ }
7
+ export interface LiveRuntimeDeps {
8
+ /** pid of the live daemon for `profile`, or null. */
9
+ livePid(profile: string): number | null;
10
+ /** The live pid's program, or null when the OS will not say. */
11
+ programOf(pid: number): LiveProgram | null;
12
+ realpath(p: string): string;
13
+ isExecutable(p: string): boolean;
14
+ }
15
+ export type StatusBinaryDecision = {
16
+ kind: 'same';
17
+ binary: string;
18
+ } | {
19
+ kind: 'unknown';
20
+ binary: string;
21
+ } | {
22
+ kind: 'forward-live';
23
+ binary: string;
24
+ pathBinary: string;
25
+ } | {
26
+ kind: 'mismatch';
27
+ binary: string;
28
+ live: string;
29
+ };
30
+ /** The profile a forwarded argv names, `default` otherwise. */
31
+ export declare function profileFromArgs(args: readonly string[]): string;
32
+ /**
33
+ * Decide which binary renders `status`. Never throws: a probe that cannot
34
+ * answer leaves the PATH binary in place (`unknown`).
35
+ */
36
+ export declare function decideStatusBinary(pathBinary: string, profile: string, deps: LiveRuntimeDeps): StatusBinaryDecision;
37
+ /** One stderr line naming the binary that rendered status, when it is not obvious. */
38
+ export declare function describeStatusBinary(decision: StatusBinaryDecision): string | null;
39
+ export declare function scriptFromArgs(exec: string, args: string): string | null;
40
+ export declare function defaultLiveRuntimeDeps(): LiveRuntimeDeps;