@skrr-ai/cli 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-command.d.ts +7 -0
- package/dist/base-command.js +38 -8
- package/dist/commands/agents/chat.js +2 -1
- package/dist/commands/agents/create.js +21 -8
- package/dist/commands/agents/update.d.ts +55 -0
- package/dist/commands/agents/update.js +119 -23
- package/dist/commands/commitments/next-tick.js +9 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.js +16 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/tasks/approvals/list.d.ts +22 -0
- package/dist/commands/tasks/approvals/list.js +80 -3
- package/dist/commands/tasks/create.d.ts +5 -0
- package/dist/commands/tasks/create.js +28 -7
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.js +30 -3
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +15 -0
- package/dist/lib/agent-config.js +46 -10
- package/dist/lib/agentic-stream.d.ts +21 -1
- package/dist/lib/agentic-stream.js +40 -6
- package/dist/lib/auth-failure.js +4 -1
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +348 -13
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +4 -0
- package/dist/lib/commitments.js +12 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/cred-envelope.d.ts +12 -1
- package/dist/lib/cred-envelope.js +20 -3
- package/dist/lib/daemonBroker.d.ts +26 -4
- package/dist/lib/daemonBroker.js +82 -10
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +53 -4
- package/dist/lib/dedicated-machines.js +95 -5
- package/dist/lib/delegated-cli.js +16 -0
- package/dist/lib/first-party-harness-broker.js +6 -4
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +254 -40
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +72 -1
- package/dist/lib/login.js +24 -12
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.js +33 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +9 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3138 -3100
- package/oclif.manifest.json +26470 -26449
- package/package.json +1 -1
package/dist/lib/keychain.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -169,10 +290,13 @@ function readSlot(account, service = currentService()) {
|
|
|
169
290
|
// re-login; the bundle is unrecoverable without the DEK).
|
|
170
291
|
// - Legacy plaintext + path active → returns plaintext + needsMigration;
|
|
171
292
|
// we re-write in place to upgrade the slot.
|
|
172
|
-
const result = (0, cred_envelope_1.maybeDecryptOnRead)(raw);
|
|
293
|
+
const result = (0, cred_envelope_1.maybeDecryptOnRead)(raw, { foreignOwnerRoot: foreignOwnerRootAt(service) });
|
|
173
294
|
if (result.plaintext === null)
|
|
174
295
|
return null;
|
|
175
|
-
|
|
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 {
|
|
@@ -184,6 +308,31 @@ function readSlot(account, service = currentService()) {
|
|
|
184
308
|
}
|
|
185
309
|
return result.plaintext;
|
|
186
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* The config root named by `service`'s owner record, when it is NOT this
|
|
313
|
+
* process's root; otherwise undefined. The record is non-secret JSON
|
|
314
|
+
* (`{configRoot, cliId}`), so it can be read before the secret it describes.
|
|
315
|
+
*
|
|
316
|
+
* Only a diagnostic input. Whether the bundle is ours is still decided by
|
|
317
|
+
* `auth-storage`'s `judgeOwnership`, which also weighs the token's own `did`
|
|
318
|
+
* — so the secret is decrypted as before, and only what a failure SAYS changes.
|
|
319
|
+
*/
|
|
320
|
+
function foreignOwnerRootAt(service) {
|
|
321
|
+
const raw = readSlot(ACCOUNT_OWNER, service);
|
|
322
|
+
if (!raw)
|
|
323
|
+
return undefined;
|
|
324
|
+
let root;
|
|
325
|
+
try {
|
|
326
|
+
root = JSON.parse(raw)?.configRoot;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
if (typeof root !== 'string' || root.length === 0)
|
|
332
|
+
return undefined;
|
|
333
|
+
const resolved = path.resolve(root);
|
|
334
|
+
return resolved === resolvedConfigRoot() ? undefined : resolved;
|
|
335
|
+
}
|
|
187
336
|
/**
|
|
188
337
|
* Escape a value for `security -i` (stdin interactive mode).
|
|
189
338
|
* `security` accepts double-quoted args with `\"` and `\\` backslash escapes.
|
|
@@ -191,11 +340,11 @@ function readSlot(account, service = currentService()) {
|
|
|
191
340
|
function quoteForSecurityInteractive(v) {
|
|
192
341
|
return '"' + v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
193
342
|
}
|
|
194
|
-
function writeSlot(account, value, service = currentService()) {
|
|
343
|
+
function writeSlot(account, value, service = currentService(), raw = false) {
|
|
195
344
|
try {
|
|
196
345
|
// 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;
|
|
346
|
+
// Empty / non-secret slots are passthrough, and so is a raw copy.
|
|
347
|
+
const wireValue = !raw && isSecretAccount(account) ? (0, cred_envelope_1.maybeEncryptForWrite)(value) : value;
|
|
199
348
|
// -i reads commands from stdin so the JWT never appears in argv (it
|
|
200
349
|
// would otherwise be visible to same-UID `ps` observers). -U updates
|
|
201
350
|
// in place instead of failing with "item already exists".
|
|
@@ -231,13 +380,11 @@ function deleteSlot(account, service = currentService()) {
|
|
|
231
380
|
}
|
|
232
381
|
}
|
|
233
382
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
return null;
|
|
237
|
-
const token = readSlot(ACCOUNT_TOKEN);
|
|
383
|
+
function readEntryAt(service, account, expiresAccount) {
|
|
384
|
+
const token = readSlot(account, service);
|
|
238
385
|
if (!token)
|
|
239
386
|
return null;
|
|
240
|
-
const expiresRaw = readSlot(
|
|
387
|
+
const expiresRaw = readSlot(expiresAccount, service);
|
|
241
388
|
let expiresAt;
|
|
242
389
|
if (expiresRaw) {
|
|
243
390
|
const parsed = Number.parseInt(expiresRaw, 10);
|
|
@@ -246,51 +393,41 @@ function readToken() {
|
|
|
246
393
|
}
|
|
247
394
|
return expiresAt !== undefined ? { token, expiresAt } : { token };
|
|
248
395
|
}
|
|
249
|
-
function
|
|
250
|
-
if (!
|
|
251
|
-
return false;
|
|
252
|
-
if (!token)
|
|
253
|
-
return false;
|
|
254
|
-
if (!writeSlot(ACCOUNT_TOKEN, token))
|
|
396
|
+
function writeEntryAt(service, account, expiresAccount, token, expiresAt) {
|
|
397
|
+
if (!writeSlot(account, token, service))
|
|
255
398
|
return false;
|
|
256
399
|
if (expiresAt && Number.isFinite(expiresAt)) {
|
|
257
|
-
writeSlot(
|
|
400
|
+
writeSlot(expiresAccount, String(Math.trunc(expiresAt)), service);
|
|
258
401
|
}
|
|
259
402
|
else {
|
|
260
403
|
// Stale expiry from a prior write would lie about the token's lifetime.
|
|
261
|
-
deleteSlot(
|
|
404
|
+
deleteSlot(expiresAccount, service);
|
|
262
405
|
}
|
|
263
406
|
return true;
|
|
264
407
|
}
|
|
265
|
-
function
|
|
408
|
+
function readToken() {
|
|
266
409
|
if (!isAvailable())
|
|
267
410
|
return null;
|
|
268
|
-
|
|
411
|
+
return readEntryAt(currentService(), ACCOUNT_TOKEN, ACCOUNT_EXPIRES);
|
|
412
|
+
}
|
|
413
|
+
function writeToken(token, expiresAt) {
|
|
414
|
+
if (!isAvailable())
|
|
415
|
+
return false;
|
|
269
416
|
if (!token)
|
|
417
|
+
return false;
|
|
418
|
+
return writeEntryAt(currentService(), ACCOUNT_TOKEN, ACCOUNT_EXPIRES, token, expiresAt);
|
|
419
|
+
}
|
|
420
|
+
function readRefreshToken() {
|
|
421
|
+
if (!isAvailable())
|
|
270
422
|
return null;
|
|
271
|
-
|
|
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 };
|
|
423
|
+
return readEntryAt(currentService(), ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES);
|
|
279
424
|
}
|
|
280
425
|
function writeRefreshToken(token, expiresAt) {
|
|
281
426
|
if (!isAvailable())
|
|
282
427
|
return false;
|
|
283
428
|
if (!token)
|
|
284
429
|
return false;
|
|
285
|
-
|
|
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;
|
|
430
|
+
return writeEntryAt(currentService(), ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES, token, expiresAt);
|
|
294
431
|
}
|
|
295
432
|
/**
|
|
296
433
|
* Read the API origin that issued the active token bundle.
|
|
@@ -321,6 +458,82 @@ function deleteServerOrigin() {
|
|
|
321
458
|
return;
|
|
322
459
|
deleteSlot(ACCOUNT_SERVER_ORIGIN);
|
|
323
460
|
}
|
|
461
|
+
/** Raw owner record (`{configRoot, cliId}` JSON) of this root's own slot. */
|
|
462
|
+
function readOwner() {
|
|
463
|
+
if (!isAvailable())
|
|
464
|
+
return null;
|
|
465
|
+
const value = readSlot(ACCOUNT_OWNER);
|
|
466
|
+
return value && value.length > 0 ? value : null;
|
|
467
|
+
}
|
|
468
|
+
function writeOwner(owner) {
|
|
469
|
+
if (!isAvailable())
|
|
470
|
+
return false;
|
|
471
|
+
if (!owner)
|
|
472
|
+
return false;
|
|
473
|
+
return writeSlot(ACCOUNT_OWNER, owner);
|
|
474
|
+
}
|
|
475
|
+
function deleteOwner() {
|
|
476
|
+
if (!isAvailable())
|
|
477
|
+
return;
|
|
478
|
+
deleteSlot(ACCOUNT_OWNER);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Read a whole bundle from a slot this root does not own (the legacy slot, or a
|
|
482
|
+
* displaced one). The caller decides whether it is ours; nothing here rewrites
|
|
483
|
+
* a slot on another config's behalf.
|
|
484
|
+
*/
|
|
485
|
+
function readBundleAt(slot) {
|
|
486
|
+
if (!isAvailable())
|
|
487
|
+
return null;
|
|
488
|
+
const service = serviceForSlot(slot);
|
|
489
|
+
const token = readEntryAt(service, ACCOUNT_TOKEN, ACCOUNT_EXPIRES);
|
|
490
|
+
if (!token)
|
|
491
|
+
return null;
|
|
492
|
+
return {
|
|
493
|
+
token,
|
|
494
|
+
refresh: readEntryAt(service, ACCOUNT_REFRESH, ACCOUNT_REFRESH_EXPIRES),
|
|
495
|
+
serverOrigin: readSlot(ACCOUNT_SERVER_ORIGIN, service) || null,
|
|
496
|
+
owner: readSlot(ACCOUNT_OWNER, service) || null,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const BUNDLE_ACCOUNTS = [
|
|
500
|
+
ACCOUNT_TOKEN,
|
|
501
|
+
ACCOUNT_EXPIRES,
|
|
502
|
+
ACCOUNT_REFRESH,
|
|
503
|
+
ACCOUNT_REFRESH_EXPIRES,
|
|
504
|
+
ACCOUNT_SERVER_ORIGIN,
|
|
505
|
+
ACCOUNT_OWNER,
|
|
506
|
+
];
|
|
507
|
+
/**
|
|
508
|
+
* Copy every item of one slot to another BYTE FOR BYTE (the displacement
|
|
509
|
+
* rescue). Raw on purpose: the secret items may be sealed under the owning
|
|
510
|
+
* config's envelope key, and re-sealing them under ours would make the copy
|
|
511
|
+
* unreadable to the one config it exists for.
|
|
512
|
+
*/
|
|
513
|
+
function copyBundle(from, to) {
|
|
514
|
+
if (!isAvailable())
|
|
515
|
+
return false;
|
|
516
|
+
const source = serviceForSlot(from);
|
|
517
|
+
const target = serviceForSlot(to);
|
|
518
|
+
if (readSlot(ACCOUNT_TOKEN, source, true) === null)
|
|
519
|
+
return false;
|
|
520
|
+
for (const account of BUNDLE_ACCOUNTS) {
|
|
521
|
+
const value = readSlot(account, source, true);
|
|
522
|
+
if (value === null)
|
|
523
|
+
deleteSlot(account, target);
|
|
524
|
+
else if (!writeSlot(account, value, target, true))
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
/** Remove every CLI item under a slot this root does not own. The caller proves ownership. */
|
|
530
|
+
function deleteAllAt(slot) {
|
|
531
|
+
if (!isAvailable())
|
|
532
|
+
return;
|
|
533
|
+
const service = serviceForSlot(slot);
|
|
534
|
+
for (const account of BUNDLE_ACCOUNTS)
|
|
535
|
+
deleteSlot(account, service);
|
|
536
|
+
}
|
|
324
537
|
function deleteToken() {
|
|
325
538
|
if (!isAvailable())
|
|
326
539
|
return;
|
|
@@ -423,6 +636,7 @@ function deleteAll() {
|
|
|
423
636
|
deleteToken();
|
|
424
637
|
deleteRefreshToken();
|
|
425
638
|
deleteServerOrigin();
|
|
639
|
+
deleteOwner();
|
|
426
640
|
}
|
|
427
641
|
/**
|
|
428
642
|
* Pull `--profile` out of raw argv.
|
package/dist/lib/label-ref.d.ts
CHANGED
|
@@ -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-id` (OSK-12053, OSK-12166).
|
|
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>;
|
package/dist/lib/label-ref.js
CHANGED
|
@@ -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,71 @@ async function resolveLabelRefs(refs, context) {
|
|
|
171
174
|
}
|
|
172
175
|
return resolved;
|
|
173
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Help for a `--tag` flag that sits beside `--label-id` (OSK-12053, OSK-12166).
|
|
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-id ' +
|
|
189
|
+
'(it takes a label name or id).';
|
|
190
|
+
/**
|
|
191
|
+
* The `--tag` values that equal a declared label's name, each paired with that
|
|
192
|
+
* label — the one case where the mistake is certain enough to say so.
|
|
193
|
+
*
|
|
194
|
+
* Best-effort by construction: it only ever feeds a WARNING, so a vocabulary
|
|
195
|
+
* that cannot be read yields no matches rather than an error, and the tags are
|
|
196
|
+
* stored as typed either way. Nothing is converted into a label implicitly —
|
|
197
|
+
* which one a person meant is theirs to say, and a warning that names the exact
|
|
198
|
+
* flag costs one retry.
|
|
199
|
+
*/
|
|
200
|
+
async function tagsNamingLabels(tags, context) {
|
|
201
|
+
const input = (tags ?? []).map((tag) => tag.trim()).filter(Boolean);
|
|
202
|
+
if (input.length === 0)
|
|
203
|
+
return [];
|
|
204
|
+
let vocabulary;
|
|
205
|
+
try {
|
|
206
|
+
vocabulary = await collectLabelVocabulary(context);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const hits = [];
|
|
212
|
+
for (const tag of input) {
|
|
213
|
+
const needle = tag.toLowerCase();
|
|
214
|
+
const label = vocabulary.find((row) => (row.name ?? '').toLowerCase() === needle);
|
|
215
|
+
if (label)
|
|
216
|
+
hits.push({ tag, label });
|
|
217
|
+
}
|
|
218
|
+
return hits;
|
|
219
|
+
}
|
|
220
|
+
/** The stderr line for `tagsNamingLabels` hits; an empty string when there are none. */
|
|
221
|
+
function describeTagsNamingLabels(hits) {
|
|
222
|
+
if (hits.length === 0)
|
|
223
|
+
return '';
|
|
224
|
+
const names = hits.map((hit) => `"${hit.tag}"`).join(', ');
|
|
225
|
+
// `--label-id` is the flag the help LISTS; `--label` is a hidden alias of it,
|
|
226
|
+
// so a warning that named the alias sent people to a flag they could not
|
|
227
|
+
// find in `--help` (OSK-12166).
|
|
228
|
+
const flags = hits.map((hit) => `--label-id ${hit.label.name}`).join(' ');
|
|
229
|
+
return (`--tag ${names} ${hits.length === 1 ? 'was' : 'were'} stored as free-form ` +
|
|
230
|
+
`${hits.length === 1 ? 'tag' : 'tags'}, NOT as the declared label of the same name, ` +
|
|
231
|
+
`so a label filter will not find this task. To attach the label, pass ${flags}.`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The whole `--tag` check a write command runs: the warning for tags that equal
|
|
235
|
+
* a declared label AND were not also attached through `--label` (passing both
|
|
236
|
+
* is someone doing it right). An empty string means say nothing.
|
|
237
|
+
*/
|
|
238
|
+
async function tagLabelWarning(tags, attachedLabelIds, context) {
|
|
239
|
+
if (!tags || tags.length === 0)
|
|
240
|
+
return '';
|
|
241
|
+
const attached = new Set(attachedLabelIds ?? []);
|
|
242
|
+
const hits = await tagsNamingLabels(tags, context);
|
|
243
|
+
return describeTagsNamingLabels(hits.filter((hit) => !attached.has(hit.label.id)));
|
|
244
|
+
}
|
package/dist/lib/login.js
CHANGED
|
@@ -31,12 +31,13 @@ const node_child_process_1 = require("node:child_process");
|
|
|
31
31
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
32
32
|
const auth_storage_1 = require("./auth-storage");
|
|
33
33
|
const config_1 = require("./config");
|
|
34
|
-
const
|
|
34
|
+
const cli_identity_1 = require("./cli-identity");
|
|
35
35
|
const ssh_detect_1 = require("./ssh-detect");
|
|
36
36
|
const device_code_1 = require("./device-code");
|
|
37
37
|
const device_identity_1 = require("./device-identity");
|
|
38
38
|
const oauthLogin_1 = require("./oauthLogin");
|
|
39
39
|
const daemonBroker_1 = require("./daemonBroker");
|
|
40
|
+
const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
|
|
40
41
|
/**
|
|
41
42
|
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
42
43
|
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
@@ -73,16 +74,16 @@ async function cliLogin(opts = {}) {
|
|
|
73
74
|
const serverUrl = (opts.serverUrl ?? cfg.baseURL).replace(/\/+$/, '');
|
|
74
75
|
const scope = opts.scope ?? 'cli';
|
|
75
76
|
// Persist the cliId on first login so future refreshes present a
|
|
76
|
-
// stable device identity.
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
81
|
-
}
|
|
77
|
+
// stable device identity. A named profile signing in with no credential is
|
|
78
|
+
// issued its OWN id here, so the hand-off confirm below cannot retire another
|
|
79
|
+
// profile's session (OSK-12156); see `cli-identity.ts`.
|
|
80
|
+
const cliId = (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
82
81
|
const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
|
|
83
82
|
const ssh = (0, ssh_detect_1.detectSshSession)();
|
|
84
83
|
/** Set when the broker was refused for a reason that WAITING fixes. */
|
|
85
84
|
let brokerRateLimited = false;
|
|
85
|
+
/** Set when a live daemon answered the hand-off and refused it (OSK-12149). */
|
|
86
|
+
let brokerRefusal = null;
|
|
86
87
|
// --- Daemon-as-broker pre-flight ---------------------------------------
|
|
87
88
|
// When the local `oversky` daemon is already authenticated for this user
|
|
88
89
|
// on this machine, the cleanest UX is no second browser flow at all:
|
|
@@ -129,7 +130,8 @@ async function cliLogin(opts = {}) {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
console.log('');
|
|
132
|
-
console.log(' Authorized via the skrr background service on this computer (no browser needed).'
|
|
133
|
+
console.log(' Authorized via the skrr background service on this computer (no browser needed). ' +
|
|
134
|
+
'Stored a durable session for this profile.');
|
|
133
135
|
return {
|
|
134
136
|
token: brokerResult.accessToken,
|
|
135
137
|
refreshToken: brokerResult.refreshToken,
|
|
@@ -179,6 +181,15 @@ async function cliLogin(opts = {}) {
|
|
|
179
181
|
console.log('');
|
|
180
182
|
console.log(describeBrokerBaseUrlMismatch(brokerResult, serverUrl));
|
|
181
183
|
}
|
|
184
|
+
// The daemon answered and refused. Say so before whatever comes next —
|
|
185
|
+
// a browser that opens with no stated cause, or a refusal about terminals
|
|
186
|
+
// that says nothing about the daemon, both hid the real reason.
|
|
187
|
+
brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(brokerResult);
|
|
188
|
+
if (brokerRefusal && !(0, ssh_detect_1.isNonInteractive)()) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(` Asked the local skrr service for a session first. ${brokerRefusal.headline}`);
|
|
191
|
+
console.log(' Continuing with a sign-in of this CLI’s own.');
|
|
192
|
+
}
|
|
182
193
|
}
|
|
183
194
|
// --- Decision tree (first match wins) ---
|
|
184
195
|
//
|
|
@@ -199,7 +210,10 @@ async function cliLogin(opts = {}) {
|
|
|
199
210
|
? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
|
|
200
211
|
'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
|
|
201
212
|
`retry shortly, or sign in now with ${exports.HEADLESS_LOGIN_HINT}`
|
|
202
|
-
:
|
|
213
|
+
: brokerRefusal
|
|
214
|
+
? `${brokerRefusal.headline} ${brokerRefusal.remedy}\n` +
|
|
215
|
+
`skrr login would otherwise open a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`
|
|
216
|
+
: `skrr login opens a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`);
|
|
203
217
|
}
|
|
204
218
|
if (ssh.detected) {
|
|
205
219
|
// Device-code's "open this URL on your laptop" UX is the correct
|
|
@@ -282,9 +296,7 @@ async function cliLoginWithRefreshToken(opts) {
|
|
|
282
296
|
// the operator retries. The refresh exchange itself is idempotent on
|
|
283
297
|
// the server side (rotation is the unit of retry); writing the cliId
|
|
284
298
|
// up-front never causes a server-state mismatch.
|
|
285
|
-
|
|
286
|
-
(0, config_1.saveConfig)({ ...cfg, cliId: opts.cliId });
|
|
287
|
-
}
|
|
299
|
+
(0, cli_identity_1.persistPresentedCliId)(cfg, opts.cliId);
|
|
288
300
|
let pair;
|
|
289
301
|
try {
|
|
290
302
|
pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, opts.refreshToken, opts.cliId, {
|
package/dist/lib/oauthLogin.js
CHANGED
|
@@ -44,7 +44,7 @@ const node_http_1 = __importDefault(require("node:http"));
|
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const auth_storage_1 = require("./auth-storage");
|
|
46
46
|
const config_1 = require("./config");
|
|
47
|
-
const
|
|
47
|
+
const cli_identity_1 = require("./cli-identity");
|
|
48
48
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// PKCE helpers
|
|
@@ -272,13 +272,7 @@ async function runOAuthLogin(opts) {
|
|
|
272
272
|
});
|
|
273
273
|
// Resolve cliId — explicit override wins; otherwise persist on disk
|
|
274
274
|
// so subsequent refreshes present the same device identity.
|
|
275
|
-
|
|
276
|
-
if (!cliId) {
|
|
277
|
-
cliId = cfg.cliId ?? (0, device_id_1.generateCliId)();
|
|
278
|
-
if (!cfg.cliId) {
|
|
279
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
275
|
+
const cliId = opts?.cliId ?? (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
282
276
|
const { codeVerifier, codeChallenge } = generatePkce();
|
|
283
277
|
const state = generateState();
|
|
284
278
|
return new Promise((resolve) => {
|