Initialize repository with harness code and assets

Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
Yaowei Zheng
2026-07-19 14:06:53 +08:00
committed by GitHub
parent 056bed7aeb
commit 45bfae6e94
543 changed files with 92949 additions and 0 deletions
@@ -0,0 +1,224 @@
/**
* Unit tests for the Workspace files service: directory-listing order, read/write,
* path confinement (`..` traversal and symlink escape), size-limit protection,
* batch existence checks (files/stat); and the Agent delete route (default_agent
* cannot be deleted, owner-only, directory and index cleanup).
*/
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { WorkspaceFilesService } from "../src/services/workspace-files-service.js";
import type {
AgentCreateResponse,
ProjectCreateResponse,
SessionCreateResponse,
} from "../src/api/types.js";
import { apiClient, createTestApp, makeTempRoot, provisionUser } from "./helpers.js";
import type { TestApp } from "./helpers.js";
describe("workspace-files-service", () => {
let ws: string;
let outside: string;
const svc = new WorkspaceFilesService();
beforeEach(async () => {
ws = await makeTempRoot();
outside = await makeTempRoot();
await fs.mkdir(path.join(ws, "sub"));
await fs.writeFile(path.join(ws, "b.txt"), "hello");
await fs.writeFile(path.join(ws, "sub", "c.md"), "# md");
await fs.writeFile(path.join(outside, "secret.txt"), "secret");
});
afterEach(async () => {
await fs.rm(ws, { recursive: true, force: true });
await fs.rm(outside, { recursive: true, force: true });
});
it("列目录:dir 在前、按名称排序;子目录路径生效", async () => {
const root = await svc.list(ws, "");
expect(root.entries.map((e) => `${e.kind}:${e.name}`)).toEqual(["dir:sub", "file:b.txt"]);
const sub = await svc.list(ws, "sub");
expect(sub.entries.map((e) => e.name)).toEqual(["c.md"]);
});
it("读文件:内容与 content-type;目录/不存在报错", async () => {
const file = await svc.read(ws, "sub/c.md");
expect(file.data.toString()).toBe("# md");
expect(file.contentType).toContain("markdown");
await expect(svc.read(ws, "sub")).rejects.toMatchObject({ status: 400 });
await expect(svc.read(ws, "nope.txt")).rejects.toMatchObject({ status: 404 });
});
it("写文件:覆盖写入;父目录缺失时自动补建(上传文件夹保留目录结构)", async () => {
await svc.write(ws, "sub/new.txt", Buffer.from("data"));
expect(await fs.readFile(path.join(ws, "sub", "new.txt"), "utf8")).toBe("data");
await svc.write(ws, "missing/deep/x.txt", Buffer.from("d"));
expect(await fs.readFile(path.join(ws, "missing", "deep", "x.txt"), "utf8")).toBe("d");
});
it("路径限域:`..` 穿越与绝对路径均拒绝", async () => {
await expect(svc.list(ws, "../")).rejects.toMatchObject({ status: 400 });
await expect(svc.read(ws, `../${path.basename(outside)}/secret.txt`)).rejects.toMatchObject({
status: 400,
});
await expect(svc.write(ws, "../escape.txt", Buffer.from("x"))).rejects.toMatchObject({
status: 400,
});
});
it("Workspace 为文件系统根:子目录可正常下钻(前缀拼接 '//' 回归)", async () => {
const root = path.parse(ws).root;
const sub = await svc.list(root, path.relative(root, path.join(ws, "sub")));
expect(sub.entries.map((e) => e.name)).toEqual(["c.md"]);
});
it("指向 Workspace 内目录的符号链接:kind 为 dir 且可下钻", async () => {
await fs.symlink(path.join(ws, "sub"), path.join(ws, "link-sub"));
const root = await svc.list(ws, "");
expect(root.entries.map((e) => `${e.kind}:${e.name}`)).toEqual([
"dir:link-sub",
"dir:sub",
"file:b.txt",
]);
const viaLink = await svc.list(ws, "link-sub");
expect(viaLink.entries.map((e) => e.name)).toEqual(["c.md"]);
});
it("符号链接逃逸:链接指向 Workspace 外时读写均拒绝", async () => {
await fs.symlink(outside, path.join(ws, "link-out"));
await expect(svc.list(ws, "link-out")).rejects.toMatchObject({ status: 400 });
await expect(svc.read(ws, "link-out/secret.txt")).rejects.toMatchObject({ status: 400 });
// Writing outside via a directory symlink: caught by the parent-directory realpath check.
await expect(svc.write(ws, "link-out/evil.txt", Buffer.from("x"))).rejects.toMatchObject({
status: 400,
});
// Auto-creation under a missing path is equally restricted: if the nearest
// existing ancestor is a symlink pointing outside, mkdir must not be used to escape.
await expect(svc.write(ws, "link-out/new/evil.txt", Buffer.from("x"))).rejects.toMatchObject({
status: 400,
});
expect(
await fs
.stat(path.join(outside, "new"))
.then(() => true)
.catch(() => false),
).toBe(false);
});
it("末段符号链接写入:O_NOFOLLOW 拒绝借刀覆盖域外文件", async () => {
// The Agent has a symlink inside the Workspace pointing to an outside file; an upload attempts to overwrite it.
const victim = path.join(outside, "secret.txt");
await fs.symlink(victim, path.join(ws, "report.pdf"));
await expect(svc.write(ws, "report.pdf", Buffer.from("PWNED"))).rejects.toMatchObject({
status: 400,
});
// The outside file's content is unchanged.
expect(await fs.readFile(victim, "utf8")).toBe("secret");
});
});
describe("files/stat 路由(批量存在性检查)", () => {
let t: TestApp;
let owner: ReturnType<typeof apiClient>;
let outsider: ReturnType<typeof apiClient>;
let sessionId: string;
beforeEach(async () => {
t = await createTestApp();
const a = await provisionUser(t.app, "owner");
const b = await provisionUser(t.app, "outsider");
owner = apiClient(t.app, a.cookie);
outsider = apiClient(t.app, b.cookie);
const created = (await (
await owner.post("/api/projects", { projectId: "owner-stat", name: "项目" })
).json()) as ProjectCreateResponse;
const projectId = created.project.projectId;
await owner.put(`/api/projects/${projectId}/models`, {
defaultModel: { provider: "anthropic", modelId: "claude-sonnet-4-6" },
models: [{ provider: "anthropic", modelId: "claude-sonnet-4-6", contextWindow: 128000 }],
});
const sess = (await (
await owner.post(`/api/projects/${projectId}/agents/default_agent/sessions`, {})
).json()) as SessionCreateResponse;
sessionId = sess.session.sessionId;
await fs.mkdir(path.join(sess.session.workspace, "sub"));
await fs.writeFile(path.join(sess.session.workspace, "a.txt"), "A");
await fs.writeFile(path.join(sess.session.workspace, "sub", "b.md"), "B");
});
afterEach(async () => {
await t.cleanup();
});
it("存在的文件保序去重返回;不存在 / 目录 / 越界一律按不存在计且恒 200", async () => {
const res = await owner.post(`/api/sessions/${sessionId}/files/stat`, {
paths: ["sub/b.md", "a.txt", "sub/b.md", "nope.txt", "sub", "../escape.txt", "/etc/passwd"],
});
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ existing: ["sub/b.md", "a.txt"] });
const empty = await owner.post(`/api/sessions/${sessionId}/files/stat`, { paths: [] });
expect(empty.status).toBe(200);
expect(await empty.json()).toEqual({ existing: [] });
});
it("非法 body → 400:非数组 / 非字符串项 / 超量 / 超长", async () => {
const url = `/api/sessions/${sessionId}/files/stat`;
expect((await owner.post(url, { paths: "a.txt" })).status).toBe(400);
expect((await owner.post(url, { paths: [1] })).status).toBe(400);
const tooMany = Array.from({ length: 101 }, () => "a.txt");
expect((await owner.post(url, { paths: tooMany })).status).toBe(400);
expect((await owner.post(url, { paths: ["x".repeat(513)] })).status).toBe(400);
});
it("外人访问 → 404(不泄露存在性)", async () => {
const res = await outsider.post(`/api/sessions/${sessionId}/files/stat`, {
paths: ["a.txt"],
});
expect(res.status).toBe(404);
});
});
describe("agent 删除路由", () => {
let t: TestApp;
let owner: ReturnType<typeof apiClient>;
let outsider: ReturnType<typeof apiClient>;
let projectId: string;
beforeEach(async () => {
t = await createTestApp();
const a = await provisionUser(t.app, "owner");
const b = await provisionUser(t.app, "outsider");
owner = apiClient(t.app, a.cookie);
outsider = apiClient(t.app, b.cookie);
const created = (await (
await owner.post("/api/projects", { projectId: "owner-ws", name: "项目" })
).json()) as ProjectCreateResponse;
projectId = created.project.projectId;
});
afterEach(async () => {
await t.cleanup();
});
it("owner 删除 Agent:204,目录与列表项消失;default_agent 409;外人 404", async () => {
const created = (await (
await owner.post(`/api/projects/${projectId}/agents`, { agentId: "temp_agent", name: "临时" })
).json()) as AgentCreateResponse;
const agentId = created.agent.agentId;
const dir = path.join(t.root, projectId, "agents", agentId);
await fs.access(dir); // directory exists after creation
const outsiderRes = await outsider.delete(`/api/projects/${projectId}/agents/${agentId}`);
expect(outsiderRes.status).toBe(404); // no access → don't leak existence
const res = await owner.delete(`/api/projects/${projectId}/agents/${agentId}`);
expect(res.status).toBe(204);
await expect(fs.access(dir)).rejects.toThrow();
const list = (await (await owner.get(`/api/projects/${projectId}/agents`)).json()) as {
agents: Array<{ agentId: string }>;
};
expect(list.agents.some((x) => x.agentId === agentId)).toBe(false);
const def = await owner.delete(`/api/projects/${projectId}/agents/default_agent`);
expect(def.status).toBe(409);
});
});