Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ jobs:
"$smoke_root/wheel-venv/bin/pip" install /tmp/dyro-dist/dyro-*.whl
"$smoke_root/wheel-venv/bin/python" -c "import experiments.local_agent_dispatch"
"$smoke_root/wheel-venv/bin/python" -I -c "import dyro.continuation"
"$smoke_root/wheel-venv/bin/python" -I -c "from dyro.console.assets import validate_assets; validate_assets()"
DYRO_LOCAL_AGENT_DISPATCH_HOME="$smoke_root/wheel-dispatch-home" "$smoke_root/wheel-venv/bin/dyro" dispatch doctor >"$smoke_root/dispatch-doctor.json"
uv run python -m venv "$smoke_root/sdist-venv"
"$smoke_root/sdist-venv/bin/pip" install /tmp/dyro-dist/dyro-*.tar.gz
"$smoke_root/sdist-venv/bin/python" -I -c "import dyro.continuation"
"$smoke_root/sdist-venv/bin/python" -I -c "from dyro.console.assets import validate_assets; validate_assets()"

windows-dispatch-import:
name: Windows dispatch import / fail-closed smoke
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ jobs:
uv run python -m venv "$smoke_root/wheel-venv"
"$smoke_root/wheel-venv/bin/pip" install "$GITHUB_WORKSPACE"/dist/dyro-*.whl
"$smoke_root/wheel-venv/bin/python" -c "import experiments.local_agent_dispatch"
"$smoke_root/wheel-venv/bin/python" -I -c "from dyro.console.assets import validate_assets; validate_assets()"
DYRO_LOCAL_AGENT_DISPATCH_HOME="$smoke_root/wheel-dispatch-home" "$smoke_root/wheel-venv/bin/dyro" dispatch doctor >"$smoke_root/wheel-doctor.json"
uv run python -c "import json; json.load(open('$smoke_root/wheel-doctor.json'))"
uv run python -m venv "$smoke_root/sdist-venv"
"$smoke_root/sdist-venv/bin/pip" install "$GITHUB_WORKSPACE"/dist/dyro-*.tar.gz
"$smoke_root/sdist-venv/bin/python" -c "import experiments.local_agent_dispatch"
"$smoke_root/sdist-venv/bin/python" -I -c "from dyro.console.assets import validate_assets; validate_assets()"
DYRO_LOCAL_AGENT_DISPATCH_HOME="$smoke_root/sdist-dispatch-home" "$smoke_root/sdist-venv/bin/dyro" dispatch doctor >"$smoke_root/sdist-doctor.json"
uv run python -c "import json; json.load(open('$smoke_root/sdist-doctor.json'))"

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ include docs/tool-catalog.md
include docs/updates.md
include docs/workspace-blueprints.md
include examples/blueprints/acme-platform.toml
recursive-include src/dyro/console/assets *
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@

DyroEngineeringFlow is not coupled to Codex, Claude, or any business domain. Each team supplies a `dyro.toml` Profile for repositories, layouts, agent adapters, and delivery policy; business rules, model cost, and release practices stay in that Profile.

## Local web Console

Run `dyro console` from any directory to open a read-only local dashboard for registered workspaces. It binds only to `127.0.0.1` on a random port, hands the browser a one-time fragment secret, and never exposes a network listener or browser write actions.

```bash
dyro console
dyro --workspace platform console --no-open
dyro --root /path/to/profile console
```

The dashboard surfaces workspace health, attention, task execution counts, active objectives, and a safe next CLI command. It never runs agents, changes task state, marks a workspace as recently used, merges, pushes, or contacts external services. See the [Console design](docs/designs/local-web-console.md) for the read-only boundary and recovery behavior.

## What it enforces

- A task belongs to exactly one development line—never a mixed feature or hotfix workspace.
Expand Down
12 changes: 12 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@

DyroEngineeringFlow 不绑定 Codex、Claude 或任何业务领域。每个团队通过 `dyro.toml` Profile 定义仓库、目录布局、Agent adapter 与交付策略;业务规则、模型成本和发布实践始终留在各自的 Profile 中。

## 本地 Web 控制台

可在任意目录运行 `dyro console`,打开已登记工作区的只读本地总览。它只绑定随机端口的 `127.0.0.1`,通过一次性 fragment secret 与浏览器建立会话,不暴露网络监听,也不提供浏览器写操作。

```bash
dyro console
dyro --workspace platform console --no-open
dyro --root /path/to/profile console
```

控制台展示工作区健康、需要关注的事项、Task 执行计数、活跃 Objective 和一条安全的下一步 CLI 命令。它不会启动 Agent、改变任务状态、标记最近使用、合并、push 或访问外部服务。只读边界与恢复方式见 [Console 设计](docs/designs/local-web-console.md)。

