Skip to content

feat(storage): add composable shared storage backends#217

Open
ian-pascoe wants to merge 5 commits into
mainfrom
feat/portable-cloud-runtime-deployment
Open

feat(storage): add composable shared storage backends#217
ian-pascoe wants to merge 5 commits into
mainfrom
feat/portable-cloud-runtime-deployment

Conversation

@ian-pascoe

@ian-pascoe ian-pascoe commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Self-hosting operators can keep immutable container-staged Caplets while choosing filesystem, SQLite, PostgreSQL, or S3-compatible storage for dashboard-managed Caplets, credentials, settings, sessions, activity, backups, restores, and migrations. The public contract is now one provider-shaped storage block; internal generation identity, namespaces, and resolved secret material remain implementation details rather than operator-facing configuration.

This gives single-node and horizontally scaled deployments the same administration and lifecycle model without treating a replica-local filesystem as shared state.

Design decisions

Area Contract
Composition Filesystem-staged definitions reserve their IDs and remain immutable; dashboard-managed records use the configured storage provider.
Public configuration storage.provider selects filesystem, SQLite, PostgreSQL, or S3 with only that provider's physical placement and secret-reference fields. Legacy authority, public authority IDs, target namespaces, and provider-specific top-level aliases are rejected.
Internal consistency Every provider still implements the same Writable Authority protocol, generation checks, idempotent receipts, fencing, backup, restore, migration, and lifecycle contracts. Opaque identity never becomes a user-selectable storage coordinate.
Physical isolation PostgreSQL owns one fixed caplets schema; S3 owns a validated bucket/root prefix; local providers canonicalize their paths and reject overlap with staged or migration inputs.
Secrets and profiles Connections, optional S3 credentials, and vault keys resolve from deployment-time references. Lifecycle commands accept exactly one canonical CAPLETS_STORAGE_PROFILE_<NAME> declaration and fail closed on malformed or unresolved input.
Operator surfaces CLI help, structured lifecycle JSON, generated schemas/reference docs, self-hosting guidance, and dashboard health terminology all describe Storage and Storage Generations while preserving opaque CAS payloads internally.

Validation

  • pnpm verify passed end to end:
    • formatting, lint, generated Code Mode API, schema and docs freshness, typecheck, deterministic benchmarks, package-runtime checks, and all builds
    • 184 test files passed, 2 skipped
    • 2,489 tests passed, 12 skipped
  • Focused storage-remediation suite: 10 files passed, 2 PostgreSQL live files skipped; 274 tests passed, 12 skipped.
  • A full structured review used 14 specialist lenses. All 12 validated findings were fixed, including explicit filesystem routing, fail-closed profile/secret selection, PostgreSQL DDL/schema qualification, S3 root ownership, and secret-free coordinator APIs.
  • Headless agent-browser smoke tests loaded /dashboard/runtime, /dashboard/settings, and /dashboard/caplets; the disconnected Current Host authorization shell rendered with no page or console errors. Authenticated Storage Generation interactions require a running Current Host and operator client, so they are covered by dashboard component and HTTP integration tests.
  • TEST_POSTGRES_URL was not available for the final refactor pass, so live PostgreSQL authority suites remained explicitly skipped; deterministic migration byte-alignment and fixed-schema assertions passed.

Related

Fixes #210.
Fixes #211.
Fixes #212.
Fixes #213.
Fixes #214.
Fixes #215.
Fixes #216.


Compound Engineering
Codex

Summary by CodeRabbit

  • New Features
    • Added self-hosted shared storage support (filesystem, SQLite, PostgreSQL, S3-compatible) with an authority-backed runtime and encrypted shared state.
    • Expanded dashboard with generation-aware authority health, caplet CRUD with safer conflict handling, settings/setup approvals, and authority-backed session/security presentation.
    • Added new CLI commands for Current Host administration and storage lifecycle (inventory, migrate, backup/restore, schema/provider).
  • Documentation
    • Added new shared storage configuration guidance, self-hosting guide, and terminology/glossary updates.
  • CI
    • Added optional live storage validation runs for provider evidence.
  • Tests
    • Expanded storage, migration, backup/restore, dashboard, and multi-replica coverage, plus optional live provider matrix checks.

Support filesystem, SQLite, PostgreSQL, and S3-backed writable authority state alongside immutable staged Caplets. Add replica-safe runtime activation, Current Host administration, migrations and recovery, dashboard and CLI parity, provider packaging, operator documentation, and deterministic cross-provider verification.
@socket-security

socket-security Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​better-sqlite3@​7.6.131001007180100
Addedpostgres@​3.4.99910010081100
Addedbetter-sqlite3@​12.11.18810010091100
Addeddrizzle-kit@​0.31.10991009698100
Added@​aws-sdk/​client-s3@​3.1085.09910010098100

View full report

@socket-security

socket-security Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm better-sqlite3 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: packages/core/package.jsonnpm/better-sqlite3@12.11.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/better-sqlite3@12.11.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces composable shared storage with filesystem, SQLite, PostgreSQL, and S3-compatible authorities; generation-aware runtime coordination; authority-backed dashboard, CLI, vault, OAuth, session, and setup state; migration and backup workflows; and deterministic/provider-specific validation.

Changes

Shared storage and runtime

