Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ docs/superpowers/

# Project specific files and folders
planning/
.planning/
tmp/
appsettings.local.json
samples/GroundControl.Samples.LinkConsole/groundcontrol-cache.json
19 changes: 10 additions & 9 deletions docs/design-docs/Domain-Model.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,19 +417,20 @@ When an admin publishes a snapshot for a project:

1. **Collect entries**: Gather all config entries from the project's attached templates and the project's own entries.
2. **Merge with override**: For any key that exists in both a template and the project, the project-level entry takes precedence (full replacement of all scoped values for that key).
3. **Interpolate variables**: For each scoped value in each entry:
a. Find all `{{variableName}}` references in the value string.
b. For each variable, resolve its value using the two-tier system:
- First check for a project-level variable with a matching scope.
- Fall back to the global variable with a matching scope.
- Use the same scope resolution algorithm (most-specific match wins).
c. Replace the placeholder with the resolved variable value.
4. **Validate**: Ensure all variable references were resolved (no unresolved `{{...}}` placeholders remain).
5. **Encrypt sensitive values**: Encrypt values marked as sensitive using the configured encryption provider.
3. **Fan out and interpolate variables**: For each merged entry, expand its source scoped values into the dim-space cartesian of scope tuples touched by the variables they reference, then resolve each variable per tuple:
a. Scan each source value for `{{variableName}}` references and look each name up using the two-tier system (project-level first, then global).
b. Build the set of target scope tuples by taking, per referenced dimension, the union of distinct values plus an "unspecified" axis, and producing the cartesian product.
c. Merge each target with the entry's own source scope, dropping conflicting combinations.
d. For each surviving final tuple, run scope resolution (most-specific match wins, falling back to the variable's unscoped default) and substitute the placeholder with the resolved value.
e. Within one source value, deduplicate emissions for the same final tuple by retaining the most-specific target. Across source values for the same entry, an emission whose source had a more specific scope tuple wins — the **explicit-wins** rule lets a literal scoped value on the entry override a fan-out emission from a scopeless sibling.
4. **Validate**: Ensure every required target tuple resolved. A placeholder is unresolved if its name matches no variable, or if `ScopeResolver` returns no value for at least one required target tuple. Any unresolved placeholder blocks publish (HTTP 422) with the offending name reported back.
5. **Encrypt sensitive values**: Encrypt values marked as sensitive using the configured encryption provider. Per-entry sensitivity is preserved: any sensitive variable contributing to any tuple flips the entire resolved entry to sensitive.
6. **Store snapshot**: Persist the immutable snapshot with a new incremented version number.
7. **Activate**: Set the project's `activeSnapshotId` to the new snapshot.
8. **Notify**: Trigger the change notification system to alert connected clients.

Fan-out is deterministic: given the same project state, two resolves produce identical resolved entries (canonical scope-tuple ordering across emissions), so the diff hash gating preview-vs-publish 409 detection remains stable. Scopeless config entries that reference scoped variables produce one snapshot value per scope tuple the variable touches — clients later pick the matching tuple at read time without any further interpolation.

**Failure modes and atomicity:**

- Steps 1–5 are pure computation with no side effects. If any step fails, no snapshot is created.
Expand Down
2 changes: 1 addition & 1 deletion docs/design-docs/Technical-Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Admin publishes snapshot for project
Server resolves: merge templates + project overrides
Server interpolates variables per scope variant
Server fans entries out across referenced variables' scope tuples and interpolates per tuple
Server encrypts sensitive values
Expand Down
8 changes: 4 additions & 4 deletions docs/guide/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Variables come in two tiers:

Use variables for values that appear in many entries, such as a connection string prefix, an API endpoint, or a shared secret. This lets you change the value in one place instead of updating every entry that uses it.

Variables are resolved at publish time. If a configuration value references a variable that is undefined or cannot be resolved for the target scope, the publish fails with an error telling you exactly which variable is missing.
Variables are resolved at publish time. A scoped variable's per-scope values automatically propagate to every entry that references it. So a scopeless entry like `MyEntry = "{{MyVariable}}"` ends up with one resolved value per scope in the published snapshot, without you having to repeat the scope tuples on every entry. If a configuration value references a variable that is undefined or cannot be resolved for one of the scopes it needs, the publish fails with an error telling you exactly which variable is missing.

For a full reference of variable structure, ownership tiers, two-tier resolution, sensitivity, and group/system-wide visibility rules, see [Variables](variables.md).
For a full reference of variable structure, ownership tiers, the two-tier lookup, sensitivity, and group/system-wide visibility rules, see [Variables](variables.md).

## Configuration Entries

Expand All @@ -72,11 +72,11 @@ An entry can be marked as **sensitive**. Sensitive values are encrypted at rest
A snapshot is an immutable, point-in-time capture of a project's fully resolved configuration. You create a snapshot by performing a "publish" action, which:

1. Merges template entries with project entries (project entries take precedence)
2. Interpolates all variable references
2. Resolves every variable reference, expanding scopeless entries that use scoped variables into one resolved value per scope
3. Encrypts sensitive values
4. Stores the result as a new, versioned snapshot

Clients always receive configuration from the **active** snapshot. Snapshots are versioned sequentially (1, 2, 3, ...) and cannot be modified after creation.
Clients always receive configuration from the **active** snapshot. They pick the matching scope from the snapshot at request time, with no further resolution work. Snapshots are versioned sequentially (1, 2, 3, ...) and cannot be modified after creation.

If you need to revert a configuration change, activate a previous snapshot. The old snapshot becomes the active one and all clients immediately receive that version's configuration.

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ cd GroundControl
aspire start src/GroundControl.AppHost
```

The Aspire dashboard opens in your browser. Find the `api` resource note the HTTP URL it is bound to. The rest of this guide uses `http://localhost:8080` as a placeholder; substitute the URL from the dashboard.
The Aspire dashboard opens in your browser. Find the `api` resource and note the HTTP URL it is bound to. The rest of this guide uses `http://localhost:8080` as a placeholder; substitute the URL from the dashboard.

Verify the API is ready:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/sdk/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ builder.Configuration.AddGroundControl(options =>
```

- Only entries the server has marked as sensitive go through the protector; non-sensitive entries (feature flags, URLs, thresholds) stay plaintext and remain readable for diagnostics.
- If the protector is not configured, every entry is cached plaintext an explicit opt-out.
- If the protector is not configured, every entry is cached plaintext. This is an explicit opt-out.
- The SDK treats ciphertext as opaque; key rotation and algorithm versioning are your protector's responsibility.
- If `Unprotect` throws, or if the cache was written under a different protector configuration than the one in effect now, the file is treated as a cache miss and the next save overwrites it.
- Cache portability depends entirely on your protector (e.g., DPAPI keys are per-machine; an AES implementation with a shared key is portable).
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/server/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ graph TD

## None (development)

All requests are treated as a system admin no login required. No auth endpoints are exposed. This mode is suitable for local development and personal homelab use.
All requests are treated as a system admin, with no login required. No auth endpoints are exposed. This mode is suitable for local development and personal homelab use.

```json
{
Expand Down Expand Up @@ -65,7 +65,7 @@ openssl rand -base64 32
- Password: the value of `Authentication:Seed:AdminPassword`
- Full system admin permissions

> **Note:** The admin seed is idempotent — restarting the server won't duplicate the account. If you change the password in the seed config, it updates the existing admin's password.
> **Note:** The admin seed is idempotent. Restarting the server won't duplicate the account. If you change the password in the seed config, it updates the existing admin's password.

### JWT settings

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/server/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Keys are stored on the file system and the key XML is encrypted at rest with an
}
```

> **Certificate rotation:** generate the new cert, deploy it as `FileSystemCertificate:Path`, move the old cert into `FileSystemCertificate:PreviousPaths`, and perform a rolling restart. New key ring entries are encrypted with the new cert; entries written under the previous cert remain decryptable as long as that cert stays in the previous list. Remove a cert from `PreviousPaths` only after every key encrypted with it has expired (90+ days by default) or been re-encrypted — otherwise the data those keys protect becomes permanently unreadable.
> **Certificate rotation:** generate the new cert, deploy it as `FileSystemCertificate:Path`, move the old cert into `FileSystemCertificate:PreviousPaths`, and perform a rolling restart. New key ring entries are encrypted with the new cert; entries written under the previous cert remain decryptable as long as that cert stays in the previous list. Remove a cert from `PreviousPaths` only after every key encrypted with it has expired (90+ days by default) or been re-encrypted. Otherwise the data those keys protect becomes permanently unreadable.

### Redis mode

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/server/deployment.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Deploying GroundControl

> **Heads up:** GroundControl is still under active development. This guide covers **local development only** running the server on your machine for evaluation or contribution. Production-deployment guidance (multi-instance hardening, Data Protection key management, change-notifier topology, container images) will be published in a later release.
> **Heads up:** GroundControl is still under active development. This guide covers **local development only**: running the server on your machine for evaluation or contribution. Production-deployment guidance (multi-instance hardening, Data Protection key management, change-notifier topology, container images) will be published in a later release.

## Prerequisites

Expand All @@ -26,7 +26,7 @@ curl http://localhost:8080/healthz/ready # substitute the URL from the dashboa

## Running without Aspire

If you prefer to run the API directly against your own MongoDB instance for example, when contributing and debugging a single project set the required environment variables and run `dotnet run`:
If you prefer to run the API directly against your own MongoDB instance (for example, when contributing and debugging a single project), set the required environment variables and run `dotnet run`:

```bash
export ConnectionStrings__Storage="mongodb://localhost:27017"
Expand Down
Loading
Loading