eve-hub 0.5.6 → 0.6.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.
- package/CHANGELOG.md +19 -0
- package/README.md +5 -4
- package/execution-context.mjs +79 -0
- package/fork-env.mjs +3 -2
- package/host-browser.js +3 -1
- package/host-sandbox.d.ts +1 -2
- package/host-sandbox.js +10 -5
- package/{schedules/classify.d.ts → instructions/engineering.d.ts} +1 -2
- package/instructions/engineering.js +6 -0
- package/main-instructions.mjs +13 -6
- package/memory/workspace.js +8 -3
- package/memory.mjs +78 -24
- package/package.json +9 -2
- package/prepare.mjs +219 -0
- package/registry-fetch.mjs +7 -0
- package/schedules/dash-classify.mjs +86 -39
- package/schedules/dash-pass.mjs +51 -0
- package/schedules/dash-rules.mjs +9 -2
- package/schedules/dash-run.mjs +16 -7
- package/schedules/dash.js +14 -16
- package/schedules/hub.d.mts +2 -1
- package/schedules/hub.mjs +4 -1
- package/schedules/lane-facts.mjs +134 -0
- package/schedules/thread-brief.mjs +4 -2
- package/schedules/track-summaries.mjs +108 -0
- package/stubs.mjs +46 -0
- package/subagent-cap.d.mts +4 -3
- package/templates/engineering.md +24 -0
- package/templates/execution-context-hosted-main.md +4 -0
- package/templates/execution-context-local-main.md +7 -0
- package/templates/execution-context-local.md +4 -0
- package/templates/main-instructions.md +5 -11
- package/templates/workspace-protocol.md +9 -7
- package/tools/apply_tags.d.ts +6 -6
- package/tools/apply_tags.js +4 -4
- package/tools/create_agent.d.ts +2 -0
- package/tools/create_agent.js +1 -0
- package/tools/create_thread.d.ts +4 -4
- package/tools/create_thread.js +5 -5
- package/tools/dash_update.d.ts +4 -4
- package/tools/dash_update.js +7 -7
- package/tools/list_threads.d.ts +2 -2
- package/tools/list_threads.js +2 -2
- package/tools/post_message.d.ts +18 -0
- package/tools/post_message.js +11 -5
- package/tools/tag_threads.d.ts +6 -6
- package/tools/tag_threads.js +4 -4
- package/tools/update_thread.d.ts +4 -4
- package/tools/update_thread.js +4 -4
- package/vendor/world-turso.mjs +6416 -0
- package/vercel.mjs +15 -0
- package/workspace.mjs +39 -3
- package/world.mjs +27232 -0
- package/schedules/classify.js +0 -20
- package/schedules/tracks-lead.mjs +0 -107
- package/schedules/tracks.d.ts +0 -11
- package/schedules/tracks.js +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Every bot gets an execution-context prompt (`agents/<bot>/agent/instructions/execution-context.ts`), chosen by its definition in `hub.workspace.json`: `runsOn` on the agent, else a workspace-wide `"runsOn"` (a local hub's workspace declares `"local"`; a deployable repo declares `"hosted"`), else hosted. A local bot's comes from the kit's templates; the Vercel build writes a hosted bot's from what it produced: the eve-hub version, the roster and which bots run on the Mac, the deployment, team and repo commit (or that the hub uploaded the files), the kit tools it removed, and the dashboard links to its logs. The shared prompts keep only what is true in both contexts: main's restart, team, memory and Dash-schedule paragraphs, and the image and local-file link rules, moved into the per-context pages. `hub workspace sync` declares `"runsOn": "local"` in a workspace file that declares nothing.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- A workspace repo carries its bots, memory and workspace file; the Vercel build prepares it (eve-hub/vercel), including bots that run on the Mac (`runsOn: "local"`). The Mac runner downloads the deployed commit through the hub instead of reading a folder.
|
|
14
|
+
|
|
15
|
+
## 0.5.7
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- One `dash` schedule (every 8 minutes, and when a bot's turn ends) replaces `dash`, `classify` and `tracks`: one GitHub query, a model call per thread whose facts changed and a model call per track with news. The `classify.ts` and `tracks.ts` stubs are retired; the track-writer bot is no longer woken.
|
|
20
|
+
- `post_message` takes a reviewer's `verdict` (ship, fix-first, rethink) and `decisions`; a ship without a change-explainer for the PR shows "no explainer" on the Dash.
|
|
21
|
+
|
|
3
22
|
## 0.5.6
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ The bot-side code every hub agent shares (milestone M3 in [`docs/architecture`](
|
|
|
8
8
|
| `tools/create_thread.ts`, `post_message.ts`, `archive_thread.ts`, `unarchive_thread.ts`, `list_agents.ts`, `create_agent.ts`, `remove_agent.ts`, `restart_agent.ts`, `agent_send.ts`, `tag_threads.ts`, `apply_tags.ts` | The orchestrator's tools | `main`'s `agent/tools/<same name>.ts` |
|
|
9
9
|
| `hooks/sandbox.ts` | Migrates legacy Docker sessions to the host sandbox (`defineHook`) | `agent/hooks/sandbox.ts` |
|
|
10
10
|
| `instructions/main.ts`, `main-instructions.mjs` | main's instructions (`defineInstructions`), rendered at build time from the kit's `templates/main-instructions.md` + `workspace-protocol.md`, so no workspace carries a copy | `main`'s `agent/instructions.ts`; workspace-only additions in `agent/instructions/*.md` |
|
|
11
|
-
| `instructions/protocol.ts` | The shared protocol alone, rendered from `templates/workspace-protocol.md` | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md` (who the bot is) |
|
|
12
|
-
| `
|
|
11
|
+
| `instructions/protocol.ts` | The shared protocol alone, rendered from `templates/workspace-protocol.md` followed by `templates/engineering.md` | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md` (who the bot is) |
|
|
12
|
+
| `instructions/engineering.ts` | The engineering page alone (`templates/engineering.md`), for a subagent that doesn't carry the protocol | the subagent's `instructions/engineering.ts` |
|
|
13
|
+
| `memory/workspace.ts`, `memory.mjs` | an eve memory slot (default export, constant `workspace` scope) whose provider recalls the workspace's `memory/<id>.md` entries (plus the `memory/<domain>/` folders the bot lists as `domains` in its roster entry), one message each, at every turn start and after compaction, only the ones whose text changed since the session last saw them, as new messages after the history; `validateMemory(root)` for a workspace's tests | every bot's (main too) `agent/memory/workspace.ts`: `export { default } from "eve-hub/memory/workspace"` |
|
|
13
14
|
| `memory/bot.ts` | the same for the bot's own `agents/<bot>/memory/<id>.md` entries; the bot is found from its directory (`findBotDir`), scope is its name | every bot's `agent/memory/bot.ts`: `export { default } from "eve-hub/memory/bot"` |
|
|
14
15
|
| `channels/eve-roster-user.ts` | `eveChannel({ auth: [rosterUser, localDev()] })`: eve's default steer policy, and the registry's `x-roster-user` human as a user principal | every bot's `agent/channels/eve.ts` |
|
|
15
16
|
| `channels/eve.ts` | Re-exports `eve-roster-user.ts`, for stubs written before every bot shared one channel | older `agent/channels/eve.ts` stubs |
|
|
@@ -56,11 +57,11 @@ Add a changeset (`pnpm changeset`, pick `eve-hub`) when a tool's name, schema, o
|
|
|
56
57
|
|
|
57
58
|
## Published package (deployed workspaces)
|
|
58
59
|
|
|
59
|
-
A deployed workspace (
|
|
60
|
+
A deployed workspace (lib/workspace-template.mjs, staged by hosted-hub/scripts/stage-workspace.mjs) has no hub checkout to link, so it installs the kit from npm, published as [`eve-hub`](https://www.npmjs.com/package/eve-hub): `"eve-hub": "<version>"`. `node packages/agent-kit/scripts/pack.mjs <outDir>` builds the package: every `.ts` module compiled to `.js` + `.d.ts` in place (node won't strip types under `node_modules`), the `exports` map pointed at them, and `templates/` included.
|
|
60
61
|
|
|
61
62
|
Releases go through Changesets, as in eve. A PR that changes the kit adds a changeset (`pnpm changeset`) and applies it (`pnpm changeset version`); when it merges, `.github/workflows/release.yml` runs `changeset publish` (no bot commits to main: its ruleset allows only PR merges): the kit's `prepack` stages the package into `dist/npm` (its `publishConfig.directory`), npm gets it, and the `eve-hub@<version>` GitHub release is created. npm auth is the repo's `NPM_TOKEN` secret (an npm automation token). The template pins the version this checkout carries.
|
|
62
63
|
|
|
63
|
-
The template's main gets a Vercel Sandbox and only the kit stubs that run without the hub's machine (lib/workspace-template.mjs `DEPLOYED_KIT_FILES`; eve's own bash replaces the kit's). The published tarball (`scripts/pack.mjs`) leaves out the kit__worker extension, which mounts only on the hub's machine. The hub reaches each bot's eve channel with `Authorization: Bearer $BOT_TOKEN` (`bot-token.mjs`); the kit's calls to the hub (`registry-fetch.mjs`, at `ROSTER_REGISTRY_URL`) carry `Authorization: Bearer $HUB_TOKEN` and, since the hosted hub keeps Vercel Authentication on, `x-vercel-protection-bypass: $HUB_PROTECTION_BYPASS` (the hub project's automation bypass secret), each when set.
|
|
64
|
+
The template's main gets a Vercel Sandbox and only the kit stubs that run without the hub's machine (lib/workspace-template.mjs `DEPLOYED_KIT_FILES`; eve's own bash replaces the kit's). The template's builder also mounts eve-code (`eve/extensions/code`: `apply_patch`, `grep`, the review skills, a read-only worker) and `eve/extensions/git` (`git({})`), both with no `github` option. The bot uses `gh` and `git` from bash, whose GitHub token is the sandbox firewall's (`VERCEL_SANDBOX`), and no `gh` tool is contributed. A deployed workspace's eve is the one pin in `hosted-hub/bots/package.json`, which staging copies over the template's (an eve snapshot with both `eve/world-hub` and `eve/extensions/git`; `hosted-hub/test/stage-workspace.test.mjs` checks the staged imports resolve against it). The hub and local bots stay on the hub's eve. The published tarball (`scripts/pack.mjs`) leaves out the kit__worker extension, which mounts only on the hub's machine. The hub reaches each bot's eve channel with `Authorization: Bearer $BOT_TOKEN` (`bot-token.mjs`); the kit's calls to the hub (`registry-fetch.mjs`, at `ROSTER_REGISTRY_URL`) carry `Authorization: Bearer $HUB_TOKEN` and, since the hosted hub keeps Vercel Authentication on, `x-vercel-protection-bypass: $HUB_PROTECTION_BYPASS` (the hub project's automation bypass secret), each when set.
|
|
64
65
|
|
|
65
66
|
## Vercel Kit connection
|
|
66
67
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// A bot's execution-context prompt. A bot is defined in hub.workspace.json
|
|
2
|
+
// (`runsOn` on the agent, else the file's own default, else hosted:
|
|
3
|
+
// workspace.mjs agentRunsOn), and the page it gets follows from that field
|
|
4
|
+
// alone: the local page from the kit's templates (every bot's
|
|
5
|
+
// instructions/execution-context.ts stub calls `executionContext`), or, for a
|
|
6
|
+
// bot the Vercel build deploys, a hosted page the build writes over that stub
|
|
7
|
+
// (prepare.mjs), because only the build knows the deployment. The shared
|
|
8
|
+
// prompts (templates/main-instructions.md, workspace-protocol.md) hold only
|
|
9
|
+
// what is true in both contexts.
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { templatesDir } from "./main-instructions.mjs";
|
|
13
|
+
import { agentRunsOn, parseWorkspace } from "./workspace.mjs";
|
|
14
|
+
|
|
15
|
+
const page = (name, options) => readFileSync(join(templatesDir(options), `execution-context-${name}.md`), "utf8").trimEnd();
|
|
16
|
+
|
|
17
|
+
// The build's Vercel facts. The team and project name come from the build's
|
|
18
|
+
// OIDC token (its `owner` and `project` claims); anything the build doesn't
|
|
19
|
+
// carry stays undefined and the page leaves it out. A malformed token fails
|
|
20
|
+
// the build.
|
|
21
|
+
const claims = (jwt) => JSON.parse(Buffer.from(jwt.split(".")[1], "base64url").toString());
|
|
22
|
+
|
|
23
|
+
export function deploymentFacts(env = process.env) {
|
|
24
|
+
const oidc = env.VERCEL_OIDC_TOKEN ? claims(env.VERCEL_OIDC_TOKEN) : {};
|
|
25
|
+
const repo = env.VERCEL_GIT_REPO_OWNER && env.VERCEL_GIT_REPO_SLUG ? `${env.VERCEL_GIT_REPO_OWNER}/${env.VERCEL_GIT_REPO_SLUG}` : undefined;
|
|
26
|
+
return {
|
|
27
|
+
team: oidc.owner,
|
|
28
|
+
project: oidc.project,
|
|
29
|
+
projectId: env.VERCEL_PROJECT_ID,
|
|
30
|
+
deployment: env.VERCEL_DEPLOYMENT_ID,
|
|
31
|
+
url: env.VERCEL_URL,
|
|
32
|
+
environment: env.VERCEL_ENV,
|
|
33
|
+
repo,
|
|
34
|
+
branch: env.VERCEL_GIT_COMMIT_REF,
|
|
35
|
+
sha: env.VERCEL_GIT_COMMIT_SHA,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const list = (items) => items.map((item) => `\`${item}\``).join(", ");
|
|
40
|
+
|
|
41
|
+
export function hostedContext({ bot, roster, kit, removed, deployment: d }, options) {
|
|
42
|
+
const mac = roster.filter((agent) => agent.runsOn === "local").map((agent) => agent.name);
|
|
43
|
+
const cloud = roster.filter((agent) => agent.runsOn !== "local").map((agent) => agent.name);
|
|
44
|
+
const dashboard = d.team && d.project ? `https://vercel.com/${d.team}/${d.project}` : undefined;
|
|
45
|
+
const lines = [
|
|
46
|
+
"## Execution context",
|
|
47
|
+
`You are \`${bot}\`, an eve agent deployed on Vercel, not on a laptop: \`hub.workspace.json\` defines you as hosted.`,
|
|
48
|
+
"",
|
|
49
|
+
`- **Workspace:** ${cloud.length ? `${list(cloud)} run on Vercel` : "no bot runs on Vercel"}${mac.length ? `; ${list(mac)} run on the owner's Mac through the hub runner` : ""}. One hosted hub holds the threads and routes messages to each.`,
|
|
50
|
+
`- **Package:** your tools, hooks, channel and instructions come from the \`eve-hub\` package, version ${kit}.`,
|
|
51
|
+
`- **Deployment:** ${[
|
|
52
|
+
(d.project ?? d.projectId) && `project \`${d.project ?? d.projectId}\``,
|
|
53
|
+
d.team && `team \`${d.team}\``,
|
|
54
|
+
d.deployment && `deployment \`${d.deployment}\``,
|
|
55
|
+
d.environment && `${d.environment} environment`,
|
|
56
|
+
d.url && `at \`https://${d.url}\``,
|
|
57
|
+
].filter(Boolean).join(", ") || "no Vercel facts were available to this build"}. Each bot is its own service at \`/eve/<name>\` in that one project.`,
|
|
58
|
+
`- **Source:** ${d.repo ? `built from \`${d.repo}\`${d.branch ? ` on \`${d.branch}\`` : ""}${d.sha ? ` at commit \`${d.sha}\`` : ""}` : "no git repository: the hub uploaded these files"}. You are made of files in it: \`agents/${bot}/\`, \`memory/\` and \`hub.workspace.json\`.`,
|
|
59
|
+
d.repo
|
|
60
|
+
? "- **Changing it:** commit to that repository; Vercel builds and deploys the commit."
|
|
61
|
+
: "- **Changing it:** there is no repository to commit to. The hub replaces these files on its next deploy, so an edit made only in this deployment does not last.",
|
|
62
|
+
"- **Limits:** there is no `hub` CLI, you cannot restart yourself or any bot, and there are no host paths: your shell is a Vercel Sandbox with its own filesystem, not the owner's machine.",
|
|
63
|
+
`- **Kit tools this build removed:** ${removed.length ? list(removed) : "none"}. They need the hub's machine, or change the roster, which a deployment does by commit.`,
|
|
64
|
+
`- **Logs:** ${dashboard ? `[runtime logs](${dashboard}/logs)${d.deployment ? ` and [this deployment](${dashboard}/${d.deployment.replace(/^dpl_/, "")})` : ""} in the Vercel dashboard` : "the Vercel project's runtime logs in the dashboard"}.`,
|
|
65
|
+
...(bot === "main" ? [page("hosted-main", options)] : []),
|
|
66
|
+
];
|
|
67
|
+
return `${lines.join("\n")}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// What a bot's instructions/execution-context.ts stub renders. Hosted is the
|
|
71
|
+
// Vercel build's to write (hostedContext), never this process's guess.
|
|
72
|
+
export function executionContext(bot, workspace, options) {
|
|
73
|
+
const parsed = parseWorkspace(workspace);
|
|
74
|
+
const runsOn = agentRunsOn(parsed, bot);
|
|
75
|
+
if (runsOn !== "local") {
|
|
76
|
+
throw new Error(`${bot} is defined as hosted in hub.workspace.json, and a hosted bot's execution context is written by its Vercel build (eve-hub/vercel). A bot on this machine needs "runsOn": "local", on the agent or at the top of hub.workspace.json.`);
|
|
77
|
+
}
|
|
78
|
+
return ["## Execution context", `You are \`${bot}\`: \`hub.workspace.json\` defines you as local.`, "", page("local", options), ...(bot === "main" ? [page("local-main", options)] : [])].join("\n") + "\n";
|
|
79
|
+
}
|
package/fork-env.mjs
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
// the fork brief only restate this; the sandbox enforces the defaults.
|
|
6
6
|
// Short-lived either way: a fork can be retired (and an in-place fork's
|
|
7
7
|
// thread go back to the shared root) while its session lives on.
|
|
8
|
-
import {
|
|
8
|
+
import { registryFetch, sandboxRegistry } from "./registry-fetch.mjs";
|
|
9
9
|
|
|
10
10
|
const TTL_MS = 3000;
|
|
11
11
|
|
|
12
|
-
export function createForkEnv({ registry =
|
|
12
|
+
export function createForkEnv({ registry = sandboxRegistry(), fetchImpl = globalThis.fetch, now = Date.now } = {}) {
|
|
13
13
|
const cache = new Map(); // sessionId -> { config, at }
|
|
14
14
|
return async function lookup(sessionId) {
|
|
15
|
+
if (registry === null) return null; // no local hub: no forks
|
|
15
16
|
const hit = cache.get(sessionId);
|
|
16
17
|
if (hit && now() - hit.at < TTL_MS) return hit.config;
|
|
17
18
|
let config = null;
|
package/host-browser.js
CHANGED
|
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
12
12
|
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { registryFetch } from "./registry-fetch.mjs";
|
|
14
|
+
import { registryFetch, sandboxRegistry } from "./registry-fetch.mjs";
|
|
15
15
|
export const VIEWPORT = { width: 1280, height: 800 };
|
|
16
16
|
const FRAME_MS = 200; // at most 5 frames/s leave for the registry
|
|
17
17
|
const IDLE_MS = 10 * 60_000; // an unused browser is closed; the next use reopens its last URL
|
|
@@ -43,6 +43,8 @@ export function findChrome() {
|
|
|
43
43
|
return existsSync(chrome) ? { path: chrome, shell: false } : null;
|
|
44
44
|
}
|
|
45
45
|
function reportScreen(body) {
|
|
46
|
+
if (!sandboxRegistry())
|
|
47
|
+
return;
|
|
46
48
|
void registryFetch(`/api/sandbox/screen`, {
|
|
47
49
|
method: "POST",
|
|
48
50
|
headers: { "content-type": "application/json" },
|
package/host-sandbox.d.ts
CHANGED
|
@@ -104,11 +104,10 @@ export interface Fence {
|
|
|
104
104
|
(what: string): Promise<void>;
|
|
105
105
|
readonly sessionId: string;
|
|
106
106
|
readonly root: string;
|
|
107
|
-
readonly registry: string;
|
|
108
107
|
admit(kind: TicketKind, op: TicketOp, what: string, group?: TicketGroup): Promise<Ticket>;
|
|
109
108
|
}
|
|
110
109
|
export declare function ownershipGuard(sessionId: string, { registry, rootSessionId, agent }?: {
|
|
111
|
-
registry?: string;
|
|
110
|
+
registry?: string | null;
|
|
112
111
|
rootSessionId?: string;
|
|
113
112
|
agent?: string;
|
|
114
113
|
}): Fence;
|
package/host-sandbox.js
CHANGED
|
@@ -15,7 +15,7 @@ import { promisify } from "node:util";
|
|
|
15
15
|
import { defineSandboxProvider } from "eve/sandbox/provider";
|
|
16
16
|
import { closeBrowser } from "./host-browser.js";
|
|
17
17
|
import { createForkEnv } from "./fork-env.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { registryFetch, registryHeaders, sandboxRegistry } from "./registry-fetch.mjs";
|
|
19
19
|
const execFileAsync = promisify(execFile);
|
|
20
20
|
export const HOST_ROOT = resolve(process.env.ROSTER_HOST_ROOT ?? join(homedir(), "wrk"));
|
|
21
21
|
// The provider's lifecycle, exported so tests drive start/resume directly.
|
|
@@ -145,11 +145,13 @@ export function killTree(child) {
|
|
|
145
145
|
// Live terminal: every process's output is teed to the roster registry so the
|
|
146
146
|
// UI can show each bot's sandbox as it works. Best-effort; never blocks.
|
|
147
147
|
let procSeq = 0;
|
|
148
|
-
const forkConfig = createForkEnv({ registry:
|
|
148
|
+
const forkConfig = createForkEnv({ registry: sandboxRegistry() });
|
|
149
149
|
// `retries`: the exit report is the one that must land (without it the
|
|
150
150
|
// registry eventually marks the proc interrupted), so it rides out a registry
|
|
151
151
|
// restart of a few seconds.
|
|
152
152
|
function reportProc(body, retries = 0) {
|
|
153
|
+
if (!sandboxRegistry())
|
|
154
|
+
return;
|
|
153
155
|
void registryFetch(`/api/sandbox/proc`, {
|
|
154
156
|
method: "POST",
|
|
155
157
|
headers: { "content-type": "application/json" },
|
|
@@ -233,7 +235,8 @@ function teeProcess(child, sessionId, command, cwd, rootSessionId) {
|
|
|
233
235
|
// with identical parameters until a definite answer or a bounded deadline,
|
|
234
236
|
// after which the op is not performed (a paused launcher is killed; a "not-run"
|
|
235
237
|
// release is queued for the id) and SessionFenceError is thrown.
|
|
236
|
-
// HUB_OWNERSHIP_GUARD=off disables tickets entirely
|
|
238
|
+
// HUB_OWNERSHIP_GUARD=off disables tickets entirely, as does having no local
|
|
239
|
+
// registry (sandboxRegistry(): a laptop bot of the hosted hub).
|
|
237
240
|
//
|
|
238
241
|
// LIMITS of the guarantee: (1) unfenced work while the registry is unreachable
|
|
239
242
|
// is outside it, by policy, and only reported after the fact; (2) descendants
|
|
@@ -402,8 +405,10 @@ export function stopRevocationPollers() {
|
|
|
402
405
|
p.stop();
|
|
403
406
|
pollers.clear();
|
|
404
407
|
}
|
|
405
|
-
export function ownershipGuard(sessionId, { registry =
|
|
408
|
+
export function ownershipGuard(sessionId, { registry = sandboxRegistry(), rootSessionId, agent = process.env.HUB_AGENT } = {}) {
|
|
406
409
|
const root = rootSessionId ?? sessionId;
|
|
410
|
+
if (registry === null)
|
|
411
|
+
return Object.assign(async () => { }, { sessionId, root, admit: async () => UNFENCED }); // no local hub: none to fence with
|
|
407
412
|
let warned = false;
|
|
408
413
|
const warnOnce = (message) => {
|
|
409
414
|
if (!warned)
|
|
@@ -507,7 +512,7 @@ export function ownershipGuard(sessionId, { registry = registryBase(), rootSessi
|
|
|
507
512
|
if (!ownership.owner)
|
|
508
513
|
throw refusal(what, ownership);
|
|
509
514
|
};
|
|
510
|
-
return Object.assign(check, { sessionId, root,
|
|
515
|
+
return Object.assign(check, { sessionId, root, admit });
|
|
511
516
|
}
|
|
512
517
|
// The paused launcher. /bin/sh runs this with fd 3 a socket to us and the
|
|
513
518
|
// shell-startup variables moved to HUB_LAUNCH_* (so neither sh nor anything it
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineInstructions } from "eve/instructions";
|
|
2
|
+
import { engineeringPage } from "../main-instructions.mjs";
|
|
3
|
+
// The engineering page alone, for a subagent that doesn't carry the workspace
|
|
4
|
+
// protocol (the protocol ends with this same page). A subagent re-exports this as
|
|
5
|
+
// its instructions/engineering.ts.
|
|
6
|
+
export default defineInstructions({ content: engineeringPage() });
|
package/main-instructions.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// main's instructions, rendered from the kit's templates: templates/main-instructions.md,
|
|
2
|
-
// then the shared protocol (templates/workspace-protocol.md
|
|
2
|
+
// then the shared protocol (templates/workspace-protocol.md, then the engineering
|
|
3
|
+
// page, templates/engineering.md). One source for
|
|
3
4
|
// `hub workspace init` (lib/agent-template.mjs) and for every workspace's main,
|
|
4
5
|
// which imports it (instructions/main.ts) instead of carrying a copy, so a
|
|
5
6
|
// template edit reaches each workspace on main's next build. Every other bot
|
|
@@ -11,6 +12,7 @@ import { findWorkspaceRoot } from "./workspace.mjs";
|
|
|
11
12
|
|
|
12
13
|
const HEAD = "main-instructions.md";
|
|
13
14
|
const PROTOCOL = "workspace-protocol.md";
|
|
15
|
+
const ENGINEERING = "engineering.md";
|
|
14
16
|
const templatesOf = (kitDir) => join(kitDir, "templates");
|
|
15
17
|
|
|
16
18
|
// The templates sit next to the kit in the hub checkout. Run by node, this
|
|
@@ -27,13 +29,18 @@ export function templatesDir({ env = process.env, cwd = process.cwd() } = {}) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function mainInstructions(options) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
return `${read(HEAD).trimEnd()}\n\n${read(PROTOCOL)}`;
|
|
32
|
+
const head = readFileSync(join(templatesDir(options), HEAD), "utf8");
|
|
33
|
+
return `${head.trimEnd()}\n\n${workspaceProtocol(options)}`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// The
|
|
36
|
+
// The engineering page alone: the protocol ends with it, and a subagent that
|
|
37
|
+
// never sees the protocol includes it (instructions/engineering.ts).
|
|
38
|
+
export function engineeringPage(options) {
|
|
39
|
+
return readFileSync(join(templatesDir(options), ENGINEERING), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The shared protocol: every bot but main appends it after its own
|
|
36
43
|
// instructions.md (agent/instructions/protocol.ts re-exports instructions/protocol.ts).
|
|
37
44
|
export function workspaceProtocol(options) {
|
|
38
|
-
return readFileSync(join(templatesDir(options), PROTOCOL), "utf8")
|
|
45
|
+
return `${readFileSync(join(templatesDir(options), PROTOCOL), "utf8")}\n${engineeringPage(options)}`;
|
|
39
46
|
}
|
package/memory/workspace.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { defineMemory, defineMemoryProvider } from "eve/memory";
|
|
2
|
-
import { loadMemoryRecall } from "../memory.mjs";
|
|
2
|
+
import { botDomains, findBotDir, loadMemoryRecall } from "../memory.mjs";
|
|
3
3
|
import { findWorkspaceRoot } from "../workspace.mjs";
|
|
4
4
|
// The workspace's memory/ entries as an eve memory slot (see ../memory.mjs).
|
|
5
5
|
// Recall re-reads memory/ at the start of every turn and after compaction and
|
|
6
6
|
// returns only the entries whose text changed since this session last saw
|
|
7
7
|
// them; eve appends those as user-role messages after the history, so the
|
|
8
8
|
// system prompt and the cached prefix stay put when an entry changes.
|
|
9
|
-
// One constant scope and namespace
|
|
10
|
-
|
|
9
|
+
// One constant scope and namespace. The entries are memory/ (every bot) plus
|
|
10
|
+
// the folders for the domains this bot lists in hub.workspace.json.
|
|
11
|
+
const recall = ({ messages }) => {
|
|
12
|
+
const root = findWorkspaceRoot();
|
|
13
|
+
const dir = findBotDir();
|
|
14
|
+
return { messages: loadMemoryRecall(root, messages, { domains: dir ? botDomains(root, dir) : [] }) };
|
|
15
|
+
};
|
|
11
16
|
export default defineMemory({
|
|
12
17
|
description: "The human's standing decisions for this workspace, from its memory/ directory.",
|
|
13
18
|
namespace: "hub-workspace-memory",
|
package/memory.mjs
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
// them at the start of every turn (memory/workspace.ts, memory/bot.ts), so an
|
|
6
6
|
// added, edited or superseded entry reaches the bot's next turn with no
|
|
7
7
|
// rebuild or restart, appended after the history. Node built-ins only.
|
|
8
|
+
// A workspace may group entries into domains: folders under memory/, nestable
|
|
9
|
+
// (memory/ui/, memory/ui/motion/), same entry format. memory/ itself is the
|
|
10
|
+
// domain every bot shares. A bot lists its domains in its hub.workspace.json
|
|
11
|
+
// entry (`domains: ["ui/motion"]`) and recalls memory/ plus each listed
|
|
12
|
+
// folder and every folder above it. The kit names no domain; a workspace
|
|
13
|
+
// defines its own.
|
|
8
14
|
// Shared entries name no bot: bots pick who acts next from the live workspace
|
|
9
15
|
// state, never from a name, and a rule for one role says so in its text. A
|
|
10
16
|
// rule for one bot goes in that bot's memory/ (or its instructions.md).
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
import { createHash } from "node:crypto";
|
|
25
31
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
26
32
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
27
|
-
import { isWorkspace } from "./workspace.mjs";
|
|
33
|
+
import { isWorkspace, readWorkspaceFile } from "./workspace.mjs";
|
|
28
34
|
|
|
29
35
|
export const MEMORY_DIR = "memory";
|
|
30
36
|
export const MAX_RULE_LINES = 3;
|
|
@@ -82,28 +88,64 @@ export function parseEntry(text, file = "entry.md") {
|
|
|
82
88
|
return { id: f.id, status: f.status, since: f.since, evidence: f.evidence, ...(supersededBy ? { supersededBy } : {}), rule: lines.join(" ") };
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
// The folders under <root>/memory a bot with these domains recalls, as paths
|
|
92
|
+
// relative to memory/: "" (the shared root), then each domain and its ancestors.
|
|
93
|
+
export function domainFolders(domains = []) {
|
|
94
|
+
const folders = new Set([""]);
|
|
95
|
+
for (const domain of domains) {
|
|
96
|
+
const parts = domain.split("/");
|
|
97
|
+
for (let i = 1; i <= parts.length; i++) folders.add(parts.slice(0, i).join("/"));
|
|
98
|
+
}
|
|
99
|
+
return [...folders].sort();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Every folder under <root>/memory, "" included: what a workspace's tests validate.
|
|
103
|
+
export function allMemoryFolders(root) {
|
|
104
|
+
const folders = [""];
|
|
105
|
+
const walk = (rel) => {
|
|
106
|
+
for (const d of readdirSync(join(root, MEMORY_DIR, rel), { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
107
|
+
if (!d.isDirectory() || d.name.startsWith(".")) continue;
|
|
108
|
+
folders.push(rel ? `${rel}/${d.name}` : d.name);
|
|
109
|
+
walk(rel ? `${rel}/${d.name}` : d.name);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (existsSync(join(root, MEMORY_DIR))) walk("");
|
|
113
|
+
return folders;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Every entry in the given folders of <dir>/memory (default: the root only),
|
|
117
|
+
// sorted by id, and one error string per file that isn't a valid entry, per
|
|
118
|
+
// listed folder that doesn't exist, and per id used twice. No memory/
|
|
119
|
+
// directory is an empty memory. `label` names the directory in errors;
|
|
120
|
+
// `knownIds` are ids a superseded_by may also point at (a bot's entries may
|
|
121
|
+
// hand over to a shared one).
|
|
122
|
+
export function readMemory(root, { label = MEMORY_DIR, knownIds = new Set(), folders = [""] } = {}) {
|
|
92
123
|
const entries = [];
|
|
93
124
|
const errors = [];
|
|
94
|
-
const
|
|
95
|
-
for (const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
125
|
+
const where = new Map();
|
|
126
|
+
for (const folder of folders) {
|
|
127
|
+
const dir = folder ? join(root, MEMORY_DIR, folder) : join(root, MEMORY_DIR);
|
|
128
|
+
const prefix = folder ? `${label}/${folder}` : label;
|
|
129
|
+
if (!existsSync(dir)) {
|
|
130
|
+
if (folder) errors.push(`${prefix}: domain folder doesn't exist`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const files = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md") && !d.name.startsWith(".") && d.name.toLowerCase() !== "readme.md").map((d) => d.name).sort();
|
|
134
|
+
for (const name of files) {
|
|
135
|
+
const file = `${prefix}/${name}`;
|
|
136
|
+
try {
|
|
137
|
+
const entry = parseEntry(readFileSync(join(dir, name), "utf8"), file);
|
|
138
|
+
if (`${entry.id}.md` !== name) throw new MemoryError(`${file}: id ${entry.id} doesn't match the file name; rename it ${entry.id}.md`);
|
|
139
|
+
if (where.has(entry.id)) throw new MemoryError(`${file}: id ${entry.id} is also ${where.get(entry.id)}; an id is used once`);
|
|
140
|
+
where.set(entry.id, file);
|
|
141
|
+
entries.push(entry);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
errors.push(error instanceof MemoryError ? error.message : `${file}: ${error.message}`);
|
|
144
|
+
}
|
|
103
145
|
}
|
|
104
146
|
}
|
|
105
147
|
const ids = new Set(entries.map((e) => e.id));
|
|
106
|
-
for (const e of entries) if (e.supersededBy && !ids.has(e.supersededBy) && !knownIds.has(e.supersededBy)) errors.push(`${
|
|
148
|
+
for (const e of entries) if (e.supersededBy && !ids.has(e.supersededBy) && !knownIds.has(e.supersededBy)) errors.push(`${where.get(e.id)}: superseded_by ${e.supersededBy} has no entry`);
|
|
107
149
|
entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
108
150
|
return { entries, errors };
|
|
109
151
|
}
|
|
@@ -190,10 +232,11 @@ const warnOnce = (msg) => {
|
|
|
190
232
|
console.warn(`memory: ${msg}`);
|
|
191
233
|
};
|
|
192
234
|
|
|
193
|
-
const unreadableIds = (errors) => new Set(errors.map((e) => /(?:^|\/)memory\/([^/:]+)\.md:/.exec(e)?.[1]).filter(Boolean));
|
|
235
|
+
const unreadableIds = (errors) => new Set(errors.map((e) => /(?:^|\/)memory\/(?:[^/:]+\/)*([^/:]+)\.md:/.exec(e)?.[1]).filter(Boolean));
|
|
194
236
|
|
|
195
|
-
|
|
196
|
-
|
|
237
|
+
// `domains`: the bot's domains (hub.workspace.json), see the header.
|
|
238
|
+
export function loadMemoryRecall(root, history = [], { warn = warnOnce, domains = [] } = {}) {
|
|
239
|
+
const { entries, errors } = readMemory(root, { folders: domainFolders(domains) });
|
|
197
240
|
for (const error of errors) warn(error);
|
|
198
241
|
return memoryRecall(entries, history, { unreadable: unreadableIds(errors) });
|
|
199
242
|
}
|
|
@@ -203,7 +246,7 @@ export function loadMemoryRecall(root, history = [], { warn = warnOnce } = {}) {
|
|
|
203
246
|
// never by shadowing it), so a clash is an error and that file is skipped.
|
|
204
247
|
export function readBotMemory(root, botDir) {
|
|
205
248
|
const label = `${relative(root, botDir).split(sep).join("/")}/${MEMORY_DIR}`;
|
|
206
|
-
const shared = new Set(readMemory(root).entries.map((e) => e.id));
|
|
249
|
+
const shared = new Set(readMemory(root, { folders: allMemoryFolders(root) }).entries.map((e) => e.id));
|
|
207
250
|
const { entries, errors } = readMemory(botDir, { label, knownIds: shared });
|
|
208
251
|
const clash = entries.filter((e) => shared.has(e.id));
|
|
209
252
|
for (const e of clash) errors.push(`${label}/${e.id}.md: id ${e.id} is also ${MEMORY_DIR}/${e.id}.md; give it its own id and supersede the other explicitly`);
|
|
@@ -231,10 +274,21 @@ export function findBotDir({ env = process.env, cwd = process.cwd() } = {}) {
|
|
|
231
274
|
return null;
|
|
232
275
|
}
|
|
233
276
|
|
|
277
|
+
// The domains the bot in `botDir` lists in its hub.workspace.json entry. The
|
|
278
|
+
// bot is its directory name, as in findBotDir.
|
|
279
|
+
export function botDomains(root, botDir) {
|
|
280
|
+
return readWorkspaceFile(root).agents.find((a) => a.name === basename(botDir))?.domains ?? [];
|
|
281
|
+
}
|
|
282
|
+
|
|
234
283
|
// For a workspace's tests: throws one MemoryError listing every bad file,
|
|
235
|
-
// shared and per bot (agents/*/memory/)
|
|
284
|
+
// shared (every folder) and per bot (agents/*/memory/), and every domain a bot
|
|
285
|
+
// lists that has no folder. Returns the shared entries.
|
|
236
286
|
export function validateMemory(root) {
|
|
237
|
-
const { entries, errors } = readMemory(root);
|
|
287
|
+
const { entries, errors } = readMemory(root, { folders: allMemoryFolders(root) });
|
|
288
|
+
if (isWorkspace(root)) {
|
|
289
|
+
const folders = new Set(allMemoryFolders(root));
|
|
290
|
+
for (const { name, domains = [] } of readWorkspaceFile(root).agents) for (const d of domains) if (!folders.has(d)) errors.push(`hub.workspace.json: ${name} lists domain ${d}, which has no memory/${d}/ folder`);
|
|
291
|
+
}
|
|
238
292
|
const agents = join(root, "agents");
|
|
239
293
|
if (existsSync(agents)) for (const name of readdirSync(agents).sort()) if (existsSync(join(agents, name, MEMORY_DIR))) errors.push(...readBotMemory(root, join(agents, name)).errors);
|
|
240
294
|
if (errors.length) throw new MemoryError(`${errors.length} bad memory entr${errors.length === 1 ? "y" : "ies"}:\n${errors.join("\n")}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
|
|
6
6
|
"type": "module",
|
|
@@ -23,13 +23,20 @@
|
|
|
23
23
|
"./subagent-cap": "./subagent-cap.mjs",
|
|
24
24
|
"./session-limits": "./session-limits.mjs",
|
|
25
25
|
"./main-instructions": "./main-instructions.mjs",
|
|
26
|
+
"./execution-context": "./execution-context.mjs",
|
|
26
27
|
"./connections/*": "./connections/*.js",
|
|
27
|
-
"./bot-token": "./bot-token.mjs"
|
|
28
|
+
"./bot-token": "./bot-token.mjs",
|
|
29
|
+
"./prepare": "./prepare.mjs",
|
|
30
|
+
"./vercel": "./vercel.mjs",
|
|
31
|
+
"./world": "./world.mjs"
|
|
28
32
|
},
|
|
29
33
|
"peerDependencies": {
|
|
30
34
|
"eve": "*",
|
|
31
35
|
"zod": "4.5.4",
|
|
32
36
|
"@vercel/connect": "^2.4.1",
|
|
33
37
|
"@vercel/oidc": "4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ai": "^7.0.122"
|
|
34
41
|
}
|
|
35
42
|
}
|