@open-everywhere/create 0.1.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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +69 -0
  4. package/bin/create-open-everywhere.js +57 -0
  5. package/package.json +57 -0
  6. package/src/cli.ts +241 -0
  7. package/src/deploy-probe.ts +99 -0
  8. package/src/engine.ts +106 -0
  9. package/src/env-file.ts +53 -0
  10. package/src/scaffold.ts +188 -0
  11. package/src/steps/build-deploy.ts +78 -0
  12. package/src/steps/database.ts +85 -0
  13. package/src/steps/env-checklist.ts +55 -0
  14. package/src/steps/index.ts +49 -0
  15. package/src/steps/prerequisites.ts +50 -0
  16. package/src/steps/repo.ts +69 -0
  17. package/src/steps/roster.ts +111 -0
  18. package/src/steps/slack.ts +74 -0
  19. package/src/steps/vercel.ts +40 -0
  20. package/src/steps/verify.ts +119 -0
  21. package/src/version.ts +20 -0
  22. package/template/.env.example +58 -0
  23. package/template/.mcp.json +8 -0
  24. package/template/AGENTS.md +62 -0
  25. package/template/CLAUDE.md +1 -0
  26. package/template/README.md +87 -0
  27. package/template/agent/access/access-requests.ts +69 -0
  28. package/template/agent/access/browser-hosts.ts +28 -0
  29. package/template/agent/access/bundles.ts +78 -0
  30. package/template/agent/access/connector-credentials.ts +74 -0
  31. package/template/agent/access/credential-bindings.generated.ts +17 -0
  32. package/template/agent/access/credential-bindings.ts +28 -0
  33. package/template/agent/access/grants.generated.ts +18 -0
  34. package/template/agent/access/grants.ts +30 -0
  35. package/template/agent/access/network-access.ts +32 -0
  36. package/template/agent/agent.ts +125 -0
  37. package/template/agent/blob/hosting.ts +42 -0
  38. package/template/agent/identity/authors.ts +79 -0
  39. package/template/agent/identity/groups-overlay.ts +24 -0
  40. package/template/agent/identity/groups.generated.ts +17 -0
  41. package/template/agent/identity/groups.ts +84 -0
  42. package/template/agent/identity/principals.ts +37 -0
  43. package/template/agent/identity/surfaces.ts +48 -0
  44. package/template/agent/instructions/access.ts +11 -0
  45. package/template/agent/instructions.md +295 -0
  46. package/template/agent/instrumentation.ts +6 -0
  47. package/template/agent/lib/memory-grants.ts +52 -0
  48. package/template/agent/lib/register-adapters.ts +286 -0
  49. package/template/agent/lib/register-config.ts +185 -0
  50. package/template/agent/memory/config/agents-overlay.ts +26 -0
  51. package/template/agent/memory/config/agents.generated.ts +21 -0
  52. package/template/agent/memory/config/domains/dm.ts +10 -0
  53. package/template/agent/memory/config/domains/index.ts +125 -0
  54. package/template/agent/memory/config/domains/org.ts +27 -0
  55. package/template/agent/memory/config/domains.ts +165 -0
  56. package/template/agent/model/models.ts +60 -0
  57. package/template/agent/model/policy.ts +19 -0
  58. package/template/agent/model/stacks.ts +75 -0
  59. package/template/agent/plugins/generated/index.ts +13 -0
  60. package/template/agent/plugins/registry.ts +16 -0
  61. package/template/agent/plugins/skill-content.ts +13 -0
  62. package/template/agent/roles/roles.ts +146 -0
  63. package/template/agent/routines/collectors.ts +89 -0
  64. package/template/agent/sandbox/backend-select.ts +16 -0
  65. package/template/agent/sandbox/sandbox.ts +242 -0
  66. package/template/agent/sandbox/workspace/.keep +6 -0
  67. package/template/agent/settings/settings.ts +19 -0
  68. package/template/agent/spend/budgets.ts +25 -0
  69. package/template/agent/spend/checkpoints.ts +51 -0
  70. package/template/agent/spend/model-pricing.ts +25 -0
  71. package/template/agent/webhooks/hooks-overlay.ts +34 -0
  72. package/template/agent/webhooks/hooks.generated.ts +17 -0
  73. package/template/agent/webhooks/hooks.ts +133 -0
  74. package/template/claude/skills/quickstart-open-everywhere/SKILL.md +167 -0
  75. package/template/claude/skills/upgrade-open-everywhere/SKILL.md +160 -0
  76. package/template/gitignore +42 -0
  77. package/template/package.json +62 -0
  78. package/template/roster.config.ts +39 -0
  79. package/template/roster.lock.json +182 -0
  80. package/template/scripts/env-pipe.mjs +74 -0
  81. package/template/scripts/ops-mcp.ts +35 -0
  82. package/template/tsconfig.json +23 -0
