@everystack/mcp 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.md CHANGED
@@ -42,6 +42,25 @@ everystack db:fingerprint # Content-address the live base schema vs the Mode
42
42
  everystack db:diff # The state edge between two declared states — no DB, CI-pure
43
43
  ```
44
44
 
45
+ **Authorization (the brownfield on-ramp + the red team)**
46
+ ```bash
47
+ everystack db:authz:pull # Introspect live authz (rls/grants/policies/secdef) → reviewable contract files
48
+ everystack db:authz:diff # Live DB vs the committed contract — the CI gate (exit 1 on drift)
49
+ everystack db:authz:test # Red-team enforcement: SET ROLE + attempt per role/table/command
50
+ everystack db:authz:owner # Red-team owner isolation: two identities per owner-scoped table (catches IDOR)
51
+ everystack db:authz:report # Render the committed contract as a human review — no database
52
+ ```
53
+ All four DB-touching verbs take `--stage <name>` OR a direct venue: `--database-url <url>`,
54
+ or an inherited `ADMIN_DATABASE_URL`/`DATABASE_URL`. Same SQL, same evaluation, same
55
+ verdict — and every run names the venue it judged, because a security verdict against an
56
+ unintended database is worse than no verdict.
57
+
58
+ Use the direct venue to **rehearse an authz migration locally**. Translating hand-written
59
+ RLS into Model abilities is the hardest part of adopting the Model system, and iterating
60
+ against a deployed stage makes the loop minutes long instead of seconds. `db:authz:test`
61
+ and `db:authz:owner` WRITE in order to prove a role cannot write; every probe runs inside
62
+ a transaction that is always rolled back, on either venue.
63
+
45
64
  **Safe remote migration (protected stages)**
46
65
  ```bash
47
66
  everystack db:plan --stage X --out X.plan.json # Mint a reviewable edge — read-only, ephemeral
@@ -195,6 +195,11 @@ from which git commit), and rebuilds dependents automatically in dependency orde
195
195
  Fix the source to match, or rebuild from source with `--overwrite-drift`.
196
196
  - **Grant drift converges without ceremony** — a hand-run GRANT/REVOKE surfaces as an
197
197
  idempotent `regrant` back to the declared abilities. Declared authz is authoritative.
