@homeflare/alchemy 0.36.0 → 0.37.1

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.
Files changed (116) hide show
  1. package/README.md +14 -0
  2. package/dist/argocd/cluster.d.ts +102 -0
  3. package/dist/argocd/cluster.d.ts.map +1 -0
  4. package/dist/argocd/credentials.d.ts +37 -0
  5. package/dist/argocd/credentials.d.ts.map +1 -0
  6. package/dist/argocd/fake-argocd.d.ts +42 -0
  7. package/dist/argocd/fake-argocd.d.ts.map +1 -0
  8. package/dist/argocd/git-credentials.d.ts +29 -0
  9. package/dist/argocd/git-credentials.d.ts.map +1 -0
  10. package/dist/argocd/index.d.ts +19 -0
  11. package/dist/argocd/index.d.ts.map +1 -0
  12. package/dist/argocd/index.js +306 -0
  13. package/dist/argocd/index.js.map +17 -0
  14. package/dist/argocd/providers.d.ts +13 -0
  15. package/dist/argocd/providers.d.ts.map +1 -0
  16. package/dist/argocd/repo-creds.d.ts +67 -0
  17. package/dist/argocd/repo-creds.d.ts.map +1 -0
  18. package/dist/argocd/repository.d.ts +87 -0
  19. package/dist/argocd/repository.d.ts.map +1 -0
  20. package/dist/argocd/resource.d.ts +90 -0
  21. package/dist/argocd/resource.d.ts.map +1 -0
  22. package/dist/cloudflare/index.js +4 -4
  23. package/dist/cloudflare/index.js.map +3 -3
  24. package/dist/cloudflare/r2-bucket-lock.d.ts +14 -5
  25. package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -1
  26. package/dist/litellm/index.js.map +2 -2
  27. package/dist/litellm/operations.d.ts.map +1 -1
  28. package/dist/proxmox/ceph-daemon-form.d.ts +4 -5
  29. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
  30. package/dist/proxmox/ceph-daemon-wire.d.ts +31 -0
  31. package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -0
  32. package/dist/proxmox/ceph-daemon.d.ts +40 -6
  33. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  34. package/dist/proxmox/ceph-fs-distilled.d.ts +33 -0
  35. package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -0
  36. package/dist/proxmox/ceph-fs-wire.d.ts +20 -11
  37. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  38. package/dist/proxmox/ceph-fs.d.ts +30 -6
  39. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  40. package/dist/proxmox/ceph-osd-tree.d.ts +12 -3
  41. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -1
  42. package/dist/proxmox/ceph-osd-write.d.ts +17 -2
  43. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  44. package/dist/proxmox/ceph-osd.d.ts +6 -0
  45. package/dist/proxmox/ceph-osd.d.ts.map +1 -1
  46. package/dist/proxmox/ceph-pool-settle.d.ts +19 -12
  47. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
  48. package/dist/proxmox/ceph-pool-wire.d.ts +37 -0
  49. package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -0
  50. package/dist/proxmox/ceph-pool.d.ts +10 -5
  51. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  52. package/dist/proxmox/group-wire.d.ts +8 -13
  53. package/dist/proxmox/group-wire.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +605 -483
  55. package/dist/proxmox/index.js.map +20 -17
  56. package/dist/proxmox/storage-wire.d.ts +15 -11
  57. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  58. package/dist/proxmox/unreadable-read.d.ts +7 -7
  59. package/dist/proxmox/user-wire.d.ts +8 -11
  60. package/dist/proxmox/user-wire.d.ts.map +1 -1
  61. package/dist/proxmox/user.d.ts +5 -5
  62. package/docs/argocd-kubernetes.md +100 -0
  63. package/docs/argocd.md +139 -0
  64. package/docs/credentials.md +9 -0
  65. package/docs/distilled-interim.md +15 -3
  66. package/docs/litellm.md +17 -0
  67. package/docs/peers.md +7 -0
  68. package/docs/provider-standard.md +24 -6
  69. package/docs/proxmox.md +20 -0
  70. package/docs/unifi.md +45 -0
  71. package/docs/upstream-conformance-bun.md +67 -0
  72. package/docs/upstream-conformance-conforms.md +55 -0
  73. package/docs/upstream-conformance-families.md +95 -0
  74. package/docs/upstream-conformance-grafana.md +106 -0
  75. package/docs/upstream-conformance.md +20 -298
  76. package/package.json +8 -3
  77. package/src/argocd/cluster.test.ts +120 -0
  78. package/src/argocd/cluster.ts +222 -0
  79. package/src/argocd/credentials.ts +38 -0
  80. package/src/argocd/fake-argocd.ts +60 -0
  81. package/src/argocd/git-credentials.ts +71 -0
  82. package/src/argocd/index.ts +40 -0
  83. package/src/argocd/providers.ts +17 -0
  84. package/src/argocd/repo-creds.test.ts +123 -0
  85. package/src/argocd/repo-creds.ts +115 -0
  86. package/src/argocd/repository.test.ts +167 -0
  87. package/src/argocd/repository.ts +165 -0
  88. package/src/argocd/resource.ts +150 -0
  89. package/src/cloudflare/r2-bucket-lock.test.ts +54 -18
  90. package/src/cloudflare/r2-bucket-lock.ts +25 -18
  91. package/src/litellm/operations.ts +8 -4
  92. package/src/proxmox/ceph-daemon-form.ts +7 -8
  93. package/src/proxmox/ceph-daemon-wire.ts +112 -0
  94. package/src/proxmox/ceph-daemon-write.test.ts +158 -0
  95. package/src/proxmox/ceph-daemon.ts +70 -53
  96. package/src/proxmox/ceph-fs-delete.test.ts +81 -0
  97. package/src/proxmox/ceph-fs-distilled.ts +100 -0
  98. package/src/proxmox/ceph-fs-wire.ts +17 -31
  99. package/src/proxmox/ceph-fs-write.test.ts +67 -0
  100. package/src/proxmox/ceph-fs.ts +68 -73
  101. package/src/proxmox/ceph-osd-tree.ts +14 -5
  102. package/src/proxmox/ceph-osd-write.test.ts +61 -0
  103. package/src/proxmox/ceph-osd-write.ts +33 -6
  104. package/src/proxmox/ceph-osd.ts +6 -0
  105. package/src/proxmox/ceph-pool-settle.ts +24 -17
  106. package/src/proxmox/ceph-pool-wire.ts +101 -0
  107. package/src/proxmox/ceph-pool-write.test.ts +146 -0
  108. package/src/proxmox/ceph-pool.ts +87 -98
  109. package/src/proxmox/group-wire.ts +7 -12
  110. package/src/proxmox/read-failure-propagation.test.ts +5 -5
  111. package/src/proxmox/storage-wire.ts +9 -10
  112. package/src/proxmox/typed-absence.test.ts +146 -0
  113. package/src/proxmox/unreadable-read.ts +7 -7
  114. package/src/proxmox/user-wire.ts +7 -10
  115. package/src/proxmox/user.ts +5 -5
  116. package/src/proxmox/distilled-task-await.test.ts +0 -124
