@open-everywhere/create 0.1.4 → 0.2.1

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 (32) hide show
  1. package/NOTICE +34 -4
  2. package/package.json +4 -4
  3. package/src/app-naming.ts +47 -0
  4. package/src/cli.ts +10 -6
  5. package/src/engine.ts +5 -1
  6. package/src/steps/roster.ts +3 -0
  7. package/src/steps/slack.ts +16 -3
  8. package/template/AGENTS.md +4 -1
  9. package/template/README.md +21 -2
  10. package/template/agent/access/bundles.ts +9 -0
  11. package/template/agent/access/credential-bindings.generated.ts +6 -5
  12. package/template/agent/access/grants.generated.ts +6 -5
  13. package/template/agent/access/network-access.ts +1 -1
  14. package/template/agent/access/pre-approval-rules.ts +81 -0
  15. package/template/agent/blob/hosting.ts +1 -1
  16. package/template/agent/identity/groups.generated.ts +6 -5
  17. package/template/agent/instructions.md +7 -5
  18. package/template/agent/instrumentation/otel.ts +13 -0
  19. package/template/agent/instrumentation/otlp.ts +4 -0
  20. package/template/agent/lib/register-adapters.ts +21 -282
  21. package/template/agent/lib/register-config.ts +5 -0
  22. package/template/agent/lib/register-docs.ts +17 -0
  23. package/template/agent/memory/config/agents.generated.ts +6 -5
  24. package/template/agent/sandbox/backend-select.ts +2 -2
  25. package/template/agent/sandbox/sandbox.ts +127 -73
  26. package/template/agent/spend/checkpoints.ts +5 -3
  27. package/template/agent/webhooks/hooks.generated.ts +6 -5
  28. package/template/claude/skills/upgrade-open-everywhere/SKILL.md +52 -4
  29. package/template/package.json +1 -1
  30. package/template/roster.lock.json +7 -1
  31. package/template/scripts/ops-mcp.ts +16 -0
  32. package/template/agent/instrumentation.ts +0 -6
@@ -1,286 +1,25 @@
1
- // Adapter selection — the app's choice of vendor implementations, wired at startup.
2
- // Mirrors register-config.ts: a side-effect import from agent.ts. Core ships only
3
- // portable defaults (env broker, sqlite stores, Eve-owned sandbox egress); this is
4
- // where the deploy opts into Vercel/Neon. Self-host swaps these lines (or rolls its
5
- // own adapter package) without touching core.
6
-
7
- import { registerOeOauth } from "@open-everywhere/adapters/agent/oe-oauth/register.js";
8
- import { oeOauthEnabled, loadOeOauthConfig } from "@open-everywhere/adapters/agent/oe-oauth/config.js";
9
- import { registerAgentBrowser } from "@open-everywhere/adapters/agent/browser/register.js";
10
- import { agentBrowserEnabled, loadAgentBrowserConfig } from "@open-everywhere/adapters/agent/browser/config.js";
11
- import { registerAwsAccess } from "@open-everywhere/adapters/agent/aws/register.js";
12
- import { awsAccessEnabled } from "@open-everywhere/adapters/agent/aws/config.js";
13
- import { registerDirectoryAdapter } from "@open-everywhere/adapters/agent/directory/register.js";
14
- import {
15
- registerDbCredentialBroker,
16
- CREDENTIAL_KEY_ENV,
17
- registerGitHubConfigPrProvider,
18
- setBlobPutter,
19
- registerAuditDropCounter,
20
- emitTelemetry,
21
- getOperationalStore,
22
- bootVerifyAuditChainTail,
23
- bootBackfillLinkTenants,
24
- registerOperationalPgliteLazy,
25
- resolveOperationalSelection,
26
- resolveMemoryProviderId,
27
- onVercel,
28
- declareProviderSelection,
29
- assertConnectorMemoryGrants,
30
- applySubtreeSurfaceClaims,
31
- reconcileSurfaceCommands,
32
- } from "@open-everywhere/core/agent/adapter-api.js";
33
- import { SELECTED_PROVIDERS, SELECTED_PROVIDER_IDS } from "./providers.generated.js";
34
- import { loadDomains, loadSilos } from "../memory/config/domains.js";
35
- import { domainMemoryGrants } from "./memory-grants.js";
36
- import { registerNeon } from "@open-everywhere/adapters/agent/postgres/neon.js";
37
- import { vercelBlobPutter } from "@open-everywhere/adapters/agent/blob/hosting.js";
38
-
39
- // NOTE: these are STATIC imports on purpose. Eve bundles each authored module
40
- // (agent.ts and its import graph) into a SINGLE rolldown chunk — a literal dynamic
41
- // `import("…")` of an internal module code-splits into a second chunk and trips Eve's
42
- // "expected one authored module" invariant. Static import keeps it one chunk. Laziness
43
- // is preserved where it matters: registerNeon() only CONSTRUCTS the @neondatabase/serverless
44
- // driver via an inner variable-specifier import when it is actually CALLED (guarded below),
45
- // and registerNotionProvider() just writes registry rows (its memory token resolves lazily via
46
- // the broker). Provider BUNDLES (@open-everywhere/provider-*) are wired here the same way — one
47
- // static import + one register call per bundle.
48
-
49
- // PROVIDER SELECTION — declared FIRST, before any bundle registers (OPE-116 stage 5). The table
50
- // (./providers.generated.ts) is the single list of provider bundles this deployment carries; core
51
- // takes the ids as opaque strings and every platform-keyed registration seam then refuses anything
52
- // outside them, so an unselected provider is structurally absent from every registry rather than
53
- // half-wired. Declaring first matters: a row that lands ahead of the declaration cannot be retracted
54
- // (core warns loudly if one does).
1
+ // Adapter wiring — a side-effect import from agent.ts, beside register-config.ts.
55
2
  //