Layer / File(s) Summary
Authority contracts and providers
packages/core/src/storage/*
Adds writable-authority contracts, filesystem, SQL, and S3 providers, migrations, backups, bundle caching, provenance, health, fencing, and generation semantics.
Runtime composition and lifecycle
packages/core/src/runtime.ts, packages/core/src/storage/coordinator.ts, packages/core/src/serve/*
Adds async authority assembly, immutable runtime epochs, leases, refresh coordination, health reporting, and shared-runtime lifecycle handling.
Current Host administration
packages/core/src/current-host/*, packages/core/src/remote-control/*
Adds generation-aware caplet, catalog, settings, setup, vault, client, diagnostics, and remote-control operations with idempotency and conflict handling.
Dashboard and secret persistence
packages/core/src/dashboard/*, packages/core/src/vault/*, packages/core/src/auth/*
Adds authority-backed sessions, activity, vault values, grants, OAuth token storage, setup approvals, redaction, and cross-replica state handling.
CLI and configuration
packages/core/src/cli/*, packages/core/src/config.ts, schemas/*
Adds authority bootstrap configuration, current-host commands, storage inventory/migration/backup/schema commands, shared installs, and async auth flows.
Validation and delivery
packages/core/test/*, scripts/test-storage-providers.ts, .github/workflows/ci.yml
Adds provider contracts, multi-replica tests, dashboard tests, storage lifecycle tests, package boundary checks, deterministic matrices, and optional live AWS/R2 validation.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

Poem

A rabbit found a shared new store,
Where generations hop ashore.
Vaults and caplets, safe and bright,
Sync through epochs day and night.
“Hop on!” cried Bun, “the tests all cheer!”
🐇 Storage blooms from ear to ear.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Portable cloud-runtime/project-binding docs, prototype code, and scripts appear unrelated to the shared-storage/provider issues in scope. Move the portable cloud runtime/project-binding work into a separate PR unless it is required by the shared-storage feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The storage/provider changes and tests line up with the linked issues' goals for no-op catalog updates, provider contracts, TOCTOU fixes, S3 scale, Postgres batching, and session cleanup.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main storage-backend addition in the pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/portable-cloud-runtime-deployment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces composable shared storage backends for Caplets, replacing single-node filesystem state with a provider-agnostic WritableAuthority abstraction that supports filesystem, SQLite, PostgreSQL, and S3 providers. The architecture separates immutable container-staged Caplet definitions from dashboard-managed state (sessions, activity, settings, vault), uses generation-identity compare-and-swap commits, and adds encrypted receipts, maintenance fencing, backup/restore, and live migration across providers.

  • New storage layer (packages/core/src/storage/): WritableAuthority interface, four provider implementations, coordinator with polling refresh, backup/restore with AES-256-GCM, and fenced cross-provider migration.
  • Dashboard sessions and activity (packages/core/src/dashboard/): Two session store implementations (filesystem lock-based DashboardSessionStore and authority-backed AuthorityDashboardSessionStore) with encrypted CSRF tokens, idle/absolute timeouts, and touch-throttled last-used persistence.
  • CLI and HTTP surface (packages/core/src/cli/storage.ts, serve/http.ts): Storage inventory, backup, restore, migrate, and schema commands; dashboard login/session endpoints updated to route through the authority-backed store when configured.

Confidence Score: 5/5

The PR is safe to merge. All four storage providers implement the same well-defined interface, the backup format is authenticated with AES-256-GCM and cross-checked against stored generation identity, and the migration path is fenced, race-rechecked, and publishes only after staged verification.

The storage abstraction, commit/replay logic, maintenance fencing, and backup/restore are consistently implemented across providers. Session authentication is layered. The two findings are latent design issues that do not affect current behavior.

packages/core/src/storage/s3-authority.ts for the encodeKeyPart encoding scheme; packages/core/src/dashboard/activity-log.ts for the multi-process truncation race in DashboardActivityLog.append.

Important Files Changed

Filename Overview
packages/core/src/storage/s3-authority.ts S3 provider with sharded receipt/session indexes, immutable candidate objects, and ETag-based CAS for head. The encodeKeyPart function is non-injective, creating a latent collision for inputs containing literal _XX sequences vs. characters that percent-encode to %XX.
packages/core/src/storage/backup.ts AES-256-GCM backup with authenticated header, provider/namespace/generation identity cross-checks, and key fingerprint guard. Format is correct and self-consistent.
packages/core/src/storage/migration.ts Fenced, empty-target migration with source race recheck, staged publish, and post-publish verification. Fence release is correctly guarded by finally blocks at both source and destination levels.
packages/core/src/dashboard/session-store.ts Dual session store implementations: filesystem (directory-lock mutex) and authority-backed (encrypted CSRF token, touch-throttled auxiliary commits). The assertAuthorityOperator helper silently passes when the snapshot has no remoteCredentials.
packages/core/src/dashboard/activity-log.ts Dual activity log implementations: JSONL append-file (no cross-process lock) and authority-backed. The filesystem class has a multi-process truncation race near MAX_ACTIVITY_ENTRIES, but is only used in single-node deployments.
packages/core/src/storage/composition.ts Clean precedence composition: staged filesystem sources then authority records, with staged/authority ID collision detection and SHA-256 fingerprinting of staged paths.
packages/core/src/remote/authority-codec.ts Domain codec encrypts commit results with AES-GCM via the vault key; decrypts replayed results from stored receipts. Timing-safe equality for hash comparison.
packages/core/src/storage/coordinator.ts Runtime coordinator with reference-counted PreparedRuntimeView, polling-based authority refresh, epoch-lease tracking, and graceful shutdown.
packages/core/src/storage/factory.ts Provider registry with deduplication guard and lifecycle identity assertion. Clean separation between provider registration and authority construction.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Dashboard Client
    participant H as HTTP Handler
    participant RS as RemoteCredentialService
    participant SS as AuthorityDashboardSessionStore
    participant CDC as AuthorityDomainCodec
    participant A as WritableAuthority

    C->>H: POST /dashboard/login/complete
    H->>RS: completePendingLogin(requiredRole: operator)
    RS-->>H: credentials clientId
    H->>SS: create(operatorClientId)
    SS->>CDC: read()
    CDC->>A: readHead plus readGeneration
    A-->>CDC: generation snapshot
    CDC-->>SS: head and snapshot
    SS->>SS: assertAuthorityOperator pruneExpiredSessions
    SS->>CDC: commit snapshot plus session
    CDC->>A: commit SemanticCommandEnvelope
    Note over A: CAS on expectedGeneration
    A-->>CDC: AuthorityCommitResult
    CDC-->>SS: kind generation result
    SS->>A: commitAuxiliary session_touch
    SS-->>H: cookieValue session
    H-->>C: Set-Cookie plus JSON session
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Dashboard Client
    participant H as HTTP Handler
    participant RS as RemoteCredentialService
    participant SS as AuthorityDashboardSessionStore
    participant CDC as AuthorityDomainCodec
    participant A as WritableAuthority

    C->>H: POST /dashboard/login/complete
    H->>RS: completePendingLogin(requiredRole: operator)
    RS-->>H: credentials clientId
    H->>SS: create(operatorClientId)
    SS->>CDC: read()
    CDC->>A: readHead plus readGeneration
    A-->>CDC: generation snapshot
    CDC-->>SS: head and snapshot
    SS->>SS: assertAuthorityOperator pruneExpiredSessions
    SS->>CDC: commit snapshot plus session
    CDC->>A: commit SemanticCommandEnvelope
    Note over A: CAS on expectedGeneration
    A-->>CDC: AuthorityCommitResult
    CDC-->>SS: kind generation result
    SS->>A: commitAuxiliary session_touch
    SS-->>H: cookieValue session
    H-->>C: Set-Cookie plus JSON session
Loading

Reviews (4): Last reviewed commit: "refactor(storage): expose provider-shape..." | Re-trigger Greptile

type MaintenanceFenceLease,
} from "./migration";

const BACKUP_MAGIC = Buffer.from("CAPLETS-AUTHORITY-BACKUP\\0", "utf8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The BACKUP_MAGIC constant uses a double-escaped \\0, which in JavaScript source produces a literal backslash character (\, 0x5C) followed by the digit 0 (0x30) — not a null terminator byte. Because both writer and reader share the same constant the format is self-consistent today, but the magic is 26 bytes ending in \0 (the two-character sequence) rather than the intended 25 bytes ending in a NUL byte. Any future correction of this escape to "\0" would silently invalidate every backup written by this version, since the magic check would fail on the first byte of the suffix.

Suggested change
const BACKUP_MAGIC = Buffer.from("CAPLETS-AUTHORITY-BACKUP\\0", "utf8");
const BACKUP_MAGIC = Buffer.from("CAPLETS-AUTHORITY-BACKUP\0", "utf8");

Fix in Codex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/serve/http.ts (1)

1527-1548: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the runtime lease alive for attachEvents SSE responses. The handler returns a long-lived text/event-stream, but the request lease is released as soon as the route handler resolves. attachEventsResponse() continues using source.manifestRevision() and engine.onReload() after that, so a runtime reload can retire the view while the stream is still open. Hold the lease until the SSE stream closes, like the MCP/attach-session paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/serve/http.ts` around lines 1527 - 1548, The attachEvents
route must retain its runtime lease for the entire SSE lifetime instead of
releasing it when the handler resolves. Update the handler around
attachEventsResponse and requestEngine to acquire or reuse the lease and release
it from the stream’s close/cancel cleanup, preserving the existing error
response path and ensuring source.manifestRevision() and engine.onReload()
remain valid until the stream closes.
packages/core/src/serve/session.ts (1)

153-164: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Lease can leak if pre-close cleanup throws.

unsubscribeReload(), codeModeSessions.close(), and clearRegistrations() run outside the try/finally. If any of them throws, this.closed is already true, so a later close() call is a no-op and runtimeLease.release() never runs — permanently pinning the retained PreparedRuntimeView open. CapletsEngine.close() already uses try/finally for this exact reason; mirror it here.

🔒️ Guard all cleanup so the lease is always released
   async close(): Promise<void> {
     if (this.closed) return;
     this.closed = true;
-    this.unsubscribeReload();
-    this.codeModeSessions.close();
-    this.clearRegistrations();
     try {
+      this.unsubscribeReload();
+      this.codeModeSessions.close();
+      this.clearRegistrations();
       await this.server.close();
     } finally {
       this.runtimeLease?.release();
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/serve/session.ts` around lines 153 - 164, Update
Session.close() so unsubscribeReload(), codeModeSessions.close(),
clearRegistrations(), and server.close() all execute within the try/finally that
releases runtimeLease. Preserve the existing closed guard and cleanup order
while ensuring runtimeLease?.release() runs even when any pre-close cleanup
throws.
🧹 Nitpick comments (6)
packages/core/README.md (1)

138-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dangling intro line under "Configuration and source ownership".

Line 138 ("The bootstrap provider union is:") ends with a colon but no union body follows before line 140 restates the intro. Reads like an editing artifact — either drop line 138 or fold it into the field list on lines 142–146.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/README.md` around lines 138 - 146, The “The bootstrap provider
union is:” line is a dangling introduction in the “Configuration and source
ownership” section. Remove it or merge its wording into the following
provider-specific field list, ensuring the section has only one clear
introduction before the filesystem, sqlite, postgresql, and s3 entries.
packages/core/src/storage/sql/migrations/postgres/0000_initial.sql (1)

61-63: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant index duplicates the primary key.

authority_events already has PRIMARY KEY (authority_id, watermark), which creates a btree on exactly (authority_id, watermark). The extra authority_events_after_idx on the same columns/order is fully redundant and only adds write overhead on a table this PR scales to thousands of events. Since this is a new, checksummed migration, dropping it now is safe before release.

♻️ Proposed change
-CREATE INDEX IF NOT EXISTS authority_events_after_idx ON authority_events (authority_id, watermark);

The same redundant index also appears in packages/core/src/storage/sql/migrate.ts (POSTGRES_INITIAL_SQL and SQLITE_INITIAL_SQL); keep all copies in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/sql/migrations/postgres/0000_initial.sql` around
lines 61 - 63, Remove the redundant authority_events_after_idx creation from the
PostgreSQL migration and from both POSTGRES_INITIAL_SQL and SQLITE_INITIAL_SQL
in migrate.ts, while preserving the composite primary key on (authority_id,
watermark). Keep all initial schema definitions synchronized.
packages/core/src/storage/filesystem-authority.ts (1)

1280-1282: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use a deterministic receipt comparator

stableJsonStringify(...).localeCompare(...) is locale-sensitive, so receipt order can differ across runtimes. authorityExportDigest hashes the full export object, so that order change also changes the digest and can make cross-machine restore/migration comparisons disagree. Switch both receipt sort sites to a bytewise/codepoint comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/filesystem-authority.ts` around lines 1280 - 1282,
Update both receipt sort sites to use a locale-independent bytewise/codepoint
comparator instead of localeCompare when comparing stableJsonStringify results.
Preserve the existing receipt ordering input to authorityExportDigest so export
digests remain deterministic across runtimes.
packages/core/src/storage/sql/migrations/sqlite/0000_initial.sql (1)

54-62: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant index duplicates the primary key.

authority_events already has PRIMARY KEY (authority_id, watermark), which SQLite backs with an index on exactly those columns in that order. authority_events_after_idx on (authority_id, watermark) adds no lookup benefit and only costs write/storage overhead. Consider dropping it, or if the intent was range scans keyed differently, adjust the column set (e.g. add a covering/ordering column).

♻️ Drop the redundant index
-CREATE INDEX IF NOT EXISTS authority_events_after_idx ON authority_events (authority_id, watermark);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/sql/migrations/sqlite/0000_initial.sql` around
lines 54 - 62, Remove the redundant authority_events_after_idx definition from
the authority_events migration, since the composite PRIMARY KEY on
(authority_id, watermark) already provides the same index ordering. Keep the
authority_events table and primary-key constraint unchanged.
packages/core/src/storage/migration.ts (1)

1033-1033: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant no-op ternary.

Both branches evaluate to recordId, so the condition has no effect. The caplets/records case is already handled by the dedicated branch just below (which continues), so this only ever runs for other keys where the value is recordId regardless. Simplify to avoid the misleading appearance of branch-specific behavior.

♻️ Suggested simplification
-    const nestedRecordId = key === "caplets" || key === "records" ? recordId : recordId;
+    const nestedRecordId = recordId;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/migration.ts` at line 1033, In the migration logic,
replace the redundant conditional assignment to nestedRecordId with a direct
recordId assignment. The dedicated caplets/records branch and its continue
behavior should remain unchanged.
packages/core/src/storage/s3-authority.ts (1)

3138-3143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused watermark helpers Remove the unused watermark helpers nextWatermark() and latestAuxiliaryWatermark(). parseEvent() and eventsPrefix() only support latestAuxiliaryWatermark(), so keep them if that helper stays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/s3-authority.ts` around lines 3138 - 3143, Remove
the unused nextWatermark() and latestAuxiliaryWatermark() helpers, along with
any now-unused supporting logic. Since parseEvent() and eventsPrefix() exist
only for latestAuxiliaryWatermark(), remove those methods as well unless
latestAuxiliaryWatermark() is retained.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 58-59: Update both actions/checkout steps in the CI workflow to
set persist-credentials to false, ensuring the GITHUB_TOKEN is not retained in
the checked-out repository for subsequent steps.

In `@apps/dashboard/src/components/DashboardApp.tsx`:
- Around line 2110-2124: The delete flow around dashboardMutationStatus must
only perform success side effects when phase is "active". Keep the pending
branch polling as-is, but handle degraded without refreshing, clearing the
editor, or showing the deletion-success toast; rely on the existing
MutationStatusNotice state instead.

In `@docs/product/self-hosting.md`:
- Around line 23-25: The MinIO image reference in the s3 self-hosting
documentation uses an invalid 63-character digest. Re-derive the correct SHA-256
digest for the pinned MinIO image and replace the value in the deterministic
local fixture description, preserving the digest-pinned image format.

In `@packages/core/src/dashboard/session-store.ts`:
- Around line 440-449: Update the session validation flow around
codec.commitAuxiliary and the touched result so only “missing” and “revoked”
throw AUTH_FAILED. Handle “conflict” as a valid session outcome by returning the
session with persistedLastUsedAt, or skip the touch while preserving the
existing session response behavior.
- Around line 665-684: Update assertAuthorityOperator so a missing, non-object,
or array-valued remoteCredentials immediately throws the existing AUTH_FAILED
CapletsError instead of returning. Preserve the current client lookup and
operator/revocation checks for valid remoteCredentials objects, ensuring create
and validate fail closed when the authority domain is absent.

In `@packages/core/src/engine.ts`:
- Around line 775-794: Update assertSynchronousAuthority to catch and ignore
only CapletsError instances with code CONFIG_NOT_FOUND; allow
malformed-bootstrap and other CapletsError failures from loadAuthorityBootstrap
to propagate, while preserving the existing filesystem-provider check and
ASYNC_AUTHORITY_REQUIRED behavior.

In `@packages/core/src/remote-control/client.ts`:
- Around line 186-195: Update currentHostMutationArguments to normalize an
explicit null expectedGeneration to the unset representation before constructing
the mutation arguments, so dispatch.ts never receives null while undefined
behavior remains unchanged. Alternatively, if null is not a supported semantic,
remove it from RemoteCurrentHostMutationOptions and preserve the existing
non-null forwarding behavior.

In `@packages/core/src/remote-control/dispatch.ts`:
- Around line 792-814: Update optionalGeneration to treat a null argument value
the same as undefined and return undefined, while preserving validation and
identity parsing for non-null values. Ensure nullable expectedGeneration inputs
remain valid through the remote-control request path without changing unrelated
mutation behavior.

In `@packages/core/src/serve/stdio.ts`:
- Around line 21-32: Wrap the asyncAuthority setup block around
assembleCapletsHost, runtimeHost.retain, and CapletsMcpSession construction in
try/catch; if retain or session creation fails, await runtimeHost.close() before
rethrowing the original error. Keep the non-asyncAuthority path unchanged and
preserve the existing successful connection flow.

In `@packages/core/src/storage/bundle-cache.ts`:
- Around line 378-384: Fix the top-level markdown fallback in inferEntryPath by
matching a literal period before “md” rather than a backslash, so exactly one
normalized top-level .md path is returned. Preserve the existing CAPLET.md
preference and CONFIG_INVALID error for zero or multiple candidates.
- Around line 164-209: Update cleanup() to consult the live this.pending set
when deciding whether to remove each fingerprint directory, rather than relying
only on the initial pending snapshot; preserve the existing protections for
active, pinned, and referenced fingerprints. Add a regression test covering
materialize() starting after cleanup begins for an already-cached fingerprint,
ensuring cleanup does not remove that directory.

In `@packages/core/src/storage/composition.ts`:
- Around line 421-422: Replace locale-dependent entry sorting with deterministic
code-point/ordinal comparison in fingerprintDirectory and the sibling collectors
collectFilesSync and collectFiles. Update each comparator to compare names using
the same ordinal ordering, preserving the existing collection and fingerprint
behavior.

In `@packages/core/test/packed-package.test.ts`:
- Around line 158-165: Update optionalProviderDenyLoader’s blocked regex so the
drizzle-orm alternation matches the package prefix without the redundant inner
separator/end boundary, ensuring drizzle-orm and all subpath imports such as
drizzle-orm/sqlite-core are denied consistently.

In `@packages/core/test/storage-sql-migrations.test.ts`:
- Around line 21-27: Remove the metadata-only test “pins the stable Drizzle and
native driver packaging” from storage-sql-migrations.test.ts. Keep the existing
behavioral migration coverage unchanged, relying on those tests and package
review rather than asserting literal dependency versions.

In `@README.md`:
- Around line 234-245: Revise the S3 and Cloudflare R2 sentence in the
“Self-hosted shared storage” section to state the provider support or validation
status clearly and naturally. Preserve the requirement for credentialed live
verification, while replacing the awkward “before they should be called
validated” phrasing.

---

Outside diff comments:
In `@packages/core/src/serve/http.ts`:
- Around line 1527-1548: The attachEvents route must retain its runtime lease
for the entire SSE lifetime instead of releasing it when the handler resolves.
Update the handler around attachEventsResponse and requestEngine to acquire or
reuse the lease and release it from the stream’s close/cancel cleanup,
preserving the existing error response path and ensuring
source.manifestRevision() and engine.onReload() remain valid until the stream
closes.

In `@packages/core/src/serve/session.ts`:
- Around line 153-164: Update Session.close() so unsubscribeReload(),
codeModeSessions.close(), clearRegistrations(), and server.close() all execute
within the try/finally that releases runtimeLease. Preserve the existing closed
guard and cleanup order while ensuring runtimeLease?.release() runs even when
any pre-close cleanup throws.

---

Nitpick comments:
In `@packages/core/README.md`:
- Around line 138-146: The “The bootstrap provider union is:” line is a dangling
introduction in the “Configuration and source ownership” section. Remove it or
merge its wording into the following provider-specific field list, ensuring the
section has only one clear introduction before the filesystem, sqlite,
postgresql, and s3 entries.

In `@packages/core/src/storage/filesystem-authority.ts`:
- Around line 1280-1282: Update both receipt sort sites to use a
locale-independent bytewise/codepoint comparator instead of localeCompare when
comparing stableJsonStringify results. Preserve the existing receipt ordering
input to authorityExportDigest so export digests remain deterministic across
runtimes.

In `@packages/core/src/storage/migration.ts`:
- Line 1033: In the migration logic, replace the redundant conditional
assignment to nestedRecordId with a direct recordId assignment. The dedicated
caplets/records branch and its continue behavior should remain unchanged.

In `@packages/core/src/storage/s3-authority.ts`:
- Around line 3138-3143: Remove the unused nextWatermark() and
latestAuxiliaryWatermark() helpers, along with any now-unused supporting logic.
Since parseEvent() and eventsPrefix() exist only for latestAuxiliaryWatermark(),
remove those methods as well unless latestAuxiliaryWatermark() is retained.

In `@packages/core/src/storage/sql/migrations/postgres/0000_initial.sql`:
- Around line 61-63: Remove the redundant authority_events_after_idx creation
from the PostgreSQL migration and from both POSTGRES_INITIAL_SQL and
SQLITE_INITIAL_SQL in migrate.ts, while preserving the composite primary key on
(authority_id, watermark). Keep all initial schema definitions synchronized.

In `@packages/core/src/storage/sql/migrations/sqlite/0000_initial.sql`:
- Around line 54-62: Remove the redundant authority_events_after_idx definition
from the authority_events migration, since the composite PRIMARY KEY on
(authority_id, watermark) already provides the same index ordering. Keep the
authority_events table and primary-key constraint unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc48427d-1ece-4a76-b53c-8cde55d56aad

📥 Commits

Reviewing files that changed from the base of the PR and between 98df606 and 0c30178.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (124)
  • .changeset/composable-shared-storage.md
  • .github/workflows/ci.yml
  • CONCEPTS.md
  • README.md
  • apps/dashboard/src/components/DashboardApp.test.tsx
  • apps/dashboard/src/components/DashboardApp.tsx
  • apps/docs/src/content/docs/reference/config.mdx
  • apps/landing/public/config.schema.json
  • docs/agents/domain.md
  • docs/plans/2026-07-11-002-feat-portable-cloud-runtime-deployment-plan.md
  • docs/plans/2026-07-11-003-feat-composable-shared-storage-plan.md
  • docs/product/self-hosting.md
  • docs/research/2026-07-11-cloud-runtime-provider-feasibility.md
  • package.json
  • packages/core/README.md
  • packages/core/package.json
  • packages/core/prototypes/cloud-project-binding/model.ts
  • packages/core/prototypes/cloud-project-binding/tui.ts
  • packages/core/rolldown.config.ts
  • packages/core/src/auth.ts
  • packages/core/src/auth/store.ts
  • packages/core/src/cli.ts
  • packages/core/src/cli/auth.ts
  • packages/core/src/cli/commands.ts
  • packages/core/src/cli/completion.ts
  • packages/core/src/cli/install.ts
  • packages/core/src/cli/setup-caplet.ts
  • packages/core/src/cli/storage.ts
  • packages/core/src/cli/vault.ts
  • packages/core/src/cloud/runtime-adapter.ts
  • packages/core/src/config.ts
  • packages/core/src/current-host/caplet-operations.ts
  • packages/core/src/current-host/catalog-operations.ts
  • packages/core/src/current-host/catalog.ts
  • packages/core/src/current-host/client-operations.ts
  • packages/core/src/current-host/operations.ts
  • packages/core/src/current-host/settings-operations.ts
  • packages/core/src/current-host/vault-operations.ts
  • packages/core/src/dashboard/activity-log.ts
  • packages/core/src/dashboard/session-store.ts
  • packages/core/src/downstream.ts
  • packages/core/src/engine.ts
  • packages/core/src/errors.ts
  • packages/core/src/google-discovery/manager.ts
  • packages/core/src/index.ts
  • packages/core/src/native.ts
  • packages/core/src/native/service.ts
  • packages/core/src/remote-control/client.ts
  • packages/core/src/remote-control/dispatch.ts
  • packages/core/src/remote-control/types.ts
  • packages/core/src/remote/authority-codec.ts
  • packages/core/src/remote/server-credential-store.ts
  • packages/core/src/runtime.ts
  • packages/core/src/serve/http.ts
  • packages/core/src/serve/index.ts
  • packages/core/src/serve/native-session.ts
  • packages/core/src/serve/session.ts
  • packages/core/src/serve/stdio.ts
  • packages/core/src/setup/local-store.ts
  • packages/core/src/setup/runner.ts
  • packages/core/src/storage/backup.ts
  • packages/core/src/storage/bundle-cache.ts
  • packages/core/src/storage/composition.ts
  • packages/core/src/storage/conformance.ts
  • packages/core/src/storage/coordinator.ts
  • packages/core/src/storage/factory.ts
  • packages/core/src/storage/filesystem-authority.ts
  • packages/core/src/storage/migration.ts
  • packages/core/src/storage/s3-authority.ts
  • packages/core/src/storage/sql/authority.ts
  • packages/core/src/storage/sql/migrate.ts
  • packages/core/src/storage/sql/migrations/postgres/0000_initial.sql
  • packages/core/src/storage/sql/migrations/postgres/0001_head_guard.sql
  • packages/core/src/storage/sql/migrations/postgres/0002_maintenance_lease.sql
  • packages/core/src/storage/sql/migrations/postgres/meta/_journal.json
  • packages/core/src/storage/sql/migrations/sqlite/0000_initial.sql
  • packages/core/src/storage/sql/migrations/sqlite/0001_head_guard.sql
  • packages/core/src/storage/sql/migrations/sqlite/0002_maintenance_lease.sql
  • packages/core/src/storage/sql/migrations/sqlite/meta/_journal.json
  • packages/core/src/storage/sql/schema-postgres.ts
  • packages/core/src/storage/sql/schema-sqlite.ts
  • packages/core/src/storage/types.ts
  • packages/core/src/vault/access.ts
  • packages/core/src/vault/index.ts
  • packages/core/src/vault/keys.ts
  • packages/core/src/vault/types.ts
  • packages/core/test/attach-service-wiring.test.ts
  • packages/core/test/authority-access-session.test.ts
  • packages/core/test/authority-codec.test.ts
  • packages/core/test/authority-secrets.test.ts
  • packages/core/test/cli-completion.test.ts
  • packages/core/test/cli-remote.test.ts
  • packages/core/test/cli-storage.test.ts
  • packages/core/test/current-host-administration.test.ts
  • packages/core/test/current-host-catalog-operations.test.ts
  • packages/core/test/dashboard-activity.test.ts
  • packages/core/test/dashboard-api.test.ts
  • packages/core/test/dashboard-authority-mutations.test.ts
  • packages/core/test/fixtures/storage/minio-pinned.compose.yml
  • packages/core/test/fixtures/storage/provider-matrix.compose.yml
  • packages/core/test/packed-package.test.ts
  • packages/core/test/remote-control-client.test.ts
  • packages/core/test/serve-http.test.ts
  • packages/core/test/serve-session.test.ts
  • packages/core/test/setup-runner.test.ts
  • packages/core/test/storage-backup.test.ts
  • packages/core/test/storage-bundle-cache.test.ts
  • packages/core/test/storage-composition.test.ts
  • packages/core/test/storage-contract.test.ts
  • packages/core/test/storage-coordinator.test.ts
  • packages/core/test/storage-filesystem-authority.test.ts
  • packages/core/test/storage-migration.test.ts
  • packages/core/test/storage-multi-replica.test.ts
  • packages/core/test/storage-postgres-authority-batching.test.ts
  • packages/core/test/storage-postgres-authority.test.ts
  • packages/core/test/storage-provider-contract.ts
  • packages/core/test/storage-provider-matrix.test.ts
  • packages/core/test/storage-s3-authority.test.ts
  • packages/core/test/storage-s3-conformance.test.ts
  • packages/core/test/storage-sql-migrations.test.ts
  • packages/core/test/storage-sqlite-authority.test.ts
  • pnpm-workspace.yaml
  • schemas/caplets-config.schema.json
  • scripts/test-storage-providers.ts

Comment thread .github/workflows/ci.yml
Comment on lines +58 to +59
- name: Checkout
uses: actions/checkout@v7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on actions/checkout.

Neither checkout disables credential persistence, so the GITHUB_TOKEN is written to .git/config and remains available to any later step. This is especially relevant for storage-provider-live, which also loads long-lived cloud secrets into the job environment.

🔒️ Proposed hardening
       - name: Checkout
         uses: actions/checkout@v7
+        with:
+          persist-credentials: false

As per static analysis (zizmor artipacked: does not set persist-credentials: false).

Also applies to: 94-95

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 58-59: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 58 - 59, Update both actions/checkout
steps in the CI workflow to set persist-credentials to false, ensuring the
GITHUB_TOKEN is not retained in the checked-out repository for subsequent steps.

Source: Linters/SAST tools

Comment thread apps/dashboard/src/components/DashboardApp.tsx
Comment on lines +23 to +25
- `s3` — an S3-compatible authority using conditional object writes and ETags. The deterministic
local fixture uses the digest-pinned MinIO image
`docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the pinned digest length in the doc.
rg -n '14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936' docs/product/self-hosting.md
python3 - <<'PY'
d="14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936"
print("length:", len(d), "valid_sha256:", len(d)==64)
PY

Repository: spiritledsoftware/caplets

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '360,375p' docs/product/self-hosting.md | cat -n
rg -n '14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936' docs/product/self-hosting.md

Repository: spiritledsoftware/caplets

Length of output: 1453


Pinned MinIO digest is invalid.
sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936 is only 63 hex chars, so it cannot be pulled as a valid SHA-256 image reference. Re-derive the digest and replace this reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/product/self-hosting.md` around lines 23 - 25, The MinIO image reference
in the s3 self-hosting documentation uses an invalid 63-character digest.
Re-derive the correct SHA-256 digest for the pinned MinIO image and replace the
value in the deterministic local fixture description, preserving the
digest-pinned image format.

Comment on lines +440 to +449
const touched = await this.codec.commitAuxiliary({
kind: "session_touch",
sessionId: session.sessionId,
lastUsedAt: now.toISOString(),
expectedRevision: auxiliaryRecord?.revision ?? "",
expectedGeneration: read.head,
});
if (touched.kind === "revoked" || touched.kind === "missing" || touched.kind === "conflict") {
throw new CapletsError("AUTH_FAILED", "Dashboard session is no longer active.");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C4 '"session_touch"|kind:\s*"conflict"' packages/core/src/storage
ast-grep outline packages/core/src/storage/sql/authority.ts --match 'commitAuxiliary'

Repository: spiritledsoftware/caplets

Length of output: 23875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the session-store flow around the reported lines.
sed -n '380,500p' packages/core/src/dashboard/session-store.ts

# Inspect the auxiliary commit result type and the storage implementations that define `conflict`.
sed -n '1,120p' packages/core/src/storage/types.ts
sed -n '1640,1710p' packages/core/src/storage/s3-authority.ts
sed -n '1310,1385p' packages/core/src/storage/sql/authority.ts
sed -n '400,435p' packages/core/src/storage/filesystem-authority.ts

Repository: spiritledsoftware/caplets

Length of output: 17238


Do not fail validation on session_touch conflicts. commitAuxiliary() returns conflict for stale expectedGeneration/expectedRevision, so a concurrent authority commit or touch can make a valid session fail here. Only missing/revoked mean the session is gone; on conflict, return the session with persistedLastUsedAt (or skip the touch) instead of throwing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/dashboard/session-store.ts` around lines 440 - 449, Update
the session validation flow around codec.commitAuxiliary and the touched result
so only “missing” and “revoked” throw AUTH_FAILED. Handle “conflict” as a valid
session outcome by returning the session with persistedLastUsedAt, or skip the
touch while preserving the existing session response behavior.

Comment on lines +665 to +684
function assertAuthorityOperator(root: Record<string, unknown>, clientId: string): void {
const remote = root.remoteCredentials;
if (!remote || typeof remote !== "object" || Array.isArray(remote)) return;
const clients = (remote as { clients?: unknown }).clients;
if (!Array.isArray(clients)) return;
const client = clients.find(
(candidate) =>
candidate &&
typeof candidate === "object" &&
!Array.isArray(candidate) &&
(candidate as Record<string, unknown>).clientId === clientId,
);
if (
!client ||
(client as Record<string, unknown>).role !== "operator" ||
(client as Record<string, unknown>).revokedAt
) {
throw new CapletsError("AUTH_FAILED", "Dashboard operator client is no longer authorized.");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C3 'remoteCredentials' packages/core/src/remote packages/core/src/dashboard

Repository: spiritledsoftware/caplets

Length of output: 1400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== session-store outline =="
ast-grep outline packages/core/src/dashboard/session-store.ts --view expanded | sed -n '1,220p'

echo
echo "== server-credential-store outline =="
ast-grep outline packages/core/src/remote/server-credential-store.ts --view expanded | sed -n '1,260p'

echo
echo "== session-store relevant slices =="
sed -n '600,760p' packages/core/src/dashboard/session-store.ts

echo
echo "== server-credential-store relevant slices =="
sed -n '820,980p' packages/core/src/remote/server-credential-store.ts

echo
echo "== search for validate/create and remoteCredentials.clients =="
rg -n -C 3 'remoteCredentials|clients|create\(|validate\(' packages/core/src/dashboard/session-store.ts packages/core/src/remote/server-credential-store.ts

Repository: spiritledsoftware/caplets

Length of output: 40480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== session-store create/validate slices =="
sed -n '317,460p' packages/core/src/dashboard/session-store.ts

echo
echo "== authority-domain codec references =="
rg -n -C 3 'class AuthorityDomainCodec|domainSnapshot\(|read\(|write\(|snapshot' packages/core/src/remote/server-credential-store.ts packages/core/src/dashboard/session-store.ts

echo
echo "== root mutation / domain snapshot handling around codec methods =="
sed -n '1410,1495p' packages/core/src/remote/server-credential-store.ts

echo
echo "== dashboard store authority mutate slices =="
sed -n '317,430p' packages/core/src/dashboard/session-store.ts

Repository: spiritledsoftware/caplets

Length of output: 37239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find AuthorityDomainCodec declaration =="
rg -n -C 4 'class AuthorityDomainCodec|domainSnapshot\(|commitAuxiliary\(|readAuxiliary\(' packages/core/src

echo
echo "== codec file candidates =="
fd -a 'authority-domain|codec|domain' packages/core/src

echo
echo "== likely codec file outlines =="
for f in $(fd -a 'authority-domain' packages/core/src); do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded | sed -n '1,220p'
done

Repository: spiritledsoftware/caplets

Length of output: 27257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search for remoteCredentials restore/initialization/migration =="
rg -n -C 3 'remoteCredentials|dashboardSessions|restoreState|exportState|migrate|snapshot.*remoteCredentials|snapshot.*dashboardSessions' packages/core/src

echo
echo "== authority storage restore semantics =="
rg -n -C 3 'async restoreState|restoreState\(|exportState\(' packages/core/src/storage

echo
echo "== authority snapshot types / domains =="
rg -n -C 3 'type AuthorityExport|type AuthoritySnapshot|domains?:|snapshot:' packages/core/src/storage packages/core/src/remote packages/core/src/dashboard

Repository: spiritledsoftware/caplets

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== instantiations of dashboard session stores =="
rg -n -C 2 'new (Authority)?DashboardSessionStore|DashboardSessionStore\(' packages/core/src

echo
echo "== authority read/restore export shapes =="
sed -n '1,120p' packages/core/src/remote/authority-codec.ts
echo
sed -n '120,220p' packages/core/src/remote/authority-codec.ts
echo
echo "== filesystem authority restore/export relevant slices =="
sed -n '483,560p' packages/core/src/storage/filesystem-authority.ts
echo
sed -n '885,980p' packages/core/src/storage/s3-authority.ts

Repository: spiritledsoftware/caplets

Length of output: 15110


Fail closed when remoteCredentials is missing
AuthorityDomainCodec.read() can return an empty snapshot, and assertAuthorityOperator() returns without checking remoteCredentials.clients in that case. That lets create mint a dashboard session for any operatorClientId, and validate skips operator revocation checks for existing sessions. Treat the missing domain as unauthorized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/dashboard/session-store.ts` around lines 665 - 684, Update
assertAuthorityOperator so a missing, non-object, or array-valued
remoteCredentials immediately throws the existing AUTH_FAILED CapletsError
instead of returning. Preserve the current client lookup and operator/revocation
checks for valid remoteCredentials objects, ensuring create and validate fail
closed when the authority domain is absent.

Comment on lines +378 to +384
function inferEntryPath(paths: string[]): string {
const markdown = paths.filter((path) => path === "CAPLET.md" || path.endsWith("/CAPLET.md"));
if (markdown.length === 1) return markdown[0]!;
const topLevel = paths.filter((path) => !path.includes("/") && /\\.md$/iu.test(path));
if (topLevel.length === 1) return topLevel[0]!;
throw new CapletsError("CONFIG_INVALID", "Authority bundle must identify one entry document");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Dead fallback: entry-path inference from a single top-level .md file can never succeed.

const topLevel = paths.filter((path) => !path.includes("/") && /\\.md$/iu.test(path));

The regex is \\.md$ — an escaped literal backslash character followed by any char (.) then md$, not an escaped dot. Since path values only ever contain forward slashes at this point (normalizeBundlePath already converts \/ before any path reaches here), this regex can never match a real bundle path. This means: whenever a bundle has no CAPLET.md/*/CAPLET.md and no explicit entryPath, the "single top-level markdown file" fallback is dead code, and inferEntryPath always throws "Authority bundle must identify one entry document" even for a bundle with exactly one clear top-level .md file. It fails safe (clear CONFIG_INVALID error) rather than corrupting anything, but the documented fallback behavior doesn't work.

