@open-everywhere/create 0.7.0 → 0.8.0

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.
Files changed (34) hide show
  1. package/README.md +5 -3
  2. package/package.json +3 -3
  3. package/src/steps/build-deploy.ts +2 -2
  4. package/template/.dockerignore +35 -0
  5. package/template/AGENTS.md +7 -0
  6. package/template/Dockerfile +60 -0
  7. package/template/README.md +13 -0
  8. package/template/agent/agent.ts +25 -8
  9. package/template/agent/instructions.md +6 -0
  10. package/template/agent/lib/register-config.ts +5 -1
  11. package/template/agent/memory/config/domains/org.ts +2 -2
  12. package/template/agent/model/models.ts +17 -0
  13. package/template/agent/model/stacks.ts +23 -6
  14. package/template/agent/settings/settings.ts +21 -2
  15. package/template/agent/spend/model-pricing.ts +3 -1
  16. package/template/claude/skills/quickstart-open-everywhere/SKILL.md +96 -9
  17. package/template/claude/skills/upgrade-open-everywhere/SKILL.md +16 -3
  18. package/template/deploy/egress-proxy/Dockerfile +30 -0
  19. package/template/deploy/egress-proxy/control/acl.ts +228 -0
  20. package/template/deploy/egress-proxy/control/handlers.ts +174 -0
  21. package/template/deploy/egress-proxy/control/helper.cjs +108 -0
  22. package/template/deploy/egress-proxy/control/server.ts +122 -0
  23. package/template/deploy/egress-proxy/entrypoint.sh +62 -0
  24. package/template/deploy/egress-proxy/gen-ca.sh +42 -0
  25. package/template/deploy/egress-proxy/install-ca.sh +39 -0
  26. package/template/deploy/egress-proxy/squid.conf +93 -0
  27. package/template/deploy/egress-proxy/tier-b.conf +42 -0
  28. package/template/docker-compose.yml +135 -0
  29. package/template/gitignore +4 -0
  30. package/template/package.json +4 -3
  31. package/template/roster.lock.json +9 -1
  32. package/template/scripts/goldens/roster-plan.txt +8 -0
  33. package/template/scripts/model-config-check.ts +81 -0
  34. package/template/scripts/ops-mcp.ts +1 -1
package/README.md CHANGED
@@ -38,9 +38,11 @@ coding agent and ask it to set up Open Everywhere. The agent reads the app's `AG
38
38
  route first: **Vercel** (the `vercel` CLI plus Marketplace Neon) or **Docker** (a self-hosted
39
39
  container that satisfies the
40
40
  [deployment contract](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md)).
41
- Vercel is the one route that the project runs end to end. The Docker route is partly proven, and a
42
- scaffolded app carries no `Dockerfile`: see the
43
- [deployment contract](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md).
41
+ Vercel is the one route that the project runs end to end. The Docker route is partly proven: a
42
+ scaffolded app carries its `Dockerfile` and a `docker-compose.yml` with the egress proxy and Postgres,
43
+ and the
44
+ [deployment contract](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md)
45
+ says which requirements that stack meets.
44
46
 
45
47
  ## Use
46
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-everywhere/create",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "npx scaffold and setup check for Open Everywhere — one step engine, reported by --check for an agent-led setup.",
@@ -46,8 +46,8 @@
46
46
  "test:env-pipe": "tsx scripts/env-pipe-test.ts"
47
47
  },
48
48
  "dependencies": {
49
- "@open-everywhere/core": "0.7.0",
50
- "@open-everywhere/provider-slack": "0.7.0",
49
+ "@open-everywhere/core": "0.8.0",
50
+ "@open-everywhere/provider-slack": "0.8.0",
51
51
  "tsx": "^4.23.9"
52
52
  },
