From 086d00c31760044cf316a43b2d9112f8d72e1084 Mon Sep 17 00:00:00 2001
From: SaladDay <1203511142@qq.com>
Date: Tue, 27 Oct 2026 18:44:42 +0000
Subject: [PATCH] Choose network access in Parsar environment templates
---
example/parsar/README.md | 2 +-
example/parsar/README.zh.md | 2 +-
example/parsar/server/runtime-profile.mjs | 16 ++++++++
example/parsar/src/EnvironmentFields.tsx | 49 +++++++++++++++++++++++
example/parsar/src/lib/product.ts | 1 +
example/parsar/tests/product.test.mjs | 26 ++++++++++++
6 files changed, 94 insertions(+), 2 deletions(-)
diff --git a/example/parsar/README.md b/example/parsar/README.md
index 80ad30f2b..9872c5b8a 100644
--- a/example/parsar/README.md
+++ b/example/parsar/README.md
@@ -22,7 +22,7 @@ Open http://127.0.0.1:18180. `OPENAI_BASE_URL` ends in `/v1`; remote endpoints r
| Object | What it is |
| --- | --- |
| **资源库** | Building blocks, grouped by what uses them: models for Agent engines; Skills, MCP servers, Plugins, knowledge and instruction fragments for Agents; credentials for MCP. Resources are shared: editing one affects everything that uses it, and an item in use cannot be removed. |
-| **环境模板** | The prepared workspace: npm and Python packages, a Bash setup script, environment variables, Feishu bot variables and initial files. |
+| **环境模板** | The prepared workspace: npm and Python packages, a Bash setup script, network access (allowed, blocked or limited to listed domains), environment variables, Feishu bot variables and initial files. |
| **Agent** | What people talk to: name, description, instructions, how it runs, one required environment template, capabilities and behavior switches. How it runs mirrors a local Codex configuration: Harness, model, reasoning effort, web search and, under **高级**, reasoning summary, verbosity and service tier. **复制** creates an independent copy. |
| **飞书机器人** | One bot is bound to at most one Agent. It routes its chats to that Agent and is the Agent’s Feishu identity in every conversation, including Web chats. |
| **对话** | One public Core Session with its own Environment. It freezes the Agent and environment configuration when it starts. |
diff --git a/example/parsar/README.zh.md b/example/parsar/README.zh.md
index 78b21334a..c0505f0e3 100644
--- a/example/parsar/README.zh.md
+++ b/example/parsar/README.zh.md
@@ -27,7 +27,7 @@ pnpm --filter @oac/parsar-example dev
| 对象 | 含义 |
| --- | --- |
| **资源库** | 组装用的零件,按使用方分组:模型供 Agent 的执行引擎使用;Skills、MCP 服务、Plugins、知识和指令片段供 Agent 使用;凭据供 MCP 使用。资源是共享的:修改一项会影响所有使用它的对象,正在使用的条目不能移除。 |
-| **环境模板** | 准备好的工作区:npm 与 Python 软件包、Bash 初始化脚本、环境变量、飞书机器人变量和初始文件。 |
+| **环境模板** | 准备好的工作区:npm 与 Python 软件包、Bash 初始化脚本、联网权限(允许、禁止或仅允许指定域名)、环境变量、飞书机器人变量和初始文件。 |
| **Agent** | 用户对话的对象:名称、描述、指令、运行方式、一个必选的环境模板、能力和行为开关。运行方式对应本地 Codex 的配置:Harness、模型、推理强度、网页搜索,以及 **高级** 中的推理摘要、输出详细度和服务层级。**复制**会生成一份独立副本。 |
| **飞书机器人** | 一个机器人最多绑定一个 Agent。它把自己的聊天路由给该 Agent,并在所有对话中(包括网页对话)作为该 Agent 的飞书身份。 |
| **对话** | 一个公开 Core Session,拥有独立的 Environment,在开始时冻结 Agent 和环境配置。 |
diff --git a/example/parsar/server/runtime-profile.mjs b/example/parsar/server/runtime-profile.mjs
index bfcbc2d4f..50d200a41 100644
--- a/example/parsar/server/runtime-profile.mjs
+++ b/example/parsar/server/runtime-profile.mjs
@@ -29,6 +29,20 @@ export function runtimeProfile(body, previous) {
Object.keys(body.packages).some((k) => !["npm", "python"].includes(k))
)
throw new AppError(400, "环境模板支持 npm 和 Python 软件包。");
+ // Core validates the domains; Parsar only maps the choice.
+ const access = body.network?.access;
+ let network;
+ if (access === "disabled") network = { access };
+ else if (access === "restricted") {
+ const domains = body.network.allowed_domains;
+ if (!Array.isArray(domains) || domains.some((d) => typeof d !== "string"))
+ throw new AppError(400, "请填写允许访问的域名,每行一个。");
+ network = {
+ access,
+ allowed_domains: domains.map((d) => d.trim()).filter(Boolean),
+ };
+ } else if (access !== undefined && access !== "enabled")
+ throw new AppError(400, "网络设置无效。");
const env = Object.create(null);
const input = body.env === undefined ? previous?.env || {} : body.env;
if (
@@ -94,6 +108,7 @@ export function runtimeProfile(body, previous) {
description: text(body.description || "", "说明", 1000),
packages,
setup_script: text(body.setup_script || "", "初始化脚本", 100000),
+ ...(network && { network }),
env,
bot_env,
files: normalized,
@@ -114,6 +129,7 @@ export function runtimeEnvironment(runtime) {
return {
type: "openai_hosted",
packages: runtime.packages || { npm: [], python: [] },
+ ...(runtime.network && { network: runtime.network }),
env: { ...runtime.env },
setup_commands: runtime.setup_script
? [{ command: runtime.setup_script, cwd: "/workspace" }]
diff --git a/example/parsar/src/EnvironmentFields.tsx b/example/parsar/src/EnvironmentFields.tsx
index 0ead69047..d94a35024 100644
--- a/example/parsar/src/EnvironmentFields.tsx
+++ b/example/parsar/src/EnvironmentFields.tsx
@@ -99,6 +99,54 @@ export function EnvironmentFields({
)}
/>
+
+
+
+ {value.network?.access === "restricted" && (
+
+
+ )}
;
env?: Record;
prepare?: boolean;
diff --git a/example/parsar/tests/product.test.mjs b/example/parsar/tests/product.test.mjs
index 258312071..f2cb86081 100644
--- a/example/parsar/tests/product.test.mjs
+++ b/example/parsar/tests/product.test.mjs
@@ -6,6 +6,32 @@ import { join } from "node:path";
import { homedir } from "node:os";
import { openStore, dataPath, AppError } from "../server/store.mjs";
import { productAPI } from "../server/product.mjs";
+import { runtimeEnvironment } from "../server/runtime-profile.mjs";
+
+test("environment template network maps to the official field and is omitted when enabled", async (t) => {
+ const store = openStore(":memory:");
+ t.after(() => store.close());
+ const { put } = await setup(store, async () => ({ id: randomUUID() }));
+ const request = async (network) =>
+ runtimeEnvironment(await put("runtimes", { name: "Net", network }));
+ assert.ok(!("network" in (await request(undefined))));
+ assert.ok(!("network" in (await request({ access: "enabled" }))));
+ assert.deepEqual((await request({ access: "disabled" })).network, {
+ access: "disabled",
+ });
+ assert.deepEqual(
+ (
+ await request({
+ access: "restricted",
+ allowed_domains: [" api.example.com ", ""],
+ })
+ ).network,
+ { access: "restricted", allowed_domains: ["api.example.com"] },
+ );
+ await assert.rejects(put("runtimes", { name: "Bad", network: { access: "x" } }), {
+ status: 400,
+ });
+});
test("environment templates prepare hosted Sessions, protect variables and freeze retries", async (t) => {
const store = openStore(":memory:");