@everystack/mcp 0.4.5 → 0.4.6

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/index.cjs CHANGED
@@ -23521,11 +23521,37 @@ var secretInPublicEnv = {
23521
23521
  }
23522
23522
  };
23523
23523
 
23524
+ // src/gates/detectors/stage-name-as-trust-boundary.ts
23525
+ var STAGE_COMPARISON = /\b(?:ENVIRONMENT|environment|stage)\s*[=!]==\s*['"](?:dev|development|prod|production|staging|local)['"]/;
23526
+ var SECURITY_CONTEXT = /\b(?:details|error|stack|message|throw|drop|reset|seed|truncate|delete|secret|token|password|credential|auth|admin|bypass|skip|allow|disable|unsafe|insecure)/i;
23527
+ var stageNameAsTrustBoundary = {
23528
+ id: "stage-name-as-trust-boundary",
23529
+ tier: "framework",
23530
+ severity: "deny",
23531
+ guide: "A stage name is not a trust boundary \u2014 ENVIRONMENT comes from $app.stage, so this asks what someone named the stage, not whether the caller is trusted. Gate on an explicit option that defaults to the safe answer, and never return error text to a caller: return the requestId instead.",
23532
+ conform: "an explicit opt-in on the handler/plugin options (see config.allowDevelopment in @everystack/security apple-attest); for error responses, return { error, requestId } and debug with everystack logs:query --traceId <id>",
23533
+ verify: "pnpm --filter @everystack/server test \u2014 __tests__/error-response.test.ts fails on any stage-name gate",
23534
+ async detect(ctx) {
23535
+ if (ctx.tool !== "Write" && ctx.tool !== "Edit") return null;
23536
+ const text = ctx.content;
23537
+ if (!text || !/ENVIRONMENT|environment|stage/.test(text)) return null;
23538
+ const lines = text.split("\n");
23539
+ for (let i = 0; i < lines.length; i++) {
23540
+ if (!STAGE_COMPARISON.test(lines[i])) continue;
23541
+ const window = lines.slice(i, i + 3).join(" ");
23542
+ if (!SECURITY_CONTEXT.test(window)) continue;
23543
+ return `line ${i + 1} gates behaviour on the stage name (${lines[i].trim().slice(0, 80)}) \u2014 ENVIRONMENT comes from $app.stage, so this is a deployment label, not a trust decision`;
23544
+ }
23545
+ return null;
23546
+ }
23547
+ };
23548
+
23524
23549
  // src/gates/registry.ts
23525
23550
  var FRAMEWORK_GATES = [
23526
23551
  handWrittenMigration,
23527
23552
  embeddedDataBundle,
23528
- secretInPublicEnv
23553
+ secretInPublicEnv,
23554
+ stageNameAsTrustBoundary
23529
23555
  ];
23530
23556
  function gatesFor(_cwd) {
23531
23557
  return FRAMEWORK_GATES;
@@ -23735,7 +23761,7 @@ async function runGovernanceCli(argv) {
23735
23761
  }
23736
23762
 
23737
23763
  // src/index.ts
23738
- var version2 = (true ? "0.4.5" : null) ?? "0.3.0-dev";
23764
+ var version2 = (true ? "0.4.6" : null) ?? "0.3.0-dev";
23739
23765
  var INSTRUCTIONS = [
23740
23766
  "You govern how any agent builds everystack \u2014 a self-hosted application stack for Expo apps on AWS.",
23741
23767
  "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.5",
3
+ "version": "0.4.6",
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.48",
44
- "@everystack/server": "0.4.18",
45
- "@everystack/model": "0.4.12"
43
+ "@everystack/cli": "0.4.49",
44
+ "@everystack/model": "0.4.13",
45
+ "@everystack/server": "0.4.19"
46
46
  },
47
47
  "scripts": {
48
48
  "test": "jest",
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Cheat: a stage name used as a trust boundary.
3
+ *
4
+ * `ENVIRONMENT` is set from `$app.stage` by the SST helper, so `ENVIRONMENT === 'dev'` does not
5
+ * ask "is this caller trusted" — it asks "what did someone call this stage". The two questions
6
+ * look identical in a diff and diverge the moment a stage named `dev` is publicly reachable.
7
+ *
8
+ * This shipped in the framework and reached two consumers, neither of whom chose anything but a
9
+ * stage name: a 500 returned raw PostgreSQL text — relation and column names — to unauthenticated
10
+ * callers. It survived its first fix because the condition had been COPIED into a second file and
11
+ * only one was found, which is why this is a gate and not a note in a review checklist.
12
+ *
13
+ * The correct shape is next door in `@everystack/security`: apple-attest reads the environment
14
+ * from the attestation CERTIFICATE (a fact about the client, not a deployment label) and still
15
+ * requires an explicit `config.allowDevelopment` to relax. Derive the fact from something
16
+ * trustworthy, then require an opt-in that fails closed.
17
+ */
18
+
19
+ import type { CheatGate, ToolCallContext } from '../types.js';
20
+
21
+ /**
22
+ * A stage/environment label compared against a deployment name. Covers the spellings that have
23
+ * actually appeared: `process.env.ENVIRONMENT === 'dev'`, `ctx.environment !== 'dev'`, and the
24
+ * inverted `!== 'production'` form that reads as "anywhere but prod".
25
+ */
26
+ const STAGE_COMPARISON =
27
+ /\b(?:ENVIRONMENT|environment|stage)\s*[=!]==\s*['"](?:dev|development|prod|production|staging|local)['"]/;
28
+
29
+ /**
30
+ * Security-relevant company on the same line or nearby: what makes a stage comparison a GATE
31
+ * rather than a label choice. Picking which docs string to print by stage is fine; deciding what
32
+ * a caller may see, or whether a destructive verb runs, is not.
33
+ */
34
+ // No trailing \b: these appear camelCased far more often than bare — `skipAuth`, `allowInsecure`,
35
+ // `disableRls`, `adminOnly`. Requiring a word boundary on the right made the detector miss
36
+ // `if (ENVIRONMENT !== 'production') { skipAuth = true }`, which is the exact shape it exists for.
37
+ const SECURITY_CONTEXT =
38
+ /\b(?:details|error|stack|message|throw|drop|reset|seed|truncate|delete|secret|token|password|credential|auth|admin|bypass|skip|allow|disable|unsafe|insecure)/i;
39
+
40
+ export const stageNameAsTrustBoundary: CheatGate = {
41
+ id: 'stage-name-as-trust-boundary',
42
+ tier: 'framework',
43
+ severity: 'deny',
44
+ guide:
45
+ 'A stage name is not a trust boundary — ENVIRONMENT comes from $app.stage, so this asks what someone named the stage, not whether the caller is trusted. Gate on an explicit option that defaults to the safe answer, and never return error text to a caller: return the requestId instead.',
46
+ conform:
47
+ 'an explicit opt-in on the handler/plugin options (see config.allowDevelopment in @everystack/security apple-attest); for error responses, return { error, requestId } and debug with everystack logs:query --traceId <id>',
48
+ verify:
49
+ 'pnpm --filter @everystack/server test — __tests__/error-response.test.ts fails on any stage-name gate',
50
+
51
+ async detect(ctx: ToolCallContext): Promise<string | null> {
52
+ if (ctx.tool !== 'Write' && ctx.tool !== 'Edit') return null;
53
+ const text = ctx.content;
54
+ // Cheap pre-filter: only content naming a stage label pays for the line scan.
55
+ if (!text || !/ENVIRONMENT|environment|stage/.test(text)) return null;
56
+
57
+ const lines = text.split('\n');
58
+ for (let i = 0; i < lines.length; i++) {
59
+ if (!STAGE_COMPARISON.test(lines[i])) continue;
60
+
61
+ // The comparison alone is not the cheat — `channel = ENVIRONMENT` is legitimate. Look at the
62
+ // line and the two after it, which is where the guarded body sits.
63
+ const window = lines.slice(i, i + 3).join(' ');
64
+ if (!SECURITY_CONTEXT.test(window)) continue;
65
+
66
+ return `line ${i + 1} gates behaviour on the stage name (${lines[i].trim().slice(0, 80)}) — ENVIRONMENT comes from $app.stage, so this is a deployment label, not a trust decision`;
67
+ }
68
+ return null;
69
+ },
70
+ };
@@ -11,12 +11,14 @@ import type { CheatGate } from './types.js';
11
11
  import { handWrittenMigration } from './detectors/hand-written-migration.js';
12
12
  import { embeddedDataBundle } from './detectors/embedded-data-bundle.js';
13
13
  import { secretInPublicEnv } from './detectors/secret-in-public-env.js';
14
+ import { stageNameAsTrustBoundary } from './detectors/stage-name-as-trust-boundary.js';
14
15
 
15
16
  /** Framework-tier gates, on by default. */
16
17
  export const FRAMEWORK_GATES: CheatGate[] = [
17
18
  handWrittenMigration,
18
19
  embeddedDataBundle,
19
20
  secretInPublicEnv,
21
+ stageNameAsTrustBoundary,
20
22
  ];
21
23
 
22
24
  /** The gates that apply to a working directory. Project-tier composition is added later. */