🐛 Proposed fix
-  const topLevel = paths.filter((path) => !path.includes("/") && /\\.md$/iu.test(path));
+  const topLevel = paths.filter((path) => !path.includes("/") && /\.md$/iu.test(path));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/bundle-cache.ts` around lines 378 - 384, Fix the
top-level markdown fallback in inferEntryPath by matching a literal period
before “md” rather than a backslash, so exactly one normalized top-level .md
path is returned. Preserve the existing CAPLET.md preference and CONFIG_INVALID
error for zero or multiple candidates.

Comment on lines +421 to +422
const entries = await readdir(current, { withFileTypes: true });
entries.sort((left, right) => left.name.localeCompare(right.name));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js String.prototype.localeCompare determinism across locales LANG LC_COLLATE ICU

💡 Result:

In Node.js, the behavior of String.prototype.localeCompare is not inherently deterministic across different environments because it depends on the International Components for Unicode (ICU) library, which implements locale-sensitive string comparisons [1][2]. Determinism Factors: 1. ICU Data Availability: The accuracy and availability of locale-specific sorting depend on the ICU data set included with the Node.js binary [1][3]. Modern Node.js versions (v13+) include "full-icu" support by default, providing comprehensive locale data [4][5]. Older versions or builds using "small-icu" only include English data, leading to non-locale-aware fallback behavior for other languages [1][3]. 2. Environment Variables: ICU respects the LANG and LC_ALL environment variables to determine the default locale if one is not explicitly provided as an argument to localeCompare [6][7]. Changes to these environment variables in the host system will alter the results of localeCompare calls that omit the locale argument [6]. 3. Upstream Changes: Because localeCompare relies on ICU (and by extension the V8 engine), updates to the underlying ICU library or V8 engine in newer Node.js releases can introduce changes in sorting behavior for specific locales [6][4]. To achieve deterministic behavior: * Explicitly specify the locale and collation options in the localeCompare call (e.g., string.localeCompare(other, 'en-US', { sensitivity: 'base' })) instead of relying on the environment's default locale [6]. * Ensure the Node.js environment is configured with the expected ICU data set (e.g., ensuring "full-icu" is available) to avoid differences between platforms or build configurations [1][8][5]. If locale-independent sorting is required, standard comparison operators (<, >, ===) or string locale-agnostic methods should be used instead of localeCompare, as they perform comparisons based on UCS code point values [9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section and any other localeCompare uses in the file.
sed -n '380,560p' packages/core/src/storage/composition.ts

printf '\n--- localeCompare search ---\n'
rg -n "localeCompare\(" packages/core/src/storage/composition.ts

printf '\n--- surrounding repository matches ---\n'
rg -n "localeCompare\(" packages/core/src -g '!**/dist/**' -g '!**/build/**'

Repository: spiritledsoftware/caplets

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe localeCompare behavior in this Node runtime with a few locales and sample strings.
node - <<'JS'
const pairs = [
  ['a', 'A'],
  ['ä', 'z'],
  ['ß', 'ss'],
  ['é', 'e'],
  ['file2', 'file10'],
];

const locales = [undefined, 'en', 'sv', 'de', 'fr', 'und'];

for (const [a, b] of pairs) {
  console.log(`PAIR ${JSON.stringify(a)} vs ${JSON.stringify(b)}`);
  for (const locale of locales) {
    const label = locale === undefined ? 'default' : locale;
    try {
      const res = locale === undefined ? a.localeCompare(b) : a.localeCompare(b, locale);
      console.log(`  ${label}: ${res}`);
    } catch (e) {
      console.log(`  ${label}: ERROR ${e && e.message}`);
    }
  }
}
JS

Repository: spiritledsoftware/caplets

Length of output: 534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant function and any nearby comments/docstrings with line numbers.
nl -ba packages/core/src/storage/composition.ts | sed -n '390,540p'

printf '\n--- search for fingerprint-related wording ---\n'
rg -n "fingerprint|replica|determin|mount-path" packages/core/src/storage/composition.ts

Repository: spiritledsoftware/caplets

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe whether the default locale changes with environment variables in this runtime.
for envspec in \
  "LANG=C LC_ALL=C" \
  "LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8" \
  "LANG=sv_SE.UTF-8 LC_ALL=sv_SE.UTF-8" \
  "LANG=de_DE.UTF-8 LC_ALL=de_DE.UTF-8" \
  "LANG=fr_FR.UTF-8 LC_ALL=fr_FR.UTF-8"
do
  echo "== $envspec =="
  bash -lc "$envspec node - <<'JS'
const opts = new Intl.Collator().resolvedOptions();
console.log(JSON.stringify(opts));
console.log('ä'.localeCompare('z'));
JS"
done

Repository: spiritledsoftware/caplets

Length of output: 1077


Use ordinal sorting here and in the sibling file collectors. localeCompare() without an explicit locale can change with host locale/ICU data, so identical staged trees can hash to different fingerprints on different replicas. Switch to code-point comparison in fingerprintDirectory, collectFilesSync, and collectFiles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/composition.ts` around lines 421 - 422, Replace
locale-dependent entry sorting with deterministic code-point/ordinal comparison
in fingerprintDirectory and the sibling collectors collectFilesSync and
collectFiles. Update each comparator to compare names using the same ordinal
ordering, preserving the existing collection and fingerprint behavior.