56
- // 🔴 `./providers.generated.ts` is BUILD OUTPUT (OPE-375), gitignored and written by `oe build` from
57
- // `roster.config.ts`'s `providers` map — the same edit that generates each bundle's app-tree stubs.
58
- // Do not hand-edit it; change roster.config.ts.
59
- declareProviderSelection(SELECTED_PROVIDER_IDS);
60
-
61
- // Connector-plane user-auth flow (OAuth mint/authorize) — core is vendor-SDK-free since R2
62
- // (OPE-82) and fails closed on the mint paths when no flow is registered, so this deploy's
63
- // choice must be wired explicitly. The built-in v1 flow is OWN-ROLLED OAuth (OPE-88), registered
64
- // when the master flag OE_OAUTH_ENABLED is armed AND a provider config block is present
65
- // (OE_OAUTH_CONFIG / OE_OAUTH_NOTION_RESOURCE) — DCR+PKCE against a hosted MCP server, tokens in
66
- // our own AES vault, callback at GET /oauth/v1/callback. Flag-off OR config-absent ⇒ NO flow
67
- // registers and core's OAuth mint paths fail closed (the callback route 404s) — there is no
68
- // vendor fallback (the legacy Vercel Connect adapter was deleted; rollback is a git revert).
69
- {
70
- const oeOauthCfg = oeOauthEnabled() ? loadOeOauthConfig() : undefined;
71
- if (oeOauthCfg) {
72
- registerOeOauth({ providers: oeOauthCfg });
73
- }
74
- }
75
-
76
- // Make the "db" credential broker resolvable by name (task #113 — a core, vendor-SDK-free broker
77
- // whose store is our own operational DB). REGISTRATION = CONFIGURED (no selection any more): register
78
- // it iff its backing config exists — the AES key (OE_CREDENTIAL_ENCRYPTION_KEY, AES-256-GCM) that
79
- // encrypts every stored credential at rest. Without the key the broker fails closed on both store and
80
- // read, so registering it would falsely advertise the "Set API key" chat intake (which keys off db
81
- // being REGISTERED). A credential stamped `broker: "db"` still fails closed here if the key is absent —
82
- // registration just gates the OFFER. It resolves credentials an org member pastes into the Slack
83
- // "Set API key" modal, encrypted at rest.
84
- if (process.env[CREDENTIAL_KEY_ENV]) {
85
- registerDbCredentialBroker();
86
- }
87
-
88
- // Wire the agent-browser toolkit (the in-sandbox browser plane, OPE-43). Mirrors the oe-oauth
89
- // guard: register ONLY when the master flag (browser.enabled) is armed AND the operator domain
90
- // allowlist parses (loadAgentBrowserConfig is fail-closed + loud on a flag-on misconfig). Flag-off
91
- // OR config-absent ⇒ nothing registers ⇒ byte-identical (no browser tools mount, no extra egress
92
- // hosts, no sandbox bake — the bake side reads the SAME config in agent/sandbox/sandbox.ts).
93
- // registerAgentBrowser itself can still refuse (returns false, loud) if the vendor extension's
94
- // config binding didn't take — the uncontained-defaults hazard fails closed there.
95
- // 🔴 THE PLATFORM GATE IS INSIDE loadAgentBrowserConfig (OPE-1793), so this mount needs no
96
- // `onVercel()` of its own: off Vercel the config is `undefined`, nothing registers, and neither the
97
- // 20 `browser_*` tools nor the declared egress hosts open for a browser the image cannot run.
98
- {
99
- const browserCfg = agentBrowserEnabled() ? loadAgentBrowserConfig() : undefined;
100
- if (browserCfg) {
101
- registerAgentBrowser(browserCfg);
102
- }
103
- }
104
-
105
- // Wire the AWS-access layer (review/design/aws-access-plan.md, Door 1, Phase 0). Mirrors the oe-oauth
106
- // guard: register ONLY when the master flag (OE_AWS_ENABLED) is armed. Flag-off ⇒ nothing registers ⇒
107
- // byte-identical behaviour (no AWS identity provider, no `aws` executor) and core's AWS mint/exec
108
- // paths fail closed (requireAwsIdentityProvider / requireAwsExecutor throw — never a silent no-op).
109
- // registerAwsAccess constructs the OIDC→STS identity provider + the `aws` CLI executor and hands each
110
- // to core's DI seam; there is no fallback (rollback is not arming the flag).
111
- if (awsAccessEnabled()) {
112
- registerAwsAccess();
113
- }
114
-
115
- // Wire the identity-DIRECTORY plane (review/design/identity-linking-plan.md, P2 — Okta first). Same
116
- // guard shape as the three above: registerDirectoryAdapter() is a no-op unless OE_DIRECTORY_PROVIDER
117
- // names an adapter, and fails LOUD if it names one whose config is incomplete. Selector-off ⇒
118
- // nothing registers ⇒ core's DirectoryProvider port stays empty and the feature is cleanly OFF —
119
- // the daily sync schedule logs one line and touches nothing, and the control verbs say so. The IdP
120
- // is a sync source, never a live dependency: nothing on a request path reads this.
121
- registerDirectoryAdapter();
122
-
123
- // (The "github" memory backing MOVED to the github provider bundle at OPE-390 — it registers
124
- // itself from `provider.register()` in the SELECTED_PROVIDERS loop below, beside the rest of that
125
- // vendor's wiring. Core keeps only the first-party postgres/sqlite backings.)
126
-
127
- // Register the GitHub config-repo PR mechanism as the default ConfigPrProvider (OPE-86 / R6). The
128
- // credential-binding flow (credentials/binding-pr.ts) opens the custody-flip PR through this port;
129
- // GitHub is the built-in default, a self-host swaps in a GitLab/Gitea provider here. Unconditional:
130
- // harmless when unused (resolveWriteToken only runs on a real binding PR and fails loud there if no
131
- // GitHub credential is configured — same as before this seam existed).
132
- registerGitHubConfigPrProvider();
133
-
134
- // THE PROVIDER BUNDLES (OPE-114 stages 3-4, selection-driven since stage 5). One boot call per
135
- // SELECTED bundle, taken straight from the generated table — there is no second, hand-maintained
136
- // call list to drift out of step with it, which is the whole point (this module names no provider;
137
- // read roster.config.ts's `providers` map, or ./providers.generated.ts, for what each bundle does). Every call is unconditional and cheap: a bundle's
138
- // register fn only writes registry rows, and every live vendor call behind them resolves its
139
- // credential lazily at call time.
140
- for (const provider of SELECTED_PROVIDERS) {
141
- provider.register();
142
- }
143
-
144
- // THE PREFETCH INVARIANT (OPE-390), cross-validated at boot and fail-LOUD — HERE, not in
145
- // register-config.ts, and the ordering is the whole reason: a connector-owned memory capability is
146
- // DECLARED by its provider bundle's register() in the loop directly above, so a check that ran
147
- // earlier would see an empty capability registry and pass every silo vacuously (the same boot-order
148
- // trap assertWindowSourcesVouched documents on the vouch register).
3
+ // The wiring itself is PRODUCT code: `registerDefaultAdapters()` in `@open-everywhere/adapters`
4
+ // registers the default adapters, this app's selected provider bundles and the operational store,
5
+ // with the product's selection rules and floors. An upgrade of the package upgrades the wiring, so
6
+ // this file needs no merge on upgrade.
149
7
  //
