@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,55 @@
1
+ // Step 7 — env var checklist. A rollup, not a new check: it reuses `preflight()` from
2
+ // packages/core/agent/ops/quickstart.ts — the same logic the ops MCP server's `preflight` tool
3
+ // serves — so the two frontends agree on what "ready" means. Reports presence and fix hints ONLY,
4
+ // never a value (AGENTS.md: never log a secret). Purely a report: nothing here writes.
5
+ //
6
+ // THE ROUTE IS THE PREFLIGHT TARGET (OPE-2233). `--route vercel` judges against `target: "vercel"`
7
+ // and `--route docker` against `target: "docker"`. The targets differ on exactly one check — the
8
+ // model route — because only a Vercel deployment authenticates the AI Gateway with OIDC. Passing
9
+ // one target for both would have reported "not needed on Vercel" to a Docker operator, which is
10
+ // the doc-overclaim defect class in a status report.
11
+
12
+ import { preflight } from "@open-everywhere/core/agent/ops/quickstart.js";
13
+ import { routeOf, type SetupStep, type StepContext } from "../engine.js";
14
+ import { mergedEnv, missNote } from "../env-file.js";
15
+
16
+ export const envChecklistStep: SetupStep = {
17
+ id: "env-checklist",
18
+ title: "Env var checklist",
19
+ check(ctx: StepContext) {
20
+ const env = mergedEnv(ctx.cwd, ctx.env) as NodeJS.ProcessEnv;
21
+ const route = routeOf(ctx);
22
+ const { checks, ready } = preflight(env, { target: route });
23
+ 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
+ };
31
+ },
32
+ // 🔴 `guide()` IS STATIC, AND THAT IS DELIBERATE (OPE-2233). It used to dump the live preflight
33
+ // rows, which read `process.versions.node` and the operator's `.env.local`. `guide()` is now read
34
+ // by `scripts/setup-steps-docs.ts` to GENERATE a committed reference page, so a live reading
35
+ // would make the generator's output depend on the machine that ran it. The live rows are what
36
+ // `check()` and the ops `preflight` tool are for.
37
+ guide(ctx: StepContext) {
38
+ const route = routeOf(ctx);
39
+ return {
40
+ summary:
41
+ "A presence-only checklist (it never prints a value). The full surface is in the configuration reference: " +
42
+ "https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/configuration-reference.md",
43
+ instructions: [
44
+ "Both routes need two values: DATABASE_URL and OE_CREDENTIAL_ENCRYPTION_KEY. DATABASE_URL resolves BOTH stores to postgres on its own (OPE-2230), so there is no provider selector to set — a selector pointing away from postgres is the only thing that breaks it.",
45
+ "Mint OE_CREDENTIAL_ENCRYPTION_KEY with the ops tool `scaffold_credential_key`: it writes a base64 32-byte AES-256 key into .env.local and never prints it. The encrypted credential store fails closed on every read and write without it.",
46
+ route === "docker"
47
+ ? "The Docker route also needs a model route: AI_GATEWAY_API_KEY. There is no OIDC off Vercel. Running a different model backend is an adapter swap in app code, not an env setting — https://github.com/open-everywhere/open-everywhere/blob/main/docs/extending/adapter-model.md."
48
+ : "The Vercel route needs NO AI_GATEWAY_API_KEY: a deployed project authenticates to the AI Gateway with OIDC.",
49
+ "Each chat surface adds its own names — Slack reads SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET from the environment.",
50
+ "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.",
52
+ ],
53
+ };
54
+ },
55
+ };
@@ -0,0 +1,49 @@
1
+ // The ordered step list — the ONE setup checklist. `@open-everywhere/create --check` reports it,
2
+ // the `quickstart-open-everywhere` skill reads that report, and `scripts/setup-steps-docs.ts`
3
+ // renders it into docs/deploy/setup-steps.md, so the human reference page cannot drift from the
4
+ // checks.
5
+ //
6
+ // Order: prerequisites, repo, roster, Vercel, database, Slack, env checklist, build + deploy,
7
+ // final verification. The ROSTER (OPE-258, config-driven since OPE-375) sits immediately after
8
+ // `repo`, because it is the first thing that needs `node_modules` and the last thing that should
9
+ // be discovered after a deploy: a checkout whose slot tree was never materialised boots fine and
10
+ // silently has no hands.
11
+ //
12
+ // 🔴 THE TELEMETRY-DISCLOSURE STEP IS GONE (OPE-2233). It existed to show a consent prompt, and
13
+ // the interactive wizard was the only surface that could show one. With the wizard retired the
14
+ // `@open-everywhere/create` CLI emits no telemetry at all — see docs/deploy/telemetry.md.
15
+
16
+ import type { SetupStep } from "../engine.js";
17
+ import { prerequisitesStep } from "./prerequisites.js";
18
+ import { repoStep } from "./repo.js";
19
+ import { rosterStep } from "./roster.js";
20
+ import { vercelStep } from "./vercel.js";
21
+ import { databaseStep } from "./database.js";
22
+ import { slackStep } from "./slack.js";
23
+ import { envChecklistStep } from "./env-checklist.js";
24
+ import { buildDeployStep } from "./build-deploy.js";
25
+ import { verifyStep } from "./verify.js";
26
+
27
+ export const SETUP_STEPS: readonly SetupStep[] = [
28
+ prerequisitesStep,
29
+ repoStep,
30
+ rosterStep,
31
+ vercelStep,
32
+ databaseStep,
33
+ slackStep,
34
+ envChecklistStep,
35
+ buildDeployStep,
36
+ verifyStep,
37
+ ];
38
+
39
+ export {
40
+ prerequisitesStep,
41
+ repoStep,
42
+ rosterStep,
43
+ vercelStep,
44
+ databaseStep,
45
+ slackStep,
46
+ envChecklistStep,
47
+ buildDeployStep,
48
+ verifyStep,
49
+ };
@@ -0,0 +1,50 @@
1
+ // Step 1 — prerequisites: a Node version inside @open-everywhere/core's `engines.node` range, and
2
+ // `git` on PATH. Guide-only: installing a Node version or git is an OS-level action, so the step
3
+ // states what is needed and the person (or their agent, with their consent) does it.
4
+ //
5
+ // The range is read from core's manifest (the same reader the ops `preflight` uses), so this check
6
+ // and the ops server cannot disagree about the Node floor.
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import { coreNodeRange, nodeRangeSatisfied } from "@open-everywhere/core/agent/ops/package-info.js";
10
+ import type { SetupStep } from "../engine.js";
11
+
12
+ /** Is `git` on PATH? */
13
+ function gitAvailable(): boolean {
14
+ try {
15
+ execFileSync("git", ["--version"], { stdio: "ignore" });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /** The Node verdict, split out so a test can drive any version against any range. */
23
+ export function nodeVerdict(version: string, range: string | undefined): { ok: boolean; detail: string } {
24
+ if (range === undefined) return { ok: false, detail: `node ${version} (cannot read @open-everywhere/core's engines.node — run \`npm install\`)` };
25
+ const ok = nodeRangeSatisfied(range, version);
26
+ if (ok === undefined) return { ok: false, detail: `node ${version} (cannot parse the engines range "${range}")` };
27
+ return { ok, detail: ok ? `node ${version}` : `node ${version} (need ${range})` };
28
+ }
29
+
30
+ export const prerequisitesStep: SetupStep = {
31
+ id: "prerequisites",
32
+ title: "Prerequisites (Node 24, git)",
33
+ check() {
34
+ const node = nodeVerdict(process.versions.node, coreNodeRange());
35
+ const git = gitAvailable();
36
+ const satisfied = node.ok && git;
37
+ const detail = `${node.detail}${git ? ", git present" : ", git NOT found on PATH"}`;
38
+ const next = !node.ok ? "nvm install 24 && nvm use 24" : !git ? "install git: https://git-scm.com/downloads" : undefined;
39
+ return { satisfied, detail, next };
40
+ },
41
+ guide() {
42
+ return {
43
+ summary: `Open Everywhere needs the Node range @open-everywhere/core declares (${coreNodeRange() ?? "see its engines.node"}) and git.`,
44
+ instructions: [
45
+ "Install Node 24: `nvm install 24 && nvm use 24` (or your platform's installer).",
46
+ "Install git if `git --version` fails (https://git-scm.com/downloads).",
47
+ ],
48
+ };
49
+ },
50
+ };
@@ -0,0 +1,69 @@
1
+ // Step 2 — repo: are we standing inside an Open Everywhere app with deps installed? The app comes
2
+ // from `npx @open-everywhere/create <dir>` (the scaffold, src/scaffold.ts), or from a clone or fork
3
+ // of the repository. This step never clones anything, and since OPE-2233 it never installs
4
+ // anything either: the agent runs `npm install` in the user's shell, which runs the app's own
5
+ // `postinstall` (`oe-eve-patch`) and patches the installed eve.
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { SetupStep, StepContext } from "../engine.js";
10
+
11
+ interface AppState {
12
+ /** True when the app is here AND its deps are installed — the step's satisfied state. */
13
+ readonly ok: boolean;
14
+ /** True when the directory is an Open Everywhere app, installed or not. */
15
+ readonly app: boolean;
16
+ readonly detail: string;
17
+ }
18
+
19
+ /**
20
+ * What actually makes a directory an Open Everywhere app: eve's agent entry (`agent/agent.ts`
21
+ * — eve discovers the graph by filesystem convention, so nothing runs without it) plus a
22
+ * `package.json` that reaches `@open-everywhere/core`, either as a dependency or through the
23
+ * monorepo's workspaces.
24
+ *
25
+ * 🔴 It deliberately does NOT look for `AGENTS.md`. Nothing at runtime reads that file, and it is
26
+ * the contributor guide, so a scaffolded consumer app or a fork that deleted it would fail. Sniff
27
+ * for the thing that has to be there for the product to boot.
28
+ */
29
+ function inspectApp(cwd: string): AppState {
30
+ const pkgPath = join(cwd, "package.json");
31
+ const agentEntry = join(cwd, "agent", "agent.ts");
32
+ if (!existsSync(pkgPath)) return { ok: false, app: false, detail: "no package.json here — not an Open Everywhere app" };
33
+ if (!existsSync(agentEntry)) {
34
+ return { ok: false, app: false, detail: "no agent/agent.ts here — not an Open Everywhere app (eve finds the agent graph by convention)" };
35
+ }
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
38
+ dependencies?: Record<string, string>;
39
+ devDependencies?: Record<string, string>;
40
+ workspaces?: string[];
41
+ };
42
+ const hasCoreDep = Boolean(pkg.dependencies?.["@open-everywhere/core"] ?? pkg.devDependencies?.["@open-everywhere/core"]);
43
+ const hasWorkspaces = Array.isArray(pkg.workspaces) && pkg.workspaces.some((w) => w.includes("packages"));
44
+ if (!hasCoreDep && !hasWorkspaces) return { ok: false, app: false, detail: "package.json here doesn't reach @open-everywhere/core" };
45
+ } catch {
46
+ return { ok: false, app: false, detail: "package.json here is unreadable" };
47
+ }
48
+ const installed = existsSync(join(cwd, "node_modules"));
49
+ return { ok: installed, app: true, detail: installed ? "app found, deps installed" : "app found, but run `npm install`" };
50
+ }
51
+
52
+ export const repoStep: SetupStep = {
53
+ id: "repo",
54
+ title: "Repo (app + install)",
55
+ check(ctx: StepContext) {
56
+ const { ok, app, detail } = inspectApp(ctx.cwd);
57
+ return { satisfied: ok, detail, next: ok ? undefined : app ? "npm install" : "npx @open-everywhere/create my-agent && cd my-agent" };
58
+ },
59
+ guide() {
60
+ return {
61
+ summary: "Get an Open Everywhere app with deps installed.",
62
+ instructions: [
63
+ "Easiest: `npx @open-everywhere/create my-agent` scaffolds the quickstart app into ./my-agent and prints the next steps.",
64
+ "Or clone the repository: `git clone https://github.com/open-everywhere/open-everywhere && cd open-everywhere && npm install`.",
65
+ "Then re-run this check FROM inside that app.",
66
+ ],
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,111 @@
1
+ // Step 3 — the roster: does this checkout declare what its agent can do, and is the materialised
2
+ // eve slot tree in step with that declaration?
3
+ //
4
+ // THIS IS THE SCAFFOLD-TIME HALF of OPE-375 (it replaces OPE-258's shim-counting step). eve
5
+ // discovers tools, channels, hooks, schedules, skills and connections by FILESYSTEM CONVENTION —
6
+ // one file per contribution, named after the file — but those six directories are now BUILD OUTPUT:
7
+ // `oe build` writes them from `roster.config.ts` (the declaration) plus `roster.lock.json` (the
8
+ // reviewed roll-call). Both of those are committed; the tree is gitignored.
9
+ //
10
+ // 🔴 THE FAILURE THIS STEP EXISTS TO CATCH IS SILENT. eve does not error on an empty `agent/tools/`
11
+ // — it mounts nothing and boots green. A fresh clone, a `git clean -xdf`, or a build command that
12
+ // skipped the `prebuild` hook therefore yields an agent with NO HANDS that looks perfectly healthy.
13
+ // So the check is not "does the config exist" alone; it is `oe build --check`, which asserts the
14
+ // on-disk tree byte-equals what the declaration plans.
15
+ //
16
+ // 🔴 THIS STEP NEVER RUNS `oe build --accept`, AND SINCE OPE-2233 IT RUNS NOTHING AT ALL. It
17
+ // reports and hands over the command. `--accept` rewrites the lock, which is the act of REVIEWING
18
+ // a capability: deciding what the agent may do belongs to the operator, never to a setup tool
19
+ // acting on their behalf.
20
+
21
+ import { execFileSync } from "node:child_process";
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
24
+ import type { SetupStep, StepContext } from "../engine.js";
25
+
26
+ /** What this step knows about a checkout, or why it can't tell. */
27
+ type RosterState =
28
+ | { readonly kind: "unknown"; readonly detail: string }
29
+ | { readonly kind: "known"; readonly detail: string; readonly clean: boolean };
30
+
31
+ /**
32
+ * The `oe` CLI as installed in the target checkout, as a command and its leading argv; `undefined`
33
+ * when deps are not installed yet.
34
+ *
35
+ * It runs core's declared `oe` bin script with the CURRENT Node, never the `node_modules/.bin` shim.
36
+ * On Windows that shim is `oe.cmd`, and Node refuses to spawn a `.cmd` without a shell
37
+ * (CVE-2024-27980), so the check always failed there and the step reported every Windows checkout as
38
+ * unmaterialised (OPE-2211). Reading the bin from core's own manifest works the same on every platform.
39
+ */
40
+ function oeBin(cwd: string): { readonly command: string; readonly args: readonly string[] } | undefined {
41
+ const manifest = join(cwd, "node_modules", "@open-everywhere", "core", "package.json");
42
+ if (!existsSync(manifest)) return undefined;
43
+ let rel: unknown;
44
+ try {
45
+ const pkg = JSON.parse(readFileSync(manifest, "utf8")) as { bin?: string | Record<string, string> };
46
+ rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.oe;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ if (typeof rel !== "string") return undefined;
51
+ const script = join(dirname(manifest), rel);
52
+ return existsSync(script) ? { command: process.execPath, args: [script] } : undefined;
53
+ }
54
+
55
+ /**
56
+ * Inspect the roster. Every "cannot tell" outcome is UNKNOWN and unsatisfied — never a pass. A step
57
+ * that treated a missing CLI as "nothing to do" would report a hands-free checkout as correctly
58
+ * configured, which is the one answer that must not be reachable.
59
+ */
60
+ export function inspectRoster(cwd: string): RosterState {
61
+ if (!existsSync(join(cwd, "roster.config.ts"))) {
62
+ return { kind: "unknown", detail: "roster.config.ts is missing — every Open Everywhere app declares its roster there" };
63
+ }
64
+ if (!existsSync(join(cwd, "roster.lock.json"))) {
65
+ return {
66
+ kind: "known",
67
+ clean: false,
68
+ detail: "roster.lock.json is missing — nothing has been REVIEWED, so `oe build` will refuse to mount anything",
69
+ };
70
+ }
71
+ const bin = oeBin(cwd);
72
+ if (bin === undefined) return { kind: "unknown", detail: "the `oe` CLI is not installed — run `npm install` first (the repo step covers this)" };
73
+ try {
74
+ execFileSync(bin.command, [...bin.args, "build", "--check"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
75
+ return { kind: "known", clean: true, detail: "the materialised slot tree matches roster.config.ts + roster.lock.json" };
76
+ } catch (err) {
77
+ const e = err as { stdout?: string; stderr?: string };
78
+ const first = `${e.stderr ?? ""}${e.stdout ?? ""}`.split("\n").find((l) => l.includes("·"))?.trim();
79
+ return { kind: "known", clean: false, detail: first ?? "`oe build --check` reports the tree does not match the roster declaration" };
80
+ }
81
+ }
82
+
83
+ /** The command that clears a roster red, read off the red itself. */
84
+ function rosterNext(detail: string): string {
85
+ if (/npm install/.test(detail)) return "npm install";
86
+ if (/UNREVIEWED|missing — nothing has been REVIEWED/.test(detail)) return "review the contribution in roster.config.ts, then `npx oe build --accept` and read the roster.lock.json diff";
87
+ if (/NOT BUILT/.test(detail)) return "npm run build";
88
+ return "npx oe build";
89
+ }
90
+
91
+ export const rosterStep: SetupStep = {
92
+ id: "roster",
93
+ title: "Roster (roster.config.ts → the eve slot tree)",
94
+ check(ctx: StepContext) {
95
+ const state = inspectRoster(ctx.cwd);
96
+ const satisfied = state.kind === "known" && state.clean;
97
+ return { satisfied, detail: state.detail, next: satisfied ? undefined : rosterNext(state.detail) };
98
+ },
99
+ guide() {
100
+ return {
101
+ summary: "`roster.config.ts` declares what your agent can do; `oe build` materialises the eve slot tree from it.",
102
+ instructions: [
103
+ "roster.config.ts is terse: which packages contribute, which contributions this deployment EXCLUDES (with a reason), and which eve built-ins it takes away.",
104
+ "roster.lock.json is the expanded, REVIEWED roll-call. Both are committed — the slot tree they generate is not.",
105
+ "An upgrade that ships a NEW tool fails the build until you decide about it, then `npx oe build --accept` records the decision. There is no bypass flag.",
106
+ "`npx oe build --show` prints the tree it would write, without writing it; `npm run lint:roster` is the gate.",
107
+ "eve mounts NOTHING (and still boots) if the tree is missing — so never skip the build step.",
108
+ ],
109
+ };
110
+ },
111
+ };
@@ -0,0 +1,74 @@
1
+ // Step 6 — Slack app creation. Manifest output plus token-collection guidance (the minimal path
2
+ // from the Slack surface guide). App creation itself is necessarily manual (Slack's UI, and
3
+ // workspace-admin approval), so this step guides and then verifies by env presence.
4
+ //
5
+ // The manifest itself lives in the Slack bundle (OPE-1603) — @open-everywhere/provider-slack/
6
+ // agent/app-manifest.js — so this step and the ops MCP tool `surface_manifest` print the SAME
7
+ // document. The bundle resolves SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET ENV-FIRST (the block
8
+ // below has the second home), so an operator who sets them by hand puts both in `.env.local` and
9
+ // then on the deployment.
10
+ //
11
+ // REQUIRED ON BOTH ROUTES (OPE-2233). The old `local` exemption is gone with the local goal: a
12
+ // deployment with no chat surface configured answers nobody, whichever route it took.
13
+ //
14
+ // 🔴 THERE ARE TWO HOMES SINCE OPE-2231, AND THIS CHECK READS ONE. The bundle resolves both
15
+ // secrets ENV-FIRST and then from the deployment's own encrypted credential store, which is where
16
+ // a `/setup` run puts them — so a deployment configured that way is CORRECT and this step still
17
+ // reports it unsatisfied, because `.env.local` and the shell are all it can read. That makes the
18
+ // wording load-bearing: the detail says "missing from .env.local and this shell", never a bare
19
+ // "missing", and it names `/setup` and the status endpoint as the places that can see the store.
20
+ // This honesty rode the retired `quickstart` plan's Slack step before OPE-2233 retired it; it
21
+ // belongs here now, because this is the one ordering left.
22
+
23
+ import { SLACK_DEFAULT_APP_NAME, slackAppManifestTemplate } from "@open-everywhere/provider-slack/agent/app-manifest.js";
24
+ import { docUrl } from "@open-everywhere/core/agent/ops/quickstart.js";
25
+ import { routeOf, type SetupStep, type StepContext } from "../engine.js";
26
+ import { isSet, mergedEnv, missNote } from "../env-file.js";
27
+
28
+ const SLACK_ENV = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"] as const;
29
+
30
+ export const slackStep: SetupStep = {
31
+ id: "slack",
32
+ title: "Slack app",
33
+ check(ctx: StepContext) {
34
+ const env = mergedEnv(ctx.cwd, ctx.env);
35
+ const missing = SLACK_ENV.filter((name) => !isSet(env[name]));
36
+ if (missing.length === 0) {
37
+ return {
38
+ satisfied: true,
39
+ detail:
40
+ "SLACK_BOT_TOKEN + SLACK_SIGNING_SECRET set in the environment. 🔴 Setting these two makes the deployment IGNORE anything " +
41
+ "/setup stored, and CLOSES /setup: the resolver is env-first, so landing that page cannot change which Slack app a working " +
42
+ "deployment speaks as",
43
+ };
44
+ }
45
+ return {
46
+ satisfied: false,
47
+ detail:
48
+ `missing from .env.local and this shell: ${missing.join(", ")}. This check reads those two homes only — a deployment ` +
49
+ "configured by /setup keeps both secrets in its own encrypted credential store, and reports unsatisfied here while working " +
50
+ `correctly. The status endpoint and /setup itself are what can see the store. ${missNote(routeOf(ctx))}`,
51
+ next:
52
+ 'open https://<host>/setup once the deployment is up (it creates and installs the Slack app from one pasted configuration ' +
53
+ '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`",
55
+ };
56
+ },
57
+ guide(ctx: StepContext) {
58
+ return {
59
+ summary: "Create a Slack app from the manifest below, then copy its bot token and signing secret.",
60
+ instructions: [
61
+ "https://api.slack.com/apps → Create New App → From an app manifest → pick the workspace → JSON → paste:",
62
+ slackAppManifestTemplate(ctx.appName === undefined ? {} : { appName: ctx.appName }),
63
+ `The app is named "${ctx.appName ?? SLACK_DEFAULT_APP_NAME}". A second install in one workspace needs its own name: re-run with --app-name "<name>".`,
64
+ "Install to Workspace, then OAuth & Permissions → copy the Bot User OAuth Token (xoxb-…) = SLACK_BOT_TOKEN.",
65
+ "Basic Information → App Credentials → copy the Signing Secret = SLACK_SIGNING_SECRET.",
66
+ "Write both with the ops tool `scaffold_env`. Never paste a secret into the chat: the person pastes it where the tool reads it.",
67
+ "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.",
68
+ "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.",
69
+ "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).",
70
+ `Full walkthrough: ${docUrl("docs/deploy/surfaces/slack.md")}.`,
71
+ ],
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,40 @@
1
+ // Step 4 — Vercel project link. `.vercel/project.json` is what `vercel link`/`vercel deploy` writes
2
+ // once a directory is linked to a Vercel project; its presence is a real-world, resumable signal.
3
+ // Guide-only, like every step since OPE-2233: the agent runs `vercel link` in the user's shell.
4
+ //
5
+ // The DOCKER route needs no Vercel project, so the step is satisfied there (docs/deploy/contract.md
6
+ // is that route's reference).
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { routeOf, type SetupStep, type StepContext } from "../engine.js";
11
+
12
+ export const vercelStep: SetupStep = {
13
+ id: "vercel",
14
+ title: "Vercel project link",
15
+ check(ctx: StepContext) {
16
+ if (routeOf(ctx) === "docker") return { satisfied: true, detail: "not needed on the Docker route (--route docker)" };
17
+ const p = join(ctx.cwd, ".vercel", "project.json");
18
+ if (!existsSync(p)) return { satisfied: false, detail: "no .vercel/project.json — not linked yet", next: "npx vercel link" };
19
+ try {
20
+ const linked = JSON.parse(readFileSync(p, "utf8")) as { projectId?: string };
21
+ return linked.projectId
22
+ ? { satisfied: true, detail: "linked to a Vercel project" }
23
+ : { satisfied: false, detail: ".vercel/project.json is malformed", next: "npx vercel link" };
24
+ } catch {
25
+ return { satisfied: false, detail: ".vercel/project.json is unreadable", next: "npx vercel link" };
26
+ }
27
+ },
28
+ guide(ctx: StepContext) {
29
+ if (routeOf(ctx) === "docker") {
30
+ return {
31
+ summary: "The Docker route needs no Vercel project.",
32
+ instructions: ["Nothing to do: `--route docker` skips this step. The deployment contract is the Docker route's reference."],
33
+ };
34
+ }
35
+ return {
36
+ summary: "Link this app to a Vercel project. Every later `vercel` command acts on the linked project.",
37
+ instructions: ["`npx vercel link` — follow the prompts (device auth in your browser)."],
38
+ };
39
+ },
40
+ };
@@ -0,0 +1,119 @@
1
+ // Step 9 — final verification. A report, not a new check: it re-runs the substantive steps' own
2
+ // `check()`s (reuse over reimplementation) and rolls them into one go/no-go line, mirroring
3
+ // docs/deploy/setup-runbook.md's own "verify before going live" section. This step's only job is
4
+ // to tell you the truth.
5
+
6
+ import { routeOf, type SetupStep, type StepContext } from "../engine.js";
7
+ import { prerequisitesStep } from "./prerequisites.js";
8
+ import { repoStep } from "./repo.js";
9
+ import { slackStep } from "./slack.js";
10
+ import { databaseStep } from "./database.js";
11
+ import { buildDeployStep } from "./build-deploy.js";
12
+ import { rosterStep } from "./roster.js";
13
+ import { envChecklistStep } from "./env-checklist.js";
14
+
15
+ /**
16
+ * What the DEPLOYMENT's own environment must carry. Named here, in the one step whose job is to
17
+ * say whether setup is finished, because no `check()` in this engine can observe it: `.env.local`
18
+ * is gitignored and never ships. DATABASE_URL is on the list because the Vercel route's Neon
19
+ * integration sets it on the project while the Docker route's operator sets it by hand.
20
+ *
21
+ * 🔴 THE TWO STORE SELECTORS ARE NOT ON THIS LIST (OPE-2230). A present `DATABASE_URL` resolves
22
+ * both stores to postgres on its own, so naming them here would send an operator to set two
23
+ * variables that change nothing — the exact fix list that ticket removed.
24
+ */
25
+ const DEPLOYMENT_ENV_NAMES = ["DATABASE_URL", "OE_CREDENTIAL_ENCRYPTION_KEY", "SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"] as const;
26
+
27
+ /**
28
+ * The go/no-go rule, split out from `check()` so it can be tested on its own. That split is
29
+ * not ceremony: every real `check()` in a scratch directory fails on repo/slack/build too, so
30
+ * a test driven through `verifyStep.check` reports "not ready" no matter what this rule says,
31
+ * and an assertion on it passes for the wrong reason. Dropping a term here has to be provable.
32
+ *
33
+ * `database` earns its seat because a roll-up that omits it can say "ready" about an instance
34
+ * that loses every memory and every audit row on its next deploy — this check's most
35
+ * consequential possible lie. Since OPE-2233 both routes need Postgres, so the term bites any
36
+ * checkout still on the embedded floor.
37
+ */
38
+ export function verifyVerdict(parts: {
39
+ readonly prereq: boolean;
40
+ readonly repo: boolean;
41
+ readonly slack: boolean;
42
+ readonly database: boolean;
43
+ readonly build: boolean;
44
+ /** The roster tree and the env checklist. Optional so the five-term callers keep their shape. */
45
+ readonly roster?: boolean;
46
+ readonly env?: boolean;
47
+ }): { satisfied: boolean; detail: string } {
48
+ const failing = [
49
+ !parts.prereq && "prerequisites",
50
+ !parts.repo && "repo",
51
+ parts.roster === false && "roster",
52
+ parts.env === false && "env vars",
53
+ !parts.slack && "slack (or another surface)",
54
+ !parts.database && "durable state backing",
55
+ !parts.build && "build",
56
+ ].filter((x): x is string => Boolean(x));
57
+ return {
58
+ satisfied: failing.length === 0,
59
+ detail: failing.length === 0
60
+ ? "ready: every step this check can see is in place"
61
+ : `not ready yet: ${failing.join(", ")}`,
62
+ };
63
+ }
64
+
65
+ export const verifyStep: SetupStep = {
66
+ id: "verify",
67
+ title: "Final verification",
68
+ async check(ctx: StepContext) {
69
+ const [prereq, repo, roster, slack, database, env, build] = await Promise.all([
70
+ prerequisitesStep.check(ctx),
71
+ repoStep.check(ctx),
72
+ rosterStep.check(ctx),
73
+ slackStep.check(ctx),
74
+ databaseStep.check(ctx),
75
+ envChecklistStep.check(ctx),
76
+ buildDeployStep.check(ctx),
77
+ ]);
78
+ const verdict = verifyVerdict({
79
+ prereq: prereq.satisfied,
80
+ repo: repo.satisfied,
81
+ roster: roster.satisfied,
82
+ slack: slack.satisfied,
83
+ database: database.satisfied,
84
+ env: env.satisfied,
85
+ build: build.satisfied,
86
+ });
87
+ if (!verdict.satisfied) return { ...verdict, next: "follow the `next` of each unsatisfied step above, then re-run this check" };
88
+ // 🔴 WHAT "READY" DOES NOT MEAN (OPE-2233 review). Every step above reads `.env.local` and this
89
+ // shell; NONE of them reads the deployment's own environment, and the build+deploy probe passes
90
+ // on the chat route's own `401 unauthorized`, which that route returns WITHOUT its signing
91
+ // secret. So a full house here is reachable for a deployment whose credential store fails
92
+ // closed on every read and whose Slack app can never verify. The retired `quickstart` plan
93
+ // carried a permanently-`todo` env-push step that said this by construction; a satisfied step
94
+ // cannot, so the satisfied `next` says it instead.
95
+ return {
96
+ ...verdict,
97
+ next:
98
+ "set the deployment's OWN environment — this check never reads it: " +
99
+ `${DEPLOYMENT_ENV_NAMES.join(", ")} (${routeOf(ctx) === "docker" ? "the container's environment" : "the Vercel project's Production environment"}). ` +
100
+ "Then confirm the Slack app's Event Subscriptions shows the request URL as Verified (that proves SLACK_SIGNING_SECRET on the deployment; this check cannot), and send the app a direct message",
101
+ };
102
+ },
103
+ guide(ctx: StepContext) {
104
+ const vercel = routeOf(ctx) === "vercel";
105
+ return {
106
+ summary: "Before going live, run a real task in a pilot channel and walk the setup runbook's safety checklist (access bundles, egress enforcement, spend caps).",
107
+ instructions: [
108
+ `🔴 THIS CHECK NEVER READS THE DEPLOYMENT'S OWN ENVIRONMENT. Every step above reads .env.local and your shell. \`.env.local\` is gitignored and never ships, so the deployment reads only what you set ON it: ${DEPLOYMENT_ENV_NAMES.join(", ")}. A full house here says nothing about any of them.`,
109
+ vercel
110
+ ? "Set each one for Production without printing it: `npm run -s env:pipe -- <NAME> | npx vercel env add <NAME> production`. Always pipe it straight into `vercel env add`; never run `env:pipe` bare, because a shell that is not a terminal prints the value. The Neon integration already set DATABASE_URL there, and it selects postgres for both stores on its own — no provider selector to push. A variable added after a deploy reaches the functions only on the NEXT deploy."
111
+ : "Set each one in the container's environment (a compose file, your orchestrator's secret store — not a tracked file). DATABASE_URL selects postgres for both stores on its own. A variable added after the container starts reaches the app only on the next start.",
112
+ "Mint OE_CREDENTIAL_ENCRYPTION_KEY with the ops tool `scaffold_credential_key`, which writes it to .env.local and never prints it. Without it the encrypted credential store fails closed on every read and write.",
113
+ "Not needed on a deployment: the two store selectors (DATABASE_URL resolves both), OE_PUBLIC_BASE_URL (only for a custom domain) and OE_INTAKE_PAGE_SIGNING_KEY (the deployment mints it into the credential store). On the Vercel route, AI_GATEWAY_API_KEY is not needed either — OIDC covers it.",
114
+ "Send the app a direct message in Slack, then @mention it in a channel you invited it to.",
115
+ "Safety checklist: https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/setup-runbook.md#safety-checklist-the-anti-footgun-list",
116
+ ],
117
+ };
118
+ },
119
+ };
package/src/version.ts ADDED
@@ -0,0 +1,20 @@
1
+ // This package's own version, read from its package.json at runtime (never hand-duplicated) —
2
+ // used by the CLI's `--version` flag, the `--check` report, and the scaffold, which stamps it into
3
+ // every template file that carries the version placeholder. Falls back to "0.0.0" if the read ever
4
+ // fails (a packaged or renamed layout, a missing file) rather than throwing: a version string must
5
+ // never crash a scaffold.
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ function readVersion(): string {
11
+ try {
12
+ const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
13
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
14
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "0.0.0";
15
+ } catch {
16
+ return "0.0.0";
17
+ }
18
+ }
19
+
20
+ export const CLI_VERSION = readVersion();