Comment on lines +158 to +165
function optionalProviderDenyLoader(): string {
return `const blocked = /^(?:@aws-sdk\\/client-s3|better-sqlite3|drizzle-orm(?:\\/|$)|postgres)(?:\\/|$)/;
export async function resolve(specifier, context, nextResolve) {
if (blocked.test(specifier)) throw new Error("optional provider evaluated: " + specifier);
return nextResolve(specifier, context, nextResolve);
}
`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '130,190p' packages/core/test/packed-package.test.ts

Repository: spiritledsoftware/caplets

Length of output: 2051


🏁 Script executed:

python3 - <<'PY'
import re

pattern = re.compile(r'^(?:`@aws-sdk`\/client-s3|better-sqlite3|drizzle-orm(?:\/|$)|postgres)(?:\/|$)')
samples = [
    "drizzle-orm",
    "drizzle-orm/",
    "drizzle-orm/sqlite-core",
    "drizzle-orm/pg-core",
    "drizzle-orm/better-sqlite3",
    "`@aws-sdk/client-s3`",
    "`@aws-sdk/client-s3/foo`",
    "better-sqlite3/abc",
    "postgres/sql",
]
for s in samples:
    print(s, bool(pattern.search(s)), pattern.search(s).group(0) if pattern.search(s) else None)
PY

Repository: spiritledsoftware/caplets

Length of output: 486


Fix the drizzle-orm branch in the deny-loader regex
drizzle-orm(?:\/|$) already consumes the separator, so drizzle-orm/sqlite-core and other subpath imports bypass the boundary check. Drop the inner (?:\/|$) from that alternation.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile as execFileCallback } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/packed-package.test.ts` around lines 158 - 165, Update
optionalProviderDenyLoader’s blocked regex so the drizzle-orm alternation
matches the package prefix without the redundant inner separator/end boundary,
ensuring drizzle-orm and all subpath imports such as drizzle-orm/sqlite-core are
denied consistently.

Comment on lines +21 to +27
it("pins the stable Drizzle and native driver packaging", () => {
expect(corePackage.dependencies["drizzle-orm"]).toBe("0.45.2");
expect(corePackage.dependencies["postgres"]).toBe("3.4.9");
expect(corePackage.dependencies["better-sqlite3"]).toBe("12.11.1");
expect(corePackage.devDependencies["drizzle-kit"]).toBe("0.31.10");
expect(corePackage.devDependencies["@types/better-sqlite3"]).toBe("7.6.13");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Metadata-pin test duplicates package.json literals without covering behavior.

This test only re-asserts dependency/devDependency version strings from package.json. The rest of the suite already exercises the migration behavior these pins support (histories, replay idempotency, advisory-lock serialization). As per coding guidelines: "Do not add tests that only assert package metadata... when stronger behavior coverage already exists." Consider dropping it; a stray dependency bump should be caught by the behavioral migration tests and lockfile review rather than a literal-equality snapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/storage-sql-migrations.test.ts` around lines 21 - 27,
Remove the metadata-only test “pins the stable Drizzle and native driver
packaging” from storage-sql-migrations.test.ts. Keep the existing behavioral
migration coverage unchanged, relying on those tests and package review rather
than asserting literal dependency versions.