150
- // What it refuses: a domain whose memory list names a connector-owned silo the domain holds no
151
- // connector grant for, or whose container reaches past that grant (a github silo on an ungranted
152
- // repo). Memory rides the same route as connector access; a silo must not be a side door onto a
153
- // source the connector plane would refuse. First-party postgres/sqlite silos are skipped by
154
- // construction — the deployment holds their credential, so there is nothing to check.
155
- assertConnectorMemoryGrants(loadDomains(), loadSilos(), domainMemoryGrants);
156
-
157
- // THE SUBTREE CLAIM (OPE-539), settled HERE for the SAME boot-order reason as the line above. An
158
- // operator widens ONE trust entry with `subtree: true` (agent/identity/surfaces.ts); the platform
159
- // that HONOURS the word declares so from its own bundle's register(), in the loop above. So this is
160
- // the first line at which both halves are up:
161
- // • a claim on a platform no SELECTED bundle honours throws — the registry never accepts a line
162
- // that would grant nothing;
163
- // • every platform that does honour it receives its keys, and validates them by its own rules
164
- // (a Notion key whose last segment is not a page id is a boot error here).
165
- // Core names no vendor either side of this call.
166
- applySubtreeSurfaceClaims();
8
+ // To add or replace an adapter, put the registration in `customAdapters` below. It runs after every
9
+ // default adapter and provider bundle registers, and before the boot checks that read them. Keep
10
+ // imports STATIC: Eve bundles this module's import graph into one chunk, and a dynamic `import()`
11
+ // splits it.
167
12
 