## 核心约束

- 一个任务只能属于一条开发线,不能混用功能版本与 Hotfix 工作区。
Expand Down
17 changes: 17 additions & 0 deletions docs/designs/local-web-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,23 @@ C05 提供前台入口 `dyro console [--no-open] [--port PORT]`,但不增加
- bare `dyro` Home 新增“查看全部项目控制台”。它复用当前已登记工作区作为初始焦点,且 Home 的
dry-run 会保留零副作用;需要不登记某个 Profile 时,应显式使用 `dyro --root PATH console`。

### 4.6 C06 已落地的离线总览界面

C06 将已认证的总览接口接入无框架浏览器界面,优先让使用者在一页内识别需要处理的工程:

- `index.html`、`app.js` 和 `styles.css` 是 wheel 内的固定资源;每个启动都通过 SHA-256 与大小
manifest 验证,缺失、漂移或未知 asset 使 listener fail closed,绝不从 cwd 或 source tree 回退;
- 页面先将 fragment 中的一次性 bootstrap secret 读入局部变量并立即清除 URL,再交换为只存在
当前 tab `sessionStorage` 的 bearer。它不用 cookie、`localStorage`、外部资源、inline script、
`innerHTML` 或服务端 mutation;
- 全局页显示 attention、Task 状态分布、active Objective、健康与 freshness、推荐命令。所有
workspace 文本通过 DOM `textContent` 写入;复制只在浏览器内进行;
- 可点击卡片查看当前 C04 summary,支持条件 ETag 刷新。页面在后台时暂停轮询,恢复可见时只恢复
单一轮询定时器;会话过期或本地读取失败显示一条恢复说明,不猜测数据或修改项目。

C06 仅消费已发布的 overview/workspace 只读 DTO;Objective、Task 证据链、图和活动的深度 API
仍须在对应 Core 投影准备完成后通过单独的只读扩展交付,不能由浏览器自行推导。

## 5. 模块设计

建议模块边界:
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,16 @@ dev = ["build==1.3.0", "ruff==0.15.2", "twine==6.2.0"]
packages = [
"dyro",
"dyro.console",
"dyro.console.assets",
"dyro.continuation",
"experiments",
"experiments.local_agent_dispatch",
"experiments.local_agent_dispatch.adapters",
]

[tool.setuptools.package-data]
"dyro.console" = ["assets/*"]

[tool.unittest]
start-directory = "tests"

Expand Down
70 changes: 70 additions & 0 deletions src/dyro/console/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Verified package resources for the offline local Console shell."""

from __future__ import annotations

from dataclasses import dataclass
import hashlib
from importlib.resources import files


@dataclass(frozen=True)
class ConsoleAsset:
name: str
content_type: str
sha256: str
size: int
body: bytes


# The values are generated from the checked-in resources. The server validates
# every byte before listening so it never silently falls back to cwd or source
# tree files when a packaged asset is missing or has drifted.
ASSET_MANIFEST = {
"index.html": (
"text/html; charset=utf-8",
"8980f489da7e7fbd6c6023fc3338c7ff768e14dfade2c13d6b53b69f6304821a",
1949,
),
"app.js": (
"text/javascript; charset=utf-8",
"034405c85e20d7fbcde3d07b0bf0a29abf2c20c9ce5be16b579b48561d69965f",
10484,
),
"styles.css": (
"text/css; charset=utf-8",
"4759e04327b0a29d0bcc039d09d64f633670014ecf7fe111244ec78f60d091af",
4842,
),
}


class ConsoleAssetError(RuntimeError):
"""A packaged static asset is missing, malformed, or has unexpected bytes."""


def load_asset(name: str) -> ConsoleAsset:
if name not in ASSET_MANIFEST:
raise ConsoleAssetError("unknown Console asset")
content_type, expected_sha256, expected_size = ASSET_MANIFEST[name]
resource = files("dyro.console").joinpath("assets", name)
if not resource.is_file():
raise ConsoleAssetError("missing Console asset")
try:
body = resource.read_bytes()
except OSError as exc:
raise ConsoleAssetError("unreadable Console asset") from exc
digest = hashlib.sha256(body).hexdigest()
if (
not expected_sha256
or expected_size <= 0
or len(body) != expected_size
or digest != expected_sha256
):
raise ConsoleAssetError("invalid Console asset manifest")
return ConsoleAsset(name, content_type, digest, len(body), body)


def validate_assets() -> None:
"""Fail closed unless every public resource matches the fixed manifest."""
for name in ASSET_MANIFEST:
load_asset(name)
Loading