@@ -1,3 +1,10 @@
1
+ /**
2
+ * `Proxmox.Storage`'s READ side: how a live item becomes attributes, the read itself, and
3
+ * `matches`. Split out of storage.ts (2026-09-24, the distilled migration); the write side is
4
+ * storage-form.ts (that file's own header has the reasoning) — both stay under the 250-line cap,
5
+ * the api-token.ts/api-token-form.ts seam.
6
+ */
7
+ import * as storage from '@distilled.cloud/proxmox/storage';
1
8
  import * as Effect from 'effect/Effect';
2
9
  import type { StorageProps } from './storage.ts';
3
10
  import { UNREADABLE, type Unreadable } from './unreadable-read.ts';
@@ -33,19 +40,16 @@ export declare const same: <T>(declared: T | undefined, live: T) => boolean;
33
40
  * `readUser`/`readGroup` do, so a brand-new declaration can still be created; `readStorageOrFail`
34
41
  * (used by `diff`) does not.
35
42
  */
36
- export declare const readStorageOrFail: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
43
+ export declare const readStorageOrFail: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | storage.StorageNotFound | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
37
44
  /**
38
- * ⛔ FOLDS A GENUINE READ FAILURE TO `undefined` TOO, AND ONLY `read`/`reconcile` MAY USE IT —
39
- * user.ts's `readUser` has the full reasoning. Safe here because a wrongful fold costs at most a
40
- * redundant `createStorage` POST, which PVE refuses loudly ("storage ID already exists") rather
41
- * than silently corrupting anything.
42
- * ⛔ `storage.ts`'s `diff` does **NOT** use this — the SAME bug class as the credential denial fix:
43
- * folding a TRANSIENT failure into "absent" at PLAN TIME is what forced a false `update` with
44
- * nothing compared (the ORIGINAL cries-wolf bug this family shipped with, per unreadable-read.ts's
45
- * own header). `diff` calls `readStorageOrFail` directly, so a genuine failure there propagates
46
- * and fails the whole plan loudly instead.
45
+ * ⛔ ONLY `StorageNotFound` MEANS ABSENT. The initial distilled transport swap preserved a
46
+ * catch-all fold because PVE reports missing objects as HTTP 500. SDK PR 265 now types that
47
+ * measured message, so permission errors, unrelated 500s and exhausted transports propagate
48
+ * instead of triggering a speculative create (decision 49 follow-up, 2026-09-24).
49
+ * ★ `readStorageOrFail` remains the strict path used for an already-confirmed state row. This
50
+ * change narrows cold reads/reconcile without changing that existing drift/diff contract.
47
51
  */
48
- export declare const readStorage: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
52
+ export declare const readStorage: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes | undefined, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
49
53
  /** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
50
54
  export declare const dropUnreadable: (live: StorageAttributes | Unreadable | undefined) => StorageAttributes | undefined;
51
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"storage-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage-wire.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAmBD,eAAO,MAAM,QAAQ,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YACN,CAAC;AAE9D,eAAO,MAAM,IAAI,GAAI,CAAC,YAAY,CAAC,GAAG,SAAS,QAAQ,CAAC,YACX,CAAC;AAwB9C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,UAAW,YAAY,m2BAGyC,CAAC;AAE/F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,UAAW,YAAY,kIACuB,CAAC;AAEvE,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,iBAAiB,GAAG,UAAU,GAAG,SAAS,kCACvC,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,iBAAiB,SAAS,YAAY,YAOjC,CAAC"}
1
+ {"version":3,"file":"storage-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage-wire.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,OAAO,MAAM,kCAAkC,CAAC;AAC5D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAmBD,eAAO,MAAM,QAAQ,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YACN,CAAC;AAE9D,eAAO,MAAM,IAAI,GAAI,CAAC,YAAY,CAAC,GAAG,SAAS,QAAQ,CAAC,YACX,CAAC;AAwB9C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,UAAW,YAAY,63BAGyC,CAAC;AAE/F;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,UAAW,YAAY,+2BAG5C,CAAC;AAEJ,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,iBAAiB,GAAG,UAAU,GAAG,SAAS,kCACvC,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,iBAAiB,SAAS,YAAY,YAOjC,CAAC"}
@@ -18,10 +18,10 @@
18
18
  * on a lane that cannot prove one is needed and (b) never takes the rest of the plan down with
19
19
  * it — the "noop-with-warning" reading of the fix, over the alternative of a typed plan error.
20
20
  *
21
- * ⛔ ONLY THE CREDENTIAL DENIAL IS CAUGHT HERE. Every other read failure (a genuine
22
- * `Cause.TimeoutError`, `PveClusterExhausted`, a network error) keeps folding into `undefined`
23
- * exactly as it always has — this narrows ONE typed tag out of that fold, via `catchTag`, and
24
- * changes nothing else about it. See credential-errors.ts's own ⛔ for why 403 alone qualifies.
21
+ * ⛔ ONLY THE CREDENTIAL DENIAL IS CAUGHT HERE. Other errors propagate to the caller.
22
+ * Legacy callers may still fold them, but the typed-absence follow-up makes User/Group/
23
+ * Storage catch only their SDK missing-object tag. Never infer a resource is absent from
24
+ * this sentinel. See credential-errors.ts's own ⛔ for why the OpenBao mint's 403 qualifies.
25
25
  */
26
26
  import * as Effect from 'effect/Effect';
27
27
  import type { PveCredentialDenied } from './credential-errors.ts';
@@ -29,9 +29,9 @@ import type { PveCredentialDenied } from './credential-errors.ts';
29
29
  export declare const UNREADABLE: unique symbol;
30
30
  export type Unreadable = typeof UNREADABLE;