168
- // Neon (hosted Postgres) is LIVE-ONLY — only wire it when a Postgres backing is actually selected
169
- // AND a DATABASE_URL exists. registerNeon() is a no-op at import; it only builds the driver (inner
170
- // variable-specifier import) when called here.
171
- //
172
- // 🔴 THE SELECTION IS THE RUNTIME'S OWN RULE, NOT A SECOND ENV TEST (OPE-2230). Both selectors
173
- // DEFAULT to postgres when DATABASE_URL is present, so a hand-written `OE_MEMORY_PROVIDER ===
174
- // "postgres"` here refuses to build the driver for exactly the deployment the default was added for:
175
- // the Neon Marketplace integration sets DATABASE_URL and nothing else, `getOperationalStore()` then
176
- // resolves to postgres, no client is ever registered, and the first audit append throws. The
177
- // scaffolded app is dead rather than degraded, and it mounts no status route to say why.
178
- // `scripts/scaffold-test.ts` reds if this returns to testing the two env names.
179
- const operationalSelection = resolveOperationalSelection(process.env);
180
- const postgresSelected =
181
- resolveMemoryProviderId(process.env) === "postgres" ||
182
- (operationalSelection.id === "postgres" && operationalSelection.floor === "none");
183
- if (postgresSelected && process.env.DATABASE_URL) {
184
- await registerNeon();
185
- }
186
-
187
- // Embedded-Postgres (PGlite) operational FLOOR (OPE-113) — the zero-wiring default. Arm it when
188
- // NO provider is selected AND there is no DATABASE_URL: getOperationalStore() then resolves to the
189
- // same postgres.ts store backed by an in-process, file-backed PGlite instance (one dialect
190
- // everywhere; sqlite is deprecated). Skipped on Vercel (the floor is per-instance ephemeral — the
191
- // provider's own guard fails loud there, and OE_ALLOW_EPHEMERAL_OPERATIONAL=1 is the single-instance
192
- // escape) and whenever a provider/DATABASE_URL is set (hosted path, or explicit sqlite). registerNeon
193
- // above never fires on this branch, so there's no double-register. Takes a single-writer lockfile and
194
- // refuses to boot against a data dir a live process already holds (PGlite has no cross-process lock).
195
- // OPE-391: armed LAZILY (registerOperationalPgliteLazy). The store is registered synchronously —
196
- // getOperationalStore() resolves to the floor from this line on — but the lock + WASM boot + schema
197
- // init happen on the first QUERY. `eve start` evaluates this very module inside the CLI process (its
198
- // sandbox-prewarm step) and then spawns the built server as a child that evaluates it again; an
199
- // eager arm made the CLI parent the lock holder and the server child refused to boot ("LOCKED by a
200
- // LIVE process"), so a fresh zero-wiring directory could never start. Deferring the open means only
201
- // the process that actually uses the store touches the data dir. The boot-time store checks ride
202
- // along as the first-open callback (see below) so they still run exactly once, in that process.
203
- const pgliteFloorArmed =
204
- operationalSelection.floor === "pglite" &&
205
- (!onVercel() || process.env.OE_ALLOW_EPHEMERAL_OPERATIONAL === "1");
206
- if (pgliteFloorArmed) {
207
- registerOperationalPgliteLazy(undefined, runBootStoreChecks);
208
- }
209
-
210
- // Wire the audit drop counter (audit-wedge WS-5). audit.ts can import telemetry.ts neither
211
- // statically (module-init cycle: telemetry → commands/config → approvals → audit, TDZ on the
212
- // action REGISTRY) nor dynamically (the one-chunk invariant above), so it exposes a registration
213
- // seam and THIS boot module — which already sits outside that cycle — injects the emitter.
214
- registerAuditDropCounter((action, platform) => emitTelemetry("oe.audit.dropped", { "oe.action": action, "oe.platform": platform }));
215
-
216
- // Governance-channel floor tripwire member supplier (OPE-110): now registered inside
217
- // registerSlackProvider() above — the supplier arm is slack code, so it rides the bundle's one
218
- // boot call instead of a dedicated barrel function (OPE-114 stage 4).
219
-
220
- // Vercel Blob putter for the HTML-artifact hosting seam (core agent/blob/hosting.ts). Same
221
- // posture as the gateway ModelBackend: core owns the injection point, this is the app's vendor
222
- // choice. Registering unconditionally is harmless — hostHtmlArtifact is only ever reached when
223
- // an org has opted in (resolveBlobHosting), and the putter itself fails loud if the OIDC env
224
- // isn't present.
225
- setBlobPutter(vercelBlobPutter);
226
-
227
- // The two boot-time store checks, in one place so they can run either eagerly (hosted/sqlite paths,
228
- // where the store is live the moment it is registered) or on the embedded floor's FIRST OPEN
229
- // (OPE-391 — the lazy arm above; running them inline would defeat the whole point by opening PGlite
230
- // in eve's CLI process). Same posture either way: availability over integrity, never throws.
231
- //
232
- // 1. Audit hash-chain tail-check (OPE-52, audit-wedge WS-6c): re-verify the last N per-org audit
233
- // links and log ONE loud warn on any break (silent out-of-band deletion/mutation is the thing it
234
- // catches). If the operational store can't even be resolved (a provider misconfig, or the
235
- // Vercel-ephemeral-sqlite guard that is SUPPOSED to fail loud), leave surfacing that to the
236
- // app's own first store use rather than swallowing it in a different guise.
237
- // 2. Identity-link tenant backfill (OPE-309 phase 2): stamp the deployment's sole admitted
238
- // workspace onto link rows written before `identity_linked_ids` had a `tenant` column. Same
239
- // posture, sharper reason: a row it cannot fix is already INERT (an un-named row matches
240
- // nothing, so the link is dormant and standing is lost rather than mis-granted), so there is
241
- // nothing a thrown boot would protect. It re-runs on every start and only ever fills blanks, so
242
- // declaring the workspace's channel heals it.
243
- async function runBootStoreChecks(): Promise<void> {
244
- try {
245
- await bootVerifyAuditChainTail(getOperationalStore());
246
- } catch {
247
- /* store not resolvable — its first real use raises the reason loudly; skip the check */
248
- }
249
- try {
250
- await bootBackfillLinkTenants();
251
- } catch {
252
- /* store not resolvable — see above; bootBackfillLinkTenants swallows everything else */
253
- }
254
- }
255
-
256
- if (!pgliteFloorArmed) await runBootStoreChecks();
13
+ import { registerDefaultAdapters } from "@open-everywhere/adapters/agent/register-defaults.js";
14
+ import { SELECTED_PROVIDERS } from "./providers.generated.js";
15
+ import { loadDomains, loadSilos } from "../memory/config/domains.js";
16
+ import { domainMemoryGrants } from "./memory-grants.js";
257
17
 
