@tea-agent/loop-agent 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +21 -0
- package/README.md +4 -6
- package/dist/commands/init.js +3 -3
- package/dist/governance/exec-plans.js +4 -0
- package/dist/worker/cli.js +13 -12
- package/dist/worker/console/doctor.js +55 -2
- package/dist/worker/console/index.js +1 -0
- package/dist/worker/console/loopback.js +2 -2
- package/dist/worker/console/observe-link.js +7 -2
- package/dist/worker/console/operator-actions.js +30 -2
- package/dist/worker/console/operator-selection.js +92 -0
- package/dist/worker/console/operator-surface-health.js +23 -0
- package/dist/worker/console/recovery-cta.js +2 -2
- package/dist/worker/console/routes.js +45 -19
- package/dist/worker/console/security.js +29 -4
- package/dist/worker/console/server.js +106 -8
- package/dist/worker/console/static/assets/index-3vsjZJHq.js +16 -0
- package/dist/worker/console/static/assets/index-i1wV4LrY.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +63 -21
- package/dist/worker/observe/server.js +3 -10
- package/dist/worker/observe/static/index.html +6 -3
- package/dist/worker/observe/static/styles.css +53 -0
- package/docs/README.md +3 -2
- package/docs/architecture/evolution.md +6 -6
- package/docs/architecture/worker-and-feature.md +9 -9
- package/package.json +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +2 -2
- package/skills/loop-agent/references/command-reference.md +4 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
- package/dist/worker/console/static/assets/index-KUSib7aM.js +0 -16
- package/dist/worker/console/static/assets/index-ucIzpaGJ.css +0 -1
package/AGENTS.md
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
10. 检查 `git status --short --branch`。
|
|
55
55
|
11. 运行本次任务相关的最小基线验证。
|
|
56
56
|
12. 如果用户提到"后端测试"、"接口测试"、"pytest"、"自动化测试",在任务 `task.json` 中设置 `taskKind: "backend-test"` 再 `dag run-task`;不要用 `--profile backend-test`(CLI 不接受该值,专用模板只走 taskKind)。知识回写用 `taskKind: "knowledge-sync"`(须 `featureId`),图谱开荒用 `taskKind: "knowledge-graph-bootstrap"`。`--profile` 仅表示治理强度:`auto|minimal|standard|reviewed|supervised`。
|
|
57
|
-
13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker observe serve --repo . --port 8787`
|
|
57
|
+
13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker console serve --repo . --port 8790` 启动统一 Operator Console;`/inspect/` 提供只读检视。`agent-worker observe serve --repo . --port 8787` 仅为兼容入口。
|
|
58
58
|
14. 如果用户要求“合并 `<source>` 到 `<target>`”或“合并 origin/main 到当前分支”,先阅读 `docs/branch-merge-guideline.md`,按影响自动选择快速、标准或深度模式;始终冻结 source SHA、审查双方功能、运行 merge-tree、生成 source-SHA 合并报告,并在提交前再次 fetch 防止主干前进。
|
|
59
59
|
|
|
60
60
|
## 会话协议
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
- 长期决策写入 `docs/`,不要只留在聊天里。
|
|
90
90
|
- 分支合并遵循 `docs/branch-merge-guideline.md`;快速模式只用于可证明的低风险/no-op 合并,涉及冲突、init/package/runtime/release/public API 时必须升级为标准或深度模式。
|
|
91
91
|
- 后端测试、接口/API 测试、pytest 或明确的后端自动化测试,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。`backend-test` 是 `taskKind`,不是 `--profile` 的可选值;`dag run-task` 继续使用 `--profile auto` 选择治理等级。仅说“自动化测试”且前后端不明时,先根据任务源和项目技术栈判断,禁止无条件路由。
|
|
92
|
-
-
|
|
92
|
+
- 本地 Operator Console:`agent-worker console serve --repo . --port 8790`,访问 `http://127.0.0.1:8790/`;其中 `/inspect/` 为只读运行检视。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` / `--debug`,不要直接暴露到公开网络。
|
|
93
93
|
- 面向使用者的新增、修改、删除或修复,应同步更新根目录 `CHANGELOG.md`;保持版本级摘要即可,不写过细技术细节。
|
|
94
94
|
- 面向用户的中文更新日志、README 和说明文档应使用自然、结果导向的表达:先说明用户能获得什么或问题如何改善,保留必要的命令和产品术语,避免逐字翻译、内部实现细节和无意义的中英混杂。
|
|
95
95
|
- 涉及 `loop-agent init` 或目标项目投影的改动,必须同步考虑目标项目生成物:`AGENTS.md`、`README.md`、`harness.json`、`ai_workspace/loop-agent/`、`scripts/`、`.agents/skills/`、`.harness/prompts`、`.gitignore`(loop-agent runtime managed block)和 npm 包内置 assets;目标项目根 `docs/` 和根 `skills/` 的旧投影需要由 `init update --apply-safe` 安全迁移或退役。
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.19.0] - 2026-07-23
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- Operator Console 统一整合 Operate 与 Inspect,在同一服务即可检视只读 Dashboard、DAG、时间线与 artifacts
|
|
10
|
+
- 原 Observe 服务进入兼容期,仍可运行但会输出迁移提示,引导平滑过渡到统一控制台
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- 新增统一 Operator Console:`console serve` 同时提供 Operate 与 Inspect,支持通过 `/inspect/` 直接检视运行详情与 DAG
|
|
15
|
+
- Console 健康检查接口升级,包含 Inspect 就绪状态与路由能力信息
|
|
16
|
+
- `console serve` 新增局域网绑定(`--host 0.0.0.0`)与调试日志(`--debug`)选项,并在非本地访问时保留安全校验
|
|
17
|
+
|
|
18
|
+
### 改进
|
|
19
|
+
|
|
20
|
+
- 优化操作与观测统一界面的导航与布局,合并确认与运行为「开始运行」,DAG 详情跳转路径更加准确
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- 修复执行计划索引将命名约定占位符(如 `YYYY-MM-DD-<topic>.md`)误判为缺失计划引用的问题
|
|
25
|
+
|
|
5
26
|
## [0.18.1] - 2026-07-22
|
|
6
27
|
|
|
7
28
|
### 重点更新
|
package/README.md
CHANGED
|
@@ -85,24 +85,22 @@ loop-agent dag doctor --run-id <run-id> --markdown
|
|
|
85
85
|
loop-agent dag report --run-id <run-id>
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
### Official 本地操作面(CLI +
|
|
88
|
+
### Official 本地操作面(CLI + 统一 Operator Console)
|
|
89
89
|
|
|
90
90
|
| 入口 | 命令 | 角色 |
|
|
91
91
|
| --- | --- | --- |
|
|
92
92
|
| 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
|
|
93
|
-
| Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790
|
|
94
|
-
| Observe | `agent-worker observe serve --repo . --port 8787` |
|
|
93
|
+
| Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790`,含 Operate 与 Inspect) |
|
|
94
|
+
| Observe | `agent-worker observe serve --repo . --port 8787` | 兼容期独立只读入口;不再作为推荐路径 |
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
97
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
98
98
|
agent-worker console serve --repo .
|
|
99
99
|
agent-worker console doctor --repo .
|
|
100
100
|
|
|
101
|
-
# 终端 B:独立 Observe(不经 Console mount/proxy)
|
|
102
|
-
agent-worker observe serve --repo . --port 8787
|
|
103
101
|
```
|
|
104
102
|
|
|
105
|
-
访问 Console <http://127.0.0.1:8790
|
|
103
|
+
访问 Console <http://127.0.0.1:8790/>;顶部「检视」以及 `/inspect/` 提供原 Observe 的只读 Dashboard、DAG、时间线与 artifacts。需要保留的旧书签或脚本仍可在兼容窗口使用 Observe `:8787`。openCode 等主会话仍是 **Compatibility / Operator Assist**,不得升级为与 Official 同等保证。
|
|
106
104
|
|
|
107
105
|
## 核心边界
|
|
108
106
|
|
package/dist/commands/init.js
CHANGED
|
@@ -783,13 +783,13 @@ function buildManagedAgentsBlock(input) {
|
|
|
783
783
|
"",
|
|
784
784
|
"### 运行看板(只读)",
|
|
785
785
|
"",
|
|
786
|
-
"
|
|
786
|
+
"启动统一 Operator Console(含只读 Inspect):",
|
|
787
787
|
"",
|
|
788
788
|
"```bash",
|
|
789
|
-
"agent-worker
|
|
789
|
+
"agent-worker console serve --repo . --port 8790",
|
|
790
790
|
"```",
|
|
791
791
|
"",
|
|
792
|
-
"浏览器打开 `http://127.0.0.1:
|
|
792
|
+
"浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` 做局域网访问(无网络鉴权,仅可信网络),`--debug` 输出请求日志。不要直接暴露到公开网络。`agent-worker observe serve` 仅为兼容入口。",
|
|
793
793
|
"",
|
|
794
794
|
"### DAG 诊断与收口",
|
|
795
795
|
"",
|
|
@@ -177,6 +177,10 @@ function localIndexReference(rawReference) {
|
|
|
177
177
|
.replaceAll("\\", "/");
|
|
178
178
|
if (!normalized.endsWith(".md") || normalized.includes("/"))
|
|
179
179
|
return null;
|
|
180
|
+
// Naming-convention placeholders (e.g. `YYYY-MM-DD-<topic>.md`) and other
|
|
181
|
+
// unsafe stems are documentation, not index entries. Align with assertSafePlanId.
|
|
182
|
+
if (/[<>:"|?*]/.test(normalized))
|
|
183
|
+
return null;
|
|
180
184
|
return normalized;
|
|
181
185
|
}
|
|
182
186
|
function extractIndexReferences(indexContent) {
|
package/dist/worker/cli.js
CHANGED
|
@@ -559,9 +559,9 @@ export function buildAgentWorkerProgram() {
|
|
|
559
559
|
.command("serve")
|
|
560
560
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
561
561
|
.option("--port <port>", "HTTP port", "8787")
|
|
562
|
-
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no auth)", "127.0.0.1")
|
|
562
|
+
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
563
563
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
564
|
-
.description("Start read-only observe HTTP server")
|
|
564
|
+
.description("Start read-only observe HTTP server (compat entry; prefer console serve)")
|
|
565
565
|
.action(async (options) => {
|
|
566
566
|
const repoRoot = path.resolve(options.repo);
|
|
567
567
|
const server = await createObserveServer({
|
|
@@ -570,6 +570,9 @@ export function buildAgentWorkerProgram() {
|
|
|
570
570
|
port: Number.parseInt(options.port, 10),
|
|
571
571
|
debug: options.debug === true,
|
|
572
572
|
});
|
|
573
|
+
process.stderr.write("[OBSERVE_SERVE_DEPRECATED] agent-worker observe serve 作为独立看板入口将在兼容窗口后下线。\n" +
|
|
574
|
+
"请改用:agent-worker console serve --repo <repo> --port 8790\n" +
|
|
575
|
+
"统一 Operator Console 已包含原 Observe 的只读检视能力:http://127.0.0.1:8790/\n");
|
|
573
576
|
process.stdout.write(`${server.url}\n`);
|
|
574
577
|
await new Promise((resolve) => {
|
|
575
578
|
const shutdown = () => resolve();
|
|
@@ -593,22 +596,18 @@ export function buildAgentWorkerProgram() {
|
|
|
593
596
|
.command("serve")
|
|
594
597
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
595
598
|
.option("--port <port>", "HTTP port", "8790")
|
|
596
|
-
.option("--host <host>", "Bind host (default 127.0.0.1;
|
|
599
|
+
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
600
|
+
.option("--debug", "Log request source/method/path/status to stderr")
|
|
597
601
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
598
|
-
.description("Start Loop Operator Console HTTP server (static SPA + Operator API)")
|
|
602
|
+
.description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
|
|
599
603
|
.action(async (options) => {
|
|
600
604
|
const { createConsoleServer } = await import("./console/server.js");
|
|
601
|
-
const { isLoopbackHost } = await import("./console/loopback.js");
|
|
602
|
-
if (!isLoopbackHost(options.host)) {
|
|
603
|
-
process.stderr.write(`console serve: non-loopback host "${options.host}" is not allowed (fail closed)\n`);
|
|
604
|
-
process.exitCode = 1;
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
605
|
const repoRoot = path.resolve(options.repo);
|
|
608
606
|
const server = await createConsoleServer({
|
|
609
607
|
repoRoot,
|
|
610
608
|
host: options.host,
|
|
611
609
|
port: Number.parseInt(options.port, 10),
|
|
610
|
+
debug: options.debug === true,
|
|
612
611
|
appDataRoot: options.appData
|
|
613
612
|
? path.resolve(options.appData)
|
|
614
613
|
: undefined,
|
|
@@ -625,13 +624,15 @@ export function buildAgentWorkerProgram() {
|
|
|
625
624
|
.command("doctor")
|
|
626
625
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
627
626
|
.option("--json", "Emit JSON report on stdout")
|
|
628
|
-
.option("--observe-url <url>", "
|
|
629
|
-
.
|
|
627
|
+
.option("--observe-url <url>", "Legacy external Observe base URL to probe (non-blocking)")
|
|
628
|
+
.option("--console-url <url>", "Unified Console base URL to probe")
|
|
629
|
+
.description("Report sibling identity, preflight, and unified Inspect readiness")
|
|
630
630
|
.action(async (options) => {
|
|
631
631
|
const { formatConsoleDoctorHuman, runConsoleDoctor } = await import("./console/doctor.js");
|
|
632
632
|
const report = await runConsoleDoctor({
|
|
633
633
|
repoRoot: path.resolve(options.repo),
|
|
634
634
|
observeBaseUrl: options.observeUrl,
|
|
635
|
+
consoleBaseUrl: options.consoleUrl,
|
|
635
636
|
});
|
|
636
637
|
if (options.json) {
|
|
637
638
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdtemp } from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -7,6 +8,8 @@ import { probePiReadiness, } from "./pi-readiness.js";
|
|
|
7
8
|
import { DEFAULT_OBSERVE_BASE_URL, OBSERVE_START_COMMAND, probeAndClassifyObserve, } from "./observe-health-match.js";
|
|
8
9
|
import { repoFingerprintV1 } from "./repo-fingerprint.js";
|
|
9
10
|
import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
11
|
+
import { defaultObserveStaticDir, ROUTES } from "../observe/routes.js";
|
|
12
|
+
import { deriveObserveRouteCapabilities } from "../observe/health.js";
|
|
10
13
|
async function probeObserve(url, localFingerprint, fetchImpl) {
|
|
11
14
|
const classified = await probeAndClassifyObserve({
|
|
12
15
|
baseUrl: url,
|
|
@@ -42,6 +45,48 @@ async function probeObserve(url, localFingerprint, fetchImpl) {
|
|
|
42
45
|
message: classified.message,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
48
|
+
async function probeInspect(consoleBaseUrl, fetchImpl) {
|
|
49
|
+
const routeCapabilities = deriveObserveRouteCapabilities(ROUTES);
|
|
50
|
+
if (!consoleBaseUrl) {
|
|
51
|
+
const ready = existsSync(path.join(defaultObserveStaticDir(), "index.html"));
|
|
52
|
+
return {
|
|
53
|
+
status: ready ? "ready" : "degraded",
|
|
54
|
+
source: "static-assets",
|
|
55
|
+
routeCapabilities,
|
|
56
|
+
message: ready
|
|
57
|
+
? "Inspect 静态资源可用;启动 agent-worker console serve 以提供统一入口"
|
|
58
|
+
: "Inspect 静态资源缺失;请运行构建或检查发布包",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const url = `${consoleBaseUrl.replace(/\/+$/, "")}/api/health`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetchImpl(url, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
signal: AbortSignal.timeout(1500),
|
|
66
|
+
});
|
|
67
|
+
const body = (await response.json());
|
|
68
|
+
const ready = response.ok &&
|
|
69
|
+
body.ok === true &&
|
|
70
|
+
body.schemaVersion === 1 &&
|
|
71
|
+
body.inspect?.ready === true;
|
|
72
|
+
return {
|
|
73
|
+
status: ready ? "ready" : "degraded",
|
|
74
|
+
source: "console-health",
|
|
75
|
+
url,
|
|
76
|
+
routeCapabilities: body.inspect?.routeCapabilities ?? routeCapabilities,
|
|
77
|
+
message: ready ? undefined : "Console health 未确认 Inspect 已就绪",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
status: "degraded",
|
|
83
|
+
source: "console-health",
|
|
84
|
+
url,
|
|
85
|
+
routeCapabilities,
|
|
86
|
+
message: error instanceof Error ? error.message : String(error),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
45
90
|
/**
|
|
46
91
|
* Run console doctor checks. Observe unavailability never fails the report.
|
|
47
92
|
*/
|
|
@@ -115,8 +160,14 @@ export async function runConsoleDoctor(options) {
|
|
|
115
160
|
message: "cannot run preflight without sibling loop-agent bin",
|
|
116
161
|
};
|
|
117
162
|
}
|
|
118
|
-
const
|
|
119
|
-
const observe =
|
|
163
|
+
const inspect = await probeInspect(options.consoleBaseUrl, options.fetchImpl ?? fetch);
|
|
164
|
+
const observe = options.observeBaseUrl
|
|
165
|
+
? await probeObserve(options.observeBaseUrl, fingerprint, options.fetchImpl ?? fetch)
|
|
166
|
+
: {
|
|
167
|
+
status: "unavailable",
|
|
168
|
+
url: `${DEFAULT_OBSERVE_BASE_URL}/api/health`,
|
|
169
|
+
message: "未探测兼容 Observe;统一 Inspect 不依赖 8787",
|
|
170
|
+
};
|
|
120
171
|
const pi = await probePiReadiness({
|
|
121
172
|
override: options.piReadinessProbe,
|
|
122
173
|
siblingControllerOk: Boolean(controllerIdentity && "packageVersion" in controllerIdentity),
|
|
@@ -133,6 +184,7 @@ export async function runConsoleDoctor(options) {
|
|
|
133
184
|
piReadiness: pi.state,
|
|
134
185
|
pi,
|
|
135
186
|
observe,
|
|
187
|
+
inspect,
|
|
136
188
|
generatedAt: new Date().toISOString(),
|
|
137
189
|
};
|
|
138
190
|
}
|
|
@@ -148,6 +200,7 @@ export function formatConsoleDoctorHuman(report) {
|
|
|
148
200
|
`pi.interviewSession: ${report.pi.probe.interviewSessionOk}`,
|
|
149
201
|
`preflight: ${report.preflight.ok ? `ok (${report.preflight.summary})` : `fail ${report.preflight.code}: ${report.preflight.message}`}`,
|
|
150
202
|
`observe: ${report.observe.status}${report.observe.status === "unavailable" || report.observe.status === "mismatch" ? ` (${report.observe.message})` : ""}`,
|
|
203
|
+
`inspect: ${report.inspect.status}${report.inspect.message ? ` (${report.inspect.message})` : ""}`,
|
|
151
204
|
];
|
|
152
205
|
if (report.observe.status === "unavailable" ||
|
|
153
206
|
report.observe.status === "mismatch") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createConsoleServer, defaultConsoleStaticDir } from "./server.js";
|
|
2
|
+
export { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
2
3
|
export { runConsoleDoctor, formatConsoleDoctorHuman } from "./doctor.js";
|
|
3
4
|
export { isLoopbackHost } from "./loopback.js";
|
|
4
5
|
export { repoFingerprintV1, normalizeWorktreeRealpath, } from "./repo-fingerprint.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Console-local loopback host check.
|
|
3
|
-
*
|
|
2
|
+
* Console-local loopback host check.
|
|
3
|
+
* Used for default bind safety messaging and mutation Host checks on loopback binds.
|
|
4
4
|
*/
|
|
5
5
|
export function isLoopbackHost(host) {
|
|
6
6
|
const normalized = host.trim().toLowerCase();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Observe
|
|
3
|
-
*
|
|
2
|
+
* Deprecated Observe deep-link compatibility helpers.
|
|
3
|
+
* New unified-surface callers should use buildInspectDeepLink.
|
|
4
4
|
*/
|
|
5
|
+
import { buildInspectUrl, selectionFromObserveLinkTarget, } from "./operator-selection.js";
|
|
5
6
|
function stripTrailingSlash(baseUrl) {
|
|
6
7
|
return baseUrl.replace(/\/+$/, "");
|
|
7
8
|
}
|
|
@@ -31,3 +32,7 @@ export function buildObserveDeepLink(baseUrl, target) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
/** Build a canonical same-origin Inspect URL for the unified Operator surface. */
|
|
36
|
+
export function buildInspectDeepLink(origin, target) {
|
|
37
|
+
return buildInspectUrl(origin, selectionFromObserveLinkTarget(target));
|
|
38
|
+
}
|
|
@@ -5,7 +5,7 @@ import { getTaskPaths } from "../../task/runtime.js";
|
|
|
5
5
|
import { stageUtf8Text } from "./app-data.js";
|
|
6
6
|
import { ALL_CONFIRMATION_CHALLENGES, hashDagBytes, } from "./dag-confirmation.js";
|
|
7
7
|
import { capabilityForObserveTarget, DEFAULT_OBSERVE_BASE_URL, OBSERVE_START_COMMAND, probeAndClassifyObserve, } from "./observe-health-match.js";
|
|
8
|
-
import { buildObserveDeepLink, } from "./observe-link.js";
|
|
8
|
+
import { buildInspectDeepLink, buildObserveDeepLink, } from "./observe-link.js";
|
|
9
9
|
import { resolveLatestDagRunIdForTask } from "./resolve-dag-run-for-task.js";
|
|
10
10
|
import { repoFingerprintV1 } from "./repo-fingerprint.js";
|
|
11
11
|
import { scheduleOperation, } from "./operation-runner.js";
|
|
@@ -285,7 +285,8 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
case "observeLink": {
|
|
288
|
-
const
|
|
288
|
+
const consoleOrigin = ctx.getConsoleOrigin?.();
|
|
289
|
+
const baseUrl = ctx.observeBaseUrl ?? consoleOrigin ?? DEFAULT_OBSERVE_BASE_URL;
|
|
289
290
|
const dagRunId = str(p.dagRunId);
|
|
290
291
|
const taskId = str(p.taskId);
|
|
291
292
|
const featureId = str(p.featureId);
|
|
@@ -320,6 +321,33 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
320
321
|
const requiredCapability = target
|
|
321
322
|
? capabilityForObserveTarget(target)
|
|
322
323
|
: undefined;
|
|
324
|
+
const usesUnifiedSurface = Boolean(consoleOrigin) &&
|
|
325
|
+
(!ctx.observeBaseUrl ||
|
|
326
|
+
ctx.observeBaseUrl.replace(/\/+$/, "") ===
|
|
327
|
+
consoleOrigin?.replace(/\/+$/, ""));
|
|
328
|
+
if (usesUnifiedSurface) {
|
|
329
|
+
const inspectPath = target
|
|
330
|
+
? buildInspectDeepLink(consoleOrigin, target).slice(consoleOrigin.length)
|
|
331
|
+
: "/inspect/#/";
|
|
332
|
+
return {
|
|
333
|
+
kind: "sync",
|
|
334
|
+
status: 200,
|
|
335
|
+
body: operatorSucceeded("observeLink", {
|
|
336
|
+
url: target
|
|
337
|
+
? buildInspectDeepLink(consoleOrigin, target)
|
|
338
|
+
: `${consoleOrigin.replace(/\/+$/, "")}/inspect/#/`,
|
|
339
|
+
inspectPath,
|
|
340
|
+
status: "same-origin",
|
|
341
|
+
requiredCapability: requiredCapability ?? null,
|
|
342
|
+
dagRunId: target?.kind === "dag"
|
|
343
|
+
? target.dagRunId
|
|
344
|
+
: resolvedFromTask?.dagRunId,
|
|
345
|
+
taskId: taskId ?? undefined,
|
|
346
|
+
home: openHome || undefined,
|
|
347
|
+
fallbackToTask: fallbackToTask || undefined,
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
323
351
|
let localFingerprint;
|
|
324
352
|
try {
|
|
325
353
|
localFingerprint = repoFingerprintV1(ctx.repoRoot);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function segment(value) {
|
|
2
|
+
return encodeURIComponent(value);
|
|
3
|
+
}
|
|
4
|
+
function nonEmpty(value) {
|
|
5
|
+
return typeof value === "string" && value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function encodeInspectHash(selection) {
|
|
8
|
+
switch (selection.kind) {
|
|
9
|
+
case "feature":
|
|
10
|
+
return `#/feature/${segment(selection.featureId)}`;
|
|
11
|
+
case "feature-task":
|
|
12
|
+
return `#/feature/${segment(selection.featureId)}/task/${segment(selection.taskId)}`;
|
|
13
|
+
case "legacy-task":
|
|
14
|
+
return `#/task/${segment(selection.taskId)}`;
|
|
15
|
+
case "dag-run":
|
|
16
|
+
// Inspect currently owns DAG-level routing. nodeId remains URL-adjacent
|
|
17
|
+
// selection state until the static router explicitly supports it.
|
|
18
|
+
return `#/dag/${segment(selection.dagRunId)}`;
|
|
19
|
+
case "worker-run":
|
|
20
|
+
return `#/run/${segment(selection.workerRunId)}`;
|
|
21
|
+
case "batch":
|
|
22
|
+
return `#/batch/${segment(selection.batchRunId)}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function buildInspectPath(selection) {
|
|
26
|
+
return `/inspect/${selection ? encodeInspectHash(selection) : "#/"}`;
|
|
27
|
+
}
|
|
28
|
+
export function buildInspectUrl(origin, selection) {
|
|
29
|
+
const base = origin.trim().replace(/\/+$/, "");
|
|
30
|
+
if (!base)
|
|
31
|
+
throw new Error("buildInspectUrl: origin is required");
|
|
32
|
+
return `${base}${buildInspectPath(selection)}`;
|
|
33
|
+
}
|
|
34
|
+
function decodeSegment(value) {
|
|
35
|
+
if (!nonEmpty(value))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const decoded = decodeURIComponent(value);
|
|
39
|
+
return decoded ? decoded : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function decodeInspectHash(hash) {
|
|
46
|
+
const normalized = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
47
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
48
|
+
if (parts.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const [kind, first, third, fourth] = parts;
|
|
51
|
+
const id = decodeSegment(first);
|
|
52
|
+
if (!id)
|
|
53
|
+
return null;
|
|
54
|
+
if (kind === "feature" && parts.length === 2) {
|
|
55
|
+
return { kind: "feature", featureId: id };
|
|
56
|
+
}
|
|
57
|
+
if (kind === "feature" && parts.length === 4 && third === "task") {
|
|
58
|
+
const taskId = decodeSegment(fourth);
|
|
59
|
+
return taskId ? { kind: "feature-task", featureId: id, taskId } : null;
|
|
60
|
+
}
|
|
61
|
+
if (kind === "task" && parts.length === 2)
|
|
62
|
+
return { kind: "legacy-task", taskId: id };
|
|
63
|
+
if (kind === "dag" && parts.length === 2)
|
|
64
|
+
return { kind: "dag-run", dagRunId: id };
|
|
65
|
+
if (kind === "run" && parts.length === 2)
|
|
66
|
+
return { kind: "worker-run", workerRunId: id };
|
|
67
|
+
if (kind === "batch" && parts.length === 2)
|
|
68
|
+
return { kind: "batch", batchRunId: id };
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export function selectionFromObserveLinkTarget(target) {
|
|
72
|
+
switch (target.kind) {
|
|
73
|
+
case "dag":
|
|
74
|
+
return { kind: "dag-run", dagRunId: target.dagRunId };
|
|
75
|
+
case "task":
|
|
76
|
+
return { kind: "legacy-task", taskId: target.taskId };
|
|
77
|
+
case "feature":
|
|
78
|
+
return { kind: "feature", featureId: target.featureId };
|
|
79
|
+
case "worker":
|
|
80
|
+
return { kind: "worker-run", workerRunId: target.workerRunId };
|
|
81
|
+
case "batch":
|
|
82
|
+
return { kind: "batch", batchRunId: target.batchRunId };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Canonicalize only the historical Console root hash shape. */
|
|
86
|
+
export function normalizeLegacyConsoleDagHash(pathnameAndHash) {
|
|
87
|
+
const [pathname, hash = ""] = pathnameAndHash.split("#", 2);
|
|
88
|
+
if ((pathname === "/" || pathname === "") && /^\/dag\/[^/]+$/.test(hash)) {
|
|
89
|
+
return `/inspect/#${hash}`;
|
|
90
|
+
}
|
|
91
|
+
return pathnameAndHash;
|
|
92
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deriveObserveRouteCapabilities, resolveObservePackageIdentity, } from "../observe/health.js";
|
|
2
|
+
import { repoFingerprintV1 } from "./repo-fingerprint.js";
|
|
3
|
+
export function buildOperatorSurfaceHealthV1(input) {
|
|
4
|
+
const { packageName, packageVersion } = resolveObservePackageIdentity();
|
|
5
|
+
const snapshotAvailable = input.routes.some((route) => route.method === "GET" && route.pattern.test("/api/snapshot"));
|
|
6
|
+
const eventStreamAvailable = input.routes.some((route) => route.method === "GET" && route.pattern.test("/api/events/stream"));
|
|
7
|
+
return {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
ok: true,
|
|
10
|
+
product: "loop-operator-console",
|
|
11
|
+
repoFingerprint: repoFingerprintV1(input.repoRoot),
|
|
12
|
+
packageName,
|
|
13
|
+
packageVersion,
|
|
14
|
+
piReadiness: input.piReadiness,
|
|
15
|
+
inspect: {
|
|
16
|
+
ready: input.inspectReady,
|
|
17
|
+
routeCapabilities: deriveObserveRouteCapabilities(input.routes),
|
|
18
|
+
snapshotAvailable,
|
|
19
|
+
eventStreamAvailable,
|
|
20
|
+
},
|
|
21
|
+
generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -38,9 +38,9 @@ const CTA = {
|
|
|
38
38
|
},
|
|
39
39
|
observeLink: {
|
|
40
40
|
id: "observeLink",
|
|
41
|
-
label: "
|
|
41
|
+
label: "打开检视",
|
|
42
42
|
action: "observeLink",
|
|
43
|
-
commandHint: "agent-worker
|
|
43
|
+
commandHint: "agent-worker console serve --repo . --port 8790",
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
const MATRIX = {
|
|
@@ -3,7 +3,8 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { dispatchOperatorAction } from "./operator-actions.js";
|
|
5
5
|
import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
|
|
6
|
-
import {
|
|
6
|
+
import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
7
|
+
import { ROUTES } from "../observe/routes.js";
|
|
7
8
|
import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
8
9
|
export function sendJson(res, status, body, extraHeaders) {
|
|
9
10
|
const payload = JSON.stringify(body);
|
|
@@ -58,24 +59,33 @@ async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
|
|
|
58
59
|
throw new Error(`invalid JSON body: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
async function
|
|
62
|
-
let fingerprint;
|
|
62
|
+
export async function handleConsoleHealthRequest(_req, res, ctx) {
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
sendJson(res, 200, buildOperatorSurfaceHealthV1({
|
|
65
|
+
repoRoot: ctx.repoRoot,
|
|
66
|
+
piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
|
|
67
|
+
inspectReady: ctx.inspect.ready,
|
|
68
|
+
routes: ROUTES,
|
|
69
|
+
}));
|
|
65
70
|
}
|
|
66
71
|
catch {
|
|
67
|
-
|
|
72
|
+
sendJson(res, 200, {
|
|
73
|
+
schemaVersion: 1,
|
|
74
|
+
ok: true,
|
|
75
|
+
product: "loop-operator-console",
|
|
76
|
+
repoFingerprint: "unavailable",
|
|
77
|
+
packageName: "unknown",
|
|
78
|
+
packageVersion: "0.0.0",
|
|
79
|
+
piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
|
|
80
|
+
inspect: {
|
|
81
|
+
ready: ctx.inspect.ready,
|
|
82
|
+
routeCapabilities: ctx.inspect.routeCapabilities,
|
|
83
|
+
snapshotAvailable: true,
|
|
84
|
+
eventStreamAvailable: true,
|
|
85
|
+
},
|
|
86
|
+
generatedAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
68
88
|
}
|
|
69
|
-
const readiness = ctx.operator?.getReadiness();
|
|
70
|
-
sendJson(res, 200, {
|
|
71
|
-
ok: true,
|
|
72
|
-
product: "loop-operator-console",
|
|
73
|
-
productName: "Loop Operator Console",
|
|
74
|
-
repoRoot: ctx.repoRoot,
|
|
75
|
-
repoFingerprint: fingerprint,
|
|
76
|
-
piReadiness: readiness?.state ?? "setup-required",
|
|
77
|
-
generatedAt: new Date().toISOString(),
|
|
78
|
-
});
|
|
79
89
|
}
|
|
80
90
|
async function handleReadiness(_req, res, ctx) {
|
|
81
91
|
const report = ctx.operator?.getReadiness();
|
|
@@ -93,6 +103,7 @@ async function handleSessionPing(req, res, ctx) {
|
|
|
93
103
|
bootToken: ctx.bootToken.value,
|
|
94
104
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
95
105
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
106
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
96
107
|
});
|
|
97
108
|
if (failure) {
|
|
98
109
|
sendJson(res, failure.status, {
|
|
@@ -118,6 +129,7 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
118
129
|
bootToken: ctx.bootToken.value,
|
|
119
130
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
120
131
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
132
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
121
133
|
});
|
|
122
134
|
if (failure) {
|
|
123
135
|
sendJson(res, failure.status, {
|
|
@@ -304,13 +316,13 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
304
316
|
res.writeHead(200, headers);
|
|
305
317
|
res.end(content);
|
|
306
318
|
}
|
|
307
|
-
export async function
|
|
319
|
+
export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
308
320
|
const method = (req.method ?? "GET").toUpperCase();
|
|
309
321
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
310
322
|
const pathname = decodeURIComponent(url.pathname);
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
return
|
|
323
|
+
if (!pathname.startsWith("/api/operator/v1/") &&
|
|
324
|
+
!pathname.startsWith("/api/session/")) {
|
|
325
|
+
return false;
|
|
314
326
|
}
|
|
315
327
|
if (method === "GET" && pathname === "/api/operator/v1/readiness") {
|
|
316
328
|
await handleReadiness(req, res, ctx);
|
|
@@ -342,6 +354,7 @@ export async function handleConsoleRequest(req, res, ctx) {
|
|
|
342
354
|
bootToken: ctx.bootToken.value,
|
|
343
355
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
344
356
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
357
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
345
358
|
});
|
|
346
359
|
if (failure) {
|
|
347
360
|
sendJson(res, failure.status, {
|
|
@@ -360,6 +373,19 @@ export async function handleConsoleRequest(req, res, ctx) {
|
|
|
360
373
|
});
|
|
361
374
|
return true;
|
|
362
375
|
}
|
|
376
|
+
sendJson(res, 404, { error: "Not found" });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
export async function handleConsoleRequest(req, res, ctx) {
|
|
380
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
381
|
+
const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
|
|
382
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
383
|
+
await handleConsoleHealthRequest(req, res, ctx);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const handled = await handleConsoleOperatorRequest(req, res, ctx);
|
|
387
|
+
if (handled)
|
|
388
|
+
return true;
|
|
363
389
|
if (pathname.startsWith("/api/")) {
|
|
364
390
|
sendJson(res, 404, { error: "Not found" });
|
|
365
391
|
return true;
|