@skrr-ai/cli 0.1.24 → 0.1.26
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 +4 -4
- package/dist/base-command.js +11 -10
- package/dist/commands/code/install.js +13 -0
- package/dist/commands/harnesses/leases/billing.d.ts +9 -6
- package/dist/commands/harnesses/leases/billing.js +19 -24
- package/dist/commands/machines/dedicated/attach.d.ts +28 -0
- package/dist/commands/machines/dedicated/attach.js +75 -0
- package/dist/commands/machines/dedicated/catalog.d.ts +17 -0
- package/dist/commands/machines/dedicated/catalog.js +75 -0
- package/dist/commands/machines/dedicated/cp.d.ts +22 -0
- package/dist/commands/machines/dedicated/cp.js +76 -0
- package/dist/commands/machines/dedicated/create.d.ts +6 -2
- package/dist/commands/machines/dedicated/create.js +29 -20
- package/dist/commands/machines/dedicated/destroy.d.ts +6 -2
- package/dist/commands/machines/dedicated/destroy.js +41 -24
- package/dist/commands/machines/dedicated/exec.d.ts +28 -0
- package/dist/commands/machines/dedicated/exec.js +105 -0
- package/dist/commands/machines/dedicated/grow.d.ts +6 -2
- package/dist/commands/machines/dedicated/grow.js +14 -19
- package/dist/commands/machines/dedicated/health-check.d.ts +2 -2
- package/dist/commands/machines/dedicated/health-check.js +16 -14
- package/dist/commands/machines/dedicated/incident-close.d.ts +3 -2
- package/dist/commands/machines/dedicated/incident-close.js +11 -14
- package/dist/commands/machines/dedicated/index.js +37 -16
- package/dist/commands/machines/dedicated/list.d.ts +2 -2
- package/dist/commands/machines/dedicated/list.js +28 -17
- package/dist/commands/machines/dedicated/restart.d.ts +5 -2
- package/dist/commands/machines/dedicated/restart.js +8 -19
- package/dist/commands/machines/dedicated/show.d.ts +2 -2
- package/dist/commands/machines/dedicated/show.js +28 -30
- package/dist/commands/machines/dedicated/sign-in.d.ts +19 -0
- package/dist/commands/machines/dedicated/sign-in.js +49 -0
- package/dist/commands/machines/dedicated/snapshot.d.ts +5 -2
- package/dist/commands/machines/dedicated/snapshot.js +11 -19
- package/dist/commands/machines/dedicated/spending-limit.d.ts +22 -0
- package/dist/commands/machines/dedicated/spending-limit.js +50 -0
- package/dist/commands/machines/dedicated/start.d.ts +5 -2
- package/dist/commands/machines/dedicated/start.js +8 -19
- package/dist/commands/machines/dedicated/stop.d.ts +5 -2
- package/dist/commands/machines/dedicated/stop.js +8 -19
- package/dist/commands/machines/dedicated/terminal.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal.js +42 -0
- package/dist/commands/spaces/auto-executor/preview.d.ts +8 -0
- package/dist/commands/spaces/auto-executor/preview.js +41 -0
- package/dist/commands/spaces/task-rollups.d.ts +14 -0
- package/dist/commands/spaces/task-rollups.js +66 -0
- package/dist/commands/spaces/work-sync/telemetry.js +22 -0
- package/dist/commands/tasks/assign.js +1 -1
- package/dist/commands/tasks/create.js +10 -4
- package/dist/commands/tasks/list.js +27 -0
- package/dist/lib/api-fetch.js +9 -1
- package/dist/lib/assignee-resolver.d.ts +7 -6
- package/dist/lib/daemonBroker.d.ts +28 -0
- package/dist/lib/daemonBroker.js +60 -19
- package/dist/lib/dedicated-copy.d.ts +22 -0
- package/dist/lib/dedicated-copy.js +115 -0
- package/dist/lib/dedicated-lease-command.d.ts +50 -0
- package/dist/lib/dedicated-lease-command.js +158 -0
- package/dist/lib/dedicated-machines.d.ts +248 -12
- package/dist/lib/dedicated-machines.js +385 -25
- package/dist/lib/dedicated-terminal.d.ts +89 -0
- package/dist/lib/dedicated-terminal.js +275 -0
- package/dist/lib/dedicated-wait.d.ts +72 -0
- package/dist/lib/dedicated-wait.js +195 -0
- package/dist/lib/harnesses.d.ts +6 -0
- package/dist/lib/idempotency-scope.d.ts +10 -0
- package/dist/lib/idempotency-scope.js +16 -0
- package/dist/lib/node-adapter.d.ts +1 -1
- package/dist/lib/node-adapter.js +13 -11
- package/dist/lib/sky-code.d.ts +30 -1
- package/dist/lib/sky-code.js +44 -2
- package/dist/lib/triggers.js +7 -0
- package/dist/lib/work-sync.d.ts +35 -0
- 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 +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +71 -21
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +17 -1
- 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/dist/esm/kek/linux.js +71 -21
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +16 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2657 -2596
- package/oclif.manifest.json +5075 -4294
- package/package.json +1 -1
package/dist/lib/sky-code.d.ts
CHANGED
|
@@ -94,6 +94,32 @@ export declare function resolveEngine(env?: NodeJS.ProcessEnv): EngineResolution
|
|
|
94
94
|
* into a mode the user did not ask for.
|
|
95
95
|
*/
|
|
96
96
|
export declare function buildEngineArgs(subcommand: string | null, passthrough: readonly string[]): string[];
|
|
97
|
+
/**
|
|
98
|
+
* The uid the Dedicated Runtime workload exec wrapper stamps into everything it
|
|
99
|
+
* launches (`infrastructure/packer/files/skrr-workload-exec.c`). Mirrors the
|
|
100
|
+
* daemon's `DEDICATED_WORKLOAD_UID_ENV`.
|
|
101
|
+
*/
|
|
102
|
+
export declare const DEDICATED_WORKLOAD_UID_ENV = "SKRR_WORKLOAD_UID";
|
|
103
|
+
/**
|
|
104
|
+
* True when this process is tenant code on a Dedicated Runtime guest: a
|
|
105
|
+
* terminal, `skrr machines dedicated exec`, an agent's Bash, or an engine
|
|
106
|
+
* session the daemon launched.
|
|
107
|
+
*
|
|
108
|
+
* The variable alone is not enough — anyone can export it — so the process must
|
|
109
|
+
* also BE that uid, the same two-part test the daemon applies. Getting it wrong
|
|
110
|
+
* only changes which message is printed; nothing is authorized by it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function isDedicatedRuntimeWorkload(env?: NodeJS.ProcessEnv, getuid?: (() => number) | undefined): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Why nothing installs an engine on a Dedicated Runtime.
|
|
115
|
+
*
|
|
116
|
+
* The daemon there installs into its own 0700 home, which the workload identity
|
|
117
|
+
* that runs every engine cannot enter, so both the daemon and the control plane
|
|
118
|
+
* refuse (`DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE`). A build in the user's
|
|
119
|
+
* own HOME would run in this terminal but never in a session the daemon starts,
|
|
120
|
+
* so it is not offered either.
|
|
121
|
+
*/
|
|
122
|
+
export declare const DEDICATED_RUNTIME_ENGINE_MESSAGE = "On a Dedicated Runtime, coding engines ship with the machine image. They cannot be installed on a running machine; a newer engine arrives with a newer image.";
|
|
97
123
|
/**
|
|
98
124
|
* Message shown when no engine is installed.
|
|
99
125
|
*
|
|
@@ -116,8 +142,11 @@ export declare function buildEngineArgs(subcommand: string | null, passthrough:
|
|
|
116
142
|
* Keep this in step with what actually ships. An install instruction that does
|
|
117
143
|
* not work is worse than none: it moves the failure from "I could not find how
|
|
118
144
|
* to install it" to "I did what it said and it broke".
|
|
145
|
+
*
|
|
146
|
+
* That is exactly why a Dedicated Runtime gets different text: every route
|
|
147
|
+
* below fails there (see {@link DEDICATED_RUNTIME_ENGINE_MESSAGE}).
|
|
119
148
|
*/
|
|
120
|
-
export declare function notInstalledMessage(): string;
|
|
149
|
+
export declare function notInstalledMessage(env?: NodeJS.ProcessEnv): string;
|
|
121
150
|
/**
|
|
122
151
|
* Spawn the engine with stdio inherited, resolving with its exit code.
|
|
123
152
|
*
|
package/dist/lib/sky-code.js
CHANGED
|
@@ -17,13 +17,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.ENGINE_PATH_ENV = void 0;
|
|
20
|
+
exports.DEDICATED_RUNTIME_ENGINE_MESSAGE = exports.DEDICATED_WORKLOAD_UID_ENV = exports.ENGINE_PATH_ENV = void 0;
|
|
21
21
|
exports.engineHome = engineHome;
|
|
22
22
|
exports.skyCodeInstructionHome = skyCodeInstructionHome;
|
|
23
23
|
exports.managedEnginePath = managedEnginePath;
|
|
24
24
|
exports.legacyManagedEnginePath = legacyManagedEnginePath;
|
|
25
25
|
exports.resolveEngine = resolveEngine;
|
|
26
26
|
exports.buildEngineArgs = buildEngineArgs;
|
|
27
|
+
exports.isDedicatedRuntimeWorkload = isDedicatedRuntimeWorkload;
|
|
27
28
|
exports.notInstalledMessage = notInstalledMessage;
|
|
28
29
|
exports.inheritedProviderCredentials = inheritedProviderCredentials;
|
|
29
30
|
exports.hasProviderCredential = hasProviderCredential;
|
|
@@ -204,6 +205,35 @@ function resolveEngine(env = process.env) {
|
|
|
204
205
|
function buildEngineArgs(subcommand, passthrough) {
|
|
205
206
|
return subcommand === null ? [...passthrough] : [subcommand, ...passthrough];
|
|
206
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* The uid the Dedicated Runtime workload exec wrapper stamps into everything it
|
|
210
|
+
* launches (`infrastructure/packer/files/skrr-workload-exec.c`). Mirrors the
|
|
211
|
+
* daemon's `DEDICATED_WORKLOAD_UID_ENV`.
|
|
212
|
+
*/
|
|
213
|
+
exports.DEDICATED_WORKLOAD_UID_ENV = 'SKRR_WORKLOAD_UID';
|
|
214
|
+
/**
|
|
215
|
+
* True when this process is tenant code on a Dedicated Runtime guest: a
|
|
216
|
+
* terminal, `skrr machines dedicated exec`, an agent's Bash, or an engine
|
|
217
|
+
* session the daemon launched.
|
|
218
|
+
*
|
|
219
|
+
* The variable alone is not enough — anyone can export it — so the process must
|
|
220
|
+
* also BE that uid, the same two-part test the daemon applies. Getting it wrong
|
|
221
|
+
* only changes which message is printed; nothing is authorized by it.
|
|
222
|
+
*/
|
|
223
|
+
function isDedicatedRuntimeWorkload(env = process.env, getuid = process.getuid?.bind(process)) {
|
|
224
|
+
const uid = Number(env[exports.DEDICATED_WORKLOAD_UID_ENV]);
|
|
225
|
+
return Number.isInteger(uid) && uid > 0 && typeof getuid === 'function' && getuid() === uid;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Why nothing installs an engine on a Dedicated Runtime.
|
|
229
|
+
*
|
|
230
|
+
* The daemon there installs into its own 0700 home, which the workload identity
|
|
231
|
+
* that runs every engine cannot enter, so both the daemon and the control plane
|
|
232
|
+
* refuse (`DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE`). A build in the user's
|
|
233
|
+
* own HOME would run in this terminal but never in a session the daemon starts,
|
|
234
|
+
* so it is not offered either.
|
|
235
|
+
*/
|
|
236
|
+
exports.DEDICATED_RUNTIME_ENGINE_MESSAGE = 'On a Dedicated Runtime, coding engines ship with the machine image. They cannot be installed on a running machine; a newer engine arrives with a newer image.';
|
|
207
237
|
/**
|
|
208
238
|
* Message shown when no engine is installed.
|
|
209
239
|
*
|
|
@@ -226,8 +256,20 @@ function buildEngineArgs(subcommand, passthrough) {
|
|
|
226
256
|
* Keep this in step with what actually ships. An install instruction that does
|
|
227
257
|
* not work is worse than none: it moves the failure from "I could not find how
|
|
228
258
|
* to install it" to "I did what it said and it broke".
|
|
259
|
+
*
|
|
260
|
+
* That is exactly why a Dedicated Runtime gets different text: every route
|
|
261
|
+
* below fails there (see {@link DEDICATED_RUNTIME_ENGINE_MESSAGE}).
|
|
229
262
|
*/
|
|
230
|
-
function notInstalledMessage() {
|
|
263
|
+
function notInstalledMessage(env = process.env) {
|
|
264
|
+
if (isDedicatedRuntimeWorkload(env)) {
|
|
265
|
+
return [
|
|
266
|
+
'The skrr Code engine (`sky-code`) is not on this machine.',
|
|
267
|
+
'',
|
|
268
|
+
exports.DEDICATED_RUNTIME_ENGINE_MESSAGE,
|
|
269
|
+
'This machine was built from an image without it; it arrives when the',
|
|
270
|
+
"machine's image is replaced. `claude` and `codex` are on the image now.",
|
|
271
|
+
].join('\n');
|
|
272
|
+
}
|
|
231
273
|
return [
|
|
232
274
|
'The skrr Code engine (`sky-code`) is not installed on this machine.',
|
|
233
275
|
'',
|
package/dist/lib/triggers.js
CHANGED
|
@@ -147,6 +147,13 @@ exports.TRIGGER_EVENT_CATALOG = {
|
|
|
147
147
|
'task.assigned',
|
|
148
148
|
'task.completed',
|
|
149
149
|
'task.archived',
|
|
150
|
+
'task.unassigned',
|
|
151
|
+
'task.scheduled',
|
|
152
|
+
'task.rescheduled',
|
|
153
|
+
'task.unscheduled',
|
|
154
|
+
'task.deleted',
|
|
155
|
+
'task.execution_completed',
|
|
156
|
+
'task.execution_failed',
|
|
150
157
|
],
|
|
151
158
|
space: [
|
|
152
159
|
'space.created',
|
package/dist/lib/work-sync.d.ts
CHANGED
|
@@ -27,9 +27,44 @@ export type WorkSyncEventStats = {
|
|
|
27
27
|
p95Bytes: number | null;
|
|
28
28
|
mismatchCount: number;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* What happened to WRITES of one operation kind, from the durable command queue.
|
|
32
|
+
* Rates are `null` when their denominator is zero: conflict rates are over
|
|
33
|
+
* settled commands (accepted + dead-lettered), the duplicate rate over deliveries.
|
|
34
|
+
*/
|
|
35
|
+
export type WorkSyncCommandStats = {
|
|
36
|
+
commands: number;
|
|
37
|
+
deliveries: number;
|
|
38
|
+
duplicateDeliveries: number;
|
|
39
|
+
accepted: number;
|
|
40
|
+
pending: number;
|
|
41
|
+
deadLettered: number;
|
|
42
|
+
versionConflicts: number;
|
|
43
|
+
authorizationChanged: number;
|
|
44
|
+
expired: number;
|
|
45
|
+
retryExhausted: number;
|
|
46
|
+
otherRejections: number;
|
|
47
|
+
rates: {
|
|
48
|
+
versionConflict: number | null;
|
|
49
|
+
authorizationChanged: number | null;
|
|
50
|
+
duplicateDelivery: number | null;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
30
53
|
export type WorkSyncTelemetrySummary = {
|
|
31
54
|
sampledEvents: number;
|
|
32
55
|
events: Record<string, WorkSyncEventStats>;
|
|
56
|
+
/** Absent from a server older than the write-side rates. */
|
|
57
|
+
commands?: Record<string, WorkSyncCommandStats>;
|
|
58
|
+
/**
|
|
59
|
+
* Cold opens of a task detail plane that found (hit) or did not find (miss) a
|
|
60
|
+
* completed coverage record on the device. `hitRate` is null with no cold
|
|
61
|
+
* opens. Absent from a server older than the coverage read path.
|
|
62
|
+
*/
|
|
63
|
+
coverage?: {
|
|
64
|
+
hits: number;
|
|
65
|
+
misses: number;
|
|
66
|
+
hitRate: number | null;
|
|
67
|
+
};
|
|
33
68
|
repairBacklog: number;
|
|
34
69
|
rollout: {
|
|
35
70
|
verdict: 'ready' | 'hold' | 'insufficient_evidence';
|
|
@@ -32,7 +32,7 @@ export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithS
|
|
|
32
32
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
33
33
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
|
|
34
34
|
export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
|
|
35
|
-
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
35
|
+
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
36
36
|
export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
|
|
37
37
|
export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
|
|
38
38
|
export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './skyCodeChannels.js';
|
|
@@ -11,7 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.challenge = exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
|
|
12
12
|
exports.tokenHash = exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = void 0;
|
|
13
13
|
exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = void 0;
|
|
14
|
-
exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.SKY_CODE_FEED_BASE_ENV = exports.skyCodeFeedBase = exports.skyCodeChannelPrefix = exports.resolveSkyCodeChannel = exports.resolveChannelManifestUrl = exports.isSkyCodeChannel = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.SKY_CODE_CHANNELS = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.DEFAULT_SKY_CODE_CHANNEL = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
|
|
14
|
+
exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.SKY_CODE_FEED_BASE_ENV = exports.skyCodeFeedBase = exports.skyCodeChannelPrefix = exports.resolveSkyCodeChannel = exports.resolveChannelManifestUrl = exports.isSkyCodeChannel = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.SKY_CODE_CHANNELS = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.DEFAULT_SKY_CODE_CHANNEL = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
|
|
15
15
|
// Daemon scope vocabulary — single source of truth for non-Mongoose tiers
|
|
16
16
|
// (daemon, desktop, cli). Schema-bound canonical lives in
|
|
17
17
|
// `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
|
|
@@ -289,6 +289,7 @@ Object.defineProperty(exports, "sanitizeSpawnEnv", { enumerable: true, get: func
|
|
|
289
289
|
Object.defineProperty(exports, "SENSITIVE_ENV_VARS", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_VARS; } });
|
|
290
290
|
Object.defineProperty(exports, "SENSITIVE_ENV_PREFIXES", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_PREFIXES; } });
|
|
291
291
|
Object.defineProperty(exports, "SENSITIVE_ENV_SUFFIXES", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_SUFFIXES; } });
|
|
292
|
+
Object.defineProperty(exports, "SENSITIVE_ENV_NAME_TOKENS", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_NAME_TOKENS; } });
|
|
292
293
|
Object.defineProperty(exports, "SENSITIVE_ENV_PREFIX_EXCEPTIONS", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_PREFIX_EXCEPTIONS; } });
|
|
293
294
|
/* Local-machine identity — the names this product leaves on a disk. Exported so
|
|
294
295
|
the CLI, daemon and Desktop share one definition instead of 47 literals. */
|
|
@@ -64,7 +64,6 @@ exports.__resetKekCacheForTest = __resetKekCacheForTest;
|
|
|
64
64
|
* cleaning up the corresponding Secret Service entry when finished.
|
|
65
65
|
*/
|
|
66
66
|
const node_child_process_1 = require("node:child_process");
|
|
67
|
-
const node_util_1 = require("node:util");
|
|
68
67
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
69
68
|
const node_os_1 = __importDefault(require("node:os"));
|
|
70
69
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -104,7 +103,6 @@ const HKDF_INFO = Buffer.from('oversky-cred-kek-v1');
|
|
|
104
103
|
/** Filesystem paths for Tier-3 IKM components. */
|
|
105
104
|
const PATH_LOGINUID = '/proc/self/loginuid';
|
|
106
105
|
const PATH_MACHINE_ID = '/etc/machine-id';
|
|
107
|
-
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
108
106
|
let _execFileForTest = null;
|
|
109
107
|
/**
|
|
110
108
|
* @internal Test seam — inject a mock execFile for hermetic tests so the
|
|
@@ -118,23 +116,38 @@ function __setExecFileForTest(fn) {
|
|
|
118
116
|
_secretToolProbeCache = undefined;
|
|
119
117
|
_tierSelection = null;
|
|
120
118
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Default execFile wrapper used when no test override is installed.
|
|
121
|
+
*
|
|
122
|
+
* `input` is WRITTEN to the child's stdin here, and stdin is always closed. The
|
|
123
|
+
* previous version forwarded `input` as an execFile option — but only the SYNC
|
|
124
|
+
* child_process functions honour `input`; async `execFile` ignores it. So
|
|
125
|
+
* `secret-tool store`, which reads the secret from stdin, got neither the secret
|
|
126
|
+
* nor EOF and waited forever: Tier-2 enrollment could not complete on any Linux
|
|
127
|
+
* desktop with a working Secret Service. Closing stdin also means no other call
|
|
128
|
+
* can block on a tool that unexpectedly reads it.
|
|
129
|
+
*/
|
|
130
|
+
const defaultExec = (file, args, options) => new Promise((resolve, reject) => {
|
|
131
|
+
const { input, ...execOptions } = options ?? {};
|
|
132
|
+
const child = (0, node_child_process_1.execFile)(file, args, { encoding: 'utf-8', ...execOptions }, (err, stdout, stderr) => {
|
|
133
|
+
// Defensive coercion — when callers override the encoding, output may
|
|
134
|
+
// surface as Buffer; normalize both shapes.
|
|
135
|
+
const out = typeof stdout === 'string' ? stdout : Buffer.from(stdout).toString('utf-8');
|
|
136
|
+
const errOut = typeof stderr === 'string' ? stderr : Buffer.from(stderr).toString('utf-8');
|
|
137
|
+
if (err) {
|
|
138
|
+
// Same shape promisify(execFile) produced, which the probe reads.
|
|
139
|
+
reject(Object.assign(err, { stdout: out, stderr: errOut }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
resolve({ stdout: out, stderr: errOut });
|
|
126
143
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? result.stderr
|
|
135
|
-
: Buffer.from(result.stderr).toString('utf-8'),
|
|
136
|
-
};
|
|
137
|
-
};
|
|
144
|
+
if (child.stdin) {
|
|
145
|
+
// A child that exits without reading stdin makes the write fail with EPIPE;
|
|
146
|
+
// the exit status is what reports that, not this stream.
|
|
147
|
+
child.stdin.on('error', () => undefined);
|
|
148
|
+
child.stdin.end(input ?? '');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
138
151
|
function exec(file, args, options) {
|
|
139
152
|
const impl = _execFileForTest ?? defaultExec;
|
|
140
153
|
return impl(file, args, { encoding: 'utf-8', ...options });
|
|
@@ -166,6 +179,41 @@ function readFileTrimmed(path) {
|
|
|
166
179
|
// ---------------------------------------------------------------------------
|
|
167
180
|
/** `undefined` = not yet probed; `boolean` = cached probe outcome. */
|
|
168
181
|
let _secretToolProbeCache;
|
|
182
|
+
/**
|
|
183
|
+
* True when `secret-tool` never started, as opposed to starting and exiting
|
|
184
|
+
* non-zero.
|
|
185
|
+
*
|
|
186
|
+
* `execFile` reports the two differently: a process that ran and failed carries
|
|
187
|
+
* its numeric exit code, and a spawn that failed carries a STRING errno. Only
|
|
188
|
+
* `ENOENT` used to count as "missing", so every other spawn failure read as a
|
|
189
|
+
* working binary — and the probe then picked the libsecret tier, whose `store`
|
|
190
|
+
* cannot succeed either, which disables the credential envelope and makes the CLI
|
|
191
|
+
* refuse to persist any credential at all.
|
|
192
|
+
*
|
|
193
|
+
* The case that found it: a PATH entry the process may not search. execvp
|
|
194
|
+
* reports EACCES rather than ENOENT once it has met one, and a systemd unit that
|
|
195
|
+
* makes /snap inaccessible while PATH still lists /snap/bin (the Dedicated Runtime
|
|
196
|
+
* daemon sandbox did) turns every missing binary into EACCES.
|
|
197
|
+
*/
|
|
198
|
+
function secretToolDidNotRun(err) {
|
|
199
|
+
const { code, status } = err;
|
|
200
|
+
return (typeof code === 'string' && SECRET_TOOL_UNRUNNABLE.has(code)) || status === 127;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Spawn errnos that mean the binary cannot be executed from here, permanently
|
|
204
|
+
* for this process. Deliberately NOT every string errno: EAGAIN, EMFILE or ENOMEM
|
|
205
|
+
* are transient, and caching "absent" on one of those would move a machine whose
|
|
206
|
+
* libsecret works onto the machine-id tier for the rest of the process — where the
|
|
207
|
+
* existing wrapped key cannot be opened and would be re-enrolled over.
|
|
208
|
+
*/
|
|
209
|
+
const SECRET_TOOL_UNRUNNABLE = new Set([
|
|
210
|
+
'ENOENT',
|
|
211
|
+
'EACCES',
|
|
212
|
+
'ENOTDIR',
|
|
213
|
+
'ELOOP',
|
|
214
|
+
'ENOEXEC',
|
|
215
|
+
'EPERM',
|
|
216
|
+
]);
|
|
169
217
|
/**
|
|
170
218
|
* Probe secret-tool availability. Two checks:
|
|
171
219
|
* 1. `secret-tool --version` — ENOENT or exit 127 means binary missing.
|
|
@@ -183,9 +231,7 @@ async function probeSecretTool() {
|
|
|
183
231
|
await exec('secret-tool', ['--version']);
|
|
184
232
|
}
|
|
185
233
|
catch (err) {
|
|
186
|
-
|
|
187
|
-
const status = err.status;
|
|
188
|
-
if (code === 'ENOENT' || status === 127) {
|
|
234
|
+
if (secretToolDidNotRun(err)) {
|
|
189
235
|
_secretToolProbeCache = false;
|
|
190
236
|
return false;
|
|
191
237
|
}
|
|
@@ -199,6 +245,10 @@ async function probeSecretTool() {
|
|
|
199
245
|
_secretToolProbeCache = true;
|
|
200
246
|
}
|
|
201
247
|
catch (err) {
|
|
248
|
+
if (secretToolDidNotRun(err)) {
|
|
249
|
+
_secretToolProbeCache = false;
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
202
252
|
const stderr = err.stderr ?? '';
|
|
203
253
|
const isTransportError = DBUS_TRANSPORT_ERRORS.some((s) => stderr.includes(s));
|
|
204
254
|
if (isTransportError) {
|
|
@@ -65,6 +65,16 @@ export declare const SENSITIVE_ENV_PREFIXES: readonly string[];
|
|
|
65
65
|
* per-request allow-list, never by ambient inheritance.
|
|
66
66
|
*/
|
|
67
67
|
export declare const SENSITIVE_ENV_SUFFIXES: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* Credential-shaped name TOKENS: an underscore-delimited segment anywhere in
|
|
70
|
+
* the name, not only at its end. Rotation slots put the qualifier AFTER the
|
|
71
|
+
* credential word — `SKRR_EDGE_ORIGIN_SECRET_SECONDARY`,
|
|
72
|
+
* `SCHEDULER_SIGNING_SECRET_PREVIOUS` — which every suffix above misses, so a
|
|
73
|
+
* second copy of a live secret would ride into a harness while the first was
|
|
74
|
+
* stripped. Whole-segment matching keeps `SECRETS_MANAGER_REGION` and
|
|
75
|
+
* `SECRETARY` untouched.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SENSITIVE_ENV_NAME_TOKENS: readonly string[];
|
|
68
78
|
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
69
79
|
export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
|
|
70
80
|
/**
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* serve that case: a deny-list with a hole is not a deny-list.
|
|
41
41
|
*/
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
|
|
43
|
+
exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
|
|
44
44
|
exports.sanitizeSpawnEnv = sanitizeSpawnEnv;
|
|
45
45
|
/**
|
|
46
46
|
* Env vars that must NEVER reach a spawned subprocess.
|
|
@@ -110,6 +110,16 @@ exports.SENSITIVE_ENV_SUFFIXES = [
|
|
|
110
110
|
'_PASSWORD',
|
|
111
111
|
'_CREDENTIALS',
|
|
112
112
|
];
|
|
113
|
+
/**
|
|
114
|
+
* Credential-shaped name TOKENS: an underscore-delimited segment anywhere in
|
|
115
|
+
* the name, not only at its end. Rotation slots put the qualifier AFTER the
|
|
116
|
+
* credential word — `SKRR_EDGE_ORIGIN_SECRET_SECONDARY`,
|
|
117
|
+
* `SCHEDULER_SIGNING_SECRET_PREVIOUS` — which every suffix above misses, so a
|
|
118
|
+
* second copy of a live secret would ride into a harness while the first was
|
|
119
|
+
* stripped. Whole-segment matching keeps `SECRETS_MANAGER_REGION` and
|
|
120
|
+
* `SECRETARY` untouched.
|
|
121
|
+
*/
|
|
122
|
+
exports.SENSITIVE_ENV_NAME_TOKENS = ['SECRET'];
|
|
113
123
|
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
114
124
|
exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = new Set([
|
|
115
125
|
// Headless flag is read by spawned tools to know they're inside the daemon —
|
|
@@ -145,6 +155,12 @@ function sanitizeSpawnEnv(env) {
|
|
|
145
155
|
if (!stripped && exports.SENSITIVE_ENV_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
|
|
146
156
|
stripped = true;
|
|
147
157
|
}
|
|
158
|
+
if (!stripped) {
|
|
159
|
+
const segments = key.split('_');
|
|
160
|
+
if (exports.SENSITIVE_ENV_NAME_TOKENS.some((token) => segments.includes(token))) {
|
|
161
|
+
stripped = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
148
164
|
if (stripped)
|
|
149
165
|
continue;
|
|
150
166
|
out[key] = value;
|
|
@@ -32,7 +32,7 @@ export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithS
|
|
|
32
32
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
33
33
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
|
|
34
34
|
export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
|
|
35
|
-
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
35
|
+
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
36
36
|
export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
|
|
37
37
|
export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
|
|
38
38
|
export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './skyCodeChannels.js';
|
|
@@ -135,7 +135,7 @@ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConf
|
|
|
135
135
|
// spawns the SAME engine and did not). `daemon/src/env-sanitize.ts` re-exports
|
|
136
136
|
// these so its own importers are unchanged, and keeps `filterCustomEnv` — that
|
|
137
137
|
// governs SERVER-PROVIDED env, which no other process is handed.
|
|
138
|
-
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
138
|
+
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
139
139
|
/* Local-machine identity — the names this product leaves on a disk. Exported so
|
|
140
140
|
the CLI, daemon and Desktop share one definition instead of 47 literals. */
|
|
141
141
|
export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
* cleaning up the corresponding Secret Service entry when finished.
|
|
55
55
|
*/
|
|
56
56
|
import { execFile } from 'node:child_process';
|
|
57
|
-
import { promisify } from 'node:util';
|
|
58
57
|
import crypto from 'node:crypto';
|
|
59
58
|
import os from 'node:os';
|
|
60
59
|
import fs from 'node:fs';
|
|
@@ -94,7 +93,6 @@ const HKDF_INFO = Buffer.from('oversky-cred-kek-v1');
|
|
|
94
93
|
/** Filesystem paths for Tier-3 IKM components. */
|
|
95
94
|
const PATH_LOGINUID = '/proc/self/loginuid';
|
|
96
95
|
const PATH_MACHINE_ID = '/etc/machine-id';
|
|
97
|
-
const execFileAsync = promisify(execFile);
|
|
98
96
|
let _execFileForTest = null;
|
|
99
97
|
/**
|
|
100
98
|
* @internal Test seam — inject a mock execFile for hermetic tests so the
|
|
@@ -108,23 +106,38 @@ export function __setExecFileForTest(fn) {
|
|
|
108
106
|
_secretToolProbeCache = undefined;
|
|
109
107
|
_tierSelection = null;
|
|
110
108
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Default execFile wrapper used when no test override is installed.
|
|
111
|
+
*
|
|
112
|
+
* `input` is WRITTEN to the child's stdin here, and stdin is always closed. The
|
|
113
|
+
* previous version forwarded `input` as an execFile option — but only the SYNC
|
|
114
|
+
* child_process functions honour `input`; async `execFile` ignores it. So
|
|
115
|
+
* `secret-tool store`, which reads the secret from stdin, got neither the secret
|
|
116
|
+
* nor EOF and waited forever: Tier-2 enrollment could not complete on any Linux
|
|
117
|
+
* desktop with a working Secret Service. Closing stdin also means no other call
|
|
118
|
+
* can block on a tool that unexpectedly reads it.
|
|
119
|
+
*/
|
|
120
|
+
const defaultExec = (file, args, options) => new Promise((resolve, reject) => {
|
|
121
|
+
const { input, ...execOptions } = options ?? {};
|
|
122
|
+
const child = execFile(file, args, { encoding: 'utf-8', ...execOptions }, (err, stdout, stderr) => {
|
|
123
|
+
// Defensive coercion — when callers override the encoding, output may
|
|
124
|
+
// surface as Buffer; normalize both shapes.
|
|
125
|
+
const out = typeof stdout === 'string' ? stdout : Buffer.from(stdout).toString('utf-8');
|
|
126
|
+
const errOut = typeof stderr === 'string' ? stderr : Buffer.from(stderr).toString('utf-8');
|
|
127
|
+
if (err) {
|
|
128
|
+
// Same shape promisify(execFile) produced, which the probe reads.
|
|
129
|
+
reject(Object.assign(err, { stdout: out, stderr: errOut }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve({ stdout: out, stderr: errOut });
|
|
116
133
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
? result.stderr
|
|
125
|
-
: Buffer.from(result.stderr).toString('utf-8'),
|
|
126
|
-
};
|
|
127
|
-
};
|
|
134
|
+
if (child.stdin) {
|
|
135
|
+
// A child that exits without reading stdin makes the write fail with EPIPE;
|
|
136
|
+
// the exit status is what reports that, not this stream.
|
|
137
|
+
child.stdin.on('error', () => undefined);
|
|
138
|
+
child.stdin.end(input ?? '');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
128
141
|
function exec(file, args, options) {
|
|
129
142
|
const impl = _execFileForTest ?? defaultExec;
|
|
130
143
|
return impl(file, args, { encoding: 'utf-8', ...options });
|
|
@@ -156,6 +169,41 @@ function readFileTrimmed(path) {
|
|
|
156
169
|
// ---------------------------------------------------------------------------
|
|
157
170
|
/** `undefined` = not yet probed; `boolean` = cached probe outcome. */
|
|
158
171
|
let _secretToolProbeCache;
|
|
172
|
+
/**
|
|
173
|
+
* True when `secret-tool` never started, as opposed to starting and exiting
|
|
174
|
+
* non-zero.
|
|
175
|
+
*
|
|
176
|
+
* `execFile` reports the two differently: a process that ran and failed carries
|
|
177
|
+
* its numeric exit code, and a spawn that failed carries a STRING errno. Only
|
|
178
|
+
* `ENOENT` used to count as "missing", so every other spawn failure read as a
|
|
179
|
+
* working binary — and the probe then picked the libsecret tier, whose `store`
|
|
180
|
+
* cannot succeed either, which disables the credential envelope and makes the CLI
|
|
181
|
+
* refuse to persist any credential at all.
|
|
182
|
+
*
|
|
183
|
+
* The case that found it: a PATH entry the process may not search. execvp
|
|
184
|
+
* reports EACCES rather than ENOENT once it has met one, and a systemd unit that
|
|
185
|
+
* makes /snap inaccessible while PATH still lists /snap/bin (the Dedicated Runtime
|
|
186
|
+
* daemon sandbox did) turns every missing binary into EACCES.
|
|
187
|
+
*/
|
|
188
|
+
function secretToolDidNotRun(err) {
|
|
189
|
+
const { code, status } = err;
|
|
190
|
+
return (typeof code === 'string' && SECRET_TOOL_UNRUNNABLE.has(code)) || status === 127;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Spawn errnos that mean the binary cannot be executed from here, permanently
|
|
194
|
+
* for this process. Deliberately NOT every string errno: EAGAIN, EMFILE or ENOMEM
|
|
195
|
+
* are transient, and caching "absent" on one of those would move a machine whose
|
|
196
|
+
* libsecret works onto the machine-id tier for the rest of the process — where the
|
|
197
|
+
* existing wrapped key cannot be opened and would be re-enrolled over.
|
|
198
|
+
*/
|
|
199
|
+
const SECRET_TOOL_UNRUNNABLE = new Set([
|
|
200
|
+
'ENOENT',
|
|
201
|
+
'EACCES',
|
|
202
|
+
'ENOTDIR',
|
|
203
|
+
'ELOOP',
|
|
204
|
+
'ENOEXEC',
|
|
205
|
+
'EPERM',
|
|
206
|
+
]);
|
|
159
207
|
/**
|
|
160
208
|
* Probe secret-tool availability. Two checks:
|
|
161
209
|
* 1. `secret-tool --version` — ENOENT or exit 127 means binary missing.
|
|
@@ -173,9 +221,7 @@ async function probeSecretTool() {
|
|
|
173
221
|
await exec('secret-tool', ['--version']);
|
|
174
222
|
}
|
|
175
223
|
catch (err) {
|
|
176
|
-
|
|
177
|
-
const status = err.status;
|
|
178
|
-
if (code === 'ENOENT' || status === 127) {
|
|
224
|
+
if (secretToolDidNotRun(err)) {
|
|
179
225
|
_secretToolProbeCache = false;
|
|
180
226
|
return false;
|
|
181
227
|
}
|
|
@@ -189,6 +235,10 @@ async function probeSecretTool() {
|
|
|
189
235
|
_secretToolProbeCache = true;
|
|
190
236
|
}
|
|
191
237
|
catch (err) {
|
|
238
|
+
if (secretToolDidNotRun(err)) {
|
|
239
|
+
_secretToolProbeCache = false;
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
192
242
|
const stderr = err.stderr ?? '';
|
|
193
243
|
const isTransportError = DBUS_TRANSPORT_ERRORS.some((s) => stderr.includes(s));
|
|
194
244
|
if (isTransportError) {
|
|
@@ -65,6 +65,16 @@ export declare const SENSITIVE_ENV_PREFIXES: readonly string[];
|
|
|
65
65
|
* per-request allow-list, never by ambient inheritance.
|
|
66
66
|
*/
|
|
67
67
|
export declare const SENSITIVE_ENV_SUFFIXES: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* Credential-shaped name TOKENS: an underscore-delimited segment anywhere in
|
|
70
|
+
* the name, not only at its end. Rotation slots put the qualifier AFTER the
|
|
71
|
+
* credential word — `SKRR_EDGE_ORIGIN_SECRET_SECONDARY`,
|
|
72
|
+
* `SCHEDULER_SIGNING_SECRET_PREVIOUS` — which every suffix above misses, so a
|
|
73
|
+
* second copy of a live secret would ride into a harness while the first was
|
|
74
|
+
* stripped. Whole-segment matching keeps `SECRETS_MANAGER_REGION` and
|
|
75
|
+
* `SECRETARY` untouched.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SENSITIVE_ENV_NAME_TOKENS: readonly string[];
|
|
68
78
|
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
69
79
|
export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
|
|
70
80
|
/**
|
|
@@ -106,6 +106,16 @@ export const SENSITIVE_ENV_SUFFIXES = [
|
|
|
106
106
|
'_PASSWORD',
|
|
107
107
|
'_CREDENTIALS',
|
|
108
108
|
];
|
|
109
|
+
/**
|
|
110
|
+
* Credential-shaped name TOKENS: an underscore-delimited segment anywhere in
|
|
111
|
+
* the name, not only at its end. Rotation slots put the qualifier AFTER the
|
|
112
|
+
* credential word — `SKRR_EDGE_ORIGIN_SECRET_SECONDARY`,
|
|
113
|
+
* `SCHEDULER_SIGNING_SECRET_PREVIOUS` — which every suffix above misses, so a
|
|
114
|
+
* second copy of a live secret would ride into a harness while the first was
|
|
115
|
+
* stripped. Whole-segment matching keeps `SECRETS_MANAGER_REGION` and
|
|
116
|
+
* `SECRETARY` untouched.
|
|
117
|
+
*/
|
|
118
|
+
export const SENSITIVE_ENV_NAME_TOKENS = ['SECRET'];
|
|
109
119
|
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
110
120
|
export const SENSITIVE_ENV_PREFIX_EXCEPTIONS = new Set([
|
|
111
121
|
// Headless flag is read by spawned tools to know they're inside the daemon —
|
|
@@ -141,6 +151,12 @@ export function sanitizeSpawnEnv(env) {
|
|
|
141
151
|
if (!stripped && SENSITIVE_ENV_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
|
|
142
152
|
stripped = true;
|
|
143
153
|
}
|
|
154
|
+
if (!stripped) {
|
|
155
|
+
const segments = key.split('_');
|
|
156
|
+
if (SENSITIVE_ENV_NAME_TOKENS.some((token) => segments.includes(token))) {
|
|
157
|
+
stripped = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
144
160
|
if (stripped)
|
|
145
161
|
continue;
|
|
146
162
|
out[key] = value;
|