31
31
  /**
32
- * Catches ONLY `PveCredentialDenied` and turns it into the sentinel, so a caller that also
33
- * `.pipe(Effect.orElseSucceed(() => undefined))` for every OTHER failure never conflates a
34
- * refusal with a genuine absence. Compose this INSIDE that fold, never after it.
32
+ * Catches ONLY `PveCredentialDenied` and returns the sentinel. Compose this BEFORE the
33
+ * caller's absence handling so a credential refusal cannot become a genuine absence.
34
+ * This helper itself classifies neither SDK missing-object tags nor transport failures.
35
35
  *
36
36
  * ⚠️ `E | PveCredentialDenied` IN, `E` OUT — not a generic `E` narrowed by `Exclude`. `catchTag`
37
37
  * needs the tag to be STATICALLY present in the error union to typecheck at all; a bare
@@ -29,19 +29,16 @@ export declare const matches: (attributes: UserAttributes, props: UserProps) =>
29
29
  * `PveClusterExhausted`, then silently folded to "absent" by the old `orElseSucceed`, forcing a
30
30
  * false `update` on every account.
31
31
  */
32
- export declare const readUserOrFail: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
32
+ export declare const readUserOrFail: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError | access.UserNotFound, import("effect/unstable/http/HttpClient").HttpClient>;
33
33
  /**
34
- * ⛔ FOLDS A GENUINE READ FAILURE TO `undefined` TOO, AND ONLY `read`/`reconcile` MAY USE IT. A
35
- * missing user is a thrown 500 (user.ts's own header), so `reconcile`'s create branch NEEDS
36
- * "absent" from a failure to ever run — without this fold, a brand-new account could never be
37
- * created. Safe here because a wrongful fold costs at most a redundant `createAccessUser` POST,
38
- * which PVE refuses loudly rather than silently corrupting anything.
39
- * ⛔ `user.ts`'s `diff` does **NOT** use this — the SAME bug class as the credential denial fix:
40
- * folding a TRANSIENT failure into "absent" at PLAN TIME is what forced a false `update` with
41
- * nothing compared. `diff` calls `readUserOrFail` directly, so a genuine failure there
42
- * propagates and fails the whole plan loudly instead.
34
+ * ⛔ ONLY `UserNotFound` MEANS ABSENT. The initial distilled transport swap preserved a
35
+ * catch-all fold because PVE reports missing objects as HTTP 500. SDK PR 265 now types that
36
+ * measured message, so permission errors, unrelated 500s and exhausted transports propagate
37
+ * instead of triggering a speculative create (decision 49 follow-up, 2026-09-24).
38
+ * ★ `readUserOrFail` remains the strict path used for an already-confirmed state row. This
39
+ * change narrows cold reads/reconcile without changing that existing drift/diff contract.
43
40
  */
