Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ one origin. Apps stay generic — every product rule belongs in a package (see
migrations and each mounted Corbits library's idempotent migration under a
Postgres advisory lock.
- `src/corbits-mount.ts` mounts the Corbits mount block:
- `@corbits/artifacts` (`mountArtifacts` and `mountWorkflowArtifacts`)
- `@corbits/artifacts` (`createArtifactRoutes` and `createWorkflowArtifactRoutes`)
- Live workflow-run SSE stream routes (`src/run-stream.ts`)
- `@corbits/agent-token` (`mountAgentTokens` and `createAgentTokenVerifier`)
- `@corbits/mailbox` (`mountMailbox` with `src/mailbox-persist.ts` and `src/mailbox-send.ts`)
- `@corbits/mailbox` (`createMailboxRoutes` with `src/mailbox-persist.ts` and `src/mailbox-send.ts`)
- `@corbits/cron` (`createCronRoutes` and `createCronTicker`)
- `@corbits/memory` (`createMemory` and `mountWorkflowMemory`)
- `@corbits/memory` (`createMemory`, `createMemoryRoutes`, and `mountWorkflowMemory`)
- `@corbits/oauth-core/hub` (`mountOAuthCallback`, `mountOAuthLogin`, and `createOAuthTokenRefresher` for Codex and xAI)
- `@corbits/mcp/hub` (`mountMcpDiscovery`)
- `@corbits/webhooks` (`installWebhooks`)
- `@corbits/webhooks` (`createHookRoutes`)
- Serves no static files. In dev, `vite dev` (`apps/web`) serves the SPA and
proxies `/api` to the hub; in e2e, `e2e/lib/browser.ts` serves the built
`apps/web/dist` in front of the hub on one origin.
Expand Down
22 changes: 11 additions & 11 deletions apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@
"test": "bun test"
},
"dependencies": {
"@corbits/agent-token": "github:corbitsdev/corbits-agent-token#ed286f4592190f911de480ef5330ce03d8605f00",
"@corbits/artifacts": "github:corbitsdev/corbits-artifacts#4a2a3c22187991a6ce598faf794908fedd252e98",
"@corbits/codex-provider": "0.1.2",
"@corbits/credential-http": "^0.1.0",
"@corbits/cron": "0.2.1",
"@corbits/agent-token": "0.2.2",
"@corbits/artifacts": "0.3.0",
"@corbits/codex-provider": "0.1.3",
"@corbits/credential-http": "^0.1.2",
"@corbits/cron": "0.2.2",
"@corbits/error-sink": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#65590a85fa143251b3ac20ba2eca92fc35e70e51",
"@corbits/mcp": "^0.3.0",
"@corbits/memory": "github:corbitsdev/corbits-memory#8be608becc749fa0795a899826277823dca6db3e",
"@corbits/oauth-core": "^0.3.0",
"@corbits/webhooks": "github:corbitsdev/webhooks#3f4f83147fd826d3aec5bcac7ef4b0ae7f75c00d",
"@corbits/xai-provider": "0.1.2",
"@corbits/mailbox": "0.2.1",
"@corbits/mcp": "^0.3.1",
"@corbits/memory": "0.2.1",
"@corbits/oauth-core": "^0.3.2",
"@corbits/webhooks": "0.3.2",
"@corbits/xai-provider": "0.1.3",
"@intx/authz": "workspace:*",
"@intx/crypto": "0.5.0",
"@intx/db": "workspace:*",
Expand Down
191 changes: 96 additions & 95 deletions apps/hub/src/corbits-mount.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createAgentTokenVerifier, mountAgentTokens } from "@corbits/agent-token";
import {
createArtifactRoutes,
createWorkflowArtifactRoutes,
InlineContentStore,
mountArtifacts,
mountWorkflowArtifacts,
type WorkflowArtifactEnv,
} from "@corbits/artifacts";
import {
CODEX_PROVIDER,
Expand All @@ -15,13 +14,13 @@ import { createCronRoutes, createCronTicker, createRunTriggerCronDeliver } from
import { reportError } from "@corbits/error-sink";
import {
createInMemoryMailboxEventBus,
createMailboxDb,
createMailboxPersist,
mountMailbox,
createMailboxRoutes,
} from "@corbits/mailbox";
import { mountMcpDiscovery } from "@corbits/mcp/hub";
import {
createMemory,
createMemoryRoutes,
loadMemoryConfig,
mountWorkflowMemory,
type WorkflowMemoryEnv,
Expand All @@ -34,10 +33,10 @@ import {
type OAuthLoginProviders,
} from "@corbits/oauth-core/hub";
import {
createHookRoutes,
createRunTriggerDeliverer,
createTenantSystemSender,
installWebhooks,
type HookMailRouter,
type HookRouter,
} from "@corbits/webhooks";
import {
XAI_PROVIDER,
Expand Down Expand Up @@ -79,7 +78,6 @@ const TENANT_PREFIX = "/api/tenants/:tenantId";
export type MountCorbitsDeps = {
readonly app: Hono<AppEnv>;
readonly db: ReturnType<typeof createDB>["db"];
readonly databaseUrl: string;
readonly port: number;
readonly grantStore: ReturnType<typeof createGrantStore>;
readonly principalKeyStore: PrincipalKeyStore;
Expand All @@ -92,26 +90,24 @@ export type MountedCorbits = {
stop(): void;
};

export async function mountCorbits({
export function mountCorbits({
app,
db,
databaseUrl,
port,
grantStore,
principalKeyStore,
credentialCipher,
sidecarRouter,
lookups,
}: MountCorbitsDeps): Promise<MountedCorbits> {
}: MountCorbitsDeps): MountedCorbits {
const log = getLogger(["hub"]);
const requireGrant = createRequireGrant({
grantStore,
conditionRegistry: grantConditionRegistry,
});

const { db: mailboxDb } = createMailboxDb(databaseUrl);
const mailboxBus = createInMemoryMailboxEventBus();
const persistMail = createMailboxPersist(mailboxDb, {
const persistMail = createMailboxPersist(db, {
upstream: async () => [],
authorizeSender: createHubMailboxAuthorizeSender(db),
bus: mailboxBus,
Expand All @@ -120,15 +116,14 @@ export async function mountCorbits({
// mutating it in place ensures agent outbound mail also lands in principal inboxes.
lookups.persistMail = persistMail;

{
const artifactsApi = new Hono<TenantEnv>();
mountArtifacts(artifactsApi, {
app.route(
TENANT_PREFIX,
createArtifactRoutes({
db,
contentStore: InlineContentStore,
requireGrant,
});
app.route(TENANT_PREFIX, artifactsApi);
}
}),
);

app.route(
TENANT_PREFIX,
Expand All @@ -144,7 +139,6 @@ export async function mountCorbits({
mountAgentTokens(agentTokensApp, {
db,
requireGrant: requireGrant("credential:*", "create"),
resolveTenantId: (ctx) => (ctx as { get(key: "tenant"): { id: string } }).get("tenant").id,
// Scoped to the `workflow` source asset so the workbench can mint the
// token before the deploy that creates the run.
resolveDefinition: async (tenantId, definitionId) => {
Expand All @@ -161,6 +155,9 @@ export async function mountCorbits({
app.route(TENANT_PREFIX, agentTokensApp);
}

// The run routes sit at the hub root outside `/api/tenants/:tenantId`, so
// `verify` seeds `tenant` on the context from the run named by
// `x-workflow-run-address` before `createAgentTokenVerifier` reads it.
const verifyAgentToken = createAgentTokenVerifier({ db });
const resolveAgentRun = async (runAddress: string) => {
if (runAddress === "") return null;
Expand All @@ -174,82 +171,83 @@ export async function mountCorbits({
runId: run.id,
};
};
const agentTokenAuth = {
verify: async (ctx: unknown) => {
const c = ctx as {
req: { header(name: string): string | undefined };
set(key: "tenant", value: { id: string }): void;
};
const runScope = await resolveAgentRun(c.req.header("x-workflow-run-address") ?? "");
if (runScope === null) return undefined;
c.set("tenant", { id: runScope.tenantId });
return verifyAgentToken(c as Parameters<typeof verifyAgentToken>[0]);
},
resolveRun: resolveAgentRun,
};

{
const workflowArtifactsApi = new Hono<WorkflowArtifactEnv>();
const workflowRunAuthenticator = createWorkflowRunAuthenticator({ db });
mountWorkflowArtifacts(workflowArtifactsApi, {
db,
contentStore: InlineContentStore,
resolveRunScope: (token, runAddress) => workflowRunAuthenticator.resolve(token, runAddress),
agentToken: {
verify: (ctx) => verifyAgentToken(ctx),
resolveRun: resolveAgentRun,
},
});
app.route("/api/workflow-artifacts", workflowArtifactsApi);
app.route(
"/api/workflow-artifacts",
createWorkflowArtifactRoutes({
db,
contentStore: InlineContentStore,
resolveRunScope: (token, runAddress) => workflowRunAuthenticator.resolve(token, runAddress),
agentToken: agentTokenAuth,
}),
);
}

{
const mailboxApp = new Hono<TenantEnv>();
mailboxApp.use("/me/inbox/send", captureMailboxRequest());
mountMailbox(mailboxApp, {
db: mailboxDb,
bus: mailboxBus,
resolvePrincipal: (ctx) => {
const c = ctx as { get(key: "tenant" | "principal"): { id: string } };
return {
tenantId: c.get("tenant").id,
principalId: c.get("principal").id,
};
},
senderAddressFor: async (principal) => {
const [tenantRow] = await db
.select({ domain: tenantTable.domain })
.from(tenantTable)
.where(eq(tenantTable.id, principal.tenantId))
.limit(1);
if (tenantRow === undefined) {
throw new Error(`no tenant "${principal.tenantId}" to address a mailbox sender from`);
}
const [principalRow] = await db
.select({ refId: principalTable.refId })
.from(principalTable)
.where(eq(principalTable.id, principal.principalId))
.limit(1);
if (principalRow === undefined) {
throw new Error(`no principal "${principal.principalId}" to address a mailbox sender as`);
}
// Small models lowercase addresses before replying, so stamp lowercase canonically.
return `${principalRow.refId}@${tenantRow.domain}`.toLowerCase();
},
deliver: createMailboxDeliver({ app, persistMail }),
});
mailboxApp.route(
"/",
createMailboxRoutes({
db,
bus: mailboxBus,
requireGrant,
senderAddressFor: async (principal) => {
const [tenantRow] = await db
.select({ domain: tenantTable.domain })
.from(tenantTable)
.where(eq(tenantTable.id, principal.tenantId))
.limit(1);
if (tenantRow === undefined) {
throw new Error(`no tenant "${principal.tenantId}" to address a mailbox sender from`);
}
const [principalRow] = await db
.select({ refId: principalTable.refId })
.from(principalTable)
.where(eq(principalTable.id, principal.principalId))
.limit(1);
if (principalRow === undefined) {
throw new Error(
`no principal "${principal.principalId}" to address a mailbox sender as`,
);
}
// Small models lowercase addresses before replying, so stamp lowercase canonically.
return `${principalRow.refId}@${tenantRow.domain}`.toLowerCase();
},
deliver: createMailboxDeliver({ app, persistMail }),
}),
);
app.route(`${TENANT_PREFIX}/mailbox`, mailboxApp);
}

const systemTriggerMailRouter: HookMailRouter = {
routeMail: (address, rawMessage, authenticatedSender, messageId) => {
const systemTriggerMailRouter: HookRouter = {
routeMail: async (address, rawMessage, authenticatedSender, messageId, runGrants) => {
if (!sidecarRouter.getRoutableAddresses().includes(address)) {
return false;
}
void sidecarRouter
.routeMail(address, rawMessage, authenticatedSender, messageId)
.catch((error: unknown) => {
reportError(error, {
operation: "hub.systemTriggerMailRouter.routeMail",
extra: { address, authenticatedSender },
});
});
return true;
},
sendRunGrants: (address, runId, stepGrants, senderIdentities) =>
sidecarRouter.sendRunGrants(
return sidecarRouter.routeMail(
address,
runId,
stepGrants as Parameters<typeof sidecarRouter.sendRunGrants>[2],
senderIdentities as Parameters<typeof sidecarRouter.sendRunGrants>[3],
),
rawMessage,
authenticatedSender,
messageId,
runGrants,
);
},
};

app.route(
Expand Down Expand Up @@ -319,22 +317,23 @@ export async function mountCorbits({
cronTicker.start();

{
const memoryApp = new Hono<TenantEnv>();
const memory = createMemory({
app: memoryApp,
config: loadMemoryConfig(),
grantStore,
conditionRegistry: grantConditionRegistry,
});
app.route("/", memoryApp);
app.route(
`${TENANT_PREFIX}/memory`,
createMemoryRoutes({
memory,
requireGrant,
}),
);

const workflowMemoryApi = new Hono<WorkflowMemoryEnv>();
mountWorkflowMemory(workflowMemoryApi, {
memory,
agentToken: {
verify: (ctx) => verifyAgentToken(ctx),
resolveRun: resolveAgentRun,
},
agentToken: agentTokenAuth,
});
app.route("/api/workflow-memory", workflowMemoryApi);
}
Expand Down Expand Up @@ -431,13 +430,15 @@ export async function mountCorbits({
app.route(TENANT_PREFIX, mcpApi);
}

await installWebhooks({
app,
db,
credentialCipher,
principalKeyStore,
router: systemTriggerMailRouter,
});
app.route(
"/api/hooks",
createHookRoutes({
db,
credentialCipher,
principalKeyStore,
router: systemTriggerMailRouter,
}),
);

return {
stop() {
Expand Down
Loading
Loading