Skip to content

Commit dcdbbae

Browse files
committed
docs: self-packaging across architecture / decisions / security / CLI
Part of #40. Closes #50. Captures the rationale, mechanism, and operational surface of self-packaging (#41-#49) in the locations operators and future maintainers actually look. - docs/spec/ARCHITECTURE.md * New "Self-Packaging (optional)" subsection under Key Components, naming archive_io / selfpath / bundle_locator / macho_bundle / EmbeddedArchiveFileProvider / EmbeddedFileSystem / pack with file paths. * New "Self-Packaging Bootstrap" data-flow block that traces the sequence from main() through detectAndRegisterEmbeddedBundle to FileProviderFactory dispatch to RegisterEmbeddedFileSystem. - docs/spec/DESIGN_DECISIONS.md * New §9 "Self-Packaging via Appended ZIP" with four sub-decisions: 9a. ZIP appended after the executable -- why-not tar / 7z / custom container; the `bytes_in_last_block(1)` spike gotcha. 9b. Reuse IFileProvider -- why-not extract-to-tmpdir. 9c. embed:// DuckDB FileSystem -- why-not force everything through IFileProvider (streaming); the Glob / SeekPosition spike-runtime catch. 9d. macOS reserved-segment + re-codesign -- why-not append-and- ad-hoc-sign (notarisation). * Each item lists Decision / Rationale / Why-not alternatives / Tradeoffs, matching the file's existing structure. * Summary updated to add "Deployability" as the sixth design goal that §9 serves. - docs/spec/components/security.md * New "Secrets and the bundle" section covering the two enforcement mechanisms (pack-time deny list and runtime env-var contract). Lists every credential env var with its scope. * Cross-reference to DESIGN_DECISIONS §9 and CONFIG_REFERENCE §1.4. * Best-practices list grows an 8th item ("never bundle secrets") with a forward pointer to the section. * Source-files table grows `src/pack.cpp (IsSecretExcluded)`. - docs/CLI_REFERENCE.md * New "## 3. Self-Packaging Subcommands" section with full reference for `pack`, `info`, `unpack`, plus a macOS subsection covering the reserved-segment + codesign flow and the --macos-append legacy escape hatch. * Sections 4-7 renumbered (Environment Variables / Usage Examples / Signal Handling / Exit Codes); TOC updated to match. * Environment-variables table grows FLAPI_CONFIG, FLAPI_LOG_LEVEL, SOURCE_DATE_EPOCH, CODESIGN_IDENTITY entries. - AGENTS.md (target of the CLAUDE.md symlink) * New "6. Self-Packaging" entry under Core Concepts -- short operator-style overview, command examples, mechanism, secrets invariant, reproducibility note. Forward links to DESIGN_DECISIONS §9 and CLI_REFERENCE §3 for depth.
1 parent ec12140 commit dcdbbae

5 files changed

Lines changed: 402 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,56 @@ Template variables available:
401401
- `context.conn.*`: Connection properties (paths, credentials)
402402
- `context.auth.*`: Authentication context
403403

