@runuai/host 0.9.138 → 0.9.140

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.
@@ -352,6 +352,19 @@ function resolveExtras(
352
352
  execEnv: { [d.selectVar]: containerDir },
353
353
  configDir: { hostDir: row.configDir, containerDir },
354
354
  });
355
+ } else if (row.authKind === "config-dir" && isStoreHeldRow(row)) {
356
+ // ADR-136 sign-in box: the material lives in the store, not here. The
357
+ // row is listed (label, scope — what the capabilities frame carries)
358
+ // and never provisioned: the box runs no task.
359
+ out.push({
360
+ id: row.id,
361
+ kind,
362
+ label: row.label,
363
+ isDefault: false,
364
+ authKind: "config-dir",
365
+ scope,
366
+ execEnv: {},
367
+ });
355
368
  }
356
369
  }
357
370
  return out;
@@ -822,14 +835,22 @@ export interface AddAccountResult {
822
835
  * Claude accepts a `setup-token` token or an `sk-ant-…` key; Codex accepts an
823
836
  * OpenAI API key (written into an isolated `~/.codex-acct-<id>/auth.json`).
824
837
  */
825
- export function addEngineAccount(
838
+ /** What a labeled account will hold once persisted — the validated shape
839
+ * shared by the local writer (owned and shared hosts) and the store-only
840
+ * writer (the ADR-136 sign-in box), so both refuse the same inputs with the
841
+ * same words. Never logged. */
842
+ type LabeledAccountPlan =
843
+ | { authKind: "env"; label: string; env: Record<string, string>; message: string }
844
+ | { authKind: "config-dir"; label: string; files: Record<string, string>; message: string };
845
+
846
+ type PlannedAccount = { ok: false; message: string } | { ok: true; plan: LabeledAccountPlan };
847
+
848
+ function planLabeledAccount(
826
849
  kind: string,
827
850
  label: string,
828
851
  creds: { apiKey?: string; token?: string },
829
- seams: Partial<AccountSeams> = {},
830
- scope: string = HOST_ACCOUNT_SCOPE,
831
- ): AddAccountResult {
832
- const s = withDefaults(seams);
852
+ scope: string,
853
+ ): PlannedAccount {
833
854
  if (!isEngineAccountScope(scope)) {
834
855
  return { ok: false, message: "The account scope is invalid." };
835
856
  }
@@ -840,72 +861,106 @@ export function addEngineAccount(
840
861
  if (/\s/.test(secret) || secret.length < 12) {
841
862
  return { ok: false, message: "That doesn't look like a token. Paste just the value." };
842
863
  }
843
-
844
- const id = newId();
845
- const now = Date.now();
846
-
847
864
  if (kind === "claude") {
848
865
  const envVar = secret.startsWith("sk-ant-api")
849
866
  ? "ANTHROPIC_API_KEY"
850
867
  : "CLAUDE_CODE_OAUTH_TOKEN";
851
- getDb()
852
- .insert(schema.engineAccounts)
853
- .values({
854
- id,
855
- kind,
856
- label: trimmedLabel,
868
+ return {
869
+ ok: true,
870
+ plan: {
857
871
  authKind: "env",
858
- secretEnc: packSecret(JSON.stringify({ [envVar]: secret })),
859
- configDir: null,
860
- createdAt: now,
861
- scope,
862
- position: nextPosition(kind, scope),
863
- })
864
- .run();
865
- mirrorEngineAccount(id);
866
- return { ok: true, message: `Added Claude account "${trimmedLabel}".`, id };
872
+ label: trimmedLabel,
873
+ env: { [envVar]: secret },
874
+ message: `Added Claude account "${trimmedLabel}".`,
875
+ },
876
+ };
867
877
  }
868
-
869
878
  const envKeyVar = API_KEY_ENV_VARS[kind];
870
879
  if (envKeyVar) {
871
- getDb()
872
- .insert(schema.engineAccounts)
873
- .values({
874
- id,
875
- kind,
876
- label: trimmedLabel,
880
+ return {
881
+ ok: true,
882
+ plan: {
877
883
  authKind: "env",
878
- secretEnc: packSecret(JSON.stringify({ [envKeyVar]: secret })),
879
- configDir: null,
880
- createdAt: now,
881
- scope,
882
- position: nextPosition(kind, scope),
883
- })
884
- .run();
885
- mirrorEngineAccount(id);
884
+ label: trimmedLabel,
885
+ env: { [envKeyVar]: secret },
886
+ message: `Added ${API_KEY_ENGINE_LABELS[kind] ?? kind} account "${trimmedLabel}".`,
887
+ },
888
+ };
889
+ }
890
+ if (kind === "codex") {
886
891
  return {
887
892
  ok: true,
888
- message: `Added ${API_KEY_ENGINE_LABELS[kind] ?? kind} account "${trimmedLabel}".`,
889
- id,
893
+ plan: {
894
+ authKind: "config-dir",
895
+ label: trimmedLabel,
896
+ files: { "auth.json": `${JSON.stringify({ OPENAI_API_KEY: secret }, null, 2)}\n` },
897
+ message: `Added Codex account "${trimmedLabel}".`,
898
+ },
890
899
  };
891
900
  }
901
+ return {
902
+ ok: false,
903
+ message: `Adding a second ${kind} account isn't supported yet — connect the primary account from the Engines panel.`,
904
+ };
905
+ }
892
906
 
893
- if (kind === "codex") {
894
- const d = KINDS.codex as KindDescriptor;
895
- const hostDir = (d.extraHostDir as (o: string, i: string) => string)(
896
- s.ownerHome(),
897
- id,
898
- );
907
+ function planCapturedConfigDirAccount(
908
+ kind: "codex" | "grok",
909
+ label: string,
910
+ authJson: string,
911
+ scope: string,
912
+ ): PlannedAccount {
913
+ const engineLabel = kind === "codex" ? "Codex" : "Grok";
914
+ if (!isEngineAccountScope(scope)) {
915
+ return { ok: false, message: "The account scope is invalid." };
916
+ }
917
+ const trimmedLabel = label.trim();
918
+ if (!trimmedLabel) return { ok: false, message: "Give the account a label." };
919
+ try {
920
+ const parsed: unknown = JSON.parse(authJson);
921
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
922
+ throw new Error("not an object");
923
+ }
924
+ } catch {
925
+ return { ok: false, message: `The captured ${engineLabel} credential is not valid.` };
926
+ }
927
+ return {
928
+ ok: true,
929
+ plan: {
930
+ authKind: "config-dir",
931
+ label: trimmedLabel,
932
+ files: { "auth.json": authJson.endsWith("\n") ? authJson : `${authJson}\n` },
933
+ message: `Added ${engineLabel} account "${trimmedLabel}".`,
934
+ },
935
+ };
936
+ }
937
+
938
+ /** Persist a plan on THIS host — a sealed env row or an isolated config dir
939
+ * under the owner's home — and mirror it to the store when one is configured. */
940
+ function persistPlannedAccountLocally(
941
+ kind: string,
942
+ scope: string,
943
+ plan: LabeledAccountPlan,
944
+ s: AccountSeams,
945
+ ): AddAccountResult {
946
+ const id = newId();
947
+ let configDir: string | null = null;
948
+ if (plan.authKind === "config-dir") {
949
+ const d = KINDS[kind];
950
+ if (!d?.extraHostDir) {
951
+ return { ok: false, message: `${kind} has no config-dir layout.` };
952
+ }
953
+ const hostDir = d.extraHostDir(s.ownerHome(), id);
899
954
  try {
900
- const file = join(hostDir, "auth.json");
901
955
  mkdirSync(hostDir, { recursive: true });
902
- writeFileSync(file, `${JSON.stringify({ OPENAI_API_KEY: secret }, null, 2)}\n`, {
903
- mode: 0o600,
904
- });
905
- try {
906
- chmodSync(file, 0o600);
907
- } catch {
908
- /* best effort */
956
+ for (const [basename, content] of Object.entries(plan.files)) {
957
+ const file = join(hostDir, basename);
958
+ writeFileSync(file, content, { mode: 0o600 });
959
+ try {
960
+ chmodSync(file, 0o600);
961
+ } catch {
962
+ /* best effort */
963
+ }
909
964
  }
910
965
  } catch (err) {
911
966
  return {
@@ -915,28 +970,36 @@ export function addEngineAccount(
915
970
  }`,
916
971
  };
917
972
  }
918
- getDb()
919
- .insert(schema.engineAccounts)
920
- .values({
921
- id,
922
- kind,
923
- label: trimmedLabel,
924
- authKind: "config-dir",
925
- secretEnc: null,
926
- configDir: hostDir,
927
- createdAt: now,
928
- scope,
929
- position: nextPosition(kind, scope),
930
- })
931
- .run();
932
- mirrorEngineAccount(id);
933
- return { ok: true, message: `Added Codex account "${trimmedLabel}".`, id };
973
+ configDir = hostDir;
934
974
  }
975
+ getDb()
976
+ .insert(schema.engineAccounts)
977
+ .values({
978
+ id,
979
+ kind,
980
+ label: plan.label,
981
+ authKind: plan.authKind,
982
+ secretEnc: plan.authKind === "env" ? packSecret(JSON.stringify(plan.env)) : null,
983
+ configDir,
984
+ createdAt: Date.now(),
985
+ scope,
986
+ position: nextPosition(kind, scope),
987
+ })
988
+ .run();
989
+ mirrorEngineAccount(id);
990
+ return { ok: true, message: plan.message, id };
991
+ }
935
992
 
936
- return {
937
- ok: false,
938
- message: `Adding a second ${kind} account isn't supported yet — connect the primary account from the Engines panel.`,
939
- };
993
+ export function addEngineAccount(
994
+ kind: string,
995
+ label: string,
996
+ creds: { apiKey?: string; token?: string },
997
+ seams: Partial<AccountSeams> = {},
998
+ scope: string = HOST_ACCOUNT_SCOPE,
999
+ ): AddAccountResult {
1000
+ const planned = planLabeledAccount(kind, label, creds, scope);
1001
+ if (!planned.ok) return planned;
1002
+ return persistPlannedAccountLocally(kind, scope, planned.plan, withDefaults(seams));
940
1003
  }
941
1004
 
942
1005
  /**
@@ -972,63 +1035,9 @@ function addConfigDirAccountFromAuthJson(
972
1035
  seams: Partial<AccountSeams>,
973
1036
  scope: string,
974
1037
  ): AddAccountResult {
975
- const engineLabel = kind === "codex" ? "Codex" : "Grok";
976
- if (!isEngineAccountScope(scope)) {
977
- return { ok: false, message: "The account scope is invalid." };
978
- }
979
- const s = withDefaults(seams);
980
- const trimmedLabel = label.trim();
981
- if (!trimmedLabel) return { ok: false, message: "Give the account a label." };
982
- try {
983
- const parsed: unknown = JSON.parse(authJson);
984
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
985
- throw new Error("not an object");
986
- }
987
- } catch {
988
- return { ok: false, message: `The captured ${engineLabel} credential is not valid.` };
989
- }
990
-
991
- const id = newId();
992
- const d = KINDS[kind] as KindDescriptor;
993
- const hostDir = (d.extraHostDir as (o: string, i: string) => string)(
994
- s.ownerHome(),
995
- id,
996
- );
997
- try {
998
- const file = join(hostDir, "auth.json");
999
- mkdirSync(hostDir, { recursive: true });
1000
- writeFileSync(file, authJson.endsWith("\n") ? authJson : `${authJson}\n`, {
1001
- mode: 0o600,
1002
- });
1003
- try {
1004
- chmodSync(file, 0o600);
1005
- } catch {
1006
- /* best effort */
1007
- }
1008
- } catch (err) {
1009
- return {
1010
- ok: false,
1011
- message: `Could not write the account config: ${
1012
- err instanceof Error ? err.message : String(err)
1013
- }`,
1014
- };
1015
- }
1016
- getDb()
1017
- .insert(schema.engineAccounts)
1018
- .values({
1019
- id,
1020
- kind,
1021
- label: trimmedLabel,
1022
- authKind: "config-dir",
1023
- secretEnc: null,
1024
- configDir: hostDir,
1025
- createdAt: Date.now(),
1026
- scope,
1027
- position: nextPosition(kind, scope),
1028
- })
1029
- .run();
1030
- mirrorEngineAccount(id);
1031
- return { ok: true, message: `Added ${engineLabel} account "${trimmedLabel}".`, id };
1038
+ const planned = planCapturedConfigDirAccount(kind, label, authJson, scope);
1039
+ if (!planned.ok) return planned;
1040
+ return persistPlannedAccountLocally(kind, scope, planned.plan, withDefaults(seams));
1032
1041
  }
1033
1042
 
1034
1043
  /** Remove an EXTRA account (idempotent). The synthesized default can't be
@@ -1346,6 +1355,11 @@ function setStoreRef(id: string, storeRef: string): void {
1346
1355
  }
1347
1356
 
1348
1357
  async function putRow(store: CredentialStore, row: StoreRow, hostId: string | null): Promise<boolean> {
1358
+ if (isStoreHeldRow(row)) {
1359
+ // ADR-136: nothing here to mirror — an empty env put would overwrite the
1360
+ // only copy of the credential.
1361
+ throw new Error(`account ${row.id} is store-held; this runner holds no material to mirror`);
1362
+ }
1349
1363
  const name = engineAccountParameterName(row, hostId);
1350
1364
  if (!name) {
1351
1365
  if (!warnedNoHostId) {
@@ -1368,6 +1382,13 @@ export function mirrorEngineAccount(id: string): void {
1368
1382
  queueStoreWrite(`mirror ${id}`, async (store) => {
1369
1383
  const row = selectRow(id);
1370
1384
  if (!row) return;
1385
+ if (isStoreHeldRow(row)) {
1386
+ // ADR-136 sign-in box: a rename or reorder rides as metadata tags —
1387
+ // the secret is not here to re-put, and tags are what a replacement
1388
+ // box reads back.
1389
+ await store.tag(row.storeRef!, engineAccountMetadataTags(row));
1390
+ return;
1391
+ }
1371
1392
  await putRow(store, row, storeHostId);
1372
1393
  });
1373
1394
  }
@@ -1565,4 +1586,288 @@ export function applyStoredEngineAccount(
1565
1586
  throw new Error("stored auth kind does not match the local row");
1566
1587
  }
1567
1588
  return { kind: stored.kind, id: stored.id, imported: false };
1568
- }
1589
+ }
1590
+
1591
+ // ---------------------------------------------------------------------------
1592
+ // Store-only accounts (ADR-136 sign-in box, docs/shared-host-retirement.md).
1593
+ //
1594
+ // The small always-on machine behind the "Uai Cloud" row writes every capture
1595
+ // STRAIGHT into the credential store and keeps only a metadata row — id, kind,
1596
+ // label, scope, position, store_ref — with no sealed secret and no config dir.
1597
+ // Its store identity is write-only (put, tag, delete, describe names; never
1598
+ // get, never decrypt), so a compromised box can break sign-ins but cannot read
1599
+ // one. Labels therefore also ride as parameter TAGS, which
1600
+ // `ssm:ListTagsForResource` returns without a KMS grant: a replacement box
1601
+ // rebuilds its list from names + tags alone (`reconcileEngineAccountMetadataFromStore`).
1602
+ // ---------------------------------------------------------------------------
1603
+
1604
+ export const ACCOUNT_LABEL_TAG = "uai_label_b64";
1605
+ export const ACCOUNT_POSITION_TAG = "uai_position";
1606
+ export const ACCOUNT_CREATED_AT_TAG = "uai_created_at";
1607
+ export const ACCOUNT_AUTH_KIND_TAG = "uai_auth_kind";
1608
+ /** SSM tag values: 256 chars of `[A-Za-z0-9_.:/=+@ -]`; base64url fits. */
1609
+ const MAX_TAG_VALUE_CHARS = 256;
1610
+
1611
+ /** A row whose material lives ONLY in the store: a store_ref and no secret. */
1612
+ export function isStoreHeldRow(row: {
1613
+ authKind: string;
1614
+ secretEnc: string | null;
1615
+ configDir: string | null;
1616
+ storeRef?: string | null;
1617
+ }): boolean {
1618
+ if (!row.storeRef) return false;
1619
+ return row.authKind === "env" ? row.secretEnc === null : row.configDir === null;
1620
+ }
1621
+
1622
+ /** The secret-free metadata a parameter carries as tags. The label is
1623
+ * base64url so any label character survives SSM's tag charset; a label too
1624
+ * long to fit is shortened (the store value keeps the full one). */
1625
+ export function engineAccountMetadataTags(row: {
1626
+ label: string;
1627
+ authKind: string;
1628
+ position?: number | null;
1629
+ createdAt: number;
1630
+ }): Record<string, string> {
1631
+ let label = row.label;
1632
+ let encoded = Buffer.from(label, "utf8").toString("base64url");
1633
+ while (encoded.length > MAX_TAG_VALUE_CHARS && label.length > 1) {
1634
+ label = label.slice(0, -1);
1635
+ encoded = Buffer.from(label, "utf8").toString("base64url");
1636
+ }
1637
+ return {
1638
+ [ACCOUNT_LABEL_TAG]: encoded,
1639
+ [ACCOUNT_POSITION_TAG]: String(row.position ?? 0),
1640
+ [ACCOUNT_CREATED_AT_TAG]: String(row.createdAt),
1641
+ [ACCOUNT_AUTH_KIND_TAG]: row.authKind,
1642
+ };
1643
+ }
1644
+
1645
+ function derivedAccountLabel(kind: string, id: string): string {
1646
+ const engine = kind.charAt(0).toUpperCase() + kind.slice(1);
1647
+ return `${engine} ${id.slice(-6)}`;
1648
+ }
1649
+
1650
+ function labelFromTags(tags: Record<string, string>, kind: string, id: string): string {
1651
+ const encoded = tags[ACCOUNT_LABEL_TAG];
1652
+ if (encoded) {
1653
+ try {
1654
+ const label = Buffer.from(encoded, "base64url").toString("utf8").trim();
1655
+ if (isEngineAccountLabel(label)) return label;
1656
+ } catch {
1657
+ /* fall through */
1658
+ }
1659
+ }
1660
+ return derivedAccountLabel(kind, id);
1661
+ }
1662
+
1663
+ async function persistPlannedAccountToStore(
1664
+ kind: string,
1665
+ scope: string,
1666
+ plan: LabeledAccountPlan,
1667
+ ): Promise<AddAccountResult> {
1668
+ const store = configuredCredentialStore();
1669
+ if (!store) {
1670
+ return { ok: false, message: "This runner has no credential store to hold the account." };
1671
+ }
1672
+ if (scope === HOST_ACCOUNT_SCOPE) {
1673
+ return { ok: false, message: "Store-only accounts belong to a workspace, not the runner." };
1674
+ }
1675
+ const id = newId();
1676
+ const now = Date.now();
1677
+ const position = nextPosition(kind, scope);
1678
+ const row = { id, kind, label: plan.label, authKind: plan.authKind, scope, position, createdAt: now };
1679
+ const name = engineAccountParameterName(row);
1680
+ if (!name) {
1681
+ return { ok: false, message: "Store-only accounts belong to a workspace, not the runner." };
1682
+ }
1683
+ const stored: StoredEngineAccount = {
1684
+ v: 1,
1685
+ ...row,
1686
+ ...(plan.authKind === "env" ? { env: plan.env } : { files: plan.files }),
1687
+ };
1688
+ try {
1689
+ await store.put(name, JSON.stringify(stored));
1690
+ } catch (err) {
1691
+ console.warn(
1692
+ `[engine-accounts] store-only ${kind}/${id}: put failed (${err instanceof Error ? err.message : String(err)})`,
1693
+ );
1694
+ return {
1695
+ ok: false,
1696
+ message: "The credential store refused the write; nothing was kept on this runner.",
1697
+ };
1698
+ }
1699
+ try {
1700
+ await store.tag(name, engineAccountMetadataTags(row));
1701
+ } catch (err) {
1702
+ console.warn(
1703
+ `[engine-accounts] store-only ${kind}/${id}: tagging failed (${err instanceof Error ? err.message : String(err)}) — a replacement sign-in box lists it as "${derivedAccountLabel(kind, id)}"`,
1704
+ );
1705
+ }
1706
+ getDb()
1707
+ .insert(schema.engineAccounts)
1708
+ .values({
1709
+ id,
1710
+ kind,
1711
+ label: plan.label,
1712
+ authKind: plan.authKind,
1713
+ secretEnc: null,
1714
+ configDir: null,
1715
+ createdAt: now,
1716
+ scope,
1717
+ position,
1718
+ storeRef: name,
1719
+ })
1720
+ .run();
1721
+ return { ok: true, message: plan.message, id };
1722
+ }
1723
+
1724
+ /** `addEngineAccount` for the sign-in box: the same validation, the store
1725
+ * written FIRST and awaited, then a metadata-only row. A store failure is the
1726
+ * caller's failure and leaves nothing behind. */
1727
+ export async function addEngineAccountToStore(
1728
+ kind: string,
1729
+ label: string,
1730
+ creds: { apiKey?: string; token?: string },
1731
+ scope: string,
1732
+ ): Promise<AddAccountResult> {
1733
+ const planned = planLabeledAccount(kind, label, creds, scope);
1734
+ if (!planned.ok) return planned;
1735
+ return persistPlannedAccountToStore(kind, scope, planned.plan);
1736
+ }
1737
+
1738
+ /** `addCodexAccountFromAuthJson` / `addGrokAccountFromAuthJson` for the
1739
+ * sign-in box: the captured auth.json goes to the store, never to a dir. */
1740
+ export async function addCapturedConfigDirAccountToStore(
1741
+ kind: "codex" | "grok",
1742
+ label: string,
1743
+ authJson: string,
1744
+ scope: string,
1745
+ ): Promise<AddAccountResult> {
1746
+ const planned = planCapturedConfigDirAccount(kind, label, authJson, scope);
1747
+ if (!planned.ok) return planned;
1748
+ return persistPlannedAccountToStore(kind, scope, planned.plan);
1749
+ }
1750
+
1751
+ export interface PurgeLocalEngineAccountSecretsResult {
1752
+ /** Workspace rows whose sealed secret or config dir was removed. */
1753
+ stripped: number;
1754
+ /** Rows deleted outright: host-scope accounts and rows never mirrored. */
1755
+ removed: number;
1756
+ errors: number;
1757
+ }
1758
+
1759
+ /**
1760
+ * Sign-in box start: nothing the previous release (or an operator) left on
1761
+ * this machine may hold a credential. Workspace rows keep their metadata and
1762
+ * lose their material; host-scope rows and rows the store never received are
1763
+ * deleted — the box holds workspace accounts only, and a secret with no copy
1764
+ * in the store still may not live here.
1765
+ */
1766
+ export function purgeLocalEngineAccountSecrets(): PurgeLocalEngineAccountSecretsResult {
1767
+ const result: PurgeLocalEngineAccountSecretsResult = { stripped: 0, removed: 0, errors: 0 };
1768
+ const rows = getDb().select().from(schema.engineAccounts).all() as StoreRow[];
1769
+ for (const row of rows) {
1770
+ const scope = row.scope ?? HOST_ACCOUNT_SCOPE;
1771
+ if (scope === HOST_ACCOUNT_SCOPE || !row.storeRef) {
1772
+ removeLocalAccountRow(row);
1773
+ result.removed++;
1774
+ continue;
1775
+ }
1776
+ if (isStoreHeldRow(row)) continue;
1777
+ if (row.configDir) {
1778
+ try {
1779
+ rmSync(row.configDir, { recursive: true, force: true });
1780
+ } catch {
1781
+ result.errors++;
1782
+ }
1783
+ }
1784
+ getDb()
1785
+ .update(schema.engineAccounts)
1786
+ .set({ secretEnc: null, configDir: null })
1787
+ .where(eq(schema.engineAccounts.id, row.id))
1788
+ .run();
1789
+ result.stripped++;
1790
+ }
1791
+ return result;
1792
+ }
1793
+
1794
+ export interface ReconcileEngineAccountMetadataResult {
1795
+ skipped: boolean;
1796
+ imported: number;
1797
+ removed: number;
1798
+ errors: number;
1799
+ }
1800
+
1801
+ /**
1802
+ * Sign-in box start: make the metadata rows agree with the store WITHOUT
1803
+ * reading a value. Names come from `describe-parameters` (allowed on `*`),
1804
+ * labels and order from each new parameter's tags. A workspace parameter this
1805
+ * box lacks becomes a metadata row; a row whose parameter is gone is dropped;
1806
+ * host-scope rows are dropped (the box has none). A failed listing touches
1807
+ * nothing.
1808
+ */
1809
+ export async function reconcileEngineAccountMetadataFromStore(): Promise<ReconcileEngineAccountMetadataResult> {
1810
+ const result: ReconcileEngineAccountMetadataResult = { skipped: false, imported: 0, removed: 0, errors: 0 };
1811
+ const store = configuredCredentialStore();
1812
+ if (!store) return { ...result, skipped: true };
1813
+ const prefix = credentialStorePrefix();
1814
+ const warn = (line: string): void => console.warn(`[engine-accounts] metadata reconcile: ${line}`);
1815
+ let names: string[];
1816
+ try {
1817
+ names = await store.describeNames(`${prefix}/orgs`);
1818
+ } catch (err) {
1819
+ warn(`listing ${prefix}/orgs failed: ${err instanceof Error ? err.message : String(err)}`);
1820
+ return { ...result, errors: 1 };
1821
+ }
1822
+ const rows = getDb().select().from(schema.engineAccounts).all() as StoreRow[];
1823
+ const byRef = new Set(rows.map((r) => r.storeRef).filter((ref): ref is string => Boolean(ref)));
1824
+ const seen = new Set<string>();
1825
+ for (const name of names) {
1826
+ const at = parseEngineAccountParameterName(name, prefix);
1827
+ if (!at || at.scope === HOST_ACCOUNT_SCOPE) continue;
1828
+ seen.add(name);
1829
+ if (byRef.has(name)) continue;
1830
+ let tags: Record<string, string> = {};
1831
+ try {
1832
+ tags = await store.listTags(name);
1833
+ } catch (err) {
1834
+ result.errors++;
1835
+ warn(`tags of ${name} unreadable: ${err instanceof Error ? err.message : String(err)}`);
1836
+ }
1837
+ const taggedAuthKind = tags[ACCOUNT_AUTH_KIND_TAG];
1838
+ const authKind: AccountAuthKind =
1839
+ taggedAuthKind === "env" || taggedAuthKind === "config-dir"
1840
+ ? taggedAuthKind
1841
+ : (KINDS[at.kind]?.authKind ?? "env");
1842
+ const position = Number.parseInt(tags[ACCOUNT_POSITION_TAG] ?? "", 10);
1843
+ const createdAt = Number.parseInt(tags[ACCOUNT_CREATED_AT_TAG] ?? "", 10);
1844
+ try {
1845
+ getDb()
1846
+ .insert(schema.engineAccounts)
1847
+ .values({
1848
+ id: at.id,
1849
+ kind: at.kind,
1850
+ label: labelFromTags(tags, at.kind, at.id),
1851
+ authKind,
1852
+ secretEnc: null,
1853
+ configDir: null,
1854
+ scope: at.scope,
1855
+ position: Number.isFinite(position) ? position : 0,
1856
+ storeRef: name,
1857
+ createdAt: Number.isFinite(createdAt) ? createdAt : Date.now(),
1858
+ })
1859
+ .run();
1860
+ result.imported++;
1861
+ } catch (err) {
1862
+ result.errors++;
1863
+ warn(`importing ${name} failed: ${err instanceof Error ? err.message : String(err)}`);
1864
+ }
1865
+ }
1866
+ for (const row of rows) {
1867
+ const scope = row.scope ?? HOST_ACCOUNT_SCOPE;
1868
+ if (scope !== HOST_ACCOUNT_SCOPE && row.storeRef && seen.has(row.storeRef)) continue;
1869
+ removeLocalAccountRow(row);
1870
+ result.removed++;
1871
+ }
1872
+ return result;
1873
+ }