@open-everywhere/create 0.2.1 → 0.4.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 (33) hide show
  1. package/README.md +51 -11
  2. package/bin/create-open-everywhere.js +4 -0
  3. package/package.json +3 -3
  4. package/src/cli.ts +37 -4
  5. package/src/deploy-status.ts +84 -0
  6. package/src/engine.ts +4 -3
  7. package/src/env-file.ts +11 -9
  8. package/src/scaffold.ts +21 -0
  9. package/src/steps/database.ts +4 -3
  10. package/src/steps/env-checklist.ts +28 -9
  11. package/src/steps/slack.ts +11 -2
  12. package/template/.env.example +10 -1
  13. package/template/AGENTS.md +17 -3
  14. package/template/README.md +22 -7
  15. package/template/agent/access/access-requests.ts +7 -2
  16. package/template/agent/access/browser-hosts.ts +3 -2
  17. package/template/agent/access/bundles.ts +7 -0
  18. package/template/agent/access/connector-credentials.ts +6 -0
  19. package/template/agent/access/mcp-servers.ts +48 -0
  20. package/template/agent/access/pre-approval-rules.ts +27 -11
  21. package/template/agent/lib/register-config.ts +0 -10
  22. package/template/agent/memory/config/domains/index.ts +15 -0
  23. package/template/agent/memory/config/domains/org.ts +18 -4
  24. package/template/agent/memory/config/domains.ts +8 -5
  25. package/template/agent/model/models.ts +6 -4
  26. package/template/agent/model/stacks.ts +2 -2
  27. package/template/agent/sandbox/backend-select.ts +2 -1
  28. package/template/agent/sandbox/sandbox.ts +41 -30
  29. package/template/agent/settings/settings.ts +17 -2
  30. package/template/claude/skills/quickstart-open-everywhere/SKILL.md +115 -22
  31. package/template/claude/skills/upgrade-open-everywhere/SKILL.md +29 -13
  32. package/template/scripts/goldens/roster-plan.txt +156 -0
  33. package/template/agent/blob/hosting.ts +0 -42
package/README.md CHANGED
@@ -1,17 +1,46 @@
1
1
  # @open-everywhere/create
2
2
 
3
- The scaffold and setup check for Open Everywhere.
3
+ <!-- HERO LINE: Tommy decides -->
4
4
 
