diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2cd9664df..d380c86b4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,10 +48,10 @@ Agents API is the primary infrastructure deliverable. Parsar is an ordinary clie
### Optional application example
-`example/parsar/` is an optional, independently started Agent workbench. Its [README](example/parsar/README.md) owns its product behavior. The boundary rules are:
+`example/parsar/` is an optional, independently started Agent workbench maintained in this repository, with no dependency on the external Parsar product repository. Its [README](example/parsar/README.md) owns its product behavior. The boundary rules are:
-- It calls only public `/v1` APIs. Its Project key stays server-side; it never holds a Core key or issues machine credentials. Core-only credential issuance stays in the operator console.
-- It may reuse product UI and keep a small product-owned SQLite database (Node's built-in module, Node 22.13+), outside the checkout and isolated by Core origin and Project key fingerprint. Provider keys never reach the browser.
+- It calls only public `/v1` APIs. Its Project key stays server-side; it never holds a Core key or issues machine credentials. Self-hosted connection displays the public Session installation command unchanged; Core owns bootstrap authorization and machine credential issuance.
+- It may keep a small product-owned SQLite database (Node's built-in module, Node 22.13+), outside the checkout and isolated by Core origin and Project key fingerprint. Provider keys never reach the browser.
- Core owns Skills and all execution and history state. The example stores only Session references and pending creation requests with stable idempotency keys.
- Product resources use `/app/` and never become Core API or database conventions.
- It is excluded from Core distributions and cannot become a service dependency.
diff --git a/example/parsar/README.md b/example/parsar/README.md
index be5d87dec..825e739f5 100644
--- a/example/parsar/README.md
+++ b/example/parsar/README.md
@@ -1,6 +1,6 @@
# Parsar Agent workbench
-A small product application on OpenAgentCore, using Parsar UI. Manage models,
+An independently started small application on OpenAgentCore. Manage models,
Skills, HTTP MCP services and runtime configurations; compose reusable Agent
configurations and start independent Sessions. Continue the same Session to keep
its history and live workspace. There is no task layer or shared workspace.
@@ -33,9 +33,12 @@ For a built version, run `pnpm --filter @oac/parsar-example build` and then
to the browser; leaving the key blank while editing preserves it. Discovery
sends only that Provider's key and does not follow redirects. The list expects
the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual
- entry available. This imports the catalog only: Core still owns execution
- connections for hosted/text-only Sessions. Self-hosted Sessions instead pass
- this Provider URL/key explicitly to Core, using the selected harness protocol.
+ entry available. Codex and Claude Code workspace Sessions pass the selected Provider URL/key
+ explicitly to Core, using the harness protocol. These Sessions require an HTTPS
+ Base URL and API key. Text-only Sessions and hosted MiniMax Code use Core's
+ deployment connection; the creation dialog states that their Provider is only
+ a catalog group. The example does not yet expose MiniMax Code's required token
+ limits.
2. **Skills:** create a SKILL.md resource or upload a ZIP. Inspect versions,
upload a new version, and choose the default. Core validates and stores bundles.
3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted
@@ -53,7 +56,18 @@ For a built version, run `pnpm --filter @oac/parsar-example build` and then
Create, copy and edit configurations without allocating a runtime.
6. **Sessions:** open an Agent, choose a runtime and send the first message.
Read streaming replies and tool activity, continue the conversation, cancel a running turn and reopen
- history. Each hosted Session gets its own workspace; text-only has no workspace.
+ history. Unsupported Skill/MCP/runtime combinations are explained before creation.
+ The Session's **Files** dialog uploads one file at a time (up to 5 MiB) through
+ Core's public Environment Files API while idle. Each upload gets a unique path
+ under `/workspace/inputs`; a path relative to the working directory is inserted into the message draft,
+ not sent automatically. Unsent drafts, including uploaded file paths, survive
+ reloads and navigation in the same browser tab and remain scoped to the Session. This keeps the API's `/workspace` alias distinct from
+ the physical user-machine directory. Ask the Agent to save generated files
+ under `./outputs`;
+ published Artifacts can be paged and downloaded from the same dialog, even when
+ the live workspace is unavailable. Downloads remain binary; errors stay in the
+ dialog. Text-only Sessions have no file actions.
+ Each hosted Session gets its own workspace; text-only has no workspace.
Agent edits and catalog updates apply to future Sessions. Core freezes existing
Session configuration, including resolved Skill versions. Continuing a Session
@@ -66,12 +80,12 @@ model availability or MCP connectivity. Skills require a hosted runtime.
The loopback Node server has two small responsibilities: a fixed allowlist proxy
for public Core resources, and `/app/` CRUD for product-owned configuration. One
SQLite table stores typed JSON records. There is no ORM, background scheduler,
-execution database or imported Parsar backend.
+execution database.
SQLite files live in `~/.oac/data/parsar-example/`. Set the absolute
`OAC_EXAMPLE_DATA_DIR` to relocate them. A hash of the Core origin and Project key
selects the file; changing either selects a different local catalog. Back up the
-SQLite file with the server stopped. No key is stored in it. Builds and caches
+SQLite file with the server stopped. The Core Project key is not stored in it. Provider keys are stored there. Builds and caches
use `${OAC_DEV_HOME:-$HOME/.oac}`.
Use one server process and a dedicated Project for this local single-user example.
@@ -97,7 +111,8 @@ A few more implementation details:
- **Providers and models** are saved together in one SQLite transaction. Editing
reads a Provider and its models as one snapshot; changing a model advances the
Provider's revision.
-- **Browser calls** use `OpenAIAgentsClient`; only Session creation goes through a
+- **Browser calls** use `OpenAIAgentsClient`, with a small public HTTP reader for
+ Artifacts (not yet exposed by that client); only Session creation goes through a
small server adapter. Closing the browser aborts the upstream stream, never the
running Session.
- **Workspaces:** hosted Sessions each get their own. User-machine Sessions use the
@@ -136,28 +151,16 @@ checks Skill/Plugin installation, same-Session file reuse, and workspace isolati
between two Sessions. It asks the model to call DeepWiki; inspect the recorded
tool activity to distinguish a successful call from an attempted call.
-## UI provenance
-
-The copied UI primitives, `src/lib/utils.ts`, `src/style.css`
-and `public/*` originate in [Parsar](https://github.com/MiniMax-AI-Dev/parsar)
-revision `90fafede`, under [MIT](LICENSE). The responsive body minimum width and
-application pages are local adaptations. Navigation animation adapts
-[Motion Primitives](https://github.com/ibelick/motion-primitives), with reduced-motion
-support and its [MIT notice](MOTION-PRIMITIVES-LICENSE). Multica's resource and
-Agent organization informs the product flow; no Multica code is copied.
-
## Connect a user machine
-Run a matching Core and daemon version.
-
Create a user-machine runtime and start a Session with an Agent using Codex or
-Claude Code. No initial Turn is sent. Open **Connect user machine**, obtain the
-Session's executor credential in Core Web (Session log), save the credential on
-the host as instructed, and run the generated platform-specific install/start
-command. The daemon binary and native harness must already be installed. Windows
-Claude Code also requires Git Bash. The page reads Core's public Environment
+Claude Code. No initial Turn is sent. Open **Connect user machine** and run the
+Core-provided command on the target host. The installer downloads the matching
+native distribution, installs the selected harness and starts the connection.
+Windows Claude Code also requires Git Bash. The page reads Core's public Environment
status and enables sending after it reports connected. The example backend does
-not need or accept the administrator Core key.
+not need or accept the administrator Core key. Commands carry temporary
+Environment-scoped authorization; do not share them. Session refresh renews them.
The selected model Provider needs an HTTPS Base URL and API key. Its protocol is
Responses for Codex and Anthropic for Claude Code. Core freezes and delivers the
@@ -172,6 +175,4 @@ included in this example because its required token limits are not exposed.
The daemon runs with the starting user's permissions and adds no sandbox. Runtime
home is separate per Session; stopping it preserves local files. Credential
-rotation and revocation remain Core console operations. Native install commands
-are for fresh installations; restart an existing one with the same Runtime home
-and `start`, without rerunning `install`.
+rotation and revocation remain Core console operations. Reuse the original installation directory when reconnecting.
diff --git a/example/parsar/server.mjs b/example/parsar/server.mjs
index a4557c5d9..bf81f97e6 100644
--- a/example/parsar/server.mjs
+++ b/example/parsar/server.mjs
@@ -11,6 +11,12 @@ import { productAPI } from "./server/product.mjs";
const root = fileURLToPath(new URL(".", import.meta.url));
const routes = [
[/^\/v1\/agents\/environments\/[a-f0-9-]{36}$/, ["GET"]],
+ [/^\/v1\/agents\/environments\/[a-f0-9-]{36}\/files$/, ["GET", "POST"]],
+ [/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts$/, ["GET"]],
+ [
+ /^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts\/[a-f0-9-]{36}\/content$/,
+ ["GET"],
+ ],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/(items|turns)$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/events$/, ["GET", "POST"]],
@@ -189,7 +195,12 @@ export function createHandler(
const streaming =
req.method === "GET" && url.pathname.endsWith("/events");
const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30_000);
+ const fileWrite =
+ req.method === "POST" && url.pathname.endsWith("/files");
+ const timer = setTimeout(
+ () => controller.abort(),
+ fileWrite ? 240_000 : 30_000,
+ );
res.on("close", () => controller.abort());
try {
const chunks = [];
@@ -198,7 +209,8 @@ export function createHandler(
size += chunk.length;
if (
size >
- (url.pathname.startsWith("/v1/skills")
+ (url.pathname.startsWith("/v1/skills") ||
+ url.pathname.endsWith("/files")
? 8 * 1024 * 1024
: 1024 * 1024)
)
@@ -249,6 +261,18 @@ export function createHandler(
await pipeline(Readable.fromWeb(upstream.body), res);
return;
}
+ if (url.pathname.endsWith("/content") && upstream.ok && upstream.body) {
+ // Downloads stay binary and are never rendered as active browser content.
+ clearTimeout(timer);
+ res.writeHead(upstream.status, {
+ "Content-Type": "application/octet-stream",
+ "Content-Disposition":
+ upstream.headers.get("content-disposition") || "attachment",
+ "Cache-Control": "no-store",
+ });
+ await pipeline(Readable.fromWeb(upstream.body), res);
+ return;
+ }
const body = await upstream.text();
res.writeHead(upstream.status, {
"Content-Type": "application/json",
diff --git a/example/parsar/server/sessions.mjs b/example/parsar/server/sessions.mjs
index 30a73eed2..2cfdd72e6 100644
--- a/example/parsar/server/sessions.mjs
+++ b/example/parsar/server/sessions.mjs
@@ -1,3 +1,4 @@
+import { sessionRestriction } from "../shared/session-profile.mjs";
import { zipSync, strToU8 } from "fflate";
import { AppError, text, uuid } from "./store.mjs";
@@ -14,41 +15,28 @@ function requestFor(store, id, body) {
throw new AppError(400, "绑定的 MCP 服务标识不能重复。");
const environment = { type: runtime.environment };
const selfHosted = environment.type === "self_hosted";
+ const restriction = sessionRestriction(agent, runtime);
+ if (restriction) throw new AppError(400, restriction);
let modelProvider;
- if (selfHosted) {
- if (agent.skill_ids.length)
- throw new AppError(
- 400,
- "此示例的用户机器只使用本地能力目录,暂不传递托管 Skill。请使用未绑定托管 Skill 的 Agent,并在运行时填写本地能力目录。",
- );
- if (mcps.length)
- throw new AppError(
- 400,
- "用户机器的 MCP 请通过本地 Plugin 能力目录配置。当前 Core 不接受此处的 MCP 服务绑定,请使用未绑定 MCP 的 Agent。",
- );
- if (agent.harness === "mcode")
- throw new AppError(
- 400,
- "此示例的用户机器接入先支持 Codex 和 Claude Code,请选择其中一个执行引擎。",
- );
+ if (environment.type !== "none" && agent.harness !== "mcode") {
const provider = store.get("providers", model.provider_id);
if (!provider?.base_url?.startsWith("https://") || !provider.api_key)
throw new AppError(
400,
- "用户机器需要模型 Provider 的 HTTPS Base URL 和 API Key,请先在模型页面配置。",
+ "请先在模型页面配置所选 Provider 的 HTTPS Base URL 和 API Key。",
);
modelProvider = {
protocol: agent.harness === "codex" ? "responses" : "anthropic",
base_url: provider.base_url,
api_key: provider.api_key,
};
+ }
+ if (selfHosted) {
environment.workspace_directory = runtime.workspace_directory;
environment.capability_directories = runtime.capability_directories || [];
}
const tools = [{ type: "web_search", mode: "disabled" }];
if (environment.type === "none" || selfHosted) {
- if (agent.skill_ids.length)
- throw new AppError(400, "Skills 需要托管运行环境。");
tools.push(
...mcps.map((mcp) => ({
type: "mcp",
@@ -64,11 +52,6 @@ function requestFor(store, id, body) {
skill_id,
}));
if (mcps.length) {
- if (agent.harness === "mcode")
- throw new AppError(
- 400,
- "MiniMax Code 暂不支持托管环境中的 HTTP MCP,请选择 Claude Code 或 Codex。",
- );
const name = "agent-mcp";
const description = "Agent HTTP MCP bindings";
const archive = zipSync({
diff --git a/example/parsar/shared/session-profile.d.mts b/example/parsar/shared/session-profile.d.mts
new file mode 100644
index 000000000..6b4d31f52
--- /dev/null
+++ b/example/parsar/shared/session-profile.d.mts
@@ -0,0 +1,4 @@
+export function sessionRestriction(
+ agent: { harness: string; skill_ids: string[]; mcp_ids: string[] },
+ runtime?: { environment: string },
+): string | undefined;
diff --git a/example/parsar/shared/session-profile.mjs b/example/parsar/shared/session-profile.mjs
new file mode 100644
index 000000000..96ea469fd
--- /dev/null
+++ b/example/parsar/shared/session-profile.mjs
@@ -0,0 +1,16 @@
+// Product guidance only; Core remains authoritative for capability validation.
+export function sessionRestriction(agent, runtime) {
+ if (!runtime) return undefined;
+ if (runtime.environment === "self_hosted") {
+ if (agent.skill_ids.length)
+ return "用户机器使用本地能力目录,不能绑定托管 Skill。请先编辑 Agent,移除托管 Skill。";
+ if (agent.mcp_ids.length)
+ return "用户机器的 MCP 需通过本地 Plugin 配置。请先编辑 Agent,移除 MCP 绑定。";
+ if (agent.harness === "mcode")
+ return "当前示例的用户机器支持 Codex 和 Claude Code,请先编辑 Agent 的执行引擎。";
+ }
+ if (runtime.environment === "none" && agent.skill_ids.length)
+ return "Skills 需要托管运行环境,请选择托管沙箱或移除 Skill 绑定。";
+ if (agent.harness === "mcode" && agent.mcp_ids.length)
+ return "MiniMax Code 暂不支持此处的 HTTP MCP,请选择 Claude Code 或 Codex。";
+}
diff --git a/example/parsar/src/Composer.tsx b/example/parsar/src/Composer.tsx
index df7325c06..9f80c9ea5 100644
--- a/example/parsar/src/Composer.tsx
+++ b/example/parsar/src/Composer.tsx
@@ -14,10 +14,14 @@ export function Composer({
session,
turnId,
connectionPending = false,
+ attachment = "",
+ onPendingChange,
}: {
session: AgentSession;
turnId?: string;
connectionPending?: boolean;
+ attachment?: string;
+ onPendingChange?: (pending: boolean) => void;
}) {
const cache = useQueryClient();
const storageKey = `oac-example-message-${session.id}`;
@@ -30,7 +34,24 @@ export function Composer({
}
},
);
- const [text, setText] = useState(pending?.text || "");
+ useEffect(() => {
+ onPendingChange?.(Boolean(pending));
+ }, [pending, onPendingChange]);
+ const draftKey = `oac-example-draft-${session.id}`;
+ const [text, setText] = useState(
+ () => pending?.text || sessionStorage.getItem(draftKey) || "",
+ );
+ useEffect(() => {
+ if (text) sessionStorage.setItem(draftKey, text);
+ else sessionStorage.removeItem(draftKey);
+ }, [draftKey, text]);
+ useEffect(() => {
+ if (attachment)
+ setText(
+ (previous) =>
+ `${previous}${previous ? "\n" : ""}请处理文件:${attachment}`,
+ );
+ }, [attachment]);
const [cancelKey, setCancelKey] = useState(createIdempotencyKey);
const [cancelRequested, setCancelRequested] = useState(false);
useEffect(() => {
@@ -62,6 +83,7 @@ export function Composer({
requestReturned(session.id, operation.key);
},
onSuccess: () => {
+ sessionStorage.removeItem(draftKey);
setText("");
setPending(null);
sessionStorage.removeItem(storageKey);
diff --git a/example/parsar/src/ConfigurationFields.tsx b/example/parsar/src/ConfigurationFields.tsx
index 928059071..a76a165d3 100644
--- a/example/parsar/src/ConfigurationFields.tsx
+++ b/example/parsar/src/ConfigurationFields.tsx
@@ -94,7 +94,7 @@ export function ConfigurationFields({
{group.key === "skill_ids"
? "最多选择 8 个技能,运行时需支持托管沙箱。新会话使用技能的默认版本。"
- : "最多选择 8 个 MCP。Core 负责连接服务,服务地址需从 Core 网络可达。"}
+ : "最多选择 8 个 MCP。托管会话从工作区连接,纯文本会话从 Core 连接;用户机器请在本地 Plugin 中配置。"}