Source: Coding guidelines

Comment thread README.md
@ian-pascoe ian-pascoe changed the title feat(storage): add composable shared authority backends feat(storage): add composable shared storage backends Jul 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/config.ts (1)

2050-2062: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Explicit default filesystem roots should not hard-fail The inventory builder always adds resolveCapletsRoot(globalPath) as an authority source, and the duplicate check rejects repeated canonical paths even for the same owner. That makes storage.provider = "filesystem" with path set to the documented default root fail with duplicate ownership (authority, authority), while omitting path works. Skip the extra authority entry when it matches the configured root, or ignore identical-owner duplicates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/config.ts` around lines 2050 - 2062, Update the canonical
ownership validation around canonicalOwners so repeated canonical paths with the
same owner do not throw; retain the CONFIG_INVALID error for duplicates claimed
by different owners. Ensure explicit filesystem paths matching the default
authority root remain valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/cli.ts`:
- Around line 4337-4349: Update resolveCliAuthorityContext() so a JSON parse
failure for an existing defaultStorageConfigPath surfaces CONFIG_INVALID instead
of returning undefined. Preserve the existing direct-storage and profile
resolution behavior for valid configurations, and ensure callers cannot fall
back to FileVaultStore or other local setup after this invalid-config condition.

In `@packages/core/src/storage/s3-authority.ts`:
- Around line 54-77: Update canonicalS3Root and its callers so the S3 object
prefix incorporates the authority namespace alongside path, ensuring authorities
sharing a bucket and path remain isolated. Preserve the existing path
sanitization and unsafe-segment validation while constructing a deterministic
namespace-specific .caplets prefix.

