# T02 — Pulumi project skeleton — Handoff **Task:** PLAN-002 §10 T02 · **Mode:** BUILD · **Date:** 2026-06-30 **Author role:** implementation agent · **Status:** complete, validated by execution (see below). > The lead reviews and commits. Nothing was `git add`/`git commit`ed by this task. --- ## 1. Files created / touched ### Workspace root - `package.json` — Bun workspace root (`@olsitec/foundation`, private, `workspaces: ["packages/*","bootstrap"]`). - Removed placeholders `bootstrap/.gitkeep`, `packages/.gitkeep` (replaced by real content). - `bun.lock` — generated by `bun install` (links the workspace; no secrets — verified). ### Vendored module `packages/pulumi-docker/` (`@olsitec/pulumi-docker`) - `index.ts`, `tsconfig.json`, `.editorconfig`, `.gitignore` — copied **verbatim** from `/Users/andiolsi/work/olsicloud4/pulumi/modules/docker/`. - `package.json` — renamed `docker` → `@olsitec/pulumi-docker`; added `version` `0.0.0`, `main`/`types` (`index.ts`), and an explicit `@pulumi/pulumi` dependency (see §4). - `VENDORED.md` — source path, copy date, Stage-1 note, Stage-2 deferral. ### Vendored module `packages/pulumi-vault/` (`@olsitec/pulumi-vault`) - `index.ts`, `policy.ts`, `tsconfig.json`, `.editorconfig`, `.gitignore` — copied **verbatim** from `/Users/andiolsi/work/olsicloud4/pulumi/modules/vault/`. - `olsitec-types.ts` — **NEW**: the 5 type-only declarations the upstream `index.ts` imported from `../../modules/olsitec`, copied verbatim (see §3). The single import line in `index.ts` was re-pointed `../../modules/olsitec` → `./olsitec-types`. No runtime logic changed. - `tsconfig.json` — `files` extended with `policy.ts` + `olsitec-types.ts` (standalone typecheck). - `package.json` — renamed `vault` → `@olsitec/pulumi-vault`; `version`/`main`/`types`; added explicit `@pulumi/pulumi` dependency (§4). - `VENDORED.md` — source path, copy date, Stage-1 note, the type-only re-home, and a flag that the inherited Layer-1 credential surface (`VaultProject`/minio/garage/…) is to be trimmed in a later Layer-0 refactor (NOT in Stage-1, which preserves source). ### `bootstrap/` (the egg, single Pulumi project) - `Pulumi.yaml` — `name: foundation`, `runtime: nodejs` + `options.packagemanager: bun`. - `package.json` — depends on `@olsitec/pulumi-docker`/`@olsitec/pulumi-vault` (`workspace:*`) + `@pulumi/pulumi`. - `tsconfig.json` — Olsitec-standard compiler options; `files: [config.ts, index.ts]`. - `config.ts` — `FoundationConfig` interface (CONTRACT_001 §1.1) + `loadConfig()` (fails closed) + `sshPrivateKeyPath()` (ENV). - `index.ts` — no-op scaffold: calls `loadConfig()`, creates NO resources, exports non-secret outputs. - `Pulumi.foundation.yaml` — NON-secret placeholders only; no secrets, no encryptionsalt. ### Documentation - `documentation/agents/task_002_pulumi_skeleton/000_subtask_outline.md` - `documentation/agents/task_002_pulumi_skeleton/003_handoff.md` (this file) --- ## 2. Validated-by-execution vs. authored-only **Validated by running it (env: bun 1.3.9, pulumi v3.243.0, node v24.10.0, macOS):** - `bun install` links the workspace; `bun pm ls` shows all three workspace members; `require.resolve("@olsitec/pulumi-docker"|"@olsitec/pulumi-vault")` from `bootstrap` resolves to `packages/*/index.ts`. ✅ (acceptance: workspace resolves the two packages) - `tsc --noEmit` exit 0 for **all three** projects (bootstrap + both vendored packages). ✅ - `pulumi preview` under the **passphrase** secrets provider (local `file://bootstrap/state` backend + throwaway `PULUMI_CONFIG_PASSPHRASE` in ENV): exit 0, loads config, **0 real resources** (only the no-op Stack), prints the scaffold outputs. ✅ (acceptance: preview runs on empty/stub stack, passphrase provider) - **Fail-closed demonstrated:** removing `foundation:vm.host` + `foundation:features.backup` made `pulumi preview` exit 1 with one error listing both missing keys; restoring made it pass again. ✅ (acceptance: config rejects missing required keys) - **No secrets committed:** `bootstrap/state/` is gitignored (`git check-ignore` confirms); the committed `Pulumi.foundation.yaml` has **no** real `encryptionsalt:`/`secure: v1:` value line (only explanatory comment prose mentions the words); `bun.lock` has no secret leakage. ✅ **Authored but NOT executed (out of scope — no VM, no `pulumi up` ever):** - Actual Docker-over-SSH provisioning, Vault init/unseal, any container — all LATER tasks (T03+). - The vendored modules' runtime behaviour was not exercised (they are libraries; only typechecked + consumed by the typechecked bootstrap). Their logic is byte-identical to the upstream source apart from the documented type-only re-home in pulumi-vault. --- ## 3. CONTRACT_001 ambiguities / decisions - **Config key shape (resolved by execution).** CONTRACT_001 §1.1 shows a nested interface but §1.2 gives flat dotted example keys (`foundation:hosts.forge`, `foundation:features.forgejo`). Pulumi stores and exposes these **flat** — `getObject("hosts")` does NOT reassemble `hosts.forge`+`hosts.vault` into an object. `loadConfig()` therefore reads each leaf by its full dotted key with the typed accessor for its kind (`get`/`getNumber`/`getBoolean`, and `getObject` for the two arrays), then assembles the nested `FoundationConfig`. This is the idiomatic match for the §1.2 YAML and is robust to Pulumi quoting scalars (`"2222"`, `"true"`). No contract change needed. - **No further ambiguity blocked typing.** Every §1.1 field maps 1:1 to a placeholder key. - **Secrets (§1.3) intentionally not required** by `loadConfig()` — they are seeded by later tasks (T05 Vault capture etc.); requiring them now would block the T02 acceptance (empty/stub stack). ## 4. Bun-vs-npm decision + rationale **Decision: Bun** (Olsitec footgun 16.3 prefers Bun; 000_TOPOLOGY.md §3 specifies Bun workspaces). - Pulumi's nodejs runtime supports `options.packagemanager: bun`; `pulumi preview` ran cleanly under it (the program executes via Pulumi's bundled ts-node against bun-installed deps). **No fallback to npm was needed.** - **One vendoring fix forced by Bun's isolated store:** the upstream modules never declared `@pulumi/pulumi` directly (they relied on it being hoisted into their own `node_modules`). Bun's `.bun/` store layout does not hoist it where standalone `tsc` on a package can find it, so both vendored `package.json`s now list `@pulumi/pulumi` (`^3.138.0`, matching olsitec-core) as an explicit dependency. This is a **packaging-metadata** correction (the import was always real); **no module logic changed**. It also makes the packages correct for Stage-2 publishing. (This is the escalation-worthy "vendoring reveals a dependency" item — judged minor and fixed in place, not a design change.) ## 5. Recommended follow-ups 1. **Strip `encryptionsalt` before each commit of `Pulumi.foundation.yaml`.** Every `pulumi preview/up` under the passphrase provider re-appends an `encryptionsalt:` line and re-quotes scalars. The committed file has been left clean; a header comment documents the strip step. Once the first real secret is added (T05), the encryptionsalt becomes load-bearing and SHOULD then be committed alongside the `secure: v1:` values — revisit this guidance at that point. 2. **`packages/pulumi-vault` Layer-0 trim (later task).** `VaultBootstrap`/`VaultProject` still carry the Layer-1 minio/garage/cockroach/mongo credential surface. The egg only needs `VaultInitialization` (init/unseal capture) + a minimal `VaultBootstrap`. Trim per 000_TOPOLOGY.md §5.1 "refactor for Layer 0" — out of scope for Stage-1 vendoring. 3. **Run `pin-digests`** to replace `PIN_DIGEST` in `VERSIONS` before any real `pulumi up` (T01/preflight concern). 4. **Stage-2 publish** (`@olsitec/pulumi-*` to the foundation Forgejo npm registry) once it exists — semantic-release-monorepo + Conventional Commits (000_TOPOLOGY.md §5, memory: olsitec-charts-conventional-commits). 5. **Lint config** not vendored — the upstream modules had no per-package eslint file (only an `.editorconfig`, which was copied); olsitec-core's `eslint.config.mjs` is project-level. Add a workspace-level lint setup in a later tooling task if desired. ## 6. How to reproduce the validation ```sh cd ~/work/olsitec-foundation/foundation bun install bunx tsc --noEmit -p bootstrap/tsconfig.json bunx tsc --noEmit -p packages/pulumi-docker/tsconfig.json bunx tsc --noEmit -p packages/pulumi-vault/tsconfig.json cd bootstrap export PULUMI_HOME="$(pwd)/state/.pulumi-home" export PULUMI_CONFIG_PASSPHRASE='' # never commit pulumi login "file://$(pwd)/state" pulumi stack init foundation --secrets-provider=passphrase # first time only pulumi preview --stack foundation # NOTE: preview rewrites Pulumi.foundation.yaml — `git checkout` it or strip the # appended `encryptionsalt:` line before committing. state/ is gitignored. ```