@runuai/host 0.9.113 → 0.9.115
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/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +2 -0
- package/lib/codex-auth.ts +79 -3
- package/lib/credential-store-pickup.ts +135 -0
- package/lib/credential-store.ts +36 -0
- package/lib/engine-accounts.ts +134 -2
- package/lib/env.ts +3 -0
- package/lib/host-env.ts +106 -1
- package/lib/orchestrator.ts +52 -0
- package/lib/token-broker-lambda.ts +51 -0
- package/lib/token-broker-local.ts +368 -0
- package/lib/token-broker-ssm.ts +185 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +119 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +161 -0
- package/lib/user-credentials-store.ts +104 -0
- package/package.json +3 -1
- package/scripts/agent/task-up.sh +31 -1
- package/src/main.ts +34 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane `xai-oauth` — Grok subscription sign-ins (`grok login --device-auth`).
|
|
3
|
+
*
|
|
4
|
+
* `auth.json` is keyed by `<issuer>::<client-id>`; each entry carries
|
|
5
|
+
* `key` (the bearer the CLI sends), `refresh_token`, `expires_at` (ISO),
|
|
6
|
+
* `oidc_issuer` and `oidc_client_id`. Refresh is a standard OIDC
|
|
7
|
+
* `grant_type=refresh_token` against `https://auth.x.ai/oauth2/token`
|
|
8
|
+
* (confirmed from the issuer's discovery document, 2026-09-09; refresh_token
|
|
9
|
+
* is among its supported grants).
|
|
10
|
+
*
|
|
11
|
+
* ASSUMPTION, recorded until observed on a live refresh: `key` is the OIDC
|
|
12
|
+
* access token, so a refresh replaces it with `access_token`. The lane never
|
|
13
|
+
* writes on an answer that lacks `access_token`, so a wrong assumption costs
|
|
14
|
+
* a failed refresh (retried, then the reactive path), never a corrupted
|
|
15
|
+
* sign-in.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
BROKER_REFRESH_PLACEHOLDER,
|
|
20
|
+
isTerminalOAuthError,
|
|
21
|
+
parseObjectFile,
|
|
22
|
+
type CredentialFiles,
|
|
23
|
+
type CredentialHeader,
|
|
24
|
+
type LaneHttp,
|
|
25
|
+
type RefreshOutcome,
|
|
26
|
+
type TokenLane,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
export const XAI_ISSUER = "https://auth.x.ai";
|
|
30
|
+
export const XAI_TOKEN_ENDPOINT = `${XAI_ISSUER}/oauth2/token`;
|
|
31
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
interface Entry {
|
|
34
|
+
key: string;
|
|
35
|
+
doc: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function entriesOf(files: CredentialFiles): { doc: Record<string, unknown>; entries: Entry[] } | null {
|
|
39
|
+
const doc = parseObjectFile(files["auth.json"]);
|
|
40
|
+
if (!doc) return null;
|
|
41
|
+
const entries: Entry[] = [];
|
|
42
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
43
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
44
|
+
entries.push({ key, doc: value as Record<string, unknown> });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { doc, entries };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expiryOf(entry: Record<string, unknown>): number | null {
|
|
51
|
+
const raw = entry.expires_at;
|
|
52
|
+
if (typeof raw !== "string") return null;
|
|
53
|
+
const ms = Date.parse(raw);
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const xaiOAuthLane: TokenLane = {
|
|
58
|
+
id: "xai-oauth",
|
|
59
|
+
endpoints: [XAI_TOKEN_ENDPOINT],
|
|
60
|
+
// Observed 2026-09-09: the token lives SIX hours (create_time → expires_at).
|
|
61
|
+
// Refresh with three hours left — the hourly cycle then renews it at about
|
|
62
|
+
// four hours of age — and every four hours when expiry cannot be read.
|
|
63
|
+
schedule: { leadMs: 3 * HOUR_MS, everyMs: 4 * HOUR_MS },
|
|
64
|
+
reactiveSignals: [/invalid_grant/i, /token (?:has )?expired/i, /log ?in again/i, /unauthorized/i],
|
|
65
|
+
|
|
66
|
+
matches(header: CredentialHeader, files: CredentialFiles): boolean {
|
|
67
|
+
if (header.kind !== "grok" || header.authKind !== "config-dir") return false;
|
|
68
|
+
const parsed = entriesOf(files);
|
|
69
|
+
return (
|
|
70
|
+
parsed !== null && parsed.entries.some((e) => typeof e.doc.refresh_token === "string")
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
expiresAt(files: CredentialFiles): number | null {
|
|
75
|
+
const parsed = entriesOf(files);
|
|
76
|
+
if (!parsed) return null;
|
|
77
|
+
const known = parsed.entries.map((e) => expiryOf(e.doc)).filter((v): v is number => v !== null);
|
|
78
|
+
return known.length ? Math.min(...known) : null;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
brokeredCopy(files: CredentialFiles): CredentialFiles {
|
|
82
|
+
const parsed = entriesOf(files);
|
|
83
|
+
if (!parsed) return files;
|
|
84
|
+
let changed = false;
|
|
85
|
+
for (const entry of parsed.entries) {
|
|
86
|
+
if (
|
|
87
|
+
typeof entry.doc.refresh_token === "string" &&
|
|
88
|
+
entry.doc.refresh_token !== BROKER_REFRESH_PLACEHOLDER
|
|
89
|
+
) {
|
|
90
|
+
entry.doc.refresh_token = BROKER_REFRESH_PLACEHOLDER;
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return changed ? { ...files, "auth.json": `${JSON.stringify(parsed.doc, null, 2)}\n` } : files;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async refresh(files: CredentialFiles, http: LaneHttp, now: number): Promise<RefreshOutcome> {
|
|
98
|
+
const parsed = entriesOf(files);
|
|
99
|
+
if (!parsed) return { ok: false, terminal: true, reason: "auth.json is not readable" };
|
|
100
|
+
let refreshed = 0;
|
|
101
|
+
let expiresAt: number | null = null;
|
|
102
|
+
for (const entry of parsed.entries) {
|
|
103
|
+
const current = entry.doc.refresh_token;
|
|
104
|
+
if (typeof current !== "string" || current === BROKER_REFRESH_PLACEHOLDER) continue;
|
|
105
|
+
const issuer = typeof entry.doc.oidc_issuer === "string" ? entry.doc.oidc_issuer : XAI_ISSUER;
|
|
106
|
+
if (issuer.replace(/\/$/, "") !== XAI_ISSUER) {
|
|
107
|
+
return { ok: false, terminal: true, reason: "unexpected issuer" };
|
|
108
|
+
}
|
|
109
|
+
const clientId = entry.doc.oidc_client_id;
|
|
110
|
+
if (typeof clientId !== "string" || !clientId) {
|
|
111
|
+
return { ok: false, terminal: true, reason: "entry has no client id" };
|
|
112
|
+
}
|
|
113
|
+
let res;
|
|
114
|
+
try {
|
|
115
|
+
res = await http.postForm(XAI_TOKEN_ENDPOINT, {
|
|
116
|
+
grant_type: "refresh_token",
|
|
117
|
+
refresh_token: current,
|
|
118
|
+
client_id: clientId,
|
|
119
|
+
});
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
terminal: false,
|
|
124
|
+
reason: `request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (res.status < 200 || res.status >= 300) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
terminal: isTerminalOAuthError(res.status, res.json),
|
|
131
|
+
reason: `HTTP ${res.status}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const body =
|
|
135
|
+
res.json && typeof res.json === "object" && !Array.isArray(res.json)
|
|
136
|
+
? (res.json as Record<string, unknown>)
|
|
137
|
+
: null;
|
|
138
|
+
const access = body?.access_token;
|
|
139
|
+
if (typeof access !== "string" || !access) {
|
|
140
|
+
return { ok: false, terminal: false, reason: "response missing access_token" };
|
|
141
|
+
}
|
|
142
|
+
entry.doc.key = access;
|
|
143
|
+
if (typeof body?.refresh_token === "string" && body.refresh_token) {
|
|
144
|
+
entry.doc.refresh_token = body.refresh_token;
|
|
145
|
+
}
|
|
146
|
+
const expiresIn = body?.expires_in;
|
|
147
|
+
if (typeof expiresIn === "number" && Number.isFinite(expiresIn)) {
|
|
148
|
+
const at = now + expiresIn * 1000;
|
|
149
|
+
entry.doc.expires_at = new Date(at).toISOString();
|
|
150
|
+
expiresAt = expiresAt === null ? at : Math.min(expiresAt, at);
|
|
151
|
+
}
|
|
152
|
+
refreshed += 1;
|
|
153
|
+
}
|
|
154
|
+
if (refreshed === 0) return { ok: false, terminal: true, reason: "no refresh token to use" };
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
files: { ...files, "auth.json": `${JSON.stringify(parsed.doc, null, 2)}\n` },
|
|
158
|
+
expiresAt,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -41,6 +41,15 @@ import {
|
|
|
41
41
|
setMcpConnectionStoreRef,
|
|
42
42
|
type StoredMcpConnection,
|
|
43
43
|
} from "./mcp-connections";
|
|
44
|
+
import {
|
|
45
|
+
deleteProjectEnvLocal,
|
|
46
|
+
importProjectEnvFromStore,
|
|
47
|
+
listProjectEnvStoreRows,
|
|
48
|
+
projectEnvParameterName,
|
|
49
|
+
projectEnvStoreValue,
|
|
50
|
+
setProjectEnvStoreRef,
|
|
51
|
+
type StoredProjectEnv,
|
|
52
|
+
} from "./host-env";
|
|
44
53
|
import {
|
|
45
54
|
deleteKey as deleteSshKeyLocal,
|
|
46
55
|
importSshKeyFromStore,
|
|
@@ -308,3 +317,98 @@ export async function reconcileUserCredentialsFromStore(): Promise<ReconcileUser
|
|
|
308
317
|
}
|
|
309
318
|
return result;
|
|
310
319
|
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Slice 3: project env values, `<prefix>/projects/<projectId>/env/<KEY>`.
|
|
323
|
+
// Same three moves as above, own listing.
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
export function parseProjectEnvParameterName(
|
|
327
|
+
name: string,
|
|
328
|
+
prefix: string = credentialStorePrefix(),
|
|
329
|
+
): { projectId: string; key: string } | null {
|
|
330
|
+
if (!name.startsWith(`${prefix}/projects/`)) return null;
|
|
331
|
+
const parts = name.slice(prefix.length + 1).split("/");
|
|
332
|
+
if (parts.length !== 4 || parts[2] !== "env") return null;
|
|
333
|
+
if (!parts.every((p) => isParameterPathSegment(p))) return null;
|
|
334
|
+
const [, projectId, , key] = parts as [string, string, string, string];
|
|
335
|
+
return { projectId, key };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function parseStoredProjectEnv(value: string): StoredProjectEnv {
|
|
339
|
+
const o = obj(value);
|
|
340
|
+
if (o.kind !== "project-env") throw new Error("not a project env value");
|
|
341
|
+
return {
|
|
342
|
+
v: 1,
|
|
343
|
+
kind: "project-env",
|
|
344
|
+
projectId: str(o, "projectId"),
|
|
345
|
+
key: str(o, "key"),
|
|
346
|
+
value: str(o, "value"),
|
|
347
|
+
updatedAt: num(o, "updatedAt"),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function reconcileProjectEnvFromStore(): Promise<ReconcileUserCredentialsResult> {
|
|
352
|
+
const result: ReconcileUserCredentialsResult = {
|
|
353
|
+
skipped: false,
|
|
354
|
+
imported: 0,
|
|
355
|
+
uploaded: 0,
|
|
356
|
+
removed: 0,
|
|
357
|
+
errors: 0,
|
|
358
|
+
};
|
|
359
|
+
const store = configuredCredentialStore();
|
|
360
|
+
if (!store) return { ...result, skipped: true };
|
|
361
|
+
const prefix = credentialStorePrefix();
|
|
362
|
+
const path = `${prefix}/projects`;
|
|
363
|
+
const warn = (line: string): void => console.warn(`[project-env] credential store: ${line}`);
|
|
364
|
+
let listed: Array<{ name: string; value: string }>;
|
|
365
|
+
try {
|
|
366
|
+
listed = await store.list(path);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
result.errors++;
|
|
369
|
+
warn(`listing ${path} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
const local = new Map(listProjectEnvStoreRows().map((r) => [`${r.projectId}/${r.key}`, r]));
|
|
373
|
+
const seen = new Set<string>();
|
|
374
|
+
for (const param of listed) {
|
|
375
|
+
const at = parseProjectEnvParameterName(param.name, prefix);
|
|
376
|
+
if (!at) continue;
|
|
377
|
+
seen.add(param.name);
|
|
378
|
+
try {
|
|
379
|
+
const value = parseStoredProjectEnv(param.value);
|
|
380
|
+
if (value.projectId !== at.projectId || value.key !== at.key) {
|
|
381
|
+
throw new Error("payload does not describe the value its path names");
|
|
382
|
+
}
|
|
383
|
+
const row = local.get(`${at.projectId}/${at.key}`);
|
|
384
|
+
if (row) {
|
|
385
|
+
if (row.storeRef !== param.name) setProjectEnvStoreRef(at.projectId, at.key, param.name);
|
|
386
|
+
} else if (importProjectEnvFromStore(value, param.name)) {
|
|
387
|
+
result.imported++;
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
result.errors++;
|
|
391
|
+
warn(`${param.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const row of local.values()) {
|
|
395
|
+
try {
|
|
396
|
+
if (row.storeRef) {
|
|
397
|
+
if (row.storeRef.startsWith(`${path}/`) && !seen.has(row.storeRef)) {
|
|
398
|
+
deleteProjectEnvLocal(row.projectId, row.key);
|
|
399
|
+
result.removed++;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const name = projectEnvParameterName(row.projectId, row.key);
|
|
404
|
+
const value = projectEnvStoreValue(row.projectId, row.key);
|
|
405
|
+
if (!name || !value) continue;
|
|
406
|
+
await upload(store, name, value, (ref) => setProjectEnvStoreRef(row.projectId, row.key, ref));
|
|
407
|
+
result.uploaded++;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
result.errors++;
|
|
410
|
+
warn(`${row.projectId}/${row.key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runuai/host",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.115",
|
|
4
4
|
"description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uai Tech <team@runuai.com>",
|
|
@@ -76,10 +76,12 @@
|
|
|
76
76
|
"zod": "^3.23.8"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
+
"@aws-sdk/client-ssm": "3.899.0",
|
|
79
80
|
"@types/node": "^22.9.0",
|
|
80
81
|
"@types/ws": "^8.18.1",
|
|
81
82
|
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
|
82
83
|
"@typescript-eslint/parser": "^8.59.4",
|
|
84
|
+
"esbuild": "0.25.8",
|
|
83
85
|
"eslint": "^9.14.0",
|
|
84
86
|
"typescript": "^5.6.3",
|
|
85
87
|
"vitest": "^2.1.4"
|
package/scripts/agent/task-up.sh
CHANGED
|
@@ -2828,6 +2828,22 @@ if crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
|
2828
2828
|
/usr/bin/mkdir -p /home/node/.codex >/dev/null; then
|
|
2829
2829
|
for codex_item in auth.json config.toml AGENTS.md version.json installation_id rules; do
|
|
2830
2830
|
if [ -e "$UAI_OWNER_HOME/.codex/$codex_item" ]; then
|
|
2831
|
+
# ADR-135: the task's copy must not carry the rotating refresh token —
|
|
2832
|
+
# N copies racing to refresh it revoke the whole family. Replace it with
|
|
2833
|
+
# the broker placeholder (the field must exist; codex refuses a missing
|
|
2834
|
+
# one). The host is the only refresher and re-pushes on refresh.
|
|
2835
|
+
if [ "$codex_item" = auth.json ] && command -v jq >/dev/null 2>&1; then
|
|
2836
|
+
brokered_auth="$(mktemp "${TMPDIR:-/tmp}/uai-codex-auth.XXXXXX")"
|
|
2837
|
+
if jq 'if (.tokens|type) == "object" and (.tokens.refresh_token|type) == "string" then .tokens.refresh_token = "uai-broker-managed-no-refresh" else . end' \
|
|
2838
|
+
"$UAI_OWNER_HOME/.codex/auth.json" > "$brokered_auth" 2>/dev/null && [ -s "$brokered_auth" ]; then
|
|
2839
|
+
crt cp "$brokered_auth" "$app_container":/home/node/.codex/auth.json >/dev/null \
|
|
2840
|
+
|| log "ERROR: crt cp of brokered .codex/auth.json failed; Codex in this task may not authenticate"
|
|
2841
|
+
rm -f "$brokered_auth"
|
|
2842
|
+
continue
|
|
2843
|
+
fi
|
|
2844
|
+
rm -f "$brokered_auth"
|
|
2845
|
+
log "warning: could not broker .codex/auth.json; copying it verbatim"
|
|
2846
|
+
fi
|
|
2831
2847
|
crt cp "$UAI_OWNER_HOME/.codex/$codex_item" \
|
|
2832
2848
|
"$app_container":/home/node/.codex/ >/dev/null \
|
|
2833
2849
|
|| log "ERROR: crt cp of .codex/$codex_item failed; Codex in this task may not authenticate"
|
|
@@ -2865,11 +2881,25 @@ crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
|
2865
2881
|
|
|
2866
2882
|
# Copy Grok subscription/config into a task-private /home/node/.grok. The Linux
|
|
2867
2883
|
# `grok` binary is baked into the image; here we copy the arch-independent
|
|
2868
|
-
# auth + config
|
|
2884
|
+
# auth + config. auth.json is an OIDC token: the task's copy gets the broker
|
|
2885
|
+
# placeholder in every entry's refresh_token (ADR-135) so it can never rotate
|
|
2886
|
+
# the family; the host is the only refresher.
|
|
2869
2887
|
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2870
2888
|
/usr/bin/mkdir -p /home/node/.grok >/dev/null 2>&1 || true
|
|
2871
2889
|
for grok_item in auth.json config.toml models_cache.json agent_id; do
|
|
2872
2890
|
if [ -e "$UAI_OWNER_HOME/.grok/$grok_item" ]; then
|
|
2891
|
+
if [ "$grok_item" = auth.json ] && command -v jq >/dev/null 2>&1; then
|
|
2892
|
+
brokered_auth="$(mktemp "${TMPDIR:-/tmp}/uai-grok-auth.XXXXXX")"
|
|
2893
|
+
if jq 'with_entries(if (.value|type) == "object" and (.value.refresh_token|type) == "string" then .value.refresh_token = "uai-broker-managed-no-refresh" else . end)' \
|
|
2894
|
+
"$UAI_OWNER_HOME/.grok/auth.json" > "$brokered_auth" 2>/dev/null && [ -s "$brokered_auth" ]; then
|
|
2895
|
+
crt cp "$brokered_auth" "$app_container":/home/node/.grok/auth.json >/dev/null 2>&1 \
|
|
2896
|
+
|| log "warning: crt cp of brokered .grok/auth.json failed; grok may need re-login"
|
|
2897
|
+
rm -f "$brokered_auth"
|
|
2898
|
+
continue
|
|
2899
|
+
fi
|
|
2900
|
+
rm -f "$brokered_auth"
|
|
2901
|
+
log "warning: could not broker .grok/auth.json; copying it verbatim"
|
|
2902
|
+
fi
|
|
2873
2903
|
crt cp "$UAI_OWNER_HOME/.grok/$grok_item" \
|
|
2874
2904
|
"$app_container":/home/node/.grok/ >/dev/null 2>&1 \
|
|
2875
2905
|
|| log "warning: crt cp of .grok/$grok_item failed; grok may need re-login"
|
package/src/main.ts
CHANGED
|
@@ -57,7 +57,13 @@ import {
|
|
|
57
57
|
deleteUserSshIdentity,
|
|
58
58
|
removeTaskSshIdentityFromContainer,
|
|
59
59
|
} from "../lib/git-identity";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
reinjectCodexRunningTasks,
|
|
62
|
+
reinjectGrokRunningTasks,
|
|
63
|
+
watchCodexAuth,
|
|
64
|
+
} from "../lib/codex-auth";
|
|
65
|
+
import { startCredentialStorePickup } from "../lib/credential-store-pickup";
|
|
66
|
+
import { configureTokenBroker, startTokenBrokerLoop } from "../lib/token-broker-local";
|
|
61
67
|
import {
|
|
62
68
|
ensureKeyForUser as ensureSshKeyForUser,
|
|
63
69
|
getPublicKey as getSshPublicKey,
|
|
@@ -157,7 +163,10 @@ import {
|
|
|
157
163
|
listEngineAccountsAllScopes,
|
|
158
164
|
reconcileEngineAccountsFromStore,
|
|
159
165
|
} from "../lib/engine-accounts";
|
|
160
|
-
import {
|
|
166
|
+
import {
|
|
167
|
+
reconcileProjectEnvFromStore,
|
|
168
|
+
reconcileUserCredentialsFromStore,
|
|
169
|
+
} from "../lib/user-credentials-store";
|
|
161
170
|
import { createEngineConnectManager } from "../lib/engine-connect";
|
|
162
171
|
import { createEngineLoginManager } from "../lib/engine-login";
|
|
163
172
|
import {
|
|
@@ -459,6 +468,17 @@ void reconcileEngineAccountsFromStore({ hostId })
|
|
|
459
468
|
`[engine-accounts] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
460
469
|
);
|
|
461
470
|
});
|
|
471
|
+
// ADR-135: one refresher per rotating credential. Owned host → the in-process
|
|
472
|
+
// loop refreshes and re-pushes; Uai-operated host → the broker Lambda
|
|
473
|
+
// refreshes and this host picks up changed parameter versions.
|
|
474
|
+
const repushBrokeredCopies = async (kind: string): Promise<void> => {
|
|
475
|
+
if (kind === "codex") await reinjectCodexRunningTasks();
|
|
476
|
+
if (kind === "grok") await reinjectGrokRunningTasks();
|
|
477
|
+
await getOrchestrator().refreshEngineAccountCopies(kind);
|
|
478
|
+
};
|
|
479
|
+
configureTokenBroker({ repush: repushBrokeredCopies });
|
|
480
|
+
startTokenBrokerLoop();
|
|
481
|
+
startCredentialStorePickup({ repush: repushBrokeredCopies });
|
|
462
482
|
void reconcileUserCredentialsFromStore()
|
|
463
483
|
.then((r) => {
|
|
464
484
|
if (r.skipped) return;
|
|
@@ -471,6 +491,18 @@ void reconcileUserCredentialsFromStore()
|
|
|
471
491
|
`[user-credentials] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
472
492
|
);
|
|
473
493
|
});
|
|
494
|
+
void reconcileProjectEnvFromStore()
|
|
495
|
+
.then((r) => {
|
|
496
|
+
if (r.skipped) return;
|
|
497
|
+
console.log(
|
|
498
|
+
`[project-env] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
|
|
499
|
+
);
|
|
500
|
+
})
|
|
501
|
+
.catch((error: unknown) => {
|
|
502
|
+
console.warn(
|
|
503
|
+
`[project-env] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
504
|
+
);
|
|
505
|
+
});
|
|
474
506
|
// Reconcile a durable retention-off setting before recovery/connect can make
|
|
475
507
|
// crash-left owner-facing terminal history observable again. Correctness
|
|
476
508
|
// tombstones remain intact inside applyTerminalHistoryRetention.
|