404+
### 6. Self-Packaging (single-binary deploy)
405+
406+
The same `flapi` binary that serves the API can also fold an entire
407+
config tree into itself, producing a self-contained executable
408+
deployable via `scp`.
409+
410+
```bash
411+
# Pack a config tree into a new bundled binary
412+
flapi pack --in ./examples --out flapi-prod
413+
414+
# Inspect what's bundled
415+
./flapi-prod info
416+
417+
# Extract the bundle for debugging
418+
./flapi-prod unpack --to /tmp/extracted
419+
420+
# Run it -- serves the bundled config from any cwd
421+
cd /tmp && ./flapi-prod
422+
```
423+
424+
**How it works:**
425+
426+
- A ZIP archive is appended after the executable (or, on macOS,
427+
written into a reserved `__FLAPI/__bundle` Mach-O segment that
428+
was allocated at link time -- 16 MiB default, knob
429+
`FLAPI_RESERVED_BUNDLE_MIB`). Mach-O is re-codesigned so the
430+
output is notarisable.
431+
- At startup, `bundle_locator` either reverse-scans the EOCD
432+
signature from EOF (Linux / Windows) or probes the reserved
433+
section (macOS). On hit, entries are decompressed once into a
434+
shared `ArchiveEntries`.
435+
- `EmbeddedArchiveFileProvider` (implements `IFileProvider`) serves
436+
config / SQL templates from that map. `FileProviderFactory`
437+
dispatches non-remote paths to it when a bundle is present.
438+
- For SQL templates that use `read_csv()` / `read_parquet()`, an
439+
`EmbeddedFileSystem` is registered with DuckDB on the `embed://`
440+
scheme, so `read_csv('embed://data/cities.csv')` resolves to the
441+
same in-memory bytes.
442+
443+
**Secrets never go in the bundle.** `pack` refuses files matching
444+
`*.env`, `secrets/*`, `*.pem`, `*.key` by default. Credentials come
445+
from environment variables at runtime (`AWS_*`, `GOOGLE_*`, `AZURE_*`,
446+
`FLAPI_CONFIG_SERVICE_TOKEN`, `{{env.VARNAME}}` interpolation in
447+
YAML). See [DESIGN_DECISIONS §9](docs/spec/DESIGN_DECISIONS.md#9-self-packaging-via-appended-zip)
448+
for the rationale and [CLI_REFERENCE §3](docs/CLI_REFERENCE.md#3-self-packaging-subcommands)
449+
for full subcommand options.
450+
451+
**Reproducibility.** Set `SOURCE_DATE_EPOCH` (epoch seconds) before
452+
`flapi pack` and the output is bit-identical across runs.
453+
404454
## Key Patterns
405455

406456
### Safe Query Building Pattern

docs/CLI_REFERENCE.md

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ This document provides a complete reference for the `flapi` server executable's
1919
- [Validate Configuration](#validate-configuration---validate-config)
2020
- [Configuration Service](#configuration-service---config-service)
2121
- [Configuration Service Token](#configuration-service-token---config-service-token)
22-
3. [Environment Variables](#3-environment-variables)
23-
4. [Usage Examples](#4-usage-examples)
22+
3. [Self-Packaging Subcommands](#3-self-packaging-subcommands)
23+
- [`flapi pack`](#flapi-pack----create-a-self-contained-binary)
24+
- [`flapi info`](#flapi-info----inspect-the-running-binarys-bundle)
25+
- [`flapi unpack`](#flapi-unpack---to-dir----dump-the-bundle-for-debugging)
26+
- [macOS notarisation specifics](#macos-notarisation-specifics)
27+
4. [Environment Variables](#4-environment-variables)
28+
5. [Usage Examples](#5-usage-examples)
2429
- [Basic Startup](#basic-startup)
2530
- [Development Mode](#development-mode)
2631
- [Production Mode](#production-mode)
2732
- [CI/CD Validation](#cicd-validation)
28-
5. [Signal Handling](#5-signal-handling)
29-
6. [Exit Codes](#6-exit-codes)
33+
6. [Signal Handling](#6-signal-handling)
34+
7. [Exit Codes](#7-exit-codes)
3035
- [Related Documentation](#related-documentation)
3136

3237
---
@@ -396,12 +401,130 @@ export FLAPI_NO_TELEMETRY=1
396401
397402
---
398403

399-
## 3. Environment Variables
404+
## 3. Self-Packaging Subcommands
405+
406+
flapi can fold its config tree (flapi.yaml + endpoint YAMLs + SQL
407+
templates + small data files) into the binary itself, producing a
408+
single self-contained artifact deployable via `scp`. The same binary
409+
that serves the API also produces new bundled artifacts -- there is
410+
no separate packager.
411+
412+
See [DESIGN_DECISIONS.md §9](./spec/DESIGN_DECISIONS.md#9-self-packaging-via-appended-zip)
413+
for the architectural rationale.
414+
415+
### `flapi pack` -- create a self-contained binary
416+
417+
```
418+
flapi pack --in <config-dir> --out <new-binary> [--allow-secrets] [--macos-append]
419+
```
420+
421+
| Option | Required | Description |
422+
|--------|----------|-------------|
423+
| `--in` | yes | Directory containing `flapi.yaml` and friends. Walked recursively. |
424+
| `--out` | yes | Path for the bundled output binary. Overwritten if it exists. |
425+
| `--allow-secrets` | no | Bypass the default secret deny list. Testing only -- production users must never set this. |
426+
| `--macos-append` | no | macOS only: append the archive after `__LINKEDIT` instead of overwriting the reserved `__FLAPI/__bundle` segment. **Not notarisable.** |
427+
428+
**Default secret deny list** (refusal with non-zero exit, unless
429+
`--allow-secrets`):
430+
431+
- `*.env` at any depth
432+
- `secrets/` segment at any depth
433+
- `*.pem` at any depth
434+
- `*.key` at any depth
435+
436+
**Reproducible builds.** Set `SOURCE_DATE_EPOCH` to stamp every
437+
archive entry with a deterministic mtime; the produced binary is
438+
then bit-identical across runs given the same input.
439+
440+
**Re-pack idempotence.** If the host binary already has a trailing
441+
bundle, `pack` strips it from the _copy_ (not the running binary)
442+
before appending the new one, so repeated invocations don't grow the
443+
output.
444+
445+
**Example:**
446+
447+
```bash
448+
SOURCE_DATE_EPOCH=1700000000 flapi pack --in ./examples --out flapi-prod
449+
chmod +x flapi-prod # exec bits already preserved
450+
scp flapi-prod user@host:/opt/flapi/ # one-file deploy
451+
ssh user@host '/opt/flapi/flapi-prod' # serves bundled config from any cwd
452+
```
453+
454+
> **Implementation:** `src/pack.cpp`, `src/archive_io.cpp` | **Tests:** `test/cpp/pack_test.cpp`, `test/integration/test_self_packaging.py`
455+
456+
### `flapi info` -- inspect the running binary's bundle
457+
458+
```
459+
flapi info
460+
```
461+
462+
Prints the EOCD offset, bundle size, and entry list (with byte
463+
counts). Exits non-zero with `"Bundle: none (filesystem mode)"` if
464+
the binary has no appended (or in-section, on macOS) bundle.
465+
466+
**Example:**
467+
468+
```bash
469+
$ ./flapi-prod info
470+
Binary: /opt/flapi/flapi-prod
471+
Bundle offset: 70123456
472+
Bundle size: 12534 bytes
473+
Entries (17):
474+
flapi.yaml (1024 bytes)
475+
sqls/customers.yaml (412 bytes)
476+
...
477+
```
478+
479+
### `flapi unpack --to <dir>` -- dump the bundle for debugging
480+
481+
```
482+
flapi unpack --to <dir>
483+
```
484+
485+
Writes every bundle entry to `<dir>` (creating intermediate
486+
directories as needed), preserving paths. Useful for diffing a
487+
deployed bundle against a development tree.
488+
489+
**Example:**
490+
491+
```bash
492+
$ ./flapi-prod unpack --to /tmp/extracted
493+
Unpacked 17 entries to /tmp/extracted
494+
495+
$ diff -ru ./examples /tmp/extracted
496+
# (empty -- bundle matches source tree)
497+
```
498+
499+
### macOS notarisation specifics
500+
501+
On Darwin, `flapi` is linked with a reserved `__FLAPI/__bundle`
502+
Mach-O section (default 16 MiB, knob `FLAPI_RESERVED_BUNDLE_MIB` at
503+
CMake configure time). `flapi pack` overwrites this section in
504+
place and re-invokes `codesign --force --sign $CODESIGN_IDENTITY`
505+
(defaulting to `-` for ad-hoc) so the freshly bundled binary has a
506+
fresh valid signature. The output is suitable for `notarytool
507+
submit`.
508+
509+
The `--macos-append` flag falls back to the Linux/Windows-style
510+
trailing-bytes layout. Use it only for local debugging -- the
511+
signature is intentionally invalid and the binary will fail
512+
notarisation.
513+
514+
> **Implementation:** `src/macho_bundle.cpp` | **Tests:** `test/cpp/macho_bundle_test.cpp`, `test/integration/test_self_packaging_macos.py`
515+
516+
---
517+
518+
## 4. Environment Variables
400519

401520
| Variable | Description | Used By |
402521
|----------|-------------|---------|
522+
| `FLAPI_CONFIG` | Path to `flapi.yaml` (fallback for `-c`) | `--config` fallback |
523+
| `FLAPI_LOG_LEVEL` | Log verbosity (fallback for `--log-level`); invalid values exit 1 | `--log-level` fallback |
403524
| `FLAPI_CONFIG_SERVICE_TOKEN` | Authentication token for configuration service API | `--config-service-token` fallback |
404525
| `FLAPI_NO_TELEMETRY` | Disable telemetry when set to `1`, `true`, or `yes` | `--no-telemetry` fallback |
526+
| `SOURCE_DATE_EPOCH` | Mtime stamped on every entry by `flapi pack` (reproducible builds) | `flapi pack` |
527+
| `CODESIGN_IDENTITY` | macOS only: identity passed to `codesign --sign` after `flapi pack`. Defaults to `-` (ad-hoc). | `flapi pack` |
405528

406529
**Configuration File Variables:**
407530

@@ -418,7 +541,7 @@ See [Configuration Reference - Environment Variables](./CONFIG_REFERENCE.md#10-e
418541
419542
---
420543
421-
## 4. Usage Examples
544+
## 5. Usage Examples
422545
423546
### Basic Startup
424547
@@ -474,7 +597,7 @@ fi
474597

475598
---
476599

477-
## 5. Signal Handling
600+
## 6. Signal Handling
478601

479602
| Signal | Behavior |
480603
|--------|----------|
@@ -499,7 +622,7 @@ On receiving a shutdown signal, the server:
499622
500623
---
501624

502-
## 6. Exit Codes
625+
## 7. Exit Codes
503626

504627
| Code | Description |
505628
|------|-------------|

docs/spec/ARCHITECTURE.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ Storage and external data access:
139139
| **AuthMiddleware** | `src/auth_middleware.cpp` | JWT/Basic/OIDC authentication |
140140
| **RateLimitMiddleware** | `src/rate_limit_middleware.cpp` | Request rate limiting |
141141

142+
### Self-Packaging (optional)
143+
144+
These components are loaded only when the running binary contains an
145+
appended (or, on macOS, in-section) ZIP bundle. They let the same
146+
artifact serve the API _and_ produce new bundled artifacts via
147+
`flapi pack`.
148+
149+
| Component | File | Purpose |
150+
|-----------|------|---------|
151+
| **archive_io** | `src/archive_io.cpp` | RAII wrapper around libarchive; reads/writes ZIPs in memory, with `bytes_in_last_block=1` and `SOURCE_DATE_EPOCH` mtime stamping for reproducible builds. |
152+
| **selfpath** | `src/selfpath.cpp` | Cross-platform self-binary path (`/proc/self/exe`, `_NSGetExecutablePath`, `GetModuleFileNameW`). |
153+
| **bundle_locator** | `src/bundle_locator.cpp` | Reverse-scans for the ZIP EOCD record (Linux/Windows); on macOS prefers the reserved `__FLAPI/__bundle` Mach-O section. Tolerates trailing zero padding. |
154+
| **macho_bundle** | `src/macho_bundle.cpp` | 64-bit Mach-O header + LC_SEGMENT_64 parser; writes the archive into the reserved section in place and re-invokes `codesign`. |
155+
| **EmbeddedArchiveFileProvider** | `src/embedded_archive_file_provider.cpp` | `IFileProvider` implementation backed by a `std::shared_ptr<const ArchiveEntries>`. Sibling of `LocalFileProvider` / `DuckDBVFSProvider`. |
156+
| **EmbeddedFileSystem** | `src/duckdb_embed_fs.cpp` | `duckdb::FileSystem` for the `embed://` scheme. Lets SQL templates do `read_csv('embed://data/x.csv')`. Same `ArchiveEntries` instance as `EmbeddedArchiveFileProvider`. |
157+
| **pack** | `src/pack.cpp` | `flapi pack` / `info` / `unpack` subcommand logic. Enforces a default secret deny list (`*.env`, `secrets/*`, `*.pem`, `*.key`). |
158+
142159
## Data Flow
143160

144161
### REST Request Flow
@@ -167,6 +184,36 @@ Storage and external data access:
167184

168185
For detailed request flows with sequence diagrams, see [REQUEST_LIFECYCLE.md](./REQUEST_LIFECYCLE.md).
169186

187+
### Self-Packaging Bootstrap
188+
189+
When `flapi` starts, _before_ loading the config:
190+
191+
```
192+
1. main() calls detectAndRegisterEmbeddedBundle()
193+
├─ bundle_locator::LocateBundleInSelf()
194+
│ macOS: probe __FLAPI/__bundle Mach-O section first
195+
│ fallback: reverse-scan EOCD signature from EOF
196+
├─ if bundle found: read slice → archive_io::ReadArchive()
197+
└─ store entries in FileProviderFactory (process-wide shared_ptr)
198+
199+
2. main() proceeds to initializeConfig()
200+
ConfigLoader.loadYamlFile("flapi.yaml")
201+
├─ FileProviderFactory::CreateProvider("flapi.yaml")
202+
│ bundle present + non-remote path → EmbeddedArchiveFileProvider
203+
│ no bundle + non-remote → LocalFileProvider
204+
│ any remote scheme → DuckDBVFSProvider
205+
└─ provider.ReadFile() returns bytes (from bundle or disk)
206+
207+
3. After DatabaseManager is up, main() calls
208+
RegisterEmbeddedFileSystem() which adds the embed:// VFS to
209+
DuckDB so SQL templates can `read_csv('embed://data/foo.csv')`.
210+
```
211+
212+
If no bundle is present (Linux/Windows shipped without `pack`, or
213+
the trailing 1 KiB has been truncated), all bundle-aware components
214+
silently return nullopt and the binary serves from the local
215+
filesystem -- existing behaviour, zero churn.
216+
170217
## Protocol Support
171218

172219
flAPI supports two protocols from a unified configuration:

0 commit comments

Comments
 (0)