---

Outside diff comments:
In `@packages/core/src/config.ts`:
- Around line 2050-2062: Update the canonical ownership validation around
canonicalOwners so repeated canonical paths with the same owner do not throw;
retain the CONFIG_INVALID error for duplicates claimed by different owners.
Ensure explicit filesystem paths matching the default authority root remain
valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b9b7049-6bbf-4668-9673-3b389a94578c

📥 Commits

Reviewing files that changed from the base of the PR and between 0c30178 and 4011081.

📒 Files selected for processing (46)
  • .changeset/composable-shared-storage.md
  • README.md
  • apps/dashboard/src/components/DashboardApp.test.tsx
  • apps/dashboard/src/components/DashboardApp.tsx
  • apps/docs/src/content/docs/configuration.mdx
  • apps/docs/src/content/docs/reference/config.mdx
  • apps/landing/public/config.schema.json
  • docs/plans/2026-07-12-001-refactor-provider-shaped-storage-config-plan.md
  • docs/product/self-hosting.md
  • packages/core/README.md
  • packages/core/src/cli.ts
  • packages/core/src/cli/storage.ts
  • packages/core/src/config.ts
  • packages/core/src/engine.ts
  • packages/core/src/index.ts
  • packages/core/src/serve/http.ts
  • packages/core/src/serve/stdio.ts
  • packages/core/src/storage/backup.ts
  • packages/core/src/storage/coordinator.ts
  • packages/core/src/storage/factory.ts
  • packages/core/src/storage/filesystem-authority.ts
  • packages/core/src/storage/migration.ts
  • packages/core/src/storage/s3-authority.ts
  • packages/core/src/storage/sql/authority.ts
  • packages/core/src/storage/sql/migrate.ts
  • packages/core/src/storage/sql/migrations/postgres/0000_initial.sql
  • packages/core/src/storage/sql/migrations/postgres/0001_head_guard.sql
  • packages/core/src/storage/sql/migrations/postgres/0002_maintenance_lease.sql
  • packages/core/src/storage/sql/schema-postgres.ts
  • packages/core/test/cli-storage.test.ts
  • packages/core/test/config.test.ts
  • packages/core/test/dashboard-authority-mutations.test.ts
  • packages/core/test/serve-http.test.ts
  • packages/core/test/serve-stdio.test.ts
  • packages/core/test/storage-backup.test.ts
  • packages/core/test/storage-contract.test.ts
  • packages/core/test/storage-coordinator.test.ts
  • packages/core/test/storage-migration.test.ts
  • packages/core/test/storage-postgres-authority-batching.test.ts
  • packages/core/test/storage-postgres-authority.test.ts
  • packages/core/test/storage-s3-authority.test.ts
  • packages/core/test/storage-s3-conformance.test.ts
  • packages/core/test/storage-sql-migrations.test.ts
  • schemas/caplets-config.schema.json
  • scripts/generate-docs-reference.ts
  • scripts/test-storage-providers.ts
