Skip to content

Commit ad737f4

Browse files
fix(cloud): make push payload limit configurable
Closes #316
1 parent 2f8e5e8 commit ad737f4

15 files changed

Lines changed: 136 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Breaking changes are always marked with a `type:breaking-change` label and docum
2121

2222
<!-- Changes that are merged but not yet released are tracked here until the next tag. -->
2323

24+
### Cloud sync
25+
26+
- **fix(cloud):** make chunk and mutation push payload limits configurable with `ENGRAM_CLOUD_MAX_PUSH_BYTES` while preserving the 8 MiB default.
27+
2428
### Pi package (`pi-engram`)
2529

2630
- **fix(plugin):** allow `mem_session_summary` to accept an explicit `project` fallback when automatic project detection is unavailable.
@@ -49,7 +53,7 @@ New and updated routes registered in `internal/cloud/dashboard/dashboard.go`:
4953
Background mutation-based replication for `engram serve` and `engram mcp`:
5054

5155
- **feat(autosync):** `internal/cloud/autosync.Manager` — lease-guarded background push/pull goroutine enabled by `ENGRAM_CLOUD_AUTOSYNC=1` + `ENGRAM_CLOUD_TOKEN` + `ENGRAM_CLOUD_SERVER`
52-
- **feat(cloudserver):** add `POST /sync/mutations/push` (batch up to 100 mutations, 8 MiB body cap, per-project auth + pause gate returning HTTP 409 `sync-paused`)
56+
- **feat(cloudserver):** add `POST /sync/mutations/push` (batch up to 100 mutations, configurable body cap defaulting to 8 MiB, per-project auth + pause gate returning HTTP 409 `sync-paused`)
5357
- **feat(cloudserver):** add `GET /sync/mutations/pull?since_seq=N&limit=M` (server-side filtered by enrolled projects; fail-closed when `EnrolledProjectsProvider` not implemented)
5458
- **feat(autosync):** phases: `idle`, `pushing`, `pulling`, `healthy`, `push_failed`, `pull_failed`, `backoff`, `disabled`
5559
- **feat(autosync):** reason codes: `transport_failed`, `auth_required`, `policy_forbidden`, `server_unsupported`, `internal_error`, `sync-paused`

DOCS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ Cloud runtime envs for `engram cloud serve`:
548548
| `ENGRAM_DATABASE_URL` | yes | Postgres DSN for cloud chunk storage/dashboard read model |
549549
| `ENGRAM_PORT` | no | Runtime port (default `8080`) |
550550
| `ENGRAM_CLOUD_HOST` | no | Bind host (default `127.0.0.1`; use `0.0.0.0` for containers) |
551+
| `ENGRAM_CLOUD_MAX_PUSH_BYTES` | no | Max chunk/mutation push request body bytes (default `8388608`) |
551552
| `ENGRAM_CLOUD_ALLOWED_PROJECTS` | yes | Comma-separated allowlist; always required (authenticated + insecure modes) |
552553
| `ENGRAM_CLOUD_TOKEN` | yes (authenticated mode) | Enables bearer auth mode |
553554
| `ENGRAM_JWT_SECRET` | yes (authenticated mode) | Must be explicitly set and non-default when token mode is enabled |

cmd/engram/cloud.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ var newCloudRuntime = func(cfg cloud.Config) (cloudServerRuntime, error) {
112112
cloudserver.WithHost(cfg.BindHost),
113113
cloudserver.WithProjectAuthorizer(projectAuth),
114114
cloudserver.WithDashboardAdminToken(cfg.AdminToken),
115+
cloudserver.WithMaxPushBodyBytes(cfg.MaxPushBodyBytes),
115116
cloudserver.WithSyncStatusProvider(cloudDashboardStatusProvider{store: cs, projects: allowedProjects}),
116117
),
117118
store: cs,

cmd/engram/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,6 +2335,8 @@ Environment:
23352335
ENGRAM_DATABASE_URL
23362336
Postgres DSN for engram cloud serve
23372337
ENGRAM_CLOUD_HOST Bind host for engram cloud serve (default: 127.0.0.1)
2338+
ENGRAM_CLOUD_MAX_PUSH_BYTES
2339+
Max cloud push payload bytes (default: 8388608)
23382340
ENGRAM_CLOUD_TOKEN Bearer token required in authenticated cloud serve mode
23392341
ENGRAM_CLOUD_INSECURE_NO_AUTH
23402342
Set to 1 ONLY for local insecure cloud serve mode (no auth)

