opencode-agora 0.1.0__py3-none-any.whl
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.
- agora_mcp.py +240 -0
- agorad/__init__.py +1 -0
- agorad/access/__init__.py +1 -0
- agorad/access/anchors.py +22 -0
- agorad/access/gitx.py +206 -0
- agorad/access/oc.py +124 -0
- agorad/access/pages.py +25 -0
- agorad/access/store.py +181 -0
- agorad/core/__init__.py +1 -0
- agorad/core/app.py +137 -0
- agorad/core/auth.py +134 -0
- agorad/core/gate.py +281 -0
- agorad/core/health.py +182 -0
- agorad/core/msg.py +92 -0
- agorad/core/peer.py +90 -0
- agorad/core/recover.py +83 -0
- agorad/core/report.py +189 -0
- agorad/core/state.py +183 -0
- agorad/core/task.py +308 -0
- agorad/installer.py +44 -0
- agorad/main.py +181 -0
- agorad/transport/__init__.py +1 -0
- agorad/transport/httpd.py +316 -0
- board_tui.py +649 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora +244 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/cred.ts +112 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/envPath.ts +33 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/hook.ts +132 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/index.ts +160 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/inject.ts +49 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/lift.ts +161 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/agora-plugin/scope.ts +36 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/install.sh +222 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/skills/agora-dispatch/SKILL.md +95 -0
- opencode_agora-0.1.0.data/data/share/opencode-agora/skills/agora-ops/SKILL.md +60 -0
- opencode_agora-0.1.0.dist-info/METADATA +195 -0
- opencode_agora-0.1.0.dist-info/RECORD +41 -0
- opencode_agora-0.1.0.dist-info/WHEEL +5 -0
- opencode_agora-0.1.0.dist-info/entry_points.txt +5 -0
- opencode_agora-0.1.0.dist-info/licenses/LICENSE +21 -0
- opencode_agora-0.1.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode v2.1.4 插件入口——T5 探针实证接线(/tmp/opencode/oc-probe.json):
|
|
3
|
+
* 稳定运行时=plain-object `{id, setup(ctx)}`(async-Hooks 形态静默失效,禁用);
|
|
4
|
+
* 钩子=ctx.session.hook("prompt", ev)(ev {sessionID, prompt:{text}, delivery})+
|
|
5
|
+
* ctx.tool.hook("execute.before", ev)(ev {tool, sessionID, input}——MCP 工具同名覆盖,
|
|
6
|
+
* tool 名="<server>_<tool>";改 ev.input 直达 MCP 服务端,已实测)。
|
|
7
|
+
* 会话 cwd=ctx.session.get({sessionID}).location.directory(异步取,失败回落 process.cwd())。
|
|
8
|
+
* 语义与 agorad 服务侧一致:豁免面 B3、读引导、主会话例外、服务不可达默认拒。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { getCredential, pullCredential, type PullResult } from "./cred";
|
|
13
|
+
import { onShellCreate, type ShellCreatePayload } from "./envPath";
|
|
14
|
+
import { onInject } from "./inject";
|
|
15
|
+
import {
|
|
16
|
+
AGORAD_TIMEOUT_MS, agoradUrl, ensureOnce, resetLift, LIFT_RECOVER, type LiftResult,
|
|
17
|
+
} from "./lift";
|
|
18
|
+
import { isDormant } from "./scope";
|
|
19
|
+
import { agoraHome, enforceRelevant, isWriteTool, NO_CRED_HINT, readishGated, shouldBlock } from "./hook";
|
|
20
|
+
|
|
21
|
+
// 稳定运行时 ctx 本地面类型(只注本插件消费面,不引 @opencode 包)
|
|
22
|
+
interface OcToolEvent {
|
|
23
|
+
tool: string;
|
|
24
|
+
sessionID: string;
|
|
25
|
+
input?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface OcPromptEvent {
|
|
28
|
+
sessionID: string;
|
|
29
|
+
prompt?: { text?: string };
|
|
30
|
+
}
|
|
31
|
+
export interface OcCtx {
|
|
32
|
+
tool: { hook(name: "execute.before", fn: (ev: OcToolEvent) => Promise<void>): void };
|
|
33
|
+
session: {
|
|
34
|
+
hook(name: "prompt", fn: (ev: OcPromptEvent) => Promise<void>): void;
|
|
35
|
+
get(args: { sessionID: string }): Promise<{ location?: { directory?: string } } | undefined>;
|
|
36
|
+
};
|
|
37
|
+
/** v2.2.0 起 shell 工具/终端 spawn 前钩子;旧运行时缺省(可选挂载)。 */
|
|
38
|
+
shell?: {
|
|
39
|
+
hook(name: "create.before", fn: (ev: ShellCreatePayload) => void | Promise<void>): void;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let enforceOverride: boolean | null = null; // 测试/特殊接线显式旗;生产置 null 走服务持旗拉取
|
|
44
|
+
|
|
45
|
+
export function setEnforce(on: boolean | null): void {
|
|
46
|
+
enforceOverride = on; // null=清除显式旗,恢复服务持旗拉取
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** root 牌接管文件(F1 唯一落盘点,0600);缺失/损坏返回 null(调用方按无牌走安全向)。 */
|
|
50
|
+
function readRootToken(): string | null {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(join(agoraHome(), ".agora", "root.token"), "utf8").trim() || null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 服务持旗拉取(设计 §5.1 核验面,lift/doctor 接线):lift 定端口→doctor 查询面读
|
|
59
|
+
* /health.enforce_on(root 牌接管文件授权);服务不可达/异常一律 true=默认拒(安全向)。 */
|
|
60
|
+
async function pullEnforce(): Promise<boolean> {
|
|
61
|
+
const lift = await ensureOnce();
|
|
62
|
+
if (!lift.ok) return true;
|
|
63
|
+
try {
|
|
64
|
+
const tok = readRootToken();
|
|
65
|
+
if (tok === null) return true;
|
|
66
|
+
const r = await fetch(agoradUrl(lift.port, "/health"), {
|
|
67
|
+
headers: { "X-Agora-Token": tok },
|
|
68
|
+
signal: AbortSignal.timeout(AGORAD_TIMEOUT_MS),
|
|
69
|
+
});
|
|
70
|
+
if (!r.ok) return true;
|
|
71
|
+
return ((await r.json()) as { enforce_on?: boolean }).enforce_on !== false;
|
|
72
|
+
} catch {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
id: "agora",
|
|
79
|
+
setup(ctx: OcCtx): void {
|
|
80
|
+
ctx.session.hook("prompt", async (event) => {
|
|
81
|
+
onInject({ sessionID: event.sessionID, text: event.prompt?.text ?? "" });
|
|
82
|
+
});
|
|
83
|
+
// F18 worker PATH 补全:shell spawn 前 payload.env 按引用直改(补 ~/.bun/bin、~/.local/bin);
|
|
84
|
+
// 旧运行时无 shell 命名空间→可选挂载静默跳过。
|
|
85
|
+
ctx.shell?.hook("create.before", (event) => onShellCreate(event));
|
|
86
|
+
ctx.tool.hook("execute.before", async (event) => {
|
|
87
|
+
// R29:休眠分支短路在 lift/pullEnforce/attach/readGuide 之前——休眠仓零 HTTP 零文件读(除名册缓存)。
|
|
88
|
+
const isAgora = /^agora_/.test(event.tool);
|
|
89
|
+
let cwd = process.cwd();
|
|
90
|
+
try {
|
|
91
|
+
cwd = (await ctx.session.get({ sessionID: event.sessionID }))?.location?.directory || cwd;
|
|
92
|
+
} catch {
|
|
93
|
+
// 会话读取失败按服务 cwd 兜底:envDir 含括判定走 deny 安全向
|
|
94
|
+
}
|
|
95
|
+
if (isDormant(cwd)) {
|
|
96
|
+
// F12 启用域:名册内休眠仓——普通工具直通;agora_* 直接拒(不 ensureOnce,零 HTTP)。
|
|
97
|
+
if (isAgora) throw new Error("此仓未启用 agora");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// 服务不可达默认拒:lift 失败静默,判定链按无凭据走引导;拒因携 resetLift 恢复指引。
|
|
101
|
+
// 旗按需拉取(T6 评审):预过滤先行——非引导类工具(MCP/grep 等)零开销直过。
|
|
102
|
+
// F27:agora_* 附牌面也需 lift 端口(attach 分流拉取),不论 enforce 旗。
|
|
103
|
+
const on = enforceOverride ?? (enforceRelevant(event.tool) ? await pullEnforce() : false);
|
|
104
|
+
let lift: LiftResult | null = null;
|
|
105
|
+
if (on || isAgora) lift = await ensureOnce();
|
|
106
|
+
if (isAgora && !lift.ok) {
|
|
107
|
+
// 冷启动(设计 §5.4):首个 agora_* 调用即拉起 agorad;败态即复位 memo——
|
|
108
|
+
// 下轮 edit 重探自愈(免重启恢复,兑现 lift.ts LIFT_RECOVER 承诺)。
|
|
109
|
+
resetLift();
|
|
110
|
+
}
|
|
111
|
+
const input = event.input ?? {};
|
|
112
|
+
const target =
|
|
113
|
+
(typeof input.filePath === "string" && input.filePath) ||
|
|
114
|
+
(typeof input.path === "string" && input.path) ||
|
|
115
|
+
"";
|
|
116
|
+
let cred = getCredential(event.sessionID);
|
|
117
|
+
let credSrc = cred ? "cache" : "miss";
|
|
118
|
+
let pull: PullResult | null = null;
|
|
119
|
+
// F27 拉取闭合三面:write/readish 判定面(enforce on 才消费凭据)+agora_* 附牌面
|
|
120
|
+
// (不论旗——附牌语义独立于判定旗)。节流按 sid,多面共用不重复拉。
|
|
121
|
+
if (!cred && lift?.ok && (isAgora || (on && (isWriteTool(event.tool) || readishGated(event.tool, target))))) {
|
|
122
|
+
pull = await pullCredential(event.sessionID, lift.port);
|
|
123
|
+
if (pull.cred) {
|
|
124
|
+
cred = pull.cred;
|
|
125
|
+
credSrc = "pulled";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 派发方增强①(2026-10-29):AGORA_HOOK_DEBUG=1 时 stderr 记判定链触发与凭据
|
|
129
|
+
// 来源——分诊「钩子没触发」vs「缓存空」两态(2.2.0 病灶);stderr 入服务日志不污染 TUI。
|
|
130
|
+
if (process.env.AGORA_HOOK_DEBUG) {
|
|
131
|
+
process.stderr.write(
|
|
132
|
+
`[agora-plugin] tool=${event.tool} sid=${event.sessionID} cred=${credSrc} pull=${pull?.reason ?? "-"} enforce=${on}\n`);
|
|
133
|
+
}
|
|
134
|
+
// 附牌主案(T5 实证②:改 ev.input 直达 MCP):仅 agora MCP 工具面附 _token。
|
|
135
|
+
// F27 attach 分流:缓存/拉取命中的 worker 牌优先;拉取 404=非 worker 会话→维持
|
|
136
|
+
// root 牌接管文件(主会话合法路径不动);节流/不可达/lift 败态→不附牌——绝不落
|
|
137
|
+
// root(堵破口:worker 冷缓存首调不得以 root 身份行动,task_cancel/enforce 级)。
|
|
138
|
+
// _sid 携调用者真实会话 id(R25-A:服务登记主会话绑定,拍人注入真会话)。
|
|
139
|
+
if (isAgora) {
|
|
140
|
+
const token = cred?.token ?? (pull?.reason === "not_worker" ? readRootToken() : null);
|
|
141
|
+
if (token) {
|
|
142
|
+
input._token = token;
|
|
143
|
+
input._sid = event.sessionID;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let hint = shouldBlock(event.tool, target, cwd, cred, on, pull?.reason ?? null);
|
|
147
|
+
if (hint) {
|
|
148
|
+
// F27 拒因分诊(信息量增量,语义不变):write 面 NO_CRED_HINT 剩不可达/节流两态
|
|
149
|
+
// ——404(not_worker)已由裁定 A 镜像路由进 DISPATCH_HINT,真非 worker 附文退役。
|
|
150
|
+
if (hint === NO_CRED_HINT && pull && pull.reason !== "ok") {
|
|
151
|
+
const why = pull.reason === "throttled"
|
|
152
|
+
? "拉取节流中(30s 窗,稍候重试)"
|
|
153
|
+
: "agorad 拉取不可达/异常(安全向拒)";
|
|
154
|
+
hint = `${hint}(${why})`;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(lift && !lift.ok ? `${hint}(${LIFT_RECOVER};拒因:${lift.reason})` : hint);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inject 事件处理(F4):session.prompt 钩子(T5 实证形状:event {sessionID, messageID,
|
|
3
|
+
* prompt:{text}, delivery})→BOOT 环境位登记+credential 消息提取→cred 缓存填充。
|
|
4
|
+
* 消息文本形状与 agorad 侧对齐(task.py dispatch 序):BOOT 先行("env: {env}" 登记环境位)、
|
|
5
|
+
* credential 后至("[agora] credential {tid}\ntoken: {token}",envDir 取缓存既有值合流)。
|
|
6
|
+
* env_dir 来源按最小:沿用缓存既有 envDir(重派换牌不丢环境位),缺省 undefined→hook 走 deny 安全向。
|
|
7
|
+
* 牌只驻内存缓存(T5 实证:v2.1.4 会话无插件可见落盘目录);进程重启丢牌由服务侧 recover
|
|
8
|
+
* reinject 重发兜底。
|
|
9
|
+
*/
|
|
10
|
+
import { cacheCredential, getCredential, type Cred } from "./cred";
|
|
11
|
+
|
|
12
|
+
export interface OcInject {
|
|
13
|
+
sessionID: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CRED_MSG = /\[agora\] credential (\S+)\ntoken: (\S+)/;
|
|
18
|
+
const BOOT_MSG = /\[agora\] (gt-\S+|task-\S+|ag-\S+): /;
|
|
19
|
+
const BOOT_ENV = /^env: (\S+)$/m;
|
|
20
|
+
|
|
21
|
+
export function onInject(msg: OcInject): void {
|
|
22
|
+
const boot = BOOT_MSG.exec(msg.text);
|
|
23
|
+
if (boot) {
|
|
24
|
+
// BOOT 先行(dispatch 序:create 携 boot prompt→credential inject):登记任务环境位,
|
|
25
|
+
// credential 随后合流补 token——缺此步 worker 永远无 envDir=hook 恒 deny 安全向。
|
|
26
|
+
const env = BOOT_ENV.exec(msg.text);
|
|
27
|
+
if (env) {
|
|
28
|
+
cacheCredential({
|
|
29
|
+
token: getCredential(msg.sessionID)?.token ?? "",
|
|
30
|
+
sid: msg.sessionID,
|
|
31
|
+
role: "worker",
|
|
32
|
+
tid: boot[1],
|
|
33
|
+
envDir: env[1],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const m = CRED_MSG.exec(msg.text);
|
|
39
|
+
if (!m) return;
|
|
40
|
+
const [, tid, token] = m;
|
|
41
|
+
const cred: Cred = {
|
|
42
|
+
token,
|
|
43
|
+
sid: msg.sessionID,
|
|
44
|
+
role: "worker",
|
|
45
|
+
tid,
|
|
46
|
+
envDir: getCredential(msg.sessionID)?.envDir,
|
|
47
|
+
};
|
|
48
|
+
cacheCredential(cred);
|
|
49
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lift 服务拉起(F4/设计 §4.8):探测 $AGORA_HOME/.agora/service.json→任意 HTTP 应答(含 401)
|
|
3
|
+
=就绪;未运行则 detached 拉起 agorad(脱会话新会话,F27;env 带 AGORA_HOME+AGORA_OC_URL[+AGORA_OC_PASSWORD])
|
|
4
|
+
轮询至 10s 上限。超时拒因两态:没装(main.py 缺)vs 拉不起(起而不就绪/OC 发现落空)。
|
|
5
|
+
* T5 实证接线:AGORA_OC_URL 发现源=$XDG_STATE_HOME/opencode/service.json(serve --service 写
|
|
6
|
+
{url,password,pid,version});API 需 Basic opencode:<password>(oc.py 消费,agorad 只透传)。
|
|
7
|
+
* script 解析序:opts.script → env AGORAD_SCRIPT → $AGORA_HOME/.agora/agorad/main.py(install
|
|
8
|
+
载荷)→ 与插件同仓(dev 布局)——插件独立安装场景不再假定同仓。
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { agoraHome } from "./hook";
|
|
14
|
+
|
|
15
|
+
export class LiftTimeout extends Error {}
|
|
16
|
+
|
|
17
|
+
export interface LiftOpts {
|
|
18
|
+
home?: string;
|
|
19
|
+
script?: string;
|
|
20
|
+
stateHome?: string; // T5 发现源根(测试注入隔离)
|
|
21
|
+
timeoutMs?: number; // 真值 10s;测试注入短化
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SCRIPT = join(import.meta.dir, "..", "agorad", "main.py");
|
|
26
|
+
/** proc 只消费 kill:类型收窄免引 Bun 命名空间(测试桩同形)。 */
|
|
27
|
+
interface Killable {
|
|
28
|
+
kill(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readPort(home: string): number | null {
|
|
32
|
+
try {
|
|
33
|
+
const sj = JSON.parse(readFileSync(join(home, ".agora", "service.json"), "utf8")) as {
|
|
34
|
+
port?: unknown;
|
|
35
|
+
};
|
|
36
|
+
return typeof sj.port === "number" ? sj.port : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 任意 HTTP 应答(含 401)=就绪;连接拒/超时=未起。 */
|
|
43
|
+
async function probe(port: number): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(1000) });
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** opencode 服务发现(T5):读 $XDG_STATE_HOME/opencode/service.json → {url,password}。 */
|
|
53
|
+
export function discoverOc(
|
|
54
|
+
stateHome = process.env.XDG_STATE_HOME || join(homedir(), ".local/state"),
|
|
55
|
+
): { url: string; password?: string } | null {
|
|
56
|
+
try {
|
|
57
|
+
const sj = JSON.parse(readFileSync(join(stateHome, "opencode", "service.json"), "utf8")) as {
|
|
58
|
+
url?: unknown;
|
|
59
|
+
password?: unknown;
|
|
60
|
+
};
|
|
61
|
+
return typeof sj.url === "string" && sj.url
|
|
62
|
+
? { url: sj.url, password: typeof sj.password === "string" ? sj.password : undefined }
|
|
63
|
+
: null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveScript(opts: LiftOpts, home: string): string {
|
|
70
|
+
return (
|
|
71
|
+
opts.script ||
|
|
72
|
+
process.env.AGORAD_SCRIPT ||
|
|
73
|
+
join(home, ".agora", "agorad", "main.py") || // install.sh 载荷位(独立安装)
|
|
74
|
+
DEFAULT_SCRIPT // dev 同仓布局兜底
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function ensureService(
|
|
79
|
+
opts: LiftOpts = {},
|
|
80
|
+
): Promise<{ port: number; proc?: Killable }> {
|
|
81
|
+
const { home = agoraHome(), timeoutMs = 10_000, intervalMs = 100 } = opts;
|
|
82
|
+
const script = resolveScript(opts, home);
|
|
83
|
+
let port = readPort(home);
|
|
84
|
+
if (port !== null && (await probe(port))) return { port };
|
|
85
|
+
if (!existsSync(script)) throw new LiftTimeout(`agorad 未安装:找不到 ${script}`);
|
|
86
|
+
let ocUrl = process.env.AGORA_OC_URL;
|
|
87
|
+
let ocPassword = process.env.AGORA_OC_PASSWORD;
|
|
88
|
+
if (!ocUrl) {
|
|
89
|
+
const found = discoverOc(opts.stateHome);
|
|
90
|
+
if (!found) {
|
|
91
|
+
throw new LiftTimeout(
|
|
92
|
+
"agorad 拉起失败:AGORA_OC_URL 未设置且服务发现落空($XDG_STATE_HOME/opencode/service.json 不存在——opencode serve --service 在运行吗)",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
ocUrl = found.url;
|
|
96
|
+
ocPassword ??= found.password; // T5 修正:发现的 password 必须随 env 透传(缺它=agorad 全 401)
|
|
97
|
+
}
|
|
98
|
+
const proc = Bun.spawn(["python3", script], {
|
|
99
|
+
detached: true, // 脱会话=原 setsid 包装等价(POSIX 新会话/Windows 新进程组),macOS 无 setsid 亦成立(Req: F27)
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
AGORA_HOME: home,
|
|
103
|
+
AGORA_OC_URL: ocUrl,
|
|
104
|
+
...(ocPassword ? { AGORA_OC_PASSWORD: ocPassword } : {}),
|
|
105
|
+
},
|
|
106
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
107
|
+
});
|
|
108
|
+
const deadline = Date.now() + timeoutMs;
|
|
109
|
+
for (;;) {
|
|
110
|
+
port = readPort(home);
|
|
111
|
+
if (port !== null && (await probe(port))) return { port, proc };
|
|
112
|
+
if (Date.now() >= deadline) {
|
|
113
|
+
proc.kill(); // 杀残留:不留给守护孤儿(起而不写/起不来)
|
|
114
|
+
throw new LiftTimeout(
|
|
115
|
+
`agorad 拉起失败:${Math.round(timeoutMs / 1000)}s 内未就绪(脚本 ${script})`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
await Bun.sleep(intervalMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type LiftResult = { ok: true; port: number } | { ok: false; reason: string };
|
|
123
|
+
|
|
124
|
+
let memo: Promise<LiftResult> | null = null;
|
|
125
|
+
|
|
126
|
+
function fresh(): Promise<LiftResult> {
|
|
127
|
+
return ensureService()
|
|
128
|
+
.then((r): LiftResult => ({ ok: true, port: r.port }))
|
|
129
|
+
.catch((e: unknown): LiftResult => ({ ok: false, reason: e instanceof Error ? e.message : String(e) }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 懒调 memo 版(失败同样 memo:每进程至多一跑,防每次工具调用重吃轮询);resetLift 后重试。
|
|
133
|
+
* 成功 memo 每次轻校验(readPort+probe ~1ms):agorad 重启换口/死亡即自愈——首见者换血 memo 并重拉,
|
|
134
|
+
* 并发后来者共享在飞自愈(单飞不重 spawn;resetLift 竞态回填,结果成败照常 memo)。
|
|
135
|
+
* service.json 瞬态读失(半写/暂缺)不认陈旧:回探 memo 口,活则守 memo(防误判漂移重拉出双守护)。
|
|
136
|
+
* (事故 2026-10-29:agorad 换随机端口后 memo 指死口,全会话 401 直至重启 serve。) */
|
|
137
|
+
export function ensureOnce(): Promise<LiftResult> {
|
|
138
|
+
memo ??= fresh();
|
|
139
|
+
const cached = memo;
|
|
140
|
+
return cached.then(async (r) => {
|
|
141
|
+
if (!r.ok) return r;
|
|
142
|
+
const port = readPort(agoraHome());
|
|
143
|
+
if ((port === null || port === r.port) && (await probe(port ?? r.port))) return r;
|
|
144
|
+
if (memo !== cached && memo !== null) return memo; // 他人已换血:共享在飞自愈
|
|
145
|
+
memo = fresh(); // 首见者发起自愈(或 resetLift 竞态回填);自愈经 ensureService 自身 probe 恰一次,不递归
|
|
146
|
+
return memo;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 复位失败 memo:用户救活 agorad 后恢复本会话(T4 doctor 接线)。 */
|
|
151
|
+
export function resetLift(): void {
|
|
152
|
+
memo = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const LIFT_RECOVER = "agorad 不可达已 memo(每进程一次):救活服务后经 doctor resetLift 恢复,无需重启会话";
|
|
156
|
+
|
|
157
|
+
/** agorad 回环面共用寻址+超时(拉旗 pullEnforce/拉凭据 pullCredential 同源防漂移)。 */
|
|
158
|
+
export const AGORAD_TIMEOUT_MS = 2000;
|
|
159
|
+
export function agoradUrl(port: number, path: string): string {
|
|
160
|
+
return `http://127.0.0.1:${port}${path}`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scope 启用域(F12,R26/R29):中央名册 $AGORA_HOME/.agora/inactive-repos 按仓休眠。
|
|
3
|
+
* 名册=仓根绝对路径一行一个(尾斜杠容错,空白/相对路径坏行跳过,缺失=全员活跃);
|
|
4
|
+
* 每次调用重读名册(热载,即改即生效——去缓存后行为);isDormant 精确路径匹配。
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { agoraHome } from "./hook";
|
|
9
|
+
|
|
10
|
+
let cache: string[] | null = null; // 测试兼容(resetScope 清缓存钩子);生产已去缓存
|
|
11
|
+
|
|
12
|
+
function norm(p: string): string {
|
|
13
|
+
return p.replace(/\/+$/, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 读名册:缺失/不可读=空名册(全员活跃,安全向=现行行为)。每次调用重读(R26 修订:去进程缓存,名册改动即生效)。 */
|
|
17
|
+
export function loadInactiveRepos(): string[] {
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(join(agoraHome(), ".agora", "inactive-repos"), "utf8")
|
|
20
|
+
.split("\n")
|
|
21
|
+
.map((l) => norm(l.trim()))
|
|
22
|
+
.filter((l) => l.startsWith("/"));
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 会话仓根是否休眠:对启动时缓存精确匹配(worktree toplevel≠主检出路径→不受名册影响)。 */
|
|
29
|
+
export function isDormant(repoRoot: string): boolean {
|
|
30
|
+
return loadInactiveRepos().includes(norm(repoRoot));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 新会话重置(R26 不热载:名册变更经重启会话生效;亦供测试隔离)。 */
|
|
34
|
+
export function resetScope(): void {
|
|
35
|
+
cache = null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agora 三部件装配+root 牌接管(F1,T4)。幂等:连跑两遍落盘结果不变。
|
|
3
|
+
# 流程:依赖检查→骨架(含 F12 名册)→MCP 注册(合并不覆盖)→插件/skills 复制→root 牌接管→持牌冒烟→停。
|
|
4
|
+
# 更新路径:git pull 后重跑本脚本——插件(~/.config/opencode/plugins/agora)与 agorad
|
|
5
|
+
# 载荷($AGORA_HOME/.agora/agorad)是拷贝不是引用,拉新代码不重跑不生效。
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
REPO="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
if [ -z "${AGORA_HOME:-}" ] && [ -s "$HOME/.agora-home" ]; then # 复跑保既有装(评审环):env > 指针 > 缺省
|
|
10
|
+
AGORA_HOME="$(python3 -c 'import sys
|
|
11
|
+
try:
|
|
12
|
+
print(open(sys.argv[1], encoding="utf-8").read().split("\n", 1)[0].strip())
|
|
13
|
+
except (OSError, ValueError):
|
|
14
|
+
pass' "$HOME/.agora-home" 2>/dev/null || true)" # 坏指针严格解码失败→空→缺省(读全量取首行,与 pointer_home 同契约,评审环)
|
|
15
|
+
fi
|
|
16
|
+
AGORA_HOME="${AGORA_HOME:-$HOME/plaza}" # 与 agorad env_config 同缺省(设计 §3)
|
|
17
|
+
# F31 pkg 分发:AGORA_PKG_ROOT 在位=wheel 数据面(plugin/skills/install.sh/agora 拷贝源,
|
|
18
|
+
# 由 agora-install 注入);SRC 统一两模式拷贝源(repo 模式即本仓)
|
|
19
|
+
PKG="${AGORA_PKG_ROOT:-}"
|
|
20
|
+
SRC="${PKG:-$REPO}"
|
|
21
|
+
printf '%s\n' "$AGORA_HOME" > "$HOME/.agora-home" # gt-32c051 指针:无 env 组件统一发现(MCP 壳启动亦写)
|
|
22
|
+
STATE_OC="$HOME/.local/state/opencode/service.json" # T5 实证:serve --service 写此处 {url,password}
|
|
23
|
+
OC_URL="${AGORA_OC_URL:-}"
|
|
24
|
+
OC_PASSWORD="${AGORA_OC_PASSWORD:-}"
|
|
25
|
+
if [ -z "$OC_URL" ] && [ -r "$STATE_OC" ]; then # T5 服务发现回填;读不到回落占位(oc 不可达不挡服务)
|
|
26
|
+
OC_URL="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("url",""))' "$STATE_OC" 2>/dev/null || true)"
|
|
27
|
+
OC_PASSWORD="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("password",""))' "$STATE_OC" 2>/dev/null || true)"
|
|
28
|
+
fi
|
|
29
|
+
OC_URL="${OC_URL:-http://127.0.0.1:4096}" # 占位:仅当 service.json 缺失
|
|
30
|
+
OC_DIR="$HOME/.config/opencode"
|
|
31
|
+
PLUGIN_DIR="$OC_DIR/plugins/agora"
|
|
32
|
+
SERVICE_JSON="$AGORA_HOME/.agora/service.json"
|
|
33
|
+
ROOT_TOKEN="$AGORA_HOME/.agora/root.token"
|
|
34
|
+
AD_LOG="$AGORA_HOME/.agora/install-agorad.log" # 拉起稳定日志唯一权威路径(step6 与 cleanup 扫尾同源,防单侧改名漂移)
|
|
35
|
+
AD_PID=""; LOG=""
|
|
36
|
+
|
|
37
|
+
fail() { echo "install: $*" >&2; exit 1; }
|
|
38
|
+
cleanup() {
|
|
39
|
+
[ -n "$AD_PID" ] && { kill "$AD_PID" 2>/dev/null || true; wait "$AD_PID" 2>/dev/null || true; }
|
|
40
|
+
# 取牌即抹(GNU/BSD 通吃 -i.bak;换 inode,先抹=丢 fd 后续留痕)。连同 .1 一起抹:
|
|
41
|
+
# 前轮被信号打断(无 EXIT trap)时牌行滞留旧日志,本轮 mv 成 .1 后会长驻留档
|
|
42
|
+
[ -n "$LOG" ] && { for f in "$LOG" "$LOG.1"; do
|
|
43
|
+
sed -i.bak '/^AGORA_ROOT_TOKEN: /d' "$f" 2>/dev/null && rm -f "$f.bak"; done; }
|
|
44
|
+
# 残牌无条件扫尾(键 AGORA_HOME——复用实例轮 LOG 空亦压着前轮被杀 cleanup 的残留):
|
|
45
|
+
# .bak 全清;.1 通常不被活实例持 fd(例外:前轮信号打断的孤儿实例在 alive 探测
|
|
46
|
+
# 0.5s 窗内恰不可达时仍持 .1 fd——换 inode 会吞其尾部留痕,牌行已不可经路径触达,
|
|
47
|
+
# 无暴露面,属接受面);活 .log 本体不抹(sed -i 换 inode 会吞活实例后续留痕,
|
|
48
|
+
# gt-e38e41)——活轮残牌属接受面,下一轮启动路径必抹
|
|
49
|
+
[ -n "$AGORA_HOME" ] && {
|
|
50
|
+
sed -i.bak '/^AGORA_ROOT_TOKEN: /d' "$AD_LOG.1" 2>/dev/null || true
|
|
51
|
+
rm -f "$AD_LOG.bak" "$AD_LOG.1.bak"; }
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
trap cleanup EXIT
|
|
55
|
+
|
|
56
|
+
# ---- 1) 依赖检查 ----
|
|
57
|
+
missing=""
|
|
58
|
+
for c in python3 git opencode bun; do # setsid 软依赖(F27):macOS 原生无,缺位走拉起处 nohup 兜底
|
|
59
|
+
command -v "$c" >/dev/null 2>&1 || missing="$missing $c"
|
|
60
|
+
done
|
|
61
|
+
[ -z "$missing" ] || fail "缺依赖:$missing —— 安装后重试"
|
|
62
|
+
|
|
63
|
+
# ---- 2) 骨架 ----
|
|
64
|
+
mkdir -p "$AGORA_HOME/state/messages" "$AGORA_HOME/wiki" "$AGORA_HOME/.agora"
|
|
65
|
+
# F12 名册空骨架:已有不覆盖(用户自录条目不可破坏);0644 owner 可追加
|
|
66
|
+
[ -e "$AGORA_HOME/.agora/inactive-repos" ] || (umask 022; : > "$AGORA_HOME/.agora/inactive-repos")
|
|
67
|
+
|
|
68
|
+
# ---- 3) MCP 注册(mcp 段合并不覆盖;注册形状 T5 对账)----
|
|
69
|
+
# F31:pkg 模式命令走 PATH 入口 agora-mcp;repo 模式仍指本仓壳(命令 JSON bash 侧构造作 heredoc 第 4 参)
|
|
70
|
+
mkdir -p "$OC_DIR"
|
|
71
|
+
if [ -n "$PKG" ]; then
|
|
72
|
+
MCP_CMD='["agora-mcp"]'
|
|
73
|
+
else
|
|
74
|
+
MCP_CMD="$(python3 -c 'import json,sys;print(json.dumps([sys.argv[1],sys.argv[2]]))' python3 "$REPO/agora_mcp.py")"
|
|
75
|
+
fi
|
|
76
|
+
python3 - "$OC_DIR/opencode.json" "$REPO/agora_mcp.py" "$AGORA_HOME" "$MCP_CMD" <<'PY'
|
|
77
|
+
import json, sys
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
p, mcp_py, home = (Path(a) for a in sys.argv[1:4])
|
|
80
|
+
cfg = {}
|
|
81
|
+
if p.exists():
|
|
82
|
+
try:
|
|
83
|
+
cfg = json.loads(p.read_text(encoding="utf-8"))
|
|
84
|
+
except ValueError:
|
|
85
|
+
sys.exit(f"install: {p} 非法 JSON——拒改,请手修后重试")
|
|
86
|
+
if not isinstance(cfg, dict):
|
|
87
|
+
sys.exit(f"install: {p} 顶层须为 JSON 对象——拒改")
|
|
88
|
+
cfg.setdefault("mcp", {})["agora"] = { # 重跑同值覆写=幂等;他 server 原样保留
|
|
89
|
+
"type": "local",
|
|
90
|
+
"command": json.loads(sys.argv[4]),
|
|
91
|
+
"environment": {"AGORA_HOME": str(home)},
|
|
92
|
+
}
|
|
93
|
+
p.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
94
|
+
PY
|
|
95
|
+
|
|
96
|
+
# ---- 4) 插件复制(opencode 只扫插件目录;整目录复制保相对导入;测试件不随装)----
|
|
97
|
+
mkdir -p "$PLUGIN_DIR"
|
|
98
|
+
for f in "$SRC"/agora-plugin/*.ts; do
|
|
99
|
+
case "$f" in *.test.ts) continue ;; esac
|
|
100
|
+
cp -f "$f" "$PLUGIN_DIR"/
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
# ---- 4a) skills 复制(主会话编排剧本;纯 md 整目录拷,同值覆写=幂等)----
|
|
104
|
+
mkdir -p "$OC_DIR/skills"
|
|
105
|
+
for d in "$SRC"/skills/*/; do
|
|
106
|
+
cp -rf "${d%/}" "$OC_DIR/skills/"
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
# ---- 4b) agorad 载荷随装(T5 追加项:lift 不再假定与插件同仓——独立安装场景从
|
|
110
|
+
# $AGORA_HOME/.agora/agorad/main.py 解析;env AGORAD_SCRIPT 可覆写)。
|
|
111
|
+
# 递归拷贝含 access/core/transport 子包(漏子包=ModuleNotFoundError,lift 必超时)----
|
|
112
|
+
mkdir -p "$AGORA_HOME/.agora" "$HOME/.local/bin"
|
|
113
|
+
if [ -n "$PKG" ]; then
|
|
114
|
+
# F31 pkg 模式:载荷源=agora-install 注入的 site-packages agorad 包;
|
|
115
|
+
# board_tui 载荷+chmod+agora-board 符号链接三行跳过(console script 替代)
|
|
116
|
+
# 收敛 repo→pkg 迁移残留:旧载荷删之防混版;符号链接只清指向本仓载荷位的残留——
|
|
117
|
+
# pipx 活链接同在 ~/.local/bin(默认 bin 目录),无差别 rm 即自毁 console script(round-2 B 册)
|
|
118
|
+
rm -f "$AGORA_HOME/.agora/board_tui.py"
|
|
119
|
+
if [ -L "$HOME/.local/bin/agora-board" ] && [ "$(readlink "$HOME/.local/bin/agora-board")" = "$AGORA_HOME/.agora/board_tui.py" ]; then
|
|
120
|
+
rm -f "$HOME/.local/bin/agora-board"
|
|
121
|
+
fi
|
|
122
|
+
AD_SRC="${AGORA_AGORAD_SRC:?pkg 模式须由 agora-install 注入 AGORA_AGORAD_SRC}"
|
|
123
|
+
rm -rf "$AGORA_HOME/.agora/agorad"
|
|
124
|
+
cp -r "$AD_SRC" "$AGORA_HOME/.agora/"
|
|
125
|
+
else
|
|
126
|
+
rm -rf "$AGORA_HOME/.agora/agorad"
|
|
127
|
+
cp -r "$REPO/agorad" "$AGORA_HOME/.agora/"
|
|
128
|
+
cp -f "$REPO/board_tui.py" "$AGORA_HOME/.agora/"
|
|
129
|
+
chmod +x "$AGORA_HOME/.agora/board_tui.py"
|
|
130
|
+
ln -sf "$AGORA_HOME/.agora/board_tui.py" "$HOME/.local/bin/agora-board"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ---- 4c) agora CLI(F14:wait 阻塞谓词;bin 目录 4b 已建)----
|
|
134
|
+
cp -f "$SRC/agora" "$HOME/.local/bin/agora"
|
|
135
|
+
chmod +x "$HOME/.local/bin/agora"
|
|
136
|
+
find "$AGORA_HOME/.agora/agorad" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
137
|
+
|
|
138
|
+
# ---- 5) AGORA_OC_URL 发现结果落档(lift 运行时自发现同源;此处只留档不作为消费面)----
|
|
139
|
+
cat > "$AGORA_HOME/.agora/env" <<EOF
|
|
140
|
+
# opencode 服务发现(T5 实证:\$XDG_STATE_HOME/opencode/service.json)
|
|
141
|
+
# AGORA_OC_URL=$OC_URL
|
|
142
|
+
EOF
|
|
143
|
+
|
|
144
|
+
# ---- 6) root 牌接管 ----
|
|
145
|
+
agorad_alive() { # service.json port 可连=在运行(任意应答含 401 同义)
|
|
146
|
+
python3 - "$SERVICE_JSON" <<'PY'
|
|
147
|
+
import json, socket, sys
|
|
148
|
+
try:
|
|
149
|
+
port = json.load(open(sys.argv[1]))["port"]
|
|
150
|
+
socket.create_connection(("127.0.0.1", int(port)), timeout=0.5).close()
|
|
151
|
+
except Exception:
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
PY
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if agorad_alive; then
|
|
157
|
+
echo "install: agorad 已在运行——复用既有实例与 root.token"
|
|
158
|
+
else
|
|
159
|
+
LOG="$AD_LOG" # 稳定日志(gt-e38e41):不随 install 清理,拉起/死因可后验
|
|
160
|
+
[ -f "$LOG" ] && mv -f "$LOG" "$LOG.1" # 上一轮留档一轮
|
|
161
|
+
(umask 077; : > "$LOG") # 0600:日志含 root 牌行,不得按 umask 0644 落盘
|
|
162
|
+
# 脱会话(gt-71b6af):setsid 在位原位 exec($! 即 daemon pid);macOS 无 setsid → nohup
|
|
163
|
+
# 兜底(F27:$! 同为 python pid,cleanup/第 8 步击杀语义两形态一致;SIGHUP 由 main.py handler 留痕退出)。
|
|
164
|
+
# 凭据经子壳 export 进 environ——不落 argv(env 变参形会随 /proc/<pid>/cmdline 对全体本地用户可读)
|
|
165
|
+
(
|
|
166
|
+
export AGORA_HOME="$AGORA_HOME" AGORA_OC_URL="$OC_URL"
|
|
167
|
+
if [ -n "$OC_PASSWORD" ]; then export AGORA_OC_PASSWORD="$OC_PASSWORD"; fi
|
|
168
|
+
if [ -n "$PKG" ]; then # F31 pkg 模式:agorad 走 PATH console script
|
|
169
|
+
if command -v setsid >/dev/null 2>&1; then exec setsid nohup agorad; fi
|
|
170
|
+
exec nohup agorad
|
|
171
|
+
fi
|
|
172
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
173
|
+
exec setsid nohup python3 "$REPO/agorad/main.py"
|
|
174
|
+
fi
|
|
175
|
+
exec nohup python3 "$REPO/agorad/main.py"
|
|
176
|
+
) >"$LOG" 2>&1 &
|
|
177
|
+
AD_PID=$!
|
|
178
|
+
ready=""
|
|
179
|
+
for _ in {1..100}; do # 10s 上限:等就绪行(root 牌行在其前 flush)
|
|
180
|
+
if grep -q "^agorad ready" "$LOG" 2>/dev/null; then ready=1; break; fi
|
|
181
|
+
kill -0 "$AD_PID" 2>/dev/null || break
|
|
182
|
+
sleep 0.1
|
|
183
|
+
done
|
|
184
|
+
# 失败转储抹牌行:token 行先于 ready 行落日志(main.py),起而不就绪的转储
|
|
185
|
+
# 不得把 root 牌泼进终端/agent 转录(流式过滤,无 -i,GNU/BSD 同形)
|
|
186
|
+
[ -n "$ready" ] || { sed '/^AGORA_ROOT_TOKEN: /d' "$LOG" >&2; fail "agorad 启动失败(10s 未就绪,见上方输出)"; }
|
|
187
|
+
tok="$(awk '/^AGORA_ROOT_TOKEN: /{print $2; exit}' "$LOG")"
|
|
188
|
+
if [ -n "$tok" ]; then
|
|
189
|
+
(umask 077; printf '%s\n' "$tok" > "$ROOT_TOKEN") # 0600(F1 root 牌唯一落盘点)
|
|
190
|
+
elif [ ! -s "$ROOT_TOKEN" ]; then
|
|
191
|
+
fail "未捕获 root 牌且无既有 root.token——异常态,请清 $AGORA_HOME/tokens 后重装"
|
|
192
|
+
fi # else: 复装复用既有 root.token(牌持久化,重启不换)
|
|
193
|
+
fi
|
|
194
|
+
[ -s "$ROOT_TOKEN" ] || fail "root.token 缺失(运行中实例无牌可接管)"
|
|
195
|
+
|
|
196
|
+
# ---- 7) 冒烟:持 root 牌 /health 只读可达(不碰 enforce——复用既有实例的更新场景
|
|
197
|
+
# 若做 off→on 写冒烟,会在真实例上开毫秒级 enforce OFF 窗;enforce 面由插件/e2e 覆盖)----
|
|
198
|
+
python3 - "$SERVICE_JSON" "$ROOT_TOKEN" <<'PY'
|
|
199
|
+
import json, sys, urllib.request
|
|
200
|
+
port = json.load(open(sys.argv[1]))["port"]
|
|
201
|
+
token = open(sys.argv[2]).read().strip()
|
|
202
|
+
req = urllib.request.Request(
|
|
203
|
+
f"http://127.0.0.1:{port}/health",
|
|
204
|
+
headers={"X-Agora-Token": token})
|
|
205
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
206
|
+
d = json.loads(r.read() or b"{}")
|
|
207
|
+
assert d.get("ok"), f"冒烟失败: {d}"
|
|
208
|
+
PY
|
|
209
|
+
|
|
210
|
+
# ---- 8) 停(只停本脚本拉起的实例)----
|
|
211
|
+
if [ -n "$AD_PID" ]; then
|
|
212
|
+
kill "$AD_PID" 2>/dev/null || true
|
|
213
|
+
wait "$AD_PID" 2>/dev/null || true
|
|
214
|
+
AD_PID=""
|
|
215
|
+
echo "install: 冒烟毕已停本脚本拉起的实例(装后服务由插件 lift 按需拉起)——拉起日志 $LOG"
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
echo "install: 完成($AGORA_HOME)"
|
|
219
|
+
[ -n "$PKG" ] && echo "install: 更新=pipx upgrade opencode-agora 后重跑 agora-install" # pkg 用户无 git 仓,git pull 指引不适用
|
|
220
|
+
echo "install: 启用域名册 $AGORA_HOME/.agora/inactive-repos(空=全启用;仓根路径一行一个,新会话生效)"
|
|
221
|
+
echo "install: 更新=git pull 后重跑本脚本(插件与 agorad 载荷是拷贝不是引用,不重跑不生效)"
|
|
222
|
+
echo "install: lift 失败可重启会话或等 doctor;工具拒因携 resetLift 恢复指引"
|