💤 Files with no reviewable changes (1)
  • packages/core/test/storage-backup.test.ts
🚧 Files skipped from review as they are similar to previous changes (27)
  • packages/core/src/storage/sql/migrations/postgres/0001_head_guard.sql
  • packages/core/src/storage/sql/migrations/postgres/0002_maintenance_lease.sql
  • .changeset/composable-shared-storage.md
  • README.md
  • packages/core/src/storage/sql/schema-postgres.ts
  • packages/core/test/storage-postgres-authority.test.ts
  • packages/core/src/storage/sql/migrations/postgres/0000_initial.sql
  • packages/core/src/serve/stdio.ts
  • packages/core/test/storage-postgres-authority-batching.test.ts
  • apps/dashboard/src/components/DashboardApp.test.tsx
  • packages/core/README.md
  • packages/core/src/storage/factory.ts
  • packages/core/src/engine.ts
  • packages/core/src/storage/backup.ts
  • packages/core/test/storage-s3-conformance.test.ts
  • packages/core/test/storage-contract.test.ts
  • scripts/test-storage-providers.ts
  • packages/core/test/dashboard-authority-mutations.test.ts
  • packages/core/src/cli/storage.ts
  • packages/core/test/storage-s3-authority.test.ts
  • packages/core/src/storage/migration.ts
  • packages/core/src/storage/coordinator.ts
  • packages/core/src/storage/sql/authority.ts
  • apps/dashboard/src/components/DashboardApp.tsx
  • packages/core/src/storage/filesystem-authority.ts
  • packages/core/src/storage/sql/migrate.ts
  • packages/core/src/serve/http.ts