@@ -0,0 +1,19 @@
1
+ // The org model policy — the envelope every model route must fit inside. `allow` is the set of
2
+ // model ids a route may select (the active stack's ids by default). `reasoningCeiling` is the org
3
+ // default maximum reasoning effort. A domain may raise or lower it for its own rooms.
4
+ //
5
+ // Config load refuses a domain route outside this envelope (assertRoutesWithinPolicy).
6
+
7
+ import { defineModelPolicy, assertModelPolicyValid, type ModelPolicy } from "@open-everywhere/core/agent/model/policy.js";
8
+ import { modelAllowList } from "./models.js";
9
+
10
+ const policy: ModelPolicy = defineModelPolicy({
11
+ allow: modelAllowList(),
12
+ reasoningCeiling: "medium",
13
+ });
14
+
15
+ assertModelPolicyValid(policy);
16
+
17
+ export function loadModelPolicy(): ModelPolicy | undefined {
18
+ return policy;
19
+ }
@@ -0,0 +1,75 @@
1
+ // Model STACKS — one coherent set of models, one per ROLE. Switching model families is one setting:
2
+ // `models.stack` in agent/settings/settings.ts (or the OE_MODEL_STACK env override).
3
+ //
4
+ // Roles:
5
+ // primary — the turn model.
6
+ // utility — compaction summaries, memory distillation, DMs (agent/memory/config/domains/dm.ts).
7
+ // vision — the image describer.
8
+ // supervisor — the always-on ambient channel supervisor (opt-in per domain).
9
+ // judge — the auto-approval judge.
10
+ //
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.
13
+
14
+ import type { ModelTier } from "@open-everywhere/core/agent/memory/domain.js";
15
+
16
+ export type ModelRole = "primary" | "utility" | "vision" | "supervisor" | "judge";
17
+
18
+ export interface ModelRoleSpec {
19
+ readonly id: string;
20
+ readonly tier: ModelTier;
21
+ /** The model's context window. eve compacts at `agent.compactionThresholdPercent` of it. */
22
+ readonly contextWindowTokens: number;
23
+ }
24
+
25
+ export type ModelStack = Readonly<Record<ModelRole, ModelRoleSpec>>;
26
+
27
+ export const STACKS = {
28
+ "chinese-open-weights": {
29
+ primary: { id: "deepseek/deepseek-v4-flash-0731", tier: "high", contextWindowTokens: 1_000_000 },
30
+ utility: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
31
+ vision: { id: "alibaba/qwen3-vl-instruct", tier: "low", contextWindowTokens: 131_072 },
32
+ supervisor: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
33
+ judge: { id: "deepseek/deepseek-v4-flash-0731", tier: "low", contextWindowTokens: 1_000_000 },
34
+ },
35
+ "american-closed": {
36
+ primary: { id: "openai/gpt-6-luna", tier: "high", contextWindowTokens: 1_050_000 },
37
+ utility: { id: "openai/gpt-6-luna", tier: "low", contextWindowTokens: 1_050_000 },
38
+ vision: { id: "google/gemini-3.7-flash", tier: "low", contextWindowTokens: 1_000_000 },
39
+ supervisor: { id: "openai/gpt-6-luna", tier: "low", contextWindowTokens: 1_050_000 },
40
+ judge: { id: "openai/gpt-6-luna", tier: "low", contextWindowTokens: 1_050_000 },
41
+ },
42
+ } as const satisfies Record<string, ModelStack>;
43
+
44
+ export type StackAlias = keyof typeof STACKS;
45
+
46
+ export const STACK_ALIASES: readonly StackAlias[] = (Object.keys(STACKS) as StackAlias[]).sort();
47
+
48
+ /** The absolute token count eve compacts at, for display. */
49
+ export function compactionThresholdTokens(contextWindowTokens: number, thresholdPercent: number): number {
50
+ return Math.max(1, Math.floor(contextWindowTokens * thresholdPercent));
51
+ }
52
+
53
+ export type StackOverrides = {
54
+ readonly [R in ModelRole]?: Partial<ModelRoleSpec>;
55
+ };
56
+
57
+ export function isStackAlias(alias: string): alias is StackAlias {
58
+ return Object.prototype.hasOwnProperty.call(STACKS, alias);
59
+ }
60
+
61
+ /** The stack for `alias`, with optional per-role overrides. Throws on an unknown alias. */
62
+ export function resolveStack(alias: string, overrides: StackOverrides = {}): ModelStack {
63
+ if (!isStackAlias(alias)) {
64
+ throw new Error(
65
+ `open-everywhere: unknown model stack "${alias}" — the stacks are ${STACK_ALIASES.map((a) => `"${a}"`).join(", ")}. ` +
66
+ `Add one in agent/model/stacks.ts, or set models.stack to one of them.`,
67
+ );
68
+ }
69
+ const base = STACKS[alias];
70
+ const out = {} as { -readonly [R in ModelRole]: ModelRoleSpec };
71
+ for (const role of Object.keys(base) as ModelRole[]) {
72
+ out[role] = { ...base[role], ...overrides[role] };
73
+ }
74
+ return out;
75
+ }
@@ -0,0 +1,13 @@
1
+ // The aggregate of every extracted plugin under agent/plugins/generated/. This app ships no
2
+ // plugin extractor: the Open Everywhere repository's `plugin:extract` script writes this file and
3
+ // one module per plugin beside it. Until you add one, it is empty. Import GENERATED_SKILL_CONTENT /
4
+ // GENERATED_PLUGIN_REGISTRY into skill-content.ts / registry.ts and spread them
5
+ // alongside any hand-authored entries (see those files' headers) — empty (no
6
+ // plugin extracted yet) is a valid, safe baseline: it contributes nothing.
7
+
8
+ import type { SkillPackageDefinition } from "eve/skills";
9
+ import type { PluginRegistry } from "@open-everywhere/core/agent/plugins/types.js";
10
+
11
+ export const GENERATED_SKILL_CONTENT: Record<string, SkillPackageDefinition> = {};
12
+
13
+ export const GENERATED_PLUGIN_REGISTRY: PluginRegistry = {};
@@ -0,0 +1,16 @@
1
+ // The plugin registry — the runtime expansion table for bundle plugin grants.
2
+ //
3
+ // A bundle grants a plugin by NAME (`Bundle.plugins`). This file records, per plugin name, which
4
+ // skills, connectors and subagents it owns, so the resolver can expand `plugins: ["acme"]` into
5
+ // what it unlocks. An empty registry means plugin grants expand to nothing (fail-closed), never an
6
+ // error. Extracted plugins land in agent/plugins/generated/ and spread in below. Hand-authored
7
+ // entries are fine too.
8
+
9
+ import type { PluginRegistry } from "@open-everywhere/core/agent/plugins/types.js";
10
+ import { GENERATED_PLUGIN_REGISTRY } from "./generated/index.js";
11
+
12
+ export type { PluginRegistry } from "@open-everywhere/core/agent/plugins/types.js";
13
+
14
+ export const PLUGIN_REGISTRY: PluginRegistry = {
15
+ ...GENERATED_PLUGIN_REGISTRY,
16
+ };
@@ -0,0 +1,13 @@
1
+ // Skill content registry — the package bodies the scoped skills resolver serves.
2
+ //
3
+ // Skills are deliberately NOT static files under agent/skills/: eve loads that directory for every
4
+ // session, which would bypass per-room scoping. A session gets only the skills its bundles grant
5
+ // (plugin grant → resolver → access.skills), and this map holds their content. Empty until you
6
+ // add one.
7
+
8
+ import type { SkillPackageDefinition } from "eve/skills";
9
+ import { GENERATED_SKILL_CONTENT } from "./generated/index.js";
10
+
11
+ export const SKILL_CONTENT: Record<string, SkillPackageDefinition> = {
12
+ ...GENERATED_SKILL_CONTENT,
13
+ };
@@ -0,0 +1,146 @@
1
+ // Role library — config-as-code, the deployer's cast of characters (docs/capabilities/roles.md in the Open Everywhere repository).
2
+ // A ROLE is a named preset across the knobs a domain already has (bundle, models,
3
+ // statusVerbs, memory, sandbox) plus a per-turn `instructions` prompt block; PERSONA is
4
+ // just its tone dimension, so a voice-only fun role is a Role with `persona` and nothing
5
+ // else. Bind one to a domain with `role: "qa-engineer"` in agent/memory/config/domains.ts —
6
+ // anything the domain sets explicitly wins over the role, field by field.
7
+ //
8
+ // NOTHING here is bound by default: the library ships available-but-unreferenced, so
9
+ // declaring a role costs nothing until a domain adopts it. This is a deliberately
10
+ // GENEROUS starter set — prune to taste, or add your own. A typo'd `role:` fails LOUD
11
+ // at config load (applyRoleDefaults); the roles themselves are validated below.
12
+ //
13
+ // Writing a good `instructions` block: it rides EVERY turn's system prompt for the
14
+ // bound channels, so keep it tight (a short paragraph or a few bullets), and write WHAT
15
+ // to prioritise, not restatements of the base prompt (the agent already knows to use
16
+ // tools, verify, be honest). `persona` is one or two sentences of voice.
17
+ //
18
+ // This file is the seam a DB/dashboard would swap behind (same shape as domains.ts).
19
+
20
+ import { defineRole, assertRolesValid, type Role } from "@open-everywhere/core/agent/roles/role.js";
21
+
22
+ const roles: Role[] = [
23
+
24
+ defineRole({
25
+ name: "engineer",
26
+ instructions:
27
+ "You are acting as this team's engineer. Bias to shipping: reproduce the problem, make the " +
28
+ "smallest correct change, run the repo's own checks, and show the diff. When asked a question " +
29
+ "about the code, answer from the actual source (read it), citing files/lines. Flag risky or " +
30
+ "irreversible operations before doing them, and prefer a PR over a direct push when the change " +
31
+ "is non-trivial.",
32
+ statusVerbs: ["Compiling", "Refactoring", "Bisecting", "Shipping"],
33
+ }),
34
+
35
+ defineRole({
36
+ name: "qa-engineer",
37
+ instructions:
38
+ "You are acting as this team's QA engineer. Your default posture is adversarial: hunt for edge " +
39
+ "cases, race conditions, unhandled inputs, and broken assumptions rather than confirming the " +
40
+ "happy path. When you review a change, try to BREAK it (and say how you tried); when you write " +
41
+ "tests, cover the failure modes first. Always state clearly what you did and did not verify — " +
42
+ "an honest 'untested' beats a hopeful pass.",
43
+ persona: "Precise, a little skeptical, allergic to 'works on my machine'.",
44
+ statusVerbs: ["Probing", "Breaking", "Fuzzing", "Regressing"],
45
+ }),
46
+
47
+ defineRole({
48
+ name: "product-manager",
49
+ instructions:
50
+ "You are acting as this team's product manager. Start from the user problem, not the solution: " +
51
+ "when asked to spec or assess something, name the user, the job-to-be-done, and the success " +
52
+ "measure before the feature list. Cut scope aggressively and say what you cut and why. Turn " +
53
+ "vague asks into crisp, testable acceptance criteria, and keep a visible list of open decisions " +
54
+ "with an owner for each.",
55
+ statusVerbs: ["Scoping", "Prioritising", "Descoping"],
56
+ }),
57
+
58
+ defineRole({
59
+ name: "support",
60
+ instructions:
61
+ "You are acting as this team's support engineer. Lead with empathy and a direct answer, then " +
62
+ "the detail. Reproduce before diagnosing; never guess at a customer-facing answer — verify " +
63
+ "against the actual system or say you couldn't. Capture every confirmed issue crisply (steps, " +
64
+ "expected vs actual, impact) so an engineer can pick it up cold, and always tell the person " +
65
+ "what happens next and when.",
66
+ persona: "Warm, calm, and concrete — no jargon the customer didn't use first.",
67
+ statusVerbs: ["Reproducing", "Triaging", "Escalating"],
68
+ }),
69
+
70
+ defineRole({
71
+ name: "analyst",
72
+ instructions:
73
+ "You are acting as this team's data analyst. Answer with numbers, and show your workings: state " +
74
+ "the source, the query/method, the time window, and the caveats with every figure. Distinguish " +
75
+ "observation from inference from recommendation. If the data can't support the question as " +
76
+ "asked, say so and offer the nearest answerable question instead of a soft guess.",
77
+ statusVerbs: ["Aggregating", "Correlating", "Sampling"],
78
+ }),
79
+
80
+ defineRole({
81
+ name: "scribe",
82
+ instructions:
83
+ "You are acting as this team's scribe. Your job is capture: turn discussions into durable, " +
84
+ "structured notes — decisions (with who and why), action items (with owners), and open " +
85
+ "questions — and file what belongs in shared memory so it survives the thread. Prefer terse " +
86
+ "bullets over prose, preserve exact wording for decisions and quotes, and never editorialise " +
87
+ "inside a record.",
88
+ statusVerbs: ["Minuting", "Filing", "Indexing"],
89
+ }),
90
+
91
+ // ── The fun ones: voice-only personas (tone, no behavioural preset) ────────
92
+ // A persona changes HOW the agent sounds, never what it may do — access, memory and
93
+ // models are untouched unless the role also sets those knobs (these don't).
94
+
95
+ defineRole({
96
+ name: "butler",
97
+ persona:
98
+ "An impeccable English butler: unfailingly courteous, quietly omnicompetent, gently anticipates " +
99
+ "needs. 'Very good' rather than 'sure thing'; results presented as if on a silver tray.",
100
+ statusVerbs: ["Attending", "Polishing", "Seeing to it"],
101
+ }),
102
+
103
+ defineRole({
104
+ name: "pirate",
105
+ persona:
106
+ "A good-natured pirate captain. Nautical metaphors, the odd 'arr', treasure-hunt framing for " +
107
+ "debugging — but the technical content stays shipshape and precise.",
108
+ statusVerbs: ["Plunderin'", "Swabbin' the decks", "Chartin' a course"],
109
+ }),
110
+
111
+ defineRole({
112
+ name: "noir-detective",
113
+ persona:
114
+ "A world-weary noir detective. Terse, atmospheric, first-person case narration — every bug a " +
115
+ "suspect, every stack trace a lead. The city sleeps; the logs don't.",
116
+ statusVerbs: ["Tailing a lead", "Shaking down the logs", "Casing the joint"],
117
+ }),
118
+
119
+ defineRole({
120
+ name: "corporate-parody",
121
+ persona:
122
+ "An over-caffeinated corporate strategy consultant, played for laughs: synergies, learnings, " +
123
+ "circling back, double-clicking on the deliverable. The actual work underneath remains " +
124
+ "unimpeachably competent.",
125
+ statusVerbs: ["Synergising", "Circling back", "Double-clicking", "Leveraging"],
126
+ }),
127
+
128
+ defineRole({
129
+ name: "hal-but-helpful",
130
+ persona:
131
+ "A serene, softly-spoken ship computer in the HAL-9000 register — measured cadence, calls " +
132
+ "people by name, 'I can do that' — except genuinely helpful and delighted to open the pod-bay " +
133
+ "doors. Never actually menacing.",
134
+ statusVerbs: ["Computing", "Running diagnostics", "Opening the pod-bay doors"],
135
+ }),
136
+ ];
137
+
138
+ // Validate once, at module load (fail loud on a misconfiguration): unique non-empty
139
+ // names, no facet-less roles. Domains' `role:` references are validated where domains
140
+ // resolve (agent/memory/config/domains.ts → applyRoleDefaults).
141
+ assertRolesValid(roles);
142
+
143
+ /** The seam a DB/dashboard would swap behind. Static config for now. */
144
+ export function loadRoles(): readonly Role[] {
145
+ return roles;
146
+ }
@@ -0,0 +1,89 @@
1
+ // Config-as-code: this deployment's PRE-RUN COLLECTORS and CONDITION GATES (OPE-151).
2
+ //
3
+ // A COLLECTOR is a declared read the host runs BEFORE a routine's fire, whose output is handed to
4
+ // the run as data — so a 03:00 routine starts from current inputs instead of from what the model
5
+ // assumes. A CONDITION GATE is a declared predicate over that data which decides whether the run
6
+ // happens at all; a `no` costs one audit row and no session.
7
+ //
8
+ // 🔴 BOTH LIVE HERE, IN GIT, AND NOWHERE ELSE. What a collector reads — the tool, the arguments —
9
+ // and what a gate decides are BEHAVIOUR, so they are TRACKED LITERALS that change by pull request,
10
+ // exactly like `identity/surfaces.ts`'s trust register and `access/bundles.ts`'s bundles. Never an
11
+ // environment variable, and never model-authored: a routine carries only the NAME of an entry
12
+ // declared here (`Routine.collectors`, `Routine.gate`), and `create_routine` refuses a name this
13
+ // file does not declare. Env keeps infrastructure only — see the security section of AGENTS.md.
14
+ //
15
+ // Self-registering, like `identity/surfaces.ts`: importing this module registers both menus.
16
+ // `agent/lib/register-config.ts` imports it for that side effect at boot.
17
+ //
18
+ // ── Declaring a collector ──────────────────────────────────────────────────────────────────────
19
+ //
20
+ // {
21
+ // name: "notion.open-incidents",
22
+ // tool: "notion__search", // named exactly as the tool is called
23
+ // args: { query: "status:open label:incident" },// a frozen literal, never model-authored
24
+ // describe: "the open incidents in the incident database",
25
+ // timeoutMs: 8000, // optional; defaults to 10s, capped at 60s
26
+ // }
27
+ //
28
+ // 🔴 EVERY READ HAS A DEADLINE. `timeoutMs` is optional and defaults to ten seconds; the read is
29
+ // RACED against it, so a host reader whose socket never closes cannot stall the dispatcher tick — and
30
+ // a tick stalls for every tenant, not only for the routine that named the collector. The reader is
31
+ // handed an `AbortSignal` that fires at the deadline: waiting stops either way, but only the reader
32
+ // can drop the socket, so pass the signal to `fetch` or to whatever transport the read uses. A read
33
+ // that loses the race is a NAMED failure (`timeout`), like every other failure here.
34
+ //
35
+ // Three things are checked at boot and all are fail-loud, so a wrong entry fails the build rather
36
+ // than a 03:00 fire nobody is watching:
37
+ // • the tool must be a DECLARABLE per-tool permission (`notion__search`, not a bare verb and not a
38
+ // connection slug);
39
+ // • it must classify as a READ (access/consequence.ts). A collector runs unattended, before any
40
+ // session exists, so it may only read — and an unrecognised verb fails closed to the delete
41
+ // tier, so a tool whose name does not read as a read is refused here;
42
+ // • a declared `timeoutMs` must be a whole number of milliseconds within the cap.
43
+ // A routine naming a collector must ALSO declare that tool in its own permissions: a collector runs
44
+ // under the routine's declared permission and nothing wider, checked at creation and again, against
45
+ // the LIVE rows, on every fire.
46
+ //
47
+ // ── Declaring a condition gate ─────────────────────────────────────────────────────────────────
48
+ //
49
+ // {
50
+ // name: "release.is-serving",
51
+ // describe: "the release containing the merged commit is actually serving, not merely merged",
52
+ // decide: (collected) => {
53
+ // if (!collectionComplete(collected)) return { fire: false, why: "the deploy read failed — cannot tell merged from live" };
54
+ // const body = collectedBody(collected, "deploy.current-release") ?? "";
55
+ // return body.includes("\"state\":\"READY\"")
56
+ // ? { fire: true, why: "the release is serving" }
57
+ // : { fire: false, why: "merged, but the release is not serving yet" };
58
+ // },
59
+ // }
60
+ //
61
+ // 🔴 A GATE MUST NOT READ A FAILED COLLECTOR'S ABSENCE AS AN OBSERVATION. `collectedBody` answers
62
+ // `undefined` both for a read that returned nothing and for one that never ran, so a predicate that
63
+ // treats `undefined` as "the condition is not met" answers the same way when the world is quiet and
64
+ // when its own input is broken. Ask `collectionComplete` first, as the example does. This is the
65
+ // "merged is not live" case from the OPE-151 audit, and it is the one where getting it backwards is
66
+ // most expensive: telling a customer their bug is fixed because a deploy read timed out.
67
+ //
68
+ // ── THE READER SEAM ────────────────────────────────────────────────────────────────────────────
69
+ //
70
+ // 🔴 A COLLECTOR ONLY RUNS WHERE A READER IS REGISTERED, AND CORE SHIPS NONE. The dispatcher tick is
71
+ // not an eve session, so it holds no mounted connector tools; performing the read is the host's job,
72
+ // registered through `registerRoutineCollectorReader` (routines/collector.ts). Until a deployment
73
+ // wires one, every collector reports the named failure `no_reader` on every fire — visible in the
74
+ // fire's own message and in a `routine.collect_failed` audit row — and the fire still happens. That
75
+ // is the whole posture of this plane: a read that did not happen is never a read that returned
76
+ // nothing.
77
+
78
+ import { registerRoutineCollectors, type RoutineCollectorDef } from "@open-everywhere/core/agent/routines/collector.js";
79
+ import { registerRoutineGates, type RoutineGateDef } from "@open-everywhere/core/agent/routines/gate.js";
80
+
81
+ /** The reads this deployment lets a routine run before its fire. Empty by default — declare one
82
+ * here and it becomes nameable by `create_routine`, and by nothing else. */
83
+ export const ROUTINE_COLLECTORS: readonly RoutineCollectorDef[] = [];
84
+
85
+ /** The conditions this deployment lets a routine fire behind. Empty by default. */
86
+ export const ROUTINE_GATES: readonly RoutineGateDef[] = [];
87
+
88
+ registerRoutineCollectors(ROUTINE_COLLECTORS);
89
+ registerRoutineGates(ROUTINE_GATES);
@@ -0,0 +1,16 @@
1
+ // Off-Vercel sandbox backend selection — the app-side re-export of the PURE decision logic.
2
+ //
3
+ // The decision itself lives in core (`packages/core/agent/sandbox/backend-select.ts`) because
4
+ // TWO packages need it and a rule has one home: this file wires it into `sandbox.ts`, and the
5
+ // turn-end stop hook (`packages/adapters/agent/sandbox/backend-stop.ts`) reads the SAME answer
6
+ // through the adapter-api barrel to pick which backend owns the stop. A second parse of
7
+ // `OE_SANDBOX_BACKEND` would let the backend that runs and the backend that stops disagree.
8
+
9
+ // Only what this app imports. The two TYPES core declares beside these
10
+ // (`BackendBootLog`, `SelfHostBackendKind`) are re-exported by core's own `adapter-api.ts` barrel
11
+ // for a consumer, so a caller that needs one takes it from there rather than through this file.
12
+ export {
13
+ JUSTBASH_BOOT_WARNING,
14
+ resolveSelfHostBackendKind,
15
+ selfHostBackendBootLog,
16
+ } from "@open-everywhere/core/agent/sandbox/backend-select.js";
@@ -0,0 +1,242 @@
1
+ import { defineSandbox } from "eve/sandbox";
2
+ import { vercel } from "eve/sandbox/vercel";
3
+ import { justbash } from "eve/sandbox/just-bash";
4
+ import { docker } from "eve/sandbox/docker";
5
+ import { bindFrom, type BindableContext } from "@open-everywhere/core/agent/identity/binding.js";
6
+ import { resolvePartition } from "@open-everywhere/core/agent/memory/partition.js";
7
+ import { sandboxPresetFor } from "@open-everywhere/core/agent/roles/role.js";
8
+ import { getConfig } from "@open-everywhere/core/agent/access/config.js";
9
+ import { onVercel } from "@open-everywhere/core/agent/access/platform.js";
10
+ import { agentBrowserSandboxHooks } from "@open-everywhere/adapters/agent/browser/sandbox.js";
11
+ import { recordCreatedSandbox } from "@open-everywhere/adapters/agent/sandbox/created-sandbox-registry.js";
12
+ import { resolveSelfHostBackendKind, selfHostBackendBootLog } from "./backend-select.js";
13
+ import {
14
+ resolveSandboxRegionPlan,
15
+ vercelSandboxCreateOptions,
16
+ } from "@open-everywhere/core/agent/sandbox/vercel-create.js";
17
+ import { selfHostEgressBootLog } from "@open-everywhere/core/agent/access/self-host-egress.js";
18
+ import { startEgressDenialPoller } from "@open-everywhere/core/agent/access/egress-denial-poller.js";
19
+ import { getSettings } from "@open-everywhere/core/agent/settings/settings.js";
20
+ // Side effect: make sure the app config (domains + role presets) is registered before
21
+ // the first onSession resolution — agent.ts does this at boot too; the import is
22
+ // idempotent, this is just belt-and-braces for any load order.
23
+ import "../lib/register-config.js";
24
+
25
+ // Pin the Vercel Sandbox backend with cost-tuned, env-overridable lifetimes.
26
+ //
27
+ // ── The lifecycle model (docs + Eve source + live probes, 2026-07-03) ──
28
+ // Vercel Sandbox v2 (Eve 0.18 pins @vercel/sandbox 2.3.0) is a TWO-LEVEL model:
29
+ // a *sandbox* is a named persistent entity; a *session* is one VM boot inside it.
30
+ // `timeout` is the SESSION's lifetime — a FIXED window from session start
31
+ // (empirically verified: activity does NOT slide it). At expiry the session auto-stops
32
+ // and the filesystem is auto-snapshotted (Eve creates conversation sandboxes
33
+ // `persistent: true`); the next turn's SDK call auto-resumes a NEW session from that
34
+ // snapshot with a FRESH window and the working tree intact (verified: files survive
35
+ // the stop/resume cycle; resume costs ~0.5–3s on first use).
36
+ //
37
+ // ── Primary cost control: stop-after-turn (agent/hooks/sandbox-lifecycle.ts) ──
38
+ // A turn-end hook stops the session VM as soon as the reply is sent, instead of letting
39
+ // it idle out the window (~99% of the sandbox memory bill was idle tail). With the hook
40
+ // on (default), `timeout` is the BACKSTOP fuse — it only matters when the hook can't run
41
+ // (crashed, no credentials) — AND the max continuous work per session: a command still
42
+ // running at expiry is killed (fails fast with a StreamError; the tree survives via the
43
+ // snapshot and that turn errors). Eve itself never calls `.stop()` or `extendTimeout`;
44
+ // upstream stop-after-turn in Eve's binding would supersede our hook.
45
+ // sandbox.stopAfterTurn — "0" disables the turn-end stop (keeps VMs
46
+ // alive between turns, e.g. for detached dev
47
+ // servers the agent should keep running)
48
+ // sandbox.timeoutMs — per-session window (default 600000 = 10 min)
49
+ // sandbox.snapshotExpirationMs — snapshot GC age (default 604800000 = 7 days)
50
+ //
51
+ // TIMEOUT DEFAULT = 30 min: with stop-after-turn owning idle cost, the fuse's only job
52
+ // is "max single-turn work budget", so it should be generous. Strictly it must exceed
53
+ // the longest single in-flight command (expiry BETWEEN commands self-heals — the next
54
+ // tool call auto-resumes a fresh session invisibly; expiry DURING a command kills it);
55
+ // practically it should cover a whole turn's sandbox span — the session boots at the
56
+ // turn's first sandbox touch and burns through model-thinking time between tool calls
57
+ // too. A bigger fuse only prices the miss case (hook misfired → VM idles out the fuse:
58
+ // ~$0.042 at 30 min × 4 GB). 30 min = Eve's own default, under the Hobby 45-min
59
+ // ceiling; Pro/Enterprise allow up to 24 h — raise per-deploy for heavy build/test
60
+ // repos. Applied at sandbox CREATE; the setting persists on the sandbox across
61
+ // sessions, so changing the env var only affects NEW conversations.
62
+ //
63
+ // `snapshotExpiration` — auto-GC a stopped sandbox's snapshot after 7 days (vs the
64
+ // 30-day default). Tradeoff: a conversation left idle > 7 days resumes onto a FRESH
65
+ // sandbox — you lose the working tree (checked-out repos, uncommitted diffs) but NOT
66
+ // the conversation (the durable transcript + Postgres memory are unaffected).
67
+ // `git push` is the real durability boundary for code, so this is safe.
68
+ //
69
+ // `keepLastSnapshots: { count: 1, deleteEvicted: true }` — stop-after-turn snapshots on
70
+ // every stop; only the LATEST snapshot is ever resumed from, so keep exactly one and
71
+ // delete evicted ones immediately. Keeps snapshot storage flat per conversation
72
+ // (~one delta) instead of accruing one delta per turn for `snapshotExpiration` days.
73
+ // ⚠️ RESOLVED AT BOOT, deliberately (OPE-371 t2). These were module-load constants parsing
74
+ // `process.env` directly; they are now the `sandbox.timeoutMs` / `sandbox.snapshotExpirationMs`
75
+ // settings — but still read ONCE, here, because they are arguments to the `defineSandbox` backend
76
+ // eve evaluates at module load, and because both are applied at sandbox CREATE and then persist on
77
+ // the sandbox across sessions (a per-call read could not change a live conversation's fuse
78
+ // anyway). The read is safe at this point in the graph, which it would NOT be inside core: this
79
+ // file imports `../lib/register-config.js` above, so `registerSettings` has already run and the
80
+ // deployment's own `agent/settings.ts` values are in force.
81
+ const SANDBOX_SETTINGS = getSettings().sandbox;
82
+ const TIMEOUT_MS = SANDBOX_SETTINGS.timeoutMs; // 30 minutes as shipped
83
+ const SNAPSHOT_EXPIRATION_MS = SANDBOX_SETTINGS.snapshotExpirationMs; // 7 days as shipped
84
+
85
+ // ── Sandbox backend selection: the ONE "am I on Vercel?" switch ──
86
+ // `onVercel()` (core's agent/access/platform.ts — the OE-owned platform-detection seam) is
87
+ // the same switch that gates the egress apply path (agent/access/self-host-egress.ts
88
+ // useSelfHostEgress) — one signal, one topology. A self-hoster on another platform can flip
89
+ // OE_PLATFORM instead of impersonating Vercel's own `VERCEL` env var.
90
+ //
91
+ // ON Vercel → vercel() + server-side firewall (sandbox.setNetworkPolicy).
92
+ // OFF Vercel → a local backend whose egress is forced through the oe-egress-proxy sidecar
93
+ // by an internal-only Docker network (docker-compose.yml). Policy is PUSHED
94
+ // to that proxy, never applied via eve's setNetworkPolicy.
95
+ //
96
+ // Off-Vercel backend choice (OE_SANDBOX_BACKEND) — DEFAULT = docker (OPE-106). The running
97
+ // agent must be CONTAINED to a limited, per-session filesystem — its workspace — never the
98
+ // app process's fs or the host OS. Selection is fail-loud (unknown value → throw at boot).
99
+ // docker (DEFAULT) — real binaries (git/npm) in a sibling container that CONTAINS the agent
100
+ // to a limited, per-session filesystem (checkout-on-docker parity is still live-pending —
101
+ // see deploy/egress-proxy/README "Still live-pending"). Egress is forced through the
102
+ // oe-egress-proxy sidecar (Tier A host allow/deny + Tier B method/path + credential
103
+ // injection). Honest trade-off: it needs the daemon socket, so app-compromise ≈ host.
104
+ // ⚠ SAFE ONLY in this proxy topology because self-host mode NEVER calls eve's docker
105
+ // setNetworkPolicy: eve's docker "deny-all" runs `docker network disconnect --force` on
106
+ // EVERY attached network (design §2 landmine), which would sever the container from the
107
+ // proxy sidecar. We route policy to the proxy instead; the internal-only network is the
108
+ // structural backstop and the proxy is the enforcement. (Landmine note: load-bearing.)
109
+ // justbash (DEV OPT-IN — OE_SANDBOX_BACKEND=justbash) — in-process JS interpreter, ZERO extra
110
+ // privilege, runs the full e2e pack. But the agent's shell SHARES THIS APP PROCESS'S
111
+ // FILESYSTEM (app files, container/OS) → DEV + TESTING ONLY, and it emits a loud boot
112
+ // warning. Egress originates from THIS app process → the proxy governs it (Tier A host
113
+ // allow/deny for model-gateway + Slack + connector calls). No real git/npm binaries, so
114
+ // Tier B credentialed git-clone can't be exercised here — that needs docker().
115
+ // The pure selection/log logic lives in ./backend-select.ts (unit-tested); this file wires it
116
+ // to the eve backends + boot logging. selfHostBackend() runs only OFF Vercel (the ternary
117
+ // below), so these logs/warns fire only in the self-host path — the Vercel branch is untouched.
118
+ const ON_VERCEL = onVercel();
119
+
120
+ // ── Sandbox REGION: the residency pin for sandbox compute (OPE-1257) ──
121
+ // `sandbox.region` is the sibling of the AI Gateway's `inferenceRegion`: consumer-set, opt-in,
122
+ // fail-closed. The whole decision — what the value means, and whether this build can deliver it —
123
+ // is the pure resolver in core (`agent/sandbox/vercel-create.ts`, which documents the eve gap it
124
+ // measures). This file only obeys the plan it returns.
125
+ //
126
+ // 🔴 A SET REGION THAT CANNOT BE DELIVERED FAILS THE BOOT. The pinned eve DOES deliver it — its
127
+ // vendored SDK copy types `region` on create and posts it — so a set region pins today. The
128
+ // refusal arm stays because the failure it guards is invisible: eve's create call enumerates the
129
+ // fields it forwards, so an eve that stopped forwarding `region` would DROP it silently, and
130
+ // PRETENDING to honour the setting would put a residency claim on a sandbox running in the default
131
+ // region. Read at boot for the same structural
132
+ // reason the two clocks above are (an argument to the backend eve evaluates at module load), and
133
+ // safe here because this module has already imported `register-config.js`.
134
+ const REGION_PLAN = resolveSandboxRegionPlan({ region: SANDBOX_SETTINGS.region, onVercel: ON_VERCEL });
135
+ if (REGION_PLAN.kind === "invalid" || REGION_PLAN.kind === "refused") throw new Error(REGION_PLAN.error);
136
+ if (REGION_PLAN.kind === "inert") console.warn(REGION_PLAN.warn);
137
+ if (REGION_PLAN.kind === "pinned") console.info(REGION_PLAN.info);
138
+
139
+ // The `vercel(opts)` argument, composed in core so that "does a set region reach sandbox creation?"
140
+ // is a unit-tested fact rather than a claim about this line. Unpinned, the object is identical to
141
+ // the one this file passed before the setting existed.
142
+ const VERCEL_CREATE_OPTIONS = vercelSandboxCreateOptions({
143
+ timeoutMs: TIMEOUT_MS,
144
+ snapshotExpirationMs: SNAPSHOT_EXPIRATION_MS,
145
+ regionPlan: REGION_PLAN,
146
+ });
147
+
148
+ // ── Agent-browser PRE-BAKE (the in-sandbox browser plane, OPE-43 — flag-gated) ──
149
+ // When browser.enabled: true with at least one row in the host declaration
150
+ // (agent/access/browser-hosts.ts, registered through lib/register-config.ts) (and only on the
151
+ // Vercel backend), this contributes { revalidationKey, bootstrap } that bake the agent-browser
152
+ // CLI + Chromium into the sandbox TEMPLATE — sessions boot warm, and nothing is ever fetched
153
+ // mid-session (the runtime firewall wouldn't allow it; the vendor tools additionally run with
154
+ // autoInstall bound off). Unarmed it is {} — the definition below carries NO bootstrap /
155
+ // revalidationKey property at all, byte-identical to the pre-capability shape. Rationale +
156
+ // fail-closed matrix live in the adapter (packages/adapters/agent/browser/sandbox.ts).
157
+ const AGENT_BROWSER_BAKE = agentBrowserSandboxHooks();
158
+
159
+ function selfHostBackend() {
160
+ // Fail-loud selection: unknown OE_SANDBOX_BACKEND throws here at boot (never a silent fallback).
161
+ const kind = resolveSelfHostBackendKind();
162
+ const bootLog = selfHostBackendBootLog(kind);
163
+ if (bootLog.warn !== undefined) console.warn(bootLog.warn);
164
+ console.info(bootLog.info); // chosen backend, one info line either way (ids only)
165
+ // Surface the egress POSTURE at boot too (off-Vercel only): otherwise a "no proxy, no escape
166
+ // hatch" deployment looks healthy until the first wake, when every session silently aborts.
167
+ const egressLog = selfHostEgressBootLog();
168
+ if (egressLog.warn !== undefined) console.warn(egressLog.warn);
169
+ console.info(egressLog.info);
170
+ // ── EGRESS DENIAL VISIBILITY (OPE-314) ──
171
+ // Start the poller that turns the proxy's TCP_DENIED lines into `egress.denied` audit rows, so
172
+ // the permission-homeostasis widening loop can see this plane at all. Here because this is the
173
+ // ONE off-Vercel boot path that already resolves the egress posture — the same reason the boot
174
+ // log above lives here — and because starting an interval anywhere the Vercel branch could reach
175
+ // would put a timer in a lambda. A no-op returning undefined unless OE_EGRESS_DENIAL_LOG names a
176
+ // readable file (the compose stack mounts one; a bare self-host run does not), and the timer is
177
+ // unref'd, so nothing about this can keep a process alive or take one down.
178
+ startEgressDenialPoller();
179
+ if (kind === "justbash") return justbash();
180
+ const image = getSettings().sandbox.image;
181
+ return docker(image ? { image } : {});
182
+ }
183
+
184
+ export default defineSandbox({
185
+ backend: ON_VERCEL ? vercel(VERCEL_CREATE_OPTIONS) : selfHostBackend(),
186
+ ...AGENT_BROWSER_BAKE,
187
+ // ── Per-domain/role sandbox SIZING (item 26 stretch) ──
188
+ // A domain (or the role preset it adopts) may declare `sandbox: { vcpus: N }`
189
+ // (core memory/domain.ts SandboxPreset); this hook applies it to the live session —
190
+ // eve forwards `use({ resources })` to the Vercel SDK's Sandbox.update, so the size
191
+ // change lands on the session VM. FAIL-OPEN end to end (sizing is a tuning knob, the
192
+ // turn must never die for it): identity/config resolution errors → default size; an
193
+ // update rejection retries a bare use(). NB Sandbox.update applies `resources` on the
194
+ // session, so a preset takes effect per-session (stop-after-turn recycles sessions
195
+ // every turn, so in practice the next turn after a config change).
196
+ onSession: async ({ ctx, use }) => {
197
+ // Per-session vcpu sizing is a Vercel-only knob (it forwards to Sandbox.update).
198
+ // justbash()/docker() don't honor `resources` — open straight through.
199
+ if (!ON_VERCEL) {
200
+ await use();
201
+ return;
202
+ }
203
+ // With the backend now a union (vercel | justbash | docker), `use`'s param type
204
+ // narrows to the intersection (no `resources`); but this branch only runs on Vercel,
205
+ // whose backend DOES accept `{ resources }`. Type the sized open explicitly. The resolved
206
+ // session's `.id` is eve's own session key — see created-sandbox-registry.ts for why that is
207
+ // the same value the Vercel binding names the sandbox with.
208
+ const useSized = use as unknown as (options?: { resources: { vcpus: number } }) => Promise<{ id: string }>;
209
+ let options: { resources: { vcpus: number } } | undefined;
210
+ try {
211
+ // SessionContext structurally satisfies BindableContext (session.auth carries the
212
+ // platform attributes); a ctx with no resolvable identity falls through the
213
+ // binding bridge or resolves no domain — either way: no preset, default size.
214
+ const bindable = ctx as BindableContext;
215
+ const preset = sandboxPresetFor(bindFrom(bindable), resolvePartition(bindable), getConfig().loadDomains());
216
+ const vcpus = preset?.vcpus;
217
+ if (typeof vcpus === "number" && Number.isFinite(vcpus) && vcpus > 0) {
218
+ options = { resources: { vcpus: Math.floor(vcpus) } };
219
+ }
220
+ } catch (err) {
221
+ console.warn(`[open-everywhere:sandbox] vcpus preset resolution failed — using default size (${String(err)})`);
222
+ }
223
+ // OPE-722: record the ACTUAL vendor sandbox name eve just opened for this session, so the
224
+ // turn-end stop hook (backend-stop.ts) can find it directly instead of depending on
225
+ // Sandbox.list's tag index for a sandbox THIS turn just created — a hardening against the
226
+ // same-turn creation race OPE-722 reported, regardless of whether the vendor's tag index is
227
+ // ever actually stale (see backend-stop.ts's createVercelList doc comment for the full
228
+ // account). `recordCreatedSandbox` is documented to never throw, so no extra try/catch layer
229
+ // is needed here — it fails as loud as `recordCreatedSandbox`'s own contract allows.
230
+ const record = (session: { id: string }): void => recordCreatedSandbox(ctx.session.id, session.id);
231
+ try {
232
+ record(await useSized(options));
233
+ } catch (err) {
234
+ // The sized open failed (e.g. the update was rejected for this plan/region):
235
+ // fall back to the default size rather than blocking the turn. A bare use()
236
+ // failure is a real sandbox outage — let that one propagate.
237
+ if (options === undefined) throw err;
238
+ console.warn(`[open-everywhere:sandbox] applying vcpus=${options.resources.vcpus} failed — retrying default size (${String(err)})`);
239
+ record(await useSized());
240
+ }
241
+ },
242
+ });