@indigoai-us/hq-cli 5.228.1 → 5.228.2
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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@ interface CachedWorkerMode {
|
|
|
3
3
|
version: number;
|
|
4
4
|
value: boolean;
|
|
5
5
|
}
|
|
6
|
+
export type FlagRegistryWorkerModeCacheOperation = "read" | "write";
|
|
7
|
+
export type FlagRegistryWorkerModeCacheStep = "mkdir" | "write_temp" | "link" | "unlink" | "prune" | "readdir" | "read";
|
|
8
|
+
/** Build bounded filesystem diagnostics without exposing the state directory. */
|
|
9
|
+
export declare function flagRegistryWorkerModeCacheFailureContext(error: unknown, operation: FlagRegistryWorkerModeCacheOperation, filePath: string, env?: NodeJS.ProcessEnv): {
|
|
10
|
+
exception: unknown;
|
|
11
|
+
tags: Record<string, string>;
|
|
12
|
+
};
|
|
6
13
|
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
14
|
export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
|
|
8
15
|
/** A missing, invalid, or inaccessible cache never enables the rollout. */
|
|
@@ -1,8 +1,116 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
|
|
7
|
+
class FlagRegistryWorkerModeCacheStepError extends Error {
|
|
8
|
+
cacheStep;
|
|
9
|
+
originalError;
|
|
10
|
+
constructor(cacheStep, originalError) {
|
|
11
|
+
super(`Flag registry worker-mode cache failed at ${cacheStep}`);
|
|
12
|
+
this.cacheStep = cacheStep;
|
|
13
|
+
this.originalError = originalError;
|
|
14
|
+
this.name = "FlagRegistryWorkerModeCacheStepError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const KNOWN_ERRNOS = new Set([
|
|
18
|
+
"EACCES",
|
|
19
|
+
"EBUSY",
|
|
20
|
+
"EEXIST",
|
|
21
|
+
"EIO",
|
|
22
|
+
"EISDIR",
|
|
23
|
+
"EMFILE",
|
|
24
|
+
"ENFILE",
|
|
25
|
+
"ENOENT",
|
|
26
|
+
"ENOSPC",
|
|
27
|
+
"ENOTDIR",
|
|
28
|
+
"EPERM",
|
|
29
|
+
"EROFS",
|
|
30
|
+
]);
|
|
31
|
+
function withCacheFsStep(cacheStep, operation) {
|
|
32
|
+
try {
|
|
33
|
+
return operation();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof FlagRegistryWorkerModeCacheStepError)
|
|
37
|
+
throw error;
|
|
38
|
+
throw new FlagRegistryWorkerModeCacheStepError(cacheStep, error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function originalCacheError(error) {
|
|
42
|
+
return error instanceof FlagRegistryWorkerModeCacheStepError
|
|
43
|
+
? error.originalError
|
|
44
|
+
: error;
|
|
45
|
+
}
|
|
46
|
+
function cacheErrorCode(error) {
|
|
47
|
+
const original = originalCacheError(error);
|
|
48
|
+
return typeof original === "object" && original !== null && "code" in original
|
|
49
|
+
? original.code
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
function directoryHasUchg(directory) {
|
|
53
|
+
if (process.platform !== "darwin")
|
|
54
|
+
return "unknown";
|
|
55
|
+
try {
|
|
56
|
+
const rawFlags = execFileSync("/usr/bin/stat", ["-f", "%f", directory], { timeout: 1000 }).toString("utf8").trim();
|
|
57
|
+
if (!/^\d+$/.test(rawFlags))
|
|
58
|
+
return "unknown";
|
|
59
|
+
const flags = Number(rawFlags);
|
|
60
|
+
if (!Number.isSafeInteger(flags))
|
|
61
|
+
return "unknown";
|
|
62
|
+
return String((flags & 0x00000002) !== 0 || (flags & 0x00020000) !== 0);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// This bounded diagnostic is best-effort; preserve unknown and the original cache error.
|
|
66
|
+
return "unknown";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Build bounded filesystem diagnostics without exposing the state directory. */
|
|
70
|
+
export function flagRegistryWorkerModeCacheFailureContext(error, operation, filePath, env = process.env) {
|
|
71
|
+
const cacheError = error instanceof FlagRegistryWorkerModeCacheStepError
|
|
72
|
+
? error
|
|
73
|
+
: undefined;
|
|
74
|
+
const directory = path.dirname(filePath);
|
|
75
|
+
let dirExists = "unknown";
|
|
76
|
+
let stats;
|
|
77
|
+
try {
|
|
78
|
+
stats = fs.statSync(directory);
|
|
79
|
+
dirExists = "true";
|
|
80
|
+
}
|
|
81
|
+
catch (metadataError) {
|
|
82
|
+
if (cacheErrorCode(metadataError) === "ENOENT")
|
|
83
|
+
dirExists = "false";
|
|
84
|
+
// Keep inaccessible metadata unknown; diagnostics must not hide the cache error.
|
|
85
|
+
}
|
|
86
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
87
|
+
const ownerMatches = typeof uid === "number" && typeof stats?.uid === "number"
|
|
88
|
+
? String(stats.uid === uid)
|
|
89
|
+
: "unknown";
|
|
90
|
+
const dirMode = typeof stats?.mode === "number"
|
|
91
|
+
? (stats.mode & 0o777).toString(8).padStart(3, "0")
|
|
92
|
+
: "unknown";
|
|
93
|
+
const dirHasUchg = directoryHasUchg(directory);
|
|
94
|
+
const rawErrno = cacheErrorCode(error);
|
|
95
|
+
const errno = typeof rawErrno === "string" && KNOWN_ERRNOS.has(rawErrno)
|
|
96
|
+
? rawErrno
|
|
97
|
+
: "UNKNOWN";
|
|
98
|
+
const stateDirSource = env.HQ_STATE_DIR?.trim() ? "env" : "home";
|
|
99
|
+
return {
|
|
100
|
+
exception: originalCacheError(error),
|
|
101
|
+
tags: {
|
|
102
|
+
cache_op: operation,
|
|
103
|
+
cache_step: cacheError?.cacheStep ?? (operation === "read" ? "read" : "write_temp"),
|
|
104
|
+
errno,
|
|
105
|
+
state_dir_source: stateDirSource,
|
|
106
|
+
dir_exists: dirExists,
|
|
107
|
+
dir_owner_matches_uid: ownerMatches,
|
|
108
|
+
dir_mode: dirMode,
|
|
109
|
+
dir_has_uchg: dirHasUchg,
|
|
110
|
+
cache_path_kind: "<stateDir>/flags/<basename>",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
6
114
|
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
115
|
export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
|
|
8
116
|
const basename = scopeIdentifiers.length === 0
|
|
@@ -17,11 +125,11 @@ export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
|
|
|
17
125
|
export function readCachedFlagRegistryWorkerMode(filePath) {
|
|
18
126
|
let latest = readCacheRecordIfPresent(filePath);
|
|
19
127
|
const directory = path.dirname(filePath);
|
|
20
|
-
if (!fs.existsSync(directory))
|
|
128
|
+
if (!withCacheFsStep("read", () => fs.existsSync(directory)))
|
|
21
129
|
return latest;
|
|
22
130
|
const basename = path.basename(filePath);
|
|
23
131
|
const versionedPrefix = `${basename}.v`;
|
|
24
|
-
for (const entry of fs.readdirSync(directory)) {
|
|
132
|
+
for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
|
|
25
133
|
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
26
134
|
continue;
|
|
27
135
|
const versionedPath = path.join(directory, entry);
|
|
@@ -32,7 +140,7 @@ export function readCachedFlagRegistryWorkerMode(filePath) {
|
|
|
32
140
|
return latest;
|
|
33
141
|
}
|
|
34
142
|
export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
|
|
35
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
143
|
+
withCacheFsStep("mkdir", () => fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }));
|
|
36
144
|
const existing = readCachedFlagRegistryWorkerMode(filePath);
|
|
37
145
|
if (existing && snapshot.version < existing.version)
|
|
38
146
|
return;
|
|
@@ -42,26 +150,27 @@ export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
|
|
|
42
150
|
const versionedPath = `${filePath}.v${snapshot.version}.json`;
|
|
43
151
|
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
44
152
|
try {
|
|
45
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
|
|
153
|
+
withCacheFsStep("write_temp", () => fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
|
|
46
154
|
encoding: "utf8",
|
|
47
155
|
mode: 0o600,
|
|
48
156
|
flag: "wx",
|
|
49
|
-
});
|
|
157
|
+
}));
|
|
50
158
|
try {
|
|
51
|
-
fs.linkSync(tempPath, versionedPath);
|
|
159
|
+
withCacheFsStep("link", () => fs.linkSync(tempPath, versionedPath));
|
|
52
160
|
}
|
|
53
161
|
catch (error) {
|
|
54
162
|
if (!isAlreadyExistsError(error))
|
|
55
163
|
throw error;
|
|
56
164
|
const existingVersion = readCacheRecord(versionedPath);
|
|
57
165
|
if (existingVersion.value !== snapshot.value) {
|
|
58
|
-
throw new Error("Conflicting flag-registry worker cache values share a version");
|
|
166
|
+
throw new FlagRegistryWorkerModeCacheStepError("link", new Error("Conflicting flag-registry worker cache values share a version"));
|
|
59
167
|
}
|
|
60
168
|
}
|
|
61
169
|
}
|
|
62
170
|
finally {
|
|
63
|
-
if (fs.existsSync(tempPath))
|
|
64
|
-
fs.unlinkSync(tempPath);
|
|
171
|
+
if (withCacheFsStep("unlink", () => fs.existsSync(tempPath))) {
|
|
172
|
+
withCacheFsStep("unlink", () => fs.unlinkSync(tempPath));
|
|
173
|
+
}
|
|
65
174
|
}
|
|
66
175
|
pruneOlderCacheRecords(filePath);
|
|
67
176
|
}
|
|
@@ -76,7 +185,8 @@ function isCachedWorkerMode(value) {
|
|
|
76
185
|
"value" in value && typeof value.value === "boolean";
|
|
77
186
|
}
|
|
78
187
|
function readCacheRecord(filePath) {
|
|
79
|
-
const
|
|
188
|
+
const contents = withCacheFsStep("read", () => fs.readFileSync(filePath, "utf8"));
|
|
189
|
+
const value = JSON.parse(contents);
|
|
80
190
|
if (!isCachedWorkerMode(value)) {
|
|
81
191
|
throw new Error("Cached flag-registry worker mode has an invalid shape");
|
|
82
192
|
}
|
|
@@ -97,14 +207,14 @@ function pruneOlderCacheRecords(filePath) {
|
|
|
97
207
|
const directory = path.dirname(filePath);
|
|
98
208
|
const basename = path.basename(filePath);
|
|
99
209
|
const versionedPrefix = `${basename}.v`;
|
|
100
|
-
for (const entry of fs.readdirSync(directory)) {
|
|
210
|
+
for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
|
|
101
211
|
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
102
212
|
continue;
|
|
103
213
|
const recordPath = path.join(directory, entry);
|
|
104
214
|
const record = readCacheRecordIfPresent(recordPath);
|
|
105
215
|
if (record && record.version < latest.version) {
|
|
106
216
|
try {
|
|
107
|
-
fs.unlinkSync(recordPath);
|
|
217
|
+
withCacheFsStep("prune", () => fs.unlinkSync(recordPath));
|
|
108
218
|
}
|
|
109
219
|
catch (error) {
|
|
110
220
|
if (!isNotFoundError(error))
|
|
@@ -112,11 +222,11 @@ function pruneOlderCacheRecords(filePath) {
|
|
|
112
222
|
}
|
|
113
223
|
}
|
|
114
224
|
}
|
|
115
|
-
if (fs.existsSync(filePath)) {
|
|
225
|
+
if (withCacheFsStep("prune", () => fs.existsSync(filePath))) {
|
|
116
226
|
const legacyRecord = readCacheRecordIfPresent(filePath);
|
|
117
227
|
if (legacyRecord && legacyRecord.version < latest.version) {
|
|
118
228
|
try {
|
|
119
|
-
fs.unlinkSync(filePath);
|
|
229
|
+
withCacheFsStep("prune", () => fs.unlinkSync(filePath));
|
|
120
230
|
}
|
|
121
231
|
catch (error) {
|
|
122
232
|
if (!isNotFoundError(error))
|
|
@@ -136,11 +246,9 @@ function readCacheRecordIfPresent(filePath) {
|
|
|
136
246
|
}
|
|
137
247
|
}
|
|
138
248
|
function isAlreadyExistsError(error) {
|
|
139
|
-
return
|
|
140
|
-
"code" in error && error.code === "EEXIST";
|
|
249
|
+
return cacheErrorCode(error) === "EEXIST";
|
|
141
250
|
}
|
|
142
251
|
function isNotFoundError(error) {
|
|
143
|
-
return
|
|
144
|
-
"code" in error && error.code === "ENOENT";
|
|
252
|
+
return cacheErrorCode(error) === "ENOENT";
|
|
145
253
|
}
|
|
146
254
|
//# sourceMappingURL=flag-registry-worker-mode-cache.js.map
|
|
@@ -14,7 +14,7 @@ import { workContextRoot } from "./work-context/paths.js";
|
|
|
14
14
|
import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
|
|
15
15
|
import { loadCachedTokens, resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
16
16
|
import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
|
|
17
|
-
import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
|
|
17
|
+
import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryWorkerModeCacheFailureContext, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
|
|
18
18
|
import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
|
|
19
19
|
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
20
20
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
@@ -140,8 +140,13 @@ function selfContainedWorkerEnabled(company) {
|
|
|
140
140
|
return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
|
|
141
141
|
}
|
|
142
142
|
catch (error) {
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
const failure = flagRegistryWorkerModeCacheFailureContext(error, "read", filePath);
|
|
144
|
+
Sentry.captureException(failure.exception, {
|
|
145
|
+
tags: {
|
|
146
|
+
component: "flag-registry-worker-mode-cache",
|
|
147
|
+
worker: "flag-registry",
|
|
148
|
+
...failure.tags,
|
|
149
|
+
},
|
|
145
150
|
});
|
|
146
151
|
return false;
|
|
147
152
|
}
|
|
@@ -165,8 +170,14 @@ function cacheWorkerModeFromSnapshot(snapshot, company) {
|
|
|
165
170
|
writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
|
|
166
171
|
}
|
|
167
172
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
|
|
174
|
+
const failure = flagRegistryWorkerModeCacheFailureContext(error, "write", filePath);
|
|
175
|
+
Sentry.captureException(failure.exception, {
|
|
176
|
+
tags: {
|
|
177
|
+
component: "flag-registry-worker-mode-cache",
|
|
178
|
+
worker: "flag-registry",
|
|
179
|
+
...failure.tags,
|
|
180
|
+
},
|
|
170
181
|
});
|
|
171
182
|
}
|
|
172
183
|
}
|