44
- export declare const readUser: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
41
+ export declare const readUser: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes | undefined, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
45
42
  /** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
46
43
  export declare const dropUnreadable: (live: UserAttributes | Unreadable | undefined) => UserAttributes | undefined;
47
44
  //# sourceMappingURL=user-wire.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/user-wire.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,iCAAiC,CAAC;AAC1D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,eAAO,MAAM,WAAW,2BAA2B,CAAC;AACpD,eAAO,MAAM,WAAW,mCAAmC,CAAC;AA2D5D,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,uBAGnD,CAAC;AAEH,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,oBAGnD,CAAC;AAoBH;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,cAAc,SAAS,SAAS,YAOF,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,UAAW,SAAS,g2BAG+C,CAAC;AAE/F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,UAAW,SAAS,+HAC0B,CAAC;AAEpE,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,cAAc,GAAG,UAAU,GAAG,SAAS,+BACpC,CAAC"}
1
+ {"version":3,"file":"user-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/user-wire.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,iCAAiC,CAAC;AAC1D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,eAAO,MAAM,WAAW,2BAA2B,CAAC;AACpD,eAAO,MAAM,WAAW,mCAAmC,CAAC;AA2D5D,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,uBAGnD,CAAC;AAEH,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,oBAGnD,CAAC;AAoBH;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,cAAc,SAAS,SAAS,YAOF,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,UAAW,SAAS,s3BAG+C,CAAC;AAE/F;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,UAAW,SAAS,42BACqD,CAAC;AAE/F,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,cAAc,GAAG,UAAU,GAAG,SAAS,+BACpC,CAAC"}
@@ -34,11 +34,11 @@
34
34
  * · `Sys.Audit` (or `User.Modify`) for the read, which the `read` role's auditor already has.
35
35
  *
36
36
  * ⚠️ A MISSING USER IS A 500, NOT A 404 — MEASURED against the live cluster, "no such user
37
- * ('x@pve')". distilled turns that into an `InternalServerError`, with no distinct "not found"
38
- * tag to catch. `user-wire.ts`'s `readUser` (used by `read`/`reconcile`, where the create
39
- * workflow needs "absent" from exactly this failure) still folds it; `readUserOrFail` (used by
40
- * `diff` below) does not — see that function's own header for why folding it there was the
41
- * cries-wolf bug all over again, just for a genuine PVE quirk instead of a permission denial.
37
+ * ('x@pve')". The first SDK migration exposed it as `InternalServerError`, so its create
38
+ * workflow needed a catch-all fold. SDK PR #265 now recognizes `UserNotFound`; `readUser`
39
+ * catches only that tag, letting other cold-read/reconcile failures propagate. The strict
40
+ * `readUserOrFail` path for confirmed rows remains unchanged: its original transient-failure
41
+ * fix stopped the cries-wolf update with nothing compared (user-wire.ts).
42
42
  *
43
43
  * ⚠️ THE WIRE URL FOR THIS FAMILY CHANGED, THOUGH NOTHING A CALLER SEES DID. MEASURED
44
44
  * (user.test.ts): distilled's `{userid}` label substitution percent-encodes it —
@@ -0,0 +1,100 @@
1
+ # Argo CD `Application`/`AppProject` — via upstream's `Kubernetes` family, no new code
2
+
3
+ Companion to [argocd.md](./argocd.md#the-rest-vs-crd-split-the-actual-design-decision). `Application`
4
+ and `AppProject` are Kubernetes CRDs (`argoproj.io/v1alpha1`), not Argo CD REST-only objects — the
5
+ upstream-correct owner is Alchemy's own `Kubernetes.Manifest`
6
+ (`alchemy/Kubernetes`, v2.0.0-beta.79), which this kit does not wrap, re-implement, or need to:
7
+ decision 49 ("Kubernetes uses UPSTREAM Alchemy's existing Kubernetes family... don't build a
8
+ second one"). This page is documentation only — **no code in this kit declares an `Application` or
9
+ `AppProject`**; it shows how a stack does, once a cluster exists.
10
+
11
+ ## Why not an `Argocd.Application` REST resource?
12
+
13
+ Argo CD's `ApplicationService` REST API is real and full-featured (`Create`/`Get`/`Update`/
14
+ `Patch`/`Delete`/`List`/`Sync`/`Rollback`/…), but underneath it the object it manages **is** the
15
+ `Application` CRD — the same object `kubectl get applications.argoproj.io -n argocd` lists, with
16
+ its own versioned OpenAPI schema, validated by the Kubernetes API server itself, not by Argo CD's
17
+ application logic. A second, house-maintained REST wrapper around that would duplicate exactly the
18
+ generic capability `Kubernetes.Manifest` already provides — apply any object, CRDs included, via
19
+ server-side apply — for no benefit over declaring the CRD directly. Argo CD's own docs recommend
20
+ exactly this: `kubectl`/GitOps management of `Application`/`AppProject` manifests is the primary
21
+ documented workflow, not a detour around the REST API.
22
+
23
+ ## Declaring an `AppProject`
24
+
25
+ ```ts
26
+ import * as Kubernetes from 'alchemy/Kubernetes';
27
+ import * as Effect from 'effect/Effect';
28
+
29
+ const project = Effect.gen(function* () {
30
+ return yield* Kubernetes.Manifest('HomeflareProject', {
31
+ cluster, // a Kubernetes.KubeConfig(...) or a managed cluster resource
32
+ manifest: {
33
+ apiVersion: 'argoproj.io/v1alpha1',
34
+ kind: 'AppProject',
35
+ metadata: { name: 'homeflare', namespace: 'argocd' },
36
+ spec: {
37
+ description: 'HomeFlare estate applications',
38
+ sourceRepos: ['https://forgejo.homeflare.dev/homeflare/*'],
39
+ destinations: [{ namespace: '*', server: 'https://kubernetes.default.svc' }],
40
+ clusterResourceWhitelist: [{ group: '*', kind: '*' }],
41
+ },
42
+ },
43
+ });
44
+ });
45
+ ```
46
+
47
+ ## Declaring an `Application`
48
+
49
+ ```ts
50
+ const app = Effect.gen(function* () {
51
+ return yield* Kubernetes.Manifest('HomeflareKitApp', {
52
+ cluster,
53
+ manifest: {
54
+ apiVersion: 'argoproj.io/v1alpha1',
55
+ kind: 'Application',
56
+ metadata: { name: 'homeflare-kit', namespace: 'argocd' },
57
+ spec: {
58
+ project: 'homeflare',
59
+ source: {
60
+ repoURL: 'https://forgejo.homeflare.dev/homeflare/kit.git',
61
+ targetRevision: 'main',
62
+ path: 'deploy/manifests',
63
+ },
64
+ destination: { server: 'https://kubernetes.default.svc', namespace: 'homeflare-kit' },
65
+ syncPolicy: { automated: { prune: true, selfHeal: true } },
66
+ },
67
+ },
68
+ });
69
+ });
70
+ ```
71
+
72
+ `Kubernetes.Manifest` resolves unknown kinds through the cluster's own API discovery endpoint
73
+ (`Manifest.ts:31-32,89-93`), so `Application`/`AppProject` need no house registration — the CRD
74
+ just has to already exist on the cluster (Argo CD's own install manifest creates it).
75
+
76
+ ## Ordering: the CRD before the object
77
+
78
+ `Application`/`AppProject` are CRD-backed, so the `argoproj.io` `CustomResourceDefinition`s must
79
+ exist before a `Kubernetes.Manifest` for either can apply — normally true the moment Argo CD
80
+ itself is installed (its own install manifest, or `Kubernetes.HelmChart` installing the
81
+ `argo-cd` chart, both ship the CRDs). A stack that installs Argo CD and declares Applications in
82
+ the same deploy should thread the install as a dependency (an `Output` the Application's `cluster`
83
+ or an explicit ordering prop depends on) so Plan waits on it — an `Output` referenced in props is a
84
+ real dependency edge Alchemy's own Plan/Apply order on (v2.0.0-beta.79), the general rule this
85
+ follows.
86
+
87
+ ## Registering the Talos-on-PVE cluster itself with Argo CD
88
+
89
+ Argo CD's own control-plane cluster (where Argo CD runs) never needs registering — it is implicit
90
+ as `https://kubernetes.default.svc`. A **separate** cluster Argo CD deploys onto (or, if Argo CD
91
+ itself runs off-cluster) uses `Argocd.Cluster` from this kit ([argocd.md](./argocd.md)) — that
92
+ registration genuinely is Argo CD's own non-CRD storage (a labeled `Secret`), covered by the REST
93
+ family, not this page.
94
+
95
+ ## Installing Argo CD itself
96
+
97
+ Out of scope for this change (no cluster exists to install onto), but the answer is also "no new
98
+ code": `Kubernetes.HelmChart` (`alchemy/Kubernetes`) installs the upstream `argo-cd` Helm chart
99
+ the same way any other chart installs, and `Kubernetes.Manifest` covers a plain-manifest install
100
+ if the chart is not wanted. Neither needs anything from this kit.
package/docs/argocd.md ADDED
@@ -0,0 +1,139 @@
1
+ # Argo CD — `@homeflare/alchemy/argocd`
2
+
3
+ Built 2026-09-24, ahead of need — Tim: "we are using talos on pve once we get things settled ...
4
+ i thought we are using argocd, i wanted it on the radar" and "even if we don't use it yet but we
5
+ know we will." **No Talos-on-PVE cluster and no Argo CD instance exist on the estate yet.** Every
6
+ test in this family runs against `fake-argocd.ts`; nothing here has touched a real server.
7
+
8
+ ## Is `@distilled.cloud/argocd` real, and does it match the kit's pins?
9
+
10
+ ✅ **MEASURED**, `npm view @distilled.cloud/argocd`, 2026-09-24: published, `latest` =
11
+ **1.0.0-rc.12**, `peerDependencies.effect` = `>=4.0.0-rc.115`, `dependencies["@distilled.cloud/
12
+ core"]` = `1.0.0-rc.12` — exactly the kit's own pins (`packages/alchemy/package.json`'s `effect`
13
+ and every other `@distilled.cloud/*` peer). Added as a direct peer dependency, the same shape as
14
+ `@distilled.cloud/forgejo`/`discord`/`cloudflare`/`google-workspace` — **no interim package**
15
+ ([docs/distilled-interim.md](./distilled-interim.md) applies only when the real package is
16
+ unpublished; it is not, here).
17
+
18
+ ## Does upstream Alchemy already ship (or build) ArgoCD? (decision 49)
19
+
20
+ ✅ **MEASURED, no**: `grep -ril "argocd"` across `alchemy-run/alchemy` v2.0.0-beta.79 matches
21
+ exactly two files, both AWS EKS "add-on capability" JSDoc examples (`src/AWS/EKS/
22
+ DescribeCapability.ts:20`, `ListCapabilities.ts` — `describeCapability({ capabilityName: "argocd"
23
+ })`, an EKS add-on name, unrelated to a provider). No `src/Argocd/` directory, no changeset, no
24
+ CHANGELOG entry. So decision 49 ("upstream wins... don't build a second one") does not apply here
25
+ — there is nothing upstream to duplicate.
26
+
27
+ ## What upstream's `Kubernetes` family already ships (measured)
28
+
29
+ `packages/alchemy/src/Kubernetes/` (v2.0.0-beta.79): `Connection.ts`, `ClusterAdapter.ts`,
30
+ `BuiltinAdapters.ts`, `Deployment.ts`, `HelmChart.ts`, `Job.ts`, `Manifest.ts`, `Providers.ts`.
31
+ `Kubernetes.Manifest` (`Manifest.ts:34-284`) applies **any literal Kubernetes object — built-in
32
+ kinds and CRDs alike** via server-side apply, with a worked "Custom resource (CRD)" example in its
33
+ own doc comment (`Manifest.ts:116-127`). This is the upstream-correct owner for `Application` and
34
+ `AppProject` — see [argocd-kubernetes.md](./argocd-kubernetes.md) for why and how, with examples.
35
+ `HelmChart.ts` is how the house will install Argo CD itself later (its own Helm chart) — no new
36
+ code needed there either.
37
+
38
+ ## The REST-vs-CRD split (the actual design decision)
39
+
40
+ Measured against Argo CD's declarative-setup documentation and the generated operation list in
41
+ `@distilled.cloud/argocd`'s `services/argocd.ts` (106 operations, distilled homeflare/base):
42
+
43
+ | Object | Backing store | Owner |
44
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
45
+ | `Application` | `argoproj.io/v1alpha1` CRD, its own OpenAPI schema, listable with plain `kubectl` | **`Kubernetes.Manifest`** — [argocd-kubernetes.md](./argocd-kubernetes.md) |
46
+ | `AppProject` | `argoproj.io/v1alpha1` CRD, same as above | **`Kubernetes.Manifest`** |
47
+ | Repository | a `Secret` labeled `argocd.argoproj.io/secret-type: repository` — no CRD of its own; Argo CD's own storage format, not a published Kubernetes API type | **`Argocd.Repository`** (this family) |
48
+ | RepoCreds | a `Secret` labeled `…secret-type: repo-creds` — same reasoning | **`Argocd.RepoCreds`** |
49
+ | Cluster | a `Secret` labeled `…secret-type: cluster` — same reasoning | **`Argocd.Cluster`** |
50
+
51
+ The line is not "REST API exists" (all seven have full REST CRUD — `ApplicationServiceService`,
52
+ `ProjectService`, `RepositoryService`, `RepoCredsService`, `ClusterService`) — it is **whether the
53
+ object is a real, versioned Kubernetes API type**. `Application`/`AppProject` are; Repository/
54
+ RepoCreds/Cluster are Argo CD's own encoding smuggled into a generic `Secret`, which
55
+ `Kubernetes.Manifest` could technically apply too, but only by hand-reconstructing Argo CD's
56
+ private labeling convention — exactly the vendor-specific knowledge a REST SDK exists to own.
57
+
58
+ ## What this family builds
59
+
60
+ Three resources on `@distilled.cloud/argocd/argocd`, the peer-dependency pattern
61
+ `packages/alchemy/src/forgejo` already uses:
62
+
63
+ - **`Argocd.Repository`** (`repository.ts`) — `/api/v1/repositories`.
64
+ - **`Argocd.RepoCreds`** (`repo-creds.ts`) — `/api/v1/repocreds`, a URL-prefix credential
65
+ template. No single-item `Get` operation exists (measured against the operation list) —
66
+ `fetchLive` lists and finds, the same shape `forgejo/org-actions-secrets.ts` and
67
+ `discord/guild-application-command.ts` use for the same reason.
68
+ - **`Argocd.Cluster`** (`cluster.ts`) — `/api/v1/clusters`. Only bearer-token and static
69
+ TLS-client-cert auth are modeled; `awsAuthConfig` (EKS IAM) and `execProviderConfig` (an exec
70
+ plugin) are unbuilt — neither applies to a Talos cluster.
71
+
72
+ All three share one engine (`resource.ts`, `argocdOperations`/`argocdHandlers`), a near-verbatim
73
+ copy of `forgejo/resource.ts` — both SDKs are plain bearer-REST with a real typed `NotFound`, so
74
+ the same read/diff/reconcile/destroy shape applies unchanged.
75
+
76
+ ## The SDK gap that _isn't_ there (measured, and why it matters)
77
+
78
+ 🔴 Unlike Discord (`docs/discord.md#sdk-gaps`), Argo CD's generated error TYPE is **not** narrowed
79
+ to `DEFAULT_ERROR_STATUSES`. Measured directly, `packages/argocd/src/protocol.ts` (distilled
80
+ homeflare/base):
81
+
82
+ ```ts
83
+ export type ArgocdOpError =
84
+ | InstanceType<(typeof API_ERRORS)[number]> // the FULL 12-class list, incl. NotFound/Forbidden/Conflict
85
+ | UnknownArgocdError
86
+ | ConfigError
87
+ | HttpClientError.HttpClientError;
88
+ ```
89
+
90
+ `API_ERRORS` (not `DEFAULT_ERRORS`) is the full `HTTP_STATUS_MAP` set. And no operation carries a
91
+ `patches/` file (`git ls-tree` on `packages/argocd/` shows none, unlike forgejo's `patches/
92
+ organization/orgDelete.json` exemplar) — every op's own declared `errors: [...]` array is just
93
+ `[UnknownArgocdError]`. That looked, at first read, like the same gap Discord has. It is not:
94
+ `core/protocol-rest.ts#decode` (lines ~320-370) checks the operation's own typed matchers first,
95
+ then **unconditionally** falls back to `HTTP_STATUS_MAP[status]` — a 404 maps to `NotFound`
96
+ regardless of whether the operation declared it. `UnknownArgocdError` is reached only for a status
97
+ outside that map. **So `catchTag('NotFound', ...)` works correctly today, with no distilled patch
98
+ needed** — confirmed by `repository.test.ts`/`repo-creds.test.ts`/`cluster.test.ts` running the
99
+ REAL protocol against a fake 404/401/403, not a hand-rolled status check.
100
+
101
+ ## Secrets never in state
102
+
103
+ Every secret-shaped field (`password`, `sshPrivateKey`, `bearerToken`, `tlsClientCertKey` /
104
+ `tlsClientKeyData`) is a prop typed `FromEnv` (`{ fromEnv: 'ENV_VAR_NAME' }`,
105
+ `../secrets/write-only.ts`) — never a literal value. `git-credentials.ts` (Repository/RepoCreds)
106
+ and `cluster.ts`'s own resolver read the named variable from the deploying process's own
107
+ environment **at call time**, refuse with a typed domain error
108
+ (`ArgocdSecretEnvUnsetError`/`ArgocdClusterSecretEnvUnsetError`) if it is unset, and send the
109
+ value on the wire — never into a prop, an attribute, or Alchemy's state store.
110
+
111
+ ⚠️ **No drift detection on secret fields, on purpose** — the same simplification
112
+ `forgejo/org-actions-secrets.ts` makes: Argo CD's `Get`/`List` never returns a password, private
113
+ key or bearer token back, so there is nothing to diff against. `matches` compares only the fields
114
+ the API does return; a changed credential is written once on create and never rotated
115
+ automatically. Rotate by destroy-then-create, or a manual call outside Alchemy. A seal-based check
116
+ (`../secrets/write-only.ts#seal`, as `proxmox/pbs-notification-target-lifecycle.ts` uses) is
117
+ possible later if this becomes a real problem.
118
+
119
+ ## Reconcile is one upsert call, always
120
+
121
+ Every `Create*` operation this family calls takes `upsert: true` as a query parameter (measured:
122
+ `CreateRepositoryServiceRepositoryRequest`, `CreateRepoCredsServiceRepositoryCredentialsRequest`,
123
+ `CreateClusterServiceRequest`, all `.pipe(T.Query())`) — a true create-or-converge in one request.
124
+ `resource.ts#reconcile` therefore has no separate `Update*` path: it reads, and calls `upsert`
125
+ only when the declared props and the live object disagree on a plain field (S10 — converged means
126
+ zero writes).
127
+
128
+ ## `exactOptionalPropertyTypes`, and `present()`
129
+
130
+ The kit's `tsconfig.base.json` turns this on, and every generated Argo CD request type declares
131
+ optional fields the strict way. `resource.ts#present()` strips undefined-valued keys into
132
+ genuinely-absent keys before any create/upsert call — one shared helper instead of
133
+ `forgejo/repository.ts#editForm`'s per-field conditional spread repeated three times.
134
+
135
+ ## Ownership: silent, like forgejo — not `Unowned` like discord/netbox
136
+
137
+ No `adopt(true)`/`Unowned` wrapping. A live object found on a cold read (no persisted state) is
138
+ treated as ours, mirroring `forgejo/resource.ts`'s own choice rather than `discord/resource.ts`'s
139
+ `Unowned` marker. Documented, not accidental — the same tradeoff forgejo already ships with.
@@ -65,3 +65,12 @@ this one variable. `Authorization: Bearer <value>` is still built by the package
65
65
  `GoogleWorkspaceKeyRef`/`describeKeyRef` (this subpath's own `credentials.ts`) are a typed,
66
66
  key-free REFERENCE to where that service-account key lives, for a stack file to keep alongside its
67
67
  declarations — full setup: [google-workspace.md](./google-workspace.md).
68
+
69
+ The Argo CD subpath (new 2026-09-24, `@distilled.cloud/argocd`, built ahead of any live instance)
70
+ resolves credentials the same way, through the SDK's own `CredentialsFromEnv` layer, which reads
71
+ `ARGOCD_TOKEN` (required) and `ARGOCD_SERVER` (optional instance origin, default
72
+ `https://localhost:8080`) — the package's own names, unchanged. `Authorization: Bearer <value>` is
73
+ built by the package's protocol layer. The repository/cluster CREDENTIALS this subpath's three
74
+ resources declare (a repo's password/SSH key, a cluster's bearer token/TLS key) are a separate,
75
+ per-object concern from the instance's own API token above — write-only `FromEnv` references, never
76
+ a prop or attribute either: see [argocd.md](./argocd.md#secrets-never-in-state).
@@ -34,9 +34,21 @@ changes and nothing else does.
34
34
  Follow `.agents/skills/distilled-sdk/SKILL.md` in the local distilled clone —
35
35
  Steps 1–8 (never Step 9, which pushes). One worktree per vendor, branch
36
36
  `homeflare/<vendor>`, off `homeflare/base`. Spec sourcing, `patches/`,
37
- `convert.ts`/`generate.ts`, typecheck, `pnpm format`, `pnpm specs:check`.
38
- Commit in the local clone only — ⛔ **never push it anywhere; no forks, no
39
- PRs, no issues or comments on `alchemy-run/distilled` from this route.**
37
+ `convert.ts`/`generate.ts`, `pnpm typecheck:ci` (never plain `typecheck`,
38
+ which skips generated code — `noCheck: true`), `pnpm format`,
39
+ `pnpm specs:check`. Commit in the local clone only — ⛔ **never push it
40
+ anywhere; no forks, no PRs, no issues or comments on `alchemy-run/distilled`
41
+ from this route.**
42
+
43
+ ⚠️ **A vendor with no machine-readable spec still gets a `SPEC_REPOS` entry**
44
+ (`stacks/distilled-submodules/SpecRepos.ts`) with `blocked` and a reason. Upstream's
45
+ Slack entry is the precedent. A blocked entry still declares the mirror repository,
46
+ but skips fetch scaffolding; it does not pretend there is a spec to fetch.
47
+ Caddy follows it (Q7, decision 49, 2026-09-24): the local `homeflare/caddy` worktree's
48
+ `packages/caddy/docs/provenance.md` records the admin-API spec search against Caddy
49
+ 2.11.4's Go source and website. Its admin-operation model is hand-authored, and its
50
+ `SpecRepos.ts` entry records that reason. The separately discovered config-tree
51
+ metadata is not an admin-operation specification.
40
52
 
41
53
  ### 2. Copy into a kit workspace package
42
54
 
package/docs/litellm.md CHANGED
@@ -33,6 +33,23 @@ another `PROXY_ADMIN` key: `update_config_general_settings`, the function every
33
33
  through, is `PROXY_ADMIN` only. Never a prop — see [docs/credentials.md](./credentials.md) for the
34
34
  house rule.
35
35
 
36
+ `CredentialsFromEnv` ends in `Effect.orDie` on a missing/misspelled env var — matching distilled's
37
+ own convention (73 of 80 `packages/*/src/credentials.ts` end this way at `homeflare/base`, 78 of 80
38
+ at `origin/main`), not the house's own S20/S24 numbering. A worktree commit briefly changed this to
39
+ a typed `ConfigError` (`LitellmOpError` already declares one, and `makeRestProtocol`'s `encode` step
40
+ does thread a real credentials failure into the operation's own error channel — the plumbing for a
41
+ typed refusal is genuinely there); Tim's answer to Q6 (2026-09-24 walk-down, decision 49 "upstream
42
+ wins") reverted it, because that divergence was the house going stricter than distilled's own
43
+ convention on a point distilled is not silent on, and decision 49 says match exactly, not stricter.
44
+
45
+ ★ **HELD upstream proposal, not raised.** If distilled's own convention changes — typed
46
+ `ConfigError` in place of `orDie` on a missing credential — this package (and the other 72) would
47
+ be one line each to update: `Credentials`'s `Context.Service` value type gains `ConfigError`, and
48
+ the layer drops its trailing `Effect.orDie` in favor of the `Effect.mapError` it already has to
49
+ build the error. LiteLLM would make a reasonable pilot, since its protocol layer already declares
50
+ and threads `ConfigError`. Nobody has proposed this to `alchemy-run/distilled`; it stays here as an
51
+ idea for Tim to raise, or not, later.
52
+
36
53
  ## Refusals
37
54
 
38
55
  This resource refuses a plan rather than writing something it cannot safely undo:
package/docs/peers.md CHANGED
@@ -78,3 +78,10 @@ upstream, the alias's target changes (or the entry becomes a normal peer, matchi
78
78
  `HttpClient` client the same way `/netbox` did) is the same shape**: aliased onto
79
79
  `@homeflare/distilled-litellm@0.2.0` as a plain `dependencies` entry, not a peer — nothing to
80
80
  add here, nothing for `peers.test.ts` to check.
81
+
82
+ ⚠️ **`@distilled.cloud/argocd` (added 2026-09-24, `/argocd`, built ahead of a Talos-on-PVE
83
+ cluster that does not exist yet) is required for the same reason `@distilled.cloud/forgejo` and
84
+ `/discord` are: a plain dependency of this package, not something a hoisting installer can hide
85
+ from a strict one.** Pinned to `1.0.0-rc.12` — measured (`npm view @distilled.cloud/argocd`,
86
+ 2026-09-24) as genuinely published at that version, matching every other pin on this page; no
87
+ interim alias was needed (see [distilled-interim.md](./distilled-interim.md) for when one is).
@@ -1,7 +1,11 @@
1
1
  # The provider standard
2
2
 
3
- Status: in force. Verified 2026-09-22 against `alchemy@2.0.0-beta.79` (tag
3
+ Status: in force. Verified 2026-09-24 against `alchemy@2.0.0-beta.79` (tag
4
4
  `v2.0.0-beta.79`, commit `473c3959`) and the distilled commit that tag pins (`c2a78002`).
5
+ Decision 49 ("upstream wins", 2026-09-24): where upstream has a rule or clear convention, the
6
+ house follows it exactly, neither looser nor stricter; a house rule only fills a gap upstream
7
+ is silent on. This update corrects S20 and clarifies S21 against the pinned source; it
8
+ does not claim a new audit of every rule or family.
5
9
 
6
10
  This page is the kit-side statement of the house standard for a custom Alchemy provider.
7
11
  The full rule set is the `alchemy-provider-standard` skill in the estate's workflow plugin.
@@ -64,11 +68,25 @@ already ships is a finding, even when it works.
64
68
  CPU-only Node call (`node:crypto` `createHash`, `Buffer`) goes inside `Effect.sync`, which
65
69
  is upstream's own example. When a promise cannot be avoided, use `Effect.tryPromise`,
66
70
  never `Effect.promise`.
67
- - **No defects (S20).** Lifecycle operations contain no `Effect.orDie` and no
68
- `Effect.die`. A refusal is a typed error.
69
- - **Typed errors (S21, S22).** Handle errors with `Effect.catchTag` over the SDK's union.
70
- Where the kit generates its own client from a vendor schema, the status-to-tag mapping
71
- lives in that client, once, and never in a resource.
71
+ - **Lifecycle errors (S20).** Upstream's rule is: "Do not use `Effect.orDie` in the
72
+ lifecycle operations since this will crash the whole IaC engine."
73
+ Cite: `AGENTS.md@v2.0.0-beta.79#L630`. It names `Effect.orDie` in lifecycle operations;
74
+ S20 adds no separate ban on `Effect.die` or on code outside those operations.
75
+ `Effect.die` still creates a defect. The engine's narrow recovery exception is the
76
+ best-effort read for an interrupted create: `Effect.catchDefect` degrades a crashed
77
+ recovery read to nothing recovered (`packages/alchemy/src/Plan.ts@tag#L1447`,
78
+ `packages/alchemy/src/Apply.ts@tag#L2218`). Ordinary lifecycle calls have no such
79
+ recovery wrapper (`Plan.ts@tag#L1303-L1315`); this is not a general safe-defect path.
80
+ Expected refusals stay typed errors.
81
+ - **Typed errors (S21, S22).** Handle errors with `Effect.catchTag` over the SDK's union;
82
+ other typed SDK errors can propagate unchanged, as upstream's R2 `BucketSippy` does.
83
+ The resource-specific-tag patch rule applies to errors **outside** that typed union
84
+ (`AGENTS.md@tag#L733-L738`). Where a package's protocol includes status classes in
85
+ its union (argocd, hetzner, fly-io and neon at distilled `c2a78002`), catch those tags
86
+ directly: upstream `Hetzner/Certificate.ts@tag#L251,L288-L291` catches `NotFound`
87
+ for absent reads and successful deletes. No replacement tag is needed for an already
88
+ typed `NotFound`. Where the kit generates its own client from a vendor schema, the
89
+ status-to-tag mapping lives in that client, once, and never in a resource.
72
90
  - **Credentials (S24).** Credentials come from an `alchemy/Auth` provider and a lazy
