foundation/documentation/agents/task_002_pulumi_skeleton/003_handoff.md
Andreas Niemann 57c4eadea7 feat(bootstrap): Bun-workspace skeleton + typed config + vendored modules — T02
- Bun workspaces (packages/* + bootstrap); Pulumi nodejs runtime under
  packagemanager: bun (no npm fallback needed).
- bootstrap/config.ts: typed FoundationConfig per CONTRACT_001; loadConfig()
  fails closed, aggregating all missing+malformed keys in one error. Reads flat
  dotted keys; image digests excluded (they live in VERSIONS, D5).
- bootstrap/Pulumi.foundation.yaml: non-secret placeholders only (RFC-5737 vm.host,
  .invalid offsite); no encryptionsalt/secrets committed (D2). pulumi preview = 0
  resources under the passphrase provider via gitignored file:// state backend.
- Stage-1 vendoring: packages/pulumi-{docker,vault} as @olsitec/* (source-only,
  logic unchanged). vault's 5 type-only imports from modules/olsitec re-homed
  verbatim into pulumi-vault/olsitec-types.ts to keep the egg self-contained.

Realizes PLAN-002 §10 T02; ADR-005 / 000_TOPOLOGY.md §5 Stage-1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:06:21 +02:00

8.8 KiB

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 commited 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.tsNEW: 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.jsonfiles 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.yamlname: 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.tsFoundationConfig 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 flatgetObject("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.jsons 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.)
  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

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='<throwaway-dev-only>'   # 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.