diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml
index 1acd8086..ba5cab6b 100644
--- a/.github/workflows/octop-desktop.yml
+++ b/.github/workflows/octop-desktop.yml
@@ -30,7 +30,7 @@ on:
default: true
type: boolean
release_tag:
- description: "GitHub Release tag to attach to (e.g. v0.0.1). Used when not running on a v* ref."
+ description: "GitHub Release tag to attach to (e.g. v0.0.2). Used when not running on a v* ref."
required: false
default: ""
type: string
@@ -187,6 +187,9 @@ jobs:
- name: Assemble green zip
env:
GREEN_HOST_PLAT: ${{ matrix.plat }}
+ # Default v0.0.2: Python host only. Set SHIP_OPENXYOS_RUNTIME=1 to
+ # embed Node/openXYOS (~500MB) in the portable zip + NSIS payload.
+ SKIP_ORG_SIDECAR: "1"
run: |
set -euo pipefail
echo "GREEN_HOST_PLAT=${GREEN_HOST_PLAT} RUNNER_ARCH=${RUNNER_ARCH:-} uname=$(uname -ms)"
@@ -220,20 +223,11 @@ jobs:
PYTHONNOUSERSITE=1 \
"$py" desktop/portable/verify_imports.py \
"${verify_args[@]}"
- if [[ ! -f "${staging}/org-sidecar/openxyos/backend/server.ts" ]]; then
- echo "org-sidecar missing under ${staging}" >&2
- exit 1
- fi
- if [[ ! -f "${staging}/org-sidecar/openxyos/dist/index.html" ]]; then
- echo "openXYOS frontend missing under ${staging}/org-sidecar/openxyos/dist" >&2
- exit 1
- fi
- if [[ -f "${staging}/org-sidecar/node/node.exe" || -x "${staging}/org-sidecar/node/bin/node" ]]; then
- echo "org-sidecar Node runtime present"
- else
- echo "bundled Node missing under ${staging}/org-sidecar/node" >&2
+ if [[ -d "${staging}/org-sidecar" ]]; then
+ echo "default v0.0.2 portable zip must not embed org-sidecar (set SHIP_OPENXYOS_RUNTIME=1 for opt-in builds)" >&2
exit 1
fi
+ echo "slim portable zip: org-sidecar absent (in-host Organization)"
# macOS: fail if any native extension linked Homebrew/MacPorts paths.
if [[ "${{ matrix.plat }}" == darwin-* ]]; then
REPO_ROOT="$PWD" # shellcheck source=desktop/portable/_common.sh
@@ -268,11 +262,15 @@ jobs:
- name: Package desktop app with bundled portable runtime
working-directory: desktop/src
+ env:
+ # Slim NSIS: do not pass SHIP_OPENXYOS_RUNTIME=1 (no openxyos-runtime.zip).
+ SHIP_OPENXYOS_RUNTIME: "0"
run: >-
wails3 task package
ARCH=${{ matrix.arch }}
VERSION=${{ env.OCTOP_VERSION }}
PORTABLE_ZIP=../portable/release/FreeOS-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip
+ SHIP_OPENXYOS_RUNTIME=0
# archive: false — upload the prebuilt zip as-is. Default archive=true would
# wrap it again, so Actions UI / "Download artifact" becomes zip-in-zip.
diff --git a/.github/workflows/sync-main-to-develop.yml b/.github/workflows/sync-main-to-develop.yml
index 57a768d6..520a70cf 100644
--- a/.github/workflows/sync-main-to-develop.yml
+++ b/.github/workflows/sync-main-to-develop.yml
@@ -14,7 +14,7 @@ on:
workflow_dispatch:
inputs:
version:
- description: "Release tag being synced, e.g. v0.0.1. Blank = read main's pyproject.toml."
+ description: "Release tag being synced, e.g. v0.0.2. Blank = read main's pyproject.toml."
required: false
type: string
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad8a655e..7ee432ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
### 修复
+
- Windows 安装预配不再把 Node 工作目录选到残留的嵌套 `openxyos\\openxyos`:顶层已有 `backend-dist/server.js` 与 `dist/index.html` 时必须用 live 根。嵌套 cwd 会让 `node backend-dist/server.js` 立刻退出且 stdout/stderr 为空,安装空等 90s 后以退出码 12 失败。`start-sidecar.ps1` / 预配 / 桌面 Go / Python 启动路径统一按此选择 cwd;每次启动截断 `start.log`;Shell.Application 若未刷新日志则改走 explorer / Start-Process,Node fail-fast 不再伪装成 livez 超时。
- Windows 安装详情不再把 openXYOS 预配的 UTF-8 Node/PowerShell 控制台(`[Error] POST /api/auth`、`[seed]`、中文 Server/WebSocket 状态)按系统 ANSI/GBK 打成乱码。NSIS 只用 `nsExec::Exec` 等退出码,详情页只显示本地化步骤结果;完整日志写入 `%LOCALAPPDATA%\\FreeOS\\openxyos\\provision.log` / `start.log`(UTF-8)。解压前停止并等待旧的 FreeOS openXYOS Node 退出;livez 通过即成功,不把启动期 auth 日志当失败。
- 组织页互生长通道两端都落到可选用状态:推送到 openXYOS 时员工/人才写成列表页能看到的 `active` / `internal` / `available`;从 openXYOS 回流时即使没有技能字段也会登记为同事,技能/插件/MCP 写入 `org-skills` / `org-plugins` / `org-mcps`,并把聊天专家挂到当前用户(修复 `user_id=NULL` 导致「专家」页看不见)。导入后跳到「专家」页,推送/循环后关闭管理抽屉并刷新预览。
@@ -97,11 +98,25 @@
- First launch on desktop / loopback opens a local guest session — no login wall. Register or sign in only when saving, exporting, or publishing to an account.
- Login, splash, favicons, and desktop icons use the circular XYAI mark. Product version remains 0.0.1.
+## [0.0.2] - 2026-09-18
+
+当前 FreeOS 产品版本,与 0.0.1 边车时代 Windows 安装包区分。`1.0.0` 对本阶段过早;后续按 [semver](https://semver.org/spec/v2.0.0.html) 随产品成熟度递增。规范来源是 `pyproject.toml`(同步 `octop.__version__`、桌面 / NSIS / FnOS 回退值,以及 CI 产物名)。发布工作流需要标签时使用 `v0.0.2`;不重写已推送的历史 tag。
+
+命令核对:`uv run freeos --version` 与 `uv run python -c "import octop; print(octop.__version__)"` 均应输出 `0.0.2`。
+
+### 变更
+
+- 桌面单一运行时:组织控制面能力在 FreeOS/Octop Python 宿主内运行(`org_os` / `/api/org-module/*` / 组织页原生工作台)。完整 openXYOS Node(3780)改为可选导出/同步,不再是安装或首屏的硬依赖。
+- 组织页默认不再嵌入 `127.0.0.1:3780`,也不再以 livez /「重启边车」挡住登录或首屏。互生长(导入 / 推送 / 循环)走宿主内镜像与同事/专家列表。
+- Windows 安装不再解压或启动 `openxyos-runtime`,也不再因 livez 失败中止安装。桌面启动默认不设置 `OPENXYOS_BASE_URL`,不拉起 Node;需要时设 `FREEOS_ORG_SIDECAR=1`。
+- `freeos org loop run` 不再 `ensure_sidecar` 或空等 3780。未配置边车时只写 `{FREEOS_HOME}/openxyos-mirror/` 与生命周期登记。
+- 产品版本从 `0.0.1` 升到 `0.0.2`,安装包 / NSIS DisplayVersion / `octop.__version__` 同步,便于与边车时代构建区分。后续 GitHub Release 标签为 `v0.0.2`。
+- 默认 Windows NSIS / 绿色便携包不再打入 `openxyos-runtime.zip` 或 `org-sidecar`(约 500MB Node 运行包)。`OPENXYOS_RUNTIME_ZIP_PRESENT` 默认关闭;仅 `SHIP_OPENXYOS_RUNTIME=1` 的构建才带边车。
+
## [0.0.1] - 2026-09-15
-当前 FreeOS 产品版本。`1.0.0` 对本阶段过早;后续按 [semver](https://semver.org/spec/v2.0.0.html) 随产品成熟度递增。规范来源是 `pyproject.toml`(同步 `octop.__version__`、桌面 / NSIS / FnOS 回退值,以及 CI 产物名)。发布工作流需要标签时使用 `v0.0.1`;不重写已推送的历史 tag。
+首个 FreeOS 产品版本(桌面默认拉起本机 openXYOS Node 边车)。后续版本见 [0.0.2]。规范来源是 `pyproject.toml`。历史发布标签为 `v0.0.1`;不重写已推送的历史 tag。
-命令核对:`uv run freeos --version` 与 `uv run python -c "import octop; print(octop.__version__)"` 均应输出 `0.0.1`。
### 新增
diff --git a/README.md b/README.md
index 2b81ef3a..2d11d728 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
.exe`(ARM 电脑)。
2. 双击安装包。安装程序会放到「程序文件」并创建开始菜单和桌面快捷方式。
3. 打开 **FreeOS**。第一次启动会解压内置运行环境(可能要一两分钟),然后直接进入可用会话,无需先登录。
-4. 保存、导出或发布到账号时再注册或登录。组织控制台(openXYOS)已随安装包内置,无需再装 Node;侧栏 **Organization** 默认打开。
+4. 保存、导出或发布到账号时再注册或登录。侧栏 **Organization** 使用宿主内组织能力,不必再装 Node,也不依赖本机 `127.0.0.1:3780`。完整 openXYOS Node 栈是可选项(导出/同步/高级部署)。
数据目录默认是 `%USERPROFILE%\.freeos`(可用环境变量 `FREEOS_HOME` 改)。旧版 Octop 的 `~/.octop` 仍会被识别。卸载安装包会清空安装目录(默认为 `Program Files\FreeOS`)并删除快捷方式,但**不会**删除该用户数据目录;详见 [desktop/README.md](desktop/README.md#windows-uninstall)。
@@ -71,7 +71,7 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md).
### Prerequisites
- Python 3.12+ (the project uses [uv](https://docs.astral.sh/uv/))
-- Node.js 20.19+ only if you start the organization sidecar from source, or rebuild the dashboard. The Windows installer already bundles Node + openXYOS.
+- Node.js 20.19+ only if you opt into the organization sidecar (`FREEOS_ORG_SIDECAR=1`) from source, or rebuild the dashboard. A fresh FreeOS install does not need Node.
### From this repository
@@ -112,22 +112,25 @@ Full host suite: `uv run pytest` / `make test-fast` (needs the usual extra servi
## Enable the organization module
-The host stays Python. openXYOS stays a TypeScript sidecar under `modules/openxyos/`. Toggle it from any of:
+The host stays Python. Organization first paint is the native dashboard page
+plus `/api/org-module/*` (catalog, ingest/import, growth loop, employees).
+The TypeScript sidecar under `modules/openxyos/` is optional.
| Surface | Action |
|---|---|
-| Dashboard | Sidebar → **Organization** → enable switch |
-| CLI | `uv run freeos org enable` · `uv run freeos org status` |
+| Dashboard | Sidebar → **Organization** (in-host workbench) |
+| CLI | `uv run freeos org enable` · `uv run freeos org status` · `uv run freeos org loop run` |
| Plugin | Admin → Plugins → **Organization OS** (`org-os`) |
| API | `PATCH /api/org-module` with `{ "enabled": true }` |
-Then start the sidecar (Node 20+):
+Optional Node sidecar (export/sync/advanced deploy only):
```bash
-bash scripts/run-org-sidecar.sh
+FREEOS_ORG_SIDECAR=1 bash scripts/run-org-sidecar.sh
```
-Default origin: `http://127.0.0.1:3780` (`FREEOS_ORG_SIDECAR_URL` / `OPENXYOS_BASE_URL` / `FREEOS_ORG_SIDECAR_PORT` to override). When `/api/health/livez` succeeds, `/organization` embeds the org console and `/api/org-module/sidecar/*` proxies to it.
+Default origin if opted in: `http://127.0.0.1:3780`. Organization and
+`freeos org loop run` do **not** wait for `/api/health/livez`.
**Auth:** FreeOS JWT users and openXYOS tenants are separate. The proxy forwards `X-FreeOS-User*` and `X-FreeOS-Tenant-Id`. One tenant = one workspace/sandbox — not prompt isolation.
diff --git a/README_CN.md b/README_CN.md
index ff9e225c..c6e95038 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -11,7 +11,7 @@
-
+
diff --git a/dashboard/src/api/modules/orgModule.ts b/dashboard/src/api/modules/orgModule.ts
index 9c11d45a..59f181f8 100644
--- a/dashboard/src/api/modules/orgModule.ts
+++ b/dashboard/src/api/modules/orgModule.ts
@@ -49,8 +49,24 @@ export interface OrgControlPlane {
approvals: number;
}
+export interface OrgColleague {
+ slug: string;
+ name: string;
+ lifecycle: string;
+ agent_id: string;
+ spawned: boolean;
+}
+
+export interface OrgExpert {
+ agent_id: string;
+ slug: string;
+ name: string;
+}
+
export interface OrgOverview {
enabled: boolean;
+ runtime?: "in_host" | string;
+ sidecar_optional?: boolean;
sidecar_reachable: boolean;
sidecar_embed_ok?: boolean;
sidecar_url: string;
@@ -61,6 +77,14 @@ export interface OrgOverview {
last_sync: string | null;
freeos: OrgPlaneCounts;
openxyos: OrgControlPlane;
+ colleagues?: OrgColleague[];
+ experts?: OrgExpert[];
+ org_surfaces?: {
+ employees: number;
+ talent: number;
+ skills: number;
+ plugins: number;
+ };
last_loop: Record | null;
notes: string[];
catalog: OrgCapability[];
diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json
index e852ab8b..0c2fc469 100644
--- a/dashboard/src/locales/en.json
+++ b/dashboard/src/locales/en.json
@@ -19,10 +19,25 @@
"statusBarTitle": "Organization",
"on": "Enabled",
"off": "Disabled",
- "sidecarUp": "Local openXYOS connected",
- "sidecarEmbedFailed": "Preview failed to load",
- "sidecarDown": "Opening local openXYOS",
- "sidecarOpening": "Opening local openXYOS",
+ "sidecarUp": "Optional openXYOS connected",
+ "sidecarEmbedFailed": "Optional preview failed to load",
+ "sidecarDown": "In-host organization",
+ "sidecarOpening": "In-host organization",
+ "inHostReady": "In-host organization ready",
+ "sidecarOptionalChip": "Node sidecar optional",
+ "advancedConsole": "Advanced: optional openXYOS",
+ "colleaguesTitle": "Colleagues and experts",
+ "colleaguesEmpty": "No colleagues yet. Import, produce, or run the growth loop.",
+ "orgSurfacesTitle": "Organization assets (in-host)",
+ "runtimeInHost": "FreeOS host",
+ "sidecarAdvancedHint": "The Node console on port 3780 is optional export/sync. Organization and the growth loop work without it.",
+ "packLocalDone": "Wrote the local organization mirror. Assets are selectable below and on Experts.",
+ "expertsHint": "Spawned colleagues are also listed on Experts.",
+ "surfaceEmployees": "Employees",
+ "surfaceTalent": "Talent",
+ "surfacePlugins": "Plugins",
+ "openExperts": "Open Experts",
+ "colleagueLifecycle": "Lifecycle",
"lastSync": "Last sync",
"restartSidecar": "Restart openXYOS frontend and backend",
"restartSidecarCta": "Restart frontend and backend",
@@ -104,7 +119,7 @@
"timelineRemote": "Pushed to control plane",
"timelineGovernance": "High-risk tool blocked by governance",
"emptyTitle": "First look at Organization",
- "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. After Setup the local console opens on its own; you can also 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. Organization on the right owns department employees, blueprints, modules, and approvals — all in this host. Run the loop once to see a real cycle. The Node sidecar is optional.",
"startSidecarTitle": "Opening local openXYOS",
"startSidecarBody": "Organization opens the local test URL http://127.0.0.1:3780 directly.",
"startSidecarAction": "Open local openXYOS",
diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json
index d25da8a3..66fc3aa6 100644
--- a/dashboard/src/locales/zh.json
+++ b/dashboard/src/locales/zh.json
@@ -19,10 +19,25 @@
"statusBarTitle": "组织",
"on": "已启用",
"off": "已关闭",
- "sidecarUp": "本机 openXYOS 已连接",
- "sidecarEmbedFailed": "预览页未能加载",
- "sidecarDown": "正在打开本机 openXYOS",
- "sidecarOpening": "正在打开本机 openXYOS",
+ "sidecarUp": "可选 openXYOS 已连接",
+ "sidecarEmbedFailed": "可选预览未能加载",
+ "sidecarDown": "宿主内组织已就绪",
+ "sidecarOpening": "宿主内组织已就绪",
+ "inHostReady": "宿主内组织已就绪",
+ "sidecarOptionalChip": "Node 边车可选",
+ "advancedConsole": "高级:可选 openXYOS",
+ "colleaguesTitle": "同事与专家",
+ "colleaguesEmpty": "还没有同事。请导入、从语料生产,或运行自增长循环。",
+ "orgSurfacesTitle": "组织资产(宿主内)",
+ "runtimeInHost": "FreeOS 宿主",
+ "sidecarAdvancedHint": "3780 上的 Node 控制台只用于导出/同步/高级部署。组织页和自增长循环不依赖它。",
+ "packLocalDone": "已写入本地组织镜像。资产可在下方和「专家」页选用。",
+ "expertsHint": "登记后的同事也会出现在「专家」页。",
+ "surfaceEmployees": "员工",
+ "surfaceTalent": "人才",
+ "surfacePlugins": "插件",
+ "openExperts": "打开专家页",
+ "colleagueLifecycle": "生命周期",
"lastSync": "最近同步",
"restartSidecar": "重启 openXYOS 前后端服务",
"restartSidecarCta": "重启前后端服务",
@@ -104,7 +119,7 @@
"timelineRemote": "已推送到控制面",
"timelineGovernance": "高风险工具被治理拦截",
"emptyTitle": "第一次打开组织",
- "emptyBody": "左侧 FreeOS 负责同事、尚未入部门的专家 / 智能助手、技能、MCP 与任务;右侧 openXYOS 负责部门员工、蓝图、模块与审批。安装完成后会自动打开本机 openXYOS;也可直接运行组织循环看一次真实闭环。",
+ "emptyBody": "左侧 FreeOS 负责同事、尚未入部门的专家 / 智能助手、技能、MCP 与任务;右侧组织能力负责部门员工、蓝图、模块与审批——都在本宿主内。直接运行组织循环即可看到真实闭环。Node 边车是可选项。",
"startSidecarTitle": "正在打开本机 openXYOS",
"startSidecarBody": "组织界面直接访问本机测试网址 http://127.0.0.1:3780。",
"startSidecarAction": "打开本机 openXYOS",
diff --git a/dashboard/src/pages/Organization/Organization.module.less b/dashboard/src/pages/Organization/Organization.module.less
index 7c1da28f..9bb3889e 100644
--- a/dashboard/src/pages/Organization/Organization.module.less
+++ b/dashboard/src/pages/Organization/Organization.module.less
@@ -1,13 +1,9 @@
/*
* Organization page layout
* ------------------------
- * The main view is a slim blue status bar + a full-area iframe. The iframe
- * box is height-constrained to the remaining window (not stretched to match
- * leftover chrome). That matters for openXYOS dialogs: `position:fixed`
- * inside the iframe is relative to the iframe element's box. A tall iframe
- * (old two-column layout) centered modals far below the visible FreeOS
- * viewport. Keeping the iframe == the visible pane centers overlays both
- * vertically and horizontally.
+ * The main view is a slim blue status bar + the in-host Organization
+ * workbench (catalog, colleagues, growth loop). The optional Node console
+ * is not required for first paint.
*/
.page {
position: relative;
@@ -19,6 +15,47 @@
gap: 0;
}
+.workbench {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 20px 24px 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.colleagueList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.colleagueRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ border-radius: 12px;
+ padding: 10px 14px;
+ background: var(--fn-bg-secondary);
+}
+
+.colleagueName {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 650;
+}
+
+.colleagueMeta {
+ margin: 2px 0 0;
+ font-size: 12px;
+ color: var(--fn-text-secondary);
+}
+
.statusBar {
display: flex;
align-items: flex-end;
diff --git a/dashboard/src/pages/Organization/index.test.tsx b/dashboard/src/pages/Organization/index.test.tsx
index c8398851..d8ce6bdc 100644
--- a/dashboard/src/pages/Organization/index.test.tsx
+++ b/dashboard/src/pages/Organization/index.test.tsx
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import OrganizationPage from "./index";
import { orgModuleApi, type OrgOverview } from "../../api/modules/orgModule";
import { message } from "../../utils/antdMessage";
-import { tryOpenInOrgBrowser } from "../../utils/orgBrowserHost";
function renderOrg() {
return render(
@@ -22,11 +21,13 @@ function renderOrg() {
const overview: OrgOverview = {
enabled: true,
- sidecar_reachable: true,
- sidecar_embed_ok: true,
+ runtime: "in_host",
+ sidecar_optional: true,
+ sidecar_reachable: false,
+ sidecar_embed_ok: false,
sidecar_url: "http://127.0.0.1:3780",
start_available: false,
- install_ready: true,
+ install_ready: false,
start_command: "",
home: "/tmp",
last_sync: null,
@@ -41,14 +42,27 @@ const overview: OrgOverview = {
tasks: 0,
},
openxyos: {
- reachable: true,
+ reachable: false,
url: "http://127.0.0.1:3780",
- detail: "ok",
+ detail: "sidecar optional; not configured",
modules: 4,
governance: true,
tenant_id: "1",
approvals: 0,
},
+ colleagues: [
+ {
+ slug: "ops-coordinator",
+ name: "Ops",
+ lifecycle: "active",
+ agent_id: "org-ops-coordinator",
+ spawned: true,
+ },
+ ],
+ experts: [
+ { agent_id: "org-ops-coordinator", slug: "ops-coordinator", name: "Ops" },
+ ],
+ org_surfaces: { employees: 1, talent: 0, skills: 2, plugins: 1 },
last_loop: null,
notes: [],
catalog: [
@@ -105,76 +119,64 @@ describe("OrganizationPage", () => {
vi.mocked(orgModuleApi.startSidecar).mockReset();
vi.mocked(orgModuleApi.restartSidecar).mockReset();
vi.mocked(orgModuleApi.probeLivez).mockReset();
- vi.mocked(orgModuleApi.probeLivez).mockResolvedValue({
- reachable: true,
- url: "http://127.0.0.1:3780",
- detail: "ok",
- });
vi.mocked(orgModuleApi.assemble).mockReset();
vi.mocked(orgModuleApi.pack).mockReset();
+ vi.mocked(orgModuleApi.runLoop).mockReset();
vi.mocked(message.success).mockReset();
vi.mocked(message.error).mockReset();
});
- it("embeds a multi-tab browser for local openXYOS and keeps extras behind drawers", async () => {
+ it("paints the in-host workbench without a 3780 iframe or livez gate", async () => {
renderOrg();
- await waitFor(() => {
- expect(screen.getByTestId("org-mini-browser")).toBeInTheDocument();
- });
- const frame = screen.getByTestId("org-browser-frame");
- expect(frame).toHaveAttribute(
- "src",
- expect.stringMatching(/^http:\/\/127\.0\.0\.1:3780\/\?freeos_embed=1/),
- );
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "http://127.0.0.1:3780/",
- );
- expect(screen.getByTestId("org-browser-back")).toBeDisabled();
- expect(screen.getByTestId("org-browser-forward")).toBeDisabled();
- expect(screen.getByTestId("org-browser-reload")).toBeEnabled();
- const restart = screen.getByTestId("org-restart-sidecar");
- const lastSync = screen.getByTestId("org-last-sync");
- expect(
- restart.compareDocumentPosition(lastSync) &
- Node.DOCUMENT_POSITION_FOLLOWING,
- ).toBeTruthy();
- expect(screen.queryByText("organization.openSidecar")).toBeNull();
- expect(screen.getByTestId("org-download-source")).toHaveTextContent(
- "organization.downloadSourceBar",
- );
-
- expect(screen.queryByText("organization.assembleAction")).toBeNull();
- expect(screen.queryByText("organization.startSidecarAction")).toBeNull();
- expect(screen.queryByTestId("org-download-source-drawer")).toBeNull();
-
- fireEvent.click(screen.getByTestId("org-enable-module"));
expect(
- await screen.findByText("organization.catalogTitle"),
+ await screen.findByTestId("org-native-workbench"),
).toBeInTheDocument();
+ expect(screen.getByTestId("org-runtime-chip")).toHaveTextContent(
+ "organization.inHostReady",
+ );
+ expect(screen.getByText("organization.assembleAction")).toBeInTheDocument();
+ expect(screen.getByText("organization.packAction")).toBeInTheDocument();
+ expect(screen.getByText("organization.loopAction")).toBeInTheDocument();
+ expect(screen.getByTestId("org-colleagues")).toHaveTextContent("Ops");
+ expect(screen.getByTestId("org-surfaces")).toBeInTheDocument();
+ expect(screen.queryByTestId("org-mini-browser")).toBeNull();
+ expect(screen.queryByTestId("org-browser-frame")).toBeNull();
+ expect(screen.queryByTestId("org-sidecar-gate")).toBeNull();
+ expect(screen.queryByTestId("org-restart-sidecar")).toBeNull();
+ expect(screen.queryByTestId("org-restart-overlay")).toBeNull();
+ expect(orgModuleApi.startSidecar).not.toHaveBeenCalled();
+ expect(orgModuleApi.probeLivez).not.toHaveBeenCalled();
+ });
- fireEvent.click(screen.getByTestId("org-manage-os"));
- expect(
- await screen.findByText("organization.assembleTitle"),
- ).toBeInTheDocument();
- expect(screen.getByText("organization.loopTitle")).toBeInTheDocument();
- expect(
- screen.getByTestId("org-download-source-drawer"),
- ).toBeInTheDocument();
- expect(screen.queryByTestId("org-preview-blank")).toBeNull();
+ it("does not auto-start or gate login when the optional sidecar is down", async () => {
+ vi.mocked(orgModuleApi.overview).mockResolvedValue({
+ ...overview,
+ sidecar_reachable: false,
+ start_available: true,
+ install_ready: true,
+ });
+ renderOrg();
+ await screen.findByTestId("org-native-workbench");
+ await new Promise((resolve) => setTimeout(resolve, 30));
+ expect(orgModuleApi.startSidecar).not.toHaveBeenCalled();
+ expect(screen.queryByTestId("org-sidecar-gate")).toBeNull();
+ expect(screen.getByText("organization.assembleAction")).toBeInTheDocument();
});
- it("downloads latest source from the status bar after picking a folder", async () => {
+ it("keeps sidecar start and source download behind Advanced", async () => {
pickDesktopFolder.mockResolvedValue("D:\\源码\\openXYOS");
vi.mocked(orgModuleApi.downloadSource).mockResolvedValue({
path: "D:\\源码\\openXYOS\\openXYOS-main.zip",
});
-
renderOrg();
- fireEvent.click(await screen.findByTestId("org-download-source"));
+ await screen.findByTestId("org-native-workbench");
+ expect(screen.queryByTestId("org-download-source")).toBeNull();
+ expect(screen.queryByTestId("org-start-sidecar")).toBeNull();
+ fireEvent.click(screen.getByTestId("org-advanced-console"));
+ fireEvent.click(await screen.findByTestId("org-download-source"));
await waitFor(() => {
- expect(pickDesktopFolder).toHaveBeenCalled();
expect(orgModuleApi.downloadSource).toHaveBeenCalledWith(
"D:\\源码\\openXYOS",
);
@@ -182,225 +184,37 @@ describe("OrganizationPage", () => {
expect(message.success).toHaveBeenCalledWith(
"organization.downloadSourceDone",
);
- expect(screen.queryByText("organization.assembleTitle")).toBeNull();
- });
-
- it("does not download when the folder picker is cancelled", async () => {
- pickDesktopFolder.mockResolvedValue(null);
-
- renderOrg();
- fireEvent.click(await screen.findByTestId("org-download-source"));
-
- await waitFor(() => {
- expect(pickDesktopFolder).toHaveBeenCalled();
- });
- expect(orgModuleApi.downloadSource).not.toHaveBeenCalled();
- expect(message.error).not.toHaveBeenCalled();
});
- it("opens trapped window.open / host URLs as extra tabs", async () => {
- renderOrg();
- await screen.findByTestId("org-mini-browser");
-
- expect(
- tryOpenInOrgBrowser("https://github.com/XYAIStudio/openXYOS", "GitHub"),
- ).toBe(true);
- await waitFor(() => {
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "https://github.com/XYAIStudio/openXYOS",
- );
- });
- expect(
- screen
- .getAllByTestId("org-browser-frame")
- .some(
- (node) =>
- node.getAttribute("src") ===
- "https://github.com/XYAIStudio/openXYOS",
- ),
- ).toBe(true);
-
- expect(window.open("https://example.com/docs", "_blank")).toBeNull();
- await waitFor(() => {
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "https://example.com/docs",
- );
- });
- });
-
- it("navigates the active tab from the address bar", async () => {
- renderOrg();
- const address = await screen.findByTestId("org-address-bar");
- fireEvent.change(address, {
- target: { value: "https://example.com/path" },
- });
- fireEvent.submit(address.closest("form") as HTMLFormElement);
- await waitFor(() => {
- expect(screen.getByTestId("org-browser-frame")).toHaveAttribute(
- "src",
- "https://example.com/path",
- );
- });
- expect(screen.getByTestId("org-browser-back")).toBeEnabled();
- fireEvent.click(screen.getByTestId("org-browser-back"));
- await waitFor(() => {
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "http://127.0.0.1:3780/",
- );
- });
- fireEvent.click(screen.getByTestId("org-browser-reload"));
- await waitFor(() => {
- expect(screen.getByTestId("org-browser-frame")).toHaveAttribute(
- "data-tab-id",
- "org-home",
- );
- });
- });
-
- it("restarts openXYOS and returns the embed to the local home page", async () => {
- vi.mocked(orgModuleApi.restartSidecar).mockImplementation(async () => {
- await new Promise((resolve) => setTimeout(resolve, 20));
- return {
- started: true,
- already: false,
- reachable: true,
- url: "http://127.0.0.1:3780",
- command: "start-sidecar",
- detail: "restarted openXYOS frontend and backend",
- launcher: "",
- };
- });
- renderOrg();
- const address = await screen.findByTestId("org-address-bar");
- fireEvent.change(address, {
- target: { value: "https://example.com/docs" },
- });
- fireEvent.submit(address.closest("form") as HTMLFormElement);
- fireEvent.click(screen.getByTestId("org-restart-sidecar"));
- const overlay = await screen.findByTestId("org-restart-overlay");
- expect(overlay).toBeInTheDocument();
- expect(overlay).toHaveTextContent("organization.restartOverlayTitle");
- expect(screen.getAllByTestId("org-restart-step")).toHaveLength(5);
- await waitFor(() => {
- expect(orgModuleApi.restartSidecar).toHaveBeenCalled();
- });
- await waitFor(() => {
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "http://127.0.0.1:3780/",
- );
- });
- expect(screen.queryByTestId("org-restart-overlay")).toBeNull();
- expect(message.success).toHaveBeenCalledWith(
- "organization.restartSidecarDone",
- );
- });
-
- it("gates the local test page when livez is down and prompts restart", async () => {
- vi.mocked(orgModuleApi.overview).mockResolvedValue({
- ...overview,
- sidecar_reachable: false,
- sidecar_embed_ok: false,
- start_available: false,
- install_ready: false,
- });
- vi.mocked(orgModuleApi.probeLivez).mockResolvedValue({
- reachable: false,
- url: "http://127.0.0.1:3780",
- detail: "sidecar unreachable",
- });
- renderOrg();
- expect(await screen.findByTestId("org-sidecar-gate")).toBeInTheDocument();
- expect(screen.getByTestId("org-sidecar-gate")).toHaveTextContent(
- "organization.previewNeedsRestart",
- );
- expect(screen.getByTestId("org-browser-frame")).toHaveAttribute(
- "src",
- "about:blank",
- );
- expect(screen.getByTestId("org-restart-sidecar")).toBeInTheDocument();
- vi.mocked(orgModuleApi.restartSidecar).mockImplementation(async () => {
- await new Promise((resolve) => setTimeout(resolve, 80));
- return {
- started: true,
- already: false,
- reachable: true,
- url: "http://127.0.0.1:3780",
- command: "start-sidecar",
- detail: "ok",
- launcher: "",
- };
- });
- fireEvent.click(screen.getByTestId("org-sidecar-gate-restart"));
- expect(
- await screen.findByTestId("org-restart-overlay"),
- ).toBeInTheDocument();
- });
-
- it("probes livez before opening a sidecar URL from the address bar", async () => {
- vi.mocked(orgModuleApi.overview).mockResolvedValue({
- ...overview,
+ it("imports into FreeOS and opens Experts so colleagues are selectable", async () => {
+ vi.mocked(orgModuleApi.assemble).mockResolvedValue({
sidecar_reachable: false,
- start_available: false,
- install_ready: false,
- });
- vi.mocked(orgModuleApi.probeLivez).mockResolvedValue({
- reachable: false,
- url: "http://127.0.0.1:3780",
- detail: "sidecar unreachable",
- });
- renderOrg();
- await screen.findByTestId("org-sidecar-gate");
- fireEvent.submit(
- screen.getByTestId("org-address-bar").closest("form") as HTMLFormElement,
- );
- await waitFor(() => {
- expect(orgModuleApi.probeLivez).toHaveBeenCalled();
+ employees: ["ops-coordinator"],
+ spawned: [
+ {
+ agent_id: "org-ops-coordinator",
+ slug: "ops-coordinator",
+ name: "Ops",
+ },
+ ],
+ imported: { skills: ["org-governance"], plugins: ["bridge"] },
+ skills: ["org-governance"],
+ plugins: ["bridge"],
+ preview_path: "/experts",
+ notes: [
+ "using in-host org storage and bundled openXYOS blueprint fixtures",
+ ],
});
- expect(screen.getByTestId("org-sidecar-gate")).toBeInTheDocument();
- expect(screen.getByTestId("org-browser-frame")).toHaveAttribute(
- "src",
- "about:blank",
- );
- });
- it("auto-starts the sidecar when Organization is opened offline", async () => {
- vi.mocked(orgModuleApi.overview).mockResolvedValue({
- ...overview,
- sidecar_reachable: false,
- sidecar_embed_ok: false,
- start_available: true,
- });
- vi.mocked(orgModuleApi.startSidecar).mockResolvedValue({
- started: true,
- already: false,
- reachable: true,
- url: "http://127.0.0.1:3780",
- command: "",
- detail: "ok",
- launcher: "",
- });
renderOrg();
- await waitFor(() => {
- expect(orgModuleApi.startSidecar).toHaveBeenCalled();
- });
- });
+ fireEvent.click(await screen.findByTestId("org-assemble"));
- it("surfaces a blank-preview error when livez is up but embed origin fails", async () => {
- vi.mocked(orgModuleApi.overview).mockResolvedValue({
- ...overview,
- sidecar_embed_ok: false,
- });
- renderOrg();
- expect(await screen.findByTestId("org-preview-blank")).toHaveTextContent(
- "organization.previewBlank",
- );
- expect(
- screen.getByText("organization.sidecarEmbedFailed"),
- ).toBeInTheDocument();
+ expect(await screen.findByTestId("experts-page")).toBeInTheDocument();
+ expect(orgModuleApi.assemble).toHaveBeenCalled();
+ expect(message.success).toHaveBeenCalledWith("organization.assembleDone");
});
- it("shows pack landed counts and opens the employees preview", async () => {
- vi.mocked(orgModuleApi.overview).mockResolvedValue(overview);
+ it("packs to the in-host mirror without opening a 3780 preview", async () => {
vi.mocked(orgModuleApi.pack).mockResolvedValue({
pack: {
directory: "/tmp/pack",
@@ -413,62 +227,47 @@ describe("OrganizationPage", () => {
applied: {
pack_dir: "/tmp/pack",
mirror_dir: "/tmp/mirror",
- remote_applied: true,
+ remote_applied: false,
mirrored: true,
- notes: ["control plane accepted the FreeOS ingest"],
- tenant_id: 2,
- preview_path: "/employees",
- landed: {
- tenant_id: 2,
- preview: "/employees",
- landed: {
- employees: { created: 2, updated: 0 },
- talent: { created: 2, updated: 0 },
- plugins: { created: 11, updated: 0 },
- skills: { created: 12, updated: 0 },
- mcp: { created: 1, updated: 0 },
- },
- },
+ notes: ["OPENXYOS_BASE_URL unset; applied to local mirror only"],
},
});
renderOrg();
- fireEvent.click(await screen.findByTestId("org-manage-os"));
- fireEvent.click(screen.getByTestId("org-pack"));
+ fireEvent.click(await screen.findByTestId("org-pack"));
- await waitFor(() => {
- expect(screen.getByTestId("org-address-bar")).toHaveValue(
- "http://127.0.0.1:3780/employees",
- );
- });
- expect(screen.queryByText("organization.assembleTitle")).toBeNull();
+ expect(await screen.findByTestId("org-last-receipt")).toHaveTextContent(
+ "organization.packLocalDone",
+ );
+ expect(screen.queryByTestId("org-browser-frame")).toBeNull();
expect(message.success).toHaveBeenCalledWith("organization.packDone");
});
- it("imports into FreeOS and opens Experts so colleagues are selectable", async () => {
- vi.mocked(orgModuleApi.assemble).mockResolvedValue({
- sidecar_reachable: true,
- employees: ["ops-coordinator"],
- spawned: [
- {
- agent_id: "org-ops-coordinator",
- slug: "ops-coordinator",
- name: "Ops",
- },
- ],
- imported: { skills: ["org-governance"], plugins: ["bridge"] },
- skills: ["org-governance"],
- plugins: ["bridge"],
- preview_path: "/experts",
- notes: ["imported live control-plane export"],
+ it("runs the growth loop without requiring sidecar health", async () => {
+ vi.mocked(orgModuleApi.runLoop).mockResolvedValue({
+ ok: true,
+ tenant_id: "1",
+ home: "/tmp",
+ skills: ["org-employees"],
+ employees: ["policy-analyst"],
+ agents: [{ slug: "policy-analyst" }],
+ lifecycle: { active: "active" },
+ pack_dir: "/tmp/pack",
+ mirror_dir: "/tmp/mirror",
+ remote_applied: false,
+ imported_roundtrip: {},
+ governance_blocked: true,
+ governance: {},
+ notes: ["Loop produced colleagues"],
});
renderOrg();
- fireEvent.click(await screen.findByTestId("org-manage-os"));
- fireEvent.click(screen.getByTestId("org-assemble"));
+ fireEvent.click(await screen.findByTestId("org-loop"));
- expect(await screen.findByTestId("experts-page")).toBeInTheDocument();
- expect(orgModuleApi.assemble).toHaveBeenCalled();
- expect(message.success).toHaveBeenCalledWith("organization.assembleDone");
+ await waitFor(() => {
+ expect(orgModuleApi.runLoop).toHaveBeenCalled();
+ });
+ expect(message.success).toHaveBeenCalledWith("organization.loopOk");
+ expect(screen.queryByTestId("org-sidecar-gate")).toBeNull();
});
});
diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx
index ea711ac1..e723abf8 100644
--- a/dashboard/src/pages/Organization/index.tsx
+++ b/dashboard/src/pages/Organization/index.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Drawer, Input, Space, Switch, Tag } from "antd";
import {
@@ -7,7 +7,6 @@ import {
Download,
Package,
Play,
- RefreshCw,
Settings2,
Shield,
Users,
@@ -31,40 +30,10 @@ import {
} from "../../utils/desktopFolder";
import { message } from "../../utils/antdMessage";
import { resolveOpenxyosSourceDest } from "./pickSourceDest";
-import { sidecarRecoverPhase, shouldShowPreviewBlank } from "./sidecarRecover";
-import { confirmSidecarLivez, sidecarPreviewGate } from "./sidecarLivez";
-import OrgMiniBrowser from "./OrgMiniBrowser";
-import OrgRestartOverlay from "./OrgRestartOverlay";
-import {
- DEFAULT_ORG_URL,
- closeOrgTab,
- createHomeTab,
- goBackOrgTab,
- goForwardOrgTab,
- isSidecarOriginUrl,
- navigateOrgTab,
- normalizeOrgUrl,
- openOrgTab,
- parseOrgNavigatedMessage,
- parseOrgOpenTabMessage,
- reloadOrgTab,
- sidecarOriginOf,
- tabTitleFromUrl,
- type OrgBrowserTab,
-} from "./orgBrowser";
-import { installOrgPageWindowTrap } from "./orgPageWindowTrap";
-import { registerOrgBrowserHost } from "../../utils/orgBrowserHost";
import styles from "./Organization.module.less";
-type ActionKey =
- | "assemble"
- | "pack"
- | "loop"
- | "sidecar"
- | "restart"
- | "produce"
- | null;
-type DrawerKey = "module" | "manage" | null;
+type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | "produce" | null;
+type DrawerKey = "module" | "advanced" | null;
type LastReceipt = {
kind: "assemble" | "pack" | "loop";
@@ -123,51 +92,6 @@ export default function OrganizationPage() {
const [produceName, setProduceName] = useState("");
const [produceIma, setProduceIma] = useState("");
const [landed, setLanded] = useState | null>(null);
- const autoStartRef = useRef(false);
- const iframeRefs = useRef>({});
- const livezOkRef = useRef(false);
- const [previewNonce, setPreviewNonce] = useState("");
- const [livezOk, setLivezOk] = useState(false);
- const [autoStarting, setAutoStarting] = useState(false);
- const [autoStartFailed, setAutoStartFailed] = useState(false);
- const [restartFinished, setRestartFinished] = useState(false);
- const [openBlocked, setOpenBlocked] = useState(false);
- const homeTitle = t("organization.homeTabTitle");
- const [tabs, setTabs] = useState(() => [
- createHomeTab(DEFAULT_ORG_URL, homeTitle),
- ]);
- const [activeId, setActiveId] = useState("org-home");
- const [addressValue, setAddressValue] = useState(DEFAULT_ORG_URL);
-
- const showPreview = useCallback(
- (path: string) => {
- const dest = normalizeOrgUrl(
- `${(overview?.sidecar_url || DEFAULT_ORG_URL).replace(/\/$/, "")}${
- path.startsWith("/") ? path : `/${path}`
- }`,
- );
- const origin = sidecarOriginOf(
- (overview?.sidecar_url || DEFAULT_ORG_URL).replace(/\/$/, ""),
- );
- if (isSidecarOriginUrl(dest, origin) && !livezOkRef.current) {
- void confirmSidecarLivez({
- origin,
- apiProbe: orgModuleApi.probeLivez,
- }).then((ok) => {
- setLivezOk(ok);
- livezOkRef.current = ok;
- if (!ok) setOpenBlocked(true);
- });
- }
- setTabs((current) =>
- navigateOrgTab(current, "org-home", dest, homeTitle),
- );
- setActiveId("org-home");
- setAddressValue(dest);
- setPreviewNonce(String(Date.now()));
- },
- [homeTitle, overview?.sidecar_url],
- );
const applyOverview = useCallback(
async (quiet = false) => {
@@ -175,9 +99,6 @@ export default function OrganizationPage() {
try {
const next = await orgModuleApi.overview();
setOverview(next);
- setLivezOk(Boolean(next.sidecar_reachable));
- livezOkRef.current = Boolean(next.sidecar_reachable);
- if (next.sidecar_reachable) setOpenBlocked(false);
if (next.module_toggles) setModuleToggles(next.module_toggles);
const proof = next.last_loop;
if (proof && typeof proof === "object" && proof.landed) {
@@ -198,43 +119,11 @@ export default function OrganizationPage() {
[t],
);
- const load = useCallback(async () => {
- await applyOverview(false);
- }, [applyOverview]);
-
- useEffect(() => {
- void load();
- }, [load]);
-
useEffect(() => {
- if (overview?.sidecar_reachable) return;
- const id = window.setInterval(() => {
- void applyOverview(true);
- }, 4000);
- return () => window.clearInterval(id);
- }, [applyOverview, overview?.sidecar_reachable]);
+ void applyOverview(false);
+ }, [applyOverview]);
- const sidecarUp = Boolean(overview?.sidecar_reachable) && livezOk;
- const embedOk = overview?.sidecar_embed_ok;
- const previewBlank = shouldShowPreviewBlank({
- sidecarUp,
- embedOk,
- });
- const recoverPhase = sidecarRecoverPhase({
- sidecarUp,
- installReady: Boolean(overview?.install_ready),
- startAvailable: Boolean(overview?.start_available),
- autoStarting,
- autoStartFailed,
- });
- const previewGate = sidecarPreviewGate({
- livezOk: sidecarUp,
- restarting: busy === "restart",
- });
- const showRestartGate =
- Boolean(overview) &&
- previewGate === "needsRestart" &&
- (openBlocked || recoverPhase === "needsRestart");
+ const sidecarUp = Boolean(overview?.sidecar_reachable);
const firstRun = useMemo(() => {
if (!overview) return false;
return (
@@ -261,15 +150,6 @@ export default function OrganizationPage() {
}
};
- const waitForSidecar = async () => {
- for (let i = 0; i < 30; i += 1) {
- const next = await applyOverview(true);
- if (next?.sidecar_reachable) return next;
- await new Promise((resolve) => window.setTimeout(resolve, 1000));
- }
- return applyOverview(true);
- };
-
const runAction = async (
key: Exclude,
fn: () => Promise,
@@ -277,7 +157,7 @@ export default function OrganizationPage() {
setBusy(key);
try {
await fn();
- if (key !== "sidecar" && key !== "restart") {
+ if (key !== "sidecar") {
await applyOverview(false);
}
} catch (err) {
@@ -291,46 +171,11 @@ export default function OrganizationPage() {
const startSidecar = () =>
runAction("sidecar", async () => {
- setAutoStarting(true);
- setAutoStartFailed(false);
- try {
- const result = await orgModuleApi.startSidecar();
- setLastActionNotes([result.detail, result.command].filter(Boolean));
- const next = result.reachable
- ? await applyOverview(true)
- : await waitForSidecar();
- if (next?.sidecar_reachable) {
- setAutoStartFailed(false);
- return;
- }
- setAutoStartFailed(true);
- } catch {
- setAutoStartFailed(true);
- } finally {
- setAutoStarting(false);
- }
+ const result = await orgModuleApi.startSidecar();
+ setLastActionNotes([result.detail, result.command].filter(Boolean));
+ await applyOverview(true);
});
- const canSilentStart = Boolean(
- overview?.start_available || overview?.install_ready,
- );
-
- useEffect(() => {
- if (autoStartRef.current) return;
- if (!overview) return;
- const embedBroken =
- overview.sidecar_reachable && overview.sidecar_embed_ok === false;
- if (overview.sidecar_reachable && !embedBroken) return;
- if (!embedBroken && !canSilentStart) return;
- autoStartRef.current = true;
- void startSidecar();
- }, [
- canSilentStart,
- overview,
- overview?.sidecar_reachable,
- overview?.sidecar_embed_ok,
- ]);
-
const toggleModule = async (key: string, enabled: boolean) => {
const next = { ...moduleToggles, [key]: enabled };
setModuleToggles(next);
@@ -448,9 +293,8 @@ export default function OrganizationPage() {
skills: landedCount(result.applied.landed, "skills"),
plugins: landedCount(result.applied.landed, "plugins"),
}),
- t("organization.packPreviewEmployees"),
]
- : [];
+ : [t("organization.packLocalDone")];
setLastActionNotes([
...result.pack.notes,
...result.applied.notes,
@@ -462,9 +306,6 @@ export default function OrganizationPage() {
});
setDrawer(null);
await applyOverview(true);
- if (result.applied.remote_applied) {
- showPreview(result.applied.preview_path || "/employees");
- }
message.success(t("organization.packDone"));
});
@@ -481,25 +322,11 @@ export default function OrganizationPage() {
`${t("organization.timelineRemote")}: ${flagLabel(
Boolean(proof.remote_applied),
)}`,
- ...(proof.remote_applied
- ? [
- t("organization.packLanded", {
- tenant: landedTenant(proof.landed),
- employees: landedCount(proof.landed, "employees"),
- talent: landedCount(proof.landed, "talent"),
- skills: landedCount(proof.landed, "skills"),
- plugins: landedCount(proof.landed, "plugins"),
- }),
- t("organization.packPreviewEmployees"),
- ]
- : []),
+ t("organization.packLocalDone"),
],
});
setDrawer(null);
await applyOverview(true);
- if (proof.remote_applied) {
- showPreview("/employees");
- }
message.success(
proof.ok ? t("organization.loopOk") : t("organization.loopPartial"),
);
@@ -514,8 +341,8 @@ export default function OrganizationPage() {
? t("organization.progressLoop")
: busy === "produce"
? t("organization.progressProduce")
- : busy === "restart"
- ? t("organization.restartingSidecar")
+ : busy === "sidecar"
+ ? t("organization.autoStartingSidecar")
: null;
const lastLoop = (loopProof ?? overview?.last_loop) as OrgLoopProof | null;
@@ -523,209 +350,8 @@ export default function OrganizationPage() {
? formatServerIsoDateTime(overview.last_sync, timeZone)
: t("organization.neverSynced");
const catalog = overview?.catalog ?? [];
- const disabledKeys = useMemo(
- () =>
- catalog
- .filter((row) => moduleToggles[row.key] === false)
- .map((row) => row.key),
- [catalog, moduleToggles],
- );
- const localConsoleUrl = (overview?.sidecar_url || DEFAULT_ORG_URL).replace(
- /\/$/,
- "",
- );
- const sidecarOrigin = sidecarOriginOf(localConsoleUrl);
-
- const confirmLivez = useCallback(async (): Promise => {
- const origin = sidecarOriginOf(
- (overview?.sidecar_url || DEFAULT_ORG_URL).replace(/\/$/, ""),
- );
- const ok = await confirmSidecarLivez({
- origin,
- apiProbe: orgModuleApi.probeLivez,
- });
- setLivezOk(ok);
- livezOkRef.current = ok;
- if (ok) setOpenBlocked(false);
- return ok;
- }, [overview?.sidecar_url]);
-
- const openTab = useCallback(
- (raw: string, title?: string, reuse = true) => {
- const url = normalizeOrgUrl(raw);
- const origin = sidecarOriginOf(
- (overview?.sidecar_url || DEFAULT_ORG_URL).replace(/\/$/, ""),
- );
- if (isSidecarOriginUrl(url, origin) && !livezOkRef.current) {
- void confirmLivez().then((ok) => {
- if (!ok) setOpenBlocked(true);
- });
- }
- const label = title || tabTitleFromUrl(url, homeTitle);
- let nextActive = "";
- let nextAddress = url;
- setTabs((current) => {
- const next = openOrgTab(current, url, label, reuse);
- nextActive = next.activeId;
- nextAddress =
- next.tabs.find((tab) => tab.id === next.activeId)?.url ?? url;
- return next.tabs;
- });
- if (nextActive) setActiveId(nextActive);
- setAddressValue(nextAddress);
- return true;
- },
- [confirmLivez, homeTitle, overview?.sidecar_url],
- );
-
- const submitAddress = useCallback(() => {
- const url = normalizeOrgUrl(addressValue, localConsoleUrl + "/");
- if (isSidecarOriginUrl(url, sidecarOrigin) && !livezOkRef.current) {
- void confirmLivez().then((ok) => {
- if (!ok) setOpenBlocked(true);
- });
- }
- setTabs((current) =>
- navigateOrgTab(current, activeId, url, tabTitleFromUrl(url, homeTitle)),
- );
- setAddressValue(url);
- }, [
- activeId,
- addressValue,
- confirmLivez,
- homeTitle,
- localConsoleUrl,
- sidecarOrigin,
- ]);
-
- const goHomeAfterRestart = useCallback(() => {
- const home = normalizeOrgUrl(localConsoleUrl + "/");
- setTabs([createHomeTab(home, homeTitle)]);
- setActiveId("org-home");
- setAddressValue(home);
- setPreviewNonce(String(Date.now()));
- }, [homeTitle, localConsoleUrl]);
-
- const onBrowserBack = useCallback(() => {
- setTabs((current) => {
- const next = goBackOrgTab(current, activeId, homeTitle);
- const url = next.find((tab) => tab.id === activeId)?.url;
- if (url) setAddressValue(url);
- return next;
- });
- }, [activeId, homeTitle]);
-
- const onBrowserForward = useCallback(() => {
- setTabs((current) => {
- const next = goForwardOrgTab(current, activeId, homeTitle);
- const url = next.find((tab) => tab.id === activeId)?.url;
- if (url) setAddressValue(url);
- return next;
- });
- }, [activeId, homeTitle]);
-
- const onBrowserReload = useCallback(() => {
- const tab = tabs.find((row) => row.id === activeId);
- if (
- tab &&
- isSidecarOriginUrl(tab.url, sidecarOrigin) &&
- !livezOkRef.current
- ) {
- void confirmLivez().then((ok) => {
- if (!ok) setOpenBlocked(true);
- });
- return;
- }
- setTabs((current) => reloadOrgTab(current, activeId));
- }, [activeId, confirmLivez, sidecarOrigin, tabs]);
-
- const restartSidecar = () =>
- runAction("restart", async () => {
- setRestartFinished(false);
- setOpenBlocked(false);
- const result = await orgModuleApi.restartSidecar();
- setLastActionNotes([result.detail, result.command].filter(Boolean));
- const next = result.reachable
- ? await applyOverview(true)
- : await waitForSidecar();
- if (next?.sidecar_reachable) {
- setRestartFinished(true);
- await new Promise((resolve) => window.setTimeout(resolve, 600));
- goHomeAfterRestart();
- message.success(t("organization.restartSidecarDone"));
- return;
- }
- message.error(
- t("organization.restartSidecarFailed", {
- detail: result.detail || t("organization.actionFailed"),
- }),
- );
- });
-
- const pushTogglesToPreview = useCallback(() => {
- for (const frame of Object.values(iframeRefs.current)) {
- frame?.contentWindow?.postMessage(
- { type: "freeos:module-toggles", disabled: disabledKeys },
- "*",
- );
- }
- }, [disabledKeys]);
-
- useEffect(() => {
- pushTogglesToPreview();
- }, [pushTogglesToPreview]);
-
- useEffect(() => {
- const home = normalizeOrgUrl(localConsoleUrl + "/");
- setTabs((current) => {
- const existing = current.find((tab) => tab.id === "org-home");
- if (!existing || existing.url === home) return current;
- if (existing.url !== DEFAULT_ORG_URL) return current;
- return navigateOrgTab(current, "org-home", home, homeTitle);
- });
- setAddressValue((current) =>
- current === DEFAULT_ORG_URL ? home : current,
- );
- }, [homeTitle, localConsoleUrl]);
-
- useEffect(() => {
- const onMessage = (event: MessageEvent) => {
- const opened = parseOrgOpenTabMessage(event.data);
- if (opened) {
- openTab(opened.url, opened.title);
- return;
- }
- const navigated = parseOrgNavigatedMessage(event.data);
- if (!navigated) return;
- setTabs((current) =>
- navigateOrgTab(
- current,
- activeId,
- navigated.url,
- navigated.title || tabTitleFromUrl(navigated.url, homeTitle),
- false,
- ),
- );
- setAddressValue(navigated.url);
- };
- window.addEventListener("message", onMessage);
- return () => window.removeEventListener("message", onMessage);
- }, [activeId, homeTitle, openTab]);
-
- useEffect(() => {
- const stopHost = registerOrgBrowserHost(openTab);
- const stopTrap = installOrgPageWindowTrap();
- return () => {
- stopTrap();
- stopHost();
- };
- }, [openTab]);
-
- useEffect(() => {
- if (sidecarUp) {
- setPreviewNonce((current) => current || String(Date.now()));
- }
- }, [sidecarUp]);
+ const colleagues = overview?.colleagues ?? [];
+ const orgSurfaces = overview?.org_surfaces;
return (
@@ -738,42 +364,19 @@ export default function OrganizationPage() {
{overview?.enabled ? t("organization.on") : t("organization.off")}
+
+ {t("organization.inHostReady")}
+
- {previewBlank
- ? t("organization.sidecarEmbedFailed")
- : sidecarUp
+ {sidecarUp
? t("organization.sidecarUp")
- : t("organization.sidecarOpening")}
+ : t("organization.sidecarOptionalChip")}
- }
- loading={busy === "restart"}
- disabled={busy !== null && busy !== "restart"}
- onClick={() => void restartSidecar()}
- data-testid="org-restart-sidecar"
- title={t("organization.restartSidecar")}
- >
- {t("organization.restartSidecar")}
-
{t("organization.lastSync")}: {lastSync}
- }
- loading={downloading || pickingFolder}
- onClick={() => void downloadSource()}
- data-testid="org-download-source"
- title={t("organization.downloadSource")}
- >
- {t("organization.downloadSourceBar")}
-
- {
- setActiveId(id);
- const tab = tabs.find((row) => row.id === id);
- if (tab) setAddressValue(tab.url);
- }}
- onCloseTab={(id) => {
- let nextActive = activeId;
- let nextAddress = addressValue;
- setTabs((current) => {
- const next = closeOrgTab(current, activeId, id);
- nextActive = next.activeId;
- nextAddress =
- next.tabs.find((row) => row.id === next.activeId)?.url ??
- addressValue;
- return next.tabs;
- });
- setActiveId(nextActive);
- setAddressValue(nextAddress);
- }}
- onNewTab={() => openTab(localConsoleUrl + "/", homeTitle, false)}
- onBack={onBrowserBack}
- onForward={onBrowserForward}
- onReload={onBrowserReload}
- onFrameLoad={pushTogglesToPreview}
- />
- {(busy === "restart" || showRestartGate) && (
- void restartSidecar()}
- />
- )}
-
-
- setDrawer(null)}
- width={480}
- destroyOnHidden
- >
-
-
-
- {t("organization.toggleLabel")}
- void toggle(checked)}
- />
-
-
{t("organization.glossary")}
-
-
- {t("organization.catalogTitle")}
-
-
- {t("organization.catalogToggleHint")}
-
-
- {catalog.map((row) => (
-
-
- {isZh ? row.label_zh : row.label}{" "}
-
- {row.locked
- ? t("organization.locked")
- : moduleToggles[row.key] === false
- ? t("organization.catalogDisabled")
- : t("organization.catalogEnabled")}
-
-
-
- {isZh ? row.description_zh : row.description}
-
- {!row.locked && (
-
- void toggleModule(row.key, checked)
- }
- />
- )}
-
- ))}
-
-
-
-
-
- setDrawer(null)}
- width={720}
- destroyOnHidden
- >
-
+
{t("organization.heroStory")}
{t("organization.glossary")}
@@ -984,7 +475,9 @@ export default function OrganizationPage() {
{t("organization.controlPlane")}
-
openXYOS
+
+ {t("organization.runtimeInHost")}
+
@@ -1009,9 +502,7 @@ export default function OrganizationPage() {
- {sidecarUp
- ? t("organization.sidecarUp")
- : t("organization.sidecarOpening")}
+ {t("organization.inHostReady")}
{t("organization.metricHealth")}
@@ -1029,6 +520,87 @@ export default function OrganizationPage() {
+
+
+ {t("organization.colleaguesTitle")}
+
+
+ {t("organization.expertsHint")}
+
+ {colleagues.length ? (
+
+ {colleagues.map((row) => (
+ -
+
+
{row.name}
+
+ {row.slug} · {t("organization.colleagueLifecycle")}:{" "}
+ {row.lifecycle}
+ {row.agent_id ? ` · ${row.agent_id}` : ""}
+
+
+
+ {row.spawned
+ ? t("organization.catalogEnabled")
+ : t("organization.catalogDisabled")}
+
+
+ ))}
+
+ ) : (
+
+ {t("organization.colleaguesEmpty")}
+
+ )}
+ navigate("/experts")}
+ data-testid="org-open-experts"
+ >
+ {t("organization.openExperts")}
+
+
+
+
+
+ {t("organization.orgSurfacesTitle")}
+
+
+
+
+ {metric(orgSurfaces?.employees)}
+
+
+ {t("organization.surfaceEmployees")}
+
+
+
+
+ {metric(orgSurfaces?.talent)}
+
+
+ {t("organization.surfaceTalent")}
+
+
+
+
+ {metric(orgSurfaces?.skills)}
+
+
+ {t("organization.metricSkills")}
+
+
+
+
+ {metric(orgSurfaces?.plugins)}
+
+
+ {t("organization.surfacePlugins")}
+
+
+
+
+
{progressLabel && (
{progressLabel}
@@ -1169,45 +741,115 @@ export default function OrganizationPage() {
)}
-
-
- {t("organization.landedTitle")}
-
- {landed ? (
+ {landed ? (
+
+
+ {t("organization.landedTitle")}
+
{JSON.stringify(landed, null, 2)}
- ) : (
- {t("organization.landedEmpty")}
- )}
+
+ ) : null}
+
+
+
+ {t("organization.catalogTitle")}
+
+
+ {t("organization.catalogToggleHint")}
+
+
+ {catalog.map((row) => (
+
+
+ {isZh ? row.label_zh : row.label}{" "}
+
+ {row.locked
+ ? t("organization.locked")
+ : moduleToggles[row.key] === false
+ ? t("organization.catalogDisabled")
+ : t("organization.catalogEnabled")}
+
+
+
+ {isZh ? row.description_zh : row.description}
+
+ {!row.locked && (
+
+ void toggleModule(row.key, checked)
+ }
+ />
+ )}
+
+ ))}
+
+
+
- {(sidecarUp || overview?.enabled) && (
-
+ setDrawer(null)}
+ width={480}
+ destroyOnHidden
+ >
+
+
+
+ {t("organization.toggleLabel")}
+ void toggle(checked)}
+ />
+
+
{t("organization.glossary")}
+
+
+
+ setDrawer(null)}
+ width={560}
+ destroyOnHidden
+ >
+
+
+ {t("organization.sidecarAdvancedHint")}
+
+
+ {overview?.sidecar_url || "http://127.0.0.1:3780"}
+
+
+ void startSidecar()}
+ data-testid="org-start-sidecar"
+ >
+ {t("organization.startSidecarAction")}
+
+ }
+ loading={downloading || pickingFolder}
+ onClick={() => void downloadSource()}
+ data-testid="org-download-source"
+ >
+ {t("organization.downloadSource")}
+
+
+ {!canPickDesktopFolder() && (
+
setSourceDest(event.target.value)}
+ placeholder={t("organization.downloadSourceDest")}
+ />
)}
diff --git a/desktop/README.md b/desktop/README.md
index 74118968..4828c9ba 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -1,8 +1,8 @@
# FreeOS desktop (Wails v3 + green portable)
Windows end-user product: **download the NSIS `.exe` → install → open FreeOS**
-(Octop host shell + bundled openXYOS organization console). This is **not**
-`src/octop/infra/desktop` (remote desktop streaming).
+(Octop host shell + in-host Organization). The full openXYOS Node stack is
+**optional**. This is **not** `src/octop/infra/desktop` (remote desktop streaming).
| Path | Role |
|------|------|
@@ -21,32 +21,17 @@ Same precedence as the FreeOS CLI/server:
5. new installs → `~/.freeos`
- Green runtime extract → `{home}/portable/`
-- Organization sidecar data → `{home}/org-os/`
-- Local openXYOS workdir (FE+BE) → `%LOCALAPPDATA%\FreeOS\openxyos` on Windows,
- else `{home}/openxyos`. During Setup the FreeOS installer `nsExec`s a
- shipped **provisioner subprocess** (`provision-openxyos.ps1`) that expands
- `openxyos-runtime.zip` with `tar.exe` into that live workdir (and keeps
- `$INSTDIR\openxyos` as a sealed copy; the `openxyos-runtime` zip and folder
- are deleted after a successful provision), starts FE/BE
- at medium integrity (Node inherits `CORS_ORIGIN` / `NODE_ENV`; stdout/stderr
- go to UTF-8 `start.log`), and writes `.install-ready` only after
- `http://127.0.0.1:3780/api/health/livez` is healthy. The Setup detail list
- shows localized step results only (`nsExec::Exec`, not `ExecToLog`); full
- PowerShell/Node console stays in `%LOCALAPPDATA%\FreeOS\openxyos\provision.log`
- and `start.log` so UTF-8 Chinese is not misread as GBK. Before extract it
- stops only FreeOS openXYOS Node (`start.pid` / live and `$INSTDIR\openxyos`
- paths) and waits until that owned process is gone. After extract+heal, Node
- starts with cwd at the live root when `backend-dist/server.js` and
- `dist/index.html` exist there (a leftover nested `openxyos\openxyos` tree
- must not win — that cwd exits immediately with an empty console). It unpacks into a LocalAppData
- temp dir, logs tar stderr to `provision.log`, and treats a non-zero tar as
- success when the layout heals. Transient `[Error] POST /api/auth` / `[seed]`
- lines in `start.log` are not a provision failure when livez is healthy. If
- Node dies before livez, the provisioner exits 10 with a `start.log` excerpt
- in `provision.log` instead of waiting out a livez timeout. A failed
- subprocess is a failed install step (retry the provisioner). There is no
- Windows logon autostart: when FreeOS starts later, it brings local
- openXYOS up with it. Organization embeds `http://127.0.0.1:3780` directly.
+- Organization data (in-host) → `{home}/tenants/`, `{home}/openxyos-mirror/`, `{home}/org-skills/`
+- Optional openXYOS Node sidecar data → `{home}/org-os/`
+- Optional local openXYOS workdir (FE+BE) → `%LOCALAPPDATA%\FreeOS\openxyos` on Windows,
+ else `{home}/openxyos`. **Setup does not extract or start this stack.**
+ Default v0.0.2 installers do **not** copy `openxyos-runtime.zip`.
+ Opt-in builds (`SHIP_OPENXYOS_RUNTIME=1`) may add the zip plus
+ `provision-openxyos.ps1` / `start-sidecar.ps1` for later
+ `FREEOS_ORG_SIDECAR=1` use. Missing zip, tar.exe, or
+ `http://127.0.0.1:3780/api/health/livez` must not fail install.
+ There is no Windows logon autostart. Organization first paint is the
+ FreeOS-native page (`/api/org-module/*`), not an iframe of 3780.
- Shell prefs → `{home}/desktop-settings.json`
## Windows install finish
@@ -58,38 +43,21 @@ unelevated explorer token so the first run does not stamp `%USERPROFILE%\.freeos
as High integrity. Uncheck to skip. Chinese installer strings are compiled
with `makensis -INPUTCHARSET UTF8` from a UTF-8 BOM `project.nsi`.
-The install log is no longer only `FreeOS.exe` + shortcuts. A healthy package
-also copies `openxyos-runtime.zip` plus the provisioner (`provision-openxyos.ps1`
-/ `.cmd`, `start-sidecar.ps1` / `.cmd`). Setup **nsExecs that provisioner as
-a child process** (`nsExec::Exec`, stdout discarded) and waits for exit 0.
-The child extracts with Windows
-`tar.exe` (quoted paths, so `Program Files` works) into a LocalAppData
-temp dir, then heals that tree into `%LOCALAPPDATA%\FreeOS\openxyos` after
-stopping only the FreeOS openXYOS Node that would lock those files and
-waiting until it is gone. tar
-stdout/stderr go to UTF-8 `provision.log`, not the NSIS detail list. A non-zero tar is not exit 3 when the
-layout is complete after heal. It then starts Node at medium integrity
-(IShellDispatch2 — never High-IL Node from the elevated installer), waits
-for livez, and writes `.install-ready` only when healthy. `$INSTDIR\openxyos`
-is the sealed install copy. After success the provisioner removes
-`$INSTDIR\openxyos-runtime.zip` (and similarly named archives) plus the
-`$INSTDIR\openxyos-runtime` folder, and a sibling `openxyos-runtime` under
-`%LOCALAPPDATA%\FreeOS` if one was staged there. A failed provision leaves
-those artifacts for debugging. A README-only tree or a
-failed health check is a provision failure, not a skipped success. Setup does
-**not** register HKCU Run or a logon scheduled task.
-
-Why LocalAppData, written by an elevated installer: `$INSTDIR` is typically
-`C:\Program Files\FreeOS`, which a later unelevated `FreeOS.exe` cannot
-mutate. The app’s live root is `%LOCALAPPDATA%\FreeOS\openxyos`. Setup
-reads the installing user’s `LOCALAPPDATA` environment variable (not NSIS
-`$LOCALAPPDATA` after `SetShellVarContext all`, which is ProgramData) and
-writes the complete FE+BE there so first launch does not pay a copy/unpack
-cost. Organization then embeds `http://127.0.0.1:3780` directly.
-Downloading the latest openXYOS source uses the same
-native folder picker as the project workdir (any drive), not a typed path only.
-
-Override the workdir with `FREEOS_OPENXYOS_HOME`.
+A healthy default package is **FreeOS.exe + the Python host** — no
+`openxyos-runtime.zip` (~500MB Node payload). Pass
+`SHIP_OPENXYOS_RUNTIME=1` to `wails3 task package` (and when assembling
+the green zip) to embed the zip plus `provision-openxyos.ps1` /
+`start-sidecar.ps1` for advanced Node export/sync. Setup **does not**
+`nsExec` the provisioner, extract with `tar.exe`, start Node, or wait
+for livez. A README-only or missing runtime tree is not an install
+failure. Setup does **not** register HKCU Run or a logon scheduled task.
+
+Organization uses the in-host `/api/org-module/*` surfaces. Downloading the
+latest openXYOS source (advanced) uses the same native folder picker as the
+project workdir (any drive), not a typed path only.
+
+Override an optional sidecar workdir with `FREEOS_OPENXYOS_HOME`. Opt in with
+`FREEOS_ORG_SIDECAR=1`.
## Windows uninstall
@@ -97,9 +65,9 @@ The NSIS uninstaller (Settings → Apps, or `uninstall.exe` in the install
folder) removes program-owned files. If FreeOS (or its host / org-sidecar)
is still running, it **asks** before continuing (Chinese / English). Cancel
aborts uninstall and leaves processes running. Confirm closes them
-(graceful, then force) and then wipes the install directory. Version stays
-`0.0.1`; rebuilds replace the existing GitHub Release `v0.0.1` assets
-rather than cutting a new tag.
+(graceful, then force) and then wipes the install directory. Product
+version is `0.0.2`; ship artifacts as `v0.0.2` rather than overwriting
+the sidecar-era `v0.0.1` Windows builds.
**Removes**
@@ -130,19 +98,14 @@ would treat that tree as program files (unless it is named `User Data` /
On first open the shell:
-1. Extracts the bundled portable **Python host** (the openXYOS FE+BE tree
- was already written to `%LOCALAPPDATA%\FreeOS\openxyos` during Setup).
-2. If that live workdir is already complete, skips any copy/unpack. A
- copy from `$INSTDIR` / `portable/org-sidecar` is heal-only (other
- Windows users, or a tree that Setup never populated).
-3. If `http://127.0.0.1:3780/api/health/livez` is already up from Setup,
- attaches to it. Otherwise FreeOS starts local openXYOS from the live
- workdir and waits until livez responds. There is no logon autostart.
-4. Starts the FreeOS host with `FREEOS_HOME`, `FREEOS_ORG_ENABLE=1`, and
- `FREEOS_OPENXYOS_HOME`.
-5. Opens the desktop window on the host UI with a local guest session (no
- login wall). Chat and **Organization** are both available — no separate
- Node install. Register or sign in later when a save needs an account.
+1. Extracts the bundled portable **Python host**.
+2. Does **not** extract or start `openxyos-runtime` unless `FREEOS_ORG_SIDECAR=1`.
+3. Starts the FreeOS host with `FREEOS_HOME` and `FREEOS_ORG_ENABLE=1`.
+ It does not set `OPENXYOS_BASE_URL` / `FREEOS_ORG_SIDECAR_URL` by default.
+4. Opens the desktop window on the host UI with a local guest session (no
+ login wall). Chat and **Organization** are both available in-host — no
+ Node process on 3780 is required. Register or sign in later when a save
+ needs an account.
## Build green zip
@@ -152,7 +115,9 @@ From repo root (needs Node 20+ so openXYOS can be built into the zip):
make -f desktop/portable/Makefile green
```
-Skip the sidecar only when debugging the Python runtime: `SKIP_ORG_SIDECAR=1`.
+The Node sidecar is optional. Package it with the green zip when you want
+advanced export/sync; skip it with `SKIP_ORG_SIDECAR=1`. Desktop/runtime
+never requires it for Organization.
CI: `.github/workflows/octop-desktop.yml` (**name:** FreeOS Desktop Package)
builds native platform/arch variants. `v*` tags and `workflow_dispatch`
diff --git a/desktop/package-release.sh b/desktop/package-release.sh
index 9a856b5b..66afe8ff 100755
--- a/desktop/package-release.sh
+++ b/desktop/package-release.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Build one native FreeOS desktop release: Dashboard → portable runtime
-# (including bundled Node + openXYOS) → Wails package.
+# (Python host; Node/openXYOS only if SHIP_OPENXYOS_RUNTIME=1) → Wails package.
# Run once per native platform; the GitHub Actions matrix runs all six variants.
set -euo pipefail
diff --git a/desktop/portable/README.md b/desktop/portable/README.md
index 1bd2142c..677b0612 100644
--- a/desktop/portable/README.md
+++ b/desktop/portable/README.md
@@ -1,6 +1,6 @@
# FreeOS 绿色便携包(多平台)
-解压即用:内置便携 CPython + FreeOS 宿主 + Node/openXYOS 组织边车,通过 `start.sh` / `start.bat` 启动。
+解压即用:内置便携 CPython + FreeOS 宿主(组织能力在宿主内)。Node/openXYOS 边车可选(`FREEOS_ORG_SIDECAR=1`),通过 `start.sh` / `start.bat` 启动。
**不依赖**系统 Python / Node。单独解压绿包时用浏览器打开 Dashboard;完整「安装即用」产品是 Wails NSIS 安装包。
首启走正常 setup wizard。
@@ -35,7 +35,7 @@ make -f desktop/portable/Makefile green
FreeOS-/
runtime/ # python-build-standalone
packages/ # FreeOS 宿主 + 依赖(site-packages,可搬迁)
- org-sidecar/ # 内置 Node + 已构建的 openXYOS
+ org-sidecar/ # 可选;默认不打。SHIP_OPENXYOS_RUNTIME=1 才带 Node + openXYOS
launch.py # 启动引导(site.addsitedir / Windows pywin32)
start.sh # macOS / Linux
start.bat # Windows
diff --git a/desktop/portable/package.sh b/desktop/portable/package.sh
index c6e64a4e..8b592563 100755
--- a/desktop/portable/package.sh
+++ b/desktop/portable/package.sh
@@ -14,7 +14,7 @@
# FreeOS-/
# runtime/ portable CPython
# packages/ site-packages (uv --target, relocatable)
-# org-sidecar/ bundled Node + openXYOS (organization console)
+# org-sidecar/ optional Node + openXYOS (only if SHIP_OPENXYOS_RUNTIME=1)
# start.sh / start.bat
# README.txt
#
@@ -241,9 +241,17 @@ assemble_one() {
cp "${TEMPLATES}/README.txt" "${staging}/README.txt"
chmod +x "${staging}/start.sh"
- echo "[package] ${plat}: bundling openXYOS sidecar" >&2
+ # Default v0.0.2 portable zip is the Python host only. The Node/openXYOS
+ # sidecar (~500MB) is opt-in: SHIP_OPENXYOS_RUNTIME=1 or SKIP_ORG_SIDECAR=0.
+ if [[ "${SHIP_OPENXYOS_RUNTIME:-0}" == "1" ]]; then
+ SKIP_ORG_SIDECAR=0
+ elif [[ -z "${SKIP_ORG_SIDECAR:-}" ]]; then
+ SKIP_ORG_SIDECAR=1
+ fi
+ export SKIP_ORG_SIDECAR
+ echo "[package] ${plat}: org sidecar (SKIP_ORG_SIDECAR=${SKIP_ORG_SIDECAR})" >&2
bash "${REPO_ROOT}/desktop/portable/bundle-org-sidecar.sh" "$plat" "$staging"
- if [[ "${SKIP_ORG_SIDECAR:-0}" != "1" ]]; then
+ if [[ "${SKIP_ORG_SIDECAR}" != "1" ]]; then
if [[ ! -f "${staging}/org-sidecar/openxyos/dist/index.html" ]]; then
echo "[package] ERROR: openXYOS frontend missing under ${staging}/org-sidecar/openxyos/dist" >&2
exit 1
diff --git a/desktop/portable/templates/README.txt b/desktop/portable/templates/README.txt
index 536eb4f7..7728ccee 100644
--- a/desktop/portable/templates/README.txt
+++ b/desktop/portable/templates/README.txt
@@ -1,9 +1,9 @@
FreeOS green portable package
=============================
-Extract this zip anywhere. It includes a portable CPython runtime, FreeOS
-(Octop-compatible host), and a bundled Node + openXYOS organization sidecar.
-No system Python or Node install is required.
+Extract this zip anywhere. It includes a portable CPython runtime and the
+FreeOS host (in-host Organization). No system Python is required. The
+Node/openXYOS sidecar is optional and is not in the default v0.0.2 zip.
Start
-----
@@ -15,14 +15,15 @@ Defaults: http://127.0.0.1:8088 data dir = ./data (FREEOS_HOME)
./start.sh --home /path/to/data --host 127.0.0.1 --port 8088
First launch follows the normal FreeOS setup wizard (create admin password).
-The organization module is enabled automatically; the sidecar listens on
-http://127.0.0.1:3780 and the dashboard /organization page embeds it.
+Organization uses the in-host /api/org-module surfaces. A Node process on
+3780 is not required. Set FREEOS_ORG_SIDECAR=1 only for advanced export/sync
+when a sidecar tree is present.
Layout
------
runtime/ portable CPython
packages/ FreeOS host + locked dependencies (site-packages)
- org-sidecar/ bundled Node + built openXYOS
+ org-sidecar/ optional Node + openXYOS (SHIP_OPENXYOS_RUNTIME=1 builds only)
launch.py entry bootstrap (loads packages/ + Windows pywin32 DLLs)
start.sh / start.bat
README.txt
diff --git a/desktop/portable/templates/start.bat b/desktop/portable/templates/start.bat
index 1c6f65e0..4a992c8a 100644
--- a/desktop/portable/templates/start.bat
+++ b/desktop/portable/templates/start.bat
@@ -69,8 +69,6 @@ exit /b 0
:run
if not defined FREEOS_ORG_ENABLE set "FREEOS_ORG_ENABLE=1"
-if not defined FREEOS_ORG_SIDECAR_URL set "FREEOS_ORG_SIDECAR_URL=http://127.0.0.1:3780"
-if not defined OPENXYOS_BASE_URL set "OPENXYOS_BASE_URL=%FREEOS_ORG_SIDECAR_URL%"
if not exist "%FREEOS_HOME%" mkdir "%FREEOS_HOME%"
set "PY=%ROOT%\runtime\python.exe"
@@ -90,10 +88,20 @@ set "PYTHONUTF8=1"
set "PYTHONIOENCODING=utf-8"
set "PYTHONPATH="
+if /I "%FREEOS_ORG_SIDECAR%"=="1" goto startSidecar
+if /I "%FREEOS_ORG_SIDECAR%"=="true" goto startSidecar
+if /I "%FREEOS_ORG_SIDECAR%"=="yes" goto startSidecar
+if /I "%FREEOS_ORG_SIDECAR%"=="on" goto startSidecar
+echo [freeos] organization is in-host (set FREEOS_ORG_SIDECAR=1 for the Node sidecar)
+goto afterSidecar
+:startSidecar
+if not defined FREEOS_ORG_SIDECAR_URL set "FREEOS_ORG_SIDECAR_URL=http://127.0.0.1:3780"
+if not defined OPENXYOS_BASE_URL set "OPENXYOS_BASE_URL=%FREEOS_ORG_SIDECAR_URL%"
if exist "%ROOT%\org-sidecar\start-sidecar.bat" (
- echo [freeos] organization sidecar → %FREEOS_ORG_SIDECAR_URL%
+ echo [freeos] optional organization sidecar → %FREEOS_ORG_SIDECAR_URL%
start "" /B "%ROOT%\org-sidecar\start-sidecar.bat"
)
+:afterSidecar
echo [freeos] home=%FREEOS_HOME%
echo [freeos] http://%HOST%:%PORT%
diff --git a/desktop/portable/templates/start.sh b/desktop/portable/templates/start.sh
index 9e5ebf13..494c3c53 100755
--- a/desktop/portable/templates/start.sh
+++ b/desktop/portable/templates/start.sh
@@ -58,8 +58,6 @@ done
export FREEOS_HOME OCTOP_HOME
export FREEOS_ORG_ENABLE="${FREEOS_ORG_ENABLE:-1}"
-export FREEOS_ORG_SIDECAR_URL="${FREEOS_ORG_SIDECAR_URL:-http://127.0.0.1:3780}"
-export OPENXYOS_BASE_URL="${OPENXYOS_BASE_URL:-${FREEOS_ORG_SIDECAR_URL}}"
mkdir -p "$FREEOS_HOME"
PY=""
@@ -78,10 +76,19 @@ export PYTHONUTF8=1
export PYTHONIOENCODING=utf-8
unset PYTHONPATH || true
-if [[ -x "${ROOT}/org-sidecar/start-sidecar.sh" ]]; then
- echo "[freeos] organization sidecar → ${FREEOS_ORG_SIDECAR_URL}"
- "${ROOT}/org-sidecar/start-sidecar.sh" &
-fi
+case "${FREEOS_ORG_SIDECAR:-}" in
+ 1|true|yes|on)
+ export FREEOS_ORG_SIDECAR_URL="${FREEOS_ORG_SIDECAR_URL:-http://127.0.0.1:3780}"
+ export OPENXYOS_BASE_URL="${OPENXYOS_BASE_URL:-${FREEOS_ORG_SIDECAR_URL}}"
+ if [[ -x "${ROOT}/org-sidecar/start-sidecar.sh" ]]; then
+ echo "[freeos] optional organization sidecar → ${FREEOS_ORG_SIDECAR_URL}"
+ "${ROOT}/org-sidecar/start-sidecar.sh" &
+ fi
+ ;;
+ *)
+ echo "[freeos] organization is in-host (set FREEOS_ORG_SIDECAR=1 for the Node sidecar)"
+ ;;
+esac
echo "[freeos] home=${FREEOS_HOME}"
echo "[freeos] http://${HOST}:${PORT}"
diff --git a/desktop/src/Taskfile.yml b/desktop/src/Taskfile.yml
index 0227d839..6931d4e1 100644
--- a/desktop/src/Taskfile.yml
+++ b/desktop/src/Taskfile.yml
@@ -18,7 +18,7 @@ tasks:
- task: "{{OS}}:build"
package:
- summary: Packages a production build
+ summary: Packages a production build (Windows NSIS is slim unless SHIP_OPENXYOS_RUNTIME=1)
cmds:
- task: "{{OS}}:package"
diff --git a/desktop/src/build/config.yml b/desktop/src/build/config.yml
index edabf6a7..21c2ea70 100644
--- a/desktop/src/build/config.yml
+++ b/desktop/src/build/config.yml
@@ -8,10 +8,10 @@ info:
companyName: "XYAI Studio"
productName: "FreeOS"
productIdentifier: "com.xyaistudio.freeos"
- description: "FreeOS desktop — Octop shell + openXYOS"
+ description: "FreeOS desktop — Python host with in-host organization"
copyright: "(c) 2026, XYAI Studio"
- comments: "Wails v3 + green portable + openXYOS sidecar"
- version: "0.0.1"
+ comments: "Wails v3 + green portable. Node sidecar optional via FREEOS_ORG_SIDECAR=1."
+ version: "0.0.2"
dev_mode:
root_path: .
diff --git a/desktop/src/build/windows/Taskfile.yml b/desktop/src/build/windows/Taskfile.yml
index bd6ff0bc..4aa63598 100644
--- a/desktop/src/build/windows/Taskfile.yml
+++ b/desktop/src/build/windows/Taskfile.yml
@@ -41,7 +41,7 @@ tasks:
PORTABLE_ZIP: '{{.PORTABLE_ZIP | default (printf "../portable/release/FreeOS-portable-windows-%s-%s.zip" (.ARCH | default ARCH) (.VERSION | default "dev"))}}'
stage:openxyos-runtime:
- summary: Extract org-sidecar from the portable zip for the NSIS payload
+ summary: Opt-in only — extract org-sidecar from the portable zip for NSIS
cmds:
- cmd: >-
python "{{.ROOT_DIR}}/build/stage_openxyos_runtime.py" "{{.PORTABLE_ZIP}}"
@@ -54,18 +54,32 @@ tasks:
vars:
PORTABLE_ZIP: '{{.PORTABLE_ZIP | default (printf "../portable/release/FreeOS-portable-windows-%s-%s.zip" (.ARCH | default ARCH) (.VERSION | default "dev"))}}'
+ maybe-stage:openxyos-runtime:
+ summary: Stage openxyos-runtime.zip only when SHIP_OPENXYOS_RUNTIME=1
+ status:
+ - test "{{.SHIP_OPENXYOS_RUNTIME}}" != "1"
+ cmds:
+ - task: stage:openxyos-runtime
+ vars:
+ ARCH: "{{.ARCH}}"
+ PORTABLE_ZIP: "{{.PORTABLE_ZIP}}"
+ VERSION: "{{.VERSION}}"
+ vars:
+ SHIP_OPENXYOS_RUNTIME: '{{.SHIP_OPENXYOS_RUNTIME | default "0"}}'
+
package:
- summary: Packages an NSIS installer with the portable runtime embedded
+ summary: Packages a slim NSIS installer (Python host). Pass SHIP_OPENXYOS_RUNTIME=1 to embed openxyos-runtime.zip.
cmds:
- task: stage:portable
vars:
ARCH: "{{.ARCH}}"
PORTABLE_ZIP: "{{.PORTABLE_ZIP}}"
- - task: stage:openxyos-runtime
+ - task: maybe-stage:openxyos-runtime
vars:
ARCH: "{{.ARCH}}"
PORTABLE_ZIP: "{{.PORTABLE_ZIP}}"
VERSION: "{{.VERSION}}"
+ SHIP_OPENXYOS_RUNTIME: "{{.SHIP_OPENXYOS_RUNTIME}}"
- task: build
vars:
PRODUCTION: "true"
@@ -75,6 +89,7 @@ tasks:
vars:
ARCH: "{{.ARCH}}"
VERSION: "{{.VERSION}}"
+ SHIP_OPENXYOS_RUNTIME: "{{.SHIP_OPENXYOS_RUNTIME}}"
- cmd: >-
powershell -NoProfile -Command
"Remove-Item -Force '{{.BIN_DIR}}/{{.APP_NAME}}-Desktop-windows-{{.ARCH}}.zip' -ErrorAction SilentlyContinue;
@@ -84,6 +99,7 @@ tasks:
vars:
ARCH: "{{.ARCH | default ARCH}}"
VERSION: '{{.VERSION | default "dev"}}'
+ SHIP_OPENXYOS_RUNTIME: '{{.SHIP_OPENXYOS_RUNTIME | default "0"}}'
PORTABLE_ZIP: '{{.PORTABLE_ZIP | default (printf "../portable/release/FreeOS-portable-windows-%s-%s.zip" .ARCH .VERSION)}}'
ARCHIVE: '{{.BIN_DIR}}/{{.APP_NAME}}-desktop-windows-{{.ARCH}}-{{.VERSION}}.exe'
@@ -108,7 +124,8 @@ tasks:
vars:
ARCH: '{{.ARCH | default ARCH}}'
VERSION: '{{.VERSION | default "dev"}}'
- VERSION_NSIS_DEFINE: '{{if and .VERSION (ne .VERSION "dev")}}-DINFO_PRODUCTVERSION="{{.VERSION}}" {{end}}'
+ VERSION_NSIS_DEFINE: '{{if and .VERSION (ne .VERSION "dev")}}-DINFO_PRODUCTVERSION="{{.VERSION}}" {{end}}{{if eq .SHIP_OPENXYOS_RUNTIME "1"}}-DOPENXYOS_RUNTIME_ZIP_PRESENT {{end}}'
+ SHIP_OPENXYOS_RUNTIME: '{{.SHIP_OPENXYOS_RUNTIME | default "0"}}'
ARG_FLAG: '{{if eq (.ARCH | default ARCH) "amd64"}}AMD64{{else}}ARM64{{end}}'
ARCHIVE: '{{.BIN_DIR}}/{{.APP_NAME}}-desktop-windows-{{.ARCH}}-{{.VERSION}}.exe'
diff --git a/desktop/src/build/windows/info.json b/desktop/src/build/windows/info.json
index f118ea00..71e63a03 100644
--- a/desktop/src/build/windows/info.json
+++ b/desktop/src/build/windows/info.json
@@ -1,15 +1,15 @@
{
"fixed": {
- "file_version": "0.0.1"
+ "file_version": "0.0.2"
},
"info": {
"0000": {
- "ProductVersion": "0.0.1",
+ "ProductVersion": "0.0.2",
"CompanyName": "XYAI Studio",
"FileDescription": "FreeOS desktop",
"LegalCopyright": "Copyright © 2026 XYAI Studio",
"ProductName": "FreeOS",
- "Comments": "FreeOS desktop — Octop shell + openXYOS"
+ "Comments": "FreeOS desktop — Python host with in-host organization"
}
}
}
diff --git a/desktop/src/build/windows/nsis/project.nsi b/desktop/src/build/windows/nsis/project.nsi
index 0238abda..ea00e263 100644
--- a/desktop/src/build/windows/nsis/project.nsi
+++ b/desktop/src/build/windows/nsis/project.nsi
@@ -52,10 +52,14 @@ ManifestDPIAware true
LangString FINISH_RUN ${LANG_SIMPCHINESE} "运行 FreeOS"
LangString FINISH_RUN ${LANG_ENGLISH} "Run FreeOS"
-LangString OPENXYOS_WORKDIR ${LANG_SIMPCHINESE} "创建 openXYOS 工作目录(写入 %LOCALAPPDATA%\\FreeOS\\openxyos)"
-LangString OPENXYOS_WORKDIR ${LANG_ENGLISH} "Create the openXYOS work directory (%LOCALAPPDATA%\\FreeOS\\openxyos)"
-LangString OPENXYOS_COPY_ZIP ${LANG_SIMPCHINESE} "复制 openXYOS 运行包(前端+后端)与预配子进程"
-LangString OPENXYOS_COPY_ZIP ${LANG_ENGLISH} "Copy the openXYOS runtime payload and provisioner subprocess"
+LangString OPENXYOS_WORKDIR ${LANG_SIMPCHINESE} "组织能力在 FreeOS 宿主内运行;openXYOS Node 运行包可选"
+LangString OPENXYOS_WORKDIR ${LANG_ENGLISH} "Organization runs in the FreeOS host; the openXYOS Node runtime is optional"
+LangString OPENXYOS_COPY_ZIP ${LANG_SIMPCHINESE} "复制可选的 openXYOS 运行包(不解压、不启动)"
+LangString OPENXYOS_COPY_ZIP ${LANG_ENGLISH} "Copy the optional openXYOS runtime payload (do not extract or start)"
+LangString OPENXYOS_OPTIONAL_SKIP ${LANG_SIMPCHINESE} "跳过 openXYOS 解压与 livez。安装继续;组织页使用宿主内能力。"
+LangString OPENXYOS_OPTIONAL_SKIP ${LANG_ENGLISH} "Skipping openXYOS extract and livez. Setup continues; Organization uses the in-host module."
+LangString OPENXYOS_OPTIONAL_ABSENT ${LANG_SIMPCHINESE} "本安装包未包含 openXYOS 运行包。安装继续;组织页使用宿主内能力。"
+LangString OPENXYOS_OPTIONAL_ABSENT ${LANG_ENGLISH} "This package has no openXYOS runtime zip. Setup continues; Organization uses the in-host module."
LangString OPENXYOS_PROVISION ${LANG_SIMPCHINESE} "正在以子进程部署 openXYOS(解压、启动前后端、等待健康检查)"
LangString OPENXYOS_PROVISION ${LANG_ENGLISH} "Running the openXYOS provisioner subprocess (extract, start FE/BE, wait for health)"
LangString OPENXYOS_PROVISION_DETAIL ${LANG_SIMPCHINESE} "完整日志:%LOCALAPPDATA%\\FreeOS\\openxyos\\provision.log(UTF-8);此处只显示步骤结果"
diff --git a/desktop/src/build/windows/nsis/wails_tools.nsh b/desktop/src/build/windows/nsis/wails_tools.nsh
index 9e3aba3f..14485bf9 100644
--- a/desktop/src/build/windows/nsis/wails_tools.nsh
+++ b/desktop/src/build/windows/nsis/wails_tools.nsh
@@ -17,7 +17,7 @@
!define INFO_PRODUCTNAME "FreeOS"
!endif
!ifndef INFO_PRODUCTVERSION
- !define INFO_PRODUCTVERSION "0.0.1"
+ !define INFO_PRODUCTVERSION "0.0.2"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "(c) 2026, XYAI Studio"
@@ -34,16 +34,13 @@
!define WAILS_INSTALL_SCOPE "machine"
!endif
-# Prebuilt FE+BE zip must exist when makensis runs (CI: stage:openxyos-runtime).
-# A README-only $INSTDIR\openxyos is not a shippable installer.
+# Optional openXYOS Node runtime. Organization runs in the FreeOS host.
+# OPENXYOS_RUNTIME_ZIP_PRESENT is OFF by default. A leftover zip on disk
+# must not silently inflate the installer — only opt-in makensis
+# (-DOPENXYOS_RUNTIME_ZIP_PRESENT from SHIP_OPENXYOS_RUNTIME=1) ships it.
!ifndef OPENXYOS_RUNTIME_ZIP
!define OPENXYOS_RUNTIME_ZIP "..\openxyos-runtime.zip"
!endif
-!if /FileExists "${OPENXYOS_RUNTIME_ZIP}"
- !define OPENXYOS_RUNTIME_ZIP_PRESENT
-!else
- !error "openxyos-runtime.zip missing at desktop/src/build/windows/openxyos-runtime.zip; run stage:openxyos-runtime before makensis"
-!endif
!ifndef REQUEST_EXECUTION_LEVEL
!if "${WAILS_INSTALL_SCOPE}" == "user"
@@ -145,76 +142,30 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
${EndIf}
!macroend
-# After the FreeOS shell files are copied, run the shipped openXYOS
-# provisioner as a child process (nsExec::Exec, not ExecToLog). The
-# parent waits for exit 0. Child stdout is discarded (cmd >nul) so
-# UTF-8 Node/PowerShell logs do not mojibake in the ANSI detail list.
-# Localized DetailPrint LangStrings are the only NSIS-visible progress.
-# The child extracts with tar.exe, starts FE/BE at medium IL, waits for
-# livez, and writes .install-ready only when healthy. Do not FileWrite
-# goto-label .cmd scripts here. Do not register logon autostart.
+# Organization is in-host. Default Setup copies no openxyos-runtime.zip.
+# Opt-in builds (-DOPENXYOS_RUNTIME_ZIP_PRESENT) copy the zip + scripts
+# but still never extract, start Node, wait for livez, or Abort.
+# Do not FileWrite goto-label .cmd scripts here. Do not register logon autostart.
!macro wails.provisionOpenXYOS
!insertmacro wails.userLocalAppData
SetDetailsPrint both
DetailPrint "$(OPENXYOS_WORKDIR)"
CreateDirectory "$INSTDIR\openxyos"
- CreateDirectory "$INSTDIR\openxyos-runtime"
- CreateDirectory "$R6\FreeOS\openxyos"
- DetailPrint "$(OPENXYOS_COPY_ZIP)"
- File "/oname=openxyos-runtime.zip" "${OPENXYOS_RUNTIME_ZIP}"
- File "provision-openxyos.ps1"
- File "provision-openxyos.cmd"
- File "start-sidecar.ps1"
- File "start-sidecar.cmd"
- DetailPrint "$(OPENXYOS_PROVISION)"
- DetailPrint "$(OPENXYOS_PROVISION_DETAIL)"
- StrCpy $R4 '"$INSTDIR\provision-openxyos.cmd" -ZipPath "$INSTDIR\openxyos-runtime.zip" -InstallDir "$INSTDIR" -LiveDir "$R6\FreeOS\openxyos"'
- nsExec::Exec $R4
- Pop $0
- DetailPrint "$(OPENXYOS_PROVISION_CODE)$0"
- ${If} $0 == 12
- DetailPrint "$(OPENXYOS_PROVISION_RETRY)"
- nsExec::Exec $R4
- Pop $0
- DetailPrint "$(OPENXYOS_PROVISION_CODE)$0"
- ${EndIf}
- ${If} $0 == 0
- IfFileExists "$R6\FreeOS\openxyos\.install-ready" 0 openxyosMissingMarker
- DetailPrint "$(OPENXYOS_PROVISION_OK)"
- Goto openxyosProvisionDone
- openxyosMissingMarker:
- StrCpy $0 13
- ${EndIf}
- !insertmacro wails.openxyosFailDetail
- DetailPrint $R9
- IfSilent openxyosProvisionSilent openxyosProvisionAsk
- openxyosProvisionSilent:
- SetErrorLevel $0
- Abort
- openxyosProvisionAsk:
- MessageBox MB_RETRYCANCEL|MB_ICONSTOP "$(OPENXYOS_PROVISION_FAIL)$\r$\n$\r$\n$R9$\r$\n$\r$\n$(OPENXYOS_FAIL_CODE)$0$\r$\n$(OPENXYOS_FAIL_LOG)" IDRETRY openxyosProvisionRetry
- SetErrorLevel $0
- Abort
- openxyosProvisionRetry:
- DetailPrint "$(OPENXYOS_PROVISION_RETRY)"
- nsExec::Exec $R4
- Pop $0
- DetailPrint "$(OPENXYOS_PROVISION_CODE)$0"
- ${If} $0 == 0
- IfFileExists "$R6\FreeOS\openxyos\.install-ready" openxyosProvisionDone 0
- StrCpy $0 13
- ${EndIf}
- !insertmacro wails.openxyosFailDetail
- DetailPrint $R9
- IfSilent openxyosProvisionSilent2 openxyosProvisionFinal
- openxyosProvisionSilent2:
- SetErrorLevel $0
- Abort
- openxyosProvisionFinal:
- MessageBox MB_OK|MB_ICONSTOP "$(OPENXYOS_PROVISION_FAIL)$\r$\n$\r$\n$R9$\r$\n$\r$\n$(OPENXYOS_FAIL_CODE)$0$\r$\n$(OPENXYOS_FAIL_LOG)"
- SetErrorLevel $0
- Abort
- openxyosProvisionDone:
+ !ifdef OPENXYOS_RUNTIME_ZIP_PRESENT
+ !if /FileExists "${OPENXYOS_RUNTIME_ZIP}"
+ DetailPrint "$(OPENXYOS_COPY_ZIP)"
+ File "/oname=openxyos-runtime.zip" "${OPENXYOS_RUNTIME_ZIP}"
+ File "provision-openxyos.ps1"
+ File "provision-openxyos.cmd"
+ File "start-sidecar.ps1"
+ File "start-sidecar.cmd"
+ DetailPrint "$(OPENXYOS_OPTIONAL_SKIP)"
+ !else
+ !error "OPENXYOS_RUNTIME_ZIP_PRESENT is set but ${OPENXYOS_RUNTIME_ZIP} is missing"
+ !endif
+ !else
+ DetailPrint "$(OPENXYOS_OPTIONAL_ABSENT)"
+ !endif
SetDetailsPrint listonly
!macroend
diff --git a/desktop/src/desktop_copy.go b/desktop/src/desktop_copy.go
index f8d87d58..be5020d0 100644
--- a/desktop/src/desktop_copy.go
+++ b/desktop/src/desktop_copy.go
@@ -35,8 +35,8 @@ var desktopCopy = map[Locale]map[string]string{
copyStatusConnecting: "Connecting to FreeOS…",
copyStatusCheckingRuntime: "Checking the runtime…",
copyStatusStartingService: "Starting the FreeOS service…",
- copyStatusStartingOrg: "Starting the organization console…",
- copyStatusProvisioningOrg: "Preparing the local openXYOS environment…",
+ copyStatusStartingOrg: "Starting FreeOS…",
+ copyStatusProvisioningOrg: "Preparing the optional openXYOS sidecar…",
copyStatusReady: "FreeOS is ready",
copyStatusUsingRuntime: "Using the existing runtime…",
copyStatusBackupDatabase: "Desktop update %s found. Backing up the database…",
@@ -62,8 +62,8 @@ var desktopCopy = map[Locale]map[string]string{
copyStatusConnecting: "正在连接 FreeOS…",
copyStatusCheckingRuntime: "正在检查运行环境…",
copyStatusStartingService: "正在启动 FreeOS 服务…",
- copyStatusStartingOrg: "正在启动组织控制台…",
- copyStatusProvisioningOrg: "正在准备本机 openXYOS 环境…",
+ copyStatusStartingOrg: "正在启动 FreeOS…",
+ copyStatusProvisioningOrg: "正在准备可选的 openXYOS 边车…",
copyStatusReady: "FreeOS 已就绪",
copyStatusUsingRuntime: "正在使用已有运行环境…",
copyStatusBackupDatabase: "发现客户端新版 %s,正在备份数据库…",
diff --git a/desktop/src/download.go b/desktop/src/download.go
index d024c4a1..da187798 100644
--- a/desktop/src/download.go
+++ b/desktop/src/download.go
@@ -125,7 +125,7 @@ func shouldReplacePortable(root string) bool {
return true
}
// Keep a newer in-app FreeOS portable over an older bundled zip.
- // Do not keep Octop 0.9 / 1.0 leftovers — FreeOS is 0.0.1 and must replace them.
+ // Do not keep Octop 0.9 / 1.0 leftovers — FreeOS is 0.0.2 and must replace them.
if currentVersion != "" && bundledVersion != "" &&
compareVersions(bundledVersion, currentVersion) < 0 &&
!isOctopLineageRuntime(root) {
diff --git a/desktop/src/main.go b/desktop/src/main.go
index 6472c70e..100cf7d1 100644
--- a/desktop/src/main.go
+++ b/desktop/src/main.go
@@ -191,8 +191,12 @@ func (a *App) boot() {
}
root := portableDir()
openxyosReady := sidecarBundleReady(openxyosUserWorkDir())
- if _, perr := provisionOpenXYOS(root, locale, a.setStatus); perr != nil {
- log.Printf("openXYOS provision: %v", perr)
+ if orgSidecarWanted() {
+ if _, perr := provisionOpenXYOS(root, locale, a.setStatus); perr != nil {
+ log.Printf("openXYOS provision: %v", perr)
+ }
+ } else {
+ log.Printf("in-host organization; Node sidecar skipped (set FREEOS_ORG_SIDECAR=1 to enable)")
}
port := chooseHostPort(s.Port)
if port != s.Port {
@@ -202,12 +206,14 @@ func (a *App) boot() {
stopOctop(a.cmd)
stopOctop(a.sidecar)
a.setStatus(desktopText(locale, copyStatusStartingOrg))
- sidecar, serr := startOrgSidecar(root, port)
- a.sidecar = sidecar
- if serr != nil {
- logStartupError("organization sidecar", serr)
- } else if sidecar == nil {
- log.Printf("organization sidecar not bundled under %s", orgSidecarDir(root))
+ if orgSidecarWanted() {
+ sidecar, serr := startOrgSidecar(root, port)
+ a.sidecar = sidecar
+ if serr != nil {
+ logStartupError("organization sidecar", serr)
+ } else if sidecar == nil {
+ log.Printf("organization sidecar not bundled under %s", orgSidecarDir(root))
+ }
}
cmd, err := startOctop(root, port)
a.cmd = cmd
@@ -230,7 +236,7 @@ func (a *App) boot() {
showFatalError("FreeOS", formatFatalStartup(locale, err))
return
}
- if sidecarReady(root) {
+ if orgSidecarWanted() && sidecarReady(root) {
wait := 30 * time.Second
if firstLaunch && !openxyosReady {
wait = 60 * time.Second
diff --git a/desktop/src/process.go b/desktop/src/process.go
index 5a55f14c..12e31e58 100644
--- a/desktop/src/process.go
+++ b/desktop/src/process.go
@@ -22,29 +22,32 @@ const defaultSidecarPort = 3780
func hostLaunchEnv(root string, port int) map[string]string {
home := productHome()
env := map[string]string{
- "FREEOS_HOME": home,
- "OCTOP_HOME": home,
- "OCTOP_GREEN_PACKAGES": filepath.Join(root, "packages"),
- "PYTHONNOUSERSITE": "1",
- "PYTHONUTF8": "1",
- "PYTHONIOENCODING": "utf-8",
- "PYTHONPATH": "",
- "FREEOS_ORG_ENABLE": "1",
- "FREEOS_ORG_SIDECAR_URL": sidecarURL(),
- "FREEOS_ORG_SIDECAR_PORT": strconv.Itoa(defaultSidecarPort),
- "OPENXYOS_BASE_URL": sidecarURL(),
- "OCTOP_PORT": strconv.Itoa(port),
- "OCTOP_DESKTOP": "1",
- "FREEOS_DESKTOP": "1",
+ "FREEOS_HOME": home,
+ "OCTOP_HOME": home,
+ "OCTOP_GREEN_PACKAGES": filepath.Join(root, "packages"),
+ "PYTHONNOUSERSITE": "1",
+ "PYTHONUTF8": "1",
+ "PYTHONIOENCODING": "utf-8",
+ "PYTHONPATH": "",
+ "FREEOS_ORG_ENABLE": "1",
+ "OCTOP_PORT": strconv.Itoa(port),
+ "OCTOP_DESKTOP": "1",
+ "FREEOS_DESKTOP": "1",
}
- if bundle := resolveSidecarDir(root); bundle != "" {
- env["FREEOS_OPENXYOS_HOME"] = bundle
- }
- if install := openxyosInstallDir(); install != "" {
- env["FREEOS_OPENXYOS_INSTALL"] = install
- }
- if _, _, ingest, err := loadOrCreateSidecarSecrets(home); err == nil && ingest != "" {
- env["FREEOS_INGEST_TOKEN"] = ingest
+ if orgSidecarWanted() {
+ env["FREEOS_ORG_SIDECAR"] = "1"
+ env["FREEOS_ORG_SIDECAR_URL"] = sidecarURL()
+ env["FREEOS_ORG_SIDECAR_PORT"] = strconv.Itoa(defaultSidecarPort)
+ env["OPENXYOS_BASE_URL"] = sidecarURL()
+ if bundle := resolveSidecarDir(root); bundle != "" {
+ env["FREEOS_OPENXYOS_HOME"] = bundle
+ }
+ if install := openxyosInstallDir(); install != "" {
+ env["FREEOS_OPENXYOS_INSTALL"] = install
+ }
+ if _, _, ingest, err := loadOrCreateSidecarSecrets(home); err == nil && ingest != "" {
+ env["FREEOS_INGEST_TOKEN"] = ingest
+ }
}
return env
}
diff --git a/desktop/src/process_sidecar.go b/desktop/src/process_sidecar.go
index ae72c314..4c3277de 100644
--- a/desktop/src/process_sidecar.go
+++ b/desktop/src/process_sidecar.go
@@ -17,6 +17,15 @@ import (
"time"
)
+func orgSidecarWanted() bool {
+ switch strings.ToLower(strings.TrimSpace(os.Getenv("FREEOS_ORG_SIDECAR"))) {
+ case "1", "true", "yes", "on":
+ return true
+ default:
+ return false
+ }
+}
+
func orgSidecarDir(root string) string {
return filepath.Join(root, "org-sidecar")
}
@@ -413,6 +422,9 @@ func startViaWindowsHelper(bundle string) (*exec.Cmd, error) {
}
func startOrgSidecar(root string, dashboardPort int) (*exec.Cmd, error) {
+ if !orgSidecarWanted() {
+ return nil, nil
+ }
bundle := resolveSidecarDir(root)
if bundle == "" {
work := openxyosUserWorkDir()
diff --git a/desktop/src/process_sidecar_test.go b/desktop/src/process_sidecar_test.go
index bfe463a6..e8041410 100644
--- a/desktop/src/process_sidecar_test.go
+++ b/desktop/src/process_sidecar_test.go
@@ -304,6 +304,19 @@ func TestSidecarLiveReadsHealthURL(t *testing.T) {
}
}
+func TestStartOrgSidecarSkipsWhenNotOptedIn(t *testing.T) {
+ root := t.TempDir()
+ writeSidecarBundle(t, filepath.Join(root, "org-sidecar"))
+ t.Setenv("FREEOS_ORG_SIDECAR", "")
+ cmd, err := startOrgSidecar(root, 8088)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cmd != nil {
+ t.Fatal("Node sidecar must stay off unless FREEOS_ORG_SIDECAR is set")
+ }
+}
+
func TestStartOrgSidecarSkipsWhenNoBundle(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
diff --git a/desktop/src/settings_home_test.go b/desktop/src/settings_home_test.go
index 1b6c57ad..8ebc8093 100644
--- a/desktop/src/settings_home_test.go
+++ b/desktop/src/settings_home_test.go
@@ -48,8 +48,11 @@ func TestHostLaunchEnvSetsFreeosHomeAndOrgEnable(t *testing.T) {
if env["FREEOS_ORG_ENABLE"] != "1" {
t.Fatalf("org enable: %+v", env)
}
- if env["FREEOS_ORG_SIDECAR_URL"] != "http://127.0.0.1:3780" {
- t.Fatalf("sidecar url: %+v", env)
+ if _, ok := env["FREEOS_ORG_SIDECAR_URL"]; ok {
+ t.Fatalf("sidecar url must stay unset by default: %+v", env)
+ }
+ if _, ok := env["OPENXYOS_BASE_URL"]; ok {
+ t.Fatalf("OPENXYOS_BASE_URL must stay unset by default: %+v", env)
}
if env["OCTOP_DESKTOP"] != "1" || env["FREEOS_DESKTOP"] != "1" {
t.Fatalf("desktop flags: %+v", env)
@@ -59,6 +62,19 @@ func TestHostLaunchEnvSetsFreeosHomeAndOrgEnable(t *testing.T) {
}
}
+func TestHostLaunchEnvSidecarOptIn(t *testing.T) {
+ home := t.TempDir()
+ t.Setenv("FREEOS_HOME", home)
+ t.Setenv("FREEOS_ORG_SIDECAR", "1")
+ env := hostLaunchEnv(filepath.Join(home, "portable"), 8088)
+ if env["FREEOS_ORG_SIDECAR_URL"] != "http://127.0.0.1:3780" {
+ t.Fatalf("sidecar url: %+v", env)
+ }
+ if env["OPENXYOS_BASE_URL"] != "http://127.0.0.1:3780" {
+ t.Fatalf("openxyos url: %+v", env)
+ }
+}
+
func TestWithDesktopQueryMarksSpa(t *testing.T) {
if got := withDesktopQuery("http://127.0.0.1:8088/"); got != "http://127.0.0.1:8088/?desktop=1" {
t.Fatalf("got %q", got)
diff --git a/docs/architecture-integration.md b/docs/architecture-integration.md
index 57f51c27..3943bded 100644
--- a/docs/architecture-integration.md
+++ b/docs/architecture-integration.md
@@ -46,36 +46,40 @@ See [NOTICE](../NOTICE) and `modules/openxyos/TRADEMARKS.md`.
## Running topology
+Desktop keeps **one** resident runtime: the FreeOS/Octop Python host.
+Organization control-plane capabilities (`org_os`, `/api/org-module/*`,
+the Organization page) live in that host. The full openXYOS Node stack on
+`127.0.0.1:3780` is **optional** (export / sync / advanced deploy). It is
+not required to install FreeOS, open Organization, or run
+`freeos org loop run`. Default Windows NSIS and portable zips do **not**
+embed `openxyos-runtime.zip` (the ~500MB Node payload). Opt-in builds set
+`SHIP_OPENXYOS_RUNTIME=1`.
+
The host inserts governance in front of high-risk data-plane tools,
-generates module skills that call sidecar `/api/*` with tenant headers,
-compiles blueprints into chat-usable colleagues, and applies asset packs
-onto openXYOS-shaped surfaces.
+generates module skills, compiles blueprints into chat-usable colleagues,
+and applies asset packs onto openXYOS-shaped **in-host** surfaces
+(`{FREEOS_HOME}/openxyos-mirror/`, lifecycle registry, org-skills).
```
┌─────────────────────────────────────────────┐
Browser / IM │ FreeOS dashboard + Octop IM channels │
- │ /organization · /approve · /pending │
+ │ /organization (native) · /experts · /approve│
└───────────────┬─────────────────────────────┘
│ JWT / IM session
┌───────────────▼─────────────────────────────┐
Host │ FastAPI src/octop/api/app.py │
- (data plane) │ /api/* Octop routers │
- │ /api/org-module/status|catalog|PATCH │
- │ /api/org-module/governance/* │
- │ /api/org-module/skills/* │
- │ /api/org-module/sidecar/* (BFF proxy) │
- │ PluginManager + bundled org-os │
- │ xyos-governance-mcp (stdio MCP) │
+ (single runtime)│ /api/* Octop routers │
+ │ /api/org-module/status|catalog|overview │
+ │ /api/org-module/assemble|pack|loop|produce │
+ │ /api/org-module/employees|assets|governance │
+ │ in-host org storage + xyos-governance-mcp │
└───────┬───────────────────┬─────────────────┘
- │ HTTP │ durable pause
- │ X-FreeOS-User* │ ~/.freeos/governance/
- │ X-FreeOS-Tenant-Id│
+ │ optional HTTP │ durable pause
+ │ if opted in │ ~/.freeos/governance/
┌───────▼─────────┐ ┌─────▼─────────────────┐
- Sidecar │ openXYOS :3780 │ │ Pause + audit JSONL │
- (control plane)│ /api/governance │ │ Human must approve │
- │ /api/module-… │ │ before execute=true │
- │ own SQL.js/PG │ └───────────────────────┘
- └─────────────────┘
+ Optional │ openXYOS :3780 │ │ Pause + audit JSONL │
+ Node sidecar │ export / sync │ │ Human must approve │
+ └─────────────────┘ └───────────────────────┘
```
**Why this topology for 2 + 3**
diff --git a/pyproject.toml b/pyproject.toml
index 84c2168c..2adc247f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "octop"
-version = "0.0.1"
+version = "0.0.2"
description = "FreeOS — self-hosted multi-user multi-agent assistant with an optional organization OS module (Octop-compatible package name)"
readme = "README.md"
license = { text = "MIT" }
diff --git a/scripts/release_download_links.py b/scripts/release_download_links.py
index 4db77cfb..00378222 100644
--- a/scripts/release_download_links.py
+++ b/scripts/release_download_links.py
@@ -3,8 +3,8 @@
Same pattern as clash-verge-rev: interpolate TAG / VERSION into a fixed
filename table. Do not scrape the assets list.
- python3 scripts/release_download_links.py 0.0.1
- python3 scripts/release_download_links.py v0.0.1
+ python3 scripts/release_download_links.py 0.0.2
+ python3 scripts/release_download_links.py v0.0.2
"""
from __future__ import annotations
diff --git a/src/octop/__init__.py b/src/octop/__init__.py
index 7f31da6c..c9aa8c57 100644
--- a/src/octop/__init__.py
+++ b/src/octop/__init__.py
@@ -2,4 +2,4 @@
from __future__ import annotations
-__version__ = "0.0.1"
+__version__ = "0.0.2"
diff --git a/src/octop/api/routers/org_module.py b/src/octop/api/routers/org_module.py
index faf2f322..2cb37772 100644
--- a/src/octop/api/routers/org_module.py
+++ b/src/octop/api/routers/org_module.py
@@ -615,7 +615,7 @@ async def run_loop(
tenant_id=body.tenant_id or service.tenant_id() or "1",
blueprint_path=Path(body.blueprint_path) if body.blueprint_path else None,
policies_path=Path(body.policies_path) if body.policies_path else None,
- sidecar_url=body.base_url or service.sidecar_url(),
+ sidecar_url=body.base_url or service.explicit_sidecar_url(),
config_path=service.config_path,
owner_user_id=int(getattr(user, "id", 0) or 0) or None,
)
diff --git a/src/octop/infra/server.py b/src/octop/infra/server.py
index fd1d13bd..fe4d1210 100644
--- a/src/octop/infra/server.py
+++ b/src/octop/infra/server.py
@@ -582,33 +582,25 @@ def _apply_desktop_org_defaults(self) -> None:
logger.info("organization module enabled (FREEOS_ORG_ENABLE)")
def _ensure_desktop_org_sidecar(self) -> None:
- """Start local openXYOS FE+BE with the FreeOS host, not only on Organization."""
+ """Optionally start the Node sidecar. Organization itself is in-host."""
+ from octop.modules.org_os.service import org_module_from_paths, org_sidecar_wanted
+
desktop = (
(os.environ.get("OCTOP_DESKTOP") or os.environ.get("FREEOS_DESKTOP") or "")
.strip()
.lower()
)
org_flag = (os.environ.get("FREEOS_ORG_ENABLE") or "").strip().lower()
- from octop.modules.org_os.sidecar_launch import (
- ensure_sidecar,
- find_sidecar_runtime,
- sidecar_install_ready,
- )
-
desktop_on = desktop in {"1", "true", "yes", "on"}
org_on = org_flag in {"1", "true", "yes", "on"}
- if (
- not desktop_on
- and not org_on
- and not sidecar_install_ready()
- and find_sidecar_runtime() is None
- ):
- return
- from octop.modules.org_os.service import org_module_from_paths
-
service = org_module_from_paths(self.paths)
if desktop_on or org_on:
service.enable_from_desktop_env()
+ if not org_sidecar_wanted():
+ logger.info("organization sidecar skipped (in-host org; set FREEOS_ORG_SIDECAR=1)")
+ return
+ from octop.modules.org_os.sidecar_launch import ensure_sidecar
+
result = ensure_sidecar(service)
logger.info(
"organization sidecar ensure started=%s already=%s reachable=%s detail=%s",
diff --git a/src/octop/modules/org_os/empower.py b/src/octop/modules/org_os/empower.py
index 0115e356..8a60725d 100644
--- a/src/octop/modules/org_os/empower.py
+++ b/src/octop/modules/org_os/empower.py
@@ -76,7 +76,7 @@ def assemble_from_blueprint(
notes.extend(inbound.notes)
notes.append("imported openXYOS catalog/policies from the live sidecar")
else:
- notes.append("sidecar offline; compiling bundled openXYOS blueprint fixtures")
+ notes.append("using in-host org storage and bundled openXYOS blueprint fixtures")
store = LifecycleStore(service.home, tid)
paths = [blueprint_fixture(), *extra_blueprint_fixtures()]
@@ -85,7 +85,7 @@ def assemble_from_blueprint(
path,
home=service.home,
tenant_id=tid,
- sidecar_url=service.sidecar_url(),
+ sidecar_url=service.explicit_sidecar_url(),
)
register_compiled(
store,
@@ -196,7 +196,7 @@ def produce_from_corpus(
},
home=service.home,
tenant_id=tid,
- sidecar_url=service.sidecar_url(),
+ sidecar_url=service.explicit_sidecar_url(),
)
store = LifecycleStore(service.home, tid)
register_compiled(
diff --git a/src/octop/modules/org_os/loop/run.py b/src/octop/modules/org_os/loop/run.py
index 41102ef8..e511ca4c 100644
--- a/src/octop/modules/org_os/loop/run.py
+++ b/src/octop/modules/org_os/loop/run.py
@@ -3,13 +3,12 @@
from __future__ import annotations
import json
-import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from octop.modules.org_os.apply.apply import apply_asset_pack, import_applied_surfaces
-from octop.modules.org_os.apply.client import OpenXyosControlClient, resolve_control_plane_url
+from octop.modules.org_os.apply.client import resolve_control_plane_url
from octop.modules.org_os.assets.importer import import_openxyos_assets
from octop.modules.org_os.assets.pack import publish_asset_pack
from octop.modules.org_os.governance.interceptor import (
@@ -69,17 +68,6 @@ def to_dict(self) -> dict[str, Any]:
}
-def _wait_control_plane(url: str, *, home: Path, attempts: int = 12) -> bool:
- if not url:
- return False
- client = OpenXyosControlClient(url, home=home, timeout=2.0, retries=1)
- for _ in range(max(attempts, 1)):
- if client.get_json("/api/health/livez") is not None or client.bridge_ready():
- return True
- time.sleep(0.5)
- return False
-
-
def _promote(store: LifecycleStore, slug: str) -> tuple[dict[str, str], list[str]]:
"""Walk *slug* toward ``active`` along allowed edges only.
@@ -137,9 +125,10 @@ def run_growth_loop(
) -> LoopProof:
"""Run the finished self-growth loop against *home*.
- Uses bundled fixtures when live openXYOS is down. When
- ``OPENXYOS_BASE_URL`` (or *sidecar_url*) is set, outbound packs are
- POSTed to the real control plane as well as the durable local mirror.
+ Uses bundled fixtures and the in-host org registry. When
+ ``OPENXYOS_BASE_URL`` (or an explicit *sidecar_url*) is set, outbound
+ packs are also POSTed to that optional control plane. The loop never
+ starts or waits for a local Node sidecar.
"""
home = Path(home)
home.mkdir(parents=True, exist_ok=True)
@@ -153,19 +142,6 @@ def run_growth_loop(
if not blueprint.is_file():
raise FileNotFoundError(f"blueprint fixture missing: {blueprint}")
control_url = resolve_control_plane_url(sidecar_url)
- if control_url:
- _wait_control_plane(control_url, home=home, attempts=8)
- else:
- health = service.probe_sidecar(timeout=0.4)
- if health.reachable:
- control_url = health.url
- else:
- from octop.modules.org_os.sidecar_launch import ensure_sidecar, sidecar_can_start
-
- if sidecar_can_start():
- started = ensure_sidecar(service, wait=12.0)
- if started.reachable:
- control_url = started.url or service.sidecar_url()
imported = import_openxyos_assets(
home,
diff --git a/src/octop/modules/org_os/overview.py b/src/octop/modules/org_os/overview.py
index c98ba600..378d6a19 100644
--- a/src/octop/modules/org_os/overview.py
+++ b/src/octop/modules/org_os/overview.py
@@ -8,6 +8,7 @@
from pathlib import Path
from typing import Any
+from octop.modules.org_os.apply.client import mirror_root
from octop.modules.org_os.catalog import OPENXYOS_MODULES, catalog_keys
from octop.modules.org_os.lifecycle.store import LifecycleStore
from octop.modules.org_os.runtime.spawn import list_spawned_agents
@@ -27,6 +28,54 @@ def _count_skill_dirs(home: Path) -> int:
return sum(1 for child in root.iterdir() if child.is_dir() and (child / "SKILL.md").is_file())
+def _list_len(doc: Any, key: str) -> int:
+ if isinstance(doc, dict) and isinstance(doc.get(key), list):
+ return len(doc[key])
+ if isinstance(doc, list):
+ return len(doc)
+ return 0
+
+
+def _mirror_surfaces(home: Path, tenant_id: str) -> dict[str, int]:
+ root = mirror_root(home, tenant_id)
+ counts = {"employees": 0, "talent": 0, "skills": 0, "plugins": 0}
+ if not root.is_dir():
+ return counts
+ mapping = {
+ "employees": ("employees.json", "employees"),
+ "talent": ("talent.json", "talent"),
+ "skills": ("skills.json", "skills"),
+ "plugins": ("plugins.json", "plugins"),
+ }
+ for surface, (name, key) in mapping.items():
+ path = root / name
+ if not path.is_file():
+ continue
+ try:
+ raw = json.loads(path.read_text(encoding="utf-8"))
+ except json.JSONDecodeError:
+ continue
+ counts[surface] = _list_len(raw, key)
+ return counts
+
+
+def _colleague_rows(home: Path, tenant_id: str) -> list[dict[str, Any]]:
+ spawned = {row.slug: row for row in list_spawned_agents(home)}
+ rows: list[dict[str, Any]] = []
+ for rec in LifecycleStore(home, tenant_id).list():
+ agent = spawned.get(rec.slug)
+ rows.append(
+ {
+ "slug": rec.slug,
+ "name": rec.name or rec.slug,
+ "lifecycle": rec.lifecycle,
+ "agent_id": rec.agent_id or (agent.agent_id if agent else ""),
+ "spawned": agent is not None or bool(rec.agent_id),
+ }
+ )
+ return rows
+
+
def _read_loop_proof(home: Path) -> dict[str, Any] | None:
path = home / "asset-packs" / "loop-proof.json"
if not path.is_file():
@@ -56,10 +105,17 @@ class DualLoopOverview:
openxyos: dict[str, Any]
last_loop: dict[str, Any] | None = None
notes: list[str] = field(default_factory=list)
+ runtime: str = "in_host"
+ sidecar_optional: bool = True
+ colleagues: list[dict[str, Any]] = field(default_factory=list)
+ experts: list[dict[str, Any]] = field(default_factory=list)
+ org_surfaces: dict[str, int] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"enabled": self.enabled,
+ "runtime": self.runtime,
+ "sidecar_optional": self.sidecar_optional,
"sidecar_reachable": self.sidecar_reachable,
"sidecar_embed_ok": self.sidecar_embed_ok,
"sidecar_url": self.sidecar_url,
@@ -70,6 +126,9 @@ def to_dict(self) -> dict[str, Any]:
"last_sync": self.last_sync,
"freeos": dict(self.freeos),
"openxyos": dict(self.openxyos),
+ "colleagues": list(self.colleagues),
+ "experts": list(self.experts),
+ "org_surfaces": dict(self.org_surfaces),
"last_loop": self.last_loop,
"notes": list(self.notes),
"catalog": list(OPENXYOS_MODULES),
@@ -90,6 +149,8 @@ def build_overview(
tid = service.tenant_id() or "default"
colleagues = LifecycleStore(service.home, tid).list()
spawned = list_spawned_agents(service.home)
+ colleague_rows = _colleague_rows(service.home, tid)
+ org_surfaces = _mirror_surfaces(service.home, tid)
org_skills = _count_skill_dirs(service.home)
proof = _read_loop_proof(service.home)
last_sync = None
@@ -122,7 +183,8 @@ def build_overview(
"approvals": 0,
}
notes = [
- "FreeOS does the work. openXYOS owns organization and governance.",
+ "Organization runs in the FreeOS Python host. The Node sidecar is optional.",
+ "FreeOS does the work. Organization capabilities (catalog, employees, growth loop) live in-host.",
"Data plane: colleagues / experts / skills / MCP / tasks. Control plane: department employees / blueprints / modules / governance.",
]
embed_ok = False
@@ -131,15 +193,14 @@ def build_overview(
embed_ok = service.probe_sidecar_embed()
except Exception:
embed_ok = True
- if not status.sidecar.reachable:
- if install_ready:
+ if not embed_ok:
notes.append(
- "openXYOS sidecar is offline — reconnecting the install-time local console."
+ "Optional openXYOS console is up but the embed origin is blocked (advanced preview)."
)
- else:
- notes.append("openXYOS sidecar is offline — start it to sync blueprints and approvals.")
- elif not embed_ok:
- notes.append("openXYOS livez is up but the embed origin is blocked (blank preview).")
+ elif service.explicit_sidecar_url():
+ notes.append(
+ "Optional openXYOS sidecar is offline — in-host org storage and the growth loop still work."
+ )
return DualLoopOverview(
enabled=status.enabled,
sidecar_reachable=status.sidecar.reachable,
@@ -154,4 +215,14 @@ def build_overview(
openxyos=openxyos,
last_loop=proof,
notes=notes,
+ colleagues=colleague_rows,
+ experts=[
+ {
+ "agent_id": row.agent_id,
+ "slug": row.slug,
+ "name": row.name,
+ }
+ for row in spawned
+ ],
+ org_surfaces=org_surfaces,
)
diff --git a/src/octop/modules/org_os/service.py b/src/octop/modules/org_os/service.py
index ef4dd719..1f666987 100644
--- a/src/octop/modules/org_os/service.py
+++ b/src/octop/modules/org_os/service.py
@@ -19,6 +19,13 @@
ORG_PLUGIN_ID = "org-os"
DEFAULT_SIDECAR_URL = "http://127.0.0.1:3780"
_CONFIG_SECTION = "org_os"
+_SIDECAR_OPT_IN = {"1", "true", "yes", "on"}
+
+
+def org_sidecar_wanted() -> bool:
+ """True only when the operator explicitly opts into the Node sidecar."""
+ flag = os.environ.get("FREEOS_ORG_SIDECAR", "").strip().lower()
+ return flag in _SIDECAR_OPT_IN
@dataclass(frozen=True)
@@ -100,14 +107,27 @@ def is_enabled(self) -> bool:
return False
def sidecar_url(self) -> str:
- env = os.environ.get("FREEOS_ORG_SIDECAR_URL", "").strip()
- if env:
- return normalize_sidecar_url(env)
+ explicit = self.explicit_sidecar_url()
+ return explicit or DEFAULT_SIDECAR_URL
+
+ def explicit_sidecar_url(self) -> str:
+ """Return a configured sidecar origin, or empty when the Node stack is optional.
+
+ The implicit ``http://127.0.0.1:3780`` default is display-only. Growth
+ loop / apply must not treat that fallback as a required control plane.
+ """
+ for key in ("OPENXYOS_BASE_URL", "FREEOS_ORG_SIDECAR_URL"):
+ env = os.environ.get(key, "").strip()
+ if env:
+ return normalize_sidecar_url(env)
section = self._section()
raw = section.get("sidecar_url")
if isinstance(raw, str) and raw.strip():
- return normalize_sidecar_url(raw)
- return DEFAULT_SIDECAR_URL
+ cleaned = normalize_sidecar_url(raw)
+ if cleaned == DEFAULT_SIDECAR_URL and not org_sidecar_wanted():
+ return ""
+ return cleaned
+ return ""
def tenant_id(self) -> str:
env = os.environ.get("FREEOS_ORG_TENANT_ID", "").strip()
@@ -184,7 +204,13 @@ def set_enabled(self, enabled: bool, *, sidecar_url: str | None = None) -> None:
_write_json(self.config_path, data)
def probe_sidecar(self, timeout: float = 2.0) -> SidecarHealth:
- url = self.sidecar_url()
+ url = self.explicit_sidecar_url()
+ if not url:
+ return SidecarHealth(
+ reachable=False,
+ url=DEFAULT_SIDECAR_URL,
+ detail="sidecar optional; not configured",
+ )
livez = f"{url}/api/health/livez"
try:
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
@@ -218,7 +244,9 @@ def probe_sidecar_embed(self, timeout: float = 2.0) -> bool:
CORS whitelist answers those with HTTP 500 while livez (no Origin)
still looks healthy — the Organization iframe stays white.
"""
- url = self.sidecar_url()
+ url = self.explicit_sidecar_url()
+ if not url:
+ return False
origin = url.rstrip("/")
try:
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
@@ -234,19 +262,20 @@ def status(self) -> OrgModuleStatus:
sidecar = self.probe_sidecar()
enabled = self.is_enabled()
notes = [
+ "Organization capabilities run in the FreeOS Python host.",
"Host identity stays in FreeOS (JWT users under the platform home).",
- "openXYOS is the control plane; FreeOS is the data plane. "
+ "The optional openXYOS Node stack is for export/sync/advanced deploy, "
+ "not required for Organization or the growth loop.",
"Do not replace the FreeOS agent runtime with openXYOS chat.",
- "Proxy forwards X-FreeOS-User* and X-FreeOS-Tenant-Id; sign in to "
- "the sidecar separately for mutating org routes.",
"Plugin id org-os is seeded disabled; enable it here or via "
"Admin → Plugins / `freeos org enable`.",
"Run the finished loop with `freeos org loop run`.",
]
- if not sidecar.reachable and enabled:
+ if sidecar.reachable:
+ notes.append(f"Optional Node sidecar is reachable at {sidecar.url}.")
+ elif self.explicit_sidecar_url():
notes.append(
- f"Sidecar is off. Start it with: bash scripts/run-org-sidecar.sh "
- f"(listens on {sidecar.url})."
+ f"Optional sidecar at {sidecar.url} is offline; in-host org storage is used."
)
return OrgModuleStatus(
enabled=enabled,
diff --git a/tests/unit/cli/test_version_cmd.py b/tests/unit/cli/test_version_cmd.py
index 733a7758..d5f96173 100644
--- a/tests/unit/cli/test_version_cmd.py
+++ b/tests/unit/cli/test_version_cmd.py
@@ -13,11 +13,11 @@
_REPO_ROOT = Path(__file__).resolve().parents[3]
-def test_product_version_is_001() -> None:
+def test_product_version_is_002() -> None:
with (_REPO_ROOT / "pyproject.toml").open("rb") as fh:
declared = tomllib.load(fh)["project"]["version"]
- assert declared == "0.0.1"
- assert __version__ == "0.0.1"
+ assert declared == "0.0.2"
+ assert __version__ == "0.0.2"
def test_version_prints_orca_version() -> None:
@@ -25,7 +25,7 @@ def test_version_prints_orca_version() -> None:
result = runner.invoke(cli, ["version"])
assert result.exit_code == 0
assert "octop" in result.output.lower()
- assert "0.0.1" in result.output
+ assert "0.0.2" in result.output
def test_version_in_help() -> None:
@@ -39,4 +39,4 @@ def test_root_version_flag() -> None:
result = runner.invoke(cli, ["-v"])
assert result.exit_code == 0
assert "octop" in result.output.lower()
- assert "0.0.1" in result.output
+ assert "0.0.2" in result.output
diff --git a/tests/unit/desktop/test_nsis_detail_encoding.py b/tests/unit/desktop/test_nsis_detail_encoding.py
index 3f866763..76242c7b 100644
--- a/tests/unit/desktop/test_nsis_detail_encoding.py
+++ b/tests/unit/desktop/test_nsis_detail_encoding.py
@@ -102,11 +102,11 @@ def test_nsis_provision_does_not_exec_to_log() -> None:
provision = nsh[
nsh.index("!macro wails.provisionOpenXYOS") : nsh.index("!macro wails.openxyosFailDetail")
]
- assert "nsExec::Exec $R4" in provision
+ assert "nsExec::Exec $R4" not in provision
assert "ExecToLog" not in provision
- assert provision.count("nsExec::Exec $R4") == 3
+ assert "Abort" not in provision
cmd = PROVISION_CMD.read_text(encoding="utf-8")
assert ">nul" in cmd
nsi = NSI.read_text(encoding="utf-8-sig")
- assert "OPENXYOS_PROVISION_DETAIL" in nsi
- assert "UTF-8" in nsi
+ assert "OPENXYOS_OPTIONAL_SKIP" in nsi
+ assert "Setup continues" in nsi
diff --git a/tests/unit/desktop/test_nsis_uninstall.py b/tests/unit/desktop/test_nsis_uninstall.py
index a58c8f7b..399b8142 100644
--- a/tests/unit/desktop/test_nsis_uninstall.py
+++ b/tests/unit/desktop/test_nsis_uninstall.py
@@ -10,8 +10,6 @@
DESKTOP_README = REPO / "desktop" / "README.md"
ORG_PAGE = REPO / "dashboard" / "src" / "pages" / "Organization" / "index.tsx"
ORG_BROWSER = REPO / "dashboard" / "src" / "pages" / "Organization" / "OrgMiniBrowser.tsx"
-ORG_TABS = REPO / "dashboard" / "src" / "pages" / "Organization" / "orgBrowser.ts"
-OPENURL_GO = REPO / "desktop" / "src" / "openurl.go"
def _uninstall_section(text: str) -> str:
@@ -147,20 +145,26 @@ def test_nsis_provisions_openxyos_runtime() -> None:
assert "!insertmacro wails.provisionOpenXYOS" in nsh
assert 'File "/oname=openxyos-runtime.zip"' in nsh
assert r"$INSTDIR\openxyos" in nsh
- assert r"$INSTDIR\openxyos-runtime" in nsh
- assert r"$R6\FreeOS\openxyos" in nsh
+ assert "OPENXYOS_RUNTIME_ZIP_PRESENT" in nsh
assert "ReadEnvStr $R6 LOCALAPPDATA" in nsh
- assert "http://127.0.0.1:3780" in nsi
assert "LangString OPENXYOS_WORKDIR ${LANG_SIMPCHINESE}" in nsi
- assert "创建 openXYOS 工作目录" in nsi
+ assert "组织能力在 FreeOS 宿主内运行" in nsi
assert "openXYOS 运行包" in nsi
+ present_gate = nsh[
+ nsh.index("!ifndef OPENXYOS_RUNTIME_ZIP") : nsh.index("!ifndef REQUEST_EXECUTION_LEVEL")
+ ]
+ assert "!if /FileExists" not in present_gate
+ assert "!define OPENXYOS_RUNTIME_ZIP_PRESENT" not in present_gate
task = (REPO / "desktop" / "src" / "build" / "windows" / "Taskfile.yml").read_text(
encoding="utf-8"
)
assert "stage:openxyos-runtime" in task
+ assert "maybe-stage:openxyos-runtime" in task
+ assert "SHIP_OPENXYOS_RUNTIME" in task
assert "stage_openxyos_runtime.py" in task
package = (REPO / "desktop" / "portable" / "package.sh").read_text(encoding="utf-8")
assert "openXYOS frontend missing" in package
+ assert "SHIP_OPENXYOS_RUNTIME" in package
def _nsis_filewrite_argc(line: str) -> int:
@@ -205,7 +209,7 @@ def test_nsis_filewrite_is_exactly_two_args() -> None:
def test_nsis_runs_openxyos_provisioner_subprocess() -> None:
- """Parent copies a real provisioner and nsExecs it; livez failure aborts."""
+ """Setup copies an optional payload and never aborts on livez or a missing zip."""
nsi = NSI.read_text(encoding="utf-8-sig")
nsh = NSH.read_text(encoding="utf-8")
provision = nsh[
@@ -218,36 +222,27 @@ def test_nsis_runs_openxyos_provisioner_subprocess() -> None:
assert 'File "provision-openxyos.ps1"' in provision
assert 'File "provision-openxyos.cmd"' in provision
assert 'File "start-sidecar.ps1"' in provision
- assert "nsExec::Exec $R4" in provision
+ assert "nsExec::Exec $R4" not in provision
assert "nsExec::ExecToLog $R4" not in provision
- assert "OPENXYOS_PROVISION_DETAIL" in provision
- assert r"$INSTDIR\openxyos-runtime.zip" in provision
- assert r"$R6\FreeOS\openxyos" in provision
- # Cleanup after success lives in the provisioner, not NSIS (failed
- # provision must keep the zip / openxyos-runtime folder for debug).
- assert 'Delete "$INSTDIR\\openxyos-runtime.zip"' not in provision
- assert 'RMDir /r "$INSTDIR\\openxyos-runtime"' not in provision
- assert "Pop $0" in provision
- assert "Abort" in provision
- assert ".install-ready" in provision
- assert "OPENXYOS_FAIL_START" in nsh
- assert "OPENXYOS_FAIL_LIVEZ" in nsh
- assert "start.log" in nsi
- assert "LangString OPENXYOS_PROVISION ${LANG_SIMPCHINESE}" in nsi
- assert "LangString OPENXYOS_PROVISION_DETAIL ${LANG_SIMPCHINESE}" in nsi
- assert "provision.log(UTF-8)" in nsi
- assert "子进程" in nsi
- assert "OPENXYOS_PROBE_WARN" not in nsi
- assert "安装将继续" not in nsi
+ assert "Abort" not in provision
+ assert ".install-ready" not in provision
+ assert "OPENXYOS_OPTIONAL_SKIP" in provision
+ assert "OPENXYOS_OPTIONAL_ABSENT" in provision
+ assert 'File "/oname=openxyos-runtime.zip"' in provision
+ assert "OPENXYOS_RUNTIME_ZIP_PRESENT is set but" in provision
+ assert "openxyos-runtime.zip missing" not in nsh
+ assert "安装继续" in nsi
+ assert "Setup continues" in nsi
assert "开机自启" not in nsi
assert "PersistOpenXYOS" not in nsi
assert "PersistOpenXYOS" not in nsh
assert "Call PersistOpenXYOS" not in nsh
- assert "openxyos-runtime.zip missing" in nsh
- assert "!error" in nsh
workflow = (REPO / ".github" / "workflows" / "octop-desktop.yml").read_text(encoding="utf-8")
- assert "org-sidecar/openxyos/dist/index.html" in workflow
- assert "org-sidecar/openxyos/backend/server.ts" in workflow
+ assert 'SKIP_ORG_SIDECAR: "1"' in workflow
+ assert "SHIP_OPENXYOS_RUNTIME=0" in workflow
+ assert "must not embed org-sidecar" in workflow
+ assert "org-sidecar/openxyos/dist/index.html" not in workflow
+ assert "org-sidecar/openxyos/backend/server.ts" not in workflow
def test_desktop_readme_documents_uninstall_keep_vs_remove() -> None:
@@ -268,7 +263,23 @@ def test_desktop_readme_documents_uninstall_keep_vs_remove() -> None:
assert "tar.exe" in text
assert "README-only" in text
assert "folder picker" in text
- assert "install-time" in text.lower() or "during Setup" in text or "安装期" in text
+ assert "FREEOS_ORG_SIDECAR=1" in text
+ assert "SHIP_OPENXYOS_RUNTIME=1" in text
+ assert "in-host" in text.lower() or "in-host" in text
+ assert "does not extract" in text.lower() or "does not" in text.lower()
+
+
+def test_desktop_metadata_is_single_runtime() -> None:
+ config = (REPO / "desktop" / "src" / "build" / "config.yml").read_text(encoding="utf-8")
+ info = (REPO / "desktop" / "src" / "build" / "windows" / "info.json").read_text(
+ encoding="utf-8"
+ )
+ assert "in-host organization" in config
+ assert "FREEOS_ORG_SIDECAR=1" in config
+ assert "openXYOS sidecar" not in config
+ assert "Octop shell + openXYOS" not in config
+ assert "Octop shell + openXYOS" not in info
+ assert "in-host organization" in info
def _filewrite_payloads(block: str) -> list[str]:
@@ -324,38 +335,29 @@ def test_nsis_does_not_register_openxyos_logon_autostart() -> None:
def test_organization_embeds_local_openxyos_url() -> None:
page = ORG_PAGE.read_text(encoding="utf-8")
browser = ORG_BROWSER.read_text(encoding="utf-8")
- tabs = ORG_TABS.read_text(encoding="utf-8")
- openurl = OPENURL_GO.read_text(encoding="utf-8")
zh = (REPO / "dashboard" / "src" / "locales" / "zh.json").read_text(encoding="utf-8")
+ assert 'data-testid="org-native-workbench"' in page
+ assert 'data-testid="org-assemble"' in page
+ assert 'data-testid="org-pack"' in page
+ assert 'data-testid="org-loop"' in page
+ assert 'data-testid="org-colleagues"' in page
+ assert "org-mini-browser" not in page
+ assert "org-sidecar-gate" not in page
+ assert "org-restart-sidecar" not in page
+ assert "restartSidecar" not in page
+ assert "startSidecar" in page
+ assert 'data-testid="org-advanced-console"' in page
assert "window.prompt" not in page
assert "pickDesktopFolder" in page
- assert "canPickDesktopFolder" in page
assert "resolveOpenxyosSourceDest" in page
assert 'data-testid="org-download-source"' in page
- assert "void downloadSource()" in page
- assert "http://127.0.0.1:3780" in tabs
- assert "DEFAULT_ORG_URL" in page
- assert "