53
53
  "devDependencies": {
@@ -39,7 +39,7 @@ function deployOrigin(ctx: StepContext): string | undefined {
39
39
  /** The command that deploys, per route. */
40
40
  function deployCommand(ctx: StepContext): string {
41
41
  return routeOf(ctx) === "docker"
42
- ? "build the image and run it on your server (the deployment contract states what it must satisfy)"
42
+ ? "`docker compose up --build` on your server (the app's Dockerfile and docker-compose.yml; the deployment contract states what it must satisfy)"
43
43
  : "npx vercel deploy --prod";
44
44
  }
45
45
 
@@ -69,7 +69,7 @@ export const buildDeployStep: SetupStep = {
69
69
  "`npm run build` (= `oe build`, then `eve build`) — must be green.",
70
70
  vercel
71
71
  ? "Deploy is a SEPARATE, explicit step: `npx vercel deploy --prod` (confirm the scope and team first). A `git push` does NOT deploy."
72
- : "Deploy is a SEPARATE, explicit step: build the image and run it on your server, with the contract's environment set. See https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md.",
72
+ : "Deploy is a SEPARATE, explicit step: on your server, put the contract's environment in `.env` (with `OE_EGRESS_PROXY_TOKEN`), then run `docker compose up --build`. The app's `Dockerfile` builds the image, and `docker-compose.yml` puts it behind the egress proxy with Postgres. This stack runs the `justbash` sandbox backend: the agent's shell shares the app process's filesystem and environment. That is development grade, and it does not meet the contract's `sandbox-backend` requirement. See https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md.",
73
73
  "Then re-run `npx @open-everywhere/create --check --host https://<host>`: it probes the deployed Slack route.",
74
74
  "The probe passes on the route's own `401 unauthorized`, which proves the function booted and the route is mounted. It does NOT prove the deployment holds SLACK_SIGNING_SECRET: the route answers the same without it. Only Event Subscriptions showing **Verified** in the Slack console proves that.",
75
75
  ],
@@ -0,0 +1,35 @@
1
+ # The build context of this app's image (Dockerfile, docker-compose.yml).
2
+ #
3
+ # node_modules stays out: the image installs its own (musl) dependencies with `npm ci`. A host copy
4
+ # would put glibc native modules over them and break the runtime image.
5
+ node_modules
6
+ **/node_modules
7
+
8
+ # Version control and agent or editor state.
9
+ .git
10
+ .claude
11
+ .vscode
12
+ .idea
13
+
14
+ # Build output: the image runs its own `oe build` and `eve build`.
15
+ .vercel
16
+ .output
17
+ .nitro
18
+ .eve
19
+
20
+ # Local secrets and env values. An image layer must never carry one.
21
+ .env
22
+ .env.*
23
+ *.pem
24
+ *.key
25
+
26
+ # Local state from `npm run dev`, and the egress proxy's own build context.
27
+ .open-everywhere-data
28
+ .open-everywhere-memory
29
+ .workflow-data
30
+ deploy
31
+
32
+ # Logs and OS files.
33
+ *.log
34
+ npm-debug.log*
35
+ .DS_Store
@@ -65,6 +65,13 @@ Setup is yours to drive, with the user's consent at every command. Use the
65
65
  - `npx oe build --check` verifies the slot tree without a write, and proves the overlays hold
66
66
  only data. Run it before you merge a pull request that changes an overlay.
67
67
  - `npm run typecheck` type-checks the app.
68
+ - `npm run check:model-config` reads the list of endpoints for each model from the model's target
69
+ (the AI Gateway, unless `models.targets` names another URL). It names a demand that no provider
70
+ can meet, two regions that conflict, a model id the gateway does not know, and a `via` that
71
+ names no target. Run it after a change to the model settings, before the deploy. It needs the
72
+ network and no credential.
73
+ - `npx oe sovereignty --host vercel` (or `--host docker`) prints where each piece of the app's data
74
+ lives, which region, and which company could be compelled to hand it over. It changes nothing.
68
75
  - Deploy is a separate, explicit step. On the Vercel route that is `npx vercel deploy --prod`; a
69
76
  `git push` does NOT deploy.
70
77
 
@@ -0,0 +1,60 @@
1
+ # The Docker route's image for this app: the eve node-server build, run with `eve start`.
2
+ #
3
+ # Use it with docker-compose.yml, which puts the app behind the egress proxy (deploy/egress-proxy)
4
+ # and gives it Postgres. The requirements this image satisfies, and the ones it does not, are in the
5
+ # deployment contract:
6
+ # https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md
7
+ #
8
+ # Run `npm install` once before you build: the build installs from package-lock.json with `npm ci`.
9
+ FROM node:24-alpine AS build
10
+ WORKDIR /app
11
+
12
+ # eve collects its own CLI usage telemetry, ON by default. Each `eve` command posts the command name,
13
+ # the eve version, the platform, a random installation id and a salted hash of the git remote URL or
14
+ # of the working directory to telemetry.vercel.com. This image turns it OFF:
15
+ # `EVE_TELEMETRY_DISABLED=1` is eve's switch, and both stages set it. Open Everywhere's own telemetry
16
+ # has its own switch, `OE_TELEMETRY` (see README.md § Telemetry).
17
+ ENV EVE_TELEMETRY_DISABLED=1
18
+
19
+ # git: a dependency or the `git` memory backing can need it at build time.
20
+ RUN apk add --no-cache git
21
+
22
+ # Install first, from the lockfile, so a source edit does not repeat the install.
23
+ # The app's `postinstall` (`oe-eve-patch`) puts the Open Everywhere patch into eve, and it exits
24
+ # non-zero when the patch does not apply. npm runs a dependency's install script only when
25
+ # package.json's `allowScripts` names it, so `node-liblzma` does not compile here (just-bash then
26
+ # has no `xz` command). The probe then proves the patch is in the eve this image runs: core refuses
27
+ # to boot on an unpatched eve, so a build that ends without it gives a dead container.
28
+ COPY package.json package-lock.json ./
29
+ RUN npm ci \
30
+ && node --input-type=module -e 'const { probeEvePatch } = await import("@open-everywhere/core/agent/access/eve-patch-probe.js"); const r = await probeEvePatch(); if (r.status !== "patched") { console.error(`eve patch probe: ${r.status}`); process.exit(1); }'
31
+
32
+ # Build. VERCEL is unset here, so `eve build` writes the node-server bundle (.output/server).
33
+ # `npm run build` runs `oe build` first (the prebuild hook), then `eve build`.
34
+ # 🔴 eve provisions the sandbox template at BUILD time, on the backend the build resolves. This stage
35
+ # has no Docker daemon, so the app must declare `sandbox: { selfHostBackend: "justbash" }` in
36
+ # agent/settings/settings.ts (the template does). With `"docker"` the build fails with
37
+ # `spawn docker ENOENT`.
38
+ # The builder needs memory: `eve build` can take several GB, so the heap limit is raised.
39
+ COPY . .
40
+ RUN NODE_OPTIONS=--max-old-space-size=6144 npm run build
41
+
42
+ # ── Runtime image ──
43
+ FROM node:24-alpine AS runtime
44
+ WORKDIR /app
45
+ ENV NODE_ENV=production
46
+ # eve's CLI telemetry stays OFF for `eve start` too (see the build stage).
47
+ ENV EVE_TELEMETRY_DISABLED=1
48
+ # git and openssh-client for real git operations; ca-certificates for TLS egress.
49
+ RUN apk add --no-cache git openssh-client ca-certificates
50
+
51
+ # The whole tree: the build stage produced node_modules and .output.
52
+ COPY --from=build /app /app
53
+
54
+ # VERCEL stays UNSET, so the sandbox backend and the egress setup take the self-host branch.
55
+ ENV PORT=3000
56
+ EXPOSE 3000
57
+
58
+ # `eve start` serves the node-server build and runs the in-process routine dispatcher.
59
+ # It is the binary itself, not `npm start`: the `prestart` hook would run `oe build` again.
60
+ CMD ["npx", "eve", "start", "--host", "0.0.0.0"]
@@ -16,6 +16,11 @@ route first:
16
16
  Gateway with OIDC, so it needs no gateway key.
17
17
  - **Docker** — self-host a container that satisfies the
18
18
  [deployment contract](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md).
19
+ This app carries the image (`Dockerfile`) and the stack (`docker-compose.yml`: the app behind the
20
+ egress proxy in `deploy/egress-proxy/`, with Postgres). Run `npm install` once, then
21
+ `docker compose up --build` on the server. This stack runs the `justbash` sandbox backend: the
22
+ agent's shell shares the app process's filesystem and environment. That is development grade, and
23
+ it does not meet the contract's `sandbox-backend` requirement.
19
24
 
20
25
  These are the two supported routes. Some steps stay with you: creating the Slack app, approving
21
26
  each deploy, and pasting each secret where the agent's tools can read it but the chat cannot.
@@ -42,6 +47,12 @@ never ships. Every event and attribute is listed in the
42
47
  [telemetry disclosure](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/telemetry.md).
43
48
  The `@open-everywhere/create` CLI sends none.
44
49
 
50
+ eve, the framework under the app, has its own CLI telemetry, ON by default in eve. This app turns it
51
+ off: the `dev`, `build` and `start` scripts and both stages of the `Dockerfile` set
52
+ `EVE_TELEMETRY_DISABLED=1`. Keep that prefix on any script you add that runs `eve`. The `dev`,
53
+ `build` and `start` scripts use the POSIX form `NAME=value command`. On Windows, use WSL, or set
54
+ npm's `script-shell` to a POSIX shell such as Git Bash.
55
+
45
56
  ## Develop locally
46
57
 
47
58
  After the deploy, `npm run dev` runs the agent on this machine and talks to it in a terminal UI.
@@ -67,6 +78,8 @@ without a public tunnel.
67
78
  | `agent/webhooks/hooks.ts` | Inbound webhook triggers. |
68
79
  | `AGENTS.md`, `CLAUDE.md` | The guide a coding agent reads in this repository. |
69
80
  | `.mcp.json`, `scripts/ops-mcp.ts` | The `open-everywhere-ops` MCP server: setup status, `.env.local` values and stored secrets, for a coding agent. |
81
+ | `Dockerfile`, `.dockerignore`, `docker-compose.yml` | The Docker route: the app's image, and the stack that runs it behind the egress proxy with Postgres. |
82
+ | `deploy/egress-proxy/` | The egress proxy's image: the only container on the Docker route with a route to the internet. It is a copy of the Open Everywhere repository's `deploy/egress-proxy`. |
70
83
  | `scripts/env-pipe.mjs` | `npm run -s env:pipe`: writes one `.env.local` value to a pipe for `vercel env add`. It refuses a terminal or a file. Never run it bare. |
71
84
  | `.claude/skills/quickstart-open-everywhere/` | The setup skill that drives that server. |
72
85
 
@@ -9,8 +9,9 @@
9
9
  // app. The model per ROLE lives in agent/model/stacks.ts; pick a stack with `models.stack` in
10
10
  // agent/settings/settings.ts.
11
11
  //
12
- // Zero data retention and provider allowlists are GATEWAY org policy (AI Gateway → Settings), not
13
- // app code. This app inherits them.
12
+ // What every model call demands of its provider (zero retention, no training, a region) is
13
+ // `models.dataPolicy` in agent/settings/settings.ts. The model adapter puts it on every call. The
14
+ // gateway's own team settings (AI Gateway → Settings) apply on top.
14
15
 
15
16
  import { defineAgent } from "eve";
16
17
  import { defineDynamic } from "eve/tools";
@@ -25,9 +26,13 @@ import { registerGatewayDreamJudge, registerGatewayFailureFacetJudge } from "@op
25
26
  import { registerGatewayMeetingModels } from "@open-everywhere/adapters/agent/model/meeting-gateway.js";
26
27
  import { registerGatewayApprovalJudge } from "@open-everywhere/adapters/agent/model/approval-judge.js";
27
28
  import { registerGatewayImageDescriber } from "@open-everywhere/adapters/agent/model/gateway-vision.js";
29
+ import { registerModelConfigCheck } from "@open-everywhere/adapters/agent/model/gateway-probe.js";
30
+ import { getConfig } from "@open-everywhere/core/agent/access/config.js";
31
+ import { resolvableModels } from "@open-everywhere/core/agent/model/targets.js";
28
32
  import { isModelUsable } from "@open-everywhere/core/agent/model/policy.js";
29
33
  import {
30
34
  JUDGE_MODEL,
35
+ MODELS,
31
36
  PRIMARY_MODEL,
32
37
  PRIMARY_MODEL_CONTEXT_WINDOW_TOKENS,
33
38
  TRANSCRIPTION_ROLE,
@@ -52,14 +57,14 @@ const COMPACTION_THRESHOLD_PERCENT = getSettings().agent.compactionThresholdPerc
52
57
  const COMPACTION_THRESHOLD_TOKENS = compactionThresholdTokens(PRIMARY_MODEL_CONTEXT_WINDOW_TOKENS, COMPACTION_THRESHOLD_PERCENT);
53
58
 
54
59
  // The background judges: memory distillation, failure facets and the auto-approval judge.
55
- registerGatewayDreamJudge({ id: UTILITY_MODEL, gateway: GATEWAY_DEFAULTS });
56
- registerGatewayFailureFacetJudge({ id: UTILITY_MODEL, gateway: GATEWAY_DEFAULTS });
60
+ registerGatewayDreamJudge({ id: UTILITY_MODEL, via: MODELS.utility.via, gateway: GATEWAY_DEFAULTS });
61
+ registerGatewayFailureFacetJudge({ id: UTILITY_MODEL, via: MODELS.utility.via, gateway: GATEWAY_DEFAULTS });
57
62
 
58
63
  // The meeting bot's model calls (OPE-2459): the `gateway` transcription route, and the tool-less recap
59
64
  // completion on the same `utility` route as the dream judge. Inert until agent/meetings/policy.ts enables
60
65
  // meetings; without this line the `gateway` route is unregistered and `meeting_join` refuses before its card.
61
- registerGatewayMeetingModels({ id: UTILITY_MODEL, gateway: GATEWAY_DEFAULTS }, TRANSCRIPTION_ROLE);
62
- registerGatewayApprovalJudge({ id: JUDGE_MODEL, gateway: GATEWAY_DEFAULTS });
66
+ registerGatewayMeetingModels({ id: UTILITY_MODEL, via: MODELS.utility.via, gateway: GATEWAY_DEFAULTS }, TRANSCRIPTION_ROLE);
67
+ registerGatewayApprovalJudge({ id: JUDGE_MODEL, via: MODELS.judge.via, gateway: GATEWAY_DEFAULTS });
63
68
 
64
69
  // The image describer. Fail at boot if the org model policy forbids the vision model, rather than
65
70
  // fail every image later.
@@ -69,7 +74,19 @@ if (!isModelUsable(VISION_MODEL, loadModelPolicy())) {
69
74
  "check agent/model/policy.ts, or re-point the `vision` role in agent/model/stacks.ts.",
70
75
  );
71
76
  }
72
- registerGatewayImageDescriber({ id: VISION_MODEL });
77
+ registerGatewayImageDescriber({ id: VISION_MODEL, via: MODELS.vision.via });
78
+
79
+ // THE LIVE MODEL CONFIG CHECK (OPE-2556): the deployment status probe (`model-route`) reads the gateway's public
80
+ // endpoints of every model this deployment can resolve, and names a `models.dataPolicy` or `providers.only` demand no
81
+ // provider can meet. `create --check --host` and the setup skill read that row. Read lazily, at status time.
82
+ registerModelConfigCheck(() =>
83
+ resolvableModels(
84
+ [...Object.values(MODELS), ...[TRANSCRIPTION_ROLE.primary, TRANSCRIPTION_ROLE.fallback].flatMap((r) => (r?.via === "gateway" ? [{ id: r.id }] : []))],
85
+ getConfig().loadDomains(),
86
+ getConfig().loadRoles?.() ?? [],
87
+ getSettings().models.force,
88
+ ),
89
+ );
73
90
 
74
91
  function readAppVersion(): string {
75
92
  const version = appPackageJson.version;
@@ -120,7 +137,7 @@ export default defineAgent({
120
137
  model: defineDynamic({
121
138
  events: {
122
139
  "step.started": (() => {
123
- const resolve = gatewayModelResolver({ id: PRIMARY_MODEL, gateway: GATEWAY_DEFAULTS });
140
+ const resolve = gatewayModelResolver({ id: PRIMARY_MODEL, via: MODELS.primary.via, gateway: GATEWAY_DEFAULTS });
124
141
  return async (event: unknown, ctx: unknown) => primaryModelSelection(await resolve(event, ctx));
125
142
  })(),
126
143
  },
@@ -239,6 +239,12 @@ lookup first: search your memory, THEN reply. Never answer "I don't know" (or gu
239
239
  something you could have stored without checking — a miss because you didn't look is the one
240
240
  recall failure that's entirely on you.
241
241
 
242
+ **A fact this conversation already holds is not a lookup.** If a person said it earlier in
243
+ this conversation, or your own earlier reply said it, answer from that directly, in this
244
+ turn. This rule holds also when you saved the fact to memory: the save does not make the
245
+ conversation a less reliable source. Search memory only for what this conversation does not
246
+ hold.
247
+
242
248
  **To recall a specific fact, grep the whole memory space in one command** — every silo for
243
249
  this channel is materialised under `/workspace/memory/`, so one recursive search finds a fact
244
250
  wherever it's filed, without walking silo by silo:
@@ -11,12 +11,13 @@
11
11
  // before the first resolve.
12
12
 
13
13
  import { registerConfig } from "@open-everywhere/core/agent/access/config.js";
14
- import { registerSettings } from "@open-everywhere/core/agent/settings/settings.js";
14
+ import { getSettings, registerSettings } from "@open-everywhere/core/agent/settings/settings.js";
15
15
  import { registerModelPricing } from "@open-everywhere/core/agent/spend/pricing.js";
16
16
  import { loadModelPricing } from "../spend/model-pricing.js";
17
17
  import { SETTINGS_OVERRIDES } from "../settings/settings.js";
18
18
  import { setRegisteredConnectorCredentials } from "@open-everywhere/core/agent/access/connector-credentials.js";
19
19
  import { assertRoutesWithinPolicy } from "@open-everywhere/core/agent/model/policy.js";
20
+ import { assertRouteTargets } from "@open-everywhere/core/agent/model/targets.js";
20
21
  import { assertDomainCredentialsResolvable } from "@open-everywhere/core/agent/access/domain-credentials.js";
21
22
  import {
22
23
  assertWindowSourcesVouched,
@@ -165,6 +166,9 @@ setRegisteredConnectorCredentials(loadConnectorCredentials());
165
166
  // policy is configured). Domains carry role defaults already (applyRoleDefaults inside
166
167
  // loadDomains); roles are validated too since a role rung can carry its own route.
167
168
  assertRoutesWithinPolicy(loadDomains(), loadRoles(), loadModelPolicy());
169
+ // Every route's `via` names a `models.targets` entry, and no route still carries the 0.7.0 key `providers` (renamed
170
+ // `providerOrder`, OPE-2556). Fail LOUD: a renamed key that is read as absent would drop the order hint silently.
171
+ assertRouteTargets(loadDomains(), loadRoles(), getSettings().models.targets);
168
172
 
169
173
  // Multi-source WINDOW vouch (memory/domain.ts assertWindowSourcesVouched, window-tool-gating-design
170
174
  // §Q5): any standing WINDOW holding more than one source requires every register-gated source to be
@@ -29,7 +29,7 @@
29
29
  // changing the engaged thread sessions.
30
30
 
31
31
  import { defineDomain } from "@open-everywhere/core/agent/memory/domain.js";
32
- import { SUPERVISOR_MODEL } from "../../../model/models.js";
32
+ import { SUPERVISOR_MODEL, SUPERVISOR_VIA } from "../../../model/models.js";
33
33
 
34
34
  export const domain = defineDomain({
35
35
  name: "org",
@@ -37,5 +37,5 @@ export const domain = defineDomain({
37
37
  memory: "org-brain",
38
38
  nameDropWake: true,
39
39
  admins: [],
40
- models: { supervisor: { id: SUPERVISOR_MODEL, reasoning: "low" } },
40
+ models: { supervisor: { id: SUPERVISOR_MODEL, via: SUPERVISOR_VIA, reasoning: "low" } },
41
41
  });
@@ -7,6 +7,8 @@ import {
7
7
  registerDefaultAgentModelId,
8
8
  } from "@open-everywhere/core/agent/model/default-model.js";
9
9
  import { getSettings } from "@open-everywhere/core/agent/settings/settings.js";
10
+ import { viaProblem, type ModelTargets } from "@open-everywhere/core/agent/model/targets.js";
11
+ import { registerBareModelRoutes } from "@open-everywhere/core/agent/model/bare-model-via.js";
10
12
  import { SETTINGS_OVERRIDES } from "../settings/settings.js";
11
13
  import { resolveStack, resolveTranscription, type ModelRole, type ModelRoleSpec, type ModelStack } from "./stacks.js";
12
14
 
@@ -19,6 +21,10 @@ const STACK: ModelStack = resolveStack(STACK_ALIAS, {});
19
21
 
20
22
  // A deploy can retarget the primary (`models.force` / `models.primary` in agent/settings/settings.ts).
21
23
  // Core owns that precedence, so read the primary back from it.
24
+ // Every role's `via` names a `models.targets` entry (OPE-2556). Fail LOUD at config load: an unknown target would
25
+ // otherwise fail every call of that role at run time.
26
+ assertStackVia(STACK, MODEL_SETTINGS.targets);
27
+
22
28
  registerDefaultAgentModelId(STACK.primary.id);
23
29
 
24
30
  export const MODELS = {
@@ -29,6 +35,9 @@ export const MODELS = {
29
35
  judge: STACK.judge,
30
36
  } as const satisfies Record<string, ModelRoleSpec>;
31
37
 
38
+ // A bare model id (a declared subagent's `model`) goes through the target of the role that names it, else the primary's.
39
+ registerBareModelRoutes(Object.values(MODELS));
40
+
32
41
  /** The alias of the stack this process resolved — for `/oe status`. */
33
42
  export const MODEL_STACK = STACK_ALIAS;
34
43
  /** The meeting bot's speech-to-text role: a primary and a fallback route (stacks.ts TRANSCRIPTION). */
@@ -38,6 +47,8 @@ export const PRIMARY_MODEL = MODELS.primary.id;
38
47
  export const UTILITY_MODEL = MODELS.utility.id;
39
48
  export const VISION_MODEL = MODELS.vision.id;
40
49
  export const SUPERVISOR_MODEL = MODELS.supervisor.id;
50
+ /** The supervisor role's `models.targets` entry (OPE-2556): the org domain's supervisor route carries it. */
51
+ export const SUPERVISOR_VIA = MODELS.supervisor.via;
41
52
  export const JUDGE_MODEL = MODELS.judge.id;
42
53
  export const PRIMARY_MODEL_CONTEXT_WINDOW_TOKENS = MODELS.primary.contextWindowTokens;
43
54
 
@@ -62,3 +73,9 @@ export function allowListForStack(stack: ModelStack, extra: readonly ModelPolicy
62
73
  export function modelAllowList(extra: readonly ModelPolicyAllowEntry[] = []): ModelPolicyAllowEntry[] {
63
74
  return allowListForStack(MODELS, extra);
64
75
  }
76
+
77
+ /** Throw when a stack role's `via` names no `models.targets` entry. Exported for tests. */
78
+ export function assertStackVia(stack: ModelStack, targets: ModelTargets): void {
79
+ const bad = (Object.keys(stack) as ModelRole[]).flatMap((role) => viaProblem(stack[role].via, targets, `stack role "${role}"`) ?? []);
80
+ if (bad.length > 0) throw new Error(`open-everywhere: ${bad.join("; ")}`);
81
+ }
@@ -9,7 +9,9 @@
9
9
  // judge — the auto-approval judge.
10
10
  //
11
11
  // Every id is an AI Gateway model id. Add a stack by adding an entry, and give each new id a rate in
12
- // agent/spend/model-pricing.ts, or a USD spend cap cannot see its cost.
12
+ // agent/spend/model-pricing.ts, or a USD spend cap cannot see its cost. A role's `via` names the
13
+ // `models.targets` entry its calls go through: the European stack uses the `mistral` target in
14
+ // agent/settings/settings.ts (only the Mistral provider, zero retention).
13
15
 
14
16
  import type { ModelTier } from "@open-everywhere/core/agent/memory/domain.js";
15
17
  import type { TranscriptionRoleSpec } from "@open-everywhere/adapters/agent/model/transcription-routes.js";
@@ -21,17 +23,19 @@ export interface ModelRoleSpec {
21
23
  readonly tier: ModelTier;
22
24
  /** The model's context window. eve compacts at `agent.compactionThresholdPercent` of it. */
23
25
  readonly contextWindowTokens: number;
26
+ /** The `models.targets` entry this role's calls go through (OPE-2556). Absent: `"gateway"`. An unknown name fails config load. */
27
+ readonly via?: string;
24
28
  }
25
29
 
26
30
  export type ModelStack = Readonly<Record<ModelRole, ModelRoleSpec>>;
27
31
 
28
32
  export const STACKS = {
29
33
  "chinese-open-weights": {
30
- primary: { id: "deepseek/deepseek-v4-flash-0731", tier: "high", contextWindowTokens: 1_000_000 },
31
- utility: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
34
+ primary: { id: "deepseek/deepseek-v4.1-flash", tier: "high", contextWindowTokens: 1_000_000 },
35
+ utility: { id: "deepseek/deepseek-v4.1-flash", tier: "low", contextWindowTokens: 1_000_000 },
32
36
  vision: { id: "alibaba/qwen3-vl-instruct", tier: "low", contextWindowTokens: 131_072 },
33
- supervisor: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
34
- judge: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
37
+ supervisor: { id: "deepseek/deepseek-v4.1-flash", tier: "low", contextWindowTokens: 1_000_000 },
38
+ judge: { id: "deepseek/deepseek-v4.1-flash", tier: "low", contextWindowTokens: 1_000_000 },
35
39
  },
36
40
  "american-closed": {
37
41
  primary: { id: "openai/gpt-6-luna", tier: "high", contextWindowTokens: 1_050_000 },
@@ -40,6 +44,14 @@ export const STACKS = {
40
44
  supervisor: { id: "openai/gpt-6-luna", tier: "low", contextWindowTokens: 1_050_000 },
41
45
  judge: { id: "openai/gpt-6-luna", tier: "low", contextWindowTokens: 1_050_000 },
42
46
  },
47
+ // Mistral Small 4 (Apache 2.0) in every role, vision included, served by Mistral with zero retention.
48
+ "european-open-weights": {
49
+ primary: { id: "mistral/mistral-small", tier: "high", contextWindowTokens: 262_144, via: "mistral" },
50
+ utility: { id: "mistral/mistral-small", tier: "low", contextWindowTokens: 262_144, via: "mistral" },
51
+ vision: { id: "mistral/mistral-small", tier: "low", contextWindowTokens: 262_144, via: "mistral" },
52
+ supervisor: { id: "mistral/mistral-small", tier: "low", contextWindowTokens: 262_144, via: "mistral" },
53
+ judge: { id: "mistral/mistral-small", tier: "low", contextWindowTokens: 262_144, via: "mistral" },
54
+ },
43
55
  } as const satisfies Record<string, ModelStack>;
44
56
 
45
57
  export type StackAlias = keyof typeof STACKS;
@@ -78,7 +90,7 @@ export function resolveStack(alias: string, overrides: StackOverrides = {}): Mod
78
90
  // Speech to text for the meeting bot: a primary and a fallback route per stack, on two different providers, so
79
91
  // one provider outage falls through to the other. A route names its transport too: the AI Gateway, DeepInfra
80
92
  // (key: `oe secret set DEEPINFRA_API_KEY`), or your own OpenAI-compatible speech server. These defaults are
81
- // pending a bake-off (`npm run bakeoff:stt` in the Open Everywhere repository).
93
+ // pending a bake-off (the `bakeoff:stt` script of the Open Everywhere repository).
82
94
  export const TRANSCRIPTION = {
83
95
  "chinese-open-weights": {
84
96
  primary: { via: "deepinfra", id: "Qwen/Qwen3-ASR-1.7B" },
@@ -88,6 +100,11 @@ export const TRANSCRIPTION = {
88
100
  primary: { via: "gateway", id: "microsoft/mai-transcribe-2" },
89
101
  fallback: { via: "gateway", id: "spacexai/grok-stt" },
90
102
  },
103
+ // Mistral's Voxtral on your own speech server, then Qwen3-ASR on a second one: audio stays on your machines.
104
+ "european-open-weights": {
105
+ primary: { via: "self-hosted", id: "mistralai/Voxtral-Mini-3B-2507", baseUrl: "http://voxtral:8000/v1" },
106
+ fallback: { via: "self-hosted", id: "Qwen/Qwen3-ASR-1.7B", baseUrl: "http://qwen3-asr:8000/v1" },
107
+ },
91
108
  } as const satisfies Record<StackAlias, TranscriptionRoleSpec>;
92
109
 
93
110
  /** Fully local: your own speech server only. For a Docker deployment with no cloud model. */
@@ -9,8 +9,11 @@
9
9
  //
10
10
  // Examples:
11
11
  // identity: { name: "Ada" }, // the name the agent answers to
12
- // models: { stack: "chinese-open-weights" }, // every model role at once
13
- // sandbox: { selfHostBackend: "justbash" }, // the backend a Docker-image build must declare
12
+ // models: { stack: "chinese-open-weights" }, // every model role at once (or "european-open-weights")
13
+ // models: { dataPolicy: { zeroRetention: true } }, // every model call: only zero-retention providers
14
+ // models: { targets: { gateway: { kind: "vercel-ai-gateway", providers: { only: ["openai", "azure"] } } } }, // only these providers
15
+ // After a change to `models`, run `npm run check:model-config`: it names a model no provider can serve.
16
+ // sandbox: { selfHostBackend: "docker" }, // a per-session container; needs a Docker daemon at build time
14
17
  //
15
18
  // Every behaviour choice that used to be an env var lives here too (OPE-2411): the model pins
16
19
  // (`models.*`), the admin control plane (`control.*`), the directory sync (`directory.*`), AWS
@@ -31,4 +34,20 @@ export const SETTINGS_OVERRIDES = defineSettings({
31
34
  // refuses: no browser tool mounts, no host opens, and the boot logs one line that names the
32
35
  // platform. Set `enabled: false` to turn the browser off, and to silence that line off Vercel.
33
36
  browser: { enabled: true },
37
+ // The sandbox backend off Vercel. eve provisions the sandbox template at BUILD time, on this
38
+ // backend, so it decides where `npm run build` works: `justbash` needs nothing, and `docker` needs
39
+ // a running Docker daemon at build time. The image (Dockerfile) builds with no daemon, so the
40
+ // Docker route needs `justbash`, and so does a local build on a machine without Docker.
41
+ // justbash shares the app process's filesystem: development-grade isolation. On Vercel this line
42
+ // has no effect: the Vercel Sandbox always runs there.
43
+ sandbox: { selfHostBackend: "justbash" },
44
+ // Where model calls go. `gateway` is the shipped target. `mistral` is the European stack's route
45
+ // (`european-open-weights` in agent/model/stacks.ts): only the `mistral` provider, and every call
46
+ // demands zero retention. A role with no `via` uses `gateway`, so this entry changes no other call.
47
+ models: {
48
+ targets: {
49
+ gateway: { kind: "vercel-ai-gateway" },
50
+ mistral: { kind: "vercel-ai-gateway", providers: { only: ["mistral"] }, dataPolicy: { zeroRetention: true } },
51
+ },
52
+ },
34
53
  });
@@ -11,11 +11,13 @@ import { defineModelPricing, assertModelPricingValid, type ModelPricingTable } f
11
11
 
12
12
  const pricing: ModelPricingTable = defineModelPricing({
13
13
  // chinese-open-weights: primary, utility, supervisor and judge.
14
- "deepseek/deepseek-v4-flash-0731": { inPerMtok: 0.14, outPerMtok: 0.28 },
14
+ "deepseek/deepseek-v4.1-flash": { inPerMtok: 0.3, outPerMtok: 1.2 },
15
15
  // chinese-open-weights: vision.
16
16
  "alibaba/qwen3-vl-instruct": { inPerMtok: 0.2, outPerMtok: 0.88 },
17
17
  // american-closed: vision.
18
18
  "google/gemini-3.8-flash": { inPerMtok: 0.75, outPerMtok: 3.75 },
19
+ // european-open-weights: every role.
20
+ "mistral/mistral-small": { inPerMtok: 0.15, outPerMtok: 0.6 },
19
21
  });
20
22
 
21
23
  assertModelPricingValid(pricing);