258
- // SLASH COMMANDS REGISTER FROM THE DEPLOYMENT ITSELF (OPE-1687). An app declares WHICH ROOMS with
259
- // `declareSurfaceCommandScope` (from `@open-everywhere/core/agent/channels/command-registration.js`,
260
- // imported by register-config.ts; this app declares none); each selected provider bundle declared
261
- // HOW in the loop above. This is the ONE call that joins them, and
262
- // it names no provider — same discipline as the bundle loop.
263
- //
264
- // HERE, at the cold start, so no separate deploy step is needed. The steady-state cost is one vendor read per process start and a write only on a real difference; a
265
- // deployment that declares no room pays nothing.
266
- //
267
- // AWAITED, and it never throws — the plane catches every reconciler failure and logs one line. It is
268
- // awaited rather than left floating because a lambda can freeze the moment it answers, and a floating
269
- // promise would then be cancelled mid-flight on exactly the cold start that had the work to do.
270
- //
271
- // 🔴 THE LATENCY BOUND IS THE RECONCILER'S, and it is a real one: each vendor call carries a short
272
- // timeout AND no 429 backoff, under one budget for the whole reconcile. A timeout alone would NOT
273
- // bound this — `withRetryAfter`'s default policy allows up to 60 s of sleep between attempts, and an
274
- // `AbortSignal` covers a request, never a sleep. See `BOOT_RETRY` in the discord bundle's
275
- // `commands/discord-command-sync.ts`.
276
- //
277
- // TWO CONSEQUENCES WORTH KNOWING, both stated rather than engineered away:
278
- // • `eve start` evaluates this module TWICE — once in the CLI parent, once in the server child it
279
- // spawns — so a local start reconciles twice. Deferring behind a first-request hook the way the
280
- // PGlite floor above does would fix it and cost a seam; the hosted deployment (one evaluation per
281
- // lambda instance) is the case that matters.
282
- // • a reconcile WRITES whenever the process holds the surface's credentials and the declared rooms
283
- // are out of step with THIS checkout. Running a branch that changes the command set against the
284
- // deployment's own credentials therefore publishes that branch's commands — the warning lives on
285
- // the declaration in `agent/commands/discord-commands.ts`.
286
- await reconcileSurfaceCommands();
18
+ await registerDefaultAdapters({
19
+ // `./providers.generated.ts` is `oe build` output from roster.config.ts. Do not hand-edit it.
20
+ providers: SELECTED_PROVIDERS,
21
+ memory: { domains: loadDomains, silos: loadSilos, grants: domainMemoryGrants },
22
+ customAdapters: () => {
23
+ // This app registers no custom adapter.
24
+ },
25
+ });
@@ -75,6 +75,11 @@ import "../identity/authors.js";
75
75
  // import degrades loudly (a routine that names a collector stops being creatable) rather than
76
76
  // silently widening anything.
77
77
  import "../routines/collectors.js";
78
+ // Side effect: register the deployment-wide half of the SHIPPED MANUAL (OPE-138, OPE-2215) so the
79
+ // read_docs tool serves the configuration reference, the security docs and the how-it-works pages.
80
+ // Each provider package registers its own setup runbook from register<X>Provider() instead. Drop
81
+ // this import and read_docs serves those runbooks and nothing else.
82
+ import "./register-docs.js";
78
83
 
79
84
  // The BEHAVIOUR half of config-as-code (OPE-371), registered FIRST — before registerConfig, and
80
85
  // before the boot-time cross-validations below — because a settings-sensitive gate consulted during