5
- Part of [Open Everywhere](https://github.com/open-everywhere/open-everywhere) — an open, self-hostable autonomous teammate you tag where your
6
- team works. This package is run, not imported, and it does two things: it scaffolds a new app from
5
+ **The scaffold and setup check for Open Everywhere: an open-source AI agent that your team runs on
6
+ its own infrastructure and works with in chat, issues, docs and code.**
7
+
8
+ ```sh
9
+ npm create @open-everywhere my-agent
10
+ ```
11
+
12
+ Then open the new app in your coding agent and ask it to finish the setup.
13
+
14
+ ## What you get
15
+
16
+ One deployment of Open Everywhere gives your org all of these together:
17
+
18
+ - **Six platforms.** Slack, Microsoft Teams, Discord, GitHub, Linear and Notion, on one identity model.
19
+ - **One deployment for the whole org.** Each domain names the memory silos it uses and can bind its own credentials.
20
+ - **Changes by pull request.** The agent proposes changes to its own grants, and a human merges each one.
21
+ - **Governance you can verify.** Rules live in your repository, and the audit log is append-only and
22
+ hash-chained.
23
+ - **Quiet routines.** A condition gate that says no costs no model spend.
24
+ - **Engineering work.** Repository checkout, edits and pull requests behind an approval gate.
25
+ - **Any model.** One setting switches every model role, closed or open-weights.
26
+
27
+ The [project README](https://github.com/open-everywhere/open-everywhere#how-the-parts-fit) links each
28
+ part to its docs.
29
+
30
+ ## What this package does
31
+
32
+ This package is run, not imported, and it does two things: it scaffolds a new app from
7
33
  the quickstart template, and it reports an existing app's setup status without touching it.
8
34
 
9
35
  **Setup itself is agent-led.** There is no interactive wizard: you open the scaffolded app in your
10
36
  coding agent and ask it to set up Open Everywhere. The agent reads the app's `AGENTS.md`, drives the
11
- `open-everywhere-ops` MCP server, and reads this command's `--check` report. Two routes are
12
- supported, and the agent asks which first: **Vercel** (the `vercel` CLI plus Marketplace Neon) or
13
- **Docker** (a self-hosted container that satisfies the
37
+ `open-everywhere-ops` MCP server, and reads this command's `--check` report. The agent asks which
38
+ route first: **Vercel** (the `vercel` CLI plus Marketplace Neon) or **Docker** (a self-hosted
39
+ container that satisfies the
14
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).
15
44
 
16
45
  ## Use
17
46
 
@@ -27,7 +56,9 @@ npm create @open-everywhere my-agent
27
56
  This scaffolds the quickstart app into `./my-agent`, prints the next steps and exits. It never
28
57
  prompts. The directory must be new or empty. A fresh clone of an empty repository holds only `.git`,
29
58
  and it counts as empty: run `npx @open-everywhere/create .` inside it. The scaffold refuses a
30
- directory that holds anything else. The app depends on the eight `@open-everywhere/*` libraries at
59
+ directory that holds anything else. Run inside an empty clone named `my-agent`,
60
+ `npx @open-everywhere/create my-agent` would make `my-agent/my-agent/`, so it refuses and prints the
61
+ `.` command instead. To nest on purpose, create the inner directory first. The app depends on the eight `@open-everywhere/*` libraries at
31
62
  this CLI's own version — plus this package itself, as a dev dependency, for its `check` script — and
32
63
  on `eve` at the pin core declares; its `postinstall` applies the eve patch. Its roster mounts the launch surfaces: Slack, Notion and GitHub. The app carries `AGENTS.md`,
33
64
  a `.mcp.json` for the `open-everywhere-ops` MCP server and the `quickstart-open-everywhere` skill,
@@ -42,7 +73,10 @@ npx @open-everywhere/create --check
42
73
  `--check` prompts for nothing, changes nothing, sends no telemetry and never scaffolds. It reports
43
74
  each setup step with what is missing and the next command for it. Add `--json` for the same report
44
75
  as one JSON object, which is what an agent-driven setup reads. `--route docker` judges against the
45
- Docker route (the default is `vercel`), and `--host https://<host>` probes a deployment.
76
+ Docker route (the default is `vercel`), and `--host https://<host>` probes a deployment. With
77
+ `--host`, the env step also reads the deployment's public status route (`/eve/v1/oe/status`), so a
78
+ value set only on the deployment (in the Vercel dashboard, say) counts. Without it, the step reads
79
+ only `.env.local` and the shell.
46
80
 
47
81
  Run it from inside an Open Everywhere app — a scaffolded one, or a clone of the repository. From a
48
82
  directory that is not an app, it reports an unsatisfied repo step and tells you to scaffold one.
@@ -51,7 +85,13 @@ The same step list written out for a human is
51
85
  [Setup steps](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/setup-steps.md).
52
86
  That page is generated from the step list this command reports, so the two cannot drift.
53
87
 
54
- This CLI starts on Node 18 or newer. The app it scaffolds needs Node 24.
88
+ This CLI starts on Node 18 or newer, so `--check` can report an old Node. The app it scaffolds needs
89
+ Node 24, so a scaffold on an older Node exits non-zero and writes nothing.
90
+
91
+ The scaffolded app sends anonymous usage telemetry when it runs, ON by default. Its `README.md`
92
+ says how to turn it off (`OE_TELEMETRY=off` on the deployment), and the full disclosure is
93
+ [Telemetry](https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/telemetry.md).
94
+ This CLI itself sends none.
55
95
 
56
96
  ## Docs
57
97
 
@@ -61,8 +101,8 @@ This CLI starts on Node 18 or newer. The app it scaffolds needs Node 24.
61
101
 
62
102
  ## Status
63
103
 
64
- The 0.1 line is the first public release. It is pre-1.0: the API surface is expected to
65
- change, and a minor release may break you. Pin an exact version.
104
+ This package is pre-1.0: the API surface is expected to change, and a minor release may
105
+ break you. Pin an exact version.
66
106
 
67
107
  ## Licence
68
108
 
@@ -11,6 +11,10 @@
11
11
  // and `npm create @open-everywhere` resolves it there. The bin keeps the command name a scaffolded
12
12
  // app's `check` script already runs. One bin is declared, so `npm exec` picks it either way.
13
13
  //
14
+ // THE NODE GATE IS NOT HERE. The scaffold refuses a Node below core's range (src/cli.ts
15
+ // `scaffoldNodeRefusal`, OPE-2399), and `--check` must still run on that Node to report it. Both
16
+ // need the parsed argv, so the gate lives in the TS entry, which tsx runs on any Node it supports.
17
+ //
14
18
  // WHAT IT WRITES. With a `<dir>` argument it SCAFFOLDS, and that is the only write: it copies the
15
19
  // quickstart app (`template/`) into a new or empty `<dir>` (`src/scaffold.ts`) and prints the next
16
20
  // steps. With `--check` it only REPORTS on the current directory — every step is a pure `check()`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-everywhere/create",
3
- "version": "0.2.1",
3
+ "version": "0.4.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.2.1",
50
- "@open-everywhere/provider-slack": "0.2.1",
49
+ "@open-everywhere/core": "0.4.0",
50
+ "@open-everywhere/provider-slack": "0.4.0",
51
51
  "tsx": "^4.23.9"
52
52
  },
53
53
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // TWO JOBS, AND NOTHING ELSE (OPE-2233):
6
6
  // 1. `<dir>` SCAFFOLDS the quickstart app (scaffold.ts) and prints what to do next.
7
7
  // 2. `--check [--json]` REPORTS the setup steps for the current directory. Pure `check()` calls:
8
- // no prompts, no writes, no network except the `--host` deploy probe, and no telemetry.
8
+ // no prompts, no writes, no network except the two `--host` reads (the deploy probe and the
9
+ // status route), and no telemetry. A scaffold on a Node below core's range is refused (OPE-2399).
9
10
  //
10
11
  // 🔴 THERE IS NO INTERACTIVE WIZARD, AND THIS CLI SENDS NO TELEMETRY. The TTY wizard is retired:
11
12
  // setup is AGENT-LED (open the app in a coding agent, which reads AGENTS.md and drives the
@@ -17,8 +18,9 @@
17
18
  import { checkAll, isSetupRoute, SETUP_ROUTES, type SetupRoute, type StepContext } from "./engine.js";
18
19
  import { SETUP_STEPS } from "./steps/index.js";
19
20
  import { CLI_VERSION } from "./version.js";
20
- import { initGitRepo, scaffoldApp, ScaffoldRefusal } from "./scaffold.js";
21
+ import { initGitRepo, nestedNameRefusal, scaffoldApp, ScaffoldRefusal } from "./scaffold.js";
21
22
  import { resolve } from "node:path";
23
+ import { coreNodeRange, nodeRangeSatisfied } from "@open-everywhere/core/agent/ops/package-info.js";
22
24
  import { pathToFileURL } from "node:url";
23
25
  import { slackAppNameProblem } from "@open-everywhere/provider-slack/agent/app-manifest.js";
24
26
  import { readAppNaming } from "./app-naming.js";
@@ -39,7 +41,8 @@ const USAGE = `create-open-everywhere [<dir>] [--check [--json]] [--route vercel
39
41
  two supported routes. A local run is the development loop
40
42
  after a deploy, never a route.
41
43
  --host <url> The deployment's public origin. The build + deploy step
42
- probes its Slack route.
44
+ probes its Slack route, and the env step reads the env
45
+ verdicts its status route (/eve/v1/oe/status) reports.
43
46
  --app-name <name>
44
47
  The Slack app's name in the manifest the Slack step prints.
45
48
  Default: the app's identity.name setting, else "Open
@@ -181,6 +184,26 @@ function runScaffold(target: string): string | undefined {
181
184
  }
182
185
  }
183
186
 
187
+ /**
188
+ * The scaffold's Node gate (OPE-2399). The app needs the Node range `@open-everywhere/core` declares,
189
+ * and a scaffold on an older Node used to succeed with only npm's EBADENGINE warnings, so the person
190
+ * met the failure later and further from its cause. The gate refuses BEFORE a byte is written.
191
+ *
192
+ * It gates the scaffold only. `--check` still runs on an old Node, because its prerequisites step is
193
+ * the report that says which Node to install. An unreadable range does not block: the
194
+ * prerequisites step reports that case as "cannot tell".
195
+ */
196
+ export function scaffoldNodeRefusal(version: string, range: string | undefined): string | undefined {
197
+ if (range === undefined || nodeRangeSatisfied(range, version) !== false) return undefined;
198
+ return `refusing to scaffold: Open Everywhere needs Node ${range} (found ${version}). Run \`nvm install 24 && nvm use 24\`, then run this command again.`;
199
+ }
200
+
201
+ /** What `main` reads from the process, injectable so a test can drive any Node version. */
202
+ export interface MainOptions {
203
+ readonly nodeVersion?: string;
204
+ readonly nodeRange?: string;
205
+ }
206
+
184
207
  /** What to do after a scaffold. The scaffold never prompts, so this is always printed. */
185
208
  function nextSteps(target: string): string {
186
209
  return [
@@ -197,7 +220,7 @@ function nextSteps(target: string): string {
197
220
  ].join("\n");
198
221
  }
199
222
 
200
- export async function main(argv: readonly string[]): Promise<number> {
223
+ export async function main(argv: readonly string[], options: MainOptions = {}): Promise<number> {
201
224
  if (argv.includes("--help") || argv.includes("-h")) {
202
225
  console.log(USAGE);
203
226
  return 0;
@@ -226,6 +249,16 @@ export async function main(argv: readonly string[]): Promise<number> {
226
249
  console.error("--check changes nothing, so it cannot scaffold. Scaffold first, then run --check from inside the app.");
227
250
  return 2;
228
251
  }
252
+ const nodeRefusal = scaffoldNodeRefusal(options.nodeVersion ?? process.versions.node, "nodeRange" in options ? options.nodeRange : coreNodeRange());
253
+ if (nodeRefusal !== undefined) {
254
+ console.error(nodeRefusal);
255
+ return 1;
256
+ }
257
+ const nested = nestedNameRefusal(target, process.cwd());
258
+ if (nested !== undefined) {
259
+ console.error(nested);
260
+ return 1;
261
+ }
229
262
  const dir = runScaffold(target);
230
263
  if (dir === undefined) return 1;
231
264
  console.log(nextSteps(target));
@@ -0,0 +1,84 @@
1
+ // The deployment's own env verdicts — `GET <host>/eve/v1/oe/status` (OPE-2399).
2
+ //
3
+ // `--check` reads `.env.local` and this shell. A Vercel app whose values were set in the dashboard
4
+ // holds them only on the deployment, so the env step read it as all-missing. With `--host`, the env
5
+ // step also reads the deployment's public status route and reports the verdicts the deployment gives
6
+ // for its own environment. The route is unauthenticated by design and carries only closed-vocabulary
7
+ // verdicts plus prose core wrote (packages/core/agent/ops/status.ts), so no secret crosses here.
8
+ //
9
+ // This module imports nothing from core. The route path is a copy of core's `OE_STATUS_PATH`, and
10
+ // scripts/engine-test.ts fails when the two differ: importing `ops/status.ts` would pull the
11
+ // access, sandbox and store resolvers into a CLI that must start before anything is configured.
12
+
13
+ /** The status route. A COPY of core's `OE_STATUS_PATH` — engine-test.ts pins the two equal. */
14
+ export const DEPLOY_STATUS_PATH = "/eve/v1/oe/status";
15
+
16
+ /**
17
+ * The status rows that judge the deployment's ENVIRONMENT, in the order the env step reports them.
18
+ * `secrets` is the root-of-trust pair (DATABASE_URL and the credential key), `postgres` is the
19
+ * store selection that DATABASE_URL drives, and `model-route` is the gateway. The other rows (Node,
20
+ * sandbox, egress, workflow storage, public URL) judge the host, not the env checklist.
21
+ */
22
+ export const DEPLOYED_ENV_REQUIREMENTS = ["secrets", "postgres", "model-route"] as const;
23
+
24
+ export type Verdict = "yes" | "no" | "unknown";
25
+
26
+ export interface DeployedRequirement {
27
+ readonly id: string;
28
+ readonly met: Verdict;
29
+ readonly detail: string;
30
+ }
31
+
32
+ export type DeployedStatusRead =
33
+ | { readonly ok: true; readonly url: string; readonly requirements: readonly DeployedRequirement[] }
34
+ | { readonly ok: false; readonly url: string; readonly detail: string };
35
+
36
+ const VERDICTS: readonly string[] = ["yes", "no", "unknown"];
37
+
38
+ /** The rows of a status document, or `undefined` when the body is not one. */
39
+ export function parseStatusRequirements(body: unknown): DeployedRequirement[] | undefined {
40
+ if (typeof body !== "object" || body === null) return undefined;
41
+ const rows = (body as { requirements?: unknown }).requirements;
42
+ if (!Array.isArray(rows)) return undefined;
43
+ const out: DeployedRequirement[] = [];
44
+ for (const row of rows) {
45
+ if (typeof row !== "object" || row === null) return undefined;
46
+ const { id, met, detail } = row as Record<string, unknown>;
47
+ if (typeof id !== "string" || typeof met !== "string" || !VERDICTS.includes(met)) return undefined;
48
+ out.push({ id, met: met as Verdict, detail: typeof detail === "string" ? detail : "" });
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /** Read the deployment's status document. One GET, no retry: the deploy probe owns cold starts. */
54
+ export async function readDeployedStatus(origin: string, fetchImpl: typeof fetch = fetch, timeoutMs = 15_000): Promise<DeployedStatusRead> {
55
+ const url = `${origin}${DEPLOY_STATUS_PATH}`;
56
+ let res: Response;
57
+ try {
58
+ res = await fetchImpl(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs), redirect: "manual" });
59
+ } catch (err) {
60
+ return { ok: false, url, detail: `${url} did not answer (${err instanceof Error ? err.message : String(err)})` };
61
+ }
62
+ if (res.status !== 200) return { ok: false, url, detail: `${url} answered ${res.status}, not the status document` };
63
+ let body: unknown;
64
+ try {
65
+ body = JSON.parse(await res.text());
66
+ } catch {
67
+ return { ok: false, url, detail: `${url} answered 200, but the body is not JSON` };
68
+ }
69
+ const requirements = parseStatusRequirements(body);
70
+ if (requirements === undefined) return { ok: false, url, detail: `${url} answered 200, but the body is not a status document` };
71
+ return { ok: true, url, requirements };
72
+ }
73
+
74
+ /**
75
+ * The env step's verdict from a status document. Satisfied when the deployment reports `secrets`
76
+ * as "yes" and no env row as "no". An "unknown" row does not fail the step: the route uses it for
77
+ * "not measured" (no probe registered), never for "probably not". An absent row counts as unknown.
78
+ */
79
+ export function deployedEnvVerdict(requirements: readonly DeployedRequirement[]): { satisfied: boolean; summary: string } {
80
+ const byId = new Map(requirements.map((r) => [r.id, r]));
81
+ const rows = DEPLOYED_ENV_REQUIREMENTS.map((id) => byId.get(id) ?? { id, met: "unknown" as const, detail: "not in the document" });
82
+ const satisfied = byId.get("secrets")?.met === "yes" && rows.every((r) => r.met !== "no");
83
+ return { satisfied, summary: rows.map((r) => `${r.id}=${r.met} (${r.detail})`).join("; ") };
84
+ }
package/src/engine.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  //
7
7
  // 🔴 THERE IS NO INTERACTIVE WIZARD (OPE-2233). The TTY wizard, its `WizardIO` prompt surface and
8
8
  // every step's `automate()` are retired: setup is AGENT-LED. The agent reads this engine's report
9
- // and runs the commands with the user's consent, and the ops MCP server's `scaffold_env` /
10
- // `scaffold_credential_key` tools do the writes a step's `automate()` used to do. So this engine
9
+ // and runs the commands with the user's consent. The ops MCP server's `scaffold_env` /
10
+ // `scaffold_credential_key` tools and the person's own `oe env set <NAME>` (every secret value,
11
+ // OPE-2396) do the writes a step's `automate()` used to do. So this engine
11
12
  // is READ-ONLY by construction: `check()` inspects, `guide()` describes, and nothing writes.
12
13
  //
13
14
  // Resumability is structural, not a bookkeeping flag: `check()` inspects the REAL WORLD (env vars,
@@ -39,7 +40,7 @@ export interface StepContext {
39
40
  readonly env: NodeJS.ProcessEnv;
40
41
  /** The route the steps judge against. Absent means `vercel`. */
41
42
  readonly route?: SetupRoute;
42
- /** The deployment's public origin (`--host`), for the deploy probe. */
43
+ /** The deployment's public origin (`--host`), for the deploy probe and the env step's status read. */
43
44
  readonly host?: string;
44
45
  /** The chat app's name (`--app-name`), for the Slack manifest. It wins over {@link agentName}. */
45
46
  readonly appName?: string;
package/src/env-file.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  // Thin .env.local helpers for the setup steps' `check()`s.
2
2
  //
3
3
  // 🔴 READ-ONLY SINCE OPE-2233. This module used to carry `writeEnvLocal`, which the wizard's
4
- // `automate()` steps called. Setup is agent-led now, and the ONE door that writes `.env.local` is
5
- // the ops MCP server's `scaffold_env` tool — which gates the target filename, so a value can never
6
- // land in a tracked file (agent/ops/server.ts § envTargetRefusal). A second writer here would be a
7
- // second gate to keep right.
4
+ // `automate()` steps called. Setup is agent-led now, and two doors write `.env.local`: the ops MCP
5
+ // server's `scaffold_env` tool (non-secret config) and `oe env set <NAME>` (a secret, typed in the
6
+ // person's own terminal — OPE-2396). Both pass one gate on the target filename, so a value can never
7
+ // land in a tracked file (core agent/ops/quickstart.ts § envFileTargetRefusal). A third writer here
8
+ // would be a second gate to keep right.
8
9
  //
9
10
  // SECURITY: nothing here ever returns a secret VALUE to a caller for printing — `mergedEnv` and
10
11
  // `isSet` are presence checks only. See AGENTS.md's "never log secrets" rule.
@@ -41,13 +42,14 @@ export function isSet(v: string | undefined): boolean {
41
42
  }
42
43
 
43
44
  /**
44
- * What a "missing" detail adds (OPE-2222). `--check` reads `.env.local` and the shell, and NEVER
45
- * the deployment's own environment. Setting a value on the deployment instead is a normal thing to
46
- * do, so a miss here can be a value the deployment already holds. The check does not call a vendor
47
- * CLI to find out: that needs a login and a network round trip.
45
+ * What a "missing" detail adds (OPE-2222). Without `--host`, `--check` reads `.env.local` and the
46
+ * shell, and NEVER the deployment's own environment. Setting a value on the deployment instead is a
47
+ * normal thing to do, so a miss here can be a value the deployment already holds. The check does
48
+ * not call a vendor CLI to find out: that needs a login. It names `--host` instead, which reads the
49
+ * deployment's public status route (OPE-2399, deploy-status.ts).
48
50
  */
49
51
  export function missNote(route: SetupRoute): string {
50
52
  const where = route === "docker" ? "the container's environment" : "the Vercel project";
51
53
  const list = route === "docker" ? "`docker inspect` lists the names there" : "`npx vercel env ls production` lists the names there";
52
- return `Did you set these on ${where} instead? Then ignore this miss: the check reads only .env.local and this shell, never the deployment's own env (${list})`;
54
+ return `Did you set these on ${where} instead? Re-run with --host https://<host>: the env step then also reads the deployment's own verdicts from its status route. Without --host it reads only .env.local and this shell (${list})`;
53
55
  }
package/src/scaffold.ts CHANGED
@@ -99,6 +99,27 @@ export function isScaffoldableDir(dir: string): boolean {
99
99
  return readdirSync(dir).every((name) => name === ".git");
100
100
  }
101
101
 
102
+ /**
103
+ * The nested-name case (OPE-2399). A person who clones an empty repository named `my-agent`, enters
104
+ * it, and runs `npx @open-everywhere/create my-agent` gets `my-agent/my-agent/`. The scaffold is
105
+ * correct, but it is almost never what the person wanted. So when the target is a direct child of
106
+ * `cwd` with the same name, and `cwd` counts as empty (see {@link isScaffoldableDir}), this returns
107
+ * a refusal that names the `.` command. A person who does want the nested directory creates it
108
+ * first: `cwd` then holds it, is no longer empty, and the refusal does not apply.
109
+ */
110
+ export function nestedNameRefusal(target: string, cwd: string): string | undefined {
111
+ const dir = resolve(cwd, target);
112
+ const here = resolve(cwd);
113
+ if (resolve(dir, "..") !== here || basename(dir) !== basename(here)) return undefined;
114
+ if (!isScaffoldableDir(here)) return undefined;
115
+ return [
116
+ `refusing to scaffold: you are inside an empty directory named "${basename(here)}", so this would create ${basename(here)}/${basename(dir)}/.`,
117
+ "To scaffold into this directory, run:",
118
+ " npx @open-everywhere/create .",
119
+ `To nest the app on purpose, create the directory first (\`mkdir ${basename(dir)}\`), then run this command again.`,
120
+ ].join("\n");
121
+ }
122
+
102
123
  /**
103
124
  * Copy the template into `targetDir`. Creates the directory when it is absent. Throws a
104
125
  * {@link ScaffoldRefusal} when it exists and is not an empty directory. A directory that holds only
@@ -30,7 +30,8 @@ function postgresResolution(env: NodeJS.ProcessEnv): { operational: boolean; mem
30
30
 
31
31
  /** The next action for an unsatisfied database step, per route. */
32
32
  function nextFor(route: SetupRoute): string {
33
- const write = 'write it with the ops MCP tool `scaffold_env { values: { DATABASE_URL: "<the connection string>" } }` — it selects postgres for both stores on its own';
33
+ const write =
34
+ "ask the person to run `npx oe env set DATABASE_URL` in their own terminal and paste the connection string at its hidden prompt — it selects postgres for both stores on its own";
34
35
  return route === "vercel"
35
36
  ? `add Neon from the Vercel Marketplace (\`npx vercel install neon\`, or the project's Storage → Neon → Connect; either sets DATABASE_URL on the project), then ${write}`
36
37
  : `provision a Postgres the container can reach (the deployment contract names it), then ${write}`;
@@ -72,9 +73,9 @@ export const databaseStep: SetupStep = {
72
73
  summary: "Both routes need Postgres: an ephemeral disk (Vercel) or a replaced container (Docker) loses every local store.",
73
74
  instructions: [
74
75
  vercel
75
- ? "`npx vercel install neon` provisions Neon and connects it to the linked project, which sets DATABASE_URL there. The project's Storage → Neon → Connect does the same thing."
76
+ ? "`npx vercel install neon` provisions Neon and connects it to the linked project, which sets DATABASE_URL there. The project's Storage → Neon → Connect does the same thing. The integration can also enable Neon Auth (NEON_AUTH_BASE_URL, VITE_NEON_AUTH_URL): Open Everywhere does not use it, so leave it off, or ignore those two variables."
76
77
  : "Provision a Postgres the container can reach (Neon, RDS, or your own server). The deployment contract states what it must satisfy.",
77
- "Write the same connection string into .env.local with the ops tool `scaffold_env`. It selects postgres for the operational store and the memory backing on its own — no provider selector needed. Never paste a connection string into the chat.",
78
+ "Write the same connection string into .env.local with `npx oe env set DATABASE_URL`, run in your own terminal: it reads the value from a hidden prompt. It selects postgres for the operational store and the memory backing on its own — no provider selector needed. Never paste a connection string into the chat, and never pass it to an ops tool.",
78
79
  vercel
79
80
  ? "The deployment needs it too: the Neon integration already set DATABASE_URL on the Vercel project."
80
81
  : "The deployment needs it too: DATABASE_URL is part of the container's environment.",
@@ -12,22 +12,40 @@
12
12
  import { preflight } from "@open-everywhere/core/agent/ops/quickstart.js";
13
13
  import { routeOf, type SetupStep, type StepContext } from "../engine.js";
14
14
  import { mergedEnv, missNote } from "../env-file.js";
15
+ import { normalizeHost } from "../deploy-probe.js";
16
+ import { deployedEnvVerdict, readDeployedStatus } from "../deploy-status.js";
15
17
 
16
18
  export const envChecklistStep: SetupStep = {
17
19
  id: "env-checklist",
18
20
  title: "Env var checklist",
19
- check(ctx: StepContext) {
21
+ // WITH `--host`, THE DEPLOYMENT'S OWN VERDICT COUNTS TOO (OPE-2399). A Vercel app configured in
22
+ // the dashboard holds its values only on the deployment, so the local reading alone said
23
+ // "all missing". The step then reads the public status route (deploy-status.ts). Either reading
24
+ // can satisfy the step, and the detail names which one did. A status route that cannot be read
25
+ // leaves the local verdict as it was and says why.
26
+ async check(ctx: StepContext) {
20
27
  const env = mergedEnv(ctx.cwd, ctx.env) as NodeJS.ProcessEnv;
21
28
  const route = routeOf(ctx);
22
29
  const { checks, ready } = preflight(env, { target: route });
30
+ if (ready) return { satisfied: true, detail: `all required env vars present for the ${route} route` };
23
31
  const missing = checks.filter((c) => c.status === "missing");
24
- return {
25
- satisfied: ready,
26
- detail: ready
27
- ? `all required env vars present for the ${route} route`
28
- : `missing from .env.local and this shell: ${missing.map((c) => c.name).join(", ")}. ${missNote(route)}`,
29
- next: ready ? undefined : missing.map((c) => c.fix).filter((f): f is string => Boolean(f)).join(" · "),
30
- };
32
+ const localMiss = `missing from .env.local and this shell: ${missing.map((c) => c.name).join(", ")}`;
33
+ const localNext = missing.map((c) => c.fix).filter((f): f is string => Boolean(f)).join(" · ");
34
+ const origin = ctx.host === undefined ? undefined : normalizeHost(ctx.host);
35
+ if (origin === undefined) return { satisfied: false, detail: `${localMiss}. ${missNote(route)}`, next: localNext };
36
+ const read = await readDeployedStatus(origin);
37
+ if (!read.ok) return { satisfied: false, detail: `${localMiss}. The deployment's own verdicts could not be read: ${read.detail}`, next: localNext };
38
+ const deployed = deployedEnvVerdict(read.requirements);
39
+ return deployed.satisfied
40
+ ? {
41
+ satisfied: true,
42
+ detail: `the deployment reports its env as set (${read.url}): ${deployed.summary}. The local reading differs (${localMiss}); that is expected when the values live only on the deployment`,
43
+ }
44
+ : {
45
+ satisfied: false,
46
+ detail: `the deployment reports its env as NOT set (${read.url}): ${deployed.summary}. Locally, ${localMiss}`,
47
+ next: localNext,
48
+ };
31
49
  },
32
50
  // 🔴 `guide()` IS STATIC, AND THAT IS DELIBERATE (OPE-2233). It used to dump the live preflight
33
51
  // rows, which read `process.versions.node` and the operator's `.env.local`. `guide()` is now read
@@ -48,7 +66,8 @@ export const envChecklistStep: SetupStep = {
48
66
  : "The Vercel route needs NO AI_GATEWAY_API_KEY: a deployed project authenticates to the AI Gateway with OIDC.",
49
67
  "Each chat surface adds its own names — Slack reads SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET from the environment.",
50
68
  "Not needed on a deployment: OE_PUBLIC_BASE_URL (only for a custom domain) and OE_INTAKE_PAGE_SIGNING_KEY (the deployment mints it into the credential store).",
51
- "Run `npx @open-everywhere/create --check --json`, or the ops tool `preflight`, for the live verdict per name.",
69
+ "Run `npx @open-everywhere/create --check --json`, or the ops tool `preflight`, for the live verdict per name. Both read .env.local and the shell only.",
70
+ "Set the values on the deployment instead (the Vercel dashboard, say)? Add `--host https://<host>`: the step then also reads the deployment's own verdicts from its public `/eve/v1/oe/status` route.",
52
71
  ],
53
72
  };
54
73
  },
@@ -27,6 +27,9 @@ import { isSet, mergedEnv, missNote } from "../env-file.js";
27
27
 
28
28
  const SLACK_ENV = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"] as const;
29
29
 
30
+ /** The project mark: the agent's default avatar when identity.name is unset (OPE-2408). */
31
+ const PROJECT_MARK_URL = "https://raw.githubusercontent.com/open-everywhere/open-everywhere/main/brand/png/mark-square-1024.png";
32
+
30
33
  export const slackStep: SetupStep = {
31
34
  id: "slack",
32
35
  title: "Slack app",
@@ -51,7 +54,7 @@ export const slackStep: SetupStep = {
51
54
  next:
52
55
  'open https://<host>/setup once the deployment is up (it creates and installs the Slack app from one pasted configuration ' +
53
56
  'token, with no env var to set), or create the app from the manifest the ops tool `surface_manifest { surface: "slack" }` ' +
54
- "returns and write each pasted value with `scaffold_env`",
57
+ "returns, then have the person run `npx oe env set SLACK_BOT_TOKEN` and `npx oe env set SLACK_SIGNING_SECRET` in their own terminal",
55
58
  };
56
59
  },
57
60
  guide(ctx: StepContext) {
@@ -75,8 +78,14 @@ export const slackStep: SetupStep = {
75
78
  : "the default — set identity.name in agent/settings/settings.ts to name it"
76
79
  }). A second install in one workspace needs its own name: re-run with --app-name "<name>".`,
77
80
  "Install to Workspace, then OAuth & Permissions → copy the Bot User OAuth Token (xoxb-…) = SLACK_BOT_TOKEN.",
81
+ // OPE-2408: with no name configured the agent presents as the project, so the project mark is its avatar.
82
+ `Basic Information → Display Information → App icon: ${
83
+ ctx.appName === undefined && ctx.agentName === undefined
84
+ ? "upload the project mark"
85
+ : "upload your own icon, or the project mark"
86
+ }, ${PROJECT_MARK_URL}. Slack's manifest has no icon field, so this step stays by hand.`,
78
87
  "Basic Information → App Credentials → copy the Signing Secret = SLACK_SIGNING_SECRET.",
79
- "Write both with the ops tool `scaffold_env`. Never paste a secret into the chat: the person pastes it where the tool reads it.",
88
+ "Write both with `npx oe env set SLACK_BOT_TOKEN` and `npx oe env set SLACK_SIGNING_SECRET`, run in your own terminal: each reads the value from a hidden prompt. Never paste a secret into the chat, and never pass one to an ops tool.",
80
89
  "THE SHORT WAY, once the deployment is up: open https://<host>/setup. It creates the Slack app from this same manifest with one pasted Slack app configuration token, installs it, and keeps every credential in the deployment's own encrypted store — no env var to set. This check cannot see that store, so it still reports this step unsatisfied afterwards; the status endpoint can.",
81
90
  "The manifest's URLs say YOUR-DEPLOY-URL until you deploy. Slack cannot verify Event Subscriptions before a deploy that holds SLACK_SIGNING_SECRET is up. After the deploy, paste the manifest again with your host (App Manifest → Save Changes), then retry the verification under Event Subscriptions.",
82
91
  "Note two ids for the trust edits: the workspace (team) id T… (the segment after /client/ in the workspace URL at https://app.slack.com), and your own member id U… (your Slack profile → ⋮ More → Copy member ID).",
@@ -18,7 +18,7 @@ AI_GATEWAY_API_KEY=
18
18
  # replaced Docker container starts from an empty volume, so without it core throws at the first
19
19
  # store or DM-memory use. On the Vercel route add Neon from the Marketplace
20
20
  # (`npx vercel install neon`, or project -> Storage -> Neon -> Connect): it sets DATABASE_URL on the
21
- # project. Copy the same value here so the operator tools (`npx oe secret`, the ops server's
21
+ # project. Copy the same value here (`npx oe env set DATABASE_URL`, in your own terminal) so the operator tools (`npx oe secret`, the ops server's
22
22
  # `secret_set`) write the store the deployment reads. A local run needs nothing: it uses an embedded
23
23
  # database under .open-everywhere-data/.
24
24
  DATABASE_URL=
@@ -38,6 +38,15 @@ DATABASE_URL=
38
38
  # whose credential names `broker: "db"` in the encrypted store. The Slack variables and
39
39
  # GITHUB_WEBHOOK_SECRET below are read from the ENVIRONMENT only, so they stay environment variables.
40
40
 
41
+ # --- Telemetry (https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/telemetry.md) ---
42
+ # The deployed app sends ANONYMOUS USAGE TELEMETRY, ON BY DEFAULT: event names, enums, coarse
43
+ # buckets, public model ids and the app version, with a random per-install id (service.instance.id).
44
+ # Never message or memory content, org names, workspace, channel, user or session ids, or env
45
+ # values. The page above lists every event. To turn it
46
+ # off, set this ON THE DEPLOYMENT (the Vercel project or the container's environment): a value here
47
+ # only reaches a local run, because .env.local never ships. DO_NOT_TRACK=1 does the same.
48
+ # OE_TELEMETRY=off
49
+
41
50
  # --- Slack (https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/surfaces/slack.md) ---
42
51
  # Slack app -> OAuth & Permissions -> Bot User OAuth Token (xoxb-…).
43
52
  SLACK_BOT_TOKEN=
@@ -17,17 +17,31 @@ Setup is yours to drive, with the user's consent at every command. Use the
17
17
  - **Never propose a local run as a setup step.** `npm run dev` is the development loop after the
18
18
  deploy — changing tools and skills, evals, debugging a turn. A local run has no public HTTPS URL
19
19
  for Slack to reach and a different store, so it proves little about the deployment.
20
- - **Never ask the user to paste a secret into the chat.** They paste it where a tool reads it; you
21
- write it with `scaffold_env`, or mint it with `scaffold_credential_key`, which never prints it.
20
+ - **Never ask the user to paste a secret into the chat, and never pass one to a tool.** The user
21
+ types each secret into their own terminal with `npx oe env set <NAME>` (a hidden prompt into
22
+ `.env.local`). You handle names only: `scaffold_env { secretNames }` gives the command per name,
23
+ and `scaffold_credential_key` mints the store key without printing it.
24
+ - **Tell the user about telemetry before the first deploy.** The deployed app sends anonymous usage
25
+ telemetry, and it is ON by default: event names, enums, coarse buckets, public model ids and the
26
+ app version, with a random per-install id (`service.instance.id`). It never sends message or
27
+ memory content, org names, workspace, channel, user or session ids, or env values. To turn it off, they set `OE_TELEMETRY=off` (or
28
+ `DO_NOT_TRACK=1`) on the deployment itself. The full list of events is
29
+ `https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/telemetry.md`. Ask which
30
+ they want, and do not decide for them.
22
31
  - `npx @open-everywhere/create --check --json` is the status report: every step, with `satisfied`,
23
32
  `detail` and a `next` command. Add `--route docker` for the Docker route and
24
- `--host https://<host>` to probe a deployment. The human-readable version of the same step list is
33
+ `--host https://<host>` to probe a deployment and read the env verdicts it reports for itself. The human-readable version of the same step list is
25
34
  `https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/setup-steps.md`.
26
35
 
27
36
  ## Where things live
28
37
 
29
38
  - `roster.config.ts` — what the agent can do. `roster.lock.json` is the reviewed roll-call of every
30
39
  contribution. Both are committed.
40
+ - `scripts/goldens/roster-plan.txt` — the roster review surface: one line per slot file `oe build`
41
+ writes, with the package that mounts it. The slot tree is not committed, so this file is how a
42
+ pull request shows what the agent gains or loses. `oe build` keeps it current, and
43
+ `npx oe build --check` fails when it differs from the plan. Commit it, and read its diff with the
44
+ lock's. Never edit it by hand.
31
45
  - `agent/agent.ts` — the eve agent definition.
32
46
  - `agent/` — the config: access bundles, identity and trust, memory domains, models, settings, spend
33
47
  caps, webhooks. Each file's header says what it controls.