@makaio/client-codex 1.0.0-dev-1781368205459 → 1.0.0-dev-1783940945655

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.
@@ -7,9 +7,12 @@ import * as fs$1 from "node:fs/promises";
7
7
  import fs from "node:fs/promises";
8
8
  import * as path$1 from "node:path";
9
9
  import path from "node:path";
10
+ import * as os$1 from "node:os";
10
11
  import os from "node:os";
11
- import { randomUUID } from "node:crypto";
12
- import { ClientConfigPrimeSchema, SessionConfigSetupRequestSchema, SessionConfigSetupResponseSchema } from "@makaio/framework/contracts/client";
12
+ import { createHash, randomUUID } from "node:crypto";
13
+ import { ClientConfigPrimeSchema, SessionConfigSetupRequestSchema, SessionConfigSetupResponseSchema, SessionConfigTeardownRequestSchema, SessionConfigTeardownResponseSchema } from "@makaio/framework/contracts/client";
14
+ import { parse } from "smol-toml";
15
+ import { lock } from "proper-lockfile";
13
16
 
14
17
  //#region src/definition.ts
15
18
  /**
@@ -67,7 +70,29 @@ const clientDefinition = createClientDefinition({
67
70
  capabilities: (codexCapabilityMap.patch ?? []).map((tag) => ({ tag }))
68
71
  }],
69
72
  defaultApprovalPolicy: "full-access",
70
- defaultProviderId: "openai-codex",
73
+ authMethods: [{
74
+ id: "native",
75
+ mode: "inferred",
76
+ label: "Native account"
77
+ }, {
78
+ id: "access-token",
79
+ mode: "explicit",
80
+ label: "Access token",
81
+ fields: [{
82
+ id: "accessToken",
83
+ label: "Access token",
84
+ required: true,
85
+ secret: true,
86
+ sourceHints: [{
87
+ kind: "environment",
88
+ variable: "CODEX_ACCESS_TOKEN"
89
+ }]
90
+ }]
91
+ }],
92
+ defaultAuth: {
93
+ providerDefinitionId: "openai-codex",
94
+ methodId: "native"
95
+ },
71
96
  runtimeCapabilities: {
72
97
  supportsHooks: true,
73
98
  supportsStatusline: false,
@@ -994,10 +1019,14 @@ function extractPrompt(payload) {
994
1019
  * Returns `null` for unknown or not-yet-modeled event names so the caller
995
1020
  * skips global emission and keeps the event raw-only in `client:codex.*`.
996
1021
  * @param raw - Raw hook payload delivered on `client:codex.hook.received`
1022
+ * @param machineId - Stable runtime identity of the observing machine,
1023
+ * caller-supplied by the owning client runtime. Stamped onto
1024
+ * `client.session.started` so downstream storage receives the owning
1025
+ * machine's identity without deriving it from the writer process.
997
1026
  * @returns Normalized event with subject and typed payload, or `null` when
998
1027
  * the event name is unknown
999
1028
  */