cmd/engram/main_extra_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,7 @@ func TestCmdCloudServeStartsCloudRuntime(t *testing.T) {
15941594
t.Setenv("ENGRAM_CLOUD_HOST", "0.0.0.0")
15951595
t.Setenv("ENGRAM_CLOUD_TOKEN", "token-abc")
15961596
t.Setenv("ENGRAM_CLOUD_ALLOWED_PROJECTS", "proj-a")
1597+
t.Setenv("ENGRAM_CLOUD_MAX_PUSH_BYTES", "10485760")
15971598

15981599
var seen cloud.Config
15991600
runtimeStub := &stubCloudRuntimeServer{}
@@ -1618,6 +1619,9 @@ func TestCmdCloudServeStartsCloudRuntime(t *testing.T) {
16181619
if seen.BindHost != "0.0.0.0" {
16191620
t.Fatalf("expected cloud runtime config bind host from env, got %q", seen.BindHost)
16201621
}
1622+
if seen.MaxPushBodyBytes != 10485760 {
1623+
t.Fatalf("expected cloud runtime max push bytes from env, got %d", seen.MaxPushBodyBytes)
1624+
}
16211625
}
16221626

16231627
func TestCmdCloudServeRequiresAuthTokenByDefault(t *testing.T) {

cmd/engram/main_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ func TestPrintUsage(t *testing.T) {
181181
for _, token := range []string{
182182
"ENGRAM_DATABASE_URL",
183183
"ENGRAM_CLOUD_HOST",
184+
"ENGRAM_CLOUD_MAX_PUSH_BYTES",
184185
"ENGRAM_CLOUD_TOKEN",
185186
"ENGRAM_CLOUD_INSECURE_NO_AUTH",
186187
"Cannot be combined with ENGRAM_CLOUD_TOKEN",

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ The cloud server exposes two routes registered in `cloudserver.go` and handled b
298298
| `POST` | `/sync/mutations/push` | Accept a batch of up to 100 mutations from the client |
299299
| `GET` | `/sync/mutations/pull` | Return mutations since a cursor, filtered by caller's enrolled projects |
300300

301-
Both require `Authorization: Bearer <token>`. Push enforces the project-level sync pause (HTTP 409 on `sync_enabled=false`). Pull filters server-side.
301+
Both require `Authorization: Bearer <token>`. Push enforces the project-level sync pause (HTTP 409 on `sync_enabled=false`) and the configured cloud push body limit (`ENGRAM_CLOUD_MAX_PUSH_BYTES`, default 8 MiB). Pull filters server-side.
302302

303303
---
304304

docs/codebase/sync-and-cloud.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Business rule: **if sync is blocked, fail loudly and visibly**. No silent drops.
5959
- `GET /sync/mutations/pull`
6060
- `/dashboard/*`
6161

62+
`POST /sync/push` and `POST /sync/mutations/push` enforce the server-side push request body limit from `ENGRAM_CLOUD_MAX_PUSH_BYTES` (default 8 MiB).
63+
6264
For complete route details, use [DOCS.md — HTTP API Endpoints](../../DOCS.md#http-api-endpoints).
6365

6466
## Cloud store: `internal/cloud/cloudstore`

docs/engram-cloud/quickstart.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ Required runtime env vars:
8383
- `ENGRAM_CLOUD_HOST=0.0.0.0`
8484
- `ENGRAM_PORT=18080`
8585

86+
Optional runtime env vars:
87+
- `ENGRAM_CLOUD_MAX_PUSH_BYTES` (defaults to `8388608`)
88+
8689
Dokploy guidance:
8790
1. Create a managed Postgres service.
8891
2. Create an app from image `ghcr.io/gentleman-programming/engram:latest`.
@@ -116,6 +119,7 @@ ENGRAM_CLOUD_ADMIN=replace-with-separate-admin-token
116119
ENGRAM_JWT_SECRET=replace-with-32+-byte-random-secret
117120
ENGRAM_CLOUD_ALLOWED_PROJECTS=engram,gentle-ai
118121
ENGRAM_CLOUD_HOST=0.0.0.0
122+
ENGRAM_CLOUD_MAX_PUSH_BYTES=8388608
119123
ENGRAM_PORT=18080
120124
```
121125

@@ -125,6 +129,7 @@ Notes:
125129
- `ENGRAM_CLOUD_ADMIN` is the dashboard admin token. Use a different secret from `ENGRAM_CLOUD_TOKEN`.
126130
- `ENGRAM_JWT_SECRET` must be an explicit, non-default strong secret in authenticated mode.
127131
- `ENGRAM_CLOUD_ALLOWED_PROJECTS` is required server-side and should be a comma-separated allowlist.
132+
- `ENGRAM_CLOUD_MAX_PUSH_BYTES` optionally raises or lowers the server-side limit for chunk and mutation push request bodies. Omit it to keep the default 8 MiB limit.
128133

129134
Reference compose:
130135

internal/cloud/cloudserver/cloudserver.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,20 @@ type staticStatusProvider struct{ status dashboard.SyncStatus }
4646
func (s staticStatusProvider) Status() dashboard.SyncStatus { return s.status }
4747

4848
type CloudServer struct {
49-
store ChunkStore
50-
auth Authenticator
51-
projectAuth ProjectAuthorizer
52-
dashboardAdmin string
53-
port int
54-
host string
55-
mux *http.ServeMux
56-
syncStatus dashboard.SyncStatusProvider
57-
listenAndServe func(addr string, handler http.Handler) error
49+
store ChunkStore
50+
auth Authenticator
51+
projectAuth ProjectAuthorizer
52+
dashboardAdmin string
53+
port int
54+
host string
55+
maxPushBodyBytes int64
56+
mux *http.ServeMux
57+
syncStatus dashboard.SyncStatusProvider
58+
listenAndServe func(addr string, handler http.Handler) error
5859
}
5960

6061
const defaultHost = "127.0.0.1"
61-
const maxPushBodyBytes int64 = 8 * 1024 * 1024
62+
const defaultMaxPushBodyBytes int64 = 8 * 1024 * 1024
6263
const maxDashboardLoginBodyBytes int64 = 16 * 1024
6364
const dashboardSessionCookieName = "engram_dashboard_token"
6465

@@ -88,12 +89,21 @@ func WithDashboardAdminToken(adminToken string) Option {
8889
}
8990
}
9091

92+
func WithMaxPushBodyBytes(limit int64) Option {
93+
return func(s *CloudServer) {
94+
if limit > 0 {
95+
s.maxPushBodyBytes = limit
96+
}
97+
}
98+
}
99+
91100
func New(store ChunkStore, authSvc Authenticator, port int, opts ...Option) *CloudServer {
92101
s := &CloudServer{
93-
store: store,
94-
auth: authSvc,
95-
port: port,
96-
host: defaultHost,
102+
store: store,
103+
auth: authSvc,
104+
port: port,
105+
host: defaultHost,
106+
maxPushBodyBytes: defaultMaxPushBodyBytes,
97107
syncStatus: staticStatusProvider{status: dashboard.SyncStatus{
98108
Phase: "degraded",
99109
ReasonCode: constants.ReasonTransportFailed,
@@ -128,6 +138,13 @@ func (s *CloudServer) Handler() http.Handler {
128138
return s.mux
129139
}
130140

141+
func (s *CloudServer) pushBodyLimit() int64 {
142+
if s.maxPushBodyBytes > 0 {
143+
return s.maxPushBodyBytes
144+
}
145+
return defaultMaxPushBodyBytes
146+
}
147+
131148
func (s *CloudServer) routes() {
132149
s.mux = http.NewServeMux()
133150
s.mux.HandleFunc("GET /health", s.handleHealth)
@@ -346,6 +363,7 @@ func (s *CloudServer) handlePullChunk(w http.ResponseWriter, r *http.Request) {
346363
}
347364

348365
func (s *CloudServer) handlePushChunk(w http.ResponseWriter, r *http.Request) {
366+
maxPushBodyBytes := s.pushBodyLimit()
349367
r.Body = http.MaxBytesReader(w, r.Body, maxPushBodyBytes)
350368
var req struct {
351369
ChunkID string `json:"chunk_id"`

0 commit comments

Comments
 (0)