eve-hub 0.6.66 → 0.6.68
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 +12 -0
- package/channels/eve-roster-user.js +4 -4
- package/hooks/mention-gate.js +1 -1
- package/hub-oidc.d.mts +4 -0
- package/hub-oidc.mjs +52 -0
- package/mention-gate.mjs +2 -2
- package/package.json +4 -2
- package/prepare.mjs +5 -4
- package/registry-fetch.mjs +15 -4
- package/world.mjs +21368 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.6.68
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A deployed bot calls the hub with its own project's OIDC token on every call (the tools, the mention gate, the post-commit delivery), with no HUB_TOKEN. It takes a call as the hub's when the call's `x-hub-oidc-token` is a token of this team's from the hub project (HUB_PROJECT_ID); a call without one (an older hub) still counts by BOT_TOKEN. Tokens are checked with jose. The hub accepts the bots' tokens beside the HMAC one and sends its own beside BOT_TOKEN, and gives each bots project HUB_PROJECT_ID.
|
|
8
|
+
|
|
9
|
+
## 0.6.67
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Failed turns: a bot's latest failure is its card until it posts, with Retry on any unresolved failure.
|
|
14
|
+
|
|
3
15
|
## 0.6.66
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { eveChannel } from "eve/channels/eve";
|
|
2
2
|
import { localDev } from "eve/channels/auth";
|
|
3
|
-
import {
|
|
3
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
4
|
+
import { isHubRequest } from "../hub-oidc.mjs";
|
|
4
5
|
// The registry relays for the workspace's human and names them in
|
|
5
6
|
// `x-roster-user`. The name is trusted as a user principal only from the hub:
|
|
6
|
-
// a local-dev request, or one
|
|
7
|
-
// bot, see bot-token.mjs). Resolving it as a user lets user-scoped
|
|
7
|
+
// a local-dev request, or one from the hub (a deployed bot, see hub-oidc.mjs). Resolving it as a user lets user-scoped
|
|
8
8
|
// connections (open_pull_request's OAuth) key tokens to the configured human;
|
|
9
9
|
// `localDev()` alone is not a user, so eve fails them with principal_required.
|
|
10
10
|
const dev = localDev();
|
|
@@ -12,7 +12,7 @@ async function rosterUser(req) {
|
|
|
12
12
|
const user = req.headers.get("x-roster-user");
|
|
13
13
|
if (!user)
|
|
14
14
|
return null;
|
|
15
|
-
if (!(await dev(req)) && !
|
|
15
|
+
if (!(await dev(req)) && !(await isHubRequest(req, getVercelOidcToken)))
|
|
16
16
|
return null;
|
|
17
17
|
return { authenticator: "roster", principalId: `roster:${user}`, principalType: "user", attributes: {} };
|
|
18
18
|
}
|
package/hooks/mention-gate.js
CHANGED
|
@@ -16,7 +16,7 @@ const credential = credentialCache(process.env.VERCEL ? deploymentToken : hubCre
|
|
|
16
16
|
export default defineHook({
|
|
17
17
|
events: {
|
|
18
18
|
async "message.received"(event, ctx) {
|
|
19
|
-
if (ctx.session.parent || !process.env.HUB_TOKEN)
|
|
19
|
+
if (ctx.session.parent || !(process.env.VERCEL || process.env.HUB_TOKEN))
|
|
20
20
|
return; // subagents are never gated; nor is a bot with no hosted hub
|
|
21
21
|
const { cancel } = await screenInbound({
|
|
22
22
|
text: event.data.message,
|
package/hub-oidc.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type TeamClaims = { iss: string; owner_id: string; project_id?: string; environment?: string; user_id?: string; exp: number; [claim: string]: unknown };
|
|
2
|
+
export function looksLikeJwt(token: string | null | undefined): boolean;
|
|
3
|
+
export function verifyTeamToken(token: string, own: string, options?: { fetchImpl?: typeof fetch; now?: number }): Promise<TeamClaims | undefined>;
|
|
4
|
+
export function isHubRequest(request: Request, own: () => Promise<string>, options?: { env?: Record<string, string | undefined>; fetchImpl?: typeof fetch; now?: number }): Promise<boolean>;
|
package/hub-oidc.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Who a Vercel OIDC token says its caller is, for the hub and its bots to check each other with:
|
|
2
|
+
// a bot calls the hub with its bots project's token, the hub calls a bot with its own (`x-hub-oidc-token`).
|
|
3
|
+
// A token counts when the team's keys signed it (RS256, JWKS at `<iss>/.well-known/jwks.json`), it is within
|
|
4
|
+
// its `nbf`/`exp`, and it comes from the checker's own team: both `iss` and `owner_id` equal those of the checker's
|
|
5
|
+
// own token, so nothing is configured. The caller then checks which project (`project_id`) it may be.
|
|
6
|
+
import { createRemoteJWKSet, customFetch, decodeJwt, errors, jwtVerify } from "jose";
|
|
7
|
+
import { isBotToken } from "./bot-token.mjs";
|
|
8
|
+
|
|
9
|
+
// One key set per issuer (and per fetch): jose shares a pending fetch between concurrent checks, keeps the keys an hour, and
|
|
10
|
+
// refetches on an unknown `kid` no more than every 30 s, so a stream of made-up tokens is not a stream of fetches.
|
|
11
|
+
const keySets = new WeakMap();
|
|
12
|
+
// Not a bad token but the keys that could not be read (a timeout, a non-200 answer, an unreadable key set): an outage, so it throws.
|
|
13
|
+
const KEY_FETCH_FAILED = new Set([errors.JWKSTimeout.code, errors.JWKSInvalid.code, errors.JOSEError.code]);
|
|
14
|
+
function keysOf(iss, fetchImpl) {
|
|
15
|
+
if (!keySets.has(fetchImpl)) keySets.set(fetchImpl, new Map());
|
|
16
|
+
const sets = keySets.get(fetchImpl);
|
|
17
|
+
if (!sets.has(iss)) sets.set(iss, createRemoteJWKSet(new URL(`${iss}/.well-known/jwks.json`), { [customFetch]: fetchImpl, cooldownDuration: 30_000, cacheMaxAge: 60 * 60_000 }));
|
|
18
|
+
return sets.get(iss);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const looksLikeJwt = (token) => /^[\w-]+\.[\w-]+\.[\w-]*$/.test(String(token ?? ""));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The verified claims of `token`, or undefined for any token that is not the checker's team's.
|
|
25
|
+
* `own` is the checker's own token (its `iss` and `owner_id` say which team to trust).
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyTeamToken(token, own, { fetchImpl = fetch, now = Date.now() } = {}) {
|
|
28
|
+
const { iss, owner_id: owner } = decodeJwt(own);
|
|
29
|
+
if (!iss || !owner) return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const { payload } = await jwtVerify(token, keysOf(iss, fetchImpl), { issuer: iss, algorithms: ["RS256"], requiredClaims: ["exp"], currentDate: new Date(now) });
|
|
32
|
+
return payload.owner_id === owner ? payload : undefined;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof errors.JOSEError && !KEY_FETCH_FAILED.has(error.code)) return undefined;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether `request` comes from the hub. A call carrying `x-hub-oidc-token` is decided by that token alone: it is of this team's,
|
|
41
|
+
* from the hub project (HUB_PROJECT_ID) and no development token (the hub's own function minted it). A call without one
|
|
42
|
+
* (a hub that does not send it yet) is the workspace's BOT_TOKEN. `own` is this deployment's own token.
|
|
43
|
+
*/
|
|
44
|
+
export async function isHubRequest(request, own, { env = process.env, ...options } = {}) {
|
|
45
|
+
const token = request.headers.get("x-hub-oidc-token");
|
|
46
|
+
if (!token) return isBotToken(request.headers.get("authorization"), env.BOT_TOKEN);
|
|
47
|
+
if (!env.HUB_PROJECT_ID) return false;
|
|
48
|
+
let claims;
|
|
49
|
+
try { claims = await verifyTeamToken(token, await own(), options); }
|
|
50
|
+
catch (error) { console.error(`[eve-hub] could not check the hub's token: ${error?.message}`); return false; }
|
|
51
|
+
return claims !== undefined && claims.project_id === env.HUB_PROJECT_ID && (claims.environment === "production" || claims.environment === "preview");
|
|
52
|
+
}
|
package/mention-gate.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// HUB_MENTION_GATE_REASONING, HUB_MENTION_GATE_TIMEOUT_MS and
|
|
22
22
|
// HUB_MENTION_GATE_URL (tests).
|
|
23
23
|
import { codeRanges } from "./mention-tokens.mjs";
|
|
24
|
-
import {
|
|
24
|
+
import { hubHeaders } from "./registry-fetch.mjs";
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
|
|
27
27
|
export { codeRanges };
|
|
@@ -400,7 +400,7 @@ export function credentialCache(load, { ttlMs = 10 * 60e3, failMs = 60e3, now =
|
|
|
400
400
|
const HUB_CALL_MS = 2000;
|
|
401
401
|
|
|
402
402
|
async function hubCall(fetchImpl, url, body) {
|
|
403
|
-
const res = await fetchImpl(url, { method: "POST", headers:
|
|
403
|
+
const res = await fetchImpl(url, { method: "POST", headers: await hubHeaders({ "content-type": "application/json" }), body: JSON.stringify(body), signal: AbortSignal.timeout(HUB_CALL_MS) });
|
|
404
404
|
if (!res.ok) throw new Error(`hub ${res.status}`);
|
|
405
405
|
return res.json();
|
|
406
406
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.68",
|
|
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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./execution-context": "./execution-context.mjs",
|
|
27
27
|
"./connections/*": "./connections/*.js",
|
|
28
28
|
"./bot-token": "./bot-token.mjs",
|
|
29
|
+
"./hub-oidc": "./hub-oidc.mjs",
|
|
29
30
|
"./prepare": "./prepare.mjs",
|
|
30
31
|
"./vercel": "./vercel.mjs",
|
|
31
32
|
"./world": "./world.mjs"
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"@vercel/oidc": "4.0.0"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"ai": "^7.0.137"
|
|
41
|
+
"ai": "^7.0.137",
|
|
42
|
+
"jose": "^6.2.3"
|
|
41
43
|
}
|
|
42
44
|
}
|
package/prepare.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// has no workspace cwd);
|
|
16
16
|
// - /hub/manifest: the roster and the commit sha the deployment was built
|
|
17
17
|
// from, for the hub to sync from: a custom channel on main, routed in
|
|
18
|
-
// vercel.mjs.
|
|
18
|
+
// vercel.mjs. From the hub, as on each bot's eve channel (hub-oidc.mjs).
|
|
19
19
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { basename, dirname, join } from "node:path";
|
|
21
21
|
import { DEPLOYED_KIT_FILES, KIT_FILES, MAIN_KIT_FILES } from "./stubs.mjs";
|
|
@@ -51,7 +51,8 @@ export default code({ worker: { model, reasoning } });
|
|
|
51
51
|
|
|
52
52
|
const KIT_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
53
53
|
const MANIFEST_CHANNEL = `import { defineChannel, GET } from "eve/channels";
|
|
54
|
-
import {
|
|
54
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
55
|
+
import { isHubRequest } from "eve-hub/hub-oidc";
|
|
55
56
|
import { parseWorkspace } from "eve-hub/workspace";
|
|
56
57
|
import workspace from "../../../../hub.manifest.json" with { type: "json" };
|
|
57
58
|
|
|
@@ -66,8 +67,8 @@ const manifest = { sha, kit: ${JSON.stringify(KIT_VERSION)}, agents, humans, sub
|
|
|
66
67
|
|
|
67
68
|
export default defineChannel({
|
|
68
69
|
routes: [
|
|
69
|
-
GET("/hub/manifest", (request) => {
|
|
70
|
-
if (!
|
|
70
|
+
GET("/hub/manifest", async (request) => {
|
|
71
|
+
if (!(await isHubRequest(request, getVercelOidcToken))) return new Response("unauthorized", { status: 401 });
|
|
71
72
|
if (!sha) return new Response("no commit sha: this deployment wasn't built from git", { status: 500 });
|
|
72
73
|
return Response.json(manifest);
|
|
73
74
|
}),
|
package/registry-fetch.mjs
CHANGED
|
@@ -31,10 +31,10 @@ export const sandboxRegistry = () => (process.env.HUB_SANDBOX_SERVICES === "none
|
|
|
31
31
|
|
|
32
32
|
// Also used by the sandbox's node:http transport, which distinguishes
|
|
33
33
|
// requests that never connected from ones whose result may have been lost.
|
|
34
|
-
export function registryHeaders(init) {
|
|
34
|
+
export function registryHeaders(init, env = process.env) {
|
|
35
35
|
const headers = new Headers(init);
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
36
|
+
if (env.HUB_TOKEN && !headers.has("authorization")) headers.set("authorization", `Bearer ${env.HUB_TOKEN}`);
|
|
37
|
+
if (env.HUB_PROTECTION_BYPASS) headers.set("x-vercel-protection-bypass", env.HUB_PROTECTION_BYPASS);
|
|
38
38
|
return Object.fromEntries(headers);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -57,8 +57,19 @@ export function connectionErrorCode(error) {
|
|
|
57
57
|
|
|
58
58
|
const DROPPED = new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"]);
|
|
59
59
|
|
|
60
|
+
// Every call a bot makes to the hub: a deployed bot is the caller its own project's token says (the hub checks it: api/auth.ts);
|
|
61
|
+
// elsewhere HUB_TOKEN is the credential, if any. The one place a call's headers are made.
|
|
62
|
+
export async function hubHeaders(init, env = process.env) {
|
|
63
|
+
const merged = new Headers(init);
|
|
64
|
+
if (env.VERCEL && !merged.has("authorization")) {
|
|
65
|
+
const { getVercelOidcToken } = await import("@vercel/oidc");
|
|
66
|
+
merged.set("authorization", `Bearer ${await getVercelOidcToken()}`);
|
|
67
|
+
}
|
|
68
|
+
return registryHeaders(merged, env);
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
export async function registryFetch(path, init = {}, { base = registryBase(), deadlineMs = num(process.env.ROSTER_REGISTRY_RETRY_MS, 30_000), attemptTimeoutMs, fetchImpl = fetch } = {}) {
|
|
61
|
-
init = { ...init, headers:
|
|
72
|
+
init = { ...init, headers: await hubHeaders(init.headers) };
|
|
62
73
|
const input = `${base}${path}`;
|
|
63
74
|
const method = String(init.method ?? "GET").toUpperCase();
|
|
64
75
|
const deadline = Date.now() + deadlineMs;
|