Changelog, dev startup, README, AgentHub 0.4.0, model catalog, and landing site (#7)

Branch-length batch covering tooling, the model layer, the Web App and the public
surfaces. Highlights:

- Changelog: a per-release `changelog/<version>/` tree, grouped by the surface each
  change touches, with a root CHANGELOG.md holding one line per release.
- Dev startup: `scripts/dev-prebuild.mjs` serializes the skills+core prebuild behind a
  lock and keeps `pnpm install` current; `pnpm dev` runs server+web together.
- AgentHub 0.3.3 -> 0.4.0: OmniMessage complete payloads carry one opaque `fidelity`
  object in place of item-level `signature`/`phase`, threaded verbatim through Trace,
  replay and resume; malformed classification adapted to the new error types.
- Model layer: a model is always referenced by an explicit `(provider, model_id)` pair.
  The provider is never inferred, guessed or defaulted -- both the catalog inference and
  the unique-match config resolution are gone, and CLI, SDK, server routes and
  run_subagent all require the complete pair. Catalog gains the Qwen Token Plan, Qwen
  Pay-As-You-Go and Fireworks AI gateways, plus an expanded OpenRouter group.
- Web App: catalog preset sync and per-group speed test on the Models page, positional
  slash commands, a markdown renderer, skill-library update reminders, and a vertically
  centred draft page whose upward menus size themselves to the room available.
- Public surfaces: restructured READMEs, the penguin.ooo landing site and blog, refreshed
  benchmark results for both suites, and the demo videos playing on the landing page.

Includes the fixes from a full review of the branch: 23 confirmed findings, among them a
provider-inference bug that could send one vendor's API key to another vendor's endpoint,
and an Escape handler that destroyed the composer's contents unrecoverably.

Verified on the branch head: pnpm test (1127 passing, 7 packages), pnpm typecheck and
pnpm format:check clean, Playwright e2e 14/14.
This commit is contained in:
Yaowei Zheng
2026-07-21 17:43:31 +08:00
committed by GitHub
parent abf0a2f248
commit d4faee3a1e
214 changed files with 9406 additions and 1688 deletions
@@ -160,6 +160,10 @@ export function modelsRoutes(deps: AppDeps): Hono<AppEnv> {
if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey must be a boolean.");
req.clearApiKey = body.clearApiKey;
}
if (body.speed !== undefined) {
if (typeof body.speed !== "boolean") throw badRequest("speed must be a boolean.");
req.speed = body.speed;
}
// null = explicit clear (test against the draft, don't fall back to the stored value); empty string is treated as null.
if (body.baseUrl !== undefined) {
if (body.baseUrl !== null && typeof body.baseUrl !== "string") {
+2 -1
View File
@@ -213,7 +213,8 @@ async function upsert(
const raw = serializeSchedule(fields);
const parsed = parseScheduleFile(name, raw);
if (!parsed.ok) throw badRequest(`Invalid schedule configuration: ${parsed.error}`);
// At save time, verify the model reference resolves (resolveModelRef semantics; same rules as reconciliation) so we never persist a broken file.
// At save time, verify the (provider, modelId) pair names a configured model (same rules as reconciliation) so we never persist a broken file.
// The pairing rule itself is enforced by parseScheduleFile above, which rejects half a reference.
const refError = await validateScheduleModelRef(deps.config.root, projectId, parsed.def);
if (refError !== null) throw badRequest(`Invalid schedule configuration: ${refError}`);
await writeScheduleFile(deps.config.root, projectId, agentId, name, raw);
+21 -6
View File
@@ -108,10 +108,13 @@ export function agentSessionsRoutes(deps: AppDeps): Hono<AppEnv> {
const body = await readJson(c);
const modelId = optionalString(body, "modelId", { minLen: 1, label: "modelId" });
const provider = optionalString(body, "provider", { minLen: 1, label: "provider" });
// Model reference is submitted as a pair: provider can't appear without modelId (core does the same validation; this catches it early).
if (provider !== undefined && modelId === undefined) {
// Model reference is submitted as a pair — both or neither. Neither half is ever
// inferred from the other, so half a reference is rejected here instead of being
// resolved (core does the same validation; this catches it early). Omitting both
// falls back to the Project's default model.
if ((modelId === undefined) !== (provider === undefined)) {
throw badRequest(
"provider is specified but modelId is not: a model reference must be given as a pair.",
"modelId and provider must be given together as a model reference pair: specify both, or neither to use the Project's default model.",
);
}
const approvalMode = optionalEnum(body, "approvalMode", APPROVAL_MODES);
@@ -361,6 +364,11 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
const row = resolveSession(c);
const rel = c.req.query("path") ?? "";
const download = c.req.query("download") === "1";
// Sandboxed top-level preview ("open in a new tab" for html): the document keeps its REAL
// content type but carries a CSP sandbox WITHOUT allow-same-origin — it renders and runs
// fully in an opaque origin, so agent-generated markup cannot reach this origin's cookies
// or API. The request itself still authenticates (top-level GET sends the Lax cookie).
const preview = !download && c.req.query("preview") === "1";
const { data, fileName, contentType, scriptable } = await deps.workspaceFiles.read(
row.workspace,
rel,
@@ -368,15 +376,22 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
const disposition = download ? "attachment" : "inline";
// Same-origin XSS defense: html/svg inline previews are always returned as plain
// text (Workspace files may be Agent-generated and untrusted); downloads
// (attachment) keep the real content type. Paired with nosniff to prevent MIME
// sniffing from undoing this.
const effectiveType = !download && scriptable ? "text/plain; charset=utf-8" : contentType;
// (attachment) keep the real content type, and sandboxed previews keep it under the
// CSP above. Paired with nosniff to prevent MIME sniffing from undoing this.
const effectiveType =
!download && scriptable && !preview ? "text/plain; charset=utf-8" : contentType;
return new Response(new Uint8Array(data), {
status: 200,
headers: {
"Content-Type": effectiveType,
"Content-Disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
"X-Content-Type-Options": "nosniff",
...(preview && scriptable
? {
"Content-Security-Policy":
"sandbox allow-scripts allow-popups allow-modals allow-forms",
}
: {}),
},
});
});
+4 -3
View File
@@ -48,9 +48,10 @@ export function vaultRoutes(deps: AppDeps): Hono<AppEnv> {
deps.projectService.requireProjectOwner(c.var.user.userId, projectId);
const req = parseVaultUpdate(await readJson(c));
const res = await deps.agentConfigService.updateVault(projectId, agentId, req);
// Effective-value semantics: no hot update — an already-built runtime is neither
// evicted nor reloaded; the new value only applies to Sessions created or resumed
// afterward.
// Effective-value semantics: no hot swap into a Task already in flight, but every
// runtime built before this update is invalidated — the next Task on any Session
// of this Agent re-resumes and picks up the new vault values.
deps.manager.invalidateAgentRuntimes(projectId, agentId);
return c.json(res);
});