@@ -0,0 +1,17 @@
1
+ // Shipped-manual wiring (OPE-138) — hands core the deployment-wide half of the docs the agent can
2
+ // read about itself. SELF-REGISTERING: importing this module performs the registration, the same
3
+ // shape as agent/identity/surfaces.js (see register-config.ts, which imports both for their
4
+ // side effects, so every process that resolves config also carries the manual).
5
+ //
6
+ // This half is the UNIVERSAL manual. Each provider bundle registers its OWN setup runbook from its
7
+ // register<X>Provider(), so a de-selected bundle's docs are not in the import graph at all.
8
+ //
9
+ // The manual ships INSIDE `@open-everywhere/core` (OPE-2215), so this app serves the pages of the
10
+ // core version it installed, and an upgrade of core upgrades the manual with it. Nothing here is
11
+ // copied at scaffold time. To add your own pages, call registerDocs again with more entries. An
12
+ // entry with the same `path` as a shipped page replaces that page.
13
+
14
+ import { registerDocs } from "@open-everywhere/core/agent/adapter-api.js";
15
+ import { UNIVERSAL_DOCS } from "@open-everywhere/core/agent/docs/generated/index.js";
16
+
17
+ registerDocs(UNIVERSAL_DOCS);
@@ -1,14 +1,15 @@
1
1
  // GENERATED + MACHINE-OWNED — DO NOT HAND-EDIT.
2
2
  //
3
3
  // The agents overlay: DATA ONLY. Written exclusively by the deterministic codemod
4
- // (packages/core/agent/memory/agents-codemod.ts) on a bot-PR branch; the PR merge is the approval gate.
4
+ // (@open-everywhere/core/agent/memory/agents-codemod.js) on a bot-PR branch; the PR merge is the approval gate.
5
5
  //
6
6
  // The codemod re-emits this file from its own parsed content and refuses to rewrite it when the
7
- // bytes differ, so a hand-edit stops the next machine PR. Before a merge, nothing checks that the
8
- // file is still data only unless the repository runs its own check: the person who merges the PR
9
- // is the gate. Read a PR to this file as code, because it is code.
7
+ // data or the import differ, so a hand-edit there stops the next machine PR. Before a merge,
8
+ // nothing checks that the file is still data only unless the repository runs
9
+ // `npx oe build --check` or `npx oe overlays check`: the person who merges the PR is the gate.
10
+ // Read a PR to this file as code, because it is code.
10
11
  //
11
- // To change it by hand, edit only the data below and keep the layout exactly as emitted.
12
+ // To change it by hand, edit only the data below, then run `npx oe overlays normalize`.
12
13
 
13
14
  import type { AgentsFile } from "@open-everywhere/core/agent/memory/agents-codemod.js";
14
15
 
@@ -1,8 +1,8 @@
1
1
  // Off-Vercel sandbox backend selection — the app-side re-export of the PURE decision logic.
2
2
  //
3
- // The decision itself lives in core (`packages/core/agent/sandbox/backend-select.ts`) because
3
+ // The decision itself lives in core (`@open-everywhere/core/agent/sandbox/backend-select.js`) because
4
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
5
+ // turn-end stop hook (`@open-everywhere/adapters/agent/sandbox/backend-stop.js`) reads the SAME answer
6
6
  // through the adapter-api barrel to pick which backend owns the stop. A second parse of
7
7
  // `OE_SANDBOX_BACKEND` would let the backend that runs and the backend that stops disagree.
8
8
 
@@ -1,15 +1,18 @@
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";
1
+ import { defineSandbox, type RuntimeSandboxSession, type SandboxEnvironment, type SandboxSession } from "eve/sandbox";
2
+ import { VercelSandbox } from "eve/sandbox/vercel";
3
+ import { JustBashSandbox } from "eve/sandbox/just-bash";
4
+ import { DockerSandbox } from "eve/sandbox/docker";
5
5
  import { bindFrom, type BindableContext } from "@open-everywhere/core/agent/identity/binding.js";
6
6
  import { resolvePartition } from "@open-everywhere/core/agent/memory/partition.js";
7
7
  import { sandboxPresetFor } from "@open-everywhere/core/agent/roles/role.js";
8
8
  import { getConfig } from "@open-everywhere/core/agent/access/config.js";
9
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";
10
+ import { agentBrowserSandboxHooks, type BakeableSandbox } from "@open-everywhere/adapters/agent/browser/sandbox.js";
11
+ import { registerSandboxFirewall } from "@open-everywhere/core/agent/sandbox/network-policy.js";
12
12
  import { resolveSelfHostBackendKind, selfHostBackendBootLog } from "./backend-select.js";