73
91
  Credentials service. Short-lived credentials are minted per call, never reused, and never
74
92
  taken as props.
package/docs/proxmox.md CHANGED
@@ -21,3 +21,23 @@ or creates one from a template.
21
21
  - ★ **Measurements in the source name a reference cluster with placeholders**: `C1`, three nodes
22
22
  `node-b`, `node-c` and `node-d` with Ceph on a Thunderbolt mesh, and documentation addresses
23
23
  (RFC 5737). The shapes and counts are measured; the names and addresses are not the real ones.
24
+
25
+ ## Distilled read failures (2026-09-24)
26
+
27
+ The SDK walk-down in [PR #265](https://github.com/taslabs-net/homeflare-kit/pull/265)
28
+ added vendor-evidenced `UserNotFound`, `GroupNotFound`, `StorageNotFound` and
29
+ `CephPoolNotFound` tags for PVE's missing-object HTTP 500s. The provider now catches
30
+ those tags on cold User/Group/Storage reads and CephPool reads. Other SDK errors
31
+ propagate unchanged; a failed CephFS or mon/mgr/mds index also fails rather than
32
+ proving absence. Existing strict confirmed-row User/Group/Storage reads remain strict.
33
+
34
+ `typed-absence.test.ts` drives the real SDK protocol through fake transport, then proves
35
+ with Alchemy's engine that a failed cold read cannot send a create and an unreadable
36
+ CephFS index cannot produce a successful delete. Existing adoption fixtures prove
37
+ matching objects remain no-ops. Credential-denied warnings are still not live no-op
38
+ evidence; use the consumer's authorized read lane and `hf-adopt-verify --all`.
39
+
40
+ This is a scoped follow-up to the transport migration. CephFS delete still uses the old
41
+ client despite the SDK's now-fixed DELETE query encoding; bounded task polling and
42
+ other pre-existing provider refusals retain their own policies. No live writes are part
43
+ of this validation. Released-consumer plans remain a separate gate from these fixtures.
package/docs/unifi.md CHANGED
@@ -91,3 +91,48 @@ trigger but a future write path must respect — whole-object `PUT`, ordering en
91
91
  replace the whole list, `removeDevice` unadopting and factory-resetting hardware — are recorded
92
92
  in the distilled package's own `README.md` and in the estate's `docs/unifi-api-notes.md`
93
93
  (outside this repo).
94
+
95
+ ## Spec-version check against the live-measured console (2026-09-24)
96
+
97
+ `homeflare-network`'s `src/unifi/imported.ts` was generated 2026-09-24 against a live console
98
+ self-reporting **10.6.97** (`GET /v1/info`, recorded in that file's own header) — two minor
99
+ versions past this SDK's 10.4.57 pin. Checked whether any of the 11 imported networks or 14
100
+ imported firewall zones could decode differently on a newer controller than this SDK's
101
+ 10.4.57-pinned types expect: diffed a 10.6.97 copy of Ubiquiti's OpenAPI document (a third-party
102
+ mirror, used only as a diffing aid — never as this SDK's spec of record; see the distilled
103
+ package's own `docs/spec-version-provenance.md`) against the pinned 10.4.57 document. Zero
104
+ operations added or removed anywhere in the API; every full raw operation object (parameters,
105
+ request body, responses, `$ref`s included) for every `Networks`-tagged operation and every
106
+ `*FirewallZone*` operation is identical, byte for byte, not just its named schemas (11 operations
107
+ on each side, same set — a named-schema diff alone would miss an inline, untagged parameter
108
+ changing shape, so this checks the operations directly).
109
+
110
+ **14 of the document's 379/380 component schemas do differ somewhere** — the switch-stack/LAG
111
+ family (`Switching` tag, matching Ubiquiti's own 10.6 release notes on LAG support), the generic
112
+ `filter`-query-syntax family (`FilterExpression`/`CompoundFilterExpression`/`NotFilterExpression`/
113
+ `PropertyFilterExpression`), and one mDNS enum addition (`SHELLY`, `UniFi Devices` tag) — but
114
+ **none of the 14 is reachable from a Networks or FirewallZone operation, even transitively**:
115
+ resolved every `$ref` reachable from each of the 11 operations' full parameter/body/response
116
+ trees, recursively, in both versions, and none of the 14 changed schema names appears in either
117
+ closure. Full breakdown of the 14 (which changed vs. added/removed, and why the reachability check
118
+ had to go beyond the direct operation-object diff above) is in the distilled package's own
119
+ `docs/spec-version-provenance.md`, not this repo.
120
+
121
+ **Answer: none of the imported rows would change shape against 10.6.97.** This does not
122
+ generalize past 10.6.97, and it says nothing about any tag besides Networks/FirewallZones —
123
+ re-check before importing or declaring against `Clients`, `WiFi Broadcasts`, `ACL Rules`, or any
124
+ other tag.
125
+
126
+ ## 2026-09-24 doctrine walkdown
127
+
128
+ Read directly against `origin/main`'s `src/unifi/{resource,network,network-form,firewall-zone,
129
+ firewall-zone-form,policy}.ts`: `fetchLive` in `network.ts`/`firewall-zone.ts` folds only
130
+ `catchTag('NotFound', …)`, never a blanket catch. `destroy` always refuses via the typed
131
+ `UnifiWriteRefused` regardless of live state — correct here, not a departure from delete's usual
132
+ idempotency rule: this family will never issue a real DELETE by policy, so idempotency of a
133
+ delete it cannot perform is moot. No `Effect.orDie`/`Effect.die` anywhere in the family.
134
+ `sortedSet` normalizes every measured unordered array (`dhcpGuarding.trustedDhcpServerIpAddresses`,
135
+ `ipv6Configuration`'s two override lists, `firewall-zone-form.ts`'s `networkIds`) before it is
136
+ attributed, declared or compared. Both declaration renderers and both `matches` use
137
+ `value == null` / `deepEqual(..., { stripNullish: true })` consistently, per `network-form.ts`'s
138
+ own header. No departure found — nothing in this family needed changing.