198
+ - **An authz-only descriptor change is a regrant, not a rebuild** — changing only the grants
199
+ on a matview (`private` → `abilities: [can('read')]`, body unchanged) applies as a bare
200
+ GRANT, no recompute. Forward-looking: the first `--apply` after upgrade arms up-to-date
201
+ objects via a record-only `backfill`, so the *first* authz change on a pre-upgrade row still
202
+ rebuilds; later ones are cheap. Column-scoped grants still rebuild (attacl isn't drift-checked).
198
203
  - **Changing a function REFRESHes the matviews that call it** — their rows are stale the
199
204
  moment the function changes, and the reconciler knows it.
200
205
  - **Unmanaged objects are never touched** — live derived objects with no source and no
@@ -174,6 +174,9 @@ export default $config({
174
174
 
175
175
  // The CLI discovers resources through these outputs. The first five are required;
176
176
  // backupsBucket unlocks db:backup / db:swap / db:fork (also auto-discovered by bucket name).
177
+ // databaseInstanceId unlocks the PHYSICAL snapshot path (db:snapshot, and db:swap's default
178
+ // pre-swap rollback point). Without it db:swap falls back to a logical pg_dump against the same
179
+ // instance it is about to restore into, which contends with the restore for locks.
177
180
  return {
178
181
  routerUrl: router.url,
179
182
  apiFunctionName: api.name,
@@ -181,6 +184,7 @@ export default $config({
181
184
  updatesBucket: updates.name,
182
185
  clientBundlesBucket: clientBundles.name,
183
186
  backupsBucket: backups.name,
187
+ databaseInstanceId: database.id,
184
188
  };
185
189
  },
186
190
  });
@@ -207,6 +211,64 @@ everystack db:seed --stage dev # dev only
207
211
 
208
212
  Subsequent app-code ships are `everystack update` alone — no `sst deploy`, no Lambda redeploy.
209
213
 
214
+ ## External / shared RDS (no app VPC)
215
+
216
+ The minimal config above lets SST create the VPC and the RDS together, and `dbTask` reaches a
217
+ **private** in-VPC database. Some apps are compute-only: VPC-less Lambdas and an **external,
218
+ shared, manually-managed** RDS (often `PubliclyAccessible=true`, in the account's default VPC).
219
+ `dbTask` still works — you just hand it a **bring-your-own VPC object** instead of an `sst.aws.Vpc`.
220
+
221
+ `sst.aws.Cluster` accepts a plain VPC descriptor (raw AWS ids), so you never call
222
+ `sst.aws.Vpc.get()` (which throws on an untagged default VPC). Requires `@everystack/jobs >= 0.4.2`
223
+ for the `publicIp` option.
224
+
225
+ ```typescript
226
+ const adminDatabaseUrl = new sst.Secret('ADMIN_DATABASE_URL');
227
+
228
+ const { dbTask } = await import('@everystack/jobs/infra');
229
+ const { task: dbTaskResource } = dbTask({ sst, aws }, 'DbTask', {
230
+ // BYO-VPC: the RDS's OWN VPC. Put the RDS's security group in `securityGroups` — the Task's SG
231
+ // then self-references the DB on 5432, so it connects PRIVATELY with no `0.0.0.0/0` opening.
232
+ vpc: {
233
+ securityGroups: ['sg-0fca...'], // the RDS's security group (self-reference)
234
+ containerSubnets: ['subnet-a', 'subnet-b'], // the RDS's VPC subnets (public in a default VPC)
235
+ loadBalancerSubnets: ['subnet-a', 'subnet-b'], // required by the type; same subnets are fine
236
+ },
237
+ // The default VPC has no NAT, so give the Task an egress public IP to pull its image via the IGW.
238
+ // It still reaches the RDS privately by SG (above). `publicIp` adds NO inbound security group.
239
+ publicIp: true,
240
+ adminDatabaseUrl,
241
+ pgMajor: 16,
242
+ link: [backups],
243
+ });
244
+ ```
245
+
246
+ Two requirements on the AWS side:
247
+
248
+ - **RDS security group must admit the Task.** The clean path is a **self-reference**: the RDS SG
249
+ allows ingress on 5432 from itself, and the Task uses that same SG. No public-endpoint hop, no
250
+ `0.0.0.0/0`.
251
+ - **`ADMIN_DATABASE_URL`** points at the RDS (`everystack secrets set ADMIN_DATABASE_URL <url>
252
+ --stage <stage>`). It is injected via ECS secrets, never linked (a link would leak the password
253
+ into the task definition).
254
+ - **Deploy IAM (`>= 0.4.3`):** dbTask writes its operator-URL SecureString to
255
+ `/sst/<app>/<stage>/DbTaskOperatorUrl`, under the `parameter/sst/*` prefix an SST deploy role is
256
+ already scoped to. A least-privilege deploy principal also needs ECS (cluster + task-def),
257
+ ECR (auth + repo push), EC2 `Describe*` (subnets/SGs), and `logs:CreateLogGroup` for the task's
258
+ log group. Override the param path with `paramName` to fit a different scoped prefix.
259
+
260
+ **Keep `publicIp`, never `public: true`.** SST marks `publicIp` `@deprecated` in favor of `public`,
261
+ but do NOT swap it here: `public: true` attaches an inbound security group open to `0.0.0.0/0` on
262
+ ALL protocols and demands `vpc.publicSubnets`. `publicIp: true` gives an egress IP with no inbound
263
+ rule — exactly what an outbound-only pg-binary task needs. (One inert side effect either way: when
264
+ the Task's SG is the RDS's own, the Task ENI inherits the RDS SG's inbound rules — often
265
+ `5432 from 0.0.0.0/0` for a public RDS — but nothing in the container listens, so it is unreachable.)
266
+
267
+ Fallback (no BYO-VPC change): stand up a minimal `new sst.aws.Vpc('DbVpc')` (no NAT) and let the
268
+ Task reach the RDS over its **public endpoint** + TLS. This needs the RDS SG to admit the Task's
269
+ *dynamic* Fargate public IP — practically `0.0.0.0/0:5432`, mitigated by TLS + the operator
270
+ credential. Prefer the private SG-self-reference above; use the public endpoint only as an interim.
271
+
210
272
  ## Gotchas
211
273
 
212
274
  - **Nothing serves until the first `everystack update`** — the fallback has no bundle to download yet, so `/` returns 404 while `/api` (plugin-claimed) already works.
package/dist/index.cjs CHANGED
@@ -22411,7 +22411,9 @@ function registerDesignSchemaPrompt(server) {
22411
22411
  "Declare each table with `defineModel`, following these conventions:",
22412
22412
  "- UUID primary keys: `id: field.uuid().primaryKey().defaultRandom()`",
22413
22413
  "- Timestamps: `createdAt: field.timestamptz().defaultNow().notNull()`",
22414
- "- Soft delete: a `deletedAt: field.timestamptz()` field opts the table into soft-delete",
22414
+ "- Soft delete: a `deletedAt: field.timestamptz()` field PLUS `softDelete: true` \u2014 the flag is what",
22415
+ " excludes soft-deleted rows from public reads and the data API. The field alone does nothing:",
22416
+ " visibility is declared, never inferred from a column name",
22415
22417
  "- Foreign keys via relations: `field.uuid().references(() => Author)` / `belongsTo`/`hasMany`",
22416
22418
  "- Sensitive columns: `.private()` (hidden from the API); write-guarded: `.readonly()`",
22417
22419
  "- Named exports (PascalCase model var, e.g. `export const Post = defineModel('posts', \u2026)`)",
@@ -23733,7 +23735,7 @@ async function runGovernanceCli(argv) {
23733
23735
  }
23734
23736
 
23735
23737
  // src/index.ts
23736
- var version2 = (true ? "0.4.2" : null) ?? "0.3.0-dev";
23738
+ var version2 = (true ? "0.4.4" : null) ?? "0.3.0-dev";
23737
23739
  var INSTRUCTIONS = [
23738
23740
  "You govern how any agent builds everystack \u2014 a self-hosted application stack for Expo apps on AWS.",
23739
23741
  "Your job is not only to advise but to keep the build on-script: the architecture the maintainer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Governance layer that governs how any agent builds everystack — grounding, cheat gates, and Model-aware tooling over MCP",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -40,9 +40,9 @@
40
40
  "tsx": "4.21.0",
41
41
  "typescript": "5.9.3",
42
42
  "zod": "3.25.67",
43
- "@everystack/cli": "0.4.32",
44
- "@everystack/model": "0.4.5",
45
- "@everystack/server": "0.4.14"
43
+ "@everystack/model": "0.4.8",
44
+ "@everystack/cli": "0.4.43",
45
+ "@everystack/server": "0.4.15"
46
46
  },
47
47
  "scripts": {
48
48
  "test": "jest",
@@ -41,7 +41,9 @@ export function registerDesignSchemaPrompt(server: McpServer): void {
41
41
  'Declare each table with `defineModel`, following these conventions:',
42
42
  '- UUID primary keys: `id: field.uuid().primaryKey().defaultRandom()`',
43
43
  '- Timestamps: `createdAt: field.timestamptz().defaultNow().notNull()`',
44
- '- Soft delete: a `deletedAt: field.timestamptz()` field opts the table into soft-delete',
44
+ '- Soft delete: a `deletedAt: field.timestamptz()` field PLUS `softDelete: true` — the flag is what',
45
+ ' excludes soft-deleted rows from public reads and the data API. The field alone does nothing:',
46
+ ' visibility is declared, never inferred from a column name',
45
47
  '- Foreign keys via relations: `field.uuid().references(() => Author)` / `belongsTo`/`hasMany`',
46
48
  '- Sensitive columns: `.private()` (hidden from the API); write-guarded: `.readonly()`',
47
49
  '- Named exports (PascalCase model var, e.g. `export const Post = defineModel(\'posts\', …)`)',
@@ -42,6 +42,25 @@ everystack db:fingerprint # Content-address the live base schema vs the Mode
42
42
  everystack db:diff # The state edge between two declared states — no DB, CI-pure
43
43
  ```
44
44
 
45
+ **Authorization (the brownfield on-ramp + the red team)**
46
+ ```bash
47
+ everystack db:authz:pull # Introspect live authz (rls/grants/policies/secdef) → reviewable contract files
48
+ everystack db:authz:diff # Live DB vs the committed contract — the CI gate (exit 1 on drift)
49
+ everystack db:authz:test # Red-team enforcement: SET ROLE + attempt per role/table/command
50
+ everystack db:authz:owner # Red-team owner isolation: two identities per owner-scoped table (catches IDOR)
51
+ everystack db:authz:report # Render the committed contract as a human review — no database
52
+ ```
53
+ All four DB-touching verbs take `--stage <name>` OR a direct venue: `--database-url <url>`,
54
+ or an inherited `ADMIN_DATABASE_URL`/`DATABASE_URL`. Same SQL, same evaluation, same
55
+ verdict — and every run names the venue it judged, because a security verdict against an
56
+ unintended database is worse than no verdict.
57
+
58
+ Use the direct venue to **rehearse an authz migration locally**. Translating hand-written
59
+ RLS into Model abilities is the hardest part of adopting the Model system, and iterating
60
+ against a deployed stage makes the loop minutes long instead of seconds. `db:authz:test`
61
+ and `db:authz:owner` WRITE in order to prove a role cannot write; every probe runs inside
62
+ a transaction that is always rolled back, on either venue.
63
+
45
64
  **Safe remote migration (protected stages)**
46
65
  ```bash
47
66
  everystack db:plan --stage X --out X.plan.json # Mint a reviewable edge — read-only, ephemeral
@@ -195,6 +195,11 @@ from which git commit), and rebuilds dependents automatically in dependency orde
195
195
  Fix the source to match, or rebuild from source with `--overwrite-drift`.
196
196
  - **Grant drift converges without ceremony** — a hand-run GRANT/REVOKE surfaces as an
197
197
  idempotent `regrant` back to the declared abilities. Declared authz is authoritative.
198
+ - **An authz-only descriptor change is a regrant, not a rebuild** — changing only the grants
199
+ on a matview (`private` → `abilities: [can('read')]`, body unchanged) applies as a bare
200
+ GRANT, no recompute. Forward-looking: the first `--apply` after upgrade arms up-to-date
201
+ objects via a record-only `backfill`, so the *first* authz change on a pre-upgrade row still
202
+ rebuilds; later ones are cheap. Column-scoped grants still rebuild (attacl isn't drift-checked).
198
203
  - **Changing a function REFRESHes the matviews that call it** — their rows are stale the
199
204
  moment the function changes, and the reconciler knows it.
200
205
  - **Unmanaged objects are never touched** — live derived objects with no source and no
@@ -174,6 +174,9 @@ export default $config({
174
174
 
175
175
  // The CLI discovers resources through these outputs. The first five are required;
176
176
  // backupsBucket unlocks db:backup / db:swap / db:fork (also auto-discovered by bucket name).
177
+ // databaseInstanceId unlocks the PHYSICAL snapshot path (db:snapshot, and db:swap's default
178
+ // pre-swap rollback point). Without it db:swap falls back to a logical pg_dump against the same
179
+ // instance it is about to restore into, which contends with the restore for locks.
177
180
  return {
178
181
  routerUrl: router.url,
179
182
  apiFunctionName: api.name,
@@ -181,6 +184,7 @@ export default $config({
181
184
  updatesBucket: updates.name,
182
185
  clientBundlesBucket: clientBundles.name,
183
186
  backupsBucket: backups.name,
187
+ databaseInstanceId: database.id,
184
188
  };
185
189
  },
186
190
  });
@@ -207,6 +211,64 @@ everystack db:seed --stage dev # dev only
207
211
 
208
212
  Subsequent app-code ships are `everystack update` alone — no `sst deploy`, no Lambda redeploy.
209
213
 
214
+ ## External / shared RDS (no app VPC)
215
+
216
+ The minimal config above lets SST create the VPC and the RDS together, and `dbTask` reaches a
217
+ **private** in-VPC database. Some apps are compute-only: VPC-less Lambdas and an **external,
218
+ shared, manually-managed** RDS (often `PubliclyAccessible=true`, in the account's default VPC).
219
+ `dbTask` still works — you just hand it a **bring-your-own VPC object** instead of an `sst.aws.Vpc`.
220
+
221
+ `sst.aws.Cluster` accepts a plain VPC descriptor (raw AWS ids), so you never call
222
+ `sst.aws.Vpc.get()` (which throws on an untagged default VPC). Requires `@everystack/jobs >= 0.4.2`
223
+ for the `publicIp` option.
224
+
225
+ ```typescript
226
+ const adminDatabaseUrl = new sst.Secret('ADMIN_DATABASE_URL');
227
+
228
+ const { dbTask } = await import('@everystack/jobs/infra');
229
+ const { task: dbTaskResource } = dbTask({ sst, aws }, 'DbTask', {
230
+ // BYO-VPC: the RDS's OWN VPC. Put the RDS's security group in `securityGroups` — the Task's SG
231
+ // then self-references the DB on 5432, so it connects PRIVATELY with no `0.0.0.0/0` opening.
232
+ vpc: {
233
+ securityGroups: ['sg-0fca...'], // the RDS's security group (self-reference)
234
+ containerSubnets: ['subnet-a', 'subnet-b'], // the RDS's VPC subnets (public in a default VPC)
235
+ loadBalancerSubnets: ['subnet-a', 'subnet-b'], // required by the type; same subnets are fine
236
+ },
237
+ // The default VPC has no NAT, so give the Task an egress public IP to pull its image via the IGW.
238
+ // It still reaches the RDS privately by SG (above). `publicIp` adds NO inbound security group.
239
+ publicIp: true,
240
+ adminDatabaseUrl,
241
+ pgMajor: 16,
242
+ link: [backups],
243
+ });
244
+ ```
245
+
246
+ Two requirements on the AWS side:
247
+
248
+ - **RDS security group must admit the Task.** The clean path is a **self-reference**: the RDS SG
249
+ allows ingress on 5432 from itself, and the Task uses that same SG. No public-endpoint hop, no
250
+ `0.0.0.0/0`.
251
+ - **`ADMIN_DATABASE_URL`** points at the RDS (`everystack secrets set ADMIN_DATABASE_URL <url>
252
+ --stage <stage>`). It is injected via ECS secrets, never linked (a link would leak the password
253
+ into the task definition).
254
+ - **Deploy IAM (`>= 0.4.3`):** dbTask writes its operator-URL SecureString to
255
+ `/sst/<app>/<stage>/DbTaskOperatorUrl`, under the `parameter/sst/*` prefix an SST deploy role is
256
+ already scoped to. A least-privilege deploy principal also needs ECS (cluster + task-def),
257
+ ECR (auth + repo push), EC2 `Describe*` (subnets/SGs), and `logs:CreateLogGroup` for the task's
258
+ log group. Override the param path with `paramName` to fit a different scoped prefix.
259
+
260
+ **Keep `publicIp`, never `public: true`.** SST marks `publicIp` `@deprecated` in favor of `public`,
261
+ but do NOT swap it here: `public: true` attaches an inbound security group open to `0.0.0.0/0` on
262
+ ALL protocols and demands `vpc.publicSubnets`. `publicIp: true` gives an egress IP with no inbound
263
+ rule — exactly what an outbound-only pg-binary task needs. (One inert side effect either way: when
264
+ the Task's SG is the RDS's own, the Task ENI inherits the RDS SG's inbound rules — often
265
+ `5432 from 0.0.0.0/0` for a public RDS — but nothing in the container listens, so it is unreachable.)
266
+
267
+ Fallback (no BYO-VPC change): stand up a minimal `new sst.aws.Vpc('DbVpc')` (no NAT) and let the
268
+ Task reach the RDS over its **public endpoint** + TLS. This needs the RDS SG to admit the Task's
269
+ *dynamic* Fargate public IP — practically `0.0.0.0/0:5432`, mitigated by TLS + the operator
270
+ credential. Prefer the private SG-self-reference above; use the public endpoint only as an interim.
271
+
210
272
  ## Gotchas
211
273
 
212
274
  - **Nothing serves until the first `everystack update`** — the fallback has no bundle to download yet, so `/` returns 404 while `/api` (plugin-claimed) already works.