@skrr-ai/cli 0.1.51 → 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.
- package/dist/base-command.d.ts +7 -0
- package/dist/base-command.js +38 -8
- package/dist/commands/agents/create.js +8 -4
- package/dist/commands/agents/update.d.ts +23 -0
- package/dist/commands/agents/update.js +54 -9
- 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/create.d.ts +5 -0
- package/dist/commands/tasks/create.js +26 -6
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.js +8 -1
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +14 -10
- package/dist/lib/agentic-stream.d.ts +20 -0
- package/dist/lib/agentic-stream.js +39 -5
- package/dist/lib/auth-failure.js +4 -1
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +342 -11
- 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/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 +33 -2
- package/dist/lib/dedicated-machines.js +50 -1
- 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 +228 -39
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +68 -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/data-provider/index.js +3138 -3100
- package/oclif.manifest.json +36364 -36355
- package/package.json +1 -1
|
@@ -111,8 +111,17 @@ export declare function describeDedicatedCheckpoint(lease: DedicatedLease | unde
|
|
|
111
111
|
* machine to the previous image. Files under the workspace survive both.
|
|
112
112
|
*/
|
|
113
113
|
export interface DedicatedImageStanding {
|
|
114
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* A newer APPROVED image exists that has not already failed on this machine.
|
|
116
|
+
* False is not "running the latest daemon": a release no approved image
|
|
117
|
+
* carries yet is not an update this machine can move to (OSK-12153).
|
|
118
|
+
*/
|
|
115
119
|
updateAvailable?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The daemon the approved image carries, when the server can say: this
|
|
122
|
+
* machine runs that image and its daemon reported a version. Null otherwise.
|
|
123
|
+
*/
|
|
124
|
+
approvedImageDaemonVersion?: string | null;
|
|
116
125
|
/** When the older image stops being supported; null when there is no deadline. */
|
|
117
126
|
updateBy?: string | null;
|
|
118
127
|
lastUpdate?: {
|
|
@@ -121,6 +130,26 @@ export interface DedicatedImageStanding {
|
|
|
121
130
|
settledAt?: string | null;
|
|
122
131
|
} | null;
|
|
123
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The latest daemon release, as `show` read it from the signed release feed:
|
|
135
|
+
* a version, or why it could not be read. Never inferred from the machine.
|
|
136
|
+
*/
|
|
137
|
+
export type LatestDaemonRelease = {
|
|
138
|
+
version: string;
|
|
139
|
+
} | {
|
|
140
|
+
error: string;
|
|
141
|
+
};
|
|
142
|
+
/** -1 / 0 / 1, or null when either side is not a version. */
|
|
143
|
+
export declare function compareDaemonVersions(a: string, b: string): number | null;
|
|
144
|
+
/**
|
|
145
|
+
* Which daemon the approved image carries against the latest daemon release
|
|
146
|
+
* (OSK-12153). `image update: current` used to be the whole answer, while the
|
|
147
|
+
* daemon on the same machine said a newer release existed: "current" means no
|
|
148
|
+
* newer APPROVED image, and a release that no approved image carries yet is not
|
|
149
|
+
* something this machine can move to. Both halves are said, and an unknown
|
|
150
|
+
* half is said to be unknown rather than left out.
|
|
151
|
+
*/
|
|
152
|
+
export declare function describeDedicatedDaemonRelease(image: DedicatedImageStanding, latest: LatestDaemonRelease | null | undefined): string | null;
|
|
124
153
|
/** One line saying where a machine's image stands and what the owner can do. */
|
|
125
154
|
export declare function describeDedicatedImageStanding(image: DedicatedImageStanding, leaseId?: string): string;
|
|
126
155
|
/**
|
|
@@ -527,7 +556,9 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
|
|
|
527
556
|
*/
|
|
528
557
|
/** One line saying what a deletion still allows. */
|
|
529
558
|
export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
|
|
530
|
-
export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[]
|
|
559
|
+
export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[], opts?: {
|
|
560
|
+
latestDaemonRelease?: LatestDaemonRelease | null;
|
|
561
|
+
}): string[];
|
|
531
562
|
/** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
|
|
532
563
|
export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
|
|
533
564
|
/**
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
4
4
|
exports.describeDedicatedCheckpoint = describeDedicatedCheckpoint;
|
|
5
|
+
exports.compareDaemonVersions = compareDaemonVersions;
|
|
6
|
+
exports.describeDedicatedDaemonRelease = describeDedicatedDaemonRelease;
|
|
5
7
|
exports.describeDedicatedImageStanding = describeDedicatedImageStanding;
|
|
6
8
|
exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
|
|
7
9
|
exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
|
|
@@ -73,6 +75,52 @@ function describeDedicatedCheckpoint(lease) {
|
|
|
73
75
|
return null;
|
|
74
76
|
return (0, data_provider_1.describeMachineCheckpointOutcome)(operation.checkpoint, { action: operation.action });
|
|
75
77
|
}
|
|
78
|
+
function versionParts(version) {
|
|
79
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(version || '').trim());
|
|
80
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
81
|
+
}
|
|
82
|
+
/** -1 / 0 / 1, or null when either side is not a version. */
|
|
83
|
+
function compareDaemonVersions(a, b) {
|
|
84
|
+
const left = versionParts(a);
|
|
85
|
+
const right = versionParts(b);
|
|
86
|
+
if (!left || !right)
|
|
87
|
+
return null;
|
|
88
|
+
for (let i = 0; i < 3; i += 1) {
|
|
89
|
+
if (left[i] !== right[i])
|
|
90
|
+
return left[i] < right[i] ? -1 : 1;
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Which daemon the approved image carries against the latest daemon release
|
|
96
|
+
* (OSK-12153). `image update: current` used to be the whole answer, while the
|
|
97
|
+
* daemon on the same machine said a newer release existed: "current" means no
|
|
98
|
+
* newer APPROVED image, and a release that no approved image carries yet is not
|
|
99
|
+
* something this machine can move to. Both halves are said, and an unknown
|
|
100
|
+
* half is said to be unknown rather than left out.
|
|
101
|
+
*/
|
|
102
|
+
function describeDedicatedDaemonRelease(image, latest) {
|
|
103
|
+
if (!latest)
|
|
104
|
+
return null;
|
|
105
|
+
const carried = image.approvedImageDaemonVersion || null;
|
|
106
|
+
const latestText = 'version' in latest
|
|
107
|
+
? `latest release skrrd ${latest.version}`
|
|
108
|
+
: `latest release unknown (${latest.error})`;
|
|
109
|
+
if (!carried) {
|
|
110
|
+
return image.updateAvailable ? latestText : `approved image's daemon unknown · ${latestText}`;
|
|
111
|
+
}
|
|
112
|
+
if (!('version' in latest)) {
|
|
113
|
+
return `approved image carries skrrd ${carried} · ${latestText}`;
|
|
114
|
+
}
|
|
115
|
+
const compared = compareDaemonVersions(carried, latest.version);
|
|
116
|
+
if (compared === 0) {
|
|
117
|
+
return `approved image carries skrrd ${carried}, the latest release`;
|
|
118
|
+
}
|
|
119
|
+
if (compared === -1) {
|
|
120
|
+
return `approved image carries skrrd ${carried} · latest release skrrd ${latest.version} is not in an approved image yet, so there is nothing to update to`;
|
|
121
|
+
}
|
|
122
|
+
return `approved image carries skrrd ${carried} · ${latestText}`;
|
|
123
|
+
}
|
|
76
124
|
/** One line saying where a machine's image stands and what the owner can do. */
|
|
77
125
|
function describeDedicatedImageStanding(image, leaseId) {
|
|
78
126
|
const last = image.lastUpdate;
|
|
@@ -547,7 +595,7 @@ function describeDedicatedDeletion(deletion, leaseId) {
|
|
|
547
595
|
}
|
|
548
596
|
return 'nothing to restore; every copy is deleted when the window closes';
|
|
549
597
|
}
|
|
550
|
-
function describeDedicatedLease(lease, harnesses = []) {
|
|
598
|
+
function describeDedicatedLease(lease, harnesses = [], opts = {}) {
|
|
551
599
|
const lines = [dedicatedLeaseDisplayName(lease)];
|
|
552
600
|
const row = (label, value) => {
|
|
553
601
|
if (value === undefined || value === null || value === '')
|
|
@@ -584,6 +632,7 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
584
632
|
row('image', image);
|
|
585
633
|
if (lease.health?.image) {
|
|
586
634
|
row('image update', describeDedicatedImageStanding(lease.health.image, dedicatedLeaseId(lease)));
|
|
635
|
+
row('image daemon', describeDedicatedDaemonRelease(lease.health.image, opts.latestDaemonRelease));
|
|
587
636
|
}
|
|
588
637
|
if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
|
|
589
638
|
row('daemon', [
|
|
@@ -260,6 +260,22 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
|
260
260
|
const body = (await res.json().catch(() => null));
|
|
261
261
|
if (res.status === 403 && body?.code === 'DELEGATED_CLI_CAPABILITY_REJECTED')
|
|
262
262
|
return null;
|
|
263
|
+
if ((res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') &&
|
|
264
|
+
(typeof body?.upstreamCode === 'string' || typeof body?.upstreamStatus === 'number')) {
|
|
265
|
+
// The daemon named the server's refusal of its own mint (OSK-12151):
|
|
266
|
+
// say that, with the code, rather than guess at three possible causes.
|
|
267
|
+
const named = [
|
|
268
|
+
typeof body?.upstreamStatus === 'number' ? `HTTP ${body.upstreamStatus}` : '',
|
|
269
|
+
typeof body?.upstreamCode === 'string' ? body.upstreamCode : '',
|
|
270
|
+
]
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.join(' ');
|
|
273
|
+
// The daemon's `message` already names the refusal; fall back to the
|
|
274
|
+
// status and code when an older daemon sent none.
|
|
275
|
+
throw new Error(typeof body?.message === 'string' && body.message
|
|
276
|
+
? `the daemon's delegated CLI broker could not mint a credential: ${body.message}`
|
|
277
|
+
: `the skrr server refused the daemon's request for this session's credential (${named}).`);
|
|
278
|
+
}
|
|
263
279
|
if (res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') {
|
|
264
280
|
throw new Error(`the daemon's delegated CLI broker is alive but cannot mint a credential ` +
|
|
265
281
|
`(HTTP ${res.status}). The daemon cannot reach the auth upstream, holds ` +
|
|
@@ -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");
|
|
@@ -859,9 +860,10 @@ function hasLocalCredential() {
|
|
|
859
860
|
/**
|
|
860
861
|
* This installation's runtime id.
|
|
861
862
|
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
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,
|
|
865
867
|
* because `OVERSKY_CLI_ID` is overlaid verbatim with no format check and a
|
|
866
868
|
* hand-set value would otherwise come back as an opaque
|
|
867
869
|
* `runtime-id-namespace-mismatch`.
|
|
@@ -869,7 +871,7 @@ function hasLocalCredential() {
|
|
|
869
871
|
function localRuntimeId() {
|
|
870
872
|
const configured = (() => {
|
|
871
873
|
try {
|
|
872
|
-
return (0, config_1.loadConfig)()
|
|
874
|
+
return (0, cli_id_1.resolveCliId)((0, config_1.loadConfig)());
|
|
873
875
|
}
|
|
874
876
|
catch {
|
|
875
877
|
return undefined;
|
package/dist/lib/keychain.d.ts
CHANGED
|
@@ -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
|
/**
|
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.
|
|
@@ -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
|
-
|
|
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
|
|
235
|
-
|
|
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(
|
|
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
|
|
250
|
-
if (!
|
|
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(
|
|
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(
|
|
379
|
+
deleteSlot(expiresAccount, service);
|
|
262
380
|
}
|
|
263
381
|
return true;
|
|
264
382
|
}
|
|
265
|
-
function
|
|
383
|
+
function readToken() {
|
|
266
384
|
if (!isAvailable())
|
|
267
385
|
return null;
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
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` (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>;
|