1000
- function normalizeCodexHook(raw) {
1029
+ function normalizeCodexHook(raw, machineId) {
1001
1030
  const subject = CODEX_EVENT_MAP.get(raw.eventName);
1002
1031
  if (subject === void 0) return null;
1003
1032
  const base = {
@@ -1010,7 +1039,10 @@ function normalizeCodexHook(raw) {
1010
1039
  switch (subject) {
1011
1040
  case ClientSubjects.session.started: return {
1012
1041
  subject,
1013
- payload: { ...base }
1042
+ payload: {
1043
+ ...base,
1044
+ ...machineId !== void 0 && { machineId }
1045
+ }
1014
1046
  };
1015
1047
  case ClientSubjects.session.userPrompt.submitted: return {
1016
1048
  subject,
@@ -1063,6 +1095,7 @@ function normalizeCodexHook(raw) {
1063
1095
  * - `wiring.apply` — install wiring entries into the target scope
1064
1096
  * - `wiring.remove` — uninstall wiring entries from the target scope
1065
1097
  * - `sessionConfig.setup` — seed an isolated session config directory
1098
+ * - `sessionConfig.destroy` — reconcile and clear isolated native auth
1066
1099
  *
1067
1100
  * **Subject conventions:**
1068
1101
  * - Raw Codex-native events flow in the `client:codex.*` namespace only.
@@ -1077,6 +1110,10 @@ const { subjects, namespaceDomain } = createClientNamespace("codex", {
1077
1110
  "sessionConfig.setup": {
1078
1111
  request: SessionConfigSetupRequestSchema,
1079
1112
  response: SessionConfigSetupResponseSchema
1113
+ },
1114
+ "sessionConfig.destroy": {
1115
+ request: SessionConfigTeardownRequestSchema,
1116
+ response: SessionConfigTeardownResponseSchema
1080
1117
  }
1081
1118
  });
1082
1119
  /**
@@ -1094,6 +1131,7 @@ const { subjects, namespaceDomain } = createClientNamespace("codex", {
1094
1131
  * - `CodexClientSubjects.wiring.apply` → `client:codex.wiring.apply`
1095
1132
  * - `CodexClientSubjects.wiring.remove` → `client:codex.wiring.remove`
1096
1133
  * - `CodexClientSubjects.sessionConfig.setup` → `client:codex.sessionConfig.setup`
1134
+ * - `CodexClientSubjects.sessionConfig.destroy` → `client:codex.sessionConfig.destroy`
1097
1135
  */
1098
1136
  const CodexClientSubjects = subjects;
1099
1137
  /**
@@ -1104,70 +1142,1017 @@ const CodexClientSubjects = subjects;
1104
1142
  const CODEX_CLIENT_NAMESPACE = namespaceDomain;
1105
1143
 
1106
1144
  //#endregion
1107
- //#region src/runtime/session-config-handler.ts
1145
+ //#region src/runtime/native-auth-errors.ts
1108
1146
  /**
1109
- * Codex session config setup handler.
1110
- *
1111
- * Implements the `client:codex.sessionConfig.setup` delegation request by
1112
- * materializing the Codex config files needed for an isolated session
1113
- * directory.
1147
+ * Secret-safe error helpers for Codex native authentication storage.
1148
+ * @packageDocumentation
1149
+ */
1150
+ /**
1151
+ * Build an error that preserves a safe discriminator but never the secret-bearing message.
1152
+ * @param operation - Safe operation label.
1153
+ * @param error - Original failure, whose message is deliberately discarded.
1154
+ * @returns Sanitized error safe for bus propagation and logs.
1155
+ */
1156
+ function sanitizedNativeAuthError(operation, error) {
1157
+ const errorName = error instanceof Error && error.name ? error.name : "UnknownError";
1158
+ const code = error?.code;
1159
+ const safeCode = typeof code === "string" || typeof code === "number" ? `/${String(code)}` : "";
1160
+ return /* @__PURE__ */ new Error(`Codex native-auth ${operation} failed (${errorName}${safeCode})`);
1161
+ }
1162
+ /**
1163
+ * Preserve both sanitized failures when cleanup follows an earlier error.
1164
+ * @param existing - Earlier failure, when present.
1165
+ * @param next - Newly observed sanitized failure.
1166
+ * @param message - Safe aggregate description.
1167
+ * @returns The new failure or an aggregate retaining both failures.
1168
+ */
1169
+ function mergeNativeAuthErrors(existing, next, message) {
1170
+ return existing === void 0 ? next : new AggregateError([existing, next], message);
1171
+ }
1172
+
1173
+ //#endregion
1174
+ //#region src/runtime/native-keyring-credential-store.ts
1175
+ /** Load the optional native keyring addon only when a keyring operation runs. */
1176
+ let nativeKeyringModule;
1177
+ /**
1178
+ * Create one native keyring entry without evaluating the addon on file-only paths.
1179
+ * @param service - Keyring service name.
1180
+ * @param account - Keyring account name.
1181
+ * @returns Native asynchronous keyring entry.
1182
+ */
1183
+ async function createNativeKeyringEntry(service, account) {
1184
+ nativeKeyringModule ??= import("@napi-rs/keyring");
1185
+ const { AsyncEntry } = await nativeKeyringModule;
1186
+ return new AsyncEntry(service, account);
1187
+ }
1188
+ /**
1189
+ * Check the portable keyring binding's missing-entry discriminator.
1190
+ * @param error - Keyring operation failure.
1191
+ * @returns Whether the keyring reports an absent entry.
1192
+ */
1193
+ function isMissingKeyringEntry(error) {
1194
+ return error instanceof Error && error.name === "NoEntry";
1195
+ }
1196
+ /** Native cross-platform keyring implementation matching Codex's keyring-rs store. */
1197
+ const nativeKeyringCredentialStore = {
1198
+ async read(service, account) {
1199
+ try {
1200
+ return await (await createNativeKeyringEntry(service, account)).getPassword() ?? null;
1201
+ } catch (error) {
1202
+ if (isMissingKeyringEntry(error)) return null;
1203
+ throw error;
1204
+ }
1205
+ },
1206
+ async write(service, account, value) {
1207
+ await (await createNativeKeyringEntry(service, account)).setPassword(value);
1208
+ },
1209
+ async delete(service, account) {
1210
+ try {
1211
+ await (await createNativeKeyringEntry(service, account)).deleteCredential();
1212
+ } catch (error) {
1213
+ if (!isMissingKeyringEntry(error)) throw error;
1214
+ }
1215
+ }
1216
+ };
1217
+
1218
+ //#endregion
1219
+ //#region src/runtime/native-auth-source-lock.ts
1220
+ /**
1221
+ * Canonical Codex native-auth identity and source-lock primitives.
1222
+ * @packageDocumentation
1223
+ */
1224
+ /** Secret-free cross-process lock-directory suffix beside canonical CODEX_HOME. */
1225
+ const CODEX_AUTH_CAS_LOCK_FILE = ".makaio-codex-auth-cas.lock";
1226
+ /** Lock finalization failed after the guarded native-auth operation committed. */
1227
+ var CodexNativeAuthSourceLockFinalizationError = class extends Error {
1228
+ constructor() {
1229
+ super("Codex native-auth source lock is uncertain after committed operation");
1230
+ this.name = "CodexNativeAuthSourceLockFinalizationError";
1231
+ }
1232
+ };
1233
+ /** Lock freshness and retry policy shared by every Codex native-auth CAS. */
1234
+ const CODEX_AUTH_CAS_LOCK_OPTIONS = {
1235
+ stale: 3e4,
1236
+ update: 1e4,
1237
+ retries: {
1238
+ retries: 50,
1239
+ factor: 1,
1240
+ minTimeout: 20,
1241
+ maxTimeout: 100
1242
+ }
1243
+ };
1244
+ /**
1245
+ * Resolve the canonical identity shared by Codex file and keyring auth stores.
1246
+ * @param codexHome - CODEX_HOME path.
1247
+ * @returns Canonical path plus Codex's derived keyring account.
1248
+ */
1249
+ async function identifyCodexAuthHome(codexHome) {
1250
+ return createCodexAuthHomeIdentity(await canonicalizeCodexHome(codexHome));
1251
+ }
1252
+ /**
1253
+ * Derive a cleanup-only identity from the normalized input path.
1114
1254
  *
1115
- * The handler copies `config.toml` and `auth.json` from the base config
1116
- * directory (when present) into the session-scoped directory, then primes the
1117
- * session directory to ensure `check_for_update_on_startup = false` is set.
1255
+ * This deliberately performs no `realpath` lookup. A lease directory may have
1256
+ * disappeared or been replaced by a symlink before restart cleanup; following
1257
+ * that new target could delete an unrelated canonical keyring credential.
1258
+ * @param codexHome - Original lease CODEX_HOME path.
1259
+ * @returns Lexically normalized identity safe for conservative keyring cleanup.
1260
+ */
1261
+ function identifyCodexAuthHomeLexically(codexHome) {
1262
+ return createCodexAuthHomeIdentity(path$1.resolve(codexHome));
1263
+ }
1264
+ /**
1265
+ * Inspect a lease target while rejecting a symlink or non-directory final path.
1266
+ * @param codexHome - Isolated lease CODEX_HOME path.
1267
+ * @returns Stable canonical identity, or a cleanup-only lexical fallback.
1268
+ */
1269
+ async function inspectCodexLeaseTarget(codexHome) {
1270
+ const fallbackIdentity = identifyCodexAuthHomeLexically(codexHome);
1271
+ let before;
1272
+ try {
1273
+ before = await fs$1.lstat(codexHome);
1274
+ } catch (error) {
1275
+ if (error.code === "ENOENT") return {
1276
+ status: "missing",
1277
+ fallbackIdentity
1278
+ };
1279
+ throw sanitizedNativeAuthError("lease target inspection", error);
1280
+ }
1281
+ if (before.isSymbolicLink() || !before.isDirectory()) return {
1282
+ status: "unsafe",
1283
+ fallbackIdentity
1284
+ };
1285
+ const identity = await identifyCodexAuthHome(codexHome);
1286
+ let after;
1287
+ try {
1288
+ after = await fs$1.lstat(codexHome);
1289
+ } catch (error) {
1290
+ if (error.code === "ENOENT") return {
1291
+ status: "unsafe",
1292
+ fallbackIdentity
1293
+ };
1294
+ throw sanitizedNativeAuthError("lease target verification", error);
1295
+ }
1296
+ if (after.isSymbolicLink() || !after.isDirectory() || before.dev !== after.dev || before.ino !== after.ino) return {
1297
+ status: "unsafe",
1298
+ fallbackIdentity
1299
+ };
1300
+ return {
1301
+ status: "safe",
1302
+ identity
1303
+ };
1304
+ }
1305
+ /**
1306
+ * Serialize one canonical Codex native-auth mutation by CODEX_HOME.
1118
1307
  *
1119
- * When `sessionDir` and `baseConfigDir` resolve to the same path (the
1120
- * framework passes `sessionDir` as `baseConfigDir` when no profile is
1121
- * configured) the copy step is skipped and only the prime step runs.
1308
+ * Account integrations and session-lease compare-and-swap operations must use
1309
+ * this same lock so a lease cannot overwrite a concurrently selected account.
1310
+ * @param codexHome - Canonical Codex config home whose auth source is mutated.
1311
+ * @param operation - Source operation that must retain exclusive ownership.
1312
+ * @returns Operation result after the source lock is released.
1313
+ */
1314
+ async function withCodexNativeAuthSourceLock(codexHome, operation) {
1315
+ const result = await executeCodexNativeAuthSourceLock(codexHome, operation);
1316
+ if (result.coordination === "uncertain") throw new CodexNativeAuthSourceLockFinalizationError();
1317
+ return result.value;
1318
+ }
1319
+ /**
1320
+ * Execute against one CODEX_HOME while preserving committed operation results
1321
+ * when lock finalization becomes uncertain.
1322
+ * @param codexHome - Canonical Codex config home.
1323
+ * @param operation - Native-auth operation guarded by the source lock.
1324
+ * @returns Operation value plus released/uncertain coordination state.
1325
+ */
1326
+ async function executeCodexNativeAuthSourceLock(codexHome, operation) {
1327
+ return executeIdentifiedCodexNativeAuthSourceLock(await identifyCodexAuthHome(codexHome), operation);
1328
+ }
1329
+ /**
1330
+ * Serialize one operation against an already-pinned Codex auth identity.
1331
+ * @param identity - Canonical source CODEX_HOME identity.
1332
+ * @param operation - Source operation that must retain exclusive ownership.
1333
+ * @returns Operation result after the source lock is released.
1334
+ */
1335
+ async function withIdentifiedCodexNativeAuthSourceLock(identity, operation) {
1336
+ const result = await executeIdentifiedCodexNativeAuthSourceLock(identity, operation);
1337
+ if (result.coordination === "uncertain") throw new CodexNativeAuthSourceLockFinalizationError();
1338
+ return result.value;
1339
+ }
1340
+ /**
1341
+ * Execute against an already-pinned identity and retain post-commit lock state.
1342
+ * @param identity - Canonical source CODEX_HOME identity.
1343
+ * @param operation - Native-auth operation guarded by the source lock.
1344
+ * @returns Operation value plus released/uncertain coordination state.
1345
+ */
1346
+ async function executeIdentifiedCodexNativeAuthSourceLock(identity, operation) {
1347
+ if (!inspectionMatchesIdentity(await inspectCodexLeaseTarget(identity.canonicalPath), identity)) throw new Error("Codex native-auth source identity is no longer a stable directory");
1348
+ let compromisedError;
1349
+ let release;
1350
+ try {
1351
+ release = await lock(identity.canonicalPath, {
1352
+ realpath: false,
1353
+ lockfilePath: buildCodexNativeAuthSourceLockPath(identity.canonicalPath),
1354
+ ...CODEX_AUTH_CAS_LOCK_OPTIONS,
1355
+ onCompromised: (error) => {
1356
+ compromisedError = sanitizedNativeAuthError("source lock compromised", error);
1357
+ }
1358
+ });
1359
+ } catch (error) {
1360
+ throw sanitizedNativeAuthError("source lock acquisition", error);
1361
+ }
1362
+ let outcome;
1363
+ if (compromisedError !== void 0) outcome = {
1364
+ status: "rejected",
1365
+ reason: compromisedError
1366
+ };
1367
+ else try {
1368
+ if (!inspectionMatchesIdentity(await inspectCodexLeaseTarget(identity.canonicalPath), identity)) throw new Error("Codex native-auth source identity changed before operation");
1369
+ outcome = {
1370
+ status: "fulfilled",
1371
+ value: await operation()
1372
+ };
1373
+ } catch (reason) {
1374
+ outcome = {
1375
+ status: "rejected",
1376
+ reason
1377
+ };
1378
+ }
1379
+ let releaseError;
1380
+ try {
1381
+ await release();
1382
+ } catch (error) {
1383
+ releaseError = sanitizedNativeAuthError("source lock release", error);
1384
+ }
1385
+ if (outcome.status === "rejected") {
1386
+ let operationError = outcome.reason;
1387
+ if (compromisedError !== void 0 && operationError !== compromisedError) operationError = mergeNativeAuthErrors(operationError, compromisedError, "Codex native-auth operation and source lock compromise both failed");
1388
+ if (releaseError !== void 0) operationError = mergeNativeAuthErrors(operationError, releaseError, "Codex native-auth operation and source lock release both failed");
1389
+ throw operationError;
1390
+ }
1391
+ return {
1392
+ value: outcome.value,
1393
+ coordination: compromisedError !== void 0 || releaseError !== void 0 ? "uncertain" : "released"
1394
+ };
1395
+ }
1396
+ /**
1397
+ * Build a sibling lock anchor that survives CODEX_HOME rename/replacement.
1398
+ * @param canonicalPath - Canonical CODEX_HOME path.
1399
+ * @returns Secret-free sibling lock directory path.
1400
+ */
1401
+ function buildCodexNativeAuthSourceLockPath(canonicalPath) {
1402
+ return `${canonicalPath}${CODEX_AUTH_CAS_LOCK_FILE}`;
1403
+ }
1404
+ /**
1405
+ * Verify a present or absent source still represents the pinned lexical identity.
1406
+ * @param inspection - Current safety inspection for the source path.
1407
+ * @param identity - Identity pinned before lock acquisition.
1408
+ * @returns Whether the inspected path still represents the pinned identity.
1409
+ */
1410
+ function inspectionMatchesIdentity(inspection, identity) {
1411
+ if (inspection.status === "unsafe") return false;
1412
+ const inspectedIdentity = inspection.status === "safe" ? inspection.identity : inspection.fallbackIdentity;
1413
+ return inspectedIdentity.canonicalPath === identity.canonicalPath && inspectedIdentity.keyringAccount === identity.keyringAccount;
1414
+ }
1415
+ /**
1416
+ * Build the exact Codex keyring identity for one already-normalized path.
1417
+ * @param canonicalPath - Canonical or deliberately lexical absolute path.
1418
+ * @returns Path plus `cli|sha256(path).slice(0, 16)` account.
1419
+ */
1420
+ function createCodexAuthHomeIdentity(canonicalPath) {
1421
+ return {
1422
+ canonicalPath,
1423
+ keyringAccount: `cli|${createHash("sha256").update(canonicalPath).digest("hex").slice(0, 16)}`
1424
+ };
1425
+ }
1426
+ /**
1427
+ * Canonicalize CODEX_HOME exactly as Codex does, falling back when absent.
1428
+ * @param codexHome - Candidate CODEX_HOME path.
1429
+ * @returns Canonical path, or the absolute input path when absent.
1430
+ */
1431
+ async function canonicalizeCodexHome(codexHome) {
1432
+ try {
1433
+ return await fs$1.realpath(codexHome);
1434
+ } catch (error) {
1435
+ if (error.code === "ENOENT") return path$1.resolve(codexHome);
1436
+ throw sanitizedNativeAuthError("CODEX_HOME canonicalization", error);
1437
+ }
1438
+ }
1439
+
1440
+ //#endregion
1441
+ //#region src/runtime/native-auth-store.ts
1442
+ /**
1443
+ * Codex native authentication storage primitives.
1122
1444
  *
1123
- * Returns `{ env: { CODEX_HOME: sessionDir } }` so the spawned Codex process
1124
- * inherits the isolated session directory as its configuration root.
1445
+ * Mirrors Codex App Server 0.130's `file`, `keyring`, and `auto` credential
1446
+ * store semantics without interpreting credential payloads. Credential values
1447
+ * remain inside this client-owned runtime boundary; the only persisted lease
1448
+ * state contains store identities and SHA-256 digests.
1125
1449
  * @packageDocumentation
1126
1450
  */
1451
+ /** Codex's fixed keyring service name for CLI authentication. */
1452
+ const CODEX_AUTH_KEYRING_SERVICE = "Codex Auth";
1453
+ /** Non-secret lease metadata file stored inside the isolated CODEX_HOME. */
1454
+ const CODEX_AUTH_LEASE_METADATA_FILE = ".makaio-codex-auth-lease.json";
1455
+ const CodexAuthHomeIdentitySchema = z.object({
1456
+ canonicalPath: z.string().refine((value) => path$1.isAbsolute(value), "Expected an absolute path"),
1457
+ keyringAccount: z.string().regex(/^cli\|[0-9a-f]{16}$/)
1458
+ }).strict().superRefine((identity, ctx) => {
1459
+ const expected = `cli|${createHash("sha256").update(identity.canonicalPath).digest("hex").slice(0, 16)}`;
1460
+ if (identity.keyringAccount !== expected) ctx.addIssue({
1461
+ code: "custom",
1462
+ message: "Keyring account does not match canonical CODEX_HOME"
1463
+ });
1464
+ });
1465
+ const DigestSchema = z.string().regex(/^[0-9a-f]{64}$/);
1466
+ /** Strict schema for the secret-free native auth lease generation. */
1467
+ const CodexAuthLeaseMetadataSchema = z.object({
1468
+ version: z.literal(1),
1469
+ backend: z.object({
1470
+ configured: z.enum([
1471
+ "none",
1472
+ "file",
1473
+ "keyring",
1474
+ "auto"
1475
+ ]),
1476
+ effective: z.enum([
1477
+ "none",
1478
+ "file",
1479
+ "keyring"
1480
+ ])
1481
+ }).strict(),
1482
+ canonicalIdentity: CodexAuthHomeIdentitySchema.nullable(),
1483
+ targetIdentity: CodexAuthHomeIdentitySchema,
1484
+ sourceGenerationDigest: DigestSchema.nullable(),
1485
+ initialTargetDigest: DigestSchema.nullable()
1486
+ }).strict().superRefine((metadata, ctx) => {
1487
+ if (metadata.backend.effective !== "none" !== (metadata.canonicalIdentity !== null && metadata.sourceGenerationDigest !== null && metadata.initialTargetDigest !== null)) ctx.addIssue({
1488
+ code: "custom",
1489
+ message: "Materialized metadata must carry one complete generation"
1490
+ });
1491
+ if (metadata.sourceGenerationDigest !== null && metadata.initialTargetDigest !== null && metadata.sourceGenerationDigest !== metadata.initialTargetDigest) ctx.addIssue({
1492
+ code: "custom",
1493
+ message: "The initial target generation must equal the source generation"
1494
+ });
1495
+ if (metadata.backend.configured === "none" && metadata.backend.effective !== "none") ctx.addIssue({
1496
+ code: "custom",
1497
+ message: "An empty lease cannot materialize native authentication"
1498
+ });
1499
+ if (metadata.backend.configured === "file" && metadata.backend.effective === "keyring") ctx.addIssue({
1500
+ code: "custom",
1501
+ message: "File mode cannot select keyring authentication"
1502
+ });
1503
+ if (metadata.backend.configured === "keyring" && metadata.backend.effective === "file") ctx.addIssue({
1504
+ code: "custom",
1505
+ message: "Keyring mode cannot select file authentication"
1506
+ });
1507
+ });
1508
+ /**
1509
+ * Compute a stable SHA-256 digest without interpreting the credential value.
1510
+ * @param value - Opaque credential payload.
1511
+ * @returns Lowercase hexadecimal SHA-256 digest.
1512
+ */
1513
+ function digestCodexCredential(value) {
1514
+ return createHash("sha256").update(value).digest("hex");
1515
+ }
1127
1516
  /**
1128
- * Copy a file if it exists at the source path; silently skip when absent.
1129
- * @param src - Source file path.
1130
- * @param dst - Destination file path.
1517
+ * Local native auth store used by the session config handler.
1131
1518
  */
1132
- async function copyIfPresent(src, dst) {
1519
+ var CodexNativeAuthStore = class {
1520
+ keyring;
1521
+ /**
1522
+ * @param keyring - Cross-platform keyring implementation.
1523
+ */
1524
+ constructor(keyring = nativeKeyringCredentialStore) {
1525
+ this.keyring = keyring;
1526
+ }
1527
+ /**
1528
+ * Resolve `cli_auth_credentials_store` from the canonical config.
1529
+ * Codex 0.130 defaults this setting to `file`.
1530
+ * @param identity - Canonical CODEX_HOME identity.
1531
+ * @returns Configured credential store mode.
1532
+ */
1533
+ async resolveMode(identity) {
1534
+ const content = await readTextIfPresent(path$1.join(identity.canonicalPath, "config.toml"), "configuration read");
1535
+ if (content === null) return "file";
1536
+ let parsed;
1537
+ try {
1538
+ parsed = parse(content);
1539
+ } catch {
1540
+ throw new Error("Codex native-auth configuration is invalid");
1541
+ }
1542
+ const configured = parsed["cli_auth_credentials_store"];
1543
+ if (configured === void 0) return "file";
1544
+ if (configured === "file" || configured === "keyring" || configured === "auto") return configured;
1545
+ throw new Error("Codex native-auth configuration selects an unsupported credential store");
1546
+ }
1547
+ /**
1548
+ * Load a credential using Codex 0.130's configured store order.
1549
+ * @param identity - Canonical CODEX_HOME identity.
1550
+ * @param mode - Configured store mode.
1551
+ * @returns Effective credential plus keyring-fallback status.
1552
+ */
1553
+ async readEffective(identity, mode) {
1554
+ if (mode === "file") return {
1555
+ credential: await this.readFileCredential(identity),
1556
+ keyringUnavailable: false
1557
+ };
1558
+ if (mode === "keyring") return {
1559
+ credential: await this.readKeyringCredential(identity),
1560
+ keyringUnavailable: false
1561
+ };
1562
+ try {
1563
+ const keyringCredential = await this.readKeyringCredential(identity);
1564
+ if (keyringCredential !== null) return {
1565
+ credential: keyringCredential,
1566
+ keyringUnavailable: false
1567
+ };
1568
+ } catch {
1569
+ return {
1570
+ credential: await this.readFileCredential(identity),
1571
+ keyringUnavailable: true
1572
+ };
1573
+ }
1574
+ return {
1575
+ credential: await this.readFileCredential(identity),
1576
+ keyringUnavailable: false
1577
+ };
1578
+ }
1579
+ /**
1580
+ * Read the credential generation that should be considered for lease refresh.
1581
+ *
1582
+ * `auto` normally prefers keyring. During a running process, however, a
1583
+ * transient keyring save failure can put a fresher generation in `auth.json`
1584
+ * while the unchanged keyring entry remains. Detect that exact fallback by
1585
+ * comparing both stores to the lease's initial generation.
1586
+ * @param identity - Isolated target CODEX_HOME identity.
1587
+ * @param mode - Configured store mode.
1588
+ * @param initialDigest - Digest materialized when the lease was created.
1589
+ * @returns Refresh candidate plus keyring availability status.
1590
+ */
1591
+ async readRefreshCandidate(identity, mode, initialDigest) {
1592
+ if (mode !== "auto") return this.readEffective(identity, mode);
1593
+ let keyringCredential;
1594
+ try {
1595
+ keyringCredential = await this.readKeyringCredential(identity);
1596
+ } catch {
1597
+ return {
1598
+ credential: await this.readFileCredential(identity),
1599
+ keyringUnavailable: true
1600
+ };
1601
+ }
1602
+ const fileCredential = await this.readFileCredential(identity);
1603
+ if (keyringCredential !== null && digestCodexCredential(keyringCredential.value) !== initialDigest) return {
1604
+ credential: keyringCredential,
1605
+ keyringUnavailable: false
1606
+ };
1607
+ if (fileCredential !== null && digestCodexCredential(fileCredential.value) !== initialDigest) return {
1608
+ credential: fileCredential,
1609
+ keyringUnavailable: false
1610
+ };
1611
+ return {
1612
+ credential: keyringCredential ?? fileCredential,
1613
+ keyringUnavailable: false
1614
+ };
1615
+ }
1616
+ /**
1617
+ * Read one concrete backend without fallback.
1618
+ * @param identity - Canonical CODEX_HOME identity.
1619
+ * @param backend - Concrete store to read.
1620
+ * @returns Credential value, or `null` when absent.
1621
+ */
1622
+ async readBackend(identity, backend) {
1623
+ return (backend === "file" ? await this.readFileCredential(identity) : await this.readKeyringCredential(identity))?.value ?? null;
1624
+ }
1625
+ /**
1626
+ * Clone an opaque value into one concrete target backend.
1627
+ * @param identity - Target CODEX_HOME identity.
1628
+ * @param backend - Concrete target store.
1629
+ * @param value - Opaque credential payload.
1630
+ */
1631
+ async writeBackend(identity, backend, value) {
1632
+ if (backend === "file") {
1633
+ await writePrivateFileAtomically(path$1.join(identity.canonicalPath, "auth.json"), value);
1634
+ return;
1635
+ }
1636
+ await this.writeKeyring(identity, value);
1637
+ await removeFileIfPresent(path$1.join(identity.canonicalPath, "auth.json"), "credential fallback removal").catch(() => void 0);
1638
+ }
1639
+ /**
1640
+ * Persist a refreshed credential using Codex 0.130's save semantics.
1641
+ * @param identity - Canonical CODEX_HOME identity.
1642
+ * @param mode - Configured store mode.
1643
+ * @param value - Refreshed opaque credential payload.
1644
+ */
1645
+ async saveConfigured(identity, mode, value) {
1646
+ if (mode === "file") {
1647
+ await this.writeBackend(identity, "file", value);
1648
+ return;
1649
+ }
1650
+ if (mode === "keyring") {
1651
+ await this.writeBackend(identity, "keyring", value);
1652
+ return;
1653
+ }
1654
+ try {
1655
+ await this.writeBackend(identity, "keyring", value);
1656
+ } catch {
1657
+ await this.writeBackend(identity, "file", value);
1658
+ }
1659
+ }
1660
+ /**
1661
+ * Run a generation check and conditional write under one cross-process lock.
1662
+ * @param identity - Canonical source CODEX_HOME identity.
1663
+ * @param operation - CAS operation to execute while the lock is held.
1664
+ * @returns Operation result.
1665
+ */
1666
+ async withSourceLock(identity, operation) {
1667
+ return withIdentifiedCodexNativeAuthSourceLock(identity, operation);
1668
+ }
1669
+ /**
1670
+ * Remove a target keyring credential.
1671
+ * @param identity - Target CODEX_HOME identity.
1672
+ */
1673
+ async deleteKeyring(identity) {
1674
+ try {
1675
+ await this.keyring.delete(CODEX_AUTH_KEYRING_SERVICE, identity.keyringAccount);
1676
+ } catch (error) {
1677
+ throw sanitizedNativeAuthError("keyring cleanup", error);
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Remove the file credential from a target CODEX_HOME.
1682
+ * @param identity - Target CODEX_HOME identity.
1683
+ */
1684
+ async deleteFile(identity) {
1685
+ await removeFileIfPresent(path$1.join(identity.canonicalPath, "auth.json"), "credential cleanup");
1686
+ }
1687
+ /**
1688
+ * Atomically persist strict secret-free lease metadata with mode `0600`.
1689
+ * @param sessionDir - Isolated CODEX_HOME.
1690
+ * @param metadata - Validated metadata generation.
1691
+ */
1692
+ async writeLeaseMetadata(sessionDir, metadata) {
1693
+ const parsed = CodexAuthLeaseMetadataSchema.parse(metadata);
1694
+ await writePrivateFileAtomically(path$1.join(sessionDir, CODEX_AUTH_LEASE_METADATA_FILE), `${JSON.stringify(parsed)}\n`);
1695
+ }
1696
+ /**
1697
+ * Read and strictly parse secret-free lease metadata.
1698
+ * @param sessionDir - Isolated CODEX_HOME.
1699
+ * @returns Found, missing, or invalid outcome without echoing file contents.
1700
+ */
1701
+ async readLeaseMetadata(sessionDir) {
1702
+ let content;
1703
+ try {
1704
+ content = await fs$1.readFile(path$1.join(sessionDir, CODEX_AUTH_LEASE_METADATA_FILE), "utf-8");
1705
+ } catch (error) {
1706
+ if (error.code === "ENOENT") return { status: "missing" };
1707
+ throw sanitizedNativeAuthError("lease metadata read", error);
1708
+ }
1709
+ try {
1710
+ return {
1711
+ status: "found",
1712
+ metadata: CodexAuthLeaseMetadataSchema.parse(JSON.parse(content))
1713
+ };
1714
+ } catch {
1715
+ return { status: "invalid" };
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Read a file-backed credential.
1720
+ * @param identity - Canonical CODEX_HOME identity.
1721
+ * @returns Opaque file credential, or `null` when absent.
1722
+ */
1723
+ async readFileCredential(identity) {
1724
+ const value = await readTextIfPresent(path$1.join(identity.canonicalPath, "auth.json"), "credential file read");
1725
+ return value === null ? null : {
1726
+ backend: "file",
1727
+ value
1728
+ };
1729
+ }
1730
+ /**
1731
+ * Read a keyring-backed credential.
1732
+ * @param identity - Canonical CODEX_HOME identity.
1733
+ * @returns Opaque keyring credential, or `null` when absent.
1734
+ */
1735
+ async readKeyringCredential(identity) {
1736
+ try {
1737
+ const value = await this.keyring.read(CODEX_AUTH_KEYRING_SERVICE, identity.keyringAccount);
1738
+ return value === null ? null : {
1739
+ backend: "keyring",
1740
+ value
1741
+ };
1742
+ } catch (error) {
1743
+ throw sanitizedNativeAuthError("keyring read", error);
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Write a keyring-backed credential.
1748
+ * @param identity - Canonical CODEX_HOME identity.
1749
+ * @param value - Opaque credential payload.
1750
+ */
1751
+ async writeKeyring(identity, value) {
1752
+ try {
1753
+ await this.keyring.write(CODEX_AUTH_KEYRING_SERVICE, identity.keyringAccount, value);
1754
+ } catch (error) {
1755
+ throw sanitizedNativeAuthError("keyring write", error);
1756
+ }
1757
+ }
1758
+ };
1759
+ /**
1760
+ * Read a text file with ENOENT represented as `null`.
1761
+ * @param filePath - File path to read.
1762
+ * @param operation - Sanitized operation label for failures.
1763
+ * @returns File contents, or `null` when absent.
1764
+ */
1765
+ async function readTextIfPresent(filePath, operation) {
1133
1766
  try {
1134
- await fs$1.copyFile(src, dst);
1767
+ return await fs$1.readFile(filePath, "utf-8");
1135
1768
  } catch (error) {
1136
- if (error.code !== "ENOENT") throw error;
1769
+ if (error.code === "ENOENT") return null;
1770
+ throw sanitizedNativeAuthError(operation, error);
1771
+ }
1772
+ }
1773
+ /**
1774
+ * Write one private file via create/write/sync/rename.
1775
+ * @param filePath - Destination path.
1776
+ * @param value - File contents.
1777
+ */
1778
+ async function writePrivateFileAtomically(filePath, value) {
1779
+ await fs$1.mkdir(path$1.dirname(filePath), { recursive: true });
1780
+ const temporaryPath = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
1781
+ let handle;
1782
+ let operationError;
1783
+ try {
1784
+ handle = await fs$1.open(temporaryPath, "wx", 384);
1785
+ await handle.writeFile(value, "utf-8");
1786
+ await handle.sync();
1787
+ await handle.close();
1788
+ handle = void 0;
1789
+ await fs$1.rename(temporaryPath, filePath);
1790
+ await fs$1.chmod(filePath, 384);
1791
+ } catch (error) {
1792
+ operationError = sanitizedNativeAuthError("private file write", error);
1137
1793
  }
1794
+ if (handle !== void 0) try {
1795
+ await handle.close();
1796
+ } catch (error) {
1797
+ operationError = mergeNativeAuthErrors(operationError, sanitizedNativeAuthError("private file close", error), "Codex native-auth private file write and close both failed");
1798
+ }
1799
+ try {
1800
+ await fs$1.rm(temporaryPath, { force: true });
1801
+ } catch (error) {
1802
+ operationError = mergeNativeAuthErrors(operationError, sanitizedNativeAuthError("private temporary file cleanup", error), "Codex native-auth private file operation and cleanup both failed");
1803
+ }
1804
+ if (operationError !== void 0) throw operationError;
1138
1805
  }
1139
1806
  /**
1140
- * Handle `client:codex.sessionConfig.setup` by seeding the session-scoped
1141
- * directory with the appropriate Codex config files.
1807
+ * Remove a file without exposing filesystem details or contents.
1808
+ * @param filePath - File path to remove.
1809
+ * @param operation - Sanitized operation label for failures.
1810
+ */
1811
+ async function removeFileIfPresent(filePath, operation) {
1812
+ try {
1813
+ await fs$1.rm(filePath, { force: true });
1814
+ } catch (error) {
1815
+ throw sanitizedNativeAuthError(operation, error);
1816
+ }
1817
+ }
1818
+
1819
+ //#endregion
1820
+ //#region src/runtime/session-config-handler.ts
1821
+ /**
1822
+ * Codex session config lease handler.
1142
1823
  *
1143
- * Steps:
1144
- * 1. Create `sessionDir` (recursive, no-op when it already exists).
1145
- * 2. When `sessionDir` and `baseConfigDir` are distinct paths, copy
1146
- * `config.toml` and `auth.json` from `baseConfigDir` into `sessionDir`
1147
- * (each copy is skipped when the source file does not exist).
1148
- * 3. Prime `sessionDir` via {@link handleCodexConfigPrime} to ensure
1149
- * `check_for_update_on_startup = false` is set.
1150
- * @param payload - Session config setup delegation payload. Only `sessionDir`,
1151
- * `baseConfigDir`, and `projectDir` are used; `platform` and
1152
- * `configInheritance` are accepted for interface compatibility but are not
1153
- * required for Codex's simpler config model.
1154
- * @returns Environment variables for the spawned Codex process: `CODEX_HOME`
1155
- * pointing to `sessionDir`.
1156
- */
1157
- async function handleCodexSessionConfigSetup(payload) {
1158
- const { sessionDir, baseConfigDir, projectDir } = payload;
1159
- await fs$1.mkdir(sessionDir, { recursive: true });
1160
- if (path$1.resolve(sessionDir) !== path$1.resolve(baseConfigDir)) {
1161
- await copyIfPresent(path$1.join(baseConfigDir, "config.toml"), path$1.join(sessionDir, "config.toml"));
1162
- await copyIfPresent(path$1.join(baseConfigDir, "auth.json"), path$1.join(sessionDir, "auth.json"));
1163
- }
1164
- await handleCodexConfigPrime({
1165
- clientId: "codex",
1166
- configDir: sessionDir,
1167
- phase: "session-create",
1168
- ...projectDir !== void 0 ? { projectDir } : {}
1169
- });
1170
- return { env: { CODEX_HOME: sessionDir } };
1824
+ * Materializes file/keyring native auth into an isolated CODEX_HOME and
1825
+ * reconciles refreshes on teardown with a generation-checked write-back.
1826
+ * `empty` inheritance never reads or clones canonical native credentials.
1827
+ * @packageDocumentation
1828
+ */
1829
+ /**
1830
+ * Codex-owned setup and teardown component for isolated config leases.
1831
+ */
1832
+ var CodexSessionConfigHandler = class {
1833
+ authStore;
1834
+ nativeConfigDir;
1835
+ /** Runtime copy of metadata used when a lease directory disappeared early. */
1836
+ activeMetadata = /* @__PURE__ */ new Map();
1837
+ /**
1838
+ * @param authStore - Native file/keyring storage implementation.
1839
+ * @param nativeConfigDir - Native CODEX_HOME used when no profile source exists.
1840
+ */
1841
+ constructor(authStore = new CodexNativeAuthStore(), nativeConfigDir = path$1.join(os$1.homedir(), ".codex")) {
1842
+ this.authStore = authStore;
1843
+ this.nativeConfigDir = nativeConfigDir;
1844
+ }
1845
+ /**
1846
+ * Seed an isolated CODEX_HOME according to the requested inheritance policy.
1847
+ * @param payload - Validated setup delegation payload.
1848
+ * @returns Process environment and truthful native-auth materialization status.
1849
+ */
1850
+ async setup(payload) {
1851
+ const { sessionDir, configInheritance, projectDir } = payload;
1852
+ await fs$1.mkdir(sessionDir, { recursive: true });
1853
+ const targetInspection = await inspectCodexLeaseTarget(sessionDir);
1854
+ if (targetInspection.status !== "safe") throw new Error("Codex native-auth lease target must be a stable directory");
1855
+ const targetIdentity = targetInspection.identity;
1856
+ const sourceConfigDir = this.resolveSourceConfigDir(payload);
1857
+ const sourceIdentity = configInheritance === "empty" ? void 0 : await identifyCodexAuthHome(sourceConfigDir);
1858
+ if (sourceIdentity !== void 0 && identitiesEqual(sourceIdentity, targetIdentity)) throw new Error("Codex native-auth source and lease target must be different directories");
1859
+ const emptyMetadata = createEmptyMetadata(targetIdentity);
1860
+ await this.recordMetadata(sessionDir, emptyMetadata);
1861
+ try {
1862
+ let authMaterialized = false;
1863
+ let mode;
1864
+ if (sourceIdentity !== void 0) {
1865
+ const result = await this.authStore.withSourceLock(sourceIdentity, async () => {
1866
+ await this.materializeGeneralConfig(sourceIdentity.canonicalPath, sessionDir, configInheritance);
1867
+ return this.materializeNativeAuthLocked(sourceIdentity, targetIdentity, sessionDir);
1868
+ });
1869
+ authMaterialized = result.materialized;
1870
+ mode = result.mode;
1871
+ } else {
1872
+ await this.materializeGeneralConfig(sourceConfigDir, sessionDir, configInheritance);
1873
+ await this.authStore.deleteFile(targetIdentity);
1874
+ }
1875
+ if (configInheritance === "auth-only" && mode !== void 0) await fs$1.writeFile(path$1.join(sessionDir, "config.toml"), `cli_auth_credentials_store = "${mode}"\n`, { mode: 384 });
1876
+ await handleCodexConfigPrime({
1877
+ clientId: "codex",
1878
+ configDir: sessionDir,
1879
+ phase: "session-create",
1880
+ ...projectDir !== void 0 ? { projectDir } : {}
1881
+ });
1882
+ return {
1883
+ env: { CODEX_HOME: sessionDir },
1884
+ authMaterialized
1885
+ };
1886
+ } catch (error) {
1887
+ await this.rollbackFailedSetup(sessionDir).catch((cleanupError) => {
1888
+ throw new AggregateError([error, cleanupError], "Codex session config setup and cleanup both failed");
1889
+ });
1890
+ throw error;
1891
+ }
1892
+ }
1893
+ /**
1894
+ * Compare-and-swap refreshed native auth back to its canonical store, then
1895
+ * remove the session-specific keyring entry.
1896
+ * @param payload - Validated teardown delegation payload.
1897
+ * @returns Successful client-owned teardown result.
1898
+ */
1899
+ async teardown(payload) {
1900
+ const leaseKey = path$1.resolve(payload.sessionDir);
1901
+ const activeMetadata = this.activeMetadata.get(leaseKey);
1902
+ const inspection = await this.inspectLeaseForTeardown(payload.sessionDir);
1903
+ const inspectedIdentity = inspection.targetInspection.status === "safe" ? inspection.targetInspection.identity : inspection.targetInspection.fallbackIdentity;
1904
+ const cleanupIdentity = activeMetadata?.targetIdentity ?? inspectedIdentity;
1905
+ let operationError = inspection.operationError;
1906
+ if (operationError === void 0) try {
1907
+ await this.reconcileInspectedLease(payload.sessionDir, inspection, activeMetadata);
1908
+ } catch (error) {
1909
+ operationError = error;
1910
+ }
1911
+ try {
1912
+ for (const cleanupError of await this.collectTargetCleanupErrors(payload.sessionDir, cleanupIdentity)) operationError = mergeOperationErrors(operationError, cleanupError, "Codex native-auth write-back and target cleanup both failed");
1913
+ } finally {
1914
+ this.activeMetadata.delete(leaseKey);
1915
+ }
1916
+ if (operationError !== void 0) throw operationError;
1917
+ return { success: true };
1918
+ }
1919
+ /**
1920
+ * Inspect the lease target and its metadata without starting write-back.
1921
+ * @param sessionDir - Original isolated CODEX_HOME path.
1922
+ * @returns Target state, metadata state, and any sanitized inspection failure.
1923
+ */
1924
+ async inspectLeaseForTeardown(sessionDir) {
1925
+ let operationError;
1926
+ let targetInspection;
1927
+ try {
1928
+ targetInspection = await inspectCodexLeaseTarget(sessionDir);
1929
+ } catch (error) {
1930
+ operationError = error;
1931
+ targetInspection = {
1932
+ status: "missing",
1933
+ fallbackIdentity: identifyCodexAuthHomeLexically(sessionDir)
1934
+ };
1935
+ }
1936
+ let metadataRead = { status: "missing" };
1937
+ if (targetInspection.status === "safe") try {
1938
+ metadataRead = await this.authStore.readLeaseMetadata(sessionDir);
1939
+ } catch (error) {
1940
+ operationError = mergeOperationErrors(operationError, error, "Codex native-auth lease inspection encountered multiple failures");
1941
+ }
1942
+ else if (targetInspection.status === "unsafe") operationError = mergeOperationErrors(operationError, /* @__PURE__ */ new Error("Codex native-auth lease target is unsafe; write-back was skipped"), "Codex native-auth lease inspection encountered multiple failures");
1943
+ return {
1944
+ metadataRead,
1945
+ operationError,
1946
+ targetInspection
1947
+ };
1948
+ }
1949
+ /**
1950
+ * Validate one inspected lease generation and reconcile a refreshed target.
1951
+ * @param sessionDir - Original isolated CODEX_HOME path.
1952
+ * @param inspection - Verified target and metadata inspection result.
1953
+ * @param activeMetadata - In-memory setup generation, when this process created the lease.
1954
+ */
1955
+ async reconcileInspectedLease(sessionDir, inspection, activeMetadata) {
1956
+ if (inspection.metadataRead.status === "invalid") throw new Error("Codex native-auth lease metadata is invalid; write-back was skipped");
1957
+ if (inspection.metadataRead.status === "found") {
1958
+ const inspectedIdentity = inspection.targetInspection.status === "safe" ? inspection.targetInspection.identity : inspection.targetInspection.fallbackIdentity;
1959
+ const expectedTargetIdentity = activeMetadata?.targetIdentity ?? inspectedIdentity;
1960
+ const diskMetadata = inspection.metadataRead.metadata;
1961
+ if (!identitiesEqual(diskMetadata.targetIdentity, expectedTargetIdentity)) throw new Error("Codex native-auth lease target identity is invalid; write-back was skipped");
1962
+ if (activeMetadata !== void 0 && !metadataEqual(diskMetadata, activeMetadata)) throw new Error("Codex native-auth lease generation metadata changed; write-back was skipped");
1963
+ const currentTarget = await inspectCodexLeaseTarget(sessionDir);
1964
+ if (currentTarget.status !== "safe" || !identitiesEqual(currentTarget.identity, expectedTargetIdentity)) throw new Error("Codex native-auth lease target changed during teardown; write-back was skipped");
1965
+ await this.reconcileRefresh(diskMetadata);
1966
+ } else if (activeMetadata?.backend.effective !== void 0 && activeMetadata.backend.effective !== "none") throw new Error("Codex native-auth lease metadata is missing; write-back was skipped");
1967
+ }
1968
+ /**
1969
+ * Resolve profile source or the native CODEX_HOME fallback.
1970
+ * @param payload - Setup payload carrying source and target paths.
1971
+ * @returns Canonical source candidate selected for this lease.
1972
+ */
1973
+ resolveSourceConfigDir(payload) {
1974
+ return path$1.resolve(payload.sessionDir) === path$1.resolve(payload.baseConfigDir) ? this.nativeConfigDir : payload.baseConfigDir;
1975
+ }
1976
+ /**
1977
+ * Materialize general config independently from native authentication.
1978
+ * @param sourceConfigDir - Resolved source CODEX_HOME.
1979
+ * @param sessionDir - Isolated target CODEX_HOME.
1980
+ * @param inheritance - Requested config inheritance policy.
1981
+ */
1982
+ async materializeGeneralConfig(sourceConfigDir, sessionDir, inheritance) {
1983
+ const target = path$1.join(sessionDir, "config.toml");
1984
+ await fs$1.rm(target, { force: true });
1985
+ if (inheritance !== "full") return;
1986
+ try {
1987
+ await fs$1.copyFile(path$1.join(sourceConfigDir, "config.toml"), target);
1988
+ } catch (error) {
1989
+ if (error.code !== "ENOENT") throw error;
1990
+ }
1991
+ }
1992
+ /**
1993
+ * Resolve and clone native auth while the caller owns the canonical source lock.
1994
+ * @param canonicalIdentity - Canonical auth source pinned during setup.
1995
+ * @param targetIdentity - Isolated target store identity.
1996
+ * @param sessionDir - Isolated target CODEX_HOME.
1997
+ * @returns Materialization status and configured storage mode.
1998
+ */
1999
+ async materializeNativeAuthLocked(canonicalIdentity, targetIdentity, sessionDir) {
2000
+ const mode = await this.authStore.resolveMode(canonicalIdentity);
2001
+ const sourceRead = await this.authStore.readEffective(canonicalIdentity, mode);
2002
+ const sourceCredential = sourceRead.credential;
2003
+ if (sourceCredential === null) {
2004
+ await this.authStore.deleteFile(targetIdentity);
2005
+ if (mode === "keyring" || mode === "auto" && !sourceRead.keyringUnavailable) await this.authStore.deleteKeyring(targetIdentity);
2006
+ await this.recordMetadata(sessionDir, {
2007
+ version: 1,
2008
+ backend: {
2009
+ configured: mode,
2010
+ effective: "none"
2011
+ },
2012
+ canonicalIdentity,
2013
+ targetIdentity,
2014
+ sourceGenerationDigest: null,
2015
+ initialTargetDigest: null
2016
+ });
2017
+ return {
2018
+ materialized: false,
2019
+ mode
2020
+ };
2021
+ }
2022
+ const digest = digestCodexCredential(sourceCredential.value);
2023
+ const metadata = {
2024
+ version: 1,
2025
+ backend: {
2026
+ configured: mode,
2027
+ effective: sourceCredential.backend
2028
+ },
2029
+ canonicalIdentity,
2030
+ targetIdentity,
2031
+ sourceGenerationDigest: digest,
2032
+ initialTargetDigest: digest
2033
+ };
2034
+ await this.recordMetadata(sessionDir, metadata);
2035
+ if (sourceCredential.backend === "file" && mode === "auto" && !sourceRead.keyringUnavailable) await this.authStore.deleteKeyring(targetIdentity);
2036
+ await this.authStore.writeBackend(targetIdentity, sourceCredential.backend, sourceCredential.value);
2037
+ const targetValue = await this.authStore.readBackend(targetIdentity, sourceCredential.backend);
2038
+ if (targetValue === null || digestCodexCredential(targetValue) !== digest) throw new Error("Codex native-auth target generation verification failed");
2039
+ return {
2040
+ materialized: true,
2041
+ mode
2042
+ };
2043
+ }
2044
+ /**
2045
+ * Atomically record metadata on disk and retain it for missing-directory teardown.
2046
+ * @param sessionDir - Isolated target CODEX_HOME.
2047
+ * @param metadata - Secret-free lease generation metadata.
2048
+ */
2049
+ async recordMetadata(sessionDir, metadata) {
2050
+ await this.authStore.writeLeaseMetadata(sessionDir, metadata);
2051
+ this.activeMetadata.set(path$1.resolve(sessionDir), metadata);
2052
+ }
2053
+ /**
2054
+ * Remove only resources materialized before a setup failure.
2055
+ * @param sessionDir - Isolated target CODEX_HOME.
2056
+ */
2057
+ async rollbackFailedSetup(sessionDir) {
2058
+ const leaseKey = path$1.resolve(sessionDir);
2059
+ const metadata = this.activeMetadata.get(leaseKey);
2060
+ if (metadata === void 0) return;
2061
+ try {
2062
+ const cleanupErrors = await this.collectTargetCleanupErrors(sessionDir, metadata.targetIdentity);
2063
+ if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex native-auth setup rollback failed");
2064
+ } finally {
2065
+ this.activeMetadata.delete(leaseKey);
2066
+ }
2067
+ }
2068
+ /**
2069
+ * Attempt keyring cleanup unconditionally and file cleanup only for the
2070
+ * still-verified lease directory.
2071
+ * @param sessionDir - Original isolated CODEX_HOME path.
2072
+ * @param trustedIdentity - Target identity captured before materialization.
2073
+ * @returns Every sanitized inspection or cleanup failure.
2074
+ */
2075
+ async collectTargetCleanupErrors(sessionDir, trustedIdentity) {
2076
+ const errors = [];
2077
+ let inspection;
2078
+ try {
2079
+ inspection = await inspectCodexLeaseTarget(sessionDir);
2080
+ } catch (error) {
2081
+ errors.push(error);
2082
+ }
2083
+ const cleanupOperations = [() => this.authStore.deleteKeyring(trustedIdentity)];
2084
+ if (inspection?.status === "safe" && identitiesEqual(inspection.identity, trustedIdentity)) cleanupOperations.push(() => this.authStore.deleteFile(trustedIdentity));
2085
+ else if (inspection?.status === "unsafe" || inspection?.status === "safe" && !identitiesEqual(inspection.identity, trustedIdentity)) errors.push(/* @__PURE__ */ new Error("Codex native-auth lease target became unsafe; file cleanup was skipped"));
2086
+ const cleanupResults = await Promise.allSettled(cleanupOperations.map(async (operation) => operation()));
2087
+ errors.push(...cleanupResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
2088
+ return errors;
2089
+ }
2090
+ /**
2091
+ * Reconcile one changed target under the canonical source's cross-process lock.
2092
+ * @param metadata - Validated on-disk lease generation.
2093
+ */
2094
+ async reconcileRefresh(metadata) {
2095
+ const canonicalIdentity = metadata.canonicalIdentity;
2096
+ const mode = metadata.backend.configured;
2097
+ const sourceGenerationDigest = metadata.sourceGenerationDigest;
2098
+ const initialTargetDigest = metadata.initialTargetDigest;
2099
+ if (mode === "none" || metadata.backend.effective === "none" || canonicalIdentity === null || sourceGenerationDigest === null || initialTargetDigest === null) return;
2100
+ const targetCredential = (await this.authStore.readRefreshCandidate(metadata.targetIdentity, mode, initialTargetDigest)).credential;
2101
+ if (targetCredential === null) return;
2102
+ if (digestCodexCredential(targetCredential.value) === initialTargetDigest) return;
2103
+ await this.authStore.withSourceLock(canonicalIdentity, async () => {
2104
+ const currentMode = await this.authStore.resolveMode(canonicalIdentity);
2105
+ if (currentMode !== mode) return;
2106
+ const canonicalCredential = (await this.authStore.readEffective(canonicalIdentity, currentMode)).credential;
2107
+ if (canonicalCredential !== null && canonicalCredential.backend === metadata.backend.effective && digestCodexCredential(canonicalCredential.value) === sourceGenerationDigest) await this.authStore.saveConfigured(canonicalIdentity, mode, targetCredential.value);
2108
+ });
2109
+ }
2110
+ };
2111
+ /**
2112
+ * Compare canonical identities without trusting metadata aliases.
2113
+ * @param left - First canonical identity.
2114
+ * @param right - Second canonical identity.
2115
+ * @returns Whether path and derived keyring account both match.
2116
+ */
2117
+ function identitiesEqual(left, right) {
2118
+ return left.canonicalPath === right.canonicalPath && left.keyringAccount === right.keyringAccount;
2119
+ }
2120
+ /**
2121
+ * Compare the complete trusted lease generation with its on-disk marker.
2122
+ * @param left - First strict metadata generation.
2123
+ * @param right - Second strict metadata generation.
2124
+ * @returns Whether every source, target, backend, and digest field matches.
2125
+ */
2126
+ function metadataEqual(left, right) {
2127
+ return left.version === right.version && left.backend.configured === right.backend.configured && left.backend.effective === right.backend.effective && (left.canonicalIdentity === null && right.canonicalIdentity === null || left.canonicalIdentity !== null && right.canonicalIdentity !== null && identitiesEqual(left.canonicalIdentity, right.canonicalIdentity)) && identitiesEqual(left.targetIdentity, right.targetIdentity) && left.sourceGenerationDigest === right.sourceGenerationDigest && left.initialTargetDigest === right.initialTargetDigest;
2128
+ }
2129
+ /**
2130
+ * Create the explicit no-auth marker written before setup begins.
2131
+ * @param targetIdentity - Isolated target CODEX_HOME identity.
2132
+ * @returns Initial secret-free lease metadata.
2133
+ */
2134
+ function createEmptyMetadata(targetIdentity) {
2135
+ return {
2136
+ version: 1,
2137
+ backend: {
2138
+ configured: "none",
2139
+ effective: "none"
2140
+ },
2141
+ canonicalIdentity: null,
2142
+ targetIdentity,
2143
+ sourceGenerationDigest: null,
2144
+ initialTargetDigest: null
2145
+ };
2146
+ }
2147
+ /**
2148
+ * Preserve every failure observed across write-back and mandatory cleanup.
2149
+ * @param existing - Earlier failure, when present.
2150
+ * @param next - Newly observed failure.
2151
+ * @param message - Safe aggregate description.
2152
+ * @returns The single failure or an aggregate retaining both.
2153
+ */
2154
+ function mergeOperationErrors(existing, next, message) {
2155
+ return existing === void 0 ? next : new AggregateError([existing, next], message);
1171
2156
  }
1172
2157
 
1173
2158
  //#endregion
@@ -1312,7 +2297,7 @@ const MANAGED_SESSION_CAP = 1e4;
1312
2297
  * `client.runtime.started` for the adapter-managed session gate, and
1313
2298
  * registers request handlers for `config.hooks.list`, `config.hooks.add`,
1314
2299
  * `config.hooks.remove`, `config.prime`, `wiring.list`, `wiring.apply`,
1315
- * `wiring.remove`, and `sessionConfig.setup`.
2300
+ * `wiring.remove`, `sessionConfig.setup`, and `sessionConfig.destroy`.
1316
2301
  * 2. On each incoming raw event, calls {@link normalizeCodexHook}.
1317
2302
  * 3. Emits the normalized subject when the event is recognized; silently
1318
2303
  * ignores unknown events. Normalized `client.session.*` events are
@@ -1326,6 +2311,15 @@ var CodexClientSessionService = class extends BaseService {
1326
2311
  /** Cached active config-dir resolution; reset when the active Codex version changes. */
1327
2312
  cachedConfigDir;
1328
2313
  /**
2314
+ * Stable runtime identity of the machine that owns the client sessions
2315
+ * observed by this service. Stamped onto `client.session.started` payloads
2316
+ * so downstream storage receives the owning machine's identity without
2317
+ * deriving it from the writer process.
2318
+ */
2319
+ machineId;
2320
+ /** Client-owned native auth materialization and refresh reconciliation. */
2321
+ sessionConfigHandler;
2322
+ /**
1329
2323
  * Set of `adapterSessionId` values known to be owned by an adapter-managed
1330
2324
  * Codex runtime. Populated by {@link handleRuntimeStarted} when a
1331
2325
  * `client.runtime.started` event arrives with `clientId === CLIENT_ID` and
@@ -1344,15 +2338,21 @@ var CodexClientSessionService = class extends BaseService {
1344
2338
  * @param settings - Optional {@link CodexClientSettings} instance for tests
1345
2339
  * that need exact filesystem paths. Production callers should omit it so
1346
2340
  * the service can resolve the active managed config dir via the bus.
2341
+ * @param machineId - Stable runtime identity of the observing machine,
2342
+ * caller-supplied from the extension context. Omit in tests or when the
2343
+ * identity is unavailable.
2344
+ * @param sessionConfigHandler - Optional native auth lease handler override.
1347
2345
  */
1348
- constructor(bus = MakaioBus, settings) {
2346
+ constructor(bus = MakaioBus, settings, machineId, sessionConfigHandler = new CodexSessionConfigHandler()) {
1349
2347
  super(bus);
1350
2348
  this.settingsOverride = settings;
2349
+ this.machineId = machineId;
2350
+ this.sessionConfigHandler = sessionConfigHandler;
1351
2351
  }
1352
2352
  /**
1353
2353
  * Register the raw hook ingress handler, config management request handlers,
1354
2354
  * wiring management request handlers, the config-prime lifecycle handler,
1355
- * and the session config setup handler on the bus.
2355
+ * and the session config setup/teardown handlers on the bus.
1356
2356
  *
1357
2357
  * Also subscribes to `client.runtime.started` to track adapter-managed
1358
2358
  * sessions for the {@link handleHookReceived} suppression gate.
@@ -1394,7 +2394,10 @@ var CodexClientSessionService = class extends BaseService {
1394
2394
  ctx.setResult(await handleCodexConfigPrime(ctx.payload));
1395
2395
  });
1396
2396
  this.registerHandler(CodexClientSubjects.sessionConfig.setup, async (ctx) => {
1397
- ctx.setResult(await handleCodexSessionConfigSetup(ctx.payload));
2397
+ ctx.setResult(await this.sessionConfigHandler.setup(ctx.payload));
2398
+ });
2399
+ this.registerHandler(CodexClientSubjects.sessionConfig.destroy, async (ctx) => {
2400
+ ctx.setResult(await this.sessionConfigHandler.teardown(ctx.payload));
1398
2401
  });
1399
2402
  }
1400
2403
  /**
@@ -1485,7 +2488,7 @@ var CodexClientSessionService = class extends BaseService {
1485
2488
  * @param raw - Raw hook payload delivered on `client:codex.hook.received`
1486
2489
  */
1487
2490
  async handleHookReceived(raw) {
1488
- const normalized = normalizeCodexHook(raw);
2491
+ const normalized = normalizeCodexHook(raw, this.machineId);
1489
2492
  if (normalized === null) return;
1490
2493
  if (this.isAdapterManagedSession(normalized.payload.adapterSessionId)) return;
1491
2494
  switch (normalized.subject) {
@@ -1547,4 +2550,4 @@ function isResolveBinaryMissingGlobalBinary(error) {
1547
2550
  }
1548
2551
 
1549
2552
  //#endregion
1550
- export { CodexNativeHooksFileSchema as _, CodexWiringSchemas as a, clientDefinition as b, CodexConfigHooksAddResponseSchema as c, CodexConfigHooksRemoveRequestSchema as d, CodexConfigHooksRemoveResponseSchema as f, CodexNativeHookMatcherGroupSchema as g, CodexNativeCommandHookSchema as h, normalizeCodexHook as i, CodexConfigHooksListRequestSchema as l, CodexHookEntrySchema as m, CODEX_CLIENT_NAMESPACE as n, AbsolutePathSchema as o, CodexConfigSchemas as p, CodexClientSubjects as r, CodexConfigHooksAddRequestSchema as s, CodexClientSessionService as t, CodexConfigHooksListResponseSchema as u, CodexScopeHookRecordSchema as v, CodexScopeSchema as y };
2553
+ export { clientDefinition as C, CodexScopeSchema as S, CodexHookEntrySchema as _, CODEX_CLIENT_NAMESPACE as a, CodexNativeHooksFileSchema as b, CodexWiringSchemas as c, CodexConfigHooksAddResponseSchema as d, CodexConfigHooksListRequestSchema as f, CodexConfigSchemas as g, CodexConfigHooksRemoveResponseSchema as h, withCodexNativeAuthSourceLock as i, AbsolutePathSchema as l, CodexConfigHooksRemoveRequestSchema as m, buildCodexNativeAuthSourceLockPath as n, CodexClientSubjects as o, CodexConfigHooksListResponseSchema as p, executeCodexNativeAuthSourceLock as r, normalizeCodexHook as s, CodexClientSessionService as t, CodexConfigHooksAddRequestSchema as u, CodexNativeCommandHookSchema as v, CodexScopeHookRecordSchema as x, CodexNativeHookMatcherGroupSchema as y };