Comment thread packages/core/src/cli.ts
Comment on lines +4337 to +4349
if (existsSync(defaultStorageConfigPath)) {
try {
const parsed = JSON.parse(readFileSync(defaultStorageConfigPath, "utf8")) as {
storage?: unknown;
};
hasDirectStorage =
parsed !== null && typeof parsed === "object" && parsed.storage !== undefined;
} catch {
return undefined;
}
}
const profile = hasDirectStorage ? undefined : singleStorageProfileName(env);
if (!hasDirectStorage && profile === undefined) return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm this is the only place shared-config parse errors are handled and that
# downstream selectors would otherwise surface the error.
rg -nP -C4 'catch\s*\{\s*\n?\s*return undefined' packages/core/src/cli.ts
rg -nP -C3 'JSON\.parse\(readFileSync' packages/core/src/cli.ts

Repository: spiritledsoftware/caplets

Length of output: 767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant config helper and authority resolution flow.
sed -n '640,700p' packages/core/src/cli.ts
printf '\n---\n'
sed -n '4300,4465p' packages/core/src/cli.ts
printf '\n---\n'
rg -n -C3 'readUserConfigObject\(|singleStorageProfileName\(|resolveCliAuthorityContext\(|runWithAuthority\(' packages/core/src/cli.ts

Repository: spiritledsoftware/caplets

Length of output: 14440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect representative commands that choose local stores when authority is absent.
sed -n '3028,3198p' packages/core/src/cli.ts
printf '\n---\n'
sed -n '3226,3338p' packages/core/src/cli.ts
printf '\n---\n'
sed -n '3432,3570p' packages/core/src/cli.ts

Repository: spiritledsoftware/caplets

Length of output: 16476


Avoid silently falling back to local stores when the shared config is invalid. If defaultStorageConfigPath exists but can’t be parsed, resolveCliAuthorityContext() returns undefined, and these CLI paths drop to FileVaultStore/local setup instead of surfacing CONFIG_INVALID. That can write secrets to the wrong storage target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/cli.ts` around lines 4337 - 4349, Update
resolveCliAuthorityContext() so a JSON parse failure for an existing
defaultStorageConfigPath surfaces CONFIG_INVALID instead of returning undefined.
Preserve the existing direct-storage and profile resolution behavior for valid
configurations, and ensure callers cannot fall back to FileVaultStore or other
local setup after this invalid-config condition.

Comment on lines +54 to +77
function canonicalS3Root(path: string | undefined): string {
const normalized = (path ?? "").replace(/^\/+|\/+$/gu, "");
if (!normalized) return ".caplets/";
for (const character of normalized) {
const codePoint = character.codePointAt(0);
if (
codePoint === undefined ||
codePoint < 0x20 ||
(codePoint >= 0x7f && codePoint <= 0x9f) ||
character === "\\"
) {
throw new CapletsError("CONFIG_INVALID", "S3 storage path contains unsafe characters");
}
}
const segments = normalized.split("/");
if (
segments.some(
(segment) => segment === "" || segment === "." || segment === ".." || segment === ".caplets",
)
) {
throw new CapletsError("CONFIG_INVALID", "S3 storage path contains an unsafe segment");
}
return `${segments.join("/")}/.caplets/`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether namespace ever contributes to S3 key generation, and how prior code scoped keys.
rg -nP -C2 'this\.namespace' packages/core/src/storage/s3-authority.ts | rg -nP 'Key|prefix|rootPrefix|key\(' 
rg -nP -C3 'rootPrefix|canonicalS3Root' packages/core/src/storage/s3-authority.ts
# Confirm docs/config express a per-namespace path requirement for S3.
rg -nP -C2 'namespace|path' schemas/caplets-config.schema.json | rg -i 's3|namespace|path'

Repository: spiritledsoftware/caplets

Length of output: 8035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the S3 authority file and inspect the parts that touch namespace/path.
ast-grep outline packages/core/src/storage/s3-authority.ts --view expanded

printf '\n--- constructor / namespace usage ---\n'
rg -n -C3 'namespace|rootPrefix|canonicalS3Root|validateGeneration|CONFIG_INVALID' packages/core/src/storage/s3-authority.ts

printf '\n--- S3 schema/docs around path ---\n'
sed -n '90,150p' schemas/caplets-config.schema.json

printf '\n--- any tests around S3 authority path/namespace isolation ---\n'
rg -n -C2 's3-authority|namespace|path' packages/core test tests | head -n 200

Repository: spiritledsoftware/caplets

Length of output: 50381


Namespace needs to be part of the S3 prefix rootPrefix is derived only from path, so two authorities sharing a bucket/path but using different namespaces will target the same objects and then fail validateGeneration with CONFIG_INVALID. Either include namespace in the key prefix or make the unique-path requirement explicit and enforced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/storage/s3-authority.ts` around lines 54 - 77, Update
canonicalS3Root and its callers so the S3 object prefix incorporates the
authority namespace alongside path, ensuring authorities sharing a bucket and
path remain isolated. Preserve the existing path sanitization and unsafe-segment
validation while constructing a deterministic namespace-specific .caplets
prefix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant