diff --git a/CHANGELOG.md b/CHANGELOG.md index 54cd2301..859df7e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ ### 新增 - 组织页双向互生长:FreeOS 资产经 `/api/freeos/ingest`(共享写入令牌,无需边车登录墙)落到 openXYOS 员工/人才/插件/技能表;控制面导出再装配回 FreeOS。循环在边车健康时不再把「已推送到控制面」写成 false。 -- 组织页可从本地语料或 ima / 云知识库指针一键生产同事;本地模型一键下载后会注册到 Ollama 供应商。 +- 组织页可从本地语料或 ima / 云知识库指针一键生产尚未入部门的专家 / 智能助手;本地模型一键下载后会注册到 Ollama 供应商。 +- 组织页用语对齐:员工=已入部门;同事=同一组织的同伴;专家 / 智能助手=尚未入部门。导入把部门员工登记为同事;导出把专家写入控制面员工。 ### 变更 @@ -17,7 +18,7 @@ - 边车启动会写入并传递 `FREEOS_INGEST_TOKEN`;Python 启动器同时识别 `%LOCALAPPDATA%\FreeOS\openxyos`、`$INSTDIR\openxyos` 与 portable `org-sidecar`。 - Windows 安装包写入预构建 openXYOS 运行包(`$INSTDIR\openxyos` + `openxyos-runtime.zip`),首次启动再落到 `%LOCALAPPDATA%\FreeOS\openxyos` / `~/.freeos/openxyos`,自动拉起 `http://127.0.0.1:3780`,组织页不必再点「启动边车」。 - 项目创建用系统文件夹选择器(Wails 原生对话框,任意盘符);项目/任务/对话区默认展示实时对话列表,并提供建群/群聊入口。 -- Organization OS 首页双循环:FreeOS 数据面与 openXYOS 控制面的实时状态、一键装配员工 / 打包回流 / 运行 org loop,以及边车离线时的启动恢复。 +- Organization OS 首页双循环:FreeOS 数据面与 openXYOS 控制面的实时状态、一键导入部门员工为同事 / 打包专家为员工 / 运行 org loop,以及边车离线时的启动恢复。 - FreeOS 官网源码:`website/`(Vite + React 静态站,正式域名 https://freeos.cnxy.tech)。视觉与章节节奏对齐 XYAI Labs(天蓝→亮光、毛玻璃、01/02/…、底座 / 系统 / 生态),文案仍是 FreeOS 产品自己的话;圆形标志 + XYAI 机器人六姿态,无 Octop 红或章鱼形象。构建产物可整包上传到 `/www/wwwroot/freeos.cnxy.tech`。 ### 变更 diff --git a/README.md b/README.md index 7861e8fd..2b81ef3a 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ Architecture