13
+ // The KIND type has one home — core's adapter-api barrel — so the backend that runs and the backend
14
+ // that stops cannot disagree about its spelling (see ./backend-select.ts's header).
15
+ import type { SelfHostBackendKind } from "@open-everywhere/core/agent/adapter-api.js";
13
16
  import {
14
17
  resolveSandboxRegionPlan,
15
18
  vercelSandboxCreateOptions,
@@ -148,22 +151,67 @@ const VERCEL_CREATE_OPTIONS = vercelSandboxCreateOptions({
148
151
  // ── Agent-browser PRE-BAKE (the in-sandbox browser plane, OPE-43 — flag-gated) ──
149
152
  // When browser.enabled: true with at least one row in the host declaration
150
153
  // (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).
154
+ // Vercel backend), this bakes the agent-browser CLI + Chromium into the sandbox TEMPLATE —
155
+ // sessions boot warm, and nothing is ever fetched mid-session (the runtime firewall wouldn't allow
156
+ // it; the vendor tools additionally run with autoInstall bound off). Unarmed it is {}, and the
157
+ // environment below carries NO `prepare` at all. Rationale + fail-closed matrix live in the adapter
158
+ // (@open-everywhere/adapters/agent/browser/sandbox.js).
157
159
  const AGENT_BROWSER_BAKE = agentBrowserSandboxHooks();
158
160
 
159
- function selfHostBackend() {
161
+ // 🔴 eve 0.69.0 TOOK `revalidationKey` AWAY, AND THE TEMPLATE KEY IS NOW THE ENVIRONMENT'S OWN HASH
162
+ // (OPE-2247). Through 0.68.x the bake contributed `{ revalidationKey, bootstrap }` to the sandbox
163
+ // definition, and bumping the agent-browser pin changed that key, which rebuilt the template. 0.69.0
164
+ // replaced both: setup is the environment's `prepare`, and a template is keyed by
165
+ // `sha256(stableSerialize(environment options))` — which serialises a function by its SOURCE TEXT
166
+ // (`shared/sandbox-environment.js`), not by the values it closes over. So ARMING or DISARMING the
167
+ // bake rebuilds the template (the `prepare` key appears or disappears), and EDITING this file's
168
+ // prepare body rebuilds it, but bumping the agent-browser version alone does NOT: the pin lives
169
+ // inside the vendor installer, behind a closure, where the hash cannot see it. The consequence is a
170
+ // stale warm template after a pin bump, not an unfenced session. Re-deriving a revalidation input the
171
+ // hash can see belongs with the browser plane, not with this port — it is handed to the orchestrator
172
+ // as a follow-up rather than guessed at here, because every field of these options is forwarded to
173
+ // Vercel's own `Sandbox.create`, so inventing a carrier key is a change to the create call.
174
+ const AGENT_BROWSER_PREPARE =
175
+ AGENT_BROWSER_BAKE.bootstrap === undefined
176
+ ? {}
177
+ : {
178
+ // `installAgentBrowser` reads only `run` off the session it is given, verified in the vendor
179
+ // bytes: `sandbox.id` appears once in `@agent-browser/eve/dist/sandbox/sandbox.js`, inside
180
+ // `runAgentBrowser` (the TOOL path), never on the install path. eve 0.69.0's `prepare` hands
181
+ // over a `SandboxSession`, which carries `run` and no `id` — and `BakeableSandbox.id` is
182
+ // optional for exactly that reason, so this is a widening to the adapter's own shape rather
183
+ // than a cast that hides a missing field.
184
+ prepare: async (sandbox: SandboxSession): Promise<void> => {
185
+ const bootstrap = AGENT_BROWSER_BAKE.bootstrap;
186
+ if (bootstrap === undefined) return;
187
+ await bootstrap({ use: async () => sandbox satisfies BakeableSandbox });
188
+ },
189
+ };
190
+
191
+ // ── The three provider ENVIRONMENTS (eve 0.64.0 + 0.66.0 shape) ──
192
+ // eve replaced the object-form `defineSandbox({ backend, … })` with environments a selector opens.
193
+ // The environment is also the capability carrier: only a Vercel session takes a full
194
+ // `SandboxNetworkPolicy`, Docker takes the coarse `"allow-all"` / `"deny-all"` alone, and just-bash
195
+ // takes none because its policy is fixed at creation. Core's firewall sites reach whichever one is
196
+ // live through `registerSandboxFirewall` below, because they cannot import this file.
197
+ const VERCEL_ENVIRONMENT = VercelSandbox.environment({ ...VERCEL_CREATE_OPTIONS, ...AGENT_BROWSER_PREPARE });
198
+
199
+ interface SelfHostSandbox {
200
+ readonly kind: SelfHostBackendKind;
201
+ /** The environment itself, for the named `environment` export eve's compiler requires. */
202
+ readonly environment: SandboxEnvironment<object | undefined, SandboxSession>;
203
+ /** Opening is a closure because each provider's open options differ, and the union of two
204
+ * environments has no callable `open` signature. */
205
+ readonly open: () => Promise<RuntimeSandboxSession>;
206
+ }
207
+
208
+ function selfHostEnvironment(): SelfHostSandbox {
160
209
  // Fail-loud selection: unknown OE_SANDBOX_BACKEND throws here at boot (never a silent fallback).
161
210
  const kind = resolveSelfHostBackendKind();
162
211
  const bootLog = selfHostBackendBootLog(kind);
163
212
  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.
213
+ console.info(bootLog.info);
214
+ // ── EGRESS POSTURE (self-host only) ──
167
215
  const egressLog = selfHostEgressBootLog();
168
216
  if (egressLog.warn !== undefined) console.warn(egressLog.warn);
169
217
  console.info(egressLog.info);
@@ -176,67 +224,73 @@ function selfHostBackend() {
176
224
  // readable file (the compose stack mounts one; a bare self-host run does not), and the timer is
177
225
  // unref'd, so nothing about this can keep a process alive or take one down.
178
226
  startEgressDenialPoller();
179
- if (kind === "justbash") return justbash();
227
+ if (kind === "justbash") {
228
+ const environment = JustBashSandbox.environment({});
229
+ registerSandboxFirewall({ carries: "none", backend: "justbash" });
230
+ return { kind, environment, open: async () => await environment.open() };
231
+ }
180
232
  const image = getSettings().sandbox.image;
181
- return docker(image ? { image } : {});
233
+ const environment = image ? DockerSandbox.environment({ image }) : DockerSandbox.environment({});
234
+ registerSandboxFirewall({ carries: "coarse", environment });
235
+ return { kind, environment, open: async () => await environment.open() };
182
236
  }
183
237
 
184
- export default defineSandbox({
185
- backend: ON_VERCEL ? vercel(VERCEL_CREATE_OPTIONS) : selfHostBackend(),
186
- ...AGENT_BROWSER_BAKE,
238
+ // Resolved at module load, exactly as the backend ternary was: one boot decision, one set of logs,
239
+ // and the registration core's firewall sites read must be in place before the first turn.
240
+ const SELF_HOST = ON_VERCEL ? undefined : selfHostEnvironment();
241
+ if (ON_VERCEL) registerSandboxFirewall({ carries: "full", environment: VERCEL_ENVIRONMENT });
242
+
243
+ /**
244
+ * The environment eve's COMPILER reads, and it must be exactly one (`compiler/normalize-sandbox.js`:
245
+ * "Sandbox must export an environment"). eve records its `provider` name and this export's name in
246
+ * the compiled binding, so the built app is specialised to the backend resolved HERE, at module load —
247
+ * which is what the old `backend:` ternary already did, so nothing about that is new. The selector
248
+ * below opens the same environment; this export is how the build learns which provider that is.
249
+ */
250
+ export const environment: SandboxEnvironment<object | undefined, SandboxSession> =
251
+ SELF_HOST === undefined ? (VERCEL_ENVIRONMENT as SandboxEnvironment<object | undefined, SandboxSession>) : SELF_HOST.environment;
252
+
253
+ export default defineSandbox(async (context) => {
187
254
  // ── Per-domain/role sandbox SIZING (item 26 stretch) ──
188
255
  // 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());
256
+ // (core memory/domain.ts SandboxPreset); this selector applies it when it opens the session —
257
+ // eve forwards `resources` to the Vercel SDK's create/update, so the size change lands on the
258
+ // session VM. FAIL-OPEN end to end (sizing is a tuning knob, the turn must never die for it):
259
+ // identity/config resolution errors → default size; a rejected sized open retries unsized.
260
+ // Per-session vcpu sizing is a Vercel-only knob, so every other backend opens straight through.
261
+ if (SELF_HOST !== undefined) return await SELF_HOST.open();
262
+ let options: { resources: { vcpus: number } } | undefined;
263
+ try {
264
+ // SandboxSelectorContext structurally satisfies BindableContext (session.auth carries the
265
+ // platform attributes); a context with no resolvable identity falls through the binding bridge
266
+ // or resolves no domain — either way: no preset, default size.
267
+ const bindable = context as unknown as BindableContext;
268
+ const preset = sandboxPresetFor(bindFrom(bindable), resolvePartition(bindable), getConfig().loadDomains());
269
+ const vcpus = preset?.vcpus;
270
+ if (typeof vcpus === "number" && Number.isFinite(vcpus) && vcpus > 0) {
271
+ options = { resources: { vcpus: Math.floor(vcpus) } };
240
272
  }
241
- },
273
+ } catch (err) {
274
+ console.warn(`[open-everywhere:sandbox] vcpus preset resolution failed — using default size (${String(err)})`);
275
+ }
276
+ // 🔴 OPE-722's CREATED-SANDBOX RECORD IS GONE, BECAUSE eve 0.69.0 NO LONGER EXPOSES THE NAME
277
+ // (OPE-2247). The old `onSession` hook's `use()` resolved to an object carrying `.id` — the vendor
278
+ // sandbox name — and this file recorded it so the turn-end stop hook could reach the sandbox with a
279
+ // primary-key `Sandbox.get` rather than `Sandbox.list`'s tag index. On 0.69.0 neither
280
+ // `SandboxSession` nor `RuntimeSandboxSession` carries an `id`, so there is nothing to record and no
281
+ // authored surface that can learn it. The stop hook degrades to exactly the pre-OPE-722 path:
282
+ // `created-sandbox-registry.ts` documents an absent record as "falls back to the vendor's tag
283
+ // index, exactly as before this fix", so stop-after-turn still works and the narrow create-then-stop
284
+ // race OPE-722 hardened against is open again. Re-deriving a name the hook can use is handed to the
285
+ // orchestrator as a follow-up; it is not guessable from these bytes.
286
+ if (options === undefined) return await VERCEL_ENVIRONMENT.open();
287
+ try {
288
+ return await VERCEL_ENVIRONMENT.open(options);
289
+ } catch (err) {
290
+ // The sized open failed (e.g. the size was rejected for this plan/region): fall back to the
291
+ // default size rather than blocking the turn. A bare open() failure is a real sandbox outage —
292
+ // that one propagates, from the call below.
293
+ console.warn(`[open-everywhere:sandbox] applying vcpus=${options.resources.vcpus} failed — retrying default size (${String(err)})`);
294
+ return await VERCEL_ENVIRONMENT.open();
295
+ }
242
296
  });