-**FreeOS** is an independent open-source host. The data plane is derived from [Octop](https://github.com/TencentCloud/Octop) (MIT). The organization control plane comes from [openXYOS](https://github.com/XYAIStudio/openXYOS) (Apache-2.0). They are complementary: FreeOS **produces** AI employees, skills, plugins, and MCPs; those assets **assemble into openXYOS**; openXYOS blueprints, catalog, policies, and talent **feed back** into FreeOS to spawn or upgrade more runtime agents. +**FreeOS** is an independent open-source host. The data plane is derived from [Octop](https://github.com/TencentCloud/Octop) (MIT). The organization control plane comes from [openXYOS](https://github.com/XYAIStudio/openXYOS) (Apache-2.0). They are complementary: FreeOS **produces** experts / assistants (not yet in a department), skills, plugins, and MCPs; those assets **assemble into openXYOS** as department employees; openXYOS blueprints, catalog, policies, and talent **feed back** into FreeOS as colleagues. The web shell, README banner, favicons, and PWA icons use the FreeOS circular mark (gray ring, yellow / green / red teardrops, blue center). ## Run the self-growth loop -This is the product path. One command compiles a blueprint into an AI employee, promotes it `draft → market → recruit → shadow → active`, registers a FreeOS chat agent, publishes an asset pack, applies that pack onto openXYOS-shaped surfaces (live HTTP when `OPENXYOS_BASE_URL` is set, otherwise a durable local mirror), imports the control plane back, and proves governance still **blocks** high-risk tools. +This is the product path. One command compiles a blueprint into a colleague, promotes it `draft → market → recruit → shadow → active`, registers a FreeOS chat agent, publishes an asset pack, applies that pack onto openXYOS-shaped surfaces as department employees (live HTTP when `OPENXYOS_BASE_URL` is set, otherwise a durable local mirror), imports the control plane back, and proves governance still **blocks** high-risk tools. ```bash git clone https://github.com/XYAIStudio/FreeOS.git diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index 7660e674..8523afb7 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -22,7 +22,7 @@ "controlPlane": "Control plane", "flowUp": "Assets back", "flowDown": "Blueprints out", - "metricEmployees": "Digital employees", + "metricEmployees": "Colleagues / experts", "metricSkills": "Org skills", "metricMcp": "MCP / connectors", "metricTasks": "Tasks", @@ -31,25 +31,25 @@ "metricHealth": "Control-plane health", "metricTenant": "Tenant", "assembleTitle": "Bring openXYOS into FreeOS", - "assembleBody": "Import AI employees, skills, and plugins from the live control plane (or bundled blueprints) and register them for chat.", + "assembleBody": "Import department employees, skills, and plugins from the live control plane (or bundled blueprints) and register them as colleagues.", "assembleAction": "Import into FreeOS", - "assembleDone": "Imported {{count}} digital employee(s) into FreeOS", + "assembleDone": "Imported {{count}} colleague(s) into FreeOS", "packTitle": "Send FreeOS assets to openXYOS", - "packBody": "Pack FreeOS agents, skills, plugins, and MCP, then land them on the control plane. You will see what was written.", + "packBody": "Pack experts / assistants not yet in a department, plus skills, plugins, and MCP, then land them on the control plane as department employees. You will see what was written.", "packAction": "Push to openXYOS", "packDone": "Asset pack published", "packRemoteYes": "Landed on the live openXYOS control plane", "packRemoteNo": "Sidecar offline: wrote the local mirror only", "loopTitle": "Run the growth loop", - "loopBody": "Produce on FreeOS, assemble into openXYOS, feed back, and leave durable proof below.", + "loopBody": "Produce experts / assistants on FreeOS, land them on the control plane as department employees, feed back as colleagues, and leave durable proof below.", "loopAction": "Run growth loop", - "produceTitle": "Produce a colleague from a corpus", - "produceBody": "Distill a local folder or attach an ima / cloud knowledge URL, then compile a usable FreeOS expert.", + "produceTitle": "Produce an expert from a corpus", + "produceBody": "Distill a local folder or attach an ima / cloud knowledge URL, then compile a FreeOS expert / assistant that is not yet in a department.", "produceAction": "Produce from corpus", "produceDone": "Produced {{name}}", "produceKb": "Knowledge base id (optional)", "produceIma": "ima / cloud knowledge URL (optional)", - "produceName": "Colleague name", + "produceName": "Expert name", "landedTitle": "What landed", "landedEmpty": "Nothing landed on the control plane yet.", "yes": "Yes", @@ -57,24 +57,24 @@ "progressAssemble": "Importing from openXYOS…", "progressPack": "Packing and pushing to openXYOS…", "progressLoop": "Running the growth loop…", - "progressProduce": "Producing a colleague from the corpus…", + "progressProduce": "Producing an expert from the corpus…", "loopOk": "Organization loop completed", "loopPartial": "Loop ran; some steps did not finish (see timeline)", "timelineTitle": "Loop timeline", "timelineEmpty": "No loop proof yet. Run the organization loop to see real results here.", "timelineOk": "Loop result", - "timelineEmployees": "Employees", + "timelineEmployees": "Colleagues", "timelineSkills": "Skills", "timelineRemote": "Pushed to control plane", "timelineGovernance": "High-risk tool blocked by governance", "emptyTitle": "First look at Organization", - "emptyBody": "FreeOS on the left owns employees, skills, MCP, and tasks. openXYOS on the right owns blueprints, modules, and approvals. Start the sidecar, or run the loop once to see a real cycle.", + "emptyBody": "FreeOS on the left owns colleagues, experts / assistants not yet in a department, skills, MCP, and tasks. openXYOS on the right owns department employees, blueprints, modules, and approvals. Start the sidecar, or run the loop once to see a real cycle.", "startSidecarTitle": "openXYOS sidecar is offline", "startSidecarBody": "Live approvals and remote apply need the sidecar. Start it here; if no launcher is bundled, use the command beside the button.", "startSidecarAction": "Start sidecar", "sidecarStarted": "Sidecar is reachable", "sidecarStartPending": "Start attempted; refresh in a moment", - "bridgeHint": "FreeOS produces employees, skills, plugins, and MCPs. Those assets assemble into openXYOS; org blueprints and policies feed back into FreeOS.", + "bridgeHint": "FreeOS produces experts / assistants not yet in a department, plus skills, plugins, and MCPs. Those assets assemble into openXYOS as department employees; org blueprints and policies feed back as colleagues.", "home": "Data home", "sidecar": "Sidecar URL", "notesTitle": "How this module is wired", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 48a45841..ba6d18a7 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -22,7 +22,7 @@ "controlPlane": "控制面", "flowUp": "资产回流", "flowDown": "蓝图下发", - "metricEmployees": "数字员工", + "metricEmployees": "同事 / 专家", "metricSkills": "组织技能", "metricMcp": "MCP / 连接器", "metricTasks": "任务", @@ -31,25 +31,25 @@ "metricHealth": "控制面健康", "metricTenant": "租户", "assembleTitle": "把 openXYOS 装进 FreeOS", - "assembleBody": "从在线控制面导入 AI 员工、技能和插件(或使用捆绑蓝图),并注册到对话。", + "assembleBody": "从在线控制面导入已入部门的员工、技能和插件(或使用捆绑蓝图),注册为本组织同事。", "assembleAction": "导入到 FreeOS", - "assembleDone": "已导入 {{count}} 名数字员工到 FreeOS", + "assembleDone": "已导入 {{count}} 名同事到 FreeOS", "packTitle": "把 FreeOS 资产推到 openXYOS", - "packBody": "打包 FreeOS 智能体、技能、插件与 MCP,写入控制面,并显示落地结果。", + "packBody": "打包尚未入部门的专家 / 智能助手、技能、插件与 MCP,写入控制面成为部门员工,并显示落地结果。", "packAction": "推送到 openXYOS", "packDone": "资产包已发布", "packRemoteYes": "已写入在线 openXYOS 控制面", "packRemoteNo": "边车离线:仅写入本地镜像", "loopTitle": "运行自增长循环", - "loopBody": "在 FreeOS 生产、装配进 openXYOS、再回流,并在下方留下可核对的证据。", + "loopBody": "在 FreeOS 生产专家 / 智能助手,写入控制面成为部门员工,再回流为本组织同事,并在下方留下可核对的证据。", "loopAction": "运行自增长循环", - "produceTitle": "从语料生产同事", - "produceBody": "蒸馏本地文件夹,或挂接 ima / 云知识库地址,编译成可用的 FreeOS 专家。", + "produceTitle": "从语料生产专家", + "produceBody": "蒸馏本地文件夹,或挂接 ima / 云知识库地址,编译成尚未入部门的 FreeOS 专家 / 智能助手。", "produceAction": "从语料生产", "produceDone": "已生产 {{name}}", "produceKb": "知识库编号(可选)", "produceIma": "ima / 云知识库地址(可选)", - "produceName": "同事名称", + "produceName": "专家名称", "landedTitle": "落地结果", "landedEmpty": "控制面还没有落地记录。", "yes": "是", @@ -57,24 +57,24 @@ "progressAssemble": "正在从 openXYOS 导入…", "progressPack": "正在打包并推送到 openXYOS…", "progressLoop": "正在运行自增长循环…", - "progressProduce": "正在从语料生产同事…", + "progressProduce": "正在从语料生产专家…", "loopOk": "组织循环已跑通", "loopPartial": "循环已执行,部分步骤未完成(见时间线)", "timelineTitle": "循环时间线", "timelineEmpty": "还没有循环记录。运行组织循环后,这里会显示真实结果。", "timelineOk": "循环结果", - "timelineEmployees": "员工", + "timelineEmployees": "同事", "timelineSkills": "技能数", "timelineRemote": "已推送到控制面", "timelineGovernance": "高风险工具被治理拦截", "emptyTitle": "第一次打开组织", - "emptyBody": "左侧 FreeOS 负责员工、技能、MCP 与任务;右侧 openXYOS 负责蓝图、模块与审批。先启动边车,或直接运行组织循环看一次真实闭环。", + "emptyBody": "左侧 FreeOS 负责同事、尚未入部门的专家 / 智能助手、技能、MCP 与任务;右侧 openXYOS 负责部门员工、蓝图、模块与审批。先启动边车,或直接运行组织循环看一次真实闭环。", "startSidecarTitle": "openXYOS 边车未运行", "startSidecarBody": "控制面离线时无法做 live 审批与远程回流。可一键启动本机边车;若启动器不在当前环境,请按旁边的命令手动启动。", "startSidecarAction": "启动边车", "sidecarStarted": "边车已可达", "sidecarStartPending": "已尝试启动,请稍候再刷新", - "bridgeHint": "FreeOS 生产员工、技能、插件与 MCP;这些资产组装进 openXYOS,组织蓝图与策略再回流到 FreeOS。", + "bridgeHint": "FreeOS 生产尚未入部门的专家 / 智能助手、技能、插件与 MCP;推入 openXYOS 后成为部门员工;组织蓝图与策略再回流,登记为本组织同事。", "home": "数据目录", "sidecar": "边车地址", "notesTitle": "模块如何接入", diff --git a/docs/asset-loop.md b/docs/asset-loop.md index 77e30329..2d76761f 100644 --- a/docs/asset-loop.md +++ b/docs/asset-loop.md @@ -2,9 +2,9 @@ FreeOS **grows its own AI workforce**. The loop is the product: -1. **Produce** runtime assets on FreeOS (the data plane): AI employees, skills, plugins, MCP connectors. -2. **Assemble** those assets into openXYOS (the control plane) so org modules get stronger. -3. **Feed back** openXYOS org structure, blueprints, governance, the module catalog, and the talent market into FreeOS to spawn or upgrade more agents and skills. +1. **Produce** runtime assets on FreeOS (the data plane): experts / assistants not yet in a department, skills, plugins, MCP connectors. +2. **Assemble** those assets into openXYOS (the control plane) as department employees so org modules get stronger. +3. **Feed back** openXYOS org structure, blueprints, governance, the module catalog, and the talent market into FreeOS as colleagues — same-organization peers you can chat with. 4. **Repeat.** Each cycle adds colleagues and capabilities that are usable in FreeOS chat and governed. openXYOS does **not** replace FreeOS chat. FreeOS still runs the agents. Governance **blocks** high-risk tools until a human approves. diff --git a/src/octop/api/routers/org_module.py b/src/octop/api/routers/org_module.py index f00f4dbf..9db1d0ee 100644 --- a/src/octop/api/routers/org_module.py +++ b/src/octop/api/routers/org_module.py @@ -134,7 +134,7 @@ async def org_module_start_sidecar( return payload -@router.post("/assemble", summary="Assemble digital employees from an openXYOS blueprint") +@router.post("/assemble", summary="Import department employees from openXYOS as FreeOS colleagues") async def org_module_assemble( request: Request, server: OctopServer = Depends(get_server), @@ -154,7 +154,7 @@ class OrgProduceBody(BaseModel): name: str = "" -@router.post("/produce", summary="Produce a FreeOS colleague from a knowledge corpus") +@router.post("/produce", summary="Produce a FreeOS expert / assistant from a knowledge corpus") async def org_module_produce( request: Request, body: OrgProduceBody, diff --git a/src/octop/i18n/en.json b/src/octop/i18n/en.json index 38dd45fb..e0fd2274 100644 --- a/src/octop/i18n/en.json +++ b/src/octop/i18n/en.json @@ -713,14 +713,14 @@ "generated_skills": "generated {count} module skills from the openXYOS catalog", "compiled_blueprint": "compiled blueprint {name} → {slug}", "compiled_workspace": "compiled blueprint → {path}", - "spawned_agent": "spawned FreeOS agent {agent_id}", + "spawned_agent": "spawned FreeOS assistant {agent_id}", "imported_live": "imported openXYOS catalog/policies from the live sidecar", "sidecar_offline_compile": "sidecar offline; compiling bundled openXYOS blueprint fixtures", "wrote_policies": "wrote imported governance policies (fail-closed; engine loads them)", "local_mirror": "Local mirror is the durable record. HTTP apply is best-effort.", "nothing_auto_enabled": "Nothing is auto-enabled on the control plane.", "hero": "FreeOS does the work. openXYOS owns organization and governance.", - "planes": "Data plane: employees / skills / MCP / tasks. Control plane: blueprints / modules / governance.", + "planes": "Data plane: colleagues / experts / skills / MCP / tasks. Control plane: department employees / blueprints / modules / governance.", "sidecar_offline": "openXYOS sidecar is offline — start it to sync blueprints and approvals.", "mirror_only": "OPENXYOS_BASE_URL unset; applied to local mirror only.", "local_mirror_short": "Local mirror is the durable record.", @@ -732,13 +732,13 @@ "ingest_denied_mirror": "sidecar rejected the ingest token; mirror is complete", "public_ok": "control plane accepted drafts on public routes", "control_rejected": "control plane at {url} did not accept drafts; mirror is complete", - "loop_done": "Loop produced employees, published an asset pack, applied it, and imported back.", + "loop_done": "Loop produced colleagues, published an asset pack, applied department employees, and imported back.", "pack_review": "Not installed into the sidecar. Review openxyos/ then enable per tenant.", "pack_tenant": "One tenant = one workspace; do not unpack this pack into a shared sandbox.", "pack_redact": "Agent .env values are redacted except tenant/slug/schema keys.", "corpus_pointer": "attached cloud corpus pointer → {path}", "corpus_distilled": "distilled {count} corpus files → {path}", - "corpus_empty": "no knowledge mount; producing a corpus-ready colleague workspace", + "corpus_empty": "no knowledge mount; producing a corpus-ready expert workspace", "permissions_unreachable": "sidecar permissions unreachable; pass --policies or retry when signed in" }, "sidecar": { diff --git a/src/octop/i18n/zh.json b/src/octop/i18n/zh.json index eb86e7da..272da1ed 100644 --- a/src/octop/i18n/zh.json +++ b/src/octop/i18n/zh.json @@ -713,14 +713,14 @@ "generated_skills": "已从 openXYOS 目录生成 {count} 个模块技能", "compiled_blueprint": "已编译蓝图 {name} → {slug}", "compiled_workspace": "已编译蓝图 → {path}", - "spawned_agent": "已生成 FreeOS 智能体 {agent_id}", + "spawned_agent": "已生成 FreeOS 智能助手 {agent_id}", "imported_live": "已从在线边车导入 openXYOS 目录与策略", "sidecar_offline_compile": "边车离线;正在编译捆绑的 openXYOS 蓝图示例", "wrote_policies": "已写入导入的治理策略(默认关闭;引擎会加载)", "local_mirror": "本地镜像是可靠记录。HTTP 写入只是尽力而为。", "nothing_auto_enabled": "控制面不会自动启用任何模块。", "hero": "FreeOS 负责干活,openXYOS 负责组织与治理。", - "planes": "数据面:员工 / 技能 / MCP / 任务。控制面:蓝图 / 模块 / 治理。", + "planes": "数据面:同事 / 专家 / 技能 / MCP / 任务。控制面:部门员工 / 蓝图 / 模块 / 治理。", "sidecar_offline": "openXYOS 边车离线 — 启动后才能同步蓝图与审批。", "mirror_only": "未设置 OPENXYOS_BASE_URL;仅写入本地镜像。", "local_mirror_short": "本地镜像是可靠记录。", @@ -732,13 +732,13 @@ "ingest_denied_mirror": "边车拒绝了写入令牌;本地镜像已写完", "public_ok": "控制面已通过公开路由接受草稿", "control_rejected": "控制面 {url} 未接受草稿;本地镜像已写完", - "loop_done": "循环已生成员工、发布资产包、写入并回流。", + "loop_done": "循环已生成同事、发布资产包、写入部门员工并回流。", "pack_review": "尚未装进边车。请审阅 openxyos/ 后再按租户启用。", "pack_tenant": "一个租户对应一个工作区;不要把此包解到共享沙箱。", - "pack_redact": "智能体 .env 已脱敏,仅保留租户/别名/模式键。", + "pack_redact": "专家 / 智能助手 .env 已脱敏,仅保留租户/别名/模式键。", "corpus_pointer": "已挂接云知识库指针 → {path}", "corpus_distilled": "已蒸馏 {count} 个语料文件 → {path}", - "corpus_empty": "没有知识库挂载;正在生成可供语料使用的同事工作区", + "corpus_empty": "没有知识库挂载;正在生成可供语料使用的专家工作区", "permissions_unreachable": "边车权限接口不可达;请传入策略或登录后再试" }, "sidecar": { diff --git a/src/octop/modules/org_os/assets/importer.py b/src/octop/modules/org_os/assets/importer.py index 3c53b5e4..a2c45ac4 100644 --- a/src/octop/modules/org_os/assets/importer.py +++ b/src/octop/modules/org_os/assets/importer.py @@ -97,7 +97,7 @@ def import_openxyos_assets( if record is not None: spawned = spawn_colleague_agent(home, record) result.agents.append(spawned.agent_id) - result.notes.append(f"spawned FreeOS agent {spawned.agent_id}") + result.notes.append(f"spawned FreeOS assistant {spawned.agent_id}") if from_sidecar: client = OpenXyosControlClient(sidecar_url, home=home) @@ -144,7 +144,7 @@ def import_openxyos_assets( if record is not None: spawned = spawn_colleague_agent(home, record) result.agents.append(spawned.agent_id) - result.notes.append(f"spawned FreeOS agent {spawned.agent_id}") + result.notes.append(f"spawned FreeOS assistant {spawned.agent_id}") for item in exported.get("skills") or []: if not isinstance(item, dict): continue diff --git a/src/octop/modules/org_os/catalog.py b/src/octop/modules/org_os/catalog.py index a5afd4ff..d6e2bc29 100644 --- a/src/octop/modules/org_os/catalog.py +++ b/src/octop/modules/org_os/catalog.py @@ -52,8 +52,8 @@ class OrgCapability(TypedDict): "key": "employees", "label": "People & agents", "label_zh": "人机资源", - "description": "Human employees, AI employees, and talent market.", - "description_zh": "人类员工、AI 员工、备选员工与人才市场", + "description": "Department employees and the talent market.", + "description_zh": "已入部门的员工与人才市场", "locked": False, }, { diff --git a/src/octop/modules/org_os/empower.py b/src/octop/modules/org_os/empower.py index 06730269..68533729 100644 --- a/src/octop/modules/org_os/empower.py +++ b/src/octop/modules/org_os/empower.py @@ -2,9 +2,11 @@ from __future__ import annotations +import os from pathlib import Path from typing import Any +from octop.infra.utils.host_dirs import assert_safe_host_path from octop.modules.org_os.apply.apply import apply_asset_pack from octop.modules.org_os.assets.importer import import_openxyos_assets from octop.modules.org_os.assets.pack import publish_asset_pack @@ -16,6 +18,41 @@ from octop.modules.org_os.service import OrgModuleService +def _corpus_dest(home: Path) -> str: + """Return ``{FREEOS_HOME}/org-corpus/default`` only — no user path fragments.""" + home_s = os.path.realpath(os.fspath(home)) + dest_s = os.path.realpath(os.path.join(home_s, "org-corpus", "default")) + if dest_s != home_s and not dest_s.startswith(home_s + os.sep): + raise ValueError("corpus dest is outside FREEOS_HOME") + dest = assert_safe_host_path(dest_s, restrict_to_root=home_s) + dest_s = os.path.realpath(os.fspath(dest)) + os.makedirs(dest_s, exist_ok=True) + return dest_s + + +def _corpus_excerpts(dest_s: str) -> list[dict[str, str]]: + references: list[dict[str, str]] = [] + if not os.path.isdir(dest_s): + return references + for child in sorted(os.listdir(dest_s)): + if not child.endswith(".md") or child in {".", ".."}: + continue + if os.sep in child or "/" in child or "\\" in child: + continue + path_s = os.path.realpath(os.path.join(dest_s, child)) + if not path_s.startswith(dest_s + os.sep) or not os.path.isfile(path_s): + continue + try: + with open(path_s, encoding="utf-8") as handle: + excerpt = handle.read()[:1500] + except OSError: + continue + references.append({"name": os.path.splitext(child)[0], "excerpt": excerpt}) + if len(references) >= 12: + break + return references + + def assemble_from_blueprint(service: OrgModuleService) -> dict[str, Any]: """Import control-plane catalog/policies (when live) and compile real blueprints.""" tid = service.tenant_id() or "default" @@ -93,44 +130,41 @@ def produce_from_corpus( ima_url: str = "", name: str = "", ) -> dict[str, Any]: - """Build a digital employee from a local/cloud knowledge mount (ima pointer OK).""" + """Build an expert / assistant from a local/cloud knowledge mount (ima pointer OK).""" from octop.infra.knowledge.local_mount import ( attach_cloud_pointer, distill_readonly, load_mounts, ) + # Caller-supplied distill_path is accepted but never joined into filesystem + # paths — only ``{FREEOS_HOME}/org-corpus/`` is written. + _ = distill_path tid = service.tenant_id() or "default" - dest = ( - Path(distill_path) if distill_path else (service.home / "org-corpus" / (kb_id or "default")) - ) - dest.mkdir(parents=True, exist_ok=True) + dest_s = _corpus_dest(service.home) notes: list[str] = [] copied = 0 mounts = load_mounts(service.home) mount = mounts.get(kb_id) if kb_id else None cloud = ima_url or (mount.cloud_url if mount is not None else "") if cloud: - pointer = attach_cloud_pointer(cloud, str(dest), provider="ima") + pointer = attach_cloud_pointer(cloud, dest_s, provider="ima") copied = int(pointer.get("copied") or 0) - notes.append(f"attached cloud corpus pointer → {dest}") + notes.append(f"attached cloud corpus pointer → {dest_s}") elif mount is not None and mount.source_path: - distilled = distill_readonly(mount.source_path, str(dest)) + distilled = distill_readonly(mount.source_path, dest_s) copied = int(distilled.get("copied") or 0) - notes.append(f"distilled {copied} corpus files → {dest}") + notes.append(f"distilled {copied} corpus files → {dest_s}") else: - notes.append("no knowledge mount; producing a corpus-ready colleague workspace") + notes.append("no knowledge mount; producing a corpus-ready expert workspace") - references: list[dict[str, str]] = [] - for path in sorted(dest.glob("*.md"))[:12]: - excerpt = path.read_text(encoding="utf-8", errors="replace")[:1500] - references.append({"name": path.stem, "excerpt": excerpt}) + references = _corpus_excerpts(dest_s) capabilities = [item["name"] for item in references] or ["corpus-research", "org-writing"] - colleague = name.strip() or (kb_id.replace("_", " ").strip() if kb_id else "Corpus Analyst") + expert = name.strip() or (kb_id.replace("_", " ").strip() if kb_id else "Corpus Analyst") compiled = compile_blueprint( { "schema": "openxyos.agent-blueprint.v1", - "name": colleague, + "name": expert, "positioning": "Produces work from a distilled local or ima corpus.", "industry": "knowledge", "capabilities": capabilities, @@ -145,20 +179,20 @@ def produce_from_corpus( register_compiled( store, slug=compiled.slug, - name=colleague, + name=expert, workspace=compiled.workspace, lifecycle="draft", ) record = store.get(compiled.slug) if record is None: - raise RuntimeError(f"failed to register colleague {compiled.slug}") + raise RuntimeError(f"failed to register expert {compiled.slug}") spawned = spawn_colleague_agent(service.home, record) notes.append(f"compiled blueprint → {compiled.workspace}") - notes.append(f"spawned FreeOS agent {spawned.agent_id}") + notes.append(f"spawned FreeOS assistant {spawned.agent_id}") return { "slug": compiled.slug, "workspace": str(compiled.workspace), - "distill_path": str(dest), + "distill_path": dest_s, "copied": copied, "spawned": spawned.to_dict(), "notes": notes, diff --git a/src/octop/modules/org_os/loop/run.py b/src/octop/modules/org_os/loop/run.py index badb0d4a..5a8659a6 100644 --- a/src/octop/modules/org_os/loop/run.py +++ b/src/octop/modules/org_os/loop/run.py @@ -218,7 +218,7 @@ def run_growth_loop( *imported.notes, *applied.notes, *feedback.notes, - "Loop produced employees, published an asset pack, applied it, and imported back.", + "Loop produced colleagues, published an asset pack, applied department employees, and imported back.", ], ) proof.ok = bool( diff --git a/src/octop/modules/org_os/notes.py b/src/octop/modules/org_os/notes.py index f61faba9..82d35e3b 100644 --- a/src/octop/modules/org_os/notes.py +++ b/src/octop/modules/org_os/notes.py @@ -24,7 +24,7 @@ ("path",), ), ( - re.compile(r"^spawned FreeOS agent (.+)$"), + re.compile(r"^spawned FreeOS assistant (.+)$"), "org.notes.spawned_agent", ("agent_id",), ), @@ -88,8 +88,8 @@ ), ( re.compile( - r"^Data plane: employees / skills / MCP / tasks\. " - r"Control plane: blueprints / modules / governance\.$" + r"^Data plane: colleagues / experts / skills / MCP / tasks\. " + r"Control plane: department employees / blueprints / modules / governance\.$" ), "org.notes.planes", (), @@ -153,7 +153,7 @@ ), ( re.compile( - r"^Loop produced employees, published an asset pack, applied it, and imported back\.$" + r"^Loop produced colleagues, published an asset pack, applied department employees, and imported back\.$" ), "org.notes.loop_done", (), @@ -186,7 +186,7 @@ ("count", "path"), ), ( - re.compile(r"^no knowledge mount; producing a corpus-ready colleague workspace$"), + re.compile(r"^no knowledge mount; producing a corpus-ready expert workspace$"), "org.notes.corpus_empty", (), ), diff --git a/src/octop/modules/org_os/overview.py b/src/octop/modules/org_os/overview.py index 59c4c6c6..98c77a8c 100644 --- a/src/octop/modules/org_os/overview.py +++ b/src/octop/modules/org_os/overview.py @@ -118,7 +118,7 @@ def build_overview( } notes = [ "FreeOS does the work. openXYOS owns organization and governance.", - "Data plane: employees / skills / MCP / tasks. Control plane: blueprints / modules / governance.", + "Data plane: colleagues / experts / skills / MCP / tasks. Control plane: department employees / blueprints / modules / governance.", ] if not status.sidecar.reachable: notes.append("openXYOS sidecar is offline — start it to sync blueprints and approvals.") diff --git a/tests/unit/test_org_module.py b/tests/unit/test_org_module.py index 4763a976..66e18f33 100644 --- a/tests/unit/test_org_module.py +++ b/tests/unit/test_org_module.py @@ -222,6 +222,23 @@ def test_produce_from_corpus_spawns_colleague(tmp_path: Path) -> None: assert result["spawned"]["agent_id"] +def test_produce_from_corpus_ignores_outside_distill_path(tmp_path: Path) -> None: + from octop.modules.org_os.empower import produce_from_corpus + + outside = tmp_path.parent / "outside-corpus" + service = OrgModuleService(config_path=tmp_path / "config.json", home=tmp_path) + result = produce_from_corpus( + service, + name="Safe Analyst", + distill_path=str(outside / ".." / "etc"), + kb_id="../etc", + ) + dest = Path(result["distill_path"]) + assert dest == (tmp_path / "org-corpus" / "default").resolve() + assert dest.is_relative_to(tmp_path.resolve()) + assert not outside.exists() + + def test_governance_enable_persists(tmp_path: Path) -> None: config = tmp_path / "config.json" service = OrgModuleService(config_path=config, home=tmp_path) diff --git a/tests/unit/test_org_notes.py b/tests/unit/test_org_notes.py index b1d922ca..4622249e 100644 --- a/tests/unit/test_org_notes.py +++ b/tests/unit/test_org_notes.py @@ -8,7 +8,7 @@ def test_localize_loop_notes_zh() -> None: "started but not reachable yet; check logs/org-sidecar.log", "Local mirror is the durable record. HTTP apply is best-effort.", "control plane accepted the FreeOS ingest", - "Loop produced employees, published an asset pack, applied it, and imported back.", + "Loop produced colleagues, published an asset pack, applied department employees, and imported back.", ], "zh", ) @@ -16,7 +16,7 @@ def test_localize_loop_notes_zh() -> None: assert "尚未可达" in notes[1] assert "本地镜像" in notes[2] assert "控制面已接受" in notes[3] - assert "循环已生成员工" in notes[4] + assert "循环已生成同事" in notes[4] def test_localize_unknown_note_passthrough() -> None: